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