From 9a972c9cc4a8652b31697817d12d884e0dfa60dd Mon Sep 17 00:00:00 2001 From: Donaldo Date: Tue, 10 Mar 2026 14:24:17 +0100 Subject: [PATCH 1/3] docs: add fork constitution Documents fork identity, architectural principles (raw PTY passthrough, no emulation, no deps), C style conventions, upstream policy, build instructions, and test process. Co-Authored-By: Claude Sonnet 4.6 --- constitution.md | 145 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 constitution.md diff --git a/constitution.md b/constitution.md new file mode 100644 index 0000000..29f97af --- /dev/null +++ b/constitution.md @@ -0,0 +1,145 @@ +# atch — Fork Constitution + +## 1. Identity + +This repository is a macOS-compatible fork of [mobydeck/atch](https://github.com/mobydeck/atch) +(GPL licence). The upstream project targets Linux exclusively and links with +`-static`; this fork lifts that constraint and adds macOS-specific headers so +the binary builds and runs natively on Darwin. + +The fork exists for two reasons: + +1. **macOS build support** — upstream does not handle `util.h` (Darwin) vs + `pty.h` / `libutil.h` (Linux) and links with `-static` which is unsupported + on macOS. +2. **UX evolutions** — session management improvements that may or may not be + suitable for upstream (see § Upstream policy). + +Fork: +Upstream: + +--- + +## 2. Architectural Principles + +### Raw PTY passthrough — no terminal emulation + +atch multiplexes a PTY session over a Unix socket. The master process owns the +PTY and forwards raw bytes to every attached client; clients write raw bytes back +to the master. There is **no terminal emulation layer**, no VT100/ANSI parser, +no screen buffer reconstruction. Sequences reach the real terminal of each +attaching client unchanged. + +### Minimalism + +- Pure C, no external runtime dependencies beyond the system C library and + `openpty(3)` / `forkpty(3)` (provided by `-lutil` on Linux, `util.h` on + Darwin). +- No autoconf, no cmake, no pkg-config. A single `makefile` drives the build. +- No third-party libraries. If a feature requires a dependency, reconsider the + feature. + +### Source layout + +| File | Role | +|------|------| +| `atch.c` | Main entry point, command dispatch, shared utilities | +| `atch.h` | Shared declarations, includes, protocol constants | +| `config.h` | Compile-time feature flags and tunables | +| `master.c` | PTY master process (session owner) | +| `attach.c` | Attaching client process | + +--- + +## 3. C Style + +Observe and match the conventions already present in the codebase: + +- **Indentation**: tabs (1 tab = 1 level). +- **Brace placement**: opening brace on the same line for control structures; + on a new line for function definitions. +- **Comment style**: `/* single-line */` and the `**`-prefixed block form for + multi-line explanations (`/* \n** text\n*/`). +- **Function length**: keep functions short and focused; extract helpers rather + than nesting logic. +- **Naming**: `snake_case` for functions and variables; `UPPER_CASE` for + macros and `enum` constants. +- **Error handling**: check every syscall return value; use `errno` for + diagnostics; prefer early-return on error over deep nesting. +- **String safety**: `snprintf` instead of `sprintf`; explicit size arguments + on all buffer operations. +- **Compiler warnings**: code must compile cleanly under `-W -Wall`. + +--- + +## 4. Upstream Policy + +| Change type | Action | +|-------------|--------| +| Generic bug fix (Linux + macOS) | Open a PR upstream; cherry-pick the fix here once merged or if upstream is slow to respond | +| macOS-specific fix (e.g. `util.h`, no `-static`) | Keep in this fork; do not send upstream | +| UX feature (session history, log rotation, kill `--force`, …) | Open a PR upstream if the change is general-purpose; keep here otherwise | +| Breaking protocol change | Discuss upstream before implementing | + +The guiding principle: upstream is the source of truth for the protocol and the +core PTY loop. This fork adds a compatibility shim and UX polish; it does not +diverge architecturally. + +--- + +## 5. Build + +### Prerequisites + +- macOS: Xcode Command Line Tools (`xcode-select --install`). +- Linux: `gcc`, `make`, `libutil` (or `libbsd`). + +### Local build + +```sh +make clean && make +``` + +The `makefile` detects the platform via `uname -s` and omits `-static` on +Darwin automatically. + +### Docker / cross-compile (Linux release binary) + +```sh +make build-docker # build Linux binary via Docker +make release # build amd64 + arm64 tarballs in ./release/ +``` + +### Relevant makefile variables + +| Variable | Default | Purpose | +|----------|---------|---------| +| `VERSION` | `dev` | Embedded in the binary via `-DPACKAGE_VERSION` | +| `arch` | host arch | Target architecture for Docker build | +| `BUILDDIR` | `.` | Output directory for the binary | + +--- + +## 6. Tests + +The test suite is a POSIX shell script (`tests/test.sh`) that emits TAP output. +It requires the compiled `atch` binary as its only argument and runs in an +isolated `$HOME` under `/tmp`. + +### Run on Linux (or via Docker) + +```sh +make test # builds Docker image + runs tests inside the container +``` + +### Run directly (if atch is already compiled locally) + +```sh +sh tests/test.sh ./atch +``` + +The tests cover: session create/attach/detach/kill, `push`, `list`, `current`, +`clear`, the `-q` quiet flag, log-cap (`-C`), kill `--force`, and `start`. + +There are currently no unit tests for individual C functions; all tests are +integration tests at the CLI level. From dfa576e1d1f9189be5b8e62cc5e0338176e6f7d8 Mon Sep 17 00:00:00 2001 From: Donaldo Date: Tue, 10 Mar 2026 14:29:43 +0100 Subject: [PATCH 2/3] docs(man): add atch.1 man page (section 1) Documents all commands (attach, new, start, run, push, kill, clear, list, current), all options (-e, -E, -r, -R, -z, -q, -t, -C, -f), FILES, ENVIRONMENT (ATCH_SESSION), EXIT STATUS, and EXAMPLES sections. Includes tests/test_man.sh with 33 TAP assertions verified via mandoc. Also removes the *.1 gitignore rule so that the committed man page is tracked directly rather than generated by pandoc. Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 1 - atch.1 | 272 ++++++++++++++++++++++++++++++++++++++++++++++ tests/test_man.sh | 102 +++++++++++++++++ 3 files changed, 374 insertions(+), 1 deletion(-) create mode 100644 atch.1 create mode 100644 tests/test_man.sh diff --git a/.gitignore b/.gitignore index 420cca7..159dca2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ !.gitignore *.o *~ -*.1 *.1.md /atch build/ diff --git a/atch.1 b/atch.1 new file mode 100644 index 0000000..686c7fa --- /dev/null +++ b/atch.1 @@ -0,0 +1,272 @@ +.TH ATCH 1 "2024" "atch" "User Commands" +.SH NAME +atch \- terminal session manager with persistent history and multi-client attach +.SH SYNOPSIS +.B atch +[\fIoptions\fR] [\fIsession\fR [\fIcommand...\fR]] +.br +.B atch +\fIcommand\fR [\fIoptions\fR] ... +.SH DESCRIPTION +.B atch +is a lightweight terminal session manager for macOS (and Linux). +It creates a pseudo-terminal (PTY) managed by a background master process, +allowing multiple clients to attach and detach at will. +Session output is persisted to a disk log so that late-joining clients can +replay what they missed. +.PP +The simplest invocation is: +.PP +.RS +.B atch \fIsession\fR +.RE +.PP +This attaches to an existing session named \fIsession\fR, or creates it (running +your shell) if it does not yet exist. +.PP +A session name without a slash is resolved to a socket under +\fI~/.cache/atch/\fR. A name that contains a slash is used as a full socket +path, which may reside anywhere the current user can write to. +.SH COMMANDS +.TP +.BR "atch " [\fIsession\fR " [" \fIcommand...\fR ]] +Attach-or-create. If \fIsession\fR exists, attach to it. If not, create it +(running \fIcommand\fR, or the user's shell when no command is given) and +attach. Requires a TTY. +.TP +.B atch attach \fIsession\fR +.br +Aliases: \fBa\fR +.br +Strict attach. Fail with exit code 1 if \fIsession\fR does not exist. +Requires a TTY. +.TP +.B atch new \fIsession\fR [\fIcommand...\fR] +.br +Alias: \fBn\fR +.br +Create a new session running \fIcommand\fR (or the user's shell if omitted) and +immediately attach to it. Requires a TTY. +.TP +.B atch start \fIsession\fR [\fIcommand...\fR] +.br +Alias: \fBs\fR +.br +Create a new session in the background (detached). Exits immediately after the +master process is launched. Does not require a TTY. +.TP +.B atch run \fIsession\fR [\fIcommand...\fR] +Create a new session with the master process staying in the foreground +(no fork). Useful for supervisor-managed processes or debugging. +.TP +.B atch push \fIsession\fR +.br +Alias: \fBp\fR +.br +Read from standard input and send the bytes verbatim to \fIsession\fR. +Useful for scripted input injection. Does not require a TTY. +.TP +.B atch kill [\fB\-f\fR|\fB\-\-force\fR] \fIsession\fR +.br +Alias: \fBk\fR +.br +Stop \fIsession\fR by sending SIGTERM to the child process group, waiting for a +short grace period, then sending SIGKILL if the process has not exited. With +\fB\-f\fR or \fB\-\-force\fR, SIGKILL is sent immediately without a grace +period. +.TP +.B atch clear [\fIsession\fR] +Truncate the on-disk log of \fIsession\fR to zero bytes. If \fIsession\fR is +omitted and the environment variable \fBATCH_SESSION\fR is set (i.e. the +command is run from within an atch session), the innermost session in the +ancestry chain is cleared. +.TP +.B atch list +.br +Aliases: \fBl\fR, \fBls\fR +.br +List all sessions in the default session directory. Each entry shows the +session name, age, and whether the socket is alive or stale (\fB[stale]\fR). +.TP +.B atch current +Print the name of the current session (read from \fBATCH_SESSION\fR). When +nested, the full ancestry chain is printed, separated by \fB>\fR. Exits with +code 1 when not inside an atch session. +.SH OPTIONS +The following options may appear before or after the subcommand (except where +noted). Options that take an argument (\fB\-e\fR, \fB\-r\fR, \fB\-R\fR, +\fB\-C\fR) consume the next argument. +.TP +.BI \-e " char" +Set the detach character. \fIchar\fR may be a literal character or a caret +notation such as \fB^A\fR, \fB^B\fR, etc. Use \fB^?\fR for DEL. The default +detach character is \fB^\\\fR (Ctrl-Backslash). +.TP +.B \-E +Disable the detach character entirely. Typing the detach sequence will be +forwarded to the session instead of causing a detach. +.TP +.BI \-r " method" +Set the redraw method used when reattaching. \fImethod\fR is one of: +.RS +.TP +.B none +No automatic redraw. +.TP +.B ctrl_l +Send a Ctrl-L character (form-feed) to the session. +.TP +.B winch +Send a SIGWINCH signal to the session (the default when a TTY is present). +.RE +.TP +.BI \-R " method" +Set the clear method used when reattaching. \fImethod\fR is one of: +.RS +.TP +.B none +No automatic clear (default). +.TP +.B move +Clear by moving the cursor. +.RE +.TP +.B \-z +Disable the suspend key (Ctrl-Z). When set, the suspend character is +forwarded to the session rather than suspending the client. +.TP +.B \-q +Quiet mode. Suppress informational messages such as "session created" or +"session stopped". Error messages are not suppressed. +.TP +.B \-t +Disable VT100/ANSI terminal assumptions. Use this when the local terminal is +not an ANSI-compatible terminal. +.TP +.BI \-C " size" +Set the maximum on-disk log size. Older bytes are trimmed when the log grows +beyond this limit. \fIsize\fR may be a plain integer (bytes), or a number +followed by \fBk\fR/\fBK\fR (kibibytes) or \fBm\fR/\fBM\fR (mebibytes). +Use \fB0\fR to disable logging entirely. The default is \fB1m\fR (1 MiB). +.TP +.B \-f ", " \-\-force +Only valid with the \fBkill\fR subcommand. Skip the SIGTERM grace period and +send SIGKILL immediately. +.SH FILES +.TP +.I ~/.cache/atch/ +Unix domain socket for the named session. +.TP +.I ~/.cache/atch/.log +Persistent output log for the named session. The log is trimmed to the cap +set by \fB\-C\fR (default 1 MiB) every time the limit is reached. When a +session ends, an end marker is appended before the file is closed. +.PP +When \fI$HOME\fR is unset or is the root directory, sockets are stored under +\fI/tmp/.atch-/\fR instead. +.SH ENVIRONMENT +.TP +.B ATCH_SESSION +Set by \fBatch\fR in the environment of every child process it spawns. +Contains the colon-separated ancestry chain of socket paths (outermost first), +ending with the socket of the innermost (current) session. For a +non-nested session, the value is a single socket path with no colon. +.PP +The environment variable name is derived from the basename of the \fBatch\fR +binary at startup: non-alphanumeric characters are replaced with underscores +and the result is uppercased, then \fB_SESSION\fR is appended. For example, +a binary named \fBssh2incus-atch\fR uses \fBSSH2INCUS_ATCH_SESSION\fR. +.TP +.B HOME +Used to locate the default session directory. See \fBFILES\fR above. +.TP +.B SHELL +Used as the default command when no \fIcommand\fR is given to \fBnew\fR, +\fBstart\fR, or the implicit attach-or-create form. Falls back to the passwd +database and then to \fI/bin/sh\fR. +.SH EXIT STATUS +.TP +.B 0 +Success. +.TP +.B 1 +An error occurred (session not found, no TTY available, invalid arguments, etc.) +or the invoked command exited with a non-zero status. +.SH EXAMPLES +Create a new session named \fBwork\fR and attach to it: +.PP +.RS +.B atch work +.RE +.PP +Or equivalently: +.PP +.RS +.B atch new work +.RE +.PP +Detach from the current session by typing the detach sequence \fBCtrl-\\\fR. +.PP +List all sessions: +.PP +.RS +.B atch list +.RE +.PP +Reattach to a running session: +.PP +.RS +.B atch attach work +.RE +.PP +Start a long-running process in the background, without a terminal: +.PP +.RS +.B atch start build make -j8 +.RE +.PP +Inject a command into a running session: +.PP +.RS +.B printf 'echo hello\en' | atch push work +.RE +.PP +Stop a session gracefully: +.PP +.RS +.B atch kill work +.RE +.PP +Stop a session immediately (no grace period): +.PP +.RS +.B atch kill -f work +.RE +.PP +Truncate the session log: +.PP +.RS +.B atch clear work +.RE +.PP +Show the current session name from inside a session: +.PP +.RS +.B atch current +.RE +.PP +Start a session with a custom detach character and 512 KiB log cap: +.PP +.RS +.B atch start -e '^A' -C 512k myapp ./myapp +.RE +.SH SEE ALSO +.BR dtach (1), +.BR tmux (1), +.BR screen (1), +.BR nohup (1) +.SH BUGS +Report bugs at \fIhttps://github.com/mobydeck/atch\fR. +.SH AUTHORS +Originally written by the mobydeck team. +macOS fork maintained at \fIhttps://github.com/mobydeck/atch\fR. diff --git a/tests/test_man.sh b/tests/test_man.sh new file mode 100644 index 0000000..6501629 --- /dev/null +++ b/tests/test_man.sh @@ -0,0 +1,102 @@ +#!/bin/sh +# Man page tests for atch. +# Usage: sh tests/test_man.sh [path-to-atch.1] +# Verifies structure, mandatory sections, and content of the man page. + +MAN_PAGE="${1:-./atch.1}" + +PASS=0 +FAIL=0 +T=0 + +ok() { + T=$((T + 1)); PASS=$((PASS + 1)) + printf "ok %d - %s\n" "$T" "$1" +} + +fail() { + T=$((T + 1)); FAIL=$((FAIL + 1)) + printf "not ok %d - %s\n" "$T" "$1" + [ -n "$2" ] && printf " # expected : %s\n # got : %s\n" "$2" "$3" +} + +assert_contains() { + case "$3" in *"$2"*) ok "$1" ;; *) fail "$1" "(contains '$2')" "$3" ;; esac +} + +assert_exit() { + if [ "$2" = "$3" ]; then ok "$1"; else fail "$1" "exit $2" "exit $3"; fi +} + +printf "TAP version 13\n" + +# ── 1. file exists ──────────────────────────────────────────────────────────── + +if [ -f "$MAN_PAGE" ]; then + ok "man page file exists" +else + fail "man page file exists" "file" "not found at $MAN_PAGE" + printf "\n1..%d\n" "$T" + printf "# %d passed, %d failed\n" "$PASS" "$FAIL" + exit 1 +fi + +CONTENT=$(cat "$MAN_PAGE") + +# ── 2. mandatory roff sections ──────────────────────────────────────────────── + +assert_contains "section NAME present" ".SH NAME" "$CONTENT" +assert_contains "section SYNOPSIS present" ".SH SYNOPSIS" "$CONTENT" +assert_contains "section DESCRIPTION present" ".SH DESCRIPTION" "$CONTENT" +assert_contains "section COMMANDS present" ".SH COMMANDS" "$CONTENT" +assert_contains "section OPTIONS present" ".SH OPTIONS" "$CONTENT" +assert_contains "section FILES present" ".SH FILES" "$CONTENT" +assert_contains "section ENVIRONMENT present" ".SH ENVIRONMENT" "$CONTENT" +assert_contains "section EXIT STATUS present" ".SH EXIT STATUS" "$CONTENT" +assert_contains "section EXAMPLES present" ".SH EXAMPLES" "$CONTENT" +assert_contains "section SEE ALSO present" ".SH SEE ALSO" "$CONTENT" +assert_contains "section AUTHORS present" ".SH AUTHORS" "$CONTENT" + +# ── 3. TH macro (title header) ─────────────────────────────────────────────── + +assert_contains "TH macro section 1" ".TH ATCH 1" "$CONTENT" + +# ── 4. commands documented ─────────────────────────────────────────────────── + +assert_contains "command 'attach' documented" "attach" "$CONTENT" +assert_contains "command 'new' documented" "new" "$CONTENT" +assert_contains "command 'start' documented" "start" "$CONTENT" +assert_contains "command 'run' documented" "run" "$CONTENT" +assert_contains "command 'push' documented" "push" "$CONTENT" +assert_contains "command 'kill' documented" "kill" "$CONTENT" +assert_contains "command 'clear' documented" "clear" "$CONTENT" +assert_contains "command 'list' documented" "list" "$CONTENT" +assert_contains "command 'current' documented" "current" "$CONTENT" + +# ── 5. options documented ───────────────────────────────────────────────────── + +assert_contains "option -e documented" "\\-e" "$CONTENT" +assert_contains "option -E documented" "\\-E" "$CONTENT" +assert_contains "option -r documented" "\\-r" "$CONTENT" +assert_contains "option -R documented" "\\-R" "$CONTENT" +assert_contains "option -z documented" "\\-z" "$CONTENT" +assert_contains "option -q documented" "\\-q" "$CONTENT" +assert_contains "option -t documented" "\\-t" "$CONTENT" +assert_contains "option -C documented" "\\-C" "$CONTENT" +assert_contains "option -f for kill documented" "\\-f" "$CONTENT" + +# ── 6. environment variable documented ─────────────────────────────────────── + +assert_contains "ATCH_SESSION documented" "ATCH_SESSION" "$CONTENT" + +# ── 7. man renders without error ───────────────────────────────────────────── + +mandoc "$MAN_PAGE" > /dev/null 2>&1 +assert_exit "man renders without error (mandoc)" 0 "$?" + +# ── summary ────────────────────────────────────────────────────────────────── + +printf "\n1..%d\n" "$T" +printf "# %d passed, %d failed\n" "$PASS" "$FAIL" + +[ "$FAIL" -eq 0 ] From 555b3c95bea70ffb67dc40d99235bfa79764acb1 Mon Sep 17 00:00:00 2001 From: Donaldo Date: Tue, 10 Mar 2026 14:29:47 +0100 Subject: [PATCH 3/3] build(makefile): add install target and PREFIX variable Adds PREFIX ?= /usr/local and an install target that copies: - the atch binary to $(PREFIX)/bin/atch - the man page to $(PREFIX)/share/man/man1/atch.1 Co-Authored-By: Claude Sonnet 4.6 --- makefile | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/makefile b/makefile index 949074c..d6ce6ef 100644 --- a/makefile +++ b/makefile @@ -4,6 +4,9 @@ CFLAGS = -g -O2 -W -Wall -I. -DPACKAGE_VERSION=\"$(VERSION)\" LDFLAGS = LIBS = -lutil +PREFIX ?= /usr/local + + OBJ = attach.o master.o atch.o SRC = attach.c master.c atch.c @@ -24,10 +27,16 @@ atch.1: atch.1.md man: atch.1 +install: atch + install -d $(PREFIX)/bin + install -m 755 atch $(PREFIX)/bin/atch + install -d $(PREFIX)/share/man/man1 + install -m 644 atch.1 $(PREFIX)/share/man/man1/atch.1 + clean: rm -f atch $(OBJ) *.1.md *.c~ -.PHONY: fmt +.PHONY: install fmt fmt: docker run --rm -v "$$PWD":/src -w /src alpine:latest sh -c "apk add --no-cache indent && indent -linux $(SRCS) && indent -linux $(SRCS)"