@mark1russell7/harness 0.14.2 → 0.14.4

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.
@@ -0,0 +1,23 @@
1
+ # The harness's client of Apache Kafka: kafka-clients 4.3.2 on Temurin 21.0.12.1. It runs as a
2
+ # container in the client's network namespace, as the harness's own TCP client does. It prints the
3
+ # same JSON lines. AdminKafka is the admin of a lab: it makes each topic, reads the state of a
4
+ # partition, and reads the whole partition after a run.
5
+ # lab.ts stages the build context: this folder, under harness/image/kafka. The build gets each jar
6
+ # from Maven Central and checks its SHA-256 (jars.sha256), thus a build cannot get another jar. The
7
+ # jars are kafka-clients and each runtime dependency of its POM.
8
+ FROM eclipse-temurin:21.0.12.1_1-jdk-alpine@sha256:0bfc69a4758a86710e5c474032d28400a8bd00874766f9e8b1642ac2fd293159 AS build
9
+ WORKDIR /build
10
+ COPY harness/image/kafka/jars.sha256 ./
11
+ RUN wget -q https://repo1.maven.org/maven2/org/apache/kafka/kafka-clients/4.3.2/kafka-clients-4.3.2.jar \
12
+ && wget -q https://repo1.maven.org/maven2/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar \
13
+ && wget -q https://repo1.maven.org/maven2/com/github/luben/zstd-jni/1.5.6-10/zstd-jni-1.5.6-10.jar \
14
+ && wget -q https://repo1.maven.org/maven2/at/yawk/lz4/lz4-java/1.11.4/lz4-java-1.11.4.jar \
15
+ && wget -q https://repo1.maven.org/maven2/org/xerial/snappy/snappy-java/1.1.10.11/snappy-java-1.1.10.11.jar \
16
+ && sha256sum -c jars.sha256
17
+ COPY harness/image/kafka/ClientKafka.java harness/image/kafka/AdminKafka.java ./
18
+ RUN javac --release 21 -cp "kafka-clients-4.3.2.jar:slf4j-api-1.7.36.jar" -d classes ClientKafka.java AdminKafka.java
19
+
20
+ FROM eclipse-temurin:21.0.12.1_1-jre-alpine@sha256:51ab5e3302e7141ce665ca3ea85e8b5cd648eafbc3c0c90dd79d6537684e4555
21
+ WORKDIR /opt/harness/kafka
22
+ COPY --from=build /build/kafka-clients-4.3.2.jar /build/slf4j-api-1.7.36.jar /build/zstd-jni-1.5.6-10.jar /build/lz4-java-1.11.4.jar /build/snappy-java-1.1.10.11.jar ./lib/
23
+ COPY --from=build /build/classes ./classes
@@ -0,0 +1,5 @@
1
+ edaf0818414a61a98d1d2e58d6966be8c9f780cef6ded2ffbab10dd841b2bfb8 kafka-clients-4.3.2.jar
2
+ d3ef575e3e4979678dc01bf1dcce51021493b4d11fb7f1be8ad982877c16a1c0 slf4j-api-1.7.36.jar
3
+ b6c3237e24b8252a5a9c3a0e1ae30ef8f323412e74bd358548b7a9e527d676df zstd-jni-1.5.6-10.jar
4
+ 58c8e0b813960d2a248e050c353baea73139b48a2ffc55382d42c69707e17325 lz4-java-1.11.4.jar
5
+ 58d245ec749fc608ecb175e934a16f2840442c5742aed91002cfd4cb9c6c7aee snappy-java-1.1.10.11.jar
@@ -1,5 +1,5 @@
1
1
  // client-mongo HOST PORT --mode idle|inflight|stream [--hosts H:P,H:P] [--set NAME] [--op write|read-long]
2
- // [--interval-ms N] [--max-s N] [--go PATH] [--w majority|1] [--before] [--options JSON]
2
+ // [--interval-ms N] [--max-s N] [--go PATH] [--w majority|1] [--before] [--from N] [--options JSON]
3
3
  //
4
4
  // The client of a MongoDB cell: the driver mongodb 7.7.0 on Node 24.21.0, against a replica set.
