diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79e7d75..b3493bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,7 +61,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: perf, memory, and 30s fuzz gates - run: cargo test --release --test acceptance -- --ignored --nocapture + run: cargo test --release --test acceptance -- --ignored --nocapture --test-threads=1 m4-perf-gates: name: M4 Perf Gates @@ -71,7 +71,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: tree-sitter parse + highlight latency budgets - run: cargo test --release --test m4_acceptance -- --ignored --nocapture + run: cargo test --release --test m4_acceptance -- --ignored --nocapture --test-threads=1 # m5-perf-gates: luajit-only by design. # The keystroke-to-render gate measures protocol-path latency (Unix diff --git a/src/daemon.rs b/src/daemon.rs index 31b08b7..3bc7d49 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -79,6 +79,18 @@ fn daemon_debug_enabled() -> bool { std::env::var_os(PMACS_ATTACH_DEBUG).is_some_and(|v| !v.is_empty() && v != "0") } +#[cfg(any(target_os = "linux", target_os = "android"))] +fn peer_uid(stream: &UnixStream) -> Option { + nix::sys::socket::getsockopt(stream, nix::sys::socket::sockopt::PeerCredentials) + .ok() + .map(|cred| cred.uid()) +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +fn peer_uid(_stream: &UnixStream) -> Option { + None +} + fn daemon_debug(msg: impl AsRef) { if daemon_debug_enabled() { eprintln!("pmacs daemon debug: {}", msg.as_ref()); @@ -698,16 +710,13 @@ fn per_attach_thread( // reconnect → same color slot. If SO_PEERCRED fails (e.g., // non-Unix peer, kernel API unavailable), fall back to a // per-FrontendId slot (degrades to per-connection stability). - let color_slot = - match nix::sys::socket::getsockopt(&stream, nix::sys::socket::sockopt::PeerCredentials) { - Ok(cred) => daemon_state.color_slot_for_uid(cred.uid()), - Err(_) => { - // Fallback: use frontend_id-based slot; per-connection - // stability only (no cross-reconnect within session). - u8::try_from(frontend_id.0 % (crate::overlay_color::PALETTE_LEN as u64)) - .unwrap_or(0) - } - }; + let color_slot = if let Some(uid) = peer_uid(&stream) { + daemon_state.color_slot_for_uid(uid) + } else { + // Fallback: use frontend_id-based slot; per-connection + // stability only (no cross-reconnect within session). + u8::try_from(frontend_id.0 % (crate::overlay_color::PALETTE_LEN as u64)).unwrap_or(0) + }; let session_state = crate::presence::SessionState::new(req.protocol_version, negotiated_caps, color_slot); diff --git a/src/process.rs b/src/process.rs index 2116a98..4bc19bf 100644 --- a/src/process.rs +++ b/src/process.rs @@ -389,7 +389,7 @@ const READER_SEND_POLL_INTERVAL: Duration = Duration::from_millis(50); /// parser worker may still have already-read bytes in flight. This is /// not process termination grace; it is only the final output flush /// before the runtime handles are dropped. -const EXIT_OUTPUT_DRAIN_TIMEOUT: Duration = Duration::from_millis(250); +const EXIT_OUTPUT_DRAIN_TIMEOUT: Duration = Duration::from_secs(2); // --------------------------------------------------------------------------- // Supervisor diff --git a/tests/m5_7_acceptance.rs b/tests/m5_7_acceptance.rs index 097d558..ec0733d 100644 --- a/tests/m5_7_acceptance.rs +++ b/tests/m5_7_acceptance.rs @@ -294,10 +294,18 @@ fn cli_attach_invokes_test_ssh_with_expected_argv() { }); let lines: Vec<&str> = argv.lines().collect(); // build_ssh_command for `ssh:mac-studio` (no user, no instance) - // emits `[-T, host, "pmacs", "--daemon-attach"]`. + // defaults to the stderr protocol channel and advertises the + // remote fd through env. assert_eq!( lines, - vec!["-T", "mac-studio", "pmacs", "--daemon-attach"], + vec![ + "-T", + "mac-studio", + "env", + "PMACS_ATTACH_PROTOCOL_FD=2", + "pmacs", + "--daemon-attach", + ], "argv shape: {lines:?}" ); } @@ -330,6 +338,8 @@ fn cli_attach_with_user_and_instance_name_passes_through_dash_l_and_dash_dash_so "-l", "alice", "workstation", + "env", + "PMACS_ATTACH_PROTOCOL_FD=2", "pmacs", "--daemon-attach", "--socket", diff --git a/tests/m5_8_acceptance.rs b/tests/m5_8_acceptance.rs index f71ccee..6a5b24c 100644 --- a/tests/m5_8_acceptance.rs +++ b/tests/m5_8_acceptance.rs @@ -68,6 +68,8 @@ use pmacs::protocol::{ mod common; use common::pty::spawn_pmacs_in_pty; +const PMACS_ATTACH_SSH_PROTOCOL: &str = "PMACS_ATTACH_SSH_PROTOCOL"; + // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- @@ -145,6 +147,7 @@ fn handshake_retry_cap_fires_after_three_failed_handshakes() { let output = Command::new(env!("CARGO_BIN_EXE_pmacs")) .args(["--attach", "host"]) .env(PMACS_TEST_SSH_BIN, &fake_ssh) + .env(PMACS_ATTACH_SSH_PROTOCOL, "stdout") .env(PMACS_TEST_BACKOFF_SCALE_MS, "1") .env("HOME", isolated_home) .env("XDG_CONFIG_HOME", isolated_home) @@ -261,6 +264,7 @@ fn ssh_stderr_from_handshake_attempts_reaches_user() { let output = Command::new(env!("CARGO_BIN_EXE_pmacs")) .args(["--attach", "host"]) .env(PMACS_TEST_SSH_BIN, &fake_ssh) + .env(PMACS_ATTACH_SSH_PROTOCOL, "stdout") .env(PMACS_TEST_BACKOFF_SCALE_MS, "1") .env("HOME", isolated_home) .env("XDG_CONFIG_HOME", isolated_home) @@ -438,6 +442,7 @@ fn ssh_dies_mid_pump_then_reconnect_succeeds() { &["--attach", "host"], &[ (PMACS_TEST_SSH_BIN, fx.fake_ssh.as_path()), + (PMACS_ATTACH_SSH_PROTOCOL, Path::new("stdout")), (PMACS_TEST_BACKOFF_SCALE_MS, Path::new("1")), ("HOME", fx.isolated_home.as_path()), ("XDG_CONFIG_HOME", fx.isolated_home.as_path()), @@ -489,6 +494,7 @@ fn ctrl_c_during_reconnect_sleep_yields_clean_exit() { &["--attach", "host"], &[ (PMACS_TEST_SSH_BIN, fx.fake_ssh.as_path()), + (PMACS_ATTACH_SSH_PROTOCOL, Path::new("stdout")), (PMACS_TEST_BACKOFF_SCALE_MS, Path::new("1000")), ("HOME", fx.isolated_home.as_path()), ("XDG_CONFIG_HOME", fx.isolated_home.as_path()), diff --git a/tests/m6_5_repl_acceptance.rs b/tests/m6_5_repl_acceptance.rs index 3456316..ae7478a 100644 --- a/tests/m6_5_repl_acceptance.rs +++ b/tests/m6_5_repl_acceptance.rs @@ -32,11 +32,17 @@ use pmacs::editor::EditorState; use std::fmt::Write as _; use std::path::PathBuf; -use std::sync::Mutex; +use std::sync::{Mutex, MutexGuard}; use std::time::{Duration, Instant}; static PUMP_TEST_LOCK: Mutex<()> = Mutex::new(()); +fn pump_test_guard() -> MutexGuard<'static, ()> { + PUMP_TEST_LOCK + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) +} + /// Locate a shell binary for tests that require one. Returns the /// resolved path or `None` if the shell is neither at `PMACS_TEST_` /// nor on `PATH`. Per-test selective skipping (rather than skipping @@ -70,6 +76,7 @@ fn locate_shell(name: &str) -> Option { /// Construct a fresh editor and run the given Lua chunk against it. fn run(chunk: &str) { + let _guard = pump_test_guard(); let mut editor = EditorState::new(); editor .lua_host @@ -84,7 +91,7 @@ fn run(chunk: &str) { /// `poll_until` pattern but routes through `tick_processes` so the M6.5 /// after-tick contract is exercised end-to-end. fn run_with_pump(setup_chunk: &str, predicate_chunk: &str, timeout_ms: u64) { - let _guard = PUMP_TEST_LOCK.lock().expect("pump test lock"); + let _guard = pump_test_guard(); let mut editor = EditorState::new(); editor .lua_host