feat(debug): enable Python debug workflow and debug server integration

Adds an end-to-end Python debug workflow for the extension.
Includes a new prepare-debug.ps1 script and a VS Code task.
Extends the Python server and extension to coordinate a debug session and safe startup.

- Add prepare-debug.ps1 and a VS Code task to build the debug bundle
- Enable Python debug wiring in the server and tests
- Ensure a single stable Python debug session during startup
This commit is contained in:
Christoph Brandau
2026-08-17 11:02:22 +02:00
parent 35a4357551
commit 33cf282b0a
14 changed files with 349 additions and 73 deletions
+22
View File
@@ -14,6 +14,7 @@
"vscode-languageclient": "^9.0.1"
},
"devDependencies": {
"@types/fs-extra": "^11.0.4",
"@types/node": "^22.10.5",
"@types/vscode": "^1.96.0"
},
@@ -21,6 +22,27 @@
"vscode": "^1.96.0"
}
},
"node_modules/@types/fs-extra": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz",
"integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/jsonfile": "*",
"@types/node": "*"
}
},
"node_modules/@types/jsonfile": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz",
"integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "22.10.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
+1
View File
@@ -12,6 +12,7 @@
"vscode-languageclient": "^9.0.1"
},
"devDependencies": {
"@types/fs-extra": "^11.0.4",
"@types/node": "^22.10.5",
"@types/vscode": "^1.96.0"
}
+51 -30
View File
@@ -39,13 +39,26 @@ async function createServer(
initializationOptions: IInitOptions
): Promise<LanguageClient> {
const command = settings.interpreter[0]
if (!command) {
throw new Error("No Python interpreter is configured for the language server.")
}
const cwd = settings.cwd
// Set debugger path needed for debugging python code.
const newEnv = { ...process.env }
const debuggerPath = await getDebuggerPath()
const isDebugScript = await fsapi.pathExists(DEBUG_SERVER_SCRIPT_PATH)
if (newEnv.USE_DEBUGPY && debuggerPath) {
const debugRequested = newEnv.USE_DEBUGPY?.toLowerCase() === "true"
if (debugRequested && !isDebugScript) {
throw new Error(`Python debug bootstrap not found: ${DEBUG_SERVER_SCRIPT_PATH}`)
}
const debuggerPath = debugRequested ? await getDebuggerPath() : undefined
if (debugRequested && !debuggerPath) {
throw new Error(
"Python debugging was requested, but the Python Debugger extension did not provide debugpy."
)
}
if (debugRequested && debuggerPath) {
newEnv.DEBUGPY_PATH = debuggerPath
} else {
newEnv.USE_DEBUGPY = "False"
@@ -57,10 +70,13 @@ async function createServer(
// Set notification type
newEnv.LS_SHOW_NOTIFICATION = settings.showNotifications
const args =
newEnv.USE_DEBUGPY === "False" || !isDebugScript
? settings.interpreter.slice(1).concat([SERVER_SCRIPT_PATH])
: settings.interpreter.slice(1).concat([DEBUG_SERVER_SCRIPT_PATH])
const serverScript = debugRequested ? DEBUG_SERVER_SCRIPT_PATH : SERVER_SCRIPT_PATH
const interpreterArgs = settings.interpreter.slice(1)
if (debugRequested && !interpreterArgs.includes("-Xfrozen_modules=off")) {
interpreterArgs.push("-Xfrozen_modules=off")
}
const args = interpreterArgs.concat([serverScript])
traceInfo(`Python debug mode: ${debugRequested ? "enabled" : "disabled"}`)
traceInfo(`Server run command: ${[command, ...args].join(" ")}`)
const serverOptions: ServerOptions = {
@@ -108,35 +124,40 @@ export async function restartServer(
const projectRoot = await getProjectRoot()
const workspaceSetting = await getWorkspaceSettings(serverId, projectRoot, true)
const newLSClient = await createServer(workspaceSetting, serverId, serverName, outputChannel, {
settings: await getExtensionSettings(serverId, true),
globalSettings: await getGlobalSettings(serverId, false)
})
traceInfo(`Server: Start requested.`)
_disposables.push(
newLSClient.onDidChangeState((e) => {
switch (e.newState) {
case State.Stopped:
traceVerbose(`Server State: Stopped`)
break
case State.Starting:
traceVerbose(`Server State: Starting`)
break
case State.Running:
traceVerbose(`Server State: Running`)
break
}
})
)
try {
const newLSClient = await createServer(
workspaceSetting,
serverId,
serverName,
outputChannel,
{
settings: await getExtensionSettings(serverId, true),
globalSettings: await getGlobalSettings(serverId, false)
}
)
traceInfo(`Server: Start requested.`)
_disposables.push(
newLSClient.onDidChangeState((e) => {
switch (e.newState) {
case State.Stopped:
traceVerbose(`Server State: Stopped`)
break
case State.Starting:
traceVerbose(`Server State: Starting`)
break
case State.Running:
traceVerbose(`Server State: Running`)
break
}
})
)
await newLSClient.start()
const level = getLSClientTraceLevel(outputChannel.logLevel, env.logLevel)
await newLSClient.setTrace(level)
return newLSClient
} catch (ex) {
traceError(`Server: Start failed: ${ex}`)
disposeServerResources()
return undefined
}
const level = getLSClientTraceLevel(outputChannel.logLevel, env.logLevel)
await newLSClient.setTrace(level)
return newLSClient
}
+18 -3
View File
@@ -38,6 +38,7 @@ export async function activate(context: vscode.ExtensionContext) {
const serverInfo = loadServerDefaults()
const serverName = serverInfo.name
const serverId = serverInfo.module
const pythonDebugMode = process.env.USE_DEBUGPY?.toLowerCase() === "true"
// Setup logging
const outputChannel = createOutputChannel(serverName)
@@ -101,10 +102,15 @@ export async function activate(context: vscode.ExtensionContext) {
return runServerQueue
}
if (!pythonDebugMode) {
context.subscriptions.push(
onDidChangePythonInterpreter(async () => {
await runServer()
})
)
}
context.subscriptions.push(
onDidChangePythonInterpreter(async () => {
await runServer()
}),
onDidChangeConfiguration(async (e: vscode.ConfigurationChangeEvent) => {
if (checkIfConfigurationChanged(e, serverId)) {
await runServer()
@@ -116,6 +122,15 @@ export async function activate(context: vscode.ExtensionContext) {
)
setImmediate(async () => {
if (pythonDebugMode) {
// A debugpy listen session is attached to exactly one process. Do not
// subscribe to interpreter changes during startup, as the Python
// extension can emit a duplicate event and restart that process.
traceLog("Python debug mode: starting one stable server session")
await runServer()
return
}
const interpreter = getInterpreterFromSetting(serverId)
if (interpreter === undefined || interpreter.length === 0) {
traceLog(`Python extension loading`)