@mark1russell7/harness 0.11.24 → 0.11.26

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-wInJC-GH.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
  /**
@@ -7,6 +7,7 @@
7
7
  // [--permit-without-stream] [--max-connection-idle D] [--max-connection-age D]
8
8
  // [--max-connection-age-grace D] [--listener-keepalive D]
9
9
  // [--listener-keepalive-interval D] [--listener-keepalive-count N]
10
+ // [--name S] [--clock ns|ms]
10
11
  //
11
12
  // Each D is a duration of Go, for example 20s. A setting at 0 is the default of grpc-go or of Go. A
12
13
  // negative --listener-keepalive turns the TCP keepalive of the accepted connections off.
@@ -16,7 +17,13 @@
16
17
  // NOT_FOUND. Watch sends SERVING and then holds the stream open until the client ends it.
17
18
  //
18
19
  // The lines: listening (the ready line of the target), accept (each connection, with its addresses),
19
- // hold, held and hold-ended (a held reply), watch and watch-ended, and stopping.
20
+ // hold, held and hold-ended (a held reply), watch and watch-ended, and stopping. At SIGTERM or SIGINT,
21
+ // the server writes stopping and then calls GracefulStop: it closes its listener, sends GOAWAY on each
22
+ // connection and waits for each call in flight.
23
+ //
24
+ // A kind cell runs the server in a pod. --name gives the name of the pod: each line has it in the
25
+ // field p, and each reply of Check has it in the header x-pod. --clock ms gives t in milliseconds, the
26
+ // unit of the lines of a kind cell.
20
27
  package main
21
28
 
22
29
  import (
@@ -37,6 +44,7 @@ import (
37
44
  "google.golang.org/grpc/codes"
38
45
  healthpb "google.golang.org/grpc/health/grpc_health_v1"
39
46
  "google.golang.org/grpc/keepalive"
47
+ "google.golang.org/grpc/metadata"
40
48
  "google.golang.org/grpc/status"
41
49
  )
42
50
 
@@ -47,11 +55,28 @@ func now() int64 {
47
55
  return ts.Nano()
48
56
  }
49
57
 
58
+ // name is the name of the server in its lines and its replies (--name), or "" for none.
59
+ var name string
60
+
61
+ // millis tells if t of each line is in milliseconds (--clock ms) and not in nanoseconds.
62
+ var millis bool
63
+
64
+ // stamp gives t of a line: the time on CLOCK_MONOTONIC in the unit of --clock.
65
+ func stamp() any {
66
+ if millis {
67
+ return float64(now()) / 1e6
68
+ }
69
+ return now()
70
+ }
71
+
50
72
  // emit writes one JSON line with its event and its time.
51
73
  func emit(ev string, fields map[string]any) {
52
- line := map[string]any{"ev": ev, "t": now()}
53
- for name, value := range fields {
54
- line[name] = value
74
+ line := map[string]any{"ev": ev, "t": stamp()}
75
+ if name != "" {
76
+ line["p"] = name
77
+ }
78
+ for field, value := range fields {
79
+ line[field] = value
55
80
  }
56
81
  out, err := json.Marshal(line)
57
82
  if err != nil {
@@ -67,6 +92,11 @@ type health struct {
67
92
 
68
93
  func (health) Check(ctx context.Context, request *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) {
69
94
  service := request.GetService()
95
+ if name != "" {
96
+ if err := grpc.SetHeader(ctx, metadata.Pairs("x-pod", name)); err != nil {
97
+ return nil, err
98
+ }
99
+ }
70
100
  if service == "" {
71
101
  return &healthpb.HealthCheckResponse{Status: healthpb.HealthCheckResponse_SERVING}, nil
72
102
  }
@@ -128,7 +158,14 @@ func main() {
128
158
  keepIdle := flag.Duration("listener-keepalive", 0, "the TCP keepalive idle of each accepted connection (0: 15 s, negative: off)")
129
159
  keepInterval := flag.Duration("listener-keepalive-interval", 0, "the TCP keepalive interval (0: 15 s)")
130
160
  keepCount := flag.Int("listener-keepalive-count", 0, "the TCP keepalive count (0: 9)")
161
+ flag.StringVar(&name, "name", "", "the name of the server in each line (p) and each reply of Check (x-pod)")
162
+ clock := flag.String("clock", "ns", "the unit of t: ns or ms")
131
163
  flag.Parse()
164
+ if *clock != "ns" && *clock != "ms" {
165
+ emit("fail", map[string]any{"why": "--clock ns|ms"})
166
+ os.Exit(1)
167
+ }
168
+ millis = *clock == "ms"
132
169
 
133
170
  // at the defaults this is net.Listen: Go turns the keepalive on with its own defaults (15 s, 15 s, 9)
134
171
  config := net.ListenConfig{KeepAlive: *keepIdle}
@@ -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.24",
4
+ "version": "0.11.26",
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",