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