Skip to content

Commit 81a2af6

Browse files
authored
Adds logging support for python debug adapter (#8272)
* Add debug session logging via tracker * Add log messages * Ensure we only log as needed. * Change log file name * Add news item * Adding unit test for debug session logger * Improve coverage. * Address review comments.
1 parent 19f64ea commit 81a2af6

File tree

7 files changed

+249
-14
lines changed

7 files changed

+249
-14
lines changed

news/1 Enhancements/8106.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add logging support for python debug adapter.

src/client/debugger/extension/adapter/activator.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,21 @@ import { IDebugService } from '../../../common/application/types';
99
import { DebugAdapterDescriptorFactory } from '../../../common/experimentGroups';
1010
import { IDisposableRegistry, IExperimentsManager } from '../../../common/types';
1111
import { DebuggerTypeName } from '../../constants';
12-
import { IDebugAdapterDescriptorFactory } from '../types';
12+
import { IDebugAdapterDescriptorFactory, IDebugSessionLoggingFactory } from '../types';
1313

1414
@injectable()
1515
export class DebugAdapterActivator implements IExtensionSingleActivationService {
1616
constructor(
1717
@inject(IDebugService) private readonly debugService: IDebugService,
18-
@inject(IDebugAdapterDescriptorFactory) private factory: IDebugAdapterDescriptorFactory,
18+
@inject(IDebugAdapterDescriptorFactory) private descriptorFactory: IDebugAdapterDescriptorFactory,
19+
@inject(IDebugSessionLoggingFactory) private debugSessionLoggingFactory: IDebugSessionLoggingFactory,
1920
@inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry,
2021
@inject(IExperimentsManager) private readonly experimentsManager: IExperimentsManager
21-
) {}
22+
) { }
2223
public async activate(): Promise<void> {
2324
if (this.experimentsManager.inExperiment(DebugAdapterDescriptorFactory.experiment)) {
24-
this.disposables.push(this.debugService.registerDebugAdapterDescriptorFactory(DebuggerTypeName, this.factory));
25+
this.disposables.push(this.debugService.registerDebugAdapterTrackerFactory(DebuggerTypeName, this.debugSessionLoggingFactory));
26+
this.disposables.push(this.debugService.registerDebugAdapterDescriptorFactory(DebuggerTypeName, this.descriptorFactory));
2527
} else {
2628
this.experimentsManager.sendTelemetryIfInExperiment(DebugAdapterDescriptorFactory.control);
2729
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
'use strict';
4+
5+
import * as fs from 'fs';
6+
import { inject, injectable } from 'inversify';
7+
import * as path from 'path';
8+
import { DebugAdapterTracker, DebugAdapterTrackerFactory, DebugConfiguration, DebugSession, ProviderResult } from 'vscode';
9+
import { DebugProtocol } from 'vscode-debugprotocol';
10+
11+
import { IFileSystem } from '../../../common/platform/types';
12+
import { StopWatch } from '../../../common/utils/stopWatch';
13+
import { EXTENSION_ROOT_DIR } from '../../../constants';
14+
15+
class DebugSessionLoggingTracker implements DebugAdapterTracker {
16+
private readonly enabled: boolean = false;
17+
private stream: fs.WriteStream | undefined;
18+
private timer = new StopWatch();
19+
20+
constructor(private readonly session: DebugSession, fileSystem: IFileSystem) {
21+
this.enabled = this.session.configuration.logToFile as boolean;
22+
if (this.enabled) {
23+
const fileName = `debugger.vscode_${this.session.id}.log`;
24+
this.stream = fileSystem.createWriteStream(path.join(EXTENSION_ROOT_DIR, fileName));
25+
}
26+
}
27+
28+
public onWillStartSession() {
29+
this.timer.reset();
30+
this.log(`Starting Session:\n${this.stringify(this.session.configuration)}\n`);
31+
}
32+
33+
public onWillReceiveMessage(message: DebugProtocol.Message) {
34+
this.log(`Client --> Adapter:\n${this.stringify(message)}\n`);
35+
}
36+
37+
public onDidSendMessage(message: DebugProtocol.Message) {
38+
this.log(`Client <-- Adapter:\n${this.stringify(message)}\n`);
39+
}
40+
41+
public onWillStopSession() {
42+
this.log('Stopping Session\n');
43+
}
44+
45+
public onError(error: Error) {
46+
this.log(`Error:\n${this.stringify(error)}\n`);
47+
}
48+
49+
public onExit(code: number | undefined, signal: string | undefined) {
50+
this.log(`Exit:\nExit-Code: ${code ? code : 0}\nSignal: ${signal ? signal : 'none'}\n`);
51+
}
52+
53+
private log(message: string) {
54+
if (this.enabled) {
55+
this.stream!.write(`${this.timer.elapsedTime} ${message}`);
56+
}
57+
}
58+
59+
private stringify(data: DebugProtocol.Message | Error | DebugConfiguration) {
60+
return JSON.stringify(data, null, 4);
61+
}
62+
}
63+
64+
@injectable()
65+
export class DebugSessionLoggingFactory implements DebugAdapterTrackerFactory {
66+
constructor(@inject(IFileSystem) private readonly fileSystem: IFileSystem) { }
67+
68+
public createDebugAdapterTracker(session: DebugSession): ProviderResult<DebugAdapterTracker> {
69+
return new DebugSessionLoggingTracker(session, this.fileSystem);
70+
}
71+
}

src/client/debugger/extension/serviceRegistry.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { IServiceManager } from '../../ioc/types';
88
import { AttachRequestArguments, LaunchRequestArguments } from '../types';
99
import { DebugAdapterActivator } from './adapter/activator';
1010
import { DebugAdapterDescriptorFactory } from './adapter/factory';
11+
import { DebugSessionLoggingFactory } from './adapter/logging';
1112
import { DebuggerBanner } from './banner';
1213
import { PythonDebugConfigurationService } from './configuration/debugConfigurationService';
1314
import { LaunchJsonCompletionProvider } from './configuration/launch.json/completionProvider';
@@ -26,7 +27,7 @@ import { IDebugConfigurationProviderFactory, IDebugConfigurationResolver } from
2627
import { ChildProcessAttachEventHandler } from './hooks/childProcessAttachHandler';
2728
import { ChildProcessAttachService } from './hooks/childProcessAttachService';
2829
import { IChildProcessAttachService, IDebugSessionEventHandlers } from './hooks/types';
29-
import { DebugConfigurationType, IDebugAdapterDescriptorFactory, IDebugConfigurationProvider, IDebugConfigurationService, IDebuggerBanner } from './types';
30+
import { DebugConfigurationType, IDebugAdapterDescriptorFactory, IDebugConfigurationProvider, IDebugConfigurationService, IDebuggerBanner, IDebugSessionLoggingFactory } from './types';
3031

3132
export function registerTypes(serviceManager: IServiceManager) {
3233
serviceManager.addSingleton<IExtensionSingleActivationService>(IExtensionSingleActivationService, LaunchJsonCompletionProvider);
@@ -47,4 +48,5 @@ export function registerTypes(serviceManager: IServiceManager) {
4748
serviceManager.addSingleton<IDebugEnvironmentVariablesService>(IDebugEnvironmentVariablesService, DebugEnvironmentVariablesHelper);
4849
serviceManager.addSingleton<IExtensionSingleActivationService>(IExtensionSingleActivationService, DebugAdapterActivator);
4950
serviceManager.addSingleton<IDebugAdapterDescriptorFactory>(IDebugAdapterDescriptorFactory, DebugAdapterDescriptorFactory);
51+
serviceManager.addSingleton<IDebugSessionLoggingFactory>(IDebugSessionLoggingFactory, DebugSessionLoggingFactory);
5052
}

src/client/debugger/extension/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
'use strict';
55

6-
import { CancellationToken, DebugAdapterDescriptorFactory, DebugConfigurationProvider, WorkspaceFolder } from 'vscode';
6+
import { CancellationToken, DebugAdapterDescriptorFactory, DebugAdapterTrackerFactory, DebugConfigurationProvider, WorkspaceFolder } from 'vscode';
77
import { InputStep, MultiStepInput } from '../../common/utils/multiStepInput';
88
import { RemoteDebugOptions } from '../debugAdapter/types';
99
import { DebugConfigurationArguments } from '../types';
@@ -44,3 +44,7 @@ export interface IDebugAdapterDescriptorFactory extends DebugAdapterDescriptorFa
4444
}
4545

4646
export type DebugAdapterPtvsdPathInfo = { extensionVersion: string; ptvsdPath: string };
47+
48+
export const IDebugSessionLoggingFactory = Symbol('IDebugSessionLoggingFactory');
49+
50+
export interface IDebugSessionLoggingFactory extends DebugAdapterTrackerFactory { }

src/test/debugger/extension/adapter/activator.unit.test.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,21 @@ import { FileSystem } from '../../../../client/common/platform/fileSystem';
2222
import { IDisposableRegistry, IPythonSettings } from '../../../../client/common/types';
2323
import { DebugAdapterActivator } from '../../../../client/debugger/extension/adapter/activator';
2424
import { DebugAdapterDescriptorFactory } from '../../../../client/debugger/extension/adapter/factory';
25-
import { IDebugAdapterDescriptorFactory } from '../../../../client/debugger/extension/types';
25+
import { DebugSessionLoggingFactory } from '../../../../client/debugger/extension/adapter/logging';
26+
import { IDebugAdapterDescriptorFactory, IDebugSessionLoggingFactory } from '../../../../client/debugger/extension/types';
2627
import { clearTelemetryReporter } from '../../../../client/telemetry';
2728
import { EventName } from '../../../../client/telemetry/constants';
2829
import { noop } from '../../../core';
2930
import { MockOutputChannel } from '../../../mockClasses';
3031

3132
// tslint:disable-next-line: max-func-body-length
32-
suite('Debugging - Adapter Factory Registration', () => {
33+
suite('Debugging - Adapter Factory and logger Registration', () => {
3334
const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST;
3435
const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST;
3536
let activator: IExtensionSingleActivationService;
3637
let debugService: IDebugService;
37-
let factory: IDebugAdapterDescriptorFactory;
38+
let descriptorFactory: IDebugAdapterDescriptorFactory;
39+
let loggingFactory: IDebugSessionLoggingFactory;
3840
let disposableRegistry: IDisposableRegistry;
3941
let experimentsManager: ExperimentsManager;
4042
let spiedInstance: ExperimentsManager;
@@ -80,9 +82,10 @@ suite('Debugging - Adapter Factory Registration', () => {
8082
spiedInstance = spy(experimentsManager);
8183

8284
debugService = mock(DebugService);
83-
factory = mock(DebugAdapterDescriptorFactory);
85+
descriptorFactory = mock(DebugAdapterDescriptorFactory);
86+
loggingFactory = mock(DebugSessionLoggingFactory);
8487
disposableRegistry = [];
85-
activator = new DebugAdapterActivator(instance(debugService), instance(factory), disposableRegistry, experimentsManager);
88+
activator = new DebugAdapterActivator(instance(debugService), instance(descriptorFactory), instance(loggingFactory), disposableRegistry, experimentsManager);
8689
});
8790

8891
teardown(() => {
@@ -100,17 +103,19 @@ suite('Debugging - Adapter Factory Registration', () => {
100103

101104
await activator.activate();
102105

103-
verify(debugService.registerDebugAdapterDescriptorFactory('python', instance(factory))).once();
106+
verify(debugService.registerDebugAdapterTrackerFactory('python', instance(loggingFactory))).once();
107+
verify(debugService.registerDebugAdapterDescriptorFactory('python', instance(descriptorFactory))).once();
104108
});
105109

106110
test('Register a disposable item if inside the DA experiment', async () => {
107111
when(spiedInstance.inExperiment(DebugAdapterExperiment.experiment)).thenReturn(true);
108112
const disposable = { dispose: noop };
113+
when(debugService.registerDebugAdapterTrackerFactory(anything(), anything())).thenReturn(disposable);
109114
when(debugService.registerDebugAdapterDescriptorFactory(anything(), anything())).thenReturn(disposable);
110115

111116
await activator.activate();
112117

113-
assert.deepEqual(disposableRegistry, [disposable]);
118+
assert.deepEqual(disposableRegistry, [disposable, disposable]);
114119
});
115120

116121
test('Send experiment group telemetry if inside the DA experiment', async () => {
@@ -127,7 +132,8 @@ suite('Debugging - Adapter Factory Registration', () => {
127132

128133
await activator.activate();
129134

130-
verify(debugService.registerDebugAdapterDescriptorFactory('python', instance(factory))).never();
135+
verify(debugService.registerDebugAdapterTrackerFactory('python', instance(loggingFactory))).never();
136+
verify(debugService.registerDebugAdapterDescriptorFactory('python', instance(descriptorFactory))).never();
131137
});
132138

133139
test('Don\'t register a disposable item if not inside the DA experiment', async () => {
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import * as assert from 'assert';
7+
import * as fs from 'fs';
8+
import * as path from 'path';
9+
import { anything, instance, mock, verify, when } from 'ts-mockito';
10+
import { DebugSession, WorkspaceFolder } from 'vscode';
11+
import { DebugProtocol } from 'vscode-debugprotocol';
12+
13+
import { FileSystem } from '../../../../client/common/platform/fileSystem';
14+
import { EXTENSION_ROOT_DIR } from '../../../../client/constants';
15+
import { DebugSessionLoggingFactory } from '../../../../client/debugger/extension/adapter/logging';
16+
17+
// tslint:disable-next-line: max-func-body-length
18+
suite('Debugging - Session Logging', () => {
19+
const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST;
20+
const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST;
21+
let loggerFactory: DebugSessionLoggingFactory;
22+
let fsService: FileSystem;
23+
let writeStream: fs.WriteStream;
24+
25+
setup(() => {
26+
fsService = mock(FileSystem);
27+
writeStream = mock(fs.WriteStream);
28+
29+
process.env.VSC_PYTHON_UNIT_TEST = undefined;
30+
process.env.VSC_PYTHON_CI_TEST = undefined;
31+
32+
loggerFactory = new DebugSessionLoggingFactory(instance(fsService));
33+
});
34+
35+
teardown(() => {
36+
process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST;
37+
process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST;
38+
});
39+
40+
function createSession(id: string, workspaceFolder?: WorkspaceFolder): DebugSession {
41+
return {
42+
configuration: {
43+
name: '',
44+
request: 'launch',
45+
type: 'python'
46+
},
47+
id: id,
48+
name: 'python',
49+
type: 'python',
50+
workspaceFolder,
51+
customRequest: () => Promise.resolve()
52+
};
53+
}
54+
55+
function createSessionWithLogging(id: string, logToFile: boolean, workspaceFolder?: WorkspaceFolder): DebugSession {
56+
const session = createSession(id, workspaceFolder);
57+
session.configuration.logToFile = logToFile;
58+
return session;
59+
}
60+
61+
class TestMessage implements DebugProtocol.ProtocolMessage {
62+
public seq: number;
63+
public type: string;
64+
public id: number;
65+
public format: string;
66+
public variables?: { [key: string]: string };
67+
public sendTelemetry?: boolean;
68+
public showUser?: boolean;
69+
public url?: string;
70+
public urlLabel?: string;
71+
constructor(id: number, seq: number, type: string) {
72+
this.id = id;
73+
this.format = 'json';
74+
this.seq = seq;
75+
this.type = type;
76+
}
77+
}
78+
79+
test('Create logger using session without logToFile', async () => {
80+
const session = createSession('test1');
81+
const filePath = path.join(EXTENSION_ROOT_DIR, `debugger.vscode_${session.id}.log`);
82+
83+
await loggerFactory.createDebugAdapterTracker(session);
84+
85+
verify(fsService.createWriteStream(filePath)).never();
86+
});
87+
88+
test('Create logger using session with logToFile set to false', async () => {
89+
const session = createSessionWithLogging('test2', false);
90+
const filePath = path.join(EXTENSION_ROOT_DIR, `debugger.vscode_${session.id}.log`);
91+
92+
when(fsService.createWriteStream(filePath)).thenReturn(instance(writeStream));
93+
when(writeStream.write(anything())).thenReturn(true);
94+
const logger = await loggerFactory.createDebugAdapterTracker(session);
95+
if (logger) {
96+
logger.onWillStartSession!();
97+
}
98+
99+
verify(fsService.createWriteStream(filePath)).never();
100+
verify(writeStream.write(anything())).never();
101+
});
102+
103+
test('Create logger using session with logToFile set to true', async () => {
104+
const session = createSessionWithLogging('test3', true);
105+
const filePath = path.join(EXTENSION_ROOT_DIR, `debugger.vscode_${session.id}.log`);
106+
const logs: string[] = [];
107+
108+
when(fsService.createWriteStream(filePath)).thenReturn(instance(writeStream));
109+
when(writeStream.write(anything())).thenCall((msg) => logs.push(msg));
110+
111+
const message = new TestMessage(1, 1, 'test-message');
112+
const logger = await loggerFactory.createDebugAdapterTracker(session);
113+
114+
if (logger) {
115+
logger.onWillStartSession!();
116+
assert.ok(logs.pop()!.includes('Starting Session'));
117+
118+
logger.onDidSendMessage!(message);
119+
const sentLog = logs.pop();
120+
assert.ok(sentLog!.includes('Client <-- Adapter'));
121+
assert.ok(sentLog!.includes('test-message'));
122+
123+
logger.onWillReceiveMessage!(message);
124+
const receivedLog = logs.pop();
125+
assert.ok(receivedLog!.includes('Client --> Adapter'));
126+
assert.ok(receivedLog!.includes('test-message'));
127+
128+
logger.onWillStopSession!();
129+
assert.ok(logs.pop()!.includes('Stopping Session'));
130+
131+
logger.onError!(new Error('test error message'));
132+
assert.ok(logs.pop()!.includes('Error'));
133+
134+
logger.onExit!(111, '222');
135+
const exitLog1 = logs.pop();
136+
assert.ok(exitLog1!.includes('Exit-Code: 111'));
137+
assert.ok(exitLog1!.includes('Signal: 222'));
138+
139+
logger.onExit!(undefined, undefined);
140+
const exitLog2 = logs.pop();
141+
assert.ok(exitLog2!.includes('Exit-Code: 0'));
142+
assert.ok(exitLog2!.includes('Signal: none'));
143+
}
144+
145+
verify(fsService.createWriteStream(filePath)).once();
146+
verify(writeStream.write(anything())).times(7);
147+
assert.deepEqual(logs, []);
148+
});
149+
});

0 commit comments

Comments
 (0)