From d68aae36d5abd7e8a2d034f36b5e83c28cdbbb6a Mon Sep 17 00:00:00 2001 From: Yuki Hattori Date: Sun, 22 Sep 2024 10:50:48 +0900 Subject: [PATCH] Make faster browser detection by asynchronization --- src/browser/finder.ts | 2 +- src/browser/finders/chrome.ts | 29 ++++------ src/browser/finders/edge.ts | 33 +++++------ src/browser/finders/firefox.ts | 60 +++++++++----------- src/browser/finders/utils.ts | 100 ++++++++++++++++++++++++--------- 5 files changed, 127 insertions(+), 97 deletions(-) diff --git a/src/browser/finder.ts b/src/browser/finder.ts index c8c28f80..f42647f7 100644 --- a/src/browser/finder.ts +++ b/src/browser/finder.ts @@ -32,7 +32,7 @@ export const findBrowser = async ( preferredPath: await (async () => { if (opts.preferredPath) { const normalized = await normalizeDarwinAppPath(opts.preferredPath) - if (isExecutable(normalized)) return normalized + if (await isExecutable(normalized)) return normalized } return undefined })(), diff --git a/src/browser/finders/chrome.ts b/src/browser/finders/chrome.ts index 43034cc3..dcf64390 100644 --- a/src/browser/finders/chrome.ts +++ b/src/browser/finders/chrome.ts @@ -8,7 +8,7 @@ import { error, CLIErrorCode } from '../../error' import { ChromeBrowser } from '../browsers/chrome' import { ChromeCdpBrowser } from '../browsers/chrome-cdp' import type { BrowserFinder, BrowserFinderResult } from '../finder' -import { getPlatform, isExecutable, which } from './utils' +import { findExecutableBinary, getPlatform } from './utils' const chrome = (path: string): BrowserFinderResult => ({ path, @@ -19,7 +19,7 @@ export const chromeFinder: BrowserFinder = async ({ preferredPath } = {}) => { if (preferredPath) return chrome(preferredPath) const platform = await getPlatform() - const installation = (() => { + const installation = await (async () => { switch (platform) { case 'darwin': return darwinFast() @@ -32,7 +32,7 @@ export const chromeFinder: BrowserFinder = async ({ preferredPath } = {}) => { case 'wsl1': return wsl()[0] } - return fallback() + return await fallback() /* c8 ignore stop */ })() @@ -41,18 +41,11 @@ export const chromeFinder: BrowserFinder = async ({ preferredPath } = {}) => { error('Chrome browser could not be found.', CLIErrorCode.NOT_FOUND_BROWSER) } -const fallbackExecutableNames = [ - 'google-chrome-stable', - 'google-chrome', - 'chrome', // FreeBSD Chromium - 'chromium-browser', - 'chromium', -] as const - -const fallback = () => { - for (const executableName of fallbackExecutableNames) { - const executablePath = which(executableName) - if (executablePath && isExecutable(executablePath)) return executablePath - } - return undefined -} +const fallback = async () => + await findExecutableBinary([ + 'google-chrome-stable', + 'google-chrome', + 'chrome', // FreeBSD Chromium + 'chromium-browser', + 'chromium', + ]) diff --git a/src/browser/finders/edge.ts b/src/browser/finders/edge.ts index b833eb4e..154e29ce 100644 --- a/src/browser/finders/edge.ts +++ b/src/browser/finders/edge.ts @@ -7,32 +7,29 @@ import { import { ChromeBrowser } from '../browsers/chrome' import { ChromeCdpBrowser } from '../browsers/chrome-cdp' import type { BrowserFinder, BrowserFinderResult } from '../finder' -import { getPlatform, isExecutable } from './utils' +import { findExecutable, getPlatform } from './utils' const edge = (path: string): BrowserFinderResult => ({ path, acceptedBrowsers: [ChromeBrowser, ChromeCdpBrowser], }) -const findExecutable = (paths: string[]): string | undefined => - paths.find((p) => isExecutable(p)) - export const edgeFinder: BrowserFinder = async ({ preferredPath } = {}) => { if (preferredPath) return edge(preferredPath) const platform = await getPlatform() - const installation = (() => { + const installation = await (async () => { switch (platform) { case 'darwin': - return edgeFinderDarwin() + return await edgeFinderDarwin() case 'linux': - return edgeFinderLinux() + return await edgeFinderLinux() case 'win32': - return edgeFinderWin32() + return await edgeFinderWin32() // CI cannot test against WSL environment /* c8 ignore start */ case 'wsl1': - return edgeFinderWSL1() + return await edgeFinderWSL1() } return undefined /* c8 ignore stop */ @@ -43,23 +40,23 @@ export const edgeFinder: BrowserFinder = async ({ preferredPath } = {}) => { error('Edge browser could not be found.', CLIErrorCode.NOT_FOUND_BROWSER) } -const edgeFinderDarwin = () => - findExecutable([ +const edgeFinderDarwin = async () => + await findExecutable([ '/Applications/Microsoft Edge Canary.app/Contents/MacOS/Microsoft Edge Canary', '/Applications/Microsoft Edge Dev.app/Contents/MacOS/Microsoft Edge Dev', '/Applications/Microsoft Edge Beta.app/Contents/MacOS/Microsoft Edge Beta', '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge', ]) -const edgeFinderLinux = () => - findExecutable([ +const edgeFinderLinux = async () => + await findExecutable([ '/opt/microsoft/msedge-canary/msedge', '/opt/microsoft/msedge-dev/msedge', '/opt/microsoft/msedge-beta/msedge', '/opt/microsoft/msedge/msedge', ]) -const edgeFinderWin32 = ({ +const edgeFinderWin32 = async ({ programFiles = process.env.PROGRAMFILES, programFilesX86 = process.env['PROGRAMFILES(X86)'], localAppData = process.env.LOCALAPPDATA, @@ -67,7 +64,7 @@ const edgeFinderWin32 = ({ programFiles?: string programFilesX86?: string localAppData?: string -} = {}): string | undefined => { +} = {}): Promise => { const paths: string[] = [] for (const prefix of [programFiles, programFilesX86, localAppData]) { @@ -81,13 +78,13 @@ const edgeFinderWin32 = ({ ) } - return findExecutable(paths) + return await findExecutable(paths) } -const edgeFinderWSL1 = () => { +const edgeFinderWSL1 = async () => { const localAppData = resolveWindowsEnvSync('LOCALAPPDATA') - return edgeFinderWin32({ + return await edgeFinderWin32({ programFiles: '/mnt/c/Program Files', programFilesX86: '/mnt/c/Program Files (x86)', localAppData: localAppData ? resolveWSLPathToGuestSync(localAppData) : '', diff --git a/src/browser/finders/firefox.ts b/src/browser/finders/firefox.ts index f5a6262a..967d3aff 100644 --- a/src/browser/finders/firefox.ts +++ b/src/browser/finders/firefox.ts @@ -2,33 +2,30 @@ import path from 'node:path' import { error, CLIErrorCode } from '../../error' import { FirefoxBrowser } from '../browsers/firefox' import type { BrowserFinder, BrowserFinderResult } from '../finder' -import { getPlatform, isExecutable, which } from './utils' +import { getPlatform, findExecutable, findExecutableBinary } from './utils' const firefox = (path: string): BrowserFinderResult => ({ path, acceptedBrowsers: [FirefoxBrowser], }) -const findExecutable = (paths: string[]): string | undefined => - paths.find((p) => isExecutable(p)) - export const firefoxFinder: BrowserFinder = async ({ preferredPath } = {}) => { if (preferredPath) return firefox(preferredPath) const platform = await getPlatform() - const installation = (() => { + const installation = await (async () => { switch (platform) { case 'darwin': - return firefoxFinderDarwin() + return await firefoxFinderDarwin() case 'win32': - return firefoxFinderWin32() + return await firefoxFinderWin32() // CI cannot test against WSL environment /* c8 ignore start */ case 'wsl1': - return firefoxFinderWSL1() + return await firefoxFinderWSL1() /* c8 ignore stop */ } - return firefoxFinderFallback() + return await firefoxFinderFallback() })() if (installation) return firefox(installation) @@ -36,8 +33,8 @@ export const firefoxFinder: BrowserFinder = async ({ preferredPath } = {}) => { error('Firefox browser could not be found.', CLIErrorCode.NOT_FOUND_BROWSER) } -const firefoxFinderDarwin = () => - findExecutable([ +const firefoxFinderDarwin = async () => + await findExecutable([ '/Applications/Firefox Nightly.app/Contents/MacOS/firefox', '/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox', '/Applications/Firefox.app/Contents/MacOS/firefox', // Firefox stable, ESR, and beta @@ -57,7 +54,7 @@ const winPossibleDrives = () => { return Array.from(possibleDriveSet).sort() } -const firefoxFinderWin32 = () => { +const firefoxFinderWin32 = async () => { const prefixes: string[] = [] for (const drive of winPossibleDrives()) { @@ -70,7 +67,7 @@ const firefoxFinderWin32 = () => { } } - return findExecutable( + return await findExecutable( prefixes.flatMap((prefix) => [ path.join(prefix, 'Nightly', 'firefox.exe'), path.join(prefix, 'Firefox Nightly', 'firefox.exe'), @@ -80,7 +77,7 @@ const firefoxFinderWin32 = () => { ) } -const firefoxFinderWSL1 = () => { +const firefoxFinderWSL1 = async () => { const prefixes: string[] = [] for (const drive of winPossibleDrives()) { @@ -88,7 +85,7 @@ const firefoxFinderWSL1 = () => { prefixes.push(`/mnt/${drive}/Program Files (x86)`) } - return findExecutable( + return await findExecutable( prefixes.flatMap((prefix) => [ path.join(prefix, 'Nightly', 'firefox.exe'), path.join(prefix, 'Firefox Nightly', 'firefox.exe'), @@ -98,22 +95,17 @@ const firefoxFinderWSL1 = () => { ) } -// In Linux, Firefox must have only an executable name `firefox` in every -// editions, but some packages may provide different executable names. -const fallbackExecutableNames = [ - 'firefox-nightly', - 'firefox-developer-edition', - 'firefox-developer', - 'firefox-dev', - 'firefox-beta', - 'firefox', - 'firefox-esr', -] as const - -const firefoxFinderFallback = () => { - for (const executableName of fallbackExecutableNames) { - const executablePath = which(executableName) - if (executablePath && isExecutable(executablePath)) return executablePath - } - return undefined -} +const firefoxFinderFallback = async () => + await findExecutableBinary( + // In Linux, Firefox must have only an executable name `firefox` in every + // editions, but some packages may provide different executable names. + [ + 'firefox-nightly', + 'firefox-developer-edition', + 'firefox-developer', + 'firefox-dev', + 'firefox-beta', + 'firefox', + 'firefox-esr', + ] + ) diff --git a/src/browser/finders/utils.ts b/src/browser/finders/utils.ts index 02e074ab..69efd479 100644 --- a/src/browser/finders/utils.ts +++ b/src/browser/finders/utils.ts @@ -1,25 +1,69 @@ -import { execFileSync } from 'node:child_process' +import { execFile } from 'node:child_process' +import { promisify } from 'node:util' import fs from 'node:fs' import path from 'node:path' import { parse as parsePlist } from 'fast-plist' import { debugBrowserFinder } from '../../utils/debug' import { isWSL } from '../../utils/wsl' -// Common +const execFilePromise = promisify(execFile) + export const getPlatform = async () => (await isWSL()) === 1 ? 'wsl1' : process.platform -export const isAccessible = (path: string, mode?: number) => { +export const isAccessible = async (path: string, mode?: number) => { try { - fs.accessSync(path, mode) + await fs.promises.access(path, mode) return true } catch { return false } } -export const isExecutable = (path: string) => - isAccessible(path, fs.constants.X_OK) +export const isExecutable = async (path: string) => + await isAccessible(path, fs.constants.X_OK) + +const findFirst = async ( + paths: string[], + predicate: (path: string) => Promise +) => { + const pathsCount = paths.length + + return new Promise((resolve) => { + const result = Array(pathsCount) + const resolved = Array(pathsCount) + + paths.forEach((p, index) => { + predicate(p) + .then((ret) => { + result[index] = ret + resolved[index] = !!ret + }) + .catch((e) => { + debugBrowserFinder('%o', e) + resolved[index] = false + }) + .finally(() => { + let target: number | undefined + + for (let i = pathsCount - 1; i >= 0; i -= 1) { + if (resolved[i] !== false) target = i + } + + if (target === undefined) { + resolve(undefined) + } else if (resolved[target]) { + resolve(result[target]) + } + }) + }) + }) +} + +export const findExecutable = async (paths: string[]) => + await findFirst(paths, async (path) => + (await isExecutable(path)) ? path : undefined + ) // Linux export const isSnapBrowser = async (executablePath: string) => { @@ -37,26 +81,6 @@ export const isSnapBrowser = async (executablePath: string) => { return false } -export const which = (command: string) => { - if (process.platform === 'win32') { - debugBrowserFinder( - '"which %s" command is not available on Windows.', - command - ) - return undefined - } - - try { - const [ret] = execFileSync('which', [command], { stdio: 'pipe' }) - .toString() - .split(/\r?\n/) - - return ret - } catch { - return undefined - } -} - const isShebang = (path: string) => { let fd: number | null = null @@ -75,6 +99,30 @@ const isShebang = (path: string) => { return false } +export const findExecutableBinary = async (binaries: string[]) => + await findFirst(binaries, async (binary) => { + const binaryPath = await which(binary) + if (binaryPath && (await isExecutable(binaryPath))) return binaryPath + return undefined + }) + +const which = async (command: string) => { + if (process.platform === 'win32') { + debugBrowserFinder( + '"which %s" command is not available on Windows.', + command + ) + return undefined + } + + try { + const { stdout } = await execFilePromise('which', [command]) + return stdout.split(/\r?\n/)[0] + } catch { + return undefined + } +} + // Darwin const darwinAppDirectoryMatcher = /.app\/?$/