Skip to content

Commit e4941a6

Browse files
Apply PR #25962: feat(desktop): move server to utilityProcess
2 parents 565f47e + d1cb190 commit e4941a6

7 files changed

Lines changed: 444 additions & 115 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: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
1-
import { execFileSync } from "node:child_process"
2-
import { existsSync, readFileSync, readdirSync } from "node:fs"
1+
import { execFile } from "node:child_process"
2+
import { access, readFile, readdir } from "node:fs/promises"
33
import { dirname, extname, join } from "node:path"
44
import { resolveWslHome, runWslInDistro } from "./wsl"
5+
import util from "node:util"
56

6-
export function checkAppExists(appName: string): boolean {
7+
const execFilePromise = util.promisify(execFile)
8+
9+
const exists = (path: string) =>
10+
access(path)
11+
.then(() => true)
12+
.catch(() => false)
13+
14+
export function checkAppExists(appName: string) {
715
if (process.platform === "win32") return true
816
if (process.platform === "linux") return true
917
return checkMacosApp(appName)
1018
}
1119

12-
export function resolveAppPath(appName: string): string | null {
20+
export function resolveAppPath(appName: string) {
1321
if (process.platform !== "win32") return appName
1422
return resolveWindowsAppPath(appName)
1523
}
@@ -57,26 +65,25 @@ export async function wslPath(path: string, mode: "windows" | "linux" | null, di
5765
}
5866
}
5967

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

6371
const home = process.env.HOME
6472
if (home) locations.push(`${home}/Applications/${appName}.app`)
6573

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

76-
function resolveWindowsAppPath(appName: string): string | null {
83+
async function resolveWindowsAppPath(appName: string): Promise<string | null> {
7784
let output: string
7885
try {
79-
output = execFileSync("where", [appName]).toString()
86+
output = execFilePromise("where", [appName]).toString()
8087
} catch {
8188
return null
8289
}
@@ -91,8 +98,8 @@ function resolveWindowsAppPath(appName: string): string | null {
9198
const exe = paths.find((path) => hasExt(path, "exe"))
9299
if (exe) return exe
93100

94-
const resolveCmd = (path: string) => {
95-
const content = readFileSync(path, "utf8")
101+
const resolveCmd = async (path: string) => {
102+
const content = await readFile(path, "utf8")
96103
for (const token of content.split('"').map((value: string) => value.trim())) {
97104
const lower = token.toLowerCase()
98105
if (!lower.includes(".exe")) continue
@@ -110,31 +117,31 @@ function resolveWindowsAppPath(appName: string): string | null {
110117
return join(current, part)
111118
}, base)
112119

113-
if (existsSync(resolved)) return resolved
120+
if (await exists(resolved)) return resolved
114121
}
115122

116-
if (existsSync(token)) return token
123+
if (await exists(token)) return token
117124
}
118125

119126
return null
120127
}
121128

122129
for (const path of paths) {
123130
if (hasExt(path, "cmd") || hasExt(path, "bat")) {
124-
const resolved = resolveCmd(path)
131+
const resolved = await resolveCmd(path)
125132
if (resolved) return resolved
126133
}
127134

128135
if (!extname(path)) {
129136
const cmd = `${path}.cmd`
130-
if (existsSync(cmd)) {
131-
const resolved = resolveCmd(cmd)
137+
if (await exists(cmd)) {
138+
const resolved = await resolveCmd(cmd)
132139
if (resolved) return resolved
133140
}
134141

135142
const bat = `${path}.bat`
136-
if (existsSync(bat)) {
137-
const resolved = resolveCmd(bat)
143+
if (await exists(bat)) {
144+
const resolved = await resolveCmd(bat)
138145
if (resolved) return resolved
139146
}
140147
}
@@ -151,7 +158,7 @@ function resolveWindowsAppPath(appName: string): string | null {
151158
const dirs = [dirname(path), dirname(dirname(path)), dirname(dirname(dirname(path)))]
152159
for (const dir of dirs) {
153160
try {
154-
for (const entry of readdirSync(dir)) {
161+
for (const entry of await readdir(dir)) {
155162
const candidate = join(dir, entry)
156163
if (!hasExt(candidate, "exe")) continue
157164
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: 40 additions & 29 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+
type SidecarListener,
56+
spawnWslSidecar,
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,19 @@ 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()
151158
wslServers.stopAll()
152-
app.exit(0)
159+
void killSidecar().finally(() => app.exit(0))
153160
})
154161
}
155162

@@ -216,22 +223,24 @@ async function initialize() {
216223
if (mainWindow) sendSqliteMigrationProgress(mainWindow, progress)
217224
})
218225

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-
230226
logger.log("spawning sidecar", { url })
231-
const { listener, health } = await spawnLocalServer(hostname, port, password, () => {
232-
ensureLoopbackNoProxy()
233-
useEnvProxy()
234-
})
227+
const { listener, health } = await spawnLocalServer(
228+
hostname,
229+
port,
230+
password,
231+
() => {
232+
ensureLoopbackNoProxy()
233+
useEnvProxy()
234+
},
235+
{
236+
needsMigration,
237+
userDataPath: app.getPath("userData"),
238+
onSqliteProgress: (progress) => initEmitter.emit("sqlite", progress),
239+
onStdout: (message) => logger.log("sidecar stdout", { message }),
240+
onStderr: (message) => logger.warn("sidecar stderr", { message }),
241+
onExit: (code) => logger.warn("sidecar exited", { code }),
242+
},
243+
)
235244
server = listener
236245
serverReady.resolve({
237246
url,
@@ -333,19 +342,21 @@ registerIpcHandlers({
333342
setBackgroundColor: (color) => setBackgroundColor(color),
334343
})
335344

336-
function killSidecar() {
345+
async function killSidecar() {
337346
if (!server) return
338-
server.stop()
347+
const current = server
339348
server = null
349+
await current.stop()
340350
}
341351

342352
function relaunchApp() {
343353
// app.exit() skips before-quit / will-quit, so relaunch callers must
344354
// explicitly stop sidecars here rather than relying on process hooks.
345-
killSidecar()
346355
wslServers.stopAll()
347-
app.relaunch()
348-
app.exit(0)
356+
void killSidecar().finally(() => {
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)