1 // Copyright 2024 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "components/metrics/structured/lib/persistent_proto_internal.h"
6
7 #include <atomic>
8 #include <memory>
9 #include <string>
10 #include <utility>
11
12 #include "base/files/file_util.h"
13 #include "base/files/important_file_writer.h"
14 #include "base/functional/bind.h"
15 #include "base/rand_util.h"
16 #include "base/task/bind_post_task.h"
17 #include "base/task/sequenced_task_runner.h"
18 #include "base/task/task_traits.h"
19 #include "base/task/thread_pool.h"
20
21 namespace metrics::structured::internal {
22
23 namespace {
24
25 // Attempts to read from |filepath| and returns a string with the file content
26 // if successful.
Read(const base::FilePath & filepath)27 base::expected<std::string, ReadStatus> Read(const base::FilePath& filepath) {
28 if (!base::PathExists(filepath)) {
29 return base::unexpected(ReadStatus::kMissing);
30 }
31
32 std::string proto_str;
33 if (!base::ReadFileToString(filepath, &proto_str)) {
34 return base::unexpected(ReadStatus::kReadError);
35 }
36
37 return base::ok(std::move(proto_str));
38 }
39
40 } // namespace
41
PersistentProtoInternal(const base::FilePath & path,base::TimeDelta write_delay,PersistentProtoInternal::ReadCallback on_read,PersistentProtoInternal::WriteCallback on_write)42 PersistentProtoInternal::PersistentProtoInternal(
43 const base::FilePath& path,
44 base::TimeDelta write_delay,
45 PersistentProtoInternal::ReadCallback on_read,
46 PersistentProtoInternal::WriteCallback on_write)
47 : on_write_(std::move(on_write)),
48 task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
49 {base::TaskPriority::BEST_EFFORT, base::MayBlock(),
50 base::TaskShutdownBehavior::BLOCK_SHUTDOWN})),
51 proto_file_(std::make_unique<base::ImportantFileWriter>(
52 path,
53 task_runner_,
54 write_delay,
55 "StructuredMetricsPersistentProto")) {
56 task_runner_->PostTaskAndReplyWithResult(
57 FROM_HERE, base::BindOnce(&Read, proto_file_->path()),
58 base::BindOnce(&PersistentProtoInternal::OnReadComplete,
59 weak_factory_.GetWeakPtr(), std::move(on_read)));
60 }
61
62 PersistentProtoInternal::~PersistentProtoInternal() = default;
63
OnReadComplete(ReadCallback callback,base::expected<std::string,ReadStatus> read_status)64 void PersistentProtoInternal::OnReadComplete(
65 ReadCallback callback,
66 base::expected<std::string, ReadStatus> read_status) {
67 ReadStatus status;
68
69 // If the path was updated then we may not need to update the pointer.
70 if (proto_ == nullptr) {
71 proto_ = GetProto();
72 }
73
74 if (read_status.has_value()) {
75 status = ReadStatus::kOk;
76
77 // Parses the value of |read_status| into |proto_| but attempts to preserve
78 // any existing content. Optional values will be overwritten and repeated
79 // fields will be appended with new values.
80 if (!proto_->MergeFromString(read_status.value())) {
81 status = ReadStatus::kParseError;
82 QueueWrite();
83 }
84 } else {
85 status = read_status.error();
86 }
87
88 // If there was an error, write an empty proto.
89 if (status != ReadStatus::kOk) {
90 QueueWrite();
91 }
92
93 // Purge the read proto if |purge_after_reading_|.
94 if (purge_after_reading_) {
95 Purge();
96 purge_after_reading_ = false;
97 }
98
99 std::move(callback).Run(std::move(status));
100 }
101
QueueWrite()102 void PersistentProtoInternal::QueueWrite() {
103 // Read |updating_path_| to check if we are actively updating the path of this
104 // proto.
105 if (updating_path_.load()) {
106 return;
107 }
108
109 // |proto_| will be null if OnReadComplete() has not finished executing. It is
110 // up to the user to verify that OnReadComplete() has finished with callback
111 // |on_read_| before calling QueueWrite().
112 CHECK(proto_);
113
114 // Serialize the proto into a buffer for write to occur. Because the IO
115 // happens on a separate sequence, serialization must happen on this sequence
116 // since PersistentProto is not thread-safe.
117 SerializeProtoForWrite();
118
119 proto_file_->ScheduleWrite(this);
120 }
121
OnWriteAttempt(bool write_successful)122 void PersistentProtoInternal::OnWriteAttempt(bool write_successful) {
123 write_buffer_.clear();
124
125 if (write_successful) {
126 OnWriteComplete(WriteStatus::kOk);
127 } else {
128 OnWriteComplete(WriteStatus::kWriteError);
129 }
130 }
131
OnWriteComplete(const WriteStatus status)132 void PersistentProtoInternal::OnWriteComplete(const WriteStatus status) {
133 on_write_.Run(status);
134 }
135
Purge()136 void PersistentProtoInternal::Purge() {
137 if (proto_) {
138 proto_->Clear();
139 QueueFileDelete();
140 } else {
141 purge_after_reading_ = true;
142 }
143 }
144
SerializeData()145 std::optional<std::string> PersistentProtoInternal::SerializeData() {
146 proto_file_->RegisterOnNextWriteCallbacks(
147 base::BindOnce(base::IgnoreResult(&base::CreateDirectory),
148 proto_file_->path().DirName()),
149 base::BindPostTask(
150 base::SequencedTaskRunner::GetCurrentDefault(),
151 base::BindOnce(&PersistentProtoInternal::OnWriteAttempt,
152 weak_factory_.GetWeakPtr())));
153 return write_buffer_;
154 }
155
StartWriteForTesting()156 void PersistentProtoInternal::StartWriteForTesting() {
157 SerializeProtoForWrite();
158
159 proto_file_->ScheduleWrite(this);
160 proto_file_->DoScheduledWrite();
161 }
162
UpdatePath(const base::FilePath & path,ReadCallback on_read,bool remove_existing)163 void PersistentProtoInternal::UpdatePath(const base::FilePath& path,
164 ReadCallback on_read,
165 bool remove_existing) {
166 updating_path_.store(true);
167
168 // Clean up the state of the current |proto_file_|.
169 FlushQueuedWrites();
170
171 // If the previous file should be cleaned up then schedule the cleanup on
172 // separate thread.
173 if (remove_existing) {
174 task_runner_->PostTask(FROM_HERE,
175 base::BindOnce(base::IgnoreResult(&base::DeleteFile),
176 proto_file_->path()));
177 }
178
179 // Overwrite the ImportantFileWriter with a new one at the new path.
180 proto_file_ = std::make_unique<base::ImportantFileWriter>(
181 path, task_runner_, proto_file_->commit_interval(),
182 "StructuredMetricsPersistentProto");
183
184 task_runner_->PostTaskAndReplyWithResult(
185 FROM_HERE, base::BindOnce(&Read, proto_file_->path()),
186 base::BindOnce(&PersistentProtoInternal::OnReadComplete,
187 weak_factory_.GetWeakPtr(), std::move(on_read)));
188
189 updating_path_.store(false);
190
191 // Write the content of the proto back to the path in case it has changed. If
192 // an error occurs while reading |path| then 2 write can occur.
193 //
194 // It is possible in tests that the profile is added before the pre-profile
195 // events have been loaded, which initializes the proto. In this case, we do
196 // not want to queue a write.
197 if (proto_) {
198 QueueWrite();
199 }
200 }
201
DeallocProto()202 void PersistentProtoInternal::DeallocProto() {
203 FlushQueuedWrites();
204 proto_ = nullptr;
205 }
206
QueueFileDelete()207 void PersistentProtoInternal::QueueFileDelete() {
208 task_runner_->PostTask(FROM_HERE,
209 base::BindOnce(base::IgnoreResult(&base::DeleteFile),
210 proto_file_->path()));
211 }
212
FlushQueuedWrites()213 void PersistentProtoInternal::FlushQueuedWrites() {
214 if (proto_file_->HasPendingWrite()) {
215 proto_file_->DoScheduledWrite();
216 }
217 }
218
SerializeProtoForWrite()219 void PersistentProtoInternal::SerializeProtoForWrite() {
220 // If the write buffer is not empty, it means that a write is already in
221 // progress.
222 if (!write_buffer_.empty()) {
223 return;
224 }
225
226 if (!proto_->SerializeToString(&write_buffer_)) {
227 write_buffer_.clear();
228 OnWriteComplete(WriteStatus::kSerializationError);
229 return;
230 }
231 }
232
233 } // namespace metrics::structured::internal
234