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
- Linux, AL extension 17.0.2273547 (EditorServices Host v17.0.34.45391), any OnPrem launch config with
"request": "attach", project without al.packageCachePath set.
- Launch VS Code so its CWD is
$HOME (the default for desktop launchers and code from ~).
- Start the attach config, sign in to the web client, hit a breakpoint.
- 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
Describe the issue
On Linux, AL debugging (both
attachandlaunch) against an OnPrem server is effectively broken in real VS Code, while the very same debug adapter binary works fine when driven headlessly. Symptoms:attachtakes 15ΓÇô30 seconds to complete (instead of <1 s).stackTracerequest gets no response (observed >3 minutes with none).DebuggerServices.logfills with:Root cause
Microsoft.Dynamics.Nav.EditorServices.Protocol.dll,SettingsExtensions.GetPackageCachePaths:The debug adapter is a separate process from the language server and never receives
al.packageCachePath: no CLI argument carries it (getLanguageServerOptionsin the extension does not emit it) and the extension never sendsal/setActiveWorkspaceover the DAP channel. So during a debug session this fallback always fires.The extension spawns the adapter via
new DebugAdapterExecutable(path, args)without acwd, 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*.appscan triggered by the firststackTrace-induced compilation (DebugAdapterStackTraceHandler→Workspace.CurrentSolution.GetCompilationAsync→MovedSymbolsPackageCacheLoader.LoadMovedObjectsManifests) and duringattachprocessing 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
"request": "attach", project withoutal.packageCachePathset.$HOME(the default for desktop launchers andcodefrom~).DebuggerServices.log.Control experiment: spawning the identical adapter binary with the project folder as CWD makes attach complete in ~1 s and
stackTracerespond in ~3 s ΓÇö confirming the CWD fallback is the trigger.attachstackTrace$HOME(what VS Code uses on Linux)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
cwdwhen spawning the adapter. InAlDebugAdapterDescriptorFactory.createDescriptor, the workspace folder is already in scope (it is used for/projectRoot:). Add theDebugAdapterExecutableOptionsthird argument: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,stackTraceresponds 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 thedebuggingcontextsession 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 whensettings.WorkspacePathis empty, so a workspace path is guaranteed at the fallback site. Default to the documented default ofal.packageCachePath(./.alpackagesrelative to the project) instead of the process CWD:(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.packageCachePathto the debug adapter. Two mechanisms already exist and just aren't wired up:getLanguageServerOptions()already serializes comparable settings (e.g.al.testCoverageCachePath→/testCoverageCachePath:); emit/packageCachePath:the same way and populateSettings.AlResourceConfigurationSettings.PackageCachePathsfrom it in the host.SetActiveWorkspaceRequestHandler(al/setActiveWorkspace) is registered byChannelBase, so the debug adapter can already receive full workspace settings — the extension simply never sends that request on debug sessions. Sending it right afterinitialize(beforeattach/launch) would populate the settings ahead of the first compilation.4. Hardening — make the scan cycle-safe regardless of root. The recursive
*.appenumeration reached fromMovedSymbolsPackageCacheLoader.LoadMovedObjectsManifestsandReferenceLoaderFactory.CreateReferenceLoader(viaIFileSystem.GetFiles(cachePath, "*.app")) should enumerate with: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/CreateSourceAsyncput raw machine-local filesystem paths into DAPSource.path—location.GetLineSpan().Pathfor in-project frames, andFilePathHelper.GetPreviewFileUri(project.ProjectFolder, …)withOrigin = "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
$HOME)Internal work item: AB#638489