From ff91c18bdf12b142d6f21f8ed5fd81e9835aae82 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Tue, 15 Mar 2022 11:29:27 +1300 Subject: [PATCH] fix: use `wslpath` to resolve Windows paths (#200) --- src/chrome-finder.ts | 9 ++-- src/chrome-launcher.ts | 4 +- src/utils.ts | 37 ++++++++++++-- test/utils-test.ts | 108 +++++++++++++++++++++++++++++++++++------ 4 files changed, 134 insertions(+), 24 deletions(-) diff --git a/src/chrome-finder.ts b/src/chrome-finder.ts index 52a1af364..4e0548cd9 100644 --- a/src/chrome-finder.ts +++ b/src/chrome-finder.ts @@ -12,7 +12,7 @@ import {execSync, execFileSync} from 'child_process'; import escapeRegExp = require('escape-string-regexp'); const log = require('lighthouse-logger'); -import {getLocalAppDataPath, ChromePathNotSetError} from './utils'; +import {getWSLLocalAppDataPath, toWSLPath, ChromePathNotSetError} from './utils'; const newLineRegex = /\r?\n/; @@ -175,9 +175,10 @@ export function linux() { export function wsl() { // Manually populate the environment variables assuming it's the default config - process.env.LOCALAPPDATA = getLocalAppDataPath(`${process.env.PATH}`); - process.env.PROGRAMFILES = '/mnt/c/Program Files'; - process.env['PROGRAMFILES(X86)'] = '/mnt/c/Program Files (x86)'; + process.env.LOCALAPPDATA = getWSLLocalAppDataPath(`${process.env.PATH}`); + process.env.PROGRAMFILES = toWSLPath('C:/Program Files', '/mnt/c/Program Files'); + process.env['PROGRAMFILES(X86)'] = + toWSLPath('C:/Program Files (x86)', '/mnt/c/Program Files (x86)'); return win32(); } diff --git a/src/chrome-launcher.ts b/src/chrome-launcher.ts index e705cfd5f..6d65e3d9f 100644 --- a/src/chrome-launcher.ts +++ b/src/chrome-launcher.ts @@ -11,7 +11,7 @@ import * as net from 'net'; import * as chromeFinder from './chrome-finder'; import {getRandomPort} from './random-port'; import {DEFAULT_FLAGS} from './flags'; -import {makeTmpDir, defaults, delay, getPlatform, toWinDirFormat, InvalidUserDataDirectoryError, UnsupportedPlatformError, ChromeNotInstalledError} from './utils'; +import {makeTmpDir, defaults, delay, getPlatform, toWin32Path, InvalidUserDataDirectoryError, UnsupportedPlatformError, ChromeNotInstalledError} from './utils'; import {ChildProcess} from 'child_process'; const log = require('lighthouse-logger'); const spawn = childProcess.spawn; @@ -172,7 +172,7 @@ class Launcher { if (!this.useDefaultProfile) { // Place Chrome profile in a custom location we'll rm -rf later // If in WSL, we need to use the Windows format - flags.push(`--user-data-dir=${isWsl ? toWinDirFormat(this.userDataDir) : this.userDataDir}`); + flags.push(`--user-data-dir=${isWsl ? toWin32Path(this.userDataDir) : this.userDataDir}`); } flags.push(...this.chromeFlags); diff --git a/src/utils.ts b/src/utils.ts index 5454131e0..1492705af 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -6,7 +6,7 @@ 'use strict'; import {join} from 'path'; -import {execSync} from 'child_process'; +import {execSync, execFileSync} from 'child_process'; import {mkdirSync} from 'fs'; import isWsl = require('is-wsl'); @@ -65,7 +65,7 @@ export function makeTmpDir() { return makeUnixTmpDir(); case 'wsl': // We populate the user's Windows temp dir so the folder is correctly created later - process.env.TEMP = getLocalAppDataPath(`${process.env.PATH}`); + process.env.TEMP = getWSLLocalAppDataPath(`${process.env.PATH}`); case 'win32': return makeWin32TmpDir(); default: @@ -73,8 +73,9 @@ export function makeTmpDir() { } } -export function toWinDirFormat(dir: string = ''): string { +function toWinDirFormat(dir: string = ''): string { const results = /\/mnt\/([a-z])\//.exec(dir); + if (!results) { return dir; } @@ -84,13 +85,41 @@ export function toWinDirFormat(dir: string = ''): string { .replace(/\//g, '\\'); } -export function getLocalAppDataPath(path: string): string { +export function toWin32Path(dir: string = ''): string { + if (/[a-z]:\\/iu.test(dir)) { + return dir; + } + + try { + return execFileSync('wslpath', ['-w', dir]).toString().trim(); + } catch { + return toWinDirFormat(dir); + } +} + +export function toWSLPath(dir: string, fallback: string): string { + try { + return execFileSync('wslpath', ['-u', dir]).toString().trim(); + } catch { + return fallback; + } +} + +function getLocalAppDataPath(path: string): string { const userRegExp = /\/mnt\/([a-z])\/Users\/([^\/:]+)\/AppData\//; const results = userRegExp.exec(path) || []; return `/mnt/${results[1]}/Users/${results[2]}/AppData/Local`; } +export function getWSLLocalAppDataPath(path: string): string { + const userRegExp = /\/([a-z])\/Users\/([^\/:]+)\/AppData\//; + const results = userRegExp.exec(path) || []; + + return toWSLPath( + `${results[1]}:\\Users\\${results[2]}\\AppData\\Local`, getLocalAppDataPath(path)); +} + function makeUnixTmpDir() { return execSync('mktemp -d -t lighthouse.XXXXXXX').toString().trim(); } diff --git a/test/utils-test.ts b/test/utils-test.ts index d702eea98..72cb0dd66 100644 --- a/test/utils-test.ts +++ b/test/utils-test.ts @@ -16,24 +16,104 @@ 'use strict'; import * as assert from 'assert'; -import {toWinDirFormat, getLocalAppDataPath} from '../src/utils'; +import { toWin32Path, toWSLPath, getWSLLocalAppDataPath } from '../src/utils'; +import * as sinon from 'sinon'; +import * as child_process from 'child_process'; -describe('WSL path format to Windows', () => { - it('transforms basic path', () => { - const wsl = '/mnt/c/Users/user1/AppData/'; - const windows = 'C:\\Users\\user1\\AppData\\'; - assert.strictEqual(toWinDirFormat(wsl), windows); - }); +const execFileSyncStub = sinon.stub(child_process, 'execFileSync').callThrough(); + +const asBuffer = (str: string): Buffer => Buffer.from(str, 'utf-8'); + +describe('toWin32Path', () => { + beforeEach(() => execFileSyncStub.reset()); + + it('calls toWin32Path -w', () => { + execFileSyncStub.returns(asBuffer('')); + + toWin32Path(''); + + assert.ok(execFileSyncStub.calledWith('wslpath', ['-w', ''])); + }) + + describe('when the path is already in Windows format', () => { + it('returns early', () => { + execFileSyncStub.returns(asBuffer('')); - it('transforms if drive letter is different than c', () => { - const wsl = '/mnt/d/Users/user1/AppData'; - const windows = 'D:\\Users\\user1\\AppData'; - assert.strictEqual(toWinDirFormat(wsl), windows); + assert.strictEqual(toWin32Path('D:\\'), 'D:\\'); + assert.strictEqual(toWin32Path('C:\\'), 'C:\\'); + + assert.ok(execFileSyncStub.notCalled); + }); + }) + + describe('when wslpath is not available', () => { + beforeEach(() => execFileSyncStub.throws(new Error('oh noes!'))); + + it('falls back to the toWinDirFormat method', () => { + const wsl = '/mnt/c/Users/user1/AppData/'; + const windows = 'C:\\Users\\user1\\AppData\\'; + + assert.strictEqual(toWin32Path(wsl), windows); + }); + + it('supports the drive letter not being C', () => { + const wsl = '/mnt/d/Users/user1/AppData'; + const windows = 'D:\\Users\\user1\\AppData'; + + assert.strictEqual(toWin32Path(wsl), windows); + }) }); +}) + +describe('toWSLPath', () => { + beforeEach(() => execFileSyncStub.reset()); + + it('calls wslpath -u', () => { + execFileSyncStub.returns(asBuffer('')); + + toWSLPath('', ''); + + assert.ok(execFileSyncStub.calledWith('wslpath', ['-u', ''])); + }) + + it('trims off the trailing newline', () => { + execFileSyncStub.returns(asBuffer('the-path\n')); + + assert.strictEqual(toWSLPath('', ''), 'the-path'); + }) + + describe('when wslpath is not available', () => { + beforeEach(() => execFileSyncStub.throws(new Error('oh noes!'))); + + it('uses the fallback path', () => { + assert.strictEqual( + toWSLPath('C:/Program Files', '/mnt/c/Program Files'), + '/mnt/c/Program Files' + ); + }) + }) +}) + +describe('getWSLLocalAppDataPath', () => { + beforeEach(() => execFileSyncStub.reset()); + + it('transforms it to a Linux path using wslpath', () => { + execFileSyncStub.returns(asBuffer('/c/folder/')); - it('getLocalAppDataPath returns a correct path', () => { const path = '/mnt/c/Users/user1/.bin:/mnt/c/Users/user1:/mnt/c/Users/user1/AppData/'; - const appDataPath = '/mnt/c/Users/user1/AppData/Local'; - assert.strictEqual(getLocalAppDataPath(path), appDataPath); + + assert.strictEqual(getWSLLocalAppDataPath(path), '/c/folder/'); + assert.ok(execFileSyncStub.calledWith('wslpath', ['-u', 'c:\\Users\\user1\\AppData\\Local'])); + }); + + describe('when wslpath is not available', () => { + beforeEach(() => execFileSyncStub.throws(new Error('oh noes!'))); + + it('falls back to the getLocalAppDataPath method', () => { + const path = '/mnt/c/Users/user1/.bin:/mnt/c/Users/user1:/mnt/c/Users/user1/AppData/'; + const appDataPath = '/mnt/c/Users/user1/AppData/Local'; + + assert.strictEqual(getWSLLocalAppDataPath(path), appDataPath); + }); }); });