Documentation
¶
Overview ¶
Package enginehost is the process that keeps a conversation alive between connections.
`codeaf chat --host devbox` runs `ssh devbox codeaf engine` and speaks internal/remote's protocol over the pipes. In version 1 that process WAS the conversation: it opened the session, answered frames, and died with the pipe, so a closed laptop lid and a dropped wifi both ended a running turn. This package is the other half of version 2's answer — a host that outlives the pipe, holding the engine, while `codeaf engine` becomes a splice between the ssh pipes and a unix socket.
── THE GRAIN IS ONE HOST PER WORKSPACE, AND THE CHDIR LAW DECIDES IT ────────
A host holds one engine per open session, but every session it holds is about the SAME directory, and that is not an arbitrary carving. `codeaf engine` moves the process into the workspace before it assembles anything (cmd/codeaf engine.go), and that chdir is the only chdir in the tree — the process's own idea of where it is has to agree with the session config's. A host holding two workspaces would have to break that or lie about it. So the socket lives under a directory named for the workspace, a host is born in it, and a hello asking for a different one reaches a different host through a different socket. Nothing about the protocol changes; the routing happens before the first frame.
── NOTHING HERE IS REQUIRED ─────────────────────────────────────────────────
THE FALLBACK IS NOT OPTIONAL. A machine where the socket directory cannot be made, where the path is too long for a unix socket, where the lock cannot be taken, or where the host simply refuses to start must still take a remote session — as the version-1 engine did, on the pipe, with the welcome saying remote.Welcome.Persistent is false. Every door in this package is written to be allowed to fail: the caller reads the error as "not today" and serves the connection itself.
Index ¶
- Constants
- Variables
- func Ask(workspace string, ask remote.WhoIs) (remote.HostSelf, error)
- func Attach(workspace string, spawn func() error) (net.Conn, error)
- func BuildMoment() time.Time
- func Dial(workspace string) (net.Conn, error)
- func DialStartingHost(workspace string) (net.Conn, error)
- func Dir(workspace string) (string, error)
- func Held() ([]string, error)
- func LockPath(workspace string) (string, error)
- func Retire(workspace string, anyway bool) error
- func Run(workspace string, opts Options) error
- func SocketPath(workspace string) (string, error)
- func SocketPathFits(path string) bool
- func Spawn(workspace, name string, args ...string) error
- func Splice(in io.Reader, out io.Writer, conn net.Conn) error
- func Stop(workspace string) (bool, error)
- func ThisBinary() string
- type Holder
- type Host
- type Options
Constants ¶
const SocketLimit = 103
SocketLimit is the most bytes a unix socket path may weigh.
It is macOS's rather than Linux's because THE SMALLEST LIMIT IS THE ONE THAT TRAVELS: the same codeaf home can be shared over a network mount, and a host that worked on one machine and refused on another for a reason nobody could see would be worse than one honest refusal everywhere. Exceeding it is not a fault — CODEAF_HOME can be anywhere — so it is answered as "no host today" and the caller falls back to the pipe.
IT IS 103, NOT 104. macOS's sun_path is 104 bytes and the NUL that ends the name takes one of them. At 104 this answered "fits" for a path bind() then refused with "invalid argument": a state root in $TMPDIR whose socket path came to exactly 104 bytes started no host, took no fallback, and codeaf did not open at all.
Variables ¶
var ErrHostBusy = errors.New("engine host: this host is holding work in flight")
ErrHostBusy is a host that will not retire because it is holding work: a surface attached, a turn running, or a question waiting for an answer. It is not a fault and the caller must not treat it as one — the machine is doing exactly what somebody asked it to.
var ErrHostRunning = errors.New("engine host: another host already holds this workspace")
ErrHostRunning is Run finding that this workspace already has a host. It is not a failure and the caller must not print it: the machine is in exactly the state that was asked for, by another process.
var ErrNoHostAnswered = errors.New("engine host: no host answered")
ErrNoHostAnswered is a host that had somewhere to listen but did not answer before the birth wait ran out. It stays distinct from ErrSocketPathTooLong so the caller can tell a failed start from one that was never possible.
var ErrNothingHolding = errors.New("engine host: nothing is holding this workspace")
ErrNothingHolding is a workspace nobody is holding: no socket, or a socket nobody is listening on. It is the ordinary state of every folder, and a caller says "none" rather than reporting a failure.
var ErrSocketPathTooLong = errors.New("engine host: the state path is too long for a socket")
ErrSocketPathTooLong is a state root deeper than a unix socket may be named in, and it is the one failure on this road that is settled BEFORE anything is started: no lock, no process, no wait. The caller can therefore name this refusal separately from a host that had somewhere to listen but did not come up.
Functions ¶
func Ask ¶
Ask puts internal/remote's version exchange to this workspace's host on a connection of its own, and fails when no host answers.
THE CONNECTION IS SPENT ON THE QUESTION AND NEVER BECOMES A SURFACE. A hello would open a conversation — the expensive, journal-locking act this whole exchange exists to keep from happening against the wrong build — so the question travels alone and the caller dials again for the real thing.
func Attach ¶
Attach is the door `codeaf engine` knocks on: a connection to this workspace's host, starting one if nothing answers.
THE SPAWN RACE IS GUARDED BY THE LOCK THE HOST ITSELF HOLDS, which is the discipline this tree already uses for anything one machine may only do once (internal/filelock, and the standing tick's own lock). Whoever takes the lock spawns; whoever finds it busy knows a host is alive or being born and simply waits for the socket. Two spawns that race are not a fault either — the second host to start finds the lock held and exits without a word — so the worst case here is one wasted process, never two hosts on one workspace.
Every failure answers the same way: no connection and a reason, which the caller reads as "serve this one on the pipe".
func BuildMoment ¶
BuildMoment is [hostBinary.builtAt] for the process asking: the same rule on both sides of the comparison, so a host and the window deciding whether to replace it are measured with one ruler.
func Dial ¶
Dial connects to a host that is already running, and fails when none is. It never starts one — see Attach for that.
func DialStartingHost ¶ added in v0.4.0
DialStartingHost dials a workspace's host, retrying a refused or briefly missing connection ONLY while the host lock is held. A host takes that lock before it removes the old socket and listens, so a failure with the lock held is the small remove-to-listen window and is worth a retry; a failure with the lock free, or no lock file at all, means no host is coming up and it answers at once. So the ordinary state after a host crashed and left a stale socket costs a headless launch nothing rather than the whole retry budget on every start.
The gate is the lock FILE, read with a non-blocking flock released at once, so it is the same answer across pid namespaces. On Windows errors.Is(err, syscall.ECONNREFUSED) never matches, so the retry never engages there and the first Dial result stands.
func Dir ¶
Dir is where the host for one workspace keeps its socket: a directory under ~/.codeaf/v3/hosts, resolved through internal/home so CODEAF_HOME moves it with everything else.
THE DIRECTORY IS NAMED BY A HASH AND NOT BY THE PATH, which is the one place this parts from the session layout's own encoding (chatv3_layout.go turns separators into dashes and keeps the name readable). A socket path has a hard ceiling of about a hundred bytes, and a workspace six directories deep would spend all of it — so the name is short by construction and the readable answer is written INSIDE the directory instead ([placeName]).
func Held ¶
Retire asks this workspace's host to go, and does not return until it has.
IT IS THE HOST THAT SAYS WHETHER IT MAY. A host holding a turn, a surface or an unanswered question answers ErrHostBusy and stays where it is; anyway asks for it regardless, which is one person's own `codeaf engine --stop` and nothing else. Either way the ending is the host's own shutdown — every conversation closed, every journal flushed — and never a signal from outside. Held is every workspace this machine has a host directory for, in the plain text each host wrote there ([placeName]).
IT READS THE DIRECTORIES AND NOT THE PROCESS TABLE. A host is known by the state it left under ~/.codeaf/v3/hosts, so this answers for hosts started by any build and by any terminal, including one whose process has gone and left its socket behind. Whether anybody is actually listening is the caller's next question, asked through Dial or Stop — and a directory whose host is gone answers "nothing was holding it", which is the truth a sweep wants.
A directory with no workspace file is SKIPPED rather than guessed at: the name is a hash and there is no way back from it to a path, so the honest answer for one is nothing at all. Nothing here is an error a caller should stop for — a state root that cannot be read is a machine with no hosts.
func LockPath ¶ added in v0.4.0
LockPath is the file a host flocks for its lifetime, beside its socket. It is exported so a test can hold the lock a host would, to model the window in which a host has taken its lock and is replacing a stale socket.
func Run ¶
Run becomes this workspace's host and does not return until it is done: the idle policy retires it, a signal asks it to stop, or the listener breaks.
It answers ErrHostRunning when another host already holds this workspace, which the caller treats as success — somebody else did the job.
func SocketPath ¶
SocketPath is the socket one workspace's host listens on, refused up front when the path is longer than a unix socket may be. It makes nothing — see [where] — so a caller that is going to listen on it takes Dir as well.
func SocketPathFits ¶
SocketPathFits exposes the shared Unix-socket ceiling to other doors that place a socket under the codeaf state root. Keeping the number here prevents ssh control sockets and engine-host sockets from drifting across platforms.
func Spawn ¶
Spawn starts a host process and lets go of it.
IT IS DETACHED ON PURPOSE AND THAT IS THE POINT OF THE WHOLE LANE. The process asking for it is `codeaf engine` under sshd, and when the connection drops sshd takes down everything in that session's process group — which is exactly the death this package exists to survive. So the child gets a session of its own ([detach]) and none of the parent's input or output: its stdout is the one thing that must never carry a stray byte, because a host's stdout is nothing at all and its parent's is the protocol.
STDERR IS THE ONE PIPE THAT LEAVES LAST WORDS BEHIND. A host that dies at birth is the one process that knew the reason, and sending that reason to /dev/null made the failure unknowable; it appends to the same host log that a running Host writes, with /dev/null as the best-effort fallback when that log cannot be opened.
func Splice ¶
Splice is `codeaf engine` once it has a host: everything the surface says goes to the socket, everything the host says goes back, and nothing in between is read.
THE PROXY UNDERSTANDS NO FRAMES, and that is deliberate. The handshake, the resume cursors, the held questions and the stream sequence numbers are all between the surface and the engine; a splice that parsed them would be a third opinion about a protocol with two ends. It copies bytes.
EITHER SIDE ENDING ENDS BOTH. Stdin closing is the ssh connection going away, and the host must see that as the torn pipe it is — so the socket is closed rather than left half-open, and the host reads it as a surface that will be back (internal/remote's server.leave states what it does with that).
func Stop ¶
Stop is `codeaf engine --stop`: whatever is holding this workspace on this machine, stopped, whichever build it is.
IT HAS TO WORK ON A BUILD THAT PREDATES THE EXCHANGE, because that is the only build a person ever needs it for — the stale half in the middle, still answering a socket after the binary under it was replaced. So a host that cannot be asked anything is ended the one way the operating system offers: the kernel names the process on the other end of the socket ([peerPID]) and it is sent the signal every build of the host has always answered by flushing its journals and exiting.
It answers false when nothing was holding this workspace, which is not a failure and is the ordinary case.
func ThisBinary ¶
func ThisBinary() string
ThisBinary is the file this process was started from, "" when the platform cannot say.
Types ¶
type Holder ¶
type Holder struct {
// Self is the host's own account of itself, with the pid and the binary
// filled from the kernel when the host is too old to say them.
Self remote.HostSelf
// Answered is whether the host answered the question at all. False is a
// build older than the exchange: it is known only by the process on the
// other end of its socket.
Answered bool
}
Holder is what is holding one workspace, as far as it could be learned.
func Inspect ¶
Inspect asks the host holding this workspace what it is, and asks nothing else of it: the question carries no stand-down.
THE KERNEL IS ASKED FIRST, for Stop's reason: the answer to the question may be that the process cannot answer questions, and a pid is still worth having for a person deciding what to do about it.
type Host ¶
type Host struct {
// contains filtered or unexported fields
}
Host is one workspace's conversations, and the socket they are reached through.
type Options ¶
type Options struct {
// Boot opens one conversation for a hello — cmd/codeaf's bootEngine, the
// same closure the pipe engine hands to [remote.Serve].
//
// It is called with the host's own lock held, so ONE CONVERSATION IS OPENED
// AT A TIME. That is not for speed, it is for the session file: two
// connections asking for the same conversation at the same moment must not
// both open it, because the second would find the journal locked and mint a
// second session under a person who asked for one.
Boot func(remote.Hello) (*remote.Engine, error)
// Key says which conversation a hello wants, so that two surfaces asking
// for the same one are handed the same one.
//
// IT ANSWERS A TRANSCRIPT PATH, INCLUDING FOR A HELLO THAT NAMED NOTHING.
// "The workspace's latest" is a question about this machine's disk, and the
// door that starts the host is the half of this pair that can read it
// (cmd/codeaf's [engineHelloKey]) — resolving it here, at the door, is what
// lets the lookups below find a conversation this host is ALREADY holding
// rather than booting a second agent onto its journal.
//
// A nil Key falls back to the hello's own session, which is what a test with
// no door behind it means, and the empty string it answers for a hello that
// named nothing is then the boot's problem rather than an identity.
Key func(remote.Hello) string
}
Options is what a host needs from the door that starts it, which is the same two things every version of this has needed: how to open a conversation, and which conversation a hello is asking for.