@git.zone/tsrust 1.9.1 → 1.10.1

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.
Files changed (46) hide show
  1. package/dist_ts/00_commitinfo_data.js +2 -2
  2. package/dist_ts/index.d.ts +1 -0
  3. package/dist_ts/index.js +2 -1
  4. package/dist_ts/mod_artifact/classes.artifactassembler.d.ts +6 -0
  5. package/dist_ts/mod_artifact/classes.artifactassembler.js +169 -6
  6. package/dist_ts/mod_artifact/index.d.ts +1 -1
  7. package/dist_ts/mod_artifact/index.js +2 -2
  8. package/dist_ts/mod_cargo/classes.cargorunner.js +8 -5
  9. package/dist_ts/mod_cli/classes.tsrustcli.d.ts +1 -0
  10. package/dist_ts/mod_cli/classes.tsrustcli.js +30 -3
  11. package/dist_ts/mod_cli/helpers.targets.d.ts +2 -0
  12. package/dist_ts/mod_cli/helpers.targets.js +1 -1
  13. package/dist_ts/mod_matrix/classes.matrixcommandrunner.d.ts +25 -0
  14. package/dist_ts/mod_matrix/classes.matrixcommandrunner.js +266 -0
  15. package/dist_ts/mod_matrix/classes.nativematrixbuilder.d.ts +64 -0
  16. package/dist_ts/mod_matrix/classes.nativematrixbuilder.js +1078 -0
  17. package/dist_ts/mod_matrix/helpers.matrixconfig.d.ts +30 -0
  18. package/dist_ts/mod_matrix/helpers.matrixconfig.js +129 -0
  19. package/dist_ts/mod_matrix/index.d.ts +3 -0
  20. package/dist_ts/mod_matrix/index.js +4 -0
  21. package/dist_ts/mod_provenance/classes.gitstate.d.ts +1 -0
  22. package/dist_ts/mod_provenance/classes.gitstate.js +13 -3
  23. package/dist_ts/mod_provenance/classes.provenancestore.d.ts +1 -0
  24. package/dist_ts/mod_provenance/classes.provenancestore.js +5 -1
  25. package/dist_ts/mod_provenance/index.d.ts +1 -1
  26. package/dist_ts/mod_provenance/index.js +2 -2
  27. package/dist_ts/mod_toolchain/classes.toolchainmanager.d.ts +6 -0
  28. package/dist_ts/mod_toolchain/classes.toolchainmanager.js +215 -40
  29. package/package.json +2 -2
  30. package/readme.hints.md +12 -1
  31. package/readme.md +69 -2
  32. package/ts/00_commitinfo_data.ts +1 -1
  33. package/ts/index.ts +1 -0
  34. package/ts/mod_artifact/classes.artifactassembler.ts +201 -5
  35. package/ts/mod_artifact/index.ts +2 -0
  36. package/ts/mod_cargo/classes.cargorunner.ts +9 -4
  37. package/ts/mod_cli/classes.tsrustcli.ts +34 -2
  38. package/ts/mod_cli/helpers.targets.ts +3 -0
  39. package/ts/mod_matrix/classes.matrixcommandrunner.ts +294 -0
  40. package/ts/mod_matrix/classes.nativematrixbuilder.ts +1459 -0
  41. package/ts/mod_matrix/helpers.matrixconfig.ts +222 -0
  42. package/ts/mod_matrix/index.ts +23 -0
  43. package/ts/mod_provenance/classes.gitstate.ts +13 -2
  44. package/ts/mod_provenance/classes.provenancestore.ts +4 -0
  45. package/ts/mod_provenance/index.ts +1 -0
  46. package/ts/mod_toolchain/classes.toolchainmanager.ts +242 -43
