Skip to content

Commit b119489

Browse files
nagilsonMiYanni
andauthored
Fix Powershell Execution Policy & Language Mode Logic (#2096)
* Set Execution Policy to Bypass. For #2095 Also includes whitespace changes. I should probably just make a PR that does all of that sometime. You can hide the whitespace changes in the GH diff view. See my read up there. Which I copied below. # Context There are language modes, which constrain powershell language features, and then execution policies, which constrain running scripts in general. I have had to set flags to enable running scripts by default from a powershell terminal, but there is no documentation indicating anything but that the language mode default is full language, or unrestricted. I also tried a sandbox and it was Full by default. 'FullLanguage is the default language mode for default sessions on all versions of Windows.' https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_modes?view=powershell-7.4&viewFallbackFrom=powershell-7.3#:~:text=The%20FullLanguage%20mode%20permits%20all%20language%20elements%20in%20the%20session.%20FullLanguage%20is%20the%20default%20language%20mode%20for%20default%20sessions%20on%20all%20versions%20of%20Windows. What causes our error to trigger is not Get-ExecutionPolicy but ExecutionContext.SessionState.LanguageMode. We should try to set the execution policy to bypass instead of restricted in our code as it prompts a dialogue for permission by default on unrestricted which could be the cause of unknown failures/timeouts, which is an awesome insight from @jacdavis. Even if the script is signed. Sadly this option does not seem to exist to bypass language mode like how you can for 1 script via execution policy. The error that spawned this issue triggers by checking the language mode: Your machine policy ConstrainedLanguage disables PowerShell language features that may be needed to install .NET. Read more at: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_language_modes?view=powershell-7.3. If you cannot safely and confidently change the execution policy, try setting a custom existingDotnetPath following our instructions here: https://github.com/dotnet/vscode-dotnet-runtime/blob/main/Documentation/troubleshooting-runtime.md. There is code I wrote that just fails it if ExecutionContext.SessionState.LanguageMode is set to ConstrainedLanguage or NoLanguage. vscode-dotnet-runtime/vscode-dotnet-runtime-library/src/Acquisition/AcquisitionInvoker.ts Line 195 in ef5299b if(languageMode === 'ConstrainedLanguage' || languageMode === 'NoLanguage') I dont think those 2 to 4 daily people you see with that error are going to go away. The code is failing on them because it sees their language mode is restricted which is not by default and is set by a SysAdmin. If the install script team knows if we can run the script in a way with a constrained language mode, then we could do that, but that sounds impossible. * Fix the error message about language mode and handle new error The language mode error said it was for execution policy which is very confusing. Now there are 2 separate errors and it has a clear message for both. * Handle Each Error Better * Consider that ubuntu 22 04 now supports .net 9 * Fix linting issue * Improve Test Logic * Fix Architecture Logic & Remove Old Code The ubuntu 22 04 version sometimes has 9.0 but older patches of it may not. CI machines are stuck on this logic and fail depending on the machine instance version so we need to work with either case for now. Also, the architecture should not be null in the local state, we want the data to be in sync after the install is made, so I've updated this logic. I removed the test language change that was there as well. * Fix the version compare in the linux test * Dont try to call powershell on non win os * Url doesn't need specific PowerShell version in it. Co-authored-by: Michael Yanni <[email protected]> * Fix url formation Co-authored-by: Michael Yanni <[email protected]> --------- Co-authored-by: Michael Yanni <[email protected]>
1 parent 6afcda9 commit b119489

File tree

6 files changed

+994
-627
lines changed

6 files changed

+994
-627
lines changed

sample/yarn.lock

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
version "1.17.0"
4343
resolved "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@azure/core-rest-pipeline/-/core-rest-pipeline-1.17.0.tgz"
4444
integrity sha1-Vdr6EJNVPFSe1tjbymmqUFx7OqM=
45+
"resolved" "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@azure/core-rest-pipeline/-/core-rest-pipeline-1.17.0.tgz"
4546
dependencies:
4647
"@azure/abort-controller" "^2.0.0"
4748
"@azure/core-auth" "^1.8.0"

vscode-dotnet-runtime-extension/src/test/functional/DotnetCoreAcquisitionExtension.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -622,7 +622,7 @@ suite('DotnetCoreAcquisitionExtension End to End', function ()
622622
else
623623
{
624624
const distroVersion = await new LinuxVersionResolver(mockAcquisitionContext, getMockUtilityContext()).getRunningDistro();
625-
assert.equal(result[0].version, Number(distroVersion) > 22.04 ? '9.0.1xx' : '8.0.1xx', 'The SDK did not recommend the version it was supposed to, which should be N.0.1xx based on surface level distro knowledge. If a new version is available, this test may need to be updated to the newest version.');
625+
assert.equal(result[0].version, Number(distroVersion.version) >= 22.04 ? '9.0.1xx' : '8.0.1xx', `The SDK did not recommend the version (it said ${result[0].version}) it was supposed to, which should be N.0.1xx based on surface level distro knowledge, version ${distroVersion.version}. If a new version is available, this test may need to be updated to the newest version.`);
626626
}
627627
}).timeout(standardTimeoutTime);
628628

