@rehearsal-labs/retest 0.2.0 → 0.3.0
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/README.md +6 -5
- package/dist/api/source-location.d.ts.map +1 -1
- package/dist/api/source-location.js +3 -1
- package/dist/browser/builds.d.ts +35 -6
- package/dist/browser/builds.d.ts.map +1 -1
- package/dist/browser/builds.js +115 -16
- package/dist/browser/cdp/transport.d.ts.map +1 -1
- package/dist/browser/cdp/transport.js +4 -4
- package/dist/browser/chromium-process.d.ts +23 -0
- package/dist/browser/chromium-process.d.ts.map +1 -1
- package/dist/browser/chromium-process.js +78 -13
- package/dist/browser/electron.d.ts +1 -1
- package/dist/browser/electron.d.ts.map +1 -1
- package/dist/browser/electron.js +6 -3
- package/dist/browser/executables.d.ts +16 -1
- package/dist/browser/executables.d.ts.map +1 -1
- package/dist/browser/executables.js +50 -18
- package/dist/browser/firefox/accessible-names.d.ts +5 -4
- package/dist/browser/firefox/accessible-names.d.ts.map +1 -1
- package/dist/browser/firefox/accessible-names.js +11 -6
- package/dist/browser/firefox/bridge.js +3 -2
- package/dist/browser/firefox/executable.d.ts +14 -14
- package/dist/browser/firefox/executable.d.ts.map +1 -1
- package/dist/browser/firefox/executable.js +29 -21
- package/dist/browser/firefox/launch.js +1 -1
- package/dist/browser/firefox/navigation.d.ts +5 -2
- package/dist/browser/firefox/navigation.d.ts.map +1 -1
- package/dist/browser/firefox/navigation.js +48 -14
- package/dist/browser/firefox/orphans.d.ts.map +1 -1
- package/dist/browser/firefox/orphans.js +34 -2
- package/dist/browser/firefox/page.d.ts.map +1 -1
- package/dist/browser/firefox/page.js +113 -19
- package/dist/browser/firefox/process.d.ts +20 -1
- package/dist/browser/firefox/process.d.ts.map +1 -1
- package/dist/browser/firefox/process.js +124 -22
- package/dist/browser/firefox/profile.d.ts +5 -0
- package/dist/browser/firefox/profile.d.ts.map +1 -1
- package/dist/browser/firefox/profile.js +17 -1
- package/dist/browser/firefox/storage.d.ts.map +1 -1
- package/dist/browser/firefox/storage.js +4 -2
- package/dist/browser/firefox/subscriptions.d.ts +26 -0
- package/dist/browser/firefox/subscriptions.d.ts.map +1 -0
- package/dist/browser/firefox/subscriptions.js +10 -0
- package/dist/browser/launch.d.ts +6 -0
- package/dist/browser/launch.d.ts.map +1 -1
- package/dist/browser/launch.js +14 -1
- package/dist/cli/commands/doctor.js +3 -3
- package/dist/cli/commands/inspect.js +3 -2
- package/dist/cli/commands/install.d.ts.map +1 -1
- package/dist/cli/commands/install.js +26 -13
- package/dist/cli/commands/licences.d.ts +22 -0
- package/dist/cli/commands/licences.d.ts.map +1 -1
- package/dist/cli/commands/licences.js +62 -20
- package/dist/cli/commands/run.js +2 -2
- package/dist/cli/doctor/checks.js +4 -1
- package/dist/cli/inspect/diagnostics.js +3 -3
- package/dist/cli/inspect/test-timeline.js +10 -10
- package/dist/cli/install/doctor-rows.d.ts.map +1 -1
- package/dist/cli/install/doctor-rows.js +5 -4
- package/dist/cli/install/file-changes.d.ts +44 -0
- package/dist/cli/install/file-changes.d.ts.map +1 -0
- package/dist/cli/install/file-changes.js +81 -0
- package/dist/cli/install/install-archive.d.ts +3 -1
- package/dist/cli/install/install-archive.d.ts.map +1 -1
- package/dist/cli/install/install-archive.js +80 -33
- package/dist/cli/install/lock.d.ts.map +1 -1
- package/dist/cli/install/lock.js +108 -21
- package/dist/cli/install/media-doctor.d.ts.map +1 -1
- package/dist/cli/install/media-doctor.js +2 -1
- package/dist/cli/install/media-install.d.ts.map +1 -1
- package/dist/cli/install/media-install.js +38 -19
- package/dist/cli/install/media-pins.d.ts +7 -1
- package/dist/cli/install/media-pins.d.ts.map +1 -1
- package/dist/cli/install/media-pins.js +23 -12
- package/dist/cli/install/media-record.d.ts.map +1 -1
- package/dist/cli/install/media-record.js +10 -9
- package/dist/cli/install/media-report.d.ts +3 -1
- package/dist/cli/install/media-report.d.ts.map +1 -1
- package/dist/cli/install/media-report.js +7 -5
- package/dist/cli/install/media-tools.d.ts.map +1 -1
- package/dist/cli/install/media-tools.js +6 -4
- package/dist/cli/install/process-start.d.ts +3 -2
- package/dist/cli/install/process-start.d.ts.map +1 -1
- package/dist/cli/install/process-start.js +4 -0
- package/dist/cli/install/report.d.ts.map +1 -1
- package/dist/cli/install/report.js +11 -7
- package/dist/cli/install/supervisor-install.d.ts +33 -0
- package/dist/cli/install/supervisor-install.d.ts.map +1 -0
- package/dist/cli/install/supervisor-install.js +158 -0
- package/dist/cli/install/supervisor-locate.d.ts +47 -0
- package/dist/cli/install/supervisor-locate.d.ts.map +1 -0
- package/dist/cli/install/supervisor-locate.js +113 -0
- package/dist/cli/install/supervisor-pins.d.ts +21 -0
- package/dist/cli/install/supervisor-pins.d.ts.map +1 -0
- package/dist/cli/install/supervisor-pins.js +28 -0
- package/dist/cli/install/supervisor-record.d.ts +31 -0
- package/dist/cli/install/supervisor-record.d.ts.map +1 -0
- package/dist/cli/install/supervisor-record.js +104 -0
- package/dist/cli/install/supervisor-report.d.ts +11 -0
- package/dist/cli/install/supervisor-report.d.ts.map +1 -0
- package/dist/cli/install/supervisor-report.js +33 -0
- package/dist/cli/install/supervisor-tools.d.ts +43 -0
- package/dist/cli/install/supervisor-tools.d.ts.map +1 -0
- package/dist/cli/install/supervisor-tools.js +114 -0
- package/dist/cli/install/unpack.d.ts +23 -3
- package/dist/cli/install/unpack.d.ts.map +1 -1
- package/dist/cli/install/unpack.js +124 -10
- package/dist/cli/install/zip-member.d.ts +15 -0
- package/dist/cli/install/zip-member.d.ts.map +1 -0
- package/dist/cli/install/zip-member.js +77 -0
- package/dist/cli/interrupt.d.ts +1 -1
- package/dist/cli/interrupt.d.ts.map +1 -1
- package/dist/cli/interrupt.js +15 -10
- package/dist/cli/terminal.d.ts +9 -1
- package/dist/cli/terminal.d.ts.map +1 -1
- package/dist/cli/terminal.js +14 -0
- package/dist/config/define.d.ts +2 -2
- package/dist/config/define.js +2 -2
- package/dist/diagnostics/firefox-collector.d.ts +11 -10
- package/dist/diagnostics/firefox-collector.d.ts.map +1 -1
- package/dist/diagnostics/firefox-collector.js +16 -11
- package/dist/media/client.d.ts +2 -1
- package/dist/media/client.d.ts.map +1 -1
- package/dist/media/client.js +16 -5
- package/dist/media/host.d.ts +32 -0
- package/dist/media/host.d.ts.map +1 -0
- package/dist/media/host.js +50 -0
- package/dist/media/locate.d.ts.map +1 -1
- package/dist/media/locate.js +4 -3
- package/dist/native/processes.d.ts.map +1 -1
- package/dist/native/processes.js +7 -3
- package/dist/protocol/files.d.ts +1 -1
- package/dist/protocol/files.d.ts.map +1 -1
- package/dist/protocol/files.js +10 -5
- package/dist/reporters/commands.d.ts +12 -3
- package/dist/reporters/commands.d.ts.map +1 -1
- package/dist/reporters/commands.js +29 -11
- package/dist/reporters/human-card.js +1 -1
- package/dist/reporters/human.d.ts +3 -1
- package/dist/reporters/human.d.ts.map +1 -1
- package/dist/reporters/human.js +8 -8
- package/dist/reporters/style.d.ts +21 -2
- package/dist/reporters/style.d.ts.map +1 -1
- package/dist/reporters/style.js +11 -2
- package/dist/runner/app-server.d.ts +27 -7
- package/dist/runner/app-server.d.ts.map +1 -1
- package/dist/runner/app-server.js +74 -24
- package/dist/runner/child.js +6 -2
- package/dist/runner/media-leftovers.d.ts +7 -3
- package/dist/runner/media-leftovers.d.ts.map +1 -1
- package/dist/runner/media-leftovers.js +27 -6
- package/dist/runner/resources.d.ts +8 -0
- package/dist/runner/resources.d.ts.map +1 -1
- package/dist/runner/resources.js +12 -1
- package/dist/runner/run-session.d.ts.map +1 -1
- package/dist/runner/run-session.js +9 -4
- package/dist/runner/run.d.ts.map +1 -1
- package/dist/runner/run.js +2 -1
- package/dist/runner/target-drivers.d.ts +5 -2
- package/dist/runner/target-drivers.d.ts.map +1 -1
- package/dist/runner/target-drivers.js +10 -1
- package/dist/runner/test-file-process.d.ts +8 -0
- package/dist/runner/test-file-process.d.ts.map +1 -1
- package/dist/runner/test-file-process.js +26 -3
- package/dist/runner/test-pages.d.ts.map +1 -1
- package/dist/runner/test-pages.js +5 -2
- package/dist/shared/environment.d.ts +17 -0
- package/dist/shared/environment.d.ts.map +1 -0
- package/dist/shared/environment.js +27 -0
- package/dist/shared/metadata-process-worker.js +15 -7
- package/dist/shared/metadata-process.d.ts +5 -0
- package/dist/shared/metadata-process.d.ts.map +1 -1
- package/dist/shared/metadata-process.js +36 -6
- package/dist/shared/posix-path.d.ts +12 -0
- package/dist/shared/posix-path.d.ts.map +1 -1
- package/dist/shared/posix-path.js +16 -0
- package/dist/shared/private-file.d.ts +38 -0
- package/dist/shared/private-file.d.ts.map +1 -0
- package/dist/shared/private-file.js +132 -0
- package/dist/shared/process-ownership.d.ts +52 -2
- package/dist/shared/process-ownership.d.ts.map +1 -1
- package/dist/shared/process-ownership.js +104 -11
- package/dist/shared/process-supervisor.d.ts +309 -0
- package/dist/shared/process-supervisor.d.ts.map +1 -0
- package/dist/shared/process-supervisor.js +737 -0
- package/dist/shared/process-system.d.ts +113 -0
- package/dist/shared/process-system.d.ts.map +1 -0
- package/dist/shared/process-system.js +254 -0
- package/dist/shared/regular-file.d.ts +2 -2
- package/dist/shared/regular-file.d.ts.map +1 -1
- package/dist/shared/regular-file.js +19 -2
- package/dist/store/artifacts.d.ts +5 -4
- package/dist/store/artifacts.d.ts.map +1 -1
- package/dist/store/artifacts.js +18 -5
- package/dist/store/run-store.d.ts +5 -2
- package/dist/store/run-store.d.ts.map +1 -1
- package/dist/store/run-store.js +48 -4
- package/media/Cargo.lock +16 -0
- package/media/Cargo.toml +16 -1
- package/media/src/encoder.rs +56 -12
- package/media/src/frame.rs +7 -1
- package/media/src/main.rs +20 -2
- package/media/src/place.rs +230 -11
- package/media/src/process_ownership_windows.rs +1058 -0
- package/media/src/recording.rs +15 -7
- package/media/src/replies.rs +55 -0
- package/media/src/server.rs +72 -2
- package/media/src/store.rs +71 -2
- package/package.json +9 -4
- package/src/cli/install/media-notices.txt +465 -1
- package/supervisor/Cargo.lock +25 -0
- package/supervisor/Cargo.toml +22 -0
- package/supervisor/NOTICES +464 -0
- package/supervisor/build.rs +44 -0
- package/supervisor/src/command.rs +612 -0
- package/supervisor/src/jobs.rs +1650 -0
- package/supervisor/src/main.rs +71 -0
- package/supervisor/src/reply.rs +367 -0
|
@@ -0,0 +1,1650 @@
|
|
|
1
|
+
//! The Win32 side: job objects, and the process readings each answer is made of.
|
|
2
|
+
//!
|
|
3
|
+
//! Every job is named in this session's namespace, so a later invocation can open it, and created only when the name
|
|
4
|
+
//! is new, so a job someone made first is never taken over. Each one ends every process it holds once its last handle
|
|
5
|
+
//! closes. The handle that keeps it is duplicated into the runner before anything is put in it: from then on the job
|
|
6
|
+
//! lives exactly as long as the runner, and when the runner ends, however it ends, Windows closes that handle and ends
|
|
7
|
+
//! everything the job holds. A process is only ever read or ended through a handle opened on it, which pins it, so a pid
|
|
8
|
+
//! that went to another process between two calls is caught by its start, never acted on.
|
|
9
|
+
|
|
10
|
+
use std::collections::{HashMap, HashSet};
|
|
11
|
+
use std::ffi::{OsStr, c_void};
|
|
12
|
+
use std::iter::once;
|
|
13
|
+
use std::mem::{size_of, zeroed};
|
|
14
|
+
use std::os::windows::ffi::OsStrExt;
|
|
15
|
+
use std::ptr::{null, null_mut};
|
|
16
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
17
|
+
use std::thread::sleep;
|
|
18
|
+
use std::time::{Duration, Instant};
|
|
19
|
+
|
|
20
|
+
use windows_sys::Wdk::System::Threading::{
|
|
21
|
+
NtQueryInformationProcess, ProcessBasicInformation, ProcessCommandLineInformation,
|
|
22
|
+
};
|
|
23
|
+
use windows_sys::Win32::Foundation::{
|
|
24
|
+
CloseHandle, CompareObjectHandles, DUPLICATE_CLOSE_SOURCE, DUPLICATE_SAME_ACCESS,
|
|
25
|
+
DuplicateHandle, ERROR_ALREADY_EXISTS, ERROR_FILE_NOT_FOUND, ERROR_INVALID_PARAMETER,
|
|
26
|
+
ERROR_MORE_DATA, FILETIME, GetLastError, HANDLE, INVALID_HANDLE_VALUE, SetLastError,
|
|
27
|
+
};
|
|
28
|
+
use windows_sys::Win32::System::Console::{
|
|
29
|
+
AttachConsole, CTRL_BREAK_EVENT, CTRL_C_EVENT, FreeConsole, GenerateConsoleCtrlEvent,
|
|
30
|
+
GetConsoleProcessList, SetConsoleCtrlHandler,
|
|
31
|
+
};
|
|
32
|
+
use windows_sys::Win32::System::Diagnostics::ToolHelp::{
|
|
33
|
+
CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW, TH32CS_SNAPPROCESS,
|
|
34
|
+
};
|
|
35
|
+
use windows_sys::Win32::System::JobObjects::{
|
|
36
|
+
AssignProcessToJobObject, CreateJobObjectW, IsProcessInJob, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
|
|
37
|
+
JOBOBJECT_BASIC_PROCESS_ID_LIST, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
|
|
38
|
+
JobObjectBasicProcessIdList, JobObjectExtendedLimitInformation, OpenJobObjectW,
|
|
39
|
+
QueryInformationJobObject, SetInformationJobObject, TerminateJobObject,
|
|
40
|
+
};
|
|
41
|
+
use windows_sys::Win32::System::SystemInformation::GetSystemTimePreciseAsFileTime;
|
|
42
|
+
use windows_sys::Win32::System::Threading::{
|
|
43
|
+
GetCurrentProcess, GetCurrentProcessId, GetExitCodeProcess, GetProcessTimes, OpenProcess,
|
|
44
|
+
PROCESS_DUP_HANDLE, PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SET_QUOTA,
|
|
45
|
+
PROCESS_TERMINATE, QueryFullProcessImageNameW, TerminateProcess,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
use crate::command::{Command, JobName, Key};
|
|
49
|
+
use crate::reply::{Identity, Outcome, Reply};
|
|
50
|
+
|
|
51
|
+
// Job access rights, from winnt.h; windows-sys keeps them in a much larger module than the rest of this file needs.
|
|
52
|
+
const JOB_OBJECT_QUERY: u32 = 0x0004;
|
|
53
|
+
const JOB_OBJECT_TERMINATE: u32 = 0x0008;
|
|
54
|
+
// The exit code GetExitCodeProcess gives a process still running.
|
|
55
|
+
const STILL_ACTIVE: u32 = 259;
|
|
56
|
+
// The exit code of a process a job's end or `terminate` ended: 137, as a shell reports a process SIGKILL ended (128 + 9),
|
|
57
|
+
// so a kill is told from a crash, which Windows reports as 1, as it does a child Node kills.
|
|
58
|
+
const ENDED_EXIT_CODE: u32 = 137;
|
|
59
|
+
// How many times the processes a child started before it was held are looked for, each look taking those the last
|
|
60
|
+
// one held; a launch's helpers are a few levels deep at most. A search still finding more at the last look says so.
|
|
61
|
+
const DESCENDANT_LOOKS: usize = 8;
|
|
62
|
+
// How many parents a stray is followed up through before it counts as standing on nothing.
|
|
63
|
+
const PARENT_STEPS: usize = 64;
|
|
64
|
+
|
|
65
|
+
/// Does what `command` asks and says how it went. A refusal is an answer too, with its reason.
|
|
66
|
+
pub fn answer(command: Command) -> Reply {
|
|
67
|
+
let answered = match command {
|
|
68
|
+
Command::Version => Err(Failure::plain(
|
|
69
|
+
"--version is answered before any job is touched.",
|
|
70
|
+
)),
|
|
71
|
+
Command::Hold { runner, job } => hold(runner, &job),
|
|
72
|
+
Command::Adopt {
|
|
73
|
+
runner,
|
|
74
|
+
within,
|
|
75
|
+
job,
|
|
76
|
+
pid,
|
|
77
|
+
} => adopt(runner, &within, &job, pid),
|
|
78
|
+
Command::Members { job } => members(&job),
|
|
79
|
+
Command::Process { pid, job } => process(pid, job.as_ref()),
|
|
80
|
+
Command::End { runner, job } => end(runner, &job),
|
|
81
|
+
Command::Terminate {
|
|
82
|
+
runner,
|
|
83
|
+
job,
|
|
84
|
+
pid,
|
|
85
|
+
started,
|
|
86
|
+
} => terminate(runner, &job, pid, started),
|
|
87
|
+
Command::Release {
|
|
88
|
+
runner,
|
|
89
|
+
job,
|
|
90
|
+
handle,
|
|
91
|
+
} => release(runner, &job, handle),
|
|
92
|
+
Command::Strays {
|
|
93
|
+
runner,
|
|
94
|
+
job,
|
|
95
|
+
launches,
|
|
96
|
+
} => strays(runner, &job, &launches),
|
|
97
|
+
Command::Interrupt {
|
|
98
|
+
runner,
|
|
99
|
+
job,
|
|
100
|
+
pid,
|
|
101
|
+
key,
|
|
102
|
+
} => interrupt(runner, &job, pid, key),
|
|
103
|
+
};
|
|
104
|
+
answered.unwrap_or_else(|failure| Reply::Refused {
|
|
105
|
+
problem: failure.problem,
|
|
106
|
+
detail: failure.detail,
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// Why a request was not done: a sentence a person can read, and, when a Windows call failed, that call and its code.
|
|
111
|
+
#[derive(Debug)]
|
|
112
|
+
struct Failure {
|
|
113
|
+
problem: String,
|
|
114
|
+
detail: Option<String>,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
impl Failure {
|
|
118
|
+
fn plain(problem: impl Into<String>) -> Self {
|
|
119
|
+
Self {
|
|
120
|
+
problem: problem.into(),
|
|
121
|
+
detail: None,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// A sentence a person reads, and the job it is about, named apart from it for a diagnostic.
|
|
126
|
+
fn about(job: &JobName, problem: impl Into<String>) -> Self {
|
|
127
|
+
Self {
|
|
128
|
+
problem: problem.into(),
|
|
129
|
+
detail: Some(format!("Job {}.", job.as_str())),
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// A Windows call that failed on a named job: the job is named in the detail beside the call.
|
|
134
|
+
fn windows_on(job: &JobName, call: &str, problem: impl Into<String>) -> Self {
|
|
135
|
+
let failure = Self::windows(call, problem);
|
|
136
|
+
Self {
|
|
137
|
+
detail: failure
|
|
138
|
+
.detail
|
|
139
|
+
.map(|detail| format!("{detail} Job {}.", job.as_str())),
|
|
140
|
+
..failure
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// A Windows call that failed: `problem` says in plain words what did not happen, the detail which call it was.
|
|
145
|
+
fn windows(call: &str, problem: impl Into<String>) -> Self {
|
|
146
|
+
// SAFETY: GetLastError reads this thread's last error and has no preconditions.
|
|
147
|
+
let code = unsafe { GetLastError() };
|
|
148
|
+
Self {
|
|
149
|
+
problem: problem.into(),
|
|
150
|
+
detail: Some(format!("{call} failed with Windows error {code}.")),
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// A handle this invocation opened, closed when it goes out of scope.
|
|
156
|
+
struct Owned(HANDLE);
|
|
157
|
+
|
|
158
|
+
impl Drop for Owned {
|
|
159
|
+
fn drop(&mut self) {
|
|
160
|
+
// SAFETY: the handle was returned open by Windows and is closed only here, once.
|
|
161
|
+
unsafe { CloseHandle(self.0) };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
fn hold(runner: u32, name: &JobName) -> Result<Reply, Failure> {
|
|
166
|
+
let parents = parent_ids()?;
|
|
167
|
+
// SAFETY: GetCurrentProcessId has no preconditions.
|
|
168
|
+
let me = unsafe { GetCurrentProcessId() };
|
|
169
|
+
if parents.get(&me) != Some(&runner) {
|
|
170
|
+
return Err(Failure::plain(format!(
|
|
171
|
+
"Process {runner} did not start this request, so it was not held."
|
|
172
|
+
)));
|
|
173
|
+
}
|
|
174
|
+
let runner_process = open_process(
|
|
175
|
+
runner,
|
|
176
|
+
PROCESS_SET_QUOTA
|
|
177
|
+
| PROCESS_TERMINATE
|
|
178
|
+
| PROCESS_DUP_HANDLE
|
|
179
|
+
| PROCESS_QUERY_LIMITED_INFORMATION,
|
|
180
|
+
)?
|
|
181
|
+
.ok_or_else(|| {
|
|
182
|
+
Failure::plain(format!(
|
|
183
|
+
"Process {runner} is not running, so it was not held."
|
|
184
|
+
))
|
|
185
|
+
})?;
|
|
186
|
+
let job = create_job(name)?;
|
|
187
|
+
// The runner gets its handle while the job is still empty: had this invocation died after the runner was put in a
|
|
188
|
+
// job only it held, closing that last handle would have ended the runner.
|
|
189
|
+
let handle = duplicate_into(&job, &runner_process, runner)?;
|
|
190
|
+
// SAFETY: both handles are open, the job's with every right and the runner's with PROCESS_SET_QUOTA and PROCESS_TERMINATE.
|
|
191
|
+
if unsafe { AssignProcessToJobObject(job.0, runner_process.0) } == 0 {
|
|
192
|
+
let failure = Failure::windows(
|
|
193
|
+
"AssignProcessToJobObject",
|
|
194
|
+
format!("Process {runner} could not be put in a job."),
|
|
195
|
+
);
|
|
196
|
+
close_in(&runner_process, handle);
|
|
197
|
+
return Err(failure);
|
|
198
|
+
}
|
|
199
|
+
Ok(Reply::Held {
|
|
200
|
+
job: name.as_str().to_owned(),
|
|
201
|
+
runner,
|
|
202
|
+
handle,
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
fn adopt(runner: u32, within: &JobName, name: &JobName, pid: u32) -> Result<Reply, Failure> {
|
|
207
|
+
let runner_job = open_job(within, JOB_OBJECT_QUERY)?;
|
|
208
|
+
let runner_process = open_process(
|
|
209
|
+
runner,
|
|
210
|
+
PROCESS_DUP_HANDLE | PROCESS_QUERY_LIMITED_INFORMATION,
|
|
211
|
+
)?
|
|
212
|
+
.ok_or_else(|| {
|
|
213
|
+
Failure::plain(format!(
|
|
214
|
+
"Process {runner} is not running, so nothing was held for it."
|
|
215
|
+
))
|
|
216
|
+
})?;
|
|
217
|
+
if !in_job(&runner_process, &runner_job)? {
|
|
218
|
+
return Err(Failure::about(
|
|
219
|
+
within,
|
|
220
|
+
format!("Process {runner} is not held yet, so nothing was held for it."),
|
|
221
|
+
));
|
|
222
|
+
}
|
|
223
|
+
// The parent is read through a handle that only reads and that pins the process, so the handle opened to hold it
|
|
224
|
+
// below is this same process. A process this one cannot even read is no child the runner started.
|
|
225
|
+
let reading = match open_process(pid, PROCESS_QUERY_LIMITED_INFORMATION) {
|
|
226
|
+
Ok(Some(reading)) => Some(reading),
|
|
227
|
+
Ok(None) => {
|
|
228
|
+
return Err(Failure::plain(format!(
|
|
229
|
+
"Process {pid} is not running, so it was not held."
|
|
230
|
+
)));
|
|
231
|
+
}
|
|
232
|
+
Err(_) => None,
|
|
233
|
+
};
|
|
234
|
+
if reading.as_ref().and_then(parent_of) != Some(runner) {
|
|
235
|
+
return Err(Failure::plain(format!(
|
|
236
|
+
"Process {pid} was not started by process {runner}, so it was not held."
|
|
237
|
+
)));
|
|
238
|
+
}
|
|
239
|
+
let child = open_process(
|
|
240
|
+
pid,
|
|
241
|
+
PROCESS_SET_QUOTA | PROCESS_TERMINATE | PROCESS_QUERY_LIMITED_INFORMATION,
|
|
242
|
+
)?
|
|
243
|
+
.ok_or_else(|| Failure::plain(format!("Process {pid} is not running, so it was not held.")))?;
|
|
244
|
+
// A child that already ended cannot be put in a job; what it started before it ended can, and is held below.
|
|
245
|
+
let ended = !running(&child)?;
|
|
246
|
+
// A child starts after its parent. One that started earlier holds a pid that went to it from another process.
|
|
247
|
+
let child_start = started_at(&child)?;
|
|
248
|
+
if child_start < started_at(&runner_process)? {
|
|
249
|
+
return Err(Failure::plain(format!(
|
|
250
|
+
"Process {pid} started before process {runner}, so it is not that process's child and was not held."
|
|
251
|
+
)));
|
|
252
|
+
}
|
|
253
|
+
let job = create_job(name)?;
|
|
254
|
+
let handle = duplicate_into(&job, &runner_process, runner)?;
|
|
255
|
+
// Nothing after this point fails: a refusal from here on would leave the runner holding a job with processes in it.
|
|
256
|
+
// SAFETY: both handles are open, the job's with every right and the child's with PROCESS_SET_QUOTA and PROCESS_TERMINATE.
|
|
257
|
+
if !ended && unsafe { AssignProcessToJobObject(job.0, child.0) } == 0 {
|
|
258
|
+
let failure = Failure::windows(
|
|
259
|
+
"AssignProcessToJobObject",
|
|
260
|
+
format!("Process {pid} could not be held in a job of its own."),
|
|
261
|
+
);
|
|
262
|
+
close_in(&runner_process, handle);
|
|
263
|
+
return Err(failure);
|
|
264
|
+
}
|
|
265
|
+
let root = Identity {
|
|
266
|
+
pid,
|
|
267
|
+
parent_pid: Some(runner),
|
|
268
|
+
started_at: child_start,
|
|
269
|
+
image: image_of(&child),
|
|
270
|
+
command_line: command_line_of(&child),
|
|
271
|
+
};
|
|
272
|
+
let walk = adopt_descendants(&job, &runner_job, pid, child_start);
|
|
273
|
+
Ok(Reply::Adopted {
|
|
274
|
+
job: name.as_str().to_owned(),
|
|
275
|
+
handle,
|
|
276
|
+
root,
|
|
277
|
+
ended,
|
|
278
|
+
late: walk.late,
|
|
279
|
+
by_parent: walk.by_parent,
|
|
280
|
+
unheld: walk.unheld,
|
|
281
|
+
complete: walk.complete,
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/// What the search for the processes a child started before it was held found.
|
|
286
|
+
struct Walk {
|
|
287
|
+
late: Vec<Identity>,
|
|
288
|
+
by_parent: Vec<Identity>,
|
|
289
|
+
unheld: Vec<u32>,
|
|
290
|
+
complete: bool,
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// The processes the child started before it was held were born outside its job. Each is held now, while its parent is
|
|
294
|
+
// one of the held ones and it started between that parent and the snapshot it was found in: a process that started
|
|
295
|
+
// earlier holds a pid that went to it from another process, and one that started later took a pid the snapshot gave
|
|
296
|
+
// to a process that ended since. Processes born after the child was held are in the job already, and are not finds.
|
|
297
|
+
// A process that cannot be read is passed over, never the end of the search: the snapshot names parents by pid, and a
|
|
298
|
+
// pid of a process long gone, such as the parent a system process was started by, may be one of the held ones now.
|
|
299
|
+
//
|
|
300
|
+
// What this cannot see is a process whose parent ended before the snapshot: Windows then names a parent that no longer
|
|
301
|
+
// runs, and nothing ties the process to the launch. The runner's job still holds it, and `strays` finds it afterwards.
|
|
302
|
+
fn adopt_descendants(job: &Owned, runner_job: &Owned, root: u32, root_start: u64) -> Walk {
|
|
303
|
+
let mut held: HashMap<u32, u64> = HashMap::from([(root, root_start)]);
|
|
304
|
+
// A handle on each held process pins its pid for the whole walk: one that ends meanwhile keeps its pid, so no later
|
|
305
|
+
// process given that pid is taken for its child. The root's is the caller's.
|
|
306
|
+
let mut pins: Vec<Owned> = Vec::new();
|
|
307
|
+
let mut decided: HashSet<u32> = HashSet::from([root]);
|
|
308
|
+
let mut walk = Walk {
|
|
309
|
+
late: Vec::new(),
|
|
310
|
+
by_parent: Vec::new(),
|
|
311
|
+
unheld: Vec::new(),
|
|
312
|
+
complete: false,
|
|
313
|
+
};
|
|
314
|
+
for _ in 0..DESCENDANT_LOOKS {
|
|
315
|
+
let looked = now();
|
|
316
|
+
let Ok(parents) = parent_ids() else {
|
|
317
|
+
return walk;
|
|
318
|
+
};
|
|
319
|
+
let mut found = false;
|
|
320
|
+
for (&pid, &parent) in &parents {
|
|
321
|
+
if decided.contains(&pid) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
let Some(&parent_start) = held.get(&parent) else {
|
|
325
|
+
continue;
|
|
326
|
+
};
|
|
327
|
+
let Some((process, start)) = descendant(pid, parent, parent_start, looked) else {
|
|
328
|
+
continue;
|
|
329
|
+
};
|
|
330
|
+
decided.insert(pid);
|
|
331
|
+
let identity = Identity {
|
|
332
|
+
pid,
|
|
333
|
+
parent_pid: Some(parent),
|
|
334
|
+
started_at: start,
|
|
335
|
+
image: image_of(&process),
|
|
336
|
+
command_line: command_line_of(&process),
|
|
337
|
+
};
|
|
338
|
+
match in_job(&process, job) {
|
|
339
|
+
Ok(true) => {
|
|
340
|
+
held.insert(pid, start);
|
|
341
|
+
pins.push(process);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
Ok(false) => {}
|
|
345
|
+
Err(_) => {
|
|
346
|
+
walk.unheld.push(pid);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// Everything the launch started before it was held was born in the runner's job, which the runner was put in
|
|
351
|
+
// before it started anything. A process outside it is someone else's, whatever pid its parent was given.
|
|
352
|
+
let in_runner_job = in_job(&process, runner_job).unwrap_or(false);
|
|
353
|
+
if !in_runner_job {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
// The reading handle pins the process, so the pid opened again here is still this process.
|
|
357
|
+
let assigned = open_process(pid, PROCESS_SET_QUOTA | PROCESS_TERMINATE)
|
|
358
|
+
.ok()
|
|
359
|
+
.flatten()
|
|
360
|
+
// SAFETY: both handles are open, the job's with every right and the process's with PROCESS_SET_QUOTA
|
|
361
|
+
// and PROCESS_TERMINATE.
|
|
362
|
+
.is_some_and(
|
|
363
|
+
|writable| unsafe { AssignProcessToJobObject(job.0, writable.0) } != 0,
|
|
364
|
+
);
|
|
365
|
+
// Only an assignment counts as a find: a launch that goes on starting processes in a job of its own, as
|
|
366
|
+
// Firefox starts its content processes, would otherwise use up the looks.
|
|
367
|
+
if assigned {
|
|
368
|
+
found = true;
|
|
369
|
+
held.insert(pid, start);
|
|
370
|
+
walk.late.push(identity);
|
|
371
|
+
} else {
|
|
372
|
+
// Already in a job of its own inside the runner's, as the browser Firefox's launcher starts is, it cannot
|
|
373
|
+
// be moved beside the launch. Its parent holds that job, and the runner's job holds them both.
|
|
374
|
+
held.insert(pid, start);
|
|
375
|
+
walk.by_parent.push(identity);
|
|
376
|
+
}
|
|
377
|
+
pins.push(process);
|
|
378
|
+
}
|
|
379
|
+
if !found {
|
|
380
|
+
walk.complete = true;
|
|
381
|
+
return walk;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
walk
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// A reading handle on `pid` and its start, when it is a running process that started no earlier than its parent and no
|
|
388
|
+
// later than the snapshot taken at `looked`, and whose own handle names the parent the snapshot gave it: a pid given to
|
|
389
|
+
// another process since the snapshot is that process, with a parent and a start of its own.
|
|
390
|
+
fn descendant(pid: u32, parent: u32, parent_start: u64, looked: u64) -> Option<(Owned, u64)> {
|
|
391
|
+
let process = open_process(pid, PROCESS_QUERY_LIMITED_INFORMATION).ok()??;
|
|
392
|
+
if !running(&process).ok()? || parent_of(&process) != Some(parent) {
|
|
393
|
+
return None;
|
|
394
|
+
}
|
|
395
|
+
let start = started_at(&process).ok()?;
|
|
396
|
+
(parent_start <= start && start <= looked).then_some((process, start))
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
fn members(name: &JobName) -> Result<Reply, Failure> {
|
|
400
|
+
let job = open_job(name, JOB_OBJECT_QUERY)?;
|
|
401
|
+
let listed = process_ids(&job)?;
|
|
402
|
+
let mut processes = Vec::with_capacity(listed.len());
|
|
403
|
+
for pid in listed {
|
|
404
|
+
// A member that cannot be read is listed with its pid alone, as `strays` lists one: the job holds it, so it is
|
|
405
|
+
// one of the job's, and one process no reading can open never hides the rest.
|
|
406
|
+
let process = match open_process(pid, PROCESS_QUERY_LIMITED_INFORMATION) {
|
|
407
|
+
Ok(Some(process)) => process,
|
|
408
|
+
Ok(None) => continue,
|
|
409
|
+
Err(_) => {
|
|
410
|
+
processes.push(unread(pid));
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
// A listed process that ended since, or whose pid went to a process outside the job, is no longer a member.
|
|
415
|
+
match (running(&process), in_job(&process, &job)) {
|
|
416
|
+
(Ok(false), _) | (_, Ok(false)) => continue,
|
|
417
|
+
(Ok(true), Ok(true)) => {}
|
|
418
|
+
_ => {
|
|
419
|
+
processes.push(unread(pid));
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
processes.push(identity_of(pid, &process).unwrap_or_else(|_| unread(pid)));
|
|
424
|
+
}
|
|
425
|
+
Ok(Reply::Members {
|
|
426
|
+
job: name.as_str().to_owned(),
|
|
427
|
+
processes,
|
|
428
|
+
})
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
fn process(pid: u32, name: Option<&JobName>) -> Result<Reply, Failure> {
|
|
432
|
+
let Some(process) = open_process(pid, PROCESS_QUERY_LIMITED_INFORMATION)? else {
|
|
433
|
+
return Ok(Reply::Process {
|
|
434
|
+
process: None,
|
|
435
|
+
in_job: None,
|
|
436
|
+
});
|
|
437
|
+
};
|
|
438
|
+
if !running(&process)? {
|
|
439
|
+
return Ok(Reply::Process {
|
|
440
|
+
process: None,
|
|
441
|
+
in_job: None,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
let identity = identity_of(pid, &process)?;
|
|
445
|
+
let in_job = match name {
|
|
446
|
+
None => None,
|
|
447
|
+
Some(name) => Some(match find_job(name, JOB_OBJECT_QUERY)? {
|
|
448
|
+
Some(job) => in_job(&process, &job)?,
|
|
449
|
+
// A job nobody holds any more holds no process.
|
|
450
|
+
None => false,
|
|
451
|
+
}),
|
|
452
|
+
};
|
|
453
|
+
Ok(Reply::Process {
|
|
454
|
+
process: Some(identity),
|
|
455
|
+
in_job,
|
|
456
|
+
})
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
fn end(runner: u32, name: &JobName) -> Result<Reply, Failure> {
|
|
460
|
+
let job = open_job(name, JOB_OBJECT_TERMINATE | JOB_OBJECT_QUERY)?;
|
|
461
|
+
if holds_runner(runner, &job)? {
|
|
462
|
+
return Err(Failure::about(
|
|
463
|
+
name,
|
|
464
|
+
"These processes include Retest itself, so they were not ended.",
|
|
465
|
+
));
|
|
466
|
+
}
|
|
467
|
+
// SAFETY: the job's handle is open with JOB_OBJECT_TERMINATE.
|
|
468
|
+
if unsafe { TerminateJobObject(job.0, ENDED_EXIT_CODE) } == 0 {
|
|
469
|
+
return Err(Failure::windows_on(
|
|
470
|
+
name,
|
|
471
|
+
"TerminateJobObject",
|
|
472
|
+
"The processes could not be ended.",
|
|
473
|
+
));
|
|
474
|
+
}
|
|
475
|
+
Ok(Reply::Ended {
|
|
476
|
+
job: name.as_str().to_owned(),
|
|
477
|
+
})
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
fn terminate(runner: u32, name: &JobName, pid: u32, started: u64) -> Result<Reply, Failure> {
|
|
481
|
+
if pid == runner {
|
|
482
|
+
return Err(Failure::plain(format!(
|
|
483
|
+
"Process {pid} is Retest itself, so it was not ended."
|
|
484
|
+
)));
|
|
485
|
+
}
|
|
486
|
+
let Some(process) = open_process(pid, PROCESS_TERMINATE | PROCESS_QUERY_LIMITED_INFORMATION)?
|
|
487
|
+
else {
|
|
488
|
+
return Ok(Reply::Terminated {
|
|
489
|
+
pid,
|
|
490
|
+
outcome: Outcome::Absent,
|
|
491
|
+
});
|
|
492
|
+
};
|
|
493
|
+
if !running(&process)? {
|
|
494
|
+
return Ok(Reply::Terminated {
|
|
495
|
+
pid,
|
|
496
|
+
outcome: Outcome::Absent,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
let held = match find_job(name, JOB_OBJECT_QUERY)? {
|
|
500
|
+
Some(job) => in_job(&process, &job)?,
|
|
501
|
+
None => false,
|
|
502
|
+
};
|
|
503
|
+
if started_at(&process)? != started || !held {
|
|
504
|
+
return Ok(Reply::Terminated {
|
|
505
|
+
pid,
|
|
506
|
+
outcome: Outcome::Different,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
// SAFETY: the process's handle is open with PROCESS_TERMINATE, and it pins the process just checked.
|
|
510
|
+
if unsafe { TerminateProcess(process.0, ENDED_EXIT_CODE) } == 0 {
|
|
511
|
+
return Err(Failure::windows(
|
|
512
|
+
"TerminateProcess",
|
|
513
|
+
format!("Process {pid} could not be ended."),
|
|
514
|
+
));
|
|
515
|
+
}
|
|
516
|
+
Ok(Reply::Terminated {
|
|
517
|
+
pid,
|
|
518
|
+
outcome: Outcome::Terminated,
|
|
519
|
+
})
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Closes the runner's handle to a job only when that handle is this job and the job holds nothing, since closing the last
|
|
523
|
+
// handle of a job ends what it holds.
|
|
524
|
+
fn release(runner: u32, name: &JobName, value: usize) -> Result<Reply, Failure> {
|
|
525
|
+
let job = open_job(name, JOB_OBJECT_QUERY)?;
|
|
526
|
+
let runner_process = open_process(runner, PROCESS_DUP_HANDLE)?.ok_or_else(|| {
|
|
527
|
+
Failure::plain(format!(
|
|
528
|
+
"Process {runner} is not running, so it holds no handle to release."
|
|
529
|
+
))
|
|
530
|
+
})?;
|
|
531
|
+
let mut copy: HANDLE = null_mut();
|
|
532
|
+
// SAFETY: the runner's handle is open with PROCESS_DUP_HANDLE; Windows checks that the value names a handle there.
|
|
533
|
+
if unsafe {
|
|
534
|
+
DuplicateHandle(
|
|
535
|
+
runner_process.0,
|
|
536
|
+
value as HANDLE,
|
|
537
|
+
GetCurrentProcess(),
|
|
538
|
+
&mut copy,
|
|
539
|
+
0,
|
|
540
|
+
0,
|
|
541
|
+
DUPLICATE_SAME_ACCESS,
|
|
542
|
+
)
|
|
543
|
+
} == 0
|
|
544
|
+
{
|
|
545
|
+
return Err(Failure::windows(
|
|
546
|
+
"DuplicateHandle",
|
|
547
|
+
format!("Process {runner} holds no handle {value}, so nothing was released."),
|
|
548
|
+
));
|
|
549
|
+
}
|
|
550
|
+
let copy = Owned(copy);
|
|
551
|
+
// SAFETY: both handles are open.
|
|
552
|
+
if unsafe { CompareObjectHandles(copy.0, job.0) } == 0 {
|
|
553
|
+
return Err(Failure::about(
|
|
554
|
+
name,
|
|
555
|
+
format!("Handle {value} of process {runner} is another one, so it was left open."),
|
|
556
|
+
));
|
|
557
|
+
}
|
|
558
|
+
if !process_ids(&job)?.is_empty() {
|
|
559
|
+
return Err(Failure::about(
|
|
560
|
+
name,
|
|
561
|
+
"Processes are still running there, so its handle was kept.",
|
|
562
|
+
));
|
|
563
|
+
}
|
|
564
|
+
close_in(&runner_process, value);
|
|
565
|
+
Ok(Reply::Released {
|
|
566
|
+
job: name.as_str().to_owned(),
|
|
567
|
+
})
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// The processes the runner's job holds that no launch's job holds, that are neither the runner nor this program, and
|
|
571
|
+
// that nothing still running stands behind. Each was started by a launch, or by something a launch started, whose
|
|
572
|
+
// process had already ended when the launch was held, so no launch's job took it; it runs until the runner ends.
|
|
573
|
+
fn strays(runner: u32, name: &JobName, launches: &[JobName]) -> Result<Reply, Failure> {
|
|
574
|
+
let job = open_job(name, JOB_OBJECT_QUERY)?;
|
|
575
|
+
// A launch's job that is gone holds nothing; one that cannot be opened is left out, so what it holds is named.
|
|
576
|
+
let launch_jobs: Vec<Owned> = launches
|
|
577
|
+
.iter()
|
|
578
|
+
.filter_map(|launch| find_job(launch, JOB_OBJECT_QUERY).ok().flatten())
|
|
579
|
+
.collect();
|
|
580
|
+
// SAFETY: GetCurrentProcessId has no preconditions.
|
|
581
|
+
let me = unsafe { GetCurrentProcessId() };
|
|
582
|
+
let looked = now();
|
|
583
|
+
let mut found = Vec::new();
|
|
584
|
+
for pid in process_ids(&job)? {
|
|
585
|
+
if pid == runner || pid == me {
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
let process = match open_process(pid, PROCESS_QUERY_LIMITED_INFORMATION) {
|
|
589
|
+
Ok(Some(process)) => process,
|
|
590
|
+
Ok(None) => continue,
|
|
591
|
+
// A member that cannot even be read is named: nothing says it is anyone's.
|
|
592
|
+
Err(_) => {
|
|
593
|
+
found.push(unread(pid));
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
if !running(&process).unwrap_or(true) {
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
let start = started_at(&process).unwrap_or(0);
|
|
601
|
+
// One born after the snapshot is not in it, so its parent is not known; it is no leftover of an ended launch.
|
|
602
|
+
if start > looked
|
|
603
|
+
|| !in_job(&process, &job).unwrap_or(true)
|
|
604
|
+
|| in_any(&process, &launch_jobs)
|
|
605
|
+
{
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
if stands_behind(&process, start, runner, &launch_jobs, looked) {
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
found.push(Identity {
|
|
612
|
+
pid,
|
|
613
|
+
parent_pid: parent_of(&process),
|
|
614
|
+
started_at: start,
|
|
615
|
+
image: image_of(&process),
|
|
616
|
+
command_line: command_line_of(&process),
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
Ok(Reply::Strays {
|
|
620
|
+
job: name.as_str().to_owned(),
|
|
621
|
+
processes: found,
|
|
622
|
+
})
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Whether the process's parents, followed up while each still runs and started no later than the process after it,
|
|
626
|
+
// reach the runner or a process a launch's job holds. Each parent is read from the handle of the process it started.
|
|
627
|
+
fn stands_behind(
|
|
628
|
+
process: &Owned,
|
|
629
|
+
start: u64,
|
|
630
|
+
runner: u32,
|
|
631
|
+
launches: &[Owned],
|
|
632
|
+
looked: u64,
|
|
633
|
+
) -> bool {
|
|
634
|
+
let (mut parent_pid, mut current_start) = (parent_of(process), start);
|
|
635
|
+
for _ in 0..PARENT_STEPS {
|
|
636
|
+
let Some(parent) = parent_pid else {
|
|
637
|
+
return false;
|
|
638
|
+
};
|
|
639
|
+
// The runner ran before everything its job holds was born, so a child naming its pid is its own.
|
|
640
|
+
if parent == runner {
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
643
|
+
let Ok(Some(process)) = open_process(parent, PROCESS_QUERY_LIMITED_INFORMATION) else {
|
|
644
|
+
return false;
|
|
645
|
+
};
|
|
646
|
+
if !running(&process).unwrap_or(false) {
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
let Ok(parent_start) = started_at(&process) else {
|
|
650
|
+
return false;
|
|
651
|
+
};
|
|
652
|
+
if parent_start > current_start || parent_start > looked {
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
if in_any(&process, launches) {
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
(parent_pid, current_start) = (parent_of(&process), parent_start);
|
|
659
|
+
}
|
|
660
|
+
false
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
fn in_any(process: &Owned, jobs: &[Owned]) -> bool {
|
|
664
|
+
jobs.iter().any(|job| in_job(process, job).unwrap_or(false))
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Ctrl+C or Ctrl+Break, sent to the console of `target`, or of the job's first process with one, which reaches every
|
|
668
|
+
// process on that console as a person pressing it there would. A console Retest itself is on is never sent one. This
|
|
669
|
+
// program starts with no console, joins that one, takes the key itself without ending, and leaves it again.
|
|
670
|
+
fn interrupt(runner: u32, name: &JobName, target: Option<u32>, key: Key) -> Result<Reply, Failure> {
|
|
671
|
+
let pressed = match key {
|
|
672
|
+
Key::CtrlC => "Ctrl+C",
|
|
673
|
+
Key::CtrlBreak => "Ctrl+Break",
|
|
674
|
+
};
|
|
675
|
+
let job = open_job(name, JOB_OBJECT_QUERY)?;
|
|
676
|
+
if holds_runner(runner, &job)? {
|
|
677
|
+
return Err(Failure::about(
|
|
678
|
+
name,
|
|
679
|
+
format!("These processes include Retest itself, so {pressed} was not sent."),
|
|
680
|
+
));
|
|
681
|
+
}
|
|
682
|
+
let members = process_ids(&job)?;
|
|
683
|
+
let candidates: Vec<u32> = match target {
|
|
684
|
+
Some(pid) if members.contains(&pid) => vec![pid],
|
|
685
|
+
Some(pid) => {
|
|
686
|
+
return Err(Failure::about(
|
|
687
|
+
name,
|
|
688
|
+
format!("Process {pid} is not one of these processes, so {pressed} was not sent."),
|
|
689
|
+
));
|
|
690
|
+
}
|
|
691
|
+
None => members.into_iter().filter(|pid| *pid != runner).collect(),
|
|
692
|
+
};
|
|
693
|
+
// SAFETY: GetCurrentProcessId has no preconditions.
|
|
694
|
+
let me = unsafe { GetCurrentProcessId() };
|
|
695
|
+
// SAFETY: FreeConsole has no preconditions; it fails harmlessly when this program has no console.
|
|
696
|
+
unsafe { FreeConsole() };
|
|
697
|
+
let mut unattached = None;
|
|
698
|
+
for pid in candidates {
|
|
699
|
+
// The job's list is a list of pids: one that ended since may have gone to a process on another console. A handle
|
|
700
|
+
// pins the process while its console is joined, and only a running member of the job is joined.
|
|
701
|
+
let Ok(Some(process)) = open_process(pid, PROCESS_QUERY_LIMITED_INFORMATION) else {
|
|
702
|
+
continue;
|
|
703
|
+
};
|
|
704
|
+
if !running(&process).unwrap_or(false) || !in_job(&process, &job).unwrap_or(false) {
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
// SAFETY: AttachConsole has no memory preconditions; it fails when the process has no console.
|
|
708
|
+
if unsafe { AttachConsole(pid) } == 0 {
|
|
709
|
+
unattached = Failure::windows("AttachConsole", "").detail;
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
let on_console: Vec<u32> = console_processes()
|
|
713
|
+
.into_iter()
|
|
714
|
+
.filter(|attached| *attached != me)
|
|
715
|
+
.collect();
|
|
716
|
+
if on_console.contains(&runner) {
|
|
717
|
+
// SAFETY: as above.
|
|
718
|
+
unsafe { FreeConsole() };
|
|
719
|
+
return Err(Failure::about(
|
|
720
|
+
name,
|
|
721
|
+
format!("These processes share Retest's console, so {pressed} was not sent."),
|
|
722
|
+
));
|
|
723
|
+
}
|
|
724
|
+
// This program ignores Ctrl+C, and takes Ctrl+Break, which cannot be ignored, without ending.
|
|
725
|
+
// SAFETY: a null handler with TRUE sets the ignoring; the other handler is a function that lives as long as this
|
|
726
|
+
// program. Neither call has other preconditions.
|
|
727
|
+
unsafe {
|
|
728
|
+
SetConsoleCtrlHandler(None, 1);
|
|
729
|
+
SetConsoleCtrlHandler(Some(taken), 1);
|
|
730
|
+
}
|
|
731
|
+
let event = match key {
|
|
732
|
+
Key::CtrlC => CTRL_C_EVENT,
|
|
733
|
+
Key::CtrlBreak => CTRL_BREAK_EVENT,
|
|
734
|
+
};
|
|
735
|
+
// SAFETY: GenerateConsoleCtrlEvent has no memory preconditions; group 0 is every process on this console.
|
|
736
|
+
let sent = unsafe { GenerateConsoleCtrlEvent(event, 0) } != 0;
|
|
737
|
+
// Ctrl+Break reaches this program too, on a thread of its own; one that arrived while this program ended would
|
|
738
|
+
// end it with the key's exit code instead of its answer's. It is waited for, briefly, before leaving.
|
|
739
|
+
if sent && key == Key::CtrlBreak {
|
|
740
|
+
let started = Instant::now();
|
|
741
|
+
while !BREAK_TAKEN.load(Ordering::SeqCst) && started.elapsed() < Duration::from_secs(2)
|
|
742
|
+
{
|
|
743
|
+
sleep(Duration::from_millis(5));
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
let failure = (!sent).then(|| {
|
|
747
|
+
Failure::windows(
|
|
748
|
+
"GenerateConsoleCtrlEvent",
|
|
749
|
+
format!("{pressed} could not be sent to the console of process {pid}."),
|
|
750
|
+
)
|
|
751
|
+
});
|
|
752
|
+
// SAFETY: as above.
|
|
753
|
+
unsafe { FreeConsole() };
|
|
754
|
+
return match failure {
|
|
755
|
+
Some(failure) => Err(failure),
|
|
756
|
+
None => Ok(Reply::Interrupted {
|
|
757
|
+
job: name.as_str().to_owned(),
|
|
758
|
+
console: pid,
|
|
759
|
+
reached: on_console,
|
|
760
|
+
}),
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
Err(Failure {
|
|
764
|
+
problem: format!("None of these processes has a console, so {pressed} was not sent."),
|
|
765
|
+
detail: Some(format!(
|
|
766
|
+
"{}Job {}.",
|
|
767
|
+
unattached
|
|
768
|
+
.map(|detail| format!("{detail} "))
|
|
769
|
+
.unwrap_or_default(),
|
|
770
|
+
name.as_str()
|
|
771
|
+
)),
|
|
772
|
+
})
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Whether the Ctrl+Break this program sent has reached it too.
|
|
776
|
+
static BREAK_TAKEN: AtomicBool = AtomicBool::new(false);
|
|
777
|
+
|
|
778
|
+
// What this program does with the key it sends to the console it joined: takes it, so it goes on to answer.
|
|
779
|
+
unsafe extern "system" fn taken(_event: u32) -> windows_sys::core::BOOL {
|
|
780
|
+
BREAK_TAKEN.store(true, Ordering::SeqCst);
|
|
781
|
+
1
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// The processes on the console this program is attached to.
|
|
785
|
+
fn console_processes() -> Vec<u32> {
|
|
786
|
+
let mut capacity = 64usize;
|
|
787
|
+
loop {
|
|
788
|
+
let mut list = vec![0u32; capacity];
|
|
789
|
+
// SAFETY: the buffer holds `capacity` process ids.
|
|
790
|
+
let count = unsafe { GetConsoleProcessList(list.as_mut_ptr(), capacity as u32) } as usize;
|
|
791
|
+
if count <= capacity {
|
|
792
|
+
list.truncate(count);
|
|
793
|
+
return list;
|
|
794
|
+
}
|
|
795
|
+
capacity = count + 16;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
fn holds_runner(runner: u32, job: &Owned) -> Result<bool, Failure> {
|
|
800
|
+
let Some(runner_process) = open_process(runner, PROCESS_QUERY_LIMITED_INFORMATION)? else {
|
|
801
|
+
return Ok(false);
|
|
802
|
+
};
|
|
803
|
+
in_job(&runner_process, job)
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// A new job named `name` that ends everything in it once its last handle closes. A name that is taken is refused rather
|
|
807
|
+
// than joined: the job behind it is someone else's.
|
|
808
|
+
fn create_job(name: &JobName) -> Result<Owned, Failure> {
|
|
809
|
+
let wide = wide(name.as_str());
|
|
810
|
+
// SAFETY: SetLastError has no preconditions; it clears the code CreateJobObjectW sets only when the name exists.
|
|
811
|
+
unsafe { SetLastError(0) };
|
|
812
|
+
// SAFETY: the name is a NUL-terminated UTF-16 string that outlives the call; no security attributes are passed.
|
|
813
|
+
let handle = unsafe { CreateJobObjectW(null(), wide.as_ptr()) };
|
|
814
|
+
if handle.is_null() {
|
|
815
|
+
return Err(Failure::windows(
|
|
816
|
+
"CreateJobObjectW",
|
|
817
|
+
"Windows would not make a job for Retest's processes.",
|
|
818
|
+
));
|
|
819
|
+
}
|
|
820
|
+
// SAFETY: GetLastError reads the code CreateJobObjectW just left.
|
|
821
|
+
let existed = unsafe { GetLastError() } == ERROR_ALREADY_EXISTS;
|
|
822
|
+
let job = Owned(handle);
|
|
823
|
+
if existed {
|
|
824
|
+
return Err(Failure::about(
|
|
825
|
+
name,
|
|
826
|
+
"Another job already has this name, so it was not used.",
|
|
827
|
+
));
|
|
828
|
+
}
|
|
829
|
+
// SAFETY: an all-zero JOBOBJECT_EXTENDED_LIMIT_INFORMATION is a valid value that sets no limit.
|
|
830
|
+
let mut limits: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = unsafe { zeroed() };
|
|
831
|
+
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
|
832
|
+
// SAFETY: the pointer and size describe `limits`, which outlives the call.
|
|
833
|
+
let set = unsafe {
|
|
834
|
+
SetInformationJobObject(
|
|
835
|
+
job.0,
|
|
836
|
+
JobObjectExtendedLimitInformation,
|
|
837
|
+
(&raw const limits).cast::<c_void>(),
|
|
838
|
+
size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
|
|
839
|
+
)
|
|
840
|
+
};
|
|
841
|
+
if set == 0 {
|
|
842
|
+
return Err(Failure::windows(
|
|
843
|
+
"SetInformationJobObject",
|
|
844
|
+
"Windows would not make a job that ends its processes when Retest ends, so none was used.",
|
|
845
|
+
));
|
|
846
|
+
}
|
|
847
|
+
Ok(job)
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
fn open_job(name: &JobName, access: u32) -> Result<Owned, Failure> {
|
|
851
|
+
find_job(name, access)?
|
|
852
|
+
.ok_or_else(|| Failure::about(name, "Retest no longer holds these processes."))
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
fn find_job(name: &JobName, access: u32) -> Result<Option<Owned>, Failure> {
|
|
856
|
+
let wide = wide(name.as_str());
|
|
857
|
+
// SAFETY: the name is a NUL-terminated UTF-16 string that outlives the call; the handle is not inherited.
|
|
858
|
+
let handle = unsafe { OpenJobObjectW(access, 0, wide.as_ptr()) };
|
|
859
|
+
if handle.is_null() {
|
|
860
|
+
// SAFETY: GetLastError reads the code OpenJobObjectW just left.
|
|
861
|
+
if unsafe { GetLastError() } == ERROR_FILE_NOT_FOUND {
|
|
862
|
+
return Ok(None);
|
|
863
|
+
}
|
|
864
|
+
return Err(Failure::windows_on(
|
|
865
|
+
name,
|
|
866
|
+
"OpenJobObjectW",
|
|
867
|
+
"Retest could not open what it holds.",
|
|
868
|
+
));
|
|
869
|
+
}
|
|
870
|
+
Ok(Some(Owned(handle)))
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// The ids of every process the job holds, nested jobs included, as many as there are.
|
|
874
|
+
fn process_ids(job: &Owned) -> Result<Vec<u32>, Failure> {
|
|
875
|
+
let header = size_of::<JOBOBJECT_BASIC_PROCESS_ID_LIST>() - size_of::<usize>();
|
|
876
|
+
let mut capacity = 64usize;
|
|
877
|
+
loop {
|
|
878
|
+
// usize words keep the list aligned as Windows writes it; the first words hold the two counts.
|
|
879
|
+
let words = header.div_ceil(size_of::<usize>()) + capacity;
|
|
880
|
+
let mut buffer = vec![0usize; words];
|
|
881
|
+
let bytes = (words * size_of::<usize>()) as u32;
|
|
882
|
+
// SAFETY: the buffer is `bytes` long, writable and aligned for JOBOBJECT_BASIC_PROCESS_ID_LIST.
|
|
883
|
+
let queried = unsafe {
|
|
884
|
+
QueryInformationJobObject(
|
|
885
|
+
job.0,
|
|
886
|
+
JobObjectBasicProcessIdList,
|
|
887
|
+
buffer.as_mut_ptr().cast::<c_void>(),
|
|
888
|
+
bytes,
|
|
889
|
+
null_mut(),
|
|
890
|
+
)
|
|
891
|
+
};
|
|
892
|
+
// SAFETY: the buffer starts with a JOBOBJECT_BASIC_PROCESS_ID_LIST header Windows filled or left zeroed.
|
|
893
|
+
let list = unsafe { &*buffer.as_ptr().cast::<JOBOBJECT_BASIC_PROCESS_ID_LIST>() };
|
|
894
|
+
let assigned = list.NumberOfAssignedProcesses as usize;
|
|
895
|
+
if queried == 0 {
|
|
896
|
+
// SAFETY: GetLastError reads the code QueryInformationJobObject just left.
|
|
897
|
+
if unsafe { GetLastError() } == ERROR_MORE_DATA && assigned > capacity {
|
|
898
|
+
capacity = assigned + 16;
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
return Err(Failure::windows(
|
|
902
|
+
"QueryInformationJobObject",
|
|
903
|
+
"Windows would not list the processes in a job.",
|
|
904
|
+
));
|
|
905
|
+
}
|
|
906
|
+
let listed = list.NumberOfProcessIdsInList as usize;
|
|
907
|
+
if listed < assigned {
|
|
908
|
+
capacity = assigned + 16;
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
let first = header.div_ceil(size_of::<usize>());
|
|
912
|
+
return Ok(buffer[first..first + listed]
|
|
913
|
+
.iter()
|
|
914
|
+
.map(|&pid| pid as u32)
|
|
915
|
+
.collect());
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Every running process's parent, from one snapshot. Windows keeps the id of the process that started each one, even
|
|
920
|
+
// after that process has ended and its id went to another, which is why every use here also compares starts.
|
|
921
|
+
fn parent_ids() -> Result<HashMap<u32, u32>, Failure> {
|
|
922
|
+
// SAFETY: TH32CS_SNAPPROCESS with process id 0 snapshots every process; no other argument is read.
|
|
923
|
+
let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) };
|
|
924
|
+
if snapshot == INVALID_HANDLE_VALUE {
|
|
925
|
+
return Err(Failure::windows(
|
|
926
|
+
"CreateToolhelp32Snapshot",
|
|
927
|
+
"Windows would not list its running processes.",
|
|
928
|
+
));
|
|
929
|
+
}
|
|
930
|
+
let snapshot = Owned(snapshot);
|
|
931
|
+
// SAFETY: an all-zero PROCESSENTRY32W is valid once its size is set.
|
|
932
|
+
let mut entry: PROCESSENTRY32W = unsafe { zeroed() };
|
|
933
|
+
entry.dwSize = size_of::<PROCESSENTRY32W>() as u32;
|
|
934
|
+
let mut parents = HashMap::new();
|
|
935
|
+
// SAFETY: the snapshot is open and `entry` has its size set.
|
|
936
|
+
let mut more = unsafe { Process32FirstW(snapshot.0, &mut entry) } != 0;
|
|
937
|
+
while more {
|
|
938
|
+
parents.insert(entry.th32ProcessID, entry.th32ParentProcessID);
|
|
939
|
+
// SAFETY: as above.
|
|
940
|
+
more = unsafe { Process32NextW(snapshot.0, &mut entry) } != 0;
|
|
941
|
+
}
|
|
942
|
+
Ok(parents)
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// The time now, as the 100 ns ticks since 1601 a process's start is given in.
|
|
946
|
+
fn now() -> u64 {
|
|
947
|
+
let mut time = FILETIME {
|
|
948
|
+
dwLowDateTime: 0,
|
|
949
|
+
dwHighDateTime: 0,
|
|
950
|
+
};
|
|
951
|
+
// SAFETY: `time` is writable.
|
|
952
|
+
unsafe { GetSystemTimePreciseAsFileTime(&mut time) };
|
|
953
|
+
(u64::from(time.dwHighDateTime) << 32) | u64::from(time.dwLowDateTime)
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// An open handle on the process, or none when no process has the pid.
|
|
957
|
+
fn open_process(pid: u32, access: u32) -> Result<Option<Owned>, Failure> {
|
|
958
|
+
// SAFETY: OpenProcess has no memory preconditions; the handle is not inherited.
|
|
959
|
+
let handle = unsafe { OpenProcess(access, 0, pid) };
|
|
960
|
+
if handle.is_null() {
|
|
961
|
+
// SAFETY: GetLastError reads the code OpenProcess just left.
|
|
962
|
+
if unsafe { GetLastError() } == ERROR_INVALID_PARAMETER {
|
|
963
|
+
return Ok(None);
|
|
964
|
+
}
|
|
965
|
+
return Err(Failure::windows(
|
|
966
|
+
"OpenProcess",
|
|
967
|
+
format!("Process {pid} could not be opened."),
|
|
968
|
+
));
|
|
969
|
+
}
|
|
970
|
+
Ok(Some(Owned(handle)))
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
fn running(process: &Owned) -> Result<bool, Failure> {
|
|
974
|
+
let mut code = 0u32;
|
|
975
|
+
// SAFETY: the handle is open with PROCESS_QUERY_LIMITED_INFORMATION and `code` is writable.
|
|
976
|
+
if unsafe { GetExitCodeProcess(process.0, &mut code) } == 0 {
|
|
977
|
+
return Err(Failure::windows(
|
|
978
|
+
"GetExitCodeProcess",
|
|
979
|
+
"Windows would not say whether a process is still running.",
|
|
980
|
+
));
|
|
981
|
+
}
|
|
982
|
+
Ok(code == STILL_ACTIVE)
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
fn started_at(process: &Owned) -> Result<u64, Failure> {
|
|
986
|
+
let empty = FILETIME {
|
|
987
|
+
dwLowDateTime: 0,
|
|
988
|
+
dwHighDateTime: 0,
|
|
989
|
+
};
|
|
990
|
+
let (mut created, mut exited, mut kernel, mut user) = (empty, empty, empty, empty);
|
|
991
|
+
// SAFETY: the handle is open with PROCESS_QUERY_LIMITED_INFORMATION and each FILETIME is writable.
|
|
992
|
+
if unsafe { GetProcessTimes(process.0, &mut created, &mut exited, &mut kernel, &mut user) } == 0
|
|
993
|
+
{
|
|
994
|
+
return Err(Failure::windows(
|
|
995
|
+
"GetProcessTimes",
|
|
996
|
+
"Windows would not say when a process started.",
|
|
997
|
+
));
|
|
998
|
+
}
|
|
999
|
+
Ok((u64::from(created.dwHighDateTime) << 32) | u64::from(created.dwLowDateTime))
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// A process no handle could be opened on: its pid, and a start of 0, which no process has.
|
|
1003
|
+
fn unread(pid: u32) -> Identity {
|
|
1004
|
+
Identity {
|
|
1005
|
+
pid,
|
|
1006
|
+
parent_pid: None,
|
|
1007
|
+
started_at: 0,
|
|
1008
|
+
image: None,
|
|
1009
|
+
command_line: None,
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// One process as its own handle shows it, so its parent, start, image and command line are all this one process's.
|
|
1014
|
+
fn identity_of(pid: u32, process: &Owned) -> Result<Identity, Failure> {
|
|
1015
|
+
Ok(Identity {
|
|
1016
|
+
pid,
|
|
1017
|
+
parent_pid: parent_of(process),
|
|
1018
|
+
started_at: started_at(process)?,
|
|
1019
|
+
image: image_of(process),
|
|
1020
|
+
command_line: command_line_of(process),
|
|
1021
|
+
})
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// PROCESS_BASIC_INFORMATION, as NtQueryInformationProcess writes it on x64; only the last field is read.
|
|
1025
|
+
#[repr(C)]
|
|
1026
|
+
struct BasicInformation {
|
|
1027
|
+
exit_status: i32,
|
|
1028
|
+
peb: *mut c_void,
|
|
1029
|
+
affinity: usize,
|
|
1030
|
+
base_priority: i32,
|
|
1031
|
+
unique_process_id: usize,
|
|
1032
|
+
inherited_from: usize,
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// The pid of the process that started this one, as Windows recorded it when it was created, read from its handle.
|
|
1036
|
+
fn parent_of(process: &Owned) -> Option<u32> {
|
|
1037
|
+
// SAFETY: an all-zero BasicInformation is valid; NtQueryInformationProcess fills it.
|
|
1038
|
+
let mut information: BasicInformation = unsafe { zeroed() };
|
|
1039
|
+
let mut written = 0u32;
|
|
1040
|
+
// SAFETY: the handle is open with PROCESS_QUERY_LIMITED_INFORMATION, which this class needs, and the buffer is
|
|
1041
|
+
// exactly the size given.
|
|
1042
|
+
let status = unsafe {
|
|
1043
|
+
NtQueryInformationProcess(
|
|
1044
|
+
process.0,
|
|
1045
|
+
ProcessBasicInformation,
|
|
1046
|
+
(&raw mut information).cast(),
|
|
1047
|
+
size_of::<BasicInformation>() as u32,
|
|
1048
|
+
&mut written,
|
|
1049
|
+
)
|
|
1050
|
+
};
|
|
1051
|
+
(status >= 0)
|
|
1052
|
+
.then(|| u32::try_from(information.inherited_from).ok())
|
|
1053
|
+
.flatten()
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// UNICODE_STRING, as NtQueryInformationProcess writes it ahead of the characters it points to.
|
|
1057
|
+
#[repr(C)]
|
|
1058
|
+
struct CountedText {
|
|
1059
|
+
length: u16,
|
|
1060
|
+
maximum_length: u16,
|
|
1061
|
+
buffer: *const u16,
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// The longest command line read; Windows allows 32767 characters.
|
|
1065
|
+
const COMMAND_LINE_BYTES: usize = 70_000;
|
|
1066
|
+
|
|
1067
|
+
// The command line the process was started with, read from its handle; none when Windows would not say.
|
|
1068
|
+
fn command_line_of(process: &Owned) -> Option<String> {
|
|
1069
|
+
// Kept in u64s, so the counted text at its start is aligned as Windows writes it.
|
|
1070
|
+
let mut buffer = vec![0u64; COMMAND_LINE_BYTES / 8];
|
|
1071
|
+
let mut written = 0u32;
|
|
1072
|
+
// SAFETY: the handle is open with PROCESS_QUERY_LIMITED_INFORMATION, which this class needs since Windows 8.1, and
|
|
1073
|
+
// the buffer holds the byte count given.
|
|
1074
|
+
let status = unsafe {
|
|
1075
|
+
NtQueryInformationProcess(
|
|
1076
|
+
process.0,
|
|
1077
|
+
ProcessCommandLineInformation,
|
|
1078
|
+
buffer.as_mut_ptr().cast(),
|
|
1079
|
+
(buffer.len() * 8) as u32,
|
|
1080
|
+
&mut written,
|
|
1081
|
+
)
|
|
1082
|
+
};
|
|
1083
|
+
if status < 0 || (written as usize) < size_of::<CountedText>() {
|
|
1084
|
+
return None;
|
|
1085
|
+
}
|
|
1086
|
+
// SAFETY: Windows wrote a CountedText at the start of the aligned buffer.
|
|
1087
|
+
let text = unsafe { &*buffer.as_ptr().cast::<CountedText>() };
|
|
1088
|
+
let characters = usize::from(text.length) / 2;
|
|
1089
|
+
if characters == 0 {
|
|
1090
|
+
return Some(String::new());
|
|
1091
|
+
}
|
|
1092
|
+
let start = buffer.as_ptr() as usize;
|
|
1093
|
+
let at = text.buffer as usize;
|
|
1094
|
+
// The characters follow the counted text inside the buffer; anything else is not read.
|
|
1095
|
+
if at < start + size_of::<CountedText>() || at + characters * 2 > start + written as usize {
|
|
1096
|
+
return None;
|
|
1097
|
+
}
|
|
1098
|
+
// SAFETY: the range was checked to lie inside the buffer Windows wrote.
|
|
1099
|
+
let slice = unsafe { std::slice::from_raw_parts(text.buffer, characters) };
|
|
1100
|
+
Some(String::from_utf16_lossy(slice))
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
fn image_of(process: &Owned) -> Option<String> {
|
|
1104
|
+
let mut buffer = vec![0u16; 32_768];
|
|
1105
|
+
let mut length = buffer.len() as u32;
|
|
1106
|
+
// SAFETY: the buffer holds `length` UTF-16 units and `length` is writable.
|
|
1107
|
+
if unsafe {
|
|
1108
|
+
QueryFullProcessImageNameW(
|
|
1109
|
+
process.0,
|
|
1110
|
+
PROCESS_NAME_WIN32,
|
|
1111
|
+
buffer.as_mut_ptr(),
|
|
1112
|
+
&mut length,
|
|
1113
|
+
)
|
|
1114
|
+
} == 0
|
|
1115
|
+
{
|
|
1116
|
+
return None;
|
|
1117
|
+
}
|
|
1118
|
+
Some(String::from_utf16_lossy(&buffer[..length as usize]))
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
fn in_job(process: &Owned, job: &Owned) -> Result<bool, Failure> {
|
|
1122
|
+
let mut result = 0;
|
|
1123
|
+
// SAFETY: both handles are open, the process's with PROCESS_QUERY_LIMITED_INFORMATION and the job's with JOB_OBJECT_QUERY.
|
|
1124
|
+
if unsafe { IsProcessInJob(process.0, job.0, &mut result) } == 0 {
|
|
1125
|
+
return Err(Failure::windows(
|
|
1126
|
+
"IsProcessInJob",
|
|
1127
|
+
"Windows would not say whether a process is in a job.",
|
|
1128
|
+
));
|
|
1129
|
+
}
|
|
1130
|
+
Ok(result != 0)
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// A copy of `job`'s handle inside `target`, not inherited by anything `target` starts: a child that inherited it would
|
|
1134
|
+
// keep the job open after the runner ended.
|
|
1135
|
+
fn duplicate_into(job: &Owned, target: &Owned, pid: u32) -> Result<usize, Failure> {
|
|
1136
|
+
let mut copy: HANDLE = null_mut();
|
|
1137
|
+
// SAFETY: both handles are open, the target's with PROCESS_DUP_HANDLE, and `copy` is writable.
|
|
1138
|
+
if unsafe {
|
|
1139
|
+
DuplicateHandle(
|
|
1140
|
+
GetCurrentProcess(),
|
|
1141
|
+
job.0,
|
|
1142
|
+
target.0,
|
|
1143
|
+
&mut copy,
|
|
1144
|
+
0,
|
|
1145
|
+
0,
|
|
1146
|
+
DUPLICATE_SAME_ACCESS,
|
|
1147
|
+
)
|
|
1148
|
+
} == 0
|
|
1149
|
+
{
|
|
1150
|
+
return Err(Failure::windows(
|
|
1151
|
+
"DuplicateHandle",
|
|
1152
|
+
format!("The job could not be given to process {pid}, so nothing was put in it."),
|
|
1153
|
+
));
|
|
1154
|
+
}
|
|
1155
|
+
Ok(copy as usize)
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// Closes a handle inside another process; a failure leaves it open, which only keeps an empty job alive.
|
|
1159
|
+
fn close_in(target: &Owned, value: usize) {
|
|
1160
|
+
// SAFETY: the target's handle is open with PROCESS_DUP_HANDLE; with DUPLICATE_CLOSE_SOURCE and no target process,
|
|
1161
|
+
// Windows only closes the source handle.
|
|
1162
|
+
unsafe {
|
|
1163
|
+
DuplicateHandle(
|
|
1164
|
+
target.0,
|
|
1165
|
+
value as HANDLE,
|
|
1166
|
+
null_mut(),
|
|
1167
|
+
null_mut(),
|
|
1168
|
+
0,
|
|
1169
|
+
0,
|
|
1170
|
+
DUPLICATE_CLOSE_SOURCE,
|
|
1171
|
+
)
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
fn wide(text: &str) -> Vec<u16> {
|
|
1176
|
+
OsStr::new(text).encode_wide().chain(once(0)).collect()
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// These cases share one process as the runner and read its whole job, so they run one at a time:
|
|
1180
|
+
// `cargo test -- --test-threads=1`.
|
|
1181
|
+
#[cfg(test)]
|
|
1182
|
+
mod tests {
|
|
1183
|
+
use super::*;
|
|
1184
|
+
use std::os::windows::process::CommandExt;
|
|
1185
|
+
use std::process::{Child, Command as Spawn, Stdio};
|
|
1186
|
+
use std::sync::OnceLock;
|
|
1187
|
+
use std::thread::sleep;
|
|
1188
|
+
use std::time::{Duration, Instant};
|
|
1189
|
+
|
|
1190
|
+
use crate::command::JobName;
|
|
1191
|
+
|
|
1192
|
+
// A console of its own with no window, as Retest starts an app server.
|
|
1193
|
+
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
|
1194
|
+
|
|
1195
|
+
fn unique(label: &str) -> JobName {
|
|
1196
|
+
let nanos = std::time::SystemTime::now()
|
|
1197
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
1198
|
+
.map(|elapsed| elapsed.as_nanos())
|
|
1199
|
+
.unwrap_or_default();
|
|
1200
|
+
JobName::parse(&format!(
|
|
1201
|
+
"Local\\retest-test-{}-{label}-{nanos}",
|
|
1202
|
+
std::process::id()
|
|
1203
|
+
))
|
|
1204
|
+
.expect("a valid job name")
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// This test process plays the runner: it is put once in a job of its own, as `hold` puts Retest, and keeps that
|
|
1208
|
+
// job's handle until it exits. Children it starts from then on are born in that job.
|
|
1209
|
+
fn runner_job() -> &'static JobName {
|
|
1210
|
+
static JOB: OnceLock<JobName> = OnceLock::new();
|
|
1211
|
+
JOB.get_or_init(|| {
|
|
1212
|
+
let name = unique("runner");
|
|
1213
|
+
let job = create_job(&name).unwrap_or_else(|failure| panic!("{}", failure.problem));
|
|
1214
|
+
// SAFETY: the job's handle is open with every right, and GetCurrentProcess names this process.
|
|
1215
|
+
assert!(unsafe { AssignProcessToJobObject(job.0, GetCurrentProcess()) } != 0);
|
|
1216
|
+
std::mem::forget(job);
|
|
1217
|
+
name
|
|
1218
|
+
})
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// `cmd` that starts a `ping` of its own and waits for it: a child with a descendant, as a shell running a server is.
|
|
1222
|
+
fn shell_with_helper() -> Child {
|
|
1223
|
+
runner_job();
|
|
1224
|
+
Spawn::new("cmd.exe")
|
|
1225
|
+
.args(["/d", "/c", "ping -n 30 127.0.0.1 >nul"])
|
|
1226
|
+
.stdout(Stdio::null())
|
|
1227
|
+
.stderr(Stdio::null())
|
|
1228
|
+
.spawn()
|
|
1229
|
+
.expect("cmd starts")
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
fn adopted(runner: u32, name: &JobName, pid: u32) -> Reply {
|
|
1233
|
+
adopt(runner, runner_job(), name, pid)
|
|
1234
|
+
.unwrap_or_else(|failure| panic!("adopt failed: {}", failure.problem))
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
fn member_ids(name: &JobName) -> Vec<u32> {
|
|
1238
|
+
match members(name) {
|
|
1239
|
+
Ok(Reply::Members { processes, .. }) => {
|
|
1240
|
+
processes.iter().map(|process| process.pid).collect()
|
|
1241
|
+
}
|
|
1242
|
+
Ok(other) => panic!("members answered {other:?}"),
|
|
1243
|
+
Err(failure) => panic!("members failed: {}", failure.problem),
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
fn stray_ids(launches: &[JobName]) -> Vec<u32> {
|
|
1248
|
+
match strays(std::process::id(), runner_job(), launches) {
|
|
1249
|
+
Ok(Reply::Strays { processes, .. }) => {
|
|
1250
|
+
processes.iter().map(|process| process.pid).collect()
|
|
1251
|
+
}
|
|
1252
|
+
Ok(other) => panic!("strays answered {other:?}"),
|
|
1253
|
+
Err(failure) => panic!("strays failed: {}", failure.problem),
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
fn end_process(pid: u32) {
|
|
1258
|
+
if let Ok(Some(process)) = open_process(pid, PROCESS_TERMINATE) {
|
|
1259
|
+
// SAFETY: the handle is open with PROCESS_TERMINATE.
|
|
1260
|
+
unsafe { TerminateProcess(process.0, ENDED_EXIT_CODE) };
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
fn running_pid(pid: u32) -> bool {
|
|
1265
|
+
matches!(open_process(pid, PROCESS_QUERY_LIMITED_INFORMATION), Ok(Some(process)) if running(&process).unwrap_or(false))
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
fn children_of(parent: u32) -> Vec<u32> {
|
|
1269
|
+
parent_ids()
|
|
1270
|
+
.unwrap_or_default()
|
|
1271
|
+
.into_iter()
|
|
1272
|
+
.filter_map(|(pid, of)| (of == parent).then_some(pid))
|
|
1273
|
+
.collect()
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
fn image_ends_with(pid: u32, name: &str) -> bool {
|
|
1277
|
+
open_process(pid, PROCESS_QUERY_LIMITED_INFORMATION)
|
|
1278
|
+
.ok()
|
|
1279
|
+
.flatten()
|
|
1280
|
+
.and_then(|process| image_of(&process))
|
|
1281
|
+
.is_some_and(|image| image.to_lowercase().ends_with(name))
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
fn wait_for(what: &str, mut done: impl FnMut() -> bool) {
|
|
1285
|
+
let started = Instant::now();
|
|
1286
|
+
while !done() {
|
|
1287
|
+
assert!(
|
|
1288
|
+
started.elapsed() < Duration::from_secs(10),
|
|
1289
|
+
"{what} did not happen within 10 s"
|
|
1290
|
+
);
|
|
1291
|
+
sleep(Duration::from_millis(25));
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
#[test]
|
|
1296
|
+
fn an_adopted_child_and_what_it_starts_are_members_until_the_job_ends_them() {
|
|
1297
|
+
let runner = std::process::id();
|
|
1298
|
+
let mut child = shell_with_helper();
|
|
1299
|
+
let name = unique("adopt");
|
|
1300
|
+
let Reply::Adopted {
|
|
1301
|
+
handle,
|
|
1302
|
+
root,
|
|
1303
|
+
unheld,
|
|
1304
|
+
complete,
|
|
1305
|
+
..
|
|
1306
|
+
} = adopted(runner, &name, child.id())
|
|
1307
|
+
else {
|
|
1308
|
+
panic!("adopt answered something else")
|
|
1309
|
+
};
|
|
1310
|
+
assert_eq!(root.pid, child.id());
|
|
1311
|
+
assert_eq!(root.parent_pid, Some(runner));
|
|
1312
|
+
assert!(
|
|
1313
|
+
unheld.is_empty(),
|
|
1314
|
+
"helpers left outside the job: {unheld:?}"
|
|
1315
|
+
);
|
|
1316
|
+
assert!(complete, "the search for earlier helpers finished");
|
|
1317
|
+
assert!(handle > 0 && handle % 4 == 0);
|
|
1318
|
+
wait_for("ping to start inside the job", || {
|
|
1319
|
+
member_ids(&name).len() >= 2
|
|
1320
|
+
});
|
|
1321
|
+
let ids = member_ids(&name);
|
|
1322
|
+
assert!(ids.contains(&child.id()), "{ids:?}");
|
|
1323
|
+
// Each member's parent and command line come from its own handle: the shell's are this process and its words,
|
|
1324
|
+
// and the ping's parent is the shell.
|
|
1325
|
+
let Ok(Reply::Members { processes, .. }) = members(&name) else {
|
|
1326
|
+
panic!("members failed")
|
|
1327
|
+
};
|
|
1328
|
+
let shell = processes
|
|
1329
|
+
.iter()
|
|
1330
|
+
.find(|process| process.pid == child.id())
|
|
1331
|
+
.expect("the shell is a member");
|
|
1332
|
+
assert_eq!(shell.parent_pid, Some(runner));
|
|
1333
|
+
assert!(
|
|
1334
|
+
shell
|
|
1335
|
+
.command_line
|
|
1336
|
+
.as_deref()
|
|
1337
|
+
.is_some_and(|line| line.contains("ping -n 30 127.0.0.1")),
|
|
1338
|
+
"{:?}",
|
|
1339
|
+
shell.command_line
|
|
1340
|
+
);
|
|
1341
|
+
let ping = processes
|
|
1342
|
+
.iter()
|
|
1343
|
+
.find(|process| process.pid != child.id())
|
|
1344
|
+
.expect("the ping is a member");
|
|
1345
|
+
assert_eq!(ping.parent_pid, Some(child.id()));
|
|
1346
|
+
assert!(
|
|
1347
|
+
ping.command_line
|
|
1348
|
+
.as_deref()
|
|
1349
|
+
.is_some_and(|line| line.to_lowercase().contains("ping")),
|
|
1350
|
+
"{:?}",
|
|
1351
|
+
ping.command_line
|
|
1352
|
+
);
|
|
1353
|
+
assert!(matches!(end(runner, &name), Ok(Reply::Ended { .. })));
|
|
1354
|
+
wait_for("the job to empty", || member_ids(&name).is_empty());
|
|
1355
|
+
assert!(child.wait().is_ok());
|
|
1356
|
+
assert!(matches!(
|
|
1357
|
+
release(runner, &name, handle),
|
|
1358
|
+
Ok(Reply::Released { .. })
|
|
1359
|
+
));
|
|
1360
|
+
assert!(members(&name).is_err(), "a released job is no longer held");
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
#[test]
|
|
1364
|
+
fn a_child_that_ended_before_it_was_held_is_named_and_what_it_started_is_held() {
|
|
1365
|
+
let runner = std::process::id();
|
|
1366
|
+
runner_job();
|
|
1367
|
+
let mut child = Spawn::new("cmd.exe")
|
|
1368
|
+
.args(["/d", "/c", "start /b ping -n 30 127.0.0.1 >nul"])
|
|
1369
|
+
.stdout(Stdio::null())
|
|
1370
|
+
.stderr(Stdio::null())
|
|
1371
|
+
.spawn()
|
|
1372
|
+
.expect("cmd starts");
|
|
1373
|
+
// Waiting keeps this process's handle on the child, so its pid stays its own, as Node's does.
|
|
1374
|
+
assert!(child.wait().is_ok());
|
|
1375
|
+
let name = unique("ended");
|
|
1376
|
+
let Reply::Adopted {
|
|
1377
|
+
handle,
|
|
1378
|
+
root,
|
|
1379
|
+
ended,
|
|
1380
|
+
late,
|
|
1381
|
+
..
|
|
1382
|
+
} = adopted(runner, &name, child.id())
|
|
1383
|
+
else {
|
|
1384
|
+
panic!("adopt answered something else")
|
|
1385
|
+
};
|
|
1386
|
+
assert!(ended, "the shell had ended");
|
|
1387
|
+
assert_eq!(root.pid, child.id());
|
|
1388
|
+
assert!(
|
|
1389
|
+
late.iter().any(|process| process
|
|
1390
|
+
.image
|
|
1391
|
+
.as_deref()
|
|
1392
|
+
.is_some_and(|image| image.to_lowercase().ends_with("ping.exe"))),
|
|
1393
|
+
"the ping the shell left running is held: {late:?}"
|
|
1394
|
+
);
|
|
1395
|
+
assert!(!member_ids(&name).is_empty());
|
|
1396
|
+
assert!(matches!(end(runner, &name), Ok(Reply::Ended { .. })));
|
|
1397
|
+
wait_for("the job to empty", || member_ids(&name).is_empty());
|
|
1398
|
+
assert!(matches!(
|
|
1399
|
+
release(runner, &name, handle),
|
|
1400
|
+
Ok(Reply::Released { .. })
|
|
1401
|
+
));
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
#[test]
|
|
1405
|
+
fn a_taken_name_a_stranger_and_another_start_are_refused() {
|
|
1406
|
+
let runner = std::process::id();
|
|
1407
|
+
let mut child = shell_with_helper();
|
|
1408
|
+
let name = unique("taken");
|
|
1409
|
+
let Reply::Adopted { handle, .. } = adopted(runner, &name, child.id()) else {
|
|
1410
|
+
panic!("the first adoption failed")
|
|
1411
|
+
};
|
|
1412
|
+
assert!(
|
|
1413
|
+
matches!(adopt(runner, runner_job(), &name, child.id()), Err(failure) if failure.problem.contains("already has this name"))
|
|
1414
|
+
);
|
|
1415
|
+
// Process 4 is the System process, which this process did not start.
|
|
1416
|
+
assert!(
|
|
1417
|
+
matches!(adopt(runner, runner_job(), &unique("stranger"), 4), Err(failure) if failure.problem.contains("not started by"))
|
|
1418
|
+
);
|
|
1419
|
+
assert!(
|
|
1420
|
+
matches!(adopt(runner, &unique("nojob"), &unique("nowhere"), child.id()), Err(failure) if failure.problem.contains("no longer holds"))
|
|
1421
|
+
);
|
|
1422
|
+
assert!(
|
|
1423
|
+
matches!(release(runner, &name, handle), Err(failure) if failure.problem.contains("still running"))
|
|
1424
|
+
);
|
|
1425
|
+
let Ok(Reply::Process {
|
|
1426
|
+
process: Some(identity),
|
|
1427
|
+
in_job: Some(true),
|
|
1428
|
+
}) = process(child.id(), Some(&name))
|
|
1429
|
+
else {
|
|
1430
|
+
panic!("the child is not in its job")
|
|
1431
|
+
};
|
|
1432
|
+
assert!(matches!(
|
|
1433
|
+
terminate(runner, &name, child.id(), identity.started_at + 1),
|
|
1434
|
+
Ok(Reply::Terminated {
|
|
1435
|
+
outcome: Outcome::Different,
|
|
1436
|
+
..
|
|
1437
|
+
})
|
|
1438
|
+
));
|
|
1439
|
+
assert!(matches!(
|
|
1440
|
+
terminate(runner, &name, child.id(), identity.started_at),
|
|
1441
|
+
Ok(Reply::Terminated {
|
|
1442
|
+
outcome: Outcome::Terminated,
|
|
1443
|
+
..
|
|
1444
|
+
})
|
|
1445
|
+
));
|
|
1446
|
+
assert!(child.wait().is_ok());
|
|
1447
|
+
assert!(matches!(end(runner, &name), Ok(Reply::Ended { .. })));
|
|
1448
|
+
wait_for("the job to empty", || member_ids(&name).is_empty());
|
|
1449
|
+
assert!(matches!(
|
|
1450
|
+
release(runner, &name, handle),
|
|
1451
|
+
Ok(Reply::Released { .. })
|
|
1452
|
+
));
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// The adoption window: a shell starts a second shell, which starts a ping and ends before the first is held. The
|
|
1456
|
+
// ping's parent no longer runs, so the search cannot tie it to the launch; the runner's job still holds it, and
|
|
1457
|
+
// `strays` names it once nothing running stands behind it.
|
|
1458
|
+
#[test]
|
|
1459
|
+
fn a_grandchild_whose_parent_ended_before_the_hold_is_a_stray_of_the_runners_job() {
|
|
1460
|
+
let runner = std::process::id();
|
|
1461
|
+
runner_job();
|
|
1462
|
+
let mut child = Spawn::new("cmd.exe")
|
|
1463
|
+
.args([
|
|
1464
|
+
"/d",
|
|
1465
|
+
"/c",
|
|
1466
|
+
"cmd /d /c start /b ping -n 30 127.0.0.1 >nul & ping -n 30 127.0.0.1 >nul",
|
|
1467
|
+
])
|
|
1468
|
+
.stdout(Stdio::null())
|
|
1469
|
+
.stderr(Stdio::null())
|
|
1470
|
+
.spawn()
|
|
1471
|
+
.expect("cmd starts");
|
|
1472
|
+
// The inner shell ends as soon as it has started its ping, which then stands on nothing.
|
|
1473
|
+
let mut orphans = Vec::new();
|
|
1474
|
+
wait_for("the inner shell to start its ping and end", || {
|
|
1475
|
+
orphans = stray_ids(&[])
|
|
1476
|
+
.into_iter()
|
|
1477
|
+
.filter(|pid| image_ends_with(*pid, "ping.exe"))
|
|
1478
|
+
.collect();
|
|
1479
|
+
!orphans.is_empty()
|
|
1480
|
+
});
|
|
1481
|
+
let name = unique("window");
|
|
1482
|
+
let Reply::Adopted { handle, late, .. } = adopted(runner, &name, child.id()) else {
|
|
1483
|
+
panic!("adopt answered something else")
|
|
1484
|
+
};
|
|
1485
|
+
for pid in &orphans {
|
|
1486
|
+
assert!(
|
|
1487
|
+
late.iter().all(|process| process.pid != *pid),
|
|
1488
|
+
"the orphaned ping {pid} cannot be tied to the launch"
|
|
1489
|
+
);
|
|
1490
|
+
assert!(
|
|
1491
|
+
stray_ids(std::slice::from_ref(&name)).contains(pid),
|
|
1492
|
+
"the runner's job names the orphaned ping {pid}"
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
assert!(matches!(end(runner, &name), Ok(Reply::Ended { .. })));
|
|
1496
|
+
wait_for("the launch's job to empty", || member_ids(&name).is_empty());
|
|
1497
|
+
assert!(child.wait().is_ok());
|
|
1498
|
+
for pid in &orphans {
|
|
1499
|
+
assert!(
|
|
1500
|
+
running_pid(*pid),
|
|
1501
|
+
"ending the launch's job left {pid} running"
|
|
1502
|
+
);
|
|
1503
|
+
assert!(stray_ids(&[]).contains(pid));
|
|
1504
|
+
end_process(*pid);
|
|
1505
|
+
}
|
|
1506
|
+
assert!(matches!(
|
|
1507
|
+
release(runner, &name, handle),
|
|
1508
|
+
Ok(Reply::Released { .. })
|
|
1509
|
+
));
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// A helper born before the hold that is already in a job of its own inside the runner's, as a browser's sandboxed
|
|
1513
|
+
// helpers are, cannot be moved beside the launch: it is held by its parent, and no stray while its parent runs.
|
|
1514
|
+
#[test]
|
|
1515
|
+
fn a_helper_in_a_job_of_its_own_is_held_by_its_parent() {
|
|
1516
|
+
let runner = std::process::id();
|
|
1517
|
+
let mut child = shell_with_helper();
|
|
1518
|
+
let mut helper = Vec::new();
|
|
1519
|
+
wait_for("the shell to start its ping", || {
|
|
1520
|
+
helper = children_of(child.id());
|
|
1521
|
+
!helper.is_empty()
|
|
1522
|
+
});
|
|
1523
|
+
let sandbox = unique("sandbox");
|
|
1524
|
+
let sandbox_job =
|
|
1525
|
+
create_job(&sandbox).unwrap_or_else(|failure| panic!("{}", failure.problem));
|
|
1526
|
+
for pid in &helper {
|
|
1527
|
+
let process = open_process(*pid, PROCESS_SET_QUOTA | PROCESS_TERMINATE)
|
|
1528
|
+
.ok()
|
|
1529
|
+
.flatten()
|
|
1530
|
+
.expect("the ping opens");
|
|
1531
|
+
// SAFETY: both handles are open with the rights assignment needs.
|
|
1532
|
+
assert!(unsafe { AssignProcessToJobObject(sandbox_job.0, process.0) } != 0);
|
|
1533
|
+
}
|
|
1534
|
+
let name = unique("parented");
|
|
1535
|
+
let Reply::Adopted {
|
|
1536
|
+
handle,
|
|
1537
|
+
by_parent,
|
|
1538
|
+
unheld,
|
|
1539
|
+
complete,
|
|
1540
|
+
..
|
|
1541
|
+
} = adopted(runner, &name, child.id())
|
|
1542
|
+
else {
|
|
1543
|
+
panic!("adopt answered something else")
|
|
1544
|
+
};
|
|
1545
|
+
assert!(unheld.is_empty(), "nothing is left unheld: {unheld:?}");
|
|
1546
|
+
assert!(complete);
|
|
1547
|
+
for pid in &helper {
|
|
1548
|
+
assert!(
|
|
1549
|
+
by_parent.iter().any(|process| process.pid == *pid),
|
|
1550
|
+
"{pid} is held by its parent: {by_parent:?}"
|
|
1551
|
+
);
|
|
1552
|
+
assert!(
|
|
1553
|
+
!stray_ids(std::slice::from_ref(&name)).contains(pid),
|
|
1554
|
+
"a helper whose parent runs in the launch's job is no stray"
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
// Ending the sandbox's last handle ends the helper, as the browser's own handle does when the browser ends.
|
|
1558
|
+
drop(sandbox_job);
|
|
1559
|
+
assert!(matches!(end(runner, &name), Ok(Reply::Ended { .. })));
|
|
1560
|
+
assert!(child.wait().is_ok());
|
|
1561
|
+
wait_for("the helper to end with its job", || {
|
|
1562
|
+
helper.iter().all(|pid| !running_pid(*pid))
|
|
1563
|
+
});
|
|
1564
|
+
wait_for("the launch's job to empty", || member_ids(&name).is_empty());
|
|
1565
|
+
assert!(matches!(
|
|
1566
|
+
release(runner, &name, handle),
|
|
1567
|
+
Ok(Reply::Released { .. })
|
|
1568
|
+
));
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// Ctrl+C reaches a process on a console of its own, and not the runner. This process turns Ctrl+C on for itself
|
|
1572
|
+
// first, since one started from a service, as an SSH session or a CI step is, inherits it turned off, and the child
|
|
1573
|
+
// inherits it from here. Being the supervisor too, this process leaves its own console as it sends; a child that
|
|
1574
|
+
// shares the runner's console is refused by the real supervisor in tests/unit/process-supervisor-windows.test.ts.
|
|
1575
|
+
#[test]
|
|
1576
|
+
fn ctrl_c_reaches_a_console_of_its_own() {
|
|
1577
|
+
let runner = std::process::id();
|
|
1578
|
+
runner_job();
|
|
1579
|
+
// SAFETY: a null handler with FALSE turns Ctrl+C on for this process; it has no other preconditions.
|
|
1580
|
+
unsafe { SetConsoleCtrlHandler(None, 0) };
|
|
1581
|
+
let mut own = Spawn::new("ping.exe")
|
|
1582
|
+
.args(["-n", "30", "127.0.0.1"])
|
|
1583
|
+
.creation_flags(CREATE_NO_WINDOW)
|
|
1584
|
+
.stdout(Stdio::null())
|
|
1585
|
+
.stderr(Stdio::null())
|
|
1586
|
+
.spawn()
|
|
1587
|
+
.expect("ping starts");
|
|
1588
|
+
let name = unique("interrupt");
|
|
1589
|
+
let Reply::Adopted { handle, .. } = adopted(runner, &name, own.id()) else {
|
|
1590
|
+
panic!("adopt answered something else")
|
|
1591
|
+
};
|
|
1592
|
+
// The child makes its own console as it starts; until it has, no Ctrl+C can be sent to it.
|
|
1593
|
+
sleep(Duration::from_millis(500));
|
|
1594
|
+
let started = Instant::now();
|
|
1595
|
+
let (console, reached) = match interrupt(runner, &name, None, Key::CtrlC) {
|
|
1596
|
+
Ok(Reply::Interrupted {
|
|
1597
|
+
console, reached, ..
|
|
1598
|
+
}) => (console, reached),
|
|
1599
|
+
Ok(other) => panic!("interrupt answered {other:?}"),
|
|
1600
|
+
Err(failure) => panic!("Ctrl+C was not sent: {failure:?}"),
|
|
1601
|
+
};
|
|
1602
|
+
assert_eq!(console, own.id());
|
|
1603
|
+
assert!(!reached.contains(&runner));
|
|
1604
|
+
assert!(own.wait().is_ok());
|
|
1605
|
+
assert!(
|
|
1606
|
+
started.elapsed() < Duration::from_secs(10),
|
|
1607
|
+
"ping ended on Ctrl+C, long before its 30 pings"
|
|
1608
|
+
);
|
|
1609
|
+
wait_for("the job to empty", || member_ids(&name).is_empty());
|
|
1610
|
+
// Windows lists a process in its job for a moment after it ended, and release waits for that list to empty.
|
|
1611
|
+
wait_for("the job's handle to be given back", || {
|
|
1612
|
+
matches!(release(runner, &name, handle), Ok(Reply::Released { .. }))
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// A process's start and the clock each look of the search is stamped with are one clock. Measured here: how often,
|
|
1617
|
+
// and by how much, the start Windows gives a child reads earlier than the clock taken just before it was started.
|
|
1618
|
+
#[test]
|
|
1619
|
+
fn a_childs_start_reads_no_later_than_the_clock_once_it_has_started() {
|
|
1620
|
+
let mut early = 0u32;
|
|
1621
|
+
let mut widest = 0u64;
|
|
1622
|
+
for _ in 0..20 {
|
|
1623
|
+
let before = now();
|
|
1624
|
+
let mut child = Spawn::new("cmd.exe")
|
|
1625
|
+
.args(["/d", "/c", "exit 0"])
|
|
1626
|
+
.stdout(Stdio::null())
|
|
1627
|
+
.stderr(Stdio::null())
|
|
1628
|
+
.spawn()
|
|
1629
|
+
.expect("cmd starts");
|
|
1630
|
+
let after = now();
|
|
1631
|
+
let process = open_process(child.id(), PROCESS_QUERY_LIMITED_INFORMATION)
|
|
1632
|
+
.ok()
|
|
1633
|
+
.flatten()
|
|
1634
|
+
.expect("the child opens");
|
|
1635
|
+
let start = started_at(&process).expect("its start reads");
|
|
1636
|
+
assert!(
|
|
1637
|
+
start <= after,
|
|
1638
|
+
"a start later than the clock read once it had started"
|
|
1639
|
+
);
|
|
1640
|
+
if start < before {
|
|
1641
|
+
early += 1;
|
|
1642
|
+
widest = widest.max(before - start);
|
|
1643
|
+
}
|
|
1644
|
+
assert!(child.wait().is_ok());
|
|
1645
|
+
}
|
|
1646
|
+
println!(
|
|
1647
|
+
"starts read earlier than the clock taken before the launch: {early} of 20, by at most {widest} ticks"
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1650
|
+
}
|