エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Phoenix LiveView で Markdwon Preview

Phoenix LiveView を使って、textarea に入力した Markdown のテキストを逐次プレビューするサンプルです。

f:id:E_Mattsan:20200626201515p:plain

Phoenix 1.5 になって簡単に LiveView を利用できるようになり、動的なページを作るのが本当に簡単になりました。

もちろん万能ではないですし弱点も少なくありません。 それでも従来の web ページを構築する要領で動的なページを構築できるのは大きな利点だと思います。

Earmark - a pure-Elixir Markdown converter

Markdown から HTML への変換にここでは Earmark を利用します。

Earmark は ex_doc がドキュメントを生成するときにも利用されているパッケージです。

as_html!/2で簡単に変換することができます。

iex(1)> Earmark.as_html!("""
...(1)> # 第一部            
...(1)> ## 第一章           
...(1)> ### 第一節          
...(1)> 
...(1)> | a | b |
...(1)> |---|---|
...(1)> | 1 | 2 |
...(1)> 
...(1)> - a
...(1)> - b
...(1)> """) |> IO.puts()
<h1>
  第一部
</h1>
<h2>
  第一章
</h2>
<h3>
  第一節
</h3>
<table>
  <thead>
    <tr>
      <th style="text-align: left;">
        a
      </th>
      <th style="text-align: left;">
        b
      </th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left;">
        1
      </td>
      <td style="text-align: left;">
        2
      </td>
    </tr>
  </tbody>
</table>
<ul>
  <li>
    a
  </li>
  <li>
    b
  </li>
</ul>

MdPreviw - Markdownプレビュー app

アプリケーションを作成していきます。

Phoenix プロジェクトを用意する

LIveView を利用するプロジェクトを新たに作成します。 ここでは作業を簡単にするためデータベースを利用せず --no-ecto を指定します。

$ mix phx.new md_preview --live --no-ecto
$ cd md_preview

パッケージを追加する

mix.exs を編集して、依存パッケージに Earmark を追加します。

  defp deps do
    [
      {:phoenix, "~> 1.5.3"},
      {:phoenix_live_view, "~> 0.13.0"},
      {:floki, ">= 0.0.0", only: :test},
      {:phoenix_html, "~> 2.11"},
      {:phoenix_live_reload, "~> 1.2", only: :dev},
      {:phoenix_live_dashboard, "~> 0.2.0"},
      {:telemetry_metrics, "~> 0.4"},
      {:telemetry_poller, "~> 0.4"},
      {:gettext, "~> 0.11"},
      {:jason, "~> 1.0"},
      {:plug_cowboy, "~> 2.0"},
      {:earmark, "~> 1.4"} # 追加
    ]
  end

パッケージを取得します。

$ mix deps.get

ルーティングを追加する

live/md_preview_web/router.ex を編集して LiveView のルーティングを追加します。

  scope "/", MdPreviewWeb do
    pipe_through :browser


    live "/", PageLive, :index
    live "/markdown", MarkdownLive # 追加
  end

LiveView を書く

LiveView を書いていきます。

…とは言っても LiveView のドキュメントにある実装例ぐらい簡単なコードです。

textarea の内容の更新に逐一反応しないように phx-debounce="1000" を指定しています。 更新が止まってから 1,000 ミリ秒 = 1 秒経過してからイベントをサーバに送ります。

またレンダリング済みの HTML をエスケープせずに挿入するために Phoenix.HTML.raw/1 を利用しています。

defmodule MdPreviewWeb.MarkdownLive do
  use MdPreviewWeb, :live_view

  @impl true
  def mount(_, _, socket) do
    {:ok, assign(socket, body: "")}
  end

  @impl true
  def render(assigns) do
    ~L"""
      <div>
        <form phx-change="update">
        <textarea name="markdown[source]" class="source" phx-debounce="1000"></textarea>
        </form>
      </div>
      <div class="preview">
        <%= raw @body %>
      </div>
    """
  end

  @impl true
  def handle_event("update", %{"markdown" => %{"source" => source}}, socket) do
    body = Earmark.as_html!(source)
    {:noreply, assign(socket, body: body)}
  end
end

assets/css/app.scss にスタイルを追加して見た目を少し整えました。

.source {
  height: 20vh;
  resize: none;
  font-family: monospace;
}

.preview {
  border: solid thin #e0e0e0;
  padding: 10px;
}