vscode-dotnet-runtime-library/src/Acquisition/AcquisitionInvoker.ts

+129-72
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import path = require('path');
88

99
/* eslint-disable */ // When editing this file, please remove this and fix the linting concerns.
1010

11-
import {
11+
import
12+
{
1213
DotnetAcquisitionCompleted,
1314
DotnetAcquisitionInstallError,
1415
DotnetAcquisitionScriptError,
@@ -17,6 +18,8 @@ import {
1718
DotnetAcquisitionUnexpectedError,
1819
DotnetOfflineFailure,
1920
EventBasedError,
21+
PowershellBadExecutionPolicy,
22+
PowershellBadLanguageMode,
2023
} from '../EventStream/EventStreamEvents';
2124

2225
import { timeoutConstants } from '../Utils/ErrorHandler'
@@ -34,20 +37,22 @@ import { DotnetInstall } from './DotnetInstall';
3437
import { DotnetInstallMode } from './DotnetInstallMode';
3538
import { WebRequestWorker } from '../Utils/WebRequestWorker';
3639

37-
export class AcquisitionInvoker extends IAcquisitionInvoker {
40+
export class AcquisitionInvoker extends IAcquisitionInvoker
41+
{
3842
protected readonly scriptWorker: IInstallScriptAcquisitionWorker;
39-
protected fileUtilities : FileUtilities;
43+
protected fileUtilities: FileUtilities;
4044
private noPowershellError = `powershell.exe is not discoverable on your system. Is PowerShell added to your PATH and correctly installed? Please visit: https://learn.microsoft.com/powershell/scripting/install/installing-powershell-on-windows.
4145
You will need to restart VS Code after these changes. If PowerShell is still not discoverable, try setting a custom existingDotnetPath following our instructions here: https://github.com/dotnet/vscode-dotnet-runtime/blob/main/Documentation/troubleshooting-runtime.md.`
4246

43-
constructor(private readonly workerContext : IAcquisitionWorkerContext, private readonly utilityContext : IUtilityContext) {
47+
constructor(private readonly workerContext: IAcquisitionWorkerContext, private readonly utilityContext: IUtilityContext)
48+
{
4449

4550
super(workerContext.eventStream);
4651
this.scriptWorker = new InstallScriptAcquisitionWorker(workerContext);
4752
this.fileUtilities = new FileUtilities();
4853
}
4954

50-
public async installDotnet(installContext: IDotnetInstallationContext, install : DotnetInstall): Promise<void>
55+
public async installDotnet(installContext: IDotnetInstallationContext, install: DotnetInstall): Promise<void>
5156
{
5257
const winOS = os.platform() === 'win32';
5358
const installCommand = await this.getInstallCommand(installContext.version, installContext.installDir, installContext.installMode, installContext.architecture);
@@ -56,60 +61,76 @@ You will need to restart VS Code after these changes. If PowerShell is still not
5661
{
5762
try
5863
{
59-
let windowsFullCommand = `powershell.exe -NoProfile -NonInteractive -NoLogo -ExecutionPolicy unrestricted -Command "& { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12; & ${installCommand} }"`;
60-
if(winOS)
64+
let windowsFullCommand = `powershell.exe -NoProfile -NonInteractive -NoLogo -ExecutionPolicy bypass -Command "& { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12; & ${installCommand} }"`;
65+
let powershellReference = 'powershell.exe';
66+
if (winOS)
6167
{
62-
const powershellReference = await this.verifyPowershellCanRun(installContext, install);
68+
powershellReference = await this.verifyPowershellCanRun(installContext, install);
6369
windowsFullCommand = windowsFullCommand.replace('powershell.exe', powershellReference);
6470
}
6571

6672
cp.exec(winOS ? windowsFullCommand : installCommand,
67-
{ cwd: process.cwd(), maxBuffer: 500 * 1024, timeout: 1000 * installContext.timeoutSeconds, killSignal: 'SIGKILL' },
68-
async (error, stdout, stderr) =>
69-
{
70-
if (stdout)
73+
{ cwd: process.cwd(), maxBuffer: 500 * 1024, timeout: 1000 * installContext.timeoutSeconds, killSignal: 'SIGKILL' },
74+
async (error, stdout, stderr) =>
7175
{
76+
if (stdout)
77+
{
7278
this.eventStream.post(new DotnetAcquisitionScriptOutput(install, TelemetryUtilities.HashAllPaths(stdout)));
73-
}
74-
if (stderr)
75-
{
79+
}
80+
if (stderr)
81+
{
7682
this.eventStream.post(new DotnetAcquisitionScriptOutput(install, `STDERR: ${TelemetryUtilities.HashAllPaths(stderr)}`));
77-
}
78-
if (error)
79-
{
80-
if (!(await WebRequestWorker.isOnline(installContext.timeoutSeconds, this.eventStream)))
83+
}
84+
if (this.looksLikeBadExecutionPolicyError(stderr))
85+
{
86+
const badPolicyError = new EventBasedError('PowershellBadExecutionPolicy', `Your powershell execution policy does not allow script execution, so we can't automate the installation.
87+
Please read more at https://go.microsoft.com/fwlink/?LinkID=135170`);
88+
this.eventStream.post(new PowershellBadExecutionPolicy(badPolicyError, install));
89+
reject(badPolicyError);
90+
}
91+
if ((this.looksLikeBadLanguageModeError(stderr) || error?.code === 1) && this.badLanguageModeSet(powershellReference))
92+
{
93+
const badModeError = new EventBasedError('PowershellBadLanguageMode', `Your Language Mode disables PowerShell language features needed to install .NET. Read more at: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_language_modes.
94+
If you cannot change this flag, try setting a custom existingDotnetPath via the instructions here: https://github.com/dotnet/vscode-dotnet-runtime/blob/main/Documentation/troubleshooting-runtime.md.`);
95+
this.eventStream.post(new PowershellBadLanguageMode(badModeError, install));
96+
reject(badModeError);
97+
}
98+
if (error)
8199
{
82-
const offlineError = new EventBasedError('DotnetOfflineFailure', 'No internet connection detected: Cannot install .NET');
83-
this.eventStream.post(new DotnetOfflineFailure(offlineError, install));
84-
reject(offlineError);
100+
if (!(await WebRequestWorker.isOnline(installContext.timeoutSeconds, this.eventStream)))
101+
{
102+
const offlineError = new EventBasedError('DotnetOfflineFailure', 'No internet connection detected: Cannot install .NET');
103+
this.eventStream.post(new DotnetOfflineFailure(offlineError, install));
104+
reject(offlineError);
105+
}
106+
else if (error.signal === 'SIGKILL')
107+
{
108+
const newError = new EventBasedError('DotnetAcquisitionTimeoutError',
109+
`${timeoutConstants.timeoutMessage}, MESSAGE: ${error.message}, CODE: ${error.code}, KILLED: ${error.killed}`, error.stack);
110+
this.eventStream.post(new DotnetAcquisitionTimeoutError(error, install, installContext.timeoutSeconds));
111+
reject(newError);
112+
}
113+
else
114+
{
115+
const newError = new EventBasedError('DotnetAcquisitionInstallError',
116+
`${timeoutConstants.timeoutMessage}, MESSAGE: ${error.message}, CODE: ${error.code}, SIGNAL: ${error.signal}`, error.stack);
117+
this.eventStream.post(new DotnetAcquisitionInstallError(newError, install));
118+
reject(newError);
119+
}
85120
}
86-
else if (error.signal === 'SIGKILL') {
87-
const newError = new EventBasedError('DotnetAcquisitionTimeoutError',
88-
`${timeoutConstants.timeoutMessage}, MESSAGE: ${error.message}, CODE: ${error.code}, KILLED: ${error.killed}`, error.stack);
89-
this.eventStream.post(new DotnetAcquisitionTimeoutError(error, install, installContext.timeoutSeconds));
90-
reject(newError);
121+
else if (stderr && stderr.length > 0)
122+
{
123+
this.eventStream.post(new DotnetAcquisitionCompleted(install, installContext.dotnetPath, installContext.version));
124+
resolve();
91125
}
92126
else
93127
{
94-
const newError = new EventBasedError('DotnetAcquisitionInstallError',
95-
`${timeoutConstants.timeoutMessage}, MESSAGE: ${error.message}, CODE: ${error.code}, SIGNAL: ${error.signal}`, error.stack);
96-
this.eventStream.post(new DotnetAcquisitionInstallError(newError, install));
97-
reject(newError);
128+
this.eventStream.post(new DotnetAcquisitionCompleted(install, installContext.dotnetPath, installContext.version));
129+
resolve();
98130
}
99-
}
100-
else if (stderr && stderr.length > 0)
101-
{
102-
this.eventStream.post(new DotnetAcquisitionCompleted(install, installContext.dotnetPath, installContext.version));
103-
resolve();
104-
}
105-
else
106-
{
107-
this.eventStream.post(new DotnetAcquisitionCompleted(install, installContext.dotnetPath, installContext.version));
108-
resolve();
109-
}
110-
});
131+
});
111132
}
112-
catch (error : any)
133+
catch (error: any)
113134
{
114135
// Remove this when https://github.com/typescript-eslint/typescript-eslint/issues/2728 is done
115136
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
@@ -120,7 +141,53 @@ You will need to restart VS Code after these changes. If PowerShell is still not
120141
});
121142
}
122143

123-
private async getInstallCommand(version: string, dotnetInstallDir: string, installMode: DotnetInstallMode, architecture: string): Promise<string> {
144+
private looksLikeBadExecutionPolicyError(stderr: string): boolean
145+
{
146+
// tls12 is from the command output and gets truncated like so with this error
147+
// 135170 is the link id to the error, which may be subject to change but is a good language agnostic way to catch this
148+
// about_Execution_Policies this is a relatively language agnostic way to check as well
149+
return stderr.includes('+ ... ]::Tls12;') || stderr.includes('135170') || stderr.includes('about_Execution_Policies');
150+
}
151+
152+
private looksLikeBadLanguageModeError(stderr: string): boolean
153+
{
154+
/*
155+
This is one possible output of a failed command for this right now, but the install script might change so we account for multiple possibilities.
156+
157+
Failed to resolve the exact version number.
158+
At dotnet-install.ps1:1189 char:5
159+
+ throw "Failed to resolve the exact version number."
160+
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
161+
+ CategoryInfo : OperationStopped: (Failed to resol...version number.:String) [], RuntimeException
162+
+ FullyQualifiedErrorId : Failed to resolve the exact version number.
163+
*/
164+
165+
// Unexpected Token may also appear
166+
167+
return stderr.includes('FullyQualifiedErrorId') || stderr.includes('unexpectedToken')
168+
}
169+
170+
private badLanguageModeSet(powershellReference: string): boolean
171+
{
172+
if (os.platform() !== 'win32')
173+
{
174+
return false;
175+
}
176+
177+
try
178+
{
179+
const languageModeOutput = cp.spawnSync(powershellReference, [`-command`, `$ExecutionContext.SessionState.LanguageMode`], { cwd: path.resolve(__dirname), shell: true });
180+
const languageMode = languageModeOutput.stdout.toString().trim();
181+
return (languageMode === 'ConstrainedLanguage' || languageMode === 'NoLanguage');
182+
}
183+
catch (e: any)
184+
{
185+
return true;
186+
}
187+
}
188+
189+
private async getInstallCommand(version: string, dotnetInstallDir: string, installMode: DotnetInstallMode, architecture: string): Promise<string>
190+
{
124191
const arch = this.fileUtilities.nodeArchToDotnetArch(architecture, this.eventStream);
125192
let args = [
126193
'-InstallDir', this.escapeFilePath(dotnetInstallDir),
@@ -131,20 +198,21 @@ You will need to restart VS Code after these changes. If PowerShell is still not
131198
{
132199
args = args.concat('-Runtime', 'dotnet');
133200
}
134-
else if(installMode === 'aspnetcore')
201+
else if (installMode === 'aspnetcore')
135202
{
136203
args = args.concat('-Runtime', 'aspnetcore');
137204
}
138-
if(arch !== 'auto')
205+
if (arch !== 'auto')
139206
{
140207
args = args.concat('-Architecture', arch);
141208
}
142209

143210
const scriptPath = await this.scriptWorker.getDotnetInstallScriptPath();
144-
return `${ this.escapeFilePath(scriptPath) } ${ args.join(' ') }`;
211+
return `${this.escapeFilePath(scriptPath)} ${args.join(' ')}`;
145212
}
146213

147-
private escapeFilePath(pathToEsc: string): string {
214+
private escapeFilePath(pathToEsc: string): string
215+
{
148216
if (os.platform() === 'win32')
149217
{
150218
// Need to escape apostrophes with two apostrophes
@@ -163,52 +231,41 @@ You will need to restart VS Code after these changes. If PowerShell is still not
163231
* @remarks Some users have reported not having powershell.exe or having execution policy that fails property evaluation functions in powershell install scripts.
164232
* We use this function to throw better errors if powershell is not configured correctly.
165233
*/
166-
private async verifyPowershellCanRun(installContext : IDotnetInstallationContext, installId : DotnetInstall) : Promise<string>
234+
private async verifyPowershellCanRun(installContext: IDotnetInstallationContext, installId: DotnetInstall): Promise<string>
167235
{
168236
let knownError = false;
169237
let error = null;
170238
let command = null;
171239

172240
const possibleCommands =
173-
[
174-
CommandExecutor.makeCommand(`powershell.exe`, []),
175-
CommandExecutor.makeCommand(`%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`, []),
176-
CommandExecutor.makeCommand(`pwsh`, []),
177-
CommandExecutor.makeCommand(`powershell`, []),
178-
CommandExecutor.makeCommand(`pwsh.exe`, [])
179-
];
241+
[
242+
CommandExecutor.makeCommand(`powershell.exe`, []),
243+
CommandExecutor.makeCommand(`%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`, []),
244+
CommandExecutor.makeCommand(`pwsh`, []),
245+
CommandExecutor.makeCommand(`powershell`, []),
246+
CommandExecutor.makeCommand(`pwsh.exe`, [])
247+
];
180248

181249
try
182250
{
183251
// Check if PowerShell exists and is on the path.
184252
command = await new CommandExecutor(this.workerContext, this.utilityContext).tryFindWorkingCommand(possibleCommands);
185-
if(!command)
253+
if (!command)
186254
{
187255
knownError = true;
188256
const err = Error(this.noPowershellError);
189257
error = err;
190258
}
191-
192-
// Check Execution Policy
193-
const execPolicyOutput = cp.spawnSync(command!.commandRoot, [`-command`, `$ExecutionContext.SessionState.LanguageMode`], {cwd : path.resolve(__dirname), shell: true});
194-
const languageMode = execPolicyOutput.stdout.toString().trim();
195-
if(languageMode === 'ConstrainedLanguage' || languageMode === 'NoLanguage')
196-
{
197-
knownError = true;
198-
const err = Error(`Your machine policy ${languageMode} disables PowerShell language features that may be needed to install .NET. Read more at: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_language_modes?view=powershell-7.3.
199-
If you cannot safely and confidently change the execution policy, try setting a custom existingDotnetPath following our instructions here: https://github.com/dotnet/vscode-dotnet-runtime/blob/main/Documentation/troubleshooting-runtime.md.`);
200-
error = err;
201-
}
202259
}
203-
catch(err : any)
260+
catch (err: any)
204261
{
205-
if(!knownError)
262+
if (!knownError)
206263
{
207264
error = new Error(`${this.noPowershellError} More details: ${(err as Error).message}`);
208265
}
209266
}
210267

211-
if(error != null)
268+
if (error != null)
212269
{
213270
this.eventStream.post(new DotnetAcquisitionScriptError(error as Error, installId));
214271
throw new EventBasedError('DotnetAcquisitionScriptError', error?.message, error?.stack);

0 commit comments

Comments
 (0)