ESM アジャイル事業部 開発者ブログ

永和システムマネジメント アジャイル事業部の開発者ブログです。

Rails / OSS パッチ会オンライン 2024年3月のお知らせ

2024年3月の Rails / OSS パッチ会を 3月13日(水)に Discord でオンライン開催します。

この会をひとことでいうと、日頃のお仕事で使っている Rails をはじめとする OSS について、upstream にパッチを送る会です。

会には Ruby と Rails のコミッターである顧問の a_matsuda もいますので、例えば Rails に送るパッチのネタがあるけれど、パッチを送るに適しているかの判断やパッチを送る流れが悩ましいときなど a_matsuda に相談して足がかりにするなどできます。

開催時間は 17:00-19:00 となりますがご都合のあう方はぜひご参加下さい。

Discord の Rails/OSS パッチ会サーバーへの招待 URL は以下です👇

discord.gg

5月に開催される RubyKaigi 2024 に向けた話題などがあるかもしれません。

弊社の構文解析器研究部に関心がある方や、これからパッチ会に参加してみたいという方、OSS 開発者間の会話に興味があるので聞いてみたいという方もお気軽にどうぞ。


永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発を通じてコミュニティと成長したいエンジニアを絶賛募集しています。

agile.esm.co.jp

パターンマッチで NODE_ONCE に対応するようにした

こんにちは、構文解析研究部員の S.H. です。

社内でやっているエンジニアお茶会で (@koic) さんから以下のIssueの件で「Rubyのパーサー周りの問題ではないか」と相談を受けました。

github.com

そこで調査したところ比較的簡単に修正できそうなことが分かり、以下のPull Requestを作成して無事マージされました。

github.com

これは、その時の調査した内容などをまとめた記事になります。

もともとの挙動

RuboCop と RuboCop Performance を使っている際に以下のようなコードを自動修正すると、コンパイルエラーになるコードへと変換されてしまうようです。

自動修正前のコード:

v = "117"

case "117"
in /#{v}/
  p "Passed"
end
# => Passed

自動修正後のコード:

v = "117"

case "117"
in /#{v}/o # オプションとして `o` が末尾についている
  p "Passed"
end
# => NODE_IN: unknown node (NODE_ONCE)

自動修正前のコードとの差分はオプションとして o が末尾についているかどうかだけのようです。またエラーメッセージにある NODE_INNODE_ONCE はRuby内部で利用しているASTノードのことなのでRuby本体側の問題らしいことが分かりますね。

調査

まずはエラーメッセージを元に CRuby のソースコードを grep してみました。

NODE関連のエラーメッセージなので該当しそうな個所としては parse.y、node.c、node_dump.c、compile.c、 ruby_parser.c あたりが怪しそうでした。ただ parse.y、node.c、node_dump.c、ruby_parser.c に関してはおそらく無関係かもしれないな思っていました。

これは以下のように考えたためです。

  • parse.y、node.c、node_dump.c については、Universal Parser の C API依存削減対応や新しい NODE 追加などを直近で調べていたなので、該当しそうな箇所を見た覚えがないので無関係かもしれない
  • またIssueのコードからパターンマッチ関係で発生しているエラーらしく、以前からあるソースコードが怪しいため比較的最近追加された ruby_parser.c ではなさそう

そこで compile.c のソースコードを grep すると該当しそうなところがありました。

github.com

iseq_compile_pattern_each という関数の中でエラーメッセージを出力して、パターンマッチで受け取ったパターンのASTノードに合わせて ISeq に変換しているようです。またエラーメッセージと合わせて考えると、 NODE_ONCE というASTノードにマッチした時の処理がないためエラーになっていそうですね。

次に再現コードを作成し、最新のRuby(ruby 3.4.0dev (2024-02-01T12:17:37Z master 8531ac3115) [x86_64-linux])で実行してみました。

v = "117"

case "117"
in /#{v}/o
  p "Passed"
end

無事、コンパイルエラーになり再現できました。

test.rb:4: NODE_IN: unknown node (NODE_ONCE)
test.rb: compile error (SyntaxError)

その後、再現コードがどういったASTノードを生成しているのか dump してみました。

$ ruby --dump parsetree test.rb

以下が dump した結果になります。

###########################################################
## Do NOT use this node dump for any purpose other than  ##
## debug and research.  Compatibility is not guaranteed. ##
###########################################################

