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 { env, stdio: 'ignore' } 41 ); 42 result = status === 0; 43 } catch { 44 result = false; 45 } 46 return result; 47 } 48 49 async createTempBrowserDir(baseDirName: string) { 50 let tmpDir; 51 if (IS_WSL) { 52 // On WSL, the browser is actually launched in host, the `temp-dir` returns the linux /tmp path where host browsers cannot reach into. 53 // We should get the temp path through the `$TEMP` windows environment variable. 54 tmpDir = (await spawnAsync('powershell.exe', ['-c', 'echo "$Env:TEMP"'])).stdout.trim(); 55 return `${tmpDir}\\${baseDirName}`; 56 } else { 57 tmpDir = require('temp-dir'); 58 return path.join(tmpDir, baseDirName); 59 } 60 } 61 62 async launchAsync( 63 browserType: LaunchBrowserTypes, 64 args: string[] 65 ): Promise<LaunchBrowserInstance> { 66 const appId = this.MAP[browserType].appId; 67 await openWithSystemRootEnvironment(appId, { arguments: args }); 68 this._appId = appId; 69 return this; 70 } 71 72 async close(): Promise<void> { 73 if (this._appId != null) { 74 try { 75 // Since we wrap all spawn calls through powershell as well as from `open.openApp`, the returned ChildProcess is not the browser process. 76 // And we cannot just call `process.kill()` kill it. 77 // The implementation tries to find the pid of target chromium browser process (with --app=https://chrome-devtools-frontend.appspot.com in command arguments), 78 // and uses taskkill to terminate the process. 79 const env = await this.getPowershellEnv(); 80 await spawnAsync( 81 'powershell.exe', 82 [ 83 '-c', 84 `taskkill.exe /pid @(Get-WmiObject Win32_Process -Filter "name = '${this._appId}.exe' AND CommandLine LIKE '%chrome-devtools-frontend.appspot.com%'" | Select-Object -ExpandProperty ProcessId)`, 85 ], 86 { env, stdio: 'ignore' } 87 ); 88 } catch {} 89 this._appId = undefined; 90 } 91 } 92 93 /** 94 * This method is used to get the powershell environment variables for `Get-Package` command. 95 * Especially for powershell 7, its default `PSModulePath` is different from powershell 5 and `Get-Package` command is not available. 96 * We need to set the PSModulePath to include the default value of powershell 5. 97 */ 98 private async getPowershellEnv(): Promise<{ [key: string]: string }> { 99 if (this._powershellEnv) { 100 return this._powershellEnv; 101 } 102 const PSModulePath = ( 103 await spawnAsync('powershell.exe', ['-c', 'echo "$PSHOME\\Modules"']) 104 ).stdout.trim(); 105 this._powershellEnv = { 106 PSModulePath, 107 }; 108 return this._powershellEnv; 109 } 110} 111 112/** 113 * Due to a bug in `open` on Windows PowerShell, we need to ensure `process.env.SYSTEMROOT` is set. 114 * This environment variable is set by Windows on `SystemRoot`, causing `open` to execute a command with an "unknown" drive letter. 115 * 116 * @see https://github.com/sindresorhus/open/issues/205 117 */ 118async function openWithSystemRootEnvironment( 119 appId: string | Readonly<string[]>, 120 options?: open.OpenAppOptions 121): Promise<import('child_process').ChildProcess> { 122 const oldSystemRoot = process.env.SYSTEMROOT; 123 try { 124 process.env.SYSTEMROOT = process.env.SYSTEMROOT ?? process.env.SystemRoot; 125 return await open.openApp(appId, options); 126 } finally { 127 process.env.SYSTEMROOT = oldSystemRoot; 128 } 129} 130