Rails のコネクションプールから接続を取り出す処理を追う

こんにちは、masm11 です。

弊社では PostgreSQL のデータベースを Amazon Aurora に移そうとしていますが、 フェイルオーバー時の処理が気になっています。 ググってみたところ、MySQL の情報はたくさん出てくるのですが、PostgreSQL の情報は 少なく、欲しい情報が出てきません。

今回は、弊社で使用している Rails 5.1 でフェイルオーバー時の動作をコードレベルで追いかけてみます。

unicorn は都度接続なのか?

その前に、確認しておきたいことが一つありました。 unicorn が HTTP リクエストごとに DB に接続/切断しているのかどうかです。

config/database.yml には、以下のように設定してあります。

staging:
  adapter:   postgresql
  database:  ...
  username:  ...
  password:  ...
  host:      ...
  statement_limit:      ...

伏せ字ばかりですが、pool: の設定はしていません。

activerecord-5.1.7/lib/active_record/connection_adapters/abstract/connection_pool.rb に以下のコードがあります。

        # default max pool size to 5
        @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5

省略した場合、デフォルトは 5 ですね。プールしていそうです。

他の確認もしてみました。

$ netstat -anp | grep :5432
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
tcp        0      0 172.31.30.216:24023     172.31.26.254:5432      ESTABLISHED 13853/unicorn worke 
(以下略)

少し時間が経った後、もう一度確認したところ、:24023 の部分が変わっていませんでした。 都度接続しているなら、ここが変わっていくはずです。

やはりコネクションをプールしているようです。 unicorn はシングルスレッドで、コネクションプールから接続を取得して返却し取得して返却し… を 繰り返しているので、デフォルトの 5 でもプロセスごとに1本しか使っていないのでしょう。

都度接続なら、そのたびに Aurora の cluster endpoint を DNS で引けば常に master を向いており、 フェイルオーバー時も新しい master を向いているので、そのまま何もしなくて良いはずです。 しかし、今コネクションプールを使っているアプリケーションを(なんとかして)都度接続に変更すると、 接続のコストが高すぎるように思います。

やはり、フェイルオーバーで接続が切れた場合にどうなるのか、処理を追う必要がありそうです。

Rails のコネクションプールの処理を追う

接続を取得する際によく使うメソッドは ActiveRecord::Base.connection ですね。ここをスタート地点にします。

activerecord-5.1.7/lib/active_record/base.rb では以下のようになっています。なお、途中のコメントやコードは省略して、必要な部分のみ引用しています。

module ActiveRecord #:nodoc:
  class Base
    extend ConnectionHandling

そして activerecord-5.1.7/lib/active_record/connection_handling.rb

module ActiveRecord
  module ConnectionHandling
    # Returns the connection currently associated with the class. This can
    # also be used to "borrow" the connection to do database work unrelated
    # to any of the specific Active Records.
    def connection
      retrieve_connection
    end

とあります。スタート地点はここのようです。

同じ module の中に以下がありました。

    def retrieve_connection
      connection_handler.retrieve_connection(connection_specification_name)
    end

retrieve_connection メソッドを呼び出しています。同名ですが別のメソッドのようです。 探してみたところ、activerecord-5.1.7/lib/active_record/connection_adapters/abstract/connection_pool.rb に以下のメソッドがありました。

module ActiveRecord
  module ConnectionAdapters
    class ConnectionHandler
      # Locate the connection of the nearest super class. This can be an
      # active or defined connection: if it is the latter, it will be
      # opened and set as the active connection for the class it was defined
      # for (not necessarily the current class).
      def retrieve_connection(spec_name) #:nodoc:
        pool = retrieve_connection_pool(spec_name)
        raise ConnectionNotEstablished, "No connection pool with '#{spec_name}' found." unless pool
        conn = pool.connection
        raise ConnectionNotEstablished, "No connection for '#{spec_name}' in connection pool" unless conn
        conn
      end

クラス名から考えて、おそらくこれでしょう。 該当するコネクションプールを取得して、pool.connection で接続を取得しているようです。 同じファイルの中に以下のメソッドがあります。

module ActiveRecord
  module ConnectionAdapters
    class ConnectionPool
      # Retrieve the connection associated with the current thread, or call
      # #checkout to obtain one if necessary.
      #
      # #connection can be called any number of times; the connection is
      # held in a cache keyed by a thread.
      def connection
        @thread_cached_conns[connection_cache_key(@lock_thread || Thread.current)] ||= checkout
      end

ここでは、スレッドごとに接続をキャッシュしているようです。キャッシュになければ checkout するのですね。 checkout を見ていきましょう。

      def checkout(checkout_timeout = @checkout_timeout)
        checkout_and_verify(acquire_connection(checkout_timeout))
      end

checkout_and_verify。それっぽい名前ですね。接続が生きているか確認してそうです。このメソッドを追います。

        def checkout_and_verify(c)
          c._run_checkout_callbacks do
            c.verify!
          end
          c
        rescue
          remove c
          c.disconnect!
          raise
        end

なんだか複雑です。引数 c には acquire_connection の返り値が渡るので、接続ですね。 c.verify! でチェックしてそうです。

verify!activerecord-5.1.7/lib/active_record/connection_adapters/abstract_adapter.rb にあります。

      def verify!(*ignored)
        if ignored.size > 0
          ActiveSupport::Deprecation.warn("Passing arguments to #verify method of the connection has no effect and has been deprecated. Please remove all arguments from the #verify method call.")
        end
        reconnect! unless active?
      end

active?reconnect! が出てきました。一つずつ見ていきます。

active? は同じファイルの中にもあるのですが、

      def active?
      end

と空っぽです。おそらく他の場所でクラスを継承してオーバーライドしているのでしょう。

activerecord-5.1.7/lib/active_record/connection_adapters/postgresql_adapter.rb にありました。

      def active?
        @lock.synchronize do
          @connection.query "SELECT 1"
        end
        true
      rescue PG::Error
        false
      end

