@livingdata/pipex 0.0.8 → 0.0.10
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 +186 -16
- package/dist/__tests__/errors.js +162 -0
- package/dist/__tests__/helpers.js +41 -0
- package/dist/__tests__/types.js +8 -0
- package/dist/cli/__tests__/condition.js +23 -0
- package/dist/cli/__tests__/dag.js +154 -0
- package/dist/cli/__tests__/pipeline-loader.js +267 -0
- package/dist/cli/__tests__/pipeline-runner.js +257 -0
- package/dist/cli/__tests__/state-persistence.js +80 -0
- package/dist/cli/__tests__/state.js +58 -0
- package/dist/cli/__tests__/step-runner.js +116 -0
- package/dist/cli/commands/bundle.js +35 -0
- package/dist/cli/commands/cat.js +54 -0
- package/dist/cli/commands/clean.js +22 -0
- package/dist/cli/commands/exec.js +89 -0
- package/dist/cli/commands/export.js +32 -0
- package/dist/cli/commands/inspect.js +58 -0
- package/dist/cli/commands/list.js +39 -0
- package/dist/cli/commands/logs.js +54 -0
- package/dist/cli/commands/prune.js +26 -0
- package/dist/cli/commands/rm-step.js +41 -0
- package/dist/cli/commands/rm.js +27 -0
- package/dist/cli/commands/run-bundle.js +59 -0
- package/dist/cli/commands/run.js +44 -0
- package/dist/cli/commands/show.js +108 -0
- package/dist/cli/condition.js +11 -0
- package/dist/cli/dag.js +143 -0
- package/dist/cli/index.js +24 -105
- package/dist/cli/interactive-reporter.js +227 -0
- package/dist/cli/pipeline-loader.js +10 -110
- package/dist/cli/pipeline-runner.js +256 -111
- package/dist/cli/reporter.js +2 -107
- package/dist/cli/state.js +30 -9
- package/dist/cli/step-loader.js +25 -0
- package/dist/cli/step-resolver.js +111 -0
- package/dist/cli/step-runner.js +226 -0
- package/dist/cli/utils.js +3 -0
- package/dist/core/__tests__/bundle.js +663 -0
- package/dist/core/__tests__/condition.js +23 -0
- package/dist/core/__tests__/dag.js +154 -0
- package/dist/core/__tests__/env-file.test.js +41 -0
- package/dist/core/__tests__/event-aggregator.js +244 -0
- package/dist/core/__tests__/pipeline-loader.js +267 -0
- package/dist/core/__tests__/pipeline-runner.js +257 -0
- package/dist/core/__tests__/state-persistence.js +80 -0
- package/dist/core/__tests__/state.js +58 -0
- package/dist/core/__tests__/step-runner.js +118 -0
- package/dist/core/__tests__/stream-reporter.js +142 -0
- package/dist/core/__tests__/transport.js +50 -0
- package/dist/core/__tests__/utils.js +40 -0
- package/dist/core/bundle.js +130 -0
- package/dist/core/condition.js +11 -0
- package/dist/core/dag.js +143 -0
- package/dist/core/env-file.js +6 -0
- package/dist/core/event-aggregator.js +114 -0
- package/dist/core/index.js +14 -0
- package/dist/core/pipeline-loader.js +81 -0
- package/dist/core/pipeline-runner.js +360 -0
- package/dist/core/reporter.js +11 -0
- package/dist/core/state.js +110 -0
- package/dist/core/step-loader.js +25 -0
- package/dist/core/step-resolver.js +117 -0
- package/dist/core/step-runner.js +225 -0
- package/dist/core/stream-reporter.js +41 -0
- package/dist/core/transport.js +9 -0
- package/dist/core/utils.js +56 -0
- package/dist/engine/__tests__/workspace.js +288 -0
- package/dist/engine/docker-executor.js +32 -6
- package/dist/engine/index.js +1 -0
- package/dist/engine/workspace.js +164 -66
- package/dist/errors.js +122 -0
- package/dist/index.js +3 -0
- package/dist/kits/__tests__/index.js +23 -0
- package/dist/kits/builtin/__tests__/node.js +74 -0
- package/dist/kits/builtin/__tests__/python.js +67 -0
- package/dist/kits/builtin/__tests__/shell.js +74 -0
- package/dist/kits/builtin/node.js +10 -5
- package/dist/kits/builtin/python.js +10 -5
- package/dist/kits/builtin/shell.js +2 -1
- package/dist/kits/index.js +2 -1
- package/package.json +6 -3
- package/dist/cli/types.js +0 -3
- package/dist/engine/docker-runtime.js +0 -65
- package/dist/engine/runtime.js +0 -2
- package/dist/kits/bash.js +0 -19
- package/dist/kits/builtin/bash.js +0 -19
- package/dist/kits/node.js +0 -56
- package/dist/kits/python.js +0 -51
- package/dist/kits/types.js +0 -1
- package/dist/reporter.js +0 -13
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { ValidationError } from '../errors.js';
|
|
2
|
+
import { getKit } from '../kits/index.js';
|
|
3
|
+
import { isKitStep } from '../types.js';
|
|
4
|
+
import { slugify, mergeEnv, mergeCaches, mergeMounts } from './pipeline-loader.js';
|
|
5
|
+
/**
|
|
6
|
+
* Resolves a step definition into a fully resolved Step.
|
|
7
|
+
* Kit steps (`uses`) are expanded into image + cmd.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveStep(step) {
|
|
10
|
+
if (!step.id && !step.name) {
|
|
11
|
+
throw new ValidationError('Invalid step: at least one of "id" or "name" must be defined');
|
|
12
|
+
}
|
|
13
|
+
const id = step.id ?? slugify(step.name);
|
|
14
|
+
const { name } = step;
|
|
15
|
+
if (!isKitStep(step)) {
|
|
16
|
+
return { ...step, id, name };
|
|
17
|
+
}
|
|
18
|
+
return resolveKitStep(step, id, name);
|
|
19
|
+
}
|
|
20
|
+
function resolveKitStep(step, id, name) {
|
|
21
|
+
const kit = getKit(step.uses);
|
|
22
|
+
const kitOutput = kit.resolve(step.with ?? {});
|
|
23
|
+
return {
|
|
24
|
+
id,
|
|
25
|
+
name,
|
|
26
|
+
image: kitOutput.image,
|
|
27
|
+
cmd: kitOutput.cmd,
|
|
28
|
+
env: mergeEnv(kitOutput.env, step.env),
|
|
29
|
+
inputs: step.inputs,
|
|
30
|
+
outputPath: step.outputPath,
|
|
31
|
+
caches: mergeCaches(kitOutput.caches, step.caches),
|
|
32
|
+
mounts: mergeMounts(kitOutput.mounts, step.mounts),
|
|
33
|
+
sources: mergeMounts(kitOutput.sources, step.sources),
|
|
34
|
+
timeoutSec: step.timeoutSec,
|
|
35
|
+
allowFailure: step.allowFailure,
|
|
36
|
+
allowNetwork: step.allowNetwork ?? kitOutput.allowNetwork,
|
|
37
|
+
retries: step.retries,
|
|
38
|
+
retryDelayMs: step.retryDelayMs,
|
|
39
|
+
if: step.if
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Validates a resolved step for correctness and security.
|
|
44
|
+
*/
|
|
45
|
+
export function validateStep(step) {
|
|
46
|
+
validateIdentifier(step.id, 'step id');
|
|
47
|
+
if (!step.image || typeof step.image !== 'string') {
|
|
48
|
+
throw new ValidationError(`Invalid step ${step.id}: image is required`);
|
|
49
|
+
}
|
|
50
|
+
if (!Array.isArray(step.cmd) || step.cmd.length === 0) {
|
|
51
|
+
throw new ValidationError(`Invalid step ${step.id}: cmd must be a non-empty array`);
|
|
52
|
+
}
|
|
53
|
+
if (step.inputs) {
|
|
54
|
+
for (const input of step.inputs) {
|
|
55
|
+
validateIdentifier(input.step, `input step name in step ${step.id}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (step.mounts) {
|
|
59
|
+
validateMounts(step.id, step.mounts);
|
|
60
|
+
}
|
|
61
|
+
if (step.sources) {
|
|
62
|
+
validateMounts(step.id, step.sources);
|
|
63
|
+
}
|
|
64
|
+
if (step.caches) {
|
|
65
|
+
validateCaches(step.id, step.caches);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function validateIdentifier(id, context) {
|
|
69
|
+
if (!/^[\w-]+$/.test(id)) {
|
|
70
|
+
throw new ValidationError(`Invalid ${context}: '${id}' must contain only alphanumeric characters, underscore, and hyphen`);
|
|
71
|
+
}
|
|
72
|
+
if (id.includes('..')) {
|
|
73
|
+
throw new ValidationError(`Invalid ${context}: '${id}' cannot contain '..'`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function validateMounts(stepId, mounts) {
|
|
77
|
+
for (const mount of mounts) {
|
|
78
|
+
if (!mount.host || typeof mount.host !== 'string') {
|
|
79
|
+
throw new ValidationError(`Step ${stepId}: mount.host is required and must be a string`);
|
|
80
|
+
}
|
|
81
|
+
if (mount.host.startsWith('/')) {
|
|
82
|
+
throw new ValidationError(`Step ${stepId}: mount.host '${mount.host}' must be a relative path`);
|
|
83
|
+
}
|
|
84
|
+
if (mount.host.includes('..')) {
|
|
85
|
+
throw new ValidationError(`Step ${stepId}: mount.host '${mount.host}' must not contain '..'`);
|
|
86
|
+
}
|
|
87
|
+
if (!mount.container || typeof mount.container !== 'string') {
|
|
88
|
+
throw new ValidationError(`Step ${stepId}: mount.container is required and must be a string`);
|
|
89
|
+
}
|
|
90
|
+
if (!mount.container.startsWith('/')) {
|
|
91
|
+
throw new ValidationError(`Step ${stepId}: mount.container '${mount.container}' must be an absolute path`);
|
|
92
|
+
}
|
|
93
|
+
if (mount.container.includes('..')) {
|
|
94
|
+
throw new ValidationError(`Step ${stepId}: mount.container '${mount.container}' must not contain '..'`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function validateCaches(stepId, caches) {
|
|
99
|
+
for (const cache of caches) {
|
|
100
|
+
if (!cache.name || typeof cache.name !== 'string') {
|
|
101
|
+
throw new ValidationError(`Step ${stepId}: cache.name is required and must be a string`);
|
|
102
|
+
}
|
|
103
|
+
validateIdentifier(cache.name, `cache name in step ${stepId}`);
|
|
104
|
+
if (!cache.path || typeof cache.path !== 'string') {
|
|
105
|
+
throw new ValidationError(`Step ${stepId}: cache.path is required and must be a string`);
|
|
106
|
+
}
|
|
107
|
+
if (!cache.path.startsWith('/')) {
|
|
108
|
+
throw new ValidationError(`Step ${stepId}: cache.path '${cache.path}' must be an absolute path`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { cp, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { setTimeout } from 'node:timers/promises';
|
|
4
|
+
import { createWriteStream } from 'node:fs';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
import { ContainerCrashError, PipexError } from '../errors.js';
|
|
7
|
+
import { StateManager } from './state.js';
|
|
8
|
+
import { dirSize } from './utils.js';
|
|
9
|
+
/**
|
|
10
|
+
* Executes a single step in a workspace.
|
|
11
|
+
* Adapted from PipelineRunner.executeStep() for standalone use.
|
|
12
|
+
*/
|
|
13
|
+
export class StepRunner {
|
|
14
|
+
runtime;
|
|
15
|
+
reporter;
|
|
16
|
+
constructor(runtime, reporter) {
|
|
17
|
+
this.runtime = runtime;
|
|
18
|
+
this.reporter = reporter;
|
|
19
|
+
}
|
|
20
|
+
async run(options) {
|
|
21
|
+
const { workspace, state, step, inputs, pipelineRoot, force, ephemeral } = options;
|
|
22
|
+
const stepRef = { id: step.id, displayName: step.name ?? step.id };
|
|
23
|
+
const resolvedMounts = step.mounts?.map(m => ({
|
|
24
|
+
hostPath: resolve(pipelineRoot, m.host),
|
|
25
|
+
containerPath: m.container
|
|
26
|
+
}));
|
|
27
|
+
const currentFingerprint = this.computeFingerprint(step, inputs, resolvedMounts);
|
|
28
|
+
// Cache check (skip for ephemeral or force)
|
|
29
|
+
if (!force && !ephemeral) {
|
|
30
|
+
const cacheResult = await this.tryUseCache({ workspace, state, stepId: step.id, stepRef, fingerprint: currentFingerprint });
|
|
31
|
+
if (cacheResult) {
|
|
32
|
+
return cacheResult;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
this.reporter.emit({ event: 'STEP_STARTING', workspaceId: workspace.id, step: stepRef });
|
|
36
|
+
return this.executeStep({ workspace, state, step, stepRef, inputs, pipelineRoot, ephemeral, currentFingerprint, resolvedMounts });
|
|
37
|
+
}
|
|
38
|
+
computeFingerprint(step, inputs, resolvedMounts) {
|
|
39
|
+
const inputRunIds = step.inputs
|
|
40
|
+
?.map(i => inputs.get(i.step))
|
|
41
|
+
.filter((id) => id !== undefined);
|
|
42
|
+
return StateManager.fingerprint({
|
|
43
|
+
image: step.image,
|
|
44
|
+
cmd: step.cmd,
|
|
45
|
+
env: step.env,
|
|
46
|
+
inputRunIds,
|
|
47
|
+
mounts: resolvedMounts
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async tryUseCache({ workspace, state, stepId, stepRef, fingerprint }) {
|
|
51
|
+
const cached = state.getStep(stepId);
|
|
52
|
+
if (cached?.fingerprint === fingerprint) {
|
|
53
|
+
const runs = await workspace.listRuns();
|
|
54
|
+
if (runs.includes(cached.runId)) {
|
|
55
|
+
await workspace.linkRun(stepId, cached.runId);
|
|
56
|
+
this.reporter.emit({ event: 'STEP_SKIPPED', workspaceId: workspace.id, step: stepRef, runId: cached.runId, reason: 'cached' });
|
|
57
|
+
return { runId: cached.runId, exitCode: 0 };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
async executeStep(ctx) {
|
|
63
|
+
const { workspace, step, stepRef, inputs, pipelineRoot, ephemeral } = ctx;
|
|
64
|
+
const runId = workspace.generateRunId();
|
|
65
|
+
const stagingPath = await workspace.prepareRun(runId);
|
|
66
|
+
await this.prepareStagingInputs(workspace, step, runId, inputs);
|
|
67
|
+
await this.prepareCaches(workspace, step);
|
|
68
|
+
const { containerInputs, output, caches, mounts } = this.buildMounts(step, runId, inputs, pipelineRoot);
|
|
69
|
+
const stdoutLog = createWriteStream(join(stagingPath, 'stdout.log'));
|
|
70
|
+
const stderrLog = createWriteStream(join(stagingPath, 'stderr.log'));
|
|
71
|
+
try {
|
|
72
|
+
const result = await this.executeWithRetries({
|
|
73
|
+
ctx, containerInputs, output, caches, mounts, stdoutLog, stderrLog
|
|
74
|
+
});
|
|
75
|
+
this.reporter.result(workspace.id, stepRef, result);
|
|
76
|
+
await closeStream(stdoutLog);
|
|
77
|
+
await closeStream(stderrLog);
|
|
78
|
+
if (ephemeral) {
|
|
79
|
+
await workspace.discardRun(runId);
|
|
80
|
+
this.reporter.emit({ event: 'STEP_FINISHED', workspaceId: workspace.id, step: stepRef, ephemeral: true });
|
|
81
|
+
return { exitCode: result.exitCode };
|
|
82
|
+
}
|
|
83
|
+
return await this.commitOrDiscard({ ...ctx, runId, stagingPath, result });
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
await closeStream(stdoutLog);
|
|
87
|
+
await closeStream(stderrLog);
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async prepareStagingInputs(workspace, step, runId, inputs) {
|
|
92
|
+
if (!step.inputs) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
for (const input of step.inputs) {
|
|
96
|
+
const inputRunId = inputs.get(input.step);
|
|
97
|
+
if (inputRunId && input.copyToOutput) {
|
|
98
|
+
await cp(workspace.runArtifactsPath(inputRunId), workspace.runStagingArtifactsPath(runId), { recursive: true });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async prepareCaches(workspace, step) {
|
|
103
|
+
if (!step.caches) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
for (const cache of step.caches) {
|
|
107
|
+
await workspace.prepareCache(cache.name);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
buildMounts(step, runId, inputs, pipelineRoot) {
|
|
111
|
+
const containerInputs = [];
|
|
112
|
+
if (step.inputs) {
|
|
113
|
+
for (const input of step.inputs) {
|
|
114
|
+
const inputRunId = inputs.get(input.step);
|
|
115
|
+
if (inputRunId) {
|
|
116
|
+
containerInputs.push({ runId: inputRunId, containerPath: `/input/${input.step}` });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const output = { stagingRunId: runId, containerPath: step.outputPath ?? '/output' };
|
|
121
|
+
const caches = step.caches?.map(c => ({ name: c.name, containerPath: c.path }));
|
|
122
|
+
const mounts = step.mounts?.map(m => ({
|
|
123
|
+
hostPath: resolve(pipelineRoot, m.host),
|
|
124
|
+
containerPath: m.container
|
|
125
|
+
}));
|
|
126
|
+
return { containerInputs, output, caches, mounts };
|
|
127
|
+
}
|
|
128
|
+
async executeWithRetries({ ctx, containerInputs, output, caches, mounts, stdoutLog, stderrLog }) {
|
|
129
|
+
const { workspace, step, stepRef, pipelineRoot, ephemeral } = ctx;
|
|
130
|
+
const maxRetries = step.retries ?? 0;
|
|
131
|
+
const retryDelay = step.retryDelayMs ?? 5000;
|
|
132
|
+
let result;
|
|
133
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
134
|
+
try {
|
|
135
|
+
result = await this.runtime.run(workspace, {
|
|
136
|
+
name: `pipex-${workspace.id}-${step.id}-${Date.now()}`,
|
|
137
|
+
image: step.image,
|
|
138
|
+
cmd: step.cmd,
|
|
139
|
+
env: step.env,
|
|
140
|
+
inputs: containerInputs,
|
|
141
|
+
output,
|
|
142
|
+
caches,
|
|
143
|
+
mounts,
|
|
144
|
+
sources: step.sources?.map(m => ({
|
|
145
|
+
hostPath: resolve(pipelineRoot, m.host),
|
|
146
|
+
containerPath: m.container
|
|
147
|
+
})),
|
|
148
|
+
network: step.allowNetwork ? 'bridge' : 'none',
|
|
149
|
+
timeoutSec: step.timeoutSec
|
|
150
|
+
}, ({ stream, line }) => {
|
|
151
|
+
if (stream === 'stdout') {
|
|
152
|
+
stdoutLog.write(line + '\n');
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
stderrLog.write(line + '\n');
|
|
156
|
+
}
|
|
157
|
+
if (ephemeral && stream === 'stdout') {
|
|
158
|
+
process.stdout.write(line + '\n');
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
this.reporter.log(workspace.id, stepRef, stream, line);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (error instanceof PipexError && error.transient && attempt < maxRetries) {
|
|
168
|
+
this.reporter.emit({ event: 'STEP_RETRYING', workspaceId: workspace.id, step: stepRef, attempt: attempt + 1, maxRetries });
|
|
169
|
+
await setTimeout(retryDelay);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
async commitOrDiscard({ workspace, state, step, stepRef, inputs, resolvedMounts, currentFingerprint, runId, stagingPath, result }) {
|
|
178
|
+
const meta = {
|
|
179
|
+
runId,
|
|
180
|
+
stepId: step.id,
|
|
181
|
+
stepName: step.name,
|
|
182
|
+
startedAt: result.startedAt.toISOString(),
|
|
183
|
+
finishedAt: result.finishedAt.toISOString(),
|
|
184
|
+
durationMs: result.finishedAt.getTime() - result.startedAt.getTime(),
|
|
185
|
+
exitCode: result.exitCode,
|
|
186
|
+
image: step.image,
|
|
187
|
+
cmd: step.cmd,
|
|
188
|
+
env: step.env,
|
|
189
|
+
inputs: step.inputs?.map(i => ({
|
|
190
|
+
step: i.step,
|
|
191
|
+
runId: inputs.get(i.step),
|
|
192
|
+
mountedAs: `/input/${i.step}`
|
|
193
|
+
})),
|
|
194
|
+
mounts: resolvedMounts,
|
|
195
|
+
caches: step.caches?.map(c => c.name),
|
|
196
|
+
allowNetwork: step.allowNetwork ?? false,
|
|
197
|
+
fingerprint: currentFingerprint,
|
|
198
|
+
status: result.exitCode === 0 ? 'success' : 'failure'
|
|
199
|
+
};
|
|
200
|
+
await writeFile(join(stagingPath, 'meta.json'), JSON.stringify(meta, null, 2), 'utf8');
|
|
201
|
+
if (result.exitCode === 0 || step.allowFailure) {
|
|
202
|
+
await workspace.commitRun(runId);
|
|
203
|
+
await workspace.linkRun(step.id, runId);
|
|
204
|
+
state.setStep(step.id, runId, currentFingerprint);
|
|
205
|
+
await state.save();
|
|
206
|
+
const durationMs = result.finishedAt.getTime() - result.startedAt.getTime();
|
|
207
|
+
const artifactSize = await dirSize(workspace.runArtifactsPath(runId));
|
|
208
|
+
this.reporter.emit({ event: 'STEP_FINISHED', workspaceId: workspace.id, step: stepRef, runId, durationMs, artifactSize });
|
|
209
|
+
return { runId, exitCode: result.exitCode };
|
|
210
|
+
}
|
|
211
|
+
await workspace.discardRun(runId);
|
|
212
|
+
this.reporter.emit({ event: 'STEP_FAILED', workspaceId: workspace.id, step: stepRef, exitCode: result.exitCode });
|
|
213
|
+
throw new ContainerCrashError(step.id, result.exitCode);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function closeStream(stream) {
|
|
217
|
+
if (stream.destroyed) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
return new Promise((resolve, reject) => {
|
|
221
|
+
stream.end(() => {
|
|
222
|
+
resolve();
|
|
223
|
+
});
|
|
224
|
+
stream.on('error', reject);
|
|
225
|
+
});
|
|
226
|
+
}
|