Skip to content

Use vscode.workspace.fs and suppress startup banner for dotnet installs of PowerShell #4121

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/features/Examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class ExamplesFeature implements vscode.Disposable {
vscode.commands.executeCommand("vscode.openFolder", this.examplesPath, true);
// Return existence of the path for testing. The `vscode.openFolder`
// command should do this, but doesn't (yet).
return utils.fileExists(this.examplesPath);
return utils.checkIfFileExists(this.examplesPath);
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/features/PesterTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export class PesterTestsFeature implements vscode.Disposable {
//
// Ensure the necessary script exists (for testing). The debugger will
// start regardless, but we also pass its success along.
return utils.fileExists(this.invokePesterStubScriptPath)
return utils.checkIfFileExists(this.invokePesterStubScriptPath)
&& vscode.debug.startDebugging(vscode.workspace.workspaceFolders?.[0], launchConfig);
}
}
105 changes: 44 additions & 61 deletions src/platform.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import * as child_process from "child_process";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import * as process from "process";
import { IPowerShellAdditionalExePathSettings } from "./settings";
// This uses require so we can rewire it in unit tests!
// tslint:disable-next-line:no-var-requires
const utils = require("./utils")

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

const LinuxExePath = "/usr/bin/pwsh";
const LinuxExePath = "/usr/bin/pwsh";
const LinuxPreviewExePath = "/usr/bin/pwsh-preview";

const SnapExePath = "/snap/bin/pwsh";
const SnapPreviewExePath = "/snap/bin/pwsh-preview";
const SnapExePath = "/snap/bin/pwsh";
const SnapPreviewExePath = "/snap/bin/pwsh-preview";

const MacOSExePath = "/usr/local/bin/pwsh";
const MacOSExePath = "/usr/local/bin/pwsh";
const MacOSPreviewExePath = "/usr/local/bin/pwsh-preview";

