| @@ -20,7 +20,26 @@ const DEFAULT_SSH_PASSWORD_PROMPT_TIMEOUT_MS = 3 * 60 * 1000; · 19 unmodified lines |
| 20 | 20 | type DesktopSshPasswordPromptResolutionInput = |
| 21 | 21 | typeof DesktopSshPasswordPromptResolutionInputSchema.Type; |
| 22 | 22 | |
| 23 | | −const WINDOW_UNAVAILABLE_MESSAGE = "T3 Code window is not available for SSH authentication."; |
| 23 | +const DesktopSshPromptWindowAvailabilityStage = Schema.Literals([ |
| 24 | + "before-request", |
| 25 | + "before-presentation", |
| 26 | + "after-send", |
| 27 | + "after-restore", |
| 28 | +]); |
| 29 | + |
| 30 | +const DesktopSshPromptPresentationOperation = Schema.Literals([ |
| 31 | + "check-window-before-request", |
| 32 | + "check-window-before-presentation", |
| 33 | + "register-window-close-listener", |
| 34 | + "send-prompt-request", |
| 35 | + "check-window-after-send", |
| 36 | + "check-window-minimized", |
| 37 | + "restore-window", |
| 38 | + "check-window-after-restore", |
| 39 | + "focus-window", |
| 40 | + "remove-window-close-listener", |
| 41 | +]); |
| 42 | +type DesktopSshPromptPresentationOperation = typeof DesktopSshPromptPresentationOperation.Type; |
| 24 | 43 | |
| 25 | 44 | export class DesktopSshPromptRequestIdGenerationError extends Schema.TaggedErrorClass<DesktopSshPromptRequestIdGenerationError>()( |
| 26 | 45 | "DesktopSshPromptRequestIdGenerationError", |
| @@ -38,20 +57,22 @@ export class DesktopSshPromptWindowUnavailableError extends Schema.TaggedErrorCl · 11 unmodified lines |
| 38 | 57 | "DesktopSshPromptWindowUnavailableError", |
| 39 | 58 | { |
| 40 | 59 | destination: Schema.String, |
| 60 | + requestId: Schema.NullOr(Schema.String), |
| 61 | + stage: DesktopSshPromptWindowAvailabilityStage, |
| 41 | 62 | }, |
| 42 | 63 | ) { |
| 43 | 64 | override get message(): string { |
| 44 | | − return WINDOW_UNAVAILABLE_MESSAGE; |
| 65 | + const request = this.requestId === null ? "before a request id was assigned" : this.requestId; |
| 66 | + return `T3 Code window is unavailable during ${this.stage} for SSH authentication to ${this.destination} (request: ${request}).`; |
| 45 | 67 | } |
| 46 | 68 | } |
| 47 | 69 | |
| 48 | | −const isDesktopSshPromptWindowUnavailableError = Schema.is(DesktopSshPromptWindowUnavailableError); |
| 49 | | − |
| 50 | 70 | export class DesktopSshPromptPresentationError extends Schema.TaggedErrorClass<DesktopSshPromptPresentationError>()( |
| 51 | 71 | "DesktopSshPromptPresentationError", |
| 52 | 72 | { |
| 53 | | − requestId: Schema.String, |
| 73 | + requestId: Schema.NullOr(Schema.String), |
| 54 | 74 | destination: Schema.String, |
| 75 | + operation: DesktopSshPromptPresentationOperation, |
| 55 | 76 | cause: Schema.Defect(), |
| 56 | 77 | }, |
| 57 | 78 | ) { |
| @@ -263,9 +284,29 @@ export const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* ( · 205 unmodified lines |
| 263 | 284 | "desktop.sshPasswordPrompts.request", |
| 264 | 285 | )(function* (input) { |
| 265 | 286 | const window = yield* electronWindow.main; |
| 266 | | − if (Option.isNone(window) || window.value.isDestroyed()) { |
| 287 | + if (Option.isNone(window)) { |
| 267 | 288 | return yield* new DesktopSshPromptWindowUnavailableError({ |
| 268 | 289 | destination: input.destination, |
| 290 | + requestId: null, |
| 291 | + stage: "before-request", |
| 292 | + }); |
| 293 | + } |
| 294 | + |
| 295 | + const unavailableBeforeRequest = yield* Effect.try({ |
| 296 | + try: () => window.value.isDestroyed(), |
| 297 | + catch: (cause) => |
| 298 | + new DesktopSshPromptPresentationError({ |
| 299 | + requestId: null, |
| 300 | + destination: input.destination, |
| 301 | + operation: "check-window-before-request", |
| 302 | + cause, |
| 303 | + }), |
| 304 | + }); |
| 305 | + if (unavailableBeforeRequest) { |
| 306 | + return yield* new DesktopSshPromptWindowUnavailableError({ |
| 307 | + destination: input.destination, |
| 308 | + requestId: null, |
| 309 | + stage: "before-request", |
| 269 | 310 | }); |
| 270 | 311 | } |
| 271 | 312 | |
| @@ -319,11 +360,25 @@ export const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* ( · 47 unmodified lines |
| 319 | 360 | ), |
| 320 | 361 | ); |
| 321 | 362 | }; |
| 322 | | − const cleanup = Effect.sync(() => { |
| 363 | + const runPresentationOperation = <A>( |
| 364 | + operation: DesktopSshPromptPresentationOperation, |
| 365 | + evaluate: () => A, |
| 366 | + ) => |
| 367 | + Effect.try({ |
| 368 | + try: evaluate, |
| 369 | + catch: (cause) => |
| 370 | + new DesktopSshPromptPresentationError({ |
| 371 | + requestId, |
| 372 | + destination: input.destination, |
| 373 | + operation, |
| 374 | + cause, |
| 375 | + }), |
| 376 | + }); |
| 377 | + const cleanup = runPresentationOperation("remove-window-close-listener", () => { |
| 323 | 378 | if (!window.value.isDestroyed()) { |
| 324 | 379 | window.value.removeListener("closed", cancelOnWindowClosed); |
| 325 | 380 | } |
| 326 | | − }).pipe(Effect.andThen(removePending(pendingRef, requestId)), Effect.asVoid); |
| 381 | + }).pipe(Effect.orDie, Effect.ensuring(removePending(pendingRef, requestId)), Effect.asVoid); |
| 327 | 382 | const waitForPassword = Deferred.await(deferred).pipe( |
| 328 | 383 | Effect.timeoutOption(Duration.millis(passwordPromptTimeoutMs)), |
| 329 | 384 | Effect.flatMap( |
| @@ -339,40 +394,73 @@ export const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* ( · 9 unmodified lines |
| 339 | 394 | }), |
| 340 | 395 | ), |
| 341 | 396 | ); |
| 397 | + const preferSubmittedPassword = (error: DesktopSshPasswordPromptRequestError) => |
| 398 | + Deferred.poll(deferred).pipe( |
| 399 | + Effect.flatMap( |
| 400 | + Option.match({ |
| 401 | + onSome: (completion) => completion, |
| 402 | + onNone: () => |
| 403 | + Ref.get(pendingRef).pipe( |
| 404 | + Effect.flatMap((entries) => |
| 405 | + entries.has(requestId) ? Effect.fail(error) : Deferred.await(deferred), |
| 406 | + ), |
| 407 | + ), |
| 408 | + }), |
| 409 | + ), |
| 410 | + ); |
| 342 | 411 | |
| 343 | | − return yield* Effect.try({ |
| 344 | | − try: () => { |
| 345 | | − if (window.value.isDestroyed()) { |
| 346 | | − throw new DesktopSshPromptWindowUnavailableError({ |
| 347 | | − destination: input.destination, |
| 348 | | − }); |
| 349 | | − } |
| 350 | | − window.value.once("closed", cancelOnWindowClosed); |
| 351 | | − window.value.webContents.send(SSH_PASSWORD_PROMPT_CHANNEL, promptRequest); |
| 352 | | − if (window.value.isDestroyed()) { |
| 353 | | − throw new DesktopSshPromptWindowUnavailableError({ |
| 412 | + return yield* Effect.gen(function* () { |
| 413 | + const unavailableBeforePresentation = yield* runPresentationOperation( |
| 414 | + "check-window-before-presentation", |
| 415 | + () => window.value.isDestroyed(), |
| 416 | + ); |
| 417 | + if (unavailableBeforePresentation) { |
| 418 | + return yield* new DesktopSshPromptWindowUnavailableError({ |
| 419 | + destination: input.destination, |
| 420 | + requestId, |
| 421 | + stage: "before-presentation", |
| 422 | + }); |
| 423 | + } |
| 424 | + yield* runPresentationOperation("register-window-close-listener", () => |
| 425 | + window.value.once("closed", cancelOnWindowClosed), |
| 426 | + ); |
| 427 | + return yield* Effect.gen(function* () { |
| 428 | + yield* runPresentationOperation("send-prompt-request", () => |
| 429 | + window.value.webContents.send(SSH_PASSWORD_PROMPT_CHANNEL, promptRequest), |
| 430 | + ); |
| 431 | + yield* Effect.yieldNow; |
| 432 | + const unavailableAfterSend = yield* runPresentationOperation( |
| 433 | + "check-window-after-send", |
| 434 | + () => window.value.isDestroyed(), |
| 435 | + ); |
| 436 | + if (unavailableAfterSend) { |
| 437 | + return yield* new DesktopSshPromptWindowUnavailableError({ |
| 354 | 438 | destination: input.destination, |
| 439 | + requestId, |
| 440 | + stage: "after-send", |
| 355 | 441 | }); |
| 356 | 442 | } |
| 357 | | − if (window.value.isMinimized()) { |
| 358 | | − window.value.restore(); |
| 443 | + const minimized = yield* runPresentationOperation("check-window-minimized", () => |
| 444 | + window.value.isMinimized(), |
| 445 | + ); |
| 446 | + if (minimized) { |
| 447 | + yield* runPresentationOperation("restore-window", () => window.value.restore()); |
| 359 | 448 | } |
| 360 | | − if (window.value.isDestroyed()) { |
| 361 | | − throw new DesktopSshPromptWindowUnavailableError({ |
| 449 | + const unavailableAfterRestore = yield* runPresentationOperation( |
| 450 | + "check-window-after-restore", |
| 451 | + () => window.value.isDestroyed(), |
| 452 | + ); |
| 453 | + if (unavailableAfterRestore) { |
| 454 | + return yield* new DesktopSshPromptWindowUnavailableError({ |
| 362 | 455 | destination: input.destination, |
| 456 | + requestId, |
| 457 | + stage: "after-restore", |
| 363 | 458 | }); |
| 364 | 459 | } |
| 365 | | − window.value.focus(); |
| 366 | | − }, |
| 367 | | − catch: (cause) => |
| 368 | | − isDesktopSshPromptWindowUnavailableError(cause) |
| 369 | | − ? cause |
| 370 | | − : new DesktopSshPromptPresentationError({ |
| 371 | | − requestId, |
| 372 | | − destination: input.destination, |
| 373 | | − cause, |
| 374 | | − }), |
| 375 | | − }).pipe(Effect.andThen(waitForPassword), Effect.ensuring(cleanup)); |
| 460 | + yield* runPresentationOperation("focus-window", () => window.value.focus()); |
| 461 | + return yield* waitForPassword; |
| 462 | + }).pipe(Effect.catch(preferSubmittedPassword)); |
| 463 | + }).pipe(Effect.ensuring(cleanup)); |
| 376 | 464 | }); |
| 377 | 465 | |
| 378 | 466 | return DesktopSshPasswordPrompts.of({ |