Skip to content

Commit 6d9976f

Browse files
fix: detect .deb installs and show apt-specific update messaging (#281)
* fix: detect system-managed installs and show apt-specific update messaging On Linux system-managed installs (.deb), the ToDesktop in-app auto-updater cannot self-update the package. Detect system-managed installs by checking the executable path (installed under /opt/ or /usr/) and expose update capabilities to the renderer via a capability-based IPC contract: { canAutoUpdate: boolean, systemManaged: boolean } The renderer uses these capabilities to: - Show apt-specific instructions when an update is available - Hide the Download button when canAutoUpdate is false - Show apt guidance when Check for Updates reports up-to-date The ToDesktop updater is still attempted - if electron-updater's pkexec path works, the standard flow proceeds. Otherwise users see clear instructions to use their system package manager. Closes #280 Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-019d12c7-2178-74d2-b898-b627c26ffe01 * fix: render backtick commands as code tags and strengthen test assertions - Rename boldify to formatMarkdown; also convert backticks to <code> - SettingsView tests now mock useModal and assert the correct i18n string (upToDate vs debUpToDate) is passed to modal.alert - UpdateBanner test verifies backticks are rendered as <code> tags Amp-Thread-ID: https://ampcode.com/threads/T-019d12c7-2178-74d2-b898-b627c26ffe01 Co-authored-by: Amp <amp@ampcode.com> --------- Co-authored-by: Amp <amp@ampcode.com>
1 parent c343845 commit 6d9976f

9 files changed

Lines changed: 542 additions & 8 deletions

File tree

locales/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,9 @@
317317
"retry": "Retry",
318318
"updateCheck": "Update Check",
319319
"updateError": "Update Error",
320-
"upToDate": "You are running the latest version of ComfyUI Desktop 2.0."
320+
"upToDate": "You are running the latest version of ComfyUI Desktop 2.0.",
321+
"debAvailable": "Update **v{version}** is available. Run `sudo apt update && sudo apt upgrade` to update, or download the new .deb from the releases page.",
322+
"debUpToDate": "You are running the latest version. Updates for .deb installs are delivered through your system package manager (apt)."
321323
},
322324

