tp: publish completed bundles atomically Write bundles through a base AtomicFile helper and publish with Commit() && only after the archive is complete and flushed. Preserve existing output on failure, reject identical input/output files, and clean up temporary files on ordinary failures without changing signal or subprocess handling.
diff --git a/Android.bp b/Android.bp index e7e6672..62233d8 100644 --- a/Android.bp +++ b/Android.bp
@@ -15936,6 +15936,7 @@ name: "perfetto_src_base_base", srcs: [ "src/base/android_utils.cc", + "src/base/atomic_file.cc", "src/base/base64.cc", "src/base/cpu_info.cc", "src/base/crash_keys.cc", @@ -16052,6 +16053,7 @@ filegroup { name: "perfetto_src_base_unittests", srcs: [ + "src/base/atomic_file_unittest.cc", "src/base/base64_unittest.cc", "src/base/bits_unittest.cc", "src/base/circular_queue_unittest.cc",
diff --git a/BUILD b/BUILD index 6e664d7..3addae2 100644 --- a/BUILD +++ b/BUILD
@@ -1405,6 +1405,7 @@ name = "include_perfetto_ext_base_base", srcs = [ "include/perfetto/ext/base/android_utils.h", + "include/perfetto/ext/base/atomic_file.h", "include/perfetto/ext/base/base64.h", "include/perfetto/ext/base/bits.h", "include/perfetto/ext/base/circular_queue.h", @@ -1962,6 +1963,7 @@ srcs = [ ":src_base_check_cpu_optimizations", "src/base/android_utils.cc", + "src/base/atomic_file.cc", "src/base/base64.cc", "src/base/cpu_info.cc", "src/base/crash_keys.cc",
diff --git a/docs/learning-more/symbolization.md b/docs/learning-more/symbolization.md index f0eeaf1..2eaa528 100644 --- a/docs/learning-more/symbolization.md +++ b/docs/learning-more/symbolization.md
@@ -122,7 +122,7 @@ suppress it when running in a terminal too. See the [bundle command reference](/docs/reference/trace-processor-cli.md#subcommand-bundle) -for option semantics, color controls, and exit status. +for option semantics, color controls, output replacement, and exit status. ### {#option-2-legacy-traceconv-symbolize-deobfuscate} Option 2: Legacy `trace_processor util symbolize` / `util deobfuscate`
diff --git a/docs/reference/trace-processor-cli.md b/docs/reference/trace-processor-cli.md index 5c90ab6..5b31de0 100644 --- a/docs/reference/trace-processor-cli.md +++ b/docs/reference/trace-processor-cli.md
@@ -293,9 +293,13 @@ | Argument | Meaning | | --- | --- | -| `input` | Input trace file path. Stdin is not supported. | +| `input` | Existing regular trace file. Stdin is not supported. | | `output` | Destination file path. Stdout is not supported. Its parent directory must exist and be writable. | +Input and output must refer to different files, including through hard links. +An existing output must be a regular file. Output symlinks are rejected; specify +the target path directly. + #### Options - `--symbol-paths PATH1,PATH2,...`: additional directories to search for native @@ -350,6 +354,18 @@ directories containing the matching unstripped or debug binaries rather than mixing stripped and unstripped copies. Use `--verbose` to inspect lookup details. +#### Output replacement and cleanup + +The command writes a temporary file beside the destination. It replaces the +destination only after successfully writing and flushing the complete bundle. +An existing output is preserved if reading, enrichment, or writing fails. + +Ordinary failures remove the temporary file on cleanup. Abrupt termination, +including Ctrl-C, or a cleanup failure can leave a sibling file named +`<output>.tmp.<uuid>`. Cleanup is best effort; incomplete data is never published +as the destination. A leftover temporary file can be deleted once the process +has stopped. + #### Exit status A successfully written bundle exits with status 0, including when some symbols
diff --git a/include/perfetto/ext/base/BUILD.gn b/include/perfetto/ext/base/BUILD.gn index 9fc0876..3953a66 100644 --- a/include/perfetto/ext/base/BUILD.gn +++ b/include/perfetto/ext/base/BUILD.gn
@@ -17,6 +17,7 @@ source_set("base") { sources = [ "android_utils.h", + "atomic_file.h", "base64.h", "bits.h", "circular_queue.h",
diff --git a/include/perfetto/ext/base/atomic_file.h b/include/perfetto/ext/base/atomic_file.h new file mode 100644 index 0000000..7f3aadc --- /dev/null +++ b/include/perfetto/ext/base/atomic_file.h
@@ -0,0 +1,52 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef INCLUDE_PERFETTO_EXT_BASE_ATOMIC_FILE_H_ +#define INCLUDE_PERFETTO_EXT_BASE_ATOMIC_FILE_H_ + +#include <string> +#include "perfetto/base/status.h" +#include "perfetto/ext/base/scoped_file.h" + +namespace perfetto::base { + +// Writes a sibling temporary file and atomically replaces the destination on +// Commit(). Until then an existing destination is untouched. Does not follow +// destination symlinks. An uncommitted temporary file is deleted on +// destruction. +class AtomicFile { + public: + explicit AtomicFile(const std::string& destination); + ~AtomicFile(); + AtomicFile(const AtomicFile&) = delete; + AtomicFile& operator=(const AtomicFile&) = delete; + + Status Open(); + ScopedFile DuplicateFD() const; + // Final operation: close all DuplicateFD() handles before committing. + // On failure, destruction still attempts to remove the temporary file. + Status Commit() &&; + const std::string& temp_path() const { return temp_path_; } + + private: + const std::string destination_; + const std::string temp_path_; + ScopedFile fd_; + bool owns_temp_file_ = false; +}; + +} // namespace perfetto::base +#endif // INCLUDE_PERFETTO_EXT_BASE_ATOMIC_FILE_H_
diff --git a/include/perfetto/ext/base/file_utils.h b/include/perfetto/ext/base/file_utils.h index a5a8353..ca8bc01 100644 --- a/include/perfetto/ext/base/file_utils.h +++ b/include/perfetto/ext/base/file_utils.h
@@ -104,6 +104,11 @@ // Duplicates an open descriptor. The duplicate is not inherited across exec. ScopedFile DupFile(int fd); +// These queries follow symbolic links. False also means a path could not be +// inspected. IsSameFile compares file identities, so it detects hard links. +bool IsRegularFile(const std::string& path); +bool IsSameFile(const std::string& first, const std::string& second); + // Moves the file offset to |offset| bytes from the beginning of the file. // Returns false if |offset| cannot be represented by the platform or the seek // fails.
diff --git a/src/base/BUILD.gn b/src/base/BUILD.gn index 8866d32..a5b4920 100644 --- a/src/base/BUILD.gn +++ b/src/base/BUILD.gn
@@ -35,6 +35,7 @@ ] sources = [ "android_utils.cc", + "atomic_file.cc", "base64.cc", "cpu_info.cc", "crash_keys.cc", @@ -228,6 +229,7 @@ } sources = [ + "atomic_file_unittest.cc", "base64_unittest.cc", "bits_unittest.cc", "circular_queue_unittest.cc",
diff --git a/src/base/atomic_file.cc b/src/base/atomic_file.cc new file mode 100644 index 0000000..88501fc --- /dev/null +++ b/src/base/atomic_file.cc
@@ -0,0 +1,115 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "perfetto/ext/base/atomic_file.h" + +#include <cerrno> +#include <cstdio> +#include <cstring> + +#include "perfetto/base/build_config.h" +#include "perfetto/base/compiler.h" +#include "perfetto/base/logging.h" +#include "perfetto/ext/base/file_utils.h" +#include "perfetto/ext/base/uuid.h" + +#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) +#include <io.h> +#include <windows.h> +#else +#include <sys/stat.h> +#include <unistd.h> +#endif + +namespace perfetto::base { + +AtomicFile::AtomicFile(const std::string& destination) + : destination_(destination), + temp_path_(destination + ".tmp." + Uuidv4().ToPrettyString()) {} + +AtomicFile::~AtomicFile() { + fd_.reset(); + if (owns_temp_file_) + Unlink(temp_path_.c_str()); +} + +base::Status AtomicFile::Open() { + PERFETTO_CHECK(!fd_ && !owns_temp_file_); +#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) + DWORD attributes = GetFileAttributesA(destination_.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && + (attributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT))) + return base::ErrStatus( + "output must be a regular file, not a directory or link"); + HANDLE handle = + CreateFileA(temp_path_.c_str(), GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr); + if (handle == INVALID_HANDLE_VALUE) + return base::ErrStatus( + "cannot create temporary output beside '%s' (Windows error %lu)", + destination_.c_str(), GetLastError()); + owns_temp_file_ = true; + fd_.reset(_open_osfhandle(reinterpret_cast<intptr_t>(handle), _O_BINARY)); + if (!fd_) + CloseHandle(handle); +#else + struct stat output{}; + bool exists = lstat(destination_.c_str(), &output) == 0; + PERFETTO_MSAN_UNPOISON(&output, sizeof(output)); + if (exists && !S_ISREG(output.st_mode)) + return base::ErrStatus( + "output must be a regular file, not a directory or link: '%s'. Specify " + "the target path directly.", + destination_.c_str()); + fd_ = base::OpenFile(temp_path_, O_CREAT | O_EXCL | O_RDWR, 0644); + owns_temp_file_ = static_cast<bool>(fd_); + if (fd_ && exists && fchmod(*fd_, output.st_mode & 0777) != 0) + return base::ErrStatus("cannot preserve output permissions: %s", + strerror(errno)); +#endif + if (!fd_) + return base::ErrStatus( + "cannot create temporary output beside '%s': %s. Check that the parent " + "directory exists and is writable.", + destination_.c_str(), strerror(errno)); + return base::OkStatus(); +} + +base::ScopedFile AtomicFile::DuplicateFD() const { + PERFETTO_CHECK(fd_); + return DupFile(*fd_); +} + +base::Status AtomicFile::Commit() && { + PERFETTO_CHECK(fd_); + if (!base::FlushFile(*fd_)) + return base::ErrStatus("failed to flush output: %s", strerror(errno)); + fd_.reset(); +#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) + if (!MoveFileExA(temp_path_.c_str(), destination_.c_str(), + MOVEFILE_REPLACE_EXISTING)) + return base::ErrStatus("failed to publish '%s' (Windows error %lu)", + destination_.c_str(), GetLastError()); +#else + if (rename(temp_path_.c_str(), destination_.c_str()) != 0) + return base::ErrStatus("failed to publish '%s': %s", destination_.c_str(), + strerror(errno)); +#endif + owns_temp_file_ = false; + return base::OkStatus(); +} +} // namespace perfetto::base
diff --git a/src/base/atomic_file_unittest.cc b/src/base/atomic_file_unittest.cc new file mode 100644 index 0000000..eeab51f --- /dev/null +++ b/src/base/atomic_file_unittest.cc
@@ -0,0 +1,106 @@ +/* + * Copyright (C) 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "perfetto/ext/base/atomic_file.h" +#include <utility> + +#include "perfetto/ext/base/file_utils.h" +#include "perfetto/ext/base/temp_file.h" +#include "test/gtest_and_gmock.h" + +namespace perfetto::base { +namespace { +std::string Contents(const std::string& path) { + std::string contents; + EXPECT_TRUE(ReadFile(path, &contents)); + return contents; +} + +TEST(AtomicFileTest, PreservesDestinationUntilCommit) { + auto destination = TempFile::Create(); + ASSERT_EQ(WriteAll(destination.fd(), "old", 3), 3); + std::string temporary; + { + AtomicFile output(destination.path()); + ASSERT_TRUE(output.Open().ok()); + temporary = output.temp_path(); + auto fd = output.DuplicateFD(); + ASSERT_TRUE(fd); + ASSERT_EQ(WriteAll(*fd, "new", 3), 3); + fd.reset(); + EXPECT_EQ(Contents(destination.path()), "old"); + ASSERT_TRUE(std::move(output).Commit().ok()); + EXPECT_EQ(Contents(destination.path()), "new"); + } + EXPECT_FALSE(FileExists(temporary)); + EXPECT_EQ(Contents(destination.path()), "new"); +} + +TEST(AtomicFileTest, AbandonPreservesDestinationAndDeletesTemporaryFile) { + auto destination = TempFile::Create(); + ASSERT_EQ(WriteAll(destination.fd(), "old", 3), 3); + std::string temporary; + { + AtomicFile output(destination.path()); + ASSERT_TRUE(output.Open().ok()); + temporary = output.temp_path(); + auto fd = output.DuplicateFD(); + ASSERT_EQ(WriteAll(*fd, "incomplete", 10), 10); + } + EXPECT_FALSE(FileExists(temporary)); + EXPECT_EQ(Contents(destination.path()), "old"); +} + +TEST(AtomicFileTest, FailedCommitDeletesTemporaryFile) { + auto directory = TempDir::Create(); + std::string path = directory.path() + "/output"; + std::string temporary; + { + AtomicFile output(path); + ASSERT_TRUE(output.Open().ok()); + temporary = output.temp_path(); + ASSERT_TRUE(Mkdir(path)); + EXPECT_FALSE(std::move(output).Commit().ok()); + } + EXPECT_TRUE(DirectoryExists(path)); + EXPECT_FALSE(FileExists(temporary)); + ASSERT_TRUE(Rmdir(path)); +} + +TEST(AtomicFileTest, FailedOpenDoesNotDeleteUnownedFile) { + auto directory = TempDir::Create(); + std::string temporary; + { + AtomicFile output(directory.path() + "/output"); + temporary = output.temp_path(); + auto other = OpenFile(temporary, O_CREAT | O_EXCL | O_WRONLY, 0600); + ASSERT_TRUE(other); + ASSERT_EQ(WriteAll(*other, "other", 5), 5); + EXPECT_FALSE(output.Open().ok()); + } + EXPECT_EQ(Contents(temporary), "other"); + EXPECT_TRUE(Unlink(temporary.c_str())); +} + +TEST(AtomicFileTest, InvalidParentFailsWithoutCreatingDestination) { + auto directory = TempDir::Create(); + std::string path = directory.path() + "/missing/output"; + AtomicFile output(path); + EXPECT_FALSE(output.Open().ok()); + EXPECT_FALSE(FileExists(path)); +} +} // namespace +} // namespace perfetto::base
diff --git a/src/base/file_utils.cc b/src/base/file_utils.cc index a0d8521..79a9859 100644 --- a/src/base/file_utils.cc +++ b/src/base/file_utils.cc
@@ -279,6 +279,38 @@ #endif } +bool IsRegularFile(const std::string& path) { + struct stat st{}; + if (stat(path.c_str(), &st) != 0) + return false; + PERFETTO_MSAN_UNPOISON(&st, sizeof(st)); + return (st.st_mode & S_IFMT) == S_IFREG; +} + +bool IsSameFile(const std::string& first, const std::string& second) { +#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) + auto identity = [](const std::string& path, + BY_HANDLE_FILE_INFORMATION* info) { + ScopedPlatformHandle file(CreateFileA( + path.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); + return file && GetFileInformationByHandle(*file, info); + }; + BY_HANDLE_FILE_INFORMATION a{}, b{}; + return identity(first, &a) && identity(second, &b) && + a.dwVolumeSerialNumber == b.dwVolumeSerialNumber && + a.nFileIndexHigh == b.nFileIndexHigh && + a.nFileIndexLow == b.nFileIndexLow; +#else + struct stat a{}, b{}; + if (stat(first.c_str(), &a) != 0 || stat(second.c_str(), &b) != 0) + return false; + PERFETTO_MSAN_UNPOISON(&a, sizeof(a)); + PERFETTO_MSAN_UNPOISON(&b, sizeof(b)); + return a.st_dev == b.st_dev && a.st_ino == b.st_ino; +#endif +} + bool SeekFile(int fd, uint64_t offset) { #if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) if (fd < 0) {
diff --git a/src/trace_processor/shell/bundle_integrationtest.cc b/src/trace_processor/shell/bundle_integrationtest.cc index 0756714..41d6779 100644 --- a/src/trace_processor/shell/bundle_integrationtest.cc +++ b/src/trace_processor/shell/bundle_integrationtest.cc
@@ -638,7 +638,54 @@ EXPECT_NE(invoker.Run(), 0); } +TEST_F(TraceconvShellBundleTest, BundleFailurePreservesExistingOutput) { + base::TempFile trace = WriteTempFile(""); + base::TempFile destination = WriteTempFile("existing output"); + ArgvInvoker invoker; + invoker.Add("trace_processor_shell"); + invoker.Add("bundle"); + invoker.Add("--no-progress"); + invoker.Add("--no-auto-symbol-paths"); + invoker.Add("--proguard-map"); + invoker.Add(temp_dir_.path() + "/missing.map"); + invoker.Add(trace.path()); + invoker.Add(destination.path()); + EXPECT_NE(invoker.Run(), 0); + std::string contents; + ASSERT_TRUE(base::ReadFile(destination.path(), &contents)); + EXPECT_EQ(contents, "existing output"); +} + +TEST_F(TraceconvShellBundleTest, BundleRejectsSameInputAndOutput) { + base::TempFile trace = WriteTempFile("original trace"); + ArgvInvoker invoker; + invoker.Add("trace_processor_shell"); + invoker.Add("bundle"); + invoker.Add(trace.path()); + invoker.Add(trace.path()); + EXPECT_NE(invoker.Run(), 0); + std::string contents; + ASSERT_TRUE(base::ReadFile(trace.path(), &contents)); + EXPECT_EQ(contents, "original trace"); +} + #if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) +TEST_F(TraceconvShellBundleTest, BundleRejectsHardLinkedInputAndOutput) { + base::TempFile trace = WriteTempFile("original trace"); + std::string alias = temp_dir_.path() + "/alias"; + ASSERT_EQ(link(trace.path().c_str(), alias.c_str()), 0); + ArgvInvoker invoker; + invoker.Add("trace_processor_shell"); + invoker.Add("bundle"); + invoker.Add(trace.path()); + invoker.Add(alias); + EXPECT_NE(invoker.Run(), 0); + std::string contents; + ASSERT_TRUE(base::ReadFile(trace.path(), &contents)); + EXPECT_EQ(contents, "original trace"); + ASSERT_TRUE(base::Unlink(alias.c_str())); +} + TEST_F(TraceconvShellBundleTest, RedirectedProgressIsPlainAndWarningsRemain) { base::TempFile trace = WriteTempFile(BuildFuncgraphTrace(false)); for (bool no_progress : {false, true}) {
diff --git a/src/trace_processor/shell/bundle_subcommand.cc b/src/trace_processor/shell/bundle_subcommand.cc index 2ea702b..ac530f3 100644 --- a/src/trace_processor/shell/bundle_subcommand.cc +++ b/src/trace_processor/shell/bundle_subcommand.cc
@@ -89,6 +89,10 @@ mappings needed to make it self-contained. Both <input> and <output> must be real file paths (stdin/stdout are not supported). +The output is replaced only after the bundle is complete. Input and output +must be different regular files; specify an output symlink's target directly. +Abrupt termination may leave a temporary file beside the output. + Live progress uses stderr only when it is a terminal (except TERM=dumb). Nonempty FORCE_COLOR forces ANSI color, overriding NO_COLOR. Otherwise, nonempty NO_COLOR disables automatic color. Color does not enable progress.)"; @@ -170,23 +174,6 @@ output_file.c_str(), symlink_problem.c_str()); } - // Fail fast on output paths that cannot be created, before spending time - // reading the (potentially huge) trace. The TarWriter re-opens with O_TRUNC - // once the trace has been read successfully. -#if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) - { - base::ScopedFile probe = - base::OpenFile(output_file, O_CREAT | O_WRONLY, 0644); - if (!probe) { - return base::ErrStatus( - "bundle: cannot create output file '%s' (errno: %d, %s). Check " - "that the parent directory exists and is writable, then try " - "again.", - output_file.c_str(), errno, strerror(errno)); - } - } -#endif - trace_to_text::BundleContext context; if (!symbol_paths_.empty()) context.symbol_paths = base::SplitString(symbol_paths_, ",");
diff --git a/src/traceconv/trace_to_bundle.cc b/src/traceconv/trace_to_bundle.cc index 54634d5..d4ca9a8 100644 --- a/src/traceconv/trace_to_bundle.cc +++ b/src/traceconv/trace_to_bundle.cc
@@ -18,11 +18,15 @@ #include <cstdio> #include <string> +#include <utility> #include "perfetto/base/build_config.h" #include "perfetto/base/logging.h" #include "perfetto/base/status.h" +#include "perfetto/ext/base/atomic_file.h" +#include "perfetto/ext/base/file_utils.h" #include "perfetto/ext/base/progress_reporter.h" +#include "perfetto/ext/base/status_macros.h" #include "perfetto/ext/base/string_utils.h" #include "perfetto/trace_processor/read_trace.h" #include "perfetto/trace_processor/trace_processor.h" @@ -30,10 +34,45 @@ #include "src/trace_processor/util/trace_enrichment/trace_enrichment.h" namespace perfetto::trace_to_text { +namespace { +base::Status AddBundleEntries( + trace_processor::util::TarWriter* tar, + const std::string& input_file_path, + const trace_processor::util::EnrichmentResult& enrich_result) { + RETURN_IF_ERROR(tar->AddFileFromPath("trace.perfetto", input_file_path)); + // Add symbols if available. + if (!enrich_result.native_symbols.empty()) { + auto add_status = tar->AddFile("symbols.pb", enrich_result.native_symbols); + if (!add_status.ok()) { + return base::ErrStatus("failed to add symbols to bundle: %s", + add_status.c_message()); + } + } + + // Add deobfuscation data if available. + if (!enrich_result.deobfuscation_data.empty()) { + auto add_status = + tar->AddFile("deobfuscation.pb", enrich_result.deobfuscation_data); + if (!add_status.ok()) { + return base::ErrStatus("failed to add deobfuscation data to bundle: %s", + add_status.c_message()); + } + } + + return base::OkStatus(); +} +} // namespace base::Status TraceToBundle(const std::string& input_file_path, const std::string& output_file_path, const BundleContext& context) { + if (!base::IsRegularFile(input_file_path)) + return base::ErrStatus("bundle: input must be a regular file: '%s'", + input_file_path.c_str()); + if (base::IsSameFile(input_file_path, output_file_path)) + return base::ErrStatus("bundle: input and output refer to the same file"); + base::AtomicFile output(output_file_path); + RETURN_IF_ERROR(output.Open()); base::ProgressReporter progress(!context.no_progress); auto tp = trace_processor::TraceProcessor::CreateInstance({}); @@ -51,18 +90,6 @@ return base::ErrStatus("failed to read trace: %s", status.c_message()); fprintf(stderr, "Read trace: %.2f MB.\n", loaded_mb); - // Add original trace file directly (memory efficient). If the output path - // cannot be opened, TarWriter fails gracefully and this propagates a - // descriptive error instead of crashing. - fprintf(stderr, "Adding trace to bundle...\n"); - trace_processor::util::TarWriter tar(output_file_path); - auto add_trace_status = - tar.AddFileFromPath("trace.perfetto", input_file_path); - if (!add_trace_status.ok()) { - return base::ErrStatus("failed to create bundle: %s", - add_trace_status.c_message()); - } - // Build enrichment configuration from context. trace_processor::util::EnrichmentConfig enrich_config; enrich_config.symbol_paths = context.symbol_paths; @@ -86,25 +113,6 @@ trace_processor::util::EnrichTrace(tp.get(), enrich_config); fprintf(stderr, "Enrichment done.\n"); - // Add symbols if available. - if (!enrich_result.native_symbols.empty()) { - auto add_status = tar.AddFile("symbols.pb", enrich_result.native_symbols); - if (!add_status.ok()) { - return base::ErrStatus("failed to add symbols to bundle: %s", - add_status.c_message()); - } - } - - // Add deobfuscation data if available. - if (!enrich_result.deobfuscation_data.empty()) { - auto add_status = - tar.AddFile("deobfuscation.pb", enrich_result.deobfuscation_data); - if (!add_status.ok()) { - return base::ErrStatus("failed to add deobfuscation data to bundle: %s", - add_status.c_message()); - } - } - // Log any issues to stderr (without PERFETTO_LOG noise). if (!enrich_result.details.empty()) { fprintf(stderr, "%s", enrich_result.details.c_str()); @@ -124,7 +132,20 @@ "the requested deobfuscation data."); } - return base::OkStatus(); + fprintf(stderr, "Adding trace to bundle...\n"); + { + auto fd = output.DuplicateFD(); + if (!fd) + return base::ErrStatus("bundle: failed to duplicate output descriptor"); + trace_processor::util::TarWriter tar(std::move(fd)); + auto write_status = AddBundleEntries(&tar, input_file_path, enrich_result); + // Always finalize explicitly: the destructor otherwise crashes on an + // end-of-archive write failure (e.g. disk full). + auto finalize_status = tar.Finalize(); + RETURN_IF_ERROR(write_status); + RETURN_IF_ERROR(finalize_status); + } + return std::move(output).Commit(); } } // namespace perfetto::trace_to_text