forked from imbytecat/fullstack-starter
fix(desktop): guard shutdown race and kill sidecar process tree
This commit is contained in:
@@ -17,6 +17,9 @@
|
|||||||
"fix": "biome check --write",
|
"fix": "biome check --write",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"tree-kill": "catalog:"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@furtherverse/tsconfig": "workspace:*",
|
"@furtherverse/tsconfig": "workspace:*",
|
||||||
"@tailwindcss/vite": "catalog:",
|
"@tailwindcss/vite": "catalog:",
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import { spawn } from 'node:child_process'
|
|||||||
import { createServer } from 'node:net'
|
import { createServer } from 'node:net'
|
||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
import { app, BrowserWindow, shell } from 'electron'
|
import { app, BrowserWindow, shell } from 'electron'
|
||||||
|
import killProcessTree from 'tree-kill'
|
||||||
|
|
||||||
const DEV_SERVER_URL = 'http://localhost:3000'
|
const DEV_SERVER_URL = 'http://localhost:3000'
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null
|
let mainWindow: BrowserWindow | null = null
|
||||||
let serverProcess: ReturnType<typeof spawn> | null = null
|
let serverProcess: ReturnType<typeof spawn> | null = null
|
||||||
|
let isQuitting = false
|
||||||
|
|
||||||
const getAvailablePort = (): Promise<number> =>
|
const getAvailablePort = (): Promise<number> =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
@@ -37,7 +39,7 @@ const waitForServer = async (
|
|||||||
timeoutMs = 15_000,
|
timeoutMs = 15_000,
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
while (Date.now() - start < timeoutMs) {
|
while (Date.now() - start < timeoutMs && !isQuitting) {
|
||||||
if (await isServerReady(url)) return true
|
if (await isServerReady(url)) return true
|
||||||
await new Promise<void>((resolve) => setTimeout(resolve, 200))
|
await new Promise<void>((resolve) => setTimeout(resolve, 200))
|
||||||
}
|
}
|
||||||
@@ -56,7 +58,11 @@ const stopServerProcess = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
runningServer.kill()
|
killProcessTree(runningServer.pid, (error?: Error) => {
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to stop server process:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const spawnServer = (port: number): string => {
|
const spawnServer = (port: number): string => {
|
||||||
@@ -84,11 +90,17 @@ const spawnServer = (port: number): string => {
|
|||||||
return `http://127.0.0.1:${port}`
|
return `http://127.0.0.1:${port}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const getServerUrl = async (): Promise<string> => {
|
const getServerUrl = async (): Promise<string | null> => {
|
||||||
if (!app.isPackaged) {
|
if (!app.isPackaged) {
|
||||||
return DEV_SERVER_URL
|
return DEV_SERVER_URL
|
||||||
}
|
}
|
||||||
|
|
||||||
const port = await getAvailablePort()
|
const port = await getAvailablePort()
|
||||||
|
|
||||||
|
if (isQuitting) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return spawnServer(port)
|
return spawnServer(port)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,9 +132,18 @@ const createWindow = async () => {
|
|||||||
mainWindow.show()
|
mainWindow.show()
|
||||||
|
|
||||||
const serverUrl = await getServerUrl()
|
const serverUrl = await getServerUrl()
|
||||||
|
if (!serverUrl || isQuitting || !mainWindow || mainWindow.isDestroyed()) {
|
||||||
|
stopServerProcess()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`Waiting for server at ${serverUrl}...`)
|
console.log(`Waiting for server at ${serverUrl}...`)
|
||||||
const ready = await waitForServer(serverUrl)
|
const ready = await waitForServer(serverUrl)
|
||||||
|
if (isQuitting || !mainWindow || mainWindow.isDestroyed()) {
|
||||||
|
stopServerProcess()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
console.error(
|
console.error(
|
||||||
app.isPackaged
|
app.isPackaged
|
||||||
@@ -144,21 +165,19 @@ app.whenReady().then(createWindow)
|
|||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
|
isQuitting = true
|
||||||
stopServerProcess()
|
stopServerProcess()
|
||||||
if (process.platform === 'win32') {
|
|
||||||
app.exit(0)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
app.quit()
|
app.quit()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (!isQuitting && BrowserWindow.getAllWindows().length === 0) {
|
||||||
createWindow()
|
createWindow()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('before-quit', () => {
|
app.on('before-quit', () => {
|
||||||
|
isQuitting = true
|
||||||
stopServerProcess()
|
stopServerProcess()
|
||||||
})
|
})
|
||||||
|
|||||||
6
bun.lock
6
bun.lock
@@ -13,6 +13,9 @@
|
|||||||
"apps/desktop": {
|
"apps/desktop": {
|
||||||
"name": "@furtherverse/desktop",
|
"name": "@furtherverse/desktop",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tree-kill": "catalog:",
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@furtherverse/tsconfig": "workspace:*",
|
"@furtherverse/tsconfig": "workspace:*",
|
||||||
"@tailwindcss/vite": "catalog:",
|
"@tailwindcss/vite": "catalog:",
|
||||||
@@ -118,6 +121,7 @@
|
|||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"systeminformation": "^5.30.7",
|
"systeminformation": "^5.30.7",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
|
"tree-kill": "^1.2.2",
|
||||||
"turbo": "^2.7.5",
|
"turbo": "^2.7.5",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
@@ -1244,6 +1248,8 @@
|
|||||||
|
|
||||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||||
|
|
||||||
|
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
|
||||||
|
|
||||||
"truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="],
|
"truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="],
|
||||||
|
|
||||||
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
|
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
|
||||||
|
|||||||
@@ -61,6 +61,7 @@
|
|||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"systeminformation": "^5.30.7",
|
"systeminformation": "^5.30.7",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
|
"tree-kill": "^1.2.2",
|
||||||
"turbo": "^2.7.5",
|
"turbo": "^2.7.5",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user