SELECT 1 を実行してみて、問題なければ true、失敗なら false を返しているだけですね。 接続が生きているかの確認はここにありました。

active? は理解したので、引き続き reconnect! を見てみます。

activerecord-5.1.7/lib/active_record/connection_adapters/abstract_adapter.rb

      # Disconnects from the database if already connected, and establishes a
      # new connection with the database. Implementors should call super if they
      # override the default implementation.
      def reconnect!
        clear_cache!
        reset_transaction
      end

とあって、それだけ?? と思いましたが、activerecord-5.1.7/lib/active_record/connection_adapters/postgresql_adapter.rb にもありました。

      # Close then reopen the connection.
      def reconnect!
        @lock.synchronize do
          super
          @connection.reset
          configure_connection
        end
      end

super で先に上げた方の reconnect! を呼び出しているんですね。 そして @connection.reset です。

@connection は同じファイルで以下のように作っています。

        def connect
          @connection = PG.connect(@connection_parameters)
          configure_connection
        rescue ::PG::Error => error
          if error.message.include?("does not exist")
            raise ActiveRecord::NoDatabaseError
          else
            raise
          end
        end

PG.connect で作っています。PG は pg という gem で定義されています。 つまり @connection.resetreset メソッドは pg gem の方にあります。

せっかくここまで来たので、ついでに見ていきます。ここからは C 言語になります。

pg-0.21.0/ext/pg_connection.c で以下のようになっています。

 rb_define_method(rb_cPGconn, "reset", pgconn_reset, 0);

ruby でいう reset というメソッドは、C では pgconn_reset() 関数なのですね。 この関数は以下のようになっています。

/*
 * call-seq:
 *    conn.reset()
 *
 * Resets the backend connection. This method closes the
 * backend connection and tries to re-connect.
 */
static VALUE
pgconn_reset( VALUE self )
{
        pgconn_close_socket_io( self );
        gvl_PQreset( pg_get_pgconn(self) );
        return self;
}

いろいろやってますね。一つずつ見ていきます。

pgconn_close_socket_io() は以下の通りです。

static void
pgconn_close_socket_io( VALUE self )
{
        t_pg_connection *this = pg_get_connection( self );
        VALUE socket_io = this->socket_io;

        if ( RTEST(socket_io) ) {
#if defined(_WIN32) && defined(HAVE_RB_W32_WRAP_IO_HANDLE)
                int ruby_sd = NUM2INT(rb_funcall( socket_io, rb_intern("fileno"), 0 ));
                if( rb_w32_unwrap_io_handle(ruby_sd) ){
                        rb_raise(rb_eConnectionBad, "Could not unwrap win32 socket handle");
                }
#endif
                rb_funcall( socket_io, rb_intern("close"), 0 );
        }

        this->socket_io = Qnil;
}

見づらいので、windows 専用コードを削除してみると、以下のようになります。

static void
pgconn_close_socket_io( VALUE self )
{
        t_pg_connection *this = pg_get_connection( self );
        VALUE socket_io = this->socket_io;

        if ( RTEST(socket_io) ) {
                rb_funcall( socket_io, rb_intern("close"), 0 );
        }

        this->socket_io = Qnil;
}

ruby レベルで socket を close しているようです。

次に、pg_get_pgconn() は以下のようになっています。

PGconn *
pg_get_pgconn( VALUE self )
{
        t_pg_connection *this;
        Data_Get_Struct( self, t_pg_connection, this);

        if ( !this->pgconn )
                rb_raise( rb_eConnectionBad, "connection is closed" );

        return this->pgconn;
}

接続に対応する構造体のポインタを取得しているだけのようです。

最後に gvl_PQreset() です。パッと見、見つかりませんでしたが、gvl_wrappers.c にありました。

FOR_EACH_BLOCKING_FUNCTION( DEFINE_GVL_STUB );

FOR_EACH_BLOCKING_FUNCTION はマクロです。gvl_wrappers.h で以下のようになっています。

#define FOR_EACH_BLOCKING_FUNCTION(function) \
  (略)
        function(PQreset, GVL_TYPE_VOID, void, PGconn *, conn) \
  (略)

DEFINE_GVL_STUB が引数 function に渡されるので、

        DEFINE_GVL_STUB(PQreset, GVL_TYPE_VOID, void, PGconn *, conn)

となります。そして DEFINE_GVL_STUB がまたマクロで、以下のようになっています。

        #define DEFINE_GVL_STUB(name, when_non_void, rettype, lastparamtype, lastparamname) \
                rettype gvl_##name(FOR_EACH_PARAM_OF_##name(DEFINE_PARAM_LIST3) lastparamtype lastparamname){ \
                        return name( FOR_EACH_PARAM_OF_##name(DEFINE_PARAM_LIST1) lastparamname ); \
                }

渡された引数を使って展開すると、以下のようになります。

                void gvl_PQreset(FOR_EACH_PARAM_OF_PQreset(DEFINE_PARAM_LIST3) PGConn *conn){
                        return PQreset( FOR_EACH_PARAM_OF_PQreset(DEFINE_PARAM_LIST1) conn );
                }

FOR_EACH_PARAM_OF_PQreset もマクロで、以下のように定義されています。

#define FOR_EACH_PARAM_OF_PQreset(param)

空です。これも展開すると、以下のようになります。

                void gvl_PQreset(PGConn *conn){
                        return PQreset(conn);
                }

たくさんのコードを列挙しなくて済むようマクロが使われていたので、解読に少し手間取りましたが、 結局 gvl_PQreset()PQreset() を呼んでいるだけですね。

PQreset() のドキュメントはこちらにあります。 同じパラメータを使って同じサーバへ接続するそうです。

まとめ

ActiveRecord::Base.connection は、コネクションプールから接続を取得して返します。 コネクションプールは配列から接続を取り出して、生きていればそれを返し、死んでいたら PQreset() で 復活させてからそれを返すようです。

