Documentation
¶
Overview ¶
Package remoteinstall provisions the quil binaries on a remote host over an existing ssh connection: probe what the host is, fetch the matching release locally, verify it, and stream it into a small POSIX install script.
The laptop pushes rather than the server pulling, so a host with no route to GitHub can still be provisioned, and the installed version matches the TUI by construction rather than by pinning a variable.
Index ¶
- func DaemonStopCommand(binaryPath string) string
- func InstallCommand(t Target, src Source) string
- func Push(ctx context.Context, r Runner, t Target, src Source) error
- func ShellSingleQuote(s string) string
- func StopRemoteDaemon(ctx context.Context, r Runner, binaryPath string) (warning string, err error)
- type Platform
- type Probe
- type Remedy
- type Runner
- type Source
- type Target
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func DaemonStopCommand ¶
DaemonStopCommand asks an existing remote quil to stop its daemon before its binary is replaced.
Addressed by absolute path for the same reason the attach command is: a non-interactive shell usually cannot see ~/.local/bin.
func InstallCommand ¶
InstallCommand assembles the remote command for an install.
The install script CANNOT travel on stdin — the archive is there — so it is passed as an argument to `sh -c` instead. ssh joins its command arguments with spaces and applies no quoting of its own, so the whole string is assembled and escaped here rather than left to ssh.
func ShellSingleQuote ¶
ShellSingleQuote wraps s so a POSIX shell reads it as exactly one literal word.
Required because ssh joins its command arguments with spaces and applies no quoting of its own: whatever reaches the far side is re-split by the remote shell. Single quotes suppress every form of expansion — variables, command substitution, globbing, backslashes — so the only character needing care is ' itself, which is closed, escaped and reopened.
Every value interpolated into a remote command string goes through here. That includes values that look trustworthy: the install directory is derived from the remote's own $HOME, which can contain a space or an apostrophe.
func StopRemoteDaemon ¶
StopRemoteDaemon stops the daemon owned by an existing remote install, so the replacement binary is what serves the next attach.
It returns a WARNING string rather than an error, because the exit code alone cannot answer the question. `quil daemon stop` exits 1 both when the stop genuinely failed and when no daemon was running — and the second is the common case and precisely the state we want. Propagating non-zero would abort every upgrade of an idle host; swallowing it hides a daemon that refused to die, which then keeps serving the OLD binary (renaming over a running executable leaves the running process on its original inode), so the next attach reports a version mismatch the user has already "fixed".
So: classify on the marker our own CLI prints, and treat everything else as worth surfacing. A remote running an OLDER quil may word it differently, which costs a spurious warning — never a silent failure.
Types ¶
type Platform ¶
Platform is a remote host's Go build target.
func PlatformFor ¶
PlatformFor maps `uname -s` and `uname -m` onto a Go build target, accepting ONLY the combinations quil publishes a release archive for.
Anything else is an error rather than a nearest guess, because a wrong guess does not fail cleanly: a binary for the wrong architecture makes the remote shell report 127 or 126, and 127 is the same code as "not installed" — so a guess turns a clear refusal into an install that appears to succeed and then silently repeats.
32-bit ARM is the live case rather than a hypothetical one. A Raspberry Pi running a 64-bit kernel with a 32-bit userland reports aarch64 from uname -m while its dynamic loader is armhf, so even a "correct" reading of uname can disagree with what will actually execute.
type Probe ¶
type Probe struct {
// Home is the remote $HOME, used to site the ~/.local/bin fallback.
Home string
// Platform is the remote's Go build target, already validated against the
// set quil publishes releases for.
Platform Platform
// ExistingPath is the absolute path of an existing quil, or "" when the
// host has none.
ExistingPath string
// ExistingDirWritable reports whether ExistingPath's directory can be
// written by the connecting user AND is not writable by group or other —
// the difference between upgrading in place and falling back to
// ~/.local/bin.
ExistingDirWritable bool
}
Probe is what the remote host reported about itself.
func ParseProbe ¶
ParseProbe reads remote-probe.sh's output.
Parsing anchors on the sentinel rather than starting at line 0, because a remote shell prints its own output first often enough to matter: ~/.zshenv runs for every zsh invocation, ~/.profile may echo, an rc file touching stty emits a warning. Without the anchor each of those shifts every field by one and produces a confident, wrong diagnosis.
type Remedy ¶
type Remedy int
Remedy is what a failed remote launch calls for.
const ( // RemedyNone means the failure is not about a missing or broken install. RemedyNone Remedy = iota // RemedyInstall means the remote shell could not find quil. RemedyInstall // RemedyReinstall means the remote shell found quil and could not execute // it — almost always a binary built for another architecture. RemedyReinstall // RemedyUpgrade means quil ran over there and reported a version this TUI // cannot attach to. Distinct from RemedyInstall because the two produce // opposite headlines, and saying "Quil is not installed" about a daemon // that just answered with its version is a visible contradiction — the // probe on the very next line reports the install it supposedly lacks. RemedyUpgrade )
func ClassifyExit ¶
ClassifyExit maps an ssh exit status to a remedy.
established is the override, not a refinement: a link that delivered even one byte ran quil successfully over there, so however it exited afterwards is a remote crash rather than a missing binary. Without that check a daemon crashing with status 127 would be misdiagnosed as "not installed" and the user offered an install that fixes nothing.
type Runner ¶
type Runner interface {
Run(ctx context.Context, command string, stdin io.Reader, stdout, stderr io.Writer) (int, error)
}
Runner executes one command on the remote host and reports its exit status.
An interface rather than a direct dependency on internal/transport so the orchestration is testable without ssh, a network, or a second machine — every step below is exercised against a fake.
type Source ¶
type Source struct {
// Version is what will be installed, without a leading "v". Empty for a
// --from-dir source, whose contents carry no version metadata.
Version string
// Archive is the tar.gz itself.
Archive []byte
// SHA256 is the archive's hex digest, computed here and re-checked on the
// far side so a truncated transfer cannot install.
SHA256 string
}
Source is a verified set of binaries ready to push, packed in the tar.gz shape the remote install script expects.
func FetchRelease ¶
FetchRelease downloads a release for the remote's platform and repacks it.
version selects the release ("" means latest); it should normally be this TUI's own version, since installing a different one on the far side just inverts the mismatch it was called to fix.
The download, checksum verification and archive extraction are delegated to update.Stager rather than reimplemented: it already takes the target platform as a parameter, verifies before extracting, and its extraction is hardened against hostile archive entry names. Repacking its output costs a temp dir and buys no second copy of that logic.
type Target ¶
type Target struct {
// Dir is the directory to install into.
Dir string
// Shadowed names an existing install this one will take precedence over,
// or "" when there is none. Non-empty only when an existing install could
// not be replaced in place, which is worth telling the user: a bare
// `ssh host quil` would still find the old one.
Shadowed string
}
Target is where the binaries will be written on the remote host.
func PlanTarget ¶
PlanTarget picks the install directory.
A fresh install goes to ~/.local/bin: it needs no sudo, and the absolute path is persisted afterwards so the non-interactive PATH never has to contain it.
An upgrade replaces the existing binary in place when its directory is writable, which keeps a hand-installed /usr/local/bin copy authoritative instead of silently leaving two — the stale one still being what a bare `ssh host quil` finds. When it is not writable we fall back rather than escalate, because a sudo password prompt cannot be answered over a non-tty ssh channel, and report the shadowing so the split is visible.
func (Target) BinaryPath ¶
BinaryPath is the absolute path of the installed quil. This is what gets persisted per-destination and used verbatim as the ssh remote command.