@qlzt/dsh-subprocess-local 0.0.0-stage → 0.2.1-alpha.2
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/LICENSE +21 -0
- package/README.i18n.yaml +70 -0
- package/README.md +167 -2
- package/README.zh.md +168 -0
- package/lib/index.js +1487 -0
- package/lib/output.js +239 -0
- package/lib/runner-launch-B2zsQ1Dz.js +1529 -0
- package/lib/runner.js +436 -0
- package/lib/types/bin.d.ts +7 -0
- package/lib/types/control-spawn.d.ts +19 -0
- package/lib/types/index.d.ts +52 -0
- package/lib/types/linux-execve.d.ts +9 -0
- package/lib/types/linux-scope.d.ts +91 -0
- package/lib/types/managed-owner.d.ts +36 -0
- package/lib/types/output.d.ts +137 -0
- package/lib/types/process-inspector.d.ts +138 -0
- package/lib/types/runner-launch.d.ts +68 -0
- package/lib/types/runner-protocol.d.ts +111 -0
- package/lib/types/shell-activity.d.ts +37 -0
- package/lib/types/spawn-runner.d.ts +36 -0
- package/lib/types/spawn.d.ts +86 -0
- package/lib/types/terminal.d.ts +80 -0
- package/lib/types/windows-inspector.d.ts +79 -0
- package/lib/types/windows-job.d.ts +30 -0
- package/package.json +60 -4
- package/scripts/ensure-spawn-helper.mjs +16 -0
package/lib/output.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { closeSync, mkdtempSync, openSync, rmdirSync, unlinkSync, writeSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
//#region lib/types/output.js
|
|
6
|
+
/** Bounded output tails and private spill files shared by process providers. */
|
|
7
|
+
let spillCounter = 0;
|
|
8
|
+
let defaultSpillDir;
|
|
9
|
+
/**
|
|
10
|
+
* The default spill location: a private (0700) per-process directory under
|
|
11
|
+
* the OS tmpdir, created lazily. Predictable world-readable paths would let
|
|
12
|
+
* other local users read command output or pre-create symlinks. The directory
|
|
13
|
+
* is created once per process and never recreated: when an external
|
|
14
|
+
* temporary-file cleaner removes it while empty, the next spill open fails and
|
|
15
|
+
* the collector degrades to its in-memory tail. At a JavaScript-observable
|
|
16
|
+
* process exit the directory is removed only when it holds no completed spill
|
|
17
|
+
* file (spill files are retained as full-output recovery artifacts until an
|
|
18
|
+
* external cleanup).
|
|
19
|
+
*/
|
|
20
|
+
function privateSpillDir() {
|
|
21
|
+
defaultSpillDir ??= mkdtempSync(join(tmpdir(), "dsh-subprocess-"));
|
|
22
|
+
return defaultSpillDir;
|
|
23
|
+
}
|
|
24
|
+
/* v8 ignore next 4 -- exit listeners run after the coverage dump; removal is verified by the CI /tmp residue measurement. */
|
|
25
|
+
process.once("exit", () => {
|
|
26
|
+
if (defaultSpillDir === void 0) return;
|
|
27
|
+
try {
|
|
28
|
+
rmdirSync(defaultSpillDir);
|
|
29
|
+
} catch {}
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
32
|
+
* The stderr reporter used when no owner supplies one: a bare
|
|
33
|
+
* {@link prepareManagedProcessBinding} caller has no plugin logger, and the
|
|
34
|
+
* failure must still reach the process diagnostics.
|
|
35
|
+
* @param error - the spill failure.
|
|
36
|
+
* @param label - the failed stream label.
|
|
37
|
+
*/
|
|
38
|
+
function reportSpillFailureToStderr(error, label) {
|
|
39
|
+
process.stderr.write(`dsh-subprocess-local: ${label} spill failed; only the in-memory tail is retained: ${String(error)}\n`);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Build the reporter an owner passes as {@link SpillOptions.onFailure}: one
|
|
43
|
+
* error-level log line naming the owner and stream, with the failure appended
|
|
44
|
+
* so its `code`, `syscall`, and `path` reach the log.
|
|
45
|
+
* @param logger - the owner's plugin logger.
|
|
46
|
+
* @param owner - the component named in the line.
|
|
47
|
+
* @returns the reporter.
|
|
48
|
+
*/
|
|
49
|
+
function logSpillFailure(logger, owner) {
|
|
50
|
+
return (error, label) => {
|
|
51
|
+
const removedDirectory = error.code === "ENOENT";
|
|
52
|
+
logger.error(`${owner} could not write the complete ${label} stream to its spill file; the result keeps only the in-memory tail and reports no full-output path.` + (removedDirectory ? " The spill directory no longer exists; a temporary-file cleaner removing it while empty is the usual cause." : ""), error);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Prepare fallible output storage before starting a managed native process.
|
|
57
|
+
* This is the explicit resolve step for spill inputs: the spill directory
|
|
58
|
+
* defaults to the private per-process directory, and the failure reporter
|
|
59
|
+
* defaults to a stderr line when the caller has no logger.
|
|
60
|
+
* @param internals - optional caller-owned spill directory and failure reporter.
|
|
61
|
+
* @returns binding inputs whose spill directory is ready for use.
|
|
62
|
+
*/
|
|
63
|
+
function prepareManagedProcessBinding(internals = {}) {
|
|
64
|
+
return {
|
|
65
|
+
spillDir: internals.spillDir ?? privateSpillDir(),
|
|
66
|
+
onSpillFailure: internals.onSpillFailure ?? reportSpillFailureToStderr
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Collects one stream with a bounded in-memory tail. With spill options, on
|
|
71
|
+
* first overflow a spill file is created and every chunk (including those
|
|
72
|
+
* already collected) is appended there while the full stream remains within
|
|
73
|
+
* the cap; without them, only the in-memory tail is ever retained (the
|
|
74
|
+
* diagnostic-tail shape — a language server's stderr).
|
|
75
|
+
*
|
|
76
|
+
* Spilling is best-effort: a spill open or write failure discards the spill,
|
|
77
|
+
* reports once through {@link SpillOptions.onFailure}, and never interrupts
|
|
78
|
+
* in-memory collection, because `push()` runs inside the stream's `'data'`
|
|
79
|
+
* listener where a thrown error would become an uncaught exception.
|
|
80
|
+
*
|
|
81
|
+
* Tail-keep rationale (pi/OpenCode): errors and final results cluster at the
|
|
82
|
+
* end of command output; the spill file covers the head.
|
|
83
|
+
*/
|
|
84
|
+
var OutputCollector = class {
|
|
85
|
+
maxBytes;
|
|
86
|
+
label;
|
|
87
|
+
spill;
|
|
88
|
+
chunks = [];
|
|
89
|
+
bytes = 0;
|
|
90
|
+
dropped = false;
|
|
91
|
+
spillFd;
|
|
92
|
+
spillFile;
|
|
93
|
+
spillDisabled;
|
|
94
|
+
/** Total bytes ever pushed (not just retained). */
|
|
95
|
+
total = 0;
|
|
96
|
+
/**
|
|
97
|
+
* @param maxBytes - in-memory tail cap in bytes.
|
|
98
|
+
* @param label - stream label used in spill file names and failure reports.
|
|
99
|
+
* @param spill - spill storage; omit for tail-only collection.
|
|
100
|
+
*/
|
|
101
|
+
constructor(maxBytes, label, spill) {
|
|
102
|
+
this.maxBytes = maxBytes;
|
|
103
|
+
this.label = label;
|
|
104
|
+
this.spill = spill;
|
|
105
|
+
this.spillDisabled = spill === void 0;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Ingest one stream chunk, counting it toward the whole-stream total. On
|
|
109
|
+
* first overflow of the in-memory cap a spill file is opened (when spilling
|
|
110
|
+
* is enabled) and every chunk (already-collected ones included) is appended
|
|
111
|
+
* there from then on; the in-memory tail then drops whole chunks from its
|
|
112
|
+
* head (or the head of a single over-cap chunk) until it fits the cap again.
|
|
113
|
+
* @param chunk - the raw bytes from one stream 'data' event.
|
|
114
|
+
*/
|
|
115
|
+
push(chunk) {
|
|
116
|
+
this.total += chunk.length;
|
|
117
|
+
const overflows = this.bytes + chunk.length > this.maxBytes;
|
|
118
|
+
const spill = this.spill;
|
|
119
|
+
if (spill !== void 0 && !this.spillDisabled && (overflows || this.spillFd !== void 0)) this.spillAll(spill, chunk);
|
|
120
|
+
this.chunks.push(chunk);
|
|
121
|
+
this.bytes += chunk.length;
|
|
122
|
+
while (this.bytes > this.maxBytes) {
|
|
123
|
+
const head = this.chunks[0];
|
|
124
|
+
const excess = this.bytes - this.maxBytes;
|
|
125
|
+
if (head.length <= excess) {
|
|
126
|
+
this.chunks.shift();
|
|
127
|
+
this.bytes -= head.length;
|
|
128
|
+
} else {
|
|
129
|
+
this.chunks[0] = head.subarray(excess);
|
|
130
|
+
this.bytes -= excess;
|
|
131
|
+
}
|
|
132
|
+
this.dropped = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Open the spill file lazily and append `chunk` (and any prior chunks once).
|
|
137
|
+
* Runs inside the stream's `'data'` listener, so every filesystem failure is
|
|
138
|
+
* contained here: the spill is discarded, reported once, and collection
|
|
139
|
+
* continues with the in-memory tail alone.
|
|
140
|
+
*/
|
|
141
|
+
spillAll(spill, chunk) {
|
|
142
|
+
if (this.total > spill.maxBytes) {
|
|
143
|
+
this.discardSpill();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
if (this.spillFd === void 0) {
|
|
148
|
+
const file = join(spill.dir, `dsh-subprocess-${process.pid}-${++spillCounter}-${randomBytes(6).toString("hex")}-${this.label}.log`);
|
|
149
|
+
const fd = openSync(file, "wx", 384);
|
|
150
|
+
this.spillFile = file;
|
|
151
|
+
this.spillFd = fd;
|
|
152
|
+
for (const prior of this.chunks) writeSync(fd, prior);
|
|
153
|
+
}
|
|
154
|
+
writeSync(this.spillFd, chunk);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
this.discardSpill();
|
|
157
|
+
try {
|
|
158
|
+
spill.onFailure(error, this.label);
|
|
159
|
+
} catch (reporterFailure) {
|
|
160
|
+
process.stderr.write(`dsh-subprocess-local: spill failure reporter threw: ${String(reporterFailure)}\n`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/** Stop spilling and remove the file once it can no longer hold the complete stream. */
|
|
165
|
+
discardSpill() {
|
|
166
|
+
const fd = this.spillFd;
|
|
167
|
+
const file = this.spillFile;
|
|
168
|
+
this.spillFd = void 0;
|
|
169
|
+
this.spillFile = void 0;
|
|
170
|
+
this.spillDisabled = true;
|
|
171
|
+
if (fd !== void 0) try {
|
|
172
|
+
closeSync(fd);
|
|
173
|
+
} catch {
|
|
174
|
+
this.spillFd = fd;
|
|
175
|
+
}
|
|
176
|
+
if (file !== void 0) try {
|
|
177
|
+
unlinkSync(file);
|
|
178
|
+
} catch {}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Incremental read in whole-stream byte coordinates: returns everything
|
|
182
|
+
* pushed since `fromByte`. When `fromByte` has already slid out of the
|
|
183
|
+
* in-memory tail window, the read is `lossy` — it returns the whole
|
|
184
|
+
* retained tail and the gap is only recoverable from the spill file.
|
|
185
|
+
* @param fromByte - whole-stream offset to resume from (a prior read's `nextOffset`; 0 for the first read).
|
|
186
|
+
* @returns the delta text, the offset for the next read, the `lossy` flag, and the spill path when one was created.
|
|
187
|
+
*/
|
|
188
|
+
readFrom(fromByte) {
|
|
189
|
+
const windowStart = this.total - this.bytes;
|
|
190
|
+
const buffer = Buffer.concat(this.chunks);
|
|
191
|
+
const lossy = fromByte < windowStart;
|
|
192
|
+
return {
|
|
193
|
+
text: (lossy ? buffer : buffer.subarray(fromByte - windowStart)).toString("utf8"),
|
|
194
|
+
nextOffset: this.total,
|
|
195
|
+
lossy,
|
|
196
|
+
...this.spillFile !== void 0 ? { spillPath: this.spillFile } : {}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Copy the retained raw tail with its position in the complete observed stream.
|
|
201
|
+
* @returns independent tail bytes and the total byte count before truncation.
|
|
202
|
+
*/
|
|
203
|
+
snapshot() {
|
|
204
|
+
return {
|
|
205
|
+
bytes: Buffer.concat(this.chunks),
|
|
206
|
+
totalBytes: this.total
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Close the spill file once the stream has ended. A failed close (delayed
|
|
211
|
+
* writeback fault) stops advertising the spill path — the file may be
|
|
212
|
+
* missing its tail — while every in-memory read keeps working. Idempotent;
|
|
213
|
+
* the spawn path seals both collectors at settlement so reads after exit
|
|
214
|
+
* never point at a still-open file.
|
|
215
|
+
*/
|
|
216
|
+
seal() {
|
|
217
|
+
if (this.spillFd === void 0) return;
|
|
218
|
+
try {
|
|
219
|
+
closeSync(this.spillFd);
|
|
220
|
+
} catch {
|
|
221
|
+
this.spillFile = void 0;
|
|
222
|
+
}
|
|
223
|
+
this.spillFd = void 0;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Seal the spill file and return the final output.
|
|
227
|
+
* @returns the final collected output: tail text, truncation flag, and the spill path when intact.
|
|
228
|
+
*/
|
|
229
|
+
finalize() {
|
|
230
|
+
this.seal();
|
|
231
|
+
return {
|
|
232
|
+
text: Buffer.concat(this.chunks).toString("utf8"),
|
|
233
|
+
truncated: this.dropped,
|
|
234
|
+
...this.spillFile !== void 0 ? { spillPath: this.spillFile } : {}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
//#endregion
|
|
239
|
+
export { OutputCollector, logSpillFailure, prepareManagedProcessBinding };
|