onepipeline-api-cli 0.58.0 → 0.59.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.
@@ -7,9 +7,18 @@
7
7
  // compile, no download at install time. The platform-specific binary ships
8
8
  // inside a per-platform package (`onepipeline-api-cli-<platform>-<arch>`)
9
9
  // declared in this package's `optionalDependencies`; npm installs only the one
10
- // whose `os`/`cpu` match the host, and this shim resolves it and execs it with
10
+ // whose `os`/`cpu` match the host, and this shim resolves it and runs it with
11
11
  // the caller's argv.
12
12
  //
13
+ // On Linux and macOS it *becomes* the binary (`process.execve`): the PID the
14
+ // caller started is the server's own, so a signal sent to it reaches the server,
15
+ // a parent-PID check sees the server, and there is no node process left to
16
+ // orphan anything. There is no other path on those platforms: a node without
17
+ // `process.execve` (a release this package's `engines` excludes) is refused
18
+ // before anything starts, naming that range. Windows has no exec,
19
+ // so there the binary runs as a child this process supervises; `supervise`
20
+ // below says how that child is kept faithful.
21
+ //
13
22
  // This file is committed source; the version and the optionalDependency
14
23
  // versions are stamped from Cargo.toml at publish time by scripts/npm-build.mjs,
15
24
  // which also generates the per-platform packages from the release binaries.
@@ -89,49 +98,114 @@ function binaryPath() {
89
98
  }
90
99
  }
91
100
 
92
- // The signals that mean "stop", and the reason this launcher is asynchronous.
93
- //
94
- // A supervisor — systemd, Docker, a CI job, the smoke test in
95
- // scripts/smoke-published.sh — stops this command by signalling the process it
96
- // started, which is *this* one and not the binary behind it. Under a synchronous
97
- // spawn node has no chance to react: the default disposition kills the launcher
98
- // where it stands, so the caller sees 128+15 instead of the server's own `0`,
99
- // and the binary is left running with nothing waiting on it. So the child is
100
- // spawned asynchronously and each of these is passed on to it, which is what
101
- // lets the server run the graceful shutdown it already has and lets its exit
102
- // status be the one the caller observes.
101
+ // The signals that mean "stop", for the Windows launcher, which runs the binary
102
+ // as a child rather than becoming it.
103
103
  //
104
- // Exactly the two the server installs handlers for (`StopSignal` in
105
- // src/server.rs). Listening for a signal replaces node's default disposition, so
106
- // forwarding one the binary does not handle would trade a clean kill for a
107
- // launcher that outlives its terminal.
104
+ // Listening for one replaces node's default disposition, which would end the
105
+ // launcher where it stands and report a status the server never chose. Exactly
106
+ // the two the server installs handlers for (`StopSignal` in src/server.rs);
107
+ // listening for one the binary does not handle would trade a clean end for a
108
+ // launcher that outlives its terminal. On Windows node only ever raises
109
+ // `SIGINT`, for the console's Ctrl-C.
108
110
  const STOP_SIGNALS = ["SIGTERM", "SIGINT"];
109
111
 
