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:
49
src/commands/mod.rs
Normal file
49
src/commands/mod.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
pub mod add;
|
||||
pub mod close;
|
||||
pub mod completions;
|
||||
pub mod edit;
|
||||
pub mod list;
|
||||
pub mod open;
|
||||
pub mod remove;
|
||||
pub mod status;
|
||||
pub mod wipe;
|
||||
|
||||
use crate::cli::MappingArgs;
|
||||
use crate::profile::ProfileEdits;
|
||||
|
||||
/// Turns clap's `MappingArgs` into a `ProfileEdits`, splitting `--via`'s
|
||||
/// comma-separated raw string into the ordered hop list (spec §5.1).
|
||||
pub fn edits_from_mapping(m: &MappingArgs) -> ProfileEdits {
|
||||
let via = m.via.as_deref().map(|raw| {
|
||||
raw.split(',').map(str::trim).filter(|s| !s.is_empty()).map(str::to_string).collect::<Vec<_>>()
|
||||
});
|
||||
ProfileEdits {
|
||||
local: m.local.clone(),
|
||||
remote: m.remote.clone(),
|
||||
dynamic: m.dynamic.clone(),
|
||||
via,
|
||||
user: m.user.clone(),
|
||||
identity: m.identity.clone(),
|
||||
port: m.port,
|
||||
reconnect: m.reconnect,
|
||||
retry_interval: m.retry_interval,
|
||||
backoff_max: m.backoff_max,
|
||||
keepalive: m.keepalive,
|
||||
}
|
||||
}
|
||||
|
||||
/// `true` if `MappingArgs` carries no edits at all - used by `edit` to
|
||||
/// reject a no-op invocation the same way vmic's commands do.
|
||||
pub fn mapping_is_empty(m: &MappingArgs) -> bool {
|
||||
m.local.is_none()
|
||||
&& m.remote.is_none()
|
||||
&& m.dynamic.is_none()
|
||||
&& m.via.is_none()
|
||||
&& m.user.is_none()
|
||||
&& m.identity.is_none()
|
||||
&& m.port.is_none()
|
||||
&& m.reconnect.is_none()
|
||||
&& m.retry_interval.is_none()
|
||||
&& m.backoff_max.is_none()
|
||||
&& m.keepalive.is_none()
|
||||
}
|
||||
Reference in New Issue
Block a user