Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
10 changes: 10 additions & 0 deletions sdk/compiler/daml-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@
"type": "boolean",
"default": true,
"description": "Use the new DPM assistant over the legacy Daml Assistant if it is installed."
},
"daml.keepAliveInterval": {
"type": "number",
"default": 60000,
"description": "How often (in milliseconds) the extension pings the Daml language server to check that it is responsive."
},
"daml.keepAliveTimeout": {
"type": "number",
"default": 120000,
"description": "How long (in milliseconds) the extension will wait for the Daml language server to respond to a keep-alive ping or produce any other output before restarting it. Increase this on very large workspaces where individual requests can legitimately take longer than the default."
}
}
},
Expand Down
55 changes: 47 additions & 8 deletions sdk/compiler/daml-extension/src/language_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,20 @@ export class DamlLanguageClient {
// to requests in a timely manner. If the server fails to respond it is
// terminated with SIGTERM.
private keepAliveTimer: ReturnType<typeof setInterval> | undefined;
private keepAliveInterval = 60000; // Send KA every 60s.

// Wait for max 120s before restarting process.
// NOTE(JM): If you change this, make sure to also change the server-side timeouts to get
// detailed errors rather than cause a restart.
// Legacy Daml timeout for language server is defined in
// DA.Daml.LanguageServer.
private keepAliveTimeout = 120000;
// Interval between keep-alive pings, and how long we'll wait for a
// reply (or any other server output) before assuming the process has
// hung. Read from settings so users with large workspaces can extend
// them; defaults preserve the previous 60s / 120s behaviour.
private keepAliveInterval: number;
private keepAliveTimeout: number;

// Milliseconds-since-epoch timestamp of the last observed activity
// from the language server (either a keep-alive reply or anything
// written to stdout/stderr). Used as a secondary liveness signal:
// if the server is producing output it's alive even if it's currently
// too busy to answer the keep-alive ping in time.
private lastServerActivity: number = Date.now();

static async build(
rootPath: string,
Expand Down Expand Up @@ -122,6 +128,13 @@ export class DamlLanguageClient {
this.languageClient.registerProposedFeatures();
this.isMultiIde = multiIdeSupport;

const damlConfig = vscode.workspace.getConfiguration("daml");
this.keepAliveInterval = damlConfig.get<number>(
"keepAliveInterval",
60000,
);
this.keepAliveTimeout = damlConfig.get<number>("keepAliveTimeout", 120000);

this.virtualResourceManager = new VirtualResourceManager(
this.languageClient,
this.webviewFiles,
Expand All @@ -130,6 +143,7 @@ export class DamlLanguageClient {
this.context.subscriptions.push(this.virtualResourceManager);

this.languageClient.start().then(() => {
this.observeServerActivity();
this.startKeepAliveWatchdog();
this.languageClient.onNotification(
DamlVirtualResourceDidChangeNotification.type,
Expand Down Expand Up @@ -441,17 +455,41 @@ export class DamlLanguageClient {
if (this.keepAliveTimer) clearTimeout(this.keepAliveTimer);
}

// Subscribe to the server process's stdout/stderr so any output the
// server produces refreshes `lastServerActivity`. That lets us
// distinguish "damlc is hung" from "damlc is busy"
private observeServerActivity() {
const proc = (<any>this.languageClient)._childProcess;
if (!proc) return;
const bump = () => {
this.lastServerActivity = Date.now();
};
if (proc.stdout) proc.stdout.on("data", bump);
if (proc.stderr) proc.stderr.on("data", bump);
}

private keepAlive(languageClient: LanguageClient) {
let self = this;
function killDamlc() {
// If we've observed any output from the server within the
// keep-alive timeout window, treat it as alive-but-busy and
// reschedule instead of restarting.
if (Date.now() - self.lastServerActivity < self.keepAliveTimeout) {
self.startKeepAliveWatchdog();
return;
}

vscode.window.showErrorMessage(
"Sorry, you’ve hit a bug requiring a Daml Language Server restart. We’d greatly appreciate a bug report — ideally with example files.",
);

// Terminate the damlc process with SIGTERM. The language client will restart the process automatically.
// NOTE(JM): Verify that this works on Windows.
// https://nodejs.org/api/child_process.html#child_process_child_kill_signal
(<any>languageClient)._childProcess.kill("SIGTERM");
const proc = (<any>languageClient)._childProcess;
if (proc && typeof proc.kill === "function") {
proc.kill("SIGTERM");
}
Comment on lines +504 to +506

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Killing processes here is tricky, since we support many platforms. Which platform have you tried this on? Are you able to verify this works in Windows?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, this is what it was already doing to try to kill the process, but you're right that it's suspicious and we should check this. I don't have a Windows machine to hand, but I'll get one of my coworkers to try it.

@cgibbard cgibbard Jul 24, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://nodejs.org/api/child_process.html#subprocesskillsignal seems to indicate that on Windows, the kill method will kill the process forcefully when given any of 'SIGKILL', 'SIGTERM', 'SIGINT' or 'SIGQUIT', which perhaps is less nice than it ought to be (might not give the murdered process a chance to clean up subprocesses...), but I'm not certain yet what to do about that, if anything.

For fun, I also tracked down where this actually happens. Node delegates to the libuv library to abstract over OS-specific things, and ultimately ends up in uv__kill, here


// Restart the watchdog after 10s
setTimeout(self.startKeepAliveWatchdog, 10000);
Expand All @@ -462,6 +500,7 @@ export class DamlLanguageClient {
// Keep-alive request succeeded, clear the kill timer
// and reschedule the keep-alive.
clearTimeout(killTimer);
this.lastServerActivity = Date.now();
this.startKeepAliveWatchdog();
});
}
Expand Down
Loading