@team-agent/installer 0.5.2 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +3 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +20 -0
- package/crates/team-agent/src/cli/emit.rs +5 -0
- package/crates/team-agent/src/cli/mod.rs +121 -56
- package/crates/team-agent/src/cli/tests/named_address.rs +4 -0
- package/crates/team-agent/src/codex_app_server.rs +62 -1
- package/crates/team-agent/src/conpty/backend.rs +120 -8
- package/crates/team-agent/src/coordinator/backoff.rs +88 -2
- package/crates/team-agent/src/coordinator/conpty_shim.rs +730 -0
- package/crates/team-agent/src/coordinator/health.rs +97 -11
- package/crates/team-agent/src/coordinator/mod.rs +8 -0
- package/crates/team-agent/src/coordinator/tests/daemon.rs +2 -1
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +6 -0
- package/crates/team-agent/src/diagnose/orphans.rs +18 -7
- package/crates/team-agent/src/leader/provider_attribution.rs +19 -34
- package/crates/team-agent/src/leader/tests/lease_api.rs +7 -0
- package/crates/team-agent/src/lib.rs +14 -1
- package/crates/team-agent/src/lifecycle/launch.rs +80 -91
- package/crates/team-agent/src/lifecycle/lock.rs +40 -33
- package/crates/team-agent/src/lifecycle/restart/agent.rs +10 -18
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +7 -0
- package/crates/team-agent/src/mcp_server/wire.rs +6 -7
- package/crates/team-agent/src/messaging/tests/runtime.rs +5 -0
- package/crates/team-agent/src/packaging/tests.rs +41 -6
- package/crates/team-agent/src/packaging/types.rs +31 -3
- package/crates/team-agent/src/platform/argv.rs +324 -0
- package/crates/team-agent/src/platform/errors.rs +95 -0
- package/crates/team-agent/src/platform/file_lock.rs +418 -0
- package/crates/team-agent/src/platform/mod.rs +66 -0
- package/crates/team-agent/src/platform/process.rs +555 -0
- package/crates/team-agent/src/state/persist.rs +205 -25
- package/crates/team-agent/src/tmux_backend.rs +63 -13
- package/crates/team-agent/src/transport_factory.rs +124 -5
- package/package.json +4 -4
|
@@ -559,25 +559,17 @@ pub(super) fn drain_old_pane_and_pid(
|
|
|
559
559
|
})
|
|
560
560
|
}
|
|
561
561
|
|
|
562
|
-
/// Best-effort liveness probe for a pid.
|
|
563
|
-
///
|
|
562
|
+
/// Best-effort liveness probe for a pid.
|
|
563
|
+
///
|
|
564
|
+
/// 0.5.x Windows portability Batch 3: routes through
|
|
565
|
+
/// `crate::platform::process::pid_is_alive`. Unix uses `kill(pid, 0)`
|
|
566
|
+
/// with the `EPERM = Live` branch preserved byte-for-byte; Windows
|
|
567
|
+
/// uses `OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)` +
|
|
568
|
+
/// `GetExitCodeProcess` (STILL_ACTIVE = Live). The legacy non-Unix
|
|
569
|
+
/// `true` fallback (which broke drain by reporting every pid alive)
|
|
570
|
+
/// is gone.
|
|
564
571
|
pub(super) fn pid_is_alive(pid: u32) -> bool {
|
|
565
|
-
|
|
566
|
-
{
|
|
567
|
-
// SAFETY: kill(pid, 0) checks if pid exists without sending a signal.
|
|
568
|
-
let ret = unsafe { libc::kill(pid as i32, 0) };
|
|
569
|
-
if ret == 0 {
|
|
570
|
-
return true;
|
|
571
|
-
}
|
|
572
|
-
// EPERM also means the process exists (we just can't signal it).
|
|
573
|
-
let err = std::io::Error::last_os_error();
|
|
574
|
-
matches!(err.raw_os_error(), Some(libc::EPERM))
|
|
575
|
-
}
|
|
576
|
-
#[cfg(not(unix))]
|
|
577
|
-
{
|
|
578
|
-
let _ = pid;
|
|
579
|
-
true
|
|
580
|
-
}
|
|
572
|
+
crate::platform::process::pid_is_alive(pid)
|
|
581
573
|
}
|
|
582
574
|
|
|
583
575
|
/// Read state.agents[agent_id].pane_pid (u32) from the runtime state.
|
|
@@ -287,6 +287,7 @@ impl Drop for EnvVarGuard {
|
|
|
287
287
|
}
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
#[cfg(unix)]
|
|
290
291
|
struct TmuxShim {
|
|
291
292
|
log: std::path::PathBuf,
|
|
292
293
|
_path: EnvVarGuard,
|
|
@@ -297,6 +298,7 @@ struct TmuxShim {
|
|
|
297
298
|
_real_tmux: EnvVarGuard,
|
|
298
299
|
}
|
|
299
300
|
|
|
301
|
+
#[cfg(unix)]
|
|
300
302
|
fn install_e27_tmux_shim(expected_endpoint: &str, session_name: &str) -> TmuxShim {
|
|
301
303
|
use std::os::unix::fs::PermissionsExt;
|
|
302
304
|
|
|
@@ -441,6 +443,9 @@ fn assert_only_expected_socket_used(log: &std::path::Path, expected_endpoint: &s
|
|
|
441
443
|
}
|
|
442
444
|
}
|
|
443
445
|
|
|
446
|
+
// 0.5.x Windows portability Batch 5: E27 tests use a shell-script
|
|
447
|
+
// tmux shim + Unix socket paths. Unix-only.
|
|
448
|
+
#[cfg(unix)]
|
|
444
449
|
#[test]
|
|
445
450
|
#[serial_test::serial(env)]
|
|
446
451
|
fn e27_stop_agent_uses_attached_explicit_state_socket() {
|
|
@@ -461,6 +466,7 @@ fn e27_stop_agent_uses_attached_explicit_state_socket() {
|
|
|
461
466
|
);
|
|
462
467
|
}
|
|
463
468
|
|
|
469
|
+
#[cfg(unix)]
|
|
464
470
|
#[test]
|
|
465
471
|
#[serial_test::serial(env)]
|
|
466
472
|
fn e27_reset_agent_uses_attached_explicit_state_socket_for_stop_and_spawn() {
|
|
@@ -491,6 +497,7 @@ fn e27_reset_agent_uses_attached_explicit_state_socket_for_stop_and_spawn() {
|
|
|
491
497
|
);
|
|
492
498
|
}
|
|
493
499
|
|
|
500
|
+
#[cfg(unix)]
|
|
494
501
|
#[test]
|
|
495
502
|
#[serial_test::serial(env)]
|
|
496
503
|
fn e27_stop_agent_expands_short_state_socket_name() {
|
|
@@ -314,14 +314,13 @@ fn non_empty_env(key: &str) -> Option<String> {
|
|
|
314
314
|
.filter(|value| !value.is_empty())
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
|
|
317
|
+
// 0.5.x Windows portability Batch 3: parent-pid probe routes through
|
|
318
|
+
// `crate::platform::process::current_parent_pid`. Unix uses
|
|
319
|
+
// `libc::getppid`; Windows uses Toolhelp32Snapshot. Both return
|
|
320
|
+
// `Option<u32>`; the legacy `0` fallback (which would have silently
|
|
321
|
+
// degraded MCP metadata + orphan detection on Windows) is gone.
|
|
318
322
|
fn parent_pid() -> u32 {
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
#[cfg(not(unix))]
|
|
323
|
-
fn parent_pid() -> u32 {
|
|
324
|
-
0
|
|
323
|
+
crate::platform::process::current_parent_pid().unwrap_or(0)
|
|
325
324
|
}
|
|
326
325
|
|
|
327
326
|
fn error_response_value(id: RpcId, code: i64, message: String) -> Value {
|
|
@@ -1932,6 +1932,9 @@ fn app_server_state(
|
|
|
1932
1932
|
})
|
|
1933
1933
|
}
|
|
1934
1934
|
|
|
1935
|
+
// 0.5.x Windows portability Batch 5: FakeAppServer uses Unix domain
|
|
1936
|
+
// sockets — Unix-only.
|
|
1937
|
+
#[cfg(unix)]
|
|
1935
1938
|
#[test]
|
|
1936
1939
|
fn app_server_leader_delivery_marks_delivered_without_tmux_transport() {
|
|
1937
1940
|
let ws = tmp_ws("appdeliver");
|
|
@@ -1984,6 +1987,7 @@ fn app_server_leader_delivery_marks_delivered_without_tmux_transport() {
|
|
|
1984
1987
|
);
|
|
1985
1988
|
}
|
|
1986
1989
|
|
|
1990
|
+
#[cfg(unix)]
|
|
1987
1991
|
#[test]
|
|
1988
1992
|
fn app_server_leader_delivery_stale_tuple_fails_closed_with_event() {
|
|
1989
1993
|
let ws = tmp_ws("appstale");
|
|
@@ -2076,6 +2080,7 @@ fn app_server_leader_delivery_fails_closed_on_mode_transport_conflict() {
|
|
|
2076
2080
|
);
|
|
2077
2081
|
}
|
|
2078
2082
|
|
|
2083
|
+
#[cfg(unix)]
|
|
2079
2084
|
#[test]
|
|
2080
2085
|
fn app_server_leader_busy_is_retryable_and_does_not_steer() {
|
|
2081
2086
|
let ws = tmp_ws("appbusy");
|
|
@@ -222,12 +222,47 @@ fn linux_aarch64_is_native() {
|
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
#[test]
|
|
225
|
-
fn
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
225
|
+
fn windows_x8664_is_preview_compile_only_during_batch_0_to_5() {
|
|
226
|
+
// 0.5.x Windows portability CR C-1 (P0):
|
|
227
|
+
// Windows previously claimed `PlatformSupport::Native` but
|
|
228
|
+
// `cargo check --target x86_64-pc-windows-msvc` was RED — the
|
|
229
|
+
// Unix-only `std::os::unix` / `libc::pid_t` calls in
|
|
230
|
+
// tmux_backend/coordinator/lifecycle/packaging never compiled on
|
|
231
|
+
// Windows. Claiming Native was a MUST-NOT-13 假绿承诺 (user
|
|
232
|
+
// installs and immediately hits build failures).
|
|
233
|
+
//
|
|
234
|
+
// This test locks the honest downgrade in place until Batch 6/7
|
|
235
|
+
// real-machine gates pass. Promoting Windows back to `Native`
|
|
236
|
+
// requires:
|
|
237
|
+
// 1. Batch 6 fake-provider smoke on the SSH host (design §Batch 6)
|
|
238
|
+
// 2. Batch 7 real-machine subscription serial (CR §C-5)
|
|
239
|
+
// Both flip this test to `assert PlatformSupport::Native` in a
|
|
240
|
+
// deliberate PR — no silent regen.
|
|
241
|
+
//
|
|
242
|
+
// Truth source: `.team/artifacts/0.5.x-windows-portability-cr-verdict.md` §C-1.
|
|
243
|
+
match platform_support(ReleaseTarget::WindowsX8664) {
|
|
244
|
+
PlatformSupport::PreviewCompileOnly { preview_gate, note } => {
|
|
245
|
+
assert_eq!(preview_gate, "compile_gate");
|
|
246
|
+
assert!(note.contains("compile_gate") || note.contains("cargo check"));
|
|
247
|
+
}
|
|
248
|
+
other => panic!(
|
|
249
|
+
"Windows must stay `PreviewCompileOnly` while the compile gate is RED; \
|
|
250
|
+
promoting back to `Native` requires Batch 6 + Batch 7 real-machine gates. \
|
|
251
|
+
Got {other:?}"
|
|
252
|
+
),
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#[test]
|
|
257
|
+
fn windows_x8664_is_not_native_yet_c1_hard_lock() {
|
|
258
|
+
// Belt-and-braces companion for the above — a distinct assertion
|
|
259
|
+
// that fires clearly if a future refactor of `PlatformSupport`
|
|
260
|
+
// changes variant names.
|
|
261
|
+
let support = platform_support(ReleaseTarget::WindowsX8664);
|
|
262
|
+
assert_ne!(
|
|
263
|
+
support,
|
|
264
|
+
PlatformSupport::Native,
|
|
265
|
+
"CR C-1: Windows must NOT be Native until Batch 6/7 gates pass"
|
|
231
266
|
);
|
|
232
267
|
}
|
|
233
268
|
|
|
@@ -162,13 +162,27 @@ pub enum ReleaseTarget {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
/// 平台支持等级(§8:如实声明,不假装兼容)。
|
|
165
|
+
///
|
|
166
|
+
/// 0.5.x Windows portability CR C-1 (P0) 引入 `PreviewCompileOnly`:
|
|
167
|
+
/// Windows 目前 `cargo check --target x86_64-pc-windows-msvc` **仍红**
|
|
168
|
+
/// (三处 non-Unix fallback + platform 层未接入 + Batch 1-4 未完成);
|
|
169
|
+
/// 保持 `Native` 会构成 MUST-NOT-13 假绿承诺 → 用户装 Windows 二进制
|
|
170
|
+
/// 会真报错。降级为 `PreviewCompileOnly` 明示"当前 Windows 尚未通过
|
|
171
|
+
/// 编译门,Batch 6/7 真机订阅测通过前不承诺 Native"。
|
|
172
|
+
/// Truth source: `.team/artifacts/0.5.x-windows-portability-cr-verdict.md` §C-1。
|
|
165
173
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
166
174
|
#[serde(rename_all = "snake_case", tag = "level")]
|
|
167
175
|
pub enum PlatformSupport {
|
|
168
|
-
/// 原生一等(macOS/Linux tmux
|
|
176
|
+
/// 原生一等(macOS/Linux tmux)。
|
|
169
177
|
Native,
|
|
170
178
|
/// 需 WSL + tmux backend(显式标要求,不假装原生)。
|
|
171
179
|
RequiresWslTmux { note: String },
|
|
180
|
+
/// Windows 移植进行中,`cargo check` 仍红或 Batch 6/7 真机订阅测未过。
|
|
181
|
+
/// 用户不应期待可运行的 `.exe`;`preview_gate` 字段说明当前挡位
|
|
182
|
+
/// (`compile_gate` = Batch 0-4 编译门,`fake_provider_smoke` = Batch 6,
|
|
183
|
+
/// `subscription_realmachine` = Batch 7)。
|
|
184
|
+
/// 该档位不承诺可用,仅承诺"正在移植 + 每 Batch 有 CI 编译门追踪"。
|
|
185
|
+
PreviewCompileOnly { preview_gate: String, note: String },
|
|
172
186
|
/// 当前 release 矩阵不覆盖。
|
|
173
187
|
Unsupported { reason: String },
|
|
174
188
|
}
|
|
@@ -179,8 +193,22 @@ pub fn platform_support(target: ReleaseTarget) -> PlatformSupport {
|
|
|
179
193
|
ReleaseTarget::MacosAarch64
|
|
180
194
|
| ReleaseTarget::MacosX8664
|
|
181
195
|
| ReleaseTarget::LinuxX8664
|
|
182
|
-
| ReleaseTarget::LinuxAarch64
|
|
183
|
-
|
|
196
|
+
| ReleaseTarget::LinuxAarch64 => PlatformSupport::Native,
|
|
197
|
+
// 0.5.x Windows portability CR C-1 (P0):
|
|
198
|
+
// `cargo check --target x86_64-pc-windows-msvc` 是当前挡位;
|
|
199
|
+
// Batch 6 fake-provider smoke + Batch 7 真机订阅测通过后
|
|
200
|
+
// 才升回 `Native`。历史 tag 期间 Windows 曾声明为 Native 但
|
|
201
|
+
// 从未通过编译门,构成 MUST-NOT-13 假绿承诺 → 本 batch 降级。
|
|
202
|
+
ReleaseTarget::WindowsX8664 => PlatformSupport::PreviewCompileOnly {
|
|
203
|
+
preview_gate: "compile_gate".to_string(),
|
|
204
|
+
note: "Windows native port in progress; `cargo check --target x86_64-pc-windows-msvc` \
|
|
205
|
+
is the current gate. See \
|
|
206
|
+
`.team/artifacts/0.5.x-windows-portability-survey-design.md` §Batch 0-6 \
|
|
207
|
+
and CR verdict §C-1 for burn-down status. Promote back to `Native` only \
|
|
208
|
+
after Batch 6 (fake-provider smoke) + Batch 7 (subscription real-machine) \
|
|
209
|
+
pass on the SSH host."
|
|
210
|
+
.to_string(),
|
|
211
|
+
},
|
|
184
212
|
}
|
|
185
213
|
}
|
|
186
214
|
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
//! Process argv / environ probe.
|
|
2
|
+
//!
|
|
3
|
+
//! ## Batch 4 real implementation (leader msg_0689a63a9e40)
|
|
4
|
+
//!
|
|
5
|
+
//! Batch 4 promotes this module from a Batch 0 signature-only
|
|
6
|
+
//! scaffold to a **byte-preserving migration** of the process ancestry
|
|
7
|
+
//! + argv/env probes used by:
|
|
8
|
+
//!
|
|
9
|
+
//! - `lifecycle/launch.rs::process_ancestry_argv` +
|
|
10
|
+
//! `process_argv_tokens` + `process_parent_pid` — the
|
|
11
|
+
//! `dangerous_auto_approve` inheritance chain (0.5.0 caller-identity
|
|
12
|
+
//! 钉输入 is exactly this test surface).
|
|
13
|
+
//! - `leader/provider_attribution.rs::process_command_line` +
|
|
14
|
+
//! `process_environment` — leader provider attribution.
|
|
15
|
+
//!
|
|
16
|
+
//! ## Platform matrix
|
|
17
|
+
//!
|
|
18
|
+
//! - **Linux**: `/proc/<pid>/cmdline` (NUL-separated argv) and
|
|
19
|
+
//! `/proc/<pid>/environ` (NUL-separated KEY=VALUE).
|
|
20
|
+
//! - **macOS**: `sysctl(KERN_PROCARGS2)` for argv; environ not
|
|
21
|
+
//! currently probed (matches the pre-batch behavior).
|
|
22
|
+
//! - **non-Linux non-macOS Unix**: `ps -p <pid> -o command=` for argv.
|
|
23
|
+
//! - **Windows**: `NtQueryInformationProcess` + PEB is intrusive; for
|
|
24
|
+
//! Batch 4 we return `None` for both `argv_tokens` and
|
|
25
|
+
//! `environ_text` (design §Batch 4 Verification anchor: "unknown
|
|
26
|
+
//! argv must never infer elevated approval; keep worker permission
|
|
27
|
+
//! at provider default or require explicit user consent"). This is
|
|
28
|
+
//! the honest "we don't know" branch — callers already treat
|
|
29
|
+
//! `None` as "no elevation inherited", so Windows leaders default
|
|
30
|
+
//! to safe (non-dangerous) approval mode.
|
|
31
|
+
//!
|
|
32
|
+
//! ## CR C-3 anchor
|
|
33
|
+
//!
|
|
34
|
+
//! The `Option::None` return on Windows deliberately keeps worker
|
|
35
|
+
//! permission at provider default. Design §Batch 4 Verification: "the
|
|
36
|
+
//! safe direction is to avoid elevation, not to assume bypass" —
|
|
37
|
+
//! `detect_dangerous_approval` in `lifecycle/launch.rs` already
|
|
38
|
+
//! iterates `process_ancestry_argv(...)` and defaults to
|
|
39
|
+
//! `disabled_dangerous_approval()` when it finds no matching flag,
|
|
40
|
+
//! so a Windows probe returning `None` on every step lands on the
|
|
41
|
+
//! same disabled default. **This is intentional**: leaking a
|
|
42
|
+
//! bypass via unknown argv would be a MUST-NOT-13 假绿 failure.
|
|
43
|
+
|
|
44
|
+
/// Return the argv tokens for `pid`, or `None` if unavailable on
|
|
45
|
+
/// this platform.
|
|
46
|
+
///
|
|
47
|
+
/// - Linux: `/proc/<pid>/cmdline` NUL-separated (byte-preserving
|
|
48
|
+
/// migration of `lifecycle/launch.rs:3648-3656`).
|
|
49
|
+
/// - macOS: `sysctl(KERN_PROCARGS2)` (migration of
|
|
50
|
+
/// `lifecycle/launch.rs:3659-3711`).
|
|
51
|
+
/// - Other Unix: `ps -p <pid> -o command=` split on whitespace
|
|
52
|
+
/// (migration of `lifecycle/launch.rs:3714-3729`).
|
|
53
|
+
/// - Windows: `None` (design §Batch 4 conservative fallback — never
|
|
54
|
+
/// infer elevated approval from unknown argv).
|
|
55
|
+
pub fn argv_tokens(pid: u32) -> Option<Vec<String>> {
|
|
56
|
+
#[cfg(target_os = "linux")]
|
|
57
|
+
{
|
|
58
|
+
argv_tokens_linux(pid)
|
|
59
|
+
}
|
|
60
|
+
#[cfg(target_os = "macos")]
|
|
61
|
+
{
|
|
62
|
+
argv_tokens_macos(pid)
|
|
63
|
+
}
|
|
64
|
+
#[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
|
|
65
|
+
{
|
|
66
|
+
argv_tokens_ps_fallback(pid)
|
|
67
|
+
}
|
|
68
|
+
#[cfg(windows)]
|
|
69
|
+
{
|
|
70
|
+
let _ = pid;
|
|
71
|
+
None
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Return the environ text (NUL-joined) for `pid`, or `None`.
|
|
76
|
+
///
|
|
77
|
+
/// - Linux: `/proc/<pid>/environ` verbatim (migration of
|
|
78
|
+
/// `leader/provider_attribution.rs:113-115`).
|
|
79
|
+
/// - Other platforms: `None` (matches the pre-batch
|
|
80
|
+
/// `#[cfg(not(target_os="linux"))]` stub in
|
|
81
|
+
/// `leader/provider_attribution.rs:135-139`).
|
|
82
|
+
pub fn environ_text(pid: u32) -> Option<String> {
|
|
83
|
+
#[cfg(target_os = "linux")]
|
|
84
|
+
{
|
|
85
|
+
environ_text_linux(pid)
|
|
86
|
+
}
|
|
87
|
+
#[cfg(not(target_os = "linux"))]
|
|
88
|
+
{
|
|
89
|
+
let _ = pid;
|
|
90
|
+
None
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Return the parent PID of `pid`, or `None` if unavailable.
|
|
95
|
+
///
|
|
96
|
+
/// Byte-preserving migration of `lifecycle/launch.rs::process_parent_pid`:
|
|
97
|
+
/// - Unix: `ps -p <pid> -o ppid=` parsed as u32.
|
|
98
|
+
/// - Windows: Toolhelp32 snapshot walked for the entry matching
|
|
99
|
+
/// `pid`. Same technique as `platform::process::current_parent_pid`.
|
|
100
|
+
pub fn parent_pid(pid: u32) -> Option<u32> {
|
|
101
|
+
#[cfg(unix)]
|
|
102
|
+
{
|
|
103
|
+
parent_pid_unix(pid)
|
|
104
|
+
}
|
|
105
|
+
#[cfg(windows)]
|
|
106
|
+
{
|
|
107
|
+
parent_pid_windows(pid)
|
|
108
|
+
}
|
|
109
|
+
#[cfg(not(any(unix, windows)))]
|
|
110
|
+
{
|
|
111
|
+
let _ = pid;
|
|
112
|
+
None
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
117
|
+
// Linux implementation.
|
|
118
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
#[cfg(target_os = "linux")]
|
|
121
|
+
fn argv_tokens_linux(pid: u32) -> Option<Vec<String>> {
|
|
122
|
+
let bytes = std::fs::read(format!("/proc/{pid}/cmdline")).ok()?;
|
|
123
|
+
let argv_tokens = String::from_utf8_lossy(&bytes)
|
|
124
|
+
.split('\0')
|
|
125
|
+
.filter(|token| !token.is_empty())
|
|
126
|
+
.map(str::to_string)
|
|
127
|
+
.collect::<Vec<_>>();
|
|
128
|
+
(!argv_tokens.is_empty()).then_some(argv_tokens)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#[cfg(target_os = "linux")]
|
|
132
|
+
fn environ_text_linux(pid: u32) -> Option<String> {
|
|
133
|
+
String::from_utf8(std::fs::read(format!("/proc/{pid}/environ")).ok()?).ok()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
137
|
+
// macOS implementation.
|
|
138
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
#[cfg(target_os = "macos")]
|
|
141
|
+
fn argv_tokens_macos(pid: u32) -> Option<Vec<String>> {
|
|
142
|
+
use std::mem::size_of;
|
|
143
|
+
let mut mib = [
|
|
144
|
+
libc::CTL_KERN,
|
|
145
|
+
libc::KERN_PROCARGS2,
|
|
146
|
+
i32::try_from(pid).ok()?,
|
|
147
|
+
];
|
|
148
|
+
let mut size = 0usize;
|
|
149
|
+
let rc = unsafe {
|
|
150
|
+
libc::sysctl(
|
|
151
|
+
mib.as_mut_ptr(),
|
|
152
|
+
mib.len() as u32,
|
|
153
|
+
std::ptr::null_mut(),
|
|
154
|
+
&mut size,
|
|
155
|
+
std::ptr::null_mut(),
|
|
156
|
+
0,
|
|
157
|
+
)
|
|
158
|
+
};
|
|
159
|
+
if rc != 0 || size <= size_of::<libc::c_int>() {
|
|
160
|
+
return None;
|
|
161
|
+
}
|
|
162
|
+
let mut buf = vec![0u8; size];
|
|
163
|
+
let rc = unsafe {
|
|
164
|
+
libc::sysctl(
|
|
165
|
+
mib.as_mut_ptr(),
|
|
166
|
+
mib.len() as u32,
|
|
167
|
+
buf.as_mut_ptr().cast(),
|
|
168
|
+
&mut size,
|
|
169
|
+
std::ptr::null_mut(),
|
|
170
|
+
0,
|
|
171
|
+
)
|
|
172
|
+
};
|
|
173
|
+
if rc != 0 || size <= size_of::<libc::c_int>() {
|
|
174
|
+
return None;
|
|
175
|
+
}
|
|
176
|
+
let argc = i32::from_ne_bytes(buf.get(..size_of::<libc::c_int>())?.try_into().ok()?) as usize;
|
|
177
|
+
let mut offset = size_of::<libc::c_int>();
|
|
178
|
+
while offset < size && buf[offset] != 0 {
|
|
179
|
+
offset += 1;
|
|
180
|
+
}
|
|
181
|
+
while offset < size && buf[offset] == 0 {
|
|
182
|
+
offset += 1;
|
|
183
|
+
}
|
|
184
|
+
let raw = String::from_utf8_lossy(&buf[offset..size]);
|
|
185
|
+
let argv_tokens = raw
|
|
186
|
+
.split('\0')
|
|
187
|
+
.filter(|token| !token.is_empty())
|
|
188
|
+
.take(argc)
|
|
189
|
+
.map(str::to_string)
|
|
190
|
+
.collect::<Vec<_>>();
|
|
191
|
+
(!argv_tokens.is_empty()).then_some(argv_tokens)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
195
|
+
// Generic Unix fallback (`ps -p <pid> -o ...`).
|
|
196
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
#[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
|
|
199
|
+
fn argv_tokens_ps_fallback(pid: u32) -> Option<Vec<String>> {
|
|
200
|
+
let output = std::process::Command::new("ps")
|
|
201
|
+
.args(["-p", &pid.to_string(), "-o", "command="])
|
|
202
|
+
.output()
|
|
203
|
+
.ok()?;
|
|
204
|
+
if !output.status.success() {
|
|
205
|
+
return None;
|
|
206
|
+
}
|
|
207
|
+
let text = String::from_utf8_lossy(&output.stdout);
|
|
208
|
+
let argv_tokens = text
|
|
209
|
+
.split_whitespace()
|
|
210
|
+
.filter(|token| !token.is_empty())
|
|
211
|
+
.map(str::to_string)
|
|
212
|
+
.collect::<Vec<_>>();
|
|
213
|
+
(!argv_tokens.is_empty()).then_some(argv_tokens)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#[cfg(unix)]
|
|
217
|
+
fn parent_pid_unix(pid: u32) -> Option<u32> {
|
|
218
|
+
let output = std::process::Command::new("ps")
|
|
219
|
+
.args(["-p", &pid.to_string(), "-o", "ppid="])
|
|
220
|
+
.output()
|
|
221
|
+
.ok()?;
|
|
222
|
+
if !output.status.success() {
|
|
223
|
+
return None;
|
|
224
|
+
}
|
|
225
|
+
String::from_utf8_lossy(&output.stdout)
|
|
226
|
+
.trim()
|
|
227
|
+
.parse::<u32>()
|
|
228
|
+
.ok()
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
232
|
+
// Windows implementation.
|
|
233
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
#[cfg(windows)]
|
|
236
|
+
fn parent_pid_windows(pid: u32) -> Option<u32> {
|
|
237
|
+
use windows::Win32::System::Diagnostics::ToolHelp::{
|
|
238
|
+
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
|
|
239
|
+
TH32CS_SNAPPROCESS,
|
|
240
|
+
};
|
|
241
|
+
let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }.ok()?;
|
|
242
|
+
let mut entry = PROCESSENTRY32W {
|
|
243
|
+
dwSize: std::mem::size_of::<PROCESSENTRY32W>() as u32,
|
|
244
|
+
..Default::default()
|
|
245
|
+
};
|
|
246
|
+
let mut result = unsafe { Process32FirstW(snapshot, &mut entry) };
|
|
247
|
+
while result.is_ok() {
|
|
248
|
+
if entry.th32ProcessID == pid {
|
|
249
|
+
let ppid = entry.th32ParentProcessID;
|
|
250
|
+
unsafe {
|
|
251
|
+
let _ = windows::Win32::Foundation::CloseHandle(snapshot);
|
|
252
|
+
}
|
|
253
|
+
return Some(ppid);
|
|
254
|
+
}
|
|
255
|
+
result = unsafe { Process32NextW(snapshot, &mut entry) };
|
|
256
|
+
}
|
|
257
|
+
unsafe {
|
|
258
|
+
let _ = windows::Win32::Foundation::CloseHandle(snapshot);
|
|
259
|
+
}
|
|
260
|
+
None
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
264
|
+
// Tests.
|
|
265
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
#[cfg(test)]
|
|
268
|
+
mod tests {
|
|
269
|
+
use super::*;
|
|
270
|
+
|
|
271
|
+
#[test]
|
|
272
|
+
fn argv_tokens_returns_some_for_own_pid_on_unix_or_none_on_windows() {
|
|
273
|
+
// Byte-preserving migration of the ancestry probe. On Unix
|
|
274
|
+
// this must return a non-empty argv for our own process (test
|
|
275
|
+
// binary). On Windows the honest `None` return is intentional:
|
|
276
|
+
// callers see the "no elevation inherited" default branch,
|
|
277
|
+
// which is safer than reading unknown argv (CR C-3 anchor).
|
|
278
|
+
let my = std::process::id();
|
|
279
|
+
let argv = argv_tokens(my);
|
|
280
|
+
#[cfg(unix)]
|
|
281
|
+
{
|
|
282
|
+
let argv = argv.expect("unix must return Some argv for own pid");
|
|
283
|
+
assert!(!argv.is_empty());
|
|
284
|
+
// At least argv[0] should look like a test binary path.
|
|
285
|
+
let argv0 = &argv[0];
|
|
286
|
+
assert!(!argv0.is_empty());
|
|
287
|
+
}
|
|
288
|
+
#[cfg(windows)]
|
|
289
|
+
{
|
|
290
|
+
// Windows conservative fallback: None, so
|
|
291
|
+
// `detect_dangerous_approval` defaults to disabled.
|
|
292
|
+
assert!(argv.is_none());
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#[test]
|
|
297
|
+
fn parent_pid_returns_some_on_both_platforms() {
|
|
298
|
+
// Batch 4: parent_pid via `ps -o ppid=` (unix) or Toolhelp32
|
|
299
|
+
// (windows) — both must resolve our own ppid.
|
|
300
|
+
let my = std::process::id();
|
|
301
|
+
let ppid = parent_pid(my);
|
|
302
|
+
assert!(ppid.is_some(), "parent_pid must resolve own pid on both unix and windows");
|
|
303
|
+
assert!(ppid.unwrap() > 0);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
#[test]
|
|
307
|
+
fn environ_text_linux_only_returns_something_for_own_pid() {
|
|
308
|
+
// The environ probe is Linux-only by design (matches
|
|
309
|
+
// `leader/provider_attribution.rs:113-115`). Other platforms
|
|
310
|
+
// honestly return None; callers already treat that as
|
|
311
|
+
// "no env-based attribution".
|
|
312
|
+
let my = std::process::id();
|
|
313
|
+
let env = environ_text(my);
|
|
314
|
+
#[cfg(target_os = "linux")]
|
|
315
|
+
{
|
|
316
|
+
let env = env.expect("linux must return Some environ");
|
|
317
|
+
assert!(env.contains('='));
|
|
318
|
+
}
|
|
319
|
+
#[cfg(not(target_os = "linux"))]
|
|
320
|
+
{
|
|
321
|
+
assert!(env.is_none());
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
//! Retryable OS error classifier + human-readable os_error_name.
|
|
2
|
+
//!
|
|
3
|
+
//! Batch 0 signature only. Batch 2 migrates the raw
|
|
4
|
+
//! `libc::{EACCES, EPERM, EBUSY, ENOSPC}` matching in
|
|
5
|
+
//! `state/persist.rs::retryable_replace_error` onto these helpers so
|
|
6
|
+
//! Windows sees the same classification via
|
|
7
|
+
//! `io::ErrorKind` + raw-code mapping.
|
|
8
|
+
|
|
9
|
+
use std::io;
|
|
10
|
+
|
|
11
|
+
/// True when the OS error is a transient replace failure worth
|
|
12
|
+
/// retrying (EACCES on Windows during antivirus scan, EBUSY on
|
|
13
|
+
/// mounted filesystems, etc.). Batch 2 wires the callsite.
|
|
14
|
+
#[allow(dead_code)]
|
|
15
|
+
pub fn retryable_replace_error(error: &io::Error) -> bool {
|
|
16
|
+
// Batch 2 will migrate `state/persist.rs::retryable_replace_error`
|
|
17
|
+
// here. Batch 0 provides the signature only.
|
|
18
|
+
#[cfg(unix)]
|
|
19
|
+
{
|
|
20
|
+
matches!(
|
|
21
|
+
error.raw_os_error(),
|
|
22
|
+
Some(c)
|
|
23
|
+
if c == libc::EACCES
|
|
24
|
+
|| c == libc::EPERM
|
|
25
|
+
|| c == libc::EBUSY
|
|
26
|
+
|| c == libc::ENOSPC
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
#[cfg(not(unix))]
|
|
30
|
+
{
|
|
31
|
+
// Windows: retryable on sharing violation / access denied
|
|
32
|
+
// during antivirus scans. Map by `io::ErrorKind` so we don't
|
|
33
|
+
// depend on `windows-sys` raw codes at this layer.
|
|
34
|
+
matches!(
|
|
35
|
+
error.kind(),
|
|
36
|
+
io::ErrorKind::PermissionDenied | io::ErrorKind::WouldBlock
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Human-readable name for a raw OS error code — used in diagnostics
|
|
42
|
+
/// so operators see `EACCES` instead of `13`.
|
|
43
|
+
#[allow(dead_code)]
|
|
44
|
+
pub fn os_error_name(error: &io::Error) -> Option<&'static str> {
|
|
45
|
+
#[cfg(unix)]
|
|
46
|
+
{
|
|
47
|
+
match error.raw_os_error()? {
|
|
48
|
+
c if c == libc::EACCES => Some("EACCES"),
|
|
49
|
+
c if c == libc::EPERM => Some("EPERM"),
|
|
50
|
+
c if c == libc::EBUSY => Some("EBUSY"),
|
|
51
|
+
c if c == libc::ENOSPC => Some("ENOSPC"),
|
|
52
|
+
c if c == libc::EWOULDBLOCK => Some("EWOULDBLOCK"),
|
|
53
|
+
c if c == libc::ESRCH => Some("ESRCH"),
|
|
54
|
+
_ => None,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
#[cfg(not(unix))]
|
|
58
|
+
{
|
|
59
|
+
// Batch 2 will map ERROR_ACCESS_DENIED / ERROR_SHARING_VIOLATION
|
|
60
|
+
// / ERROR_LOCK_VIOLATION here. Batch 0 leaves the mapping to
|
|
61
|
+
// `io::Error::kind()`.
|
|
62
|
+
let _ = error;
|
|
63
|
+
None
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#[cfg(test)]
|
|
68
|
+
mod tests {
|
|
69
|
+
use super::*;
|
|
70
|
+
|
|
71
|
+
#[test]
|
|
72
|
+
fn retryable_replace_error_matches_eacces_on_unix() {
|
|
73
|
+
#[cfg(unix)]
|
|
74
|
+
{
|
|
75
|
+
let err = io::Error::from_raw_os_error(libc::EACCES);
|
|
76
|
+
assert!(retryable_replace_error(&err));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#[test]
|
|
81
|
+
fn retryable_replace_error_rejects_random_error() {
|
|
82
|
+
// A generic "not found" is not a replace-race condition.
|
|
83
|
+
let err = io::Error::from(io::ErrorKind::NotFound);
|
|
84
|
+
assert!(!retryable_replace_error(&err));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[test]
|
|
88
|
+
fn os_error_name_returns_stable_wire_string_on_unix() {
|
|
89
|
+
#[cfg(unix)]
|
|
90
|
+
{
|
|
91
|
+
let err = io::Error::from_raw_os_error(libc::EACCES);
|
|
92
|
+
assert_eq!(os_error_name(&err), Some("EACCES"));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|