@mark1russell7/harness 0.11.25 → 0.12.0
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/README.md +7 -7
- package/build/{cell-D4HZ1off.d.ts → cell-jEkGFgGL.d.ts} +9 -19
- package/build/{image-vB51btI1.js → image-Lg8LL6C3.js} +5 -33
- package/build/image-Lg8LL6C3.js.map +1 -0
- package/build/index.d.ts +3 -13
- package/build/index.js +3 -6
- package/build/index.js.map +1 -1
- package/build/schema.d.ts +1 -1
- package/build/schema.js +1 -1
- package/image/grpc-go/main.go +41 -4
- package/package.json +1 -7
- package/build/image-vB51btI1.js.map +0 -1
- package/image/procedures/Dockerfile +0 -14
- package/image/procedures/client-ws.mjs +0 -259
- package/image/procedures/lines.mjs +0 -68
- package/image/procedures/npm-lock.json +0 -122
- package/image/procedures/package.json +0 -9
- package/image/procedures/server-ws.mjs +0 -96
package/build/schema.d.ts
CHANGED
package/build/schema.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as SHIPPED_NAMES,
|
|
1
|
+
import { S as subjectProblems, a as SHIPPED_NAMES, f as imageProblems, t as CLIENT_NAMES } from "./image-Lg8LL6C3.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
//#region src/schema.ts
|
|
4
4
|
/** This function adds each problem that a check finds as an issue of a schema. */
|
package/image/grpc-go/main.go
CHANGED
|
@@ -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":
|
|
53
|
-
|
|
54
|
-
line[
|
|
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}
|
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.
|
|
4
|
+
"version": "0.12.0",
|
|
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",
|
|
@@ -86,12 +86,6 @@
|
|
|
86
86
|
"image/grpc-go/go.mod",
|
|
87
87
|
"image/grpc-go/main.go",
|
|
88
88
|
"image/etcd/Dockerfile",
|
|
89
|
-
"image/procedures/Dockerfile",
|
|
90
|
-
"image/procedures/package.json",
|
|
91
|
-
"image/procedures/npm-lock.json",
|
|
92
|
-
"image/procedures/lines.mjs",
|
|
93
|
-
"image/procedures/client-ws.mjs",
|
|
94
|
-
"image/procedures/server-ws.mjs",
|
|
95
89
|
"image/router/Dockerfile",
|
|
96
90
|
"image/router/mbx.sh",
|
|
97
91
|
"image/router/ctwatch.py",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"image-vB51btI1.js","names":[],"sources":["../src/image.ts"],"sourcesContent":["import { execFile } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport { copyFileSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, join, posix, relative, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { isDeepStrictEqual } from \"node:util\";\nimport type { BuildImage, Client, ClientImage, ImageSource, Shipped, ShippedImage, Subject, Target } from \"./cell.ts\";\n\n/**\n * This module resolves the source of each image (`ImageSource`) to an image that a lab can start.\n * A pinned image is its own name. A build context, or an image that the harness ships, becomes a\n * build with a tag from its content. Each image of the harness is a shipped source in one table.\n * Thus the procedures image of a subject is data, as the image of a caller is.\n */\n\n/**\n * These are the roots of the two packages that the shipped images read: this package, and the\n * prober that Node resolves for it. In a workspace, they are two folders side by side. From npm,\n * the prober is a link, and a Docker build context does not follow a link. Thus each image builds\n * from a context that the harness copies from the two roots.\n */\nconst ROOTS: Readonly<Record<string, string>> = {\n harness: fileURLToPath(new URL(\"..\", import.meta.url)),\n prober: dirname(realpathSync(createRequire(import.meta.url).resolve(\"@mark1russell7/prober/package.json\"))),\n};\n\n/** This function gives the path of a part of a shipped image, which is `<package>/<path>` in the context. */\nconst pathOf = (part: string): string => {\n const [root, ...rest] = part.split(\"/\");\n const base = ROOTS[root!];\n if (base === undefined) throw new Error(`${part} is in no package that the images read`);\n return join(base, ...rest);\n};\n\n/** A shipped context is the build context of a shipped image: its Dockerfile, and each file or folder that the Dockerfile reads, as `<package>/<path>`. */\ninterface ShippedContext {\n readonly name: string;\n readonly dockerfile: string;\n readonly from: readonly string[];\n}\n\n/** These are the build contexts of the images that the harness ships, by their names. */\nconst SHIPPED: Readonly<Record<Shipped, ShippedContext>> = {\n // the prober, the TCP client, the injector and the namespace admin\n harness: {\n name: \"loom-harness\",\n dockerfile: \"harness/image/Dockerfile\",\n from: [\"harness/image/Dockerfile\", \"harness/image/native\", \"prober/bpf\", \"prober/native\", \"prober/fixtures\"],\n },\n // the Node client: ioredis 6.0.0 on Node 24.21.0\n node: { name: \"loom-harness-node\", dockerfile: \"harness/image/node/Dockerfile\", from: [\"harness/image/node\"] },\n // the Go HTTP client: the http.Transport of Go 1.27.2\n \"go-http\": { name: \"loom-harness-go-http\", dockerfile: \"harness/image/go-http/Dockerfile\", from: [\"harness/image/go-http\"] },\n // the Node HTTP clients: the http.Agent of Node 24.21.0, and undici 8.12.2\n \"node-http\": { name: \"loom-harness-node-http\", dockerfile: \"harness/image/node-http/Dockerfile\", from: [\"harness/image/node-http\"] },\n // the MongoDB client: the driver mongodb 7.7.0 on Node 24.21.0, and its step-down script\n mongo: { name: \"loom-harness-mongo\", dockerfile: \"harness/image/mongo/Dockerfile\", from: [\"harness/image/mongo\"] },\n // the PostgreSQL client: node-postgres (pg 8.23.1 with pg-pool 3.14.0) on Node 24.21.0\n pg: { name: \"loom-harness-pg\", dockerfile: \"harness/image/pg/Dockerfile\", from: [\"harness/image/pg\"] },\n // the client of HikariCP 7.1.0 over pgjdbc 42.7.13 on Temurin 21.0.12.1: its jars by their SHA-256\n hikari: { name: \"loom-harness-hikari\", dockerfile: \"harness/image/hikari/Dockerfile\", from: [\"harness/image/hikari\"] },\n // the RabbitMQ client: amqplib 2.2.0 on Node 24.21.0\n amqp: { name: \"loom-harness-amqp\", dockerfile: \"harness/image/amqp/Dockerfile\", from: [\"harness/image/amqp\"] },\n // the gRPC client: grpc-js 1.14.6 on Node 24.21.0\n grpc: { name: \"loom-harness-grpc\", dockerfile: \"harness/image/grpc/Dockerfile\", from: [\"harness/image/grpc\"] },\n // the gRPC server: a server of grpc-go v1.84.0, built with Go 1.27.2, that can hold a reply\n \"grpc-go\": { name: \"loom-harness-grpc-go\", dockerfile: \"harness/image/grpc-go/Dockerfile\", from: [\"harness/image/grpc-go\"] },\n // etcd v3.6.15 from its image, on Alpine: a shell to hold it, and its binaries unchanged\n etcd: { name: \"loom-harness-etcd\", dockerfile: \"harness/image/etcd/Dockerfile\", from: [\"harness/image/etcd\"] },\n // @procedures/client 0.7.0 on Node 24.21.0: its WebSocket client, and the script of its server\n procedures: { name: \"loom-harness-procedures\", dockerfile: \"harness/image/procedures/Dockerfile\", from: [\"harness/image/procedures\"] },\n // the router of a middlebox chain: NAT and conntrack, with a form for a forgotten flow\n router: { name: \"loom-harness-router\", dockerfile: \"harness/image/router/Dockerfile\", from: [\"harness/image/router\"] },\n // PgBouncer 1.26.1, a pooler hop: built from its release tarball, by its SHA-256\n pgbouncer: { name: \"loom-harness-pgbouncer\", dockerfile: \"harness/image/pgbouncer/Dockerfile\", from: [\"harness/image/pgbouncer\"] },\n // Pgpool-II 4.7.3, a pooler hop: built from the source of its tag, by its SHA-256\n pgpool: { name: \"loom-harness-pgpool\", dockerfile: \"harness/image/pgpool/Dockerfile\", from: [\"harness/image/pgpool\"] },\n // docker-proxy of Docker Engine 29.1.5, a proxy hop: its binary from the static bundle of Docker, by its SHA-256\n \"docker-proxy\": { name: \"loom-harness-docker-proxy\", dockerfile: \"harness/image/docker-proxy/Dockerfile\", from: [\"harness/image/docker-proxy\"] },\n};\n\n/** These are the names of the images that the harness ships. */\nexport const SHIPPED_NAMES: readonly [Shipped, ...Shipped[]] = Object.keys(SHIPPED) as [Shipped, ...Shipped[]];\n\n/** This image is the stock base of the Node images of the harness: Node 24.21.0 on Alpine, pinned by digest. */\nconst NODE_BASE = \"node:24.21.0-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401c70e0e762c5b53302f905ec1c1\";\n\n/**\n * These are the subjects that the harness ships, by their names. `procedures` is\n * `@procedures/client` on Node: its WebSocket client (`client-ws.mjs`) and its server\n * (`server-ws.mjs`), from one image. Thus the client and the server use one release.\n */\nexport const SUBJECTS: Readonly<Record<\"procedures\", Subject>> = {\n procedures: {\n name: \"procedures\",\n image: { kind: \"shipped\", name: \"procedures\" },\n base: NODE_BASE,\n programs: {\n client: [\"node\", \"/opt/harness/procedures/client-ws.mjs\"],\n server: [\"node\", \"/opt/harness/procedures/server-ws.mjs\"],\n },\n },\n};\n\n/** This function gives the source of a shipped image. */\nconst shipped = (name: Shipped): ImageSource => ({ kind: \"shipped\", name });\n\n/**\n * These are the clients that the harness ships, by their names (`Client`): the image of each, and\n * its command. A cell that names a client gets this image and this command.\n */\nexport const SHIPPED_CLIENTS: Readonly<Record<Client, ClientImage>> = {\n tcp: { image: shipped(\"harness\"), command: [\"/opt/harness/client\"] },\n ioredis: { image: shipped(\"node\"), command: [\"node\", \"/opt/harness/node/client-ioredis.mjs\"] },\n \"go-http\": { image: shipped(\"go-http\"), command: [\"/opt/harness/client-go\"] },\n \"http-agent\": { image: shipped(\"node-http\"), command: [\"node\", \"/opt/harness/node-http/client-agent.mjs\"] },\n undici: { image: shipped(\"node-http\"), command: [\"node\", \"/opt/harness/node-http/client-undici.mjs\"] },\n mongodb: { image: shipped(\"mongo\"), command: [\"node\", \"/opt/harness/mongo/client-mongo.mjs\"] },\n pg: { image: shipped(\"pg\"), command: [\"node\", \"/opt/harness/pg/client-pg.mjs\"] },\n hikari: {\n image: shipped(\"hikari\"),\n command: [\"java\", \"-Dslf4j.internal.verbosity=ERROR\", \"-cp\", \"/opt/harness/hikari/classes:/opt/harness/hikari/lib/*\", \"ClientHikari\"],\n },\n amqp: { image: shipped(\"amqp\"), command: [\"node\", \"/opt/harness/amqp/client-amqp.mjs\"] },\n grpc: { image: shipped(\"grpc\"), command: [\"node\", \"/opt/harness/grpc/client-grpc.mjs\"] },\n \"procedures-ws\": { image: SUBJECTS.procedures.image, command: SUBJECTS.procedures.programs[\"client\"]! },\n};\n\n/** These are the names of the clients that the harness ships. */\nexport const CLIENT_NAMES: readonly [Client, ...Client[]] = Object.keys(SHIPPED_CLIENTS) as [Client, ...Client[]];\n\n/** This function gives the command of a program of a subject by its role. It fails when the subject has no such program. */\nexport const programOf = (subject: Subject, role: string): readonly string[] => {\n const program = Object.hasOwn(subject.programs, role) ? subject.programs[role] : undefined;\n if (program === undefined) throw new Error(`the subject ${subject.name} has no program ${role}`);\n return program;\n};\n\n/**\n * This function gives the client of a subject for a cell (`Cell.client`): the image of the subject,\n * with the command of a program. A client that the harness ships goes by its name, thus its records\n * stay the same.\n */\nexport const subjectClient = (subject: Subject, role = \"client\"): Client | ClientImage => {\n const client: ClientImage = { image: subject.image, command: programOf(subject, role) };\n return CLIENT_NAMES.find((name) => isDeepStrictEqual(SHIPPED_CLIENTS[name], client)) ?? client;\n};\n\n/** This pattern matches an image pinned by digest: a name, a tag or none, then `@sha256:` and 64 hex digits. */\nexport const PINNED: RegExp = /^[a-z0-9][A-Za-z0-9._\\-/:]*@sha256:[0-9a-f]{64}$/;\n/** This pattern matches the repository of a tag that the harness builds. Its parts are lower case, with `/` between them. */\nexport const REPOSITORY: RegExp = /^[a-z0-9]+(?:[._-]+[a-z0-9]+)*(?:\\/[a-z0-9]+(?:[._-]+[a-z0-9]+)*)*$/;\n\n/** This function tells if a path stays in its context: relative, with `/` between its parts, and no `..`. */\nexport const inContext = (path: string): boolean =>\n typeof path === \"string\" &&\n path.length > 0 &&\n !path.startsWith(\"/\") &&\n !path.includes(\"\\\\\") &&\n !/^[A-Za-z]:/.test(path) &&\n !path.split(\"/\").includes(\"..\");\n\n/** This function tells if a value is a command: an array of one text or more. */\nconst isCommand = (value: unknown): boolean =>\n Array.isArray(value) && value.length > 0 && value.every((word) => typeof word === \"string\" && word.length > 0);\n\n/**\n * This function gives the problems of the source of an image, as data. A pinned image names a\n * digest. A build names a repository, a context, and paths that stay in the context. A shipped image\n * is one that the harness ships.\n */\nexport const imageProblems = (source: ImageSource): string[] => {\n const kind = (source as { readonly kind?: unknown } | null)?.kind;\n if (kind === \"pinned\") {\n const { image } = source as { readonly image?: unknown };\n return typeof image === \"string\" && PINNED.test(image) ? [] : [`${String(image)} is not pinned by digest: name@sha256:<64 hex digits>`];\n }\n if (kind === \"shipped\") {\n const { name } = source as { readonly name?: unknown };\n return typeof name === \"string\" && Object.hasOwn(SHIPPED, name) ? [] : [`the harness ships no image ${String(name)}: ${SHIPPED_NAMES.join(\", \")}`];\n }\n if (kind === \"build\") {\n const build = source as Partial<BuildImage>;\n const problems: string[] = [];\n if (typeof build.name !== \"string\" || !REPOSITORY.test(build.name)) problems.push(`${String(build.name)} is not the name of a repository`);\n if (typeof build.context !== \"string\" || build.context.length === 0) problems.push(\"the build names no context\");\n if (build.files !== undefined && (!Array.isArray(build.files) || build.files.length === 0)) problems.push(\"the build names no files\");\n for (const path of [build.dockerfile ?? \"Dockerfile\", ...(Array.isArray(build.files) ? build.files : [])])\n if (!inContext(path)) problems.push(`${String(path)} is not a path in the context`);\n return problems;\n }\n return [`${JSON.stringify(kind)} is not a kind of image source: pinned, build or shipped`];\n};\n\n/**\n * This function gives the problems of a subject, as data. A subject has a name and a base that a\n * digest pins. Its image source has no problems, and each of its programs is a command.\n */\nexport const subjectProblems = (subject: Subject): string[] => {\n const programs = (subject as { readonly programs?: unknown }).programs;\n const roles = typeof programs === \"object\" && programs !== null ? Object.entries(programs) : [];\n return [\n ...(typeof subject.name === \"string\" && subject.name.length > 0 ? [] : [\"the subject has no name\"]),\n ...(typeof subject.base === \"string\" && PINNED.test(subject.base) ? [] : [`the base ${String(subject.base)} is not pinned by digest`]),\n ...imageProblems(subject.image),\n ...(roles.length === 0 ? [\"the subject has no programs\"] : []),\n ...roles.flatMap(([role, command]) => (isCommand(command) ? [] : [`the program ${role} is not a command`])),\n ];\n};\n\n/**\n * This function gives each `FROM` of a Dockerfile that no digest pins. A `FROM` of an earlier stage\n * by its name, or of `scratch`, needs no digest. A base from an `ARG` has no digest that the harness\n * can read, thus it is in the list too.\n */\nexport const unpinnedBases = (dockerfile: string): string[] => {\n const stages = new Set<string>([\"scratch\"]);\n const found: string[] = [];\n for (const line of dockerfile.replace(/\\\\\\r?\\n/g, \" \").split(/\\r?\\n/)) {\n const from = /^\\s*FROM\\s+(.+)$/i.exec(line)?.[1];\n if (from === undefined) continue;\n const [image, as, stage] = from.split(/\\s+/).filter((word) => !word.startsWith(\"--\"));\n if (image === undefined) continue;\n if (!stages.has(image.toLowerCase()) && !PINNED.test(image)) found.push(image);\n if (as?.toLowerCase() === \"as\" && stage !== undefined) stages.add(stage.toLowerCase());\n }\n return found;\n};\n\n/** These are the lockfiles of each manifest of packages. A manifest without a lockfile installs other packages each time. */\nconst LOCKFILES: Readonly<Record<string, readonly string[]>> = {\n \"package.json\": [\"package-lock.json\", \"npm-lock.json\", \"npm-shrinkwrap.json\", \"pnpm-lock.yaml\", \"yarn.lock\", \"bun.lock\"],\n \"Cargo.toml\": [\"Cargo.lock\"],\n};\n\n/**\n * This function gives each manifest of packages in a context (`package.json`, `Cargo.toml`) that\n * has no lockfile in its folder or in a folder above it. The paths are the paths in the context.\n */\nexport const unlockedManifests = (paths: readonly string[]): string[] => {\n const present = new Set(paths);\n return paths.filter((path) => {\n const parts = path.split(\"/\");\n const locks = LOCKFILES[parts.at(-1)!];\n if (locks === undefined) return false;\n for (let depth = parts.length - 1; depth >= 0; depth -= 1) {\n const folder = parts.slice(0, depth).join(\"/\");\n if (locks.some((lock) => present.has(folder === \"\" ? lock : `${folder}/${lock}`))) return false;\n }\n return true;\n });\n};\n\n/**\n * An image plan tells how the harness gets the image of a source. It is a pull of an image by its\n * name, or a build of a context. A build has its tag, its Dockerfile, and each file with its path.\n */\nexport type ImagePlan =\n | { readonly kind: \"pull\"; readonly image: string }\n | {\n readonly kind: \"build\";\n readonly tag: string;\n readonly dockerfile: string;\n readonly files: ReadonlyArray<{ readonly file: string; readonly at: string }>;\n };\n\n/** This function gives each file under a path, in a stable sequence, but not the output of a local build. */\nconst sources = (path: string): string[] => {\n if (statSync(path).isFile()) return [path];\n return readdirSync(path, { withFileTypes: true })\n .filter((entry) => entry.name !== \"target\" && entry.name !== \"node_modules\")\n .flatMap((entry) => sources(join(path, entry.name)))\n .sort();\n};\n\n/** A part is a file or a folder of a context: its path on the machine, and its path in the context. */\ninterface Part {\n readonly path: string;\n readonly at: string;\n}\n\n/** This function gives the name, the Dockerfile and the parts of the context of a build or of a shipped image. */\nconst contextOf = (source: BuildImage | ShippedImage): { name: string; dockerfile: string; parts: Part[] } => {\n if (source.kind === \"shipped\") {\n const { name, dockerfile, from } = SHIPPED[source.name];\n return { name, dockerfile, parts: from.map((part) => ({ path: pathOf(part), at: part })) };\n }\n const root = resolve(source.context);\n return {\n name: source.name,\n dockerfile: posix.normalize(source.dockerfile ?? \"Dockerfile\"),\n parts: (source.files ?? [\".\"]).map((file) => ({ path: resolve(root, file), at: file === \".\" ? \"\" : file })),\n };\n};\n\n/** This function gives a text for a source in a message. */\nconst labelOf = (source: ImageSource): string =>\n source.kind === \"pinned\" ? source.image : `the ${source.kind === \"build\" ? \"build\" : \"shipped image\"} ${source.name}`;\n\nconst shippedPlans = new Map<Shipped, ImagePlan>();\n\n/**\n * This function gives the plan of the image of a source. A stock image by its name, or a pinned\n * image, is a pull. A build or a shipped image is a build. Its tag is its name, then 12 digits of a\n * sha256. The sha256 reads the path of the Dockerfile, and the path and the content of each file.\n * Thus a changed program cannot operate as an old one.\n *\n * A build fails before it starts on a problem of its source. It also fails on a `FROM` that no\n * digest pins, and on a manifest of packages without a lockfile.\n */\nexport const planOf = (source: string | ImageSource): ImagePlan => {\n if (typeof source === \"string\") return { kind: \"pull\", image: source };\n const problems = imageProblems(source);\n if (problems.length > 0) throw new Error(`${labelOf(source)}: ${problems.join(\", \")}`);\n if (source.kind === \"pinned\") return { kind: \"pull\", image: source.image };\n const known = source.kind === \"shipped\" ? shippedPlans.get(source.name) : undefined;\n if (known !== undefined) return known;\n const { name, dockerfile, parts } = contextOf(source);\n const files = parts.flatMap((part) =>\n sources(part.path).map((file) => ({ file, at: join(part.at, relative(part.path, file)).replaceAll(\"\\\\\", \"/\") })),\n );\n const docker = files.find((each) => each.at === dockerfile);\n if (docker === undefined) throw new Error(`${labelOf(source)}: the context has no ${dockerfile}`);\n const unpinned = unpinnedBases(readFileSync(docker.file, \"utf8\"));\n const unlocked = unlockedManifests(files.map((each) => each.at));\n if (unpinned.length > 0 || unlocked.length > 0)\n throw new Error(\n `${labelOf(source)}: ${[\n ...unpinned.map((image) => `the base ${image} is not pinned by digest`),\n ...unlocked.map((path) => `${path} has no lockfile`),\n ].join(\", \")}`,\n );\n const hash = createHash(\"sha256\");\n hash.update(dockerfile);\n for (const { file, at } of files) {\n hash.update(at);\n hash.update(readFileSync(file));\n }\n const plan: ImagePlan = { kind: \"build\", tag: `${name}:${hash.digest(\"hex\").slice(0, 12)}`, dockerfile, files };\n if (source.kind === \"shipped\") shippedPlans.set(source.name, plan);\n return plan;\n};\n\n/** These counts are how many lines of the output of a failed build its error holds: its error lines, then its last lines. */\nconst BUILD_ERRORS = 10;\nconst BUILD_TAIL = 15;\n\n/**\n * This function gives the lines of the output of a failed build that its error holds. They are each\n * line that names an error, for example the diagnostic of a compiler, then the last lines.\n */\nexport const buildFailure = (output: string): string => {\n const lines = output.trim().split(/\\r?\\n/);\n const tail = lines.slice(-BUILD_TAIL);\n const errors = lines.slice(0, -BUILD_TAIL).filter((line) => /\\berror\\b/i.test(line)).slice(0, BUILD_ERRORS);\n return [...errors, ...(errors.length > 0 ? [\"...\"] : []), ...tail].join(\"\\n\");\n};\n\n/**\n * This function builds a staged context with the CLI of Docker, thus with BuildKit where the CLI has\n * it. Testcontainers reads nothing of the output of a build, and it fails with \"Failed to build\n * image\" only. The error of a failed build here holds its error lines and the end of its output.\n */\nconst dockerBuild = (context: string, plan: Extract<ImagePlan, { readonly kind: \"build\" }>): Promise<void> =>\n new Promise((resolve, reject) => {\n const args = [\"build\", \"--progress=plain\", \"--tag\", plan.tag, \"--file\", join(context, plan.dockerfile), context];\n execFile(\"docker\", args, { maxBuffer: 64 * 1024 * 1024 }, (error, _stdout, stderr) => {\n if (error === null) {\n resolve();\n return;\n }\n reject(new Error(`docker build of ${plan.tag} failed (${error.message.split(\"\\n\")[0]}):\\n${buildFailure(String(stderr))}`));\n });\n });\n\nconst built = new Map<string, Promise<string>>();\n\n/**\n * These are the waits before each next try of a build that fails, in milliseconds. A fault of a\n * registry or of the network often stops in seconds. In run 39278390448 of the cells, two runners\n * failed a build in the same second, and the same builds then passed.\n */\nexport const BUILD_WAITS: readonly number[] = [5_000, 20_000];\n\n/**\n * This function builds the image of a plan one time for each process, from a new context that it\n * copies the files into. Docker keeps its layers between runs. A build that fails tries again after\n * each wait (`waits`), and it writes each failure to stderr. A build that fails each try leaves the\n * cache. Thus a later cell builds again after a fault of Docker or of the network.\n */\nexport const buildOnce = (\n plan: Extract<ImagePlan, { readonly kind: \"build\" }>,\n builder: (context: string, plan: Extract<ImagePlan, { readonly kind: \"build\" }>) => Promise<void> = dockerBuild,\n waits: readonly number[] = BUILD_WAITS,\n): Promise<string> => {\n let tag = built.get(plan.tag);\n if (tag === undefined) {\n const building = (async () => {\n const context = mkdtempSync(join(tmpdir(), `${plan.tag.split(\":\")[0]!.replaceAll(\"/\", \"-\")}-`));\n try {\n for (const { file, at } of plan.files) {\n mkdirSync(dirname(join(context, at)), { recursive: true });\n copyFileSync(file, join(context, at));\n }\n for (let attempt = 0; ; attempt += 1) {\n try {\n await builder(context, plan);\n break;\n } catch (error) {\n const wait = waits[attempt];\n if (wait === undefined) throw error;\n process.stderr.write(`the build of ${plan.tag} failed (${error instanceof Error ? error.message : String(error)}): try ${attempt + 2} after ${wait} ms\\n`);\n await new Promise((resolve) => setTimeout(resolve, wait));\n }\n }\n } finally {\n rmSync(context, { recursive: true, force: true });\n }\n return plan.tag;\n })();\n // a failed build does not stay: the next caller tries again\n building.catch(() => {\n if (built.get(plan.tag) === building) built.delete(plan.tag);\n });\n built.set(plan.tag, building);\n tag = building;\n }\n return tag;\n};\n\n/**\n * This function gives an image that a lab can start, from its source. It is the name of a stock\n * image or of a pinned image, which the lab pulls, or the tag of a build. A build goes through the CLI\n * of Docker.\n */\nexport const imageOf = async (source: string | ImageSource): Promise<string> => {\n const plan = planOf(source);\n return plan.kind === \"pull\" ? plan.image : buildOnce(plan);\n};\n\n/** This function gives the image of the harness: the prober, the TCP client, the injector and the namespace admin. */\nexport const harnessImage = (): Promise<string> => imageOf(shipped(\"harness\"));\n/** This function gives the image of the Node client. */\nexport const nodeClientImage = (): Promise<string> => imageOf(shipped(\"node\"));\n/** This function gives the image of the router. */\nexport const routerImage = (): Promise<string> => imageOf(shipped(\"router\"));\n/** This function gives the image of the Go HTTP client. */\nexport const goHttpImage = (): Promise<string> => imageOf(shipped(\"go-http\"));\n/** This function gives the image of the Node HTTP clients. */\nexport const nodeHttpImage = (): Promise<string> => imageOf(shipped(\"node-http\"));\n/** This function gives the image of the MongoDB client. */\nexport const mongoClientImage = (): Promise<string> => imageOf(shipped(\"mongo\"));\n/** This function gives the image of the PostgreSQL client. */\nexport const pgClientImage = (): Promise<string> => imageOf(shipped(\"pg\"));\n/** This function gives the image of the client of HikariCP over pgjdbc. */\nexport const hikariClientImage = (): Promise<string> => imageOf(shipped(\"hikari\"));\n/** This function gives the image of the RabbitMQ client: amqplib, with the client and the admin of a cluster cell. */\nexport const amqpClientImage = (): Promise<string> => imageOf(shipped(\"amqp\"));\n\n/** This function gives the image of the gRPC client of the harness: grpc-js 1.14.6 on Node 24.21.0, with its client. */\nexport const grpcClientImage = (): Promise<string> => imageOf(shipped(\"grpc\"));\n/** This function gives the image of the procedures subject: the WebSocket client of `@procedures/client`, and its server. */\nexport const proceduresImage = (): Promise<string> => imageOf(SUBJECTS.procedures.image);\n\n/**\n * This function gives the source of the image that a target operates. It is the image of its\n * subject (`Target.subject`), a shipped image by its short form (`Target.harness`), or its stock image.\n */\nexport const targetSourceOf = (target: Target): string | ImageSource =>\n target.subject ?? (target.harness === undefined ? target.image : shipped(target.harness));\n\n/** This function gives the image that a target operates: the image of its subject, or its stock image. */\nexport const targetImageOf = (target: Target): Promise<string> => imageOf(targetSourceOf(target));\n"],"mappings":"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"}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# The procedures image of the harness: @procedures/client 0.7.0 and ws 8.22.0 on Node 24.21.0. Its WebSocket
|
|
2
|
-
# client runs as a container in the client's network namespace, as the harness's own TCP client
|
|
3
|
-
# does, and prints the same JSON lines (client-ws.mjs). A target runs the same image with the
|
|
4
|
-
# script of the procedures server (server-ws.mjs), thus the client and the server share one
|
|
5
|
-
# release of the package. The digest of the image and an npm lockfile pin them. The lockfile
|
|
6
|
-
# holds one copy of ws, thus @procedures/client/node and the scripts use one ws class.
|
|
7
|
-
# lab.ts stages the build context: this folder, under harness/image/procedures. The lockfile is
|
|
8
|
-
# npm-lock.json here, because npm leaves each package-lock.json out of a published tarball.
|
|
9
|
-
FROM node:24.21.0-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401c70e0e762c5b53302f905ec1c1
|
|
10
|
-
WORKDIR /opt/harness/procedures
|
|
11
|
-
COPY harness/image/procedures/package.json ./
|
|
12
|
-
COPY harness/image/procedures/npm-lock.json ./package-lock.json
|
|
13
|
-
RUN npm ci --omit=dev --ignore-scripts --no-audit --no-fund
|
|
14
|
-
COPY harness/image/procedures/lines.mjs harness/image/procedures/client-ws.mjs harness/image/procedures/server-ws.mjs ./
|
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
// client-ws HOST PORT --mode idle|inflight --websocket global|ws
|
|
2
|
-
// --heartbeat on|off [--heartbeat-interval-ms N --heartbeat-timeout-ms N --heartbeat-misses N]
|
|
3
|
-
// --reconnect on|off [--reconnect-initial-ms N --reconnect-multiplier X --reconnect-max-ms N
|
|
4
|
-
// --reconnect-attempts N --reconnect-jitter X]
|
|
5
|
-
// --request-timeout-ms N --connection-timeout-ms N [--hold-ms N] [--go PATH] [--max-s N]
|
|
6
|
-
//
|
|
7
|
-
// A cell's client: the WebSocket client of @procedures/client 0.7.0 (WebSocketTransport) on Node
|
|
8
|
-
// 24.21.0. --websocket gives the class of its sockets:
|
|
9
|
-
// global the WebSocketTransport of the root entry, whose default class is the global WebSocket
|
|
10
|
-
// of Node: undici 7.29.1.
|
|
11
|
-
// ws the WebSocketTransport of the Node entry @procedures/client/node, whose default class
|
|
12
|
-
// is the WebSocket of the ws package (since 0.4.0). The client gives the transport a
|
|
13
|
-
// subclass of that class, from the one copy of ws in the image: it stops when the Node
|
|
14
|
-
// entry resolves another copy.
|
|
15
|
-
// Each setting of the transport is a flag, and the client has no default: a missing flag is a
|
|
16
|
-
// failure. 0.7.0 takes each time as a duration, not a number. Both modes call `harness.echo`
|
|
17
|
-
// first, and its reply is `ready`. idle: then hold the connection. inflight: then wait for the file
|
|
18
|
-
// --go, and call `harness.echo` (or `harness.hold` for --hold-ms) into the fault.
|
|
19
|
-
//
|
|
20
|
-
// The lines are those of the harness's TCP client, on CLOCK_MONOTONIC, and the program's own acts:
|
|
21
|
-
// dial {conn} before each new socket of the transport (conn counts from 1)
|
|
22
|
-
// connected {conn, local} the socket opened: its local address
|
|
23
|
-
// ping {conn, local} before each ping of the heartbeat
|
|
24
|
-
// write {conn, type} before each other frame: a hello, a request, a cancel or a credit
|
|
25
|
-
// drop {conn, local} before the transport ends a socket by itself: close() or terminate()
|
|
26
|
-
// reconnecting {attempt} the transport starts its reconnect timer (onReconnecting)
|
|
27
|
-
// status {status} connected, disconnected (with the reason) or error, from the callbacks
|
|
28
|
-
// conn-end {conn, code} the close event of each socket, with the errno of its socket error
|
|
29
|
-
// error {errno} / eof the end of socket 1, the socket of the run, when the transport did not drop it
|
|
30
|
-
// sent / ready / data / call-error each call, its reply, or its failure
|
|
31
|
-
//
|
|
32
|
-
// The client gives the transport a subclass of the class of its sockets. The subclass writes a
|
|
33
|
-
// line, then calls its class: the transport, undici and ws do what they do without it. Its
|
|
34
|
-
// listeners of `open` and `close` are its own, thus the transport cannot remove them. A drop line
|
|
35
|
-
// with no local address is a socket that never opened: a try that failed. A WebSocket of ws emits
|
|
36
|
-
// no `error` for an error of its socket (lib/websocket.js:1411-1421), thus the subclass reads that
|
|
37
|
-
// error on the socket itself.
|
|
38
|
-
|
|
39
|
-
import { subscribe } from "node:diagnostics_channel";
|
|
40
|
-
import { createRequire } from "node:module";
|
|
41
|
-
import { fileURLToPath } from "node:url";
|
|
42
|
-
import { WebSocket as WsWebSocket } from "ws";
|
|
43
|
-
import { bootId, commandLine, emit, now, waitFor } from "./lines.mjs";
|
|
44
|
-
|
|
45
|
-
const USAGE =
|
|
46
|
-
"client-ws HOST PORT --mode idle|inflight --websocket global|ws --heartbeat on|off --reconnect on|off --request-timeout-ms N --connection-timeout-ms N";
|
|
47
|
-
const line = commandLine(USAGE, 2);
|
|
48
|
-
const [host, port] = line.args;
|
|
49
|
-
const mode = line.required("--mode");
|
|
50
|
-
if (mode !== "idle" && mode !== "inflight") line.fail("--mode is idle or inflight");
|
|
51
|
-
const socketClass = line.required("--websocket");
|
|
52
|
-
if (socketClass !== "global" && socketClass !== "ws") line.fail("--websocket is global or ws");
|
|
53
|
-
/** This function gives a duration of milliseconds, as 0.7.0 takes each time. */
|
|
54
|
-
const ms = (value) => ({ value, unit: "ms" });
|
|
55
|
-
const heartbeat = line.toggle("--heartbeat")
|
|
56
|
-
? {
|
|
57
|
-
enabled: true,
|
|
58
|
-
interval: line.number("--heartbeat-interval-ms"),
|
|
59
|
-
timeout: line.number("--heartbeat-timeout-ms"),
|
|
60
|
-
misses: line.number("--heartbeat-misses"),
|
|
61
|
-
}
|
|
62
|
-
: { enabled: false };
|
|
63
|
-
const reconnect = line.toggle("--reconnect")
|
|
64
|
-
? {
|
|
65
|
-
enabled: true,
|
|
66
|
-
initialDelay: line.number("--reconnect-initial-ms"),
|
|
67
|
-
backoffMultiplier: line.number("--reconnect-multiplier"),
|
|
68
|
-
maxDelay: line.number("--reconnect-max-ms"),
|
|
69
|
-
maxAttempts: line.number("--reconnect-attempts"),
|
|
70
|
-
jitter: line.number("--reconnect-jitter"),
|
|
71
|
-
}
|
|
72
|
-
: { enabled: false };
|
|
73
|
-
const requestTimeout = line.number("--request-timeout-ms");
|
|
74
|
-
const connectionTimeout = line.number("--connection-timeout-ms");
|
|
75
|
-
const hold = line.flag("--hold-ms") === undefined ? undefined : line.number("--hold-ms");
|
|
76
|
-
const go = line.flag("--go") ?? "/tmp/go";
|
|
77
|
-
const max = Number(line.flag("--max-s") ?? 60) * 1000;
|
|
78
|
-
|
|
79
|
-
/** The entry of the transport for each class: the root for the global WebSocket, the Node entry for ws. */
|
|
80
|
-
const ENTRY = socketClass === "ws" ? "@procedures/client/node" : "@procedures/client";
|
|
81
|
-
// the class of the Node entry is the class of the ws that it resolves: it must be the ws of this client
|
|
82
|
-
if (socketClass === "ws") {
|
|
83
|
-
const nodeEntry = fileURLToPath(import.meta.resolve(ENTRY));
|
|
84
|
-
const theirs = createRequire(nodeEntry).resolve("ws");
|
|
85
|
-
const ours = createRequire(import.meta.url).resolve("ws");
|
|
86
|
-
if (theirs !== ours) line.fail(`${ENTRY} resolves ws at ${theirs}, and this client at ${ours}`);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
emit("start", {
|
|
90
|
-
boot_id: bootId(),
|
|
91
|
-
mode,
|
|
92
|
-
client: "procedures-ws",
|
|
93
|
-
package: "@procedures/client 0.7.0",
|
|
94
|
-
entry: ENTRY,
|
|
95
|
-
websocket: socketClass,
|
|
96
|
-
node: process.version,
|
|
97
|
-
undici: process.versions.undici ?? null,
|
|
98
|
-
settings: { heartbeat, reconnect, requestTimeout, connectionTimeout, hold: hold ?? null },
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
/** These are the sockets of the transport, by their WebSocket: the number of each, its local address, and whether the transport dropped it. */
|
|
102
|
-
const sockets = new WeakMap();
|
|
103
|
-
let tries = 0;
|
|
104
|
-
/** This is the last socket error that undici published. The close event of its socket follows it. */
|
|
105
|
-
let pending;
|
|
106
|
-
|
|
107
|
-
/** This function gives the type of a frame of the transport: its JSON `type`. */
|
|
108
|
-
const typeOf = (data) => {
|
|
109
|
-
if (typeof data !== "string") return "binary";
|
|
110
|
-
try {
|
|
111
|
-
return JSON.parse(data).type ?? "unknown";
|
|
112
|
-
} catch {
|
|
113
|
-
return "text";
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
/** This function writes the local address of a socket that opened, once. */
|
|
118
|
-
const opened = (socket, local) => {
|
|
119
|
-
if (socket === undefined || socket.local !== undefined || local === null) return;
|
|
120
|
-
socket.local = local;
|
|
121
|
-
emit("connected", { conn: socket.conn, local });
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
/** This function writes the drop of a socket by the transport, before its close() or its terminate(). */
|
|
125
|
-
const dropped = (websocket, how) => {
|
|
126
|
-
const socket = sockets.get(websocket);
|
|
127
|
-
if (socket !== undefined) socket.dropped = true;
|
|
128
|
-
emit("drop", { conn: socket?.conn ?? null, local: socket?.local ?? null, how });
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* This function gives a subclass of a class of WebSockets that writes the acts of the transport.
|
|
133
|
-
* It has terminate() only when its class has it: the transport calls terminate() when it exists.
|
|
134
|
-
*/
|
|
135
|
-
const stamped = (Base) => {
|
|
136
|
-
class Stamped extends Base {
|
|
137
|
-
constructor(...args) {
|
|
138
|
-
const conn = ++tries;
|
|
139
|
-
emit("dial", { conn });
|
|
140
|
-
super(...args);
|
|
141
|
-
const socket = { conn, local: undefined, dropped: false };
|
|
142
|
-
sockets.set(this, socket);
|
|
143
|
-
// a socket of ws gives its local address at its open, and undici gives it in a diagnostics
|
|
144
|
-
// channel. ws keeps the error of its socket to itself, thus the client reads it there
|
|
145
|
-
this.addEventListener("open", () => {
|
|
146
|
-
const raw = this._socket;
|
|
147
|
-
if (raw?.localAddress !== undefined) opened(socket, `${raw.localAddress}:${raw.localPort}`);
|
|
148
|
-
raw?.on?.("error", (error) => {
|
|
149
|
-
pending = { errno: error?.code ?? error?.name ?? "unknown", at: now() };
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
this.addEventListener("error", (event) => {
|
|
153
|
-
const code = event?.error?.code;
|
|
154
|
-
if (typeof code === "string") pending = { errno: code, at: now() };
|
|
155
|
-
});
|
|
156
|
-
this.addEventListener("close", (event) => {
|
|
157
|
-
const error = pending;
|
|
158
|
-
pending = undefined;
|
|
159
|
-
emit("conn-end", { conn, local: socket.local ?? null, code: event.code, clean: event.wasClean, errno: error?.errno ?? null });
|
|
160
|
-
// the end of the socket of the run, as the client saw it: an error, or the end of the file
|
|
161
|
-
if (conn !== 1 || socket.dropped) return;
|
|
162
|
-
if (error !== undefined) emit("error", { errno: error.errno, local: socket.local ?? null }, error.at);
|
|
163
|
-
else emit("eof", { local: socket.local ?? null });
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
send(...args) {
|
|
168
|
-
const socket = sockets.get(this);
|
|
169
|
-
const type = typeOf(args[0]);
|
|
170
|
-
emit(type === "ping" ? "ping" : "write", { conn: socket?.conn ?? null, local: socket?.local ?? null, ...(type === "ping" ? {} : { type }) });
|
|
171
|
-
return super.send(...args);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
close(...args) {
|
|
175
|
-
dropped(this, "close");
|
|
176
|
-
return super.close(...args);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
const terminate = Base.prototype.terminate;
|
|
180
|
-
if (typeof terminate === "function")
|
|
181
|
-
Stamped.prototype.terminate = function (...args) {
|
|
182
|
-
dropped(this, "terminate");
|
|
183
|
-
return terminate.apply(this, args);
|
|
184
|
-
};
|
|
185
|
-
return Stamped;
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
subscribe("undici:websocket:open", (message) => {
|
|
189
|
-
const address = message.address;
|
|
190
|
-
opened(sockets.get(message.websocket), address === null || address === undefined ? null : `${address.address}:${address.port}`);
|
|
191
|
-
});
|
|
192
|
-
subscribe("undici:websocket:socket_error", (error) => {
|
|
193
|
-
pending = { errno: error?.code ?? error?.name ?? "unknown", at: now() };
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
setTimeout(() => {
|
|
197
|
-
emit("deadline");
|
|
198
|
-
process.exit(0);
|
|
199
|
-
}, max);
|
|
200
|
-
|
|
201
|
-
const { Client } = await import("@procedures/client");
|
|
202
|
-
const { WebSocketTransport } = await import(ENTRY);
|
|
203
|
-
|
|
204
|
-
let first;
|
|
205
|
-
const firstOpen = new Promise((resolve) => {
|
|
206
|
-
first = resolve;
|
|
207
|
-
});
|
|
208
|
-
const transport = new WebSocketTransport({
|
|
209
|
-
url: `ws://${host}:${port}/ws`,
|
|
210
|
-
// the global class has no terminate(): the subclass of it has none either
|
|
211
|
-
webSocket: socketClass === "ws" ? stamped(WsWebSocket) : stamped(globalThis.WebSocket),
|
|
212
|
-
heartbeat: heartbeat.enabled
|
|
213
|
-
? { enabled: true, interval: ms(heartbeat.interval), timeout: ms(heartbeat.timeout), misses: heartbeat.misses }
|
|
214
|
-
: { enabled: false },
|
|
215
|
-
reconnect: reconnect.enabled
|
|
216
|
-
? {
|
|
217
|
-
enabled: true,
|
|
218
|
-
initialDelay: ms(reconnect.initialDelay),
|
|
219
|
-
backoffMultiplier: reconnect.backoffMultiplier,
|
|
220
|
-
maxDelay: ms(reconnect.maxDelay),
|
|
221
|
-
maxAttempts: reconnect.maxAttempts,
|
|
222
|
-
jitter: reconnect.jitter,
|
|
223
|
-
}
|
|
224
|
-
: { enabled: false },
|
|
225
|
-
requestTimeout: ms(requestTimeout),
|
|
226
|
-
connectionTimeout: ms(connectionTimeout),
|
|
227
|
-
onConnect: () => {
|
|
228
|
-
emit("status", { status: "connected" });
|
|
229
|
-
first?.();
|
|
230
|
-
first = undefined;
|
|
231
|
-
},
|
|
232
|
-
onDisconnect: (reason) => emit("status", { status: "disconnected", reason: reason ?? null }),
|
|
233
|
-
onReconnecting: (attempt) => emit("reconnecting", { attempt }),
|
|
234
|
-
onError: (error) => emit("status", { status: "error", message: error?.message ?? null }),
|
|
235
|
-
});
|
|
236
|
-
const client = new Client({ transport });
|
|
237
|
-
|
|
238
|
-
const ECHO = { service: "harness", operation: "echo" };
|
|
239
|
-
const HOLD = { service: "harness", operation: "hold" };
|
|
240
|
-
|
|
241
|
-
await firstOpen;
|
|
242
|
-
try {
|
|
243
|
-
emit("sent");
|
|
244
|
-
const reply = await client.call(ECHO, {});
|
|
245
|
-
emit("ready", { reply: JSON.stringify(reply ?? null) });
|
|
246
|
-
} catch (error) {
|
|
247
|
-
emit("fail", { why: error?.message ?? String(error) });
|
|
248
|
-
process.exit(1);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (mode === "inflight") {
|
|
252
|
-
await waitFor(go);
|
|
253
|
-
// into the fault: its reply never comes, or comes after the fault
|
|
254
|
-
emit("sent");
|
|
255
|
-
client.call(hold === undefined ? ECHO : HOLD, hold === undefined ? {} : { ms: hold }).then(
|
|
256
|
-
(reply) => emit("data", { n: JSON.stringify(reply ?? null).length }),
|
|
257
|
-
(error) => emit("call-error", { code: error?.code ?? error?.name ?? null, message: error?.message ?? String(error) }),
|
|
258
|
-
);
|
|
259
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
// The JSON lines of the procedures programs of the harness: the WebSocket client and the server.
|
|
2
|
-
//
|
|
3
|
-
// Each line has `ev` and `t`, as the TCP client of the harness writes them. `t` is
|
|
4
|
-
// process.hrtime, which libuv reads from CLOCK_MONOTONIC: the clock that the prober stamps the
|
|
5
|
-
// records of the kernel with. A write to a pipe is synchronous on Linux, thus a line goes out
|
|
6
|
-
// before the program does the act that it announces.
|
|
7
|
-
|
|
8
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
-
|
|
10
|
-
/** This function gives the time now, in nanoseconds on CLOCK_MONOTONIC. */
|
|
11
|
-
export const now = () => process.hrtime.bigint();
|
|
12
|
-
|
|
13
|
-
/** This function writes one line, with the time `at` (the time now when none is given). */
|
|
14
|
-
export const emit = (ev, fields = {}, at = now()) =>
|
|
15
|
-
process.stdout.write(`${JSON.stringify({ ev, t: Number(at), ...fields })}\n`);
|
|
16
|
-
|
|
17
|
-
/** This function gives the boot id of the kernel, or null where the file does not exist. */
|
|
18
|
-
export const bootId = () => {
|
|
19
|
-
try {
|
|
20
|
-
return readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
21
|
-
} catch {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* This function reads the command line: positional arguments first, then `--name value` pairs.
|
|
28
|
-
* A setting that the caller must give and does not is a failure: the program has no default.
|
|
29
|
-
*/
|
|
30
|
-
export const commandLine = (usage, positional = 0) => {
|
|
31
|
-
const argv = process.argv.slice(2);
|
|
32
|
-
const flag = (name) => {
|
|
33
|
-
const at = argv.indexOf(name);
|
|
34
|
-
return at === -1 ? undefined : argv[at + 1];
|
|
35
|
-
};
|
|
36
|
-
const fail = (why) => {
|
|
37
|
-
emit("fail", { why: `${why}. Usage: ${usage}` });
|
|
38
|
-
process.exit(1);
|
|
39
|
-
};
|
|
40
|
-
const required = (name) => {
|
|
41
|
-
const value = flag(name);
|
|
42
|
-
if (value === undefined) fail(`no ${name}`);
|
|
43
|
-
return value;
|
|
44
|
-
};
|
|
45
|
-
const number = (name) => {
|
|
46
|
-
const value = Number(required(name));
|
|
47
|
-
if (!Number.isFinite(value)) fail(`${name} is not a number`);
|
|
48
|
-
return value;
|
|
49
|
-
};
|
|
50
|
-
const toggle = (name) => {
|
|
51
|
-
const value = required(name);
|
|
52
|
-
if (value !== "on" && value !== "off") fail(`${name} is on or off`);
|
|
53
|
-
return value === "on";
|
|
54
|
-
};
|
|
55
|
-
const args = argv.slice(0, positional);
|
|
56
|
-
if (args.length < positional || args.some((arg) => arg.startsWith("--"))) fail("the positional arguments are missing");
|
|
57
|
-
return { args, flag, required, number, toggle, fail };
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/** This function waits until the file at `path` exists. It looks each 2 ms, as the other clients of the harness do. */
|
|
61
|
-
export const waitFor = (path) =>
|
|
62
|
-
new Promise((resolve) => {
|
|
63
|
-
const look = setInterval(() => {
|
|
64
|
-
if (!existsSync(path)) return;
|
|
65
|
-
clearInterval(look);
|
|
66
|
-
resolve();
|
|
67
|
-
}, 2);
|
|
68
|
-
});
|