PQreset() のドキュメントを読むと、同じパラメータを使って同じサーバへ接続するそうでが、 接続時のパラメータとして指定するのはホスト名であって IP アドレスは指定しません。 接続しなおす時に DNS を引き直してくれるのかどうかは判りませんでした。 DNS を引き直さない場合、本当に「同じサーバ」(同じインスタンス) になってしまい、 そこはおそらく read-only です。

ドキュメントの説明が若干曖昧なので、あとは実際にテストしてみるしかないかな、と思いました。

UNIX ドメインソケットの接続対応表を作る

明けましておめでとうございます。masm11 です。

UNIX ドメインソケットってご存知ですか? 知らないけど実は使ってる (設定したことがある) 方も結構いらっしゃるかもしれません。 今回は、UNIX ドメインソケットをちょっとだけ便利にする自作ツールを一つご紹介したいと思います。

UNIX ドメインソケットとは?

まず、インターネットドメインといえば、TCP や UDP が有名ですね。 TCP はストリーム型で、つまり、送った連続データがそのまま受け取れます。また、信頼性があると言われています。UDP はデータグラム型で、つまり、一度に送れる量に制限はあるものの、一度で送ったデータは一度で受け取れます。

インターネットドメインの TCP や UDP に対して、UNIX ドメインには名前はありませんが、こちらにもストリーム型やデータグラム型があります。

では、UNIX ドメインソケットはどこで使われているのでしょうか?

nginx に以下のような設定をしたことはありませんか?

upstream app {
    server unix:/tmp/unicorn.sock;
}

unicorn には以下のように設定していますね。

listen '/tmp/unicorn.sock'

これらは /tmp/unicorn.sock を使って nginx と unicorn を接続する設定です。これが UNIX ドメインソケットです。

ls で見てみると、以下のように先頭が s になっていて、 ソケットであることを示しています。

[ec2-user@ip-172-31-30-9 ~]$ ls -al /tmp/unicorn.sock 
srwxrwxrwx 1 ec2-user ec2-user 0 1016 10:31 /tmp/unicorn.sock
[ec2-user@ip-172-31-30-9 ~]$ 

UNIX ドメインソケットは、ファイルシステム中にソケットを作り、 そのソケットを使って2つのプロセスがつながります。 従って、同じコンピュータ内でしか接続できません。

何と何がつながっている?

では、今、私のコンピュータにはどんな UNIX ドメインソケットがあるのでしょうか?

それを見るには ss というコマンドを使います。以前は netstat というコマンドが使われていましたが、Linux ではここ数年で ss コマンドに置き換わりました。

以下のように実行してみてください。

ss -anp | grep ^u_str

UNIX ドメインのストリーム型ソケットの一覧が表示されます。

例えば以下の行を見てみます (見づらいので空白は適当にまとめています)。

u_str  ESTAB  0 0  @/tmp/ibus/dbus-PSq26h33 204929  * 203227  users:(("ibus-daemon",pid=607643,fd=12))
  • u_str

    UNIX ドメインのストリーム型ソケットです。

  • ESTAB

    接続しています。

  • 0 0

    送受信バッファに溜まっているデータ量を示します。

  • @/tmp/ibus/dbus-PSq26h33 204929

    自プロセスが使っている側のソケットについての情報です。 (先頭が @ で始まっているものは、ファイルシステム中には存在しません)

  • * 203227

    接続相手側のソケットについての情報です。

  • users:(("ibus-daemon",pid=607643,fd=12))

    自プロセスについての情報です。

ibus-daemon が使っているソケットですね。

では接続相手は何者でしょうか? そう思った時は、まず * 203227 の部分を見ます。接続相手側のソケットについての情報、ですね。 これで検索してみると、ありました。

u_str  ESTAB  0 0  * 203227  * 204929  users:(("wf-panel",pid=607640,fd=19))

相手側プロセスは wf-panel であることが判ります。

ツールを作る

さて、インターネットドメインの場合は、相手側プロセスは別のコンピュータにあることが極普通なので、それがどんなプロセスなのかを知ることは困難です。しかし、UNIX ドメインなら、同じコンピュータに存在することが判っているので、相手側プロセスが何者なのか、以上のように調べれば判ります。

しかし、あくまで「調べれば判る」というレベルです。例えば、目的のプロセスが2つあって、それらがつながっているかどうか、どうすれば調べられるでしょうか? @/tmp/ibus/dbus-PSq26h33 を使ってつながっているプロセスはたくさんあることが想定されます。

luna:~ % ss -anp |grep ^u_str |grep '@/tmp/ibus/dbus-PSq26h33'  | wc -l
10
luna:~ % 

10個ですね。10個なら一行ずつ調べていけば、まぁなんとかなります。

でも、面倒ですよね?

そこで、ツールを作ってみました。

#!/usr/bin/env ruby

data = []

`ss -anp`.split("\n").each do |line|
  fields = line.split(/\s+/, 9)
  next if fields[0] != 'u_str'
  next if fields[1] != 'ESTAB'
  l_port = fields[5]
  r_port = fields[7]
  dat = {
    socket: fields[4],
    l_port: fields[5],
    r_port: fields[7],
    prog: fields[8],
  }
  data << dat
end

r_port_info = {}
data.each do |dat|
  raise "r_port duplicated: #{dat}." if r_port_info[dat[:l_port]]
  r_port_info[dat[:l_port]] = dat
end

data.each do |dat|
  l_dat = dat
  r_dat = r_port_info[dat[:r_port]]
  next unless r_dat

  next unless l_dat[:prog]
  next unless r_dat[:prog]

  puts "#{l_dat[:l_port]} #{l_dat[:r_port]} #{l_dat[:prog]} #{r_dat[:prog]}"
end

ここまでに説明した ss コマンドを使った調べ方を使って、愚直に接続元と接続先をペアにして列挙するだけのツールです。 仮に uconn という名前にしましょう。

このツールを使えば、例えば ibus-daemon と emacs がつながっているかは、以下のように簡単に確認できます。

luna:~ % uconn |grep ibus-daemon | grep emacs
199631 203253 users:(("emacs",pid=607815,fd=14)) users:(("ibus-daemon",pid=607643,fd=14))
203253 199631 users:(("ibus-daemon",pid=607643,fd=14)) users:(("emacs",pid=607815,fd=14))
luna:~ % 

