dsh-ops 0.0.0-stage → 0.2.2
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/CHANGELOG.md +202 -0
- package/LICENSE +30 -0
- package/NOTICE +106 -0
- package/PROVENANCE.md +435 -0
- package/README.en.md +126 -0
- package/README.md +115 -2
- package/README.zh.md +116 -0
- package/bin/dsh-ops.mjs +1216 -0
- package/cordis.patch.yml +160 -0
- package/docs/manual-validation.md +53 -0
- package/docs/release-0.2.1.md +72 -0
- package/docs/schema-baseline.json +64 -0
- package/docs/schema-current.json +84 -0
- package/docs/schema-measurement.md +17 -0
- package/dsh-plugin.json +88 -0
- package/icon.svg +12 -0
- package/lib/binary.js +409 -0
- package/lib/config.js +198 -0
- package/lib/handshake.js +252 -0
- package/lib/index.js +108 -0
- package/lib/jobs.js +42 -0
- package/lib/policy.js +64 -0
- package/lib/presentation.js +63 -0
- package/lib/profile-install.js +61 -0
- package/lib/rust.js +194 -0
- package/lib/session-shells.js +78 -0
- package/lib/shells.js +998 -0
- package/lib/tools.js +657 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +114 -4
- package/vendor/fastctx/Cargo.lock +3210 -0
- package/vendor/fastctx/Cargo.toml +94 -0
- package/vendor/fastctx/FORK.md +119 -0
- package/vendor/fastctx/LICENSE-APACHE +201 -0
- package/vendor/fastctx/NOTICE +40 -0
- package/vendor/fastctx/README.md +439 -0
- package/vendor/fastctx/THIRD_PARTY_LICENSES.md +17 -0
- package/vendor/fastctx/THIRD_PARTY_LICENSES_RUST.md +7914 -0
- package/vendor/fastctx/UPSTREAM.md +49 -0
- package/vendor/fastctx/build.rs +413 -0
- package/vendor/fastctx/src/background_status.rs +403 -0
- package/vendor/fastctx/src/binary.rs +75 -0
- package/vendor/fastctx/src/bounded_sort.rs +500 -0
- package/vendor/fastctx/src/budget.rs +781 -0
- package/vendor/fastctx/src/cli/mod.rs +110 -0
- package/vendor/fastctx/src/context_guard.rs +289 -0
- package/vendor/fastctx/src/control/mod.rs +6 -0
- package/vendor/fastctx/src/control/paths.rs +49 -0
- package/vendor/fastctx/src/control/settings.rs +753 -0
- package/vendor/fastctx/src/control/transaction.rs +531 -0
- package/vendor/fastctx/src/edit/document.rs +535 -0
- package/vendor/fastctx/src/edit/locks.rs +371 -0
- package/vendor/fastctx/src/edit/mod.rs +213 -0
- package/vendor/fastctx/src/edit/private_storage/unix.rs +315 -0
- package/vendor/fastctx/src/edit/private_storage/windows.rs +793 -0
- package/vendor/fastctx/src/edit/private_storage.rs +234 -0
- package/vendor/fastctx/src/edit/replace.rs +1030 -0
- package/vendor/fastctx/src/edit_server.rs +53 -0
- package/vendor/fastctx/src/encoding/reference_v011.rs +587 -0
- package/vendor/fastctx/src/encoding/snapshot_pipeline.rs +1678 -0
- package/vendor/fastctx/src/encoding.rs +1118 -0
- package/vendor/fastctx/src/file_executor.rs +1151 -0
- package/vendor/fastctx/src/file_snapshot.rs +1491 -0
- package/vendor/fastctx/src/glob_filter.rs +98 -0
- package/vendor/fastctx/src/glob_tool.rs +653 -0
- package/vendor/fastctx/src/grep_sink.rs +1162 -0
- package/vendor/fastctx/src/grep_tool.rs +2449 -0
- package/vendor/fastctx/src/lib.rs +45 -0
- package/vendor/fastctx/src/main.rs +15 -0
- package/vendor/fastctx/src/model.rs +51 -0
- package/vendor/fastctx/src/model_guidance.rs +62 -0
- package/vendor/fastctx/src/operation.rs +356 -0
- package/vendor/fastctx/src/ordered_window.rs +1235 -0
- package/vendor/fastctx/src/os_environment.rs +414 -0
- package/vendor/fastctx/src/path_codec.rs +850 -0
- package/vendor/fastctx/src/paths.rs +244 -0
- package/vendor/fastctx/src/process_identity.rs +763 -0
- package/vendor/fastctx/src/process_policy.rs +74 -0
- package/vendor/fastctx/src/read_tool/batch.rs +496 -0
- package/vendor/fastctx/src/read_tool/hex_file.rs +141 -0
- package/vendor/fastctx/src/read_tool/image_file.rs +88 -0
- package/vendor/fastctx/src/read_tool/mod.rs +245 -0
- package/vendor/fastctx/src/read_tool/pdf.rs +470 -0
- package/vendor/fastctx/src/read_tool/pdf_disabled.rs +47 -0
- package/vendor/fastctx/src/read_tool/pdf_engine.rs +664 -0
- package/vendor/fastctx/src/read_tool/text_file.rs +351 -0
- package/vendor/fastctx/src/render_plan.rs +468 -0
- package/vendor/fastctx/src/runtime/activity.rs +159 -0
- package/vendor/fastctx/src/runtime/hosts.rs +99 -0
- package/vendor/fastctx/src/runtime/journal.rs +556 -0
- package/vendor/fastctx/src/runtime/local_ipc.rs +186 -0
- package/vendor/fastctx/src/runtime/mod.rs +746 -0
- package/vendor/fastctx/src/runtime/protocol.rs +296 -0
- package/vendor/fastctx/src/runtime/session.rs +536 -0
- package/vendor/fastctx/src/runtime/windows_process.rs +66 -0
- package/vendor/fastctx/src/search_parallelism.rs +106 -0
- package/vendor/fastctx/src/search_text.rs +227 -0
- package/vendor/fastctx/src/server.rs +359 -0
- package/vendor/fastctx/src/server_manifest.rs +468 -0
- package/vendor/fastctx/src/server_support.rs +826 -0
- package/vendor/fastctx/src/session.rs +629 -0
- package/vendor/fastctx/src/shell/apply_patch_hint.rs +41 -0
- package/vendor/fastctx/src/shell/bash.rs +263 -0
- package/vendor/fastctx/src/shell/buffer.rs +108 -0
- package/vendor/fastctx/src/shell/encoding.rs +403 -0
- package/vendor/fastctx/src/shell/foreground.rs +115 -0
- package/vendor/fastctx/src/shell/jobs/admission.rs +91 -0
- package/vendor/fastctx/src/shell/jobs/background.rs +146 -0
- package/vendor/fastctx/src/shell/jobs/host.rs +830 -0
- package/vendor/fastctx/src/shell/jobs/identity.rs +29 -0
- package/vendor/fastctx/src/shell/jobs/mod.rs +1513 -0
- package/vendor/fastctx/src/shell/jobs/model.rs +244 -0
- package/vendor/fastctx/src/shell/jobs/output_log.rs +1148 -0
- package/vendor/fastctx/src/shell/jobs/store.rs +1300 -0
- package/vendor/fastctx/src/shell/mod.rs +345 -0
- package/vendor/fastctx/src/shell/normalize.rs +389 -0
- package/vendor/fastctx/src/shell/output.rs +406 -0
- package/vendor/fastctx/src/shell/process.rs +493 -0
- package/vendor/fastctx/src/shell_server.rs +156 -0
- package/vendor/fastctx/src/skip_report.rs +83 -0
- package/vendor/fastctx/src/stdio_transport.rs +177 -0
- package/vendor/fastctx/src/tool_schema.rs +204 -0
- package/vendor/fastctx/src/traversal.rs +846 -0
- package/vendor/fastctx/third-party/pdfium-7763/LICENSE +9 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/abseil.txt +202 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/agg23.txt +14 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/fast_float.txt +27 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/freetype.txt +169 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/icu.txt +542 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/lcms.txt +27 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.ijg +260 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.md +135 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libopenjpeg.txt +32 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libpng.txt +134 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libtiff.txt +21 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/llvm-libc.txt +278 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/pdfium.txt +230 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/simdutf.txt +18 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/zlib.txt +29 -0
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
//! Cross-platform process identities and direct-parent lifecycle monitoring.
|
|
2
|
+
|
|
3
|
+
use serde::{Deserialize, Serialize};
|
|
4
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
5
|
+
use std::time::Duration;
|
|
6
|
+
|
|
7
|
+
const PARENT_POLL_INTERVAL: Duration = Duration::from_millis(500);
|
|
8
|
+
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
|
9
|
+
const PROCESS_INSPECTION_RETRY: Duration = Duration::from_secs(1);
|
|
10
|
+
|
|
11
|
+
/// PID plus an operating-system process creation token, used together to reject PID reuse.
|
|
12
|
+
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
|
13
|
+
pub(crate) struct ProcessIdentity {
|
|
14
|
+
pub(crate) pid: u32,
|
|
15
|
+
pub(crate) started: String,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#[cfg(target_os = "macos")]
|
|
19
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
20
|
+
pub(crate) enum MacosProcessIdentityProbe {
|
|
21
|
+
Running(ProcessIdentity),
|
|
22
|
+
Exited,
|
|
23
|
+
Unavailable,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#[cfg(target_os = "linux")]
|
|
27
|
+
enum LinuxProcessIdentityProbe {
|
|
28
|
+
Running(ProcessIdentity),
|
|
29
|
+
Exited,
|
|
30
|
+
Unavailable,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#[cfg(target_os = "macos")]
|
|
34
|
+
pub(crate) struct MacosProcessExitWatcher {
|
|
35
|
+
queue: std::os::fd::OwnedFd,
|
|
36
|
+
pid: u32,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#[cfg(target_os = "macos")]
|
|
40
|
+
impl MacosProcessExitWatcher {
|
|
41
|
+
/// Registers a kernel exit notification for the exact process identity.
|
|
42
|
+
///
|
|
43
|
+
/// `Ok(None)` means the identity exited or was reused while the watch was being established.
|
|
44
|
+
/// Inspection failures remain errors rather than being misreported as process exit.
|
|
45
|
+
pub(crate) fn register(identity: &ProcessIdentity) -> Result<Option<Self>, String> {
|
|
46
|
+
use std::os::fd::{AsRawFd, FromRawFd};
|
|
47
|
+
|
|
48
|
+
// SAFETY: kqueue creates a new descriptor owned by this function on success.
|
|
49
|
+
let raw_queue = unsafe { libc::kqueue() };
|
|
50
|
+
if raw_queue < 0 {
|
|
51
|
+
return Err(format!(
|
|
52
|
+
"Cannot create a process exit notification for PID {}: {}",
|
|
53
|
+
identity.pid,
|
|
54
|
+
std::io::Error::last_os_error()
|
|
55
|
+
));
|
|
56
|
+
}
|
|
57
|
+
// SAFETY: kqueue returned a newly owned descriptor.
|
|
58
|
+
let queue = unsafe { std::os::fd::OwnedFd::from_raw_fd(raw_queue) };
|
|
59
|
+
// Prevent a monitored parent's descriptor from leaking into shell work spawned by FastCtx.
|
|
60
|
+
// SAFETY: queue is a live descriptor and F_GETFD/F_SETFD do not consume it.
|
|
61
|
+
let descriptor_flags = unsafe { libc::fcntl(queue.as_raw_fd(), libc::F_GETFD) };
|
|
62
|
+
if descriptor_flags < 0
|
|
63
|
+
|| unsafe {
|
|
64
|
+
libc::fcntl(
|
|
65
|
+
queue.as_raw_fd(),
|
|
66
|
+
libc::F_SETFD,
|
|
67
|
+
descriptor_flags | libc::FD_CLOEXEC,
|
|
68
|
+
)
|
|
69
|
+
} < 0
|
|
70
|
+
{
|
|
71
|
+
return Err(format!(
|
|
72
|
+
"Cannot protect the process exit notification for PID {} from descriptor inheritance: {}",
|
|
73
|
+
identity.pid,
|
|
74
|
+
std::io::Error::last_os_error()
|
|
75
|
+
));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let change = libc::kevent {
|
|
79
|
+
ident: identity.pid as libc::uintptr_t,
|
|
80
|
+
filter: libc::EVFILT_PROC,
|
|
81
|
+
flags: libc::EV_ADD | libc::EV_ENABLE | libc::EV_ONESHOT,
|
|
82
|
+
fflags: libc::NOTE_EXIT,
|
|
83
|
+
data: 0,
|
|
84
|
+
udata: std::ptr::null_mut(),
|
|
85
|
+
};
|
|
86
|
+
// SAFETY: change is a valid one-element registration list; no event buffer is requested.
|
|
87
|
+
let registered = unsafe {
|
|
88
|
+
libc::kevent(
|
|
89
|
+
queue.as_raw_fd(),
|
|
90
|
+
&change,
|
|
91
|
+
1,
|
|
92
|
+
std::ptr::null_mut(),
|
|
93
|
+
0,
|
|
94
|
+
std::ptr::null(),
|
|
95
|
+
)
|
|
96
|
+
};
|
|
97
|
+
if registered < 0 {
|
|
98
|
+
let error = std::io::Error::last_os_error();
|
|
99
|
+
if error.raw_os_error() == Some(libc::ESRCH) {
|
|
100
|
+
return Ok(None);
|
|
101
|
+
}
|
|
102
|
+
return Err(format!(
|
|
103
|
+
"Cannot register a process exit notification for PID {}: {error}",
|
|
104
|
+
identity.pid
|
|
105
|
+
));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let watcher = Self {
|
|
109
|
+
queue,
|
|
110
|
+
pid: identity.pid,
|
|
111
|
+
};
|
|
112
|
+
if watcher.wait(Duration::ZERO)? {
|
|
113
|
+
return Ok(None);
|
|
114
|
+
}
|
|
115
|
+
match macos_process_identity_probe(identity.pid) {
|
|
116
|
+
MacosProcessIdentityProbe::Running(current) if current == *identity => {
|
|
117
|
+
Ok(Some(watcher))
|
|
118
|
+
}
|
|
119
|
+
MacosProcessIdentityProbe::Running(_) | MacosProcessIdentityProbe::Exited => Ok(None),
|
|
120
|
+
MacosProcessIdentityProbe::Unavailable => Err(format!(
|
|
121
|
+
"Cannot revalidate PID {} after registering its exit notification",
|
|
122
|
+
identity.pid
|
|
123
|
+
)),
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Waits for the registered process object to emit `NOTE_EXIT`.
|
|
128
|
+
pub(crate) fn wait(&self, timeout: Duration) -> Result<bool, String> {
|
|
129
|
+
use std::os::fd::AsRawFd;
|
|
130
|
+
|
|
131
|
+
let deadline = std::time::Instant::now() + timeout;
|
|
132
|
+
let mut remaining = timeout;
|
|
133
|
+
loop {
|
|
134
|
+
let timeout = libc::timespec {
|
|
135
|
+
tv_sec: libc::time_t::try_from(remaining.as_secs())
|
|
136
|
+
.map_err(|_| "The process exit timeout is too large".to_string())?,
|
|
137
|
+
tv_nsec: libc::c_long::from(remaining.subsec_nanos()),
|
|
138
|
+
};
|
|
139
|
+
// SAFETY: an all-zero kevent is valid as an output buffer.
|
|
140
|
+
let mut event = unsafe { std::mem::zeroed::<libc::kevent>() };
|
|
141
|
+
// SAFETY: event is a writable one-element event list and timeout remains live.
|
|
142
|
+
let ready = unsafe {
|
|
143
|
+
libc::kevent(
|
|
144
|
+
self.queue.as_raw_fd(),
|
|
145
|
+
std::ptr::null(),
|
|
146
|
+
0,
|
|
147
|
+
&mut event,
|
|
148
|
+
1,
|
|
149
|
+
&timeout,
|
|
150
|
+
)
|
|
151
|
+
};
|
|
152
|
+
if ready > 0 {
|
|
153
|
+
let flags = event.flags;
|
|
154
|
+
let data = event.data;
|
|
155
|
+
if flags & libc::EV_ERROR != 0 {
|
|
156
|
+
let error = i32::try_from(data)
|
|
157
|
+
.ok()
|
|
158
|
+
.filter(|code| *code > 0)
|
|
159
|
+
.map(std::io::Error::from_raw_os_error);
|
|
160
|
+
return Err(match error {
|
|
161
|
+
Some(error) => format!(
|
|
162
|
+
"The process exit notification for PID {} failed: {error}",
|
|
163
|
+
self.pid
|
|
164
|
+
),
|
|
165
|
+
None => {
|
|
166
|
+
format!("The process exit notification for PID {} failed", self.pid)
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
let ident = event.ident;
|
|
171
|
+
let filter = event.filter;
|
|
172
|
+
let fflags = event.fflags;
|
|
173
|
+
if ident == self.pid as libc::uintptr_t
|
|
174
|
+
&& filter == libc::EVFILT_PROC
|
|
175
|
+
&& fflags & libc::NOTE_EXIT != 0
|
|
176
|
+
{
|
|
177
|
+
return Ok(true);
|
|
178
|
+
}
|
|
179
|
+
return Err(format!(
|
|
180
|
+
"The process exit notification for PID {} returned an unexpected event",
|
|
181
|
+
self.pid
|
|
182
|
+
));
|
|
183
|
+
}
|
|
184
|
+
if ready == 0 {
|
|
185
|
+
return Ok(false);
|
|
186
|
+
}
|
|
187
|
+
let error = std::io::Error::last_os_error();
|
|
188
|
+
if error.kind() != std::io::ErrorKind::Interrupted {
|
|
189
|
+
return Err(format!(
|
|
190
|
+
"Cannot wait for the process exit notification for PID {}: {error}",
|
|
191
|
+
self.pid
|
|
192
|
+
));
|
|
193
|
+
}
|
|
194
|
+
let now = std::time::Instant::now();
|
|
195
|
+
if now >= deadline {
|
|
196
|
+
return Ok(false);
|
|
197
|
+
}
|
|
198
|
+
remaining = deadline.saturating_duration_since(now);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/// Returns a process identity only while the PID can be inspected.
|
|
204
|
+
pub(crate) fn process_identity(pid: u32) -> Option<ProcessIdentity> {
|
|
205
|
+
#[cfg(target_os = "linux")]
|
|
206
|
+
{
|
|
207
|
+
match linux_process_identity_probe(pid) {
|
|
208
|
+
LinuxProcessIdentityProbe::Running(identity) => Some(identity),
|
|
209
|
+
LinuxProcessIdentityProbe::Exited | LinuxProcessIdentityProbe::Unavailable => None,
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
#[cfg(target_os = "macos")]
|
|
213
|
+
{
|
|
214
|
+
match macos_process_identity_probe(pid) {
|
|
215
|
+
MacosProcessIdentityProbe::Running(identity) => Some(identity),
|
|
216
|
+
MacosProcessIdentityProbe::Exited | MacosProcessIdentityProbe::Unavailable => None,
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
|
220
|
+
{
|
|
221
|
+
process_start_token(pid).map(|started| ProcessIdentity { pid, started })
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/// Requires both PID and creation token to match, preventing recycled PIDs from looking alive.
|
|
226
|
+
pub(crate) fn identity_is_alive(identity: &ProcessIdentity) -> bool {
|
|
227
|
+
#[cfg(target_os = "macos")]
|
|
228
|
+
{
|
|
229
|
+
let deadline = std::time::Instant::now() + PROCESS_INSPECTION_RETRY;
|
|
230
|
+
loop {
|
|
231
|
+
match macos_process_identity_probe(identity.pid) {
|
|
232
|
+
MacosProcessIdentityProbe::Running(current) => return current == *identity,
|
|
233
|
+
MacosProcessIdentityProbe::Exited => return false,
|
|
234
|
+
MacosProcessIdentityProbe::Unavailable if std::time::Instant::now() < deadline => {
|
|
235
|
+
std::thread::sleep(Duration::from_millis(25));
|
|
236
|
+
}
|
|
237
|
+
MacosProcessIdentityProbe::Unavailable => return false,
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
#[cfg(not(target_os = "macos"))]
|
|
242
|
+
{
|
|
243
|
+
process_identity(identity.pid).as_ref() == Some(identity)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/// Captures the direct parent at startup unless the documented escape hatch is enabled.
|
|
248
|
+
///
|
|
249
|
+
/// `Ok(Some(None))` means that the direct parent disappeared during capture and callers should
|
|
250
|
+
/// shut down immediately. An unsupported platform returns `Ok(None)`.
|
|
251
|
+
pub(crate) fn parent_identity_from_environment() -> Result<Option<Option<ProcessIdentity>>, String>
|
|
252
|
+
{
|
|
253
|
+
if std::env::var("FASTCTX_NO_PARENT_WATCH").ok().as_deref() == Some("1") {
|
|
254
|
+
return Ok(None);
|
|
255
|
+
}
|
|
256
|
+
let Some(pid) = direct_parent_pid()? else {
|
|
257
|
+
return Ok(None);
|
|
258
|
+
};
|
|
259
|
+
#[cfg(target_os = "linux")]
|
|
260
|
+
{
|
|
261
|
+
let deadline = std::time::Instant::now() + PROCESS_INSPECTION_RETRY;
|
|
262
|
+
loop {
|
|
263
|
+
match linux_process_identity_probe(pid) {
|
|
264
|
+
LinuxProcessIdentityProbe::Running(identity) => {
|
|
265
|
+
return Ok(Some(Some(identity)));
|
|
266
|
+
}
|
|
267
|
+
LinuxProcessIdentityProbe::Exited => return Ok(Some(None)),
|
|
268
|
+
LinuxProcessIdentityProbe::Unavailable if std::time::Instant::now() < deadline => {
|
|
269
|
+
std::thread::sleep(Duration::from_millis(25));
|
|
270
|
+
}
|
|
271
|
+
LinuxProcessIdentityProbe::Unavailable => {
|
|
272
|
+
return Err(format!(
|
|
273
|
+
"Cannot capture the MCP server parent process identity for PID {pid}: /proc inspection remained unavailable"
|
|
274
|
+
));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
#[cfg(target_os = "macos")]
|
|
280
|
+
{
|
|
281
|
+
let deadline = std::time::Instant::now() + PROCESS_INSPECTION_RETRY;
|
|
282
|
+
loop {
|
|
283
|
+
match macos_process_identity_probe(pid) {
|
|
284
|
+
MacosProcessIdentityProbe::Running(identity) => {
|
|
285
|
+
return Ok(Some(Some(identity)));
|
|
286
|
+
}
|
|
287
|
+
MacosProcessIdentityProbe::Exited => return Ok(Some(None)),
|
|
288
|
+
MacosProcessIdentityProbe::Unavailable if std::time::Instant::now() < deadline => {
|
|
289
|
+
std::thread::sleep(Duration::from_millis(25));
|
|
290
|
+
}
|
|
291
|
+
MacosProcessIdentityProbe::Unavailable => {
|
|
292
|
+
return Err(format!(
|
|
293
|
+
"Cannot capture the MCP server parent process identity for PID {pid}: libproc inspection remained unavailable"
|
|
294
|
+
));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
|
300
|
+
{
|
|
301
|
+
Ok(Some(process_identity(pid)))
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/// Blocks until the identity exits or the caller cancels the monitor.
|
|
306
|
+
///
|
|
307
|
+
/// Returns `true` only when the process exited. Cancellation is checked at most every 500 ms;
|
|
308
|
+
/// Windows, Linux, and macOS use kernel notifications bound to the process object where available.
|
|
309
|
+
pub(crate) fn wait_for_identity_exit_until(
|
|
310
|
+
identity: &ProcessIdentity,
|
|
311
|
+
cancelled: &AtomicBool,
|
|
312
|
+
) -> bool {
|
|
313
|
+
#[cfg(windows)]
|
|
314
|
+
{
|
|
315
|
+
wait_for_identity_exit_windows(identity, cancelled)
|
|
316
|
+
}
|
|
317
|
+
#[cfg(target_os = "macos")]
|
|
318
|
+
{
|
|
319
|
+
match MacosProcessExitWatcher::register(identity) {
|
|
320
|
+
Ok(None) => return true,
|
|
321
|
+
Ok(Some(watcher)) => {
|
|
322
|
+
while !cancelled.load(Ordering::Acquire) {
|
|
323
|
+
match watcher.wait(PARENT_POLL_INTERVAL) {
|
|
324
|
+
Ok(true) => return true,
|
|
325
|
+
Ok(false) => {}
|
|
326
|
+
// A broken notification is not evidence of exit. Fall back to the
|
|
327
|
+
// conservative identity probe instead of orphaning or killing a live host.
|
|
328
|
+
Err(_) => break,
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if cancelled.load(Ordering::Acquire) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// kqueue can be denied for a process the caller may still inspect. The fallback keeps
|
|
336
|
+
// the old conservative behavior without treating watcher setup failure as parent exit.
|
|
337
|
+
Err(_) => {}
|
|
338
|
+
}
|
|
339
|
+
while !cancelled.load(Ordering::Acquire) {
|
|
340
|
+
match macos_process_identity_probe(identity.pid) {
|
|
341
|
+
MacosProcessIdentityProbe::Running(current) if current == *identity => {
|
|
342
|
+
std::thread::sleep(PARENT_POLL_INTERVAL);
|
|
343
|
+
}
|
|
344
|
+
MacosProcessIdentityProbe::Running(_) | MacosProcessIdentityProbe::Exited => {
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
MacosProcessIdentityProbe::Unavailable => {
|
|
348
|
+
// A failed inspection is not evidence that the parent exited.
|
|
349
|
+
std::thread::sleep(PARENT_POLL_INTERVAL);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
false
|
|
354
|
+
}
|
|
355
|
+
#[cfg(target_os = "linux")]
|
|
356
|
+
{
|
|
357
|
+
wait_for_identity_exit_linux(identity, cancelled)
|
|
358
|
+
}
|
|
359
|
+
#[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
|
|
360
|
+
{
|
|
361
|
+
while !cancelled.load(Ordering::Acquire) && identity_is_alive(identity) {
|
|
362
|
+
std::thread::sleep(PARENT_POLL_INTERVAL);
|
|
363
|
+
}
|
|
364
|
+
!cancelled.load(Ordering::Acquire)
|
|
365
|
+
}
|
|
366
|
+
#[cfg(not(any(unix, windows)))]
|
|
367
|
+
{
|
|
368
|
+
let _ = (identity, cancelled);
|
|
369
|
+
false
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
#[cfg(unix)]
|
|
374
|
+
fn direct_parent_pid() -> Result<Option<u32>, String> {
|
|
375
|
+
// SAFETY: getppid has no preconditions and returns the caller's current direct parent PID.
|
|
376
|
+
let pid = unsafe { libc::getppid() };
|
|
377
|
+
Ok(u32::try_from(pid).ok().filter(|pid| *pid > 0))
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
#[cfg(windows)]
|
|
381
|
+
fn direct_parent_pid() -> Result<Option<u32>, String> {
|
|
382
|
+
use windows_sys::Win32::Foundation::{CloseHandle, INVALID_HANDLE_VALUE};
|
|
383
|
+
use windows_sys::Win32::System::Diagnostics::ToolHelp::{
|
|
384
|
+
CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
|
|
385
|
+
TH32CS_SNAPPROCESS,
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// SAFETY: the snapshot is closed on every path after successful creation.
|
|
389
|
+
let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) };
|
|
390
|
+
if snapshot == INVALID_HANDLE_VALUE {
|
|
391
|
+
return Err(format!(
|
|
392
|
+
"Cannot capture the MCP server parent process: {}",
|
|
393
|
+
std::io::Error::last_os_error()
|
|
394
|
+
));
|
|
395
|
+
}
|
|
396
|
+
let current = std::process::id();
|
|
397
|
+
let mut entry = PROCESSENTRY32W {
|
|
398
|
+
dwSize: std::mem::size_of::<PROCESSENTRY32W>() as u32,
|
|
399
|
+
..Default::default()
|
|
400
|
+
};
|
|
401
|
+
// SAFETY: entry has the documented size and remains valid during enumeration.
|
|
402
|
+
let mut has_entry = unsafe { Process32FirstW(snapshot, &mut entry) };
|
|
403
|
+
let mut parent = None;
|
|
404
|
+
while has_entry != 0 {
|
|
405
|
+
if entry.th32ProcessID == current {
|
|
406
|
+
parent = (entry.th32ParentProcessID > 0).then_some(entry.th32ParentProcessID);
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
// SAFETY: snapshot and entry remain valid for the next enumeration step.
|
|
410
|
+
has_entry = unsafe { Process32NextW(snapshot, &mut entry) };
|
|
411
|
+
}
|
|
412
|
+
// SAFETY: this function owns the snapshot handle.
|
|
413
|
+
unsafe {
|
|
414
|
+
CloseHandle(snapshot);
|
|
415
|
+
}
|
|
416
|
+
Ok(parent)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
#[cfg(not(any(unix, windows)))]
|
|
420
|
+
fn direct_parent_pid() -> Result<Option<u32>, String> {
|
|
421
|
+
Ok(None)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
#[cfg(target_os = "linux")]
|
|
425
|
+
fn linux_process_identity_probe(pid: u32) -> LinuxProcessIdentityProbe {
|
|
426
|
+
let source = match std::fs::read_to_string(format!("/proc/{pid}/stat")) {
|
|
427
|
+
Ok(source) => source,
|
|
428
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
|
|
429
|
+
return LinuxProcessIdentityProbe::Exited;
|
|
430
|
+
}
|
|
431
|
+
Err(_) => return LinuxProcessIdentityProbe::Unavailable,
|
|
432
|
+
};
|
|
433
|
+
let Some((_, suffix)) = source.rsplit_once(')') else {
|
|
434
|
+
return LinuxProcessIdentityProbe::Unavailable;
|
|
435
|
+
};
|
|
436
|
+
let fields = suffix.split_whitespace().collect::<Vec<_>>();
|
|
437
|
+
let Some(state) = fields.first() else {
|
|
438
|
+
return LinuxProcessIdentityProbe::Unavailable;
|
|
439
|
+
};
|
|
440
|
+
if matches!(*state, "Z" | "X") {
|
|
441
|
+
return LinuxProcessIdentityProbe::Exited;
|
|
442
|
+
}
|
|
443
|
+
let Some(started) = fields.get(19) else {
|
|
444
|
+
return LinuxProcessIdentityProbe::Unavailable;
|
|
445
|
+
};
|
|
446
|
+
LinuxProcessIdentityProbe::Running(ProcessIdentity {
|
|
447
|
+
pid,
|
|
448
|
+
started: (*started).to_string(),
|
|
449
|
+
})
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
#[cfg(target_os = "linux")]
|
|
453
|
+
fn wait_for_identity_exit_linux(identity: &ProcessIdentity, cancelled: &AtomicBool) -> bool {
|
|
454
|
+
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
|
|
455
|
+
|
|
456
|
+
let Ok(pid) = i32::try_from(identity.pid) else {
|
|
457
|
+
return false;
|
|
458
|
+
};
|
|
459
|
+
// SAFETY: pidfd_open receives a positive PID and no flags; a successful descriptor is owned
|
|
460
|
+
// below and remains bound to that process object across PID reuse.
|
|
461
|
+
let raw_pidfd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) } as i32;
|
|
462
|
+
if raw_pidfd < 0 {
|
|
463
|
+
if std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH) {
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
return wait_for_identity_exit_linux_fallback(identity, cancelled);
|
|
467
|
+
}
|
|
468
|
+
// SAFETY: pidfd_open returned a newly owned descriptor.
|
|
469
|
+
let pidfd = unsafe { OwnedFd::from_raw_fd(raw_pidfd) };
|
|
470
|
+
match linux_process_identity_probe(identity.pid) {
|
|
471
|
+
LinuxProcessIdentityProbe::Running(current) if current == *identity => {}
|
|
472
|
+
LinuxProcessIdentityProbe::Running(_) | LinuxProcessIdentityProbe::Exited => return true,
|
|
473
|
+
LinuxProcessIdentityProbe::Unavailable => {
|
|
474
|
+
return wait_for_identity_exit_linux_fallback(identity, cancelled);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
while !cancelled.load(Ordering::Acquire) {
|
|
479
|
+
let mut descriptor = libc::pollfd {
|
|
480
|
+
fd: pidfd.as_raw_fd(),
|
|
481
|
+
events: libc::POLLIN,
|
|
482
|
+
revents: 0,
|
|
483
|
+
};
|
|
484
|
+
// SAFETY: descriptor is a writable one-element pollfd array for the duration of poll.
|
|
485
|
+
let ready = unsafe {
|
|
486
|
+
libc::poll(
|
|
487
|
+
&mut descriptor,
|
|
488
|
+
1,
|
|
489
|
+
i32::try_from(PARENT_POLL_INTERVAL.as_millis())
|
|
490
|
+
.expect("the parent poll interval fits in a poll timeout"),
|
|
491
|
+
)
|
|
492
|
+
};
|
|
493
|
+
if ready > 0 {
|
|
494
|
+
if descriptor.revents & (libc::POLLIN | libc::POLLHUP | libc::POLLERR) != 0 {
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
if ready < 0 && std::io::Error::last_os_error().kind() != std::io::ErrorKind::Interrupted {
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
false
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
#[cfg(target_os = "linux")]
|
|
507
|
+
fn wait_for_identity_exit_linux_fallback(
|
|
508
|
+
identity: &ProcessIdentity,
|
|
509
|
+
cancelled: &AtomicBool,
|
|
510
|
+
) -> bool {
|
|
511
|
+
while !cancelled.load(Ordering::Acquire) {
|
|
512
|
+
match linux_process_identity_probe(identity.pid) {
|
|
513
|
+
LinuxProcessIdentityProbe::Running(current) if current == *identity => {
|
|
514
|
+
std::thread::sleep(PARENT_POLL_INTERVAL);
|
|
515
|
+
}
|
|
516
|
+
LinuxProcessIdentityProbe::Running(_) | LinuxProcessIdentityProbe::Exited => {
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
LinuxProcessIdentityProbe::Unavailable => {
|
|
520
|
+
// Inspection failure is not proof that the parent exited.
|
|
521
|
+
std::thread::sleep(PARENT_POLL_INTERVAL);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
false
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
#[cfg(target_os = "macos")]
|
|
529
|
+
#[link(name = "proc")]
|
|
530
|
+
unsafe extern "C" {
|
|
531
|
+
#[link_name = "proc_pidinfo"]
|
|
532
|
+
fn macos_proc_pidinfo(
|
|
533
|
+
pid: libc::c_int,
|
|
534
|
+
flavor: libc::c_int,
|
|
535
|
+
arg: u64,
|
|
536
|
+
buffer: *mut libc::c_void,
|
|
537
|
+
buffersize: libc::c_int,
|
|
538
|
+
) -> libc::c_int;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
#[cfg(target_os = "macos")]
|
|
542
|
+
pub(crate) fn macos_process_identity_probe(pid: u32) -> MacosProcessIdentityProbe {
|
|
543
|
+
let Ok(raw_pid) = i32::try_from(pid) else {
|
|
544
|
+
return MacosProcessIdentityProbe::Unavailable;
|
|
545
|
+
};
|
|
546
|
+
// SAFETY: proc_bsdinfo is a plain C data struct whose all-zero bit pattern is valid.
|
|
547
|
+
let mut info = unsafe { std::mem::zeroed::<libc::proc_bsdinfo>() };
|
|
548
|
+
let size = std::mem::size_of::<libc::proc_bsdinfo>();
|
|
549
|
+
// SAFETY: __error returns the calling thread's writable errno slot on macOS.
|
|
550
|
+
unsafe {
|
|
551
|
+
*libc::__error() = 0;
|
|
552
|
+
}
|
|
553
|
+
// SAFETY: info points to a writable proc_bsdinfo buffer of the exact size passed here.
|
|
554
|
+
let read = unsafe {
|
|
555
|
+
macos_proc_pidinfo(
|
|
556
|
+
raw_pid,
|
|
557
|
+
libc::PROC_PIDTBSDINFO,
|
|
558
|
+
0,
|
|
559
|
+
std::ptr::addr_of_mut!(info).cast(),
|
|
560
|
+
i32::try_from(size).expect("proc_bsdinfo fits in a C int"),
|
|
561
|
+
)
|
|
562
|
+
};
|
|
563
|
+
if usize::try_from(read).ok() == Some(size) {
|
|
564
|
+
if info.pbi_status == libc::SZOMB {
|
|
565
|
+
return MacosProcessIdentityProbe::Exited;
|
|
566
|
+
}
|
|
567
|
+
return MacosProcessIdentityProbe::Running(ProcessIdentity {
|
|
568
|
+
pid,
|
|
569
|
+
started: format!("{}:{}", info.pbi_start_tvsec, info.pbi_start_tvusec),
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// proc_pidinfo can fail transiently. Confirm that the PID is truly absent before calling it
|
|
574
|
+
// exited; a live but temporarily uninspectable parent must never shut down its server.
|
|
575
|
+
// SAFETY: kill with signal 0 performs an existence/permission probe without sending a signal.
|
|
576
|
+
unsafe {
|
|
577
|
+
*libc::__error() = 0;
|
|
578
|
+
}
|
|
579
|
+
let exists = unsafe { libc::kill(raw_pid, 0) };
|
|
580
|
+
if exists == 0 {
|
|
581
|
+
return MacosProcessIdentityProbe::Unavailable;
|
|
582
|
+
}
|
|
583
|
+
if std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH) {
|
|
584
|
+
MacosProcessIdentityProbe::Exited
|
|
585
|
+
} else {
|
|
586
|
+
MacosProcessIdentityProbe::Unavailable
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
#[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
|
|
591
|
+
fn process_start_token(pid: u32) -> Option<String> {
|
|
592
|
+
let output = std::process::Command::new("ps")
|
|
593
|
+
.args(["-p", &pid.to_string(), "-o", "lstart="])
|
|
594
|
+
.env("LC_ALL", "C")
|
|
595
|
+
.output()
|
|
596
|
+
.ok()?;
|
|
597
|
+
if !output.status.success() {
|
|
598
|
+
return None;
|
|
599
|
+
}
|
|
600
|
+
let token = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
601
|
+
(!token.is_empty()).then_some(token)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
#[cfg(windows)]
|
|
605
|
+
fn process_start_token(pid: u32) -> Option<String> {
|
|
606
|
+
use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION};
|
|
607
|
+
|
|
608
|
+
// SAFETY: the returned process handle is closed by process_start_token_from_handle.
|
|
609
|
+
let process = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
|
|
610
|
+
process_start_token_from_handle(process)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
#[cfg(windows)]
|
|
614
|
+
fn process_start_token_from_handle(
|
|
615
|
+
process: windows_sys::Win32::Foundation::HANDLE,
|
|
616
|
+
) -> Option<String> {
|
|
617
|
+
use windows_sys::Win32::Foundation::{CloseHandle, FILETIME};
|
|
618
|
+
use windows_sys::Win32::System::Threading::GetProcessTimes;
|
|
619
|
+
|
|
620
|
+
if process.is_null() {
|
|
621
|
+
return None;
|
|
622
|
+
}
|
|
623
|
+
let mut created = FILETIME {
|
|
624
|
+
dwLowDateTime: 0,
|
|
625
|
+
dwHighDateTime: 0,
|
|
626
|
+
};
|
|
627
|
+
let mut exited = created;
|
|
628
|
+
let mut kernel = created;
|
|
629
|
+
let mut user = created;
|
|
630
|
+
// SAFETY: process is live and all FILETIME output buffers are writable.
|
|
631
|
+
let success =
|
|
632
|
+
unsafe { GetProcessTimes(process, &mut created, &mut exited, &mut kernel, &mut user) };
|
|
633
|
+
// SAFETY: this helper owns the process handle supplied by process_start_token.
|
|
634
|
+
unsafe {
|
|
635
|
+
CloseHandle(process);
|
|
636
|
+
}
|
|
637
|
+
(success != 0).then(|| {
|
|
638
|
+
let ticks = (u64::from(created.dwHighDateTime) << 32) | u64::from(created.dwLowDateTime);
|
|
639
|
+
ticks.to_string()
|
|
640
|
+
})
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
#[cfg(windows)]
|
|
644
|
+
fn wait_for_identity_exit_windows(identity: &ProcessIdentity, cancelled: &AtomicBool) -> bool {
|
|
645
|
+
use windows_sys::Win32::Foundation::{CloseHandle, FILETIME, WAIT_OBJECT_0, WAIT_TIMEOUT};
|
|
646
|
+
use windows_sys::Win32::Storage::FileSystem::SYNCHRONIZE;
|
|
647
|
+
use windows_sys::Win32::System::Threading::WaitForSingleObject;
|
|
648
|
+
use windows_sys::Win32::System::Threading::{
|
|
649
|
+
GetProcessTimes, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
// SAFETY: the handle is closed below after identity verification and waiting.
|
|
653
|
+
let process = unsafe {
|
|
654
|
+
OpenProcess(
|
|
655
|
+
SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION,
|
|
656
|
+
0,
|
|
657
|
+
identity.pid,
|
|
658
|
+
)
|
|
659
|
+
};
|
|
660
|
+
if process.is_null() {
|
|
661
|
+
// Inspection failure is not proof of parent death; disabling this watch avoids
|
|
662
|
+
// terminating a live host session on a transient access failure.
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
let mut created = FILETIME {
|
|
666
|
+
dwLowDateTime: 0,
|
|
667
|
+
dwHighDateTime: 0,
|
|
668
|
+
};
|
|
669
|
+
let mut exited = created;
|
|
670
|
+
let mut kernel = created;
|
|
671
|
+
let mut user = created;
|
|
672
|
+
// SAFETY: process is live and all FILETIME output buffers are writable.
|
|
673
|
+
let inspected =
|
|
674
|
+
unsafe { GetProcessTimes(process, &mut created, &mut exited, &mut kernel, &mut user) };
|
|
675
|
+
let ticks = (u64::from(created.dwHighDateTime) << 32) | u64::from(created.dwLowDateTime);
|
|
676
|
+
let matches = inspected != 0 && ticks.to_string() == identity.started;
|
|
677
|
+
let mut exited = !matches;
|
|
678
|
+
while matches && !cancelled.load(Ordering::Acquire) {
|
|
679
|
+
// SAFETY: SYNCHRONIZE grants waiting on this live process handle.
|
|
680
|
+
let wait = unsafe {
|
|
681
|
+
WaitForSingleObject(
|
|
682
|
+
process,
|
|
683
|
+
u32::try_from(PARENT_POLL_INTERVAL.as_millis())
|
|
684
|
+
.expect("the parent poll interval fits in a Windows timeout"),
|
|
685
|
+
)
|
|
686
|
+
};
|
|
687
|
+
match wait {
|
|
688
|
+
WAIT_OBJECT_0 => {
|
|
689
|
+
exited = true;
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
WAIT_TIMEOUT => {}
|
|
693
|
+
_ => {
|
|
694
|
+
// A failed wait is not evidence that the parent exited.
|
|
695
|
+
exited = false;
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
// SAFETY: this function owns the process handle.
|
|
701
|
+
unsafe {
|
|
702
|
+
CloseHandle(process);
|
|
703
|
+
}
|
|
704
|
+
exited
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
#[cfg(not(any(unix, windows)))]
|
|
708
|
+
fn process_start_token(_pid: u32) -> Option<String> {
|
|
709
|
+
None
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
#[cfg(test)]
|
|
713
|
+
mod tests {
|
|
714
|
+
use super::{identity_is_alive, process_identity, wait_for_identity_exit_until};
|
|
715
|
+
use std::sync::atomic::AtomicBool;
|
|
716
|
+
|
|
717
|
+
#[test]
|
|
718
|
+
fn current_process_identity_is_stable_and_rejects_a_different_creation_token() {
|
|
719
|
+
let identity = process_identity(std::process::id())
|
|
720
|
+
.expect("the current process creation token should be inspectable");
|
|
721
|
+
assert!(identity_is_alive(&identity));
|
|
722
|
+
|
|
723
|
+
let mut recycled = identity;
|
|
724
|
+
recycled.started.push_str("-different-process");
|
|
725
|
+
assert!(!identity_is_alive(&recycled));
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
#[test]
|
|
729
|
+
fn process_handle_wait_observes_the_exact_process_exit() {
|
|
730
|
+
#[cfg(windows)]
|
|
731
|
+
let mut child = std::process::Command::new(
|
|
732
|
+
std::env::var_os("ComSpec").unwrap_or_else(|| "cmd.exe".into()),
|
|
733
|
+
)
|
|
734
|
+
.args(["/D", "/Q", "/C", "ping -n 2 127.0.0.1 >NUL"])
|
|
735
|
+
.spawn()
|
|
736
|
+
.unwrap();
|
|
737
|
+
#[cfg(unix)]
|
|
738
|
+
let mut child = std::process::Command::new("/bin/sh")
|
|
739
|
+
.args(["-c", "sleep 0.2"])
|
|
740
|
+
.spawn()
|
|
741
|
+
.unwrap();
|
|
742
|
+
let identity = process_identity(child.id()).unwrap();
|
|
743
|
+
let cancelled = AtomicBool::new(false);
|
|
744
|
+
assert!(wait_for_identity_exit_until(&identity, &cancelled));
|
|
745
|
+
assert!(child.wait().unwrap().success());
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
#[cfg(target_os = "macos")]
|
|
749
|
+
#[test]
|
|
750
|
+
fn process_exit_watch_observes_a_killed_child_before_it_is_reaped() {
|
|
751
|
+
let mut child = std::process::Command::new("/bin/sh")
|
|
752
|
+
.args(["-c", "sleep 30"])
|
|
753
|
+
.spawn()
|
|
754
|
+
.unwrap();
|
|
755
|
+
let identity = process_identity(child.id()).unwrap();
|
|
756
|
+
child.kill().unwrap();
|
|
757
|
+
std::thread::sleep(std::time::Duration::from_millis(100));
|
|
758
|
+
|
|
759
|
+
let cancelled = AtomicBool::new(false);
|
|
760
|
+
assert!(wait_for_identity_exit_until(&identity, &cancelled));
|
|
761
|
+
assert!(!child.wait().unwrap().success());
|
|
762
|
+
}
|
|
763
|
+
}
|