Skip to content
Open
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
25 changes: 25 additions & 0 deletions Extension/.vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
34 changes: 34 additions & 0 deletions Extension/readme.developer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.

Expand Down
50 changes: 48 additions & 2 deletions Extension/src/LanguageServer/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1683,9 +1683,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
Expand Down Expand Up @@ -4419,6 +4423,48 @@ 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.
[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>();
Expand Down
Loading