Add an opt-in for capturing sanitizer logs from the language server (#14573)
* Add an opt-in for capturing sanitizer logs from the language server
This commit is contained in:
Vendored
+25
@@ -22,6 +22,31 @@
|
||||
// you can use a watch task as a prelaunch task and it works like you'd want it to.
|
||||
"preLaunchTask": "watch"
|
||||
},
|
||||
{
|
||||
// debugs the extension with sanitizer (TSan/ASan/UBSan) reports captured to files.
|
||||
// Requires a sanitizer build of the cpptools language server. Each process writes
|
||||
// ${userHome}/cpptools-sanitizer-logs/<sanitizer>.<pid> (see readme.developer.md).
|
||||
"name": "Run Extension (capture sanitizer logs)",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"env": {
|
||||
"CPPTOOLS_SANITIZER_LOG_DIR": "${userHome}/cpptools-sanitizer-logs"
|
||||
},
|
||||
"args": [
|
||||
"--no-sandbox",
|
||||
"--disable-updates",
|
||||
"--skip-welcome",
|
||||
"--skip-release-notes",
|
||||
"--disable-workspace-trust",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/dist/**"
|
||||
],
|
||||
// you can use a watch task as a prelaunch task and it works like you'd want it to.
|
||||
"preLaunchTask": "watch"
|
||||
},
|
||||
{
|
||||
// debugs the extension (selecting the workspace)
|
||||
"name": "Run Extension-Select Workspace",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
- [From Inside VS Code](#from-inside-vs-code)
|
||||
- [From Command line](#from-command-line)
|
||||
- [Use of an isolated VS Code environment](#use-of-an-isolated-vs-code-environment)
|
||||
- [Capturing sanitizer diagnostics from the language server](#capturing-sanitizer-diagnostics-from-the-language-server)
|
||||
- [Testing](#testing)
|
||||
- [Unit tests](#unit-tests)
|
||||
- [Scenario Tests](#scenario-tests)
|
||||
@@ -99,6 +100,39 @@ your normal VS Code environment.
|
||||
> When debugging the scenario tests from VS Code, it has to use the same VS Code binary
|
||||
> as the debugger instance, so the isolated environment can't be used.
|
||||
|
||||
## Capturing sanitizer diagnostics from the language server
|
||||
|
||||
When you run a sanitizer build of the language server (a `-tsan` or `-asan-ubsan` CMake preset --
|
||||
ThreadSanitizer, or AddressSanitizer + UndefinedBehaviorSanitizer combined; on Windows use
|
||||
`windows-x64-asan`, ASan only), the sanitizer prints its
|
||||
reports to `stderr`. Because the extension talks to the language server over `stdio`, those reports are
|
||||
easy to miss, and the exit-time backtrace you see in a crash log only shows the sanitizer shutting
|
||||
down -- not the actual report.
|
||||
|
||||
To capture the reports, set the `CPPTOOLS_SANITIZER_LOG_DIR` environment variable before launching
|
||||
VS Code (or add it to the `env` of the launch config that starts the extension). The extension then
|
||||
routes each sanitizer's `log_path` into that directory, so every process -- `cpptools` and the
|
||||
`cpptools-srv`/`cpptools-srv2` children it spawns, which inherit the environment -- writes its own
|
||||
`<dir>/<sanitizer>.<pid>` file (for example `tsan.12345`). Any `TSAN_OPTIONS`/`ASAN_OPTIONS`/
|
||||
`UBSAN_OPTIONS` you already set are preserved.
|
||||
|
||||
```bash
|
||||
# macOS/Linux
|
||||
export CPPTOOLS_SANITIZER_LOG_DIR=/tmp/cpptools-san
|
||||
# ...launch VS Code with a sanitizer build of cpptools, reproduce the issue, then read the logs.
|
||||
# A -tsan build writes tsan.<pid>; a combined -asan-ubsan build writes both asan.<pid> and ubsan.<pid>.
|
||||
cat /tmp/cpptools-san/*
|
||||
```
|
||||
|
||||
The **Run Extension (capture sanitizer logs)** configuration in `.vscode/launch.json` sets this
|
||||
variable for you (to `${userHome}/cpptools-sanitizer-logs`), so you can just pick it from the Run
|
||||
and Debug dropdown instead of exporting the variable yourself.
|
||||
|
||||
The variable is opt-in: when it is unset (normal development, CI, released builds) the child
|
||||
environment is inherited unchanged, so there is no behavior change. No debugger is required -- and
|
||||
because the sanitizer writes to a file, you avoid the shutdown-time hangs that can occur when a
|
||||
debugger is attached to a sanitizer process as it exits.
|
||||
|
||||
## Testing
|
||||
The test architecture has been reorganized and the layout refactored a bit.
|
||||
|
||||
|
||||
@@ -1689,9 +1689,13 @@ export class DefaultClient implements Client {
|
||||
throw String('Missing binary at ' + serverModule);
|
||||
}
|
||||
const serverName: string = this.getName(this.rootFolder);
|
||||
// Opt-in: when CPPTOOLS_SANITIZER_LOG_DIR is set, route sanitizer (TSan/ASan/UBSan) reports
|
||||
// from a sanitizer build to files in that directory (see getSanitizerServerEnv). This is
|
||||
// undefined -- i.e. the environment is inherited unchanged -- for normal builds.
|
||||
const sanitizerServerEnv: NodeJS.ProcessEnv | undefined = getSanitizerServerEnv();
|
||||
const serverOptions: ServerOptions = {
|
||||
run: { command: serverModule, options: { detached: false, cwd: util.getExtensionFilePath("bin") } },
|
||||
debug: { command: serverModule, args: [serverName], options: { detached: true, cwd: util.getExtensionFilePath("bin") } }
|
||||
run: { command: serverModule, options: { detached: false, cwd: util.getExtensionFilePath("bin"), env: sanitizerServerEnv } },
|
||||
debug: { command: serverModule, args: [serverName], options: { detached: true, cwd: util.getExtensionFilePath("bin"), env: sanitizerServerEnv } }
|
||||
};
|
||||
|
||||
// The IntelliSense process should automatically detect when AutoPCH is
|
||||
@@ -4419,6 +4423,56 @@ function getLanguageServerFileName(): string {
|
||||
return path.resolve(util.getExtensionFilePath("bin"), extensionProcessName);
|
||||
}
|
||||
|
||||
// Opt-in helper for capturing sanitizer (TSan/ASan/UBSan) diagnostics from a sanitizer build of
|
||||
// the language server. The sanitizers print their reports to stderr, which the language-server
|
||||
// stdio can swallow. When the CPPTOOLS_SANITIZER_LOG_DIR environment variable is set, this routes
|
||||
// each sanitizer's log_path into that directory so every process -- cpptools and its
|
||||
// cpptools-srv/-srv2 children, which inherit this environment -- writes its own
|
||||
// "<dir>/<sanitizer>.<pid>" file. Any *SAN_OPTIONS the developer already set are preserved.
|
||||
// Returns undefined when the variable is unset, leaving the child environment inherited unchanged,
|
||||
// so this is a no-op for normal builds and safe to leave checked in. To use it, build a sanitizer
|
||||
// preset of the language server, set CPPTOOLS_SANITIZER_LOG_DIR before launching VS Code (or add it
|
||||
// to the launch config's "env"), reproduce, then read the "<dir>/<sanitizer>.<pid>" files.
|
||||
function getSanitizerServerEnv(): NodeJS.ProcessEnv | undefined {
|
||||
const logDirectoryEnv: string | undefined = process.env.CPPTOOLS_SANITIZER_LOG_DIR;
|
||||
if (!logDirectoryEnv) {
|
||||
return undefined;
|
||||
}
|
||||
// The language server is spawned with cwd set to the "bin" directory (see the ServerOptions
|
||||
// above), so the sanitizer runtime would interpret a relative log_path relative to "bin".
|
||||
// Resolve against that same directory here so the directory we create and the path we hand the
|
||||
// sanitizer always agree, and so a relative CPPTOOLS_SANITIZER_LOG_DIR still works.
|
||||
const logDirectory: string = path.resolve(util.getExtensionFilePath("bin"), logDirectoryEnv);
|
||||
// The sanitizer runtime opens "<log_path>.<pid>" and does not create missing directories, so
|
||||
// ensure the directory exists (best effort). If it can't be created the sanitizer just falls
|
||||
// back to stderr.
|
||||
try {
|
||||
fs.mkdirSync(logDirectory, { recursive: true });
|
||||
} catch {
|
||||
// Not fatal -- reports will go to stderr instead.
|
||||
}
|
||||
const withLogPath = (existingOptions: string | undefined, sanitizer: string): string =>
|
||||
// The sanitizer runtime flag parser treats a space (as well as ',', ':', tab, and newline)
|
||||
// as a delimiter between key=value pairs, so a single space is a valid, cross-platform
|
||||
// separator here. Do not use path.delimiter (';' on Windows), which the parser does NOT
|
||||
// treat as a delimiter and which would break parsing for the Windows ASan preset.
|
||||
//
|
||||
// The colon delimiter also matters *inside* the value: a Windows absolute path begins with a
|
||||
// drive letter and colon (e.g. C:\...), so an unquoted log_path=C:\... parses as log_path=C
|
||||
// followed by a stray \... token, and the runtime aborts at startup with
|
||||
// "expected '=' in ASAN_OPTIONS" (exit 1) before any code runs. Wrap the value in single
|
||||
// quotes: the flag parser reads a quoted value verbatim up to the closing quote, so the
|
||||
// embedded colon is preserved. Quoting is harmless on Linux/macOS (the parser strips the
|
||||
// quotes), so it is applied unconditionally rather than only on Windows.
|
||||
[existingOptions, `log_path='${path.join(logDirectory, sanitizer)}'`].filter(Boolean).join(" ");
|
||||
return {
|
||||
...process.env,
|
||||
TSAN_OPTIONS: withLogPath(process.env.TSAN_OPTIONS, "tsan"),
|
||||
ASAN_OPTIONS: withLogPath(process.env.ASAN_OPTIONS, "asan"),
|
||||
UBSAN_OPTIONS: withLogPath(process.env.UBSAN_OPTIONS, "ubsan")
|
||||
};
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
class NullClient implements Client {
|
||||
private booleanEvent = new vscode.EventEmitter<boolean>();
|
||||
|
||||
Reference in New Issue
Block a user