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