From ac1b229cc93adb3bd0a5b5c38bc5fb9fb13959e4 Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Fri, 13 Jun 2025 10:38:29 -0700 Subject: [PATCH 1/3] disable updater until we can debug more in release --- ui/desktop/src/updates.ts | 2 +- ui/desktop/src/utils/autoUpdater.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/desktop/src/updates.ts b/ui/desktop/src/updates.ts index da6e19cfee1f..1ba31dab9e75 100644 --- a/ui/desktop/src/updates.ts +++ b/ui/desktop/src/updates.ts @@ -1 +1 @@ -export const UPDATES_ENABLED = true; +export const UPDATES_ENABLED = false; diff --git a/ui/desktop/src/utils/autoUpdater.ts b/ui/desktop/src/utils/autoUpdater.ts index d61922bf7f32..21d3c80fa84e 100644 --- a/ui/desktop/src/utils/autoUpdater.ts +++ b/ui/desktop/src/utils/autoUpdater.ts @@ -48,11 +48,11 @@ export function setupAutoUpdater(tray?: Tray) { autoUpdater.autoDownload = false; // We'll trigger downloads manually autoUpdater.autoInstallOnAppQuit = true; - // Enable updates in development mode for testing - if (process.env.ENABLE_DEV_UPDATES === 'true') { - autoUpdater.forceDevUpdateConfig = true; - } - + // Enable updates in development mode for testing + if (process.env.ENABLE_DEV_UPDATES === 'true') { + autoUpdater.forceDevUpdateConfig = true; + } + // Set logger autoUpdater.logger = log; From f2aa469542cc3bc713704bc43366089753af8b03 Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Fri, 13 Jun 2025 12:00:28 -0700 Subject: [PATCH 2/3] disable updater by default and add a keycode and logging to show and debug in release builds --- .../settings/app/AppSettingsSection.tsx | 23 +- ui/desktop/src/main.ts | 63 ++- ui/desktop/src/preload.ts | 26 ++ ui/desktop/src/utils/autoUpdater.ts | 399 +++++++++++------- ui/desktop/src/utils/githubUpdater.ts | 31 ++ 5 files changed, 386 insertions(+), 156 deletions(-) diff --git a/ui/desktop/src/components/settings/app/AppSettingsSection.tsx b/ui/desktop/src/components/settings/app/AppSettingsSection.tsx index a586fd7110f9..b798a8027031 100644 --- a/ui/desktop/src/components/settings/app/AppSettingsSection.tsx +++ b/ui/desktop/src/components/settings/app/AppSettingsSection.tsx @@ -1,7 +1,6 @@ import { useState, useEffect, useRef } from 'react'; import { Switch } from '../../ui/switch'; import UpdateSection from './UpdateSection'; -import { UPDATES_ENABLED } from '../../../updates'; interface AppSettingsSectionProps { scrollToSection?: string; @@ -12,6 +11,7 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti const [dockIconEnabled, setDockIconEnabled] = useState(true); const [isMacOS, setIsMacOS] = useState(false); const [isDockSwitchDisabled, setIsDockSwitchDisabled] = useState(false); + const [updatesEnabled, setUpdatesEnabled] = useState(false); const updateSectionRef = useRef(null); // Check if running on macOS @@ -19,6 +19,25 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti setIsMacOS(window.electron.platform === 'darwin'); }, []); + // Load updater state + useEffect(() => { + window.electron.getUpdaterEnabled().then((enabled) => { + setUpdatesEnabled(enabled); + }); + + // Listen for updater state changes + const handleUpdaterStateChange = (enabled: boolean) => { + setUpdatesEnabled(enabled); + }; + + window.electron.onUpdaterStateChanged(handleUpdaterStateChange); + + // Cleanup listener on unmount + return () => { + window.electron.removeUpdaterStateListener(handleUpdaterStateChange); + }; + }, []); + // Handle scrolling to update section useEffect(() => { if (scrollToSection === 'update' && updateSectionRef.current) { @@ -125,7 +144,7 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti {/* Update Section */} - {UPDATES_ENABLED && ( + {updatesEnabled && (
diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 64be6e506cdd..9a50cf281609 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -37,12 +37,31 @@ import * as yaml from 'yaml'; import windowStateKeeper from 'electron-window-state'; import { setupAutoUpdater, + registerUpdateIpcHandlers, setTrayRef, updateTrayMenu, getUpdateAvailable, } from './utils/autoUpdater'; import { UPDATES_ENABLED } from './updates'; +// Updater toggle functions (moved here to keep updates.ts minimal for release replacement) +let updatesEnabled = UPDATES_ENABLED; + +function toggleUpdates(): boolean { + updatesEnabled = !updatesEnabled; + return updatesEnabled; +} + +function getUpdatesEnabled(): boolean { + // Only return the toggle state, ignore ENABLE_DEV_UPDATES for UI visibility + return updatesEnabled; +} + +function shouldSetupUpdater(): boolean { + // Setup updater if either the toggle is enabled OR dev updates are enabled + return updatesEnabled || process.env.ENABLE_DEV_UPDATES === 'true'; +} + // Define temp directory for pasted images const gooseTempDir = path.join(app.getPath('temp'), 'goose-pasted-images'); @@ -1157,8 +1176,11 @@ const registerGlobalHotkey = (accelerator: string) => { }; app.whenReady().then(async () => { - // Setup auto-updater - UPDATES_ENABLED && setupAutoUpdater(); + // Register update IPC handlers once + registerUpdateIpcHandlers(); + + // Setup auto-updater if enabled + shouldSetupUpdater() && setupAutoUpdater(); // Add CSP headers to all sessions session.defaultSession.webRequest.onHeadersReceived((details, callback) => { @@ -1200,6 +1222,38 @@ app.whenReady().then(async () => { // Register the default global hotkey registerGlobalHotkey('CommandOrControl+Alt+Shift+G'); + // Register hidden key combination to toggle updater (Cmd+Shift+U+P+D+A+T+E) + globalShortcut.register('CommandOrControl+Shift+U', () => { + // This is a multi-key sequence, we'll use a simpler approach + // Register a hidden key combination: Cmd/Ctrl + Alt + Shift + U for "Update toggle" + const newState = toggleUpdates(); + log.info( + `Updater toggled via keyboard shortcut. New state: ${newState ? 'ENABLED' : 'DISABLED'}` + ); + + // Show a notification to the user + new Notification({ + title: 'Goose Updater', + body: `Updates ${newState ? 'enabled' : 'disabled'}`, + }).show(); + + // If we're enabling updates and haven't set up the auto-updater yet, set it up now + if (newState) { + try { + setupAutoUpdater(tray || undefined); + log.info('Auto-updater setup completed after keyboard toggle'); + } catch (error) { + log.error('Error setting up auto-updater after keyboard toggle:', error); + } + } + + // Notify all windows about the updater state change + const windows = BrowserWindow.getAllWindows(); + windows.forEach((win) => { + win.webContents.send('updater-state-changed', newState); + }); + }); + session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => { details.requestHeaders['Origin'] = 'http://localhost:5173'; callback({ cancel: false, requestHeaders: details.requestHeaders }); @@ -1610,6 +1664,11 @@ app.whenReady().then(async () => { ipcMain.on('get-app-version', (event) => { event.returnValue = app.getVersion(); }); + + // Handler for getting updater state + ipcMain.handle('get-updater-enabled', () => { + return getUpdatesEnabled(); + }); }); /** diff --git a/ui/desktop/src/preload.ts b/ui/desktop/src/preload.ts index d79f68a5d6fc..731a9d001dbb 100644 --- a/ui/desktop/src/preload.ts +++ b/ui/desktop/src/preload.ts @@ -89,6 +89,10 @@ type ElectronAPI = { restartApp: () => void; onUpdaterEvent: (callback: (event: UpdaterEvent) => void) => void; getUpdateState: () => Promise<{ updateAvailable: boolean; latestVersion?: string } | null>; + // Updater state functions + getUpdaterEnabled: () => Promise; + onUpdaterStateChanged: (callback: (enabled: boolean) => void) => void; + removeUpdaterStateListener: (callback: (enabled: boolean) => void) => void; }; type AppConfigAPI = { @@ -96,6 +100,12 @@ type AppConfigAPI = { getAll: () => Record; }; +// Store callback wrappers for proper cleanup +const updaterStateCallbacks = new Map< + (enabled: boolean) => void, + (event: Electron.IpcRendererEvent, enabled: boolean) => void +>(); + const electronAPI: ElectronAPI = { platform: process.platform, reactReady: () => ipcRenderer.send('react-ready'), @@ -183,6 +193,22 @@ const electronAPI: ElectronAPI = { getUpdateState: (): Promise<{ updateAvailable: boolean; latestVersion?: string } | null> => { return ipcRenderer.invoke('get-update-state'); }, + // Updater state functions + getUpdaterEnabled: (): Promise => { + return ipcRenderer.invoke('get-updater-enabled'); + }, + onUpdaterStateChanged: (callback: (enabled: boolean) => void): void => { + const wrapper = (_event: Electron.IpcRendererEvent, enabled: boolean) => callback(enabled); + updaterStateCallbacks.set(callback, wrapper); + ipcRenderer.on('updater-state-changed', wrapper); + }, + removeUpdaterStateListener: (callback: (enabled: boolean) => void): void => { + const wrapper = updaterStateCallbacks.get(callback); + if (wrapper) { + ipcRenderer.off('updater-state-changed', wrapper); + updaterStateCallbacks.delete(callback); + } + }, }; const appConfigAPI: AppConfigAPI = { diff --git a/ui/desktop/src/utils/autoUpdater.ts b/ui/desktop/src/utils/autoUpdater.ts index 21d3c80fa84e..524c112e74aa 100644 --- a/ui/desktop/src/utils/autoUpdater.ts +++ b/ui/desktop/src/utils/autoUpdater.ts @@ -30,189 +30,68 @@ let githubUpdateInfo: { // Store update state let lastUpdateState: { updateAvailable: boolean; latestVersion?: string } | null = null; -// Configure auto-updater -export function setupAutoUpdater(tray?: Tray) { - if (tray) { - trayRef = tray; - } - - // Set the feed URL for GitHub releases - autoUpdater.setFeedURL({ - provider: 'github', - owner: 'block', - repo: 'goose', - releaseType: 'release', - }); +// Track if IPC handlers have been registered +let ipcUpdateHandlersRegistered = false; - // Configure auto-updater settings - autoUpdater.autoDownload = false; // We'll trigger downloads manually - autoUpdater.autoInstallOnAppQuit = true; - - // Enable updates in development mode for testing - if (process.env.ENABLE_DEV_UPDATES === 'true') { - autoUpdater.forceDevUpdateConfig = true; +// Register IPC handlers (only once) +export function registerUpdateIpcHandlers() { + if (ipcUpdateHandlersRegistered) { + return; } - // Set logger - autoUpdater.logger = log; - - // Check for updates on startup - setTimeout(() => { - log.info('Checking for updates on startup...'); - autoUpdater.checkForUpdates().catch((err) => { - log.error('Error checking for updates on startup:', err); - // If electron-updater fails, try GitHub API as fallback - if ( - err.message.includes('HttpError: 404') || - err.message.includes('ERR_CONNECTION_REFUSED') || - err.message.includes('ENOTFOUND') - ) { - log.info('Using GitHub API fallback for startup update check...'); - isUsingGitHubFallback = true; - - githubUpdater - .checkForUpdates() - .then((result) => { - if (result.error) { - sendStatusToWindow('error', result.error); - } else if (result.updateAvailable) { - // Store GitHub update info - githubUpdateInfo = { - latestVersion: result.latestVersion, - downloadUrl: result.downloadUrl, - releaseUrl: result.releaseUrl, - }; - - updateAvailable = true; - lastUpdateState = { updateAvailable: true, latestVersion: result.latestVersion }; - updateTrayIcon(true); - sendStatusToWindow('update-available', { version: result.latestVersion }); - } else { - updateAvailable = false; - lastUpdateState = { updateAvailable: false }; - updateTrayIcon(false); - sendStatusToWindow('update-not-available', { - version: autoUpdater.currentVersion.version, - }); - } - }) - .catch((fallbackError) => { - log.error('GitHub fallback also failed on startup:', fallbackError); - }); - } - }); - }, 5000); // Wait 5 seconds after app starts - - // Handle update events - autoUpdater.on('checking-for-update', () => { - log.info('Checking for update...'); - sendStatusToWindow('checking-for-update'); - }); - - autoUpdater.on('update-available', (info: UpdateInfo) => { - log.info('Update available:', info); - updateAvailable = true; - lastUpdateState = { updateAvailable: true, latestVersion: info.version }; - updateTrayIcon(true); - sendStatusToWindow('update-available', info); - }); - - autoUpdater.on('update-not-available', (info: UpdateInfo) => { - log.info('Update not available:', info); - updateAvailable = false; - lastUpdateState = { updateAvailable: false }; - updateTrayIcon(false); - sendStatusToWindow('update-not-available', info); - }); - - autoUpdater.on('error', async (err) => { - log.error('Error in auto-updater:', err); - - // Check if this is a 404 error (missing update files) or connection error - if ( - err.message.includes('HttpError: 404') || - err.message.includes('ERR_CONNECTION_REFUSED') || - err.message.includes('ENOTFOUND') - ) { - log.info('Falling back to GitHub API for update check...'); - isUsingGitHubFallback = true; - - try { - const result = await githubUpdater.checkForUpdates(); - - if (result.error) { - sendStatusToWindow('error', result.error); - } else if (result.updateAvailable) { - // Store GitHub update info - githubUpdateInfo = { - latestVersion: result.latestVersion, - downloadUrl: result.downloadUrl, - releaseUrl: result.releaseUrl, - }; - - updateAvailable = true; - updateTrayIcon(true); - sendStatusToWindow('update-available', { version: result.latestVersion }); - } else { - updateAvailable = false; - updateTrayIcon(false); - sendStatusToWindow('update-not-available', { - version: autoUpdater.currentVersion.version, - }); - } - } catch (fallbackError) { - log.error('GitHub fallback also failed:', fallbackError); - sendStatusToWindow( - 'error', - 'Unable to check for updates. Please check your internet connection.' - ); - } - } else { - sendStatusToWindow('error', err.message); - } - }); - - autoUpdater.on('download-progress', (progressObj) => { - let log_message = 'Download speed: ' + progressObj.bytesPerSecond; - log_message = log_message + ' - Downloaded ' + progressObj.percent + '%'; - log_message = log_message + ' (' + progressObj.transferred + '/' + progressObj.total + ')'; - log.info(log_message); - sendStatusToWindow('download-progress', progressObj); - }); - - autoUpdater.on('update-downloaded', (info: UpdateInfo) => { - log.info('Update downloaded:', info); - sendStatusToWindow('update-downloaded', info); - }); + log.info('Registering update IPC handlers...'); + ipcUpdateHandlersRegistered = true; // IPC handlers for renderer process ipcMain.handle('check-for-updates', async () => { try { + log.info('Manual check for updates requested'); + // Reset fallback flag isUsingGitHubFallback = false; githubUpdateInfo = {}; // Ensure auto-updater is properly initialized if (!autoUpdater.currentVersion) { + log.error('Auto-updater currentVersion is null/undefined'); throw new Error('Auto-updater not initialized. Please restart the application.'); } + log.info( + `About to check for updates with currentVersion: ${JSON.stringify(autoUpdater.currentVersion)}` + ); + log.info(`Feed URL: ${autoUpdater.getFeedURL()}`); + const result = await autoUpdater.checkForUpdates(); + log.info('Auto-updater checkForUpdates result:', result); + return { updateInfo: result?.updateInfo, error: null, }; } catch (error) { log.error('Error checking for updates:', error); + log.error('Manual check error details:', { + message: error instanceof Error ? error.message : 'Unknown error', + stack: error instanceof Error ? error.stack : 'No stack', + name: error instanceof Error ? error.name : 'Unknown', + code: + error instanceof Error && 'code' in error + ? (error as Error & { code: unknown }).code + : undefined, + toString: error?.toString(), + }); // If electron-updater fails, try GitHub API fallback if ( error instanceof Error && (error.message.includes('HttpError: 404') || error.message.includes('ERR_CONNECTION_REFUSED') || - error.message.includes('ENOTFOUND')) + error.message.includes('ENOTFOUND') || + error.message.includes('No published versions')) ) { log.info('Using GitHub API fallback in check-for-updates...'); + log.info('Manual fallback triggered by error:', error.message); isUsingGitHubFallback = true; try { @@ -365,6 +244,222 @@ export function setupAutoUpdater(tray?: Tray) { }); } +// Configure auto-updater +export function setupAutoUpdater(tray?: Tray) { + if (tray) { + trayRef = tray; + } + + log.info('Setting up auto-updater...'); + log.info(`Current app version: ${app.getVersion()}`); + log.info(`Platform: ${process.platform}, Arch: ${process.arch}`); + log.info(`NODE_ENV: ${process.env.NODE_ENV}`); + log.info(`ENABLE_DEV_UPDATES: ${process.env.ENABLE_DEV_UPDATES}`); + log.info(`App is packaged: ${app.isPackaged}`); + log.info(`App path: ${app.getAppPath()}`); + log.info(`Resources path: ${process.resourcesPath}`); + + // Set the feed URL for GitHub releases + const feedConfig = { + provider: 'github' as const, + owner: 'block', + repo: 'goose', + releaseType: 'release' as const, + }; + + log.info('Setting feed URL with config:', feedConfig); + autoUpdater.setFeedURL(feedConfig); + + // Log the feed URL after setting it + try { + const feedUrl = autoUpdater.getFeedURL(); + log.info(`Feed URL set to: ${feedUrl}`); + } catch (e) { + log.error('Error getting feed URL:', e); + } + + // Configure auto-updater settings + autoUpdater.autoDownload = false; // We'll trigger downloads manually + autoUpdater.autoInstallOnAppQuit = true; + + // Enable updates in development mode for testing + if (process.env.ENABLE_DEV_UPDATES === 'true') { + log.info('Enabling dev updates config'); + autoUpdater.forceDevUpdateConfig = true; + } + + // Additional debugging for release builds + if (app.isPackaged) { + log.info('App is packaged - this is a release build'); + // Try to get more info about the updater configuration + try { + log.info(`Auto-updater channel: ${autoUpdater.channel}`); + log.info(`Auto-updater allowPrerelease: ${autoUpdater.allowPrerelease}`); + log.info(`Auto-updater allowDowngrade: ${autoUpdater.allowDowngrade}`); + } catch (e) { + log.error('Error getting auto-updater properties:', e); + } + } else { + log.info('App is not packaged - this is a development build'); + } + + // Set logger + autoUpdater.logger = log; + + log.info('Auto-updater setup completed'); + + // Check for updates on startup + setTimeout(() => { + log.info('Checking for updates on startup...'); + log.info(`autoUpdater.currentVersion: ${JSON.stringify(autoUpdater.currentVersion)}`); + log.info(`autoUpdater.getFeedURL(): ${autoUpdater.getFeedURL()}`); + + autoUpdater.checkForUpdates().catch((err) => { + log.error('Error checking for updates on startup:', err); + log.error('Error details:', { + message: err.message, + stack: err.stack, + name: err.name, + code: 'code' in err ? err.code : undefined, + }); + + // If electron-updater fails, try GitHub API as fallback + if ( + err.message.includes('HttpError: 404') || + err.message.includes('ERR_CONNECTION_REFUSED') || + err.message.includes('ENOTFOUND') || + err.message.includes('No published versions') + ) { + log.info('Using GitHub API fallback for startup update check...'); + log.info('Fallback triggered by error containing:', err.message); + isUsingGitHubFallback = true; + + githubUpdater + .checkForUpdates() + .then((result) => { + if (result.error) { + sendStatusToWindow('error', result.error); + } else if (result.updateAvailable) { + // Store GitHub update info + githubUpdateInfo = { + latestVersion: result.latestVersion, + downloadUrl: result.downloadUrl, + releaseUrl: result.releaseUrl, + }; + + updateAvailable = true; + lastUpdateState = { updateAvailable: true, latestVersion: result.latestVersion }; + updateTrayIcon(true); + sendStatusToWindow('update-available', { version: result.latestVersion }); + } else { + updateAvailable = false; + lastUpdateState = { updateAvailable: false }; + updateTrayIcon(false); + sendStatusToWindow('update-not-available', { + version: autoUpdater.currentVersion.version, + }); + } + }) + .catch((fallbackError) => { + log.error('GitHub fallback also failed on startup:', fallbackError); + }); + } + }); + }, 5000); // Wait 5 seconds after app starts + + // Handle update events + autoUpdater.on('checking-for-update', () => { + log.info('Auto-updater: Checking for update...'); + log.info(`Auto-updater: Feed URL during check: ${autoUpdater.getFeedURL()}`); + sendStatusToWindow('checking-for-update'); + }); + + autoUpdater.on('update-available', (info: UpdateInfo) => { + log.info('Update available:', info); + updateAvailable = true; + lastUpdateState = { updateAvailable: true, latestVersion: info.version }; + updateTrayIcon(true); + sendStatusToWindow('update-available', info); + }); + + autoUpdater.on('update-not-available', (info: UpdateInfo) => { + log.info('Update not available:', info); + updateAvailable = false; + lastUpdateState = { updateAvailable: false }; + updateTrayIcon(false); + sendStatusToWindow('update-not-available', info); + }); + + autoUpdater.on('error', async (err) => { + log.error('Error in auto-updater:', err); + log.error('Auto-updater error details:', { + message: err.message, + stack: err.stack, + name: err.name, + code: 'code' in err ? err.code : undefined, + toString: err.toString(), + }); + + // Check if this is a 404 error (missing update files) or connection error + if ( + err.message.includes('HttpError: 404') || + err.message.includes('ERR_CONNECTION_REFUSED') || + err.message.includes('ENOTFOUND') || + err.message.includes('No published versions') + ) { + log.info('Falling back to GitHub API for update check...'); + log.info('Fallback triggered by error:', err.message); + isUsingGitHubFallback = true; + + try { + const result = await githubUpdater.checkForUpdates(); + + if (result.error) { + sendStatusToWindow('error', result.error); + } else if (result.updateAvailable) { + // Store GitHub update info + githubUpdateInfo = { + latestVersion: result.latestVersion, + downloadUrl: result.downloadUrl, + releaseUrl: result.releaseUrl, + }; + + updateAvailable = true; + updateTrayIcon(true); + sendStatusToWindow('update-available', { version: result.latestVersion }); + } else { + updateAvailable = false; + updateTrayIcon(false); + sendStatusToWindow('update-not-available', { + version: autoUpdater.currentVersion.version, + }); + } + } catch (fallbackError) { + log.error('GitHub fallback also failed:', fallbackError); + sendStatusToWindow( + 'error', + 'Unable to check for updates. Please check your internet connection.' + ); + } + } else { + sendStatusToWindow('error', err.message); + } + }); + + autoUpdater.on('download-progress', (progressObj) => { + let log_message = 'Download speed: ' + progressObj.bytesPerSecond; + log_message = log_message + ' - Downloaded ' + progressObj.percent + '%'; + log_message = log_message + ' (' + progressObj.transferred + '/' + progressObj.total + ')'; + log.info(log_message); + sendStatusToWindow('download-progress', progressObj); + }); + + autoUpdater.on('update-downloaded', (info: UpdateInfo) => { + log.info('Update downloaded:', info); + sendStatusToWindow('update-downloaded', info); + }); +} + interface UpdaterEvent { event: string; data?: unknown; diff --git a/ui/desktop/src/utils/githubUpdater.ts b/ui/desktop/src/utils/githubUpdater.ts index c6d4dd4fddb6..28dbaf3e5950 100644 --- a/ui/desktop/src/utils/githubUpdater.ts +++ b/ui/desktop/src/utils/githubUpdater.ts @@ -34,6 +34,8 @@ export class GitHubUpdater { async checkForUpdates(): Promise { try { log.info('GitHubUpdater: Checking for updates via GitHub API...'); + log.info(`GitHubUpdater: API URL: ${this.apiUrl}`); + log.info(`GitHubUpdater: Current app version: ${app.getVersion()}`); const response = await fetch(this.apiUrl, { headers: { @@ -42,11 +44,21 @@ export class GitHubUpdater { }, }); + log.info( + `GitHubUpdater: GitHub API response status: ${response.status} ${response.statusText}` + ); + if (!response.ok) { + const errorText = await response.text(); + log.error(`GitHubUpdater: GitHub API error response: ${errorText}`); throw new Error(`GitHub API returned ${response.status}: ${response.statusText}`); } const release: GitHubRelease = await response.json(); + log.info(`GitHubUpdater: Found release: ${release.tag_name} (${release.name})`); + log.info(`GitHubUpdater: Release published at: ${release.published_at}`); + log.info(`GitHubUpdater: Release assets count: ${release.assets.length}`); + const latestVersion = release.tag_name.replace(/^v/, ''); // Remove 'v' prefix if present const currentVersion = app.getVersion(); @@ -56,6 +68,7 @@ export class GitHubUpdater { // Compare versions const updateAvailable = compareVersions(latestVersion, currentVersion) > 0; + log.info(`GitHubUpdater: Update available: ${updateAvailable}`); if (!updateAvailable) { return { @@ -70,6 +83,8 @@ export class GitHubUpdater { let downloadUrl: string | undefined; let assetName: string; + log.info(`GitHubUpdater: Looking for asset for platform: ${platform}, arch: ${arch}`); + if (platform === 'darwin') { // macOS if (arch === 'arm64') { @@ -85,9 +100,16 @@ export class GitHubUpdater { assetName = `Goose-linux-${arch}.zip`; } + log.info(`GitHubUpdater: Looking for asset named: ${assetName}`); + log.info(`GitHubUpdater: Available assets: ${release.assets.map((a) => a.name).join(', ')}`); + const asset = release.assets.find((a) => a.name === assetName); if (asset) { downloadUrl = asset.browser_download_url; + log.info(`GitHubUpdater: Found matching asset: ${asset.name} (${asset.size} bytes)`); + log.info(`GitHubUpdater: Download URL: ${downloadUrl}`); + } else { + log.warn(`GitHubUpdater: No matching asset found for ${assetName}`); } return { @@ -98,6 +120,15 @@ export class GitHubUpdater { }; } catch (error) { log.error('GitHubUpdater: Error checking for updates:', error); + log.error('GitHubUpdater: Error details:', { + message: error instanceof Error ? error.message : 'Unknown error', + stack: error instanceof Error ? error.stack : 'No stack', + name: error instanceof Error ? error.name : 'Unknown', + code: + error instanceof Error && 'code' in error + ? (error as Error & { code: unknown }).code + : undefined, + }); return { updateAvailable: false, error: error instanceof Error ? error.message : 'Unknown error', From 97cebe0e1d9ac7aaeddbd7cbfb5a67c067aead7a Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Fri, 13 Jun 2025 13:13:53 -0700 Subject: [PATCH 3/3] change to use platform agnostic and secure zip library --- ui/desktop/package-lock.json | 33 ++++--- ui/desktop/package.json | 4 +- ui/desktop/src/utils/githubUpdater.ts | 137 ++++++++++++++++++++------ 3 files changed, 133 insertions(+), 41 deletions(-) diff --git a/ui/desktop/package-lock.json b/ui/desktop/package-lock.json index 27ccf095403a..89d02a6d92f9 100644 --- a/ui/desktop/package-lock.json +++ b/ui/desktop/package-lock.json @@ -27,6 +27,7 @@ "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@types/react-syntax-highlighter": "^15.5.13", + "@types/yauzl": "^2.10.3", "ai": "^3.4.33", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -53,7 +54,8 @@ "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", "unist-util-visit": "^5.0.0", - "uuid": "^11.1.0" + "uuid": "^11.1.0", + "yauzl": "^3.0.0" }, "devDependencies": { "@electron-forge/cli": "^7.5.0", @@ -4314,7 +4316,6 @@ "version": "22.15.28", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.28.tgz", "integrity": "sha512-I0okKVDmyKR281I0UIFV7EWAWRnR0gkuSKob5wVcByyyhr7Px/slhkQapcYX4u00ekzNWaS1gznKZnuzxwo4pw==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -4433,9 +4434,7 @@ "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, "license": "MIT", - "optional": true, "dependencies": { "@types/node": "*" } @@ -5551,7 +5550,6 @@ "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, "license": "MIT", "engines": { "node": "*" @@ -8219,6 +8217,17 @@ "@types/yauzl": "^2.9.1" } }, + "node_modules/extract-zip/node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -13002,7 +13011,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, "license": "MIT" }, "node_modules/perfect-debounce": { @@ -16232,7 +16240,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unified": { @@ -17259,14 +17266,16 @@ } }, "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.0.tgz", + "integrity": "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==", "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/yocto-queue": { diff --git a/ui/desktop/package.json b/ui/desktop/package.json index 2646f2b32ab1..4e3906a7df6d 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -101,6 +101,7 @@ "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@types/react-syntax-highlighter": "^15.5.13", + "@types/yauzl": "^2.10.3", "ai": "^3.4.33", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -127,6 +128,7 @@ "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", "unist-util-visit": "^5.0.0", - "uuid": "^11.1.0" + "uuid": "^11.1.0", + "yauzl": "^3.0.0" } } diff --git a/ui/desktop/src/utils/githubUpdater.ts b/ui/desktop/src/utils/githubUpdater.ts index 28dbaf3e5950..b4bb2d7f5557 100644 --- a/ui/desktop/src/utils/githubUpdater.ts +++ b/ui/desktop/src/utils/githubUpdater.ts @@ -1,9 +1,10 @@ import { app } from 'electron'; import { compareVersions } from 'compare-versions'; import * as fs from 'fs/promises'; +import { createWriteStream } from 'fs'; import * as path from 'path'; import * as os from 'os'; -import { spawn } from 'child_process'; +import * as yauzl from 'yauzl'; import log from './logger'; interface GitHubRelease { @@ -190,40 +191,17 @@ export class GitHubUpdater { log.info(`GitHubUpdater: Update downloaded to ${downloadPath}`); - // Auto-unzip the downloaded file + // Auto-unzip the downloaded file using yauzl (secure ZIP library) try { const tempExtractDir = path.join(downloadsDir, `temp-extract-${Date.now()}`); // Create temp extraction directory await fs.mkdir(tempExtractDir, { recursive: true }); - // Use unzip command to extract - log.info(`GitHubUpdater: Extracting ${fileName} to temp directory`); + log.info(`GitHubUpdater: Extracting ${fileName} to temp directory using yauzl`); - const unzipProcess = spawn('unzip', ['-o', downloadPath, '-d', tempExtractDir]); - - let stderr = ''; - unzipProcess.stderr.on('data', (data) => { - stderr += data.toString(); - }); - - await new Promise((resolve, reject) => { - unzipProcess.on('close', (code) => { - if (code === 0) { - resolve(); - } else { - reject(new Error(`Unzip process exited with code ${code}`)); - } - }); - - unzipProcess.on('error', (err) => { - reject(err); - }); - }); - - if (stderr && !stderr.includes('warning')) { - log.warn(`GitHubUpdater: Unzip stderr: ${stderr}`); - } + // Use yauzl to extract the ZIP file securely + await extractZipFile(downloadPath, tempExtractDir); // Check if Goose.app exists in the extracted content const appPath = path.join(tempExtractDir, 'Goose.app'); @@ -287,5 +265,108 @@ export class GitHubUpdater { } } +/** + * Securely extract a ZIP file using yauzl with security checks + * @param zipPath Path to the ZIP file + * @param extractDir Directory to extract to + */ +async function extractZipFile(zipPath: string, extractDir: string): Promise { + return new Promise((resolve, reject) => { + yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => { + if (err) { + reject(err); + return; + } + + if (!zipfile) { + reject(new Error('Failed to open ZIP file')); + return; + } + + zipfile.readEntry(); + + zipfile.on('entry', async (entry: yauzl.Entry) => { + try { + // Security check: prevent directory traversal attacks + if (entry.fileName.includes('..') || path.isAbsolute(entry.fileName)) { + log.warn(`GitHubUpdater: Skipping potentially dangerous path: ${entry.fileName}`); + zipfile.readEntry(); + return; + } + + const fullPath = path.join(extractDir, entry.fileName); + + // Ensure the resolved path is still within the extraction directory + const resolvedPath = path.resolve(fullPath); + const resolvedExtractDir = path.resolve(extractDir); + if (!resolvedPath.startsWith(resolvedExtractDir + path.sep)) { + log.warn(`GitHubUpdater: Path traversal attempt detected: ${entry.fileName}`); + zipfile.readEntry(); + return; + } + + // Handle directories + if (entry.fileName.endsWith('/')) { + await fs.mkdir(fullPath, { recursive: true }); + zipfile.readEntry(); + return; + } + + // Handle files + zipfile.openReadStream(entry, async (err, readStream) => { + if (err) { + reject(err); + return; + } + + if (!readStream) { + reject(new Error('Failed to open read stream')); + return; + } + + try { + // Ensure parent directory exists + await fs.mkdir(path.dirname(fullPath), { recursive: true }); + + // Create write stream + const writeStream = createWriteStream(fullPath); + + readStream.on('end', () => { + writeStream.end(); + zipfile.readEntry(); + }); + + readStream.on('error', (streamErr) => { + writeStream.destroy(); + reject(streamErr); + }); + + writeStream.on('error', (writeErr: Error) => { + reject(writeErr); + }); + + // Pipe the data + readStream.pipe(writeStream); + } catch (fileErr) { + reject(fileErr); + } + }); + } catch (entryErr) { + reject(entryErr); + } + }); + + zipfile.on('end', () => { + log.info('GitHubUpdater: ZIP extraction completed successfully'); + resolve(); + }); + + zipfile.on('error', (zipErr) => { + reject(zipErr); + }); + }); + }); +} + // Create singleton instance export const githubUpdater = new GitHubUpdater();