5
5
  // Each event is a JSON line with `ev` and `t`, the time on CLOCK_MONOTONIC in nanoseconds
@@ -28,7 +28,8 @@
28
28
  // write, waits for the file --go, sends one more operation into the fault, and holds the
29
29
  // connections after its end. stream sends a write each --interval-ms after the reply before, until
30
30
  // --max-s or SIGTERM. --before starts the client into a fault that holds already: its first
31
- // selection meets it.
31
+ // selection meets it. --from starts the numbers of a stream at N: a recovery drill starts a second
32
+ // client after a restore, and its numbers follow the numbers of the first client.
32
33
 
33
34
  import { randomUUID } from "node:crypto";
34
35
  import { existsSync, readFileSync } from "node:fs";
@@ -57,6 +58,7 @@ const max = Number(flag("--max-s") ?? 120) * 1000;
57
58
  const go = flag("--go") ?? "/tmp/go";
58
59
  const w = flag("--w") === "1" ? 1 : "majority";
59
60
  const before = argv.includes("--before");
61
+ const start = Number(flag("--from") ?? 1);
60
62
  const options = JSON.parse(flag("--options") ?? "{}");
61
63
 
62
64
  // ---- the two clocks: the wall clock of a log line of mongod goes onto the monotonic clock by them
@@ -242,7 +244,7 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
242
244
 
