looply-cli 0.5.2 → 0.5.3
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/dist/.src-hash +1 -1
- package/dist/agent-contained.d.ts +2 -0
- package/dist/agent-contained.d.ts.map +1 -1
- package/dist/agent-contained.js +22 -5
- package/dist/agent-contained.js.map +1 -1
- package/dist/agent-exec.d.ts.map +1 -1
- package/dist/agent-exec.js +1 -0
- package/dist/agent-exec.js.map +1 -1
- package/dist/autonomy-tick.d.ts +46 -0
- package/dist/autonomy-tick.d.ts.map +1 -1
- package/dist/autonomy-tick.js +44 -2
- package/dist/autonomy-tick.js.map +1 -1
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +131 -93
- package/dist/bin.js.map +1 -1
- package/dist/brain-advice.js +2 -2
- package/dist/brain-advice.js.map +1 -1
- package/dist/command-runner.d.ts.map +1 -1
- package/dist/command-runner.js +1 -0
- package/dist/command-runner.js.map +1 -1
- package/dist/connect-node.d.ts.map +1 -1
- package/dist/connect-node.js +8 -4
- package/dist/connect-node.js.map +1 -1
- package/dist/connect.d.ts +8 -0
- package/dist/connect.d.ts.map +1 -1
- package/dist/connect.js +22 -0
- package/dist/connect.js.map +1 -1
- package/dist/contained-node.d.ts +51 -0
- package/dist/contained-node.d.ts.map +1 -0
- package/dist/contained-node.js +116 -0
- package/dist/contained-node.js.map +1 -0
- package/dist/daemon-cmd.d.ts.map +1 -1
- package/dist/daemon-cmd.js +43 -1
- package/dist/daemon-cmd.js.map +1 -1
- package/dist/daemon.d.ts +9 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +11 -1
- package/dist/daemon.js.map +1 -1
- package/dist/exec-domains.d.ts +4 -0
- package/dist/exec-domains.d.ts.map +1 -1
- package/dist/exec-domains.js +7 -2
- package/dist/exec-domains.js.map +1 -1
- package/dist/gate-draft.d.ts +99 -0
- package/dist/gate-draft.d.ts.map +1 -0
- package/dist/gate-draft.js +544 -0
- package/dist/gate-draft.js.map +1 -0
- package/dist/git-driver.d.ts.map +1 -1
- package/dist/git-driver.js +1 -0
- package/dist/git-driver.js.map +1 -1
- package/dist/init-detect.d.ts +20 -10
- package/dist/init-detect.d.ts.map +1 -1
- package/dist/init-detect.js +27 -85
- package/dist/init-detect.js.map +1 -1
- package/dist/model-transport.d.ts.map +1 -1
- package/dist/model-transport.js +25 -8
- package/dist/model-transport.js.map +1 -1
- package/dist/notify-channel.d.ts.map +1 -1
- package/dist/notify-channel.js +1 -0
- package/dist/notify-channel.js.map +1 -1
- package/dist/provider-cmd.d.ts +56 -0
- package/dist/provider-cmd.d.ts.map +1 -0
- package/dist/provider-cmd.js +260 -0
- package/dist/provider-cmd.js.map +1 -0
- package/dist/provider-discovery.d.ts +75 -0
- package/dist/provider-discovery.d.ts.map +1 -0
- package/dist/provider-discovery.js +235 -0
- package/dist/provider-discovery.js.map +1 -0
- package/dist/provider-identity.d.ts +3 -3
- package/dist/provider-identity.d.ts.map +1 -1
- package/dist/provider-identity.js +4 -5
- package/dist/provider-identity.js.map +1 -1
- package/dist/provider-preflight.d.ts.map +1 -1
- package/dist/provider-preflight.js +1 -0
- package/dist/provider-preflight.js.map +1 -1
- package/dist/provider-ready.d.ts +66 -0
- package/dist/provider-ready.d.ts.map +1 -0
- package/dist/provider-ready.js +105 -0
- package/dist/provider-ready.js.map +1 -0
- package/dist/provider-state.d.ts.map +1 -1
- package/dist/provider-state.js +1 -0
- package/dist/provider-state.js.map +1 -1
- package/dist/refresh-gates.d.ts +47 -0
- package/dist/refresh-gates.d.ts.map +1 -0
- package/dist/refresh-gates.js +128 -0
- package/dist/refresh-gates.js.map +1 -0
- package/dist/repo-init.d.ts +5 -1
- package/dist/repo-init.d.ts.map +1 -1
- package/dist/repo-init.js +19 -3
- package/dist/repo-init.js.map +1 -1
- package/dist/run-cmd.js +4 -4
- package/dist/run-cmd.js.map +1 -1
- package/dist/run-launcher.d.ts.map +1 -1
- package/dist/run-launcher.js +2 -0
- package/dist/run-launcher.js.map +1 -1
- package/dist/serve-cmd.d.ts.map +1 -1
- package/dist/serve-cmd.js +14 -5
- package/dist/serve-cmd.js.map +1 -1
- package/dist/serve.d.ts +19 -7
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +28 -12
- package/dist/serve.js.map +1 -1
- package/dist/server-client.d.ts +12 -1
- package/dist/server-client.d.ts.map +1 -1
- package/dist/server-client.js +10 -2
- package/dist/server-client.js.map +1 -1
- package/dist/session-checkpoint-runtime.d.ts.map +1 -1
- package/dist/session-checkpoint-runtime.js +1 -0
- package/dist/session-checkpoint-runtime.js.map +1 -1
- package/dist/setup.d.ts +7 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +13 -13
- package/dist/setup.js.map +1 -1
- package/dist/shared-grants.js +2 -2
- package/dist/shared-grants.js.map +1 -1
- package/dist/toolchain-mirror.js +1 -1
- package/dist/toolchain-mirror.js.map +1 -1
- package/dist/untrusted-exec.d.ts +23 -0
- package/dist/untrusted-exec.d.ts.map +1 -1
- package/dist/untrusted-exec.js +18 -1
- package/dist/untrusted-exec.js.map +1 -1
- package/dist/untrusted-gate.d.ts.map +1 -1
- package/dist/untrusted-gate.js +4 -0
- package/dist/untrusted-gate.js.map +1 -1
- package/package.json +2 -2
- package/win/appcontainer.ps1 +97 -4
- package/win/contained-node.cjs +530 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
WHAT NODE NEEDS TO WORK INSIDE A WINDOWS APPCONTAINER, AND CANNOT DO ITSELF.
|
|
4
|
+
|
|
5
|
+
Loaded into every Node process a contained gate or agent starts, through
|
|
6
|
+
`NODE_OPTIONS=--require` (see `contained-node.ts`). It changes nothing about
|
|
7
|
+
what the container may reach — it is plain JavaScript running with the
|
|
8
|
+
container's own token, so it can do nothing the project's code could not do
|
|
9
|
+
itself. It exists so the project's code does not HANG or FAIL on two
|
|
10
|
+
properties of the boundary that were measured, not assumed:
|
|
11
|
+
|
|
12
|
+
1. PIPED CHILD PROCESSES. libuv implements piped stdio as a named pipe
|
|
13
|
+
called `\\?\pipe\uv\<ptr>-<pid>`, and an AppContainer may not create a
|
|
14
|
+
pipe outside its own `\\.\pipe\LOCAL\` namespace. libuv reads the
|
|
15
|
+
resulting ERROR_ACCESS_DENIED as a name COLLISION and retries with the
|
|
16
|
+
next name, for ever — so `spawn(…, {stdio:'pipe'})` never returns and
|
|
17
|
+
the process spins. That is why esbuild (a Go binary driven over its
|
|
18
|
+
stdin/stdout) hung, and with it Vite and Vitest.
|
|
19
|
+
|
|
20
|
+
The repair keeps the API and moves the pipe: each piped slot gets a pipe
|
|
21
|
+
under `\\.\pipe\LOCAL\`, which the container IS allowed to create; the
|
|
22
|
+
child is handed the client end as an inherited handle (which has always
|
|
23
|
+
worked), and the parent reads and writes the server end through a stream
|
|
24
|
+
with the same surface `child.stdout` always had. `spawnSync` and the
|
|
25
|
+
`exec*Sync` family go through temporary files instead — a synchronous
|
|
26
|
+
call cannot wait for an asynchronous accept.
|
|
27
|
+
|
|
28
|
+
`fork()`'s IPC channel cannot be moved: its pipe is created inside
|
|
29
|
+
libuv and set up by Node internals no preload can reach. So it FAILS
|
|
30
|
+
FAST, naming the cause, instead of hanging until the gate's timeout —
|
|
31
|
+
a test runner then needs its worker-thread pool (`vitest --pool=threads`,
|
|
32
|
+
jest `--runInBand` or `workerThreads`).
|
|
33
|
+
|
|
34
|
+
2. REALPATH. `fs.realpathSync.native` fails with EPERM on EVERY path — it
|
|
35
|
+
maps the final path back to a drive letter, which a container is not
|
|
36
|
+
allowed to ask — and the JavaScript `fs.realpath` walks up to `lstat C:\`,
|
|
37
|
+
which the container cannot read either. Jest and Vite both call it at
|
|
38
|
+
startup. On EPERM/EACCES only, the path is resolved again component by
|
|
39
|
+
component, following the links the container CAN see and taking the
|
|
40
|
+
ancestors it cannot see as they are. Any other error is unchanged.
|
|
41
|
+
|
|
42
|
+
Installed only when `LOOPLY_CONTAINED_NODE=1`, so requiring the file to test
|
|
43
|
+
it changes nothing.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
const BS = String.fromCharCode(92);
|
|
47
|
+
const LOCAL_PIPE_PREFIX = BS + BS + '.' + BS + 'pipe' + BS + 'LOCAL' + BS + 'looply-';
|
|
48
|
+
|
|
49
|
+
const IPC_REFUSAL =
|
|
50
|
+
'child_process IPC (fork, or stdio "ipc") cannot work inside the Looply Windows sandbox: ' +
|
|
51
|
+
'Node creates its IPC pipe where an AppContainer may not, and would hang for ever. ' +
|
|
52
|
+
'Use a worker-thread pool instead (vitest --pool=threads; jest --runInBand or workerThreads).';
|
|
53
|
+
|
|
54
|
+
/* ─────────────────────────────────────────────────────────── realpath ── */
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A realpath that tolerates ancestors it may not read.
|
|
58
|
+
*
|
|
59
|
+
* Walks the absolute path one component at a time. A component the container
|
|
60
|
+
* cannot `lstat` is taken as it is (it is an ancestor above the grant, and the
|
|
61
|
+
* container could not follow a link there anyway); a component it CAN see is
|
|
62
|
+
* followed if it is a link. A component that does not exist throws ENOENT, as
|
|
63
|
+
* realpath does.
|
|
64
|
+
*/
|
|
65
|
+
function lenientRealpath(fs, path, p) {
|
|
66
|
+
// The root as the platform spells it — `C:\`, or `\\server\share\` — and
|
|
67
|
+
// the components below it.
|
|
68
|
+
const split = (abs) => {
|
|
69
|
+
const root = path.parse(abs).root;
|
|
70
|
+
return [root, abs.slice(root.length).split(/[\\/]+/)];
|
|
71
|
+
};
|
|
72
|
+
let [current, pending] = split(path.resolve(p));
|
|
73
|
+
let links = 0;
|
|
74
|
+
while (pending.length > 0) {
|
|
75
|
+
const part = pending.shift();
|
|
76
|
+
if (part === '' || part === '.') continue;
|
|
77
|
+
if (part === '..') {
|
|
78
|
+
current = path.dirname(current);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const next = path.join(current, part);
|
|
82
|
+
let st = null;
|
|
83
|
+
try {
|
|
84
|
+
st = fs.lstatSync(next);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
if (!e || (e.code !== 'EPERM' && e.code !== 'EACCES')) throw e;
|
|
87
|
+
}
|
|
88
|
+
if (st !== null && st.isSymbolicLink()) {
|
|
89
|
+
if (++links > 40) {
|
|
90
|
+
const err = new Error(`ELOOP: too many symbolic links encountered, realpath '${p}'`);
|
|
91
|
+
err.code = 'ELOOP';
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
const [root, parts] = split(path.resolve(current, fs.readlinkSync(next)));
|
|
95
|
+
current = root;
|
|
96
|
+
pending = parts.concat(pending);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
current = next;
|
|
100
|
+
}
|
|
101
|
+
return current;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function denied(e) {
|
|
105
|
+
return !!e && (e.code === 'EPERM' || e.code === 'EACCES');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function toPathString(p) {
|
|
109
|
+
if (typeof p === 'string') return p;
|
|
110
|
+
if (Buffer.isBuffer(p)) return p.toString('utf8');
|
|
111
|
+
if (p instanceof URL) return require('url').fileURLToPath(p);
|
|
112
|
+
return String(p);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function encode(result, options) {
|
|
116
|
+
const enc = typeof options === 'string' ? options : options && options.encoding;
|
|
117
|
+
return enc === 'buffer' ? Buffer.from(result, 'utf8') : result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function installRealpath(fs, path) {
|
|
121
|
+
const lenient = (p, options) => encode(lenientRealpath(fs, path, toPathString(p)), options);
|
|
122
|
+
|
|
123
|
+
const wrapSync = (orig) =>
|
|
124
|
+
function realpathSync(p, options) {
|
|
125
|
+
try {
|
|
126
|
+
return orig.call(fs, p, options);
|
|
127
|
+
} catch (e) {
|
|
128
|
+
if (!denied(e)) throw e;
|
|
129
|
+
return lenient(p, options);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const wrapCallback = (orig) =>
|
|
133
|
+
function realpath(p, options, callback) {
|
|
134
|
+
if (typeof options === 'function') {
|
|
135
|
+
callback = options;
|
|
136
|
+
options = undefined;
|
|
137
|
+
}
|
|
138
|
+
orig.call(fs, p, options, (e, resolved) => {
|
|
139
|
+
if (!denied(e)) return callback(e, resolved);
|
|
140
|
+
let out;
|
|
141
|
+
try {
|
|
142
|
+
out = lenient(p, options);
|
|
143
|
+
} catch (e2) {
|
|
144
|
+
return callback(e2);
|
|
145
|
+
}
|
|
146
|
+
callback(null, out);
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const sync = wrapSync(fs.realpathSync);
|
|
151
|
+
sync.native = wrapSync(fs.realpathSync.native);
|
|
152
|
+
fs.realpathSync = sync;
|
|
153
|
+
const cb = wrapCallback(fs.realpath);
|
|
154
|
+
cb.native = wrapCallback(fs.realpath.native);
|
|
155
|
+
fs.realpath = cb;
|
|
156
|
+
|
|
157
|
+
const origPromise = fs.promises.realpath;
|
|
158
|
+
fs.promises.realpath = async function realpath(p, options) {
|
|
159
|
+
try {
|
|
160
|
+
return await origPromise.call(fs.promises, p, options);
|
|
161
|
+
} catch (e) {
|
|
162
|
+
if (!denied(e)) throw e;
|
|
163
|
+
return lenient(p, options);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* ───────────────────────────────────────────────────── piped children ── */
|
|
169
|
+
|
|
170
|
+
/** Every piped slot normalised the way Node normalises it. */
|
|
171
|
+
function normaliseStdio(stdio) {
|
|
172
|
+
if (stdio === undefined || stdio === null) stdio = 'pipe';
|
|
173
|
+
if (typeof stdio === 'string') return [stdio, stdio, stdio];
|
|
174
|
+
if (!Array.isArray(stdio)) return null; // Node will throw its own error for this
|
|
175
|
+
const out = stdio.slice();
|
|
176
|
+
for (let i = 0; i < out.length; i += 1) {
|
|
177
|
+
if (out[i] === undefined || out[i] === null) out[i] = i < 3 ? 'pipe' : 'ignore';
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const isPipe = (s) => s === 'pipe' || s === 'overlapped';
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* A pipe pair under `\\.\pipe\LOCAL\`: the child's end as a file descriptor,
|
|
186
|
+
* opened NOW, and the parent's end as a stream that is wired the moment the
|
|
187
|
+
* server accepts it — on the next turn of the loop.
|
|
188
|
+
*
|
|
189
|
+
* The server must stay open until it has accepted: closing it first closes the
|
|
190
|
+
* pending instance the child is connected to, and the child's first write fails
|
|
191
|
+
* with EPIPE.
|
|
192
|
+
*/
|
|
193
|
+
function pipePair(deps, end) {
|
|
194
|
+
const { fs, net, crypto } = deps;
|
|
195
|
+
const name = LOCAL_PIPE_PREFIX + process.pid + '-' + crypto.randomBytes(8).toString('hex');
|
|
196
|
+
const server = net.createServer();
|
|
197
|
+
server.on('error', (e) => end.destroy(e));
|
|
198
|
+
server.once('connection', (socket) => {
|
|
199
|
+
server.close();
|
|
200
|
+
end._attach(socket);
|
|
201
|
+
});
|
|
202
|
+
// Binding a pipe server is synchronous; only the accept is not.
|
|
203
|
+
server.listen(name);
|
|
204
|
+
// Never keep a process alive on its own: the child is already connected, so
|
|
205
|
+
// the accept completes on the next loop turn whenever anything else runs.
|
|
206
|
+
server.unref();
|
|
207
|
+
let fd;
|
|
208
|
+
try {
|
|
209
|
+
fd = fs.openSync(name, 'r+');
|
|
210
|
+
} catch (e) {
|
|
211
|
+
server.close();
|
|
212
|
+
throw e;
|
|
213
|
+
}
|
|
214
|
+
return fd;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function makePipeEnd(Duplex) {
|
|
218
|
+
/**
|
|
219
|
+
* The parent's end of one piped slot.
|
|
220
|
+
*
|
|
221
|
+
* A Duplex over a socket that does not exist yet: writes queue until it does,
|
|
222
|
+
* reads are pushed as they arrive and pause the socket when nobody is reading.
|
|
223
|
+
* `ref`/`unref` are kept and applied, because esbuild unrefs its service's
|
|
224
|
+
* stdio so a process using it can exit.
|
|
225
|
+
*/
|
|
226
|
+
return class PipeEnd extends Duplex {
|
|
227
|
+
constructor(options) {
|
|
228
|
+
super(options);
|
|
229
|
+
this._socket = null;
|
|
230
|
+
this._queued = [];
|
|
231
|
+
this._finalCallback = null;
|
|
232
|
+
this._refed = true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
_attach(socket) {
|
|
236
|
+
if (this.destroyed) {
|
|
237
|
+
socket.destroy();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
this._socket = socket;
|
|
241
|
+
if (!this._refed) socket.unref();
|
|
242
|
+
socket.on('data', (chunk) => {
|
|
243
|
+
if (!this.push(chunk)) socket.pause();
|
|
244
|
+
});
|
|
245
|
+
socket.on('end', () => this.push(null));
|
|
246
|
+
socket.on('error', (e) => this.destroy(e));
|
|
247
|
+
for (const [chunk, encoding, callback] of this._queued) socket.write(chunk, encoding, callback);
|
|
248
|
+
this._queued = [];
|
|
249
|
+
if (this._finalCallback !== null) {
|
|
250
|
+
const callback = this._finalCallback;
|
|
251
|
+
this._finalCallback = null;
|
|
252
|
+
this._finish(callback);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
_read() {
|
|
257
|
+
if (this._socket !== null) this._socket.resume();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
_write(chunk, encoding, callback) {
|
|
261
|
+
if (this._socket !== null) this._socket.write(chunk, encoding, callback);
|
|
262
|
+
else this._queued.push([chunk, encoding, callback]);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
_final(callback) {
|
|
266
|
+
if (this._socket !== null) this._finish(callback);
|
|
267
|
+
else this._finalCallback = callback;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** A named pipe has no half-close: the child sees EOF when the handle closes. */
|
|
271
|
+
_finish(callback) {
|
|
272
|
+
const socket = this._socket;
|
|
273
|
+
socket.end(() => {
|
|
274
|
+
socket.destroy();
|
|
275
|
+
callback();
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
_destroy(error, callback) {
|
|
280
|
+
if (this._socket !== null) this._socket.destroy();
|
|
281
|
+
for (const [, , cb] of this._queued) cb(error || new Error('stream destroyed'));
|
|
282
|
+
this._queued = [];
|
|
283
|
+
callback(error);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
ref() {
|
|
287
|
+
this._refed = true;
|
|
288
|
+
if (this._socket !== null) this._socket.ref();
|
|
289
|
+
return this;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
unref() {
|
|
293
|
+
this._refed = false;
|
|
294
|
+
if (this._socket !== null) this._socket.unref();
|
|
295
|
+
return this;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function installSpawn(deps) {
|
|
301
|
+
const { cp, fs } = deps;
|
|
302
|
+
const PipeEnd = makePipeEnd(require('stream').Duplex);
|
|
303
|
+
const original = cp.ChildProcess.prototype.spawn;
|
|
304
|
+
|
|
305
|
+
cp.ChildProcess.prototype.spawn = function spawn(options) {
|
|
306
|
+
const stdio = options && typeof options === 'object' ? normaliseStdio(options.stdio) : null;
|
|
307
|
+
if (stdio === null) return original.call(this, options);
|
|
308
|
+
if (stdio.includes('ipc')) {
|
|
309
|
+
const err = new Error(IPC_REFUSAL);
|
|
310
|
+
err.code = 'ERR_LOOPLY_CONTAINED_IPC';
|
|
311
|
+
throw err;
|
|
312
|
+
}
|
|
313
|
+
if (!stdio.some(isPipe)) return original.call(this, options);
|
|
314
|
+
|
|
315
|
+
const ends = [];
|
|
316
|
+
const fds = [];
|
|
317
|
+
try {
|
|
318
|
+
for (let i = 0; i < stdio.length; i += 1) {
|
|
319
|
+
if (!isPipe(stdio[i])) continue;
|
|
320
|
+
// Node's own shape: stdin is written, every other slot is read.
|
|
321
|
+
const end = new PipeEnd(i === 0 ? { readable: false } : { writable: false });
|
|
322
|
+
const fd = pipePair(deps, end);
|
|
323
|
+
ends.push([i, end]);
|
|
324
|
+
fds.push(fd);
|
|
325
|
+
stdio[i] = fd;
|
|
326
|
+
}
|
|
327
|
+
} catch (e) {
|
|
328
|
+
for (const fd of fds) fs.closeSync(fd);
|
|
329
|
+
for (const [, end] of ends) end.destroy();
|
|
330
|
+
throw e;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let result;
|
|
334
|
+
try {
|
|
335
|
+
result = original.call(this, Object.assign({}, options, { stdio }));
|
|
336
|
+
} finally {
|
|
337
|
+
// The child holds its own copies now. Ours must go, or the parent keeps
|
|
338
|
+
// the pipe open and never sees the child's EOF.
|
|
339
|
+
for (const fd of fds) fs.closeSync(fd);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const spawned = typeof this.pid === 'number' && this.pid !== 0;
|
|
343
|
+
for (const [i, end] of ends) {
|
|
344
|
+
if (!spawned) {
|
|
345
|
+
end.destroy();
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
this.stdio[i] = end;
|
|
349
|
+
if (i === 0) this.stdin = end;
|
|
350
|
+
if (i === 1) this.stdout = end;
|
|
351
|
+
if (i === 2) this.stderr = end;
|
|
352
|
+
if (i > 0) {
|
|
353
|
+
// Node waits for every READ slot to close before 'close', so the last
|
|
354
|
+
// of a child's output is never lost to an early 'close'. Same here,
|
|
355
|
+
// through the same counters its own `maybeClose` uses.
|
|
356
|
+
this._closesNeeded += 1;
|
|
357
|
+
end.on('close', () => {
|
|
358
|
+
this._closesGot += 1;
|
|
359
|
+
if (this._closesGot === this._closesNeeded) this.emit('close', this.exitCode, this.signalCode);
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return result;
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/* ─────────────────────────────────────────────────── synchronous calls ── */
|
|
368
|
+
|
|
369
|
+
function installSync(deps) {
|
|
370
|
+
const { cp, fs, path, os, crypto } = deps;
|
|
371
|
+
const originalSpawnSync = cp.spawnSync;
|
|
372
|
+
const DEFAULT_MAX_BUFFER = 1024 * 1024;
|
|
373
|
+
|
|
374
|
+
function spawnSync(file, args, options) {
|
|
375
|
+
if (!Array.isArray(args)) {
|
|
376
|
+
options = args;
|
|
377
|
+
args = [];
|
|
378
|
+
}
|
|
379
|
+
options = Object.assign({}, options);
|
|
380
|
+
const stdio = normaliseStdio(options.stdio);
|
|
381
|
+
if (stdio === null) return originalSpawnSync(file, args, options);
|
|
382
|
+
if (stdio.includes('ipc')) {
|
|
383
|
+
const err = new Error(IPC_REFUSAL);
|
|
384
|
+
err.code = 'ERR_LOOPLY_CONTAINED_IPC';
|
|
385
|
+
throw err;
|
|
386
|
+
}
|
|
387
|
+
if (!stdio.some(isPipe)) return originalSpawnSync(file, args, options);
|
|
388
|
+
|
|
389
|
+
const box = fs.mkdtempSync(path.join(os.tmpdir(), 'looply-sync-'));
|
|
390
|
+
const opened = [];
|
|
391
|
+
const outputs = [];
|
|
392
|
+
try {
|
|
393
|
+
for (let i = 0; i < stdio.length; i += 1) {
|
|
394
|
+
if (!isPipe(stdio[i])) continue;
|
|
395
|
+
const file_ = path.join(box, String(i) + '-' + crypto.randomBytes(4).toString('hex'));
|
|
396
|
+
if (i === 0) {
|
|
397
|
+
const input = options.input;
|
|
398
|
+
const bytes =
|
|
399
|
+
input === undefined || input === null
|
|
400
|
+
? Buffer.alloc(0)
|
|
401
|
+
: typeof input === 'string'
|
|
402
|
+
? Buffer.from(input, options.encoding && options.encoding !== 'buffer' ? options.encoding : 'utf8')
|
|
403
|
+
: Buffer.from(input.buffer, input.byteOffset, input.byteLength);
|
|
404
|
+
fs.writeFileSync(file_, bytes);
|
|
405
|
+
const fd = fs.openSync(file_, 'r');
|
|
406
|
+
opened.push(fd);
|
|
407
|
+
stdio[i] = fd;
|
|
408
|
+
} else {
|
|
409
|
+
const fd = fs.openSync(file_, 'w');
|
|
410
|
+
opened.push(fd);
|
|
411
|
+
outputs.push([i, file_]);
|
|
412
|
+
stdio[i] = fd;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
delete options.input;
|
|
416
|
+
const ret = originalSpawnSync(file, args, Object.assign(options, { stdio }));
|
|
417
|
+
for (const fd of opened.splice(0)) fs.closeSync(fd);
|
|
418
|
+
|
|
419
|
+
const maxBuffer = options.maxBuffer === undefined ? DEFAULT_MAX_BUFFER : options.maxBuffer;
|
|
420
|
+
const encoding = options.encoding && options.encoding !== 'buffer' ? options.encoding : null;
|
|
421
|
+
const output = Array.isArray(ret.output) ? ret.output.slice() : new Array(stdio.length).fill(null);
|
|
422
|
+
for (const [i, file_] of outputs) {
|
|
423
|
+
let bytes = fs.readFileSync(file_);
|
|
424
|
+
if (Number.isFinite(maxBuffer) && bytes.length > maxBuffer) {
|
|
425
|
+
bytes = bytes.subarray(0, maxBuffer);
|
|
426
|
+
if (!ret.error) {
|
|
427
|
+
const err = new Error(`spawnSync ${file} ENOBUFS`);
|
|
428
|
+
err.code = 'ENOBUFS';
|
|
429
|
+
err.errno = -4060;
|
|
430
|
+
err.syscall = `spawnSync ${file}`;
|
|
431
|
+
err.path = file;
|
|
432
|
+
err.spawnargs = args;
|
|
433
|
+
ret.error = err;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
output[i] = encoding === null ? bytes : bytes.toString(encoding);
|
|
437
|
+
}
|
|
438
|
+
ret.output = output;
|
|
439
|
+
ret.stdout = output[1] === undefined ? null : output[1];
|
|
440
|
+
ret.stderr = output[2] === undefined ? null : output[2];
|
|
441
|
+
return ret;
|
|
442
|
+
} finally {
|
|
443
|
+
for (const fd of opened) fs.closeSync(fd);
|
|
444
|
+
try {
|
|
445
|
+
fs.rmSync(box, { recursive: true, force: true });
|
|
446
|
+
} catch {
|
|
447
|
+
/* a temp directory is not this call's verdict */
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Node's own `checkExecSyncError`, which is not exported. */
|
|
453
|
+
function execSyncError(ret, command) {
|
|
454
|
+
if (ret.error) return Object.assign(ret.error, ret);
|
|
455
|
+
if (ret.status !== 0) {
|
|
456
|
+
let message = 'Command failed: ' + command;
|
|
457
|
+
if (ret.stderr && ret.stderr.length > 0) message += '\n' + ret.stderr.toString();
|
|
458
|
+
return Object.assign(new Error(message), ret);
|
|
459
|
+
}
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function execFileSync(file, args, options) {
|
|
464
|
+
if (!Array.isArray(args)) {
|
|
465
|
+
options = args;
|
|
466
|
+
args = [];
|
|
467
|
+
}
|
|
468
|
+
options = Object.assign({}, options);
|
|
469
|
+
const inheritStderr = !options.stdio;
|
|
470
|
+
const ret = spawnSync(file, args, options);
|
|
471
|
+
if (inheritStderr && ret.stderr) process.stderr.write(ret.stderr);
|
|
472
|
+
const err = execSyncError(ret, [options.argv0 || file].concat(args).join(' '));
|
|
473
|
+
if (err) throw err;
|
|
474
|
+
return ret.stdout;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function execSync(command, options) {
|
|
478
|
+
options = Object.assign({}, options);
|
|
479
|
+
options.shell = typeof options.shell === 'string' ? options.shell : true;
|
|
480
|
+
const inheritStderr = !options.stdio;
|
|
481
|
+
const ret = spawnSync(command, [], options);
|
|
482
|
+
if (inheritStderr && ret.stderr) process.stderr.write(ret.stderr);
|
|
483
|
+
const err = execSyncError(ret, command);
|
|
484
|
+
if (err) throw err;
|
|
485
|
+
return ret.stdout;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
cp.spawnSync = spawnSync;
|
|
489
|
+
cp.execFileSync = execFileSync;
|
|
490
|
+
cp.execSync = execSync;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/* ───────────────────────────────────────────────────────────── install ── */
|
|
494
|
+
|
|
495
|
+
function install() {
|
|
496
|
+
if (process.platform !== 'win32') return false;
|
|
497
|
+
if (global.__looplyContainedNode) return false;
|
|
498
|
+
global.__looplyContainedNode = true;
|
|
499
|
+
const deps = {
|
|
500
|
+
cp: require('child_process'),
|
|
501
|
+
fs: require('fs'),
|
|
502
|
+
net: require('net'),
|
|
503
|
+
os: require('os'),
|
|
504
|
+
path: require('path'),
|
|
505
|
+
crypto: require('crypto'),
|
|
506
|
+
};
|
|
507
|
+
/*
|
|
508
|
+
* Windows REWRITES `TEMP`/`TMP` for an AppContainer process, to
|
|
509
|
+
* `<LOCALAPPDATA>\Packages\<container>\AC\Temp` — whatever the environment
|
|
510
|
+
* block said — and nothing creates that directory, so `os.tmpdir()` names a
|
|
511
|
+
* path that is not there and every temp file fails with ENOENT (measured).
|
|
512
|
+
* It is under the contained root's granted `local`, so it can be made.
|
|
513
|
+
*/
|
|
514
|
+
try {
|
|
515
|
+
deps.fs.mkdirSync(deps.os.tmpdir(), { recursive: true });
|
|
516
|
+
} catch {
|
|
517
|
+
/* a temp directory we cannot make is a failure for whoever needs it */
|
|
518
|
+
}
|
|
519
|
+
installRealpath(deps.fs, deps.path);
|
|
520
|
+
installSpawn(deps);
|
|
521
|
+
installSync(deps);
|
|
522
|
+
// ESM `import { spawn } from 'node:child_process'` reads a snapshot of the
|
|
523
|
+
// CommonJS exports; refresh it so both module systems see the same functions.
|
|
524
|
+
require('module').syncBuiltinESMExports();
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
module.exports = { install, lenientRealpath, normaliseStdio, IPC_REFUSAL, LOCAL_PIPE_PREFIX };
|
|
529
|
+
|
|
530
|
+
if (process.env.LOOPLY_CONTAINED_NODE === '1') install();
|