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 // This file declares util functions for setup project.
7 #include "chrome/installer/setup/setup_util.h"
15 #include "base/command_line.h"
17 #include "base/files/file_enumerator.h"
18 #include "base/files/file_path.h"
19 #include "base/files/file_util.h"
20 #include "base/logging.h"
21 #include "base/numerics/safe_conversions.h"
22 #include "base/process/kill.h"
23 #include "base/process/launch.h"
24 #include "base/process/process_handle.h"
25 #include "base/strings/string_util.h"
26 #include "base/strings/utf_string_conversions.h"
27 #include "base/version.h"
28 #include "base/win/registry.h"
29 #include "base/win/windows_version.h"
30 #include "chrome/installer/setup/setup_constants.h"
31 #include "chrome/installer/util/app_registration_data.h"
32 #include "chrome/installer/util/copy_tree_work_item.h"
33 #include "chrome/installer/util/google_update_constants.h"
34 #include "chrome/installer/util/installation_state.h"
35 #include "chrome/installer/util/installer_state.h"
36 #include "chrome/installer/util/master_preferences.h"
37 #include "chrome/installer/util/util_constants.h"
38 #include "chrome/installer/util/work_item.h"
39 #include "courgette/courgette.h"
40 #include "courgette/third_party/bsdiff.h"
41 #include "third_party/bspatch/mbspatch.h"
47 // Launches |setup_exe| with |command_line|, save --install-archive and its
48 // value if present. Returns false if the process failed to launch. Otherwise,
49 // waits indefinitely for it to exit and populates |exit_code| as expected. On
50 // the off chance that waiting itself fails, |exit_code| is set to
51 // WAIT_FOR_EXISTING_FAILED.
52 bool LaunchAndWaitForExistingInstall(const base::FilePath
& setup_exe
,
53 const base::CommandLine
& command_line
,
56 base::CommandLine
new_cl(setup_exe
);
58 // Copy over all switches but --install-archive.
59 base::CommandLine::SwitchMap
switches(command_line
.GetSwitches());
60 switches
.erase(switches::kInstallArchive
);
61 for (base::CommandLine::SwitchMap::const_iterator i
= switches
.begin();
62 i
!= switches
.end(); ++i
) {
63 if (i
->second
.empty())
64 new_cl
.AppendSwitch(i
->first
);
66 new_cl
.AppendSwitchNative(i
->first
, i
->second
);
69 // Copy over all arguments.
70 base::CommandLine::StringVector
args(command_line
.GetArgs());
71 for (base::CommandLine::StringVector::const_iterator i
= args
.begin();
72 i
!= args
.end(); ++i
) {
73 new_cl
.AppendArgNative(*i
);
76 // Launch the process and wait for it to exit.
77 VLOG(1) << "Launching existing installer with command: "
78 << new_cl
.GetCommandLineString();
79 base::Process process
= base::LaunchProcess(new_cl
, base::LaunchOptions());
80 if (!process
.IsValid()) {
81 PLOG(ERROR
) << "Failed to launch existing installer with command: "
82 << new_cl
.GetCommandLineString();
85 if (!process
.WaitForExit(exit_code
)) {
86 PLOG(DFATAL
) << "Failed to get exit code from existing installer";
87 *exit_code
= WAIT_FOR_EXISTING_FAILED
;
89 VLOG(1) << "Existing installer returned exit code " << *exit_code
;
94 // Returns true if product |type| cam be meaningfully installed without the
95 // --multi-install flag.
96 bool SupportsSingleInstall(BrowserDistribution::Type type
) {
97 return (type
== BrowserDistribution::CHROME_BROWSER
||
98 type
== BrowserDistribution::CHROME_FRAME
);
103 int CourgettePatchFiles(const base::FilePath
& src
,
104 const base::FilePath
& patch
,
105 const base::FilePath
& dest
) {
106 VLOG(1) << "Applying Courgette patch " << patch
.value()
107 << " to file " << src
.value()
108 << " and generating file " << dest
.value();
110 if (src
.empty() || patch
.empty() || dest
.empty())
111 return installer::PATCH_INVALID_ARGUMENTS
;
113 const courgette::Status patch_status
=
114 courgette::ApplyEnsemblePatch(src
.value().c_str(),
115 patch
.value().c_str(),
116 dest
.value().c_str());
117 const int exit_code
= (patch_status
!= courgette::C_OK
) ?
118 static_cast<int>(patch_status
) + kCourgetteErrorOffset
: 0;
120 LOG_IF(ERROR
, exit_code
)
121 << "Failed to apply Courgette patch " << patch
.value()
122 << " to file " << src
.value() << " and generating file " << dest
.value()
123 << ". err=" << exit_code
;
128 int BsdiffPatchFiles(const base::FilePath
& src
,
129 const base::FilePath
& patch
,
130 const base::FilePath
& dest
) {
131 VLOG(1) << "Applying bsdiff patch " << patch
.value()
132 << " to file " << src
.value()
133 << " and generating file " << dest
.value();
135 if (src
.empty() || patch
.empty() || dest
.empty())
136 return installer::PATCH_INVALID_ARGUMENTS
;
138 const int patch_status
= courgette::ApplyBinaryPatch(src
, patch
, dest
);
139 const int exit_code
= patch_status
!= OK
?
140 patch_status
+ kBsdiffErrorOffset
: 0;
142 LOG_IF(ERROR
, exit_code
)
143 << "Failed to apply bsdiff patch " << patch
.value()
144 << " to file " << src
.value() << " and generating file " << dest
.value()
145 << ". err=" << exit_code
;
150 Version
* GetMaxVersionFromArchiveDir(const base::FilePath
& chrome_path
) {
151 VLOG(1) << "Looking for Chrome version folder under " << chrome_path
.value();
152 base::FileEnumerator
version_enum(chrome_path
, false,
153 base::FileEnumerator::DIRECTORIES
);
154 // TODO(tommi): The version directory really should match the version of
155 // setup.exe. To begin with, we should at least DCHECK that that's true.
157 scoped_ptr
<Version
> max_version(new Version("0.0.0.0"));
158 bool version_found
= false;
160 while (!version_enum
.Next().empty()) {
161 base::FileEnumerator::FileInfo find_data
= version_enum
.GetInfo();
162 VLOG(1) << "directory found: " << find_data
.GetName().value();
164 scoped_ptr
<Version
> found_version(
165 new Version(base::UTF16ToASCII(find_data
.GetName().value())));
166 if (found_version
->IsValid() &&
167 found_version
->CompareTo(*max_version
.get()) > 0) {
168 max_version
.reset(found_version
.release());
169 version_found
= true;
173 return (version_found
? max_version
.release() : NULL
);
176 base::FilePath
FindArchiveToPatch(const InstallationState
& original_state
,
177 const InstallerState
& installer_state
,
178 const base::Version
& desired_version
) {
179 if (desired_version
.IsValid()) {
180 base::FilePath
archive(installer_state
.GetInstallerDirectory(
181 desired_version
).Append(kChromeArchive
));
182 return base::PathExists(archive
) ? archive
: base::FilePath();
185 // Check based on the version number advertised to Google Update, since that
186 // is the value used to select a specific differential update. If an archive
187 // can't be found using that, fallback to using the newest version present.
188 base::FilePath patch_source
;
189 const ProductState
* product
=
190 original_state
.GetProductState(installer_state
.system_install(),
191 installer_state
.state_type());
193 patch_source
= installer_state
.GetInstallerDirectory(product
->version())
194 .Append(installer::kChromeArchive
);
195 if (base::PathExists(patch_source
))
198 scoped_ptr
<Version
> version(
199 installer::GetMaxVersionFromArchiveDir(installer_state
.target_path()));
201 patch_source
= installer_state
.GetInstallerDirectory(*version
)
202 .Append(installer::kChromeArchive
);
203 if (base::PathExists(patch_source
))
206 return base::FilePath();
209 bool DeleteFileFromTempProcess(const base::FilePath
& path
,
210 uint32 delay_before_delete_ms
) {
211 static const wchar_t kRunDll32Path
[] =
212 L
"%SystemRoot%\\System32\\rundll32.exe";
213 wchar_t rundll32
[MAX_PATH
];
215 ExpandEnvironmentStrings(kRunDll32Path
, rundll32
, arraysize(rundll32
));
216 if (!size
|| size
>= MAX_PATH
)
219 STARTUPINFO startup
= { sizeof(STARTUPINFO
) };
220 PROCESS_INFORMATION pi
= {0};
221 BOOL ok
= ::CreateProcess(NULL
, rundll32
, NULL
, NULL
, FALSE
, CREATE_SUSPENDED
,
222 NULL
, NULL
, &startup
, &pi
);
224 // We use the main thread of the new process to run:
225 // Sleep(delay_before_delete_ms);
228 // This runs before the main routine of the process runs, so it doesn't
229 // matter much which executable we choose except that we don't want to
230 // use e.g. a console app that causes a window to be created.
231 size
= static_cast<DWORD
>(
232 (path
.value().length() + 1) * sizeof(path
.value()[0]));
233 void* mem
= ::VirtualAllocEx(pi
.hProcess
, NULL
, size
, MEM_COMMIT
,
237 ::WriteProcessMemory(
238 pi
.hProcess
, mem
, path
.value().c_str(),
239 (path
.value().size() + 1) * sizeof(path
.value()[0]), &written
);
240 HMODULE kernel32
= ::GetModuleHandle(L
"kernel32.dll");
241 PAPCFUNC sleep
= reinterpret_cast<PAPCFUNC
>(
242 ::GetProcAddress(kernel32
, "Sleep"));
243 PAPCFUNC delete_file
= reinterpret_cast<PAPCFUNC
>(
244 ::GetProcAddress(kernel32
, "DeleteFileW"));
245 PAPCFUNC exit_process
= reinterpret_cast<PAPCFUNC
>(
246 ::GetProcAddress(kernel32
, "ExitProcess"));
247 if (!sleep
|| !delete_file
|| !exit_process
) {
251 ::QueueUserAPC(sleep
, pi
.hThread
, delay_before_delete_ms
);
252 ::QueueUserAPC(delete_file
, pi
.hThread
,
253 reinterpret_cast<ULONG_PTR
>(mem
));
254 ::QueueUserAPC(exit_process
, pi
.hThread
, 0);
255 ::ResumeThread(pi
.hThread
);
258 PLOG(ERROR
) << "VirtualAllocEx";
259 ::TerminateProcess(pi
.hProcess
, ~static_cast<UINT
>(0));
261 ::CloseHandle(pi
.hThread
);
262 ::CloseHandle(pi
.hProcess
);
268 bool GetExistingHigherInstaller(
269 const InstallationState
& original_state
,
271 const Version
& installer_version
,
272 base::FilePath
* setup_exe
) {
274 bool trying_single_browser
= false;
275 const ProductState
* existing_state
=
276 original_state
.GetProductState(system_install
,
277 BrowserDistribution::CHROME_BINARIES
);
278 if (!existing_state
) {
279 // The binaries aren't installed, but perhaps a single-install Chrome is.
280 trying_single_browser
= true;
282 original_state
.GetProductState(system_install
,
283 BrowserDistribution::CHROME_BROWSER
);
286 if (!existing_state
||
287 existing_state
->version().CompareTo(installer_version
) <= 0) {
291 *setup_exe
= existing_state
->GetSetupPath();
293 VLOG_IF(1, !setup_exe
->empty()) << "Found a higher version of "
294 << (trying_single_browser
? "single-install Chrome."
295 : "multi-install Chrome binaries.");
297 return !setup_exe
->empty();
300 bool DeferToExistingInstall(const base::FilePath
& setup_exe
,
301 const base::CommandLine
& command_line
,
302 const InstallerState
& installer_state
,
303 const base::FilePath
& temp_path
,
304 InstallStatus
* install_status
) {
305 // Copy a master_preferences file if there is one.
306 base::FilePath
prefs_source_path(command_line
.GetSwitchValueNative(
307 switches::kInstallerData
));
308 base::FilePath
prefs_dest_path(installer_state
.target_path().AppendASCII(
309 kDefaultMasterPrefs
));
310 scoped_ptr
<WorkItem
> copy_prefs(WorkItem::CreateCopyTreeWorkItem(
311 prefs_source_path
, prefs_dest_path
, temp_path
, WorkItem::ALWAYS
,
313 // There's nothing to rollback if the copy fails, so punt if so.
314 if (!copy_prefs
->Do())
318 if (!LaunchAndWaitForExistingInstall(setup_exe
, command_line
, &exit_code
)) {
320 copy_prefs
->Rollback();
323 *install_status
= static_cast<InstallStatus
>(exit_code
);
327 // There are 4 disjoint cases => return values {false,true}:
328 // (1) Product is being uninstalled => false.
329 // (2) Product is being installed => true.
330 // (3) Current operation ignores product, product is absent => false.
331 // (4) Current operation ignores product, product is present => true.
332 bool WillProductBePresentAfterSetup(
333 const installer::InstallerState
& installer_state
,
334 const installer::InstallationState
& machine_state
,
335 BrowserDistribution::Type type
) {
336 DCHECK(SupportsSingleInstall(type
) || installer_state
.is_multi_install());
338 const ProductState
* product_state
=
339 machine_state
.GetProductState(installer_state
.system_install(), type
);
341 // Determine if the product is present prior to the current operation.
342 bool is_present
= (product_state
!= NULL
);
343 bool is_uninstall
= installer_state
.operation() == InstallerState::UNINSTALL
;
345 // Determine if current operation affects the product.
346 const Product
* product
= installer_state
.FindProduct(type
);
347 bool is_affected
= (product
!= NULL
);
349 // Decide among {(1),(2),(3),(4)}.
350 return is_affected
? !is_uninstall
: is_present
;
353 bool AdjustProcessPriority() {
354 if (base::win::GetVersion() >= base::win::VERSION_VISTA
) {
355 DWORD priority_class
= ::GetPriorityClass(::GetCurrentProcess());
356 if (priority_class
== 0) {
357 PLOG(WARNING
) << "Failed to get the process's priority class.";
358 } else if (priority_class
== BELOW_NORMAL_PRIORITY_CLASS
||
359 priority_class
== IDLE_PRIORITY_CLASS
) {
360 BOOL result
= ::SetPriorityClass(::GetCurrentProcess(),
361 PROCESS_MODE_BACKGROUND_BEGIN
);
362 PLOG_IF(WARNING
, !result
) << "Failed to enter background mode.";
369 void MigrateGoogleUpdateStateMultiToSingle(
371 BrowserDistribution::Type to_migrate
,
372 const installer::InstallationState
& machine_state
) {
373 const HKEY root
= system_level
? HKEY_LOCAL_MACHINE
: HKEY_CURRENT_USER
;
374 const ProductState
* product
= NULL
;
375 BrowserDistribution
* dist
= NULL
;
376 LONG result
= ERROR_SUCCESS
;
377 base::win::RegKey state_key
;
379 Product
product_to_migrate(
380 BrowserDistribution::GetSpecificDistribution(to_migrate
));
382 // Copy usagestats from the binaries to the product's ClientState key.
383 product
= machine_state
.GetProductState(system_level
,
384 BrowserDistribution::CHROME_BINARIES
);
385 DWORD usagestats
= 0;
386 if (product
&& product
->GetUsageStats(&usagestats
)) {
387 dist
= product_to_migrate
.distribution();
388 result
= state_key
.Open(root
, dist
->GetStateKey().c_str(),
390 if (result
!= ERROR_SUCCESS
) {
391 LOG(ERROR
) << "Failed opening ClientState key for "
392 << dist
->GetDisplayName() << " to migrate usagestats.";
394 state_key
.WriteValue(google_update::kRegUsageStatsField
, usagestats
);
398 // Remove the migrating product from the "ap" value of other multi-install
400 for (int i
= 0; i
< BrowserDistribution::NUM_TYPES
; ++i
) {
401 BrowserDistribution::Type type
=
402 static_cast<BrowserDistribution::Type
>(i
);
403 if (type
== to_migrate
)
405 product
= machine_state
.GetProductState(system_level
, type
);
406 if (product
&& product
->is_multi_install()) {
407 installer::ChannelInfo channel_info
;
408 dist
= BrowserDistribution::GetSpecificDistribution(type
);
409 result
= state_key
.Open(root
, dist
->GetStateKey().c_str(),
410 KEY_QUERY_VALUE
| KEY_SET_VALUE
);
411 if (result
== ERROR_SUCCESS
&&
412 channel_info
.Initialize(state_key
) &&
413 product_to_migrate
.SetChannelFlags(false, &channel_info
)) {
414 VLOG(1) << "Moving " << dist
->GetDisplayName()
415 << " to channel: " << channel_info
.value();
416 channel_info
.Write(&state_key
);
421 // Remove -multi, all product modifiers, and everything else but the channel
422 // name from the "ap" value of the product to migrate.
423 dist
= product_to_migrate
.distribution();
424 result
= state_key
.Open(root
, dist
->GetStateKey().c_str(),
425 KEY_QUERY_VALUE
| KEY_SET_VALUE
);
426 if (result
== ERROR_SUCCESS
) {
427 installer::ChannelInfo channel_info
;
428 if (!channel_info
.Initialize(state_key
)) {
429 LOG(ERROR
) << "Failed reading " << dist
->GetDisplayName()
431 } else if (channel_info
.RemoveAllModifiersAndSuffixes()) {
432 VLOG(1) << "Moving " << dist
->GetDisplayName()
433 << " to channel: " << channel_info
.value();
434 channel_info
.Write(&state_key
);
439 bool IsUninstallSuccess(InstallStatus install_status
) {
440 // The following status values represent failed uninstalls:
441 // 15: CHROME_NOT_INSTALLED
442 // 20: UNINSTALL_FAILED
443 // 21: UNINSTALL_CANCELLED
444 return (install_status
== UNINSTALL_SUCCESSFUL
||
445 install_status
== UNINSTALL_REQUIRES_REBOOT
);
448 bool ContainsUnsupportedSwitch(const base::CommandLine
& cmd_line
) {
449 static const char* const kLegacySwitches
[] = {
450 // Chrome Frame ready-mode.
453 "ready-mode-temp-opt-out",
454 "ready-mode-end-temp-opt-out",
455 // Chrome Frame quick-enable.
457 // Installation of Chrome Frame.
459 "migrate-chrome-frame",
460 // Stand-alone App Launcher.
464 for (size_t i
= 0; i
< arraysize(kLegacySwitches
); ++i
) {
465 if (cmd_line
.HasSwitch(kLegacySwitches
[i
]))
471 bool IsProcessorSupported() {
472 return base::CPU().has_sse2();
475 base::string16
GetRegistrationDataCommandKey(
476 const AppRegistrationData
& reg_data
,
477 const wchar_t* name
) {
478 base::string16
cmd_key(reg_data
.GetVersionKey());
479 cmd_key
.append(1, base::FilePath::kSeparators
[0])
480 .append(google_update::kRegCommandsKey
)
481 .append(1, base::FilePath::kSeparators
[0])
486 void DeleteRegistryKeyPartial(
488 const base::string16
& path
,
489 const std::vector
<base::string16
>& keys_to_preserve
) {
490 // Downcase the list of keys to preserve (all must be ASCII strings).
491 std::set
<base::string16
> lowered_keys_to_preserve
;
493 keys_to_preserve
.begin(), keys_to_preserve
.end(),
494 std::inserter(lowered_keys_to_preserve
, lowered_keys_to_preserve
.begin()),
495 [](const base::string16
& str
) {
496 DCHECK(!str
.empty());
497 DCHECK(base::IsStringASCII(str
));
498 return base::ToLowerASCII(str
);
500 base::win::RegKey key
;
501 LONG result
= key
.Open(root
, path
.c_str(), (KEY_ENUMERATE_SUB_KEYS
|
502 KEY_QUERY_VALUE
| KEY_SET_VALUE
));
503 if (result
!= ERROR_SUCCESS
) {
504 LOG_IF(ERROR
, result
!= ERROR_FILE_NOT_FOUND
) << "Failed to open " << path
505 << "; result = " << result
;
509 // Repeatedly iterate over all subkeys deleting those that should not be
510 // preserved until only those remain. Multiple passes are needed since
511 // deleting one key may change the enumeration order of all remaining keys.
513 // Subkeys or values to be skipped on subsequent passes.
514 std::set
<base::string16
> to_skip
;
516 const size_t kMaxKeyNameLength
= 256; // MSDN says 255; +1 for terminator.
517 base::string16
name(kMaxKeyNameLength
, base::char16());
518 bool did_delete
= false; // True if at least one item was deleted.
520 DWORD name_length
= base::saturated_cast
<DWORD
>(name
.capacity());
521 name
.resize(name_length
);
522 result
= ::RegEnumKeyEx(key
.Handle(), index
, &name
[0], &name_length
,
523 nullptr, nullptr, nullptr, nullptr);
524 if (result
== ERROR_MORE_DATA
) {
525 // Unexpected, but perhaps the max key name length was raised. MSDN
526 // doesn't clearly say that name_length will contain the necessary
527 // length in this case, so double the buffer and try again.
528 name
.reserve(name
.capacity() * 2);
531 if (result
== ERROR_NO_MORE_ITEMS
) {
533 break; // All subkeys were deleted. The job is done.
534 // Otherwise, loop again.
539 if (result
!= ERROR_SUCCESS
)
541 // Shrink the string to the actual length of the name.
542 name
.resize(name_length
);
544 // Skip over this key if it couldn't be deleted on a previous iteration.
545 if (to_skip
.count(name
)) {
550 // Skip over this key if it is one of the keys to preserve.
551 if (base::IsStringASCII(name
) &&
552 lowered_keys_to_preserve
.count(base::ToLowerASCII(name
))) {
553 // Add the true name of the key to the list of keys to skip for subsequent
555 to_skip
.insert(name
);
561 result
= key
.DeleteKey(name
.c_str());
562 if (result
!= ERROR_SUCCESS
) {
563 LOG(ERROR
) << "Failed to delete subkey " << name
<< " under path "
565 // Skip over this key on subsequent iterations.
566 to_skip
.insert(name
);
573 // Delete the key if it no longer has any subkeys.
574 if (to_skip
.empty()) {
575 result
= key
.DeleteEmptyKey(L
"");
576 LOG_IF(ERROR
, result
!= ERROR_SUCCESS
) << "Failed to delete empty key "
577 << path
<< "; result: " << result
;
581 // Delete all values since subkeys are being preserved.
586 DWORD name_length
= base::saturated_cast
<int16_t>(name
.capacity());
587 name
.resize(name_length
);
588 result
= ::RegEnumValue(key
.Handle(), index
, &name
[0], &name_length
,
589 nullptr, nullptr, nullptr, nullptr);
590 if (result
== ERROR_MORE_DATA
) {
592 static_cast<DWORD
>(std::numeric_limits
<int16_t>::max())) {
593 // Double the space to hold the value name and try again.
594 name
.reserve(name
.capacity() * 2);
597 // Otherwise, the max has been exceeded. Nothing more to be done.
600 if (result
== ERROR_NO_MORE_ITEMS
) {
602 break; // All values were deleted. The job is done.
603 // Otherwise, loop again.
608 if (result
!= ERROR_SUCCESS
)
610 // Shrink the string to the actual length of the name.
611 name
.resize(name_length
);
613 // Skip over this value if it couldn't be deleted on a previous iteration.
614 if (to_skip
.count(name
)) {
619 // Delete this value.
620 result
= key
.DeleteValue(name
.c_str());
621 if (result
!= ERROR_SUCCESS
) {
622 LOG(ERROR
) << "Failed to delete value " << name
<< " in key " << path
;
623 // Skip over this value on subsequent iterations.
624 to_skip
.insert(name
);
632 ScopedTokenPrivilege::ScopedTokenPrivilege(const wchar_t* privilege_name
)
633 : is_enabled_(false) {
635 if (!::OpenProcessToken(::GetCurrentProcess(),
636 TOKEN_ADJUST_PRIVILEGES
| TOKEN_QUERY
,
640 token_
.Set(temp_handle
);
643 if (!::LookupPrivilegeValue(NULL
, privilege_name
, &privilege_luid
)) {
648 // Adjust the token's privileges to enable |privilege_name|. If this privilege
649 // was already enabled, |previous_privileges_|.PrivilegeCount will be set to 0
650 // and we then know not to disable this privilege upon destruction.
652 tp
.PrivilegeCount
= 1;
653 tp
.Privileges
[0].Luid
= privilege_luid
;
654 tp
.Privileges
[0].Attributes
= SE_PRIVILEGE_ENABLED
;
656 if (!::AdjustTokenPrivileges(token_
.Get(), FALSE
, &tp
,
657 sizeof(TOKEN_PRIVILEGES
),
658 &previous_privileges_
, &return_length
)) {
666 ScopedTokenPrivilege::~ScopedTokenPrivilege() {
667 if (is_enabled_
&& previous_privileges_
.PrivilegeCount
!= 0) {
668 ::AdjustTokenPrivileges(token_
.Get(), FALSE
, &previous_privileges_
,
669 sizeof(TOKEN_PRIVILEGES
), NULL
, NULL
);
673 } // namespace installer