243
245
  if (mode === "stream") {
244
246
  emit("ready", { at: first });
245
- for (let n = 1; !stopped; n += 1) {
247
+ for (let n = start; !stopped; n += 1) {
246
248
  await operate(n);
247
249
  await sleep(interval);
248
250
  }
@@ -39,6 +39,9 @@
39
39
  //! - `resp ADDR:PORT ARG...`: one command to a Redis-protocol server, for example
40
40
  //! `SENTINEL FAILOVER mymaster` to a Sentinel. The stamps bracket the write of the command and
41
41
  //! the first line of its reply, so a planned failover starts at a known instant.
42
+ //! - `silence` / `clear-silence`: each packet of this namespace drops, in the three chains at one
43
+ //! commit, and no peer is named. The links stay up and ARP goes on, so a router in this namespace
44
+ //! goes silent, and its neighbors can detect that only by their own timers.
42
45
  //! - `query ADDR:PORT USER DATABASE SQL`: one simple query to a server of the PostgreSQL protocol
43
46
  //! that trusts the user, for example `PAUSE` on the console of PgBouncer. The stamps bracket the
44
47
  //! write of the query and its ReadyForQuery, after the login, so the end of a PAUSE is known.
@@ -88,6 +91,30 @@ fn until(at: u64) -> Result<(), Box<dyn Error>> {
88
91
  }
89
92
  }
90
93
 
94
+ /// The rules of a namespace that goes silent, or of the end of the silence.
95
+ fn silence(action: &str) -> String {
96
+ let op = if action == "silence" { "-I" } else { "-D" };
97
+ ["INPUT", "OUTPUT", "FORWARD"].map(|chain| format!("{op} {chain} -j DROP\n")).concat()
98
+ }
99
+
100
+ /// Commits rules in one iptables-restore at an instant, between two stamps.
101
+ fn commit(body: &str, at: Option<u64>) -> Result<(u64, u64), Box<dyn Error>> {
102
+ let script = format!("*filter\n{body}COMMIT\n");
103
+ if let Some(at) = at {
104
+ until(at)?;
105
+ }
106
+ let t0 = monotonic_ns();
107
+ let mut child =
108
+ Command::new("iptables-restore").arg("--noflush").stdin(Stdio::piped()).spawn()?;
109
+ child.stdin.take().ok_or("iptables-restore took no stdin")?.write_all(script.as_bytes())?;
110
+ let status = child.wait()?;
111
+ let t1 = monotonic_ns();
112
+ if !status.success() {
113
+ return Err(format!("iptables-restore exited {status}").into());
114
+ }
115
+ Ok((t0, t1))
116
+ }
117
+
91
118
  /// The options after the positional arguments: `--at NS`, `--direction D`, `--code C`, `--above N`,
92
119
  /// `--via DEV`.
93
120
  #[derive(Default)]
@@ -559,6 +586,12 @@ fn run() -> Result<(), Box<dyn Error>> {
559
586
  println!("{}", json!({ "action": action, "device": dev, "spec": spec, "t0": t0, "t1": t1 }));
560
587
  Ok(())
561
588
  }
589
+ "silence" | "clear-silence" => {
590
+ let opts = options(&args[1..])?;
591
+ let (t0, t1) = commit(&silence(action), opts.at)?;
592
+ println!("{}", json!({ "action": action, "t0": t0, "t1": t1 }));
593
+ Ok(())
594
+ }
562
595
  _ => {
563
596
  // parsed, never pasted: only addresses reach the rules
564
597
  let list = peers(args.get(1).ok_or("and the peer's address")?)?;
@@ -567,19 +600,7 @@ fn run() -> Result<(), Box<dyn Error>> {
567
600
  for peer in &list {
568
601
  body.push_str(&rules(action, *peer, &opts)?);
569
602
  }
570
- let script = format!("*filter\n{body}COMMIT\n");
571
- if let Some(at) = opts.at {
572
- until(at)?;
573
- }
574
- let t0 = monotonic_ns();
575
- let mut child =
576
- Command::new("iptables-restore").arg("--noflush").stdin(Stdio::piped()).spawn()?;
577
- child.stdin.take().ok_or("iptables-restore took no stdin")?.write_all(script.as_bytes())?;
578
- let status = child.wait()?;
579
- let t1 = monotonic_ns();
580
- if !status.success() {
581
- return Err(format!("iptables-restore exited {status}").into());
582
- }
603
+ let (t0, t1) = commit(&body, opts.at)?;
583
604
  let named: Vec<String> = list.iter().map(ToString::to_string).collect();
584
605
  println!("{}", json!({ "action": action, "peer": named.join(","), "t0": t0, "t1": t1 }));
585
606
  Ok(())
@@ -624,6 +645,12 @@ mod tests {
624
645
  assert!(rules("reject", peer, &Options::default()).is_err());
625
646
  }
626
647
 
648
+ #[test]
649
+ fn a_silence_drops_each_chain_and_its_end_takes_the_rules_out() {
650
+ assert_eq!(silence("silence"), "-I INPUT -j DROP\n-I OUTPUT -j DROP\n-I FORWARD -j DROP\n");
651
+ assert_eq!(silence("clear-silence"), "-D INPUT -j DROP\n-D OUTPUT -j DROP\n-D FORWARD -j DROP\n");
652
+ }
653
+
627
654
  #[test]
628
655
  fn a_router_gets_forward_rules_that_face_one_device() {
629
656
  let peer: Ipv4Addr = "10.0.0.2".parse().unwrap();
@@ -0,0 +1,374 @@
1
+ //! ports target PORT
2
+ //! ports client [--rounds N] [--close reset|client|target] [--destinations N] [--settle-ms N]
3
+ //! [--retry-ms N] [--wait-s N] HOST PORT [--mode M] [--max-s N]
4
+ //!
5
+ //! This program is the two ends of the port range cells: a target that accepts each connection,
6
+ //! and a client that connects until its range has no port. Each line is JSON with an `ev` and a `t`
7
+ //! on CLOCK_MONOTONIC.
8
+ //!
9
+ //! The target listens on PORT and on PORT + 2, the two destinations of a cell. It serves one command
10
+ //! on PORT + 1, its control port: `close COUNT` waits until it holds COUNT connections, and closes them.
11
+ //!
12
+ //! The client does these steps in each round:
13
+ //!
14
+ //! 1. It connects to each destination until a connect fails, and it holds each connection. A `round`
15
+ //! line gives the ports of each destination, the errno of the failed connect and its time.
16
+ //! 2. `--close reset` closes each connection with a reset. The target then closes its sockets.
17
+ //! 3. `--close client` closes each connection with a FIN, and it waits `--settle-ms`. The target
18
+ //! then closes its sockets, between the lines `closing` and `closed`. The client connects again
19
+ //! each `--retry-ms`, until it holds each port or `--wait-s` passes. A `freed` line gives each port.
20
+ //! It then closes those connections with a reset, so that the next run finds each port free.
21
+ //! 4. `--close target` makes the target close first. The client reads the FIN of each connection,
22
+ //! closes it, and waits `--settle-ms`.
23
+ //!
24
+ //! The control connection takes a port of the range. Its destination is not a destination of the
25
+ //! search, thus it does not change a count. It has SO_LINGER 0 from its connect, so that its close is a
26
+ //! reset and holds no port. A FIN at the exit of the client holds that port in TIME_WAIT for 60 s. A
27
+ //! range of 3 has only 2 ports for a search. Thus the third run of an environment had no control port.
28
+
29
+ use prober::{errno_name, monotonic_ns};
30
+ use serde_json::{json, Value};
31
+ use std::error::Error;
32
+ use std::io::{BufRead, BufReader, Read, Write};
33
+ use std::net::{Ipv4Addr, SocketAddrV4, TcpListener, TcpStream};
34
+ use std::os::fd::AsRawFd;
35
+ use std::sync::{Arc, Mutex, MutexGuard};
36
+ use std::time::Duration;
37
+
38
+ /// the most connections that one search holds: a guard for a range that the harness did not set
39
+ const LIMIT: usize = 256;
40
+ /// how long `close COUNT` waits for the accept of the last connection, in milliseconds
41
+ const ACCEPT_WAIT_MS: u64 = 2_000;
42
+
43
+ fn emit(event: &str, fields: Value) {
44
+ let mut line = json!({ "ev": event, "t": monotonic_ns() });
45
+ if let (Some(line), Value::Object(fields)) = (line.as_object_mut(), fields) {
46
+ line.extend(fields);
47
+ }
48
+ println!("{line}");
49
+ }
50
+
51
+ fn locked(held: &Mutex<Vec<TcpStream>>) -> MutexGuard<'_, Vec<TcpStream>> {
52
+ held.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
53
+ }
54
+
55
+ /// Accepts each connection of a port on its own thread.
56
+ fn listen(port: u16, held: &Arc<Mutex<Vec<TcpStream>>>) -> std::io::Result<()> {
57
+ let listener = TcpListener::bind((Ipv4Addr::UNSPECIFIED, port))?;
58
+ let held = Arc::clone(held);
59
+ std::thread::spawn(move || {
60
+ for stream in listener.incoming().flatten() {
61
+ locked(&held).push(stream);
62
+ }
63
+ });
64
+ Ok(())
65
+ }
66
+
67
+ /// Waits until the target holds `count` connections, and closes each one. It gives the count it closed.
68
+ fn close_held(held: &Mutex<Vec<TcpStream>>, count: usize) -> usize {
69
+ for _ in 0..ACCEPT_WAIT_MS {
70
+ if locked(held).len() >= count {
71
+ break;
72
+ }
73
+ std::thread::sleep(Duration::from_millis(1));
74
+ }
75
+ let mut held = locked(held);
76
+ let closed = held.len();
77
+ held.clear();
78
+ closed
79
+ }
80
+
81
+ fn serve(stream: TcpStream, held: &Mutex<Vec<TcpStream>>) -> Result<(), Box<dyn Error>> {
82
+ stream.set_nodelay(true)?;
83
+ let mut writer = stream.try_clone()?;
84
+ for line in BufReader::new(stream).lines() {
85
+ let line = line?;
86
+ let mut words = line.split_whitespace();
87
+ let reply = match (words.next(), words.next().and_then(|word| word.parse::<usize>().ok())) {
88
+ (Some("close"), Some(count)) => json!({ "closed": close_held(held, count) }),
89
+ _ => json!({ "refused": line }),
90
+ };
91
+ writeln!(writer, "{reply}")?;
92
+ }
93
+ Ok(())
94
+ }
95
+
96
+ fn target(args: &[String]) -> Result<(), Box<dyn Error>> {
97
+ let port: u16 = args.first().ok_or("ports target PORT")?.parse()?;
98
+ let held = Arc::new(Mutex::new(Vec::new()));
99
+ listen(port, &held)?;
100
+ listen(port + 2, &held)?;
101
+ let control = TcpListener::bind((Ipv4Addr::UNSPECIFIED, port + 1))?;
102
+ emit("listening", json!({ "control": port + 1 }));
103
+ for stream in control.incoming() {
104
+ if let Err(error) = serve(stream?, &held) {
105
+ emit("control-error", json!({ "error": error.to_string() }));
106
+ }
107
+ }
108
+ Ok(())
109
+ }
110
+
111
+ #[derive(Clone, Copy, Debug, PartialEq)]
112
+ enum Close {
113
+ Reset,
114
+ Client,
115
+ Target,
116
+ }
117
+
118
+ #[derive(Debug)]
119
+ struct Plan {
120
+ host: String,
121
+ port: u16,
122
+ rounds: u32,
123
+ close: Close,
124
+ destinations: u16,
125
+ settle_ms: u64,
126
+ retry_ms: u64,
127
+ wait_s: u64,
128
+ }
129
+
130
+ fn plan_of(args: &[String]) -> Result<Plan, Box<dyn Error>> {
131
+ let mut plan = Plan {
132
+ host: String::new(),
133
+ port: 0,
134
+ rounds: 1,
135
+ close: Close::Reset,
136
+ destinations: 1,
137
+ settle_ms: 50,
138
+ retry_ms: 10,
139
+ wait_s: 80,
140
+ };
141
+ let mut places = Vec::new();
142
+ let mut rest = args.iter();
143
+ while let Some(arg) = rest.next() {
144
+ let mut value = || rest.next().ok_or_else(|| format!("{arg} needs a value"));
145
+ match arg.as_str() {
146
+ "--rounds" => plan.rounds = value()?.parse()?,
147
+ "--close" => {
148
+ plan.close = match value()?.as_str() {
149
+ "reset" => Close::Reset,
150
+ "client" => Close::Client,
151
+ "target" => Close::Target,
152
+ other => return Err(format!("no way to close named {other}").into()),
153
+ }
154
+ }
155
+ "--destinations" => plan.destinations = value()?.parse()?,
156
+ "--settle-ms" => plan.settle_ms = value()?.parse()?,
157
+ "--retry-ms" => plan.retry_ms = value()?.parse()?,
158
+ "--wait-s" => plan.wait_s = value()?.parse()?,
159
+ // the harness gives these flags to each client, and the plan does not read them
160
+ "--mode" | "--max-s" => {
161
+ value()?;
162
+ }
163
+ other if other.starts_with("--") => return Err(format!("no flag {other}").into()),
164
+ other => places.push(other.to_string()),
165
+ }
166
+ }
167
+ if let [host, port] = places.as_slice() {
168
+ plan.host = host.clone();
169
+ plan.port = port.parse()?;
170
+ return Ok(plan);
171
+ }
172
+ Err("ports client [flags] HOST PORT".into())
173
+ }
174
+
175
+ /// the control connection of the client
176
+ struct Control {
177
+ reader: BufReader<TcpStream>,
178
+ writer: TcpStream,
179
+ }
180
+
181
+ impl Control {
182
+ fn ask(&mut self, command: &str) -> Result<Value, Box<dyn Error>> {
183
+ writeln!(self.writer, "{command}")?;
184
+ let mut line = String::new();
185
+ self.reader.read_line(&mut line)?;
186
+ Ok(serde_json::from_str(&line)?)
187
+ }
188
+ }
189
+
190
+ /// Closes a connection with a reset: SO_LINGER with a time of 0, then close(2).
191
+ fn reset(stream: TcpStream) -> std::io::Result<()> {
192
+ abortive(&stream)
193
+ }
194
+
195
+ /// Sets SO_LINGER with a time of 0, so that each later close of the socket is a reset.
196
+ fn abortive(stream: &TcpStream) -> std::io::Result<()> {
197
+ let linger = libc::linger { l_onoff: 1, l_linger: 0 };
198
+ // SAFETY: the option value is on this stack, passed with its exact size
199
+ let status = unsafe {
200
+ libc::setsockopt(
201
+ stream.as_raw_fd(),
202
+ libc::SOL_SOCKET,
203
+ libc::SO_LINGER,
204
+ (&linger as *const libc::linger).cast(),
205
+ std::mem::size_of::<libc::linger>() as libc::socklen_t,
206
+ )
207
+ };
208
+ if status == 0 { Ok(()) } else { Err(std::io::Error::last_os_error()) }
209
+ }
210
+
211
+ fn errno_of(error: &std::io::Error) -> String {
212
+ error.raw_os_error().map_or_else(|| error.to_string(), errno_name)
213
+ }
214
+
215
+ /// Connects to one destination until a connect fails, and writes the `round` line.
216
+ fn search(index: u32, destination: SocketAddrV4, held: &mut Vec<TcpStream>) -> Result<(), Box<dyn Error>> {
217
+ let mut ports = Vec::new();
218
+ let (errno, refusal) = loop {
219
+ if ports.len() == LIMIT {
220
+ break ("none".to_string(), 0);
221
+ }
222
+ let before = monotonic_ns();
223
+ match TcpStream::connect(destination) {
224
+ Ok(stream) => {
225
+ ports.push(stream.local_addr()?.port());
226
+ held.push(stream);
227
+ }
228
+ Err(error) => break (errno_of(&error), monotonic_ns() - before),
229
+ }
230
+ };
231
+ emit(
232
+ "round",
233
+ json!({ "index": index, "destination": destination.port(), "ports": ports, "errno": errno, "refusal": refusal }),
234
+ );
235
+ Ok(())
236
+ }
237
+
238
+ /// Connects again each `retry_ms` until the client holds `count` ports, or the wait ends.
239
+ fn reclaim(plan: &Plan, destination: SocketAddrV4, count: usize) -> Result<Vec<TcpStream>, Box<dyn Error>> {
240
+ let mut again = Vec::new();
241
+ let mut tries = 0u32;
242
+ let end = monotonic_ns() + plan.wait_s * 1_000_000_000;
243
+ while again.len() < count && monotonic_ns() < end {
244
+ match TcpStream::connect(destination) {
245
+ Ok(stream) => {
246
+ emit("freed", json!({ "port": stream.local_addr()?.port(), "tries": tries }));
247
+ again.push(stream);
248
+ }
249
+ Err(error) if error.raw_os_error() == Some(libc::EADDRNOTAVAIL) => {
250
+ tries += 1;
251
+ std::thread::sleep(Duration::from_millis(plan.retry_ms));
252
+ }
253
+ Err(error) => {
254
+ emit("failed", json!({ "errno": errno_of(&error) }));
255
+ break;
256
+ }
257
+ }
258
+ }
259
+ Ok(again)
260
+ }
261
+
262
+ fn close_round(plan: &Plan, first: SocketAddrV4, control: &mut Control, held: Vec<TcpStream>) -> Result<(), Box<dyn Error>> {
263
+ let count = held.len();
264
+ let settle = Duration::from_millis(plan.settle_ms);
265
+ match plan.close {
266
+ Close::Reset => {
267
+ for stream in held {
268
+ reset(stream)?;
269
+ }
270
+ control.ask(&format!("close {count}"))?;
271
+ }
272
+ Close::Client => {
273
+ drop(held);
274
+ std::thread::sleep(settle);
275
+ emit("closing", json!({}));
276
+ let reply = control.ask(&format!("close {count}"))?;
277
+ emit("closed", reply);
278
+ let again = reclaim(plan, first, count)?;
279
+ let reclaimed = again.len();
280
+ for stream in again {
281
+ reset(stream)?;
282
+ }
283
+ control.ask(&format!("close {reclaimed}"))?;
284
+ }
285
+ Close::Target => {
286
+ control.ask(&format!("close {count}"))?;
287
+ for mut stream in held {
288
+ let mut sink = [0u8; 16];
289
+ // the read ends at the FIN of the target, and the close of the stream sends the FIN of the client
290
+ stream.read(&mut sink)?;
291
+ }
292
+ std::thread::sleep(settle);
293
+ }
294
+ }
295
+ Ok(())
296
+ }
297
+
298
+ fn client(args: &[String]) -> Result<(), Box<dyn Error>> {
299
+ let plan = plan_of(args)?;
300
+ let ip: Ipv4Addr = plan.host.parse()?;
301
+ let stream = TcpStream::connect((ip, plan.port + 1))?;
302
+ // a reset at each close, the exit included: the port of this connection is free for the next run
303
+ abortive(&stream)?;
304
+ stream.set_nodelay(true)?;
305
+ let mut control = Control { reader: BufReader::new(stream.try_clone()?), writer: stream };
306
+ let destinations: Vec<SocketAddrV4> = (0..plan.destinations).map(|at| SocketAddrV4::new(ip, plan.port + 2 * at)).collect();
307
+ emit("ready", json!({}));
308
+ for index in 0..plan.rounds {
309
+ let mut held = Vec::new();
310
+ for destination in &destinations {
311
+ search(index, *destination, &mut held)?;
312
+ }
313
+ close_round(&plan, destinations[0], &mut control, held)?;
314
+ }
315
+ emit("done", json!({}));
316
+ Ok(())
317
+ }
318
+
319
+ fn main() -> Result<(), Box<dyn Error>> {
320
+ let args: Vec<String> = std::env::args().skip(1).collect();
321
+ match args.first().map(String::as_str) {
322
+ Some("target") => target(&args[1..]),
323
+ Some("client") => client(&args[1..]),
324
+ _ => Err("ports target PORT | ports client [flags] HOST PORT".into()),
325
+ }
326
+ }
327
+
328
+ #[cfg(test)]
329
+ mod tests {
330
+ use super::*;
331
+
332
+ #[test]
333
+ fn a_plan_reads_its_flags_and_skips_the_flags_of_the_harness() {
334
+ let args: Vec<String> = ["--rounds", "50", "--close", "client", "--destinations", "2", "10.0.0.2", "6392", "--mode", "idle", "--max-s", "90"]
335
+ .iter()
336
+ .map(|each| each.to_string())
337
+ .collect();
338
+ let plan = plan_of(&args).expect("a plan");
339
+ assert_eq!(plan.host, "10.0.0.2");
340
+ assert_eq!(plan.port, 6392);
341
+ assert_eq!(plan.rounds, 50);
342
+ assert_eq!(plan.close, Close::Client);
343
+ assert_eq!(plan.destinations, 2);
344
+ assert_eq!(plan.retry_ms, 10);
345
+ assert!(plan_of(&["--no-such".to_string()]).is_err());
346
+ assert!(plan_of(&["--close".to_string(), "soon".to_string(), "h".to_string(), "1".to_string()]).is_err());
347
+ }
348
+
349
+ #[test]
350
+ fn a_range_with_no_free_port_is_refused_with_its_name() {
351
+ assert_eq!(errno_of(&std::io::Error::from_raw_os_error(libc::EADDRNOTAVAIL)), "EADDRNOTAVAIL");
352
+ }
353
+
354
+ #[test]
355
+ fn the_control_connection_closes_with_a_reset_and_holds_no_port() {
356
+ let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("a listener");
357
+ let stream = TcpStream::connect(listener.local_addr().expect("an address")).expect("a connect");
358
+ abortive(&stream).expect("SO_LINGER");
359
+ let mut linger = libc::linger { l_onoff: 0, l_linger: -1 };
360
+ let mut size = std::mem::size_of::<libc::linger>() as libc::socklen_t;
361
+ // SAFETY: the buffer is on this stack, passed with its exact size
362
+ let status = unsafe {
363
+ libc::getsockopt(
364
+ stream.as_raw_fd(),
365
+ libc::SOL_SOCKET,
366
+ libc::SO_LINGER,
367
+ (&mut linger as *mut libc::linger).cast(),
368
+ &mut size,
369
+ )
370
+ };
371
+ assert_eq!(status, 0);
372
+ assert_eq!((linger.l_onoff, linger.l_linger), (1, 0));
373
+ }
374
+ }