setup_util.cc revision ca12bfac764ba476d6cd062bf1dde12cc64c3f40
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.
4//
5// This file declares util functions for setup project.
6
7#include "chrome/installer/setup/setup_util.h"
8
9#include <windows.h>
10
11#include "base/command_line.h"
12#include "base/file_util.h"
13#include "base/files/file_enumerator.h"
14#include "base/files/file_path.h"
15#include "base/logging.h"
16#include "base/process_util.h"
17#include "base/strings/string_util.h"
18#include "base/version.h"
19#include "base/win/windows_version.h"
20#include "chrome/installer/setup/setup_constants.h"
21#include "chrome/installer/util/copy_tree_work_item.h"
22#include "chrome/installer/util/installation_state.h"
23#include "chrome/installer/util/installer_state.h"
24#include "chrome/installer/util/master_preferences.h"
25#include "chrome/installer/util/util_constants.h"
26#include "chrome/installer/util/work_item.h"
27#include "courgette/courgette.h"
28#include "courgette/third_party/bsdiff.h"
29#include "third_party/bspatch/mbspatch.h"
30
31namespace installer {
32
33namespace {
34
35// Launches |setup_exe| with |command_line|, save --install-archive and its
36// value if present. Returns false if the process failed to launch. Otherwise,
37// waits indefinitely for it to exit and populates |exit_code| as expected. On
38// the off chance that waiting itself fails, |exit_code| is set to
39// WAIT_FOR_EXISTING_FAILED.
40bool LaunchAndWaitForExistingInstall(const base::FilePath& setup_exe,
41                                     const CommandLine& command_line,
42                                     int* exit_code) {
43  DCHECK(exit_code);
44  CommandLine new_cl(setup_exe);
45
46  // Copy over all switches but --install-archive.
47  CommandLine::SwitchMap switches(command_line.GetSwitches());
48  switches.erase(switches::kInstallArchive);
49  for (CommandLine::SwitchMap::const_iterator i = switches.begin();
50       i != switches.end(); ++i) {
51    if (i->second.empty())
52      new_cl.AppendSwitch(i->first);
53    else
54      new_cl.AppendSwitchNative(i->first, i->second);
55  }
56
57  // Copy over all arguments.
58  CommandLine::StringVector args(command_line.GetArgs());
59  for (CommandLine::StringVector::const_iterator i = args.begin();
60       i != args.end(); ++i) {
61    new_cl.AppendArgNative(*i);
62  }
63
64  // Launch the process and wait for it to exit.
65  VLOG(1) << "Launching existing installer with command: "
66          << new_cl.GetCommandLineString();
67  base::ProcessHandle handle = INVALID_HANDLE_VALUE;
68  if (!base::LaunchProcess(new_cl, base::LaunchOptions(), &handle)) {
69    PLOG(ERROR) << "Failed to launch existing installer with command: "
70                << new_cl.GetCommandLineString();
71    return false;
72  }
73  if (!base::WaitForExitCode(handle, exit_code)) {
74    PLOG(DFATAL) << "Failed to get exit code from existing installer";
75    *exit_code = WAIT_FOR_EXISTING_FAILED;
76  } else {
77    VLOG(1) << "Existing installer returned exit code " << *exit_code;
78  }
79  return true;
80}
81
82// Returns true if product |type| cam be meaningfully installed without the
83// --multi-install flag.
84bool SupportsSingleInstall(BrowserDistribution::Type type) {
85  return (type == BrowserDistribution::CHROME_BROWSER ||
86          type == BrowserDistribution::CHROME_FRAME);
87}
88
89}  // namespace
90
91int CourgettePatchFiles(const base::FilePath& src,
92                        const base::FilePath& patch,
93                        const base::FilePath& dest) {
94  VLOG(1) << "Applying Courgette patch " << patch.value()
95          << " to file " << src.value()
96          << " and generating file " << dest.value();
97
98  if (src.empty() || patch.empty() || dest.empty())
99    return installer::PATCH_INVALID_ARGUMENTS;
100
101  const courgette::Status patch_status =
102      courgette::ApplyEnsemblePatch(src.value().c_str(),
103                                    patch.value().c_str(),
104                                    dest.value().c_str());
105  const int exit_code = (patch_status != courgette::C_OK) ?
106      static_cast<int>(patch_status) + kCourgetteErrorOffset : 0;
107
108  LOG_IF(ERROR, exit_code)
109      << "Failed to apply Courgette patch " << patch.value()
110      << " to file " << src.value() << " and generating file " << dest.value()
111      << ". err=" << exit_code;
112
113  return exit_code;
114}
115
116int BsdiffPatchFiles(const base::FilePath& src,
117                     const base::FilePath& patch,
118                     const base::FilePath& dest) {
119  VLOG(1) << "Applying bsdiff patch " << patch.value()
120          << " to file " << src.value()
121          << " and generating file " << dest.value();
122
123  if (src.empty() || patch.empty() || dest.empty())
124    return installer::PATCH_INVALID_ARGUMENTS;
125
126  const int patch_status = courgette::ApplyBinaryPatch(src, patch, dest);
127  const int exit_code = patch_status != OK ?
128                        patch_status + kBsdiffErrorOffset : 0;
129
130  LOG_IF(ERROR, exit_code)
131      << "Failed to apply bsdiff patch " << patch.value()
132      << " to file " << src.value() << " and generating file " << dest.value()
133      << ". err=" << exit_code;
134
135  return exit_code;
136}
137
138Version* GetMaxVersionFromArchiveDir(const base::FilePath& chrome_path) {
139  VLOG(1) << "Looking for Chrome version folder under " << chrome_path.value();
140  Version* version = NULL;
141  base::FileEnumerator version_enum(chrome_path, false,
142      base::FileEnumerator::DIRECTORIES);
143  // TODO(tommi): The version directory really should match the version of
144  // setup.exe.  To begin with, we should at least DCHECK that that's true.
145
146  scoped_ptr<Version> max_version(new Version("0.0.0.0"));
147  bool version_found = false;
148
149  while (!version_enum.Next().empty()) {
150    base::FileEnumerator::FileInfo find_data = version_enum.GetInfo();
151    VLOG(1) << "directory found: " << find_data.GetName().value();
152
153    scoped_ptr<Version> found_version(
154        new Version(WideToASCII(find_data.GetName().value())));
155    if (found_version->IsValid() &&
156        found_version->CompareTo(*max_version.get()) > 0) {
157      max_version.reset(found_version.release());
158      version_found = true;
159    }
160  }
161
162  return (version_found ? max_version.release() : NULL);
163}
164
165base::FilePath FindArchiveToPatch(const InstallationState& original_state,
166                                  const InstallerState& installer_state) {
167  // Check based on the version number advertised to Google Update, since that
168  // is the value used to select a specific differential update. If an archive
169  // can't be found using that, fallback to using the newest version present.
170  base::FilePath patch_source;
171  const ProductState* product =
172      original_state.GetProductState(installer_state.system_install(),
173                                     installer_state.state_type());
174  if (product) {
175    patch_source = installer_state.GetInstallerDirectory(product->version())
176        .Append(installer::kChromeArchive);
177    if (base::PathExists(patch_source))
178      return patch_source;
179  }
180  scoped_ptr<Version> version(
181      installer::GetMaxVersionFromArchiveDir(installer_state.target_path()));
182  if (version) {
183    patch_source = installer_state.GetInstallerDirectory(*version)
184        .Append(installer::kChromeArchive);
185    if (base::PathExists(patch_source))
186      return patch_source;
187  }
188  return base::FilePath();
189}
190
191bool DeleteFileFromTempProcess(const base::FilePath& path,
192                               uint32 delay_before_delete_ms) {
193  static const wchar_t kRunDll32Path[] =
194      L"%SystemRoot%\\System32\\rundll32.exe";
195  wchar_t rundll32[MAX_PATH];
196  DWORD size =
197      ExpandEnvironmentStrings(kRunDll32Path, rundll32, arraysize(rundll32));
198  if (!size || size >= MAX_PATH)
199    return false;
200
201  STARTUPINFO startup = { sizeof(STARTUPINFO) };
202  PROCESS_INFORMATION pi = {0};
203  BOOL ok = ::CreateProcess(NULL, rundll32, NULL, NULL, FALSE, CREATE_SUSPENDED,
204                            NULL, NULL, &startup, &pi);
205  if (ok) {
206    // We use the main thread of the new process to run:
207    //   Sleep(delay_before_delete_ms);
208    //   DeleteFile(path);
209    //   ExitProcess(0);
210    // This runs before the main routine of the process runs, so it doesn't
211    // matter much which executable we choose except that we don't want to
212    // use e.g. a console app that causes a window to be created.
213    size = (path.value().length() + 1) * sizeof(path.value()[0]);
214    void* mem = ::VirtualAllocEx(pi.hProcess, NULL, size, MEM_COMMIT,
215                                 PAGE_READWRITE);
216    if (mem) {
217      SIZE_T written = 0;
218      ::WriteProcessMemory(
219          pi.hProcess, mem, path.value().c_str(),
220          (path.value().size() + 1) * sizeof(path.value()[0]), &written);
221      HMODULE kernel32 = ::GetModuleHandle(L"kernel32.dll");
222      PAPCFUNC sleep = reinterpret_cast<PAPCFUNC>(
223          ::GetProcAddress(kernel32, "Sleep"));
224      PAPCFUNC delete_file = reinterpret_cast<PAPCFUNC>(
225          ::GetProcAddress(kernel32, "DeleteFileW"));
226      PAPCFUNC exit_process = reinterpret_cast<PAPCFUNC>(
227          ::GetProcAddress(kernel32, "ExitProcess"));
228      if (!sleep || !delete_file || !exit_process) {
229        NOTREACHED();
230        ok = FALSE;
231      } else {
232        ::QueueUserAPC(sleep, pi.hThread, delay_before_delete_ms);
233        ::QueueUserAPC(delete_file, pi.hThread,
234                       reinterpret_cast<ULONG_PTR>(mem));
235        ::QueueUserAPC(exit_process, pi.hThread, 0);
236        ::ResumeThread(pi.hThread);
237      }
238    } else {
239      PLOG(ERROR) << "VirtualAllocEx";
240      ::TerminateProcess(pi.hProcess, ~0);
241    }
242    ::CloseHandle(pi.hThread);
243    ::CloseHandle(pi.hProcess);
244  }
245
246  return ok != FALSE;
247}
248
249bool GetExistingHigherInstaller(
250    const InstallationState& original_state,
251    bool system_install,
252    const Version& installer_version,
253    base::FilePath* setup_exe) {
254  DCHECK(setup_exe);
255  bool trying_single_browser = false;
256  const ProductState* existing_state =
257      original_state.GetProductState(system_install,
258                                     BrowserDistribution::CHROME_BINARIES);
259  if (!existing_state) {
260    // The binaries aren't installed, but perhaps a single-install Chrome is.
261    trying_single_browser = true;
262    existing_state =
263        original_state.GetProductState(system_install,
264                                       BrowserDistribution::CHROME_BROWSER);
265  }
266
267  if (!existing_state ||
268      existing_state->version().CompareTo(installer_version) <= 0) {
269    return false;
270  }
271
272  *setup_exe = existing_state->GetSetupPath();
273
274  VLOG_IF(1, !setup_exe->empty()) << "Found a higher version of "
275      << (trying_single_browser ? "single-install Chrome."
276          : "multi-install Chrome binaries.");
277
278  return !setup_exe->empty();
279}
280
281bool DeferToExistingInstall(const base::FilePath& setup_exe,
282                            const CommandLine& command_line,
283                            const InstallerState& installer_state,
284                            const base::FilePath& temp_path,
285                            InstallStatus* install_status) {
286  // Copy a master_preferences file if there is one.
287  base::FilePath prefs_source_path(command_line.GetSwitchValueNative(
288      switches::kInstallerData));
289  base::FilePath prefs_dest_path(installer_state.target_path().AppendASCII(
290      kDefaultMasterPrefs));
291  scoped_ptr<WorkItem> copy_prefs(WorkItem::CreateCopyTreeWorkItem(
292      prefs_source_path, prefs_dest_path, temp_path, WorkItem::ALWAYS,
293      base::FilePath()));
294  // There's nothing to rollback if the copy fails, so punt if so.
295  if (!copy_prefs->Do())
296    copy_prefs.reset();
297
298  int exit_code = 0;
299  if (!LaunchAndWaitForExistingInstall(setup_exe, command_line, &exit_code)) {
300    if (copy_prefs)
301      copy_prefs->Rollback();
302    return false;
303  }
304  *install_status = static_cast<InstallStatus>(exit_code);
305  return true;
306}
307
308// There are 4 disjoint cases => return values {false,true}:
309// (1) Product is being uninstalled => false.
310// (2) Product is being installed => true.
311// (3) Current operation ignores product, product is absent => false.
312// (4) Current operation ignores product, product is present => true.
313bool WillProductBePresentAfterSetup(
314    const installer::InstallerState& installer_state,
315    const installer::InstallationState& machine_state,
316    BrowserDistribution::Type type) {
317  DCHECK(SupportsSingleInstall(type) || installer_state.is_multi_install());
318
319  const ProductState* product_state =
320      machine_state.GetProductState(installer_state.system_install(), type);
321
322  // Determine if the product is present prior to the current operation.
323  bool is_present = false;
324  if (product_state != NULL) {
325    if (type == BrowserDistribution::CHROME_FRAME) {
326      is_present = !product_state->uninstall_command().HasSwitch(
327                        switches::kChromeFrameReadyMode);
328    } else {
329      is_present = true;
330    }
331  }
332
333  bool is_uninstall = installer_state.operation() == InstallerState::UNINSTALL;
334
335  // Determine if current operation affects the product.
336  bool is_affected = false;
337  const Product* product = installer_state.FindProduct(type);
338  if (product != NULL) {
339    if (type == BrowserDistribution::CHROME_FRAME) {
340      // If Chrome Frame is being uninstalled, we don't bother to check
341      // !HasOption(kOptionReadyMode) since CF would not have been installed
342      // in the first place. If for some odd reason it weren't, we would be
343      // conservative, and cause false to be retruned since CF should not be
344      // installed then (so is_uninstall = true and is_affected = true).
345      is_affected = is_uninstall || !product->HasOption(kOptionReadyMode);
346    } else {
347      is_affected = true;
348    }
349  }
350
351  // Decide among {(1),(2),(3),(4)}.
352  return is_affected ? !is_uninstall : is_present;
353}
354
355bool AdjustProcessPriority() {
356  if (base::win::GetVersion() >= base::win::VERSION_VISTA) {
357    DWORD priority_class = ::GetPriorityClass(::GetCurrentProcess());
358    if (priority_class == 0) {
359      PLOG(WARNING) << "Failed to get the process's priority class.";
360    } else if (priority_class == BELOW_NORMAL_PRIORITY_CLASS ||
361               priority_class == IDLE_PRIORITY_CLASS) {
362      BOOL result = ::SetPriorityClass(::GetCurrentProcess(),
363                                       PROCESS_MODE_BACKGROUND_BEGIN);
364      PLOG_IF(WARNING, !result) << "Failed to enter background mode.";
365      return !!result;
366    }
367  }
368  return false;
369}
370
371ScopedTokenPrivilege::ScopedTokenPrivilege(const wchar_t* privilege_name)
372    : is_enabled_(false) {
373  if (!::OpenProcessToken(::GetCurrentProcess(),
374                          TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
375                          token_.Receive())) {
376    return;
377  }
378
379  LUID privilege_luid;
380  if (!::LookupPrivilegeValue(NULL, privilege_name, &privilege_luid)) {
381    token_.Close();
382    return;
383  }
384
385  // Adjust the token's privileges to enable |privilege_name|. If this privilege
386  // was already enabled, |previous_privileges_|.PrivilegeCount will be set to 0
387  // and we then know not to disable this privilege upon destruction.
388  TOKEN_PRIVILEGES tp;
389  tp.PrivilegeCount = 1;
390  tp.Privileges[0].Luid = privilege_luid;
391  tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
392  DWORD return_length;
393  if (!::AdjustTokenPrivileges(token_, FALSE, &tp, sizeof(TOKEN_PRIVILEGES),
394                               &previous_privileges_, &return_length)) {
395    token_.Close();
396    return;
397  }
398
399  is_enabled_ = true;
400}
401
402ScopedTokenPrivilege::~ScopedTokenPrivilege() {
403  if (is_enabled_ && previous_privileges_.PrivilegeCount != 0) {
404    ::AdjustTokenPrivileges(token_, FALSE, &previous_privileges_,
405                            sizeof(TOKEN_PRIVILEGES), NULL, NULL);
406  }
407}
408
409}  // namespace installer
410