experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(url): move single-instance lock before app.whenReady for proper URL forwarding

On macOS, when `open -a Peek URL` is invoked while Peek is already running,
Electron launches a second process. The single-instance lock must be acquired
BEFORE app.whenReady() so Electron can properly forward the Apple Event
(open-url) to the primary instance. Previously, requestSingleInstanceLock()
was called inside onReady (after app.whenReady), causing the second instance
to receive and then discard the open-url event when it quit.

Changes:
- Move requestSingleInstance() and registerSecondInstanceHandler() to module
top-level (after setProfile, before app.whenReady) in entry.ts
- Pass URL from argv as additionalData in requestSingleInstanceLock() so the
primary instance receives it via second-instance event
- Handle additionalData in second-instance handler for URL forwarding
- Add diagnostic file logging to /tmp/peek-url-debug.log to trace the
complete URL handling flow in packaged builds

Testing needed:
1. Build packaged app: yarn build:electron
2. Launch Peek normally (cold start)
3. Run: open -a Peek https://example.com (warm: app already running)
4. Run: open -a Peek https://example.com (cold: app not running)
5. Check /tmp/peek-url-debug.log for diagnostic output
6. Verify URL opens in both warm and cold start cases

+95 -11
+33 -7
backend/electron/entry.ts
··· 95 95 import { saveSessionSnapshot, restoreSessionSnapshot, readSessionCrashState, markSessionDirty, startAutosaveTimer, stopAutosaveTimer, updateAutosaveInterval } from './session.js'; 96 96 import { initDisplayWatcher, cleanupDisplayWatcher } from './display-watcher.js'; 97 97 98 + // Early diagnostic logging for URL handling (writes to /tmp/ to avoid userData path issues) 99 + try { 100 + const ts = new Date().toISOString(); 101 + fs.appendFileSync('/tmp/peek-url-debug.log', 102 + `[${ts}] entry.ts loaded: pid=${process.pid} argv=${JSON.stringify(process.argv)} packaged=${app.isPackaged}\n` 103 + ); 104 + } catch { /* ignore */ } 105 + 98 106 // Catch unhandled errors and promise rejections without showing alert dialogs 99 107 unhandled({ 100 108 showDialog: false, ··· 223 231 224 232 // Set profile in backend config 225 233 setProfile(PROFILE); 234 + 235 + // Acquire single instance lock EARLY (before app.whenReady). 236 + // This is critical on macOS: when a second instance is launched via 237 + // `open -a Peek URL`, Electron needs the lock to be held BEFORE the 238 + // second instance reaches app.whenReady. Otherwise, the second instance 239 + // processes the Apple Event (open-url) locally and then quits, losing 240 + // the URL. With early lock acquisition, Electron forwards the open-url 241 + // event to the first (primary) instance. 242 + if (!requestSingleInstance()) { 243 + // Another instance is already running. On macOS, Electron forwards 244 + // the Apple Event to the primary instance. On Windows/Linux, the 245 + // second-instance event fires with argv. Either way, quit this instance. 246 + // Note: app.quit() was already called inside requestSingleInstance(). 247 + // app.exit() terminates immediately (sync), preventing further init. 248 + app.exit(0); 249 + // TypeScript doesn't know app.exit() is noreturn, but it is. 250 + } 251 + 252 + // Windows/Linux: handle URLs when another instance tries to open. 253 + // Must be registered early (before app.whenReady) so the handler is 254 + // ready when the second-instance event fires. 255 + registerSecondInstanceHandler(); 226 256 227 257 // Profile dirs are subdir of userData dir 228 258 // {home} / {appData} / {userData} / {profileDir} ··· 441 471 // Store startup time for reporting 442 472 (global as Record<string, unknown>).__startupStart = startupStart; 443 473 444 - // Ensure single instance 445 - if (!requestSingleInstance()) { 446 - return; 447 - } 448 - 449 - // Windows/Linux: handle URLs when another instance tries to open 450 - registerSecondInstanceHandler(); 474 + // Note: requestSingleInstance() and registerSecondInstanceHandler() 475 + // are called early (before app.whenReady) for proper URL forwarding. 476 + // See the block after setProfile(PROFILE) above. 451 477 452 478 // Discover and register built-in extensions from extensions/ folder 453 479 discoverBuiltinExtensions(path.join(ROOT_DIR, 'extensions'));
+62 -4
backend/electron/main.ts
··· 37 37 let config: AppConfig; 38 38 let mainWindow: BrowserWindow | null = null; 39 39 40 + // Diagnostic logger for external URL handling. 41 + // Writes to /tmp/ so it's always accessible regardless of userData path. 42 + // This can be removed once external URL handling is confirmed working. 43 + function urlLog(msg: string): void { 44 + try { 45 + const timestamp = new Date().toISOString(); 46 + const line = `[${timestamp}] ${msg}\n`; 47 + fs.appendFileSync('/tmp/peek-url-debug.log', line); 48 + } catch { 49 + // Ignore write errors 50 + } 51 + } 52 + 40 53 // External URL handling state 41 54 let _appReady = false; 42 55 let _pendingUrls: Array<{ url: string; sourceId: string }> = []; ··· 1761 1774 * 2. Session restore has completed (so we don't race window creation) 1762 1775 */ 1763 1776 export function handleExternalUrl(url: string, sourceId = 'os'): void { 1777 + urlLog(`handleExternalUrl: url=${url} sourceId=${sourceId} appReady=${_appReady} frontendReady=${_frontendReady} sessionRestoreDone=${_sessionRestoreDone}`); 1764 1778 DEBUG && console.log('External URL received:', url, 'from:', sourceId); 1765 1779 1766 1780 if (!_appReady) { 1767 1781 _pendingUrls.push({ url, sourceId }); 1782 + urlLog(`handleExternalUrl: queued (app not ready), pendingUrls=${_pendingUrls.length}`); 1768 1783 return; 1769 1784 } 1770 1785 1771 1786 // If frontend isn't ready yet, queue and wait 1772 1787 if (!_frontendReady || !_sessionRestoreDone) { 1773 1788 _pendingUrls.push({ url, sourceId }); 1789 + urlLog(`handleExternalUrl: queued (waiting for frontend/session), pendingUrls=${_pendingUrls.length}`); 1774 1790 DEBUG && console.log('External URL queued (waiting for frontend/session):', url); 1775 1791 return; 1776 1792 } 1777 1793 1794 + urlLog(`handleExternalUrl: publishing immediately`); 1778 1795 publishExternalUrl(url, sourceId); 1779 1796 } 1780 1797 ··· 1814 1831 const urls = _pendingUrls.slice(); 1815 1832 _pendingUrls = []; 1816 1833 1817 - if (urls.length === 0) return; 1834 + if (urls.length === 0) { 1835 + urlLog('processPendingUrls: no pending URLs'); 1836 + return; 1837 + } 1818 1838 1839 + urlLog(`processPendingUrls: processing ${urls.length} pending URL(s)`); 1819 1840 DEBUG && console.log(`[url] Processing ${urls.length} pending URL(s)`); 1820 1841 1821 1842 // Process sequentially with a small delay between each ··· 1835 1856 */ 1836 1857 export function setAppReady(): void { 1837 1858 _appReady = true; 1859 + urlLog(`setAppReady: pendingUrls=${_pendingUrls.length}`); 1838 1860 // Kick off pending URL processing (will wait for frontend + session restore) 1839 1861 processPendingUrls(); 1840 1862 } ··· 1847 1869 export function notifyFrontendReady(): void { 1848 1870 if (_frontendReady) return; 1849 1871 _frontendReady = true; 1872 + urlLog(`notifyFrontendReady: pendingUrls=${_pendingUrls.length}`); 1850 1873 DEBUG && console.log('[url] Frontend ready — pubsub subscribers registered'); 1851 1874 if (_frontendReadyResolve) { 1852 1875 _frontendReadyResolve(); ··· 1861 1884 export function notifySessionRestoreDone(): void { 1862 1885 if (_sessionRestoreDone) return; 1863 1886 _sessionRestoreDone = true; 1887 + urlLog(`notifySessionRestoreDone: pendingUrls=${_pendingUrls.length}`); 1864 1888 DEBUG && console.log('[url] Session restore done'); 1865 1889 if (_sessionRestoreDoneResolve) { 1866 1890 _sessionRestoreDoneResolve(); ··· 1891 1915 * Must be called before app.ready for open-url, and in onReady for second-instance 1892 1916 */ 1893 1917 export function registerExternalUrlHandlers(): void { 1918 + urlLog('registerExternalUrlHandlers: registering open-url and open-file handlers'); 1919 + 1894 1920 // macOS: handle open-url event 1895 1921 app.on('open-url', (event, url) => { 1896 1922 event.preventDefault(); 1923 + urlLog(`open-url event fired: url=${url}`); 1897 1924 handleExternalUrl(url, 'os'); 1898 1925 }); 1899 1926 ··· 1915 1942 * Call this inside onReady after acquiring single instance lock 1916 1943 */ 1917 1944 export function registerSecondInstanceHandler(): void { 1918 - app.on('second-instance', (_event, argv) => { 1919 - // Look for URLs first 1945 + app.on('second-instance', (_event, argv, _workingDirectory, additionalData) => { 1946 + urlLog(`second-instance event: argv=${JSON.stringify(argv)} additionalData=${JSON.stringify(additionalData)}`); 1947 + 1948 + // Check additionalData first (set by the second instance via requestSingleInstanceLock) 1949 + if (additionalData && typeof additionalData === 'object' && (additionalData as any).url) { 1950 + const url = (additionalData as any).url as string; 1951 + urlLog(`second-instance: found URL in additionalData: ${url}`); 1952 + DEBUG && console.log('second-instance additionalData URL:', url); 1953 + handleExternalUrl(url, 'os'); 1954 + return; 1955 + } 1956 + 1957 + // Look for URLs in argv 1920 1958 const url = argv.find(arg => 1921 1959 arg.startsWith('http://') || arg.startsWith('https://') 1922 1960 ); 1923 1961 if (url) { 1962 + urlLog(`second-instance: found URL in argv: ${url}`); 1924 1963 DEBUG && console.log('second-instance URL:', url); 1925 1964 handleExternalUrl(url, 'os'); 1926 1965 return; ··· 1931 1970 if (arg.startsWith('-')) continue; 1932 1971 const fileUrl = filePathToUrl(arg); 1933 1972 if (fileUrl) { 1973 + urlLog(`second-instance: found file URL in argv: ${arg} -> ${fileUrl}`); 1934 1974 DEBUG && console.log('second-instance file path:', arg, '->', fileUrl); 1935 1975 handleExternalUrl(fileUrl, 'os'); 1936 1976 return; 1937 1977 } 1938 1978 } 1979 + 1980 + urlLog('second-instance: no URL found in argv or additionalData'); 1939 1981 }); 1940 1982 } 1941 1983 ··· 1944 1986 * Uses handleExternalUrl which queues until frontend + session restore are ready. 1945 1987 */ 1946 1988 export function handleCliUrl(): void { 1989 + urlLog(`handleCliUrl: argv=${JSON.stringify(process.argv)}`); 1990 + 1947 1991 // Look for URLs first 1948 1992 const urlArg = process.argv.find(arg => 1949 1993 arg.startsWith('http://') || arg.startsWith('https://') 1950 1994 ); 1951 1995 if (urlArg) { 1996 + urlLog(`handleCliUrl: found URL in argv: ${urlArg}`); 1952 1997 DEBUG && console.log('CLI URL argument:', urlArg); 1953 1998 handleExternalUrl(urlArg, 'cli'); 1954 1999 return; ··· 2009 2054 return true; 2010 2055 } 2011 2056 2012 - const gotTheLock = app.requestSingleInstanceLock(); 2057 + // Pass any URL from argv as additionalData so the primary instance receives it 2058 + // via the second-instance event. On macOS, `open -a Peek URL` may pass the URL 2059 + // as an Apple Event (handled via open-url) OR in argv, depending on the scenario. 2060 + const urlArg = process.argv.find(arg => 2061 + arg.startsWith('http://') || arg.startsWith('https://') 2062 + ); 2063 + const additionalData = urlArg ? { url: urlArg } : undefined; 2064 + 2065 + urlLog(`requestSingleInstance: attempting lock, additionalData=${JSON.stringify(additionalData)}`); 2066 + 2067 + const gotTheLock = app.requestSingleInstanceLock(additionalData); 2013 2068 if (!gotTheLock) { 2069 + urlLog('requestSingleInstance: lock NOT acquired, another instance running'); 2014 2070 console.error('APP INSTANCE ALREADY RUNNING, QUITTING'); 2015 2071 app.quit(); 2016 2072 return false; 2017 2073 } 2074 + 2075 + urlLog('requestSingleInstance: lock acquired (primary instance)'); 2018 2076 return true; 2019 2077 } 2020 2078