diff --git a/Extension/.vscode/launch.json b/Extension/.vscode/launch.json index a110f3407..6547573af 100644 --- a/Extension/.vscode/launch.json +++ b/Extension/.vscode/launch.json @@ -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/. (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", diff --git a/Extension/readme.developer.md b/Extension/readme.developer.md index 678bc32c8..0f330e156 100644 --- a/Extension/readme.developer.md +++ b/Extension/readme.developer.md @@ -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,37 @@ 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`/`-asan`/`-ubsan` CMake preset that +enables ThreadSanitizer, AddressSanitizer, or UndefinedBehaviorSanitizer), 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 +`/.` 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: +cat /tmp/cpptools-san/tsan.* +``` + +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. diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 3410e7502..101e04a9b 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -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 @@ -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 +// "/." 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 "/." 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 "." 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();