# @ NODE_SCOPE (id: 15, line: 1, location: (1,0)-(6,3))
# +- nd_tbl: :v
# +- nd_args:
# |   (null node)
# +- nd_body:
#     @ NODE_BLOCK (id: 13, line: 1, location: (1,0)-(6,3))
#     +- nd_head (1):
#     |   @ NODE_LASGN (id: 0, line: 1, location: (1,0)-(1,9))*
#     |   +- nd_vid: :v
#     |   +- nd_value:
#     |       @ NODE_STR (id: 1, line: 1, location: (1,4)-(1,9))
#     |       +- nd_lit: "117"
#     +- nd_head (2):
#         @ NODE_CASE3 (id: 12, line: 3, location: (3,0)-(6,3))*
#         +- nd_head:
#         |   @ NODE_STR (id: 2, line: 3, location: (3,5)-(3,10))
#         |   +- nd_lit: "117"
#         +- nd_body:
#             @ NODE_IN (id: 11, line: 4, location: (4,0)-(5,12))
#             +- nd_head:
#             |   @ NODE_ONCE (id: 7, line: 4, location: (4,3)-(4,10))
#             |   +- nd_body:
#             |       @ NODE_DREGX (id: 6, line: 4, location: (4,3)-(4,10))
#             |       +- nd_lit: ""
#             |       +- nd_next->nd_head:
#             |       |   @ NODE_EVSTR (id: 4, line: 4, location: (4,4)-(4,8))
#             |       |   +- nd_body:
#             |       |       @ NODE_LVAR (id: 3, line: 4, location: (4,6)-(4,7))
#             |       |       +- nd_vid: :v
#             |       +- nd_next->nd_next:
#             |           (null node)
#             +- nd_body:
#             |   @ NODE_FCALL (id: 8, line: 5, location: (5,2)-(5,12))*
#             |   +- nd_mid: :p
#             |   +- nd_args:
#             |       @ NODE_LIST (id: 10, line: 5, location: (5,4)-(5,12))
#             |       +- as.nd_alen: 1
#             |       +- nd_head:
#             |       |   @ NODE_STR (id: 9, line: 5, location: (5,4)-(5,12))
#             |       |   +- nd_lit: "Passed"
#             |       +- nd_next:
#             |           (null node)
#             +- nd_next:
#                 (null node)

これを見ると NODE_ONCE は動的に生成された正規表現用の NODE_DREGEX をラップしていることがわかります。

NODE_ONCE は6年ほど前に導入されたもので、 /regexp/o の時にのみ扱うASTノード ということも以下の記事から分かりました。

ruby-trunk-changes.hatenablog.com

また NODE_DREGX は以下の部分でパターンマッチ用のISeqを生成しているようでした。NODE_LIT(リテラル用のNODE)やNODE_STR なども同じ部分で判定しているようです。

github.com

ここまでのことから以下のことが分かりました。

  • パターンマッチ用のISeq生成処理でNODE_ONCEのケースがないため、すり抜けてエラーになっている
  • NODE_ONCENODE_DREGX をラップしているだけの NODE なので他への影響範囲は小さそう
  • NODE_DREGXと同じ部分にはNODE_LITやNODE_STRなどもあることから、そこへNODE_ONCEを渡してやればよさそう

これらの調査結果をもとに対応に進めます。

対応

iseq_compile_pattern_eachNODE_ONCE のケースでもISeqを生成するように修正してみました。すると以下の再現コードを実行できるようになりました。

v = "117"

case "117"
in /#{v}/o
  p "Passed"
end
# => Passed

あとはテストケースも追加してPull Requestを作成しました。

github.com

先日無事マージされ、現在のRubyのmasterでは修正されています。

反省点

無事修正できましたが反省点もありました。

https://bugs.ruby-lang.org にチケットを作成していなかった点です。

チケットを作成しておくことで後からチケットを見た時にどういった経緯だったかが確認しやすくなりますし、bugs.ruby-lang.org にはバックポートが必要かどうかをチケットに記載できたりもします。 そのため後からバックポートが必要な修正だった場合に、コミッターの方々が対応しやすくなる可能性があります。

こういった点からチケットを起票していなかった点は、良くないですね。

今後はチケットを起票するべきかなども考慮しつつ、対応を進められればと思いますね。


永和システムマネジメントでは、Rubyへのコントリビューションを通じてコミュニティと成長したいエンジニアを絶賛募集しています。

agile.esm.co.jp

サーバー依存のGPG errorを解決する

docker buildできない

宮崎からお送りします。yoshinoです。

私が担当しているプロジェクトでは、デプロイするために、お客様の管理するサーバー上でdockerイメージをbuildする必要があります。

先日、ローカルでは問題なくdocker buildできるのに、お客様サーバー上ではbuildできないという事象が起きて、試行錯誤しながら解決できましたので共有します(同僚のtaijuさんに助けてもらい解決できました)。

ローカルとサーバーではDockerfileの定義も少し異なり、本番用のDockerfileではhttps_proxyオプションを利用しており、 はじめはこの差異が原因かと考えたのですが、原因は別のところにありました。

docker buildで生じるエラー内容は以下のようになっていました。

Step 11/22 : RUN apt-get update
 ---> Running in 66ec271bf6ce
Get:1 http://deb.debian.org/debian bookworm InRelease [151 kB]
Get:2 http://deb.debian.org/debian bookworm-updates InRelease [52.1 kB]
Get:3 http://deb.debian.org/debian-security bookworm-security InRelease [48.0 kB]
.
.
W: GPG error: http://deb.debian.org/debian bookworm InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY .......
E: The repository 'http://deb.debian.org/debian bookworm InRelease' is not signed.

GPG error?

the public key is not availableと怒られているので、GPGのpublic keyまわりで何か問題が起きていることがわかります。
ローカルではエラーが起きていないので、単純なpublic key不足ではないだろうなと思いつつ調査します。

Dockerのコンテナに入りGPGのpublic keyが存在するかを確認すると、問題の原因につながりそうなヒントが得られました。

> apt-key list

ローカルでは登録されているpublic key が表示されているのですが、サーバーでは何も返ってこなかったのです(エラーはこの時点では起きませんでした。エラー起きて欲しかった。)

改めてpublic keyが入っているはずのディレクトリをサーバー上で確認してみます。

> ls /etc/
ls: cannot access '/etc/': Operation not permitted