app を動かす

サーバを起動し http://localhost:4000/markdown にアクセスすると、記事の先頭の画像のように textarea に入力した Markdown のテキストがすぐに変換されて表示されます。

$ iex -S mix phx.server 

レンダリングを非同期にする

イベント送信の負荷を減らすために、また編集途中の半端な状態でレンダリングされないように、入力が止まってから 1 秒ごにテキストをサーバに送信するようにしています。 つまりテキストの入力とレンダリング結果の表示のタイミングは同期していません。

このような関係にあるばあい、ブラウザからのテキストの送信とサーバからの結果の送信は分離してしまっても問題ありません。

MdPreviewWeb.MarkdownLiveイベントハンドラを次のように編集します。

"update" を受けたらすぐに自分に対して :render メッセージを送り、LiveView のイベントハンドラから抜けます。

その後 :render メッセージを受けとたら Markdown から HTML に変換して表示内容を更新します。

  @impl true
  def handle_event("update", %{"markdown" => %{"source" => source}}, socket) do
    send(self(), {:render, source})

    {:noreply, socket}
  end

  @impl true
  def handle_info({:render, source}, socket) do
    {:noreply, assign(socket, body: Earmark.as_html!(source))}
  end

このテクニックは The Pragmatic Studio の Phoenix LiveView コースで解説されています。 2020年6月27日現在 $0 で提供されています。 興味のある方はぜひ受講してみてください。

online.pragmaticstudio.com

いつか読むはずっと読まない:Real-Time Phoenix

The Pragmatic Bookshelf の「Real-Time Phoenix」。今年の3月3日に正式に出版され「Hands-On with Phoenix LiveView」で LiveView に触れられています。

pragprog.com

Phoenix.PubSub を Phoenix 以外で利用するための素振り

檄を飛ばす

檄を方々に急いで出し,決起を促す。

スーパー大辞林」より

Phoenx.PubSub は Phoenix名前空間にありますが Phoenix のプロジェクト以外でも利用できます。

バージョンが 2 になってシンプルに扱いやすくなったということで素振りをしてみました。

やること

  • Phoenix.PubSub を使って publisher から subscribers にメッセージを送る
  • アプリケーション起動時に subscribers のプロセスを起動するようにしてみる

準備

プロジェクトを作る

$ mix new geki --sub
$ cd geki

依存するパッケージに Phoenix.PubSub を追加する

Phoenix.PubSub のバージョンを確認。

$ mix hex.info phoenix_pubsub
Distributed PubSub and Presence platform

Config: {:phoenix_pubsub, "~> 2.0"}
...

mix.exs に追加。

defmodule Geki.MixProject do
  use Mix.Project

  # ...

  defp deps do
    [
      {:phoenix_pubsub, "~> 2.0"}
    ]
  end
end

Phoenix.PubSub を子プロセスに追加する

lib/geki/application.ex を編集してアプリケーションの子プロセスに Phoenix.PubSub を追加します。 ここではプロセスに Geki.PubSub という名前をつけています。