323325
"modal": {

src/main/lib/updater.test.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2+
3+
let mockPlatform = 'linux'
4+
let mockAppImage: string | undefined
5+
let mockIsPackaged = true
6+
let mockExePath = '/opt/ComfyUI Desktop 2.0/comfyui-desktop-2'
7+
8+
vi.mock('electron', () => ({
9+
app: {
10+
get isPackaged() {
11+
return mockIsPackaged
12+
},
13+
getPath: (name: string) => {
14+
if (name === 'exe') return mockExePath
15+
return ''
16+
},
17+
},
18+
ipcMain: {
19+
handle: vi.fn(),
20+
},
21+
BrowserWindow: {
22+
getAllWindows: () => [],
23+
},
24+
}))
25+
26+
vi.mock('@todesktop/runtime', () => ({
27+
default: { autoUpdater: null },
28+
}))
29+
30+
vi.mock('../settings', () => ({
31+
get: vi.fn(),
32+
}))
33+
34+
vi.mock('./quit-state', () => ({
35+
clearQuitReason: vi.fn(),
36+
setQuitReason: vi.fn(),
37+
}))
38+
39+
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')!
40+
41+
describe('isSystemPackageInstall (via get-update-capabilities)', () => {
42+
let registeredHandlers: Record<string, (...args: unknown[]) => unknown>
43+
44+
beforeEach(async () => {
45+
registeredHandlers = {}
46+
const { ipcMain } = await import('electron')
47+
vi.mocked(ipcMain.handle).mockImplementation(((channel: string, handler: (...args: unknown[]) => unknown) => {
48+
registeredHandlers[channel] = handler
49+
}) as typeof ipcMain.handle)
50+
51+
mockPlatform = 'linux'
52+
mockAppImage = undefined
53+
mockIsPackaged = true
54+
mockExePath = '/opt/ComfyUI Desktop 2.0/comfyui-desktop-2'
55+
56+
delete process.env.APPIMAGE
57+
Object.defineProperty(process, 'platform', { value: mockPlatform, configurable: true })
58+
59+
vi.resetModules()
60+
})
61+
62+
afterEach(() => {
63+
Object.defineProperty(process, 'platform', originalPlatform)
64+
})
65+
66+
async function getCapabilities(): Promise<{ canAutoUpdate: boolean; systemManaged: boolean }> {
67+
Object.defineProperty(process, 'platform', { value: mockPlatform, configurable: true })
68+
if (mockAppImage) {
69+
process.env.APPIMAGE = mockAppImage
70+
} else {
71+
delete process.env.APPIMAGE
72+
}
73+
74+
vi.resetModules()
75+
const updater = await import('./updater')
76+
updater.register()
77+
const handler = registeredHandlers['get-update-capabilities']!
78+
return handler() as { canAutoUpdate: boolean; systemManaged: boolean }
79+
}
80+
81+
it('detects .deb install under /opt/', async () => {
82+
mockExePath = '/opt/ComfyUI Desktop 2.0/comfyui-desktop-2'
83+
const caps = await getCapabilities()
84+
expect(caps).toEqual({ canAutoUpdate: false, systemManaged: true })
85+
})
86+
87+
it('detects .deb install under /usr/', async () => {
88+
mockExePath = '/usr/lib/comfyui-desktop-2/comfyui-desktop-2'
89+
const caps = await getCapabilities()
90+
expect(caps).toEqual({ canAutoUpdate: false, systemManaged: true })
91+
})
92+
93+
it('returns standard for AppImage (APPIMAGE env set)', async () => {
94+
mockAppImage = '/home/user/ComfyUI-Desktop-2.0.AppImage'
95+
const caps = await getCapabilities()
96+
expect(caps).toEqual({ canAutoUpdate: true, systemManaged: false })
97+
})
98+
99+
it('returns standard for Windows', async () => {
100+
mockPlatform = 'win32'
101+
const caps = await getCapabilities()
102+
expect(caps).toEqual({ canAutoUpdate: true, systemManaged: false })
103+
})
104+
105+
it('returns standard for macOS', async () => {
106+
mockPlatform = 'darwin'
107+
const caps = await getCapabilities()
108+
expect(caps).toEqual({ canAutoUpdate: true, systemManaged: false })
109+
})
110+
111+
it('returns standard when not packaged (dev mode)', async () => {
112+
mockIsPackaged = false
113+
const caps = await getCapabilities()
114+
expect(caps).toEqual({ canAutoUpdate: true, systemManaged: false })
115+
})
116+
117+
it('returns standard for Linux exe under /home/ (manual extract)', async () => {
118+
mockExePath = '/home/user/apps/comfyui-desktop-2'
119+
const caps = await getCapabilities()
120+
expect(caps).toEqual({ canAutoUpdate: true, systemManaged: false })
121+
})
122+
123+
it('returns standard for Linux exe under /tmp/ (temp location)', async () => {
124+
mockExePath = '/tmp/.mount_comfyui/comfyui-desktop-2'
125+
const caps = await getCapabilities()
126+
expect(caps).toEqual({ canAutoUpdate: true, systemManaged: false })
127+
})
128+
})

src/main/lib/updater.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ipcMain, BrowserWindow } from 'electron'
1+
import { app, ipcMain, BrowserWindow } from 'electron'
22
import todesktop from '@todesktop/runtime'
33
import * as settings from '../settings'
44
import { clearQuitReason, setQuitReason } from './quit-state'
@@ -13,6 +13,14 @@ let _listenersBound = false
1313
const NO_UPDATE_AVAILABLE_MESSAGE = 'No update available. Try checking for updates first.'
1414
const UPDATER_UNAVAILABLE_MESSAGE = 'ToDesktop auto-updater is unavailable.'
1515

16+
function isSystemPackageInstall(): boolean {
17+
if (process.platform !== 'linux' || !app.isPackaged) return false
18+
if (process.env.APPIMAGE) return false
19+
// .deb installs place the app under /opt/ or /usr/; check the executable path
20+
const appPath = app.getPath('exe')
21+
return appPath.startsWith('/opt/') || appPath.startsWith('/usr/')
22+
}
23+
1624
function broadcast(channel: string, data: unknown): void {
1725
BrowserWindow.getAllWindows().forEach((win) => {
1826
try {
@@ -153,6 +161,11 @@ export function register(): void {
153161

154162
ipcMain.handle('get-pending-update', () => _updateInfo)
155163

164+
ipcMain.handle('get-update-capabilities', () => {
165+
const systemManaged = isSystemPackageInstall()
166+
return { canAutoUpdate: !systemManaged, systemManaged }
167+
})
168+
156169
// Check on startup and periodically (respects autoUpdate setting at each check)
157170
const runIfEnabled = (): void => {
158171
if (settings.get('autoUpdate') !== false) runCheck('auto-check').catch(() => {})

src/preload/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ const api: ElectronApi = {
108108
downloadUpdate: () => ipcRenderer.invoke('download-update'),
109109
installUpdate: () => ipcRenderer.invoke('install-update'),
110110
getPendingUpdate: () => ipcRenderer.invoke('get-pending-update'),
111+
getUpdateCapabilities: () => ipcRenderer.invoke('get-update-capabilities'),
111112

112113
// Event listeners (return unsubscribe functions)
113114
onInstallProgress: (callback) => {

0 commit comments

Comments
 (0)