!!!!?

ディレクトリにパーミッションの関連でアクセスできない事象が発生していることがわかりました。

調べてみると、Dockerへの権限付与を行う箇所の問題であるようで、すべてのシステムコールが利用可能になるようなオプション(--security-opt="seccomp=unconfined")を付与して実行すると、buildが成功することを確認できました!!!

> docker run --security-opt="seccomp=unconfined" -it test bash
> apt-get update

原因と修正方法

原因と修正方法の話の前に、seccompというカーネル機能について知っておく必要があります(突然の「Operation not permitted」—Dockerが採用するセキュリティ機構「Seccomp」とは何か?を参考にさせていただきました)。

  • Seccompとは、Linuxカーネルが持つセキュリティ機構の一つで、Secure Computing Modelの略です。簡単に言うと、Seccompはシステムコールの許可・不許可を設定できるようにし、危険なシステムコールを実行できなくするためのものです。
  • Seccompはカーネルの機能であり、Dockerはカーネルとの仲介にlibseccompライブラリを使っています。

つまり、原因は以下のように推察されます。

Dockerで使用しているruby:3.2.2のコンテナではlsコマンドは「lstat」ではなく「statx」というシステムコールを使用しており、Dockerが起動しているサーバーではこのシステムコールが禁止されているため、operation not permitted が出力された。

Dockerが起動しているサーバーのlibseccompのバージョンが古いことにより、システムコールのルール制御も古くなり、Docker上で利用するシステムコールとのミスマッチが起きていることが原因だと考えられます。したがって、修正方法は2つあることがわかります。

1つ目は「linuxのバージョンを下げる」ことです。
例えば、Dockerのimageをruby:3.2.2-busterに指定するだけで対応可能です。
この対応でも動くことは確認できましたが、LTS のサポート期間は2024-06-30 までなので、一時的な対応となってしまいます。

2つ目は「対象サーバーのlibseccompのバージョンを上げる」ことです。
今回はこちらの方法を採用して、古かったバージョン(libseccomp-2.3.1-3.amzn2.0.1.x86_64)を最新バージョン(libseccomp-2.5.2-1.amzn2.0.1.x86_64)にアップデートすることで対応しました。

おわりに

実はこの問題が生じたのは、imageのバージョンを変更した時点ではありませんでした(そのため原因推測がより難しかった)。
はっきりと認識できているわけではないのですが、問題が生じたタイミングはdocker のcacheを削除した時点だと思います。 以下は推測になります。

ruby:3.1 -> ruby:3.2のようにイメージを変更した場合、低レイヤの部分(linux部分?)はcacheされ、そのまま使われ上位レイヤだけ変更される。そのため、例えばruby:3.1 で古いlinuxバージョンがcacheされruby:3.2では新しいバージョンのlinuxが使われているにもかかわらず、build時はcacheが使われて気がつかない。そして、cacheを削除した段階で、新しいlinuxバージョンが利用されるようになり、問題が顕在化する。

運用という最上流工程にたずさわる*1、同士の皆様の助けに少しでもなることがあれば、 著者としてこれ以上の喜びはありません。 現場からは以上です。

物理スイッチとウェブ UIを繋ぐ。Elixirで。

はじめに

近ごろ IoT プログラミングしたい欲が再起動した e.mattsan です。

IoT とは言っても Raspberry Pi などではマシンパワーも小さくなく、外部にデバイスを接続する以外は PC のプログラミングとほぼ変わりありません。

今回の記事は、題名の通り、ブラウザ上に表示したボタンでブレッドボード上の LED の点灯状態を制御し、ブレッドボード上のスイッチでブラウザ上のアイコンの表示状態の変更をすることを試みます。

Elixir とウェブアプリケーションと IoT

今回も、使う言語は Elixir です。

Elixir には Phoenix というウェブアプリケーションのフレームワークがあります。 また Phoenix ほど知られていないかもしれませんが、組み込みシステム向けの Nerves というプラットフォームも開発が進んでいます。 加えて。ハードウェア制御のための Elixir Circuits というライブラリ群も充実してきています。

www.phoenixframework.org nerves-project.org elixir-circuits.github.io

今回の記事では、一足飛びにウェブと LED を繋ぐのでなく、LED の自動点滅から始まって最終的にウェブからの制御まで進めてゆきます。 駆け足で実装してゆきますので、環境の用意など詳細は適宜ドキュメントを確認してください。

ハードウェア制御のハードルは意外に低い、ということをこの記事で実感していただけたら幸いです。

用意するもの

デバイスを制御するので、開発マシン以外にもいくつか用意が必要です。

  • 開発およびウェブアプリケーションを実行するマシン
    • ここに掲載したコードは MacBook Pro で開発し動作を確認しました
  • 組み込みシステムとして Raspberry Pi
    • 今回は Raspberry Pi Zero W を利用しています
  • Nerves アプリケーションを書き込む micro SD
    • 開発マシンから書き込めるようにアダプタ等を用意しておいてください
  • LED と物理スイッチ、電流制限や入力プルダウンのための抵抗器、およびそれらを配線するためのブレッドボードとケーブルをいくつか
  • 開発マシンと Raspberry Pi を接続する USB ケーブル
    • 通信と給電を兼ねます

準備ができたら、まずは定番の LED を点滅させるプログラムから作ってゆきます。

Nerves アプリケーションで LED を点滅させる

