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