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