ibus-daemon と emacs がちゃんとつながっていることが判りますね。

まとめ

UNIX ドメインソケットのおさらいと、ss コマンドの出力の見方、そしてその出力を整形して見やすく出力するツールを紹介しました。日常的に Linux の管理をしていると、時々このツールを使う場面があります。

ただ、途中で少し説明したとおり、ss コマンドはそこそこ最近生まれたコマンドなので、古めの OS だと存在しないくて、このツールが使えません。弊社のサーバも、ss コマンドが存在しないくらいには古い OS なので、このツールは使えません。残念です。

ではまた!

ラズパイ4でHomeKit対応見守りカメラを作ってみました

こんにちは。Tedです。

AppleのHomeKit対応製品は海外では充実していますが、日本ではまだまだ。そのために選択肢が少なく高価なのがネックです。

そこで今回はHomebridge + ラズパイを使って安価にHomeKitに対応した見守りカメラを作ってみました。

Homebridgeとは

HomebridgeはNodeJSを使ったHomeKitのエミュレーションを行うシステムです。オープンソースとして公開されています。

https://homebridge.io

用意するもの

  • ラズパイ(今回は Raspberry Pi 4B を使いました)
  • ラズパイ対応カメラ
  • 32GB マイクロSDカード
  • ラズパイ電源アダプタ
  • Pi4B用ラズパイケース(カメラを内蔵できるものを選びました)

f:id:TedWada:20191230185630j:plain
購入したラズパイ+ACアダプタセットとカメラ

他には環境を構築するためにMacBook Proおよびインターネット接続環境・有線LANケーブル・マイクロSDカードアダプタを用意しました。

「作ろう会」で作りました

今回は下記の作ろう会にて作成してみました。

ingage.connpass.com

masm11さん(当社シニアエンジニア)と植田雄太さんから助言をいただきながら進めました。

システム構築

ラズパイを使ってHomeKitカメラを作った記事はいくつかWeb上に上がっています。 たとえば下記がそうです。

appleinsider.com

このAppleInsiderの記事では構築済のシステムバイナリが提供されていますが、ラズパイ4では動きません。 そこで今回はバイナリを使わずシステムを構築していきます。

ラズパイを起動できるようにする

まずはラズパイを起動できるようにします。 今回は直付けのモニタ・キーボードは用意せずに構築していきます。

SDカードにRasbianを書き込む

下記のRasbianダウンロードサイトよりRasbianシステムをダウンロードします。 今回は Raspbian Buster with desktop and recommended software を使いました。

www.raspberrypi.org

ダウンロードしたZipアーカイブは解凍しておきます。

SDカードへの書き込みはMacにてターミナルを開いて下記のように進めます。

[~]$ diskutil list  #マイクロSDカードのアイデンティファイヤを調べておく
#ここではマイクロSDカードのアイデンティファイヤは /dev/disk1 と記述します。

[~]$ diskutil unmountDisk /dev/disk1  #いったんアンマウント
[~]$ diskutil eraseDisk MS-DOS BOOT /dev/disk1  #フォーマット
[~]$ diskutil unmountDisk /dev/disk1  #フォーマット後自動的にマウントされるので再度アンマウント

[~]$ sudo dd if=/ファイルパス/ダウンロードしたファイル.img of=/dev/rdisk1 bs=1m  #SDカードにイメージを書き込み

LANからラズパイにアクセスできるようにする

[~]$ diskutil mountDisk /dev/disk1  #書き込んだSDカードをマウント
[~]$ cd /Volumes/boot  #SDカードに移動
[~]$ touch ssh  #sshファイルを作成

ラズパイに周辺機器を取り付ける

ラズパイにカメラを取り付ける

写真のようにラズパイにカメラケーブルを取り付けます。

f:id:TedWada:20191230185939j:plain

向きを間違えないように、またしっかりとセットしてください。(見た目にはちゃんとケーブルがセットされているようでも微妙なズレで認識されなかったりします)

その他周辺を接続

バイナリを書き込んだマイクロSDカード、LANケーブルおよび電源をラズパイ本体に接続します。 ラズパイをケースに入れるのは基本的な動作確認ができてからとしました。

いよいよラズパイを起動

電源を入れると、本体の赤LEDが点灯し緑のアクセスLEDが点滅します。 10秒ほど待てばシステム起動となり、ラズパイにアクセスできるようになります。

ラズパイにSSH経由でアクセス

[~]$ ssh pi@raspberrypi.local
# 初期パスワードは raspberry (当然ながら変更しておく)

無線LANでアクセスできるようにする

上記の通りラズパイにアクセスしたら、無線LANに接続できるようにしておきます。

pi@raspberrypi:~ $ sudo vi /etc/wpa_supplicant/wpa_supplicant.conf

下記を記述します。

ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
country=JP
network={
   ssid="<接続したいSSID>"
   psk="<WEPキー>"
}

ここまでできれば一段落です。 後はラズパイにシステムを構築していきます。

ラズパイシステムを最新の状態にアップデート

pi@raspberrypi:~ $ sudo apt-get update && sudo apt-get upgrade

カメラモジュールを有効化

pi@raspberrypi:~ $ sudo raspi-config

f:id:TedWada:20191223180809p:plain
「5 Interfacing Options」を選択。

f:id:TedWada:20191223180901p:plain
「P1 Camera」を選択。

f:id:TedWada:20191223180939p:plain
「Yes」で保存。

f:id:TedWada:20191223181012p:plain
カメラモジュールが有効化された。

タイムゾーンの設定

前記 raspi-config から適切にタイムゾーンを設定しておきます。

設定場所は [4 Localisation Options] - [I2 Change Timezone] にあります。 日本だと [Asia] - [Tokyo] ですね。

Node.jsをインストール

HomebridgeはNode v4.3.2またはそれ以降のバージョンが必要です。 今回は執筆時の最新版である version 10.x を使いました。

