Remove spec §N.N citations from comments, keep them self-contained
Comments citing the spec document instead of just stating the fact directly made their usefulness depend on cross-referencing a separate file. Reworded each one to stand alone - same content, citation dropped, folded into a normal sentence where it was mid-clause rather than trailing. spec/porthole-spec.md itself is untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -48,7 +48,7 @@ pub enum Commands {
|
|||||||
Completions { shell: Shell },
|
Completions { shell: Shell },
|
||||||
|
|
||||||
/// Internal: runs the supervisor loop for one profile. Not for direct
|
/// Internal: runs the supervisor loop for one profile. Not for direct
|
||||||
/// use - `open` spawns this itself (spec §3).
|
/// use - `open` spawns this itself.
|
||||||
#[command(hide = true, name = "__supervise")]
|
#[command(hide = true, name = "__supervise")]
|
||||||
Supervise { name: String },
|
Supervise { name: String },
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ pub fn run(args: CloseArgs) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Stops `name`'s supervisor if one is actually running, and clears any
|
/// Stops `name`'s supervisor if one is actually running, and clears any
|
||||||
/// stale instance file either way (spec §5.3) - shared with `remove` and
|
/// stale instance file either way - shared with `remove` and `wipe`.
|
||||||
/// `wipe`. Returns whether anything was actually running.
|
/// Returns whether anything was actually running.
|
||||||
pub fn close_instance(name: &str, force: bool) -> Result<bool> {
|
pub fn close_instance(name: &str, force: bool) -> Result<bool> {
|
||||||
let Some(pid) = instance::running_pid(name)? else {
|
let Some(pid) = instance::running_pid(name)? else {
|
||||||
instance::delete(name)?; // clears a stale file left by a crash
|
instance::delete(name)?; // clears a stale file left by a crash
|
||||||
@@ -27,9 +27,9 @@ pub fn close_instance(name: &str, force: bool) -> Result<bool> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if force {
|
if force {
|
||||||
// spec §3 step 4: SIGKILL the whole process group (the supervisor
|
// SIGKILL the whole process group (the supervisor is its own
|
||||||
// is its own group leader via setsid), not just the supervisor pid
|
// group leader via setsid), not just the supervisor pid - a plain
|
||||||
// - a plain single-pid SIGKILL would leave `ssh` orphaned.
|
// single-pid SIGKILL would leave `ssh` orphaned.
|
||||||
unsafe { libc::kill(-pid, libc::SIGKILL) };
|
unsafe { libc::kill(-pid, libc::SIGKILL) };
|
||||||
} else {
|
} else {
|
||||||
unsafe { libc::kill(pid, libc::SIGTERM) };
|
unsafe { libc::kill(pid, libc::SIGTERM) };
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ pub mod wipe;
|
|||||||
use crate::cli::MappingArgs;
|
use crate::cli::MappingArgs;
|
||||||
use crate::profile::ProfileEdits;
|
use crate::profile::ProfileEdits;
|
||||||
|
|
||||||
/// Turns clap's `MappingArgs` into a `ProfileEdits` (spec §5.1). `--via` is
|
/// Turns clap's `MappingArgs` into a `ProfileEdits`. `--via` is
|
||||||
/// collected by clap itself: `value_delimiter = ','` splits each
|
/// collected by clap itself: `value_delimiter = ','` splits each
|
||||||
/// occurrence on commas, and the field being a `Vec` allows repeated
|
/// occurrence on commas, and the field being a `Vec` allows repeated
|
||||||
/// `--via` flags, so both `--via a,b` and `--via a --via b` reach here as
|
/// `--via` flags, so both `--via a,b` and `--via a --via b` reach here as
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
//! `open` - spec §3/§5.2. Validates, then either runs the supervisor loop
|
//! `open` validates, then either runs the supervisor loop inline
|
||||||
//! inline (`--foreground`) or spawns a detached copy of this binary
|
//! (`--foreground`) or spawns a detached copy of this binary
|
||||||
//! (`porthole __supervise <name>`) and waits briefly for it to confirm.
|
//! (`porthole __supervise <name>`) and waits briefly for it to confirm.
|
||||||
|
|
||||||
use crate::cli::OpenArgs;
|
use crate::cli::OpenArgs;
|
||||||
@@ -25,8 +25,8 @@ pub fn run(args: OpenArgs) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Opens every `reconnect: true` profile that isn't already running - the
|
/// Opens every `reconnect: true` profile that isn't already running - the
|
||||||
/// hook external autostart mechanisms are meant to call (spec §5.2/§8).
|
/// hook external autostart mechanisms are meant to call. Per-profile
|
||||||
/// Per-profile failures are warnings, not a whole-batch failure.
|
/// failures are warnings, not a whole-batch failure.
|
||||||
fn open_all(once: bool) -> Result<()> {
|
fn open_all(once: bool) -> Result<()> {
|
||||||
let profiles = profile::list_all()?;
|
let profiles = profile::list_all()?;
|
||||||
let mut opened = 0;
|
let mut opened = 0;
|
||||||
@@ -57,8 +57,8 @@ fn open_one(name: &str, foreground: bool, once: bool) -> Result<()> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
// Clear a stale instance file left by a crash before spawning. The
|
// Clear a stale instance file left by a crash before spawning. The
|
||||||
// lock, not this file, is the authority on "already open" (spec
|
// lock, not this file, is the authority on "already open" - this just
|
||||||
// §5.2) - this just keeps `status` from reading stale state mid-spawn.
|
// keeps `status` from reading stale state mid-spawn.
|
||||||
instance::delete(name)?;
|
instance::delete(name)?;
|
||||||
|
|
||||||
if foreground {
|
if foreground {
|
||||||
@@ -73,10 +73,10 @@ fn open_one(name: &str, foreground: bool, once: bool) -> Result<()> {
|
|||||||
wait_for_confirmation(name)
|
wait_for_confirmation(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawns `porthole __supervise <name>` fully detached (spec §3 steps 1-2):
|
/// Spawns `porthole __supervise <name>` fully detached: stdin from
|
||||||
/// stdin from `/dev/null`, stdout/stderr appended to the profile's log, and
|
/// `/dev/null`, stdout/stderr appended to the profile's log, and
|
||||||
/// `setsid()` in the child so it leaves this process's session and survives
|
/// `setsid()` in the child so it leaves this process's session and
|
||||||
/// the terminal closing.
|
/// survives the terminal closing.
|
||||||
fn spawn_detached(name: &str, once: bool) -> Result<()> {
|
fn spawn_detached(name: &str, once: bool) -> Result<()> {
|
||||||
let exe = std::env::current_exe()?;
|
let exe = std::env::current_exe()?;
|
||||||
let log_path = instance::log_path(name);
|
let log_path = instance::log_path(name);
|
||||||
@@ -99,8 +99,8 @@ fn spawn_detached(name: &str, once: bool) -> Result<()> {
|
|||||||
|
|
||||||
/// Blocks briefly for the detached supervisor to reach a conclusive state,
|
/// Blocks briefly for the detached supervisor to reach a conclusive state,
|
||||||
/// so an immediate failure (bad auth, bind conflict, unresolvable host) is
|
/// so an immediate failure (bad auth, bind conflict, unresolvable host) is
|
||||||
/// reported with a non-zero exit instead of `open` appearing to succeed
|
/// reported with a non-zero exit instead of `open` appearing to succeed.
|
||||||
/// (spec §5.2). The instance file's initial write is always
|
/// The instance file's initial write is always
|
||||||
/// `State::Reconnecting`, since the first attempt has not concluded yet;
|
/// `State::Reconnecting`, since the first attempt has not concluded yet;
|
||||||
/// that value is indistinguishable from "already failed once, backing
|
/// that value is indistinguishable from "already failed once, backing
|
||||||
/// off". This function waits specifically for `Up` or `Error`, not merely
|
/// off". This function waits specifically for `Up` or `Error`, not merely
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ pub fn run(args: StatusArgs) -> Result<()> {
|
|||||||
let inst = instance::load(&name)?;
|
let inst = instance::load(&name)?;
|
||||||
// An instance file whose pid isn't actually alive means the supervisor
|
// An instance file whose pid isn't actually alive means the supervisor
|
||||||
// crashed without cleaning up - report that, rather than trusting a
|
// crashed without cleaning up - report that, rather than trusting a
|
||||||
// state the process table disagrees with (spec §2.2).
|
// state the process table disagrees with.
|
||||||
let live = inst.as_ref().is_some_and(|i| instance::supervisor_alive(i.pid, &name));
|
let live = inst.as_ref().is_some_and(|i| instance::supervisor_alive(i.pid, &name));
|
||||||
|
|
||||||
if args.json {
|
if args.json {
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ pub fn run(args: WipeArgs) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Kills any supervisor process not backed by a tracked profile (e.g. one
|
/// Kills any supervisor process not backed by a tracked profile (e.g. one
|
||||||
/// orphaned after a crash), matched by cmdline rather than tracked state
|
/// orphaned after a crash), matched by cmdline rather than tracked state.
|
||||||
/// (spec §5.8). Sends SIGTERM to each supervisor's process group so its
|
/// Sends SIGTERM to each supervisor's process group so its `ssh` child is
|
||||||
/// `ssh` child is included.
|
/// included.
|
||||||
fn kill_orphaned_supervisors() -> u32 {
|
fn kill_orphaned_supervisors() -> u32 {
|
||||||
let mut killed = 0;
|
let mut killed = 0;
|
||||||
let Ok(entries) = std::fs::read_dir("/proc") else { return 0 };
|
let Ok(entries) = std::fs::read_dir("/proc") else { return 0 };
|
||||||
|
|||||||
@@ -111,9 +111,8 @@ pub fn instance_path(name: &str) -> PathBuf { state_dir().join(format!("{name}.j
|
|||||||
pub fn lock_path(name: &str) -> PathBuf { state_dir().join(format!("{name}.lock")) }
|
pub fn lock_path(name: &str) -> PathBuf { state_dir().join(format!("{name}.lock")) }
|
||||||
pub fn log_path(name: &str) -> PathBuf { state_dir().join(format!("{name}.log")) }
|
pub fn log_path(name: &str) -> PathBuf { state_dir().join(format!("{name}.log")) }
|
||||||
|
|
||||||
/// Loads the instance file for `name`, if any. `None` means closed - the
|
/// Loads the instance file for `name`, if any. `None` means closed - its
|
||||||
/// absence of this file is the closed state (spec §2.2); there is no
|
/// absence *is* the closed state; there is no separate enum value for it.
|
||||||
/// separate enum value for it.
|
|
||||||
pub fn load(name: &str) -> Result<Option<Instance>>
|
pub fn load(name: &str) -> Result<Option<Instance>>
|
||||||
{
|
{
|
||||||
let path = instance_path(name);
|
let path = instance_path(name);
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ impl Kind
|
|||||||
pub struct Profile {
|
pub struct Profile {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub kind: Kind,
|
pub kind: Kind,
|
||||||
/// Raw `-L/-R/-D` payload, without the flag itself - see spec §5.1.
|
/// Raw `-L/-R/-D` payload, without the flag itself.
|
||||||
pub mapping: String,
|
pub mapping: String,
|
||||||
/// Ordered hop list, each `[user@]host[:port]` - see spec §3.1.
|
/// Ordered hop list, each `[user@]host[:port]`.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub via: Vec<String>,
|
pub via: Vec<String>,
|
||||||
pub user: Option<String>,
|
pub user: Option<String>,
|
||||||
@@ -132,8 +132,8 @@ impl Profile
|
|||||||
|
|
||||||
/// Splits `via` into the `-J` jump-chain value (comma-joined, all but
|
/// Splits `via` into the `-J` jump-chain value (comma-joined, all but
|
||||||
/// the last hop; `None` for a single-hop `via`) and the final `ssh`
|
/// the last hop; `None` for a single-hop `via`) and the final `ssh`
|
||||||
/// connection target - see spec §3.1. Every path that constructs a
|
/// connection target. Every path that constructs a `Profile` validates
|
||||||
/// `Profile` validates `via` as non-empty.
|
/// `via` as non-empty.
|
||||||
pub fn ssh_target(&self) -> (Option<String>, &str)
|
pub fn ssh_target(&self) -> (Option<String>, &str)
|
||||||
{
|
{
|
||||||
match self.via.split_last() {
|
match self.via.split_last() {
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
//! Builds the `ssh` invocation for a profile - spec §3.1.
|
//! Builds the `ssh` invocation for a profile.
|
||||||
|
|
||||||
use std::process::{ Stdio, Command };
|
use std::process::{ Stdio, Command };
|
||||||
|
|
||||||
use crate::profile::Profile;
|
use crate::profile::Profile;
|
||||||
|
|
||||||
/// Builds the `ssh` command for `profile`, stdio wired for the supervisor
|
/// Builds the `ssh` command for `profile`, stdio wired for the supervisor
|
||||||
/// to capture (stdout/stderr piped so failure text can be classified per
|
/// to capture (stdout/stderr piped so failure text can be classified;
|
||||||
/// spec §4.2; stdin from `/dev/null` since porthole never wants a shell).
|
/// stdin from `/dev/null` since porthole never wants a shell).
|
||||||
pub fn build(profile: &Profile) -> Command {
|
pub fn build(profile: &Profile) -> Command {
|
||||||
let mut cmd = Command::new("ssh");
|
let mut cmd = Command::new("ssh");
|
||||||
cmd.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped());
|
cmd.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||||
|
|
||||||
// Forced flags, see spec §3.1.
|
// Flags forced on every invocation, not user-configurable.
|
||||||
cmd.args([
|
cmd.args([
|
||||||
"-o",
|
"-o",
|
||||||
"BatchMode=yes",
|
"BatchMode=yes",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! The `__supervise` loop - spec §3/§4. Runs as a detached, re-exec'd copy
|
//! The `__supervise` loop runs as a detached, re-exec'd copy
|
||||||
//! of this same binary (`porthole __supervise <name>`, see `main.rs`); owns
|
//! of this same binary (`porthole __supervise <name>`, see `main.rs`); owns
|
||||||
//! the `ssh` child process for one profile's entire supervised lifetime.
|
//! the `ssh` child process for one profile's entire supervised lifetime.
|
||||||
|
|
||||||
@@ -15,10 +15,10 @@ use crate::profile::{ self, Profile };
|
|||||||
use crate::instance::{ self, Lock, State, Instance };
|
use crate::instance::{ self, Lock, State, Instance };
|
||||||
|
|
||||||
/// How long a connection must survive before its uptime resets the backoff
|
/// How long a connection must survive before its uptime resets the backoff
|
||||||
/// counter back to the base delay - spec §4.1.
|
/// counter back to the base delay.
|
||||||
const STABLE_THRESHOLD_SECS: i64 = 60;
|
const STABLE_THRESHOLD_SECS: i64 = 60;
|
||||||
/// Consecutive unrecognized (not pattern-matched) failures before porthole
|
/// Consecutive unrecognized (not pattern-matched) failures before porthole
|
||||||
/// gives up on an apparently-permanently-broken profile - spec §4.2.
|
/// gives up on an apparently-permanently-broken profile.
|
||||||
const MAX_UNRECOGNIZED_STREAK: u32 = 10;
|
const MAX_UNRECOGNIZED_STREAK: u32 = 10;
|
||||||
/// How long `ssh` must stay alive before porthole treats it as connected;
|
/// How long `ssh` must stay alive before porthole treats it as connected;
|
||||||
/// see `run_ssh_once` for the heuristic this backs.
|
/// see `run_ssh_once` for the heuristic this backs.
|
||||||
@@ -32,7 +32,7 @@ extern "C" fn handle_sigterm(_sig: libc::c_int) { SHUTDOWN.store(true, Ordering:
|
|||||||
|
|
||||||
/// Traps SIGTERM and SIGINT into a flag instead of the default
|
/// Traps SIGTERM and SIGINT into a flag instead of the default
|
||||||
/// terminate-immediately behavior. This is how `close` (SIGTERM) and
|
/// terminate-immediately behavior. This is how `close` (SIGTERM) and
|
||||||
/// `-f/--foreground`'s Ctrl-C (SIGINT, spec §5.2) are distinguished from a
|
/// `-f/--foreground`'s Ctrl-C (SIGINT) are distinguished from a
|
||||||
/// dropped `ssh` connection: by which signal arrived, not by inferring
|
/// dropped `ssh` connection: by which signal arrived, not by inferring
|
||||||
/// intent from `ssh`'s exit status. In foreground mode this function runs
|
/// intent from `ssh`'s exit status. In foreground mode this function runs
|
||||||
/// in the process the terminal sends Ctrl-C to directly, since
|
/// in the process the terminal sends Ctrl-C to directly, since
|
||||||
@@ -215,7 +215,7 @@ fn join_all<const N: usize>(handles: [Option<JoinHandle<()>>; N]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Classifies `ssh`'s captured stderr per spec §4.2. Fatal patterns stop
|
/// Classifies `ssh`'s captured stderr. Fatal patterns stop
|
||||||
/// the reconnect loop outright; known-transient patterns retry without
|
/// the reconnect loop outright; known-transient patterns retry without
|
||||||
/// counting toward the unrecognized-failure escalation; anything else
|
/// counting toward the unrecognized-failure escalation; anything else
|
||||||
/// still retries, but does count toward it.
|
/// still retries, but does count toward it.
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
# ~/.config/porthole or ~/.local/state/porthole.
|
# ~/.config/porthole or ~/.local/state/porthole.
|
||||||
# - Every profile created is named "$NAME_..." (default prefix
|
# - Every profile created is named "$NAME_..." (default prefix
|
||||||
# porttestsuite); no operation targets anything outside that prefix.
|
# porttestsuite); no operation targets anything outside that prefix.
|
||||||
# - `wipe` (spec §5.8 / src/commands/wipe.rs) kills ANY process on the
|
# - `wipe` (src/commands/wipe.rs) kills ANY process on the
|
||||||
# whole system whose cmdline contains "__supervise", regardless of
|
# whole system whose cmdline contains "__supervise", regardless of
|
||||||
# which state dir it belongs to - it is NOT scoped by the sandboxing
|
# which state dir it belongs to - it is NOT scoped by the sandboxing
|
||||||
# above. Before running it, this script scans the real process table
|
# above. Before running it, this script scans the real process table
|
||||||
|
|||||||
Reference in New Issue
Block a user