diff --git a/daemon/launcher/cli_parse.hpp b/daemon/launcher/cli_parse.hpp index f1bc7b2..b289169 100644 --- a/daemon/launcher/cli_parse.hpp +++ b/daemon/launcher/cli_parse.hpp @@ -21,6 +21,7 @@ struct TraceArgs { std::vector passes; bool verbose = false; // -v bool quiet = false; // -q + bool no_source = false; // --no-source: explicit source-content opt-out bool upload = false; // --upload: start gpufl-agent for live upload std::string backend_url; // --backend-url; else GPUFL_BACKEND_URL std::string api_key; // --api-key; else GPUFL_API_KEY diff --git a/daemon/launcher/cli_trace_options.cpp b/daemon/launcher/cli_trace_options.cpp index 62cda68..e295db8 100644 --- a/daemon/launcher/cli_trace_options.cpp +++ b/daemon/launcher/cli_trace_options.cpp @@ -442,6 +442,12 @@ const CliOptionManager& traceOptions() { "monitoring-only telemetry. PcSampling / PM / Range passes may " "need NVIDIA performance-counter access.", kSection(TraceHelpSection::Capture), &parsePasses) + .add({"--no-source"}, "", + "Do not capture discovered CUDA source file content. Source " + "capture is enabled by default for analysis and source-line " + "correlation", + kSection(TraceHelpSection::Capture), + &setFlag<&TraceArgs::no_source>) // Runtime .add({"-q", "--quiet"}, "", diff --git a/daemon/launcher/trace_command_common.cpp b/daemon/launcher/trace_command_common.cpp index c5a7496..be2318d 100644 --- a/daemon/launcher/trace_command_common.cpp +++ b/daemon/launcher/trace_command_common.cpp @@ -747,6 +747,8 @@ int runTraceCommon(const TraceArgs& args, const TracePlatform& platform) { !setEnvOrPrint(platform, env::kAppName, app_name) || !setEnvOrPrint(platform, env::kLogDir, output_dir.string()) || !setEnvOrPrint(platform, env::kInjectProfile, inject::kProfileComprehensive) || + !setEnvOrPrint(platform, env::kIncludeSource, + args.no_source ? "0" : "1") || !setEnvOrPrint(platform, env::kInjectUpload, "0")) { return 2; } @@ -818,6 +820,13 @@ int runTraceCommon(const TraceArgs& args, const TracePlatform& platform) { if (!args.quiet) { std::fprintf(stderr, "[gpufl] capturing -> %s\n", output_dir.string().c_str()); + if (!args.no_source && args.upload) { + std::fprintf( + stderr, + "[gpufl] source upload enabled: discovered source file " + "content will be stored in this trace and uploaded; use " + "--no-source to exclude it\n"); + } if (args.deep_requested) { const AdaptiveCapturePlan adaptive = resolveAdaptivePlan(args); std::fprintf(stderr, "[gpufl] adaptive capture: base=%s; selected deep=", diff --git a/include/gpufl/core/env_vars.hpp b/include/gpufl/core/env_vars.hpp index fc88715..b37d21f 100644 --- a/include/gpufl/core/env_vars.hpp +++ b/include/gpufl/core/env_vars.hpp @@ -82,6 +82,10 @@ constexpr const char* kExpectNoKernelEvents = "GPUFL_EXPECT_NO_KERNEL_EVENTS"; // injected target - same switch InitOptions.enable_debug_output exposes to // embedding callers, but reachable for `gpufl trace` runs. constexpr const char* kDebugOutput = "GPUFL_DEBUG"; +// Launcher-owned source-capture contract. `gpufl trace` always publishes an +// explicit value: "1" by default, or "0" for --no-source. Applying it after +// file configuration makes the command-line privacy choice authoritative. +constexpr const char* kIncludeSource = "GPUFL_INCLUDE_SOURCE"; // Override the per-file log rotation threshold in bytes (default: // Logger::kDefaultRotateBytes = 64 MiB). Mainly for tests - a tiny value diff --git a/include/gpufl/core/startup_configuration.cpp b/include/gpufl/core/startup_configuration.cpp index e30a4c7..8bf3354 100644 --- a/include/gpufl/core/startup_configuration.cpp +++ b/include/gpufl/core/startup_configuration.cpp @@ -64,6 +64,14 @@ void resolveStartupOptions(InitOptions& options) { ConfigFileLoader::apply(options, config_path); } + // The trace launcher owns this explicit source-capture decision and always + // sets 1 or 0. Apply it after the config file so --no-source cannot be + // silently undone by inherited target configuration. Embedded users that + // do not set the variable retain their InitOptions behavior. + if (const char* value = std::getenv(env::kIncludeSource)) { + options.enable_source_collection = std::strcmp(value, "1") == 0; + } + std::string api_path = options.api_path; if (api_path.empty()) { if (const char* value = std::getenv(env::kApiPath)) { diff --git a/tests/core/test_startup_configuration.cpp b/tests/core/test_startup_configuration.cpp index 97bc911..85906c7 100644 --- a/tests/core/test_startup_configuration.cpp +++ b/tests/core/test_startup_configuration.cpp @@ -33,6 +33,7 @@ class StartupConfigurationTest : public testing::Test { void SetUp() override { saveAndUnset_(gpufl::env::kConfigFile, config_file_); saveAndUnset_(gpufl::env::kApiPath, api_path_); + saveAndUnset_(gpufl::env::kIncludeSource, include_source_); saveAndUnset_(gpufl::env::kRunId, run_id_); saveAndUnset_(gpufl::env::kSegmentEveryMs, segment_every_ms_); saveAndUnset_(gpufl::env::kSegmentMaxRows, segment_max_rows_); @@ -43,6 +44,7 @@ class StartupConfigurationTest : public testing::Test { void TearDown() override { restore_(gpufl::env::kConfigFile, config_file_); restore_(gpufl::env::kApiPath, api_path_); + restore_(gpufl::env::kIncludeSource, include_source_); restore_(gpufl::env::kRunId, run_id_); restore_(gpufl::env::kSegmentEveryMs, segment_every_ms_); restore_(gpufl::env::kSegmentMaxRows, segment_max_rows_); @@ -64,6 +66,7 @@ class StartupConfigurationTest : public testing::Test { std::optional config_file_; std::optional api_path_; + std::optional include_source_; std::optional run_id_; std::optional segment_every_ms_; std::optional segment_max_rows_; @@ -103,6 +106,41 @@ TEST_F(StartupConfigurationTest, ApiPathEnvironmentFallbackIsNormalized) { EXPECT_EQ(options.api_path, "/environment/v1"); } +TEST_F(StartupConfigurationTest, IncludeSourceEnvironmentEnablesCollection) { + setEnv(gpufl::env::kIncludeSource, "1"); + + gpufl::InitOptions options; + options.enable_source_collection = false; + gpufl::detail::resolveStartupOptions(options); + + EXPECT_TRUE(options.enable_source_collection); +} + +TEST_F(StartupConfigurationTest, IncludeSourceEnvironmentOverridesConfigFile) { + const auto temp_root = + std::filesystem::temp_directory_path() / + ("gpufl_source_privacy_config_" + + std::to_string(gpufl::detail::GetPid())); + const auto config_path = temp_root / "config.json"; + std::error_code ec; + std::filesystem::remove_all(temp_root, ec); + std::filesystem::create_directories(temp_root, ec); + ASSERT_FALSE(ec) << ec.message(); + { + std::ofstream config(config_path); + ASSERT_TRUE(config); + config << R"({"enable_source_collection":true})"; + } + setEnv(gpufl::env::kIncludeSource, "0"); + + gpufl::InitOptions options; + options.config_file = config_path.string(); + gpufl::detail::resolveStartupOptions(options); + + EXPECT_FALSE(options.enable_source_collection); + std::filesystem::remove_all(temp_root, ec); +} + TEST_F(StartupConfigurationTest, ValidSegmentationAndRolloverAreReadTogether) { setEnv(gpufl::env::kRunId, "12345678-1234-4123-8123-123456789abc"); setEnv(gpufl::env::kSegmentEveryMs, "60000"); diff --git a/tests/launcher/test_cli_parse.cpp b/tests/launcher/test_cli_parse.cpp index bbff8ed..9dad71f 100644 --- a/tests/launcher/test_cli_parse.cpp +++ b/tests/launcher/test_cli_parse.cpp @@ -286,6 +286,17 @@ TEST(CliParseTrace, VerboseAndQuiet) { EXPECT_TRUE(r.args->quiet); } +TEST(CliParseTrace, SourceCaptureDefaultsOnAndCanBeDisabled) { + auto defaults = parseTraceArgs(argsFor({"--", "./bin"})); + ASSERT_TRUE(defaults.args.has_value()) << defaults.error; + EXPECT_FALSE(defaults.args->no_source); + + auto opted_out = parseTraceArgs( + argsFor({"--no-source", "--", "./bin"})); + ASSERT_TRUE(opted_out.args.has_value()) << opted_out.error; + EXPECT_TRUE(opted_out.args->no_source); +} + TEST(CliParseTrace, BooleanFlagsRejectInlineValues) { const auto r = parseTraceArgs( argsFor({"--verbose=true", "--", "./bin"})); @@ -297,6 +308,7 @@ TEST(CliParseHelp, TraceSimpleOptionsComeFromTheRegistry) { const std::string help = traceHelp(); EXPECT_NE(help.find("-n, --name="), std::string::npos); EXPECT_NE(help.find("--backend-url="), std::string::npos); + EXPECT_NE(help.find("--no-source"), std::string::npos); EXPECT_NE(help.find("GPUFL_BACKEND_URL"), std::string::npos); } diff --git a/tests/launcher/test_trace_run_plan.cpp b/tests/launcher/test_trace_run_plan.cpp index 6d8c34a..7b3585e 100644 --- a/tests/launcher/test_trace_run_plan.cpp +++ b/tests/launcher/test_trace_run_plan.cpp @@ -177,6 +177,31 @@ TEST(TraceRunPlanTest, TraceCommonExecutesThePlannedSinglePass) { EXPECT_EQ(platform.env[gpufl::env::kAppName], "trace-plan-test"); EXPECT_EQ(platform.env[gpufl::env::kLogDir], args.output_dir); EXPECT_EQ(platform.env[gpufl::env::kProfilingEngine], "Trace"); + EXPECT_EQ(platform.env[gpufl::env::kIncludeSource], "1"); + + fs::remove_all(root, ec); +} + +TEST(TraceRunPlanTest, TraceCommonHonorsExplicitSourceCaptureOptOut) { + namespace fs = std::filesystem; + const fs::path root = fs::temp_directory_path() / + ("gpufl_trace_source_privacy_" + + std::to_string(gpufl::detail::GetPid())); + std::error_code ec; + fs::remove_all(root, ec); + fs::create_directories(root, ec); + ASSERT_FALSE(ec) << ec.message(); + { std::ofstream inject(root / "gpufl_inject.so"); ASSERT_TRUE(inject); } + + ExecutingPlanningPlatform platform(root); + platform.env[gpufl::env::kIncludeSource] = "1"; + TraceArgs args; + args.command = {"target"}; + args.output_dir = (root / "capture").string(); + args.no_source = true; + + EXPECT_EQ(gpufl::launcher::runTraceCommon(args, platform), 0); + EXPECT_EQ(platform.env[gpufl::env::kIncludeSource], "0"); fs::remove_all(root, ec); }