Skip to content

Commit 37c07ee

Browse files
Apply PR #25962: feat(desktop): move server to utilityProcess
2 parents 4b4272e + d1cb190 commit 37c07ee

7 files changed

Lines changed: 454 additions & 120 deletions

File tree

packages/desktop/electron.vite.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export default defineConfig({
3737
},
3838
build: {
3939
rollupOptions: {
40-
input: { index: "src/main/index.ts" },
40+
input: { index: "src/main/index.ts", sidecar: "src/main/sidecar.ts" },
4141
},
4242
externalizeDeps: { include: [nodePtyPkg] },
4343
},

packages/desktop/src/main/apps.ts

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
1-
import { execFileSync } from "node:child_process"
2-
import { existsSync, readFileSync, readdirSync } from "node:fs"
1+
import { execFile, execFileSync } from "node:child_process"
2+
import { access, readFile, readdir } from "node:fs/promises"
33
import { dirname, extname, join } from "node:path"
4+
import util from "node:util"
5+
46
import { resolveWslHome, runWslInDistro } from "./wsl"
57

6-
export function checkAppExists(appName: string): boolean {
8+
const execFilePromise = util.promisify(execFile)
9+
10+
const exists = (path: string) =>
11+
access(path)
12+
.then(() => true)
13+
.catch(() => false)
14+
15+
export function checkAppExists(appName: string) {
716
if (process.platform === "win32") return true
817
if (process.platform === "linux") return true
918
return checkMacosApp(appName)
1019
}
1120

12-
export function resolveAppPath(appName: string): string | null {
21+
export function resolveAppPath(appName: string) {
1322
if (process.platform !== "win32") return appName
1423
return resolveWindowsAppPath(appName)
1524
}
@@ -57,26 +66,25 @@ export async function wslPath(path: string, mode: "windows" | "linux" | null, di
5766
}
5867
}
5968

60-
function checkMacosApp(appName: string) {
69+
async function checkMacosApp(appName: string) {
6170
const locations = [`/Applications/${appName}.app`, `/System/Applications/${appName}.app`]
6271

6372
const home = process.env.HOME
6473
if (home) locations.push(`${home}/Applications/${appName}.app`)
6574

66-
if (locations.some((location) => existsSync(location))) return true
67-
68-
try {
69-
execFileSync("which", [appName])
70-
return true
71-
} catch {
72-
return false
75+
for (const location of locations) {
76+
if (await exists(location)) return true
7377
}
78+
79+
return execFilePromise("which", [appName])
80+
.then(() => true)
81+
.catch(() => false)
7482
}
7583

76-
function resolveWindowsAppPath(appName: string): string | null {
84+
async function resolveWindowsAppPath(appName: string): Promise<string | null> {
7785
let output: string
7886
try {
79-
output = execFileSync("where", [appName]).toString()
87+
output = execFilePromise("where", [appName]).toString()
8088
} catch {
8189
return null
8290
}
@@ -91,8 +99,8 @@ function resolveWindowsAppPath(appName: string): string | null {
9199
const exe = paths.find((path) => hasExt(path, "exe"))
92100
if (exe) return exe
93101

94-
const resolveCmd = (path: string) => {
95-
const content = readFileSync(path, "utf8")
102+
const resolveCmd = async (path: string) => {
103+
const content = await readFile(path, "utf8")
96104
for (const token of content.split('"').map((value: string) => value.trim())) {
97105
const lower = token.toLowerCase()
98106
if (!lower.includes(".exe")) continue
@@ -110,31 +118,31 @@ function resolveWindowsAppPath(appName: string): string | null {
110118
return join(current, part)
111119
}, base)
112120

113-
if (existsSync(resolved)) return resolved
121+
if (await exists(resolved)) return resolved
114122
}
115123

116-
if (existsSync(token)) return token
124+
if (await exists(token)) return token
117125
}
118126

119127
return null
120128
}
121129

122130
for (const path of paths) {
123131
if (hasExt(path, "cmd") || hasExt(path, "bat")) {
124-
const resolved = resolveCmd(path)
132+
const resolved = await resolveCmd(path)
125133
if (resolved) return resolved
126134
}
127135

128136
if (!extname(path)) {
129137
const cmd = `${path}.cmd`
130-
if (existsSync(cmd)) {
131-
const resolved = resolveCmd(cmd)
138+
if (await exists(cmd)) {
139+
const resolved = await resolveCmd(cmd)
132140
if (resolved) return resolved
133141
}
134142

135143
const bat = `${path}.bat`
136-
if (existsSync(bat)) {
137-
const resolved = resolveCmd(bat)
144+
if (await exists(bat)) {
145+
const resolved = await resolveCmd(bat)
138146
if (resolved) return resolved
139147
}
140148
}
@@ -151,7 +159,7 @@ function resolveWindowsAppPath(appName: string): string | null {
151159
const dirs = [dirname(path), dirname(dirname(path)), dirname(dirname(dirname(path)))]
152160
for (const dir of dirs) {
153161
try {
154-
for (const entry of readdirSync(dir)) {
162+
for (const entry of await readdir(dir)) {
155163
const candidate = join(dir, entry)
156164
if (!hasExt(candidate, "exe")) continue
157165
const stem = entry.replace(/\.exe$/i, "")

packages/desktop/src/main/env.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ interface ImportMetaEnv {
55
interface ImportMeta {
66
readonly env: ImportMetaEnv
77
}
8+
89
declare module "virtual:opencode-server" {
910
export namespace Server {
1011
export const listen: typeof import("../../../opencode/dist/types/src/node").Server.listen

packages/desktop/src/main/index.ts

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import { getCACertificates, setDefaultCACertificates } from "node:tls"
88
import type { Event } from "electron"
99
import { app, BrowserWindow, dialog } from "electron"
1010
import pkg from "electron-updater"
11-
import { drizzle } from "drizzle-orm/node-sqlite/driver"
12-
import type { Server } from "virtual:opencode-server"
1311

1412
import contextMenu from "electron-context-menu"
1513
contextMenu({ showSaveImageAs: true, showLookUpSelection: false, showSearchWithGoogle: false })
@@ -48,7 +46,15 @@ import { registerIpcHandlers, sendDeepLinks, sendMenuCommand, sendSqliteMigratio
4846
import { initLogging } from "./logging"
4947
import { parseMarkdown } from "./markdown"
5048
import { createMenu } from "./menu"
51-
import { allocatePort, getDefaultServerUrl, setDefaultServerUrl, spawnLocalServer, spawnWslSidecar } from "./server"
49+
import {
50+
allocatePort,
51+
getDefaultServerUrl,
52+
preferAppEnv,
53+
setDefaultServerUrl,
54+
spawnLocalServer,
55+
spawnWslSidecar,
56+
type SidecarListener,
57+
} from "./server"
5258
import { createWslServersController } from "./wsl-servers"
5359
import {
5460
createLoadingWindow,
@@ -63,7 +69,7 @@ const initEmitter = new EventEmitter()
6369
let initStep: InitStep = { phase: "server_waiting" }
6470

6571
let mainWindow: BrowserWindow | null = null
66-
let server: Server.Listener | null = null
72+
let server: SidecarListener | null = null
6773
const loadingComplete = defer<void>()
6874

6975
const pendingDeepLinks: string[] = []
@@ -120,6 +126,8 @@ function setupApp() {
120126
return
121127
}
122128

129+
preferAppEnv(app.getPath("userData"))
130+
123131
app.on("second-instance", (_event: Event, argv: string[]) => {
124132
const urls = argv.filter((arg: string) => arg.startsWith("opencode://"))
125133
if (urls.length) {
@@ -136,20 +144,21 @@ function setupApp() {
136144
})
137145

138146
app.on("before-quit", () => {
139-
killSidecar()
147+
void killSidecar()
140148
wslServers.stopAll()
141149
})
142150

143151
app.on("will-quit", () => {
144-
killSidecar()
152+
void killSidecar()
145153
wslServers.stopAll()
146154
})
147155

148156
for (const signal of ["SIGINT", "SIGTERM"] as const) {
149157
process.on(signal, () => {
150-
killSidecar()
151-
wslServers.stopAll()
152-
app.exit(0)
158+
void killSidecar().finally(() => {
159+
wslServers.stopAll()
160+
app.exit(0)
161+
})
153162
})
154163
}
155164

@@ -216,22 +225,24 @@ async function initialize() {
216225
if (mainWindow) sendSqliteMigrationProgress(mainWindow, progress)
217226
})
218227

219-
if (needsMigration) {
220-
const { Database, JsonMigration } = await import("virtual:opencode-server")
221-
await JsonMigration.run(drizzle({ client: Database.Client().$client }), {
222-
progress: (event: { current: number; total: number }) => {
223-
const percent = Math.round((event.current / event.total) * 100)
224-
initEmitter.emit("sqlite", { type: "InProgress", value: percent })
225-
},
226-
})
227-
initEmitter.emit("sqlite", { type: "Done" })
228-
}
229-
230228
logger.log("spawning sidecar", { url })
231-
const { listener, health } = await spawnLocalServer(hostname, port, password, () => {
232-
ensureLoopbackNoProxy()
233-
useEnvProxy()
234-
})
229+
const { listener, health } = await spawnLocalServer(
230+
hostname,
231+
port,
232+
password,
233+
() => {
234+
ensureLoopbackNoProxy()
235+
useEnvProxy()
236+
},
237+
{
238+
needsMigration,
239+
userDataPath: app.getPath("userData"),
240+
onSqliteProgress: (progress) => initEmitter.emit("sqlite", progress),
241+
onStdout: (message) => logger.log("sidecar stdout", { message }),
242+
onStderr: (message) => logger.warn("sidecar stderr", { message }),
243+
onExit: (code) => logger.warn("sidecar exited", { code }),
244+
},
245+
)
235246
server = listener
236247
serverReady.resolve({
237248
url,
@@ -333,19 +344,19 @@ registerIpcHandlers({
333344
setBackgroundColor: (color) => setBackgroundColor(color),
334345
})
335346

336-
function killSidecar() {
347+
async function killSidecar() {
337348
if (!server) return
338-
server.stop()
349+
const current = server
339350
server = null
351+
await current.stop()
340352
}
341353

342354
function relaunchApp() {
343-
// app.exit() skips before-quit / will-quit, so relaunch callers must
344-
// explicitly stop sidecars here rather than relying on process hooks.
345-
killSidecar()
346-
wslServers.stopAll()
347-
app.relaunch()
348-
app.exit(0)
355+
void killSidecar().finally(() => {
356+
wslServers.stopAll()
357+
app.relaunch()
358+
app.exit(0)
359+
})
349360
}
350361

351362
function ensureLoopbackNoProxy() {
@@ -445,7 +456,7 @@ async function installUpdate() {
445456
logger.log("installing downloaded update", {
446457
version: downloadedUpdateVersion,
447458
})
448-
killSidecar()
459+
await killSidecar()
449460
wslServers.stopAll()
450461
autoUpdater.quitAndInstall()
451462
}

packages/desktop/src/main/ipc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const pickerFilters = (ext?: string[]) => {
2121
}
2222

2323
type Deps = {
24-
killSidecar: () => void
24+
killSidecar: () => Promise<void> | void
2525
relaunch: () => void
2626
awaitInitialization: (sendStep: (step: InitStep) => void) => Promise<ServerReadyData>
2727
getWslServersState: () => Promise<WslServersState> | WslServersState

0 commit comments

Comments
 (0)