Skip to content

Commit 41f4f1b

Browse files
authored
fix(tray): avoid passing tray ref and fix update on exit during restarting (#960)
* fix(tray): avoid passing tray ref and fix update on exit * fix: remove console.log * test: remove tray ref
1 parent 1f5041d commit 41f4f1b

File tree

7 files changed

+61
-76
lines changed

7 files changed

+61
-76
lines changed

main/src/auto-update.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { safeTrayDestroy } from './system-tray'
1111
import { pollWindowReady } from './util'
1212
import { delay } from '../../utils/delay'
1313
import log from './logger'
14-
import { setQuittingState, setTearingDownState, getTray } from './app-state'
14+
import { setQuittingState, setTearingDownState } from './app-state'
1515

1616
let pendingUpdateVersion: string | null = null
1717
let updateState:
@@ -79,7 +79,7 @@ async function performUpdateInstallation(releaseName: string | null) {
7979
log.error('[update] Error stopping ToolHive: ', error)
8080
}
8181

82-
safeTrayDestroy(getTray())
82+
safeTrayDestroy()
8383

8484
log.info('[update] all cleaned up, calling autoUpdater.quitAndInstall()...')
8585
autoUpdater.quitAndInstall()
@@ -89,7 +89,7 @@ async function performUpdateInstallation(releaseName: string | null) {
8989

9090
// Attempt recovery
9191
try {
92-
safeTrayDestroy(getTray())
92+
safeTrayDestroy()
9393
log.info('[update] attempting app relaunch after update failure')
9494
// this creates a new app instance
9595
app.relaunch()

main/src/main.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,6 @@ import {
104104
setQuittingState,
105105
getTearingDownState,
106106
setTearingDownState,
107-
setTray,
108107
getTray,
109108
} from './app-state'
110109
import type { UIMessage } from 'ai'
@@ -185,7 +184,7 @@ export async function blockQuit(source: string, event?: Electron.Event) {
185184
// Stop the embedded ToolHive server
186185
stopToolhive()
187186

188-
safeTrayDestroy(getTray())
187+
safeTrayDestroy()
189188
app.quit()
190189
}
191190
}
@@ -221,10 +220,10 @@ app.whenReady().then(async () => {
221220

222221
// Initialize tray first
223222
try {
224-
setTray(initTray({ toolHiveIsRunning: false })) // Start with false, will update after ToolHive starts
223+
initTray({ toolHiveIsRunning: false }) // Start with false, will update after ToolHive starts
225224
log.info('System tray initialized successfully')
226225
// Setup application menu
227-
createApplicationMenu(getTray())
226+
createApplicationMenu()
228227
} catch (error) {
229228
log.error('Failed to initialize system tray: ', error)
230229
}
@@ -236,7 +235,7 @@ app.whenReady().then(async () => {
236235
)
237236

238237
// Start ToolHive with tray reference
239-
await startToolhive(getTray() || undefined)
238+
await startToolhive()
240239

241240
// Create main window
242241
try {
@@ -297,7 +296,7 @@ app.whenReady().then(async () => {
297296
if (getTray() && process.platform !== 'win32') {
298297
try {
299298
getTray()?.destroy()
300-
setTray(initTray({ toolHiveIsRunning: isToolhiveRunning() }))
299+
initTray({ toolHiveIsRunning: isToolhiveRunning() })
301300
} catch (error) {
302301
log.error('Failed to update tray after theme change: ', error)
303302
}
@@ -357,7 +356,7 @@ app.on('will-quit', (e) => blockQuit('will-quit', e))
357356
}
358357
} finally {
359358
stopToolhive()
360-
safeTrayDestroy(getTray())
359+
safeTrayDestroy()
361360
process.exit(0)
362361
}
363362
})
@@ -391,11 +390,9 @@ ipcMain.handle('get-auto-launch-status', () => getAutoLaunchStatus())
391390
ipcMain.handle('set-auto-launch', (_event, enabled: boolean) => {
392391
setAutoLaunch(enabled)
393392
// Update tray menu if exists
394-
if (getTray()) {
395-
updateTrayStatus(getTray()!, isToolhiveRunning())
396-
}
393+
updateTrayStatus(isToolhiveRunning())
397394
// Update menu
398-
createApplicationMenu(getTray())
395+
createApplicationMenu()
399396
return getAutoLaunchStatus()
400397
})
401398

@@ -464,7 +461,7 @@ ipcMain.handle('check-container-engine', async () => {
464461

465462
ipcMain.handle('restart-toolhive', async () => {
466463
try {
467-
await restartToolhive(getTray() || undefined)
464+
await restartToolhive()
468465
return { success: true }
469466
} catch (error) {
470467
log.error('Failed to restart ToolHive: ', error)

main/src/menu.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ import { getAutoLaunchStatus, setAutoLaunch } from './auto-launch'
33
import { updateTrayStatus } from './system-tray'
44
import log from './logger'
55

6-
function createAutoLaunchItem(
7-
accelerator: string,
8-
trayRef: Electron.Tray | null
9-
) {
6+
function createAutoLaunchItem(accelerator: string) {
107
return {
118
label: 'Start on Login',
129
type: 'checkbox' as const,
@@ -16,18 +13,16 @@ function createAutoLaunchItem(
1613
try {
1714
const currentStatus = getAutoLaunchStatus()
1815
setAutoLaunch(!currentStatus)
19-
if (trayRef) {
20-
updateTrayStatus(trayRef, true)
21-
}
22-
createApplicationMenu(trayRef)
16+
updateTrayStatus(true)
17+
createApplicationMenu()
2318
} catch (error) {
2419
log.error('Failed to toggle auto-launch: ', error)
2520
}
2621
},
2722
}
2823
}
2924

30-
export function createApplicationMenu(trayRef: Electron.Tray | null) {
25+
export function createApplicationMenu() {
3126
const isMac = process.platform === 'darwin'
3227
const defaultMenu = Menu.getApplicationMenu()?.items ?? []
3328

@@ -57,7 +52,7 @@ export function createApplicationMenu(trayRef: Electron.Tray | null) {
5752
submenu: [
5853
{ role: 'about' as const },
5954
{ type: 'separator' as const },
60-
createAutoLaunchItem(isMac ? 'Cmd+L' : 'Ctrl+L', trayRef),
55+
createAutoLaunchItem(isMac ? 'Cmd+L' : 'Ctrl+L'),
6156
{ type: 'separator' as const },
6257
...(isMac
6358
? [

main/src/system-tray.ts

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import log from './logger'
66
import { getAppVersion } from './util'
77
import { hideWindow, showWindow, showInDock } from './dock-utils'
88
import { showMainWindow, sendToMainWindowRenderer } from './main-window'
9+
import { getTray, setTray } from './app-state'
910

1011
// Safe tray destruction with error handling
11-
export function safeTrayDestroy(tray: Tray | null) {
12+
export function safeTrayDestroy() {
13+
const tray = getTray()
1214
try {
1315
if (tray && !tray.isDestroyed()) {
1416
tray.destroy()
@@ -62,13 +64,10 @@ function getIcon(): Electron.NativeImage {
6264
///////////////////////////////////////////////////
6365

6466
const createTrayWithSetup =
65-
(setupFn: (tray: Tray, toolHiveIsRunning: boolean) => void) =>
67+
(setupFn: (toolHiveIsRunning: boolean) => void) =>
6668
(toolHiveIsRunning: boolean) => {
6769
try {
68-
const tray = new Tray(getIcon())
69-
70-
setupFn(tray, toolHiveIsRunning)
71-
return tray
70+
return setupFn(toolHiveIsRunning)
7271
} catch (error) {
7372
log.error('Failed to create tray: ', error)
7473
throw error
@@ -111,10 +110,7 @@ const showWindowWithFocus = (window: BrowserWindow) => {
111110
bringToFrontOnWindows(window)
112111
}
113112

114-
const handleStartOnLogin = async (
115-
currentTray: Tray,
116-
toolHiveIsRunning: boolean
117-
) => {
113+
const handleStartOnLogin = async (toolHiveIsRunning: boolean) => {
118114
const currentStatus = getAutoLaunchStatus()
119115

120116
try {
@@ -127,10 +123,10 @@ const handleStartOnLogin = async (
127123
}
128124

129125
// Update the tray menu to reflect the new state
130-
setupTrayMenu(currentTray, toolHiveIsRunning)
126+
setupTrayMenu(toolHiveIsRunning)
131127

132128
// Update the application menu to reflect the new state
133-
createApplicationMenu(currentTray)
129+
createApplicationMenu()
134130
} catch (error) {
135131
log.error('Failed to toggle auto-launch: ', error)
136132
}
@@ -153,14 +149,14 @@ const getCurrentAppVersion = () => {
153149
}
154150
}
155151

156-
const startOnLoginMenu = (currentTray: Tray, toolHiveIsRunning: boolean) => {
152+
const startOnLoginMenu = (toolHiveIsRunning: boolean) => {
157153
const isStartOnLogin = getAutoLaunchStatus()
158154
return {
159155
label: 'Start on login',
160156
checked: isStartOnLogin,
161157
accelerator: 'CmdOrCtrl+L',
162158
type: 'checkbox' as const,
163-
click: () => handleStartOnLogin(currentTray, toolHiveIsRunning),
159+
click: () => handleStartOnLogin(toolHiveIsRunning),
164160
}
165161
}
166162

@@ -195,11 +191,11 @@ const createQuitMenuItem = () => ({
195191

196192
const createSeparator = () => ({ type: 'separator' as const })
197193

198-
const createMenuTemplate = (currentTray: Tray, toolHiveIsRunning: boolean) => [
194+
const createMenuTemplate = (toolHiveIsRunning: boolean) => [
199195
createStatusMenuItem(toolHiveIsRunning),
200196
getCurrentAppVersion(),
201197
createSeparator(),
202-
startOnLoginMenu(currentTray, toolHiveIsRunning),
198+
startOnLoginMenu(toolHiveIsRunning),
203199
createSeparator(),
204200
createShowMenuItem(),
205201
createHideMenuItem(),
@@ -233,8 +229,15 @@ const createClickHandler = () => {
233229
}
234230
}
235231

236-
function setupTrayMenu(tray: Tray, toolHiveIsRunning: boolean) {
237-
const menuTemplate = createMenuTemplate(tray, toolHiveIsRunning)
232+
// if tray is not there let's create a new one
233+
function setupTrayMenu(toolHiveIsRunning: boolean) {
234+
let tray = getTray()
235+
if (!tray || tray.isDestroyed()) {
236+
tray = new Tray(getIcon())
237+
setTray(tray)
238+
}
239+
240+
const menuTemplate = createMenuTemplate(toolHiveIsRunning)
238241
const contextMenu = Menu.buildFromTemplate(menuTemplate)
239242

240243
tray.setToolTip('ToolHive')
@@ -247,11 +250,12 @@ function setupTrayMenu(tray: Tray, toolHiveIsRunning: boolean) {
247250
}
248251

249252
// Function to update tray status without recreating the entire tray
250-
export const updateTrayStatus = (tray: Tray, toolHiveIsRunning: boolean) => {
253+
export const updateTrayStatus = (toolHiveIsRunning: boolean) => {
254+
const tray = getTray()
251255
if (!tray || tray.isDestroyed()) return
252256

253257
try {
254-
setupTrayMenu(tray, toolHiveIsRunning)
258+
setupTrayMenu(toolHiveIsRunning)
255259
} catch (error) {
256260
log.error('Failed to update tray status: ', error)
257261
}

main/src/tests/auto-update.test.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ describe('auto-update', () => {
237237
expect(vi.mocked(dialog.showMessageBox)).toHaveBeenCalled()
238238
expect(vi.mocked(stopAllServers)).toHaveBeenCalled()
239239
expect(vi.mocked(stopToolhive)).toHaveBeenCalled()
240-
expect(vi.mocked(safeTrayDestroy)).toHaveBeenCalledWith(mockTray)
240+
expect(vi.mocked(safeTrayDestroy)).toHaveBeenCalled()
241241
})
242242

243243
it('handles update-downloaded event with user clicking later', async () => {
@@ -681,7 +681,7 @@ describe('auto-update', () => {
681681
// Should call graceful exit sequence
682682
expect(vi.mocked(stopAllServers)).toHaveBeenCalled()
683683
expect(vi.mocked(stopToolhive)).toHaveBeenCalled()
684-
expect(vi.mocked(safeTrayDestroy)).toHaveBeenCalledWith(mockTray)
684+
expect(vi.mocked(safeTrayDestroy)).toHaveBeenCalled()
685685
})
686686

687687
it('handles server shutdown failure during update', async () => {
@@ -789,9 +789,7 @@ describe('auto-update', () => {
789789

790790
await updatePromise
791791

792-
expect(vi.mocked(safeTrayDestroy)).toHaveBeenCalledWith(
793-
mockTrayWithDestroy
794-
)
792+
expect(vi.mocked(safeTrayDestroy)).toHaveBeenCalled()
795793
})
796794

797795
it('respects event listener removal during update', async () => {

0 commit comments

Comments
 (0)