import spawnAsync from '@expo/spawn-async'; import open from 'open'; import path from 'path'; import { LaunchBrowserTypes, type LaunchBrowserImpl, type LaunchBrowserInstance, } from './LaunchBrowser.types'; const IS_WSL = require('is-wsl') && !require('is-docker')(); /** * Browser implementation for Windows and WSL * * To minimize the difference between Windows and WSL, the implementation wraps all spawn calls through powershell. */ export default class LaunchBrowserImplWindows implements LaunchBrowserImpl, LaunchBrowserInstance { private _appId: string | undefined; private _powershellEnv: { [key: string]: string } | undefined; MAP = { [LaunchBrowserTypes.CHROME]: { appId: 'chrome', fullName: 'Google Chrome', }, [LaunchBrowserTypes.EDGE]: { appId: 'msedge', fullName: 'Microsoft Edge', }, }; async isSupportedBrowser(browserType: LaunchBrowserTypes): Promise { let result = false; try { const env = await this.getPowershellEnv(); const { status } = await spawnAsync( 'powershell.exe', ['-c', `Get-Package -Name '${this.MAP[browserType].fullName}'`], { // @ts-expect-error: Missing NODE_ENV env, stdio: 'ignore', } ); result = status === 0; } catch { result = false; } return result; } async createTempBrowserDir(baseDirName: string) { let tmpDir; if (IS_WSL) { // On WSL, the browser is actually launched in host, the `temp-dir` returns the linux /tmp path where host browsers cannot reach into. // We should get the temp path through the `$TEMP` windows environment variable. tmpDir = (await spawnAsync('powershell.exe', ['-c', 'echo "$Env:TEMP"'])).stdout.trim(); return `${tmpDir}\\${baseDirName}`; } else { tmpDir = require('temp-dir'); return path.join(tmpDir, baseDirName); } } async launchAsync( browserType: LaunchBrowserTypes, args: string[] ): Promise { const appId = this.MAP[browserType].appId; await openWithSystemRootEnvironment(appId, { arguments: args }); this._appId = appId; return this; } async close(): Promise { if (this._appId != null) { try { // Since we wrap all spawn calls through powershell as well as from `open.openApp`, the returned ChildProcess is not the browser process. // And we cannot just call `process.kill()` kill it. // The implementation tries to find the pid of target chromium browser process (with --app=https://chrome-devtools-frontend.appspot.com in command arguments), // and uses taskkill to terminate the process. const env = await this.getPowershellEnv(); await spawnAsync( 'powershell.exe', [ '-c', `taskkill.exe /pid @(Get-WmiObject Win32_Process -Filter "name = '${this._appId}.exe' AND CommandLine LIKE '%chrome-devtools-frontend.appspot.com%'" | Select-Object -ExpandProperty ProcessId)`, ], { // @ts-expect-error: Missing NODE_ENV env, stdio: 'ignore', } ); } catch {} this._appId = undefined; } } /** * This method is used to get the powershell environment variables for `Get-Package` command. * Especially for powershell 7, its default `PSModulePath` is different from powershell 5 and `Get-Package` command is not available. * We need to set the PSModulePath to include the default value of powershell 5. */ private async getPowershellEnv(): Promise<{ [key: string]: string }> { if (this._powershellEnv) { return this._powershellEnv; } const PSModulePath = ( await spawnAsync('powershell.exe', ['-c', 'echo "$PSHOME\\Modules"']) ).stdout.trim(); this._powershellEnv = { PSModulePath, }; return this._powershellEnv; } } /** * Due to a bug in `open` on Windows PowerShell, we need to ensure `process.env.SYSTEMROOT` is set. * This environment variable is set by Windows on `SystemRoot`, causing `open` to execute a command with an "unknown" drive letter. * * @see https://github.com/sindresorhus/open/issues/205 */ async function openWithSystemRootEnvironment( appId: string | Readonly, options?: open.OpenAppOptions ): Promise { const oldSystemRoot = process.env.SYSTEMROOT; try { process.env.SYSTEMROOT = process.env.SYSTEMROOT ?? process.env.SystemRoot; return await open.openApp(appId, options); } finally { process.env.SYSTEMROOT = oldSystemRoot; } }