Skip to content

Debug adapter falls back to process CWD as package cache and recursively scans it — on Linux (CWD = $HOME) attach takes 15-30s and stackTrace never responds #8276

Description

@StefanMaron

Describe the issue

On Linux, AL debugging (both attach and launch) against an OnPrem server is effectively broken in real VS Code, while the very same debug adapter binary works fine when driven headlessly. Symptoms:

  • attach takes 15ΓÇô30 seconds to complete (instead of <1 s).
  • When a breakpoint hits, VS Code shows "Paused on Step" with an empty call stack that never populates ΓÇö the DAP stackTrace request gets no response (observed >3 minutes with none).
  • DebuggerServices.log fills with:
An exception happened while reading the package cache: '/home/<user>'. Too many levels of symbolic links : '/home/<user>/...'

Root cause

Microsoft.Dynamics.Nav.EditorServices.Protocol.dll, SettingsExtensions.GetPackageCachePaths:

string[] array = settings.AlResourceConfigurationSettings?.PackageCachePaths;
if (array == null || array.Length == 0)
{
    array = new string[1] { Directory.GetCurrentDirectory() };  // <-- fallback
}

The debug adapter is a separate process from the language server and never receives al.packageCachePath: no CLI argument carries it (getLanguageServerOptions in the extension does not emit it) and the extension never sends al/setActiveWorkspace over the DAP channel. So during a debug session this fallback always fires.

The extension spawns the adapter via new DebugAdapterExecutable(path, args) without a cwd, so the adapter inherits the VS Code extension host's working directory. On Windows that happens to be a small directory (e.g. the VS Code install dir), so the recursive *.app scan triggered by the first stackTrace-induced compilation (DebugAdapterStackTraceHandler → Workspace.CurrentSolution.GetCompilationAsync → MovedSymbolsPackageCacheLoader.LoadMovedObjectsManifests) and during attach processing finishes instantly and the bug stays invisible. On Linux the extension host CWD is typically the user's home directory: the scan walks hundreds of thousands of files, and any symlink cycle under $HOME (e.g. tooling like mise creates ~/.local/state/mise/trusted-configs/<id> -> /home/<user>) makes it never terminate — each pass dies with "Too many levels of symbolic links" and is retried.

Steps to reproduce

  1. Linux, AL extension 17.0.2273547 (EditorServices Host v17.0.34.45391), any OnPrem launch config with "request": "attach", project without al.packageCachePath set.
  2. Launch VS Code so its CWD is $HOME (the default for desktop launchers and code from ~).
  3. Start the attach config, sign in to the web client, hit a breakpoint.
  4. Observe slow attach, then "Paused on Step" with a never-loading call stack; see the package-cache exceptions in DebuggerServices.log.

Control experiment: spawning the identical adapter binary with the project folder as CWD makes attach complete in ~1 s and stackTrace respond in ~3 s ΓÇö confirming the CWD fallback is the trigger.

Adapter CWD attach first stackTrace
project folder <1 s ~3 s
$HOME (what VS Code uses on Linux) 15ΓÇô30 s no response in >3 min

How to fix ΓÇö detailed guidance

Four layers; (1) alone fixes the issue from the extension side, (2)ΓÇô(4) fix it properly in the adapter.

1. Extension (TypeScript) ΓÇö pass cwd when spawning the adapter. In AlDebugAdapterDescriptorFactory.createDescriptor, the workspace folder is already in scope (it is used for /projectRoot:). Add the DebugAdapterExecutableOptions third argument:

// before
return new vscode.DebugAdapterExecutable(serverPath, args);
// after
return new vscode.DebugAdapterExecutable(serverPath, args, { cwd: workspaceFolder.uri.fsPath });

Verified locally against the 17.0.2273547 bundle (patching the equivalent minified call in dist/extension.js): attach goes from ~25 s to ~1 s, stackTrace responds in ~3 s, and F5 launch mode becomes reliable too ΓÇö before the fix, the multi-second scan during attach/launch processing raced the browser's session creation, so the debuggingcontext session often failed to bind (sign-in loop, debugger never bound).

2. Adapter (C#) ΓÇö fix the fallback itself. In SettingsExtensions.GetPackageCachePaths, the method already early-returns when settings.WorkspacePath is empty, so a workspace path is guaranteed at the fallback site. Default to the documented default of al.packageCachePath (./.alpackages relative to the project) instead of the process CWD:

// before
array = new string[1] { Directory.GetCurrentDirectory() };
// after
array = new string[1] { ".alpackages" };  // resolved against WorkspacePath by the Select below

(The method already maps every entry through PathUtilities.CombinePath(settings.WorkspacePath.GetFilePath(), path), which handles the relative path.) Directory.GetCurrentDirectory() is never a meaningful location for a DAP process spawned by an IDE.

3. Transport ΓÇö actually deliver al.packageCachePath to the debug adapter. Two mechanisms already exist and just aren't wired up:

  • CLI: getLanguageServerOptions() already serializes comparable settings (e.g. al.testCoverageCachePath ΓåÆ /testCoverageCachePath:); emit /packageCachePath: the same way and populate Settings.AlResourceConfigurationSettings.PackageCachePaths from it in the host.
  • DAP channel: SetActiveWorkspaceRequestHandler (al/setActiveWorkspace) is registered by ChannelBase, so the debug adapter can already receive full workspace settings ΓÇö the extension simply never sends that request on debug sessions. Sending it right after initialize (before attach/launch) would populate the settings ahead of the first compilation.

4. Hardening ΓÇö make the scan cycle-safe regardless of root. The recursive *.app enumeration reached from MovedSymbolsPackageCacheLoader.LoadMovedObjectsManifests and ReferenceLoaderFactory.CreateReferenceLoader (via IFileSystem.GetFiles(cachePath, "*.app")) should enumerate with:

new EnumerationOptions
{
    RecurseSubdirectories = true,
    IgnoreInaccessible = true,
    AttributesToSkip = FileAttributes.ReparsePoint, // do not follow symlinked dirs -> no cycles
}

Today a symlink cycle surfaces as IOException ("Too many levels of symbolic links"), the pass is abandoned and retried, and the operation never completes. Also consider skipping the scan entirely when the cache directory does not exist.

Possibly related: #8153

#8153 (AL debugger cannot open files in dev containers / remote sessions — stack frame paths are remote-side paths the local window cannot open) lives in the same code path this issue is about: DebugAdapterStackTraceHandler.ToStackFrameAsync / CreateSourceAsync put raw machine-local filesystem paths into DAP Source.path — location.GetLineSpan().Path for in-project frames, and FilePathHelper.GetPreviewFileUri(project.ProjectFolder, …) with Origin = "External" for out-of-project frames. Whether a frame resolves to a real source location at all also depends on the same project compilation whose package-cache root is broken here. The two are distinct defects, but both come from the DAP host making process-local path/environment assumptions (CWD, raw fs paths) that only hold on a non-remote Windows desktop — a combined fix pass over that area would address both.

Versions

  • AL extension: 17.0.2273547 (EditorServices Host v17.0.34.45391)
  • VS Code: stable, Linux x64
  • OS: Arch Linux (reproduced generally: any Linux where the extension host CWD is $HOME)
  • Server: BC 28.1 OnPrem
    Internal work item: AB#638489

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions