diff --git a/CHANGELOG.md b/CHANGELOG.md index 15129f4..765a8ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,24 @@ the package ships without a bump fails the release gate. ## [Unreleased] +## [0.2.0] - 2026-09-03 + +### Changed + +- Takes `managoat_sandbox ~> 0.2.0`, where a command stream that closes + without an exit frame is `{:error, %{ref: ref}, :closed_before_exit}` + rather than a synthesised `{:exit, %{ref: ref}, 0}` + (managoat/managoat_sandbox#4). +- `Managoat.Runner.Connection` needed no change: a subscriber whose command + loses its exit already gets `{:error, %{ref: ref}, :runner_disconnected}`, + because the protocol has no way for a session to end without a code — the + daemon watches the process and reports what it exits with. The only route + to a missing exit is the connection going away. +- `Managoat.Runner.FakeDaemon`'s `drop` instruction says that instead of + faking it. It used to emit an exit frame with code 0; it now stops the + socket, so the subscriber gets the disconnect the real protocol would + give. A script that uses `drop` is the last thing that daemon does. + ## [0.1.0] - 2026-09-02 ### Added diff --git a/lib/managoat/runner/testing/fake_daemon.ex b/lib/managoat/runner/testing/fake_daemon.ex index c415efe..500d530 100644 --- a/lib/managoat/runner/testing/fake_daemon.ex +++ b/lib/managoat/runner/testing/fake_daemon.ex @@ -21,6 +21,16 @@ defmodule Managoat.Runner.FakeDaemon do run as real processes emitting real frames; sessions journal every frame and `attach` replays from byte zero. + `drop` is the one instruction that is not a session frame. The protocol + has no way for a session to end without an exit code — the daemon watches + the process and reports what it exits with — so the only way a subscriber + loses a command's exit is the connection going away. `drop` therefore + stops the socket, and the subscriber gets the + `{:error, %{ref: ref}, :runner_disconnected}` that `Connection`'s + teardown broadcasts, which is what `Managoat.Sandbox`'s closes-without-an- + exit-frame rule asks this adapter for. It ends the whole connection, so a + script that uses it is the last thing that daemon does. + `start/2` connects a daemon for `runner_id`; `stop/1` disconnects it (the socket process exits, so callers see the disconnected errors). It is the executable form of the protocol description in `Connection`'s moduledoc — @@ -135,6 +145,14 @@ defmodule Managoat.Runner.FakeDaemon do Enum.each(frames, &push(state, Map.put(&1, "replay_for", req_id))) daemon_loop(state) + :drop_connection -> + # `:normal` rather than `:shutdown`: the socket is linked to whoever + # started the daemon (a test process), and a non-normal exit would + # take it down. `Connection.terminate/2` runs either way, which is + # the point of the drop. + GenServer.stop(state.socket, :normal) + daemon_loop(state) + {:session_done, session_id, code} -> state = put_in(state, [:sessions, session_id, :exit], code) frame = %{"stream" => "exit", "session_id" => session_id, "code" => code} @@ -336,7 +354,7 @@ defmodule Managoat.Runner.FakeDaemon do {:stdout, d} -> emit(daemon, session_id, "stdout", d) {:stderr, d} -> emit(daemon, session_id, "stderr", d) {:exit, c} -> finish(daemon, session_id, c) - :drop -> finish(daemon, session_id, 0) + :drop -> drop(daemon) :stay -> stay(daemon, session_id) end) @@ -368,6 +386,12 @@ defmodule Managoat.Runner.FakeDaemon do send(daemon, {:session_done, session_id, code}) exit(:normal) end + + # No exit frame, ever: the connection goes instead. See the moduledoc. + defp drop(daemon) do + send(daemon, :drop_connection) + exit(:normal) + end end defmodule Managoat.Runner.FakeDaemon.Socket do diff --git a/mix.exs b/mix.exs index a91bf2a..a01fe29 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Managoat.Runner.MixProject do use Mix.Project - @version "0.1.0" + @version "0.2.0" @source_url "https://github.com/managoat/managoat_runner" def project do @@ -49,7 +49,7 @@ defmodule Managoat.Runner.MixProject do # (#1345): the umbrella resolves it the same way apps/fountain does, # and `mix hex.build` for this app succeeds inside the umbrella, which # is what lets it graduate next. - {:managoat_sandbox, "~> 0.1.0"}, + {:managoat_sandbox, "~> 0.2.0"}, # The WebSock behaviour only. The adapter that mounts a handler on a # Plug connection (websock_adapter) belongs to the host application. {:websock, "~> 0.5"}, diff --git a/mix.lock b/mix.lock index 23177ed..d65e3ba 100644 --- a/mix.lock +++ b/mix.lock @@ -15,14 +15,14 @@ "makeup": {:hex, :makeup, "1.2.2", "882d46dc0905e9ff7abf2aab61a7e6b3dcc555533977d8a23b06019e6c89ac94", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "9a1a24e5b343b8ae16abea0822c10a6f75da27af7fa802ada5251f7579bfccfa"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.1.0", "835f7e60792e08824cda445639555d7bf1bbbddb1b60b306e33cb6f6db24dc74", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "1cd6780fb1dd1a03979abaed0fe82712b0625118fd5257d3ebbf73f960c73c3c"}, - "managoat_sandbox": {:hex, :managoat_sandbox, "0.1.0", "233352bff88d336d732b71c4e33b80808dff51ef876ee281122790c03acbeae3", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:sprites, "0.2.0", [hex: :sprites, repo: "hexpm", optional: false]}], "hexpm", "df7fb117538ff9612436ce63cefa00847649f00bd71ccae1129f86029d74ebc0"}, + "managoat_sandbox": {:hex, :managoat_sandbox, "0.2.0", "344cec023949faa6f7ee2fa2f6ede6ba3a5ab526fbcc0138eb914ebdcda03fb0", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:sprites, "0.2.2", [hex: :sprites, repo: "hexpm", optional: false]}], "hexpm", "a601b2194c967407bd8136dc952d1c94f2c69423a0107d48ce9ae4e1919b41a3"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "mint": {:hex, :mint, "1.9.3", "3337184d69179695c7a9f1714d92c11e629d36c8c037a21cf490131d3d150554", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5f7c9342480c069dbbc4eeac3490303c9e01870ff01a7f1d29b6107054fc1e74"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "req": {:hex, :req, "0.7.4", "23e9ffec17de032a46a4b15ed65c09793893bf4a7c680f4bbf6227fce6bdf74d", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "4b192d63253e8dcc6221ef992ea9ebef7d3555166e8423aa5b553e86bc3c69a2"}, - "sprites": {:hex, :sprites, "0.2.0", "bc6aebb6ccd4229b0d930eaa45cf585666a81455361dfad8a3b8bdd575c1df2e", [:mix], [{:client_signals, "~> 0.4.4", [hex: :client_signals, repo: "hexpm", optional: false]}, {:gun, "~> 2.1", [hex: :gun, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "954b95bc8f4eeb37667c94e8400586e989df1a3e908f00a400e62c1bee063a12"}, + "sprites": {:hex, :sprites, "0.2.2", "6c8be38d76e0c3b453417fcd328fb8388514a79b54d8713ac24bc1d67913740b", [:mix], [{:client_signals, "~> 0.4.4", [hex: :client_signals, repo: "hexpm", optional: false]}, {:gun, "~> 2.1", [hex: :gun, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "aa5a2b258d260806a9abb288a97f97dad5311395584ce0836503c923bc4b97bb"}, "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, }