| ... | ... | @@ -20,7 +20,26 @@ const DEFAULT_SSH_PASSWORD_PROMPT_TIMEOUT_MS = 3 * 60 * 1000; |
| 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 |
| 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* ( |
| 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* ( |
| 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* ( |
| 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({ |