Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **`packageManager` pinned to pnpm@10.33.0** — a different pnpm major regenerated `pnpm-lock.yaml` incompatibly (one contributor diff silently dropped the entire security `overrides` block); the pin plus a CONTRIBUTING note make the wrong-pnpm case fail fast, and the workflows read the pin instead of duplicating a version (#478)

### Fixed
- **A typo'd attach `adapterConfig` key now gets a did-you-mean warning instead of silence** — every attach-capable adapter declares the attach keys its debugger actually consumes (`supportedAttachKeys`), and `attach_to_process` warns about keys outside that set with an edit-distance suggestion (`pathMapping (did you mean pathMappings?)`) while still **forwarding** them to the debug adapter untouched, so upstream debugger options the list doesn't model keep working with no mcp-debugger release; keys an adapter's transform genuinely drops keep warning as ignored (#450's contract). The javascript, ruby, java, and dotnet attach transforms also move from closed allowlists to the python/cpp deny-list pattern, so advanced attach options (js-debug's `localRoot`/`remoteRoot`/`sourceMaps`/`skipFiles`/`continueOnAttach`, rdbg's `localfsMap`, netcoredbg's `sourceFileMap`/`symbolOptions`) now genuinely reach the debugger. Thanks @abhijeetnardele24-hash for the `supportedAttachKeys` + did-you-mean design (#466)
- **Java attach is inspectable on the first try** — the JDI bridge never suspends the VM on attach and java's policy lacked the post-attach pause every other attach adapter has, so the session reported `paused` while the JVM kept running: empty stack, "the debugger may not be paused", and no discoverable recovery. Java now pauses the whole VM after attach (the bridge anchors its stopped event to a thread that can actually report frames), the frameless-thread notes name a real recovery (`threadId` targeting or continue+pause), and `get_local_variables` no longer contradicts the session state (#465)
- **`get_local_variables` walks down past an empty runtime top frame** — a pause inside a blocking syscall/sleep put a stdlib frame with no locals at `stackFrames[0]` and the tool returned an empty array even though the user frame's locals were one frame down and *already fetched*; extraction now re-anchors to the first frame that yields locals (zero extra DAP round-trips), reports that frame in `frame`, and a `note` discloses the walk-down. Skipped under an explicit `names:` filter (#468)
- **Statement-anchored breakpoints survive `redefine_classes`** — the hot-swap replant was purely by line, so a line-shifting swap silently rebound a `statement:`-anchored breakpoint to whatever now lives at the old line, reporting `verified: true`; anchors are now re-resolved against the new source (the `restart_debugging` machinery) and re-sent after the redefine so the JDI replant binds the moved lines. The bridge's `replantedBreakpoints` count also no longer drops re-planted function breakpoints (#464)
Expand Down
23 changes: 23 additions & 0 deletions packages/adapter-cpp/src/cpp-debug-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,29 @@ export class CppDebugAdapter extends EventEmitter implements IDebugAdapter {
readonly language = DebugLanguage.CPP;
readonly name = 'C/C++ Debug Adapter';

// CodeLLDB attach options (https://github.com/vadimcn/codelldb/blob/master/MANUAL.md)
// plus our processId sugar (normalized to pid). Unlisted keys still reach
// CodeLLDB (forwarded with a warning) — this list only powers recognition +
// typo suggestions (#466).
readonly supportedAttachKeys = [
'processId',
'pid',
'program',
'stopOnEntry',
'waitFor',
'initCommands',
'preRunCommands',
'postRunCommands',
'exitCommands',
'targetCreateCommands',
'processCreateCommands',
'expressions',
'sourceMap',
'sourceLanguages',
'relativePathBase',
'breakpointMode'
] as const;

private state: AdapterState = AdapterState.UNINITIALIZED;
private dependencies: AdapterDependencies;
private lastToolchainValidation: ToolchainValidationResult | undefined;
Expand Down
50 changes: 41 additions & 9 deletions packages/adapter-dotnet/src/DotnetDebugAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,19 @@ export class DotnetDebugAdapter extends EventEmitter implements IDebugAdapter {
readonly language = DebugLanguage.DOTNET;
readonly name = '.NET Debug Adapter (netcoredbg)';

// netcoredbg attach options plus the generic keys transformAttachConfig
// special-cases (sourcePaths feeds PDB discovery). Unlisted keys still reach
// netcoredbg (forwarded with a warning) — this list only powers recognition
// + typo suggestions (#466).
readonly supportedAttachKeys = [
'processId',
'justMyCode',
'stopOnEntry',
'sourcePaths',
'sourceFileMap',
'symbolOptions'
] as const;

private state: AdapterState = AdapterState.UNINITIALIZED;
private dependencies: AdapterDependencies;

Expand Down Expand Up @@ -405,23 +418,42 @@ export class DotnetDebugAdapter extends EventEmitter implements IDebugAdapter {
}
}

const attachConfig = {
const {
request: _request,
__attachMode: _attachMode,
host: _host,
port: _port,
processName: _processName,
identifierType: _identifierType,
...rest
} = config as Record<string, unknown>;
void _request; void _attachMode; void _host; void _port;
void _processName; void _identifierType;

// Advanced passthrough with the normalized netcoredbg attach shape on top
// (issues #450/#466). The computed keys stay authoritative — in particular
// terminateDebuggee: false (never kill the debuggee on detach) must not be
// caller-overridable.
const attachConfig: Record<string, unknown> = {
...rest,
type: 'coreclr',
request: 'attach',
name: '.NET: Attach',
processId: config.processId ? Number(config.processId) : undefined,
justMyCode: config.justMyCode ?? true,
// CRITICAL: Never terminate the debuggee on detach
terminateDebuggee: false,
sourceFileMap: pdbScanDirs ? Object.fromEntries(
pdbScanDirs.map(p => [p, p])
) : undefined,
symbolOptions: symbolSearchPaths.length > 0
? { searchPaths: symbolSearchPaths, searchMicrosoftSymbolServer: false }
: undefined
terminateDebuggee: false
};
// Computed values win; a caller-provided sourceFileMap/symbolOptions from
// the spread survives when there is nothing computed to replace it.
if (pdbScanDirs) {
attachConfig.sourceFileMap = Object.fromEntries(pdbScanDirs.map(p => [p, p]));
}
if (symbolSearchPaths.length > 0) {
attachConfig.symbolOptions = { searchPaths: symbolSearchPaths, searchMicrosoftSymbolServer: false };
}

return attachConfig;
return attachConfig as LanguageSpecificAttachConfig;
}

getDefaultAttachConfig(): Partial<GenericAttachConfig> {
Expand Down
26 changes: 26 additions & 0 deletions packages/adapter-dotnet/tests/unit/dotnet-debug-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,32 @@ describe('DotnetDebugAdapter', () => {
expect(defaults).toBeDefined();
expect(defaults!.justMyCode).toBe(true);
});

it('forwards unknown attach keys but keeps terminateDebuggee non-overridable (issues #450/#466)', () => {
const result = adapter.transformAttachConfig({
request: 'attach',
__attachMode: true,
processId: 1234,
terminateDebuggee: true, // must lose to the computed false
futureNetcoredbgOption: 'on'
} as never) as Record<string, unknown>;

expect(result.request).toBe('attach');
expect(result.__attachMode).toBeUndefined();
expect(result.futureNetcoredbgOption).toBe('on');
expect(result.terminateDebuggee).toBe(false);
});

it('keeps a caller-provided sourceFileMap when nothing is computed', () => {
getProcessExecutableDirMock.mockReturnValue(null);
const result = adapter.transformAttachConfig({
request: 'attach',
processId: 1234,
sourceFileMap: { '/remote': '/local' }
} as never) as Record<string, unknown>;

expect(result.sourceFileMap).toEqual({ '/remote': '/local' });
});
});

// ===== Connection Management =====
Expand Down
57 changes: 35 additions & 22 deletions packages/adapter-java/src/java-debug-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@ export class JavaDebugAdapter extends EventEmitter implements IDebugAdapter {
readonly language = DebugLanguage.JAVA;
readonly name = 'Java Debug Adapter (JDI)';

// Keys the JDI bridge's attach handler reads (JdiDapServer.handleAttach)
// plus the generic keys transformAttachConfig special-cases. Unlisted keys
// still reach the bridge (forwarded with a warning) — this list only powers
// recognition + typo suggestions (#466).
readonly supportedAttachKeys = [
'host',
'hostName',
'port',
'stopOnEntry',
'timeout',
'sourcePaths',
'cwd',
'env'
] as const;

private state: AdapterState = AdapterState.UNINITIALIZED;
private dependencies: AdapterDependencies;

Expand Down Expand Up @@ -319,30 +334,28 @@ export class JavaDebugAdapter extends EventEmitter implements IDebugAdapter {
}

transformAttachConfig(config: GenericAttachConfig): LanguageSpecificAttachConfig {
const attachConfig: LanguageSpecificAttachConfig = {
const {
request: _request,
__attachMode: _attachMode,
processId: _processId,
processName: _processName,
identifierType: _identifierType,
host,
port,
...rest
} = config as Record<string, unknown>;
void _request; void _attachMode; void _processId; void _processName;
void _identifierType;

// Advanced passthrough (the JDI bridge ignores keys it doesn't read) with
// the normalized attach shape on top (issues #450/#466).
return {
...rest,
type: 'java',
request: 'attach',
host: config.host || 'localhost',
port: config.port,
};

if (config.sourcePaths) {
attachConfig.sourcePaths = config.sourcePaths;
}
if (config.stopOnEntry !== undefined) {
attachConfig.stopOnEntry = config.stopOnEntry;
}
if (config.cwd) {
attachConfig.cwd = config.cwd;
}
if (config.env) {
attachConfig.env = config.env;
}
if (config.timeout !== undefined) {
attachConfig.timeout = config.timeout;
}

return attachConfig;
host: (host as string | undefined) || 'localhost',
port: port as number | undefined,
} as LanguageSpecificAttachConfig;
}

getDefaultAttachConfig(): Partial<GenericAttachConfig> {
Expand Down
66 changes: 50 additions & 16 deletions packages/adapter-javascript/src/javascript-debug-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,36 @@ export class JavascriptDebugAdapter extends EventEmitter implements IDebugAdapte
readonly language = 'javascript' as unknown as DebugLanguage;
readonly name = 'JavaScript/TypeScript Debug Adapter';

// js-debug pwa-node attach options https://github.com/microsoft/vscode-js-debug/blob/main/package.json
// plus the generic keys transformAttachConfig special-cases. Unlisted keys
// still reach js-debug (forwarded with a warning) — this list only powers
// recognition + typo suggestions (#466).
readonly supportedAttachKeys = [
'host',
'port',
'address',
'timeout',
'localRoot',
'remoteRoot',
'smartStep',
'skipFiles',
'sourceMaps',
'sourceMapPathOverrides',
'outFiles',
'outputCapture',
'resolveSourceMapLocations',
'pauseForSourceMap',
'stopOnEntry',
'justMyCode',
'cwd',
'env',
'restart',
'continueOnAttach',
'trace',
'websocketAddress',
'attachExistingChildren'
] as const;

private state: AdapterState = AdapterState.UNINITIALIZED;
private readonly dependencies: AdapterDependencies;

Expand Down Expand Up @@ -628,25 +658,29 @@ export class JavascriptDebugAdapter extends EventEmitter implements IDebugAdapte
* mode and JsDebugAdapterPolicy.performHandshake sends a real DAP 'attach'.
*/
transformAttachConfig(config: GenericAttachConfig): LanguageSpecificAttachConfig {
const attachConfig: LanguageSpecificAttachConfig = {
const {
request: _request,
__attachMode: _attachMode,
processId: _processId,
processName: _processName,
identifierType: _identifierType,
host,
port,
...rest
} = config as Record<string, unknown>;
void _request; void _attachMode; void _processId; void _processName;
void _identifierType;

// Advanced passthrough (localRoot/remoteRoot, sourceMaps, skipFiles, …)
// with the normalized pwa-node attach shape on top (issues #450/#466).
return {
...rest,
type: 'pwa-node',
request: 'attach',
name: 'Attach to Node.js process',
host: config.host || '127.0.0.1',
port: config.port,
};

if (config.stopOnEntry !== undefined) {
attachConfig.stopOnEntry = config.stopOnEntry;
}
if (config.justMyCode !== undefined) {
attachConfig.justMyCode = config.justMyCode;
}
if (config.timeout !== undefined) {
attachConfig.timeout = config.timeout;
}

return attachConfig;
host: (host as string | undefined) || '127.0.0.1',
port: port as number | undefined,
} as LanguageSpecificAttachConfig;
}

getDefaultAttachConfig(): Partial<GenericAttachConfig> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,4 +330,51 @@ describe('JavascriptDebugAdapter.transformLaunchConfig', () => {
expect(env.NODE_OPTIONS ?? '').not.toContain('exitcode-shim');
});
});

describe('transformAttachConfig passthrough (issues #450/#466)', () => {
it('normalizes the pwa-node attach shape and defaults the host', () => {
const adapter = new JavascriptDebugAdapter(deps);
const cfg = adapter.transformAttachConfig({
request: 'attach',
port: 9229,
stopOnEntry: true,
justMyCode: false,
timeout: 15000
} as any) as Record<string, unknown>;

expect(cfg.type).toBe('pwa-node');
expect(cfg.request).toBe('attach');
expect(cfg.host).toBe('127.0.0.1');
expect(cfg.port).toBe(9229);
expect(cfg.stopOnEntry).toBe(true);
expect(cfg.justMyCode).toBe(false);
expect(cfg.timeout).toBe(15000);
});

it('forwards advanced js-debug options and strips reserved keys', () => {
const adapter = new JavascriptDebugAdapter(deps);
const cfg = adapter.transformAttachConfig({
request: 'launch', // must not survive: attach transforms pin the request
__attachMode: true,
processId: 4242,
host: '10.0.0.5',
port: 9229,
localRoot: '/local/src',
remoteRoot: '/app',
sourceMaps: false,
skipFiles: ['<node_internals>/**'],
continueOnAttach: true
} as any) as Record<string, unknown>;

expect(cfg.request).toBe('attach');
expect(cfg.__attachMode).toBeUndefined();
expect(cfg.processId).toBeUndefined();
expect(cfg.host).toBe('10.0.0.5');
expect(cfg.localRoot).toBe('/local/src');
expect(cfg.remoteRoot).toBe('/app');
expect(cfg.sourceMaps).toBe(false);
expect(cfg.skipFiles).toEqual(['<node_internals>/**']);
expect(cfg.continueOnAttach).toBe(true);
});
});
});
23 changes: 23 additions & 0 deletions packages/adapter-python/src/python-debug-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,29 @@ export class PythonDebugAdapter extends EventEmitter implements IDebugAdapter {
readonly language = DebugLanguage.PYTHON;
readonly name = 'Python Debug Adapter';

// debugpy attach schema https://github.com/microsoft/debugpy/wiki/Debug-configuration-settings
// plus the generic keys transformAttachConfig special-cases. Unlisted keys
// still reach debugpy (forwarded with a warning) — this list only powers
// recognition + typo suggestions (#466).
readonly supportedAttachKeys = [
'host',
'port',
'justMyCode',
'pathMappings',
'redirectOutput',
'showReturnValue',
'subProcess',
'clientOS',
'django',
'jinja',
'stopOnEntry',
'cwd',
'env',
'logToFile',
'steppingResumesAllThreads',
'rules'
] as const;

private state: AdapterState = AdapterState.UNINITIALIZED;
private dependencies: AdapterDependencies;

Expand Down
Loading
Loading