最初に Raspberry Pi 単体で LED を点滅させる Nerves アプリケーションを作成してみます。

開発マシン上で開発し、起動可能なバイナリを作成して microSD に書き込み、それを使って Raspberry Pi を起動するという段取りになります。

Nerves 公式のドキュメントに従って環境の用意ができたら、新しい Nerves アプリケーションのプロジェクトを作成します。

mix nerves.new led
cd led

対象となるハードウェアは環境変数で指定します。 今回は Raspberry Pi Zero W を対象にするので次のように指定しておきます。

export MIX_TARGET=rpi0

GPIO パッケージを追加する

Raspberry Pi の GPIO の制御には Elixir Circuits の GPIO パッケージを使います。

Nerves アプリケーションの mix.exs ファイルを編集して依存するパッケージの指定を追加し、mix deps.get コマンドでパッケージを取得します。

# mix.exs

  defp deps do
    [
      # ... 略
      {:circuits_gpio, "~> 2.0"}
    ]
  end

LED を点滅させる

LED を点滅させるプロセスを扱うモジュールを作成します。

このモジュールでは、 1 秒ごとに自分にメッセージを送信し、メッセージを受信するたびに状態を反転して GPIO に書き込むことで点滅を実現します。

# lib/led.ex

defmodule Led do
  use GenServer

  # プロセスを起動する関数
  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  # プロセスを初期化する関数
  def init(_opts) do
    {:ok, gpio17} = Circuits.GPIO.open("GPIO17", :output) # GPIO 17 を出力として利用

    tick()

    {:ok, %{gpio17: gpio17, led: 0}} # LED の初期状態として 0 = 消灯 を記憶
  end

  # 受信した :tick メッセージをハンドリングする関数
  def handle_info(:tick, state) do
    led = 1 - state.led # LED の状態を反転

    set_led(state.gpio17, led) # GPIO に値を書き込む
    tick()

    {:noreply, %{state | led: led}} # 新しい LED の状態で更新する
  end

  defp tick do
    Process.send_after(self(), :tick, 1_000) # 1,000 ミリ秒後に自分に :tick というメッセージを送る
  end

  defp set_led(gpio, value) do
    Circuits.GPIO.write(gpio, value)
  end
end

アプリケーションを起動したときに作成したモジュールのプロセスも起動するように、lib/led/application.ex を編集して起動プロセスの一覧にモジュール名を追加します。

# lib/led/application.ex

    children =
      [
        # ... 略
        Led
      ] ++ children(target())

mix firmware コマンドで実行してバイナリイメージを作成します。

バイナリができたら、開発マシンにアダプタなどで microSD を接続してアクセスできる状態になるのを待ち、 mix firmware.burn コマンドで書き込みます。 書き込みが完了すると自動的に microSD が取り出された状態になるので、microSD を Raspberry Pi に差し替えます。

開発マシンから直接 microSD へ書き込む必要があるのはこの初回のみで、次回以降は USB でアップロード可能です。

Raspberry Pi の準備ができたら、部品をブレッドボード上で繋ぎます。 次のような並びでつないでください。

GPIO 17 --- 抵抗器 --- LED --- GND

最後に開発マシンと Raspbery Pi を USB ケーブルを繋いで Raspberry Pi を起動します。 後ほど通信を行いますので、給電専用のポートに繋がないように注意してください。

少し待つと LED が 1 秒間隔で点滅し始めることが確認できると思います。

スイッチで LED を点灯消灯する

次に、LED の点灯をスイッチで制御できるようにします。 ここでは GPIO 17 ピンの隣の GPIO 27 ピンを入力として利用することにします。

先ほどのモジュールのコードを編集し、GPIO 27 を入力として設定します。 加えて GPIO 27 の入力の立ち上がりで割り込みが発生するように設定します。

割り込みは GPIO パッケージで Elixir のメッセージに変換され送信されます。 先ほどと同じように、そのメッセージをハンドリングすることで LED の状態を制御できるようになります。

# lib/led.ex

defmodule Led do
  use GenServer

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  def init(_opts) do
    {:ok, gpio17} = Circuits.GPIO.open("GPIO17", :output)
    {:ok, gpio27} = Circuits.GPIO.open("GPIO27", :input) # GPIO 27 を入力として利用

    :ok = Circuits.GPIO.set_interrupts(gpio27, :rising) # 信号の立ち上がりの時に割り込みが発生するように設定する

    {:ok, %{gpio17: gpio17, gpio27: gpio27, led: 0}}
  end

  # 受信した GPIO メッセージをハンドリングする関数
  def handle_info({:circuits_gpio, _gpio_spec, _timestamp, _value}, state) do
    led = 1 - state.led

    set_led(state.gpio17, led)

    {:noreply, %{state | led: led}}
  end

  defp set_led(gpio, value) do
    Circuits.GPIO.write(gpio, value)
  end
end

mix firmware コマンドを実行してバイナリイメージを作成します。

バイナリを書き込む前に、ブレッドボード上にスイッチを用意します。 次のような並びでつないでください。

GPIO 27 ---+--- スイッチ --- 電源
           |
         抵抗器
           |
          GND

バイナリができたら、今度は直接 microSD に書き込む代わりに mix upload コマンドを実行して USB 経由でアップロードします。 アップロードが完了すると Raspberry Pi は自動的に再起動します。

