Skip to content

Commit 25ee100

Browse files
committed
Refactor PowerShell finder to be async
1 parent ad7177e commit 25ee100

File tree

3 files changed

+55
-76
lines changed

3 files changed

+55
-76
lines changed

Diff for: src/platform.ts

+40-61
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
import * as child_process from "child_process";
54
import * as fs from "fs";
65
import * as os from "os";
76
import * as path from "path";
87
import * as process from "process";
98
import { IPowerShellAdditionalExePathSettings } from "./settings";
9+
import { checkIfFileExists, checkIfDirectoryExists } from "./utils";
1010

1111
const WindowsPowerShell64BitLabel = "Windows PowerShell (x64)";
1212
const WindowsPowerShell32BitLabel = "Windows PowerShell (x86)";
1313

14-
const LinuxExePath = "/usr/bin/pwsh";
14+
const LinuxExePath = "/usr/bin/pwsh";
1515
const LinuxPreviewExePath = "/usr/bin/pwsh-preview";
1616

17-
const SnapExePath = "/snap/bin/pwsh";
18-
const SnapPreviewExePath = "/snap/bin/pwsh-preview";
17+
const SnapExePath = "/snap/bin/pwsh";
18+
const SnapPreviewExePath = "/snap/bin/pwsh-preview";
1919

20-
const MacOSExePath = "/usr/local/bin/pwsh";
20+
const MacOSExePath = "/usr/local/bin/pwsh";
2121
const MacOSPreviewExePath = "/usr/local/bin/pwsh-preview";
2222

