History

[codex] Redact SSH tunnel diagnostics (#3412)

Julius Marminge committed 4 months ago

Commit 4e1cdad1 parent 1d53e193 files · +178 −36Browse files

Unified · Split

3 files
  1. packages/ssh/src/command.ts +4 −1
  2. packages/ssh/src/tunnel.test.ts +122 −11
  3. packages/ssh/src/tunnel.ts +52 −24
packages/ssh/src/command.ts+4−1
@@ -350,7 +350,10 @@ export const resolveSshTarget = Effect.fn("ssh/command.resolveSshTarget")(functi · 349 unmodified lines
350 Effect.logDebug("ssh.target.resolve.succeeded", sshTargetLogFields(target)),350 Effect.logDebug("ssh.target.resolve.succeeded", sshTargetLogFields(target)),
351 ),351 ),
352 Effect.catch((cause) =>352 Effect.catch((cause) =>
353 Effect.logDebug("ssh.target.resolve.fallback", { alias: trimmedAlias, cause }).pipe(353 Effect.logDebug("ssh.target.resolve.fallback", {
354 alias: trimmedAlias,
355 failureTag: cause._tag,
356 }).pipe(
354 Effect.as({357 Effect.as({
355 alias: trimmedAlias,358 alias: trimmedAlias,
356 hostname: trimmedAlias,359 hostname: trimmedAlias,
packages/ssh/src/tunnel.test.ts+122−11
@@ -5,12 +5,14 @@ import * as Duration from "effect/Duration"; · 4 unmodified lines
5import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
6import * as Fiber from "effect/Fiber";6import * as Fiber from "effect/Fiber";
7import * as Layer from "effect/Layer";7import * as Layer from "effect/Layer";
8import * as Logger from "effect/Logger";
8import * as PlatformError from "effect/PlatformError";9import * as PlatformError from "effect/PlatformError";
9import * as Result from "effect/Result";10import * as Result from "effect/Result";
10import * as Sink from "effect/Sink";11import * as Sink from "effect/Sink";
11import * as Stream from "effect/Stream";12import * as Stream from "effect/Stream";
12import * as TestClock from "effect/testing/TestClock";13import * as TestClock from "effect/testing/TestClock";
13import * as HttpClient from "effect/unstable/http/HttpClient";14import * as HttpClient from "effect/unstable/http/HttpClient";
15import * as HttpClientError from "effect/unstable/http/HttpClientError";
14import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";16import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
15import * as ChildProcess from "effect/unstable/process/ChildProcess";17import * as ChildProcess from "effect/unstable/process/ChildProcess";
16import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";18import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
@@ -21,6 +23,7 @@ import { · 4 unmodified lines
21 SshHttpBridgeMissingUrlError,23 SshHttpBridgeMissingUrlError,
22 SshHttpBridgeNonLoopbackUrlError,24 SshHttpBridgeNonLoopbackUrlError,
23 SshPairingOutputParseError,25 SshPairingOutputParseError,
26 SshReadinessProbeError,
24 SshReadinessProbeTimeoutError,27 SshReadinessProbeTimeoutError,
25 SshReadinessTimeoutError,28 SshReadinessTimeoutError,
26} from "./errors.ts";29} from "./errors.ts";
@@ -88,6 +91,14 @@ function commandArgs(command: ChildProcess.Command): ReadonlyArray<string> { · 61 unmodified lines
88 return command._tag === "StandardCommand" ? command.args : [];91 return command._tag === "StandardCommand" ? command.args : [];
89}92}
90 93
94function flattenedLogText(value: unknown): string {
95 if (typeof value === "string") return value;
96 if (typeof value !== "object" || value === null) return String(value);
97 return Object.entries(value)
98 .flatMap(([key, nested]) => [key, flattenedLogText(nested)])
99 .join("\n");
100}
101
91describe("ssh tunnel scripts", () => {102describe("ssh tunnel scripts", () => {
92 it("builds the remote t3 runner with npx and npm fallbacks", () => {103 it("builds the remote t3 runner with npx and npm fallbacks", () => {
93 const script = SshTunnel.buildRemoteT3RunnerScript({ nodeEngineRange: TEST_NODE_ENGINE_RANGE });104 const script = SshTunnel.buildRemoteT3RunnerScript({ nodeEngineRange: TEST_NODE_ENGINE_RANGE });
@@ -255,13 +266,22 @@ describe("ssh tunnel scripts", () => { · 161 unmodified lines
255 assert.include(SshTunnel.REMOTE_PICK_PORT_SCRIPT, 'const filePath = process.argv[2] ?? "";');266 assert.include(SshTunnel.REMOTE_PICK_PORT_SCRIPT, 'const filePath = process.argv[2] ?? "";');
256 });267 });
257 268
258 it.effect("bounds each HTTP readiness probe so retries cannot hang on one request", () =>269 it.effect("bounds each HTTP readiness probe so retries cannot hang on one request", () => {
259 Effect.gen(function* () {270 const baseUrl =
271 "http://ssh-user:ssh-password@127.0.0.1:41773/private/base?token=base-secret#base-fragment";
272 const path = "/ready/private?credential=request-secret#request-fragment";
273 const requestUrl = new URL(path, baseUrl).toString();
274 const capturedLogs: Array<ReadonlyArray<unknown>> = [];
275 const logger = Logger.make(({ message }) => {
276 capturedLogs.push(Array.isArray(message) ? message : [message]);
277 });
278
279 return Effect.gen(function* () {
260 const fiber = yield* Effect.forkChild(280 const fiber = yield* Effect.forkChild(
261 Effect.result(281 Effect.result(
262 SshTunnel.waitForHttpReady({282 SshTunnel.waitForHttpReady({
263 baseUrl: "http://127.0.0.1:41773/?token=base-secret#fragment",283 baseUrl,
264 path: "/ready?credential=request-secret#fragment",284 path,
265 timeoutMs: 1_000,285 timeoutMs: 1_000,
266 intervalMs: 100,286 intervalMs: 100,
267 probeTimeoutMs: 250,287 probeTimeoutMs: 250,
@@ -281,8 +301,8 @@ describe("ssh tunnel scripts", () => { · 13 unmodified lines
281 protocol: "http:",301 protocol: "http:",
282 hostname: "127.0.0.1",302 hostname: "127.0.0.1",
283 port: "41773",303 port: "41773",
284 urlLength: 50,304 urlLength: baseUrl.length,
285 pathnameLength: 1,305 pathnameLength: new TextEncoder().encode(new URL(baseUrl).pathname).length,
286 hasQuery: true,306 hasQuery: true,
287 hasFragment: true,307 hasFragment: true,
288 });308 });
@@ -290,8 +310,8 @@ describe("ssh tunnel scripts", () => { · 1 unmodified line
290 protocol: "http:",310 protocol: "http:",
291 hostname: "127.0.0.1",311 hostname: "127.0.0.1",
292 port: "41773",312 port: "41773",
293 urlLength: 63,313 urlLength: requestUrl.length,
294 pathnameLength: 6,314 pathnameLength: new TextEncoder().encode(new URL(requestUrl).pathname).length,
295 hasQuery: true,315 hasQuery: true,
296 hasFragment: true,316 hasFragment: true,
297 });317 });
@@ -311,13 +331,91 @@ describe("ssh tunnel scripts", () => { · 13 unmodified lines
311 const probeTimeout = timeoutError.cause as SshReadinessProbeTimeoutError;331 const probeTimeout = timeoutError.cause as SshReadinessProbeTimeoutError;
312 assert.equal(probeTimeout.attempt, timeoutError.attempts);332 assert.equal(probeTimeout.attempt, timeoutError.attempts);
313 assert.isFalse("cause" in probeTimeout);333 assert.isFalse("cause" in probeTimeout);
334
335 const logText = flattenedLogText(capturedLogs);
336 assert.include(logText, "127.0.0.1");
337 assert.notInclude(logText, "ssh-user");
338 assert.notInclude(logText, "ssh-password");
339 assert.notInclude(logText, "/private/base");
340 assert.notInclude(logText, "base-secret");
341 assert.notInclude(logText, "/ready/private");
342 assert.notInclude(logText, "request-secret");
343 assert.notInclude(logText, "base-fragment");
344 assert.notInclude(logText, "request-fragment");
314 }345 }
315 }).pipe(346 }).pipe(
316 Effect.provide(347 Effect.provide(
317 Layer.merge(TestClock.layer(), Layer.succeed(HttpClient.HttpClient, hangingHttpClient)),348 Layer.mergeAll(
349 TestClock.layer(),
350 Layer.succeed(HttpClient.HttpClient, hangingHttpClient),
351 Logger.layer([logger], { mergeWithExisting: false }),
352 ),
318 ),353 ),
319 ),354 );
320 );355 });
356
357 it.effect("preserves the exact HTTP transport cause without logging request secrets", () => {
358 const baseUrl =
359 "http://transport-user:transport-password@127.0.0.1:41773/private?token=transport-secret#fragment";
360 const transportCause = new Error("socket closed");
361 const clientErrors: Array<HttpClientError.HttpClientError> = [];
362 const failingHttpClient = HttpClient.make((request) => {
363 const error = new HttpClientError.HttpClientError({
364 reason: new HttpClientError.TransportError({ request, cause: transportCause }),
365 });
366 clientErrors.push(error);
367 return Effect.fail(error);
368 });
369 const capturedLogs: Array<ReadonlyArray<unknown>> = [];
370 const logger = Logger.make(({ message }) => {
371 capturedLogs.push(Array.isArray(message) ? message : [message]);
372 });
373
374 return Effect.gen(function* () {
375 const fiber = yield* Effect.forkChild(
376 Effect.result(
377 SshTunnel.waitForHttpReady({
378 baseUrl,
379 timeoutMs: 300,
380 intervalMs: 100,
381 probeTimeoutMs: 250,
382 }),
383 ),
384 );
385 yield* Effect.yieldNow;
386 yield* TestClock.adjust(Duration.millis(300));
387
388 const result = yield* Fiber.join(fiber);
389 assert.isTrue(Result.isFailure(result));
390 if (Result.isFailure(result)) {
391 assert.instanceOf(result.failure, SshReadinessTimeoutError);
392 const timeoutError = result.failure as SshReadinessTimeoutError;
393 assert.instanceOf(timeoutError.cause, SshReadinessProbeError);
394 const probeError = timeoutError.cause as SshReadinessProbeError;
395 assert.strictEqual(probeError.cause, clientErrors.at(-1));
396 assert.strictEqual(
397 (probeError.cause as HttpClientError.HttpClientError).reason.cause,
398 transportCause,
399 );
400
401 const logText = flattenedLogText(capturedLogs);
402 assert.include(logText, "HttpClientError");
403 assert.include(logText, "TransportError");
404 assert.notInclude(logText, "transport-user");
405 assert.notInclude(logText, "transport-password");
406 assert.notInclude(logText, "/private");
407 assert.notInclude(logText, "transport-secret");
408 }
409 }).pipe(
410 Effect.provide(
411 Layer.mergeAll(
412 TestClock.layer(),
413 Layer.succeed(HttpClient.HttpClient, failingHttpClient),
414 Logger.layer([logger], { mergeWithExisting: false }),
415 ),
416 ),
417 );
418 });
321 419
322 it.effect("preserves forwarded HTTP URL error messages", () =>420 it.effect("preserves forwarded HTTP URL error messages", () =>
323 Effect.gen(function* () {421 Effect.gen(function* () {
@@ -363,6 +461,10 @@ describe("ssh tunnel scripts", () => { · 39 unmodified lines
363 cause: { type: "string" },461 cause: { type: "string" },
364 },462 },
365 );463 );
464
465 const cyclicCause: { readonly _tag: string; cause?: unknown } = { _tag: "CyclicCause" };
466 cyclicCause.cause = cyclicCause;
467 assert.include(flattenedLogText(SshTunnel.describeReadinessCause(cyclicCause)), "truncated");
366 });468 });
367 469
368 it("preserves structured readiness attributes in diagnostic output", () => {470 it("preserves structured readiness attributes in diagnostic output", () => {
@@ -542,6 +644,10 @@ describe("ssh tunnel scripts", () => { · 173 unmodified lines
542 const spawnedCommands: Array<ReadonlyArray<string>> = [];644 const spawnedCommands: Array<ReadonlyArray<string>> = [];
543 let tunnelKillCount = 0;645 let tunnelKillCount = 0;
544 let stopCommandCount = 0;646 let stopCommandCount = 0;
647 const capturedLogs: Array<ReadonlyArray<unknown>> = [];
648 const logger = Logger.make(({ message }) => {
649 capturedLogs.push(Array.isArray(message) ? message : [message]);
650 });
545 const spawner = ChildProcessSpawner.make((command) =>651 const spawner = ChildProcessSpawner.make((command) =>
546 Effect.sync(() => {652 Effect.sync(() => {
547 const args = commandArgs(command);653 const args = commandArgs(command);
@@ -568,6 +674,7 @@ describe("ssh tunnel scripts", () => { · 20 unmodified lines
568 Layer.succeed(NetService.NetService, testNetService),674 Layer.succeed(NetService.NetService, testNetService),
569 SshAuth.disabledLayer,675 SshAuth.disabledLayer,
570 SshTunnel.layer(),676 SshTunnel.layer(),
677 Logger.layer([logger], { mergeWithExisting: false }),
571 );678 );
572 const target = {679 const target = {
573 alias: "devbox",680 alias: "devbox",
@@ -590,6 +697,10 @@ describe("ssh tunnel scripts", () => { · 16 unmodified lines
590 697
591 assert.equal(spawnedCommands.filter((args) => args.includes("-N")).length, 2);698 assert.equal(spawnedCommands.filter((args) => args.includes("-N")).length, 2);
592 assert.equal(tunnelKillCount, 1);699 assert.equal(tunnelKillCount, 1);
700
701 const logText = flattenedLogText(capturedLogs);
702 assert.include(logText, "httpBaseUrlDiagnostics");
703 assert.notInclude(logText, "httpBaseUrl\nhttp://");
593 }).pipe(Effect.provide(layer), Effect.scoped);704 }).pipe(Effect.provide(layer), Effect.scoped);
594 });705 });
595});706});
packages/ssh/src/tunnel.ts+52−24
@@ -5,6 +5,8 @@ import type { · 4 unmodified lines
5import * as NetService from "@t3tools/shared/Net";5import * as NetService from "@t3tools/shared/Net";
6import { extractJsonObject, fromLenientJson } from "@t3tools/shared/schemaJson";6import { extractJsonObject, fromLenientJson } from "@t3tools/shared/schemaJson";
7import { satisfiesSemverRange } from "@t3tools/shared/semver";7import { satisfiesSemverRange } from "@t3tools/shared/semver";
8import { getUrlDiagnostics } from "@t3tools/shared/urlDiagnostics";
9import * as Cause from "effect/Cause";
8import * as Context from "effect/Context";10import * as Context from "effect/Context";
9import * as Deferred from "effect/Deferred";11import * as Deferred from "effect/Deferred";
10import * as Duration from "effect/Duration";12import * as Duration from "effect/Duration";
@@ -243,7 +245,24 @@ function applyScriptPlaceholders( · 232 unmodified lines
243 return result;245 return result;
244}246}
245 247
246export function describeReadinessCause(cause: unknown): unknown {248const SSH_FAILURE_DIAGNOSTIC_DEPTH = 6;
249
250function describeReadinessCauseAtDepth(cause: unknown, depth: number): unknown {
251 if (depth >= SSH_FAILURE_DIAGNOSTIC_DEPTH) return { truncated: true };
252 const describeNested = (nested: unknown) => describeReadinessCauseAtDepth(nested, depth + 1);
253 if (Cause.isCause(cause)) {
254 return {
255 reasons: cause.reasons.map((reason) => {
256 if (Cause.isFailReason(reason)) {
257 return { _tag: reason._tag, error: describeNested(reason.error) };
258 }
259 if (Cause.isDieReason(reason)) {
260 return { _tag: reason._tag, defect: describeNested(reason.defect) };
261 }
262 return { _tag: reason._tag, fiberId: reason.fiberId };
263 }),
264 };
265 }
247 if (isSshReadinessError(cause)) {266 if (isSshReadinessError(cause)) {
248 const { cause: nestedCause, ...attributes } = cause as SshReadinessError & {267 const { cause: nestedCause, ...attributes } = cause as SshReadinessError & {
249 readonly cause?: unknown;268 readonly cause?: unknown;
@@ -251,13 +270,15 @@ export function describeReadinessCause(cause: unknown): unknown { · 1 unmodified line
251 return {270 return {
252 ...attributes,271 ...attributes,
253 message: cause.message,272 message: cause.message,
254 ...(nestedCause === undefined ? {} : { cause: describeReadinessCause(nestedCause) }),273 ...(nestedCause === undefined ? {} : { cause: describeNested(nestedCause) }),
255 };274 };
256 }275 }
257 if (cause instanceof Error) {276 if (cause instanceof Error) {
277 const tagged = cause as Error & { readonly _tag?: unknown; readonly reason?: unknown };
258 return {278 return {
259 name: cause.name,279 ...(typeof tagged._tag === "string" ? { _tag: tagged._tag } : { name: cause.name }),
260 ...(cause.cause === undefined ? {} : { cause: describeReadinessCause(cause.cause) }),280 ...(tagged.reason === undefined ? {} : { reason: describeNested(tagged.reason) }),
281 ...(cause.cause === undefined ? {} : { cause: describeNested(cause.cause) }),
261 };282 };
262 }283 }
263 if (typeof cause !== "object" || cause === null) {284 if (typeof cause !== "object" || cause === null) {
@@ -267,17 +288,23 @@ export function describeReadinessCause(cause: unknown): unknown { · 3 unmodified lines
267 const record = cause as Readonly<Record<string, unknown>>;288 const record = cause as Readonly<Record<string, unknown>>;
268 return {289 return {
269 ...(typeof record._tag === "string" ? { _tag: record._tag } : {}),290 ...(typeof record._tag === "string" ? { _tag: record._tag } : {}),
270 ...(record.reason === undefined ? {} : { reason: describeReadinessCause(record.reason) }),291 ...(record.reason === undefined ? {} : { reason: describeNested(record.reason) }),
271 ...(record.cause === undefined ? {} : { cause: describeReadinessCause(record.cause) }),292 ...(record.cause === undefined ? {} : { cause: describeNested(record.cause) }),
272 };293 };
273}294}
274 295
275function readinessUrlDiagnostics(url: URL, urlLength: number) {296export function describeReadinessCause(cause: unknown): unknown {
297 return describeReadinessCauseAtDepth(cause, 0);
298}
299
300function readinessUrlDiagnostics(input: string) {
301 const url = new URL(input);
302 const diagnostics = getUrlDiagnostics(input);
276 return {303 return {
277 protocol: url.protocol,304 protocol: diagnostics.protocol ?? url.protocol,
278 hostname: url.hostname,305 hostname: diagnostics.hostname ?? url.hostname,
279 ...(url.port === "" ? {} : { port: url.port }),306 ...(url.port === "" ? {} : { port: url.port }),
280 urlLength,307 urlLength: diagnostics.inputLength,
281 pathnameLength: utf8ByteLength(url.pathname),308 pathnameLength: utf8ByteLength(url.pathname),
282 hasQuery: url.search !== "",309 hasQuery: url.search !== "",
283 hasFragment: url.hash !== "",310 hasFragment: url.hash !== "",
@@ -900,8 +927,8 @@ export const waitForHttpReady = Effect.fn("ssh/tunnel.waitForHttpReady")(functio · 616 unmodified lines
900 const baseUrl = new URL(input.baseUrl);927 const baseUrl = new URL(input.baseUrl);
901 const request = new URL(input.path ?? "/", baseUrl);928 const request = new URL(input.path ?? "/", baseUrl);
902 const requestUrl = request.toString();929 const requestUrl = request.toString();
903 const base = readinessUrlDiagnostics(baseUrl, utf8ByteLength(input.baseUrl));930 const base = readinessUrlDiagnostics(input.baseUrl);
904 const requestDiagnostics = readinessUrlDiagnostics(request, utf8ByteLength(requestUrl));931 const requestDiagnostics = readinessUrlDiagnostics(requestUrl);
905 const client = yield* HttpClient.HttpClient;932 const client = yield* HttpClient.HttpClient;
906 const lastProbeFailure = yield* Ref.make<unknown>(null);933 const lastProbeFailure = yield* Ref.make<unknown>(null);
907 let attempt = 0;934 let attempt = 0;
@@ -1088,6 +1115,7 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input: · 180 unmodified lines
1088 const sshCommand = yield* resolveSshCommand;1115 const sshCommand = yield* resolveSshCommand;
1089 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;1116 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
1090 const scope = yield* Scope.Scope;1117 const scope = yield* Scope.Scope;
1118 const httpBaseUrlDiagnostics = getUrlDiagnostics(input.httpBaseUrl);
1091 yield* Effect.logDebug("ssh.tunnel.spawn.start", {1119 yield* Effect.logDebug("ssh.tunnel.spawn.start", {
1092 ...sshTargetLogFields(input.resolvedTarget),1120 ...sshTargetLogFields(input.resolvedTarget),
1093 command: sshCommand,1121 command: sshCommand,
@@ -1095,7 +1123,7 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input: · 1 unmodified line
1095 localPort: input.localPort,1123 localPort: input.localPort,
1096 remotePort: input.remotePort,1124 remotePort: input.remotePort,
1097 remoteServerKind: input.remoteServerKind,1125 remoteServerKind: input.remoteServerKind,
1098 httpBaseUrl: input.httpBaseUrl,1126 httpBaseUrlDiagnostics,
1099 });1127 });
1100 const child = yield* spawner1128 const child = yield* spawner
1101 .spawn(1129 .spawn(
@@ -1128,7 +1156,7 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input: · 26 unmodified lines
1128 pid: child.pid,1156 pid: child.pid,
1129 localPort: input.localPort,1157 localPort: input.localPort,
1130 remotePort: input.remotePort,1158 remotePort: input.remotePort,
1131 httpBaseUrl: input.httpBaseUrl,1159 httpBaseUrlDiagnostics,
1132 });1160 });
1133 const tunnelEntry: SshTunnelEntry = {1161 const tunnelEntry: SshTunnelEntry = {
1134 key: input.key,1162 key: input.key,
@@ -1171,7 +1199,7 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input: · 36 unmodified lines
1171 pid: child.pid,1199 pid: child.pid,
1172 localPort: input.localPort,1200 localPort: input.localPort,
1173 remotePort: input.remotePort,1201 remotePort: input.remotePort,
1174 httpBaseUrl: input.httpBaseUrl,1202 httpBaseUrlDiagnostics,
1175 exitCode,1203 exitCode,
1176 stderrBytes: utf8ByteLength(stderr),1204 stderrBytes: utf8ByteLength(stderr),
1177 }).pipe(Effect.andThen(Effect.fail(error)));1205 }).pipe(Effect.andThen(Effect.fail(error)));
@@ -1192,7 +1220,7 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input: · 14 unmodified lines
1192 pid: child.pid,1220 pid: child.pid,
1193 localPort: input.localPort,1221 localPort: input.localPort,
1194 remotePort: input.remotePort,1222 remotePort: input.remotePort,
1195 httpBaseUrl: input.httpBaseUrl,1223 httpBaseUrlDiagnostics,
1196 }),1224 }),
1197 ),1225 ),
1198 Effect.tapError((cause) =>1226 Effect.tapError((cause) =>
@@ -1220,19 +1248,19 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input: · 21 unmodified lines
1220 processRunning,1248 processRunning,
1221 ...(Exit.isSuccess(processRunningExit)1249 ...(Exit.isSuccess(processRunningExit)
1222 ? {}1250 ? {}
1223 : { processRunningError: processRunningExit.cause }),1251 : { processRunningFailure: describeReadinessCause(processRunningExit.cause) }),
1224 localPort: input.localPort,1252 localPort: input.localPort,
1225 localPortListening: localPortAvailable === null ? null : !localPortAvailable,1253 localPortListening: localPortAvailable === null ? null : !localPortAvailable,
1226 remotePort: input.remotePort,1254 remotePort: input.remotePort,
1227 httpBaseUrl: input.httpBaseUrl,1255 httpBaseUrlDiagnostics,
1228 ...(Exit.isSuccess(localPortAvailableExit)1256 ...(Exit.isSuccess(localPortAvailableExit)
1229 ? {}1257 ? {}
1230 : { localPortProbeError: localPortAvailableExit.cause }),1258 : { localPortProbeFailure: describeReadinessCause(localPortAvailableExit.cause) }),
1231 ...(remoteLogTail === null ? {} : { remoteLogTailBytes: utf8ByteLength(remoteLogTail) }),1259 ...(remoteLogTail === null ? {} : { remoteLogTailBytes: utf8ByteLength(remoteLogTail) }),
1232 ...(Exit.isSuccess(remoteLogTailExit)1260 ...(Exit.isSuccess(remoteLogTailExit)
1233 ? {}1261 ? {}
1234 : { remoteLogTailError: remoteLogTailExit.cause }),1262 : { remoteLogTailFailure: describeReadinessCause(remoteLogTailExit.cause) }),
1235 cause,1263 failure: describeReadinessCause(cause),
1236 });1264 });
1237 }),1265 }),
1238 ),1266 ),
@@ -1370,7 +1398,7 @@ export const make = Effect.fn("ssh/tunnel.SshEnvironmentManager.make")(function* · 131 unmodified lines
1370 ...sshTargetLogFields(input.target),1398 ...sshTargetLogFields(input.target),
1371 key: input.key,1399 key: input.key,
1372 promptCount: input.promptCount,1400 promptCount: input.promptCount,
1373 cause: input.error,1401 failure: describeReadinessCause(input.error),
1374 });1402 });
1375 const promptService = yield* SshAuth.SshPasswordPrompt;1403 const promptService = yield* SshAuth.SshPasswordPrompt;
1376 if (!promptService.isAvailable) {1404 if (!promptService.isAvailable) {
@@ -1569,7 +1597,7 @@ export const make = Effect.fn("ssh/tunnel.SshEnvironmentManager.make")(function* · 192 unmodified lines
1569 key,1597 key,
1570 localPort: entry.localPort,1598 localPort: entry.localPort,
1571 remotePort: entry.remotePort,1599 remotePort: entry.remotePort,
1572 cause: readinessExit.cause,1600 failure: describeReadinessCause(readinessExit.cause),
1573 });1601 });
1574 yield* closeTunnelEntry(entry);1602 yield* closeTunnelEntry(entry);
1575 yield* cancelPendingTunnelEntry(key, resolvedTarget);1603 yield* cancelPendingTunnelEntry(key, resolvedTarget);
@@ -1597,7 +1625,7 @@ export const make = Effect.fn("ssh/tunnel.SshEnvironmentManager.make")(function* · 21 unmodified lines
1597 Effect.logWarning("ssh.environment.tunnel.create.failed", {1625 Effect.logWarning("ssh.environment.tunnel.create.failed", {
1598 ...sshTargetLogFields(resolvedTarget),1626 ...sshTargetLogFields(resolvedTarget),
1599 key,1627 key,
1600 cause,1628 failure: describeReadinessCause(cause),
1601 }),1629 }),
1602 ),1630 ),
1603 Effect.onExit((exit) =>1631 Effect.onExit((exit) =>

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t