起動シーケンスが終わるのを待ってスイッチを何回か押してみてください。 押すたびに点灯消灯が切り替わるのが確認できると思います。

ソフトウェアインタフェースを追加する

ウェブアプリケーションに移る前に、ソフトウェアインタフェースを用意しておきます。 点灯/消灯のための onoff 、状態を調べるための on? の 3 つです。 このインタフェースは、あとでウェブアプリケーションからも利用します。

モジュールに次のコードを追加します。

ここで cast, call という関数と handle_cast, handle_call というハンドラが出てきますが、これらもメッセージのやり取りで実現されています。 castcall で LED を制御するプロセスにメッセージを送り、ハンドラで受信したメッセージを処理します。 cast の場合は一方通行ですが call の場合は値を返すことができます。

# lib/led.ex

  def on do
    GenServer.cast(__MODULE__, :on)
  end

  def off do
    GenServer.cast(__MODULE__, :off)
  end

  def on? do
    GenServer.call(__MODULE__, :on?)
  end
# lib/led.ex

  def handle_cast(:on, state) do
    set_led(state.gpio17, 1)
    {:noreply, %{state | led: 1}}
  end

  def handle_cast(:off, state) do
    set_led(state.gpio17, 0)
    {:noreply, %{state | led: 0}}
  end

  def handle_call(:on?, _from, state) do
    {:reply, state.led == 1, state}
  end

もう一度バイナリを作成しアップロードします。 Raspberry Pi が再起動したら、USB で SSH 接続を試します。 開発マシンから次のように入力してください。

$ ssh nerves.local # Nerves アプリケーション標準のホスト名

接続できたら Nerves プロジェクトのロゴとプロンプトが表示されると思います。 プロンプトが表示されたら次のように関数を入力して実行してみてください。 Led.on で点灯、Led.off で消灯、Led.on? で状態の確認ができると思います。

iex(1)> Led.on
:ok
iex(2)> Led.off
:ok
iex(3)> Led.on?
false
iex(4)> Led.on
:ok
iex(5)> Led.on?
true

SSH 接続から抜けるには exit 関数を実行します。

iex(6)> exit

ウェブアプリケーションを作る

ウェブアプリケーションには Phoenix フレームワークを利用しますが、さらにブラウザ・サーバ間で双方向の通信を実現したいので LiveView という仕組みを利用します。

開発マシンで、Nerves アプリケーションのディレクトリとは別に、新しい Phoenix アプリケーションのプロジェクトを作成します。

mix phx.new web_ui --no-ecto # データベースを使用しないオプションを指定
cd web_ui

プロジェクトができたら、LiveView のモジュールの新しいファイル lib/web_ui_web/live/led_live.ex を追加します。

# lib/web_ui_web/live/led_live.ex

defmodule WebUiWeb.LedLive do
  use WebUiWeb, :live_view

  def mount(_params, _session, socket) do
    {:ok, assign(socket, led: 0)}
  end

  def render(assigns) do
    ~H"""
    <.button phx-click="on">ON</.button>
    <.button phx-click="off">OFF</.button>
    <.icon :if={@led != 1} name="hero-light-bulb" class="bg-gray-500" />
    <.icon :if={@led == 1} name="hero-light-bulb-solid" class="bg-blue-500" />
    """
  end

  def handle_event("on", _params, socket) do
    {:noreply, assign(socket, led: 1)}
  end

  def handle_event("off", _params, socket) do
    {:noreply, assign(socket, led: 0)}
  end
end

次に、作成した LiveView を利用するようにルーティングを変更します。 lib/web_ui_web/router.ex を編集し、get "/", PageController, :home となっている行を live "/", LedLive に書き換えます。

# lib/web_ui_web/router.ex

   scope "/", WebUiWeb do
     pipe_through :browser
 
-    get "/", PageController, :home
+    live "/", LedLive
   end

ここまでできたらサーバを起動します。

mix phx.server

サーバが起動したら https://localhost:4000 にアクセスしてみてください。 ON と OFF の 2 つのボタンととアイコンが 1 つ表示され、ボタンを押すとアイコンの状態が変化することが確認できると思います。

今はブラウザ上の表示だけの制御ですが、これをこの後ブレッドボード上の LED と連動させてゆきます。

ノードを接続する

連動のために、開発マシン上で実行されている Phoenix アプリケーションと Raspberry Pi 上で実行されている Nerves アプリケーションを接続することを考えます。

今回は Elixir のノードを接続する仕組みを利用します。 ノードとはここではそれぞれのアプリケーションのことと思っていてください。 同一マシン上で起動した複数のノードも、ネットワークで繋がった異なるマシンで起動したノードも、この仕組みで接続することが可能です。

Nerves アプリケーション側のノードを設定する

異なるマシンの間でノードを接続するには、ホストの名前を含んだノード名と、共通で利用するクッキーを設定しておく必要があります。

そのためにプロセスの初期化のコードに次の 3 行を追加します。

# lib/led.ex

  def init(_opts) do
    # ... 略

    # 次の 3 行を追加
    {_, 0} = System.cmd("epmd", ["-daemon"])
    {:ok, _} = Node.start(:"nerves@nerves.local")
    true = Node.set_cookie(:my_secret_cookie)

    {:ok, %{gpio17: gpio17, gpio27: gpio27, led: 0}}
  end

追加したら mix firmware & mix upload でアップロードします。

接続を確かめる

