coaxlear

coaxlear

A Crystal application that runs inside a docker container: it opens a unix socket, starts a child process, and proxies that process's stdio both to the socket and to the container's stdout.

Status: the plan is complete (phases 0–7). Transport, supervisor, two-way proxying, exit code inheritance, signal forwarding, Docker image and specs are implemented and verified. The normative contract lives in docs/INTERFACE.md. Russian versions of the documents live in docs/ru/ and are not normative.

Русская версия

What it does

  • runs a process inside the container;
  • talks to the outside world through a unix socket;
  • collects the subprocess's stdout and passes it to the container's stdout.

Coaxlear holds no business logic: it is transport and supervision.

Quick start

Build the image and start a container with the command you want supervised:

docker build -t coaxlear .

mkdir -p ./run/coaxlear
docker run -d --name app \
  -v ./run/coaxlear:/run/coaxlear \
  --restart on-failure:3 \
  coaxlear -- sh -c 'while true; do sleep 1; done'

The host connects to ./run/coaxlear/coaxlear.sock, exchanges bytes in both directions, and once the process dies reads the exit code from exit_code in that same directory.

A minimal client, in ruby:

require "socket"

socket = UNIXSocket.new("./run/coaxlear/coaxlear.sock")
socket.close_write            # let the child see EOF on stdin
STDOUT.print(socket.read)     # read the child's output to EOF
# EOF means the child is done; the exit code is now on disk
puts "exit code: #{File.read("./run/coaxlear/exit_code").strip}"

A fuller, working client is script/host_client.cr, and complete clients in all three languages are in docs/CLIENTS.md — it also shows the two rules that matter for aliases below.

Two things worth knowing before you hit them:

  • The -- separator is mandatory. docker run image COMMAND exits with code 2 and prints usage; write docker run image -- COMMAND.
  • The uid. The container runs as user 10001, and a bind mount does not remap uid. On Linux give the host directory to that uid (chown 10001) or start the container with --user $(id -u). On macOS Docker Desktop there is no problem: the uid maps to the host user.

Mount layout

Give every instance its own directory. The exit code file is derived from the socket's directory, so two instances sharing one directory overwrite each other's exit code even when their socket names differ.

For several instances the host prepares names before starting anything, using symlinks:

run/coaxlear/
  c1/coaxlear.sock      <- created by coaxlear inside container 1
  c1/exit_code          <- container 1's exit code
  c2/coaxlear.sock      <- container 2
  c2/exit_code
  links/db1 -> ../c1/coaxlear.sock     <- alias, created by the host
  links/api -> ../c2/coaxlear.sock
ln -s ../c1/coaxlear.sock ./run/links/db1
ln -s ../c2/coaxlear.sock ./run/links/api

docker run -d -v ./run/c1:/run/coaxlear coaxlear --socket /run/coaxlear/coaxlear.sock -- CMD1
docker run -d -v ./run/c2:/run/coaxlear coaxlear --socket /run/coaxlear/coaxlear.sock -- CMD2

A symlink points at a path, not at an object, so it can be created before the container exists and keeps working after the socket is recreated on the next start. Coaxlear knows nothing about aliases.

Two client-side rules follow from this, both learned the hard way:

  • Resolve an alias when you connect, not after EOF. By the end of the stream coaxlear has already removed the socket file, and resolving a dangling symlink raises.
  • The exit code sits next to the real socket. For links/db1 it is c1/exit_code, not links/exit_code.

Full rules are in docs/INTERFACE.md §7.

Restart policy

Coaxlear does not restart the child process. The container lives exactly as long as the useful work, so when the child dies the container exits with the child's code — and restarting is the host's decision:

docker run -d --restart on-failure:3 ...

Use the attempt limit. A process that fails immediately will otherwise be restarted forever, and the loop is hard to notice from the host.

A container restart is not a process restart in place: everything the process held is gone. Restarting in place was not chosen for v1.

Troubleshooting

Coaxlear's diagnostics go to the container's stderr, so docker logs app shows them. They are in Russian, exactly as the source prints them:

