Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 42 additions & 21 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type BrowserName =
| 'firefox'
| 'firefox_nightly';

type Platform = 'win32' | 'linux';
type Platform = 'win32' | 'linux' | 'darwin';

// Platform-specific browser paths
type BrowserPaths = {
Expand All @@ -62,6 +62,15 @@ const browserPaths: BrowserPaths = {
chromium_snapshot: ['chromium-snapshot', 'chromium-snapshot-bin'],
firefox: 'firefox',
firefox_nightly: 'firefox-nightly'
},

darwin: {
chrome: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
chrome_canary: '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
chromium: '/Applications/Chromium.app/Contents/MacOS/Chromium',
edge: '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
firefox: '/Applications/Firefox.app/Contents/MacOS/firefox',
firefox_nightly: '/Applications/Firefox Nightly.app/Contents/MacOS/firefox',
}
};

Expand All @@ -79,6 +88,14 @@ const getBinariesInPath = async (): Promise<string[]> => {
)).flat();
};

/**
* Clear the cached browser binary paths.
* Useful when system PATH changes or browsers are installed/removed at runtime.
*/
export const clearBrowserCache = (): void => {
_binariesInPath = undefined;
};

const exists = async (path: string): Promise<boolean> => {
if (path.includes(sep)) {
return await access(path).then(() => true).catch(() => false);
Expand Down Expand Up @@ -242,8 +259,11 @@ const startBrowser = async (url: string, { windowSize, forceBrowser, onBrowserEx
const browserInfo = await findBrowserPath(forceBrowser);

if (!browserInfo) {
log('failed to find a good browser install');
return null;
const platform = process.platform;
throw new Error(
`No supported browser found on this system (platform: ${platform}). ` +
`Install Chrome, Edge, or Firefox, or set FLUXSTACK_DESKTOP_CUSTOM_BROWSER_PATH.`
);
}

const [browserPath, browserName] = browserInfo;
Expand All @@ -259,21 +279,26 @@ const startBrowser = async (url: string, { windowSize, forceBrowser, onBrowserEx
log('using custom browser args:', customArgs);
}

const Browser = await (browserType === 'firefox' ? Firefox : Chromium)({
browserName: browserFriendlyName,
dataPath,
browserPath,
onBrowserExit,
customArgs
}, {
url,
windowSize
});
try {
const Browser = await (browserType === 'firefox' ? Firefox : Chromium)({
browserName: browserFriendlyName,
dataPath,
browserPath,
onBrowserExit,
customArgs
}, {
url,
windowSize
});

// TODO: Re-enable idle API when ready
// Browser.idle = await IdleAPI(Browser.cdp, { browserType, dataPath });
// TODO: Re-enable idle API when ready
// Browser.idle = await IdleAPI(Browser.cdp, { browserType, dataPath });

return Browser;
return Browser;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to start browser "${browserFriendlyName}" at "${browserPath}": ${message}`);
}
};

// Public API options interface
Expand All @@ -290,15 +315,11 @@ export interface OpenOptions extends BrowserOptions {
export const open = async (
url: string,
{ windowSize, onLoad, forceBrowser, onBrowserExit }: OpenOptions = {}
): Promise<any> => {
) => {
log('starting browser...');

const Browser = await startBrowser(url, { windowSize, forceBrowser, onBrowserExit });

if (!Browser) {
throw new Error('Failed to start browser');
}

if (onLoad) {
const toRun = await loadOnLoadWrapper(onLoad);

Expand Down
13 changes: 11 additions & 2 deletions src/launcher/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ interface InjectCDPMessage {

interface BrowserInfo {
product: string;
[key: string]: any;
jsVersion?: string;
protocolVersion?: string;
revision?: string;
userAgent?: string;
}

interface CDPConnection {
Expand All @@ -36,9 +39,15 @@ interface CDPAPI {
send: (method: string, params?: Record<string, any>) => Promise<any>;
}

interface IPCApi {
on: (type: string, callback: (data: any) => any | Promise<any>) => void;
removeListener: (type: string, callback: (data: any) => any | Promise<any>) => boolean;
send: (type: string, data: any, id?: string) => Promise<any>;
}

interface FluxDesktopWindow {
window: WindowAPI;
ipc: any; // IPC API type from ipc.js
ipc: IPCApi;
cdp: CDPAPI;
close: () => void;
}
Expand Down
3 changes: 1 addition & 2 deletions src/launcher/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,14 @@ type Transport = 'websocket' | 'stdio';
interface ExtraOptions {
browserName?: string;
onBrowserExit?: () => void;
[key: string]: any;
}

export default async (
browserPath: string,
args: string[],
transport: Transport,
extra: ExtraOptions
): Promise<any> => {
) => {
const port = transport === 'websocket'
? Math.floor(Math.random() * (portRange[1] - portRange[0] + 1)) + portRange[0]
: null;
Expand Down
23 changes: 21 additions & 2 deletions src/lib/cdp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export default async ({ pipe, port }: CDPConnectionOptions): Promise<CDPConnecti
let _send: (data: string) => void;
let _close: () => void;

const CDP_TIMEOUT_MS = 30_000; // 30 second default timeout for CDP commands

let msgId = 0;
const sendMessage = async (
method: string,
Expand All @@ -73,8 +75,16 @@ export default async ({ pipe, port }: CDPConnectionOptions): Promise<CDPConnecti

// log('sent', msg);

const reply = await new Promise<CDPMessage>(res => {
onReply[id] = (msg: CDPMessage) => res(msg);
const reply = await new Promise<CDPMessage>((resolve, reject) => {
const timeout = setTimeout(() => {
delete onReply[id];
reject(new Error(`CDP command "${method}" timed out after ${CDP_TIMEOUT_MS}ms`));
}, CDP_TIMEOUT_MS);

onReply[id] = (msg: CDPMessage) => {
clearTimeout(timeout);
resolve(msg);
};
});

return reply.result;
Expand Down Expand Up @@ -180,6 +190,15 @@ export default async ({ pipe, port }: CDPConnectionOptions): Promise<CDPConnecti

close: (): void => {
closed = true;

// Reject all pending replies to prevent memory leaks from hanging promises
for (const id of Object.keys(onReply)) {
delete onReply[Number(id)];
}

// Clear all message callbacks
messageCallbacks.length = 0;

_close();
}
};
Expand Down
4 changes: 3 additions & 1 deletion src/lib/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { loadIPCInjection } from './scripts';
interface BrowserInfo {
product?: string;
jsVersion?: string;
[key: string]: any;
protocolVersion?: string;
revision?: string;
userAgent?: string;
}

interface IPCSetupOptions {
Expand Down
6 changes: 2 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,10 @@ declare global {
write: (path: string, data: string | Buffer | ArrayBuffer) => Promise<number>;
file: (path: string) => {
text: () => Promise<string>;
json: () => Promise<any>;
json: () => Promise<unknown>;
exists: () => Promise<boolean>;
size: Promise<number>;
size: number;
};
spawn?: (options: any) => any;
[key: string]: any;
};
}

Expand Down
Loading