Raspberry Pi が再起動するのを待って、開発マシン側で Elixir のインタラクティブシェルを起動します。 Nerves アプリケーションと同じように、ここでもノード名とクッキーを設定します。

iex --name me@0.0.0.0 --cookie my_secret_cookie

プロンプトが表示されたら次のように関数を実行してください。 うまく接続できていれば LED が点灯するはずです。

GenServer.cast({Led, :"nerves@nerves.local"}, :on)

これはノードの名前を指定している以外は先ほど Nerves アプリケーションに追加した関数と同じ内容であることがわかると思います。 ノードを接続しても開発マシン側から Nerves アプリケーションで定義された関数が見えるわけではないので、内部の実装をそのまま使って実行しています。

接続できていれば GenServer.cast({Led, :"nerves@nerves.local"}, :off) で消灯したり、GenServer.call({Led, :"nerves@nerves.local"}, :on?) で状態を確認したりすることも可能です。

ウェブアプリケーションに LED のインタフェースを追加する

開発マシンから LED の点灯の制御の確認できたので、次はウェブアプリケーションからの操作を実装します。

今回は Nerves アプリケーションとウェブアプリケーションでコードを共有していないので、ウェブアプリケーションに LED 操作のための新しいモジュールを追加することにします。

ウェブアプリケーションに新しく lib/web_ui/led.ex というファイルを追加します。

# lib/web_ui/led.ex

defmodule WebUi.Led do
  def on do
    GenServer.cast({Led, :"nerves@nerves.local"}, :on)
  end

  def off do
    GenServer.cast({Led, :"nerves@nerves.local"}, :off)
  end

  def on? do
    GenServer.call({Led, :"nerves@nerves.local"}, :on?)
  end
end

LiveView のボタンイベントのハンドラでこの追加したモジュールの関数を呼び出します。

# lib/web_ui_web/live/led_live.ex

defmodule WebUiWeb.LedLive do
  use WebUiWeb, :live_view

  def mount(_params, _session, socket) do
    # 現在の LED の状態を取得して表示に反映する
    led =
      if WebUi.Led.on?() do
        1
      else
        0
      end

    {:ok, assign(socket, led: led)}
  end

  # ... 略

  def handle_event("on", _params, socket) do
    WebUi.Led.on() # 追加
    {:noreply, assign(socket, led: 1)}
  end

  def handle_event("off", _params, socket) do
    WebUi.Led.off() # 追加
    {:noreply, assign(socket, led: 0)}
  end
end

ページを再読み込みしたら ON / OFF のボタンを押してみてください。

LED の状態をウェブアプリケーションに通知する

いよいよ最後です。 LED の状態の変化をウェブアプリケーションに通知し、ブレッドボード上のスイッチ操作で点灯、消灯する LED の状態をブラウザの表示に反映させます。

通知を受けるプロセスを用意する

まず、先ほどウェブアプリケーションに追加したモジュールをプロセス化して、スイッチ操作のイベントメッセージを受信できるようにします。 表示に反映する前にログを出力することでメッセージを受信していることを確認します。

# lib/web_ui/led.ex

defmodule WebUi.Led do
  use GenServer

  require Logger # 追加

  # プロセスを起動する関数
  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  # ... 略

  # プロセスを初期化する関数
  def init(_opts) do
    {:ok, nil}
  end

  # スイッチ操作のイベントメッセージをハンドリングする関数
  def handle_cast(message, state) do
    Logger.debug(inspect(message)) # メッセージをログ出力する

    {:noreply, state}
  end
end

アプリケーションを起動したときにプロセスも起動するように、起動プロセスの一覧に今回作成したモジュールを追加します。

# lib/web_ui/application.ex

    children = [
      # ... 略
      WebUi.Led
    ]

Nerves アプリケーション側では LED の状態が更新されたらウェブアプリケーションへメッセージを送るようにします。

# lib/led.ex

  defp set_led(gpio, value) do
    GenServer.cast({WebUi.Led, :"me@0.0.0.0"}, {:nerves_led, value}) # 追加

    Circuits.GPIO.write(gpio, value)
  end

mix firmware & mix upload でアップロードします。

Raspberry Pi が再起動したら、ウェブアプリケーションも再起動します。 このとき、ノード名とクッキーを設定するために次のように起動してください。

elixir --name me@0.0.0.0 --cookie my_secret_cookie -S mix phx.server

ブラウザを開いて ON / OFF ボタンで操作できることが確認できたら、次はブレッドボード上のスイッチを押してみてください。 LED の状態が変化するたびにウェブアプリケーションのログにその状態が表示されることを確認できると思います。

...
[debug] {:nerves_led, 0}
[debug] {:nerves_led, 1}
...

LED の状態を表示する

ウェブアプリケーション側のプロセスでメッセージを受信できたので、今度はこのメッセージを LiveView のプロセスにブロードキャストします。 LiveView プロセスはブロードキャストされたメッセージを受けて LED の状態をブラウザの表示に反映します。

Phoenix にはこのようなときのために Phoenix.PubSub というライブラリが用意されているのでこれを利用します。

ウェブアプリケーション側の WebUi.Led モジュールに、購読を設定する subscribe 関数と、メッセージを受けた時に購読しているプロセスにブロードキャストするコードを追加します。

# lib/web_ui/led.ex

