Skip to content

Commit bfe68ea

Browse files
author
Kartik Raj
committed
Add diagnostic to validate ComSpec
1 parent 1668d06 commit bfe68ea

File tree

7 files changed

+74
-9
lines changed

7 files changed

+74
-9
lines changed

src/client/application/diagnostics/checks/pythonInterpreter.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ import { EventName } from '../../../telemetry/constants';
2828
import { IExtensionSingleActivationService } from '../../../activation/types';
2929
import { cache } from '../../../common/utils/decorators';
3030
import { noop } from '../../../common/utils/misc';
31+
import { IPythonExecutionFactory } from '../../../common/process/types';
32+
import { getOSType, OSType } from '../../../common/utils/platform';
33+
import { IFileSystem } from '../../../common/platform/types';
34+
import { traceError } from '../../../logging';
3135

3236
const messages = {
3337
[DiagnosticCodes.NoPythonInterpretersDiagnostic]: l10n.t(
@@ -36,6 +40,9 @@ const messages = {
3640
[DiagnosticCodes.InvalidPythonInterpreterDiagnostic]: l10n.t(
3741
'An Invalid Python interpreter is selected{0}, please try changing it to enable features such as IntelliSense, linting, and debugging. See output for more details regarding why the interpreter is invalid.',
3842
),
43+
[DiagnosticCodes.InvalidComspecDiagnostic]: l10n.t(
44+
"The environment variable 'Comspec' seems to be set to an invalid value. Please correct it to carry valid path to Command Prompt to enable features such as IntelliSense, linting, and debugging. See instructions which might help.",
45+
),
3946
};
4047

4148
export class InvalidPythonInterpreterDiagnostic extends BaseDiagnostic {
@@ -61,6 +68,12 @@ export class InvalidPythonInterpreterDiagnostic extends BaseDiagnostic {
6168
}
6269
}
6370

71+
export class DefaultShellDiagnostic extends BaseDiagnostic {
72+
constructor(code: DiagnosticCodes.InvalidComspecDiagnostic, resource: Resource, scope = DiagnosticScope.Global) {
73+
super(code, messages[code], DiagnosticSeverity.Error, scope, resource, undefined, 'always');
74+
}
75+
}
76+
6477
export const InvalidPythonInterpreterServiceId = 'InvalidPythonInterpreterServiceId';
6578

6679
@injectable()
@@ -73,7 +86,11 @@ export class InvalidPythonInterpreterService extends BaseDiagnosticsService
7386
@inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry,
7487
) {
7588
super(
76-
[DiagnosticCodes.NoPythonInterpretersDiagnostic, DiagnosticCodes.InvalidPythonInterpreterDiagnostic],
89+
[
90+
DiagnosticCodes.NoPythonInterpretersDiagnostic,
91+
DiagnosticCodes.InvalidPythonInterpreterDiagnostic,
92+
DiagnosticCodes.InvalidComspecDiagnostic,
93+
],
7794
serviceContainer,
7895
disposableRegistry,
7996
false,
@@ -103,6 +120,13 @@ export class InvalidPythonInterpreterService extends BaseDiagnosticsService
103120
public async _manualDiagnose(resource: Resource): Promise<IDiagnostic[]> {
104121
const workspaceService = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService);
105122
const interpreterService = this.serviceContainer.get<IInterpreterService>(IInterpreterService);
123+
const currentInterpreter = await interpreterService.getActiveInterpreter(resource);
124+
if (!currentInterpreter) {
125+
const diagnostics = await this.diagnoseDefaultShell(resource);
126+
if (diagnostics.length) {
127+
return diagnostics;
128+
}
129+
}
106130
const hasInterpreters = await interpreterService.hasInterpreters();
107131
const interpreterPathService = this.serviceContainer.get<IInterpreterPathService>(IInterpreterPathService);
108132
const isInterpreterSetToDefault = interpreterPathService.get(resource) === 'python';
@@ -118,7 +142,6 @@ export class InvalidPythonInterpreterService extends BaseDiagnosticsService
118142
];
119143
}
120144

121-
const currentInterpreter = await interpreterService.getActiveInterpreter(resource);
122145
if (!currentInterpreter) {
123146
return [
124147
new InvalidPythonInterpreterDiagnostic(
@@ -163,6 +186,17 @@ export class InvalidPythonInterpreterService extends BaseDiagnosticsService
163186

164187
private getCommandPrompts(diagnostic: IDiagnostic): { prompt: string; command?: IDiagnosticCommand }[] {
165188
const commandFactory = this.serviceContainer.get<IDiagnosticsCommandFactory>(IDiagnosticsCommandFactory);
189+
if (diagnostic.code === DiagnosticCodes.InvalidComspecDiagnostic) {
190+
return [
191+
{
192+
prompt: Common.instructions,
193+
command: commandFactory.createCommand(diagnostic, {
194+
type: 'launch',
195+
options: 'https://aka.ms/AAk3djo',
196+
}),
197+
},
198+
];
199+
}
166200
const prompts = [
167201
{
168202
prompt: Common.selectPythonInterpreter,
@@ -183,6 +217,32 @@ export class InvalidPythonInterpreterService extends BaseDiagnosticsService
183217
}
184218
return prompts;
185219
}
220+
221+
private async diagnoseDefaultShell(resource: Resource): Promise<IDiagnostic[]> {
222+
if (getOSType() !== OSType.Windows) {
223+
return [];
224+
}
225+
const executionFactory = this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory);
226+
const executionService = await executionFactory.create({ resource });
227+
try {
228+
await executionService.getExecutablePath({ throwOnError: true });
229+
} catch (ex) {
230+
if ((ex as Error).message?.includes('4058')) {
231+
// ENOENT (-4058) error is thrown by Node when the default shell is invalid.
232+
if (await this.isComspecInvalid()) {
233+
traceError('ComSpec is set to an invalid value', process.env.ComSpec);
234+
return [new DefaultShellDiagnostic(DiagnosticCodes.InvalidComspecDiagnostic, resource)];
235+
}
236+
}
237+
}
238+
return [];
239+
}
240+
241+
private async isComspecInvalid() {
242+
const comSpec = process.env.ComSpec ?? '';
243+
const fs = this.serviceContainer.get<IFileSystem>(IFileSystem);
244+
return fs.fileExists(comSpec);
245+
}
186246
}
187247

188248
function getOnCloseHandler(diagnostic: IDiagnostic): IDiagnosticMessageOnCloseHandler | undefined {

src/client/application/diagnostics/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export enum DiagnosticCodes {
1212
InvalidPythonPathInDebuggerLaunchDiagnostic = 'InvalidPythonPathInDebuggerLaunchDiagnostic',
1313
EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic = 'EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic',
1414
InvalidPythonInterpreterDiagnostic = 'InvalidPythonInterpreterDiagnostic',
15+
InvalidComspecDiagnostic = 'InvalidComspecDiagnostic',
1516
LSNotSupportedDiagnostic = 'LSNotSupportedDiagnostic',
1617
PythonPathDeprecatedDiagnostic = 'PythonPathDeprecatedDiagnostic',
1718
JustMyCodeDiagnostic = 'JustMyCodeDiagnostic',

src/client/common/process/pythonEnvironment.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class PythonEnvironment implements IPythonEnvironment {
4747
return this.cachedInterpreterInformation;
4848
}
4949

50-
public async getExecutablePath(): Promise<string | undefined> {
50+
public async getExecutablePath(options?: { throwOnError?: boolean }): Promise<string | undefined> {
5151
// If we've passed the python file, then return the file.
5252
// This is because on mac if using the interpreter /usr/bin/python2.7 we can get a different value for the path
5353
if (await this.deps.isValidExecutable(this.pythonPath)) {
@@ -59,7 +59,7 @@ class PythonEnvironment implements IPythonEnvironment {
5959
return result;
6060
}
6161
const python = this.getExecutionInfo();
62-
const promise = getExecutablePath(python, this.deps.shellExec);
62+
const promise = getExecutablePath(python, this.deps.shellExec, options);
6363
cachedExecutablePath.set(this.pythonPath, promise);
6464
return promise;
6565
}

src/client/common/process/pythonExecutionFactory.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ function createPythonService(procService: IProcessService, env: IPythonEnvironme
145145
const procs = createPythonProcessService(procService, env);
146146
return {
147147
getInterpreterInformation: () => env.getInterpreterInformation(),
148-
getExecutablePath: () => env.getExecutablePath(),
148+
getExecutablePath: (options?: { throwOnError?: boolean }) => env.getExecutablePath(options),
149149
isModuleInstalled: (m) => env.isModuleInstalled(m),
150150
getModuleVersion: (m) => env.getModuleVersion(m),
151151
getExecutionInfo: (a) => env.getExecutionInfo(a),

src/client/common/process/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export const IPythonExecutionService = Symbol('IPythonExecutionService');
8484

8585
export interface IPythonExecutionService {
8686
getInterpreterInformation(): Promise<InterpreterInformation | undefined>;
87-
getExecutablePath(): Promise<string | undefined>;
87+
getExecutablePath(options?: { throwOnError?: boolean }): Promise<string | undefined>;
8888
isModuleInstalled(moduleName: string): Promise<boolean>;
8989
getModuleVersion(moduleName: string): Promise<string | undefined>;
9090
getExecutionInfo(pythonArgs?: string[]): PythonExecInfo;
@@ -100,7 +100,7 @@ export interface IPythonExecutionService {
100100
export interface IPythonEnvironment {
101101
getInterpreterInformation(): Promise<InterpreterInformation | undefined>;
102102
getExecutionObservableInfo(pythonArgs?: string[], pythonExecutable?: string): PythonExecInfo;
103-
getExecutablePath(): Promise<string | undefined>;
103+
getExecutablePath(options?: { throwOnError?: boolean }): Promise<string | undefined>;
104104
isModuleInstalled(moduleName: string): Promise<boolean>;
105105
getModuleVersion(moduleName: string): Promise<string | undefined>;
106106
getExecutionInfo(pythonArgs?: string[], pythonExecutable?: string): PythonExecInfo;

src/client/common/utils/localize.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export namespace Diagnostics {
4949

5050
export namespace Common {
5151
export const allow = l10n.t('Allow');
52+
export const instructions = l10n.t('Instructions');
5253
export const close = l10n.t('Close');
5354
export const bannerLabelYes = l10n.t('Yes');
5455
export const bannerLabelNo = l10n.t('No');

src/client/pythonEnvironments/info/executable.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { copyPythonExecInfo, PythonExecInfo } from '../exec';
1717
export async function getExecutablePath(
1818
python: PythonExecInfo,
1919
shellExec: ShellExecFunc,
20-
timeout?: number,
20+
options?: { throwOnError?: boolean },
2121
): Promise<string | undefined> {
2222
try {
2323
const [args, parse] = getExecutable();
@@ -28,14 +28,17 @@ export async function getExecutablePath(
2828
(p, c) => (p ? `${p} ${c.toCommandArgumentForPythonExt()}` : `${c.toCommandArgumentForPythonExt()}`),
2929
'',
3030
);
31-
const result = await shellExec(quoted, { timeout: timeout ?? 15000 });
31+
const result = await shellExec(quoted, { timeout: 15000 });
3232
const executable = parse(result.stdout.trim());
3333
if (executable === '') {
3434
throw new Error(`${quoted} resulted in empty stdout`);
3535
}
3636
return executable;
3737
} catch (ex) {
3838
traceError(ex);
39+
if (options?.throwOnError) {
40+
throw ex;
41+
}
3942
return undefined;
4043
}
4144
}

0 commit comments

Comments
 (0)