openship 0.6.9 → 0.7.0
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/bare-3POKLHTW.js +29 -0
- package/dist/{chunk-UAWY6X7V.js → chunk-3KUW2UC6.js} +470 -59
- package/dist/{chunk-GNOMEMNI.js → chunk-3LWGDMJU.js} +1 -1
- package/dist/{chunk-ENKJ7XIF.js → chunk-5CFUYMPE.js} +11 -11
- package/dist/{chunk-NUT6HKK6.js → chunk-5D27WZBW.js} +5 -5
- package/dist/{chunk-NIYD7MD2.js → chunk-7MKZP4TW.js} +89 -45
- package/dist/{chunk-IAMOUDHY.js → chunk-AUBB5DLY.js} +24 -15
- package/dist/{chunk-7PEXM2UI.js → chunk-BOD2IQ3P.js} +6 -6
- package/dist/{chunk-D6HU4VUZ.js → chunk-CMGRMPRQ.js} +1 -1
- package/dist/{chunk-MUWVC5R4.js → chunk-DZBXOF5Q.js} +11 -8
- package/dist/{chunk-BO6CY4V2.js → chunk-GEHV7JBS.js} +47 -4
- package/dist/{chunk-R5DC5PEF.js → chunk-GYDROM4W.js} +39 -10
- package/dist/{chunk-TAZZZKYO.js → chunk-HBQWOJRP.js} +1 -1
- package/dist/{chunk-WEZFTL4V.js → chunk-IVRTIKNZ.js} +183 -0
- package/dist/{chunk-P7RCYELG.js → chunk-J2NDOB7D.js} +1 -1
- package/dist/{chunk-T6DVUX4F.js → chunk-J4SYG2LC.js} +14 -3
- package/dist/{chunk-VAI5VOOI.js → chunk-JAKVPLHT.js} +1 -1
- package/dist/{chunk-NPSRGIHV.js → chunk-JWW5YAFL.js} +5 -4
- package/dist/{chunk-FCGJN5Q5.js → chunk-KYTTPWRP.js} +9 -9
- package/dist/{chunk-7YBWXEM6.js → chunk-LL7M7GL4.js} +4 -4
- package/dist/{chunk-SK4FFCFR.js → chunk-MZYD3LFP.js} +1 -1
- package/dist/{chunk-6GU6JSK6.js → chunk-QNQFS37A.js} +3 -3
- package/dist/{chunk-T46BQOLY.js → chunk-RHHCAWW3.js} +3 -3
- package/dist/{chunk-5UXHT6ED.js → chunk-SPR7N2EK.js} +113 -20
- package/dist/{chunk-AHJYP7DG.js → chunk-SYQ3FOLE.js} +74 -17
- package/dist/{chunk-3MP6ACKM.js → chunk-TPGBT5UR.js} +185 -78
- package/dist/{chunk-634DDFT5.js → chunk-TT7P5ZZ3.js} +8 -8
- package/dist/{chunk-D3OEMXYP.js → chunk-XQC6KYZD.js} +12 -7
- package/dist/{chunk-X53JXRS4.js → chunk-YGA52DHX.js} +3 -3
- package/dist/{chunk-5DJOB7IX.js → chunk-ZFA3MZ5N.js} +3 -3
- package/dist/{cloud-REV3DPJH.js → cloud-RFZSO6P5.js} +19 -19
- package/dist/{detect-BBFT5GEU.js → detect-NXD3PYBM.js} +4 -4
- package/dist/{docker-EG3QSE6I.js → docker-U5APUZBI.js} +10 -10
- package/dist/{docker-edge-executor-E5TOTLMR.js → docker-edge-executor-HUQOWFQU.js} +7 -7
- package/dist/{edge-import-YJVJUGYC.js → edge-import-UXD52WQR.js} +26 -26
- package/dist/{edge-preflight-QUW4BM7N.js → edge-preflight-3PMJ5BWM.js} +13 -13
- package/dist/{ensure-container-edge-FOEFQ3PI.js → ensure-container-edge-GDPY7UJD.js} +11 -11
- package/dist/{executor-NTWDJ3DB.js → executor-XOVSO6QZ.js} +6 -6
- package/dist/{host-channel-preflight-OI2TRHSG.js → host-channel-preflight-T2QUDQXU.js} +26 -26
- package/dist/index.js +71 -49
- package/dist/{local-executor-OQ2GRN7K.js → local-executor-KR7BFCJP.js} +3 -3
- package/dist/{nginx-4U4C2XUS.js → nginx-JMLAS7YS.js} +9 -9
- package/dist/{openresty-lua-LLWT5XQW.js → openresty-lua-FJH3KEC4.js} +7 -7
- package/dist/{privilege-MNDVMDST.js → privilege-H2QTJ5TE.js} +5 -5
- package/dist/server/index.js +9627 -4439
- package/dist/server/lua/site_logger.lua +18 -1
- package/dist/server/migrations/0117_github_installation_workspace_scope.sql +24 -0
- package/dist/server/migrations/0118_git_sources.sql +66 -0
- package/dist/server/migrations/0119_backup_run_batch.sql +8 -0
- package/dist/server/migrations/0120_domain_primary_unique.sql +24 -0
- package/dist/server/migrations/0121_host_port_claim_overlap.sql +14 -0
- package/dist/server/migrations/meta/_journal.json +35 -0
- package/dist/{setup-IFIMPQZJ.js → setup-ERGE5XNW.js} +12 -12
- package/dist/{ssh-client-2XWZCMGI.js → ssh-client-WVLLOAYK.js} +2 -2
- package/dist/up-EFCE6KA6.js +61 -0
- package/package.json +1 -1
- package/dist/bare-FA66N7GC.js +0 -29
- package/dist/up-RC5ZQIAD.js +0 -61
|
@@ -8,24 +8,24 @@ import {
|
|
|
8
8
|
import {
|
|
9
9
|
getTarCreateEnv,
|
|
10
10
|
prepareSourceTarArgs
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-JAKVPLHT.js";
|
|
12
12
|
import {
|
|
13
13
|
connectSshClient,
|
|
14
14
|
openDockerDialStdioChannel,
|
|
15
15
|
openSftp,
|
|
16
16
|
openSshUnixSocket,
|
|
17
17
|
reconcileKnownHosts
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-HBQWOJRP.js";
|
|
19
19
|
import {
|
|
20
20
|
LocalExecutor
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-J4SYG2LC.js";
|
|
22
22
|
import {
|
|
23
23
|
emitBufferedLines,
|
|
24
24
|
flushBufferedLines,
|
|
25
25
|
hasLocalCommand,
|
|
26
26
|
logEntry,
|
|
27
27
|
sq
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-J2NDOB7D.js";
|
|
29
29
|
import {
|
|
30
30
|
HostChannelUnavailableError,
|
|
31
31
|
SshDisconnectedError,
|
|
@@ -42,13 +42,14 @@ import {
|
|
|
42
42
|
hostChannelAccount,
|
|
43
43
|
hostFirewallRule,
|
|
44
44
|
safeErrorMessage
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-IVRTIKNZ.js";
|
|
46
46
|
|
|
47
47
|
// ../../packages/adapters/src/system/executor.ts
|
|
48
48
|
import { existsSync, readFileSync } from "fs";
|
|
49
49
|
import { networkInterfaces } from "os";
|
|
50
50
|
|
|
51
51
|
// ../../packages/adapters/src/system/ssh-executor.ts
|
|
52
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
52
53
|
import { createReadStream } from "fs";
|
|
53
54
|
import { mkdtemp as mkdtemp2, rm as fsRm2, stat } from "fs/promises";
|
|
54
55
|
import { tmpdir as tmpdir2 } from "os";
|
|
@@ -250,6 +251,16 @@ async function uploadFileWithRsync(localFile, remoteFile, deps, onLog, opts) {
|
|
|
250
251
|
|
|
251
252
|
// ../../packages/adapters/src/system/ssh-executor.ts
|
|
252
253
|
var remoteDirname = posix.dirname;
|
|
254
|
+
var SFTP_OPERATION_TIMEOUT_MS = 3e4;
|
|
255
|
+
var CHANNEL_CLOSE_GRACE_MS = 750;
|
|
256
|
+
var TRANSPORT_CLOSE_GRACE_MS = 250;
|
|
257
|
+
function abortError(operation, signal) {
|
|
258
|
+
const reason = signal.reason;
|
|
259
|
+
const suffix = reason instanceof Error && reason.message ? `: ${reason.message}` : "";
|
|
260
|
+
const error = new Error(`SSH ${operation} cancelled${suffix}`);
|
|
261
|
+
error.name = "AbortError";
|
|
262
|
+
return error;
|
|
263
|
+
}
|
|
253
264
|
function clampWindow(value, fallback, min, max) {
|
|
254
265
|
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
255
266
|
return Math.max(min, Math.min(max, Math.floor(value)));
|
|
@@ -259,10 +270,19 @@ var SshExecutor = class _SshExecutor {
|
|
|
259
270
|
connecting = null;
|
|
260
271
|
/** One shared SFTP subsystem channel per client — see sftp(). */
|
|
261
272
|
sftpChannel = null;
|
|
273
|
+
sftpWrapper = null;
|
|
274
|
+
/** All requests cancelled on one shared channel join the same close/reset
|
|
275
|
+
* barrier, so the second listener cannot miss the first listener's `close`
|
|
276
|
+
* event and needlessly wait for the force-reset deadline. */
|
|
277
|
+
sftpQuiescence = /* @__PURE__ */ new WeakMap();
|
|
262
278
|
config;
|
|
279
|
+
/** Deployment cancellation belongs to an async call tree, not to this pooled
|
|
280
|
+
* executor instance. AsyncLocalStorage prevents one deployment from making
|
|
281
|
+
* unrelated users of the same SSH connection inherit its signal. */
|
|
282
|
+
abortScope = new AsyncLocalStorage();
|
|
263
283
|
/** Subscribers notified when the transport drops (see onDisconnect). */
|
|
264
284
|
disconnectListeners = /* @__PURE__ */ new Set();
|
|
265
|
-
/** In-flight cancelable ops — each entry aborts ONE exec/stream on a drop,
|
|
285
|
+
/** In-flight cancelable ops — each entry aborts ONE exec/stream/SFTP request on a drop,
|
|
266
286
|
* so a dead channel fails fast instead of hanging to its command timeout. */
|
|
267
287
|
inflight = /* @__PURE__ */ new Set();
|
|
268
288
|
/** Reverse-forward handlers keyed by the remote bound port (see reverseForward). */
|
|
@@ -275,15 +295,66 @@ var SshExecutor = class _SshExecutor {
|
|
|
275
295
|
}
|
|
276
296
|
this.config = config;
|
|
277
297
|
}
|
|
298
|
+
runWithAbortSignal(signal, fn) {
|
|
299
|
+
if (signal.aborted) return Promise.reject(abortError("operation", signal));
|
|
300
|
+
return this.abortScope.run(signal, async () => {
|
|
301
|
+
this.throwIfAborted("operation");
|
|
302
|
+
return fn();
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
operationSignal() {
|
|
306
|
+
return this.abortScope.getStore();
|
|
307
|
+
}
|
|
308
|
+
throwIfAborted(operation, signal = this.operationSignal()) {
|
|
309
|
+
if (signal?.aborted) throw abortError(operation, signal);
|
|
310
|
+
}
|
|
311
|
+
resolvedSignal(explicit) {
|
|
312
|
+
const ambient = this.operationSignal();
|
|
313
|
+
if (!explicit || explicit === ambient) return ambient ?? explicit;
|
|
314
|
+
if (!ambient) return explicit;
|
|
315
|
+
return AbortSignal.any([explicit, ambient]);
|
|
316
|
+
}
|
|
317
|
+
/** A handshake is already bounded by ssh2's readyTimeout. A deployment
|
|
318
|
+
* cancellation may stop waiting sooner because no remote mutation can have
|
|
319
|
+
* started before connect() resolves; the shared attempt is left alive for an
|
|
320
|
+
* unrelated/following caller instead of tearing down pooled state. */
|
|
321
|
+
awaitConnection(attempt) {
|
|
322
|
+
const signal = this.operationSignal();
|
|
323
|
+
if (!signal) return attempt;
|
|
324
|
+
this.throwIfAborted("connection", signal);
|
|
325
|
+
return new Promise((resolve, reject) => {
|
|
326
|
+
let settled = false;
|
|
327
|
+
const finish = (act) => {
|
|
328
|
+
if (settled) return;
|
|
329
|
+
settled = true;
|
|
330
|
+
signal.removeEventListener("abort", onAbort);
|
|
331
|
+
act();
|
|
332
|
+
};
|
|
333
|
+
const onAbort = () => finish(() => reject(abortError("connection", signal)));
|
|
334
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
335
|
+
if (signal.aborted) {
|
|
336
|
+
onAbort();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
attempt.then(
|
|
340
|
+
(client) => finish(() => resolve(client)),
|
|
341
|
+
(error) => finish(() => reject(error))
|
|
342
|
+
);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
278
345
|
async connect() {
|
|
279
|
-
if (this.client)
|
|
280
|
-
|
|
346
|
+
if (this.client) {
|
|
347
|
+
this.throwIfAborted("connection");
|
|
348
|
+
return this.client;
|
|
349
|
+
}
|
|
350
|
+
if (this.connecting) return this.awaitConnection(this.connecting);
|
|
281
351
|
const attempt = (async () => {
|
|
282
352
|
const client = await connectSshClient(this.config);
|
|
283
353
|
const onTransportDown = (cause) => {
|
|
284
354
|
if (this.client !== client) return;
|
|
285
355
|
this.client = null;
|
|
286
356
|
this.sftpChannel = null;
|
|
357
|
+
this.sftpWrapper = null;
|
|
287
358
|
this.handleDisconnect(cause);
|
|
288
359
|
};
|
|
289
360
|
client.on("close", () => onTransportDown());
|
|
@@ -297,7 +368,7 @@ var SshExecutor = class _SshExecutor {
|
|
|
297
368
|
if (this.connecting === attempt) this.connecting = null;
|
|
298
369
|
};
|
|
299
370
|
attempt.then(clearMemo, clearMemo);
|
|
300
|
-
return attempt;
|
|
371
|
+
return this.awaitConnection(attempt);
|
|
301
372
|
}
|
|
302
373
|
/**
|
|
303
374
|
* Subscribe to transport-level disconnects. Returns an unsubscribe fn.
|
|
@@ -318,6 +389,15 @@ var SshExecutor = class _SshExecutor {
|
|
|
318
389
|
const err = new SshDisconnectedError(
|
|
319
390
|
cause?.message ? `SSH connection lost: ${cause.message}` : "SSH connection lost"
|
|
320
391
|
);
|
|
392
|
+
this.rejectInflight(err);
|
|
393
|
+
for (const cb of [...this.disconnectListeners]) {
|
|
394
|
+
try {
|
|
395
|
+
cb(err);
|
|
396
|
+
} catch {
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
rejectInflight(err) {
|
|
321
401
|
const aborts = [...this.inflight];
|
|
322
402
|
this.inflight.clear();
|
|
323
403
|
for (const abort of aborts) {
|
|
@@ -326,12 +406,136 @@ var SshExecutor = class _SshExecutor {
|
|
|
326
406
|
} catch {
|
|
327
407
|
}
|
|
328
408
|
}
|
|
329
|
-
|
|
409
|
+
}
|
|
410
|
+
async waitForChannelClose(channel, close) {
|
|
411
|
+
return new Promise((resolve) => {
|
|
412
|
+
let settled = false;
|
|
413
|
+
const finish = (closed) => {
|
|
414
|
+
if (settled) return;
|
|
415
|
+
settled = true;
|
|
416
|
+
clearTimeout(timer);
|
|
417
|
+
channel.removeListener("close", onClose);
|
|
418
|
+
channel.removeListener("error", onError);
|
|
419
|
+
resolve(closed);
|
|
420
|
+
};
|
|
421
|
+
const onClose = () => finish(true);
|
|
422
|
+
const onError = () => {
|
|
423
|
+
};
|
|
424
|
+
const timer = setTimeout(() => finish(false), CHANNEL_CLOSE_GRACE_MS);
|
|
425
|
+
timer.unref?.();
|
|
426
|
+
channel.once("close", onClose);
|
|
427
|
+
channel.once("error", onError);
|
|
330
428
|
try {
|
|
331
|
-
|
|
429
|
+
close();
|
|
332
430
|
} catch {
|
|
431
|
+
finish(false);
|
|
333
432
|
}
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
/** Hard transport reset used only when a channel cannot prove it closed.
|
|
436
|
+
* Every operation on that transport is rejected before we return, and a
|
|
437
|
+
* later operation must establish a fresh connection. */
|
|
438
|
+
async forceResetTransport(client, cause) {
|
|
439
|
+
if (this.client !== client) return;
|
|
440
|
+
this.client = null;
|
|
441
|
+
this.connecting = null;
|
|
442
|
+
this.sftpChannel = null;
|
|
443
|
+
this.sftpWrapper = null;
|
|
444
|
+
if (this.reverseListenerClient === client) this.reverseListenerClient = null;
|
|
445
|
+
this.rejectInflight(
|
|
446
|
+
new SshDisconnectedError(`SSH connection reset to quiesce an operation: ${cause.message}`)
|
|
447
|
+
);
|
|
448
|
+
let closed = false;
|
|
449
|
+
const closePromise = new Promise((resolve) => {
|
|
450
|
+
const done = () => {
|
|
451
|
+
if (closed) return;
|
|
452
|
+
closed = true;
|
|
453
|
+
resolve();
|
|
454
|
+
};
|
|
455
|
+
client.once("close", done);
|
|
456
|
+
client.once("end", done);
|
|
457
|
+
});
|
|
458
|
+
try {
|
|
459
|
+
client.end();
|
|
460
|
+
} catch {
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
client.destroy();
|
|
464
|
+
} catch {
|
|
465
|
+
}
|
|
466
|
+
await Promise.race([
|
|
467
|
+
closePromise,
|
|
468
|
+
new Promise((resolve) => {
|
|
469
|
+
const timer = setTimeout(resolve, TRANSPORT_CLOSE_GRACE_MS);
|
|
470
|
+
timer.unref?.();
|
|
471
|
+
})
|
|
472
|
+
]);
|
|
473
|
+
}
|
|
474
|
+
quiesceSftp(client, wrapper, cause) {
|
|
475
|
+
if (wrapper) {
|
|
476
|
+
const existing = this.sftpQuiescence.get(wrapper);
|
|
477
|
+
if (existing) return existing;
|
|
478
|
+
}
|
|
479
|
+
const quiescence = this.quiesceSftpOnce(client, wrapper, cause);
|
|
480
|
+
if (wrapper) this.sftpQuiescence.set(wrapper, quiescence);
|
|
481
|
+
return quiescence;
|
|
482
|
+
}
|
|
483
|
+
async quiesceSftpOnce(client, wrapper, cause) {
|
|
484
|
+
if (!wrapper || this.sftpWrapper === wrapper) {
|
|
485
|
+
this.sftpChannel = null;
|
|
486
|
+
this.sftpWrapper = null;
|
|
487
|
+
}
|
|
488
|
+
if (wrapper) {
|
|
489
|
+
const closed = await this.waitForChannelClose(wrapper, () => wrapper.end());
|
|
490
|
+
if (closed) return;
|
|
334
491
|
}
|
|
492
|
+
await this.forceResetTransport(client, cause);
|
|
493
|
+
}
|
|
494
|
+
awaitSftpOpen(opening, client) {
|
|
495
|
+
const signal = this.operationSignal();
|
|
496
|
+
this.throwIfAborted("SFTP channel open", signal);
|
|
497
|
+
return new Promise((resolve, reject) => {
|
|
498
|
+
let settled = false;
|
|
499
|
+
let terminating = false;
|
|
500
|
+
const cleanup = () => {
|
|
501
|
+
clearTimeout(timer);
|
|
502
|
+
this.inflight.delete(abort);
|
|
503
|
+
signal?.removeEventListener("abort", onSignalAbort);
|
|
504
|
+
};
|
|
505
|
+
const finish = (act) => {
|
|
506
|
+
if (settled || terminating) return;
|
|
507
|
+
settled = true;
|
|
508
|
+
cleanup();
|
|
509
|
+
act();
|
|
510
|
+
};
|
|
511
|
+
const terminate = (error) => {
|
|
512
|
+
if (settled || terminating) return;
|
|
513
|
+
terminating = true;
|
|
514
|
+
cleanup();
|
|
515
|
+
const settle = () => {
|
|
516
|
+
settled = true;
|
|
517
|
+
reject(error);
|
|
518
|
+
};
|
|
519
|
+
void this.forceResetTransport(client, error).then(settle, settle);
|
|
520
|
+
};
|
|
521
|
+
const abort = (error) => terminate(error);
|
|
522
|
+
const onSignalAbort = () => terminate(abortError("SFTP channel open", signal));
|
|
523
|
+
const timer = setTimeout(
|
|
524
|
+
() => terminate(new Error(`SFTP channel open timed out after ${SFTP_OPERATION_TIMEOUT_MS}ms`)),
|
|
525
|
+
SFTP_OPERATION_TIMEOUT_MS
|
|
526
|
+
);
|
|
527
|
+
timer.unref?.();
|
|
528
|
+
this.inflight.add(abort);
|
|
529
|
+
signal?.addEventListener("abort", onSignalAbort, { once: true });
|
|
530
|
+
if (signal?.aborted) {
|
|
531
|
+
onSignalAbort();
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
opening.then(
|
|
535
|
+
(wrapper) => finish(() => resolve(wrapper)),
|
|
536
|
+
(error) => finish(() => reject(error))
|
|
537
|
+
);
|
|
538
|
+
});
|
|
335
539
|
}
|
|
336
540
|
/**
|
|
337
541
|
* One SFTP subsystem channel per client, opened lazily and shared by every
|
|
@@ -344,26 +548,39 @@ var SshExecutor = class _SshExecutor {
|
|
|
344
548
|
*/
|
|
345
549
|
async sftp() {
|
|
346
550
|
const client = await this.connect();
|
|
347
|
-
|
|
551
|
+
this.throwIfAborted("SFTP channel open");
|
|
552
|
+
if (this.sftpChannel) return this.awaitSftpOpen(this.sftpChannel, client);
|
|
348
553
|
const opening = openSftp(client).then((wrapper) => {
|
|
554
|
+
if (this.client !== client || this.sftpChannel !== opening) {
|
|
555
|
+
try {
|
|
556
|
+
wrapper.end();
|
|
557
|
+
} catch {
|
|
558
|
+
}
|
|
559
|
+
throw new SshDisconnectedError("SFTP channel opened after its SSH transport was reset");
|
|
560
|
+
}
|
|
349
561
|
const drop = () => {
|
|
350
|
-
if (this.sftpChannel === opening)
|
|
562
|
+
if (this.sftpChannel === opening) {
|
|
563
|
+
this.sftpChannel = null;
|
|
564
|
+
this.sftpWrapper = null;
|
|
565
|
+
}
|
|
351
566
|
};
|
|
352
567
|
wrapper.once("close", drop);
|
|
353
568
|
wrapper.once("error", drop);
|
|
569
|
+
this.sftpWrapper = wrapper;
|
|
354
570
|
return wrapper;
|
|
355
571
|
});
|
|
356
572
|
opening.catch(() => {
|
|
357
573
|
if (this.sftpChannel === opening) this.sftpChannel = null;
|
|
358
574
|
});
|
|
359
575
|
this.sftpChannel = opening;
|
|
360
|
-
return opening;
|
|
576
|
+
return this.awaitSftpOpen(opening, client);
|
|
361
577
|
}
|
|
362
578
|
/** Close + forget the shared SFTP channel (frees its session) without
|
|
363
579
|
* touching the SSH client, so file ops can reopen on the same connection. */
|
|
364
580
|
dropSftp() {
|
|
365
581
|
const ch = this.sftpChannel;
|
|
366
582
|
this.sftpChannel = null;
|
|
583
|
+
this.sftpWrapper = null;
|
|
367
584
|
if (ch) ch.then((w) => {
|
|
368
585
|
try {
|
|
369
586
|
w.end();
|
|
@@ -382,6 +599,10 @@ var SshExecutor = class _SshExecutor {
|
|
|
382
599
|
this.client.end();
|
|
383
600
|
} catch {
|
|
384
601
|
}
|
|
602
|
+
try {
|
|
603
|
+
this.client.destroy();
|
|
604
|
+
} catch {
|
|
605
|
+
}
|
|
385
606
|
this.client = null;
|
|
386
607
|
}
|
|
387
608
|
this.connecting = null;
|
|
@@ -413,10 +634,11 @@ var SshExecutor = class _SshExecutor {
|
|
|
413
634
|
* write fails spuriously and only succeeds on a manual redeploy.
|
|
414
635
|
*/
|
|
415
636
|
async withChannelRetry(fn) {
|
|
637
|
+
this.throwIfAborted("operation");
|
|
416
638
|
try {
|
|
417
639
|
return await fn();
|
|
418
640
|
} catch (err) {
|
|
419
|
-
if (_SshExecutor.isChannelError(err)) {
|
|
641
|
+
if (_SshExecutor.isChannelError(err) && !this.operationSignal()?.aborted) {
|
|
420
642
|
this.recoverFromChannelError();
|
|
421
643
|
return fn();
|
|
422
644
|
}
|
|
@@ -431,26 +653,65 @@ var SshExecutor = class _SshExecutor {
|
|
|
431
653
|
async _exec(command, opts) {
|
|
432
654
|
const client = await this.connect();
|
|
433
655
|
const timeout = opts?.timeout ?? 3e4;
|
|
656
|
+
const signal = this.operationSignal();
|
|
657
|
+
this.throwIfAborted("command", signal);
|
|
434
658
|
return new Promise((resolve, reject) => {
|
|
435
659
|
let settled = false;
|
|
660
|
+
let terminating = false;
|
|
436
661
|
let timer = null;
|
|
437
|
-
|
|
438
|
-
const
|
|
439
|
-
if (settled) return;
|
|
440
|
-
settled = true;
|
|
662
|
+
let channel = null;
|
|
663
|
+
const cleanup = () => {
|
|
441
664
|
if (timer) clearTimeout(timer);
|
|
442
665
|
this.inflight.delete(abort);
|
|
666
|
+
signal?.removeEventListener("abort", onSignalAbort);
|
|
667
|
+
};
|
|
668
|
+
const finish = (act) => {
|
|
669
|
+
if (settled || terminating) return;
|
|
670
|
+
settled = true;
|
|
671
|
+
cleanup();
|
|
443
672
|
act();
|
|
444
673
|
};
|
|
674
|
+
const terminate = (error) => {
|
|
675
|
+
if (settled || terminating) return;
|
|
676
|
+
terminating = true;
|
|
677
|
+
cleanup();
|
|
678
|
+
const quiesce = (async () => {
|
|
679
|
+
if (channel) {
|
|
680
|
+
const closed = await this.waitForChannelClose(channel, () => channel.close());
|
|
681
|
+
if (!closed) await this.forceResetTransport(client, error);
|
|
682
|
+
} else {
|
|
683
|
+
await this.forceResetTransport(client, error);
|
|
684
|
+
}
|
|
685
|
+
})();
|
|
686
|
+
const settle = () => {
|
|
687
|
+
settled = true;
|
|
688
|
+
reject(error);
|
|
689
|
+
};
|
|
690
|
+
void quiesce.then(settle, settle);
|
|
691
|
+
};
|
|
692
|
+
const abort = (err) => terminate(err);
|
|
693
|
+
const onSignalAbort = () => terminate(abortError("command", signal));
|
|
445
694
|
this.inflight.add(abort);
|
|
695
|
+
signal?.addEventListener("abort", onSignalAbort, { once: true });
|
|
696
|
+
if (signal?.aborted) {
|
|
697
|
+
onSignalAbort();
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
446
700
|
timer = setTimeout(
|
|
447
|
-
() =>
|
|
448
|
-
() => reject(new Error(`Command timed out after ${timeout}ms: ${commandForError(command)}`))
|
|
449
|
-
),
|
|
701
|
+
() => terminate(new Error(`Command timed out after ${timeout}ms: ${commandForError(command)}`)),
|
|
450
702
|
timeout
|
|
451
703
|
);
|
|
704
|
+
timer.unref?.();
|
|
452
705
|
client.exec(_SshExecutor.ENV_PREFIX + command, (err, stream) => {
|
|
453
706
|
if (err) return finish(() => reject(err));
|
|
707
|
+
channel = stream;
|
|
708
|
+
if (terminating) {
|
|
709
|
+
try {
|
|
710
|
+
stream.close();
|
|
711
|
+
} catch {
|
|
712
|
+
}
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
454
715
|
let stdout = "";
|
|
455
716
|
let stderr = "";
|
|
456
717
|
stream.on("data", (data) => {
|
|
@@ -459,6 +720,7 @@ var SshExecutor = class _SshExecutor {
|
|
|
459
720
|
stream.stderr.on("data", (data) => {
|
|
460
721
|
stderr += data.toString();
|
|
461
722
|
});
|
|
723
|
+
stream.on("error", (error) => finish(() => reject(error)));
|
|
462
724
|
stream.on("close", (code) => {
|
|
463
725
|
finish(() => {
|
|
464
726
|
if (code !== 0) {
|
|
@@ -471,10 +733,11 @@ var SshExecutor = class _SshExecutor {
|
|
|
471
733
|
});
|
|
472
734
|
}
|
|
473
735
|
streamExec(command, onLog, opts) {
|
|
474
|
-
|
|
475
|
-
|
|
736
|
+
const scopedOpts = { ...opts, signal: this.resolvedSignal(opts?.signal) };
|
|
737
|
+
return this._streamExec(command, onLog, scopedOpts).catch((err) => {
|
|
738
|
+
if (_SshExecutor.isChannelError(err) && !scopedOpts.signal?.aborted) {
|
|
476
739
|
this.recoverFromChannelError();
|
|
477
|
-
return this._streamExec(command, onLog,
|
|
740
|
+
return this._streamExec(command, onLog, scopedOpts);
|
|
478
741
|
}
|
|
479
742
|
throw err;
|
|
480
743
|
});
|
|
@@ -484,33 +747,56 @@ var SshExecutor = class _SshExecutor {
|
|
|
484
747
|
const signal = opts?.signal;
|
|
485
748
|
return new Promise((resolve, reject) => {
|
|
486
749
|
let settled = false;
|
|
750
|
+
let terminating = false;
|
|
487
751
|
const chunks = [];
|
|
488
|
-
|
|
489
|
-
const
|
|
490
|
-
if (settled) return;
|
|
491
|
-
settled = true;
|
|
752
|
+
let channel = null;
|
|
753
|
+
const cleanup = () => {
|
|
492
754
|
this.inflight.delete(abort);
|
|
493
755
|
signal?.removeEventListener("abort", onSignalAbort);
|
|
756
|
+
};
|
|
757
|
+
const finish = (act) => {
|
|
758
|
+
if (settled || terminating) return;
|
|
759
|
+
settled = true;
|
|
760
|
+
cleanup();
|
|
494
761
|
act();
|
|
495
762
|
};
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
channel
|
|
502
|
-
|
|
503
|
-
|
|
763
|
+
const terminate = (error, intentional) => {
|
|
764
|
+
if (settled || terminating) return;
|
|
765
|
+
terminating = true;
|
|
766
|
+
cleanup();
|
|
767
|
+
const quiesce = (async () => {
|
|
768
|
+
if (channel) {
|
|
769
|
+
const closed = await this.waitForChannelClose(channel, () => channel.close());
|
|
770
|
+
if (!closed) await this.forceResetTransport(client, error);
|
|
771
|
+
} else {
|
|
772
|
+
await this.forceResetTransport(client, error);
|
|
773
|
+
}
|
|
774
|
+
})();
|
|
775
|
+
const settle = () => {
|
|
776
|
+
settled = true;
|
|
777
|
+
if (intentional) resolve({ code: 0, output: chunks.join("") });
|
|
778
|
+
else reject(error);
|
|
779
|
+
};
|
|
780
|
+
void quiesce.then(settle, settle);
|
|
504
781
|
};
|
|
782
|
+
const abort = (err) => terminate(err, false);
|
|
783
|
+
const onSignalAbort = () => terminate(abortError("streaming command", signal), true);
|
|
784
|
+
this.inflight.add(abort);
|
|
785
|
+
signal?.addEventListener("abort", onSignalAbort, { once: true });
|
|
505
786
|
if (signal?.aborted) {
|
|
506
|
-
|
|
787
|
+
onSignalAbort();
|
|
507
788
|
return;
|
|
508
789
|
}
|
|
509
790
|
client.exec(_SshExecutor.ENV_PREFIX + command, (err, stream) => {
|
|
510
791
|
if (err) return finish(() => reject(err));
|
|
511
792
|
channel = stream;
|
|
512
|
-
if (signal?.aborted)
|
|
513
|
-
|
|
793
|
+
if (terminating || signal?.aborted) {
|
|
794
|
+
try {
|
|
795
|
+
stream.close();
|
|
796
|
+
} catch {
|
|
797
|
+
}
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
514
800
|
const onChunk = (data, level) => {
|
|
515
801
|
const text = data.toString();
|
|
516
802
|
if (!text) return;
|
|
@@ -519,6 +805,7 @@ var SshExecutor = class _SshExecutor {
|
|
|
519
805
|
};
|
|
520
806
|
stream.on("data", (data) => onChunk(data, "info"));
|
|
521
807
|
stream.stderr.on("data", (data) => onChunk(data, "warn"));
|
|
808
|
+
stream.on("error", (error) => finish(() => reject(error)));
|
|
522
809
|
let exitCode = null;
|
|
523
810
|
stream.on("exit", (code) => {
|
|
524
811
|
exitCode = code;
|
|
@@ -540,7 +827,62 @@ var SshExecutor = class _SshExecutor {
|
|
|
540
827
|
});
|
|
541
828
|
});
|
|
542
829
|
}
|
|
543
|
-
|
|
830
|
+
/** Turn ssh2's callback-only SFTP request into a bounded operation. A lost
|
|
831
|
+
* callback used to leave the compose deploy promise and its host mutex tail
|
|
832
|
+
* pending forever. Cancellation/timeout closes the shared subsystem first;
|
|
833
|
+
* if close itself wedges, the parent SSH transport is destroyed before this
|
|
834
|
+
* promise rejects. */
|
|
835
|
+
runSftpRequest(client, sftp, operation, start) {
|
|
836
|
+
const signal = this.operationSignal();
|
|
837
|
+
this.throwIfAborted(operation, signal);
|
|
838
|
+
return new Promise((resolve, reject) => {
|
|
839
|
+
let settled = false;
|
|
840
|
+
let terminating = false;
|
|
841
|
+
const cleanup = () => {
|
|
842
|
+
clearTimeout(timer);
|
|
843
|
+
this.inflight.delete(abort);
|
|
844
|
+
signal?.removeEventListener("abort", onSignalAbort);
|
|
845
|
+
};
|
|
846
|
+
const finish = (act) => {
|
|
847
|
+
if (settled || terminating) return;
|
|
848
|
+
settled = true;
|
|
849
|
+
cleanup();
|
|
850
|
+
act();
|
|
851
|
+
};
|
|
852
|
+
const terminate = (error) => {
|
|
853
|
+
if (settled || terminating) return;
|
|
854
|
+
terminating = true;
|
|
855
|
+
cleanup();
|
|
856
|
+
const settle = () => {
|
|
857
|
+
settled = true;
|
|
858
|
+
reject(error);
|
|
859
|
+
};
|
|
860
|
+
void this.quiesceSftp(client, sftp, error).then(settle, settle);
|
|
861
|
+
};
|
|
862
|
+
const abort = (error) => terminate(error);
|
|
863
|
+
const onSignalAbort = () => terminate(abortError(operation, signal));
|
|
864
|
+
const timer = setTimeout(
|
|
865
|
+
() => terminate(new Error(`${operation} timed out after ${SFTP_OPERATION_TIMEOUT_MS}ms`)),
|
|
866
|
+
SFTP_OPERATION_TIMEOUT_MS
|
|
867
|
+
);
|
|
868
|
+
timer.unref?.();
|
|
869
|
+
this.inflight.add(abort);
|
|
870
|
+
signal?.addEventListener("abort", onSignalAbort, { once: true });
|
|
871
|
+
if (signal?.aborted) {
|
|
872
|
+
onSignalAbort();
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
try {
|
|
876
|
+
start((error, value) => {
|
|
877
|
+
if (error) finish(() => reject(error));
|
|
878
|
+
else finish(() => resolve(value));
|
|
879
|
+
});
|
|
880
|
+
} catch (error) {
|
|
881
|
+
finish(() => reject(error));
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
async writeFile(path, content, opts) {
|
|
544
886
|
const dir = remoteDirname(path);
|
|
545
887
|
try {
|
|
546
888
|
await this.exec(`mkdir -p ${sq(dir)}`);
|
|
@@ -548,12 +890,21 @@ var SshExecutor = class _SshExecutor {
|
|
|
548
890
|
}
|
|
549
891
|
return this.withChannelRetry(async () => {
|
|
550
892
|
const sftp = await this.sftp();
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
});
|
|
893
|
+
const client = this.client;
|
|
894
|
+
if (!client) throw new SshDisconnectedError("SSH connection lost before SFTP write");
|
|
895
|
+
const write = (data, mode) => this.runSftpRequest(client, sftp, `SFTP write ${path}`, (done) => {
|
|
896
|
+
sftp.writeFile(path, data, { encoding: "utf-8", mode }, (err) => done(err));
|
|
556
897
|
});
|
|
898
|
+
const chmod2 = (mode) => this.runSftpRequest(client, sftp, `SFTP chmod ${path}`, (done) => {
|
|
899
|
+
sftp.chmod(path, mode, (err) => done(err));
|
|
900
|
+
});
|
|
901
|
+
if (opts?.mode !== void 0) {
|
|
902
|
+
await write("", opts.mode);
|
|
903
|
+
await chmod2(opts.mode);
|
|
904
|
+
await write(content);
|
|
905
|
+
} else {
|
|
906
|
+
await write(content);
|
|
907
|
+
}
|
|
557
908
|
});
|
|
558
909
|
}
|
|
559
910
|
async rename(from, to) {
|
|
@@ -562,10 +913,11 @@ var SshExecutor = class _SshExecutor {
|
|
|
562
913
|
async readFile(path) {
|
|
563
914
|
return this.withChannelRetry(async () => {
|
|
564
915
|
const sftp = await this.sftp();
|
|
565
|
-
|
|
916
|
+
const client = this.client;
|
|
917
|
+
if (!client) throw new SshDisconnectedError("SSH connection lost before SFTP read");
|
|
918
|
+
return this.runSftpRequest(client, sftp, `SFTP read ${path}`, (done) => {
|
|
566
919
|
sftp.readFile(path, { encoding: "utf-8" }, (err, data) => {
|
|
567
|
-
|
|
568
|
-
else resolve(data.toString());
|
|
920
|
+
done(err, data?.toString());
|
|
569
921
|
});
|
|
570
922
|
});
|
|
571
923
|
});
|
|
@@ -573,9 +925,11 @@ var SshExecutor = class _SshExecutor {
|
|
|
573
925
|
async exists(path) {
|
|
574
926
|
return this.withChannelRetry(async () => {
|
|
575
927
|
const sftp = await this.sftp();
|
|
576
|
-
|
|
928
|
+
const client = this.client;
|
|
929
|
+
if (!client) throw new SshDisconnectedError("SSH connection lost before SFTP stat");
|
|
930
|
+
return this.runSftpRequest(client, sftp, `SFTP stat ${path}`, (done) => {
|
|
577
931
|
sftp.stat(path, (err) => {
|
|
578
|
-
|
|
932
|
+
done(null, !err);
|
|
579
933
|
});
|
|
580
934
|
});
|
|
581
935
|
});
|
|
@@ -833,6 +1187,7 @@ var SshExecutor = class _SshExecutor {
|
|
|
833
1187
|
async dispose() {
|
|
834
1188
|
this.connecting = null;
|
|
835
1189
|
this.sftpChannel = null;
|
|
1190
|
+
this.sftpWrapper = null;
|
|
836
1191
|
this.reverseHandlers.clear();
|
|
837
1192
|
this.reverseListenerClient = null;
|
|
838
1193
|
if (this.client) {
|
|
@@ -976,6 +1331,7 @@ var SshExecutor = class _SshExecutor {
|
|
|
976
1331
|
};
|
|
977
1332
|
|
|
978
1333
|
// ../../packages/adapters/src/system/system-ssh-executor.ts
|
|
1334
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
979
1335
|
import { execFile as execFile2, spawn as spawn2 } from "child_process";
|
|
980
1336
|
import { once } from "events";
|
|
981
1337
|
import { mkdtemp as mkdtemp3, rm as fsRm3, stat as stat2, unlink } from "fs/promises";
|
|
@@ -1090,6 +1446,13 @@ async function openSystemSshReverseTunnel(opts) {
|
|
|
1090
1446
|
// ../../packages/adapters/src/system/system-ssh-executor.ts
|
|
1091
1447
|
var execFileAsync2 = promisify2(execFile2);
|
|
1092
1448
|
var remoteDirname2 = posix2.dirname;
|
|
1449
|
+
function abortError2(operation, signal) {
|
|
1450
|
+
const reason = signal.reason;
|
|
1451
|
+
const suffix = reason instanceof Error && reason.message ? `: ${reason.message}` : "";
|
|
1452
|
+
const error = new Error(`SSH ${operation} cancelled${suffix}`);
|
|
1453
|
+
error.name = "AbortError";
|
|
1454
|
+
return error;
|
|
1455
|
+
}
|
|
1093
1456
|
function clampWindow2(value, fallback, min, max) {
|
|
1094
1457
|
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
1095
1458
|
return Math.max(min, Math.min(max, Math.floor(value)));
|
|
@@ -1103,6 +1466,7 @@ function describeSshFailure(stderr, fallback) {
|
|
|
1103
1466
|
}
|
|
1104
1467
|
var SystemSshExecutor = class _SystemSshExecutor {
|
|
1105
1468
|
config;
|
|
1469
|
+
abortScope = new AsyncLocalStorage2();
|
|
1106
1470
|
controlPath = makeControlPath();
|
|
1107
1471
|
/** Resolves once the ControlMaster connection is established. */
|
|
1108
1472
|
masterPromise = null;
|
|
@@ -1119,6 +1483,25 @@ var SystemSshExecutor = class _SystemSshExecutor {
|
|
|
1119
1483
|
}
|
|
1120
1484
|
this.config = config;
|
|
1121
1485
|
}
|
|
1486
|
+
runWithAbortSignal(signal, fn) {
|
|
1487
|
+
if (signal.aborted) return Promise.reject(abortError2("operation", signal));
|
|
1488
|
+
return this.abortScope.run(signal, async () => {
|
|
1489
|
+
this.throwIfAborted("operation");
|
|
1490
|
+
return fn();
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
operationSignal() {
|
|
1494
|
+
return this.abortScope.getStore();
|
|
1495
|
+
}
|
|
1496
|
+
resolvedSignal(explicit) {
|
|
1497
|
+
const ambient = this.operationSignal();
|
|
1498
|
+
if (!explicit || explicit === ambient) return ambient ?? explicit;
|
|
1499
|
+
if (!ambient) return explicit;
|
|
1500
|
+
return AbortSignal.any([explicit, ambient]);
|
|
1501
|
+
}
|
|
1502
|
+
throwIfAborted(operation, signal = this.operationSignal()) {
|
|
1503
|
+
if (signal?.aborted) throw abortError2(operation, signal);
|
|
1504
|
+
}
|
|
1122
1505
|
baseArgs() {
|
|
1123
1506
|
return buildBaseSshArgs(this.config, this.controlPath);
|
|
1124
1507
|
}
|
|
@@ -1199,7 +1582,10 @@ var SystemSshExecutor = class _SystemSshExecutor {
|
|
|
1199
1582
|
}
|
|
1200
1583
|
/** Run a remote command, resolving with stdout/stderr/exit code (never rejects on non-zero). */
|
|
1201
1584
|
async runSsh(remoteCommand, opts) {
|
|
1585
|
+
const signal = this.operationSignal();
|
|
1586
|
+
this.throwIfAborted("command", signal);
|
|
1202
1587
|
await this.ensureMaster();
|
|
1588
|
+
this.throwIfAborted("command", signal);
|
|
1203
1589
|
return new Promise((resolve, reject) => {
|
|
1204
1590
|
const child = spawn2("ssh", [...this.baseArgs(), sshTarget(this.config), remoteCommand], {
|
|
1205
1591
|
env: sshChildEnv(this.config),
|
|
@@ -1208,7 +1594,25 @@ var SystemSshExecutor = class _SystemSshExecutor {
|
|
|
1208
1594
|
let stdout = "";
|
|
1209
1595
|
let stderr = "";
|
|
1210
1596
|
let timedOut = false;
|
|
1597
|
+
let settled = false;
|
|
1211
1598
|
let timer = null;
|
|
1599
|
+
const cleanup = () => {
|
|
1600
|
+
if (timer) clearTimeout(timer);
|
|
1601
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1602
|
+
};
|
|
1603
|
+
const finish = (act) => {
|
|
1604
|
+
if (settled) return;
|
|
1605
|
+
settled = true;
|
|
1606
|
+
cleanup();
|
|
1607
|
+
act();
|
|
1608
|
+
};
|
|
1609
|
+
const onAbort = () => {
|
|
1610
|
+
try {
|
|
1611
|
+
child.kill("SIGKILL");
|
|
1612
|
+
} catch {
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1615
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1212
1616
|
if (opts?.timeout) {
|
|
1213
1617
|
timer = setTimeout(() => {
|
|
1214
1618
|
timedOut = true;
|
|
@@ -1223,16 +1627,21 @@ var SystemSshExecutor = class _SystemSshExecutor {
|
|
|
1223
1627
|
stderr += d.toString();
|
|
1224
1628
|
});
|
|
1225
1629
|
child.on("error", (e) => {
|
|
1226
|
-
|
|
1227
|
-
reject(new Error(`ssh failed to start: ${e.message}`));
|
|
1630
|
+
finish(() => reject(new Error(`ssh failed to start: ${e.message}`)));
|
|
1228
1631
|
});
|
|
1229
1632
|
child.on("close", (code) => {
|
|
1230
|
-
if (timer) clearTimeout(timer);
|
|
1231
1633
|
const c = code ?? 1;
|
|
1232
1634
|
void this.maybeSignalDisconnect(c).catch(() => {
|
|
1233
1635
|
});
|
|
1234
|
-
|
|
1636
|
+
finish(() => {
|
|
1637
|
+
if (signal?.aborted) reject(abortError2("command", signal));
|
|
1638
|
+
else resolve({ stdout, stderr, code: c, timedOut });
|
|
1639
|
+
});
|
|
1235
1640
|
});
|
|
1641
|
+
if (signal?.aborted) {
|
|
1642
|
+
onAbort();
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1236
1645
|
if (opts?.input !== void 0) {
|
|
1237
1646
|
child.stdin.write(opts.input);
|
|
1238
1647
|
}
|
|
@@ -1253,7 +1662,7 @@ var SystemSshExecutor = class _SystemSshExecutor {
|
|
|
1253
1662
|
}
|
|
1254
1663
|
async streamExec(command, onLog, opts) {
|
|
1255
1664
|
await this.ensureMaster();
|
|
1256
|
-
const signal = opts?.signal;
|
|
1665
|
+
const signal = this.resolvedSignal(opts?.signal);
|
|
1257
1666
|
if (signal?.aborted) return { code: 0, output: "" };
|
|
1258
1667
|
return new Promise((resolve) => {
|
|
1259
1668
|
const child = spawn2(
|
|
@@ -1290,8 +1699,10 @@ var SystemSshExecutor = class _SystemSshExecutor {
|
|
|
1290
1699
|
});
|
|
1291
1700
|
});
|
|
1292
1701
|
}
|
|
1293
|
-
async writeFile(path, content) {
|
|
1294
|
-
const
|
|
1702
|
+
async writeFile(path, content, opts) {
|
|
1703
|
+
const target = sq(path);
|
|
1704
|
+
const prepare = opts?.mode === void 0 ? `cat > ${target}` : `: > ${target} && chmod ${opts.mode.toString(8)} ${target} && cat > ${target}`;
|
|
1705
|
+
const remoteCommand = `mkdir -p ${sq(remoteDirname2(path))} && ${prepare}`;
|
|
1295
1706
|
const res = await this.runSsh(remoteCommand, { input: content });
|
|
1296
1707
|
if (res.code !== 0) {
|
|
1297
1708
|
throw new Error(res.stderr.trim() || `Failed to write ${path} (exit ${res.code})`);
|
|
@@ -1691,7 +2102,7 @@ async function verifyHostChannelAuth(config) {
|
|
|
1691
2102
|
let hint = null;
|
|
1692
2103
|
let forwarding = "unknown";
|
|
1693
2104
|
try {
|
|
1694
|
-
const { connectSshClient: connectSshClient2 } = await import("./ssh-client-
|
|
2105
|
+
const { connectSshClient: connectSshClient2 } = await import("./ssh-client-WVLLOAYK.js");
|
|
1695
2106
|
const client = await connectSshClient2({
|
|
1696
2107
|
host: config.host,
|
|
1697
2108
|
port: config.port,
|