defmodule WebUi.Led do
  use GenServer

  @topic "nerves:led" # 追加

  # ... 略

  # 購読したいプロセスが呼び出す関数
  def subscribe do
    Phoenix.PubSub.subscribe(WebUi.PubSub, @topic)
  end

  # ... 略

  def handle_cast(message, state) do
    Logger.debug(inspect(message))

    # 購読しているプロセスにメッセージをブロードキャストする
    Phoenix.PubSub.broadcast(WebUi.PubSub, @topic, message)

    {:noreply, state}
  end
end

LiveView のモジュールには、購読の開始とブロードキャストされたメッセージをハンドリングするコードを追加します。

# lib/web_ui_web/live/led_live.ex

defmodule WebUiWeb.LedLive do
  use WebUiWeb, :live_view

  def mount(_params, _session, socket) do
    # 接続が確立したら購読を開始する
    if connected?(socket) do
      WebUi.Led.subscribe()
    end

    # ... 略
  end

  # ... 略

  # メッセージを受信したら表示状態を更新する
  def handle_info({:nerves_led, led}, socket) do
    {:noreply, assign(socket, led: led)}
  end
end

複数のブラウザでページを開いて、ブレッドボード上のスイッチを押したり、ブラウザ上のボタンを押したりしてみてください。 ブレッドボード上のLED とブラウザ上のアイコンの状態がすべて連動していることが確認できると思います。

考察

今回は大まかに次の図のような構成になりました。

ここで赤い線の部分が Elixir のメッセージングで通信している部分です。

  • ネットワークからのリクエスト
  • デバイスをまたいだノード間の通信
  • ハードウェアの割り込み
  • 状態通知のブロードキャスト

すべて同じメッセージングの仕組みで処理できています。

Elixir の土台である Erlang/OTP が分散コンピューティングを標榜し、元々このようなことができるように設計されているので不思議ではないのかもしれませんが。

それでも、プロセスとメッセージによるプロセス間通信という Elixir の基本的な仕組みを使って、さまざまな要素を繋ぐことができるというのは、やはり興味深いものがあります。

つい先日、Elixir の新しい書籍が出版されました。 基本的なプログラミングだけでなく Phoenix や Nerves 、今回の記事では触れなかった数値計算や機械学習についてもくわしく解説されています。

弊社とも 箱庭ラボ などを介してゆかりのある 高瀬英希 先生 が Nerves の章を執筆されています。

この記事を読んで、Elixir のことを「なんだかよくわからないけれども面白そうだ」と思われた方は、お手に取ってみるのもよいかもしれません。


また「なんだかよくわからないけれどもこんな記事を書く面白そうな人間がいるぞ」と関心を持たれた方は、こちらをのぞいてみるのもよいかもしれません。

お立ち寄り、お待ちしております。

agile.esm.co.jp

Rails / OSS パッチ会オンライン 2024年2月のお知らせ

2024年2月の Rails / OSS パッチ会を 2月28日(水)に Discord でオンライン開催します。

この会をひとことでいうと、日頃のお仕事で使っている Rails をはじめとする OSS について、upstream にパッチを送る会です。

会には Ruby と Rails のコミッターである顧問の a_matsuda もいますので、例えば Rails に送るパッチのネタがあるけれど、パッチを送るに適しているかの判断やパッチを送る流れが悩ましいときなど a_matsuda に相談して足がかりにするなどできます。

開催時間は 17:00-19:00 となりますがご都合のあう方はぜひご参加下さい。

Discord の Rails/OSS パッチ会サーバーへの招待 URL は以下です👇

discord.gg

5月に開催される RubyKaigi 2024 に向けた話題などがあるかもしれません。

弊社の構文解析器研究部に関心がある方や、これからパッチ会に参加してみたいという方、OSS 開発者間の会話に興味があるので聞いてみたいという方もお気軽にどうぞ。


永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発を通じてコミュニティと成長したいエンジニアを絶賛募集しています。

agile.esm.co.jp

【レポート】中高生国際Rubyプログラミングコンテスト2023 in Mitaka 永和賞受賞者の方との交流会を開催しました

こんにちは!maimuです。

昨年12月に東京都三鷹市で中高生国際Rubyプログラミングコンテスト2023 in Mitakaが開催されました。

www.ruby-procon.net

「つくりたい!が世界を変えていく!」をテーマに中高生がプログラミングを学び、自分でコンピューターを動かしたときの感動やつくった作品を発表する経験を通して将来への夢をはぐくむことを目的に毎年実施されているイベントです。永和システムマネジメントはスポンサーとして参加し、部門賞として「永和賞」を選出しました。

blog.agile.esm.co.jp

今回のコンテストではWebアプリ・IoT部門で金光峻希さんが最優秀賞と永和賞を受賞されました。

永和賞受賞の副賞として書籍のプレゼントと金光さんを東京の弊社オフィスにお招きして交流会を開催したため、その内容をレポートとしてまとめたいと思います!

好きなRubyメソッドは?の自己紹介からワイワイ開発タイム

金光さんとの交流会は1月末に永和システムマネジメント東京支社で開催しました。

アジャイル事業部所属の

の3名が交流会の企画担当として、当日どんな風に進めていくかなどを話し合いながら準備を進めていきました。

交流会当日は以下のタイムテーブルを用意していたのですが、話が盛り上がりすぎて時間が足りなくなるほど充実した会となりました。

