1 // Copyright (c) 2012 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.
5 #include "chrome/installer/util/installer_state.h"
11 #include "base/command_line.h"
12 #include "base/file_version_info.h"
13 #include "base/files/file_enumerator.h"
14 #include "base/files/file_util.h"
15 #include "base/logging.h"
16 #include "base/memory/scoped_ptr.h"
17 #include "base/strings/string_util.h"
18 #include "base/strings/utf_string_conversions.h"
19 #include "base/win/registry.h"
20 #include "base/win/scoped_handle.h"
21 #include "chrome/installer/util/delete_tree_work_item.h"
22 #include "chrome/installer/util/helper.h"
23 #include "chrome/installer/util/install_util.h"
24 #include "chrome/installer/util/installation_state.h"
25 #include "chrome/installer/util/master_preferences.h"
26 #include "chrome/installer/util/master_preferences_constants.h"
27 #include "chrome/installer/util/product.h"
28 #include "chrome/installer/util/work_item.h"
29 #include "chrome/installer/util/work_item_list.h"
33 bool InstallerState::IsMultiInstallUpdate(
34 const MasterPreferences
& prefs
,
35 const InstallationState
& machine_state
) {
36 // First, are the binaries present?
37 const ProductState
* binaries
=
38 machine_state
.GetProductState(level_
== SYSTEM_LEVEL
,
39 BrowserDistribution::CHROME_BINARIES
);
40 if (binaries
== NULL
) {
41 // The multi-install binaries have not been installed, so they certainly
42 // aren't being updated.
46 if (prefs
.install_chrome()) {
47 const ProductState
* product
=
48 machine_state
.GetProductState(level_
== SYSTEM_LEVEL
,
49 BrowserDistribution::CHROME_BROWSER
);
50 if (product
== NULL
) {
51 VLOG(2) << "It seems that chrome is being installed for the first time.";
54 if (!product
->channel().Equals(binaries
->channel())) {
55 VLOG(2) << "It seems that chrome is being over installed.";
60 VLOG(2) << "It seems that the binaries are being updated.";
65 InstallerState::InstallerState()
66 : operation_(UNINITIALIZED
),
67 state_type_(BrowserDistribution::CHROME_BROWSER
),
68 multi_package_distribution_(NULL
),
69 level_(UNKNOWN_LEVEL
),
70 package_type_(UNKNOWN_PACKAGE_TYPE
),
73 verbose_logging_(false) {
76 InstallerState::InstallerState(Level level
)
77 : operation_(UNINITIALIZED
),
78 state_type_(BrowserDistribution::CHROME_BROWSER
),
79 multi_package_distribution_(NULL
),
80 level_(UNKNOWN_LEVEL
),
81 package_type_(UNKNOWN_PACKAGE_TYPE
),
84 verbose_logging_(false) {
85 // Use set_level() so that root_key_ is updated properly.
89 InstallerState::~InstallerState() {
92 void InstallerState::Initialize(const base::CommandLine
& command_line
,
93 const MasterPreferences
& prefs
,
94 const InstallationState
& machine_state
) {
98 if (!prefs
.GetBool(master_preferences::kSystemLevel
, &pref_bool
))
100 set_level(pref_bool
? SYSTEM_LEVEL
: USER_LEVEL
);
102 if (!prefs
.GetBool(master_preferences::kVerboseLogging
, &verbose_logging_
))
103 verbose_logging_
= false;
105 if (!prefs
.GetBool(master_preferences::kMultiInstall
, &pref_bool
))
107 set_package_type(pref_bool
? MULTI_PACKAGE
: SINGLE_PACKAGE
);
109 if (!prefs
.GetBool(master_preferences::kMsi
, &msi_
))
112 const bool is_uninstall
= command_line
.HasSwitch(switches::kUninstall
);
114 if (prefs
.install_chrome()) {
115 Product
* p
= AddProductFromPreferences(
116 BrowserDistribution::CHROME_BROWSER
, prefs
, machine_state
);
117 VLOG(1) << (is_uninstall
? "Uninstall" : "Install")
118 << " distribution: " << p
->distribution()->GetDisplayName();
121 // Binaries are only used by Chrome.
122 if (is_multi_install() &&
123 FindProduct(BrowserDistribution::CHROME_BROWSER
)) {
124 Product
* p
= AddProductFromPreferences(
125 BrowserDistribution::CHROME_BINARIES
, prefs
, machine_state
);
126 VLOG(1) << (is_uninstall
? "Uninstall" : "Install")
127 << " distribution: " << p
->distribution()->GetDisplayName();
130 BrowserDistribution
* operand
= NULL
;
133 operation_
= UNINSTALL
;
134 } else if (!prefs
.is_multi_install()) {
135 // For a single-install, the current browser dist is the operand.
136 operand
= BrowserDistribution::GetDistribution();
137 operation_
= SINGLE_INSTALL_OR_UPDATE
;
138 } else if (IsMultiInstallUpdate(prefs
, machine_state
)) {
139 // Updates driven by Google Update take place under the multi-installer's
141 operand
= multi_package_distribution_
;
142 operation_
= MULTI_UPDATE
;
144 operation_
= MULTI_INSTALL
;
147 // Initial, over, and un-installs will take place under Chrome or Binaries
149 if (operand
== NULL
) {
150 operand
= BrowserDistribution::GetSpecificDistribution(
151 prefs
.install_chrome() ?
152 BrowserDistribution::CHROME_BROWSER
:
153 BrowserDistribution::CHROME_BINARIES
);
156 state_key_
= operand
->GetStateKey();
157 state_type_
= operand
->GetType();
159 // Parse --critical-update-version=W.X.Y.Z
160 std::string
critical_version_value(
161 command_line
.GetSwitchValueASCII(switches::kCriticalUpdateVersion
));
162 critical_update_version_
= Version(critical_version_value
);
165 void InstallerState::set_level(Level level
) {
169 root_key_
= HKEY_CURRENT_USER
;
172 root_key_
= HKEY_LOCAL_MACHINE
;
175 DCHECK(level
== UNKNOWN_LEVEL
);
176 level_
= UNKNOWN_LEVEL
;
182 void InstallerState::set_package_type(PackageType type
) {
183 package_type_
= type
;
186 multi_package_distribution_
= NULL
;
189 multi_package_distribution_
=
190 BrowserDistribution::GetSpecificDistribution(
191 BrowserDistribution::CHROME_BINARIES
);
194 DCHECK(type
== UNKNOWN_PACKAGE_TYPE
);
195 package_type_
= UNKNOWN_PACKAGE_TYPE
;
196 multi_package_distribution_
= NULL
;
201 // Returns the Chrome binaries directory for multi-install or |dist|'s directory
203 base::FilePath
InstallerState::GetDefaultProductInstallPath(
204 BrowserDistribution
* dist
) const {
206 DCHECK(package_type_
!= UNKNOWN_PACKAGE_TYPE
);
208 if (package_type_
== SINGLE_PACKAGE
) {
209 return GetChromeInstallPath(system_install(), dist
);
211 return GetChromeInstallPath(system_install(),
212 BrowserDistribution::GetSpecificDistribution(
213 BrowserDistribution::CHROME_BINARIES
));
217 // Evaluates a product's eligibility for participation in this operation.
218 // We never expect these checks to fail, hence they all terminate the process in
219 // debug builds. See the log messages for details.
220 bool InstallerState::CanAddProduct(const Product
& product
,
221 const base::FilePath
* product_dir
) const {
222 switch (package_type_
) {
224 if (!products_
.empty()) {
225 LOG(DFATAL
) << "Cannot process more than one single-install product.";
230 if (!product
.HasOption(kOptionMultiInstall
)) {
231 LOG(DFATAL
) << "Cannot process a single-install product with a "
232 "multi-install state.";
235 if (FindProduct(product
.distribution()->GetType()) != NULL
) {
236 LOG(DFATAL
) << "Cannot process more than one product of the same type.";
239 if (!target_path_
.empty()) {
240 base::FilePath default_dir
;
241 if (product_dir
== NULL
)
242 default_dir
= GetDefaultProductInstallPath(product
.distribution());
243 if (!base::FilePath::CompareEqualIgnoreCase(
244 (product_dir
== NULL
? default_dir
: *product_dir
).value(),
245 target_path_
.value())) {
246 LOG(DFATAL
) << "Cannot process products in different directories.";
252 DCHECK_EQ(UNKNOWN_PACKAGE_TYPE
, package_type_
);
258 // Adds |product|, installed in |product_dir| to this object's collection. If
259 // |product_dir| is NULL, the product's default install location is used.
260 // Returns NULL if |product| is incompatible with this object. Otherwise,
261 // returns a pointer to the product (ownership is held by this object).
262 Product
* InstallerState::AddProductInDirectory(
263 const base::FilePath
* product_dir
,
264 scoped_ptr
<Product
>* product
) {
265 DCHECK(product
!= NULL
);
266 DCHECK(product
->get() != NULL
);
267 const Product
& the_product
= *product
->get();
269 if (!CanAddProduct(the_product
, product_dir
))
272 if (package_type_
== UNKNOWN_PACKAGE_TYPE
) {
273 set_package_type(the_product
.HasOption(kOptionMultiInstall
) ?
274 MULTI_PACKAGE
: SINGLE_PACKAGE
);
277 if (target_path_
.empty()) {
278 if (product_dir
== NULL
)
279 target_path_
= GetDefaultProductInstallPath(the_product
.distribution());
281 target_path_
= *product_dir
;
284 if (state_key_
.empty())
285 state_key_
= the_product
.distribution()->GetStateKey();
287 products_
.push_back(product
->release());
288 return products_
[products_
.size() - 1];
291 Product
* InstallerState::AddProduct(scoped_ptr
<Product
>* product
) {
292 return AddProductInDirectory(NULL
, product
);
295 // Adds a product of type |distribution_type| constructed on the basis of
296 // |prefs|, setting this object's msi flag if the product is represented in
297 // |machine_state| and is msi-installed. Returns the product that was added,
298 // or NULL if |state| is incompatible with this object. Ownership is not passed
300 Product
* InstallerState::AddProductFromPreferences(
301 BrowserDistribution::Type distribution_type
,
302 const MasterPreferences
& prefs
,
303 const InstallationState
& machine_state
) {
304 scoped_ptr
<Product
> product_ptr(
305 new Product(BrowserDistribution::GetSpecificDistribution(
306 distribution_type
)));
307 product_ptr
->InitializeFromPreferences(prefs
);
309 Product
* product
= AddProductInDirectory(NULL
, &product_ptr
);
311 if (product
!= NULL
&& !msi_
) {
312 const ProductState
* product_state
= machine_state
.GetProductState(
313 system_install(), distribution_type
);
314 if (product_state
!= NULL
)
315 msi_
= product_state
->is_msi();
321 Product
* InstallerState::AddProductFromState(
322 BrowserDistribution::Type type
,
323 const ProductState
& state
) {
324 scoped_ptr
<Product
> product_ptr(
325 new Product(BrowserDistribution::GetSpecificDistribution(type
)));
326 product_ptr
->InitializeFromUninstallCommand(state
.uninstall_command());
328 // Strip off <version>/Installer/setup.exe; see GetInstallerDirectory().
329 base::FilePath product_dir
=
330 state
.GetSetupPath().DirName().DirName().DirName();
332 Product
* product
= AddProductInDirectory(&product_dir
, &product_ptr
);
335 msi_
|= state
.is_msi();
340 bool InstallerState::system_install() const {
341 DCHECK(level_
== USER_LEVEL
|| level_
== SYSTEM_LEVEL
);
342 return level_
== SYSTEM_LEVEL
;
345 bool InstallerState::is_multi_install() const {
346 DCHECK(package_type_
== SINGLE_PACKAGE
|| package_type_
== MULTI_PACKAGE
);
347 return package_type_
!= SINGLE_PACKAGE
;
350 bool InstallerState::RemoveProduct(const Product
* product
) {
351 ScopedVector
<Product
>::iterator it
=
352 std::find(products_
.begin(), products_
.end(), product
);
353 if (it
!= products_
.end()) {
354 products_
.weak_erase(it
);
360 const Product
* InstallerState::FindProduct(
361 BrowserDistribution::Type distribution_type
) const {
362 for (Products::const_iterator scan
= products_
.begin(), end
= products_
.end();
363 scan
!= end
; ++scan
) {
364 if ((*scan
)->is_type(distribution_type
))
370 Version
* InstallerState::GetCurrentVersion(
371 const InstallationState
& machine_state
) const {
372 DCHECK(!products_
.empty());
373 scoped_ptr
<Version
> current_version
;
374 // If we're doing a multi-install, the current version may be either an
375 // existing multi or an existing single product that is being migrated
376 // in place (i.e., Chrome). In the latter case, there is no existing
377 // CHROME_BINARIES installation so we need to search for the product.
378 BrowserDistribution::Type prod_type
;
379 if (package_type_
== MULTI_PACKAGE
) {
380 prod_type
= BrowserDistribution::CHROME_BINARIES
;
381 if (machine_state
.GetProductState(level_
== SYSTEM_LEVEL
,
382 prod_type
) == NULL
) {
383 // Search for a product on which we're operating that is installed in our
385 Products::const_iterator end
= products().end();
386 for (Products::const_iterator scan
= products().begin(); scan
!= end
;
388 BrowserDistribution::Type product_type
=
389 (*scan
)->distribution()->GetType();
390 const ProductState
* state
=
391 machine_state
.GetProductState(level_
== SYSTEM_LEVEL
, product_type
);
392 if (state
!= NULL
&& target_path_
.IsParent(state
->GetSetupPath())) {
393 prod_type
= product_type
;
399 prod_type
= products_
[0]->distribution()->GetType();
401 const ProductState
* product_state
=
402 machine_state
.GetProductState(level_
== SYSTEM_LEVEL
, prod_type
);
404 if (product_state
!= NULL
) {
405 const Version
* version
= NULL
;
407 // Be aware that there might be a pending "new_chrome.exe" already in the
408 // installation path. If so, we use old_version, which holds the version of
409 // "chrome.exe" itself.
410 if (base::PathExists(target_path().Append(kChromeNewExe
)))
411 version
= product_state
->old_version();
414 version
= &product_state
->version();
416 current_version
.reset(new Version(*version
));
419 return current_version
.release();
422 Version
InstallerState::DetermineCriticalVersion(
423 const Version
* current_version
,
424 const Version
& new_version
) const {
425 DCHECK(current_version
== NULL
|| current_version
->IsValid());
426 DCHECK(new_version
.IsValid());
427 if (critical_update_version_
.IsValid() &&
428 (current_version
== NULL
||
429 (current_version
->CompareTo(critical_update_version_
) < 0)) &&
430 new_version
.CompareTo(critical_update_version_
) >= 0) {
431 return critical_update_version_
;
436 bool InstallerState::IsChromeFrameRunning(
437 const InstallationState
& machine_state
) const {
438 return AnyExistsAndIsInUse(machine_state
, CHROME_FRAME_DLL
);
441 bool InstallerState::AreBinariesInUse(
442 const InstallationState
& machine_state
) const {
443 return AnyExistsAndIsInUse(
445 (CHROME_FRAME_HELPER_EXE
| CHROME_FRAME_HELPER_DLL
|
446 CHROME_FRAME_DLL
| CHROME_DLL
));
449 base::FilePath
InstallerState::GetInstallerDirectory(
450 const Version
& version
) const {
451 return target_path().AppendASCII(version
.GetString()).Append(kInstallerDir
);
455 bool InstallerState::IsFileInUse(const base::FilePath
& file
) {
456 // Call CreateFile with a share mode of 0 which should cause this to fail
457 // with ERROR_SHARING_VIOLATION if the file exists and is in-use.
458 return !base::win::ScopedHandle(CreateFile(file
.value().c_str(),
459 GENERIC_WRITE
, 0, NULL
,
460 OPEN_EXISTING
, 0, 0)).IsValid();
463 void InstallerState::Clear() {
464 operation_
= UNINITIALIZED
;
465 target_path_
.clear();
467 state_type_
= BrowserDistribution::CHROME_BROWSER
;
469 multi_package_distribution_
= NULL
;
470 critical_update_version_
= base::Version();
471 level_
= UNKNOWN_LEVEL
;
472 package_type_
= UNKNOWN_PACKAGE_TYPE
;
475 verbose_logging_
= false;
478 bool InstallerState::AnyExistsAndIsInUse(
479 const InstallationState
& machine_state
,
480 uint32 file_bits
) const {
481 static const wchar_t* const kBinaryFileNames
[] = {
484 kChromeFrameHelperDll
,
485 kChromeFrameHelperExe
,
487 DCHECK_NE(file_bits
, 0U);
488 DCHECK_LT(file_bits
, 1U << NUM_BINARIES
);
489 COMPILE_ASSERT(CHROME_DLL
== 1, no_youre_out_of_order
);
490 COMPILE_ASSERT(CHROME_FRAME_DLL
== 2, no_youre_out_of_order
);
491 COMPILE_ASSERT(CHROME_FRAME_HELPER_DLL
== 4, no_youre_out_of_order
);
492 COMPILE_ASSERT(CHROME_FRAME_HELPER_EXE
== 8, no_youre_out_of_order
);
494 // Check only for the current version (i.e., the version we are upgrading
495 // _from_). Later versions from pending in-use updates need not be checked
496 // since the current version is guaranteed to be in use if any such are.
497 scoped_ptr
<Version
> current_version(GetCurrentVersion(machine_state
));
498 if (!current_version
)
500 base::FilePath
directory(
501 target_path().AppendASCII(current_version
->GetString()));
502 for (int i
= 0; i
< NUM_BINARIES
; ++i
) {
503 if (!(file_bits
& (1U << i
)))
505 base::FilePath
file(directory
.Append(kBinaryFileNames
[i
]));
506 if (base::PathExists(file
) && IsFileInUse(file
))
512 void InstallerState::GetExistingExeVersions(
513 std::set
<std::string
>* existing_versions
) const {
515 static const wchar_t* const kChromeFilenames
[] = {
516 installer::kChromeExe
,
517 installer::kChromeNewExe
,
518 installer::kChromeOldExe
,
521 for (int i
= 0; i
< arraysize(kChromeFilenames
); ++i
) {
522 base::FilePath
chrome_exe(target_path().Append(kChromeFilenames
[i
]));
523 scoped_ptr
<FileVersionInfo
> file_version_info(
524 FileVersionInfo::CreateFileVersionInfo(chrome_exe
));
525 if (file_version_info
) {
526 base::string16 version_string
= file_version_info
->file_version();
527 if (!version_string
.empty() && base::IsStringASCII(version_string
))
528 existing_versions
->insert(base::UTF16ToASCII(version_string
));
533 void InstallerState::RemoveOldVersionDirectories(
534 const Version
& new_version
,
535 Version
* existing_version
,
536 const base::FilePath
& temp_path
) const {
538 scoped_ptr
<WorkItem
> item
;
540 std::set
<std::string
> existing_version_strings
;
541 existing_version_strings
.insert(new_version
.GetString());
542 if (existing_version
)
543 existing_version_strings
.insert(existing_version
->GetString());
545 // Make sure not to delete any version dir that is "referenced" by an existing
546 // Chrome executable.
547 GetExistingExeVersions(&existing_version_strings
);
549 // Try to delete all directories that are not in the set we care to keep.
550 base::FileEnumerator
version_enum(target_path(), false,
551 base::FileEnumerator::DIRECTORIES
);
552 for (base::FilePath next_version
= version_enum
.Next(); !next_version
.empty();
553 next_version
= version_enum
.Next()) {
554 base::FilePath
dir_name(next_version
.BaseName());
555 version
= Version(base::UTF16ToASCII(dir_name
.value()));
556 // Delete the version folder if it is less than the new version and not
557 // equal to the old version (if we have an old version).
558 if (version
.IsValid() &&
559 existing_version_strings
.count(version
.GetString()) == 0) {
560 // Note: temporarily log old version deletion at ERROR level to make it
561 // more likely we see this in the installer log.
562 LOG(ERROR
) << "Deleting old version directory: " << next_version
.value();
564 // Attempt to recursively delete the old version dir.
565 bool delete_succeeded
= base::DeleteFile(next_version
, true);
567 // Note: temporarily log old version deletion at ERROR level to make it
568 // more likely we see this in the installer log.
569 LOG_IF(ERROR
, !delete_succeeded
)
570 << "Failed to delete old version directory: " << next_version
.value();
575 void InstallerState::AddComDllList(
576 std::vector
<base::FilePath
>* com_dll_list
) const {
577 std::for_each(products_
.begin(), products_
.end(),
578 std::bind2nd(std::mem_fun(&Product::AddComDllList
),
582 void InstallerState::UpdateStage(installer::InstallerStage stage
) const {
583 InstallUtil::UpdateInstallerStage(system_install(), state_key_
, stage
);
586 void InstallerState::UpdateChannels() const {
587 DCHECK_NE(UNINSTALL
, operation_
);
588 // Update the "ap" value for the product being installed/updated. Use the
589 // current value in the registry since the InstallationState instance used by
590 // the bulk of the installer does not track changes made by UpdateStage.
591 // Create the app's ClientState key if it doesn't exist.
592 ChannelInfo channel_info
;
593 base::win::RegKey state_key
;
595 state_key
.Create(root_key_
,
597 KEY_QUERY_VALUE
| KEY_SET_VALUE
| KEY_WOW64_32KEY
);
598 if (result
== ERROR_SUCCESS
) {
599 channel_info
.Initialize(state_key
);
601 // This is a multi-install product.
602 bool modified
= channel_info
.SetMultiInstall(is_multi_install());
604 if (is_multi_install()) {
605 // Add the appropriate modifiers for all products and their options.
606 for (auto* product
: products_
)
607 modified
|= product
->SetChannelFlags(true, &channel_info
);
609 // Remove all multi-install products from the channel name.
610 modified
|= channel_info
.SetChrome(false);
611 modified
|= channel_info
.SetChromeFrame(false);
612 modified
|= channel_info
.SetAppLauncher(false);
615 VLOG(1) << "ap: " << channel_info
.value();
617 // Write the results if needed.
619 channel_info
.Write(&state_key
);
621 if (is_multi_install()) {
622 // Remove the -stage: modifier since we don't want to propagate that to
623 // the other app_guids.
624 channel_info
.SetStage(NULL
);
626 // Synchronize the other products and the package with this one.
627 ChannelInfo other_info
;
628 for (int i
= 0; i
< BrowserDistribution::NUM_TYPES
; ++i
) {
629 BrowserDistribution::Type type
=
630 static_cast<BrowserDistribution::Type
>(i
);
631 // Skip the app_guid we started with.
632 if (type
== state_type_
)
634 BrowserDistribution
* dist
= NULL
;
635 // Always operate on the binaries.
636 if (i
== BrowserDistribution::CHROME_BINARIES
) {
637 dist
= multi_package_distribution_
;
639 const Product
* product
= FindProduct(type
);
640 // Skip this one if it's for a product we're not operating on.
643 dist
= product
->distribution();
646 state_key
.Create(root_key_
,
647 dist
->GetStateKey().c_str(),
648 KEY_QUERY_VALUE
| KEY_SET_VALUE
| KEY_WOW64_32KEY
);
649 if (result
== ERROR_SUCCESS
) {
650 other_info
.Initialize(state_key
);
651 if (!other_info
.Equals(channel_info
))
652 channel_info
.Write(&state_key
);
654 LOG(ERROR
) << "Failed opening key " << dist
->GetStateKey()
655 << " to update app channels; result: " << result
;
660 LOG(ERROR
) << "Failed opening key " << state_key_
661 << " to update app channels; result: " << result
;
665 void InstallerState::WriteInstallerResult(
666 InstallStatus status
,
667 int string_resource_id
,
668 const std::wstring
* const launch_cmd
) const {
669 // Use a no-rollback list since this is a best-effort deal.
670 scoped_ptr
<WorkItemList
> install_list(
671 WorkItem::CreateNoRollbackWorkItemList());
672 const bool system_install
= this->system_install();
673 // Write the value for all products upon which we're operating.
674 Products::const_iterator end
= products().end();
675 for (Products::const_iterator scan
= products().begin(); scan
!= end
;
677 InstallUtil::AddInstallerResultItems(
678 system_install
, (*scan
)->distribution()->GetStateKey(), status
,
679 string_resource_id
, launch_cmd
, install_list
.get());
681 // And for the binaries if this is a multi-install.
682 if (is_multi_install()) {
683 InstallUtil::AddInstallerResultItems(
684 system_install
, multi_package_binaries_distribution()->GetStateKey(),
685 status
, string_resource_id
, launch_cmd
, install_list
.get());
687 if (!install_list
->Do())
688 LOG(ERROR
) << "Failed to record installer error information in registry.";
691 bool InstallerState::RequiresActiveSetup() const {
692 return system_install() && FindProduct(BrowserDistribution::CHROME_BROWSER
);
695 } // namespace installer