2323
export enum OperatingSystem {
@@ -97,17 +97,21 @@ export class PowerShellExeFinder {
9797
/**
9898
* Returns the first available PowerShell executable found in the search order.
9999
*/
100-
public getFirstAvailablePowerShellInstallation(): IPowerShellExeDetails {
101-
for (const pwsh of this.enumeratePowerShellInstallations()) {
100+
public async getFirstAvailablePowerShellInstallation(): Promise<IPowerShellExeDetails> {
101+
for await (const pwsh of this.enumeratePowerShellInstallations()) {
102102
return pwsh;
103103
}
104104
}
105105

106106
/**
107107
* Get an array of all PowerShell executables found when searching for PowerShell installations.
108108
*/
109-
public getAllAvailablePowerShellInstallations(): IPowerShellExeDetails[] {
110-
return Array.from(this.enumeratePowerShellInstallations());
109+
public async getAllAvailablePowerShellInstallations(): Promise<IPowerShellExeDetails[]> {
110+
const array: IPowerShellExeDetails[] = [];
111+
for await (const pwsh of this.enumeratePowerShellInstallations()) {
112+
array.push(pwsh);
113+
}
114+
return array;
111115
}
112116

113117
/**
@@ -137,18 +141,18 @@ export class PowerShellExeFinder {
137141
* PowerShell items returned by this object are verified
138142
* to exist on the filesystem.
139143
*/
140-
public *enumeratePowerShellInstallations(): Iterable<IPowerShellExeDetails> {
144+
public async *enumeratePowerShellInstallations(): AsyncIterable<IPowerShellExeDetails> {
141145
// Get the default PowerShell installations first
142-
for (const defaultPwsh of this.enumerateDefaultPowerShellInstallations()) {
143-
if (defaultPwsh && defaultPwsh.exists()) {
146+
for await (const defaultPwsh of this.enumerateDefaultPowerShellInstallations()) {
147+
if (defaultPwsh && await defaultPwsh.exists()) {
144148
yield defaultPwsh;
145149
}
146150
}
147151

148152
// Also show any additionally configured PowerShells
149153
// These may be duplicates of the default installations, but given a different name.
150154
for (const additionalPwsh of this.enumerateAdditionalPowerShellInstallations()) {
151-
if (additionalPwsh && additionalPwsh.exists()) {
155+
if (additionalPwsh && await additionalPwsh.exists()) {
152156
yield additionalPwsh;
153157
}
154158
}
@@ -159,7 +163,7 @@ export class PowerShellExeFinder {
159163
* Returned values may not exist, but come with an .exists property
160164
* which will check whether the executable exists.
161165
*/
162-
private *enumerateDefaultPowerShellInstallations(): Iterable<IPossiblePowerShellExe> {
166+
private async *enumerateDefaultPowerShellInstallations(): AsyncIterable<IPossiblePowerShellExe> {
163167
// Find PSCore stable first
164168
yield this.findPSCoreStable();
165169

@@ -174,7 +178,7 @@ export class PowerShellExeFinder {
174178
yield this.findPSCoreWindowsInstallation({ useAlternateBitness: true });
175179

176180
// Also look for the MSIX/UWP installation
177-
yield this.findPSCoreMsix();
181+
yield await this.findPSCoreMsix();
178182

179183
break;
180184
}
@@ -213,7 +217,7 @@ export class PowerShellExeFinder {
213217
}
214218

215219
/**
216-
* Iterates through the configured additonal PowerShell executable locations,
220+
* Iterates through the configured additional PowerShell executable locations,
217221
* without checking for their existence.
218222
*/
219223
private *enumerateAdditionalPowerShellInstallations(): Iterable<IPossiblePowerShellExe> {
@@ -227,7 +231,7 @@ export class PowerShellExeFinder {
227231
}
228232
}
229233

230-
private findPSCoreStable(): IPossiblePowerShellExe {
234+
private async findPSCoreStable(): Promise<IPossiblePowerShellExe> {
231235
switch (this.platformDetails.operatingSystem) {
232236
case OperatingSystem.Linux:
233237
return new PossiblePowerShellExe(LinuxExePath, "PowerShell");
@@ -236,11 +240,11 @@ export class PowerShellExeFinder {
236240
return new PossiblePowerShellExe(MacOSExePath, "PowerShell");
237241

238242
case OperatingSystem.Windows:
239-
return this.findPSCoreWindowsInstallation();
243+
return await this.findPSCoreWindowsInstallation();
240244
}
241245
}
242246

243-
private findPSCorePreview(): IPossiblePowerShellExe {
247+
private async findPSCorePreview(): Promise<IPossiblePowerShellExe> {
244248
switch (this.platformDetails.operatingSystem) {
245249
case OperatingSystem.Linux:
246250
return new PossiblePowerShellExe(LinuxPreviewExePath, "PowerShell Preview");
@@ -249,7 +253,7 @@ export class PowerShellExeFinder {
249253
return new PossiblePowerShellExe(MacOSPreviewExePath, "PowerShell Preview");
250254

251255
case OperatingSystem.Windows:
252-
return this.findPSCoreWindowsInstallation({ findPreview: true });
256+
return await this.findPSCoreWindowsInstallation({ findPreview: true });
253257
}
254258
}
255259

@@ -260,10 +264,10 @@ export class PowerShellExeFinder {
260264

261265
const dotnetGlobalToolExePath: string = path.join(os.homedir(), ".dotnet", "tools", exeName);
262266

263-
return new PossiblePowerShellExe(dotnetGlobalToolExePath, ".NET Core PowerShell Global Tool");
267+
return new PossiblePowerShellExe(dotnetGlobalToolExePath, ".NET Core PowerShell Global Tool", undefined, false);
264268
}
265269

266-
private findPSCoreMsix({ findPreview }: { findPreview?: boolean } = {}): IPossiblePowerShellExe {
270+
private async findPSCoreMsix({ findPreview }: { findPreview?: boolean } = {}): Promise<IPossiblePowerShellExe> {
267271
// We can't proceed if there's no LOCALAPPDATA path
268272
if (!process.env.LOCALAPPDATA) {
269273
return null;
@@ -272,7 +276,7 @@ export class PowerShellExeFinder {
272276
// Find the base directory for MSIX application exe shortcuts
273277
const msixAppDir = path.join(process.env.LOCALAPPDATA, "Microsoft", "WindowsApps");
274278

275-
if (!fileExistsSync(msixAppDir)) {
279+
if (!await checkIfDirectoryExists(msixAppDir)) {
276280
return null;
277281
}
278282

@@ -282,6 +286,7 @@ export class PowerShellExeFinder {
282286
: { pwshMsixDirRegex: PowerShellExeFinder.PwshMsixRegex, pwshMsixName: "PowerShell (Store)" };
283287

284288
// We should find only one such application, so return on the first one
289+
// TODO: Use VS Code async fs API for this.
285290
for (const subdir of fs.readdirSync(msixAppDir)) {
286291
if (pwshMsixDirRegex.test(subdir)) {
287292
const pwshMsixPath = path.join(msixAppDir, subdir, "pwsh.exe");
@@ -301,9 +306,9 @@ export class PowerShellExeFinder {
301306
return new PossiblePowerShellExe(SnapPreviewExePath, "PowerShell Preview Snap");
302307
}
303308

304-
private findPSCoreWindowsInstallation(
309+
private async findPSCoreWindowsInstallation(
305310
{ useAlternateBitness = false, findPreview = false }:
306-
{ useAlternateBitness?: boolean; findPreview?: boolean } = {}): IPossiblePowerShellExe {
311+
{ useAlternateBitness?: boolean; findPreview?: boolean } = {}): Promise<IPossiblePowerShellExe> {
307312

308313
const programFilesPath: string = this.getProgramFilesPath({ useAlternateBitness });
309314

@@ -314,13 +319,7 @@ export class PowerShellExeFinder {
314319
const powerShellInstallBaseDir = path.join(programFilesPath, "PowerShell");
315320

316321
// Ensure the base directory exists
317-
try {
318-
const powerShellInstallBaseDirLStat = fs.lstatSync(powerShellInstallBaseDir);
319-
if (!powerShellInstallBaseDirLStat.isDirectory())
320-
{
321-
return null;
322-
}
323-
} catch {
322+
if (!await checkIfDirectoryExists(powerShellInstallBaseDir)) {
324323
return null;
325324
}
326325

@@ -366,7 +365,7 @@ export class PowerShellExeFinder {
366365

367366
// Now look for the file
368367
const exePath = path.join(powerShellInstallBaseDir, item, "pwsh.exe");
369-
if (!fs.existsSync(exePath)) {
368+
if (!await checkIfFileExists(exePath)) {
370369
continue;
371370
}
372371

@@ -413,7 +412,7 @@ export class PowerShellExeFinder {
413412
displayName = WindowsPowerShell32BitLabel;
414413
}
415414

416-
winPS = new PossiblePowerShellExe(winPSPath, displayName, { knownToExist: true });
415+
winPS = new PossiblePowerShellExe(winPSPath, displayName, true);
417416

418417
if (useAlternateBitness) {
419418
this.alternateBitnessWinPS = winPS;
@@ -479,40 +478,20 @@ export function getWindowsSystemPowerShellPath(systemFolderName: string) {
479478
"powershell.exe");
480479
}
481480

482-
function fileExistsSync(filePath: string): boolean {
483-
try {
484-
// This will throw if the path does not exist,
485-
// and otherwise returns a value that we don't care about
486-
fs.lstatSync(filePath);
487-
return true;
488-
} catch {
489-
return false;
490-
}
491-
}
492-
493481
interface IPossiblePowerShellExe extends IPowerShellExeDetails {
494-
exists(): boolean;
482+
exists(): Promise<boolean>;
495483
}
496484

497485
class PossiblePowerShellExe implements IPossiblePowerShellExe {
498-
public readonly exePath: string;
499-
public readonly displayName: string;
500-
501-
private knownToExist: boolean;
502-
503486
constructor(
504-
pathToExe: string,
505-
installationName: string,
506-
{ knownToExist = false }: { knownToExist?: boolean } = {}) {
507-
508-
this.exePath = pathToExe;
509-
this.displayName = installationName;
510-
this.knownToExist = knownToExist || undefined;
511-
}
487+
public readonly exePath: string,
488+
public readonly displayName: string,
489+
private knownToExist?: boolean,
490+
public readonly supportsProperArguments: boolean = true) { }
512491

513-
public exists(): boolean {
492+
public async exists(): Promise<boolean> {
514493
if (this.knownToExist === undefined) {
515-
this.knownToExist = fileExistsSync(this.exePath);
494+
this.knownToExist = await checkIfFileExists(this.exePath);
516495
}
517496
return this.knownToExist;
518497
}

Diff for: src/session.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,9 @@ export class SessionManager implements Middleware {
148148
this.migrateWhitespaceAroundPipeSetting();
149149

150150
try {
151-
let powerShellExeDetails;
151+
let powerShellExeDetails: IPowerShellExeDetails;
152152
if (this.sessionSettings.powerShellDefaultVersion) {
153-
for (const details of this.powershellExeFinder.enumeratePowerShellInstallations()) {
153+
for await (const details of this.powershellExeFinder.enumeratePowerShellInstallations()) {
154154
// Need to compare names case-insensitively, from https://stackoverflow.com/a/2140723
155155
const wantedName = this.sessionSettings.powerShellDefaultVersion;
156156
if (wantedName.localeCompare(details.displayName, undefined, { sensitivity: "accent" }) === 0) {
@@ -161,7 +161,7 @@ export class SessionManager implements Middleware {
161161
}
162162

163163
this.PowerShellExeDetails = powerShellExeDetails ||
164-
this.powershellExeFinder.getFirstAvailablePowerShellInstallation();
164+
await this.powershellExeFinder.getFirstAvailablePowerShellInstallation();
165165

166166
} catch (e) {
167167
this.log.writeError(`Error occurred while searching for a PowerShell executable:\n${e}`);
@@ -463,7 +463,7 @@ Type 'help' to get help.
463463
private registerCommands(): void {
464464
this.registeredCommands = [
465465
vscode.commands.registerCommand("PowerShell.RestartSession", () => { this.restartSession(); }),
466-
vscode.commands.registerCommand(this.ShowSessionMenuCommandName, () => { this.showSessionMenu(); }),
466+
vscode.commands.registerCommand(this.ShowSessionMenuCommandName, async () => { await this.showSessionMenu(); }),
467467
vscode.workspace.onDidChangeConfiguration(async () => { await this.onConfigurationUpdated(); }),
468468
vscode.commands.registerCommand(
469469
"PowerShell.ShowSessionConsole", (isExecute?: boolean) => { this.showSessionConsole(isExecute); }),
@@ -795,8 +795,8 @@ Type 'help' to get help.
795795
}
796796
}
797797

798-
private showSessionMenu() {
799-
const availablePowerShellExes = this.powershellExeFinder.getAllAvailablePowerShellInstallations();
798+
private async showSessionMenu() {
799+
const availablePowerShellExes = await this.powershellExeFinder.getAllAvailablePowerShellInstallations();
800800

801801
let sessionText: string;
802802

Diff for: test/core/platform.test.ts

+9-9
Original file line numberDiff line numberDiff line change
@@ -521,19 +521,19 @@ describe("Platform module", function () {
521521
}
522522
});
523523

524-
describe("Default PowerShell installation", function () {
524+
describe("Default PowerShell installation", async function () {
525525
afterEach(function () {
526526
sinon.restore();
527527
mockFS.restore();
528528
});
529529

530530
for (const testPlatform of successTestCases) {
531-
it(`Finds it on ${testPlatform.name}`, function () {
531+
it(`Finds it on ${testPlatform.name}`, async function () {
532532
setupTestEnvironment(testPlatform);
533533

534534
const powerShellExeFinder = new platform.PowerShellExeFinder(testPlatform.platformDetails);
535535

536-
const defaultPowerShell = powerShellExeFinder.getFirstAvailablePowerShellInstallation();
536+
const defaultPowerShell = await powerShellExeFinder.getFirstAvailablePowerShellInstallation();
537537
const expectedPowerShell = testPlatform.expectedPowerShellSequence[0];
538538

539539
assert.strictEqual(defaultPowerShell.exePath, expectedPowerShell.exePath);
@@ -542,12 +542,12 @@ describe("Platform module", function () {
542542
}
543543

544544
for (const testPlatform of errorTestCases) {
545-
it(`Fails gracefully on ${testPlatform.name}`, function () {
545+
it(`Fails gracefully on ${testPlatform.name}`, async function () {
546546
setupTestEnvironment(testPlatform);
547547

548548
const powerShellExeFinder = new platform.PowerShellExeFinder(testPlatform.platformDetails);
549549

550-
const defaultPowerShell = powerShellExeFinder.getFirstAvailablePowerShellInstallation();
550+
const defaultPowerShell = await powerShellExeFinder.getFirstAvailablePowerShellInstallation();
551551
assert.strictEqual(defaultPowerShell, undefined);
552552
});
553553
}
@@ -560,12 +560,12 @@ describe("Platform module", function () {
560560
});
561561

562562
for (const testPlatform of successTestCases) {
563-
it(`Finds them on ${testPlatform.name}`, function () {
563+
it(`Finds them on ${testPlatform.name}`, async function () {
564564
setupTestEnvironment(testPlatform);
565565

566566
const powerShellExeFinder = new platform.PowerShellExeFinder(testPlatform.platformDetails);
567567

568-
const foundPowerShells = powerShellExeFinder.getAllAvailablePowerShellInstallations();
568+
const foundPowerShells = await powerShellExeFinder.getAllAvailablePowerShellInstallations();
569569

570570
for (let i = 0; i < testPlatform.expectedPowerShellSequence.length; i++) {
571571
const foundPowerShell = foundPowerShells[i];
@@ -583,12 +583,12 @@ describe("Platform module", function () {
583583
}
584584

585585
for (const testPlatform of errorTestCases) {
586-
it(`Fails gracefully on ${testPlatform.name}`, function () {
586+
it(`Fails gracefully on ${testPlatform.name}`, async function () {
587587
setupTestEnvironment(testPlatform);
588588

589589
const powerShellExeFinder = new platform.PowerShellExeFinder(testPlatform.platformDetails);
590590

591-
const foundPowerShells = powerShellExeFinder.getAllAvailablePowerShellInstallations();
591+
const foundPowerShells = await powerShellExeFinder.getAllAvailablePowerShellInstallations();
592592
assert.strictEqual(foundPowerShells.length, 0);
593593
});
594594
}

0 commit comments

Comments
 (0)