@@ -0,0 +1,294 @@
1
+ import * as childProcess from 'node:child_process';
2
+ import * as fs from 'node:fs';
3
+
4
+ export interface IMatrixCommandResult {
5
+ stdout: string;
6
+ stderr: string;
7
+ exitCode: number;
8
+ }
9
+
10
+ export interface IMatrixCommandOptions {
11
+ cwd?: string;
12
+ capture?: boolean;
13
+ timeoutMs?: number;
14
+ heartbeatStdin?: boolean;
15
+ input?: string | Buffer;
16
+ inputFile?: string;
17
+ outputFile?: string;
18
+ maximumOutputBytes?: number;
19
+ allowedExitCodes?: number[];
20
+ environment?: NodeJS.ProcessEnv;
21
+ }
22
+
23
+ export interface IMatrixCommandRunner {
24
+ execute(
25
+ commandArg: string,
26
+ argsArg: string[],
27
+ optionsArg?: IMatrixCommandOptions,
28
+ ): Promise<IMatrixCommandResult>;
29
+ }
30
+
31
+ const defaultCommandTimeoutMs = 60 * 60 * 1000;
32
+ const forceKillDelayMs = 10 * 1000;
33
+ const maximumCapturedOutputBytes = 16 * 1024 * 1024;
34
+ type TForwardedSignal = 'SIGINT' | 'SIGTERM' | 'SIGHUP';
35
+
36
+ export class MatrixCommandRunner implements IMatrixCommandRunner {
37
+ constructor(private environment: NodeJS.ProcessEnv = process.env) {}
38
+
39
+ public async execute(
40
+ commandArg: string,
41
+ argsArg: string[],
42
+ optionsArg: IMatrixCommandOptions = {},
43
+ ): Promise<IMatrixCommandResult> {
44
+ if (
45
+ [optionsArg.heartbeatStdin === true, optionsArg.input !== undefined, !!optionsArg.inputFile]
46
+ .filter(Boolean)
47
+ .length > 1
48
+ ) {
49
+ throw new Error('A matrix command can use only one stdin source');
50
+ }
51
+ if (optionsArg.capture && optionsArg.outputFile) {
52
+ throw new Error('A matrix command cannot capture stdout and write it to a file');
53
+ }
54
+ if (
55
+ optionsArg.outputFile &&
56
+ (!Number.isSafeInteger(optionsArg.maximumOutputBytes) ||
57
+ (optionsArg.maximumOutputBytes || 0) <= 0)
58
+ ) {
59
+ throw new Error('A matrix output file requires a positive byte limit');
60
+ }
61
+ const capture = optionsArg.capture === true;
62
+ let inputFileDescriptor: number | undefined;
63
+ let outputFileDescriptor: number | undefined;
64
+ let child: childProcess.ChildProcess;
65
+ try {
66
+ if (optionsArg.inputFile) {
67
+ inputFileDescriptor = fs.openSync(
68
+ optionsArg.inputFile,
69
+ fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW,
70
+ );
71
+ }
72
+ if (optionsArg.outputFile) {
73
+ outputFileDescriptor = fs.openSync(
74
+ optionsArg.outputFile,
75
+ fs.constants.O_WRONLY |
76
+ fs.constants.O_CREAT |
77
+ fs.constants.O_EXCL |
78
+ fs.constants.O_NOFOLLOW,
79
+ 0o600,
80
+ );
81
+ }
82
+ const stdinMode =
83
+ inputFileDescriptor ??
84
+ (optionsArg.heartbeatStdin || optionsArg.input !== undefined ? 'pipe' : 'ignore');
85
+ const stdoutMode = optionsArg.outputFile ? 'pipe' : capture ? 'pipe' : 'inherit';
86
+ child = childProcess.spawn(commandArg, argsArg, {
87
+ cwd: optionsArg.cwd || process.cwd(),
88
+ env: optionsArg.environment || this.environment,
89
+ stdio: [stdinMode, stdoutMode, capture ? 'pipe' : 'inherit'],
90
+ detached: true,
91
+ });
92
+ } catch (error) {
93
+ if (inputFileDescriptor !== undefined) fs.closeSync(inputFileDescriptor);
94
+ if (outputFileDescriptor !== undefined) fs.closeSync(outputFileDescriptor);
95
+ throw error;
96
+ }
97
+ if (inputFileDescriptor !== undefined) fs.closeSync(inputFileDescriptor);
98
+
99
+ let stdout = '';
100
+ let stderr = '';
101
+ let capturedOutputBytes = 0;
102
+ let timedOut = false;
103
+ let outputLimitExceeded = false;
104
+ let outputFileBytes = 0;
105
+ let outputFileError: Error | undefined;
106
+ let forwardedSignal: TForwardedSignal | undefined;
107
+ let forwardedSignalCount = 0;
108
+ let spawnError: Error | undefined;
109
+ let stdinError: Error | undefined;
110
+ let forceKillTimer: NodeJS.Timeout | undefined;
111
+ let heartbeatTimer: NodeJS.Timeout | undefined;
112
+
113
+ const killProcessGroup = (signalArg: NodeJS.Signals): void => {
114
+ if (!child.pid) return;
115
+ try {
116
+ process.kill(-child.pid, signalArg);
117
+ } catch {
118
+ try {
119
+ child.kill(signalArg);
120
+ } catch {
121
+ // The process may already have exited.
122
+ }
123
+ }
124
+ };
125
+ const terminateProcessGroup = (signalArg: NodeJS.Signals): void => {
126
+ killProcessGroup(signalArg);
127
+ if (!forceKillTimer) {
128
+ forceKillTimer = setTimeout(() => killProcessGroup('SIGKILL'), forceKillDelayMs);
129
+ }
130
+ };
131
+ const processGroupExists = (): boolean => {
132
+ if (!child.pid) return false;
133
+ try {
134
+ process.kill(-child.pid, 0);
135
+ return true;
136
+ } catch (error) {
137
+ if ((error as NodeJS.ErrnoException).code === 'ESRCH') return false;
138
+ throw error;
139
+ }
140
+ };
141
+ const waitForProcessGroupExit = async (timeoutMsArg: number): Promise<boolean> => {
142
+ const deadline = Date.now() + timeoutMsArg;
143
+ while (processGroupExists() && Date.now() < deadline) {
144
+ await new Promise((resolve) => setTimeout(resolve, 100));
145
+ }
146
+ return !processGroupExists();
147
+ };
148
+ const ensureProcessGroupStopped = async (): Promise<void> => {
149
+ if (!processGroupExists()) return;
150
+ terminateProcessGroup('SIGTERM');
151
+ if (!(await waitForProcessGroupExit(forceKillDelayMs + 1000))) {
152
+ killProcessGroup('SIGKILL');
153
+ if (!(await waitForProcessGroupExit(2000))) {
154
+ throw new Error(`${commandArg} left a live descendant process group`);
155
+ }
156
+ }
157
+ };
158
+
159
+ const signalHandlers = new Map<TForwardedSignal, () => void>();
160
+ for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP'] as TForwardedSignal[]) {
161
+ const handler = (): void => {
162
+ forwardedSignal = signal;
163
+ forwardedSignalCount += 1;
164
+ if (forwardedSignalCount === 1) terminateProcessGroup(signal);
165
+ else killProcessGroup('SIGKILL');
166
+ };
167
+ signalHandlers.set(signal, handler);
168
+ process.on(signal, handler);
169
+ }
170
+
171
+ const timeout = setTimeout(() => {
172
+ timedOut = true;
173
+ terminateProcessGroup('SIGTERM');
174
+ }, optionsArg.timeoutMs || defaultCommandTimeoutMs);
175
+
176
+ if (optionsArg.heartbeatStdin) {
177
+ child.stdin?.on('error', () => {
178
+ // Process close handles a lost heartbeat channel.
179
+ });
180
+ const sendHeartbeat = (): void => {
181
+ if (!child.stdin?.destroyed) child.stdin?.write('heartbeat\n');
182
+ };
183
+ sendHeartbeat();
184
+ heartbeatTimer = setInterval(sendHeartbeat, 1000);
185
+ } else if (optionsArg.input !== undefined) {
186
+ child.stdin?.on('error', (errorArg) => {
187
+ stdinError = errorArg;
188
+ terminateProcessGroup('SIGTERM');
189
+ });
190
+ child.stdin?.end(optionsArg.input);
191
+ }
192
+
193
+ const captureChunk = (targetArg: 'stdout' | 'stderr', chunkArg: Buffer): void => {
194
+ capturedOutputBytes += chunkArg.length;
195
+ if (capturedOutputBytes > maximumCapturedOutputBytes) {
196
+ outputLimitExceeded = true;
197
+ terminateProcessGroup('SIGTERM');
198
+ return;
199
+ }
200
+ if (targetArg === 'stdout') stdout += chunkArg.toString('utf8');
201
+ else stderr += chunkArg.toString('utf8');
202
+ };
203
+ if (optionsArg.outputFile) {
204
+ child.stdout?.on('data', (chunkArg: Buffer) => {
205
+ if (outputLimitExceeded || outputFileError || outputFileDescriptor === undefined) return;
206
+ if (outputFileBytes + chunkArg.length > (optionsArg.maximumOutputBytes || 0)) {
207
+ outputLimitExceeded = true;
208
+ terminateProcessGroup('SIGTERM');
209
+ return;
210
+ }
211
+ try {
212
+ let offset = 0;
213
+ while (offset < chunkArg.length) {
214
+ offset += fs.writeSync(
215
+ outputFileDescriptor,
216
+ chunkArg,
217
+ offset,
218
+ chunkArg.length - offset,
219
+ );
220
+ }
221
+ outputFileBytes += chunkArg.length;
222
+ } catch (error) {
223
+ outputFileError = error as Error;
224
+ terminateProcessGroup('SIGTERM');
225
+ }
226
+ });
227
+ } else {
228
+ child.stdout?.on('data', (chunkArg: Buffer) => captureChunk('stdout', chunkArg));
229
+ }
230
+ child.stderr?.on('data', (chunkArg: Buffer) => captureChunk('stderr', chunkArg));
231
+ child.once('error', (errorArg) => {
232
+ spawnError = errorArg;
233
+ });
234
+
235
+ return new Promise<IMatrixCommandResult>((resolve, reject) => {
236
+ child.once('close', (codeArg, signalArg) => {
237
+ void (async () => {
238
+ try {
239
+ if (outputFileDescriptor !== undefined) {
240
+ try {
241
+ fs.fsyncSync(outputFileDescriptor);
242
+ fs.closeSync(outputFileDescriptor);
243
+ outputFileDescriptor = undefined;
244
+ } catch (error) {
245
+ outputFileError = error as Error;
246
+ }
247
+ }
248
+ await ensureProcessGroupStopped();
249
+ const exitCode = codeArg ?? -1;
250
+ if (timedOut) {
251
+ throw new Error(`${commandArg} exceeded its command timeout`);
252
+ }
253
+ if (outputLimitExceeded) {
254
+ throw new Error(`${commandArg} exceeded the captured output limit`);
255
+ }
256
+ if (forwardedSignal) {
257
+ throw new Error(`${commandArg} was interrupted by ${forwardedSignal}`);
258
+ }
259
+ if (spawnError) throw spawnError;
260
+ if (stdinError) throw stdinError;
261
+ if (outputFileError) throw outputFileError;
262
+ if (!(optionsArg.allowedExitCodes || [0]).includes(exitCode)) {
263
+ if (capture && stdout) process.stdout.write(stdout);
264
+ if (capture && stderr) process.stderr.write(stderr);
265
+ throw new Error(
266
+ `${commandArg} exited with ${
267
+ signalArg ? `signal ${signalArg}` : `status ${exitCode}`
268
+ }`,
269
+ );
270
+ }
271
+ resolve({ stdout, stderr, exitCode });
272
+ } catch (error) {
273
+ reject(error);
274
+ } finally {
275
+ clearTimeout(timeout);
276
+ if (forceKillTimer) clearTimeout(forceKillTimer);
277
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
278
+ child.stdin?.end();
279
+ if (outputFileDescriptor !== undefined) {
280
+ try {
281
+ fs.closeSync(outputFileDescriptor);
282
+ } catch {
283
+ // Preserve the command's primary error.
284
+ }
285
+ }
286
+ for (const [signal, handler] of signalHandlers) {
287
+ process.off(signal, handler);
288
+ }
289
+ }
290
+ })();
291
+ });
292
+ });
293
+ }
294
+ }