https://linuxize.com/post/how-to-install-node-js-on-raspberry-pi/ を参考にしました。

pi@raspberrypi:~ $ curl -sL https://deb.nodesource.com/setup_10.x | sudo bash -  #最新版をget
pi@raspberrypi:~ $ sudo apt-get install -y nodejs  #インストール
pi@raspberrypi:~ $ node --version  #インストールされたバージョンを確認
v10.15.2   #などと表示されればOK

Homebridgeをインストール

pi@raspberrypi:~ $ sudo npm install -g --unsafe-perm homebridge

Homebridge用カメラプラグインをインストール

pi@raspberrypi:~ $ sudo npm install -g homebridge-camera-rpi@latest

Homebridgeの設定ファイルを編集

pi@raspberrypi:~ $ mkdir .homebridge  #最初にディレクトリを作成
pi@raspberrypi:~ $ vi .homebridge/config.json

ファイル内に下記を記述します。

{
  "bridge": {
    "name": "Homebridge",  #名称を設定
    "username": "1A:2B:3C:4D:5E:6F",  #16進数で自由に設定(重複しないようMACアドレス推奨)
    "port": 46183,
    "pin": "111-22-333"  #自由に設定
  },

  "description": "HomeCamera",  #名称を設定

  "platforms": [
    {
      "platform" : "rpi-camera", 
      "cameras": [{
        "name": "Home Camera",  #名称を設定(Homeアプリでのデバイス名)
        "rotate": 90  #0-180の回転角度
      }]
    }
  ]
}

(※2020-1-11 修正: platforms内のplatformの名称はプラグイン固有なのでその旨修正)

あとは、ラズパイ起動時に必要なモジュールが自動的にスタートするようにしておきます。 これは動作確認が終わってからでもよいでしょう。

自動起動の設定

pi@raspberrypi:~ $ sudo vi /etc/rc.local

ファイル内に下記を追加します。 スクリプト後半、exit 0 の前あたりに書きました。

/usr/bin/homebridge -U /home/pi/.homebridge/ &

(※2020-2-16 修正: homebridgeが格納されているディレクトリを修正)

Homeアプリにカメラを登録

前提としてすでにHomeKitホームハブ(HomePod, Apple TVなど)が稼働していることとします。

HomePod、Apple TV、iPad をホームハブとして設定する - Apple サポート

前記ホームハブと同じネットワークセグメントに接続されているデバイス(iPhoneなど)にてHomeアプリを起動します。

f:id:TedWada:20191229150313j:plain

Homeアプリにて [+] をタップしてデバイスを追加します。

f:id:TedWada:20191229150537j:plain

[アクセサリを追加] をタップします。

f:id:TedWada:20191229150954j:plain

[コードがないか、スキャンできません] をタップします。

f:id:TedWada:20191229151126j:plain

設定したカメラデバイスが表示されるのでタップします。

f:id:TedWada:20191229151252j:plain
カメラ名として設定したデバイスを選択する。

[このまま追加] をタップし、その後表示される画面にてconfig.jsonにて設定したpinを入力します。

f:id:TedWada:20191229151447j:plain

以上にてカメラデバイスが追加されます。

f:id:TedWada:20191229151516j:plain

正しく動作すれば図のようにカメラ画像がHomeアプリから確認できるようになります。

f:id:TedWada:20191229173256p:plain

うまく動かない場合は

カメラが正しく動いているか

pi@raspberrypi:~ $ vcgencmd get_camera  #カメラの状態を取得
supported=1 detected=1  #このように表示されていれば正しく認識されている

上記のように返らない場合はカメラケーブルの接続状態を確認しましょう。

カメラ画像が取得できるか

pi@raspberrypi:~ $ raspistill -o pictureFromCamera.jpg

取得した画像は sftp などで取得・確認します。

Homebridgeは正しく起動しているか

Homeアプリからデバイスが見えない場合、Homebridgeが正しく稼働していない可能性が高いです。

pi@raspberrypi:~ $ ps -aux | grep homebridge  #homebridgeプロセスが正しく起動しているか確認
root       432  0.2  1.8 181880 72148 ?        Sl   Dec18  36:25 homebridge  #このような表示があればプロセスは存在している

プロセスは起動しているが動作がおかしい場合はデバッグモードでエラーが起こっていないか確認しましょう。

