Skip to content

Commit 6427789

Browse files
committed
Fix #10437: Update launch.json handling to support "listen" and "connect"
1 parent 8e2603a commit 6427789

File tree

4 files changed

+144
-53
lines changed

4 files changed

+144
-53
lines changed

news/1 Enhancements/10437.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Support reverse connection ("listen" in launch.json) from debug adapter to VSCode.

package.json

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,15 +1363,46 @@
13631363
},
13641364
"attach": {
13651365
"properties": {
1366+
"connect": {
1367+
"type": "object",
1368+
"label": "Attach by connecting to debugpy over a socket.",
1369+
"properties": {
1370+
"port": {
1371+
"type": "number",
1372+
"description": "Port to connect to."
1373+
},
1374+
"host": {
1375+
"type": "string",
1376+
"description": "Hostname or IP address to connect to.",
1377+
"default": "127.0.0.1"
1378+
}
1379+
},
1380+
"required": ["port"]
1381+
},
1382+
"listen": {
1383+
"type": "object",
1384+
"label": "Attach by listening for incoming socket connection from debugpy",
1385+
"properties": {
1386+
"port": {
1387+
"type": "number",
1388+
"description": "Port to listen on."
1389+
},
1390+
"host": {
1391+
"type": "string",
1392+
"description": "Hostname or IP address of the interface to listen on.",
1393+
"default": "127.0.0.1"
1394+
}
1395+
},
1396+
"required": ["port"]
1397+
},
13661398
"port": {
13671399
"type": "number",
1368-
"description": "Debug port to attach",
1369-
"default": 0
1400+
"description": "Port to connect to."
13701401
},
13711402
"host": {
13721403
"type": "string",
1373-
"description": "IP Address of the of remote server (default is localhost or use 127.0.0.1).",
1374-
"default": "localhost"
1404+
"description": "Hostname or IP address to connect to.",
1405+
"default": "127.0.0.1"
13751406
},
13761407
"pathMappings": {
13771408
"type": "array",

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

Lines changed: 52 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -38,48 +38,62 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac
3838
const configuration = session.configuration as LaunchRequestArguments | AttachRequestArguments;
3939

4040
if (this.experimentsManager.inExperiment(DebugAdapterNewPtvsd.experiment)) {
41-
const isAttach = configuration.request === 'attach';
42-
const port = configuration.port ?? 0;
43-
// When processId is provided we may have to inject the debugger into the process.
44-
// This is done by the debug adapter, so we need to start it. The adapter will handle injecting the debugger when it receives the attach request.
45-
const processId = configuration.processId ?? 0;
46-
47-
if (isAttach && processId === 0) {
48-
if (port === 0) {
49-
throw new Error('Port or processId must be specified for request type attach');
50-
} else {
51-
return new DebugAdapterServer(port, configuration.host);
41+
// There are five distinct scenarios here:
42+
//
43+
// 1. "launch" without "debugServer";
44+
// 2. "attach" with "processId";
45+
// 3. "attach" with "listen";
46+
// 4. "attach" with "connect" (or legacy "host"/"port");
47+
// 5. "launch" or "attach" with "debugServer".
48+
//
49+
// For the first three, we want to spawn the debug adapter directly.
50+
// For the last two, the adapter is already listening on the specified socket.
51+
52+
if (configuration.debugServer !== undefined) {
53+
return new DebugAdapterServer(configuration.debugServer);
54+
} else if (configuration.request === 'attach') {
55+
if (configuration.connect !== undefined) {
56+
return new DebugAdapterServer(
57+
configuration.connect.port,
58+
configuration.connect.host ?? '127.0.0.1'
59+
);
60+
} else if (configuration.port !== undefined) {
61+
return new DebugAdapterServer(configuration.port, configuration.host ?? '127.0.0.1');
62+
} else if (configuration.listen === undefined && configuration.processId === undefined) {
63+
throw new Error('"request":"attach" requires either "connect", "listen", or "processId"');
5264
}
53-
} else {
54-
const pythonPath = await this.getPythonPath(configuration, session.workspaceFolder);
55-
// If logToFile is set in the debug config then pass --log-dir <path-to-extension-dir> when launching the debug adapter.
65+
}
66+
67+
const pythonPath = await this.getPythonPath(configuration, session.workspaceFolder);
68+
if (pythonPath.length !== 0) {
69+
if (configuration.request === 'attach' && configuration.processId !== undefined) {
70+
sendTelemetryEvent(EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS);
71+
}
72+
73+
// "logToFile" is not handled directly by the adapter - instead, we need to pass
74+
// the corresponding CLI switch when spawning it.
5675
const logArgs = configuration.logToFile ? ['--log-dir', EXTENSION_ROOT_DIR] : [];
76+
77+
if (configuration.debugAdapterPath !== undefined) {
78+
return new DebugAdapterExecutable(pythonPath, [configuration.debugAdapterPath, ...logArgs]);
79+
}
80+
5781
const debuggerPathToUse = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python', 'debugpy');
5882

59-
if (pythonPath.length !== 0) {
60-
if (processId) {
61-
sendTelemetryEvent(EventName.DEBUGGER_ATTACH_TO_LOCAL_PROCESS);
62-
}
63-
64-
if (configuration.debugAdapterPath) {
65-
return new DebugAdapterExecutable(pythonPath, [configuration.debugAdapterPath, ...logArgs]);
66-
}
67-
68-
if (await this.useNewDebugger(pythonPath)) {
69-
sendTelemetryEvent(EventName.DEBUG_ADAPTER_USING_WHEELS_PATH, undefined, { usingWheels: true });
70-
return new DebugAdapterExecutable(pythonPath, [
71-
path.join(debuggerPathToUse, 'wheels', 'debugpy', 'adapter'),
72-
...logArgs
73-
]);
74-
} else {
75-
sendTelemetryEvent(EventName.DEBUG_ADAPTER_USING_WHEELS_PATH, undefined, {
76-
usingWheels: false
77-
});
78-
return new DebugAdapterExecutable(pythonPath, [
79-
path.join(debuggerPathToUse, 'no_wheels', 'debugpy', 'adapter'),
80-
...logArgs
81-
]);
82-
}
83+
if (await this.useNewDebugger(pythonPath)) {
84+
sendTelemetryEvent(EventName.DEBUG_ADAPTER_USING_WHEELS_PATH, undefined, { usingWheels: true });
85+
return new DebugAdapterExecutable(pythonPath, [
86+
path.join(debuggerPathToUse, 'wheels', 'debugpy', 'adapter'),
87+
...logArgs
88+
]);
89+
} else {
90+
sendTelemetryEvent(EventName.DEBUG_ADAPTER_USING_WHEELS_PATH, undefined, {
91+
usingWheels: false
92+
});
93+
return new DebugAdapterExecutable(pythonPath, [
94+
path.join(debuggerPathToUse, 'no_wheels', 'debugpy', 'adapter'),
95+
...logArgs
96+
]);
8397
}
8498
}
8599
} else {

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

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -206,37 +206,82 @@ suite('Debugging - Adapter Factory', () => {
206206
assert.deepEqual(descriptor, nodeExecutable);
207207
});
208208

209-
test('Return Debug Adapter server if in DA experiment, configuration is attach and port is specified', async () => {
209+
test('Return Debug Adapter server if in DA experiment, request is "attach", and port is specified directly', async () => {
210210
const session = createSession({ request: 'attach', port: 5678, host: 'localhost' });
211211
const debugServer = new DebugAdapterServer(session.configuration.port, session.configuration.host);
212212

213213
when(spiedInstance.inExperiment(DebugAdapterNewPtvsd.experiment)).thenReturn(true);
214214
const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable);
215215

216-
// Interpreter not needed for attach
216+
// Interpreter not needed for host/port
217217
verify(interpreterService.getInterpreters(anything())).never();
218218
assert.deepEqual(descriptor, debugServer);
219219
});
220220

221-
test('Throw error if in DA experiment, configuration is attach, port is 0 and process ID is not specified', async () => {
222-
const session = createSession({ request: 'attach', port: 0, host: 'localhost' });
221+
test('Return Debug Adapter server if in DA experiment, request is "attach", and connect is specified', async () => {
222+
const session = createSession({ request: 'attach', connect: { port: 5678, host: 'localhost' } });
223+
const debugServer = new DebugAdapterServer(
224+
session.configuration.connect.port,
225+
session.configuration.connect.host
226+
);
223227

224228
when(spiedInstance.inExperiment(DebugAdapterNewPtvsd.experiment)).thenReturn(true);
225-
const promise = factory.createDebugAdapterDescriptor(session, nodeExecutable);
229+
const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable);
226230

227-
await expect(promise).to.eventually.be.rejectedWith(
228-
'Port or processId must be specified for request type attach'
229-
);
231+
// Interpreter not needed for connect
232+
verify(interpreterService.getInterpreters(anything())).never();
233+
assert.deepEqual(descriptor, debugServer);
234+
});
235+
236+
test('Return Debug Adapter executable if in DA experiment, request is "attach", and listen is specified', async () => {
237+
const session = createSession({ request: 'attach', listen: { port: 5678, host: 'localhost' } });
238+
const debugExecutable = new DebugAdapterExecutable(pythonPath, [ptvsdAdapterPathWithWheels]);
239+
240+
when(spiedInstance.inExperiment(DebugAdapterNewPtvsd.experiment)).thenReturn(true);
241+
when(interpreterService.getActiveInterpreter(anything())).thenResolve(interpreter);
242+
243+
const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable);
244+
assert.deepEqual(descriptor, debugExecutable);
245+
});
246+
247+
test('Return Debug Adapter server if in DA experiment, request is "attach", and debugServer is specified', async () => {
248+
const session = createSession({ request: 'attach', debugServer: 5678 });
249+
const debugServer = new DebugAdapterServer(session.configuration.debugServer);
250+
251+
when(spiedInstance.inExperiment(DebugAdapterNewPtvsd.experiment)).thenReturn(true);
252+
const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable);
253+
254+
// Interpreter not needed for debugServer
255+
verify(interpreterService.getInterpreters(anything())).never();
256+
assert.deepEqual(descriptor, debugServer);
257+
});
258+
259+
test('Return Debug Adapter server if in DA experiment, request is "launch", and debugServer is specified', async () => {
260+
const session = createSession({ request: 'launch', debugServer: 5678 });
261+
const debugServer = new DebugAdapterServer(session.configuration.debugServer);
262+
263+
when(spiedInstance.inExperiment(DebugAdapterNewPtvsd.experiment)).thenReturn(true);
264+
const descriptor = await factory.createDebugAdapterDescriptor(session, nodeExecutable);
265+
266+
// Interpreter not needed for debugServer
267+
verify(interpreterService.getInterpreters(anything())).never();
268+
assert.deepEqual(descriptor, debugServer);
230269
});
231270

232-
test('Throw error if in DA experiment, configuration is attach and port and process ID are not specified', async () => {
233-
const session = createSession({ request: 'attach', port: undefined, processId: undefined });
271+
test('Throw error if in DA experiment, request is "attach", and neither port, processId, listen, nor connect is specified', async () => {
272+
const session = createSession({
273+
request: 'attach',
274+
port: undefined,
275+
processId: undefined,
276+
listen: undefined,
277+
connect: undefined
278+
});
234279

235280
when(spiedInstance.inExperiment(DebugAdapterNewPtvsd.experiment)).thenReturn(true);
236281
const promise = factory.createDebugAdapterDescriptor(session, nodeExecutable);
237282

238283
await expect(promise).to.eventually.be.rejectedWith(
239-
'Port or processId must be specified for request type attach'
284+
'"request":"attach" requires either "connect", "listen", or "processId"'
240285
);
241286
});
242287

0 commit comments

Comments
 (0)