Skip to content

Empty, newline, or JSON

go
 3 min A watched control directory where an empty file with no newline resets, an empty file with a newline means now, and anything else is JSON.
Published

Sep 10, 2022

The snap-gs lobby took all its runtime control by writing files into a watched directory: up, down, stop, restart, forcerestart, and so on. Each file’s body follows a three-way grammar: a truly empty body, a body consisting of one newline, or a JSON body of arbitrary length, which may itself end in a newline.

nl := len(bs) != 0 && bs[len(bs)-1] == '\n'
if nl {
    bs = bs[:len(bs)-1]
}
switch {
case !nl && len(bs) == 0:
    *out = *in          // empty, no newline  -> reset to baseline
case nl && len(bs) == 0:
    *out = time.Now().UTC()   // empty, newline -> "now"
default:
    _ = json.Unmarshal(bs, out)  // anything else -> parsed JSON
}

So three ordinary shell commands become three different verbs against the same file:

: > spec/restart                # empty, no newline: reset to baseline
echo > spec/restart             # empty, with newline: restart NOW
echo '"2022-01-01T00:00:00Z"' > spec/restart   # JSON: an explicit timestamp

: with a redirection truncates the file to zero bytes; echo > truncates it and writes one newline. touch is not the reset verb: it creates an empty file only when the path is absent and preserves an existing body. The parser reads the reliable one-byte distinction between “clear this” and “do this now.” A JSON body supplies the timestamp explicitly instead; the lobby honors it only when the stamp postdates its own start, so a stale request left over from an earlier run is ignored rather than replayed. Removing the file entirely restores defaults. The switch supplies the command grammar: the newline selects the operation, and a nonempty value goes through Go’s JSON decoder into the already typed destination.

These direct writes can expose intermediate state to a filesystem watcher: truncation and the completed body are not one atomic operation. The .lock convention coalesced notifications for small writes, but its timed batch was not a guarantee that publication had finished.

The same protocol does double duty. Point --flagdir at a directory and these files hot-reload the process’s own CLI flags at runtime, through a sibling grammar. Empty still resets. String flags take a newline-terminated body literally and a bare body as JSON; duration flags parse newline-terminated duration text and bare bodies as JSON; integer and Boolean flags parse either nonempty form as JSON after removing one trailing newline. One convention controls both lifecycle and configuration.

The reason this mattered operationally: it needs nothing but write access to a directory. A locked-down account with no API token and no privileges beyond ordinary redirection and echo in one path could drive the whole service. The interface has limits: there is no independent schema or version layer, and JSON parse errors are deliberately swallowed rather than surfaced. A partial write can therefore reset a timestamp during truncation, then fail to install the intended value without a diagnostic. It is a control plane for people who already have the keys, not a public API. Given that, spending an entire byte of protocol on a trailing newline is a good trade: the interface is any tool that can write the watched file.