pi@raspberrypi:~ $ sudo kill <killしたいPID(上記の場合は432>  #いったんHomebridgeプロセスを終了
pi@raspberrypi:~ $ DEBUG=* homebridge -D  #デバッグモードで起動

Homebridgeの設定ファイルに誤りはないか

config.json ファイルを JSON Lint などのJSONバリデータにて確認しましょう。

突然Homeアプリからデバイスが見えなくなった

システムの設定時など、何度か再起動を繰り返していると突然Homeアプリからデバイスが見えなくなることがあります。

この場合は下記のようにペアリングをリセットすると見えるようになります。

  • HomeアプリからHomebridgeアプリをすべて削除する。
  • persist および accessories フォルダを削除する。
pi@raspberrypi:~ $ sudo rm -rf .homebridge/persist
pi@raspberrypi:~ $ sudo rm -rf .homebridge/accessories
  • config.json 内の username を(インクリするなど)別のものに変える。

上記を行った後、システムを再起動します。

それでもうまくいかない場合

下記リンクに沿って調べてみてください。

Basic Troubleshooting · nfarina/homebridge Wiki · GitHub

AWS で IPv6 を使う

こんにちは。masm11 です。

IP アドレスが枯渇したと何度も言われているにもかかわらず、IPv6 はなかなか広まりませんね。今回は AWS で IPv6 の設定をしてみたので、設定の手順について説明したいと思います。

前提としては、AWS アカウントがあって、VPC に既に EC2 インスタンスが存在し、IPv4 でアクセスできるものとします。ゴールとしては IPv6 で HTTP アクセスできるところを目指します。

IPv6 について復習

その前に、まずは IPv6 について復習しておきましょう。

  • IPv6 には NAT がない

    f:id:masm11:20191216211545p:plain

    IPv4 の時は、EC2 の手前にポートフォワーディングするものがあって、クライアントはそこのグローバルアドレスに接続し、その接続が EC2 のプライベートアドレスに転送されていました。

    一方、IPv6 には NAT はありません。従って、EC2 にグローバルアドレスを割り当て、クライアントはそのグローバルアドレスに接続することになります。

  • IP アドレスの表記方法が異なる

    まずは IPv4 の場合を見てみます。

    f:id:masm11:20191216211857p:plain

    192.168.123.234 という表記をして、192.168 の部分をネットワーク部分、123 をサブネット部分、234 をホスト部分と言ったりします。どこで区切るかは、割り当てる人が決めます。

    また、ネットワーク部分のみを指して 192.168.0.0/16、サブネット部分まで含めて 192.168.123.0/24 と表記します。

    そして全体で 32ビットです。

    次に IPv6 の場合を見てみます。

    f:id:masm11:20191216212151p:plain

    IPv6 は 128ビットあります。16進数表記で4桁ごとに : で区切っています。

    IPv4 の場合と同じように、先頭からネットワーク部分、サブネット部分、ホスト部分に分かれています。

    また、ネットワーク部分を指して 1234:5678:9abc:de00::/56、サブネット部分まで含めて 1234:5678:9abc:def0::/64 などと表記します。

AWS に設定する

では復習はこのくらいにして、実際に AWS に設定していきましょう。

VPC に IPv6 CIDR を割り当てる

まずは VPC に設定します。

設定したい VPC を選択します。

f:id:masm11:20191216212748p:plain

メニューから「CIDR の編集」を選択します。

f:id:masm11:20191216212907p:plain

以下のような画面になりますので、「IPv6 CIDR の追加」ボタンをクリックします。

f:id:masm11:20191216212953p:plain

これだけで、VPC に IPv6 CIDR が割り当てられました。

f:id:masm11:20191216213153p:plain

ここで割り当てられたのは、/56 ですから、以下の赤枠で囲った部分ですね。

f:id:masm11:20191216213237p:plain

VPC ダッシュボードに戻ると、以下のように反映されています。

f:id:masm11:20191216213338p:plain

サブネットに IPv6 CIDR を割り当てる

次に、サブネットに設定します。

まずサブネットを一つ選択します。

f:id:masm11:20191216213508p:plain

メニューから「IPv6 CIDR の編集」を選択します。

f:id:masm11:20191216213540p:plain

「IPv6 CIDR の追加」をクリックします。

f:id:masm11:20191216213614p:plain

以下のような画面になりますので、サブネット部分の2桁を入力します。

f:id:masm11:20191216213738p:plain

この作業を VPC 内の各サブネットについて行います。その結果は以下の通りです。

f:id:masm11:20191216213853p:plain

サブネット部分をそれぞれ 02, 01, 00 と設定しています。

ルートテーブルを設定する

次にルートテーブルを設定します。

まずルートテーブルを選択します。

f:id:masm11:20191216214109p:plain

メニューから「ルートの編集」を選択します。

f:id:masm11:20191216214220p:plain

以下のように、デフォルトゲートウェイをインターネットゲートウェイに向けます。デフォルトゲートウェイは、IPv4 では 0.0.0.0/0 でしたね。IPv6 では ::/0 となります。インターネットゲートウェイはたぶん一つしかないと思いますので、それを選択しておけば大丈夫です。

f:id:masm11:20191216214256p:plain

EC2 インスタンスに IPv6 アドレスを割り当てる

いよいよ EC2 インスタンスに IPv6 アドレスを割り当てます。

まずは、その EC2 インスタンスが IPv6 に対応しているかどうかを確認します。

EC2 インスタンスを作る際に以下のような画面がありました。 ここに「IPv6 サポート」という列があり、ここに「はい」と表示されているかどうかを確認します。

f:id:masm11:20191216214735p:plain

なのですが、実は現行のインスタンスタイプなら全て対応しているそうです。旧世代のタイプを使っている場合は要確認です。

次に、EC2 ダッシュボードで EC2 インスタンスを選択します。IPv6 IP の欄はまだ空欄ですね。

f:id:masm11:20191216215052p:plain

メニューから「IP アドレスの管理」を選択します。

f:id:masm11:20191216215156p:plain

IPv6 の方の「新しい IP の割り当て」をクリックします。

f:id:masm11:20191216215311p:plain

以下のように、IPv6 アドレスを入力する欄が現れますが、ここは空欄のまま、「更新する」ボタンをクリックします。

f:id:masm11:20191216215416p:plain

すると、以下のように IPv6 アドレスが自動で決まります。

f:id:masm11:20191216215525p:plain

EC2 ダッシュボードに戻ると、IPv6 IP 欄に IPv6 アドレスが表示されました。

f:id:masm11:20191216215620p:plain

しかし、この時点ではまだ OS には反映されていません。 OS に反映させる方法なら何でも良いのですが、ここでは簡単に EC2 インスタンスを再起動します。

f:id:masm11:20191216215828p:plain

端末から IPv4 で ssh して、ip a コマンドを実行すると、IPv6 アドレスが表示されました。OS が認識したことが確認できました。

f:id:masm11:20191216215850p:plain

セキュリティグループを設定する

...そろそろ飽きてきました? これで AWS 的には最後です。頑張りましょう。 セキュリティグループを設定します。

まずセキュリティグループを選択します。

f:id:masm11:20191216220316p:plain

メニューから「インバウンドのルールの編集」を選択します。

f:id:masm11:20191216220347p:plain

SSH をどこからでも接続できるようにします。IPv4 の時は 0.0.0.0/0 でしたね。IPv6 では ::/0 です。 HTTP でも接続したいので、HTTP も同様に設定します。

f:id:masm11:20191216220424p:plain

IPv6 疎通確認

では、ここまでできたので、疎通確認してみましょう。

ssh で IPv6 アドレスを指定してログインしてみます。

f:id:masm11:20191216220817p:plain

接続できました!

DNS を設定する

あとは、ブラウザからアクセスできるようにしましょう。DNS です。

今回は Route53 でなく ConoHa の DNS を使いました。 ConoHa のコントロールパネルは以下のような感じです。

f:id:masm11:20191216221146p:plain

IPv4 の場合、レコードの種類は A でした。IPv6 では AAAA です。A が4文字なのは、IPv4 アドレスは 32ビット、IPv6 アドレスは 128ビットで4倍あることに由来しているそうです。

ホスト名は aws としました。そして TTL、IPv6 アドレスを設定します。

ブラウザからアクセスする

ここで、nginx をインストールして起動する作業が入りますが、内容は省略します。必要でしたら Qiita の記事などを参考にしてみてください。

では、ブラウザからアクセスしてみます。

f:id:masm11:20191216221753p:plain

表示されました!

端末で /var/log/nginx/access.log を確認したところ、 以下のようにアクセス元が IPv6 アドレスになっています。 これで、IPv6 で HTTP アクセスできたことが確認できました。

f:id:masm11:20191216222308p:plain

まとめ

以上、AWS の IPv6 設定について見てきました。いかがだったでしょうか。項目が多かったですね。しかし、毎回この手順が全部必要なわけではなく、VPC やサブネットは一度設定してしまえばいいので、あとは EC2 の設定以降だけで済みます。

社内でこの記事の説明をした後、どこが IPv6 に対応しているか調べていましたが、なんと少ないこと。今対応すれば、まだまだ IPv6 先駆者を名乗れそうに思いました。この記事がお役に立てればと思います。

ではまた!

Ruby 関西にて inotify の話を発表してきました #rubykansai

永田 @kizashi1122 です。 昨日の第88回 Ruby 関西にて発表してきました。

デモは各 gem の README レベルの内容でした。

質疑応答

覚えている限り挙げておきますと、、、

  • 常駐プログラムの監視はどうやっていますか?
    • monit (https://mmonit.com/monit/ ) を使ってます。常駐プログラムを落とすときは、Ctrl-C(SIGINT)やkill コマンド(SIGTERM)を使うので、SIGINT、SIGTERM の trap が大事
  • inotify は NFS でも監視できますか?
    • inotify は NFSはサポートしてない(会場)
    • (帰って調べた→)http://inotify.aiken.cz/?section=inotify&page=faq には「Can I watch sysfs (procfs, nfs...)?」に対して「Simply spoken: yes, but with some limitations.」とあります。
  • Guard みんな最近つかってないんですかね?
    • なんとなく使わなくなった・・・(会場)
  • デモ中に rm ではなく、 \rm と打ったのはなんで?
    • alias rm='rm -i' としているので、エイリアスを無視して実行したかったから

体調が悪く発表後はすぐに帰らせてもらいました。 次回は懇親会にも参加したいと思ってます。

Emacs 上で Git を使おう!

こんにちは、masm11 です。

皆さん、Git 使ってますか? 使ってますよね? Git を使っている人はもう Emacs 人口を超えているのでは、と思っています。

一方で、Emacs ってご存知ですか? 高機能なエディタです。 ですが、最近はあまり人気がないらしいですね。 出来の良い IDE の登場によるのでしょうか。 でも私はそんなことはお構いなく、Emacs を使っています。

さて、今回はそんな Emacs 上で Git が使える Magitの使い方をご紹介したいと思います。

インストール

Emacs で M-x package-list-packages してしばらく待つと、magit が現れます (しばらく待たないと現れないと思います)。

f:id:masm11:20191023023351p:plain

magit で Enter を押して、Install で Enter を押すと、インストールできます。 f:id:masm11:20191023023439p:plain

そして少し設定します。私は ~/.emacs で以下のようにしています。

;; magit
(defalias 'magit 'magit-status)
(global-set-key "\C-xg" 'magit-status)

(setenv "GIT_EDITOR" "emacsclient")
(add-hook 'shell-mode-hook 'with-editor-export-git-editor)

使ってみる

  • 起動

    C-x g と入力すると、Magit が起動します。 カレントディレクトリが git 管理されているなら、そのディレクトリが対象になります。 そうでない場合は、ディレクトリを尋ねられますので入力してください。 もし、カレントディレクトリが git 管理されているけど違うディレクトリを対象にしたい 場合は、代わりに C-u C-x g と入力すれば、必ずディレクトリを尋ねてくれます。

    f:id:masm11:20191023023513p:plain

    これで Magit が起動しました。magit: <ディレクトリ名> というバッファが開いています。ここには、現在の状態が表示されています。Magit はこのバッファで操作します。

    まずはファイルを編集してください。普通に Magit とは関係なく編集します。 その後、Magit のバッファに戻ってください。戻り方は C-x b でもいいですし、 C-x g でもう一度起動しても構いません。C-x b で戻った場合は、バッファの内容を 更新するために一度 g を押してください。

    編集したファイルが Unstaged changes という項目に表示されています。 新規ファイルの場合は Untracked files に表示されています。

    f:id:masm11:20191023023549p:plain

  • stage

    Unstaged changes または Untracked files の該当ファイル名で s を押すと、 そのファイルを git add (stage) することができます。

    f:id:masm11:20191023023638p:plain

  • stage をキャンセル

    Staged changes の該当ファイル名で u を押すと、unstage できます。

  • diff

    Staged changes で d d と入力してみてください。今 stage した差分が表示 されます。

    f:id:masm11:20191023023705p:plain

    Unstaged changes がまだあるなら、そちらで d d すれば、まだ stage して いない差分が表示されます。

    Untracked files はファイル名でそのまま Enter を押すとファイルの内容が表示 されます。

  • commit

    Staged changes で c c と入力すると、stage しておいた差分を commit することができます。

    f:id:masm11:20191023023727p:plain

    commit message を入力して、C-c C-c で commit できます。キャンセルしたい場合は C-c C-k でできます。

  • log

    l l と入力すると、ログがグラフ付きで表示されます。

    f:id:masm11:20191023023750p:plain

    ログの各行で Enter すると、その commit の詳細が表示されます。

  • push

    P u と入力すると、 push できます。

    現在のブランチを初めて push する場合は、push 先を尋ねられますので、入力してください。この時、補完を活用すると便利です。2回め以降の場合は最初と同じ場所に push されます。

    f:id:masm11:20191029203540p:plain

    2箇所それぞれに push したいこともあります(あまりないと思いますが…)。 その場合、2箇所めは代わりに P p と入力すればできます。使い方は P u と似ているのですが、push 先はブランチでなく remote のみで指定します。ブランチ名は手元と同じになります。

    u の場合と p の場合でそれぞれに push 先を覚えてくれます。 push 先が心配になった場合は、P まで入力すると以下のバッファが必ず開きますので、 ここで確認できます。

    f:id:masm11:20191023023838p:plain

    この画面のとおり、実は3箇所めもあって、P e です。これは push 先を覚えてくれません。稀にしか使わない remote ブランチに使うと良いでしょう。

  • pull

    pull したい時は F u, F p, F e です。u, p, e は push の場合と同じです。

    この時、マージの commit message を求められますので、そのまま C-c C-c で commit します。

    f:id:masm11:20191023023925p:plain

    通常は vim 等が呼び出されてしまうのですが、冒頭に紹介した設定の↓この部分により、代わりに Emacs に投げることができているわけです。

(setenv "GIT_EDITOR" "emacsclient")
(add-hook 'shell-mode-hook 'with-editor-export-git-editor)
  • コンフリクトの解消

    pull するとコンフリクトすることがありますね。

    この時、Magit バッファには unmerged と表示されています。

    f:id:masm11:20191110005646p:plain

    このファイルを開いて ==== などで検索すると、コンフリクトしている箇所が見つかり、 色分けされているので一目瞭然です。

    f:id:masm11:20191110005717p:plain

    コンフリクトを解決してファイルを保存してください。

    Magit バッファに戻ると、既に stage されています。全てのコンフリクトが解消したら、 c c で commit してください。

以上で基本的な操作は一通りできると思います。

更に便利な操作

更に、もう少し便利な操作を説明します。

  • ファイル中の一部の変更のみ stage

    Unstaged changes で d d すると、stage していない差分が表示されるわけですが、 この中から特定の hunk のみを stage できます。 その hunk に合わせて s を押すだけです。

    …ところで、hunk とは何でしょうか? 差分の表示は @@ -0,0 +1,2 @@ のような @@ の行で区切られていますね? この区切られた一つひとつを hunk と呼んでいます。

    f:id:masm11:20191023024002p:plain

    この hunk ごとに stage できるわけです。しかも手軽に。

  • ファイル中の一部の変更のみ unstage する

    Staged changes の差分表示中に u を押すと、その hunk のみを unstage することができます。

  • ファイル中の一部の変更のみ元に戻す

    Unstaged changes の差分表示中に k を押すと、その hunk をキャンセルして元に戻すことができます。

差分を見ながら、「この hunk を stage」「この hunk はやっぱり変更キャンセル」などが 手軽にできるのは、とても便利です。

まとめ

私がよく使う Magit の操作を紹介してみました。 他にもいろいろな操作ができます。 Magit バッファで ? と入力すると、どのキーでどんな操作ができるのかがわかります。

f:id:masm11:20191023024035p:plain

今回はこれらのうち極一部しか説明しませんでした。いろいろ試してみると良いでしょう。

ではまた。

エンコードされた国際化ドメイン名をデコードする

こんにちは。masm11 です。

国際化ドメイン名をご存知でしょうか? インゲージ.jp みたいなやつのことですね。 しかし、時々、xn--eck2as8usb.jp と表示されていることがあって、「これ、一体どこ??」 と思うことがあります。今回はこの文字列を元に戻す方法をご紹介します。

国際化ドメイン名とは

ドメイン名には日本語は使えません。 しかし、一部には「日本語を使いたいんじゃああ!!」という人もいて、 対応する必要がありました。 ですが、やっぱりドメイン名には日本語は使えないので、 英数字に変換して使うことにしたのです。

例えば、

インゲージ.jpxn--eck2as8usb.jp

といった感じです。xn-- が付いていれば、国際化ドメイン名を変換したものです。

ブラウザなどでは自動で変換してアクセスしてくれて、便利に使えます。 ですが、時々 xn--eck2as8usb.jp だけを見せられて、「これ何?」と判断する必要に迫られる場面もあります。

変換方法

国際化ドメイン名を英数字に変換するには、Punycode と呼ばれる変換方法を使っています。

Linux では、ライブラリとしては libidn、ツールとしては idn というコマンドが 存在します。

実際に使ってみましょう。

[~]$ echo 'インゲージ.jp' | idn
xn--eck2as8usb.jp
[~]$ 

このように変換してくれます。逆に変換したい時には、-u を付けて、

[~]$ echo 'xn--eck2as8usb.jp' | idn -u
インゲージ.jp
[~]$ 

というように使います。また、

[~]$ echo 'xn--6wyu77cyid.xn--eck2as8usb.jp' | idn -u
開発部.インゲージ.jp
[~]$ 

というように、サブドメインにも対応しています。

まとめ

変換された国際化ドメイン名を元に戻す方法をご紹介しました。 これで、国際化ドメイン名で困ることが一つ減りました。

ですが、国際化ドメイン名には他にも問題があります。

[~]$ echo 'インゲ−ジ.jp' | idn
xn--t9gx68chbza60a.jp
[~]$ 

変換すると上に挙げた例と結果が異なるのですが、何故でしょうか? 実は「−」が長音記号でなくハイフンなのです。 英数字なら見間違えることは滅多にないのですが(それでも l1 は区別しづらいですが)、日本語や、もっと広く世界の言語を相手にすると、見間違えることが増えます。以前、Google.com の "G" の文字がよく似た別の文字の詐欺サイトがあったらしいですね。怖い怖い。

以上、国際化ドメイン名が嫌いな masm11 でした。