coaxlear: слушаю /run/coaxlear/coaxlear.sock        listening on the socket
coaxlear: запущен sh -c while... (pid 7)            child started
coaxlear: клиент подключился                         a host connected
coaxlear: ввод закрыт, ребёнок получил EOF в stdin   input closed, child saw EOF
coaxlear: stdin ребёнка недоступен (...), ввод прекращён   child's stdin is gone
coaxlear: адресат вывода недоступен (...), отключён  output destination dropped
coaxlear: дочерний процесс завершился, код=7         child finished, with its code

The last two are normal events, not errors: the first means the child stopped reading its stdin (usually it exited), the second that a host disconnected and was dropped from the fan-out.

Exit codes:

Code Meaning
0, 2, 42, … the child's own code
128+n the child was killed by signal n (143 = SIGTERM)
1 startup failed: socket directory missing, address busy, non-socket on the path
2 usage error: no command after --, unknown flag

Other levers: --socket PATH overrides the socket path, and COAXLEAR_SOCKET does the same from the environment (the flag wins).

To attach by hand and see what the container sees:

nc -U ./run/coaxlear/coaxlear.sock

macOS: a unix socket does not survive a bind mount on Docker Desktop. The file shows up on the host but connecting fails with ENOENT or ECONNREFUSED, while a sibling container on the same volume connects fine. Verify the channel with two containers there; on Linux — the target platform — it works as designed. Analysis in docs/DESIGN.md, "Unix socket and bind mount".

Building

The image:

docker build -t coaxlear .

The binary alone, built in docker and extracted for a fast edit-test loop:

docker build --target builder -t coaxlear:builder .
docker create --name tmp coaxlear:builder          # create without starting
docker cp tmp:/src/bin/coaxlear build/              # extract the binary
docker rm tmp

The coaxlear:deps image is the runtime environment (libraries, user, socket directory) without a binary, which is what lets the test container take the binary from a volume:

docker run -d -v ./build:/build:ro -v ./run:/run/coaxlear \
  coaxlear:deps /build/coaxlear -- COMMAND

Tests

crystal spec                              # 45 examples, ~1.3 s
crystal spec spec/config_spec.cr:5        # one example by file:line
crystal spec -e "подставляет путь"        # by full name
crystal tool format --check src spec      # formatting check
shards build                              # build the binary into bin/

End-to-end, against real containers:

zsh script/e2e_docker.sh    # 21 checks: container, socket, listener, exit code
zsh script/e2e_named.sh     # 18 checks: two instances behind named aliases

e2e_docker.sh builds the binary in docker, puts it in build/, starts a container running ls -lah /, and reads the stream to EOF plus the exit code. e2e_named.sh runs two instances through the db1 and api aliases and checks that streams do not cross and that an alias survives a restart.

On macOS both scripts move the listener into a sibling container on the same volume; on Linux they run it natively, which is the "host connected" case. Signalling into the container is deliberately not covered.

Requires crystal >= 1.20.3 (verified on 1.20.3, Shards 0.20.0).

Documentation

File About
docs/INTERFACE.md The normative contract. Paths, data directions, EOF semantics, exit codes, client requirements. Written for the host client
docs/CLIENTS.md Example clients in ruby, crystal and elixir, each covering every §5 requirement, plus the snags of each language
docs/DESIGN.md Decisions with rationale, the phase plan, dissected Crystal 1.20.3 behaviour
docs/HISTORY.md Chronological journal: what was decided, what was built, why
docs/ru/ Russian translations, not normative
AGENTS.md Repository specifics for agents and AI tooling
docs/AGENTS.ru.md Russian translation of the above, not normative

License

MIT — see LICENSE.

Repository

coaxlear

Owner
Statistic
  • 0
  • 0
  • 0
  • 0
  • 0
  • about 5 hours ago
  • October 8, 2026
License

MIT License

Links
Synced at

Thu, 08 Oct 2026 23:18:47 GMT

Languages