@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,1058 @@
|
|
|
1
|
+
//! Launch identities for an encoder and every process beneath it, on Windows.
|
|
2
|
+
//!
|
|
3
|
+
//! Windows has no `ps`, no process groups and no signals. The encoder is created suspended, put in a job object of its
|
|
4
|
+
//! own and only then let run, so it and everything it starts are in that job from birth, whatever becomes of their
|
|
5
|
+
//! parents: the job is the witness Unix has in the process group, and its member list is what remains. A member is
|
|
6
|
+
//! opened, checked to be in the job through its own handle, and recorded with its creation time and executable path.
|
|
7
|
+
//! The handle is kept: no other process can take a pid while a handle on its process is open, so the process verified
|
|
8
|
+
//! is the process ended. A member that can be read but not ended, or not read at all, is unknown: it is never ended,
|
|
9
|
+
//! it is named in the problem by pid and executable, and while it runs the job, and so cleanup, is unconfirmed.
|
|
10
|
+
//!
|
|
11
|
+
//! The job does not end its members when its handle closes. Like an encoder on Unix, one whose media process dies is
|
|
12
|
+
//! left to finish its file once its input closes, and what is left is for the runner that holds the media process to
|
|
13
|
+
//! end. Image paths stay in memory; problems name only the executable's file name.
|
|
14
|
+
|
|
15
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
16
|
+
use std::ffi::OsString;
|
|
17
|
+
use std::io;
|
|
18
|
+
use std::mem::offset_of;
|
|
19
|
+
use std::os::windows::ffi::OsStringExt;
|
|
20
|
+
use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle};
|
|
21
|
+
use std::path::Path;
|
|
22
|
+
use std::process::Child;
|
|
23
|
+
use std::thread;
|
|
24
|
+
use std::time::{Duration, Instant};
|
|
25
|
+
|
|
26
|
+
use windows_sys::Win32::Foundation::{
|
|
27
|
+
ERROR_ACCESS_DENIED, ERROR_BAD_LENGTH, ERROR_INVALID_PARAMETER, ERROR_MORE_DATA, FILETIME,
|
|
28
|
+
HANDLE, INVALID_HANDLE_VALUE, WAIT_OBJECT_0, WAIT_TIMEOUT,
|
|
29
|
+
};
|
|
30
|
+
use windows_sys::Win32::System::Diagnostics::ToolHelp::{
|
|
31
|
+
CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW, TH32CS_SNAPPROCESS,
|
|
32
|
+
TH32CS_SNAPTHREAD, THREADENTRY32, Thread32First, Thread32Next,
|
|
33
|
+
};
|
|
34
|
+
use windows_sys::Win32::System::JobObjects::{
|
|
35
|
+
AssignProcessToJobObject, CreateJobObjectW, IsProcessInJob, JOBOBJECT_BASIC_PROCESS_ID_LIST,
|
|
36
|
+
JobObjectBasicProcessIdList, QueryInformationJobObject,
|
|
37
|
+
};
|
|
38
|
+
use windows_sys::Win32::System::Threading::{
|
|
39
|
+
GetProcessTimes, OpenProcess, OpenThread, PROCESS_ACCESS_RIGHTS, PROCESS_NAME_WIN32,
|
|
40
|
+
PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SYNCHRONIZE, PROCESS_TERMINATE,
|
|
41
|
+
QueryFullProcessImageNameW, ResumeThread, THREAD_SUSPEND_RESUME, TerminateProcess,
|
|
42
|
+
WaitForSingleObject,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/// The exit code of a process ended here: the code `Child::kill` gives the launch.
|
|
46
|
+
const ENDED_EXIT_CODE: u32 = 1;
|
|
47
|
+
/// How often a snapshot that failed with `ERROR_BAD_LENGTH`, as one taken while processes start can, is taken again.
|
|
48
|
+
const SNAPSHOT_ATTEMPTS: usize = 5;
|
|
49
|
+
/// The longest image path Windows writes, in UTF-16 units.
|
|
50
|
+
const IMAGE_UNITS: usize = 32 * 1024;
|
|
51
|
+
/// How long a process that refuses TerminateProcess because it is already exiting gets to finish exiting.
|
|
52
|
+
const EXITING_WAIT_MS: u32 = 1000;
|
|
53
|
+
/// The most members a job's list is read for; a longer list fails the reading rather than growing without bound.
|
|
54
|
+
const MAX_MEMBERS: usize = 64 * 1024;
|
|
55
|
+
|
|
56
|
+
/// What tells one process from another that later has its pid.
|
|
57
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
58
|
+
pub struct Birth {
|
|
59
|
+
/// When the process was created, in 100 ns intervals since 1601.
|
|
60
|
+
created: u64,
|
|
61
|
+
/// The executable, or `None` when it could not be read.
|
|
62
|
+
image: Option<OsString>,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
impl Birth {
|
|
66
|
+
// An image path that could not be read is no difference; two readable paths that differ are refused.
|
|
67
|
+
fn matches(&self, other: &Birth) -> bool {
|
|
68
|
+
self.created == other.created
|
|
69
|
+
&& (self.image == other.image || self.image.is_none() || other.image.is_none())
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// The executable's file name, for a problem that names a process.
|
|
73
|
+
fn name(&self) -> String {
|
|
74
|
+
self.image
|
|
75
|
+
.as_deref()
|
|
76
|
+
.and_then(|image| Path::new(image).file_name())
|
|
77
|
+
.map_or_else(
|
|
78
|
+
|| "an unreadable executable".to_owned(),
|
|
79
|
+
|name| name.to_string_lossy().into_owned(),
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// How far a pid the job lists could be opened.
|
|
85
|
+
pub enum Opened<P> {
|
|
86
|
+
/// In the job, opened for reading, waiting and ending.
|
|
87
|
+
Endable(P, Birth),
|
|
88
|
+
/// In the job, and readable only: it can be named, never ended.
|
|
89
|
+
Readable(Birth),
|
|
90
|
+
/// Not in the job: it has gone, and the pid names no process or another one.
|
|
91
|
+
Elsewhere,
|
|
92
|
+
/// Not readable at all, named by the executable a process snapshot gives for the pid when it gives one.
|
|
93
|
+
Unreadable(Option<String>),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// What the host can read and do. `System` is Windows itself; tests stand in for it.
|
|
97
|
+
pub trait Host {
|
|
98
|
+
/// A process opened for reading, waiting and ending, holding its pid while it is kept.
|
|
99
|
+
type Process;
|
|
100
|
+
/// The job a launch was born in.
|
|
101
|
+
type Job;
|
|
102
|
+
fn members(&self, job: &Self::Job) -> Result<Vec<u32>, String>;
|
|
103
|
+
fn open(&self, pid: u32, job: &Self::Job) -> Opened<Self::Process>;
|
|
104
|
+
fn birth(&self, process: &Self::Process) -> Result<Birth, String>;
|
|
105
|
+
fn running(&self, process: &Self::Process) -> Result<bool, String>;
|
|
106
|
+
fn in_job(&self, process: &Self::Process, job: &Self::Job) -> Result<bool, String>;
|
|
107
|
+
fn terminate(&self, process: &Self::Process) -> io::Result<()>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
struct Record<P> {
|
|
111
|
+
birth: Birth,
|
|
112
|
+
process: P,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
pub struct Ownership<H: Host = System> {
|
|
116
|
+
host: H,
|
|
117
|
+
job: H::Job,
|
|
118
|
+
root: u32,
|
|
119
|
+
records: BTreeMap<u32, Record<H::Process>>,
|
|
120
|
+
initialized: bool,
|
|
121
|
+
retired: bool,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
impl Ownership {
|
|
125
|
+
/// Puts a launch created suspended in a job of its own, then lets it run. A launch that cannot be held is the
|
|
126
|
+
/// caller's to end through its handle; it never ran.
|
|
127
|
+
pub fn hold(launch: &mut Child) -> io::Result<Self> {
|
|
128
|
+
let failed = |what: &str| {
|
|
129
|
+
let error = io::Error::last_os_error();
|
|
130
|
+
io::Error::new(
|
|
131
|
+
error.kind(),
|
|
132
|
+
format!("the encoder could not be {what}: {error}"),
|
|
133
|
+
)
|
|
134
|
+
};
|
|
135
|
+
// SAFETY: CreateJobObjectW with no attributes and no name returns a new, uninheritable handle, or null.
|
|
136
|
+
let job = unsafe { CreateJobObjectW(std::ptr::null(), std::ptr::null()) };
|
|
137
|
+
if job.is_null() {
|
|
138
|
+
return Err(failed("given a job of its own"));
|
|
139
|
+
}
|
|
140
|
+
// SAFETY: the handle is valid and owned by nothing else.
|
|
141
|
+
let job = Job(unsafe { OwnedHandle::from_raw_handle(job) });
|
|
142
|
+
// SAFETY: both handles are live: the job's, and the launch's own, which `Child` holds.
|
|
143
|
+
if unsafe { AssignProcessToJobObject(job.handle(), launch.as_raw_handle()) } == 0 {
|
|
144
|
+
return Err(failed("put in its job"));
|
|
145
|
+
}
|
|
146
|
+
resume(launch.id()).map_err(|error| {
|
|
147
|
+
io::Error::new(
|
|
148
|
+
error.kind(),
|
|
149
|
+
format!("the encoder could not be let run in its job: {error}"),
|
|
150
|
+
)
|
|
151
|
+
})?;
|
|
152
|
+
Ok(Self::on(System, job, launch.id()))
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
impl<H: Host> Ownership<H> {
|
|
157
|
+
fn on(host: H, job: H::Job, root: u32) -> Self {
|
|
158
|
+
Self {
|
|
159
|
+
host,
|
|
160
|
+
job,
|
|
161
|
+
root,
|
|
162
|
+
records: BTreeMap::new(),
|
|
163
|
+
initialized: false,
|
|
164
|
+
retired: false,
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// Records every member of the job not yet recorded. Membership is proved through each process's own handle, so a
|
|
169
|
+
/// pid listed and since given to another process is never taken for the member it was. No age is compared: a
|
|
170
|
+
/// process can enter the job only by being born in it, and a creation time cannot order a parent and a child
|
|
171
|
+
/// started within the same tick of the clock that stamps it.
|
|
172
|
+
pub fn capture(&mut self) -> Vec<String> {
|
|
173
|
+
if self.retired {
|
|
174
|
+
return Vec::new();
|
|
175
|
+
}
|
|
176
|
+
let initial = !self.initialized;
|
|
177
|
+
self.initialized = true;
|
|
178
|
+
let mut candidates: BTreeSet<u32> = match self.host.members(&self.job) {
|
|
179
|
+
Ok(members) => members.into_iter().collect(),
|
|
180
|
+
Err(error) => return vec![error],
|
|
181
|
+
};
|
|
182
|
+
// The launch is this process's own child, not yet reaped, so its pid names it even once the job no longer
|
|
183
|
+
// lists it.
|
|
184
|
+
if initial {
|
|
185
|
+
candidates.insert(self.root);
|
|
186
|
+
}
|
|
187
|
+
let mut unknown = Vec::new();
|
|
188
|
+
for pid in candidates {
|
|
189
|
+
if self.records.contains_key(&pid) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
match self.host.open(pid, &self.job) {
|
|
193
|
+
Opened::Endable(process, birth) => {
|
|
194
|
+
self.records.insert(pid, Record { birth, process });
|
|
195
|
+
}
|
|
196
|
+
Opened::Readable(birth) => unknown.push(format!("{pid} {}", birth.name())),
|
|
197
|
+
Opened::Unreadable(name) => unknown.push(format!(
|
|
198
|
+
"{pid} {}",
|
|
199
|
+
name.unwrap_or_else(|| "an unreadable executable".to_owned())
|
|
200
|
+
)),
|
|
201
|
+
Opened::Elsewhere => {}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if initial && !self.records.contains_key(&self.root) {
|
|
205
|
+
return vec![format!(
|
|
206
|
+
"launched encoder {} could not be recorded before it exited",
|
|
207
|
+
self.root
|
|
208
|
+
)];
|
|
209
|
+
}
|
|
210
|
+
if !unknown.is_empty() {
|
|
211
|
+
return vec![format!(
|
|
212
|
+
"encoder {} has processes whose launch ownership is unknown; they were left alone: {}",
|
|
213
|
+
self.root,
|
|
214
|
+
unknown.join(", ")
|
|
215
|
+
)];
|
|
216
|
+
}
|
|
217
|
+
Vec::new()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/// Stops the launch through `launch`, the child handle it was spawned as, and every other recorded process
|
|
221
|
+
/// through the handle kept with it, after reading it again.
|
|
222
|
+
pub fn stop(&mut self, launch: &mut Child) -> Vec<String> {
|
|
223
|
+
let mut problems = self.capture();
|
|
224
|
+
problems.extend(self.stop_with_launch(&mut || stop_child(launch)));
|
|
225
|
+
problems
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Each recorded process, newest first, is read again just before it is ended and ended only while it runs as the
|
|
229
|
+
// recorded process and is still in the job. The launch is ended through its own child handle.
|
|
230
|
+
fn stop_with_launch(&mut self, launch: &mut dyn FnMut() -> io::Result<()>) -> Vec<String> {
|
|
231
|
+
if self.retired {
|
|
232
|
+
return Vec::new();
|
|
233
|
+
}
|
|
234
|
+
let mut problems = Vec::new();
|
|
235
|
+
if let Err(error) = launch() {
|
|
236
|
+
problems.push(format!(
|
|
237
|
+
"recorded encoder process {} could not be stopped: {error}",
|
|
238
|
+
self.root
|
|
239
|
+
));
|
|
240
|
+
}
|
|
241
|
+
let mut newest_first: Vec<(&u32, &Record<H::Process>)> = self.records.iter().collect();
|
|
242
|
+
newest_first.sort_by(|(_, one), (_, other)| other.birth.created.cmp(&one.birth.created));
|
|
243
|
+
for (&pid, record) in newest_first {
|
|
244
|
+
if pid == self.root {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
match self.host.running(&record.process) {
|
|
248
|
+
Ok(true) => {}
|
|
249
|
+
Ok(false) => continue,
|
|
250
|
+
Err(error) => {
|
|
251
|
+
problems.push(error);
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
let same = self.host.birth(&record.process).and_then(|birth| {
|
|
256
|
+
self.host
|
|
257
|
+
.in_job(&record.process, &self.job)
|
|
258
|
+
.map(|in_job| in_job && record.birth.matches(&birth))
|
|
259
|
+
});
|
|
260
|
+
match same {
|
|
261
|
+
Ok(true) => {}
|
|
262
|
+
Ok(false) => {
|
|
263
|
+
problems.push(format!(
|
|
264
|
+
"recorded encoder process {pid} changed identity and was left alone"
|
|
265
|
+
));
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
Err(error) => {
|
|
269
|
+
problems.push(error);
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if let Err(error) = self.host.terminate(&record.process) {
|
|
274
|
+
problems.push(format!(
|
|
275
|
+
"recorded encoder process {pid} could not be stopped: {error}"
|
|
276
|
+
));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
problems
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/// Whether anything of the launch still runs: a member of its job that is not a recorded process seen to have
|
|
283
|
+
/// exited. A member this owner never recorded, readable or not, counts as running.
|
|
284
|
+
pub fn remains(&mut self) -> Result<bool, String> {
|
|
285
|
+
if self.retired {
|
|
286
|
+
return Ok(false);
|
|
287
|
+
}
|
|
288
|
+
let members = self.host.members(&self.job)?;
|
|
289
|
+
if self.records.is_empty() && !members.is_empty() {
|
|
290
|
+
return Err(
|
|
291
|
+
"encoder launch ownership was never recorded; completion is unknown".to_owned(),
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
let mut live = false;
|
|
295
|
+
for pid in members {
|
|
296
|
+
live |= match self.records.get(&pid) {
|
|
297
|
+
Some(record) => self.host.running(&record.process)?,
|
|
298
|
+
None => true,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
if !live {
|
|
302
|
+
self.retired = true;
|
|
303
|
+
}
|
|
304
|
+
Ok(live)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
pub fn cleanup(&mut self, launch: &mut Child, limit: Duration) -> Vec<String> {
|
|
308
|
+
let mut problems = self.stop(launch);
|
|
309
|
+
let start = Instant::now();
|
|
310
|
+
loop {
|
|
311
|
+
for problem in self.capture() {
|
|
312
|
+
if !problems.contains(&problem) {
|
|
313
|
+
problems.push(problem);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
match self.remains() {
|
|
317
|
+
Ok(false) => break,
|
|
318
|
+
Err(error) => {
|
|
319
|
+
problems.push(error);
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
Ok(true) if start.elapsed() >= limit => {
|
|
323
|
+
problems.push(
|
|
324
|
+
"encoder cleanup did not confirm every recorded process had stopped"
|
|
325
|
+
.to_owned(),
|
|
326
|
+
);
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
Ok(true) => thread::sleep(Duration::from_millis(20)),
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
problems
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Stops the launch through its child handle. `Child::kill` already treats a launch that has exited as stopped.
|
|
337
|
+
fn stop_child(child: &mut Child) -> io::Result<()> {
|
|
338
|
+
child.kill()
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/// Windows itself.
|
|
342
|
+
pub struct System;
|
|
343
|
+
|
|
344
|
+
/// A process handle opened to read the process's identity, wait for it and end it.
|
|
345
|
+
pub struct Process(OwnedHandle);
|
|
346
|
+
|
|
347
|
+
impl Process {
|
|
348
|
+
fn handle(&self) -> HANDLE {
|
|
349
|
+
self.0.as_raw_handle()
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/// The job a launch was born in. Nothing else holds its handle, and closing it ends no member.
|
|
354
|
+
pub struct Job(OwnedHandle);
|
|
355
|
+
|
|
356
|
+
impl Job {
|
|
357
|
+
fn handle(&self) -> HANDLE {
|
|
358
|
+
self.0.as_raw_handle()
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
impl Host for System {
|
|
363
|
+
type Process = Process;
|
|
364
|
+
type Job = Job;
|
|
365
|
+
|
|
366
|
+
fn members(&self, job: &Job) -> Result<Vec<u32>, String> {
|
|
367
|
+
let header = offset_of!(JOBOBJECT_BASIC_PROCESS_ID_LIST, ProcessIdList);
|
|
368
|
+
let mut capacity = 64;
|
|
369
|
+
loop {
|
|
370
|
+
// The list is read into words, so its header and its pid words are aligned as the structure needs.
|
|
371
|
+
let words = header.div_ceil(size_of::<usize>()) + capacity;
|
|
372
|
+
let mut buffer = vec![0usize; words];
|
|
373
|
+
let bytes = u32::try_from(words * size_of::<usize>()).unwrap_or(u32::MAX);
|
|
374
|
+
// SAFETY: the buffer holds `bytes` writable bytes, aligned for the structure, for the call to fill.
|
|
375
|
+
let read = unsafe {
|
|
376
|
+
QueryInformationJobObject(
|
|
377
|
+
job.handle(),
|
|
378
|
+
JobObjectBasicProcessIdList,
|
|
379
|
+
buffer.as_mut_ptr().cast(),
|
|
380
|
+
bytes,
|
|
381
|
+
std::ptr::null_mut(),
|
|
382
|
+
)
|
|
383
|
+
};
|
|
384
|
+
let error = io::Error::last_os_error();
|
|
385
|
+
// SAFETY: the buffer starts with the structure's two counts, which the call wrote or left zero.
|
|
386
|
+
let list = unsafe { &*buffer.as_ptr().cast::<JOBOBJECT_BASIC_PROCESS_ID_LIST>() };
|
|
387
|
+
if read != 0 {
|
|
388
|
+
let count = (list.NumberOfProcessIdsInList as usize).min(capacity);
|
|
389
|
+
// SAFETY: `count` pids follow the header inside the buffer, as the call wrote them.
|
|
390
|
+
let pids = unsafe {
|
|
391
|
+
std::slice::from_raw_parts(
|
|
392
|
+
buffer.as_ptr().cast::<u8>().add(header).cast::<usize>(),
|
|
393
|
+
count,
|
|
394
|
+
)
|
|
395
|
+
};
|
|
396
|
+
return Ok(pids.iter().map(|&pid| pid as u32).collect());
|
|
397
|
+
}
|
|
398
|
+
if error.raw_os_error() == Some(ERROR_MORE_DATA as i32) && capacity < MAX_MEMBERS {
|
|
399
|
+
capacity = (capacity * 2).max(list.NumberOfAssignedProcesses as usize + 8);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
return Err(format!("the encoder's job could not be read: {error}"));
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
fn open(&self, pid: u32, job: &Job) -> Opened<Process> {
|
|
407
|
+
let full = PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE | PROCESS_TERMINATE;
|
|
408
|
+
let (process, endable) = match open_process(pid, full) {
|
|
409
|
+
Ok(process) => (process, true),
|
|
410
|
+
Err(_) => match open_process(pid, PROCESS_QUERY_LIMITED_INFORMATION) {
|
|
411
|
+
Ok(process) => (process, false),
|
|
412
|
+
// No process has the pid now: the member has gone since the list was read.
|
|
413
|
+
Err(error) if error.raw_os_error() == Some(ERROR_INVALID_PARAMETER as i32) => {
|
|
414
|
+
return Opened::Elsewhere;
|
|
415
|
+
}
|
|
416
|
+
Err(_) => return Opened::Unreadable(snapshot_name(pid)),
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
match (self.in_job(&process, job), self.birth(&process)) {
|
|
420
|
+
(Ok(false), _) => Opened::Elsewhere,
|
|
421
|
+
(Ok(true), Ok(birth)) if endable => Opened::Endable(process, birth),
|
|
422
|
+
(Ok(true), Ok(birth)) => Opened::Readable(birth),
|
|
423
|
+
_ => Opened::Unreadable(snapshot_name(pid)),
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
fn birth(&self, process: &Process) -> Result<Birth, String> {
|
|
428
|
+
let mut created = FILETIME::default();
|
|
429
|
+
let mut exited = FILETIME::default();
|
|
430
|
+
let mut kernel = FILETIME::default();
|
|
431
|
+
let mut user = FILETIME::default();
|
|
432
|
+
// SAFETY: the handle is live and has PROCESS_QUERY_LIMITED_INFORMATION; the four times are written in place.
|
|
433
|
+
let read = unsafe {
|
|
434
|
+
GetProcessTimes(
|
|
435
|
+
process.handle(),
|
|
436
|
+
&mut created,
|
|
437
|
+
&mut exited,
|
|
438
|
+
&mut kernel,
|
|
439
|
+
&mut user,
|
|
440
|
+
)
|
|
441
|
+
};
|
|
442
|
+
if read == 0 {
|
|
443
|
+
return Err(format!(
|
|
444
|
+
"an encoder process's creation time could not be read: {}",
|
|
445
|
+
io::Error::last_os_error()
|
|
446
|
+
));
|
|
447
|
+
}
|
|
448
|
+
let mut image = vec![0u16; IMAGE_UNITS];
|
|
449
|
+
let mut length = IMAGE_UNITS as u32;
|
|
450
|
+
// SAFETY: the buffer holds `length` UTF-16 units, and the call writes at most that many and the new length.
|
|
451
|
+
let named = unsafe {
|
|
452
|
+
QueryFullProcessImageNameW(
|
|
453
|
+
process.handle(),
|
|
454
|
+
PROCESS_NAME_WIN32,
|
|
455
|
+
image.as_mut_ptr(),
|
|
456
|
+
&mut length,
|
|
457
|
+
)
|
|
458
|
+
};
|
|
459
|
+
Ok(Birth {
|
|
460
|
+
created: (u64::from(created.dwHighDateTime) << 32) | u64::from(created.dwLowDateTime),
|
|
461
|
+
image: (named != 0)
|
|
462
|
+
.then(|| OsString::from_wide(&image[..(length as usize).min(IMAGE_UNITS)])),
|
|
463
|
+
})
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
fn running(&self, process: &Process) -> Result<bool, String> {
|
|
467
|
+
// SAFETY: the handle is live and has SYNCHRONIZE; a zero timeout only reads whether the process has exited.
|
|
468
|
+
match unsafe { WaitForSingleObject(process.handle(), 0) } {
|
|
469
|
+
WAIT_TIMEOUT => Ok(true),
|
|
470
|
+
WAIT_OBJECT_0 => Ok(false),
|
|
471
|
+
_ => Err(format!(
|
|
472
|
+
"an encoder process's exit could not be read: {}",
|
|
473
|
+
io::Error::last_os_error()
|
|
474
|
+
)),
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
fn in_job(&self, process: &Process, job: &Job) -> Result<bool, String> {
|
|
479
|
+
let mut result = 0;
|
|
480
|
+
// SAFETY: both handles are live; the process handle has PROCESS_QUERY_LIMITED_INFORMATION.
|
|
481
|
+
if unsafe { IsProcessInJob(process.handle(), job.handle(), &mut result) } == 0 {
|
|
482
|
+
return Err(format!(
|
|
483
|
+
"an encoder process's job could not be read: {}",
|
|
484
|
+
io::Error::last_os_error()
|
|
485
|
+
));
|
|
486
|
+
}
|
|
487
|
+
Ok(result != 0)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
fn terminate(&self, process: &Process) -> io::Result<()> {
|
|
491
|
+
// SAFETY: the handle is live and has PROCESS_TERMINATE; it names only the process verified through it.
|
|
492
|
+
if unsafe { TerminateProcess(process.handle(), ENDED_EXIT_CODE) } != 0 {
|
|
493
|
+
return Ok(());
|
|
494
|
+
}
|
|
495
|
+
let error = io::Error::last_os_error();
|
|
496
|
+
// A process that has exited, or is exiting, answers TerminateProcess with ERROR_ACCESS_DENIED, as a
|
|
497
|
+
// console host does whose last program has just ended. It is stopped once it has exited.
|
|
498
|
+
if error.raw_os_error() == Some(ERROR_ACCESS_DENIED as i32)
|
|
499
|
+
// SAFETY: the handle is live and has SYNCHRONIZE; the wait only reads whether the process has exited.
|
|
500
|
+
&& unsafe { WaitForSingleObject(process.handle(), EXITING_WAIT_MS) } == WAIT_OBJECT_0
|
|
501
|
+
{
|
|
502
|
+
return Ok(());
|
|
503
|
+
}
|
|
504
|
+
Err(error)
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
fn open_process(pid: u32, access: PROCESS_ACCESS_RIGHTS) -> io::Result<Process> {
|
|
509
|
+
// SAFETY: OpenProcess takes plain values and returns a new handle, or null when the process cannot be opened.
|
|
510
|
+
let handle = unsafe { OpenProcess(access, 0, pid) };
|
|
511
|
+
if handle.is_null() {
|
|
512
|
+
return Err(io::Error::last_os_error());
|
|
513
|
+
}
|
|
514
|
+
// SAFETY: a non-null handle from OpenProcess is valid and owned by nothing else.
|
|
515
|
+
Ok(Process(unsafe { OwnedHandle::from_raw_handle(handle) }))
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Lets a launch created suspended run: its one thread, found in a thread snapshot by its process, is resumed.
|
|
519
|
+
fn resume(pid: u32) -> io::Result<()> {
|
|
520
|
+
let snapshot = snapshot(TH32CS_SNAPTHREAD)?;
|
|
521
|
+
let mut entry = THREADENTRY32 {
|
|
522
|
+
dwSize: size_of::<THREADENTRY32>() as u32,
|
|
523
|
+
..Default::default()
|
|
524
|
+
};
|
|
525
|
+
let mut resumed = 0;
|
|
526
|
+
// SAFETY: the entry's size is set, and the snapshot handle is live for every call.
|
|
527
|
+
let mut listed = unsafe { Thread32First(snapshot.as_raw_handle(), &mut entry) } != 0;
|
|
528
|
+
while listed {
|
|
529
|
+
if entry.th32OwnerProcessID == pid {
|
|
530
|
+
// SAFETY: OpenThread takes plain values and returns a new handle, or null.
|
|
531
|
+
let thread = unsafe { OpenThread(THREAD_SUSPEND_RESUME, 0, entry.th32ThreadID) };
|
|
532
|
+
if thread.is_null() {
|
|
533
|
+
return Err(io::Error::last_os_error());
|
|
534
|
+
}
|
|
535
|
+
// SAFETY: the handle is valid and owned by nothing else.
|
|
536
|
+
let thread = unsafe { OwnedHandle::from_raw_handle(thread) };
|
|
537
|
+
// SAFETY: the handle is live and has THREAD_SUSPEND_RESUME.
|
|
538
|
+
if unsafe { ResumeThread(thread.as_raw_handle()) } == u32::MAX {
|
|
539
|
+
return Err(io::Error::last_os_error());
|
|
540
|
+
}
|
|
541
|
+
resumed += 1;
|
|
542
|
+
}
|
|
543
|
+
// SAFETY: as above.
|
|
544
|
+
listed = unsafe { Thread32Next(snapshot.as_raw_handle(), &mut entry) } != 0;
|
|
545
|
+
}
|
|
546
|
+
if resumed == 0 {
|
|
547
|
+
return Err(io::Error::other("its suspended thread was not found"));
|
|
548
|
+
}
|
|
549
|
+
Ok(())
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// What a process snapshot calls the executable of `pid`, for naming a process that cannot be opened.
|
|
553
|
+
fn snapshot_name(pid: u32) -> Option<String> {
|
|
554
|
+
let snapshot = snapshot(TH32CS_SNAPPROCESS).ok()?;
|
|
555
|
+
let mut entry = PROCESSENTRY32W {
|
|
556
|
+
dwSize: size_of::<PROCESSENTRY32W>() as u32,
|
|
557
|
+
..Default::default()
|
|
558
|
+
};
|
|
559
|
+
// SAFETY: the entry's size is set, and the snapshot handle is live for every call.
|
|
560
|
+
let mut listed = unsafe { Process32FirstW(snapshot.as_raw_handle(), &mut entry) } != 0;
|
|
561
|
+
while listed {
|
|
562
|
+
if entry.th32ProcessID == pid {
|
|
563
|
+
let length = entry
|
|
564
|
+
.szExeFile
|
|
565
|
+
.iter()
|
|
566
|
+
.position(|&unit| unit == 0)
|
|
567
|
+
.unwrap_or(entry.szExeFile.len());
|
|
568
|
+
return Some(String::from_utf16_lossy(&entry.szExeFile[..length]));
|
|
569
|
+
}
|
|
570
|
+
// SAFETY: as above.
|
|
571
|
+
listed = unsafe { Process32NextW(snapshot.as_raw_handle(), &mut entry) } != 0;
|
|
572
|
+
}
|
|
573
|
+
None
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// A Toolhelp snapshot, taken again when it fails with ERROR_BAD_LENGTH, as one taken while processes start can.
|
|
577
|
+
fn snapshot(kind: u32) -> io::Result<OwnedHandle> {
|
|
578
|
+
let mut attempt = 1;
|
|
579
|
+
loop {
|
|
580
|
+
// SAFETY: CreateToolhelp32Snapshot takes plain values and returns a new handle or INVALID_HANDLE_VALUE.
|
|
581
|
+
let snapshot = unsafe { CreateToolhelp32Snapshot(kind, 0) };
|
|
582
|
+
if snapshot != INVALID_HANDLE_VALUE {
|
|
583
|
+
// SAFETY: the snapshot handle is valid and owned by nothing else.
|
|
584
|
+
return Ok(unsafe { OwnedHandle::from_raw_handle(snapshot) });
|
|
585
|
+
}
|
|
586
|
+
let error = io::Error::last_os_error();
|
|
587
|
+
if error.raw_os_error() != Some(ERROR_BAD_LENGTH as i32) || attempt == SNAPSHOT_ATTEMPTS {
|
|
588
|
+
return Err(error);
|
|
589
|
+
}
|
|
590
|
+
attempt += 1;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
#[cfg(test)]
|
|
595
|
+
mod tests {
|
|
596
|
+
use super::*;
|
|
597
|
+
use std::cell::RefCell;
|
|
598
|
+
use std::os::windows::process::CommandExt;
|
|
599
|
+
use std::process::{Command, Stdio};
|
|
600
|
+
use windows_sys::Win32::System::Threading::CREATE_SUSPENDED;
|
|
601
|
+
|
|
602
|
+
#[derive(Clone)]
|
|
603
|
+
struct Fake {
|
|
604
|
+
pid: u32,
|
|
605
|
+
created: u64,
|
|
606
|
+
image: &'static str,
|
|
607
|
+
running: bool,
|
|
608
|
+
/// Listed among the job's members.
|
|
609
|
+
listed: bool,
|
|
610
|
+
/// In the job, as its own handle tells.
|
|
611
|
+
in_job: bool,
|
|
612
|
+
endable: bool,
|
|
613
|
+
readable: bool,
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
fn fake(pid: u32, created: u64) -> Fake {
|
|
617
|
+
Fake {
|
|
618
|
+
pid,
|
|
619
|
+
created,
|
|
620
|
+
image: "C:\\ffmpeg\\bin\\ffmpeg.exe",
|
|
621
|
+
running: true,
|
|
622
|
+
listed: true,
|
|
623
|
+
in_job: true,
|
|
624
|
+
endable: true,
|
|
625
|
+
readable: true,
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Processes as a host would show them. A handle is the pid and the creation time it was opened at, so it keeps
|
|
630
|
+
// naming that process whatever else later has the pid.
|
|
631
|
+
#[derive(Default)]
|
|
632
|
+
struct FakeHost {
|
|
633
|
+
processes: RefCell<Vec<Fake>>,
|
|
634
|
+
terminated: RefCell<Vec<u32>>,
|
|
635
|
+
opened: RefCell<Vec<u32>>,
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
impl FakeHost {
|
|
639
|
+
fn with(processes: Vec<Fake>) -> Self {
|
|
640
|
+
Self {
|
|
641
|
+
processes: RefCell::new(processes),
|
|
642
|
+
..Default::default()
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
fn find(&self, process: &(u32, u64)) -> Option<Fake> {
|
|
647
|
+
self.processes
|
|
648
|
+
.borrow()
|
|
649
|
+
.iter()
|
|
650
|
+
.find(|entry| entry.pid == process.0 && entry.created == process.1)
|
|
651
|
+
.cloned()
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
fn change(&self, pid: u32, change: impl Fn(&mut Fake)) {
|
|
655
|
+
for entry in self.processes.borrow_mut().iter_mut() {
|
|
656
|
+
if entry.pid == pid {
|
|
657
|
+
change(entry);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
fn birth_of(entry: &Fake) -> Birth {
|
|
664
|
+
Birth {
|
|
665
|
+
created: entry.created,
|
|
666
|
+
image: Some(entry.image.into()),
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
impl Host for FakeHost {
|
|
671
|
+
type Process = (u32, u64);
|
|
672
|
+
type Job = ();
|
|
673
|
+
|
|
674
|
+
fn members(&self, _: &()) -> Result<Vec<u32>, String> {
|
|
675
|
+
Ok(self
|
|
676
|
+
.processes
|
|
677
|
+
.borrow()
|
|
678
|
+
.iter()
|
|
679
|
+
.filter(|entry| entry.listed)
|
|
680
|
+
.map(|entry| entry.pid)
|
|
681
|
+
.collect())
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
fn open(&self, pid: u32, _: &()) -> Opened<(u32, u64)> {
|
|
685
|
+
self.opened.borrow_mut().push(pid);
|
|
686
|
+
let found = self
|
|
687
|
+
.processes
|
|
688
|
+
.borrow()
|
|
689
|
+
.iter()
|
|
690
|
+
.find(|entry| entry.pid == pid)
|
|
691
|
+
.cloned();
|
|
692
|
+
match found {
|
|
693
|
+
None => Opened::Elsewhere,
|
|
694
|
+
Some(entry) if !entry.readable => Opened::Unreadable(Some("hidden.exe".to_owned())),
|
|
695
|
+
Some(entry) if !entry.in_job => Opened::Elsewhere,
|
|
696
|
+
Some(entry) if entry.endable => {
|
|
697
|
+
Opened::Endable((entry.pid, entry.created), birth_of(&entry))
|
|
698
|
+
}
|
|
699
|
+
Some(entry) => Opened::Readable(birth_of(&entry)),
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
fn birth(&self, process: &(u32, u64)) -> Result<Birth, String> {
|
|
704
|
+
let found = self
|
|
705
|
+
.processes
|
|
706
|
+
.borrow()
|
|
707
|
+
.iter()
|
|
708
|
+
.find(|entry| entry.pid == process.0)
|
|
709
|
+
.cloned()
|
|
710
|
+
.ok_or("no such process")?;
|
|
711
|
+
Ok(birth_of(&found))
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
fn running(&self, process: &(u32, u64)) -> Result<bool, String> {
|
|
715
|
+
Ok(self.find(process).is_some_and(|entry| entry.running))
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
fn in_job(&self, process: &(u32, u64), _: &()) -> Result<bool, String> {
|
|
719
|
+
Ok(self.find(process).is_some_and(|entry| entry.in_job))
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
fn terminate(&self, process: &(u32, u64)) -> io::Result<()> {
|
|
723
|
+
self.terminated.borrow_mut().push(process.0);
|
|
724
|
+
self.change(process.0, |entry| {
|
|
725
|
+
entry.running = false;
|
|
726
|
+
entry.listed = false;
|
|
727
|
+
});
|
|
728
|
+
Ok(())
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
fn owner(processes: Vec<Fake>) -> Ownership<FakeHost> {
|
|
733
|
+
Ownership::on(FakeHost::with(processes), (), 42)
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const UNKNOWN: &str =
|
|
737
|
+
"encoder 42 has processes whose launch ownership is unknown; they were left alone";
|
|
738
|
+
|
|
739
|
+
#[test]
|
|
740
|
+
fn every_member_is_recorded_and_ended_newest_first_and_the_launch_through_its_handle() {
|
|
741
|
+
let mut ownership = owner(vec![fake(42, 100), fake(43, 200), fake(44, 300)]);
|
|
742
|
+
assert!(ownership.capture().is_empty());
|
|
743
|
+
assert_eq!(
|
|
744
|
+
ownership.records.keys().copied().collect::<Vec<_>>(),
|
|
745
|
+
[42, 43, 44]
|
|
746
|
+
);
|
|
747
|
+
let mut launches = 0;
|
|
748
|
+
let problems = ownership.stop_with_launch(&mut || {
|
|
749
|
+
launches += 1;
|
|
750
|
+
Ok(())
|
|
751
|
+
});
|
|
752
|
+
assert!(problems.is_empty(), "{problems:?}");
|
|
753
|
+
assert_eq!(launches, 1, "the launch is ended through its own handle");
|
|
754
|
+
assert_eq!(
|
|
755
|
+
*ownership.host.terminated.borrow(),
|
|
756
|
+
[44, 43],
|
|
757
|
+
"newest first"
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
#[test]
|
|
762
|
+
fn a_member_born_in_the_same_clock_tick_as_the_launch_is_recorded() {
|
|
763
|
+
let mut ownership = owner(vec![fake(42, 100), fake(43, 100)]);
|
|
764
|
+
assert!(ownership.capture().is_empty());
|
|
765
|
+
assert!(ownership.records.contains_key(&43));
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
#[test]
|
|
769
|
+
fn a_listed_pid_whose_handle_is_outside_the_job_is_never_recorded() {
|
|
770
|
+
// 50 was a member and has gone; its pid now names a process outside the job.
|
|
771
|
+
let mut reused = fake(50, 400);
|
|
772
|
+
reused.in_job = false;
|
|
773
|
+
let mut ownership = owner(vec![fake(42, 100), reused]);
|
|
774
|
+
assert!(ownership.capture().is_empty());
|
|
775
|
+
assert!(!ownership.records.contains_key(&50));
|
|
776
|
+
assert!(ownership.stop_with_launch(&mut || Ok(())).is_empty());
|
|
777
|
+
assert!(ownership.host.terminated.borrow().is_empty());
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
#[test]
|
|
781
|
+
fn a_member_that_can_only_be_read_is_named_never_ended_and_keeps_completion_unconfirmed() {
|
|
782
|
+
let mut elevated = fake(43, 200);
|
|
783
|
+
elevated.endable = false;
|
|
784
|
+
elevated.image = "C:\\Tools\\elevated.exe";
|
|
785
|
+
let mut ownership = owner(vec![fake(42, 100), elevated]);
|
|
786
|
+
assert_eq!(ownership.capture(), [format!("{UNKNOWN}: 43 elevated.exe")]);
|
|
787
|
+
assert!(ownership.stop_with_launch(&mut || Ok(())).is_empty());
|
|
788
|
+
assert!(
|
|
789
|
+
ownership.host.terminated.borrow().is_empty(),
|
|
790
|
+
"an unverified process is never ended"
|
|
791
|
+
);
|
|
792
|
+
ownership.host.change(42, |entry| {
|
|
793
|
+
entry.running = false;
|
|
794
|
+
entry.listed = false;
|
|
795
|
+
});
|
|
796
|
+
assert_eq!(
|
|
797
|
+
ownership.remains(),
|
|
798
|
+
Ok(true),
|
|
799
|
+
"the job still lists the process"
|
|
800
|
+
);
|
|
801
|
+
ownership.host.change(43, |entry| entry.listed = false);
|
|
802
|
+
assert_eq!(ownership.remains(), Ok(false));
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
#[test]
|
|
806
|
+
fn a_member_that_cannot_be_read_is_named_from_a_snapshot_and_keeps_completion_unconfirmed() {
|
|
807
|
+
let mut hidden = fake(43, 200);
|
|
808
|
+
hidden.readable = false;
|
|
809
|
+
let mut ownership = owner(vec![fake(42, 100), hidden]);
|
|
810
|
+
assert_eq!(ownership.capture(), [format!("{UNKNOWN}: 43 hidden.exe")]);
|
|
811
|
+
ownership.host.change(42, |entry| entry.running = false);
|
|
812
|
+
assert_eq!(ownership.remains(), Ok(true));
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
#[test]
|
|
816
|
+
fn a_member_whose_parent_left_is_recorded_from_the_job_alone() {
|
|
817
|
+
let mut ownership = owner(vec![fake(42, 100)]);
|
|
818
|
+
assert!(ownership.capture().is_empty());
|
|
819
|
+
// The launch exits and leaves the list; a process it started lives on, still in the job.
|
|
820
|
+
ownership.host.change(42, |entry| {
|
|
821
|
+
entry.running = false;
|
|
822
|
+
entry.listed = false;
|
|
823
|
+
});
|
|
824
|
+
ownership.host.processes.borrow_mut().push(fake(44, 300));
|
|
825
|
+
assert_eq!(ownership.remains(), Ok(true));
|
|
826
|
+
assert!(ownership.capture().is_empty());
|
|
827
|
+
assert!(ownership.records.contains_key(&44));
|
|
828
|
+
assert!(ownership.stop_with_launch(&mut || Ok(())).is_empty());
|
|
829
|
+
assert_eq!(*ownership.host.terminated.borrow(), [44]);
|
|
830
|
+
assert_eq!(ownership.remains(), Ok(false));
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
#[test]
|
|
834
|
+
fn a_launch_that_cannot_be_opened_is_never_recorded() {
|
|
835
|
+
let mut launch = fake(42, 100);
|
|
836
|
+
launch.endable = false;
|
|
837
|
+
let mut ownership = owner(vec![launch]);
|
|
838
|
+
assert_eq!(
|
|
839
|
+
ownership.capture(),
|
|
840
|
+
["launched encoder 42 could not be recorded before it exited"]
|
|
841
|
+
);
|
|
842
|
+
assert!(ownership.records.is_empty());
|
|
843
|
+
assert!(
|
|
844
|
+
ownership.remains().is_err(),
|
|
845
|
+
"an unrecorded launch leaves completion unknown"
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
#[test]
|
|
850
|
+
fn a_launch_that_has_left_the_list_is_recorded_through_the_handle_that_holds_it() {
|
|
851
|
+
let mut launch = fake(42, 100);
|
|
852
|
+
launch.running = false;
|
|
853
|
+
launch.listed = false;
|
|
854
|
+
let mut ownership = owner(vec![launch]);
|
|
855
|
+
assert!(ownership.capture().is_empty());
|
|
856
|
+
assert!(ownership.records.contains_key(&42));
|
|
857
|
+
assert_eq!(ownership.remains(), Ok(false));
|
|
858
|
+
assert!(ownership.retired);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
#[test]
|
|
862
|
+
fn an_exited_member_still_listed_holds_nothing() {
|
|
863
|
+
let mut ownership = owner(vec![fake(42, 100), fake(43, 200)]);
|
|
864
|
+
assert!(ownership.capture().is_empty());
|
|
865
|
+
ownership.host.change(42, |entry| entry.running = false);
|
|
866
|
+
ownership.host.change(43, |entry| entry.running = false);
|
|
867
|
+
assert_eq!(ownership.remains(), Ok(false));
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
#[test]
|
|
871
|
+
fn a_recorded_process_whose_handle_reads_another_birth_or_job_is_left_alone() {
|
|
872
|
+
let changes: [fn(&mut Fake); 2] = [
|
|
873
|
+
|entry| entry.image = "C:\\Windows\\System32\\notepad.exe",
|
|
874
|
+
|entry| entry.in_job = false,
|
|
875
|
+
];
|
|
876
|
+
for change in changes {
|
|
877
|
+
let mut ownership = owner(vec![fake(42, 100), fake(43, 200)]);
|
|
878
|
+
assert!(ownership.capture().is_empty());
|
|
879
|
+
ownership.host.change(43, change);
|
|
880
|
+
let problems = ownership.stop_with_launch(&mut || Ok(()));
|
|
881
|
+
assert_eq!(
|
|
882
|
+
problems,
|
|
883
|
+
["recorded encoder process 43 changed identity and was left alone"]
|
|
884
|
+
);
|
|
885
|
+
assert!(ownership.host.terminated.borrow().is_empty());
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
#[test]
|
|
890
|
+
fn confirmed_exit_retires_the_owner_before_another_stop() {
|
|
891
|
+
let mut ownership = owner(vec![fake(42, 100), fake(43, 200)]);
|
|
892
|
+
assert!(ownership.capture().is_empty());
|
|
893
|
+
ownership.host.change(42, |entry| entry.listed = false);
|
|
894
|
+
ownership.host.change(43, |entry| entry.listed = false);
|
|
895
|
+
assert_eq!(ownership.remains(), Ok(false));
|
|
896
|
+
assert!(ownership.retired);
|
|
897
|
+
let mut launches = 0;
|
|
898
|
+
assert!(
|
|
899
|
+
ownership
|
|
900
|
+
.stop_with_launch(&mut || {
|
|
901
|
+
launches += 1;
|
|
902
|
+
Ok(())
|
|
903
|
+
})
|
|
904
|
+
.is_empty()
|
|
905
|
+
);
|
|
906
|
+
assert_eq!(launches, 0, "a retired owner stops nothing again");
|
|
907
|
+
let opened = ownership.host.opened.borrow().len();
|
|
908
|
+
assert!(ownership.capture().is_empty());
|
|
909
|
+
assert_eq!(
|
|
910
|
+
ownership.host.opened.borrow().len(),
|
|
911
|
+
opened,
|
|
912
|
+
"nor reads anything"
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
#[test]
|
|
917
|
+
fn a_birth_matches_only_the_same_creation_and_any_unreadable_image() {
|
|
918
|
+
let birth = Birth {
|
|
919
|
+
created: 100,
|
|
920
|
+
image: Some("C:\\ffmpeg.exe".into()),
|
|
921
|
+
};
|
|
922
|
+
assert!(birth.matches(&Birth {
|
|
923
|
+
image: None,
|
|
924
|
+
..birth.clone()
|
|
925
|
+
}));
|
|
926
|
+
assert!(!birth.matches(&Birth {
|
|
927
|
+
created: 101,
|
|
928
|
+
..birth.clone()
|
|
929
|
+
}));
|
|
930
|
+
assert!(!birth.matches(&Birth {
|
|
931
|
+
image: Some("C:\\other.exe".into()),
|
|
932
|
+
..birth.clone()
|
|
933
|
+
}));
|
|
934
|
+
assert_eq!(birth.name(), "ffmpeg.exe");
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Runs `command` through cmd.exe, held in a job as an encoder is. The command line goes as written, unquoted.
|
|
938
|
+
fn held(command: &str) -> (Child, Ownership) {
|
|
939
|
+
let mut child = Command::new("cmd.exe")
|
|
940
|
+
.raw_arg(format!("/d /c {command}"))
|
|
941
|
+
.creation_flags(CREATE_SUSPENDED)
|
|
942
|
+
.stdin(Stdio::null())
|
|
943
|
+
.stdout(Stdio::null())
|
|
944
|
+
.stderr(Stdio::null())
|
|
945
|
+
.spawn()
|
|
946
|
+
.expect("cmd.exe starts suspended");
|
|
947
|
+
let ownership = Ownership::hold(&mut child).expect("cmd.exe is held in a job and runs");
|
|
948
|
+
(child, ownership)
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
fn images(ownership: &Ownership) -> Vec<String> {
|
|
952
|
+
ownership
|
|
953
|
+
.records
|
|
954
|
+
.values()
|
|
955
|
+
.map(|record| record.birth.name().to_lowercase())
|
|
956
|
+
.collect()
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
fn watched(ownership: &Ownership) -> Vec<Process> {
|
|
960
|
+
ownership
|
|
961
|
+
.records
|
|
962
|
+
.keys()
|
|
963
|
+
.map(|&pid| {
|
|
964
|
+
open_process(pid, PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE)
|
|
965
|
+
.expect("a recorded process opens")
|
|
966
|
+
})
|
|
967
|
+
.collect()
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
fn wait_for_ping(ownership: &mut Ownership) {
|
|
971
|
+
let deadline = Instant::now() + Duration::from_secs(10);
|
|
972
|
+
while !images(ownership).iter().any(|image| image == "ping.exe") {
|
|
973
|
+
let problems = ownership.capture();
|
|
974
|
+
assert!(problems.is_empty(), "{problems:?}");
|
|
975
|
+
assert!(
|
|
976
|
+
Instant::now() < deadline,
|
|
977
|
+
"ping.exe never appeared in the job"
|
|
978
|
+
);
|
|
979
|
+
thread::sleep(Duration::from_millis(50));
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// A real launch through cmd.exe with a real child: both are in its job, recorded, ended and confirmed gone.
|
|
984
|
+
#[test]
|
|
985
|
+
fn a_real_launch_and_its_child_are_ended_and_confirmed_gone() {
|
|
986
|
+
let (mut child, mut ownership) = held("ping -n 60 127.0.0.1 >nul");
|
|
987
|
+
wait_for_ping(&mut ownership);
|
|
988
|
+
assert!(images(&ownership).iter().any(|image| image == "cmd.exe"));
|
|
989
|
+
let processes = watched(&ownership);
|
|
990
|
+
let problems = ownership.cleanup(&mut child, Duration::from_secs(5));
|
|
991
|
+
assert!(problems.is_empty(), "{problems:?}");
|
|
992
|
+
for process in &processes {
|
|
993
|
+
assert_eq!(
|
|
994
|
+
System.running(process),
|
|
995
|
+
Ok(false),
|
|
996
|
+
"every recorded process has exited"
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
assert!(child.wait().is_ok());
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// `start /b` leaves ping running after cmd.exe has exited, so ping's parent is gone before a capture sees ping.
|
|
1003
|
+
#[test]
|
|
1004
|
+
fn a_real_process_whose_parent_exited_first_is_recorded_from_the_job_and_ended() {
|
|
1005
|
+
let (mut child, mut ownership) = held("start /b ping -n 60 127.0.0.1 >nul");
|
|
1006
|
+
assert!(child.wait().expect("cmd.exe exits at once").success());
|
|
1007
|
+
wait_for_ping(&mut ownership);
|
|
1008
|
+
assert_eq!(ownership.remains(), Ok(true), "ping still runs in the job");
|
|
1009
|
+
let processes = watched(&ownership);
|
|
1010
|
+
let problems = ownership.cleanup(&mut child, Duration::from_secs(5));
|
|
1011
|
+
assert!(problems.is_empty(), "{problems:?}");
|
|
1012
|
+
for process in &processes {
|
|
1013
|
+
assert_eq!(System.running(process), Ok(false));
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
#[test]
|
|
1018
|
+
fn a_real_launch_that_exits_retires_its_owner() {
|
|
1019
|
+
let (mut child, mut ownership) = held("exit 3");
|
|
1020
|
+
assert!(ownership.capture().is_empty());
|
|
1021
|
+
let status = child.wait().expect("cmd.exe exits");
|
|
1022
|
+
assert_eq!(status.code(), Some(3));
|
|
1023
|
+
assert_eq!(ownership.remains(), Ok(false));
|
|
1024
|
+
assert!(ownership.retired);
|
|
1025
|
+
assert!(
|
|
1026
|
+
ownership
|
|
1027
|
+
.cleanup(&mut child, Duration::from_secs(1))
|
|
1028
|
+
.is_empty()
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
#[test]
|
|
1033
|
+
fn this_process_reads_as_itself_and_is_outside_the_launch_s_job() {
|
|
1034
|
+
let process = open_process(
|
|
1035
|
+
std::process::id(),
|
|
1036
|
+
PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE,
|
|
1037
|
+
)
|
|
1038
|
+
.expect("this process opens");
|
|
1039
|
+
let birth = System.birth(&process).expect("its birth reads");
|
|
1040
|
+
let image = birth.image.clone().expect("its image reads");
|
|
1041
|
+
assert_eq!(
|
|
1042
|
+
std::path::PathBuf::from(image).canonicalize().ok(),
|
|
1043
|
+
std::env::current_exe()
|
|
1044
|
+
.ok()
|
|
1045
|
+
.and_then(|path| path.canonicalize().ok())
|
|
1046
|
+
);
|
|
1047
|
+
assert!(birth.created > 0);
|
|
1048
|
+
assert_eq!(System.running(&process), Ok(true));
|
|
1049
|
+
assert!(snapshot_name(std::process::id()).is_some());
|
|
1050
|
+
let (mut child, mut ownership) = held("exit 0");
|
|
1051
|
+
assert_eq!(System.in_job(&process, &ownership.job), Ok(false));
|
|
1052
|
+
assert!(
|
|
1053
|
+
ownership
|
|
1054
|
+
.cleanup(&mut child, Duration::from_secs(5))
|
|
1055
|
+
.is_empty()
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
}
|