Skip to content

Ship batesd as a Mix release; fix bates start crash#32

Open
tylerhunt wants to merge 8 commits into
masterfrom
mix-release-for-daemon
Open

Ship batesd as a Mix release; fix bates start crash#32
tylerhunt wants to merge 8 commits into
masterfrom
mix-release-for-daemon

Conversation

@tylerhunt
Copy link
Copy Markdown
Member

Context

PR #31 added a bates start subcommand to the existing escript, but it
crashes immediately because escripts cannot carry erlexec's
priv/exec-port C binary. The escript packaging format is the wrong tool
for the daemon. This PR splits Bates into two binaries: bates stays as
an escript for the six fast-cold-start control commands, and the daemon
ships as a Mix release named batesd.

Summary

  • Removes the broken bates start subcommand and its module + tests.
  • Moves argv parsing (--config <path>) and prerequisite verification
    into Bates.Application.start/2 (via Bates.Daemon), so the
    supervision tree boots the same way under mix phx.server (dev/test)
    and bin/batesd (release).
  • Adds a releases: keyword to mix.exs declaring the batesd release
    with a custom :steps callback that installs a thin bin/batesd
    wrapper. The wrapper parses --config in shell, exports
    BATES_CONFIG_PATH, and execs the underlying mix-release launcher's
    start subcommand. The daemon picks up BATES_CONFIG_PATH via
    Bates.Daemon.apply_env/1. (See execution notes in the archived plan
    for why the env-var path was needed instead of plain argv forwarding.)
  • Adds config :bates, skip_prereq_check: true in config/test.exs so
    mix test doesn't trip the prereq gate on dev machines without
    caddy on $PATH.
  • Adds a minimal config/prod.exs so the prod release assembles.
  • Updates the not-running diagnostic to point at batesd, not
    bates start.
  • Updates specs/cli.md and specs/system-overview.md for the new
    two-binary topology.
  • Adds a "Build and Run" section to the README.

Test plan

The agent has run automated checks. The sections below need to be
verified by hand before merge — they require live Caddy / DNS resolver
and real services to bring up.

  • mix test from source/ passes (235 tests, 0 failures).
  • mix format --check-formatted passes.
  • MIX_ENV=prod mix release batesd succeeds and produces
    _build/prod/rel/batesd/bin/batesd.
  • _build/prod/rel/batesd/bin/batesd --help does NOT print the
    mix-release subcommand list (the wrapper took).
  • _build/prod/rel/batesd/bin/batesd --bogus exits 2 with our
    usage message.
  • BATES_CONFIG_PATH=/tmp/x.toml batesd-orig eval ... confirms the
    daemon reads the env var into :bates, :config_path.
  • _build/prod/rel/batesd/bin/batesd boots and serves the dashboard
    at https://bates.test.
  • bates status, bates up <app>, bates env <app> all work
    against the running daemon.
  • Ctrl-C in the daemon terminal cleans up Caddy + services.
  • bin/batesd --config /path/to/foo.toml honors the override.
  • Running a second batesd while one is already up exits with a
    port-bind error.

tylerhunt added 8 commits May 3, 2026 00:54
PR #31 added a `bates start` subcommand to the existing escript, but it
crashed immediately because escripts cannot carry `erlexec`'s
`priv/exec-port` C binary. The escript packaging format is the wrong
tool for the daemon.

This removes `Bates.CLI.Start`, the dispatcher clause that routed
`start` to it, the `bates start` line in the usage banner, and the
test file. The matching usage assertion in `Bates.CLITest` is updated.

A subsequent commit will reintroduce the daemon as a Mix release named
`batesd` so `erlexec`'s priv directory is available at runtime.
The control commands previously told users to run `bates start` when
the daemon was unreachable. With `bates start` removed and the daemon
shipping as a Mix release named `batesd`, the diagnostic now reads
"Bates is not running. Start it with: batesd."

Also rewords `Bates.Prerequisites` and `Bates.CLI.Setup` docstrings to
describe the gates and setup work in terms of `batesd`.
Move the argv parser, config-path application, and prereq verifier
that the doomed `Bates.CLI.Start` owned into `Bates.Application.start/2`
so the same supervision tree boots correctly under both
`mix phx.server` and `bin/batesd` (the upcoming Mix release).

