Implement porthole v0.1: profiles, supervised open/close, reconnect
Full CLI per spec v0.2 - add/open/close/edit/status/list/remove/wipe/ completions, plus a hidden `__supervise` subcommand that IS the supervisor process. - profile.rs: TOML-backed profiles at ~/.config/porthole/profiles/, validated -l/-r/-d mapping + --via grammar, atomic writes. - instance.rs: JSON runtime state at ~/.local/state/porthole/, an flock-based lock file that's the source of truth for "is this open" (survives a crash/kill -9 without stale-lock cleanup), pid liveness checked against /proc rather than trusted from disk. - supervisor.rs: the __supervise loop - spawns ssh, traps SIGTERM/SIGINT into a flag (rather than inferring intent from ssh's exit status), classifies failures as fatal/known-transient/unrecognized, backs off with a stability-reset, rotates its log. - ssh.rs: builds the ssh invocation, including splitting --via into a -J jump chain plus the mandatory positional target. - open.rs: the detach/re-exec dance (setsid via pre_exec) and a bounded wait for the supervisor to reach Up/Error before open returns, so an immediate failure surfaces as a non-zero exit instead of a false "opened" - this took a real bug fix during smoke testing, since the instance file's initial state (Reconnecting, meaning "attempt in flight") was indistinguishable from "already failed once" by state alone. - close.rs: SIGTERM+wait, or SIGKILL the whole process group with --force so ssh can't be left orphaned. Smoke-tested against invalid/unreachable hosts (no real infrastructure touched): CLI surface, validation errors, add/edit/list/status/remove, the reconnect/backoff loop with live state transitions, close mid-retry, edit-while-running's warning, open --all, and wipe. cargo test: 14/14. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
63
src/error.rs
Normal file
63
src/error.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum PortholeError {
|
||||
#[error(
|
||||
"invalid name '{0}' (use 1-64 chars: letters, digits, '_' or '-'; \
|
||||
must start with a letter or digit)"
|
||||
)]
|
||||
InvalidName(String),
|
||||
|
||||
#[error("no profile named '{0}'")]
|
||||
NotFound(String),
|
||||
|
||||
#[error("profile '{0}' already exists (use 'edit' to modify it)")]
|
||||
AlreadyExists(String),
|
||||
|
||||
#[error("exactly one of -l/--local, -r/--remote, -d/--dynamic is required")]
|
||||
NoMappingKind,
|
||||
|
||||
#[error("only one of -l/--local, -r/--remote, -d/--dynamic may be given")]
|
||||
MultipleMappingKinds,
|
||||
|
||||
#[error("invalid forward spec '{0}': expected [bind:]port:host:hostport (or [bind:]port for -d)")]
|
||||
InvalidMapping(String),
|
||||
|
||||
#[error("invalid --via hop '{0}': expected [user@]host[:port]")]
|
||||
InvalidVia(String),
|
||||
|
||||
#[error("--via is required: at least one hop (its last entry is the ssh connection target)")]
|
||||
NoViaHosts,
|
||||
|
||||
#[error("nothing to do: {0}")]
|
||||
NothingToDo(String),
|
||||
|
||||
#[error("'{0}' failed to start: {1}")]
|
||||
OpenFailed(String, String),
|
||||
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("state file error: {0}")]
|
||||
Serde(String),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, PortholeError>;
|
||||
|
||||
impl From<toml::de::Error> for PortholeError {
|
||||
fn from(e: toml::de::Error) -> Self {
|
||||
PortholeError::Serde(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<toml::ser::Error> for PortholeError {
|
||||
fn from(e: toml::ser::Error) -> Self {
|
||||
PortholeError::Serde(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for PortholeError {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
PortholeError::Serde(e.to_string())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user