1 // Copyright 2020 The Chromium Authors. All rights reserved.
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 "chrome/browser/chromeos/crostini/termina_installer.h"
6
7 #include <algorithm>
8 #include <memory>
9
10 #include "base/barrier_closure.h"
11 #include "base/bind.h"
12 #include "base/callback.h"
13 #include "base/feature_list.h"
14 #include "base/task/task_traits.h"
15 #include "base/task/thread_pool.h"
16 #include "base/threading/sequenced_task_runner_handle.h"
17 #include "chrome/browser/browser_process.h"
18 #include "chrome/browser/browser_process_platform_part_chromeos.h"
19 #include "chrome/browser/chromeos/crostini/crostini_util.h"
20 #include "chromeos/constants/chromeos_features.h"
21 #include "content/public/browser/network_service_instance.h"
22 #include "services/network/public/cpp/network_connection_tracker.h"
23 #include "third_party/cros_system_api/dbus/service_constants.h"
24
25 namespace crostini {
26
TerminaInstaller()27 TerminaInstaller::TerminaInstaller() {}
~TerminaInstaller()28 TerminaInstaller::~TerminaInstaller() {}
29
Install(base::OnceCallback<void (InstallResult)> callback)30 void TerminaInstaller::Install(
31 base::OnceCallback<void(InstallResult)> callback) {
32 // The Remove*IfPresent methods require an unowned UninstallResult pointer to
33 // record their success/failure state. This has to be unowned so that in
34 // Uninstall it can be accessed further down the callback chain, but here we
35 // don't care about it, so we assign ownership of the pointer to a
36 // base::DoNothing that will delete it once called.
37 auto ptr = std::make_unique<UninstallResult>();
38 auto* uninstall_result_ptr = ptr.get();
39 auto remove_callback = base::BindOnce(
40 [](std::unique_ptr<UninstallResult> ptr) {}, std::move(ptr));
41
42 // Remove whichever version of termina we're *not* using and install the right
43 // one.
44 if (base::FeatureList::IsEnabled(chromeos::features::kCrostiniUseDlc)) {
45 RemoveComponentIfPresent(std::move(remove_callback), uninstall_result_ptr);
46 InstallDlc(std::move(callback));
47 dlc_id_ = kCrostiniDlcName;
48 } else {
49 RemoveDlcIfPresent(std::move(remove_callback), uninstall_result_ptr);
50 InstallComponent(std::move(callback));
51 dlc_id_ = base::nullopt;
52 }
53 }
54
InstallDlc(base::OnceCallback<void (InstallResult)> callback)55 void TerminaInstaller::InstallDlc(
56 base::OnceCallback<void(InstallResult)> callback) {
57 chromeos::DlcserviceClient::Get()->Install(
58 kCrostiniDlcName,
59 base::BindOnce(&TerminaInstaller::OnInstallDlc,
60 weak_ptr_factory_.GetWeakPtr(), std::move(callback)),
61 base::DoNothing());
62 }
63
OnInstallDlc(base::OnceCallback<void (InstallResult)> callback,const chromeos::DlcserviceClient::InstallResult & result)64 void TerminaInstaller::OnInstallDlc(
65 base::OnceCallback<void(InstallResult)> callback,
66 const chromeos::DlcserviceClient::InstallResult& result) {
67 CHECK(result.dlc_id == kCrostiniDlcName);
68 InstallResult response;
69 if (result.error == dlcservice::kErrorNone) {
70 response = InstallResult::Success;
71 termina_location_ = base::FilePath(result.root_path);
72 } else {
73 if (content::GetNetworkConnectionTracker()->IsOffline()) {
74 LOG(ERROR) << "Failed to install termina-dlc while offline, assuming "
75 "network issue: "
76 << result.error;
77 response = InstallResult::Offline;
78 } else {
79 LOG(ERROR) << "Failed to install termina-dlc: " << result.error;
80 response = InstallResult::Failure;
81 }
82 }
83 std::move(callback).Run(response);
84 }
85
86 using UpdatePolicy = component_updater::CrOSComponentManager::UpdatePolicy;
87
InstallComponent(base::OnceCallback<void (InstallResult)> callback)88 void TerminaInstaller::InstallComponent(
89 base::OnceCallback<void(InstallResult)> callback) {
90 scoped_refptr<component_updater::CrOSComponentManager> component_manager =
91 g_browser_process->platform_part()->cros_component_manager();
92
93 bool major_update_required =
94 component_manager->GetCompatiblePath(imageloader::kTerminaComponentName)
95 .empty();
96 bool is_offline = content::GetNetworkConnectionTracker()->IsOffline();
97
98 if (major_update_required) {
99 component_update_check_needed_ = false;
100 if (is_offline) {
101 LOG(ERROR) << "Need to load a major component update, but we're offline.";
102 std::move(callback).Run(InstallResult::Offline);
103 return;
104 }
105 }
106
107 UpdatePolicy update_policy;
108 if (component_update_check_needed_ && !is_offline) {
109 // Don't use kForce all the time because it generates traffic to
110 // ComponentUpdaterService. Also, it's only appropriate for minor version
111 // updates. Not major version incompatiblility.
112 update_policy = UpdatePolicy::kForce;
113 } else {
114 update_policy = UpdatePolicy::kDontForce;
115 }
116
117 component_manager->Load(
118 imageloader::kTerminaComponentName,
119 component_updater::CrOSComponentManager::MountPolicy::kMount,
120 update_policy,
121 base::BindOnce(&TerminaInstaller::OnInstallComponent,
122 weak_ptr_factory_.GetWeakPtr(), std::move(callback),
123 update_policy == UpdatePolicy::kForce));
124 }
125
OnInstallComponent(base::OnceCallback<void (InstallResult)> callback,bool is_update_checked,component_updater::CrOSComponentManager::Error error,const base::FilePath & path)126 void TerminaInstaller::OnInstallComponent(
127 base::OnceCallback<void(InstallResult)> callback,
128 bool is_update_checked,
129 component_updater::CrOSComponentManager::Error error,
130 const base::FilePath& path) {
131 bool is_successful =
132 error == component_updater::CrOSComponentManager::Error::NONE;
133
134 if (is_successful) {
135 termina_location_ = path;
136 } else {
137 LOG(ERROR)
138 << "Failed to install the cros-termina component with error code: "
139 << static_cast<int>(error);
140 if (is_update_checked) {
141 scoped_refptr<component_updater::CrOSComponentManager> component_manager =
142 g_browser_process->platform_part()->cros_component_manager();
143 if (component_manager) {
144 // Try again, this time with no update checking. The reason we do this
145 // is that we may still be offline even when is_offline above was
146 // false. It's notoriously difficult to know when you're really
147 // connected to the Internet, and it's also possible to be unable to
148 // connect to a service like ComponentUpdaterService even when you are
149 // connected to the rest of the Internet.
150 UpdatePolicy update_policy = UpdatePolicy::kDontForce;
151
152 LOG(ERROR) << "Retrying cros-termina component load, no update check";
153 // Load the existing component on disk.
154 component_manager->Load(
155 imageloader::kTerminaComponentName,
156 component_updater::CrOSComponentManager::MountPolicy::kMount,
157 update_policy,
158 base::BindOnce(&TerminaInstaller::OnInstallComponent,
159 weak_ptr_factory_.GetWeakPtr(), std::move(callback),
160 false));
161 return;
162 }
163 }
164 }
165
166 if (is_successful && is_update_checked) {
167 VLOG(1) << "cros-termina update check successful.";
168 component_update_check_needed_ = false;
169 }
170 InstallResult result = InstallResult::Success;
171 if (!is_successful) {
172 if (error ==
173 component_updater::CrOSComponentManager::Error::UPDATE_IN_PROGRESS) {
174 // Something else triggered an update that we have to wait on. We don't
175 // know what, or when they will be finished, so just retry every 5 seconds
176 // until we get a different result.
177 base::SequencedTaskRunnerHandle::Get()->PostDelayedTask(
178 FROM_HERE,
179 base::BindOnce(&TerminaInstaller::InstallComponent,
180 weak_ptr_factory_.GetWeakPtr(), std::move(callback)),
181 base::TimeDelta::FromSeconds(5));
182 return;
183 } else {
184 result = InstallResult::Failure;
185 }
186 }
187
188 std::move(callback).Run(result);
189 }
190
Uninstall(base::OnceCallback<void (bool)> callback)191 void TerminaInstaller::Uninstall(base::OnceCallback<void(bool)> callback) {
192 // Unset |termina_location_| now since it will become invalid at some point
193 // soon.
194 termina_location_ = base::nullopt;
195
196 // This is really a vector of bool, but std::vector<bool> has weird properties
197 // that stop us from using it in this way.
198 std::vector<UninstallResult> partial_results{0, 0};
199 UninstallResult* component_result = &partial_results[0];
200 UninstallResult* dlc_result = &partial_results[1];
201
202 // We want to get the results from both uninstall calls and combine them, and
203 // the asynchronous nature of this process means we can't use return values,
204 // so we need to pass a pointer into those calls to store their results and
205 // pass ownership of that memory into the result callback.
206 auto b_closure = BarrierClosure(
207 2, base::BindOnce(&TerminaInstaller::OnUninstallFinished,
208 weak_ptr_factory_.GetWeakPtr(), std::move(callback),
209 std::move(partial_results)));
210
211 RemoveComponentIfPresent(b_closure, component_result);
212 RemoveDlcIfPresent(b_closure, dlc_result);
213 }
214
RemoveComponentIfPresent(base::OnceCallback<void ()> callback,UninstallResult * result)215 void TerminaInstaller::RemoveComponentIfPresent(
216 base::OnceCallback<void()> callback,
217 UninstallResult* result) {
218 VLOG(1) << "Removing component";
219 scoped_refptr<component_updater::CrOSComponentManager> component_manager =
220 g_browser_process->platform_part()->cros_component_manager();
221
222 base::ThreadPool::PostTaskAndReplyWithResult(
223 FROM_HERE, {base::MayBlock()},
224 base::BindOnce(
225 [](scoped_refptr<component_updater::CrOSComponentManager>
226 component_manager) {
227 return component_manager->IsRegisteredMayBlock(
228 imageloader::kTerminaComponentName);
229 },
230 std::move(component_manager)),
231 base::BindOnce(
232 [](base::OnceCallback<void()> callback, UninstallResult* result,
233 bool is_present) {
234 scoped_refptr<component_updater::CrOSComponentManager>
235 component_manager = g_browser_process->platform_part()
236 ->cros_component_manager();
237 if (is_present) {
238 VLOG(1) << "Component present, unloading";
239 *result =
240 component_manager->Unload(imageloader::kTerminaComponentName);
241 if (!*result) {
242 LOG(ERROR) << "Failed to remove cros-termina component";
243 }
244 } else {
245 VLOG(1) << "No component present, skipping";
246 *result = true;
247 }
248 std::move(callback).Run();
249 },
250 std::move(callback), result));
251 }
252
RemoveDlcIfPresent(base::OnceCallback<void ()> callback,UninstallResult * result)253 void TerminaInstaller::RemoveDlcIfPresent(base::OnceCallback<void()> callback,
254 UninstallResult* result) {
255 if (!base::FeatureList::IsEnabled(chromeos::features::kCrostiniEnableDlc)) {
256 // No DLC service, so be a no-op.
257 *result = true;
258 std::move(callback).Run();
259 return;
260 }
261 chromeos::DlcserviceClient::Get()->GetExistingDlcs(base::BindOnce(
262 [](base::WeakPtr<TerminaInstaller> weak_this,
263 base::OnceCallback<void()> callback, UninstallResult* result,
264 const std::string& err,
265 const dlcservice::DlcsWithContent& dlcs_with_content) {
266 if (!weak_this)
267 return;
268
269 if (err != dlcservice::kErrorNone) {
270 LOG(ERROR) << "Failed to list installed DLCs: " << err;
271 *result = false;
272 std::move(callback).Run();
273 return;
274 }
275 for (const auto& dlc : dlcs_with_content.dlc_infos()) {
276 if (dlc.id() == kCrostiniDlcName) {
277 VLOG(1) << "DLC present, removing";
278 weak_this->RemoveDlc(std::move(callback), result);
279 return;
280 }
281 }
282 VLOG(1) << "No DLC present, skipping";
283 *result = true;
284 std::move(callback).Run();
285 },
286 weak_ptr_factory_.GetWeakPtr(), std::move(callback), result));
287 }
288
RemoveDlc(base::OnceCallback<void ()> callback,UninstallResult * result)289 void TerminaInstaller::RemoveDlc(base::OnceCallback<void()> callback,
290 UninstallResult* result) {
291 chromeos::DlcserviceClient::Get()->Uninstall(
292 kCrostiniDlcName,
293 base::BindOnce(
294 [](base::OnceCallback<void()> callback, UninstallResult* result,
295 const std::string& err) {
296 if (err == dlcservice::kErrorNone) {
297 VLOG(1) << "Removed DLC";
298 *result = true;
299 } else {
300 LOG(ERROR) << "Failed to remove termina-dlc: " << err;
301 *result = false;
302 }
303 std::move(callback).Run();
304 },
305 std::move(callback), result));
306 }
307
OnUninstallFinished(base::OnceCallback<void (bool)> callback,std::vector<UninstallResult> partial_results)308 void TerminaInstaller::OnUninstallFinished(
309 base::OnceCallback<void(bool)> callback,
310 std::vector<UninstallResult> partial_results) {
311 bool result = std::all_of(partial_results.begin(), partial_results.end(),
312 [](bool b) { return b; });
313 std::move(callback).Run(result);
314 }
315
GetInstallLocation()316 base::FilePath TerminaInstaller::GetInstallLocation() {
317 CHECK(termina_location_)
318 << "GetInstallLocation() called while termina not installed";
319 return *termina_location_;
320 }
321
GetDlcId()322 base::Optional<std::string> TerminaInstaller::GetDlcId() {
323 CHECK(termina_location_) << "GetDlcId() called while termina not installed";
324 return dlc_id_;
325 }
326
327 } // namespace crostini
328