@mark1russell7/harness 0.11.23 → 0.11.25

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/build/schema.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as Subject, S as ImageSource, i as ClientImage, r as Client } from "./cell-Dn1TvJcA.js";
1
+ import { C as ImageSource, et as Subject, i as ClientImage, r as Client } from "./cell-D4HZ1off.js";
2
2
  import { z } from "zod";
3
3
  //#region src/schema.d.ts
4
4
  /**
@@ -32,6 +32,9 @@
32
32
  //! CONT freeze a server and thaw it while its kernel stays up; KILL stops it, and its kernel
33
33
  //! closes its sockets; HUP asks a pooler to read its configuration again. The stamps bracket the
34
34
  //! kill(2) calls.
35
+ //! - `write NAME PATH CONTENT`: CONTENT to the file PATH in the root of the process NAME, through
36
+ //! /proc/PID/root, by a rename in one step. A proxy that watches its configuration file reloads
37
+ //! it, as after an edit. The stamps bracket the rename.
35
38
  //! - `resp ADDR:PORT ARG...`: one command to a Redis-protocol server, for example
36
39
  //! `SENTINEL FAILOVER mymaster` to a Sentinel. The stamps bracket the write of the command and
37
40
  //! the first line of its reply, so a planned failover starts at a known instant.
@@ -323,6 +326,41 @@ fn signal(args: &[String]) -> Result<(), Box<dyn Error>> {
323
326
  Ok(())
324
327
  }
325
328
 
329
+ /// an absolute path of letters, digits, dots, dashes, underscores and slashes, with no `..`
330
+ fn file_path(path: &str) -> Result<&str, Box<dyn Error>> {
331
+ if path.starts_with('/')
332
+ && path.len() <= 255
333
+ && !path.split('/').any(|part| part == "..")
334
+ && path.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_' | '/'))
335
+ {
336
+ Ok(path)
337
+ } else {
338
+ Err(format!("not a path that this injector writes: {path}").into())
339
+ }
340
+ }
341
+
342
+ /// `write NAME PATH CONTENT [--at NS]`: the file at PATH in the root of the oldest process whose name
343
+ /// is NAME, through /proc/PID/root. The content goes to PATH.inject first, and a rename puts it at
344
+ /// PATH in one step, as a program that watches the folder sees an edit. The stamps bracket the rename.
345
+ fn write(args: &[String]) -> Result<(), Box<dyn Error>> {
346
+ let name = process_name(args.get(1).ok_or("write NAME PATH CONTENT [--at NS]")?)?;
347
+ let path = file_path(args.get(2).ok_or("and the path in its root")?)?;
348
+ let content = args.get(3).ok_or("and the content")?;
349
+ let opts = options(&args[4..])?;
350
+ let pid = *processes(name)?.first().ok_or_else(|| format!("no process {name} in this process namespace"))?;
351
+ let target = format!("/proc/{pid}/root{path}");
352
+ let staged = format!("{target}.inject");
353
+ std::fs::write(&staged, content)?;
354
+ if let Some(at) = opts.at {
355
+ until(at)?;
356
+ }
357
+ let t0 = monotonic_ns();
358
+ std::fs::rename(&staged, &target)?;
359
+ let t1 = monotonic_ns();
360
+ println!("{}", json!({ "action": "write", "name": name, "pid": pid, "path": path, "bytes": content.len(), "t0": t0, "t1": t1 }));
361
+ Ok(())
362
+ }
363
+
326
364
  /// a command in the Redis protocol: an array of bulk strings
327
365
  fn resp_command(args: &[String]) -> Vec<u8> {
328
366
  let mut out = format!("*{}\r\n", args.len()).into_bytes();
@@ -473,6 +511,9 @@ fn run() -> Result<(), Box<dyn Error>> {
473
511
  if action == "signal" {
474
512
  return signal(&args);
475
513
  }
514
+ if action == "write" {
515
+ return write(&args);
516
+ }
476
517
  if action == "resp" {
477
518
  return resp(&args);
478
519
  }
@@ -638,6 +679,15 @@ mod tests {
638
679
  assert!(!zombie("814 (a) Z) S 1"));
639
680
  }
640
681
 
682
+ #[test]
683
+ fn a_written_path_is_absolute_and_plain() {
684
+ assert!(file_path("/etc/traefik/dynamic.yml").is_ok());
685
+ assert!(file_path("/config/dynamic_1.yaml").is_ok());
686
+ for path in ["", "etc/x", "/etc/../root", "/a b", "/a;b", "/$(x)", "/a\nb"] {
687
+ assert!(file_path(path).is_err(), "{path:?}");
688
+ }
689
+ }
690
+
641
691
  #[test]
642
692
  fn a_resp_command_is_an_array_of_bulk_strings() {
643
693
  let args: Vec<String> = ["SENTINEL", "FAILOVER", "mymaster"].iter().map(|a| a.to_string()).collect();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package",
3
3
  "name": "@mark1russell7/harness",
4
- "version": "0.11.23",
4
+ "version": "0.11.25",
5
5
  "description": "A fault-injection harness for connection timers: a cell runs a client against a stock target in Docker, through a topology of routers and proxies, a replicated group under Sentinels or a replica set, injects a fault, measures the kernel's own records and judges a prediction. HTTP clients of Go, Node and undici race the keep-alive of a Node server, and the MongoDB driver meets the faults of a replica set. The WebSocket client of @procedures/client drops a silent connection on its own heartbeat, and the harness times that detection and the reconnect from the program's lines and the kernel's stamps.",
6
6
  "keywords": [
7
7
  "fault-injection",