自己紹介ではアジャイル事業部恒例?の「好きなRubyメソッド」についてを紹介し、話が弾み始めた頃に金光さんが開発されたアプリに軸を移して技術選定についてや最新技術へのキャッチアップの仕方、Rubyについてなどプログラミングに関する話題でワイワイ会話しながら開発作業タイムを楽しみました。

金光さんの開発されたアプリ「SQSO Web」

金光さんはSQSO Webというアマチュア無線のログ(交信記録・業務日誌)を作成するための Web アプリケーションを開発されました。

アマチュア無線の部活用PC(GNU/Linux)で使えるログソフトが無かったことから、自分の状況に合ったログアプリをスマートフォンやタブレットからも利用できるようにするため自分自身でWebアプリとして開発したそうです。

Rakefile、ディレクトリ構造、サーバー起動用のシェルスクリプト、Rodauthの導入などバックエンドもこだわりが詰まったWebアプリです。

弊社アジャイル事業部では普段Ruby on Railsを利用して開発をしていますが、金光さんがご自身のアプリに利用された技術への洞察が深く「RodauthとRailsの組み合わせはどうですか?」と逆に提案をいただいて盛り上がる場面もありました。

スペシャルゲストの登場

金光さんのアプリではフレームワークとしてRodaを使用されています。

その繋がりでRodaの開発者であるJeremy Evansさんの書籍『研鑽Rubyプログラミング』を永和賞の副賞としてプレゼントすることになりました。

研鑽Rubyプログラミングは弊社フェローである角谷信太郎さんが翻訳者であることから、交流会当日角谷さんにもご参加いただけないかご相談したところ快諾いただき、スペシャルゲストとして盛り上げていただきました。

交流会を終えて

今回、オンラインではなくオフラインの場で交流会を開催したのですが、プログラミングが大好きな人達が同じ場に集まり、ワイワイすることで生まれる「場の熱量」を体感し、会に参加した永和メンバーもとても良い刺激をいただく機会になりました。

金光さんからも以下の感想をいただき、その場にいた全員が有意義な時間を過ごせたのではないかと思います。

交流会が始まるまでは緊張していましたが、皆様が温かく迎えて下さったお陰で、楽しく過ごすことができました。 バグの修正や新機能の追加についてなどアドバイスを頂き、大変勉強になりました。 また、読みたいと思っていた『研鑽Rubyプログラミング』を頂き、その上訳者の角谷様よりサインを書いていただき、とても感激しました。

金光さん自身は開発されたアプリの機能追加も検討されていて、今後の拡張がとても楽しみです。

金光さん、東京までご訪問いただきありがとうございました!


アジャイル事業部のRubyアジャイル受託開発は、アジャイル開発を10年以上続けて培った、決して手法や方法論ではまとめきれない、実践知や価値観、それを届ける人で構成されています。Ruby とアジャイルソフトウェア開発を通じてコミュニティと共感していけるお客様とエンジニアを絶賛募集しています。

agile.esm.co.jp

オンライン技術顧問サービスはじめました

永和システムマネジメント アジャイル事業部として『オンライン技術顧問サービス』をはじめました。ひとことでいうと、私 (@koic) による技術顧問サービスです。

どんなサービス?

アドバイザーという位置付けで参画する形での有料サービスです。基本的な動きとしては、まず GitHub リポジトリに招待していただくことになります。すでに本サービス相当の入り方をしている実績としては、招待していただいたリポジトリは OSS リポジトリと同じように私の方で巡回して見ていくことになります。その過程で、Pull Request のレビューコメントを行ったり、RuboCop のアップグレードなどの困りごとの相談に乗ったりしています。また、Ruby/Rails を軸としたコミュニティ横断的な足まわりに関する Pull Request を送ることがあります。

やらないこと

お客様によって対応を変えるということをしないため、オンラインのみの対応としており GitHub でのやりとりがベースとなります。とはいえ定期的に何か口頭で話したいといったご要望など、そちらは応相談という形になります。また、開発案件としていつまでにどこまで何を行うかといった機能開発は行いません。

どうして始めたの?

私は、アジャイルソフトウェア開発を軸に、Ruby on Rails のお仕事は Rails 初期から行っており、近年は OSS まわりでも日常的に開発を続けています。生涯で見ることのできるプロジェクトの数は限られていることもあり、これまでとこれからのノウハウをひとつでも多くのプロジェクトに届ける機会として、今回のサービスを始めてみました。

ちなみに企業として OSS に関わってみたいけれど、OSS への関わり方がわからないという Ruby/Rails を活用の企業様にもオススメです。どういうことかというと、まず私は対象リポジトリを RuboCop で実行してみるため、そこで見つかった RuboCop のバグは RuboCop 本体に取り込みます。そのように OSS 開発者がコードベースに触れることで OSS を改善してくという流れもありますので、このような視点もあわせてぜひご検討ください。

お問い合わせ先

そのほかの概略は以下のサービスページに記載しております。気になったり、後日に思い出した折りは、こちらまでお問い合わせください。

agile.esm.co.jp


アジャイル事業部のRubyアジャイル受託開発は、アジャイル開発を10年以上続けて培った、決して手法や方法論ではまとめきれない、実践知や価値観、それを届ける人で構成されています。Ruby とアジャイルソフトウェア開発を通じてコミュニティと共感していけるお客様とエンジニアを絶賛募集しています。

agile.esm.co.jp