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:
Vendored
+35
-12
@@ -10,13 +10,21 @@
|
|||||||
"type": "extensionHost",
|
"type": "extensionHost",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"runtimeExecutable": "${execPath}",
|
"runtimeExecutable": "${execPath}",
|
||||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
|
"args": [
|
||||||
"outFiles": ["${workspaceFolder}/client/**/*.js"],
|
"--extensionDevelopmentPath=${env:TEMP}/nx-post-support-vscode-debug",
|
||||||
|
"${env:TEMP}/nx-post-support-vscode-debug",
|
||||||
|
"${env:TEMP}/nx-post-support-vscode-debug/test/test.tcl"
|
||||||
|
],
|
||||||
|
"cwd": "${env:TEMP}/nx-post-support-vscode-debug",
|
||||||
|
"outFiles": ["${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js"],
|
||||||
|
"sourceMaps": true,
|
||||||
|
"resolveSourceMapLocations": [
|
||||||
|
"${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js",
|
||||||
|
"!**/node_modules/**"
|
||||||
|
],
|
||||||
|
"skipFiles": ["<node_internals>/**"],
|
||||||
"autoAttachChildProcesses": true,
|
"autoAttachChildProcesses": true,
|
||||||
"preLaunchTask": {
|
"preLaunchTask": "NX Post Support: Compile Debug"
|
||||||
"type": "npm",
|
|
||||||
"script": "watch"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Python Attach",
|
"name": "Python Attach",
|
||||||
@@ -34,10 +42,24 @@
|
|||||||
"name": "Debug Extension (hidden)",
|
"name": "Debug Extension (hidden)",
|
||||||
"type": "extensionHost",
|
"type": "extensionHost",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
|
"runtimeExecutable": "${execPath}",
|
||||||
"outFiles": ["${workspaceFolder}/client/**/*.js"],
|
"args": [
|
||||||
|
"--extensionDevelopmentPath=${env:TEMP}/nx-post-support-vscode-debug",
|
||||||
|
"${env:TEMP}/nx-post-support-vscode-debug",
|
||||||
|
"${env:TEMP}/nx-post-support-vscode-debug/test/test.tcl"
|
||||||
|
],
|
||||||
|
"cwd": "${env:TEMP}/nx-post-support-vscode-debug",
|
||||||
|
"outFiles": ["${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js"],
|
||||||
|
"sourceMaps": true,
|
||||||
|
"resolveSourceMapLocations": [
|
||||||
|
"${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js",
|
||||||
|
"!**/node_modules/**"
|
||||||
|
],
|
||||||
|
"skipFiles": ["<node_internals>/**"],
|
||||||
"env": {
|
"env": {
|
||||||
"USE_DEBUGPY": "True"
|
"USE_DEBUGPY": "True",
|
||||||
|
"NXPS_DEBUG_HOST": "127.0.0.1",
|
||||||
|
"NXPS_DEBUG_PORT": "5678"
|
||||||
},
|
},
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"hidden": true,
|
"hidden": true,
|
||||||
@@ -49,8 +71,9 @@
|
|||||||
"name": "Python debug server (hidden)",
|
"name": "Python debug server (hidden)",
|
||||||
"type": "debugpy",
|
"type": "debugpy",
|
||||||
"request": "attach",
|
"request": "attach",
|
||||||
"listen": { "host": "localhost", "port": 5678 },
|
"listen": { "host": "127.0.0.1", "port": 5678 },
|
||||||
"justMyCode": true,
|
"justMyCode": false,
|
||||||
|
"logToFile": true,
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"hidden": true,
|
"hidden": true,
|
||||||
"group": "",
|
"group": "",
|
||||||
@@ -63,7 +86,7 @@
|
|||||||
"name": "Debug Extension and Python",
|
"name": "Debug Extension and Python",
|
||||||
"configurations": ["Python debug server (hidden)", "Debug Extension (hidden)"],
|
"configurations": ["Python debug server (hidden)", "Debug Extension (hidden)"],
|
||||||
"stopAll": true,
|
"stopAll": true,
|
||||||
"preLaunchTask": "npm: watch",
|
"preLaunchTask": "NX Post Support: Compile Debug",
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"group": "",
|
"group": "",
|
||||||
|
|||||||
Vendored
+57
@@ -0,0 +1,57 @@
|
|||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$WorkspaceRoot,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$DebugRoot
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function ConvertFrom-ExtendedWindowsPath {
|
||||||
|
param([string]$Path)
|
||||||
|
|
||||||
|
if ($Path.StartsWith("\\?\UNC\", [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
return "\\" + $Path.Substring(8)
|
||||||
|
}
|
||||||
|
if ($Path.StartsWith("\\?\", [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
return $Path.Substring(4)
|
||||||
|
}
|
||||||
|
return $Path
|
||||||
|
}
|
||||||
|
|
||||||
|
$workspacePath = ConvertFrom-ExtendedWindowsPath $WorkspaceRoot
|
||||||
|
$workspaceItem = Get-Item -LiteralPath $workspacePath
|
||||||
|
if (-not $workspaceItem.PSIsContainer) {
|
||||||
|
throw "Workspace root is not a directory: $workspacePath"
|
||||||
|
}
|
||||||
|
$workspacePath = $workspaceItem.FullName
|
||||||
|
|
||||||
|
$debugPath = ConvertFrom-ExtendedWindowsPath $DebugRoot
|
||||||
|
$tempPath = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath()).TrimEnd("\")
|
||||||
|
$debugParent = [System.IO.Path]::GetFullPath((Split-Path -Parent $debugPath)).TrimEnd("\")
|
||||||
|
if (-not $debugParent.Equals($tempPath, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
throw "Debug alias must be located directly below the user temp directory: $debugPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path -LiteralPath $debugPath) {
|
||||||
|
$debugItem = Get-Item -LiteralPath $debugPath -Force
|
||||||
|
if ($debugItem.LinkType -ne "Junction") {
|
||||||
|
throw "Debug alias exists but is not a junction: $debugPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentTarget = (Get-Item -LiteralPath $debugItem.Target).FullName
|
||||||
|
if (-not $currentTarget.Equals($workspacePath, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
# Removing a junction removes only the link, never the target directory.
|
||||||
|
Remove-Item -LiteralPath $debugPath -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $debugPath)) {
|
||||||
|
New-Item -ItemType Junction -Path $debugPath -Target $workspacePath | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output "Debug extension path: $debugPath -> $workspacePath"
|
||||||
|
& npm.cmd --prefix $workspacePath run compile:debug
|
||||||
|
exit $LASTEXITCODE
|
||||||
Vendored
+26
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "NX Post Support: Compile Debug",
|
||||||
|
"type": "process",
|
||||||
|
"command": "powershell.exe",
|
||||||
|
"args": [
|
||||||
|
"-NoLogo",
|
||||||
|
"-NoProfile",
|
||||||
|
"-ExecutionPolicy",
|
||||||
|
"Bypass",
|
||||||
|
"-Command",
|
||||||
|
"& { param([string]$WorkspaceRoot, [string]$DebugRoot); $scriptRoot = $WorkspaceRoot; if ($scriptRoot.StartsWith('\\\\?\\')) { $scriptRoot = $scriptRoot.Substring(4) }; & (Join-Path $scriptRoot '.vscode\\prepare-debug.ps1') -WorkspaceRoot $WorkspaceRoot -DebugRoot $DebugRoot; exit $LASTEXITCODE }",
|
||||||
|
"${workspaceFolder}",
|
||||||
|
"${env:TEMP}\\nx-post-support-vscode-debug"
|
||||||
|
],
|
||||||
|
"problemMatcher": [],
|
||||||
|
"presentation": {
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "dedicated",
|
||||||
|
"clear": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ A comprehensive VS Code extension providing language support for NX CAM postproc
|
|||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Install from the VS Code Marketplace
|
1. Install from the VS Code Marketplace
|
||||||
2. Install Python 3.8 or higher
|
2. Install Python 3.11 or higher
|
||||||
3. Open any `.cdl`, `.tcl`, or `.def` file
|
3. Open any `.cdl`, `.tcl`, or `.def` file
|
||||||
4. The extension will automatically activate and provide language support
|
4. The extension will automatically activate and provide language support
|
||||||
|
|
||||||
@@ -43,6 +43,29 @@ Simply open any supported file type and enjoy:
|
|||||||
- Hover information
|
- Hover information
|
||||||
- Signature help while entering procedure arguments
|
- Signature help while entering procedure arguments
|
||||||
|
|
||||||
|
## Development and debugging
|
||||||
|
|
||||||
|
Install the root and client dependencies before the first debug session:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm install
|
||||||
|
npm install --prefix client
|
||||||
|
```
|
||||||
|
|
||||||
|
Use one of the checked-in VS Code launch configurations:
|
||||||
|
|
||||||
|
- **Run Extension** debugs the TypeScript extension host.
|
||||||
|
- **Debug Extension and Python** debugs both the TypeScript extension and the
|
||||||
|
Python language server. This is the recommended configuration for LSP work.
|
||||||
|
- **Python Attach** attaches manually to an already running Python process.
|
||||||
|
|
||||||
|
The launch configuration creates a fresh non-minified bundle with embedded
|
||||||
|
source maps and opens `test/test.tcl` so the extension activates immediately.
|
||||||
|
For combined debugging, the Python adapter listens on `127.0.0.1:5678`; the
|
||||||
|
language server waits for that adapter before initialization. The NX
|
||||||
|
Postprocessor Support output channel reports `Python debug mode: enabled` and
|
||||||
|
shows `_debug_server.py` in the server command when the debug path is active.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
This extension is actively maintained. For issues or feature requests, please visit our [repository](https://git.cbsk-tech.de/Christoph/nx_post_support.git).
|
This extension is actively maintained. For issues or feature requests, please visit our [repository](https://git.cbsk-tech.de/Christoph/nx_post_support.git).
|
||||||
|
|||||||
Generated
+22
@@ -14,6 +14,7 @@
|
|||||||
"vscode-languageclient": "^9.0.1"
|
"vscode-languageclient": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/fs-extra": "^11.0.4",
|
||||||
"@types/node": "^22.10.5",
|
"@types/node": "^22.10.5",
|
||||||
"@types/vscode": "^1.96.0"
|
"@types/vscode": "^1.96.0"
|
||||||
},
|
},
|
||||||
@@ -21,6 +22,27 @@
|
|||||||
"vscode": "^1.96.0"
|
"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": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.10.5",
|
"version": "22.10.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"vscode-languageclient": "^9.0.1"
|
"vscode-languageclient": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/fs-extra": "^11.0.4",
|
||||||
"@types/node": "^22.10.5",
|
"@types/node": "^22.10.5",
|
||||||
"@types/vscode": "^1.96.0"
|
"@types/vscode": "^1.96.0"
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-30
@@ -39,13 +39,26 @@ async function createServer(
|
|||||||
initializationOptions: IInitOptions
|
initializationOptions: IInitOptions
|
||||||
): Promise<LanguageClient> {
|
): Promise<LanguageClient> {
|
||||||
const command = settings.interpreter[0]
|
const command = settings.interpreter[0]
|
||||||
|
if (!command) {
|
||||||
|
throw new Error("No Python interpreter is configured for the language server.")
|
||||||
|
}
|
||||||
const cwd = settings.cwd
|
const cwd = settings.cwd
|
||||||
|
|
||||||
// Set debugger path needed for debugging python code.
|
// Set debugger path needed for debugging python code.
|
||||||
const newEnv = { ...process.env }
|
const newEnv = { ...process.env }
|
||||||
const debuggerPath = await getDebuggerPath()
|
|
||||||
const isDebugScript = await fsapi.pathExists(DEBUG_SERVER_SCRIPT_PATH)
|
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
|
newEnv.DEBUGPY_PATH = debuggerPath
|
||||||
} else {
|
} else {
|
||||||
newEnv.USE_DEBUGPY = "False"
|
newEnv.USE_DEBUGPY = "False"
|
||||||
@@ -57,10 +70,13 @@ async function createServer(
|
|||||||
// Set notification type
|
// Set notification type
|
||||||
newEnv.LS_SHOW_NOTIFICATION = settings.showNotifications
|
newEnv.LS_SHOW_NOTIFICATION = settings.showNotifications
|
||||||
|
|
||||||
const args =
|
const serverScript = debugRequested ? DEBUG_SERVER_SCRIPT_PATH : SERVER_SCRIPT_PATH
|
||||||
newEnv.USE_DEBUGPY === "False" || !isDebugScript
|
const interpreterArgs = settings.interpreter.slice(1)
|
||||||
? settings.interpreter.slice(1).concat([SERVER_SCRIPT_PATH])
|
if (debugRequested && !interpreterArgs.includes("-Xfrozen_modules=off")) {
|
||||||
: settings.interpreter.slice(1).concat([DEBUG_SERVER_SCRIPT_PATH])
|
interpreterArgs.push("-Xfrozen_modules=off")
|
||||||
|
}
|
||||||
|
const args = interpreterArgs.concat([serverScript])
|
||||||
|
traceInfo(`Python debug mode: ${debugRequested ? "enabled" : "disabled"}`)
|
||||||
traceInfo(`Server run command: ${[command, ...args].join(" ")}`)
|
traceInfo(`Server run command: ${[command, ...args].join(" ")}`)
|
||||||
|
|
||||||
const serverOptions: ServerOptions = {
|
const serverOptions: ServerOptions = {
|
||||||
@@ -108,35 +124,40 @@ export async function restartServer(
|
|||||||
const projectRoot = await getProjectRoot()
|
const projectRoot = await getProjectRoot()
|
||||||
const workspaceSetting = await getWorkspaceSettings(serverId, projectRoot, true)
|
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 {
|
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()
|
await newLSClient.start()
|
||||||
|
const level = getLSClientTraceLevel(outputChannel.logLevel, env.logLevel)
|
||||||
|
await newLSClient.setTrace(level)
|
||||||
|
return newLSClient
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
traceError(`Server: Start failed: ${ex}`)
|
traceError(`Server: Start failed: ${ex}`)
|
||||||
disposeServerResources()
|
disposeServerResources()
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const level = getLSClientTraceLevel(outputChannel.logLevel, env.logLevel)
|
|
||||||
await newLSClient.setTrace(level)
|
|
||||||
return newLSClient
|
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-3
@@ -38,6 +38,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
const serverInfo = loadServerDefaults()
|
const serverInfo = loadServerDefaults()
|
||||||
const serverName = serverInfo.name
|
const serverName = serverInfo.name
|
||||||
const serverId = serverInfo.module
|
const serverId = serverInfo.module
|
||||||
|
const pythonDebugMode = process.env.USE_DEBUGPY?.toLowerCase() === "true"
|
||||||
|
|
||||||
// Setup logging
|
// Setup logging
|
||||||
const outputChannel = createOutputChannel(serverName)
|
const outputChannel = createOutputChannel(serverName)
|
||||||
@@ -101,10 +102,15 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
return runServerQueue
|
return runServerQueue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!pythonDebugMode) {
|
||||||
|
context.subscriptions.push(
|
||||||
|
onDidChangePythonInterpreter(async () => {
|
||||||
|
await runServer()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
onDidChangePythonInterpreter(async () => {
|
|
||||||
await runServer()
|
|
||||||
}),
|
|
||||||
onDidChangeConfiguration(async (e: vscode.ConfigurationChangeEvent) => {
|
onDidChangeConfiguration(async (e: vscode.ConfigurationChangeEvent) => {
|
||||||
if (checkIfConfigurationChanged(e, serverId)) {
|
if (checkIfConfigurationChanged(e, serverId)) {
|
||||||
await runServer()
|
await runServer()
|
||||||
@@ -116,6 +122,15 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
setImmediate(async () => {
|
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)
|
const interpreter = getInterpreterFromSetting(serverId)
|
||||||
if (interpreter === undefined || interpreter.length === 0) {
|
if (interpreter === undefined || interpreter.length === 0) {
|
||||||
traceLog(`Python extension loading`)
|
traceLog(`Python extension loading`)
|
||||||
|
|||||||
+4
-2
@@ -1,19 +1,21 @@
|
|||||||
const esbuild = require("esbuild")
|
const esbuild = require("esbuild")
|
||||||
|
const path = require("path")
|
||||||
|
|
||||||
const production = process.argv.includes("--production")
|
const production = process.argv.includes("--production")
|
||||||
const watch = process.argv.includes("--watch")
|
const watch = process.argv.includes("--watch")
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const ctx = await esbuild.context({
|
const ctx = await esbuild.context({
|
||||||
|
absWorkingDir: __dirname,
|
||||||
entryPoints: ["client/src/extension.ts"],
|
entryPoints: ["client/src/extension.ts"],
|
||||||
bundle: true,
|
bundle: true,
|
||||||
format: "cjs",
|
format: "cjs",
|
||||||
minify: production,
|
minify: production,
|
||||||
sourcemap: !production,
|
sourcemap: !production,
|
||||||
sourcesContent: false,
|
sourcesContent: !production,
|
||||||
platform: "node",
|
platform: "node",
|
||||||
// outdir: "out",
|
// outdir: "out",
|
||||||
outfile: "./dist/extension.js",
|
outfile: path.join(__dirname, "dist", "extension.js"),
|
||||||
external: ["vscode"],
|
external: ["vscode"],
|
||||||
logLevel: "silent",
|
logLevel: "silent",
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|||||||
@@ -124,6 +124,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"compile": "node esbuild.js --production",
|
"compile": "node esbuild.js --production",
|
||||||
|
"compile:debug": "node esbuild.js",
|
||||||
"watch": "node esbuild.js --watch",
|
"watch": "node esbuild.js --watch",
|
||||||
"package": "node esbuild.js --production"
|
"package": "node esbuild.js --production"
|
||||||
},
|
},
|
||||||
|
|||||||
+47
-24
@@ -6,6 +6,7 @@ import os
|
|||||||
import pathlib
|
import pathlib
|
||||||
import runpy
|
import runpy
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
def update_sys_path(path_to_add: str) -> None:
|
def update_sys_path(path_to_add: str) -> None:
|
||||||
@@ -14,10 +15,43 @@ def update_sys_path(path_to_add: str) -> None:
|
|||||||
sys.path.append(path_to_add)
|
sys.path.append(path_to_add)
|
||||||
|
|
||||||
|
|
||||||
# Ensure debugger is loaded before we load anything else, to debug initialization.
|
def _debug_endpoint() -> tuple[str, int]:
|
||||||
debugger_path = os.getenv("DEBUGPY_PATH", None)
|
host = os.getenv("NXPS_DEBUG_HOST", "127.0.0.1")
|
||||||
if debugger_path:
|
raw_port = os.getenv("NXPS_DEBUG_PORT", "5678")
|
||||||
if debugger_path.endswith("debugpy"):
|
try:
|
||||||
|
port = int(raw_port)
|
||||||
|
except ValueError as error:
|
||||||
|
raise RuntimeError(f"Invalid NXPS_DEBUG_PORT: {raw_port!r}") from error
|
||||||
|
if not 1 <= port <= 65535:
|
||||||
|
raise RuntimeError(f"NXPS_DEBUG_PORT is outside the valid range: {port}")
|
||||||
|
return host, port
|
||||||
|
|
||||||
|
|
||||||
|
def _connect_debugger(debugpy, host: str, port: int, timeout: float = 15.0) -> None:
|
||||||
|
deadline = time.monotonic() + timeout
|
||||||
|
last_error: OSError | None = None
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
try:
|
||||||
|
debugpy.connect((host, port))
|
||||||
|
debugpy.wait_for_client()
|
||||||
|
return
|
||||||
|
except (ConnectionRefusedError, OSError) as error:
|
||||||
|
last_error = error
|
||||||
|
time.sleep(0.25)
|
||||||
|
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Could not connect debugpy to {host}:{port} within {timeout:.0f} seconds"
|
||||||
|
) from last_error
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
# Ensure debugger is loaded before we load anything else, so server
|
||||||
|
# initialization and module-level feature registration can be debugged.
|
||||||
|
debugger_path = os.getenv("DEBUGPY_PATH")
|
||||||
|
if not debugger_path:
|
||||||
|
raise RuntimeError("DEBUGPY_PATH is missing in Python debug mode")
|
||||||
|
|
||||||
|
if pathlib.Path(debugger_path).name.casefold() == "debugpy":
|
||||||
debugger_path = os.fspath(pathlib.Path(debugger_path).parent)
|
debugger_path = os.fspath(pathlib.Path(debugger_path).parent)
|
||||||
|
|
||||||
update_sys_path(debugger_path)
|
update_sys_path(debugger_path)
|
||||||
@@ -25,25 +59,14 @@ if debugger_path:
|
|||||||
# pylint: disable=wrong-import-position,import-error
|
# pylint: disable=wrong-import-position,import-error
|
||||||
import debugpy
|
import debugpy
|
||||||
|
|
||||||
# 5678 is the default port, If you need to change it update it here
|
host, port = _debug_endpoint()
|
||||||
# and in launch.json.
|
print(f"debugpy: waiting for VS Code at {host}:{port}", file=sys.stderr)
|
||||||
# Connecting requires the "Python debug server" listener (launch.json) to be
|
_connect_debugger(debugpy, host, port)
|
||||||
# up first. If it isn't (e.g. wrong launch config was used), don't crash the
|
print("debugpy: VS Code attached; starting language server", file=sys.stderr)
|
||||||
# whole language server - just continue running without the debugger attached.
|
|
||||||
try:
|
|
||||||
debugpy.connect(5678)
|
|
||||||
except (ConnectionRefusedError, OSError) as exc:
|
|
||||||
print(
|
|
||||||
f"debugpy: could not connect to debug adapter on port 5678 "
|
|
||||||
f"({exc}); continuing without debugging.",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
|
|
||||||
# This will ensure that execution is paused as soon as the debugger
|
server_path = os.fspath(pathlib.Path(__file__).parent / "lsp_server.py")
|
||||||
# connects to VS Code. If you don't want to pause here comment this
|
runpy.run_path(server_path, run_name="__main__")
|
||||||
# line and set breakpoints as appropriate.
|
|
||||||
# debugpy.breakpoint()
|
|
||||||
|
|
||||||
SERVER_PATH = os.fspath(pathlib.Path(__file__).parent / "lsp_server.py")
|
|
||||||
# NOTE: Set breakpoint in `lsp_server.py` before continuing.
|
if __name__ == "__main__":
|
||||||
runpy.run_path(SERVER_PATH, run_name="__main__")
|
main()
|
||||||
|
|||||||
@@ -704,7 +704,13 @@ def initialized(_params: lsp.InitializedParams):
|
|||||||
|
|
||||||
def index_workspace():
|
def index_workspace():
|
||||||
try:
|
try:
|
||||||
root = LSP_SERVER.workspace.root_path
|
try:
|
||||||
|
root = LSP_SERVER.workspace.root_path
|
||||||
|
except RuntimeError:
|
||||||
|
root = None
|
||||||
|
if not root:
|
||||||
|
log_to_output("Background indexing skipped: no workspace folder is open.")
|
||||||
|
return
|
||||||
log_to_output("Background indexing started...")
|
log_to_output("Background indexing started...")
|
||||||
root_path = pathlib.Path(root)
|
root_path = pathlib.Path(root)
|
||||||
skipped_directories = {
|
skipped_directories = {
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
THIS_DIR = Path(__file__).parent
|
||||||
|
SRC_DIR = THIS_DIR.parent.parent / "src"
|
||||||
|
if str(SRC_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(SRC_DIR))
|
||||||
|
|
||||||
|
import _debug_server
|
||||||
|
|
||||||
|
|
||||||
|
def test_debug_endpoint_defaults(monkeypatch):
|
||||||
|
monkeypatch.delenv("NXPS_DEBUG_HOST", raising=False)
|
||||||
|
monkeypatch.delenv("NXPS_DEBUG_PORT", raising=False)
|
||||||
|
|
||||||
|
assert _debug_server._debug_endpoint() == ("127.0.0.1", 5678)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("port", ["invalid", "0", "65536"])
|
||||||
|
def test_debug_endpoint_rejects_invalid_port(monkeypatch, port):
|
||||||
|
monkeypatch.setenv("NXPS_DEBUG_PORT", port)
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
_debug_server._debug_endpoint()
|
||||||
|
|
||||||
|
|
||||||
|
def test_connect_debugger_retries_until_adapter_is_ready(monkeypatch):
|
||||||
|
class FakeDebugpy:
|
||||||
|
def __init__(self):
|
||||||
|
self.connect_calls = 0
|
||||||
|
self.wait_calls = 0
|
||||||
|
|
||||||
|
def connect(self, endpoint):
|
||||||
|
assert endpoint == ("127.0.0.1", 5678)
|
||||||
|
self.connect_calls += 1
|
||||||
|
if self.connect_calls < 3:
|
||||||
|
raise ConnectionRefusedError("listener is starting")
|
||||||
|
|
||||||
|
def wait_for_client(self):
|
||||||
|
self.wait_calls += 1
|
||||||
|
|
||||||
|
fake_debugpy = FakeDebugpy()
|
||||||
|
monkeypatch.setattr(_debug_server.time, "sleep", lambda _seconds: None)
|
||||||
|
|
||||||
|
_debug_server._connect_debugger(
|
||||||
|
fake_debugpy, "127.0.0.1", 5678, timeout=1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
assert fake_debugpy.connect_calls == 3
|
||||||
|
assert fake_debugpy.wait_calls == 1
|
||||||
@@ -114,3 +114,6 @@ proc SERVICE_get_tool_data {} {
|
|||||||
LIB_GE_command_buffer_edit_replace MOM_end_of_program_LIB END_OF_PROGRAM @END_OF_PROG {
|
LIB_GE_command_buffer_edit_replace MOM_end_of_program_LIB END_OF_PROGRAM @END_OF_PROG {
|
||||||
MOM_do_template "end_of_program_rewind"
|
MOM_do_template "end_of_program_rewind"
|
||||||
} EndOfProgramRewind
|
} EndOfProgramRewind
|
||||||
|
|
||||||
|
|
||||||
|
SERVICE_remove_file "test"
|
||||||
Reference in New Issue
Block a user