| ... | @@ -20,7 +20,26 @@ const DEFAULT_SSH_PASSWORD_PROMPT_TIMEOUT_MS = 3 * 60 * 1000; | ... | @@ -20,7 +20,26 @@ const DEFAULT_SSH_PASSWORD_PROMPT_TIMEOUT_MS = 3 * 60 * 1000; |
| 20 | type DesktopSshPasswordPromptResolutionInput = | 20 | type DesktopSshPasswordPromptResolutionInput = |
| 21 | typeof DesktopSshPasswordPromptResolutionInputSchema.Type; | 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 | export class DesktopSshPromptRequestIdGenerationError extends Schema.TaggedErrorClass<DesktopSshPromptRequestIdGenerationError>()( | 44 | export class DesktopSshPromptRequestIdGenerationError extends Schema.TaggedErrorClass<DesktopSshPromptRequestIdGenerationError>()( |
| 26 | "DesktopSshPromptRequestIdGenerationError", | 45 | "DesktopSshPromptRequestIdGenerationError", |
| ... | @@ -38,20 +57,22 @@ export class DesktopSshPromptWindowUnavailableError extends Schema.TaggedErrorCl | ... | @@ -38,20 +57,22 @@ export class DesktopSshPromptWindowUnavailableError extends Schema.TaggedErrorCl |
| 38 | "DesktopSshPromptWindowUnavailableError", | 57 | "DesktopSshPromptWindowUnavailableError", |
| 39 | { | 58 | { |
| 40 | destination: Schema.String, | 59 | destination: Schema.String, |
| | 60 | requestId: Schema.NullOr(Schema.String), |
| | 61 | stage: DesktopSshPromptWindowAvailabilityStage, |
| 41 | }, | 62 | }, |
| 42 | ) { | 63 | ) { |
| 43 | override get message(): string { | 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 | export class DesktopSshPromptPresentationError extends Schema.TaggedErrorClass<DesktopSshPromptPresentationError>()( | 70 | export class DesktopSshPromptPresentationError extends Schema.TaggedErrorClass<DesktopSshPromptPresentationError>()( |
| 51 | "DesktopSshPromptPresentationError", | 71 | "DesktopSshPromptPresentationError", |
| 52 | { | 72 | { |
| 53 | requestId: Schema.String, | 73 | requestId: Schema.NullOr(Schema.String), |
| 54 | destination: Schema.String, | 74 | destination: Schema.String, |
| | 75 | operation: DesktopSshPromptPresentationOperation, |
| 55 | cause: Schema.Defect(), | 76 | cause: Schema.Defect(), |
| 56 | }, | 77 | }, |
| 57 | ) { | 78 | ) { |
| ... | @@ -263,9 +284,29 @@ export const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* ( | ... | @@ -263,9 +284,29 @@ export const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* ( |
| 263 | "desktop.sshPasswordPrompts.request", | 284 | "desktop.sshPasswordPrompts.request", |
| 264 | )(function* (input) { | 285 | )(function* (input) { |
| 265 | const window = yield* electronWindow.main; | 286 | const window = yield* electronWindow.main; |
| 266 | if (Option.isNone(window) || window.value.isDestroyed()) { | 287 | if (Option.isNone(window)) { |
| 267 | return yield* new DesktopSshPromptWindowUnavailableError({ | 288 | return yield* new DesktopSshPromptWindowUnavailableError({ |
| 268 | destination: input.destination, | 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,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 | if (!window.value.isDestroyed()) { | 378 | if (!window.value.isDestroyed()) { |
| 324 | window.value.removeListener("closed", cancelOnWindowClosed); | 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 | const waitForPassword = Deferred.await(deferred).pipe( | 382 | const waitForPassword = Deferred.await(deferred).pipe( |
| 328 | Effect.timeoutOption(Duration.millis(passwordPromptTimeoutMs)), | 383 | Effect.timeoutOption(Duration.millis(passwordPromptTimeoutMs)), |
| 329 | Effect.flatMap( | 384 | Effect.flatMap( |
| ... | @@ -339,40 +394,73 @@ export const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* ( | ... | @@ -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({ | 412 | return yield* Effect.gen(function* () { |
| 344 | try: () => { | 413 | const unavailableBeforePresentation = yield* runPresentationOperation( |
| 345 | if (window.value.isDestroyed()) { | 414 | "check-window-before-presentation", |
| 346 | throw new DesktopSshPromptWindowUnavailableError({ | 415 | () => window.value.isDestroyed(), |
| 347 | destination: input.destination, | 416 | ); |
| 348 | }); | 417 | if (unavailableBeforePresentation) { |
| 349 | } | 418 | return yield* new DesktopSshPromptWindowUnavailableError({ |
| 350 | window.value.once("closed", cancelOnWindowClosed); | 419 | destination: input.destination, |
| 351 | window.value.webContents.send(SSH_PASSWORD_PROMPT_CHANNEL, promptRequest); | 420 | requestId, |
| 352 | if (window.value.isDestroyed()) { | 421 | stage: "before-presentation", |
| 353 | throw new DesktopSshPromptWindowUnavailableError({ | 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 | destination: input.destination, | 438 | destination: input.destination, |
| | 439 | requestId, |
| | 440 | stage: "after-send", |
| 355 | }); | 441 | }); |
| 356 | } | 442 | } |
| 357 | if (window.value.isMinimized()) { | 443 | const minimized = yield* runPresentationOperation("check-window-minimized", () => |
| 358 | window.value.restore(); | 444 | window.value.isMinimized(), |
| | 445 | ); |
| | 446 | if (minimized) { |
| | 447 | yield* runPresentationOperation("restore-window", () => window.value.restore()); |
| 359 | } | 448 | } |
| 360 | if (window.value.isDestroyed()) { | 449 | const unavailableAfterRestore = yield* runPresentationOperation( |
| 361 | throw new DesktopSshPromptWindowUnavailableError({ | 450 | "check-window-after-restore", |
| | 451 | () => window.value.isDestroyed(), |
| | 452 | ); |
| | 453 | if (unavailableAfterRestore) { |
| | 454 | return yield* new DesktopSshPromptWindowUnavailableError({ |
| 362 | destination: input.destination, | 455 | destination: input.destination, |
| | 456 | requestId, |
| | 457 | stage: "after-restore", |
| 363 | }); | 458 | }); |
| 364 | } | 459 | } |
| 365 | window.value.focus(); | 460 | yield* runPresentationOperation("focus-window", () => window.value.focus()); |
| 366 | }, | 461 | return yield* waitForPassword; |
| 367 | catch: (cause) => | 462 | }).pipe(Effect.catch(preferSubmittedPassword)); |
| 368 | isDesktopSshPromptWindowUnavailableError(cause) | 463 | }).pipe(Effect.ensuring(cleanup)); |
| 369 | ? cause | | |
| 370 | : new DesktopSshPromptPresentationError({ | | |
| 371 | requestId, | | |
| 372 | destination: input.destination, | | |
| 373 | cause, | | |
| 374 | }), | | |
| 375 | }).pipe(Effect.andThen(waitForPassword), Effect.ensuring(cleanup)); | | |
| 376 | }); | 464 | }); |
| 377 | | 465 | |
| 378 | return DesktopSshPasswordPrompts.of({ | 466 | return DesktopSshPasswordPrompts.of({ |