Files
fluxer/fluxer_desktop/src/main/index.ts
T

720 lines
26 KiB
TypeScript

// SPDX-License-Identifier: AGPL-3.0-or-later
import fs from 'node:fs';
import path from 'node:path';
import {getDesktopWindowBehaviorSettings, loadDesktopConfig} from '@electron/common/DesktopConfig';
import {
DESKTOP_APP_NAME,
LINUX_DESKTOP_ENTRY_ID,
WINDOWS_APP_USER_MODEL_ID,
WINDOWS_TOAST_ACTIVATOR_CLSID,
} from '@electron/common/DesktopIdentity';
import {writeLogFilesUnder} from '@electron/common/Logger';
import {configureUserDataPath} from '@electron/common/UserDataPath';
import {
APP_STORE_ADDON_PACKAGE,
createAppStoreBoundary,
getAppStoreLoadFailure,
loadAppStoreBinding,
} from '@electron/main/AppStoreNativeBoundary';
import {registerAutostartHandlers} from '@electron/main/Autostart';
import {isStartMinimizedLaunch} from '@electron/main/AutostartLaunch';
import {
appendWindowsGpuDriverWorkaroundSwitches,
purgeChromiumRuntimeCachesIfNeeded,
} from '@electron/main/ChromiumRuntime';
import {
consumeInitialJumpListTask,
handleOpenUrl,
handleSecondInstance,
initializeDeepLinks,
} from '@electron/main/DeepLinks';
import {
DesktopAccountStoreEvent,
DesktopStorePermissionState,
probeDesktopStorePermissions,
recordDesktopAccountStoreEvent,
} from '@electron/main/DesktopAccountStoreTelemetry';
import {
closeDesktopAppStorage,
getDesktopAppStorage,
openDesktopAppStorage,
recoverDesktopAppStorage,
} from '@electron/main/DesktopAppStorage';
import {
DesktopAppStoreHealth,
desktopAppStoreStateFile,
evaluateDesktopAppStoreHealth,
readDesktopAppStoreState,
recordDesktopAppStoreSuccess,
} from '@electron/main/DesktopAppStoreHealth';
import {
formatDesktopDebugInfo,
getDesktopDebugInfo,
getLaunchNetLogPath,
hasDesktopDebugInfoArg,
logDesktopDebugInfo,
shouldResetWindowStateOnLaunch,
} from '@electron/main/DesktopDebugInfo';
import {recordDesktopLastRoute} from '@electron/main/DesktopLastRoute';
import {cleanupDesktopOutboundHTTP} from '@electron/main/DesktopOutboundHTTP';
import {registerDesktopRuntimeConfigHandlers} from '@electron/main/DesktopRuntimeConfigIpc';
import {installDesktopSystemTrustVerifier} from '@electron/main/DesktopSystemTrustVerifier';
import {destroyDesktopTray, hasActiveDesktopTray, initializeDesktopTray} from '@electron/main/DesktopTray';
import {registerDisplayMediaHandlers} from '@electron/main/DisplayMedia';
import {initializeDockMenu} from '@electron/main/DockMenu';
import {GATEWAY_SOCKET_ADDON_PACKAGE, loadGatewaySocketBinding} from '@electron/main/GatewaySocketNativeBoundary';
import {cleanupGlobalShortcuts, initializeGlobalShortcuts} from '@electron/main/GlobalShortcutsIpc';
import {
cleanupGatewayTransportHandlers,
cleanupIpcHandlers,
registerDesktopAppStorageHandlers,
registerGatewayTransportHandlers,
registerIpcHandlers,
} from '@electron/main/IpcHandlers';
import {initializeJumpList} from '@electron/main/JumpList';
import {describeLaunchDiagnosticOptions} from '@electron/main/LaunchOptions';
import {cleanupLegacyHarvestHandlers, registerLegacyHarvestHandlers} from '@electron/main/LegacyHarvestIpc';
import {initializeLegacyOriginHarvest} from '@electron/main/LegacyOriginHarvest';
import {cleanupVirtmic, registerVirtmicHandlers} from '@electron/main/LinuxAudioCapture';
import {ensureLinuxDesktopEntry} from '@electron/main/LinuxDesktopEntry';
import {cleanupDesktopLocalAppProtocol, getDesktopLocalAppProtocol} from '@electron/main/LocalAppProtocol';
import {initializeMainI18n, t} from '@electron/main/MainI18n';
import {createApplicationMenu} from '@electron/main/Menu';
import {
armOpenUrlForwarding,
armSecondInstanceForwarding,
setMainWindowFactory,
setOpenUrlSink,
setSecondInstanceSink,
} from '@electron/main/ModuleBootHandoff';
import {cleanupNativeAudio, registerNativeAudioHandlers} from '@electron/main/NativeAudio';
import {
cleanupNativeHardwareEncoderHandlers,
registerNativeHardwareEncoderHandlers,
} from '@electron/main/NativeHardwareEncoder';
import {type NativeModulePreflightResult, runNativeModulePreflight} from '@electron/main/NativeModulePreflight';
import {armNativeProbeCache} from '@electron/main/NativeProbeCache';
import {cleanupNativeScreenCapture, registerNativeScreenCaptureHandlers} from '@electron/main/NativeScreenCapture';
import {applyPreReadyChromiumConfiguration} from '@electron/main/PreReadyChromium';
import {cleanupLinuxChromiumSpellcheckDictionaries} from '@electron/main/Spellcheck';
import {
clearSavedWindowBounds,
createWindow,
getMainWindow,
hideWindow,
isMainWindowTakenOver,
setQuitting,
showWindow,
} from '@electron/main/Window';
import {removeLegacySquirrelUninstallEntry} from '@electron/main/WindowsLegacyUninstallEntry';
import {removeFluxerVulkanLayerRegistrations} from '@electron/main/WindowsVulkanLayerCleanup';
import {
DESKTOP_CAPABILITY_MANIFEST_CHANNEL,
type DesktopCapabilityManifest,
} from '@fluxer/desktop_ipc/src/CapabilityManifest';
import {DESKTOP_LAST_ROUTE_CHANNEL} from '@fluxer/desktop_ipc/src/LastRouteContract';
import {
DESKTOP_LEGACY_IMPORT_FAILURE_MARKER_KEY,
DESKTOP_LEGACY_IMPORT_MARKER_KEY,
DESKTOP_LEGACY_SESSION_MARKER_KEY,
DesktopLegacyImportPhase,
readDesktopLegacyImportPhase,
} from '@fluxer/desktop_ipc/src/StorageContract';
import {app, dialog, ipcMain, netLog, session, shell} from 'electron';
import log from 'electron-log';
log.transports.file.level = 'info';
log.transports.file.sync = false;
log.transports.console.level = 'debug';
if (process.platform === 'linux' && process.env.PULSE_LATENCY_MSEC === undefined) {
process.env.PULSE_LATENCY_MSEC = '30';
}
if (process.platform === 'linux') {
process.env['PULSE_PROP_OVERRIDE_application.name'] = DESKTOP_APP_NAME;
}
process.on('uncaughtException', (error) => {
try {
log.error('Uncaught exception (early):', error);
} catch {}
});
process.on('unhandledRejection', (reason, promise) => {
try {
log.error('Unhandled rejection (early) at:', promise, 'reason:', reason);
} catch {}
});
const userDataConfig = configureUserDataPath();
if (userDataConfig.portable) {
writeLogFilesUnder(app.getPath('logs'));
}
armNativeProbeCache(userDataConfig.base);
const processConfiguredAt = Date.now();
log.info('Configured user data storage', {
channel: userDataConfig.channel,
directory: userDataConfig.directoryName,
path: userDataConfig.base,
portable: userDataConfig.portable,
});
cleanupLinuxChromiumSpellcheckDictionaries(userDataConfig.base);
loadDesktopConfig(userDataConfig.base);
function exitCli(code: number): void {
process.exitCode = code;
setImmediate(() => {
process.exit(code);
});
}
function reportUnreadableDesktopAppStore(storeFile: string, reason: string | null): void {
log.error('[AppStore] The desktop app store worked here before and cannot be opened now', {storeFile, reason});
let choice = 0;
try {
choice = dialog.showMessageBoxSync({
type: 'error',
title: t('desktop.appStore.unreadableTitle'),
message: t('desktop.appStore.unreadableMessage'),
detail: [
t('desktop.appStore.unreadableUnchanged'),
t('desktop.appStore.unreadableAdvice'),
reason == null ? '' : t('desktop.appStore.unreadableDetails', {reason}),
]
.filter(Boolean)
.join('\n\n'),
buttons: [t('desktop.appMenu.quit'), t('desktop.appStore.showStoreFolder')],
defaultId: 0,
cancelId: 0,
noLink: true,
});
} catch (error) {
log.error('[AppStore] Failed to present the unreadable store notice:', error);
}
if (choice === 1) {
try {
shell.showItemInFolder(storeFile);
} catch (error) {
log.error('[AppStore] Failed to reveal the store folder:', error);
}
}
app.exit(1);
}
async function recordDesktopAppStoreImportState(schemaVersion: number): Promise<void> {
const storage = getDesktopAppStorage();
if (storage == null) {
return;
}
try {
const marker = await storage.getMarker(DESKTOP_LEGACY_IMPORT_MARKER_KEY);
const phase = readDesktopLegacyImportPhase(marker);
log.info('[AppStore] Legacy import state', {
phase,
failures: await storage.getMarker(DESKTOP_LEGACY_IMPORT_FAILURE_MARKER_KEY),
reconciledAt: await storage.getMarker(DESKTOP_LEGACY_SESSION_MARKER_KEY),
stateFile: desktopAppStoreStateFile(userDataConfig.base),
});
if (phase === DesktopLegacyImportPhase.DONE) {
recordDesktopAppStoreSuccess({userDataPath: userDataConfig.base, now: Date.now(), schemaVersion});
recordDesktopAccountStoreEvent(userDataConfig.base, DesktopAccountStoreEvent.MIGRATED);
}
} catch (error) {
log.warn('[AppStore] Failed to read the legacy import state:', error);
}
}
function writeCliAndExit(stream: NodeJS.WriteStream, message: string, code: number): void {
let done = false;
const finish = (): void => {
if (done) return;
done = true;
stream.off('error', finish);
exitCli(code);
};
stream.once('error', finish);
stream.write(`${message}\n`, finish);
}
let launchConfigurationError: Error | null = null;
let launchDiagnosticOptions: Record<string, unknown> = {};
try {
launchDiagnosticOptions = describeLaunchDiagnosticOptions(process.argv);
} catch (error) {
launchConfigurationError = error instanceof Error ? error : new Error(String(error));
}
if (launchConfigurationError) {
console.error(`Fluxer desktop launch configuration error: ${launchConfigurationError.message}`);
log.error('Launch configuration error:', launchConfigurationError);
app.exit(1);
} else if (hasDesktopDebugInfoArg(process.argv)) {
void getDesktopDebugInfo(userDataConfig.base, {nativeProbes: false})
.then((info) => {
try {
logDesktopDebugInfo(info);
} catch {}
writeCliAndExit(process.stdout, formatDesktopDebugInfo(info), 0);
})
.catch((error: unknown) => {
const message = error instanceof Error ? error.message : String(error);
log.error('Failed to collect desktop debug info:', error);
writeCliAndExit(process.stderr, `Failed to collect Fluxer desktop debug info: ${message}`, 1);
});
} else {
if (shouldResetWindowStateOnLaunch(process.argv)) {
clearSavedWindowBounds();
}
applyPreReadyChromiumConfiguration(userDataConfig.channel, process.argv);
log.info('Launch diagnostic modes', launchDiagnosticOptions);
const CHANNEL_APP_NAME = DESKTOP_APP_NAME;
app.setName(CHANNEL_APP_NAME);
function recordStartupPhase(phase: string, phaseStartedAt: number): void {
log.info('[Startup] Phase completed', {
phase,
durationMs: Date.now() - phaseStartedAt,
sinceConfiguredMs: Date.now() - processConfiguredAt,
});
}
function runStartupPhase<T>(phase: string, fn: () => T): T {
const phaseStartedAt = Date.now();
try {
return fn();
} finally {
recordStartupPhase(phase, phaseStartedAt);
}
}
async function runStartupPhaseAsync<T>(phase: string, fn: () => Promise<T>): Promise<T> {
const phaseStartedAt = Date.now();
try {
return await fn();
} finally {
recordStartupPhase(phase, phaseStartedAt);
}
}
async function startLaunchNetLog(): Promise<void> {
const netLogPath = getLaunchNetLogPath(userDataConfig.base, process.argv);
if (!netLogPath) {
return;
}
try {
fs.mkdirSync(path.dirname(netLogPath), {recursive: true});
await netLog.startLogging(netLogPath, {captureMode: 'default'});
log.info('[DebugInfo] Chromium net log started', {path: netLogPath});
} catch (error) {
log.error('[DebugInfo] Failed to start Chromium net log:', error);
}
}
const isLegacySquirrelStartupArg = (arg: string): boolean =>
arg === '--squirrel-install' ||
arg === '--squirrel-updated' ||
arg === '--squirrel-uninstall' ||
arg === '--squirrel-obsolete' ||
arg === '--squirrel-firstrun';
const hasLegacySquirrelArg = process.platform === 'win32' && process.argv.some(isLegacySquirrelStartupArg);
if (hasLegacySquirrelArg) {
app.exit(0);
}
try {
runStartupPhase('main-i18n', initializeMainI18n);
} catch (error) {
log.error('[Init] Failed to initialize native i18n:', error);
}
let nativeModulePreflight: NativeModulePreflightResult = {degraded: []};
try {
nativeModulePreflight = runStartupPhase('native-module-preflight', runNativeModulePreflight);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
log.error('[NativeModulePreflight] Fatal native module preflight failure:', error);
console.error(message);
try {
dialog.showErrorBox(t('desktop.startup.failedTitle'), message);
} catch {}
app.exit(1);
process.exit(1);
}
const degradedNativeModules = new Set(nativeModulePreflight.degraded);
const desktopCapabilityManifest: DesktopCapabilityManifest = {
appStore: !degradedNativeModules.has(APP_STORE_ADDON_PACKAGE) && loadAppStoreBinding() !== null,
gatewaySocket: !degradedNativeModules.has(GATEWAY_SOCKET_ADDON_PACKAGE) && loadGatewaySocketBinding() !== null,
};
log.info('[Capabilities] Desktop capability manifest', desktopCapabilityManifest);
ipcMain.on(DESKTOP_CAPABILITY_MANIFEST_CHANNEL, (event) => {
event.returnValue = desktopCapabilityManifest;
});
ipcMain.on(DESKTOP_LAST_ROUTE_CHANNEL, (_event, routePath: unknown) => {
recordDesktopLastRoute(app.getPath('userData'), routePath);
});
if (process.platform === 'win32') {
app.setToastActivatorCLSID(WINDOWS_TOAST_ACTIVATOR_CLSID);
app.setAppUserModelId(WINDOWS_APP_USER_MODEL_ID);
}
try {
runStartupPhase('local-app-scheme', () => {
getDesktopLocalAppProtocol().registerSchemes();
});
} catch (error) {
log.error('[Init] Failed to register the local app scheme privileges:', error);
app.exit(1);
process.exit(1);
}
const startupController = new AbortController();
let startupWindowsPending = true;
const gotTheLock = app.hasSingleInstanceLock() || app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
runStartupPhase('runtime-cache-guard', () => {
purgeChromiumRuntimeCachesIfNeeded(userDataConfig.base);
});
armSecondInstanceForwarding();
setSecondInstanceSink(handleSecondInstance);
setMainWindowFactory(() => createWindow());
app.on('child-process-gone', (_event, details) => {
log.error('Child process gone', details);
});
armOpenUrlForwarding();
setOpenUrlSink(handleOpenUrl);
(app.isReady()
? Promise.resolve()
: runStartupPhaseAsync('gpu-driver-workarounds', appendWindowsGpuDriverWorkaroundSwitches)
)
.then(() => app.whenReady())
.then(async () => {
log.info('App ready, initializing...');
try {
runStartupPhase('host-resolver', () => app.configureHostResolver({enableAdditionalDnsQueryTypes: false}));
} catch (error) {
log.error('[Init] Failed to configure the host resolver:', error);
}
await runStartupPhaseAsync('launch-net-log', startLaunchNetLog);
try {
runStartupPhase('system-trust', () => installDesktopSystemTrustVerifier(session.defaultSession));
} catch (error) {
log.error('[Init] Failed to install the system trust verifier:', error);
}
try {
await runStartupPhaseAsync('desktop-debug-info', async () => {
logDesktopDebugInfo(await getDesktopDebugInfo(userDataConfig.base, {nativeProbes: false}));
});
} catch (error) {
log.error('[DebugInfo] Failed to collect desktop debug info:', error);
}
try {
runStartupPhase('linux-desktop-entry', () => {
if (ensureLinuxDesktopEntry()) {
process.env.FLUXER_LINUX_PORTAL_APP_ID = LINUX_DESKTOP_ENTRY_ID;
}
});
} catch (error) {
log.error('[Init] Failed to ensure the Linux desktop entry:', error);
}
try {
runStartupPhase('deep-links', initializeDeepLinks);
} catch (error) {
log.error('[Init] Failed to initialize deep links:', error);
}
try {
runStartupPhase('jump-list', initializeJumpList);
} catch (error) {
log.error('[Init] Failed to initialize JumpList:', error);
}
try {
runStartupPhase('dock-menu', initializeDockMenu);
} catch (error) {
log.error('[Init] Failed to initialize macOS dock menu:', error);
}
let appStoreSchemaVersion = 0;
try {
const appStore = runStartupPhase('app-store-open', () =>
openDesktopAppStorage({
userDataPath: userDataConfig.base,
createBoundary: createAppStoreBoundary,
describeLoadFailure: getAppStoreLoadFailure,
}),
);
const health = evaluateDesktopAppStoreHealth({
available: appStore.status.available,
state: readDesktopAppStoreState(userDataConfig.base),
});
log.info('[AppStore] Desktop app store opened', {
available: appStore.status.available,
schemaVersion: appStore.status.schemaVersion,
storeFile: appStore.storeFile,
quarantineReason: appStore.status.quarantineReason,
quarantinedFile: appStore.quarantinedFile,
unavailableReason: appStore.status.unavailableReason,
health,
});
if (!appStore.status.available) {
recordDesktopAccountStoreEvent(userDataConfig.base, DesktopAccountStoreEvent.FALLBACK_TO_WEB);
if (health === DesktopAppStoreHealth.REGRESSED) {
reportUnreadableDesktopAppStore(appStore.storeFile, appStore.status.unavailableReason);
}
}
if (appStore.status.quarantined) {
recordDesktopAccountStoreEvent(userDataConfig.base, DesktopAccountStoreEvent.QUARANTINED);
}
const permissions = probeDesktopStorePermissions(appStore.storeFile);
if (permissions === DesktopStorePermissionState.WIDENED) {
recordDesktopAccountStoreEvent(userDataConfig.base, DesktopAccountStoreEvent.PERMISSION_HARDENING_FAILED);
log.warn(
'[AppStore] The account store is readable beyond this user and permission hardening did not hold',
{
storeFile: appStore.storeFile,
},
);
}
appStoreSchemaVersion = appStore.status.schemaVersion;
} catch (error) {
log.error('[AppStore] Failed to open the desktop app store:', error);
}
try {
const recovery = await runStartupPhaseAsync('app-store-recovery', recoverDesktopAppStorage);
if (recovery.reseedRequired) {
recordDesktopAccountStoreEvent(userDataConfig.base, DesktopAccountStoreEvent.QUARANTINED);
log.warn('[AppStore] The previous store was quarantined and the renderer re-seeds the fresh one', {
quarantineReason: recovery.status.quarantineReason,
});
}
await recordDesktopAppStoreImportState(appStoreSchemaVersion);
} catch (error) {
log.error('[AppStore] Desktop app store recovery failed:', error);
}
try {
runStartupPhase('app-store-ipc', registerDesktopAppStorageHandlers);
} catch (error) {
log.error('[AppStore] Failed to register desktop app store IPC handlers:', error);
}
try {
runStartupPhase('gateway-transport-ipc', registerGatewayTransportHandlers);
} catch (error) {
log.error('[NativeGateway] Failed to register native gateway transport IPC handlers:', error);
}
try {
runStartupPhase('ipc-handlers', registerIpcHandlers);
} catch (error) {
log.error('[Init] Failed to register IPC handlers:', error);
}
try {
runStartupPhase('autostart-handlers', registerAutostartHandlers);
} catch (error) {
log.error('[Init] Failed to register autostart handlers:', error);
}
try {
runStartupPhase('global-shortcuts', initializeGlobalShortcuts);
} catch (error) {
log.error('[Init] Failed to initialize global shortcuts:', error);
}
try {
runStartupPhase('display-media-handlers', registerDisplayMediaHandlers);
} catch (error: unknown) {
log.error('[Init] Failed to register display media handlers:', error);
}
try {
runStartupPhase('virtmic-handlers', registerVirtmicHandlers);
} catch (error: unknown) {
log.error('[Init] Failed to register virtmic handlers:', error);
}
try {
runStartupPhase('native-audio-handlers', registerNativeAudioHandlers);
} catch (error: unknown) {
log.error('[Init] Failed to register native audio handlers:', error);
}
try {
runStartupPhase('vulkan-layer-cleanup', removeFluxerVulkanLayerRegistrations);
} catch (error: unknown) {
log.error('[Init] Failed to remove stale Vulkan layer registrations:', error);
}
try {
runStartupPhase('legacy-uninstall-entry-cleanup', removeLegacySquirrelUninstallEntry);
} catch (error: unknown) {
log.error('[Init] Failed to remove the legacy Squirrel uninstall entry:', error);
}
try {
runStartupPhase('native-screen-capture-handlers', registerNativeScreenCaptureHandlers);
} catch (error: unknown) {
log.error('[Init] Failed to register native screen capture handlers:', error);
}
try {
runStartupPhase('native-hardware-encoder-handlers', registerNativeHardwareEncoderHandlers);
} catch (error: unknown) {
log.error('[Init] Failed to register native hardware encoder handlers:', error);
}
try {
runStartupPhase('legacy-harvest-ipc', registerLegacyHarvestHandlers);
} catch (error: unknown) {
log.error('[LegacyHarvest] Failed to register legacy harvest IPC handlers:', error);
}
try {
runStartupPhase('local-app-protocol', () => {
getDesktopLocalAppProtocol().register();
});
} catch (error: unknown) {
log.error('[LocalApp] Failed to register the local app protocol handler:', error);
}
try {
await runStartupPhaseAsync('legacy-origin-harvest', () =>
initializeLegacyOriginHarvest(startupController.signal),
);
} catch (error: unknown) {
log.error('[LegacyHarvest] Legacy origin harvest failed:', error);
}
try {
runStartupPhase('runtime-config-ipc', registerDesktopRuntimeConfigHandlers);
} catch (error: unknown) {
log.error('[LocalApp] Failed to register runtime config IPC handlers:', error);
}
try {
runStartupPhase('application-menu', createApplicationMenu);
} catch (error: unknown) {
log.error('[Init] Failed to create application menu:', error);
}
runStartupPhase('create-window', () => {
try {
createWindow({startHidden: isStartMinimizedLaunch()});
} finally {
startupWindowsPending = false;
}
});
const initialTask = consumeInitialJumpListTask();
if (initialTask) {
const mainWindow = getMainWindow();
mainWindow?.webContents.once('did-finish-load', () => {
if (initialTask === 'open-settings') {
mainWindow.webContents.send('open-settings');
} else {
mainWindow.webContents.send('jump-list-new-dm');
}
});
}
runStartupPhase('desktop-tray', () => {
initializeDesktopTray({
createWindow,
getMainWindow,
isMainWindowTakenOver,
hideWindow,
setQuitting,
showWindow,
});
});
if (process.env.FLUXER_OFFLINE !== '1') {
const {registerUpdater} = await import('@electron/main/Updater');
registerUpdater(getMainWindow);
}
app.on('activate', () => {
const mainWindow = getMainWindow();
if ((mainWindow === null || mainWindow.isDestroyed()) && !isMainWindowTakenOver()) {
createWindow();
} else {
showWindow();
}
});
log.info('App initialized successfully');
})
.catch((error: unknown) => {
startupWindowsPending = false;
log.error('[Startup] whenReady chain rejected:', error);
});
app.on('window-all-closed', () => {
if (startupWindowsPending) {
log.info('[Shutdown] All windows closed before startup created the main window, keeping app alive');
return;
}
if (isMainWindowTakenOver()) {
log.info('[Shutdown] The update splash closed mid update, keeping app alive to reopen the main window');
return;
}
const settings = getDesktopWindowBehaviorSettings();
if (process.platform !== 'darwin' && !(hasActiveDesktopTray() && settings.showTrayIcon && settings.closeToTray)) {
app.quit();
} else if (process.platform !== 'darwin') {
log.info('[Shutdown] All windows closed; keeping app alive because close-to-tray is enabled');
}
});
const QUIT_WATCHDOG_MS = 15000;
let quitWatchdog: NodeJS.Timeout | null = null;
function armQuitWatchdog(reason: string): void {
if (quitWatchdog) return;
quitWatchdog = setTimeout(() => {
log.warn('[Shutdown] Process did not exit before watchdog timeout; forcing exit', {
reason,
timeoutMs: QUIT_WATCHDOG_MS,
});
process.exit(0);
}, QUIT_WATCHDOG_MS);
quitWatchdog.unref?.();
}
app.on('before-quit', () => {
log.info('[Shutdown] before-quit received');
setQuitting(true);
startupController.abort();
armQuitWatchdog('before-quit');
});
let quitCleanupStarted = false;
app.on('will-quit', (event) => {
if (quitCleanupStarted) return;
quitCleanupStarted = true;
log.info('[Shutdown] will-quit cleanup started');
armQuitWatchdog('will-quit');
event.preventDefault();
startupController.abort();
try {
cleanupLegacyHarvestHandlers();
} catch (error) {
log.error('[Shutdown] Failed to remove the legacy harvest IPC handlers:', error);
}
try {
closeDesktopAppStorage();
log.info('[Shutdown] Desktop app store closed and checkpointed');
} catch (error) {
log.error('[Shutdown] Failed to close the desktop app store:', error);
}
try {
cleanupGatewayTransportHandlers();
} catch (error) {
log.error('[Shutdown] Failed to close the native gateway transport:', error);
}
try {
cleanupDesktopOutboundHTTP();
} catch (error) {
log.error('[Shutdown] Failed to close the desktop outbound HTTP client:', error);
}
cleanupIpcHandlers({quitting: true});
cleanupGlobalShortcuts();
cleanupNativeAudio();
cleanupNativeScreenCapture();
cleanupNativeHardwareEncoderHandlers();
cleanupVirtmic();
destroyDesktopTray();
const asyncCleanups: Array<Promise<unknown>> = [
cleanupDesktopLocalAppProtocol().catch((error: unknown) => {
log.error('[Shutdown] Failed to shut down the local app protocol:', error);
}),
];
if (netLog.currentlyLogging) {
asyncCleanups.push(
netLog.stopLogging().catch((error) => {
log.warn('[DebugInfo] Failed to stop Chromium net log:', error);
}),
);
}
void Promise.allSettled(asyncCleanups).finally(() => {
log.info('[Shutdown] will-quit cleanup complete; exiting');
app.exit(0);
});
});
app.on('quit', (_event, exitCode) => {
log.info('[Shutdown] quit received', {exitCode});
});
}
}