Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 25 additions & 1 deletion lib/managoat/runner/testing/fake_daemon.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 —
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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"},
Expand Down
4 changes: 2 additions & 2 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
}