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