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
//! A `boxlite serve` child process, shared by the REST end-to-end tests.
#![allow(dead_code)]
use std::net::TcpStream;
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
/// A `boxlite serve` child, torn down (gracefully) when dropped.
pub struct ServeChild {
child: Child,
port: u16,
api_key: Option<String>,
// Serve's own home. Dropped *after* the child is signalled, so its boxes are
// already stopped and the leaked-shim assertion in PerTestBoxHome's Drop is
// satisfied.
_home: boxlite_test_utils::home::PerTestBoxHome,
}
impl ServeChild {
pub fn start() -> Self {
Self::start_with(&[], None)
}
/// Start `boxlite serve` with extra `serve` arguments, e.g. `["--api-key",
/// "k"]`; pass the same key as `api_key` when it authenticates clients.
pub fn start_with(extra: &[&str], api_key: Option<&str>) -> Self {
let home = boxlite_test_utils::home::PerTestBoxHome::new();
// Ask the OS for a free port, then release it — a small race with
// whatever binds next, but the standard trick and good enough here.
let port = std::net::TcpListener::bind("127.0.0.1:0")
.expect("bind ephemeral")
.local_addr()
.unwrap()
.port();
let mut cmd = Command::new(env!("CARGO_BIN_EXE_boxlite"));
cmd.arg("--home").arg(&home.path);
cmd.arg("--config")
.arg(super::write_registry_config(&home.path));
cmd.args(["serve", "--host", "127.0.0.1", "--port", &port.to_string()])
.args(extra)
.stdout(Stdio::null())
.stderr(Stdio::null());
let child = cmd.spawn().expect("spawn boxlite serve");
let serve = Self {
child,
port,
api_key: api_key.map(str::to_string),
_home: home,
};
serve.wait_ready();
serve
}
pub fn port(&self) -> u16 {
self.port
}
pub fn url(&self) -> String {
format!("http://127.0.0.1:{}", self.port)
}
/// Run a `boxlite --url` client command against this server.
pub fn client(&self, args: &[&str]) -> std::process::Output {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_boxlite"));
cmd.args(["--url", &self.url()]).args(args);
if let Some(key) = &self.api_key {
cmd.env("BOXLITE_API_KEY", key);
}
cmd.output().expect("boxlite --url")
}
/// Wait until the server reports no boxes.
///
/// A box's teardown over REST is asynchronous — the main command exits, the
/// guest powers the VM off, the server's watcher notices and (for `--rm`)
/// removes it, all a beat after the client's `run` returns. Killing the
/// server while a box is still in that window would leak its shim, since the
/// server only runs its stop-everything path on SIGINT, not the SIGKILL a
/// dropped child gets. So drain first.
pub fn wait_until_no_boxes(&self) {
let deadline = Instant::now() + Duration::from_secs(30);
loop {
let out = self.client(&["ps", "-a"]);
let listed = String::from_utf8_lossy(&out.stdout)
.lines()
.filter(|l| l.contains("alpine"))
.count();
if out.status.success() && listed == 0 {
return;
}
assert!(
Instant::now() < deadline,
"server still has {listed} box(es) after the run — they never stopped"
);
std::thread::sleep(Duration::from_millis(500));
}
}
/// Poll a real HTTP call (`ps`) until serve answers — a bound socket is not
/// the same as a router that serves.
pub fn wait_ready(&self) {
let deadline = Instant::now() + Duration::from_secs(45);
while Instant::now() < deadline {
if TcpStream::connect(("127.0.0.1", self.port)).is_ok() {
let mut probe = Command::new(env!("CARGO_BIN_EXE_boxlite"));
probe.args(["--url", &self.url(), "ps"]);
if let Some(key) = &self.api_key {
probe.env("BOXLITE_API_KEY", key);
}
let ok = probe
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if ok {
return;
}
}
std::thread::sleep(Duration::from_millis(200));
}
panic!("boxlite serve did not become ready on port {}", self.port);
}
}
impl Drop for ServeChild {
fn drop(&mut self) {
// SIGINT: it is the signal serve wires to graceful shutdown
// (`with_graceful_shutdown(ctrl_c())`). The tests drain their boxes
// first, so this is just to end the process cleanly; a SIGKILL fallback
// below covers a serve that ignores it.
let pid = self.child.id().to_string();
let _ = Command::new("kill").args(["-s", "INT", &pid]).status();
let deadline = Instant::now() + Duration::from_secs(10);
loop {
match self.child.try_wait() {
Ok(Some(_)) => break,
Ok(None) if Instant::now() < deadline => {
std::thread::sleep(Duration::from_millis(100));
}
_ => {
let _ = self.child.kill();
let _ = self.child.wait();
break;
}
}
}
}
}