Skip to content

Commit abd98be

Browse files
authored
Check if experimental debugger is used and prompt to repair (#2482)
* Check if experimental debugger is used and prompt to repair * Touch up Markdown in news entry * Fix code review comments * Fix formatting * Fix tests
1 parent 123c69c commit abd98be

File tree

11 files changed

+179
-14
lines changed

11 files changed

+179
-14
lines changed

news/2 Fixes/2478.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Upgrade `pythonExperimental` to `python` in `launch.json`.

src/client/application/diagnostics/applicationDiagnostics.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,20 @@ import { STANDARD_OUTPUT_CHANNEL } from '../../common/constants';
99
import { ILogger, IOutputChannel } from '../../common/types';
1010
import { IServiceContainer } from '../../ioc/types';
1111
import { IApplicationDiagnostics } from '../types';
12-
import { EnvironmentPathVariableDiagnosticsServiceId } from './checks/envPathVariable';
1312
import { IDiagnostic, IDiagnosticsService } from './types';
1413

1514
@injectable()
1615
export class ApplicationDiagnostics implements IApplicationDiagnostics {
1716
constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { }
1817
public async performPreStartupHealthCheck(): Promise<void> {
19-
const envHealthCheck = this.serviceContainer.get<IDiagnosticsService>(IDiagnosticsService, EnvironmentPathVariableDiagnosticsServiceId);
20-
const diagnostics = await envHealthCheck.diagnose();
21-
this.log(diagnostics);
22-
if (diagnostics.length > 0) {
23-
await envHealthCheck.handle(diagnostics);
24-
}
18+
const diagnosticsServices = this.serviceContainer.getAll<IDiagnosticsService>(IDiagnosticsService);
19+
await Promise.all(diagnosticsServices.map(async diagnosticsService => {
20+
const diagnostics = await diagnosticsService.diagnose();
21+
this.log(diagnostics);
22+
if (diagnostics.length > 0) {
23+
await diagnosticsService.handle(diagnostics);
24+
}
25+
}));
2526
}
2627
private log(diagnostics: IDiagnostic[]): void {
2728
const logger = this.serviceContainer.get<ILogger>(ILogger);
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { inject, injectable } from 'inversify';
7+
import * as path from 'path';
8+
import { DiagnosticSeverity, WorkspaceFolder } from 'vscode';
9+
import { ICommandManager, IWorkspaceService } from '../../../common/application/types';
10+
import '../../../common/extensions';
11+
import { IFileSystem } from '../../../common/platform/types';
12+
import { IServiceContainer } from '../../../ioc/types';
13+
import { BaseDiagnostic, BaseDiagnosticsService } from '../base';
14+
import { IDiagnosticsCommandFactory } from '../commands/types';
15+
import { DiagnosticCodes } from '../constants';
16+
import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler';
17+
import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types';
18+
19+
const InvalidDebuggerTypeMessage = 'Your launch.json file needs to be updated to change the "pythonExperimental" debug ' +
20+
'configurations to use the "python" debugger type, otherwise Python debugging may ' +
21+
'not work. Would you like to automatically update your launch.json file now?';
22+
23+
export class InvalidDebuggerTypeDiagnostic extends BaseDiagnostic {
24+
constructor(message) {
25+
super(DiagnosticCodes.InvalidDebuggerTypeDiagnostic,
26+
message, DiagnosticSeverity.Error, DiagnosticScope.WorkspaceFolder);
27+
}
28+
}
29+
30+
export const InvalidDebuggerTypeDiagnosticsServiceId = 'InvalidDebuggerTypeDiagnosticsServiceId';
31+
32+
const CommandName = 'python.debugger.replaceExperimental';
33+
34+
@injectable()
35+
export class InvalidDebuggerTypeDiagnosticsService extends BaseDiagnosticsService {
36+
protected readonly messageService: IDiagnosticHandlerService<MessageCommandPrompt>;
37+
protected readonly fs: IFileSystem;
38+
constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) {
39+
super([DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic], serviceContainer);
40+
this.messageService = serviceContainer.get<IDiagnosticHandlerService<MessageCommandPrompt>>(IDiagnosticHandlerService, DiagnosticCommandPromptHandlerServiceId);
41+
const cmdManager = serviceContainer.get<ICommandManager>(ICommandManager);
42+
this.fs = this.serviceContainer.get<IFileSystem>(IFileSystem);
43+
cmdManager.registerCommand(CommandName, this.fixLaunchJson, this);
44+
}
45+
public async diagnose(): Promise<IDiagnostic[]> {
46+
if (await this.isExperimentalDebuggerUsed()) {
47+
return [new InvalidDebuggerTypeDiagnostic(InvalidDebuggerTypeMessage)];
48+
} else {
49+
return [];
50+
}
51+
}
52+
public async handle(diagnostics: IDiagnostic[]): Promise<void> {
53+
// This class can only handle one type of diagnostic, hence just use first item in list.
54+
if (diagnostics.length === 0 || !this.canHandle(diagnostics[0])) {
55+
return;
56+
}
57+
const diagnostic = diagnostics[0];
58+
const commandFactory = this.serviceContainer.get<IDiagnosticsCommandFactory>(IDiagnosticsCommandFactory);
59+
const options = [
60+
{
61+
prompt: 'Yes, update launch.json',
62+
command: commandFactory.createCommand(diagnostic, { type: 'executeVSCCommand', options: 'python.debugger.replaceExperimental' })
63+
},
64+
{
65+
prompt: 'No, I will do it later'
66+
}
67+
];
68+
69+
await this.messageService.handle(diagnostic, { commandPrompts: options });
70+
}
71+
private async isExperimentalDebuggerUsed() {
72+
const workspaceService = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService);
73+
if (!workspaceService.hasWorkspaceFolders) {
74+
return false;
75+
}
76+
77+
const results = await Promise.all(workspaceService.workspaceFolders!.map(workspaceFolder => this.isExperimentalDebuggerUsedInWorkspace(workspaceFolder)));
78+
return results.filter(used => used === true).length > 0;
79+
}
80+
private getLaunchJsonFile(workspaceFolder: WorkspaceFolder) {
81+
return path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json');
82+
}
83+
private async isExperimentalDebuggerUsedInWorkspace(workspaceFolder: WorkspaceFolder) {
84+
const launchJson = this.getLaunchJsonFile(workspaceFolder);
85+
if (!await this.fs.fileExists(launchJson)) {
86+
return false;
87+
}
88+
89+
const fileContents = await this.fs.readFile(launchJson);
90+
return fileContents.indexOf('"pythonExperimental"') > 0;
91+
}
92+
private async fixLaunchJson() {
93+
const workspaceService = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService);
94+
if (!workspaceService.hasWorkspaceFolders) {
95+
return false;
96+
}
97+
98+
await Promise.all(workspaceService.workspaceFolders!.map(workspaceFolder => this.fixLaunchJsonInWorkspace(workspaceFolder)));
99+
}
100+
private async fixLaunchJsonInWorkspace(workspaceFolder: WorkspaceFolder) {
101+
if (!await this.isExperimentalDebuggerUsedInWorkspace(workspaceFolder)) {
102+
return;
103+
}
104+
105+
const launchJson = this.getLaunchJsonFile(workspaceFolder);
106+
let fileContents = await this.fs.readFile(launchJson);
107+
const debuggerType = new RegExp('"pythonExperimental"', 'g');
108+
const debuggerLabel = new RegExp('"Python Experimental:', 'g');
109+
110+
fileContents = fileContents.replace(debuggerType, '"python"');
111+
fileContents = fileContents.replace(debuggerLabel, '"Python:');
112+
113+
await this.fs.writeFile(launchJson, fileContents);
114+
}
115+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { ICommandManager } from '../../../common/application/types';
7+
import { IServiceContainer } from '../../../ioc/types';
8+
import { IDiagnostic } from '../types';
9+
import { BaseDiagnosticCommand } from './base';
10+
11+
export class ExecuteVSCCommand extends BaseDiagnosticCommand {
12+
constructor(diagnostic: IDiagnostic, private serviceContainer: IServiceContainer, private commandName: string) {
13+
super(diagnostic);
14+
}
15+
public async invoke(): Promise<void> {
16+
const cmdManager = this.serviceContainer.get<ICommandManager>(ICommandManager);
17+
return cmdManager.executeCommand(this.commandName).then(() => undefined);
18+
}
19+
}

src/client/application/diagnostics/commands/factory.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { inject, injectable } from 'inversify';
77
import { IServiceContainer } from '../../../ioc/types';
88
import { IDiagnostic, IDiagnosticCommand } from '../types';
9+
import { ExecuteVSCCommand } from './execVSCCommand';
910
import { IgnoreDiagnosticCommand } from './ignore';
1011
import { LaunchBrowserCommand } from './launchBrowser';
1112
import { CommandOptions, IDiagnosticsCommandFactory } from './types';
@@ -22,6 +23,9 @@ export class DiagnosticsCommandFactory implements IDiagnosticsCommandFactory {
2223
case 'launch': {
2324
return new LaunchBrowserCommand(diagnostic, this.serviceContainer, options.options);
2425
}
26+
case 'executeVSCCommand': {
27+
return new ExecuteVSCCommand(diagnostic, this.serviceContainer, options.options);
28+
}
2529
default: {
2630
throw new Error(`Unknown Diagnostic command commandType '${commandType}'`);
2731
}

src/client/application/diagnostics/commands/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import { DiagnosticScope, IDiagnostic, IDiagnosticCommand } from '../types';
88
export type CommandOption<Type, Option> = { type: Type; options: Option };
99
export type LaunchBrowserOption = CommandOption<'launch', string>;
1010
export type IgnoreDiagnostOption = CommandOption<'ignore', DiagnosticScope>;
11-
export type CommandOptions = LaunchBrowserOption | IgnoreDiagnostOption;
11+
export type ExecuteVSCCommandOption = CommandOption<'executeVSCCommand', string>;
12+
export type CommandOptions = LaunchBrowserOption | IgnoreDiagnostOption | ExecuteVSCCommandOption;
1213

1314
export const IDiagnosticsCommandFactory = Symbol('IDiagnosticsCommandFactory');
1415

src/client/application/diagnostics/constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
'use strict';
55

66
export enum DiagnosticCodes {
7-
InvalidEnvironmentPathVariableDiagnostic = 'InvalidEnvironmentPathVariableDiagnostic'
7+
InvalidEnvironmentPathVariableDiagnostic = 'InvalidEnvironmentPathVariableDiagnostic',
8+
InvalidDebuggerTypeDiagnostic = 'InvalidDebuggerTypeDiagnostic'
89
}

src/client/application/diagnostics/serviceRegistry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import { IServiceManager } from '../../ioc/types';
77
import { EnvironmentPathVariableDiagnosticsService, EnvironmentPathVariableDiagnosticsServiceId } from './checks/envPathVariable';
8+
import { InvalidDebuggerTypeDiagnosticsService, InvalidDebuggerTypeDiagnosticsServiceId } from './checks/invalidDebuggerType';
89
import { DiagnosticsCommandFactory } from './commands/factory';
910
import { IDiagnosticsCommandFactory } from './commands/types';
1011
import { DiagnosticFilterService } from './filter';
@@ -15,5 +16,6 @@ export function registerTypes(serviceManager: IServiceManager) {
1516
serviceManager.addSingleton<IDiagnosticFilterService>(IDiagnosticFilterService, DiagnosticFilterService);
1617
serviceManager.addSingleton<IDiagnosticHandlerService<MessageCommandPrompt>>(IDiagnosticHandlerService, DiagnosticCommandPromptHandlerService, DiagnosticCommandPromptHandlerServiceId);
1718
serviceManager.addSingleton<IDiagnosticsService>(IDiagnosticsService, EnvironmentPathVariableDiagnosticsService, EnvironmentPathVariableDiagnosticsServiceId);
19+
serviceManager.addSingleton<IDiagnosticsService>(IDiagnosticsService, InvalidDebuggerTypeDiagnosticsService, InvalidDebuggerTypeDiagnosticsServiceId);
1820
serviceManager.addSingleton<IDiagnosticsCommandFactory>(IDiagnosticsCommandFactory, DiagnosticsCommandFactory);
1921
}

src/client/common/platform/fileSystem.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,4 +168,8 @@ export class FileSystem implements IFileSystem {
168168
});
169169
});
170170
}
171+
172+
public writeFile(filePath: string, data: {}): Promise<void> {
173+
return fs.writeFile(filePath, data);
174+
}
171175
}

src/client/common/platform/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export interface IFileSystem {
7878
getSubDirectories(rootDir: string): Promise<string[]>;
7979
arePathsSame(path1: string, path2: string): boolean;
8080
readFile(filePath: string): Promise<string>;
81+
writeFile(filePath: string, data: {}): Promise<void>;
8182
appendFileSync(filename: string, data: {}, encoding: string): void;
8283
appendFileSync(filename: string, data: {}, options?: { encoding?: string; mode?: number; flag?: string }): void;
8384
// tslint:disable-next-line:unified-signatures

0 commit comments

Comments
 (0)