defmodule Geki.Application do
  @moduledoc false

  use Application

  def start(_type, _args) do
    children = [
      {Phoenix.PubSub, name: Geki.PubSub}
    ]

    opts = [strategy: :one_for_one, name: Geki.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Subscriber モジュールを用意する

Subscriber を記述するファイル lib/geki/subscriber.ex を追加します。 ここではGenServer で実装します。

任意のメッセージを受信したら、それを単純にログに出力します。

defmodule Geki.Subscriber do
  use GenServer

  require Logger

  alias Phoenix.PubSub

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

  def init(opts) do
    name = opts[:name]
    pubsub = opts[:pubsub]
    topic = opts[:topic]

    PubSub.subscribe(pubsub, topic)

    {:ok, %{name: name}}
  end

  def handle_info(event, state) do
    Logger.info("Subscriber '#{state.name}' received #{inspect(event)}")

    {:noreply, state}
  end
end

プロセスを起動しメッセージを送る

iex 上でふるまいを確認します。

$ iex -S mix

Subscriber のプロセスを起動する

Geki.Subscriber のプロセスを起動します。 ここでは foo, bar, baz と名前をつけた 3 つのプロセスを起動しています。

iex> Geki.Subscriber.start_link(name: :foo, pubsub: Geki.PubSub, topic: "geki")
{:ok, #PID<0.204.0>}
iex> Geki.Subscriber.start_link(name: :bar, pubsub: Geki.PubSub, topic: "geki")
{:ok, #PID<0.206.0>}
iex> Geki.Subscriber.start_link(name: :baz, pubsub: Geki.PubSub, topic: "geki")
{:ok, #PID<0.208.0>}

メッセージを送る

Phoenix.PubSub.broadcast/4 を使ってメッセージをブロードキャストします。

iex> Phoenix.PubSub.broadcast(Geki.PubSub, "geki", "Hello")
:ok

12:12:23.140 [info]  Subscriber 'bar' received "Hello"

12:12:23.140 [info]  Subscriber 'foo' received "Hello"

12:12:23.140 [info]  Subscriber 'baz' received "Hello"

すこし使いやすく

Publisher モジュールを追加する

特定の topic 向けの Publisher を用意してみます。 状態を記憶するために GenServer で実装します。

Publisher を記述するファイル lib/geki/publisher.ex を追加します。

defmodule Geki.Publisher do
  use GenServer

  def start_link(opts) do
    name = opts[:name]

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

  def publish(name, event) do
    GenServer.cast(name, {:publish, event})
  end

  def init(opts) do
    pubsub = opts[:pubsub]
    topic = opts[:topic]

    {:ok, %{pubsub: pubsub, topic: topic}}
  end

  def handle_cast({:publish, event}, state) do
    Phoenix.PubSub.broadcast(state.pubsub, state.topic, event)

    {:noreply, state}
  end
end

Publisher と Subscriber を子プロセスに追加する

アプリケーションの起動時に Publisher と Subscriber のプロセスも起動するようにします。

lib/geki/application.ex を編集してアプリケーションの子プロセスに Publisher と Subscriber を追加します。

今回の Subscriber の実装では GenServer.start/3 でプロセスの名前を指定していないので、複数の Subscriber の起動を指定するとプロセスの識別ができないため起動のときにエラーになってしまいます。

そのため Supervisor.child_spec/2 でプロセスに ID をしています。

defmodule Geki.Application do
  @moduledoc false

  use Application

  def start(_type, _args) do
    children = [
      {Phoenix.PubSub, name: Geki.PubSub},
      {Geki.Publisher, name: :geki, pubsub: Geki.PubSub, topic: "geki"},
      Supervisor.child_spec({Geki.Subscriber, name: :foo, pubsub: Geki.PubSub, topic: "geki"}, id: :foo),
      Supervisor.child_spec({Geki.Subscriber, name: :bar, pubsub: Geki.PubSub, topic: "geki"}, id: :bar),
      Supervisor.child_spec({Geki.Subscriber, name: :baz, pubsub: Geki.PubSub, topic: "geki"}, id: :baz),
    ]

    opts = [strategy: :one_for_one, name: Geki.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

起動してブロードキャストする

$ iex -S mix
iex> Geki.Publisher.publish(:geki, "Hello")                
:ok

12:40:33.437 [info]  Subscriber 'foo' received "Hello"
 
12:40:33.437 [info]  Subscriber 'baz' received "Hello"

12:40:33.437 [info]  Subscriber 'bar' received "Hello"

いつか読むはずっと読まない:FUTURES

この三週間ばかり部屋の大掃除をしているのですが。 なぜかこの画集が二冊見つかりました。

FUTURE

FUTURE

  • 作者:鶴田謙二
  • 発売日: 2011/11/30
  • メディア: 大型本

file_system パッケージを使ってファイルの更新を監視する Phoenix app についての覚書

いまさらなのですが。

Phoenix app の開発時に、コードを更新したときに自動的に再読み込みをおこなうしくみを file_system というパッケージが担っているということを知りました。

ということなので。

file_system を使ってファイルの更新を監視し Phoenix LiveView を使ってリアルタイムにブラウザの表示に反映するというのをやります。 これはその覚書。


作成

アプリケーションを用意します。

Phoenix 1.5 以降であれば LiveView のためのオプション --live が追加されているので、それを指定します。 1.5 よりも前のバージョンを利用するばあいはドキュメントのインストール手順に従ってインストールしてください。

$ mix phx.new my_app --live

lib/my_app_web/live/page_live.ex

--live オプションで生成される LiveView のモジュールを次のように書き換えます。 手作業でインストールしたばあいは新たにファイルを作成します。

ここではモジュール FileSystem のプロセスにディレクト/tmp を監視させます。

また通知を受け取るためのハンドラ handle_info/2 を記述します。 受け取った通知は file_events という名前のリストに追加しています。

defmodule MyAppWeb.PageLive do
  use MyAppWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    if connected?(socket) do
      {:ok, watcher_pid} = FileSystem.start_link(dirs: ["/tmp"])
      FileSystem.subscribe(watcher_pid)
    end

    {:ok, assign(socket, :file_events, [])}
  end

  def handle_info({:file_event, watcher_pid, {path, events}}, socket) do
    {:noreply, update(socket, :file_events, &[{path, events} | &1])}
  end
end

lib/my_app_web/live/page_live.html.leex

LiveView のテンプレートを書き換え(あるいは作成)します。

<ul>
  <%= for {path, events} <- @file_events do %>
    <li>
      <span><%= path %></span>
      <span><%= inspect(events) %></span>
    </li>
  <% end %>
</ul>

lib/my_app_web/router.ex

LiveView を手作業でインストールしモジュールを新規に追加したばあいは、LiveView を利用するようにルーティングも変更します。

  scope "/", MyAppWeb do
    pipe_through :browser

    live "/", PageLive, :index
  end

実行

Phoenix app を起動します。

$ iex -S mix phx.server

FileSystem のプロセスが監視するディレクトリを操作します。

$ echo hi > /tmp/hi.txt
$ echo hello > /tmp/hello.txt

操作と同時にブラウザ上の表示が更新されることが確認できます。

f:id:E_Mattsan:20200503100015p:plain

Ruby の Enumerable モジュールの使い方の覚書

Enumerable モジュールを使って Fibonacci number を実装する例です。

# fibonacci.rb

class Fibonacci
  include Enumerable

  def each
    if block_given?
      n1, n2 = 1, 1
      loop do
        yield n1
        n1, n2 = n2, n1 + n2
      end
    else
      to_enum
    end
  end
end

先頭から 10 個の数を取得する。

$ ruby -r./fibonacci -e 'puts Fibonacci.new.take(10)'
1
1
2
3
5
8
13
21
34
55

先頭から 200 未満の数を取得する。

$ ruby -r./fibonacci -e 'puts Fibonacci.new.take_while {|f| f < 200 }'
1
1
2
3
5
8
13
21
34
55
89
144

各数の平方を先頭から 10 個取得する(だめな例)。

$ ruby -r './fibonacci' -e 'puts Fibonacci.new.map {|f| f * f }.take(10)'

#take が評価されるのは #mapすべての数 を処理してからなので、永遠にそのときはやってきません。 こんなときのための遅延評価です。

各数の平方を先頭から 10 個取得する。

$ ruby -r./fibonacci -e 'puts Fibonacci.new.lazy.map {|f| f * f }.take(10).to_a'
1
1
4
9
25
64
169
441
1156
3025

だいたいこんな感じ。

AWS SNS で SMS 送信したときの料金を調べる覚書

AWS を使って SMS を送信する機会があったので、送信にかかった利用金を CloudWatch Metrics で取得するコードを Ruby で書きました。その覚書。

require 'aws-sdk-cloudwatch'

params = {
  metric_data_queries: [
    {
      id: 'spentUSD',
      metric_stat: {
        metric: {
          namespace: 'AWS/SNS',
          metric_name: 'SMSMonthToDateSpentUSD'
        },
        period: 24 * 60 * 60, # seconds per day
        stat: 'Maximum'
      },
    }
  ],
  start_time: Time.new(2020, 1, 1),
  end_time: Time.new(2020, 4, 1)
}

client = Aws::CloudWatch::Client.new(region: 'ap-northeast-1')

metric_data_results = []

loop do
  response = client.get_metric_data(params)
  metric_data_results.concat(response.metric_data_results)

  break unless response.next_token

  params.next_token = response.next_token
end

pp metric_data_results.flatten

Railsでサーバサイドで動的にHTMLを更新したいための覚書

動機

  • Rails で動的に要素を更新するページを作りたい
  • JavaScript 勢としては、JavaScript のコードをできるだけ書かずにすませたい

今回の解決策のメリット

  • サーバサイドでレンダリングするので、View のテンプレートを利用できる

今回の解決策のデメリット

  • データの転送量が多い

よいですよね、 Phoenix LiveView 。 同じようなことを Rails でもやってみたいと思い立ち、試行錯誤してみました。

app を用意する

サンプルとして、サーバサイドで時刻を更新しブラウザ上で刻々と表示を更新する clock app を作ります。

Rails のバージョンとしては 6.0 、標準で WebPacker を利用していることを前提にしています。

$ rails new clock
$ cd clock

コントローラを追加する

ページを表示するためにコントローラを用意します。 更新は動的に行うのでコントローラのアクションは show のみ用意しています。

$ bin/rails g controlle clocks show

チャネルを追加する

更新する要素を輸送するチャネルを用意します。 クライアントからの update の要求を受け取れるようにイベントハンドラを用意しています。

$ bin/rails g channel clock update

JavaScript を編集する

本音としてはいろいろあっても、やはり JavaScript のコードをまったく書かずにすますわけにはいきませんでした。

と、いうわけで。 自動生成された JavaScript のコードを編集します。

app/javascript/packs/application.js

ページごとに .js ファイルの読み込みを制御したいので、チャネルの .js ファイルを一括で読み込んでいるコードをコメントアウトします。

require("@rails/ujs").start()
require("turbolinks").start()
// require("channels") コメントアウトする

app/javascript/channels/clock_channel.js

consumer.subscriptions.create の行の先頭に export default を追加してチャネルのオブジェクトを export します。

このファイルで定義されている関数を編集してカスタマイズするのが行儀のよい方法なのだと思います。 今回は、サーバから受信したデータをハンドリングする関数をこのオブジェクトの received にあとから代入するという方法をとります。

import consumer from "./consumer"

export default consumer.subscriptions.create("ClockChannel", {
  connected() {
    // Called when the subscription is ready for use on the server
  },

  disconnected() {
    // Called when the subscription has been terminated by the server
  },

  received(data) {
    // Called when there's incoming data on the websocket for this channel
  },

  update: function() {
    return this.perform('update');
  }
});

app/javascript/packs/clock.js

export されたチャネルのオブジェクトを import し、各イベントをハンドリングする .js ファイルを用意します。

import clockChannel from '../channels/clock_channel'

window.addEventListener('DOMContentLoaded', () => {
  // ブラウザで update がクリックされたら、チャネルの update を呼び出す
  const buttonUpdate = document.getElementById('update')
  buttonUpdate.addEventListener('click', () => { clockChannel.update() })

  // チャネルからデータを受け取ったら container に格納Gする
  const container = document.getElementById('container')
  clockChannel.received = (data) => { container.innerHTML = data.html }
})

view を編集する

app/views/clocks/show.html.erb

update のイベントを発するボタン、動的に生成された要素を格納するコンテナを配置します。 また先に用意したチャネルの .js ファイルを読み込むタグを追加します。

<button id="update">UPDATE</button>
<div id="container"></div>

<%= javascript_pack_tag 'clock' %>

channel を編集する

app/channels/clock_channel.rb

update メソッドの内容に tramsmit メソッドでデータを送信するコードを追加します。 ここでは現在時刻を文字列に変換し div で囲ったものを送っています。

class ClockChannel < ApplicationCable::Channel
  periodically :update, every: 1.second

  def subscribed
    # stream_from "some_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def update
    transmit(html: "<div>#{Time.current}</div>")
  end
end

表示を確認する

Rails サーバを起動して、追加したページをブラウザで開きます。

ページを開くとボタンが一つ表示されています。 それをクリックすると、現在時刻が表示されるはずです。

更新する要素をテンプレートで書く

更新する要素を update メソッドに直書きするのではサーバサイドで更新するメリットがありません。 view と同じようにテンプレートを使ってレンダリングするようにしてみます。

テンプレートを用意する

app/views/clocks/clock.html.erb

まず一般の view と同じようにテンプレートを用意します。

<div><%= current_time %></div>

app/views/layouts/plain.html.erb

テンプレートのレンダリングではレイアウトが利用されます。 標準で用意されているレイアウトは HTML のヘッダなどが含まれています。 ページ内の要素をレンダリングするにはそれらは不要ですので、何も装飾しないレイアウトを用意します。

<%= yield %>

channel を編集する

app/channels/clock_channel.rb

ApplicationController.render を使って用意したテンプレートでレンダリングします。

update メソッドを次のように変更します。

  def update
    html = ApplicationController.render('clocks/clock.html', layout: 'plain.html', locals: {current_time: Time.current.to_s})
    transmit(html: html)
  end

表示を確認する

再び Rails サーバを起動してページをブラウザで開きます。 編集前と変わらない表示ができています。

view と同じテンプレートを利用できるので SlimHaml を利用することもできます。

定期的に表示を更新する

Action Cable には Periodically というメソッドが用意されています。 これを利用するとクライアントから update のイベントを送ることなく、サーバが自ら定期的に更新を実行できるようになります。

app/channels/clock_channel.rb

ClockChannel クラスに periodically :update, every: 1.second の一行を追加します。

class ClockChannel < ApplicationCable::Channel
  periodically :update, every: 1.second

  # 以下略

表示を確認する

三度 Rails サーバを起動してページをブラウザで開きます。 今度は update ボタンを押さなくても秒ごとに表示が更新されることがわかります。

periodically を設定すると常に update メソッドが呼ばれていないかと気になりますが、チャネルは接続の状態( subscribed / unsubscribed )を把握しているので接続中のみメソッドを呼ぶようになっています。

いつか読むはずっと読まない:Life on Earth

博士は間違いなくわたしの科学好きに影響を与えたお一人です。

Elixir で escript からサーバにリクエストを送る覚書

事の起こり

NIF (Native Implemented Functions) を含むパッケージを escript で利用しようとしたのですが。

ビルドした実行ファイルを実行しても NIF のライブラリファイルを読み込めないとエラーになり。

検索してみたらこのとおり。


Unfortunately that's a limitation of escripts. They cannot access anything in priv and therefore they cannot access embedded .so files. In other words, it is not currently possible to build escripts with NIFs in them.

https://github.com/elixir-lang/elixir/issues/5444#issuecomment-260178836


You can not use NIFs in an escript.

https://elixirforum.com/t/escript-can-not-load-argon2-nif-wrong-approach-for-cli/25399/2


と、いうことなので。 NIF を含むサーバを起動しておいて escript の実行ファイルからプロセス間通信でリクエストを送ればよいじゃないか、と考えたしだい。

サーバを用意する

まずリクエストを受ける適当なサーバを用意します。

$ mix new my_server --sup

モジュールを用意する

適当なサーバモジュールとアプリケーションモジュールを用意します。

defmodule MyServer do
  use GenServer

  @name __MODULE__

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

  def double(value) do
    GenServer.call(@name, {:double, value})
  end

  def init(_opts) do
    {:ok, :no_state}
  end

  def handle_call({:double, value}, _from, state) do
    {:reply, value * 2, state}
  end
end
defmodule MyServer.Application do
  @moduledoc false

  use Application

  def start(_type, _args) do
    children = [
      MyServer
    ]

    opts = [strategy: :one_for_one, name: MyServer.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

release 設定を用意する

ノード間で通信を行うには、ノードに名前を設定する必要があります。

まず mix release.init コマンドを実行して設定ファイルを生成します。

$ mix release.init
* creating rel/vm.args.eex
* creating rel/env.sh.eex
* creating rel/env.bat.eex

rel/env.sh.eex (MS Windows のばあいは rel/env.bat.eex )を編集し、ファイルの末尾の 2 行のコメントアウトを外します。

この行の直前のコメントに書かれているように、これでノード名が設定されます。 デフォルトではアプリケーション名 + 127.0.0.1 という名前になるので、今回は my_server@127.0.0.1 という名前が設定されます。

export RELEASE_DISTRIBUTION=name
export RELEASE_NODE=<%= @release.name %>@127.0.0.1

cookie を設定する

安全のために cookie を設定します。

明示的に cookie を設定しない場合は自動的にランダムな値が設定されます。 その値を利用しても問題ないのですが、ここでは説明を兼ねて設定します。

mix.exs ファイルを開き、project/0 が返すキーワードリストに :release を追加します。

release には名前をつけて異なる設定で生成することができますが、ここではデフォルトのアプリケーション名に対して cookie の設定をします。

defmodule MyServer.MixProject do
  use Mix.Project

  def project do
    [
      app: :my_server,
      version: "0.1.0",
      elixir: "~> 1.10",
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      releases: [
        my_server: [
          cookie: "MY_SERVER_COOKIE"
        ]
      ]
    ]
  end

  # 以下略

end

release を作成する

mix release を実行します。

$ mix release

実行に必要なファイルが _build/dev/rel/my_server に生成されます。 今回は MIX_ENV を指定していないのででデフォルトの dev で生成されています。

サーバを起動するにはこのディレクトリに含まれる _build/dev/rel/my_server/bin/my_server を利用します。

動作を確認する

start または daemon コマンドでサーバを起動します。

$ _build/dev/rel/my_server/bin/my_server start

または

$ _build/dev/rel/my_server/bin/my_server daemon

サーバが起動したら remote コマンドでサーバのノードに接続します。 start で起動したばあいは別のコンソールを開いてコマンドを実行してください。

$ _build/dev/rel/my_server/bin/my_server remote
iex(my_server@127.0.0.1)1> MyServer.double(123)
246

動作が確認できたら stop コマンドで停止します。 start で起動したばあいは Ctrl+C でも停止できます。

$ _build/dev/rel/my_server/bin/my_server stop

escript を用意する

次にリクエストを送る適当な escript を用意します

$ mix new my_cli

モジュールを用意する

エントリポイントになる関数 main/1 を定義したモジュールを用意します。

defmodule MyCli do
  def main(["double", arg]) do
    value = String.to_integer(arg)

    result = :rpc.call(:"my_server@127.0.0.1", MyServer, :double, [value])

    IO.puts("#{value} x2 = #{result}")
  end
end

リモート呼び出しにここでは Erlang のモジュール rpc の関数 call/4 を利用します。

まずサーバを起動します。

サーバのディレクトリでサーバを起動します。

$ _build/dev/rel/my_server/bin/my_server daemon

escript のディレクトリで iex を起動します。 このとき cookie はサーバで設定した内容に合わせます。

$ iex --name my_cli@127.0.0.1 --cookie MY_SERVER_COOKIE -S mix

関数を呼び出します。

iex(my_cli@127.0.0.1)1> MyCli.main(["double", "123"])
123 x2 = 246
:ok

無事サーバの関数を呼び出せることが確認できました。

動的にノード名と cookie を設定する

上の例では iex のオプションとしてノード名と cookie を指定していますが、escript として単独の実行ファイルとして作成したいので別の方法で設定する必要があります。

ノード名の設定は Node.start/3 を、cookie の設定は Node.set_cookie/2 を使うことで実現できます。

先の main/1 を編集して動的に設定するようにしてみます。

defmodule MyCli do
  def main(["double", arg]) do
    value = String.to_integer(arg)
    {:ok, _pid} = Node.start(:"my_cli@127.0.0.1")
    Node.set_cookie(:MY_SERVER_COOKIE)

    result = :rpc.call(:"my_server@127.0.0.1", MyServer, :double, [value])

    IO.puts("#{value} x2 = #{result}")
  end
end

start/3 に渡すノード名と set_cookie/2 に渡す cookie の値はどちらも atom である必要があります。

動作を確認します。 今回はノード名と cookie を指定せずに iex を起動します。

$ iex -S mix

main/1 を呼び出します。

iex(1)> MyCli.main(["double", "123"])
123 x2 = 246
:ok
iex(my_cli@127.0.0.1)2> 

今回も無事サーバの関数を呼び出せることが確認できました。 また main/1 が終了してもノード名が設定されたままになっていることがプロンプトからわかります。

これを終了するには関数 Node.stop/0 を呼び出します。

escript を生成する

mix.exs を編集して escript の設定を追加します。

defmodule MyCli.MixProject do
  use Mix.Project

  def project do
    [
      app: :my_cli,
      version: "0.1.0",
      elixir: "~> 1.10",
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      escript: [main_module: MyCli]
    ]
  end

  # 以下略

end

escript をビルドします。

$ mix escript.build

実行します。 サーバを起動しておくことを忘れないでください。

$ ./my_cli double 123
123 x2 = 246

これでコマンドラインからサーバの機能を利用することができるようになりました。

いつか読むはずっと読まない:sense of wonder

原著が 1981 年、邦訳は 1982 年。 数年前にあった復刊フェアで手に入れました。

数学パズルものや論理パズルものの一つのつもりで手にしましたが、むしろ SF 仕立ての部分が刺さります。 古典的な SF が好きな人はどうぞ。

SFパズル

SFパズル