-
Notifications
You must be signed in to change notification settings - Fork 2.1k
port desktop app to Effect #2546
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 41 commits
Commits
Show all changes
44 commits
Select commit
Hold shift + click to select a range
aa9dd25
Port desktop backend readiness checks to Effect
juliusmarminge 4d3358e
Fix timeout silently succeeding in waitForHttpReadyEffect
cursoragent 167afc1
nit
juliusmarminge f5a6173
Refactor desktop backend startup into Effect process
juliusmarminge 91c982f
Refactor desktop runtime to Effect
juliusmarminge f9000d4
Refactor desktop shell env sync into Effect service
juliusmarminge 9e2d64c
Refactor desktop IPC into shared handlers
juliusmarminge 0f5850d
Refactor desktop SSH IPC handlers
juliusmarminge 8e162b5
Centralize desktop window and quitting state
juliusmarminge adfeee0
Refactor desktop Electron IPC into shared services
juliusmarminge 5461149
Refactor desktop Electron services
juliusmarminge a90619e
Refactor desktop update handling into dedicated services
juliusmarminge d9435a0
Refactor desktop server exposure into scoped service
juliusmarminge 3f82d86
Split desktop SSH handling into dedicated services
juliusmarminge 0f9fb93
Refactor desktop window, theme, and updater services
juliusmarminge 4ecf54a
Refactor desktop app into main-layer modules
juliusmarminge ac4ff5e
Refactor desktop IPC to use main service layers
juliusmarminge a8b9c4b
Use default update channel for desktop update filtering
juliusmarminge a122a35
Namespace preload IPC channel imports
juliusmarminge f0c204d
Refactor desktop IPC methods to use channel namespace imports
juliusmarminge e4c0d77
Reorganize desktop app modules by domain
juliusmarminge 9fb7747
Use desktop server exposure domain folder
juliusmarminge 5a3e6b1
Refine desktop error handling and Effect imports
juliusmarminge ffc7c77
Refactor contracts to use Effect subpath imports
juliusmarminge 244e066
Normalize Effect imports across shared packages
juliusmarminge f854da1
Reuse effect context for SSH password prompt cleanup
juliusmarminge b07e626
Tighten Effect tsconfig diagnostics
juliusmarminge efa529d
Refactor desktop bootstrap around direct environment inputs
juliusmarminge a59ee63
Handle desktop secret decode and window load errors
juliusmarminge ec26ca7
Inline SSH password cancel IPC result
juliusmarminge 354c05f
Normalize SSH password prompt cancelled IPC result
juliusmarminge 02fa783
Harden desktop backend restart and update handling
juliusmarminge 0ee84dc
Refactor desktop settings into dedicated services
juliusmarminge 7bdfc8c
Fix rebased desktop bootstrap config
juliusmarminge ccee7ce
Simplify desktop shutdown and timeout handling
juliusmarminge daf956b
Refactor desktop logging around run annotations
juliusmarminge d008e09
Probe backend readiness on environment endpoint
juliusmarminge 413d2f9
Improve desktop startup logging and window readiness
juliusmarminge fe6d0ee
Consolidate desktop and server observability layers
juliusmarminge 79b3420
Cancel desktop backend restart when stopped
juliusmarminge 6937dab
Add span tracing to desktop startup and lifecycle
juliusmarminge e8344e5
Merge origin/main into desktop effect rewrite
juliusmarminge dc3aa27
Skip destroyed Electron windows during appearance sync
juliusmarminge 35835a7
Unify desktop component logging
juliusmarminge File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,3 +23,5 @@ __screenshots__/ | |
| .tanstack | ||
| squashfs-root/ | ||
| .vercel | ||
| dist-electron/ | ||
| .electron-runtime/ | ||
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,238 @@ | ||
| import * as Cause from "effect/Cause"; | ||
| import * as Data from "effect/Data"; | ||
| import * as Effect from "effect/Effect"; | ||
| import * as Option from "effect/Option"; | ||
| import * as Random from "effect/Random"; | ||
| import * as Ref from "effect/Ref"; | ||
|
|
||
| import * as NetService from "@t3tools/shared/Net"; | ||
| import * as ElectronApp from "../electron/ElectronApp.ts"; | ||
| import * as ElectronDialog from "../electron/ElectronDialog.ts"; | ||
| import * as ElectronProtocol from "../electron/ElectronProtocol.ts"; | ||
| import { installDesktopIpcHandlers } from "../ipc/DesktopIpcHandlers.ts"; | ||
| import * as DesktopAppIdentity from "./DesktopAppIdentity.ts"; | ||
| import * as DesktopApplicationMenu from "../window/DesktopApplicationMenu.ts"; | ||
| import * as DesktopBackendManager from "../backend/DesktopBackendManager.ts"; | ||
| import * as DesktopEnvironment from "./DesktopEnvironment.ts"; | ||
| import * as DesktopLifecycle from "./DesktopLifecycle.ts"; | ||
| import * as DesktopServerExposure from "../serverExposure/DesktopServerExposure.ts"; | ||
| import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts"; | ||
| import * as DesktopShellEnvironment from "../shell/DesktopShellEnvironment.ts"; | ||
| import * as DesktopState from "./DesktopState.ts"; | ||
| import * as DesktopUpdates from "../updates/DesktopUpdates.ts"; | ||
|
|
||
| const DEFAULT_DESKTOP_BACKEND_PORT = 3773; | ||
| const MAX_TCP_PORT = 65_535; | ||
| const DESKTOP_BACKEND_PORT_PROBE_HOSTS = ["127.0.0.1", "0.0.0.0", "::"] as const; | ||
|
|
||
| const makeDesktopRunId = Random.nextUUIDv4.pipe( | ||
| Effect.map((value) => value.replaceAll("-", "").slice(0, 12)), | ||
| ); | ||
|
|
||
| class DesktopBackendPortUnavailableError extends Data.TaggedError( | ||
| "DesktopBackendPortUnavailableError", | ||
| )<{ | ||
| readonly startPort: number; | ||
| readonly maxPort: number; | ||
| readonly hosts: readonly string[]; | ||
| }> { | ||
| override get message() { | ||
| return `No desktop backend port is available on hosts ${this.hosts.join(", ")} between ${this.startPort} and ${this.maxPort}.`; | ||
| } | ||
| } | ||
|
|
||
| class DesktopDevelopmentBackendPortRequiredError extends Data.TaggedError( | ||
| "DesktopDevelopmentBackendPortRequiredError", | ||
| )<{}> { | ||
| override get message() { | ||
| return "T3CODE_PORT is required in desktop development."; | ||
| } | ||
| } | ||
|
|
||
| const resolveDesktopBackendPort = Effect.fn("resolveDesktopBackendPort")(function* ( | ||
| configuredPort: Option.Option<number>, | ||
| ) { | ||
| if (Option.isSome(configuredPort)) { | ||
| return { | ||
| port: configuredPort.value, | ||
| selectedByScan: false, | ||
| } as const; | ||
| } | ||
|
|
||
| const net = yield* NetService.NetService; | ||
| for (let port = DEFAULT_DESKTOP_BACKEND_PORT; port <= MAX_TCP_PORT; port += 1) { | ||
| let availableOnEveryHost = true; | ||
|
|
||
| for (const host of DESKTOP_BACKEND_PORT_PROBE_HOSTS) { | ||
| if (!(yield* net.canListenOnHost(port, host))) { | ||
| availableOnEveryHost = false; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (availableOnEveryHost) { | ||
| return { | ||
| port, | ||
| selectedByScan: true, | ||
| } as const; | ||
| } | ||
| } | ||
|
|
||
| return yield* new DesktopBackendPortUnavailableError({ | ||
| startPort: DEFAULT_DESKTOP_BACKEND_PORT, | ||
| maxPort: MAX_TCP_PORT, | ||
| hosts: DESKTOP_BACKEND_PORT_PROBE_HOSTS, | ||
| }); | ||
| }); | ||
|
|
||
| const handleFatalStartupError = Effect.fn("desktop.startup.handleFatalStartupError")(function* ( | ||
| stage: string, | ||
| error: unknown, | ||
| ): Effect.fn.Return< | ||
| void, | ||
| never, | ||
| | DesktopLifecycle.DesktopShutdown | ||
| | DesktopState.DesktopState | ||
| | ElectronApp.ElectronApp | ||
| | ElectronDialog.ElectronDialog | ||
| > { | ||
| const shutdown = yield* DesktopLifecycle.DesktopShutdown; | ||
| const state = yield* DesktopState.DesktopState; | ||
| const electronApp = yield* ElectronApp.ElectronApp; | ||
| const electronDialog = yield* ElectronDialog.ElectronDialog; | ||
| const message = error instanceof Error ? error.message : String(error); | ||
| const detail = | ||
| error instanceof Error && typeof error.stack === "string" ? `\n${error.stack}` : ""; | ||
| yield* Effect.logError("fatal startup error").pipe( | ||
| Effect.annotateLogs({ | ||
| stage, | ||
| message, | ||
| ...(detail.length > 0 ? { detail } : {}), | ||
| }), | ||
| ); | ||
| const wasQuitting = yield* Ref.getAndSet(state.quitting, true); | ||
| if (!wasQuitting) { | ||
| yield* electronDialog.showErrorBox( | ||
| "T3 Code failed to start", | ||
| `Stage: ${stage}\n${message}${detail}`, | ||
| ); | ||
| } | ||
| yield* shutdown.request; | ||
| yield* electronApp.quit; | ||
| }); | ||
|
|
||
| const fatalStartupCause = <E>(stage: string, cause: Cause.Cause<E>) => | ||
| handleFatalStartupError(stage, Cause.pretty(cause)).pipe(Effect.andThen(Effect.failCause(cause))); | ||
|
|
||
| const bootstrap = Effect.gen(function* () { | ||
| const backendManager = yield* DesktopBackendManager.DesktopBackendManager; | ||
| const state = yield* DesktopState.DesktopState; | ||
| const environment = yield* DesktopEnvironment.DesktopEnvironment; | ||
| const desktopSettings = yield* DesktopAppSettings.DesktopAppSettings; | ||
| const serverExposure = yield* DesktopServerExposure.DesktopServerExposure; | ||
| yield* Effect.logInfo("bootstrap start"); | ||
|
|
||
| if (environment.isDevelopment && Option.isNone(environment.configuredBackendPort)) { | ||
| return yield* new DesktopDevelopmentBackendPortRequiredError(); | ||
| } | ||
|
|
||
| const backendPortSelection = yield* resolveDesktopBackendPort(environment.configuredBackendPort); | ||
| const backendPort = backendPortSelection.port; | ||
| yield* Effect.logInfo( | ||
| backendPortSelection.selectedByScan | ||
| ? "selected backend port via sequential scan" | ||
| : "using configured backend port", | ||
| ).pipe( | ||
| Effect.annotateLogs({ | ||
| port: backendPort, | ||
| ...(backendPortSelection.selectedByScan ? { startPort: DEFAULT_DESKTOP_BACKEND_PORT } : {}), | ||
| }), | ||
| ); | ||
|
|
||
| const settings = yield* desktopSettings.get; | ||
| if (settings.serverExposureMode !== environment.defaultDesktopSettings.serverExposureMode) { | ||
| yield* Effect.logInfo("bootstrap restoring persisted server exposure mode").pipe( | ||
| Effect.annotateLogs({ mode: settings.serverExposureMode }), | ||
| ); | ||
| } | ||
| const serverExposureState = yield* serverExposure.configureFromSettings({ port: backendPort }); | ||
| const backendConfig = yield* serverExposure.backendConfig; | ||
| yield* Effect.logInfo("bootstrap resolved backend endpoint").pipe( | ||
| Effect.annotateLogs({ baseUrl: backendConfig.httpBaseUrl.href }), | ||
| ); | ||
| if (serverExposureState.endpointUrl) { | ||
| yield* Effect.logInfo("bootstrap enabled network access").pipe( | ||
| Effect.annotateLogs({ endpointUrl: serverExposureState.endpointUrl }), | ||
| ); | ||
| } else if (settings.serverExposureMode === "network-accessible") { | ||
| yield* Effect.logWarning( | ||
| "bootstrap fell back to local-only because no advertised network host was available", | ||
| ); | ||
| } | ||
|
|
||
| yield* installDesktopIpcHandlers; | ||
| yield* Effect.logInfo("bootstrap ipc handlers registered"); | ||
|
|
||
| if (!(yield* Ref.get(state.quitting))) { | ||
| yield* backendManager.start; | ||
| yield* Effect.logInfo("bootstrap backend start requested"); | ||
| } | ||
| }).pipe(Effect.withSpan("desktop.bootstrap")); | ||
|
|
||
| const startup = Effect.gen(function* () { | ||
| const appIdentity = yield* DesktopAppIdentity.DesktopAppIdentity; | ||
| const applicationMenu = yield* DesktopApplicationMenu.DesktopApplicationMenu; | ||
| const electronApp = yield* ElectronApp.ElectronApp; | ||
| const electronProtocol = yield* ElectronProtocol.ElectronProtocol; | ||
| const lifecycle = yield* DesktopLifecycle.DesktopLifecycle; | ||
| const shellEnvironment = yield* DesktopShellEnvironment.DesktopShellEnvironment; | ||
| const desktopSettings = yield* DesktopAppSettings.DesktopAppSettings; | ||
| const updates = yield* DesktopUpdates.DesktopUpdates; | ||
| const environment = yield* DesktopEnvironment.DesktopEnvironment; | ||
|
|
||
| yield* shellEnvironment.installIntoProcess; | ||
| const userDataPath = yield* appIdentity.resolveUserDataPath; | ||
| yield* electronApp.setPath("userData", userDataPath); | ||
| yield* Effect.logInfo("runtime logging configured").pipe( | ||
| Effect.annotateLogs({ logDir: environment.logDir }), | ||
| ); | ||
| yield* desktopSettings.load; | ||
|
|
||
| if (environment.platform === "linux") { | ||
| yield* electronApp.appendCommandLineSwitch("class", environment.linuxWmClass); | ||
| } | ||
|
|
||
| yield* appIdentity.configure; | ||
| yield* lifecycle.register; | ||
|
|
||
| yield* electronApp.whenReady.pipe( | ||
| Effect.withSpan("desktop.electron.whenReady"), | ||
| Effect.catchCause((cause) => fatalStartupCause("whenReady", cause)), | ||
| ); | ||
| yield* Effect.logInfo("app ready"); | ||
| yield* appIdentity.configure; | ||
| yield* applicationMenu.configure; | ||
| yield* electronProtocol.registerDesktopFileProtocol; | ||
| yield* updates.configure; | ||
| yield* bootstrap.pipe(Effect.catchCause((cause) => fatalStartupCause("bootstrap", cause))); | ||
| }).pipe(Effect.withSpan("desktop.startup")); | ||
|
|
||
| const scopedProgram = Effect.scoped( | ||
| Effect.gen(function* () { | ||
| const runId = yield* makeDesktopRunId; | ||
| yield* Effect.annotateLogsScoped({ scope: "desktop", runId }); | ||
| yield* Effect.annotateCurrentSpan({ scope: "desktop", runId }); | ||
|
|
||
| const shutdown = yield* DesktopLifecycle.DesktopShutdown; | ||
| const backendManager = yield* DesktopBackendManager.DesktopBackendManager; | ||
|
|
||
| yield* Effect.addFinalizer(() => | ||
| backendManager.stop().pipe(Effect.ensuring(shutdown.markComplete)), | ||
| ); | ||
|
|
||
| yield* startup; | ||
| yield* shutdown.awaitRequest; | ||
| }), | ||
| ); | ||
|
|
||
| export const program = scopedProgram.pipe(Effect.withSpan("desktop.app")); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gitignore patterns now overly broad across monorepo
Low Severity
Moving
dist-electron/and.electron-runtime/fromapps/desktop/.gitignoreto the root.gitignorebroadens matching scope from justapps/desktop/to any directory at any depth in the monorepo. Gitignore patterns without a leading path separator match anywhere in the tree. Since this is a monorepo with multiple apps/packages, any future package that happens to use adist-electron/directory would have it silently ignored.Reviewed by Cursor Bugbot for commit 01e1673. Configure here.