mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-31 20:29:56 +02:00
[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:
@@ -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 (_) {}
|
||||
|
||||
@@ -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.');
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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}>
|
||||
|
||||
Vendored
+1
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user