1edeec536SEvan Baconimport spawnAsync from '@expo/spawn-async'; 2edeec536SEvan Baconimport open from 'open'; 3edeec536SEvan Baconimport path from 'path'; 4edeec536SEvan Bacon 5edeec536SEvan Baconimport { 6edeec536SEvan Bacon LaunchBrowserTypes, 7edeec536SEvan Bacon type LaunchBrowserImpl, 8edeec536SEvan Bacon type LaunchBrowserInstance, 9edeec536SEvan Bacon} from './LaunchBrowser.types'; 10edeec536SEvan Bacon 11edeec536SEvan Baconconst IS_WSL = require('is-wsl') && !require('is-docker')(); 12edeec536SEvan Bacon 13edeec536SEvan Bacon/** 14edeec536SEvan Bacon * Browser implementation for Windows and WSL 15edeec536SEvan Bacon * 16edeec536SEvan Bacon * To minimize the difference between Windows and WSL, the implementation wraps all spawn calls through powershell. 17edeec536SEvan Bacon */ 18edeec536SEvan Baconexport default class LaunchBrowserImplWindows implements LaunchBrowserImpl, LaunchBrowserInstance { 19edeec536SEvan Bacon private _appId: string | undefined; 20edeec536SEvan Bacon private _powershellEnv: { [key: string]: string } | undefined; 21edeec536SEvan Bacon 22edeec536SEvan Bacon MAP = { 23edeec536SEvan Bacon [LaunchBrowserTypes.CHROME]: { 24edeec536SEvan Bacon appId: 'chrome', 25edeec536SEvan Bacon fullName: 'Google Chrome', 26edeec536SEvan Bacon }, 27edeec536SEvan Bacon [LaunchBrowserTypes.EDGE]: { 28edeec536SEvan Bacon appId: 'msedge', 29edeec536SEvan Bacon fullName: 'Microsoft Edge', 30edeec536SEvan Bacon }, 31edeec536SEvan Bacon }; 32edeec536SEvan Bacon 33edeec536SEvan Bacon async isSupportedBrowser(browserType: LaunchBrowserTypes): Promise<boolean> { 34edeec536SEvan Bacon let result = false; 35edeec536SEvan Bacon try { 36edeec536SEvan Bacon const env = await this.getPowershellEnv(); 37edeec536SEvan Bacon const { status } = await spawnAsync( 38edeec536SEvan Bacon 'powershell.exe', 39edeec536SEvan Bacon ['-c', `Get-Package -Name '${this.MAP[browserType].fullName}'`], 40*46f023faSEvan Bacon { 41*46f023faSEvan Bacon // @ts-expect-error: Missing NODE_ENV 42*46f023faSEvan Bacon env, 43*46f023faSEvan Bacon stdio: 'ignore', 44*46f023faSEvan Bacon } 45edeec536SEvan Bacon ); 46edeec536SEvan Bacon result = status === 0; 47edeec536SEvan Bacon } catch { 48edeec536SEvan Bacon result = false; 49edeec536SEvan Bacon } 50edeec536SEvan Bacon return result; 51edeec536SEvan Bacon } 52edeec536SEvan Bacon 53edeec536SEvan Bacon async createTempBrowserDir(baseDirName: string) { 54edeec536SEvan Bacon let tmpDir; 55edeec536SEvan Bacon if (IS_WSL) { 56edeec536SEvan Bacon // On WSL, the browser is actually launched in host, the `temp-dir` returns the linux /tmp path where host browsers cannot reach into. 57edeec536SEvan Bacon // We should get the temp path through the `$TEMP` windows environment variable. 58edeec536SEvan Bacon tmpDir = (await spawnAsync('powershell.exe', ['-c', 'echo "$Env:TEMP"'])).stdout.trim(); 59edeec536SEvan Bacon return `${tmpDir}\\${baseDirName}`; 60edeec536SEvan Bacon } else { 61edeec536SEvan Bacon tmpDir = require('temp-dir'); 62edeec536SEvan Bacon return path.join(tmpDir, baseDirName); 63edeec536SEvan Bacon } 64edeec536SEvan Bacon } 65edeec536SEvan Bacon 66edeec536SEvan Bacon async launchAsync( 67edeec536SEvan Bacon browserType: LaunchBrowserTypes, 68edeec536SEvan Bacon args: string[] 69edeec536SEvan Bacon ): Promise<LaunchBrowserInstance> { 70edeec536SEvan Bacon const appId = this.MAP[browserType].appId; 71edeec536SEvan Bacon await openWithSystemRootEnvironment(appId, { arguments: args }); 72edeec536SEvan Bacon this._appId = appId; 73edeec536SEvan Bacon return this; 74edeec536SEvan Bacon } 75edeec536SEvan Bacon 76edeec536SEvan Bacon async close(): Promise<void> { 77edeec536SEvan Bacon if (this._appId != null) { 78edeec536SEvan Bacon try { 79edeec536SEvan Bacon // Since we wrap all spawn calls through powershell as well as from `open.openApp`, the returned ChildProcess is not the browser process. 80edeec536SEvan Bacon // And we cannot just call `process.kill()` kill it. 81edeec536SEvan Bacon // The implementation tries to find the pid of target chromium browser process (with --app=https://chrome-devtools-frontend.appspot.com in command arguments), 82edeec536SEvan Bacon // and uses taskkill to terminate the process. 83edeec536SEvan Bacon const env = await this.getPowershellEnv(); 84edeec536SEvan Bacon await spawnAsync( 85edeec536SEvan Bacon 'powershell.exe', 86edeec536SEvan Bacon [ 87edeec536SEvan Bacon '-c', 88edeec536SEvan Bacon `taskkill.exe /pid @(Get-WmiObject Win32_Process -Filter "name = '${this._appId}.exe' AND CommandLine LIKE '%chrome-devtools-frontend.appspot.com%'" | Select-Object -ExpandProperty ProcessId)`, 89edeec536SEvan Bacon ], 90*46f023faSEvan Bacon { 91*46f023faSEvan Bacon // @ts-expect-error: Missing NODE_ENV 92*46f023faSEvan Bacon env, 93*46f023faSEvan Bacon stdio: 'ignore', 94*46f023faSEvan Bacon } 95edeec536SEvan Bacon ); 96edeec536SEvan Bacon } catch {} 97edeec536SEvan Bacon this._appId = undefined; 98edeec536SEvan Bacon } 99edeec536SEvan Bacon } 100edeec536SEvan Bacon 101edeec536SEvan Bacon /** 102edeec536SEvan Bacon * This method is used to get the powershell environment variables for `Get-Package` command. 103edeec536SEvan Bacon * Especially for powershell 7, its default `PSModulePath` is different from powershell 5 and `Get-Package` command is not available. 104edeec536SEvan Bacon * We need to set the PSModulePath to include the default value of powershell 5. 105edeec536SEvan Bacon */ 106edeec536SEvan Bacon private async getPowershellEnv(): Promise<{ [key: string]: string }> { 107edeec536SEvan Bacon if (this._powershellEnv) { 108edeec536SEvan Bacon return this._powershellEnv; 109edeec536SEvan Bacon } 110edeec536SEvan Bacon const PSModulePath = ( 111edeec536SEvan Bacon await spawnAsync('powershell.exe', ['-c', 'echo "$PSHOME\\Modules"']) 112edeec536SEvan Bacon ).stdout.trim(); 113edeec536SEvan Bacon this._powershellEnv = { 114edeec536SEvan Bacon PSModulePath, 115edeec536SEvan Bacon }; 116edeec536SEvan Bacon return this._powershellEnv; 117edeec536SEvan Bacon } 118edeec536SEvan Bacon} 119edeec536SEvan Bacon 120edeec536SEvan Bacon/** 121edeec536SEvan Bacon * Due to a bug in `open` on Windows PowerShell, we need to ensure `process.env.SYSTEMROOT` is set. 122edeec536SEvan Bacon * This environment variable is set by Windows on `SystemRoot`, causing `open` to execute a command with an "unknown" drive letter. 123edeec536SEvan Bacon * 124edeec536SEvan Bacon * @see https://github.com/sindresorhus/open/issues/205 125edeec536SEvan Bacon */ 126edeec536SEvan Baconasync function openWithSystemRootEnvironment( 127edeec536SEvan Bacon appId: string | Readonly<string[]>, 128edeec536SEvan Bacon options?: open.OpenAppOptions 129edeec536SEvan Bacon): Promise<import('child_process').ChildProcess> { 130edeec536SEvan Bacon const oldSystemRoot = process.env.SYSTEMROOT; 131edeec536SEvan Bacon try { 132edeec536SEvan Bacon process.env.SYSTEMROOT = process.env.SYSTEMROOT ?? process.env.SystemRoot; 133edeec536SEvan Bacon return await open.openApp(appId, options); 134edeec536SEvan Bacon } finally { 135edeec536SEvan Bacon process.env.SYSTEMROOT = oldSystemRoot; 136edeec536SEvan Bacon } 137edeec536SEvan Bacon} 138