The work lives in a new `Bates.Daemon` module as three pure helpers
(`parse_argv/1`, `apply_options/1`, `verify_prerequisites/0`) so each
piece is unit-testable without trapping `System.halt/1`. `start/2`
itself is a thin orchestrator that halts with code 2 on parse or
prereq failures, matching what `bates start` used to do.

Tests run `Application.start/2` because `:bates` is a started
application. To keep them from tripping the prereq gate (or being
rejected by the argv parser, which sees `["test"]`), `config/test.exs`
sets `config :bates, skip_prereq_check: true`, and `start/2` short-
circuits the entire boot prep when that flag is set. The default of
`false` keeps the gate active in dev and in the release.

`Bates.Daemon` does NOT call `Mix.env/0` or any other build-time
module: `Mix` is unavailable in releases.

New `Bates.DaemonTest` covers `--config` parsing (default + override),
unknown-switch and unexpected-positional rejection, application of
the parsed `--config` to `:bates, :config_path`, and the prereq
diagnostic when `caddy` is off `$PATH`.
Declare a `batesd` Mix release in `source/mix.exs`'s `releases:`
keyword. The release embeds ERTS, includes every dependency's `priv/`
directory, and starts `:bates` permanently — so `erlexec` finds its
`exec-port` C binary at runtime, which the escript packaging format
could not deliver.

`mix release` generates `bin/batesd` as a multi-subcommand dispatcher
(`start`, `daemon`, `remote`, `eval`, ...). A custom `:steps`
callback (`install_launcher/1`) renames that file to
`bin/batesd-orig` and writes a thin wrapper at `bin/batesd` that
always invokes the foreground `start` subcommand. Users see a single
command: `batesd [--config <path>]` — no subcommand maze.

The wrapper is written directly inside the `:steps` callback rather
than via `rel/overlays/`. Overlays are copied during `:assemble`,
which makes the rename-then-overlay sequence awkward; doing the
rename and the wrapper write in the same custom step keeps the
ordering obvious.

Add a minimal `config/prod.exs` so `MIX_ENV=prod mix release batesd`
can read environment-specific endpoint settings. The file mirrors
`dev.exs` for now (port and `secret_key_base`); follow-up work can
diverge as needed. `config/runtime.exs` is intentionally absent —
the release reads its only environment-dependent setting
(`--config <path>`) from argv inside `Bates.Application.start/2`.
`specs/cli.md` removes the `### bates start` section and the option
table that came with it, points the not-running message at `batesd`,
points the configuration override description at `batesd`, and
rewords the "How It Connects" `bates start` bullet to describe
`batesd`. A new `## Daemon` section documents `batesd`'s flags,
prereq behavior, and intended invocation (foreground until the
launchd v2 proposal lands). The intro paragraph now describes the
two binaries explicitly.

`specs/system-overview.md`'s `### CLI` section now spells out the
two-binary topology: `batesd` is the server (Mix release, embeds
ERTS so `erlexec` finds its priv binary), and `bates` is a thin
client escript for the control commands.
Add a "Build and Run" section between "Setup" and "Configuration"
covering both binaries: `mix escript.build` for the `bates` client
and `MIX_ENV=prod mix release batesd` for the daemon. Includes the
`--config` flag, the foreground/Ctrl-C semantics, and a couple of
sample `bates` invocations against a running daemon.
The mix-release `start` subcommand discards extra argv, and the
generated `elixir` launcher's CLI mode interprets argv as
`[script | args]` (so passing `--config` directly would make it
treat the flag as a script filename). Have the `bin/batesd`
wrapper parse `--config` in shell and export it as the
`BATES_CONFIG_PATH` environment variable, then add
`Bates.Daemon.apply_env/1` to plumb that into
`:bates, :config_path`.

`Bates.Application.start/2` now runs the env step after argv so the
env var wins when both are set (the wrapper sets it deliberately;
the argv path stays for `mix phx.server`).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant