Replace mid-sentence dashes in comments with commas/semicolons
Swapped " - " for a semicolon where it joined two independent clauses, or a comma where the following text was an appositive/dependent phrase with no subject of its own. Markdown-style list bullets in tests/live_test.sh's header (leading "- Item") are unaffected - those are structural, not sentence punctuation. Also dropped a "Spec §5.4:" citation in edit.rs missed by the earlier spec-citation cleanup (case-sensitive grep at the time didn't match the capitalized "Spec"). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
//! Atomic file writes: write to a sibling temp file, then `rename` over the
|
//! Atomic file writes: write to a sibling temp file, then `rename` over the
|
||||||
//! target. Matters here specifically for the instance JSON file, which the
|
//! target. Matters here specifically for the instance JSON file, which the
|
||||||
//! supervisor rewrites on every state change while it may be alive for
|
//! supervisor rewrites on every state change while it may be alive for
|
||||||
//! months - a reader (`status`/`list`) must never observe a half-written
|
//! months; a reader (`status`/`list`) must never observe a half-written
|
||||||
//! file, and a crash mid-write must never corrupt the last-known-good state.
|
//! file, and a crash mid-write must never corrupt the last-known-good state.
|
||||||
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|||||||
@@ -48,12 +48,12 @@ 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.
|
/// use; `open` spawns this itself.
|
||||||
#[command(hide = true, name = "__supervise")]
|
#[command(hide = true, name = "__supervise")]
|
||||||
Supervise { name: String },
|
Supervise { name: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shared mapping/connection flags for `add` and `edit` - kept as one
|
/// Shared mapping/connection flags for `add` and `edit`, kept as one
|
||||||
/// struct (`#[command(flatten)]`ed into both) so the two can never drift.
|
/// struct (`#[command(flatten)]`ed into both) so the two can never drift.
|
||||||
#[derive(Args, Default)]
|
#[derive(Args, Default)]
|
||||||
pub struct MappingArgs {
|
pub struct MappingArgs {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ 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 - shared with `remove` and `wipe`.
|
/// stale instance file either way, shared with `remove` and `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 {
|
||||||
@@ -28,7 +28,7 @@ pub fn close_instance(name: &str, force: bool) -> Result<bool> {
|
|||||||
|
|
||||||
if force {
|
if force {
|
||||||
// SIGKILL the whole process group (the supervisor is its own
|
// SIGKILL the whole process group (the supervisor is its own
|
||||||
// group leader via setsid), not just the supervisor pid - a plain
|
// group leader via setsid), not just the supervisor pid; 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 {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ pub fn run(args: EditArgs) -> Result<()> {
|
|||||||
p.apply_edits(&edits)?;
|
p.apply_edits(&edits)?;
|
||||||
profile::save(&p)?;
|
profile::save(&p)?;
|
||||||
|
|
||||||
// Spec §5.4: edit never restarts a running instance - just warn.
|
// edit never restarts a running instance, just warn.
|
||||||
if instance::running_pid(&name)?.is_some() {
|
if instance::running_pid(&name)?.is_some() {
|
||||||
ui::warn(&format!(
|
ui::warn(&format!(
|
||||||
"'{name}' is currently open; this change won't take effect until the next open/close cycle."
|
"'{name}' is currently open; this change won't take effect until the next open/close cycle."
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ pub fn edits_from_mapping(m: &MappingArgs) -> ProfileEdits {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `true` if `MappingArgs` carries no edits at all - used by `edit` to
|
/// `true` if `MappingArgs` carries no edits at all, used by `edit` to
|
||||||
/// reject a no-op invocation.
|
/// reject a no-op invocation.
|
||||||
pub fn mapping_is_empty(m: &MappingArgs) -> bool {
|
pub fn mapping_is_empty(m: &MappingArgs) -> bool {
|
||||||
m.local.is_none()
|
m.local.is_none()
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ pub fn run(args: OpenArgs) -> Result<()> {
|
|||||||
open_one(&name, args.foreground, args.once)
|
open_one(&name, args.foreground, args.once)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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. Per-profile
|
/// hook external autostart mechanisms are meant to call. 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<()> {
|
||||||
@@ -57,7 +57,7 @@ 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" - this just
|
// lock, not this file, is the authority on "already open"; this just
|
||||||
// keeps `status` from reading stale state mid-spawn.
|
// keeps `status` from reading stale state mid-spawn.
|
||||||
instance::delete(name)?;
|
instance::delete(name)?;
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ pub fn run(args: StatusArgs) -> Result<()> {
|
|||||||
let p = profile::load(&name)?;
|
let p = profile::load(&name)?;
|
||||||
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.
|
// 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));
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! Bulk profile backup/restore - a flat TOML array of the same `Profile`
|
//! Bulk profile backup/restore, a flat TOML array of the same `Profile`
|
||||||
//! records `profile::save`/`load` already read and write, so it round-trips
|
//! records `profile::save`/`load` already read and write, so it round-trips
|
||||||
//! through the exact same serialization with nothing profile-specific here.
|
//! through the exact same serialization with nothing profile-specific here.
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ fn import(path: &str, name: Option<&str>) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Identity files are never included in the export, only the path - warn so
|
/// Identity files are never included in the export, only the path; warn so
|
||||||
/// that doesn't come as a surprise on the importing end.
|
/// that doesn't come as a surprise on the importing end.
|
||||||
fn warn_about_identities(profiles: &[Profile]) {
|
fn warn_about_identities(profiles: &[Profile]) {
|
||||||
let names: Vec<&str> = profiles.iter().filter(|p| p.identity.is_some()).map(|p| p.name.as_str()).collect();
|
let names: Vec<&str> = profiles.iter().filter(|p| p.identity.is_some()).map(|p| p.name.as_str()).collect();
|
||||||
@@ -91,7 +91,7 @@ fn warn_about_identities(profiles: &[Profile]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// After import, flag any profile whose identity path doesn't resolve on
|
/// After import, flag any profile whose identity path doesn't resolve on
|
||||||
/// this machine - the most likely sign of a not-yet-copied key file.
|
/// this machine, the most likely sign of a not-yet-copied key file.
|
||||||
fn warn_about_missing_identities(profiles: &[Profile]) {
|
fn warn_about_missing_identities(profiles: &[Profile]) {
|
||||||
for p in profiles {
|
for p in profiles {
|
||||||
if let Some(identity) = &p.identity {
|
if let Some(identity) = &p.identity {
|
||||||
@@ -107,7 +107,7 @@ fn warn_about_missing_identities(profiles: &[Profile]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Expands a leading `~/` the same way `ssh` itself does at spawn time
|
/// Expands a leading `~/` the same way `ssh` itself does at spawn time
|
||||||
/// (`ssh.rs`) - without this, a valid `~/...` identity path would be
|
/// (`ssh.rs`); without this, a valid `~/...` identity path would be
|
||||||
/// misreported as missing since `Path::is_file` never expands `~` on its own.
|
/// misreported as missing since `Path::is_file` never expands `~` on its own.
|
||||||
fn expand_home(path: &str) -> PathBuf {
|
fn expand_home(path: &str) -> PathBuf {
|
||||||
match path.strip_prefix("~/").zip(dirs::home_dir()) {
|
match path.strip_prefix("~/").zip(dirs::home_dir()) {
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ 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 - its
|
/// Loads the instance file for `name`, if any. `None` means closed; its
|
||||||
/// absence *is* the closed state; there is no separate enum value for it.
|
/// absence *is* the closed state; there is no separate enum value for it.
|
||||||
pub fn load(name: &str) -> Result<Option<Instance>>
|
pub fn load(name: &str) -> Result<Option<Instance>>
|
||||||
{
|
{
|
||||||
@@ -146,7 +146,7 @@ pub fn delete(name: &str) -> Result<()>
|
|||||||
pub fn process_alive(pid: i32) -> bool { unsafe { libc::kill(pid, 0) == 0 } }
|
pub fn process_alive(pid: i32) -> bool { unsafe { libc::kill(pid, 0) == 0 } }
|
||||||
|
|
||||||
/// True only if `pid` is a live process whose cmdline identifies it as the
|
/// True only if `pid` is a live process whose cmdline identifies it as the
|
||||||
/// supervisor for `name` - guards against a stale or reused pid.
|
/// supervisor for `name`, guards against a stale or reused pid.
|
||||||
pub fn supervisor_alive(pid: i32, name: &str) -> bool
|
pub fn supervisor_alive(pid: i32, name: &str) -> bool
|
||||||
{
|
{
|
||||||
if !process_alive(pid) { return false };
|
if !process_alive(pid) { return false };
|
||||||
@@ -159,7 +159,7 @@ pub fn supervisor_alive(pid: i32, name: &str) -> bool
|
|||||||
|
|
||||||
/// The live supervisor pid for `name`, if one is currently running
|
/// The live supervisor pid for `name`, if one is currently running
|
||||||
/// (checked against the process table, not just the instance file's
|
/// (checked against the process table, not just the instance file's
|
||||||
/// last-known value - so a crash or reboot is detected as "not running"
|
/// last-known value, so a crash or reboot is detected as "not running"
|
||||||
/// rather than trusting stale on-disk state).
|
/// rather than trusting stale on-disk state).
|
||||||
pub fn running_pid(name: &str) -> Result<Option<i32>>
|
pub fn running_pid(name: &str) -> Result<Option<i32>>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ fn main()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// True for a bare `porthole` invocation, or top-level `-h`/`--help`/`help`
|
/// True for a bare `porthole` invocation, or top-level `-h`/`--help`/`help`,
|
||||||
/// - i.e. anything that should show the expanded help rather than being
|
/// i.e. anything that should show the expanded help rather than being
|
||||||
/// handled (or rejected) by a specific subcommand.
|
/// handled (or rejected) by a specific subcommand.
|
||||||
fn wants_top_level_help(args: &[String]) -> bool {
|
fn wants_top_level_help(args: &[String]) -> bool {
|
||||||
match args.get(1..) {
|
match args.get(1..) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# auth (success and failure), a real two-hop ProxyJump, a real SOCKS proxy
|
# auth (success and failure), a real two-hop ProxyJump, a real SOCKS proxy
|
||||||
# carrying real traffic. Deliberately NOT part of `cargo test` (same reason
|
# carrying real traffic. Deliberately NOT part of `cargo test` (same reason
|
||||||
# as vmic's tests/live_test.sh: this needs a real remote server, not a CI
|
# as vmic's tests/live_test.sh: this needs a real remote server, not a CI
|
||||||
# sandbox) - live-only, opt-in, run by hand.
|
# sandbox), live-only, opt-in, run by hand.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# tests/live_test.sh [options]
|
# tests/live_test.sh [options]
|
||||||
@@ -22,21 +22,21 @@
|
|||||||
# -h, --help
|
# -h, --help
|
||||||
#
|
#
|
||||||
# Known limitation: only one real server is available, so multi-hop (`-J`)
|
# Known limitation: only one real server is available, so multi-hop (`-J`)
|
||||||
# is tested by chaining the server through itself (--via user@host,user@host)
|
# is tested by chaining the server through itself (--via user@host,user@host),
|
||||||
# - a real two-hop ProxyJump handshake, just with both hops the same box.
|
# a real two-hop ProxyJump handshake, just with both hops the same box.
|
||||||
# There is no way to test a genuine distinct-host chain without a second
|
# There is no way to test a genuine distinct-host chain without a second
|
||||||
# server.
|
# server.
|
||||||
#
|
#
|
||||||
# Safety:
|
# Safety:
|
||||||
# - Profiles/instances/locks/logs are sandboxed for the whole run under
|
# - Profiles/instances/locks/logs are sandboxed for the whole run under
|
||||||
# one `PORTHOLE_STATE_DIR_OVERRIDE` temp dir (profile.rs/instance.rs
|
# one `PORTHOLE_STATE_DIR_OVERRIDE` temp dir (profile.rs/instance.rs
|
||||||
# both honor it) - this suite NEVER touches the real
|
# both honor it); this suite NEVER touches the real
|
||||||
# ~/.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` (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
|
||||||
# and skips the wipe phase entirely (not "wipe only the safe parts")
|
# and skips the wipe phase entirely (not "wipe only the safe parts")
|
||||||
# if it finds a live __supervise process that isn't one of this run's
|
# if it finds a live __supervise process that isn't one of this run's
|
||||||
@@ -48,10 +48,10 @@
|
|||||||
# never writes to your real ~/.ssh/known_hosts (only porthole's own
|
# never writes to your real ~/.ssh/known_hosts (only porthole's own
|
||||||
# spawned ssh does, against the real file, using the accept-new policy
|
# spawned ssh does, against the real file, using the accept-new policy
|
||||||
# already forced in src/ssh.rs).
|
# already forced in src/ssh.rs).
|
||||||
# - An EXIT trap always attempts full cleanup - closes/removes every
|
# - An EXIT trap always attempts full cleanup, closes/removes every
|
||||||
# test-prefixed profile in every sandbox dir used, force-kills any
|
# test-prefixed profile in every sandbox dir used, force-kills any
|
||||||
# stray matching __supervise process, deletes the throwaway bad-auth
|
# stray matching __supervise process, deletes the throwaway bad-auth
|
||||||
# key - even on failure or Ctrl-C.
|
# key, even on failure or Ctrl-C.
|
||||||
|
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||||
@@ -117,7 +117,7 @@ p_run() { LAST_OUT="$(PORTHOLE_STATE_DIR_OVERRIDE="$1" "$BIN" "${@:2}" 2>&1)"; L
|
|||||||
|
|
||||||
# status --json's "state" field is always one of closed/up/reconnecting/error
|
# status --json's "state" field is always one of closed/up/reconnecting/error
|
||||||
# (print_json normalizes a dead-supervisor instance file to "error" too, see
|
# (print_json normalizes a dead-supervisor instance file to "error" too, see
|
||||||
# status.rs) - polling that key is far more robust than scraping the padded
|
# status.rs); polling that key is far more robust than scraping the padded
|
||||||
# human-readable field.
|
# human-readable field.
|
||||||
json_field() { # json_field <state-dir> <name> <field> -> value, empty if absent/no instance
|
json_field() { # json_field <state-dir> <name> <field> -> value, empty if absent/no instance
|
||||||
p "$1" status "$2" --json 2>/dev/null | sed -n "s/.*\"$3\": \"\\{0,1\\}\\([^\",]*\\)\"\\{0,1\\},\\{0,1\\}\$/\\1/p" | head -1
|
p "$1" status "$2" --json 2>/dev/null | sed -n "s/.*\"$3\": \"\\{0,1\\}\\([^\",]*\\)\"\\{0,1\\},\\{0,1\\}\$/\\1/p" | head -1
|
||||||
@@ -140,7 +140,7 @@ wait_port_closed() { # wait_port_closed <port> [tries, x0.5s]
|
|||||||
}
|
}
|
||||||
SSH_PROBE_OPTS=(-o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null)
|
SSH_PROBE_OPTS=(-o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null)
|
||||||
|
|
||||||
# mktemp's output is captured via $(...), which forks a subshell - any
|
# mktemp's output is captured via $(...), which forks a subshell; any
|
||||||
# array append done *inside* a function called that way would be lost when
|
# array append done *inside* a function called that way would be lost when
|
||||||
# the subshell exits, so state dirs are appended here at the call site
|
# the subshell exits, so state dirs are appended here at the call site
|
||||||
# instead of through a helper function.
|
# instead of through a helper function.
|
||||||
@@ -408,7 +408,7 @@ expect_contains "warns it's left running untracked" "left running untracked"
|
|||||||
p_run "$STATE_DIR" status "${NAME}_keep"; assert_eq "profile is gone from tracking" "$LAST_CODE" "1"
|
p_run "$STATE_DIR" status "${NAME}_keep"; assert_eq "profile is gone from tracking" "$LAST_CODE" "1"
|
||||||
assert_true "the untracked process is still actually alive" pgrep -f "__supervise ${NAME}_keep\$"
|
assert_true "the untracked process is still actually alive" pgrep -f "__supervise ${NAME}_keep\$"
|
||||||
assert_true "port $KEEP_PORT is still live, untracked" port_open "$KEEP_PORT"
|
assert_true "port $KEEP_PORT is still live, untracked" port_open "$KEEP_PORT"
|
||||||
# left running on purpose - phase 15's wipe is what's being tested against it
|
# left running on purpose; phase 15's wipe is what's being tested against it
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
section "Phase 14: transfer round trip against a live profile"
|
section "Phase 14: transfer round trip against a live profile"
|
||||||
|
|||||||
Reference in New Issue
Block a user