export enum OperatingSystem {
Expand All @@ -36,6 +38,7 @@ export interface IPlatformDetails {
export interface IPowerShellExeDetails {
readonly displayName: string;
readonly exePath: string;
readonly supportsProperArguments: boolean;
}

export function getPlatformDetails(): IPlatformDetails {
Expand Down Expand Up @@ -97,17 +100,21 @@ export class PowerShellExeFinder {
/**
* Returns the first available PowerShell executable found in the search order.
*/
public getFirstAvailablePowerShellInstallation(): IPowerShellExeDetails {
for (const pwsh of this.enumeratePowerShellInstallations()) {
public async getFirstAvailablePowerShellInstallation(): Promise<IPowerShellExeDetails> {
for await (const pwsh of this.enumeratePowerShellInstallations()) {
return pwsh;
}
}

/**
* Get an array of all PowerShell executables found when searching for PowerShell installations.
*/
public getAllAvailablePowerShellInstallations(): IPowerShellExeDetails[] {
return Array.from(this.enumeratePowerShellInstallations());
public async getAllAvailablePowerShellInstallations(): Promise<IPowerShellExeDetails[]> {
const array: IPowerShellExeDetails[] = [];
for await (const pwsh of this.enumeratePowerShellInstallations()) {
array.push(pwsh);
}
return array;
}

/**
Expand Down Expand Up @@ -137,18 +144,18 @@ export class PowerShellExeFinder {
* PowerShell items returned by this object are verified
* to exist on the filesystem.
*/
public *enumeratePowerShellInstallations(): Iterable<IPowerShellExeDetails> {
public async *enumeratePowerShellInstallations(): AsyncIterable<IPowerShellExeDetails> {
// Get the default PowerShell installations first
for (const defaultPwsh of this.enumerateDefaultPowerShellInstallations()) {
if (defaultPwsh && defaultPwsh.exists()) {
for await (const defaultPwsh of this.enumerateDefaultPowerShellInstallations()) {
if (defaultPwsh && await defaultPwsh.exists()) {
yield defaultPwsh;
}
}

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

Expand All @@ -174,7 +181,7 @@ export class PowerShellExeFinder {
yield this.findPSCoreWindowsInstallation({ useAlternateBitness: true });

// Also look for the MSIX/UWP installation
yield this.findPSCoreMsix();
yield await this.findPSCoreMsix();

break;
}
Expand Down Expand Up @@ -213,7 +220,7 @@ export class PowerShellExeFinder {
}

/**
* Iterates through the configured additonal PowerShell executable locations,
* Iterates through the configured additional PowerShell executable locations,
* without checking for their existence.
*/
private *enumerateAdditionalPowerShellInstallations(): Iterable<IPossiblePowerShellExe> {
Expand All @@ -227,7 +234,7 @@ export class PowerShellExeFinder {
}
}

private findPSCoreStable(): IPossiblePowerShellExe {
private async findPSCoreStable(): Promise<IPossiblePowerShellExe> {
switch (this.platformDetails.operatingSystem) {
case OperatingSystem.Linux:
return new PossiblePowerShellExe(LinuxExePath, "PowerShell");
Expand All @@ -236,11 +243,11 @@ export class PowerShellExeFinder {
return new PossiblePowerShellExe(MacOSExePath, "PowerShell");

case OperatingSystem.Windows:
return this.findPSCoreWindowsInstallation();
return await this.findPSCoreWindowsInstallation();
}
}

private findPSCorePreview(): IPossiblePowerShellExe {
private async findPSCorePreview(): Promise<IPossiblePowerShellExe> {
switch (this.platformDetails.operatingSystem) {
case OperatingSystem.Linux:
return new PossiblePowerShellExe(LinuxPreviewExePath, "PowerShell Preview");
Expand All @@ -249,7 +256,7 @@ export class PowerShellExeFinder {
return new PossiblePowerShellExe(MacOSPreviewExePath, "PowerShell Preview");

case OperatingSystem.Windows:
return this.findPSCoreWindowsInstallation({ findPreview: true });
return await this.findPSCoreWindowsInstallation({ findPreview: true });
}
}

Expand All @@ -260,10 +267,11 @@ export class PowerShellExeFinder {

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

return new PossiblePowerShellExe(dotnetGlobalToolExePath, ".NET Core PowerShell Global Tool");
// The dotnet installed version of PowerShell does not support proper argument parsing, and so it fails with our multi-line startup banner.
return new PossiblePowerShellExe(dotnetGlobalToolExePath, ".NET Core PowerShell Global Tool", undefined, false);
}

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

if (!fileExistsSync(msixAppDir)) {
if (!await utils.checkIfDirectoryExists(msixAppDir)) {
return null;
}

Expand All @@ -282,6 +290,7 @@ export class PowerShellExeFinder {
: { pwshMsixDirRegex: PowerShellExeFinder.PwshMsixRegex, pwshMsixName: "PowerShell (Store)" };

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

private findPSCoreWindowsInstallation(
private async findPSCoreWindowsInstallation(
{ useAlternateBitness = false, findPreview = false }:
{ useAlternateBitness?: boolean; findPreview?: boolean } = {}): IPossiblePowerShellExe {
{ useAlternateBitness?: boolean; findPreview?: boolean } = {}): Promise<IPossiblePowerShellExe> {

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

Expand All @@ -314,13 +323,7 @@ export class PowerShellExeFinder {
const powerShellInstallBaseDir = path.join(programFilesPath, "PowerShell");

// Ensure the base directory exists
try {
const powerShellInstallBaseDirLStat = fs.lstatSync(powerShellInstallBaseDir);
if (!powerShellInstallBaseDirLStat.isDirectory())
{
return null;
}
} catch {
if (!await utils.checkIfDirectoryExists(powerShellInstallBaseDir)) {
return null;
}

Expand Down Expand Up @@ -366,7 +369,7 @@ export class PowerShellExeFinder {

// Now look for the file
const exePath = path.join(powerShellInstallBaseDir, item, "pwsh.exe");
if (!fs.existsSync(exePath)) {
if (!await utils.checkIfFileExists(exePath)) {
continue;
}

Expand Down Expand Up @@ -413,7 +416,7 @@ export class PowerShellExeFinder {
displayName = WindowsPowerShell32BitLabel;
}

winPS = new PossiblePowerShellExe(winPSPath, displayName, { knownToExist: true });
winPS = new PossiblePowerShellExe(winPSPath, displayName, true);

if (useAlternateBitness) {
this.alternateBitnessWinPS = winPS;
Expand Down Expand Up @@ -479,40 +482,20 @@ export function getWindowsSystemPowerShellPath(systemFolderName: string) {
"powershell.exe");
}

function fileExistsSync(filePath: string): boolean {
try {
// This will throw if the path does not exist,
// and otherwise returns a value that we don't care about
fs.lstatSync(filePath);
return true;
} catch {
return false;
}
}

interface IPossiblePowerShellExe extends IPowerShellExeDetails {
exists(): boolean;
exists(): Promise<boolean>;
}

class PossiblePowerShellExe implements IPossiblePowerShellExe {
public readonly exePath: string;
public readonly displayName: string;

private knownToExist: boolean;

constructor(
pathToExe: string,
installationName: string,
{ knownToExist = false }: { knownToExist?: boolean } = {}) {

this.exePath = pathToExe;
this.displayName = installationName;
this.knownToExist = knownToExist || undefined;
}
public readonly exePath: string,
public readonly displayName: string,
private knownToExist?: boolean,
public readonly supportsProperArguments: boolean = true) { }

public exists(): boolean {
public async exists(): Promise<boolean> {
if (this.knownToExist === undefined) {
this.knownToExist = fileExistsSync(this.exePath);
this.knownToExist = await utils.checkIfFileExists(this.exePath);
}
return this.knownToExist;
}
Expand Down
6 changes: 4 additions & 2 deletions src/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import vscode = require("vscode");
import { Logger } from "./logging";
import Settings = require("./settings");
import utils = require("./utils");
import { IEditorServicesSessionDetails, SessionManager } from "./session";
import { IEditorServicesSessionDetails } from "./session";

export class PowerShellProcess {
public static escapeSingleQuotes(psPath: string): string {
Expand Down Expand Up @@ -83,12 +83,14 @@ export class PowerShellProcess {
PowerShellProcess.escapeSingleQuotes(psesModulePath) +
"'; Start-EditorServices " + this.startPsesArgs;

// On Windows we unfortunately can't Base64 encode the startup command
// because it annoys some poorly implemented anti-virus scanners.
if (utils.isWindows) {
powerShellArgs.push(
"-Command",
startEditorServices);
} else {
// Use -EncodedCommand for better quote support on non-Windows
// Otherwise use -EncodedCommand for better quote support.
powerShellArgs.push(
"-EncodedCommand",
Buffer.from(startEditorServices, "utf16le").toString("base64"));
Expand Down
19 changes: 13 additions & 6 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,9 @@ export class SessionManager implements Middleware {
this.migrateWhitespaceAroundPipeSetting();

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

this.PowerShellExeDetails = powerShellExeDetails ||
this.powershellExeFinder.getFirstAvailablePowerShellInstallation();
await this.powershellExeFinder.getFirstAvailablePowerShellInstallation();

} catch (e) {
this.log.writeError(`Error occurred while searching for a PowerShell executable:\n${e}`);
Expand Down Expand Up @@ -215,6 +215,13 @@ export class SessionManager implements Middleware {

if (this.sessionSettings.integratedConsole.suppressStartupBanner) {
this.editorServicesArgs += "-StartupBanner '' ";
} else if (utils.isWindows && !this.PowerShellExeDetails.supportsProperArguments) {
// NOTE: On Windows we don't Base64 encode the startup command
// because it annoys some poorly implemented anti-virus scanners.
// Unfortunately this means that for some installs of PowerShell
// (such as through the `dotnet` package manager), we can't include
// a multi-line startup banner as the quotes break the command.
this.editorServicesArgs += `-StartupBanner '${this.HostName} Extension v${this.HostVersion}' `;
} else {
const startupBanner = `${this.HostName} Extension v${this.HostVersion}
Copyright (c) Microsoft Corporation.
Expand Down Expand Up @@ -463,7 +470,7 @@ Type 'help' to get help.
private registerCommands(): void {
this.registeredCommands = [
vscode.commands.registerCommand("PowerShell.RestartSession", () => { this.restartSession(); }),
vscode.commands.registerCommand(this.ShowSessionMenuCommandName, () => { this.showSessionMenu(); }),
vscode.commands.registerCommand(this.ShowSessionMenuCommandName, async () => { await this.showSessionMenu(); }),
vscode.workspace.onDidChangeConfiguration(async () => { await this.onConfigurationUpdated(); }),
vscode.commands.registerCommand(
"PowerShell.ShowSessionConsole", (isExecute?: boolean) => { this.showSessionConsole(isExecute); }),
Expand Down Expand Up @@ -795,8 +802,8 @@ Type 'help' to get help.
}
}

private showSessionMenu() {
const availablePowerShellExes = this.powershellExeFinder.getAllAvailablePowerShellInstallations();
private async showSessionMenu() {
const availablePowerShellExes = await this.powershellExeFinder.getAllAvailablePowerShellInstallations();

let sessionText: string;

Expand Down
Loading