1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
//! Helpers for Windows integration tests. Not part of the product.
use std::path::Path;
use std::sync::mpsc::{self, Receiver};
use std::thread;
use crate::constants::{DEFAULT_PTY_COLS, DEFAULT_PTY_ROWS};
use crate::pal::ids::{AppId, JobId, PtyId};
use crate::pal::processes::{
AppSpawn, Breakaway, BuildTargetProcesses, Processes, ProcessesFacade,
};
use crate::pal::pseudoconsole::{Pseudoconsole, PseudoconsoleFacade, WindowSize};
/// A process started inside a test-owned pseudoconsole.
///
/// Integration tests use this so they do not depend on the runner having an
/// interactive console (implementation.md, "Integration tests").
#[derive(Debug)]
pub struct ConsoleProcess {
processes: ProcessesFacade,
pty_host: PseudoconsoleFacade,
app: AppId,
pty: PtyId,
job: JobId,
closed: bool,
}
impl ConsoleProcess {
/// Spawn `exe` with `args` in `cwd`, attached to a new `ConPTY`.
///
/// The surrounding job permits breakaway, which models the shell an SSH
/// session provides (implementation.md, "Job breakaway").
#[must_use]
pub fn spawn(exe: &Path, args: &[String], cwd: &Path) -> Self {
Self::spawn_in_jobs(exe, args, cwd, &[Breakaway::Permitted])
}
/// Spawn `exe` the way a launcher that confines its children would.
///
/// The surrounding job forbids breakaway, which models wrappers such as
/// `cargo run` that `dure run` must refuse to detach from
/// (implementation.md, "Job breakaway").
#[must_use]
pub fn spawn_confined(exe: &Path, args: &[String], cwd: &Path) -> Self {
Self::spawn_in_jobs(exe, args, cwd, &[Breakaway::Forbidden])
}
/// Spawn `exe` inside a permissive job that itself sits in a confining one.
///
/// Breakaway is evaluated against the immediate job only, so `CreateProcessW`
/// succeeds here and leaves the supervisor a member of the outer job. This
/// models the case `dure run` can only detect after the spawn
/// (implementation.md, "Job breakaway").
#[must_use]
pub fn spawn_confined_by_ancestor(exe: &Path, args: &[String], cwd: &Path) -> Self {
Self::spawn_in_jobs(
exe,
args,
cwd,
&[Breakaway::Forbidden, Breakaway::Permitted],
)
}
fn spawn_in_jobs(exe: &Path, args: &[String], cwd: &Path, jobs: &[Breakaway]) -> Self {
let processes = ProcessesFacade::target();
let pty_host = PseudoconsoleFacade::target();
let job = BuildTargetProcesses::create_job_chain(jobs).expect("create test job");
let pty = pty_host
.create(WindowSize {
cols: DEFAULT_PTY_COLS,
rows: DEFAULT_PTY_ROWS,
})
.expect("create test pseudoconsole");
let mut command = Vec::with_capacity(args.len().saturating_add(1));
command.push(exe.to_string_lossy().into_owned());
command.extend(args.iter().cloned());
let app = processes
.spawn_app(&AppSpawn {
command,
launch_directory: cwd.to_path_buf(),
pty,
job,
})
.expect("spawn test client in pseudoconsole");
Self {
processes,
pty_host,
app,
pty,
job,
closed: false,
}
}
/// Write bytes to the child's console input.
pub fn write_input(&self, data: &[u8]) {
self.pty_host
.write_input(self.pty, data)
.expect("write test console input");
}
/// Console output as it arrives, ending once the child has exited.
///
/// A pseudoconsole keeps its read side open for as long as this process
/// holds it, so a caller waiting for a phrase the child never printed would
/// wait forever, including under mutation testing where the workspace
/// watchdog is disabled. Ending the pseudoconsole once the child is gone
/// ends the stream instead, turning that wait into a failed assertion,
/// after delivering everything the child did write.
/// Ref: docs/testing.md, "Tests must not hang".
#[must_use]
pub fn output_until_exit(&self) -> Receiver<Vec<u8>> {
let (sender, receiver) = mpsc::channel();
thread::spawn({
let pty_host = self.pty_host.clone();
let pty = self.pty;
move || {
loop {
match pty_host.read_output(pty) {
Ok(bytes) if bytes.is_empty() => break,
Ok(bytes) => {
if sender.send(bytes).is_err() {
break;
}
}
Err(_error) => break,
}
}
}
});
thread::spawn({
let processes = self.processes.clone();
let pty_host = self.pty_host.clone();
let app = self.app;
let pty = self.pty;
move || {
_ = processes.wait_app(app);
pty_host.finish(pty);
}
});
receiver
}
/// Wait for the child to exit and tear down the job and pseudoconsole.
///
/// Output is drained on a helper thread so a child that writes to the
/// pseudoconsole cannot block on a full pipe while this wait runs.
#[must_use]
pub fn wait(mut self) -> i32 {
let drain = thread::spawn({
let pty_host = self.pty_host.clone();
let pty = self.pty;
move || loop {
match pty_host.read_output(pty) {
Ok(bytes) if bytes.is_empty() => break,
Ok(_) => {}
Err(_) => break,
}
}
});
let status = self.processes.wait_app(self.app).expect("wait test child");
self.shutdown();
_ = drain.join();
status
}
fn shutdown(&mut self) {
if self.closed {
return;
}
// Same ordering the supervisor's teardown relies on: the child stays
// attached to the pseudoconsole until the job that owns its lifetime is
// closed, and closing a pseudoconsole waits for its attached clients. A
// drop while the child is still running would otherwise never reach
// `close_job`.
self.processes.close_job(self.job);
self.pty_host.close(self.pty);
self.closed = true;
}
}
impl Drop for ConsoleProcess {
fn drop(&mut self) {
self.shutdown();
}
}