remoteinstall

package
v1.46.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 1, 2026 License: MIT Imports: 17 Imported by: 0

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func DaemonStopCommand

func DaemonStopCommand(binaryPath string) string

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

func InstallCommand(t Target, src Source) string

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 Push

func Push(ctx context.Context, r Runner, t Target, src Source) error

Push streams the archive into the remote install script.

func ShellSingleQuote

func ShellSingleQuote(s string) string

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

func StopRemoteDaemon(ctx context.Context, r Runner, binaryPath string) (warning string, err error)

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

type Platform struct {
	GOOS   string
	GOARCH string
}

Platform is a remote host's Go build target.

func PlatformFor

func PlatformFor(unameS, unameM string) (Platform, error)

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.

func (Platform) String

func (p Platform) String() string

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

func ParseProbe(out string) (Probe, error)

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.

func RunProbe

func RunProbe(ctx context.Context, r Runner) (Probe, error)

RunProbe asks the remote host what it is and whether quil is already there.

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

func ClassifyExit(exitCode int, established bool) Remedy

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.

func (Remedy) String

func (r Remedy) String() string

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

func FetchRelease(ctx context.Context, version string, p Platform) (Source, error)

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.

func PackDir

func PackDir(dir string, p Platform) (Source, error)

PackDir builds a release-shaped archive from a directory holding quil and quild built for the remote's platform.

This is the --from-dir path, and the only one available to a dev build, which has no matching release to fetch.

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

func PlanTarget(p Probe) Target

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

func (t Target) BinaryPath() string

BinaryPath is the absolute path of the installed quil. This is what gets persisted per-destination and used verbatim as the ssh remote command.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL