🧪

"phx.server"コマンドの実装を追い掛ける

2021/12/25に公開

この記事は、「fukuoka.ex Elixir/Phoenix Advent Calendar 2021」の20日目の記事です。
遅刻してすみません…!
https://qiita.com/advent-calendar/2021/fukuokaex

phx.serverへの関心

ElixirのWebアプリケーションフレームワークの Phoenix Framework にて、サーバを起動するコマンドにphx.serverがあります。
Phoenixサーバを起動するためによく使う割にはどんな働きをしているのか私は知りませんでした。
そこで、今回はphx.serverの実装を追い掛けてみます。

対象バージョンと前提

今回の対象バージョンは下記の通りになります。

Elixir: 1.13
Phoenix: 1.6.5

前提は、次の2点とします。

  1. 今回の検証で用いるプロジェクトはコマンド$ mix phx.new firstphoenixを実行した直後とします。
  2. 他にPhoenixサーバが起動していないものとします。

実行コマンド

$ mix.phx.server

オプション等は設定していないものとします。

phx.serverの処理の流れ

処理の大まかな流れは大まかに下記のようになります。
細かい関数呼び出しやエラーハンドリング等の分岐は省いて正常にサーバが起動した場合の処理のみ追い掛けます。

  1. Mix.Tasks.Phx.Server.run/1
  2. Mix.Tasks.Run.run/1
  3. Mix.Tasks.Run.run/5
  4. Mix.Task.run/2
  5. Mix.Task.run_task/3
  6. Mix.TasksServer.run/1

次節以降、それぞれの処理を下記の3点で見ていきます。

  • 各関数が定義されているファイルのURL
  • $ mix phx.serverを実行した際に各関数に渡される実引数
  • 関数がやっていること

1. Mix.Tasks.Phx.Server.run/1

https://github.com/phoenixframework/phoenix/blob/v1.6.5/lib/mix/tasks/phx.server.ex#L34-L37

実引数

args = []

やっていること

  • 環境変数phoenix.serve_endpointsへの設定
  • コマンドライン引数を渡して Mix.Tasks.Run.run/1 を呼び出す

mix phx.serverを実行すると、この関数が呼ばれます。2行だけの関数です。
Application.put_env/4で環境変数を設定したら、Mix.Tasks.Run.run/1を呼び出します。

2. Mix.Tasks.Run.run/1

https://github.com/elixir-lang/elixir/blob/v1.13/lib/mix/lib/mix/tasks/run.ex#L64-L90

実引数

args = []

やっていること

  • コマンドライン引数からオプションと引数をパース
  • コマンドライン引数やオプションを渡して Mix.Tasks.Run.run/5 呼び出す

まず始めにOptionParser.parse_head!/2を呼び出します。parse_head!/2は、コマンド引数から有効なオプションをopts変数として取り出し、それ以外をhead変数に格納します。
コマンド引数のパースが終わったら、optsheadなどを渡してMix.Tasks.Run.run/5 を呼び出します。

3. Mix.Tasks.Run.run/5

https://github.com/elixir-lang/elixir/blob/v1.13/lib/mix/lib/mix/tasks/run.ex#L100-L153

実引数

args = [], 
opts = [], 
head = [], 
expr_evaluator = &Code.eval_string/1, 
file_evaluator = &Code.require_file/1

やっていること

  • オプションから値の取り出しや置換
  • コマンドライン引数の上書き
  • コンパイル設定の書き換え
  • Mix.Task.run/2を呼び出す(4番で説明後述)
  • コマンドライン引数として渡されたオプションに指定されたファイルの読み込みや処理の実行(今回はオプションは空なので処理無し)

Mix.Tasks.Run.run/5は、まずoptsに対して値の置き換えや特定の値の取り出しを行います。
続いて、System.argv/1でコマンドライン引数を上書き、Mix.Tasks.Run.process_config/1でコンパイル時の設定を書き換えます。
その後、Mix.Task.run/2を呼び出します。

4. Mix.Task.run/2

https://github.com/elixir-lang/elixir/blob/v1.12/lib/mix/lib/mix/task.ex#L353-L367

実引数

task = "app.start",
args = []

やっていること

  • taskをaliasとして呼び出すべきか、そのままタスクとして呼び出すかを判別
  • mix phx.new を実行した直後の状態なので Mix.Task.run_task/3を呼び出す

Mix.Task.run/2は2つ定義されていますが、第1引数に渡される値は文字列"app.start"ですのでis_binary/1のガード節がtrueになるdef run(task, args) when is_binary(task)の方が呼ばれます。

関数内では、mix.exsに記述したaliasの設定やサーバの起動状態によって呼ばれる処理が変わります。
mix phx.newを実行した直後は

  • Mix.Project.config()[:aliases][String.to_atom("app.start")]の結果がnil
  • 前提として他のPhoenixサーバが起動していないため!Mix.TasksServer.get({:task, task, proj})がtrue

上記2点の結果Mix.Task.run_task/3が呼ばれます。

5. Mix.Task.run_task/3

https://github.com/elixir-lang/elixir/blob/v1.12/lib/mix/lib/mix/task.ex#L369-L404

実引数

proj = Firstphoenix.MixProject,
task = "app.start",
args = []

やっていること

※ すみません、この関数については詳しく調べられておらず振る舞い、コメントや関数名からの推測が多いです。

  • タスクが有効かどうかをチェックし、タスクが定義されているモジュールを取得
  • タスクを再帰的に実行する必要があるかどうかを判定
  • タスクの再帰実行の有無やプロジェクトに積まれているタスクの有無をチェックし、Mix.TasksServer.run/1を呼び出す

引数のタスクが実行できるか、再帰実行すべきものか、プロジェクトに積まれているタスクがあるか、といったことを評価します。
実際の振る舞いを観測できていませんが、ここではMix.TasksServer.run/1を呼び出したものとして進めます。

6. Mix.TasksServer.run/1

https://github.com/elixir-lang/elixir/blob/v1.12/lib/mix/lib/mix/tasks_server.ex#L12-L18

実引数

tuple = {
  :task,
  "app.start",
  Firstphoenix.MixProject
 }

やっていること

  • Agent.get_and_update/3を呼び出す

Agent.get_and_update/3GenServer.call/3を呼び出すので、ここでサーバが起動されます。

Agent.get_and_update/3のソースコード
https://github.com/elixir-lang/elixir/blob/v1.12/lib/elixir/lib/agent.ex#L375-L377

おわりに

mix phx.serverの実装を追い掛けました。
本当は6. Mix.TasksServer.run/1の後でいくつかの処理を呼び出していますが、実行コマンドにオプションが設定されていないため何も処理を行われないので省略しました。

私の独自調査のため間違っている箇所もあるかと思いますが、何かの参考になれば幸いです。

Discussion