[eric] mac: after a crash relaunch user sees a tiny 'we had a hiccup, sessions are still here' chip for 8s instead of nothing

This commit is contained in:
eric
2026-05-31 22:22:13 -07:00
parent ec6a923b46
commit eb70337b00
5 changed files with 78 additions and 0 deletions
+13
View File
@@ -34,6 +34,10 @@ const SUPPORT_DIR = path.join(os.homedir(), 'Library', 'Application Support', 'o
const UPDATING_LOCK = path.join(SUPPORT_DIR, 'updating.lock');
const CLEAN_QUIT_LOCK = path.join(SUPPORT_DIR, 'clean-quit.lock');
const RELAUNCH_LOG = path.join(SUPPORT_DIR, 'crash-watchdog-relaunches.log');
// Touched ONLY when watchdog decides to relaunch. main.js reads + deletes it on
// next startup, forwards to renderer, which shows a small "We recovered from
// a crash" chip. Renderer-side: ipcMain.handle 'crash-recovery-info'.
const RECOVERY_MARKER = path.join(SUPPORT_DIR, 'crash-recovery.json');
const MIN_UPTIME_MS = 30_000;
const POLL_INTERVAL_MS = 2_000;
@@ -91,6 +95,15 @@ async function sleep(ms) {
// All guards passed: relaunch. `open -n` opens a fresh instance even if the
// app is registered, which it always will be (LaunchServices remembers).
recordRelaunch();
// Mark the relaunch so the next startup can show a "recovered" chip. Best-effort
// write; if the disk is full or perms fail, watchdog still relaunches silently.
try {
fs.writeFileSync(RECOVERY_MARKER, JSON.stringify({
ts: Date.now(),
parent_pid: PARENT_PID,
uptime_ms: uptime,
}));
} catch (_) {}
try {
spawn('open', ['-n', APP_BUNDLE_PATH], { detached: true, stdio: 'ignore' }).unref();
} catch (_) {}
+21
View File
@@ -2102,6 +2102,27 @@ ipcMain.handle('get-webview-preload-path', () => {
ipcMain.handle('get-update-status', () => cachedUpdateStatus);
// One-shot recovery info: if the crash-watchdog relaunched us, returns the
// {ts, parent_pid, uptime_ms} JSON it wrote and then DELETES the file so the
// chip only shows once. Returns null if no marker present (normal launch).
// macOS-only path; Windows/Linux always returns null.
let _cachedRecoveryInfo = undefined;
ipcMain.handle('get-crash-recovery-info', () => {
if (process.platform !== 'darwin') return null;
if (_cachedRecoveryInfo !== undefined) return _cachedRecoveryInfo;
const markerPath = path.join(os.homedir(), 'Library', 'Application Support', 'openswarm', 'crash-recovery.json');
try {
if (!fs.existsSync(markerPath)) { _cachedRecoveryInfo = null; return null; }
const raw = fs.readFileSync(markerPath, 'utf-8');
_cachedRecoveryInfo = JSON.parse(raw);
try { fs.unlinkSync(markerPath); } catch (_) {}
return _cachedRecoveryInfo;
} catch (_) {
_cachedRecoveryInfo = null;
return null;
}
});
ipcMain.handle('check-for-updates', async () => {
if (!autoUpdater || !isPackaged) {
sendToRenderer('update-error', 'Update check is only available in the packaged app.');
+1
View File
@@ -56,6 +56,7 @@ contextBridge.exposeInMainWorld('openswarm', {
cdpCacheClear: (wcId) => ipcRenderer.invoke('cdp-cache-clear', wcId),
capturePage: (rect) => ipcRenderer.invoke('capture-page', rect),
getUpdateStatus: () => ipcRenderer.invoke('get-update-status'),
getCrashRecoveryInfo: () => ipcRenderer.invoke('get-crash-recovery-info'),
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
downloadUpdate: () => ipcRenderer.invoke('download-update'),
installUpdate: () => ipcRenderer.invoke('install-update'),
+42
View File
@@ -2,6 +2,7 @@ import React, { useMemo, useEffect, useState, useRef, Suspense } from 'react';
import { Provider } from 'react-redux';
import { HashRouter, Routes, Route } from 'react-router-dom';
import { ThemeProvider as MuiThemeProvider, createTheme, CssBaseline } from '@mui/material';
import Box from '@mui/material/Box';
import Snackbar from '@mui/material/Snackbar';
import Alert from '@mui/material/Alert';
import { store } from '../shared/state/store';
@@ -368,6 +369,46 @@ const DefaultModelGuard: React.FC<{ children: React.ReactNode }> = ({ children }
);
};
/** Surfaces a brief recovery chip if the crash-watchdog relaunched us last cycle.
* Mac-only path (watchdog only runs on darwin); main.js returns null elsewhere.
* Auto-hides after 8s. No interaction required from the user; sessions are server-side
* so reattachment is automatic via the WS dashboard subscription that's already wired. */
const CrashRecoveryChip: React.FC = () => {
const [show, setShow] = React.useState(false);
React.useEffect(() => {
const api = (window as any).openswarm as OpenSwarmAPI | undefined;
if (!api?.getCrashRecoveryInfo) return;
api.getCrashRecoveryInfo().then((info) => {
if (info) setShow(true);
}).catch(() => {});
}, []);
React.useEffect(() => {
if (!show) return;
const t = setTimeout(() => setShow(false), 8000);
return () => clearTimeout(t);
}, [show]);
if (!show) return null;
return (
<Box sx={{
position: 'fixed', top: 16, right: 16, zIndex: 1500,
display: 'flex', alignItems: 'center', gap: 1,
bgcolor: 'background.paper',
border: '1px solid', borderColor: 'divider',
boxShadow: 3, borderRadius: '10px',
px: 1.75, py: 1, fontSize: '0.85rem',
maxWidth: 360,
}}>
<Box component="span" sx={{
width: 8, height: 8, borderRadius: '50%',
bgcolor: 'success.main',
}} />
<Box component="span">
We had a hiccup and brought you back. Your sessions are still here.
</Box>
</Box>
);
};
const UpdateListener: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const dispatch = useAppDispatch();
@@ -446,6 +487,7 @@ const ThemedApp: React.FC = () => {
<SignInGateLoader>
<DefaultModelGuard>
<UpdateListener>
<CrashRecoveryChip />
<DeepLinkListener>
<ErrorBoundary scope="routes">
<Suspense fallback={null}>
+1
View File
@@ -37,6 +37,7 @@ declare global {
getAppVersion: () => Promise<string>;
getBuildInfo: () => Promise<{ sha: string; shortSha: string; builtAt: string | null; channel: string }>;
getUpdateStatus: () => Promise<{ status: string; info: any; error: string | null }>;
getCrashRecoveryInfo?: () => Promise<{ ts: number; parent_pid: number; uptime_ms: number } | null>;
checkForUpdates: () => Promise<{ success: boolean; version?: string; error?: string }>;
downloadUpdate: () => Promise<{ success: boolean; error?: string }>;
installUpdate: () => Promise<void>;