110
- const child = spawn(binaryPath(), process.argv.slice(2), { stdio: "inherit" });
112
+ // Windows: run the binary as a child and make it indistinguishable from running
113
+ // it directly — its stdio is this process's, an interrupt reaches it as the same
114
+ // interrupt (the console delivers it to the child directly; this process passes
115
+ // nothing on), it cannot outlive this process, and this process exits with its
116
+ // status.
117
+ function supervise(binary, args) {
118
+ // Not detached: libuv puts every attached child in a job object that closes
119
+ // with this process, so a launcher that is ended — even by `TerminateProcess`
120
+ // — takes the server with it rather than orphaning it.
121
+ const child = spawn(binary, args, { stdio: "inherit" });
111
122
 
112
- child.on("error", (error) => {
113
- fail(`failed to launch the onepipeline-api binary: ${error.message}`);
114
- });
123
+ child.on("error", (error) => {
124
+ fail(`failed to launch the onepipeline-api binary: ${error.message}`);
125
+ });
115
126
 
116
- for (const signal of STOP_SIGNALS) {
117
- // Passed to the child alone. A terminal's Ctrl-C already reached the whole
118
- // foreground group, and the server treats the repeat as the same request to
119
- // stop that it is already answering.
120
- process.on(signal, () => child.kill(signal));
127
+ for (const signal of STOP_SIGNALS) {
128
+ // Caught, never passed on. The console already delivered the Ctrl-C to
129
+ // every process attached to it, the server included, and `child.kill` on
130
+ // Windows is `TerminateProcess`, which would cut short the very shutdown
131
+ // the interrupt started. This process only has to outlive it and report
132
+ // how it ended.
133
+ process.on(signal, () => {});
134
+ }
135
+
136
+ child.on("exit", (status, signal) => {
137
+ // Propagate the child's exit code verbatim — a caller scripting against the
138
+ // documented exit codes depends on seeing them — or re-raise what ended it.
139
+ // The listeners go first: re-raising into one of them would swallow it.
140
+ for (const stop of STOP_SIGNALS) {
141
+ process.removeAllListeners(stop);
142
+ }
143
+ if (signal) {
144
+ process.kill(process.pid, signal);
145
+ return;
146
+ }
147
+ process.exit(status === null ? 1 : status);
148
+ });
121
149
  }
122
150
 
123
- child.on("exit", (status, signal) => {
124
- // Re-raise a terminating signal so callers observe the true cause; otherwise
125
- // propagate the child's exit code verbatim — a caller scripting against the
126
- // documented exit codes depends on seeing them. The listeners go first:
127
- // re-raising into one of them would be this process signalling itself in a
128
- // loop instead of dying of what killed the binary.
129
- for (const stop of STOP_SIGNALS) {
130
- process.removeAllListeners(stop);
151
+ // Linux and macOS: replace this process with the binary, or fail before
152
+ // anything has started.
153
+ function become(binary, args) {
154
+ // An ignored signal stays ignored across exec, and node ignores these two at
155
+ // startup. Listening and then not listening hands each back to the default
156
+ // disposition, so the binary starts with the signals a shell would give it.
157
+ for (const inherited of ["SIGPIPE", "SIGXFSZ"]) {
158
+ process.on(inherited, () => {});
159
+ process.removeAllListeners(inherited);
131
160
  }
132
- if (signal) {
133
- process.kill(process.pid, signal);
134
- return;
161
+ // A file the kernel would refuse is reported here, before the call: node's
162
+ // 22 and 24 lines abort the whole process when `process.execve` fails rather
163
+ // than throwing, so past this point a refusal would print node's own crash
164
+ // report instead of saying what to do. Missing, not a file, and not
165
+ // executable are the refusals a broken install produces; one only the kernel
166
+ // can see (a binary for another architecture) still reaches the call.
167
+ const fs = require("node:fs");
168
+ try {
169
+ if (!fs.statSync(binary).isFile()) {
170
+ throw new Error(`${binary} is not a file`);
171
+ }
172
+ fs.accessSync(binary, fs.constants.X_OK);
173
+ } catch (error) {
174
+ fail(
175
+ `failed to launch the onepipeline-api binary: ${error.message}. ` +
176
+ `Reinstall onepipeline-api-cli. ${OTHER_INSTALLS}`
177
+ );
135
178
  }
136
- process.exit(status === null ? 1 : status);
137
- });
179
+ // Returns only by throwing, and on this package's supported range only on a
180
+ // node without `process.execve` to call, or on a release that throws for a
181
+ // refusal the check above could not see. Either way nothing was started, and
182
+ // nothing will be — running the binary as a child instead is the process
183
+ // tree this launcher exists not to build.
184
+ if (typeof process.execve !== "function") {
185
+ // The supported range is this package's `engines`, read rather than
186
+ // restated, so the diagnostic cannot name a release the manifest refuses.
187
+ const { engines } = require("../package.json");
188
+ fail(
189
+ "failed to launch the onepipeline-api binary: this command replaces " +
190
+ "itself with the binary through process.execve, which needs Node " +
191
+ `${engines.node}; this is Node ${process.versions.node}. Upgrade Node. ` +
192
+ OTHER_INSTALLS
193
+ );
194
+ }
195
+ try {
196
+ process.execve(binary, [binary, ...args], process.env);
197
+ } catch (error) {
198
+ fail(
199
+ `failed to launch the onepipeline-api binary: ${error.message}. ` +
200
+ `Reinstall onepipeline-api-cli. ${OTHER_INSTALLS}`
201
+ );
202
+ }
203
+ }
204
+
205
+ const binary = binaryPath();
206
+ const args = process.argv.slice(2);
207
+ if (process.platform === "win32") {
208
+ supervise(binary, args);
209
+ } else {
210
+ become(binary, args);
211
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onepipeline-api-cli",
3
- "version": "0.58.0",
3
+ "version": "0.59.0",
4
4
  "description": "Read API and browser view for onepipeline runs.",
5
5
  "keywords": [
6
6
  "cli",
@@ -24,12 +24,12 @@
24
24
  "bin/onepipeline-api.js"
25
25
  ],
26
26
  "engines": {
27
- "node": ">=18"
27
+ "node": ">=22.15.0 <23 || >=23.11.0"
28
28
  },
29
29
  "optionalDependencies": {
30
- "onepipeline-api-cli-linux-x64": "0.58.0",
31
- "onepipeline-api-cli-linux-arm64": "0.58.0",
32
- "onepipeline-api-cli-darwin-arm64": "0.58.0",
33
- "onepipeline-api-cli-win32-x64": "0.58.0"
30
+ "onepipeline-api-cli-linux-x64": "0.59.0",
31
+ "onepipeline-api-cli-linux-arm64": "0.59.0",
32
+ "onepipeline-api-cli-darwin-arm64": "0.59.0",
33
+ "onepipeline-api-cli-win32-x64": "0.59.0"
34
34
  }
35
35
  }