@deepseek-ai/dsh-subprocess-local 0.1.5-rc.2 → 0.1.6-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/README.i18n.yaml +2 -2
- package/README.md +20 -2
- package/README.zh.md +27 -9
- package/lib/index.js +483 -88
- package/lib/output.js +181 -0
- package/lib/{runner-launch-COYGu0Dl.js → runner-launch-DGV26RBf.js} +108 -206
- package/lib/runner.js +21 -10
- package/lib/types/control-spawn.d.ts +19 -0
- package/lib/types/index.d.ts +5 -1
- package/lib/types/linux-execve.d.ts +1 -1
- package/lib/types/linux-scope.d.ts +13 -3
- package/lib/types/managed-owner.d.ts +7 -1
- package/lib/types/output.d.ts +85 -0
- package/lib/types/process-inspector.d.ts +3 -0
- package/lib/types/runner-launch.d.ts +4 -2
- package/lib/types/runner-protocol.d.ts +2 -0
- package/lib/types/shell-activity.d.ts +37 -0
- package/lib/types/spawn-runner.d.ts +1 -1
- package/lib/types/spawn.d.ts +1 -74
- package/lib/types/terminal.d.ts +13 -2
- package/package.json +15 -9
package/lib/output.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
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. At a
|
|
13
|
+
* JavaScript-observable process exit the directory is removed only when it
|
|
14
|
+
* holds no completed spill file (spill files are retained as full-output
|
|
15
|
+
* recovery artifacts until an external cleanup).
|
|
16
|
+
*/
|
|
17
|
+
function privateSpillDir() {
|
|
18
|
+
defaultSpillDir ??= mkdtempSync(join(tmpdir(), "dsh-subprocess-"));
|
|
19
|
+
return defaultSpillDir;
|
|
20
|
+
}
|
|
21
|
+
/* v8 ignore next 4 -- exit listeners run after the coverage dump; removal is verified by the CI /tmp residue measurement. */
|
|
22
|
+
process.once("exit", () => {
|
|
23
|
+
if (defaultSpillDir === void 0) return;
|
|
24
|
+
try {
|
|
25
|
+
rmdirSync(defaultSpillDir);
|
|
26
|
+
} catch {}
|
|
27
|
+
});
|
|
28
|
+
/**
|
|
29
|
+
* Prepare fallible output storage before starting a managed native process.
|
|
30
|
+
* @param internals - optional caller-owned spill directory.
|
|
31
|
+
* @returns binding inputs whose spill directory is ready for use.
|
|
32
|
+
*/
|
|
33
|
+
function prepareManagedProcessBinding(internals = {}) {
|
|
34
|
+
return { spillDir: internals.spillDir ?? privateSpillDir() };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Collects one stream with a bounded in-memory tail. With a spill cap, on
|
|
38
|
+
* first overflow a spill file is created and every chunk (including those
|
|
39
|
+
* already collected) is appended there while the full stream remains within
|
|
40
|
+
* the cap; without one, only the in-memory tail is ever retained (the
|
|
41
|
+
* diagnostic-tail shape — a language server's stderr).
|
|
42
|
+
*
|
|
43
|
+
* Tail-keep rationale (pi/OpenCode): errors and final results cluster at the
|
|
44
|
+
* end of command output; the spill file covers the head.
|
|
45
|
+
*/
|
|
46
|
+
var OutputCollector = class {
|
|
47
|
+
maxBytes;
|
|
48
|
+
maxSpillBytes;
|
|
49
|
+
label;
|
|
50
|
+
spillDir;
|
|
51
|
+
chunks = [];
|
|
52
|
+
bytes = 0;
|
|
53
|
+
dropped = false;
|
|
54
|
+
spillFd;
|
|
55
|
+
spillFile;
|
|
56
|
+
spillDisabled;
|
|
57
|
+
/** Total bytes ever pushed (not just retained). */
|
|
58
|
+
total = 0;
|
|
59
|
+
constructor(maxBytes, maxSpillBytes, label, spillDir) {
|
|
60
|
+
this.maxBytes = maxBytes;
|
|
61
|
+
this.maxSpillBytes = maxSpillBytes;
|
|
62
|
+
this.label = label;
|
|
63
|
+
this.spillDir = spillDir;
|
|
64
|
+
this.spillDisabled = maxSpillBytes === void 0;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Ingest one stream chunk, counting it toward the whole-stream total. On
|
|
68
|
+
* first overflow of the in-memory cap a spill file is opened (when spilling
|
|
69
|
+
* is enabled) and every chunk (already-collected ones included) is appended
|
|
70
|
+
* there from then on; the in-memory tail then drops whole chunks from its
|
|
71
|
+
* head (or the head of a single over-cap chunk) until it fits the cap again.
|
|
72
|
+
* @param chunk - the raw bytes from one stream 'data' event.
|
|
73
|
+
*/
|
|
74
|
+
push(chunk) {
|
|
75
|
+
this.total += chunk.length;
|
|
76
|
+
const overflows = this.bytes + chunk.length > this.maxBytes;
|
|
77
|
+
if (!this.spillDisabled && (overflows || this.spillFd !== void 0)) this.spillAll(chunk);
|
|
78
|
+
this.chunks.push(chunk);
|
|
79
|
+
this.bytes += chunk.length;
|
|
80
|
+
while (this.bytes > this.maxBytes) {
|
|
81
|
+
const head = this.chunks[0];
|
|
82
|
+
const excess = this.bytes - this.maxBytes;
|
|
83
|
+
if (head.length <= excess) {
|
|
84
|
+
this.chunks.shift();
|
|
85
|
+
this.bytes -= head.length;
|
|
86
|
+
} else {
|
|
87
|
+
this.chunks[0] = head.subarray(excess);
|
|
88
|
+
this.bytes -= excess;
|
|
89
|
+
}
|
|
90
|
+
this.dropped = true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Open the spill file lazily and append `chunk` (and any prior chunks once). */
|
|
94
|
+
spillAll(chunk) {
|
|
95
|
+
if (this.maxSpillBytes !== void 0 && this.total > this.maxSpillBytes) {
|
|
96
|
+
this.discardSpill();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (this.spillFd === void 0) {
|
|
100
|
+
this.spillFile = join(this.spillDir, `dsh-subprocess-${process.pid}-${++spillCounter}-${randomBytes(6).toString("hex")}-${this.label}.log`);
|
|
101
|
+
this.spillFd = openSync(this.spillFile, "wx", 384);
|
|
102
|
+
for (const prior of this.chunks) writeSync(this.spillFd, prior);
|
|
103
|
+
}
|
|
104
|
+
writeSync(this.spillFd, chunk);
|
|
105
|
+
}
|
|
106
|
+
/** Stop spilling and remove the file once it can no longer hold the complete stream. */
|
|
107
|
+
discardSpill() {
|
|
108
|
+
const fd = this.spillFd;
|
|
109
|
+
const file = this.spillFile;
|
|
110
|
+
this.spillFd = void 0;
|
|
111
|
+
this.spillFile = void 0;
|
|
112
|
+
this.spillDisabled = true;
|
|
113
|
+
if (fd !== void 0) try {
|
|
114
|
+
closeSync(fd);
|
|
115
|
+
} catch {
|
|
116
|
+
this.spillFd = fd;
|
|
117
|
+
}
|
|
118
|
+
if (file !== void 0) try {
|
|
119
|
+
unlinkSync(file);
|
|
120
|
+
} catch {}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Incremental read in whole-stream byte coordinates: returns everything
|
|
124
|
+
* pushed since `fromByte`. When `fromByte` has already slid out of the
|
|
125
|
+
* in-memory tail window, the read is `lossy` — it returns the whole
|
|
126
|
+
* retained tail and the gap is only recoverable from the spill file.
|
|
127
|
+
* @param fromByte - whole-stream offset to resume from (a prior read's `nextOffset`; 0 for the first read).
|
|
128
|
+
* @returns the delta text, the offset for the next read, the `lossy` flag, and the spill path when one was created.
|
|
129
|
+
*/
|
|
130
|
+
readFrom(fromByte) {
|
|
131
|
+
const windowStart = this.total - this.bytes;
|
|
132
|
+
const buffer = Buffer.concat(this.chunks);
|
|
133
|
+
const lossy = fromByte < windowStart;
|
|
134
|
+
return {
|
|
135
|
+
text: (lossy ? buffer : buffer.subarray(fromByte - windowStart)).toString("utf8"),
|
|
136
|
+
nextOffset: this.total,
|
|
137
|
+
lossy,
|
|
138
|
+
...this.spillFile !== void 0 ? { spillPath: this.spillFile } : {}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Copy the retained raw tail with its position in the complete observed stream.
|
|
143
|
+
* @returns independent tail bytes and the total byte count before truncation.
|
|
144
|
+
*/
|
|
145
|
+
snapshot() {
|
|
146
|
+
return {
|
|
147
|
+
bytes: Buffer.concat(this.chunks),
|
|
148
|
+
totalBytes: this.total
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Close the spill file once the stream has ended. A failed close (delayed
|
|
153
|
+
* writeback fault) stops advertising the spill path — the file may be
|
|
154
|
+
* missing its tail — while every in-memory read keeps working. Idempotent;
|
|
155
|
+
* the spawn path seals both collectors at settlement so reads after exit
|
|
156
|
+
* never point at a still-open file.
|
|
157
|
+
*/
|
|
158
|
+
seal() {
|
|
159
|
+
if (this.spillFd === void 0) return;
|
|
160
|
+
try {
|
|
161
|
+
closeSync(this.spillFd);
|
|
162
|
+
} catch {
|
|
163
|
+
this.spillFile = void 0;
|
|
164
|
+
}
|
|
165
|
+
this.spillFd = void 0;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Seal the spill file and return the final output.
|
|
169
|
+
* @returns the final collected output: tail text, truncation flag, and the spill path when intact.
|
|
170
|
+
*/
|
|
171
|
+
finalize() {
|
|
172
|
+
this.seal();
|
|
173
|
+
return {
|
|
174
|
+
text: Buffer.concat(this.chunks).toString("utf8"),
|
|
175
|
+
truncated: this.dropped,
|
|
176
|
+
...this.spillFile !== void 0 ? { spillPath: this.spillFile } : {}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
//#endregion
|
|
181
|
+
export { OutputCollector, prepareManagedProcessBinding };
|