openship 0.6.9 → 0.7.1
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-AB4SMTNJ.js +29 -0
- package/dist/{chunk-P7RCYELG.js → chunk-2BAKEGGF.js} +1 -1
- package/dist/{chunk-VAI5VOOI.js → chunk-2YAY5XHK.js} +1 -1
- package/dist/{chunk-X53JXRS4.js → chunk-37XZTLBJ.js} +3 -3
- package/dist/{chunk-R5DC5PEF.js → chunk-3K65MMYV.js} +39 -10
- package/dist/{chunk-D3OEMXYP.js → chunk-3WBKLDGC.js} +12 -7
- package/dist/{chunk-ENKJ7XIF.js → chunk-3YSSXSC2.js} +12 -12
- package/dist/{chunk-3MP6ACKM.js → chunk-7MCPDYKP.js} +799 -165
- package/dist/{chunk-D6HU4VUZ.js → chunk-7TQP6723.js} +2 -2
- package/dist/{chunk-SK4FFCFR.js → chunk-AMADTKC6.js} +1 -1
- package/dist/{chunk-NPSRGIHV.js → chunk-B2BQX6JX.js} +6 -5
- package/dist/{chunk-MUWVC5R4.js → chunk-BBPKAQ54.js} +12 -9
- package/dist/{chunk-WEZFTL4V.js → chunk-COI4VFRW.js} +217 -2
- package/dist/{chunk-5DJOB7IX.js → chunk-CQ2JOXEQ.js} +3 -3
- package/dist/{chunk-7PEXM2UI.js → chunk-DQR5TPFB.js} +6 -6
- package/dist/{chunk-IAMOUDHY.js → chunk-ETK6IBCH.js} +24 -15
- package/dist/{chunk-T46BQOLY.js → chunk-FNWNUYS3.js} +3 -3
- package/dist/{chunk-UAWY6X7V.js → chunk-GPTBEPUE.js} +485 -67
- package/dist/{chunk-634DDFT5.js → chunk-H36ASAYI.js} +8 -8
- package/dist/{chunk-AHJYP7DG.js → chunk-IVMPA6RC.js} +74 -17
- package/dist/{chunk-5UXHT6ED.js → chunk-IXBOV7BE.js} +113 -20
- package/dist/{chunk-GNOMEMNI.js → chunk-KATXZ5AB.js} +1 -1
- package/dist/{chunk-T6DVUX4F.js → chunk-KCMEWNP7.js} +14 -3
- package/dist/{chunk-N5YZMO33.js → chunk-L4DRH6ZA.js} +12 -1
- package/dist/{chunk-BO6CY4V2.js → chunk-NF7NQOAP.js} +47 -4
- package/dist/{chunk-FCGJN5Q5.js → chunk-NIOLZENU.js} +9 -9
- package/dist/{chunk-7YBWXEM6.js → chunk-OEYXXSAM.js} +4 -4
- package/dist/{chunk-6GU6JSK6.js → chunk-S5GU345P.js} +3 -3
- package/dist/{chunk-TAZZZKYO.js → chunk-TKK5NPDQ.js} +2 -2
- package/dist/{chunk-NUT6HKK6.js → chunk-WG5PDWZ3.js} +5 -5
- package/dist/{chunk-NIYD7MD2.js → chunk-XJSLJLNY.js} +90 -46
- package/dist/{cloud-REV3DPJH.js → cloud-T2JOFEQL.js} +20 -20
- package/dist/{detect-BBFT5GEU.js → detect-SWLZURYX.js} +5 -5
- package/dist/{docker-EG3QSE6I.js → docker-SQNZRZXN.js} +11 -11
- package/dist/{docker-edge-executor-E5TOTLMR.js → docker-edge-executor-BSSQQS76.js} +8 -8
- package/dist/{edge-import-YJVJUGYC.js → edge-import-LUDX77C2.js} +27 -27
- package/dist/{edge-preflight-QUW4BM7N.js → edge-preflight-JAEFWBRI.js} +14 -14
- package/dist/{ensure-container-edge-FOEFQ3PI.js → ensure-container-edge-U7FTLFVJ.js} +12 -12
- package/dist/{executor-NTWDJ3DB.js → executor-GYRZ7RIU.js} +7 -7
- package/dist/host-channel-preflight-U4IZCRXR.js +54 -0
- package/dist/index.js +72 -50
- package/dist/{local-executor-OQ2GRN7K.js → local-executor-JCR46VAP.js} +3 -3
- package/dist/{nginx-4U4C2XUS.js → nginx-YA6IRPJT.js} +10 -10
- package/dist/{openresty-lua-LLWT5XQW.js → openresty-lua-DUOB64TP.js} +8 -8
- package/dist/{privilege-MNDVMDST.js → privilege-4B2TMVW2.js} +6 -6
- package/dist/server/index.js +13286 -5381
- 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/0122_data_transfer_staging.sql +45 -0
- package/dist/server/migrations/meta/_journal.json +42 -0
- package/dist/setup-T3WN3HRS.js +25 -0
- package/dist/{ssh-client-2XWZCMGI.js → ssh-client-FZDWP5PQ.js} +3 -3
- package/dist/up-U73RUN5R.js +61 -0
- package/package.json +1 -1
- package/dist/bare-FA66N7GC.js +0 -29
- package/dist/host-channel-preflight-OI2TRHSG.js +0 -54
- package/dist/setup-IFIMPQZJ.js +0 -25
- package/dist/up-RC5ZQIAD.js +0 -61
|
@@ -8,30 +8,32 @@ import {
|
|
|
8
8
|
import {
|
|
9
9
|
getTarCreateEnv,
|
|
10
10
|
prepareSourceTarArgs
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-2YAY5XHK.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-TKK5NPDQ.js";
|
|
19
19
|
import {
|
|
20
20
|
LocalExecutor
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-KCMEWNP7.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-2BAKEGGF.js";
|
|
29
29
|
import {
|
|
30
30
|
HostChannelUnavailableError,
|
|
31
31
|
SshDisconnectedError,
|
|
32
|
+
SshExecRequestError,
|
|
32
33
|
commandForError,
|
|
33
|
-
isSshAuthError
|
|
34
|
-
|
|
34
|
+
isSshAuthError,
|
|
35
|
+
isSshExecRequestError
|
|
36
|
+
} from "./chunk-L4DRH6ZA.js";
|
|
35
37
|
import {
|
|
36
38
|
HOST_CHANNEL_AUTH_REJECTED,
|
|
37
39
|
HOST_CHANNEL_NOT_PROVISIONED,
|
|
@@ -42,13 +44,14 @@ import {
|
|
|
42
44
|
hostChannelAccount,
|
|
43
45
|
hostFirewallRule,
|
|
44
46
|
safeErrorMessage
|
|
45
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-COI4VFRW.js";
|
|
46
48
|
|
|
47
49
|
// ../../packages/adapters/src/system/executor.ts
|
|
48
50
|
import { existsSync, readFileSync } from "fs";
|
|
49
51
|
import { networkInterfaces } from "os";
|
|
50
52
|
|
|
51
53
|
// ../../packages/adapters/src/system/ssh-executor.ts
|
|
54
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
52
55
|
import { createReadStream } from "fs";
|
|
53
56
|
import { mkdtemp as mkdtemp2, rm as fsRm2, stat } from "fs/promises";
|
|
54
57
|
import { tmpdir as tmpdir2 } from "os";
|
|
@@ -250,6 +253,19 @@ async function uploadFileWithRsync(localFile, remoteFile, deps, onLog, opts) {
|
|
|
250
253
|
|
|
251
254
|
// ../../packages/adapters/src/system/ssh-executor.ts
|
|
252
255
|
var remoteDirname = posix.dirname;
|
|
256
|
+
var SFTP_OPERATION_TIMEOUT_MS = 3e4;
|
|
257
|
+
var CHANNEL_CLOSE_GRACE_MS = 750;
|
|
258
|
+
var TRANSPORT_CLOSE_GRACE_MS = 250;
|
|
259
|
+
function tagExecRequestError(error) {
|
|
260
|
+
return error.message === "Unable to exec" ? new SshExecRequestError(error) : error;
|
|
261
|
+
}
|
|
262
|
+
function abortError(operation, signal) {
|
|
263
|
+
const reason = signal.reason;
|
|
264
|
+
const suffix = reason instanceof Error && reason.message ? `: ${reason.message}` : "";
|
|
265
|
+
const error = new Error(`SSH ${operation} cancelled${suffix}`);
|
|
266
|
+
error.name = "AbortError";
|
|
267
|
+
return error;
|
|
268
|
+
}
|
|
253
269
|
function clampWindow(value, fallback, min, max) {
|
|
254
270
|
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
255
271
|
return Math.max(min, Math.min(max, Math.floor(value)));
|
|
@@ -259,10 +275,19 @@ var SshExecutor = class _SshExecutor {
|
|
|
259
275
|
connecting = null;
|
|
260
276
|
/** One shared SFTP subsystem channel per client — see sftp(). */
|
|
261
277
|
sftpChannel = null;
|
|
278
|
+
sftpWrapper = null;
|
|
279
|
+
/** All requests cancelled on one shared channel join the same close/reset
|
|
280
|
+
* barrier, so the second listener cannot miss the first listener's `close`
|
|
281
|
+
* event and needlessly wait for the force-reset deadline. */
|
|
282
|
+
sftpQuiescence = /* @__PURE__ */ new WeakMap();
|
|
262
283
|
config;
|
|
284
|
+
/** Deployment cancellation belongs to an async call tree, not to this pooled
|
|
285
|
+
* executor instance. AsyncLocalStorage prevents one deployment from making
|
|
286
|
+
* unrelated users of the same SSH connection inherit its signal. */
|
|
287
|
+
abortScope = new AsyncLocalStorage();
|
|
263
288
|
/** Subscribers notified when the transport drops (see onDisconnect). */
|
|
264
289
|
disconnectListeners = /* @__PURE__ */ new Set();
|
|
265
|
-
/** In-flight cancelable ops — each entry aborts ONE exec/stream on a drop,
|
|
290
|
+
/** In-flight cancelable ops — each entry aborts ONE exec/stream/SFTP request on a drop,
|
|
266
291
|
* so a dead channel fails fast instead of hanging to its command timeout. */
|
|
267
292
|
inflight = /* @__PURE__ */ new Set();
|
|
268
293
|
/** Reverse-forward handlers keyed by the remote bound port (see reverseForward). */
|
|
@@ -275,15 +300,66 @@ var SshExecutor = class _SshExecutor {
|
|
|
275
300
|
}
|
|
276
301
|
this.config = config;
|
|
277
302
|
}
|
|
303
|
+
runWithAbortSignal(signal, fn) {
|
|
304
|
+
if (signal.aborted) return Promise.reject(abortError("operation", signal));
|
|
305
|
+
return this.abortScope.run(signal, async () => {
|
|
306
|
+
this.throwIfAborted("operation");
|
|
307
|
+
return fn();
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
operationSignal() {
|
|
311
|
+
return this.abortScope.getStore();
|
|
312
|
+
}
|
|
313
|
+
throwIfAborted(operation, signal = this.operationSignal()) {
|
|
314
|
+
if (signal?.aborted) throw abortError(operation, signal);
|
|
315
|
+
}
|
|
316
|
+
resolvedSignal(explicit) {
|
|
317
|
+
const ambient = this.operationSignal();
|
|
318
|
+
if (!explicit || explicit === ambient) return ambient ?? explicit;
|
|
319
|
+
if (!ambient) return explicit;
|
|
320
|
+
return AbortSignal.any([explicit, ambient]);
|
|
321
|
+
}
|
|
322
|
+
/** A handshake is already bounded by ssh2's readyTimeout. A deployment
|
|
323
|
+
* cancellation may stop waiting sooner because no remote mutation can have
|
|
324
|
+
* started before connect() resolves; the shared attempt is left alive for an
|
|
325
|
+
* unrelated/following caller instead of tearing down pooled state. */
|
|
326
|
+
awaitConnection(attempt) {
|
|
327
|
+
const signal = this.operationSignal();
|
|
328
|
+
if (!signal) return attempt;
|
|
329
|
+
this.throwIfAborted("connection", signal);
|
|
330
|
+
return new Promise((resolve, reject) => {
|
|
331
|
+
let settled = false;
|
|
332
|
+
const finish = (act) => {
|
|
333
|
+
if (settled) return;
|
|
334
|
+
settled = true;
|
|
335
|
+
signal.removeEventListener("abort", onAbort);
|
|
336
|
+
act();
|
|
337
|
+
};
|
|
338
|
+
const onAbort = () => finish(() => reject(abortError("connection", signal)));
|
|
339
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
340
|
+
if (signal.aborted) {
|
|
341
|
+
onAbort();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
attempt.then(
|
|
345
|
+
(client) => finish(() => resolve(client)),
|
|
346
|
+
(error) => finish(() => reject(error))
|
|
347
|
+
);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
278
350
|
async connect() {
|
|
279
|
-
if (this.client)
|
|
280
|
-
|
|
351
|
+
if (this.client) {
|
|
352
|
+
this.throwIfAborted("connection");
|
|
353
|
+
return this.client;
|
|
354
|
+
}
|
|
355
|
+
if (this.connecting) return this.awaitConnection(this.connecting);
|
|
281
356
|
const attempt = (async () => {
|
|
282
357
|
const client = await connectSshClient(this.config);
|
|
283
358
|
const onTransportDown = (cause) => {
|
|
284
359
|
if (this.client !== client) return;
|
|
285
360
|
this.client = null;
|
|
286
361
|
this.sftpChannel = null;
|
|
362
|
+
this.sftpWrapper = null;
|
|
287
363
|
this.handleDisconnect(cause);
|
|
288
364
|
};
|
|
289
365
|
client.on("close", () => onTransportDown());
|
|
@@ -297,7 +373,7 @@ var SshExecutor = class _SshExecutor {
|
|
|
297
373
|
if (this.connecting === attempt) this.connecting = null;
|
|
298
374
|
};
|
|
299
375
|
attempt.then(clearMemo, clearMemo);
|
|
300
|
-
return attempt;
|
|
376
|
+
return this.awaitConnection(attempt);
|
|
301
377
|
}
|
|
302
378
|
/**
|
|
303
379
|
* Subscribe to transport-level disconnects. Returns an unsubscribe fn.
|
|
@@ -318,6 +394,15 @@ var SshExecutor = class _SshExecutor {
|
|
|
318
394
|
const err = new SshDisconnectedError(
|
|
319
395
|
cause?.message ? `SSH connection lost: ${cause.message}` : "SSH connection lost"
|
|
320
396
|
);
|
|
397
|
+
this.rejectInflight(err);
|
|
398
|
+
for (const cb of [...this.disconnectListeners]) {
|
|
399
|
+
try {
|
|
400
|
+
cb(err);
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
rejectInflight(err) {
|
|
321
406
|
const aborts = [...this.inflight];
|
|
322
407
|
this.inflight.clear();
|
|
323
408
|
for (const abort of aborts) {
|
|
@@ -326,12 +411,136 @@ var SshExecutor = class _SshExecutor {
|
|
|
326
411
|
} catch {
|
|
327
412
|
}
|
|
328
413
|
}
|
|
329
|
-
|
|
414
|
+
}
|
|
415
|
+
async waitForChannelClose(channel, close) {
|
|
416
|
+
return new Promise((resolve) => {
|
|
417
|
+
let settled = false;
|
|
418
|
+
const finish = (closed) => {
|
|
419
|
+
if (settled) return;
|
|
420
|
+
settled = true;
|
|
421
|
+
clearTimeout(timer);
|
|
422
|
+
channel.removeListener("close", onClose);
|
|
423
|
+
channel.removeListener("error", onError);
|
|
424
|
+
resolve(closed);
|
|
425
|
+
};
|
|
426
|
+
const onClose = () => finish(true);
|
|
427
|
+
const onError = () => {
|
|
428
|
+
};
|
|
429
|
+
const timer = setTimeout(() => finish(false), CHANNEL_CLOSE_GRACE_MS);
|
|
430
|
+
timer.unref?.();
|
|
431
|
+
channel.once("close", onClose);
|
|
432
|
+
channel.once("error", onError);
|
|
330
433
|
try {
|
|
331
|
-
|
|
434
|
+
close();
|
|
332
435
|
} catch {
|
|
436
|
+
finish(false);
|
|
333
437
|
}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
/** Hard transport reset used only when a channel cannot prove it closed.
|
|
441
|
+
* Every operation on that transport is rejected before we return, and a
|
|
442
|
+
* later operation must establish a fresh connection. */
|
|
443
|
+
async forceResetTransport(client, cause) {
|
|
444
|
+
if (this.client !== client) return;
|
|
445
|
+
this.client = null;
|
|
446
|
+
this.connecting = null;
|
|
447
|
+
this.sftpChannel = null;
|
|
448
|
+
this.sftpWrapper = null;
|
|
449
|
+
if (this.reverseListenerClient === client) this.reverseListenerClient = null;
|
|
450
|
+
this.rejectInflight(
|
|
451
|
+
new SshDisconnectedError(`SSH connection reset to quiesce an operation: ${cause.message}`)
|
|
452
|
+
);
|
|
453
|
+
let closed = false;
|
|
454
|
+
const closePromise = new Promise((resolve) => {
|
|
455
|
+
const done = () => {
|
|
456
|
+
if (closed) return;
|
|
457
|
+
closed = true;
|
|
458
|
+
resolve();
|
|
459
|
+
};
|
|
460
|
+
client.once("close", done);
|
|
461
|
+
client.once("end", done);
|
|
462
|
+
});
|
|
463
|
+
try {
|
|
464
|
+
client.end();
|
|
465
|
+
} catch {
|
|
334
466
|
}
|
|
467
|
+
try {
|
|
468
|
+
client.destroy();
|
|
469
|
+
} catch {
|
|
470
|
+
}
|
|
471
|
+
await Promise.race([
|
|
472
|
+
closePromise,
|
|
473
|
+
new Promise((resolve) => {
|
|
474
|
+
const timer = setTimeout(resolve, TRANSPORT_CLOSE_GRACE_MS);
|
|
475
|
+
timer.unref?.();
|
|
476
|
+
})
|
|
477
|
+
]);
|
|
478
|
+
}
|
|
479
|
+
quiesceSftp(client, wrapper, cause) {
|
|
480
|
+
if (wrapper) {
|
|
481
|
+
const existing = this.sftpQuiescence.get(wrapper);
|
|
482
|
+
if (existing) return existing;
|
|
483
|
+
}
|
|
484
|
+
const quiescence = this.quiesceSftpOnce(client, wrapper, cause);
|
|
485
|
+
if (wrapper) this.sftpQuiescence.set(wrapper, quiescence);
|
|
486
|
+
return quiescence;
|
|
487
|
+
}
|
|
488
|
+
async quiesceSftpOnce(client, wrapper, cause) {
|
|
489
|
+
if (!wrapper || this.sftpWrapper === wrapper) {
|
|
490
|
+
this.sftpChannel = null;
|
|
491
|
+
this.sftpWrapper = null;
|
|
492
|
+
}
|
|
493
|
+
if (wrapper) {
|
|
494
|
+
const closed = await this.waitForChannelClose(wrapper, () => wrapper.end());
|
|
495
|
+
if (closed) return;
|
|
496
|
+
}
|
|
497
|
+
await this.forceResetTransport(client, cause);
|
|
498
|
+
}
|
|
499
|
+
awaitSftpOpen(opening, client) {
|
|
500
|
+
const signal = this.operationSignal();
|
|
501
|
+
this.throwIfAborted("SFTP channel open", signal);
|
|
502
|
+
return new Promise((resolve, reject) => {
|
|
503
|
+
let settled = false;
|
|
504
|
+
let terminating = false;
|
|
505
|
+
const cleanup = () => {
|
|
506
|
+
clearTimeout(timer);
|
|
507
|
+
this.inflight.delete(abort);
|
|
508
|
+
signal?.removeEventListener("abort", onSignalAbort);
|
|
509
|
+
};
|
|
510
|
+
const finish = (act) => {
|
|
511
|
+
if (settled || terminating) return;
|
|
512
|
+
settled = true;
|
|
513
|
+
cleanup();
|
|
514
|
+
act();
|
|
515
|
+
};
|
|
516
|
+
const terminate = (error) => {
|
|
517
|
+
if (settled || terminating) return;
|
|
518
|
+
terminating = true;
|
|
519
|
+
cleanup();
|
|
520
|
+
const settle = () => {
|
|
521
|
+
settled = true;
|
|
522
|
+
reject(error);
|
|
523
|
+
};
|
|
524
|
+
void this.forceResetTransport(client, error).then(settle, settle);
|
|
525
|
+
};
|
|
526
|
+
const abort = (error) => terminate(error);
|
|
527
|
+
const onSignalAbort = () => terminate(abortError("SFTP channel open", signal));
|
|
528
|
+
const timer = setTimeout(
|
|
529
|
+
() => terminate(new Error(`SFTP channel open timed out after ${SFTP_OPERATION_TIMEOUT_MS}ms`)),
|
|
530
|
+
SFTP_OPERATION_TIMEOUT_MS
|
|
531
|
+
);
|
|
532
|
+
timer.unref?.();
|
|
533
|
+
this.inflight.add(abort);
|
|
534
|
+
signal?.addEventListener("abort", onSignalAbort, { once: true });
|
|
535
|
+
if (signal?.aborted) {
|
|
536
|
+
onSignalAbort();
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
opening.then(
|
|
540
|
+
(wrapper) => finish(() => resolve(wrapper)),
|
|
541
|
+
(error) => finish(() => reject(error))
|
|
542
|
+
);
|
|
543
|
+
});
|
|
335
544
|
}
|
|
336
545
|
/**
|
|
337
546
|
* One SFTP subsystem channel per client, opened lazily and shared by every
|
|
@@ -344,26 +553,39 @@ var SshExecutor = class _SshExecutor {
|
|
|
344
553
|
*/
|
|
345
554
|
async sftp() {
|
|
346
555
|
const client = await this.connect();
|
|
347
|
-
|
|
556
|
+
this.throwIfAborted("SFTP channel open");
|
|
557
|
+
if (this.sftpChannel) return this.awaitSftpOpen(this.sftpChannel, client);
|
|
348
558
|
const opening = openSftp(client).then((wrapper) => {
|
|
559
|
+
if (this.client !== client || this.sftpChannel !== opening) {
|
|
560
|
+
try {
|
|
561
|
+
wrapper.end();
|
|
562
|
+
} catch {
|
|
563
|
+
}
|
|
564
|
+
throw new SshDisconnectedError("SFTP channel opened after its SSH transport was reset");
|
|
565
|
+
}
|
|
349
566
|
const drop = () => {
|
|
350
|
-
if (this.sftpChannel === opening)
|
|
567
|
+
if (this.sftpChannel === opening) {
|
|
568
|
+
this.sftpChannel = null;
|
|
569
|
+
this.sftpWrapper = null;
|
|
570
|
+
}
|
|
351
571
|
};
|
|
352
572
|
wrapper.once("close", drop);
|
|
353
573
|
wrapper.once("error", drop);
|
|
574
|
+
this.sftpWrapper = wrapper;
|
|
354
575
|
return wrapper;
|
|
355
576
|
});
|
|
356
577
|
opening.catch(() => {
|
|
357
578
|
if (this.sftpChannel === opening) this.sftpChannel = null;
|
|
358
579
|
});
|
|
359
580
|
this.sftpChannel = opening;
|
|
360
|
-
return opening;
|
|
581
|
+
return this.awaitSftpOpen(opening, client);
|
|
361
582
|
}
|
|
362
583
|
/** Close + forget the shared SFTP channel (frees its session) without
|
|
363
584
|
* touching the SSH client, so file ops can reopen on the same connection. */
|
|
364
585
|
dropSftp() {
|
|
365
586
|
const ch = this.sftpChannel;
|
|
366
587
|
this.sftpChannel = null;
|
|
588
|
+
this.sftpWrapper = null;
|
|
367
589
|
if (ch) ch.then((w) => {
|
|
368
590
|
try {
|
|
369
591
|
w.end();
|
|
@@ -382,6 +604,10 @@ var SshExecutor = class _SshExecutor {
|
|
|
382
604
|
this.client.end();
|
|
383
605
|
} catch {
|
|
384
606
|
}
|
|
607
|
+
try {
|
|
608
|
+
this.client.destroy();
|
|
609
|
+
} catch {
|
|
610
|
+
}
|
|
385
611
|
this.client = null;
|
|
386
612
|
}
|
|
387
613
|
this.connecting = null;
|
|
@@ -398,25 +624,28 @@ var SshExecutor = class _SshExecutor {
|
|
|
398
624
|
if (this.inflight.size > 0) this.dropSftp();
|
|
399
625
|
else this.resetConnection();
|
|
400
626
|
}
|
|
401
|
-
/** Returns true if the error is an SSH channel-open failure. */
|
|
627
|
+
/** Returns true if the error is an SSH channel-open or channel-exec failure. */
|
|
402
628
|
static isChannelError(err) {
|
|
403
629
|
if (!(err instanceof Error)) return false;
|
|
630
|
+
if (isSshExecRequestError(err)) return true;
|
|
404
631
|
const msg = err.message.toLowerCase();
|
|
405
632
|
return msg.includes("channel open failure") || msg.includes("open failed");
|
|
406
633
|
}
|
|
407
634
|
/**
|
|
408
635
|
* Run an operation, and if it fails opening an SSH channel on a half-dead
|
|
409
636
|
* cached connection ("Channel open failure: open failed" — common after the
|
|
410
|
-
* idle timeout drops the socket),
|
|
411
|
-
*
|
|
412
|
-
*
|
|
637
|
+
* idle timeout drops the socket), recover channel capacity and retry ONCE.
|
|
638
|
+
* An idle connection is replaced; a busy one stays alive so its other command
|
|
639
|
+
* is not aborted (see recoverFromChannelError). The SFTP-based ops
|
|
640
|
+
* (writeFile/readFile/exists) must go through this too, or a deploy's route
|
|
413
641
|
* write fails spuriously and only succeeds on a manual redeploy.
|
|
414
642
|
*/
|
|
415
643
|
async withChannelRetry(fn) {
|
|
644
|
+
this.throwIfAborted("operation");
|
|
416
645
|
try {
|
|
417
646
|
return await fn();
|
|
418
647
|
} catch (err) {
|
|
419
|
-
if (_SshExecutor.isChannelError(err)) {
|
|
648
|
+
if (_SshExecutor.isChannelError(err) && !this.operationSignal()?.aborted) {
|
|
420
649
|
this.recoverFromChannelError();
|
|
421
650
|
return fn();
|
|
422
651
|
}
|
|
@@ -431,26 +660,65 @@ var SshExecutor = class _SshExecutor {
|
|
|
431
660
|
async _exec(command, opts) {
|
|
432
661
|
const client = await this.connect();
|
|
433
662
|
const timeout = opts?.timeout ?? 3e4;
|
|
663
|
+
const signal = this.operationSignal();
|
|
664
|
+
this.throwIfAborted("command", signal);
|
|
434
665
|
return new Promise((resolve, reject) => {
|
|
435
666
|
let settled = false;
|
|
667
|
+
let terminating = false;
|
|
436
668
|
let timer = null;
|
|
437
|
-
|
|
438
|
-
const
|
|
439
|
-
if (settled) return;
|
|
440
|
-
settled = true;
|
|
669
|
+
let channel = null;
|
|
670
|
+
const cleanup = () => {
|
|
441
671
|
if (timer) clearTimeout(timer);
|
|
442
672
|
this.inflight.delete(abort);
|
|
673
|
+
signal?.removeEventListener("abort", onSignalAbort);
|
|
674
|
+
};
|
|
675
|
+
const finish = (act) => {
|
|
676
|
+
if (settled || terminating) return;
|
|
677
|
+
settled = true;
|
|
678
|
+
cleanup();
|
|
443
679
|
act();
|
|
444
680
|
};
|
|
681
|
+
const terminate = (error) => {
|
|
682
|
+
if (settled || terminating) return;
|
|
683
|
+
terminating = true;
|
|
684
|
+
cleanup();
|
|
685
|
+
const quiesce = (async () => {
|
|
686
|
+
if (channel) {
|
|
687
|
+
const closed = await this.waitForChannelClose(channel, () => channel.close());
|
|
688
|
+
if (!closed) await this.forceResetTransport(client, error);
|
|
689
|
+
} else {
|
|
690
|
+
await this.forceResetTransport(client, error);
|
|
691
|
+
}
|
|
692
|
+
})();
|
|
693
|
+
const settle = () => {
|
|
694
|
+
settled = true;
|
|
695
|
+
reject(error);
|
|
696
|
+
};
|
|
697
|
+
void quiesce.then(settle, settle);
|
|
698
|
+
};
|
|
699
|
+
const abort = (err) => terminate(err);
|
|
700
|
+
const onSignalAbort = () => terminate(abortError("command", signal));
|
|
445
701
|
this.inflight.add(abort);
|
|
702
|
+
signal?.addEventListener("abort", onSignalAbort, { once: true });
|
|
703
|
+
if (signal?.aborted) {
|
|
704
|
+
onSignalAbort();
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
446
707
|
timer = setTimeout(
|
|
447
|
-
() =>
|
|
448
|
-
() => reject(new Error(`Command timed out after ${timeout}ms: ${commandForError(command)}`))
|
|
449
|
-
),
|
|
708
|
+
() => terminate(new Error(`Command timed out after ${timeout}ms: ${commandForError(command)}`)),
|
|
450
709
|
timeout
|
|
451
710
|
);
|
|
711
|
+
timer.unref?.();
|
|
452
712
|
client.exec(_SshExecutor.ENV_PREFIX + command, (err, stream) => {
|
|
453
|
-
if (err) return finish(() => reject(err));
|
|
713
|
+
if (err) return finish(() => reject(tagExecRequestError(err)));
|
|
714
|
+
channel = stream;
|
|
715
|
+
if (terminating) {
|
|
716
|
+
try {
|
|
717
|
+
stream.close();
|
|
718
|
+
} catch {
|
|
719
|
+
}
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
454
722
|
let stdout = "";
|
|
455
723
|
let stderr = "";
|
|
456
724
|
stream.on("data", (data) => {
|
|
@@ -459,6 +727,7 @@ var SshExecutor = class _SshExecutor {
|
|
|
459
727
|
stream.stderr.on("data", (data) => {
|
|
460
728
|
stderr += data.toString();
|
|
461
729
|
});
|
|
730
|
+
stream.on("error", (error) => finish(() => reject(error)));
|
|
462
731
|
stream.on("close", (code) => {
|
|
463
732
|
finish(() => {
|
|
464
733
|
if (code !== 0) {
|
|
@@ -471,10 +740,11 @@ var SshExecutor = class _SshExecutor {
|
|
|
471
740
|
});
|
|
472
741
|
}
|
|
473
742
|
streamExec(command, onLog, opts) {
|
|
474
|
-
|
|
475
|
-
|
|
743
|
+
const scopedOpts = { ...opts, signal: this.resolvedSignal(opts?.signal) };
|
|
744
|
+
return this._streamExec(command, onLog, scopedOpts).catch((err) => {
|
|
745
|
+
if (_SshExecutor.isChannelError(err) && !scopedOpts.signal?.aborted) {
|
|
476
746
|
this.recoverFromChannelError();
|
|
477
|
-
return this._streamExec(command, onLog,
|
|
747
|
+
return this._streamExec(command, onLog, scopedOpts);
|
|
478
748
|
}
|
|
479
749
|
throw err;
|
|
480
750
|
});
|
|
@@ -484,33 +754,56 @@ var SshExecutor = class _SshExecutor {
|
|
|
484
754
|
const signal = opts?.signal;
|
|
485
755
|
return new Promise((resolve, reject) => {
|
|
486
756
|
let settled = false;
|
|
757
|
+
let terminating = false;
|
|
487
758
|
const chunks = [];
|
|
488
|
-
|
|
489
|
-
const
|
|
490
|
-
if (settled) return;
|
|
491
|
-
settled = true;
|
|
759
|
+
let channel = null;
|
|
760
|
+
const cleanup = () => {
|
|
492
761
|
this.inflight.delete(abort);
|
|
493
762
|
signal?.removeEventListener("abort", onSignalAbort);
|
|
763
|
+
};
|
|
764
|
+
const finish = (act) => {
|
|
765
|
+
if (settled || terminating) return;
|
|
766
|
+
settled = true;
|
|
767
|
+
cleanup();
|
|
494
768
|
act();
|
|
495
769
|
};
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
channel
|
|
502
|
-
|
|
503
|
-
|
|
770
|
+
const terminate = (error, intentional) => {
|
|
771
|
+
if (settled || terminating) return;
|
|
772
|
+
terminating = true;
|
|
773
|
+
cleanup();
|
|
774
|
+
const quiesce = (async () => {
|
|
775
|
+
if (channel) {
|
|
776
|
+
const closed = await this.waitForChannelClose(channel, () => channel.close());
|
|
777
|
+
if (!closed) await this.forceResetTransport(client, error);
|
|
778
|
+
} else {
|
|
779
|
+
await this.forceResetTransport(client, error);
|
|
780
|
+
}
|
|
781
|
+
})();
|
|
782
|
+
const settle = () => {
|
|
783
|
+
settled = true;
|
|
784
|
+
if (intentional) resolve({ code: 0, output: chunks.join("") });
|
|
785
|
+
else reject(error);
|
|
786
|
+
};
|
|
787
|
+
void quiesce.then(settle, settle);
|
|
504
788
|
};
|
|
789
|
+
const abort = (err) => terminate(err, false);
|
|
790
|
+
const onSignalAbort = () => terminate(abortError("streaming command", signal), true);
|
|
791
|
+
this.inflight.add(abort);
|
|
792
|
+
signal?.addEventListener("abort", onSignalAbort, { once: true });
|
|
505
793
|
if (signal?.aborted) {
|
|
506
|
-
|
|
794
|
+
onSignalAbort();
|
|
507
795
|
return;
|
|
508
796
|
}
|
|
509
797
|
client.exec(_SshExecutor.ENV_PREFIX + command, (err, stream) => {
|
|
510
|
-
if (err) return finish(() => reject(err));
|
|
798
|
+
if (err) return finish(() => reject(tagExecRequestError(err)));
|
|
511
799
|
channel = stream;
|
|
512
|
-
if (signal?.aborted)
|
|
513
|
-
|
|
800
|
+
if (terminating || signal?.aborted) {
|
|
801
|
+
try {
|
|
802
|
+
stream.close();
|
|
803
|
+
} catch {
|
|
804
|
+
}
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
514
807
|
const onChunk = (data, level) => {
|
|
515
808
|
const text = data.toString();
|
|
516
809
|
if (!text) return;
|
|
@@ -519,6 +812,7 @@ var SshExecutor = class _SshExecutor {
|
|
|
519
812
|
};
|
|
520
813
|
stream.on("data", (data) => onChunk(data, "info"));
|
|
521
814
|
stream.stderr.on("data", (data) => onChunk(data, "warn"));
|
|
815
|
+
stream.on("error", (error) => finish(() => reject(error)));
|
|
522
816
|
let exitCode = null;
|
|
523
817
|
stream.on("exit", (code) => {
|
|
524
818
|
exitCode = code;
|
|
@@ -540,7 +834,62 @@ var SshExecutor = class _SshExecutor {
|
|
|
540
834
|
});
|
|
541
835
|
});
|
|
542
836
|
}
|
|
543
|
-
|
|
837
|
+
/** Turn ssh2's callback-only SFTP request into a bounded operation. A lost
|
|
838
|
+
* callback used to leave the compose deploy promise and its host mutex tail
|
|
839
|
+
* pending forever. Cancellation/timeout closes the shared subsystem first;
|
|
840
|
+
* if close itself wedges, the parent SSH transport is destroyed before this
|
|
841
|
+
* promise rejects. */
|
|
842
|
+
runSftpRequest(client, sftp, operation, start) {
|
|
843
|
+
const signal = this.operationSignal();
|
|
844
|
+
this.throwIfAborted(operation, signal);
|
|
845
|
+
return new Promise((resolve, reject) => {
|
|
846
|
+
let settled = false;
|
|
847
|
+
let terminating = false;
|
|
848
|
+
const cleanup = () => {
|
|
849
|
+
clearTimeout(timer);
|
|
850
|
+
this.inflight.delete(abort);
|
|
851
|
+
signal?.removeEventListener("abort", onSignalAbort);
|
|
852
|
+
};
|
|
853
|
+
const finish = (act) => {
|
|
854
|
+
if (settled || terminating) return;
|
|
855
|
+
settled = true;
|
|
856
|
+
cleanup();
|
|
857
|
+
act();
|
|
858
|
+
};
|
|
859
|
+
const terminate = (error) => {
|
|
860
|
+
if (settled || terminating) return;
|
|
861
|
+
terminating = true;
|
|
862
|
+
cleanup();
|
|
863
|
+
const settle = () => {
|
|
864
|
+
settled = true;
|
|
865
|
+
reject(error);
|
|
866
|
+
};
|
|
867
|
+
void this.quiesceSftp(client, sftp, error).then(settle, settle);
|
|
868
|
+
};
|
|
869
|
+
const abort = (error) => terminate(error);
|
|
870
|
+
const onSignalAbort = () => terminate(abortError(operation, signal));
|
|
871
|
+
const timer = setTimeout(
|
|
872
|
+
() => terminate(new Error(`${operation} timed out after ${SFTP_OPERATION_TIMEOUT_MS}ms`)),
|
|
873
|
+
SFTP_OPERATION_TIMEOUT_MS
|
|
874
|
+
);
|
|
875
|
+
timer.unref?.();
|
|
876
|
+
this.inflight.add(abort);
|
|
877
|
+
signal?.addEventListener("abort", onSignalAbort, { once: true });
|
|
878
|
+
if (signal?.aborted) {
|
|
879
|
+
onSignalAbort();
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
try {
|
|
883
|
+
start((error, value) => {
|
|
884
|
+
if (error) finish(() => reject(error));
|
|
885
|
+
else finish(() => resolve(value));
|
|
886
|
+
});
|
|
887
|
+
} catch (error) {
|
|
888
|
+
finish(() => reject(error));
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
async writeFile(path, content, opts) {
|
|
544
893
|
const dir = remoteDirname(path);
|
|
545
894
|
try {
|
|
546
895
|
await this.exec(`mkdir -p ${sq(dir)}`);
|
|
@@ -548,12 +897,21 @@ var SshExecutor = class _SshExecutor {
|
|
|
548
897
|
}
|
|
549
898
|
return this.withChannelRetry(async () => {
|
|
550
899
|
const sftp = await this.sftp();
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
});
|
|
900
|
+
const client = this.client;
|
|
901
|
+
if (!client) throw new SshDisconnectedError("SSH connection lost before SFTP write");
|
|
902
|
+
const write = (data, mode) => this.runSftpRequest(client, sftp, `SFTP write ${path}`, (done) => {
|
|
903
|
+
sftp.writeFile(path, data, { encoding: "utf-8", mode }, (err) => done(err));
|
|
556
904
|
});
|
|
905
|
+
const chmod2 = (mode) => this.runSftpRequest(client, sftp, `SFTP chmod ${path}`, (done) => {
|
|
906
|
+
sftp.chmod(path, mode, (err) => done(err));
|
|
907
|
+
});
|
|
908
|
+
if (opts?.mode !== void 0) {
|
|
909
|
+
await write("", opts.mode);
|
|
910
|
+
await chmod2(opts.mode);
|
|
911
|
+
await write(content);
|
|
912
|
+
} else {
|
|
913
|
+
await write(content);
|
|
914
|
+
}
|
|
557
915
|
});
|
|
558
916
|
}
|
|
559
917
|
async rename(from, to) {
|
|
@@ -562,10 +920,11 @@ var SshExecutor = class _SshExecutor {
|
|
|
562
920
|
async readFile(path) {
|
|
563
921
|
return this.withChannelRetry(async () => {
|
|
564
922
|
const sftp = await this.sftp();
|
|
565
|
-
|
|
923
|
+
const client = this.client;
|
|
924
|
+
if (!client) throw new SshDisconnectedError("SSH connection lost before SFTP read");
|
|
925
|
+
return this.runSftpRequest(client, sftp, `SFTP read ${path}`, (done) => {
|
|
566
926
|
sftp.readFile(path, { encoding: "utf-8" }, (err, data) => {
|
|
567
|
-
|
|
568
|
-
else resolve(data.toString());
|
|
927
|
+
done(err, data?.toString());
|
|
569
928
|
});
|
|
570
929
|
});
|
|
571
930
|
});
|
|
@@ -573,9 +932,11 @@ var SshExecutor = class _SshExecutor {
|
|
|
573
932
|
async exists(path) {
|
|
574
933
|
return this.withChannelRetry(async () => {
|
|
575
934
|
const sftp = await this.sftp();
|
|
576
|
-
|
|
935
|
+
const client = this.client;
|
|
936
|
+
if (!client) throw new SshDisconnectedError("SSH connection lost before SFTP stat");
|
|
937
|
+
return this.runSftpRequest(client, sftp, `SFTP stat ${path}`, (done) => {
|
|
577
938
|
sftp.stat(path, (err) => {
|
|
578
|
-
|
|
939
|
+
done(null, !err);
|
|
579
940
|
});
|
|
580
941
|
});
|
|
581
942
|
});
|
|
@@ -833,6 +1194,7 @@ var SshExecutor = class _SshExecutor {
|
|
|
833
1194
|
async dispose() {
|
|
834
1195
|
this.connecting = null;
|
|
835
1196
|
this.sftpChannel = null;
|
|
1197
|
+
this.sftpWrapper = null;
|
|
836
1198
|
this.reverseHandlers.clear();
|
|
837
1199
|
this.reverseListenerClient = null;
|
|
838
1200
|
if (this.client) {
|
|
@@ -976,6 +1338,7 @@ var SshExecutor = class _SshExecutor {
|
|
|
976
1338
|
};
|
|
977
1339
|
|
|
978
1340
|
// ../../packages/adapters/src/system/system-ssh-executor.ts
|
|
1341
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
979
1342
|
import { execFile as execFile2, spawn as spawn2 } from "child_process";
|
|
980
1343
|
import { once } from "events";
|
|
981
1344
|
import { mkdtemp as mkdtemp3, rm as fsRm3, stat as stat2, unlink } from "fs/promises";
|
|
@@ -1090,6 +1453,13 @@ async function openSystemSshReverseTunnel(opts) {
|
|
|
1090
1453
|
// ../../packages/adapters/src/system/system-ssh-executor.ts
|
|
1091
1454
|
var execFileAsync2 = promisify2(execFile2);
|
|
1092
1455
|
var remoteDirname2 = posix2.dirname;
|
|
1456
|
+
function abortError2(operation, signal) {
|
|
1457
|
+
const reason = signal.reason;
|
|
1458
|
+
const suffix = reason instanceof Error && reason.message ? `: ${reason.message}` : "";
|
|
1459
|
+
const error = new Error(`SSH ${operation} cancelled${suffix}`);
|
|
1460
|
+
error.name = "AbortError";
|
|
1461
|
+
return error;
|
|
1462
|
+
}
|
|
1093
1463
|
function clampWindow2(value, fallback, min, max) {
|
|
1094
1464
|
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
1095
1465
|
return Math.max(min, Math.min(max, Math.floor(value)));
|
|
@@ -1103,6 +1473,7 @@ function describeSshFailure(stderr, fallback) {
|
|
|
1103
1473
|
}
|
|
1104
1474
|
var SystemSshExecutor = class _SystemSshExecutor {
|
|
1105
1475
|
config;
|
|
1476
|
+
abortScope = new AsyncLocalStorage2();
|
|
1106
1477
|
controlPath = makeControlPath();
|
|
1107
1478
|
/** Resolves once the ControlMaster connection is established. */
|
|
1108
1479
|
masterPromise = null;
|
|
@@ -1119,6 +1490,25 @@ var SystemSshExecutor = class _SystemSshExecutor {
|
|
|
1119
1490
|
}
|
|
1120
1491
|
this.config = config;
|
|
1121
1492
|
}
|
|
1493
|
+
runWithAbortSignal(signal, fn) {
|
|
1494
|
+
if (signal.aborted) return Promise.reject(abortError2("operation", signal));
|
|
1495
|
+
return this.abortScope.run(signal, async () => {
|
|
1496
|
+
this.throwIfAborted("operation");
|
|
1497
|
+
return fn();
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
operationSignal() {
|
|
1501
|
+
return this.abortScope.getStore();
|
|
1502
|
+
}
|
|
1503
|
+
resolvedSignal(explicit) {
|
|
1504
|
+
const ambient = this.operationSignal();
|
|
1505
|
+
if (!explicit || explicit === ambient) return ambient ?? explicit;
|
|
1506
|
+
if (!ambient) return explicit;
|
|
1507
|
+
return AbortSignal.any([explicit, ambient]);
|
|
1508
|
+
}
|
|
1509
|
+
throwIfAborted(operation, signal = this.operationSignal()) {
|
|
1510
|
+
if (signal?.aborted) throw abortError2(operation, signal);
|
|
1511
|
+
}
|
|
1122
1512
|
baseArgs() {
|
|
1123
1513
|
return buildBaseSshArgs(this.config, this.controlPath);
|
|
1124
1514
|
}
|
|
@@ -1199,7 +1589,10 @@ var SystemSshExecutor = class _SystemSshExecutor {
|
|
|
1199
1589
|
}
|
|
1200
1590
|
/** Run a remote command, resolving with stdout/stderr/exit code (never rejects on non-zero). */
|
|
1201
1591
|
async runSsh(remoteCommand, opts) {
|
|
1592
|
+
const signal = this.operationSignal();
|
|
1593
|
+
this.throwIfAborted("command", signal);
|
|
1202
1594
|
await this.ensureMaster();
|
|
1595
|
+
this.throwIfAborted("command", signal);
|
|
1203
1596
|
return new Promise((resolve, reject) => {
|
|
1204
1597
|
const child = spawn2("ssh", [...this.baseArgs(), sshTarget(this.config), remoteCommand], {
|
|
1205
1598
|
env: sshChildEnv(this.config),
|
|
@@ -1208,7 +1601,25 @@ var SystemSshExecutor = class _SystemSshExecutor {
|
|
|
1208
1601
|
let stdout = "";
|
|
1209
1602
|
let stderr = "";
|
|
1210
1603
|
let timedOut = false;
|
|
1604
|
+
let settled = false;
|
|
1211
1605
|
let timer = null;
|
|
1606
|
+
const cleanup = () => {
|
|
1607
|
+
if (timer) clearTimeout(timer);
|
|
1608
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1609
|
+
};
|
|
1610
|
+
const finish = (act) => {
|
|
1611
|
+
if (settled) return;
|
|
1612
|
+
settled = true;
|
|
1613
|
+
cleanup();
|
|
1614
|
+
act();
|
|
1615
|
+
};
|
|
1616
|
+
const onAbort = () => {
|
|
1617
|
+
try {
|
|
1618
|
+
child.kill("SIGKILL");
|
|
1619
|
+
} catch {
|
|
1620
|
+
}
|
|
1621
|
+
};
|
|
1622
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1212
1623
|
if (opts?.timeout) {
|
|
1213
1624
|
timer = setTimeout(() => {
|
|
1214
1625
|
timedOut = true;
|
|
@@ -1223,16 +1634,21 @@ var SystemSshExecutor = class _SystemSshExecutor {
|
|
|
1223
1634
|
stderr += d.toString();
|
|
1224
1635
|
});
|
|
1225
1636
|
child.on("error", (e) => {
|
|
1226
|
-
|
|
1227
|
-
reject(new Error(`ssh failed to start: ${e.message}`));
|
|
1637
|
+
finish(() => reject(new Error(`ssh failed to start: ${e.message}`)));
|
|
1228
1638
|
});
|
|
1229
1639
|
child.on("close", (code) => {
|
|
1230
|
-
if (timer) clearTimeout(timer);
|
|
1231
1640
|
const c = code ?? 1;
|
|
1232
1641
|
void this.maybeSignalDisconnect(c).catch(() => {
|
|
1233
1642
|
});
|
|
1234
|
-
|
|
1643
|
+
finish(() => {
|
|
1644
|
+
if (signal?.aborted) reject(abortError2("command", signal));
|
|
1645
|
+
else resolve({ stdout, stderr, code: c, timedOut });
|
|
1646
|
+
});
|
|
1235
1647
|
});
|
|
1648
|
+
if (signal?.aborted) {
|
|
1649
|
+
onAbort();
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1236
1652
|
if (opts?.input !== void 0) {
|
|
1237
1653
|
child.stdin.write(opts.input);
|
|
1238
1654
|
}
|
|
@@ -1253,7 +1669,7 @@ var SystemSshExecutor = class _SystemSshExecutor {
|
|
|
1253
1669
|
}
|
|
1254
1670
|
async streamExec(command, onLog, opts) {
|
|
1255
1671
|
await this.ensureMaster();
|
|
1256
|
-
const signal = opts?.signal;
|
|
1672
|
+
const signal = this.resolvedSignal(opts?.signal);
|
|
1257
1673
|
if (signal?.aborted) return { code: 0, output: "" };
|
|
1258
1674
|
return new Promise((resolve) => {
|
|
1259
1675
|
const child = spawn2(
|
|
@@ -1290,8 +1706,10 @@ var SystemSshExecutor = class _SystemSshExecutor {
|
|
|
1290
1706
|
});
|
|
1291
1707
|
});
|
|
1292
1708
|
}
|
|
1293
|
-
async writeFile(path, content) {
|
|
1294
|
-
const
|
|
1709
|
+
async writeFile(path, content, opts) {
|
|
1710
|
+
const target = sq(path);
|
|
1711
|
+
const prepare = opts?.mode === void 0 ? `cat > ${target}` : `: > ${target} && chmod ${opts.mode.toString(8)} ${target} && cat > ${target}`;
|
|
1712
|
+
const remoteCommand = `mkdir -p ${sq(remoteDirname2(path))} && ${prepare}`;
|
|
1295
1713
|
const res = await this.runSsh(remoteCommand, { input: content });
|
|
1296
1714
|
if (res.code !== 0) {
|
|
1297
1715
|
throw new Error(res.stderr.trim() || `Failed to write ${path} (exit ${res.code})`);
|
|
@@ -1691,7 +2109,7 @@ async function verifyHostChannelAuth(config) {
|
|
|
1691
2109
|
let hint = null;
|
|
1692
2110
|
let forwarding = "unknown";
|
|
1693
2111
|
try {
|
|
1694
|
-
const { connectSshClient: connectSshClient2 } = await import("./ssh-client-
|
|
2112
|
+
const { connectSshClient: connectSshClient2 } = await import("./ssh-client-FZDWP5PQ.js");
|
|
1695
2113
|
const client = await connectSshClient2({
|
|
1696
2114
|
host: config.host,
|
|
1697
2115
|
port: config.port,
|