diff options
-rw-r--r-- | chrome/installer/installer.gyp | 3 | ||||
-rw-r--r-- | chrome/installer/setup/install.cc | 12 | ||||
-rw-r--r-- | chrome/installer/setup/setup_main.cc | 46 | ||||
-rw-r--r-- | chrome/installer/setup/uninstall.cc | 62 | ||||
-rw-r--r-- | chrome/installer/util/delete_after_reboot_helper.cc | 375 | ||||
-rw-r--r-- | chrome/installer/util/delete_after_reboot_helper.h | 66 | ||||
-rw-r--r-- | chrome/installer/util/delete_after_reboot_helper_unittest.cc | 186 | ||||
-rw-r--r-- | chrome/installer/util/util_constants.h | 3 |
8 files changed, 736 insertions, 17 deletions
diff --git a/chrome/installer/installer.gyp b/chrome/installer/installer.gyp index b49f877..df05ed1 100644 --- a/chrome/installer/installer.gyp +++ b/chrome/installer/installer.gyp @@ -41,6 +41,8 @@ 'util/create_dir_work_item.h', 'util/create_reg_key_work_item.cc', 'util/create_reg_key_work_item.h', + 'util/delete_after_reboot_helper.cc', + 'util/delete_after_reboot_helper.h', 'util/delete_reg_value_work_item.cc', 'util/delete_reg_value_work_item.h', 'util/delete_tree_work_item.cc', @@ -150,6 +152,7 @@ 'util/copy_tree_work_item_unittest.cc', 'util/create_dir_work_item_unittest.cc', 'util/create_reg_key_work_item_unittest.cc', + 'util/delete_after_reboot_helper_unittest.cc', 'util/delete_reg_value_work_item_unittest.cc', 'util/delete_tree_work_item_unittest.cc', 'util/google_chrome_distribution_unittest.cc', diff --git a/chrome/installer/setup/install.cc b/chrome/installer/setup/install.cc index 24d2f80..1bc8164 100644 --- a/chrome/installer/setup/install.cc +++ b/chrome/installer/setup/install.cc @@ -15,6 +15,7 @@ #include "chrome/installer/setup/setup_constants.h" #include "chrome/installer/util/browser_distribution.h" #include "chrome/installer/util/create_reg_key_work_item.h" +#include "chrome/installer/util/delete_after_reboot_helper.h" #include "chrome/installer/util/google_update_constants.h" #include "chrome/installer/util/helper.h" #include "chrome/installer/util/install_util.h" @@ -95,7 +96,7 @@ void AddUninstallShortcutWorkItems(HKEY reg_root, uninstall_cmd.append(L"\" --"); uninstall_cmd.append(installer_util::switches::kUninstall); -#ifdef CHROME_FRAME_BUILD +#if defined(CHROME_FRAME_BUILD) uninstall_cmd.append(L" --"); uninstall_cmd.append(installer_util::switches::kForceUninstall); uninstall_cmd.append(L" --"); @@ -246,7 +247,7 @@ bool CreateOrUpdateChromeShortcuts(const std::wstring& exe_path, std::wstring arguments(L" --"); arguments.append(installer_util::switches::kUninstall); -#ifdef CHROME_FRAME_BUILD +#if defined(CHROME_FRAME_BUILD) arguments.append(L" --"); arguments.append(installer_util::switches::kForceUninstall); arguments.append(L" --"); @@ -459,6 +460,13 @@ bool InstallNewVersion(const std::wstring& exe_path, if (reg_root != HKEY_LOCAL_MACHINE && reg_root != HKEY_CURRENT_USER) return false; +#if defined(CHROME_FRAME_BUILD) + // Make sure that we don't end up deleting installed files on next reboot. + if (!RemoveFromMovesPendingReboot(install_path.c_str())) { + LOG(ERROR) << "Error accessing pending moves value."; + } +#endif + scoped_ptr<WorkItemList> install_list(WorkItem::CreateWorkItemList()); // A temp directory that work items need and the actual install directory. install_list->AddCreateDirWorkItem(FilePath::FromWStringHack(temp_dir)); diff --git a/chrome/installer/setup/setup_main.cc b/chrome/installer/setup/setup_main.cc index 8b70a1b..3b5377d 100644 --- a/chrome/installer/setup/setup_main.cc +++ b/chrome/installer/setup/setup_main.cc @@ -5,6 +5,7 @@ #include <string> #include <windows.h> #include <msi.h> +#include <shellapi.h> #include <shlobj.h> #include "base/at_exit.h" @@ -13,6 +14,7 @@ #include "base/file_util.h" #include "base/path_service.h" #include "base/registry.h" +#include "base/scoped_handle_win.h" #include "base/string_util.h" #include "base/win_util.h" #include "chrome/installer/setup/install.h" @@ -413,7 +415,7 @@ bool HandleNonInstallCmdLineOptions(const CommandLine& cmd_line, exit_code = ShowEULADialog(inner_frame); if (installer_util::EULA_REJECTED != exit_code) GoogleUpdateSettings::SetEULAConsent(true); - return true;; + return true; } else if (cmd_line.HasSwitch( installer_util::switches::kRegisterChromeBrowser)) { // If --register-chrome-browser option is specified, register all @@ -464,6 +466,40 @@ bool HandleNonInstallCmdLineOptions(const CommandLine& cmd_line, return false; } +bool ShowRebootDialog() { + // Get a token for this process. + HANDLE token; + if (!OpenProcessToken(GetCurrentProcess(), + TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, + &token)) { + LOG(ERROR) << "Failed to open token."; + return false; + } + + // Use a ScopedHandle to keep track of and eventually close our handle. + // TODO(robertshield): Add a Receive() method to base's ScopedHandle. + ScopedHandle scoped_handle(token); + + // Get the LUID for the shutdown privilege. + TOKEN_PRIVILEGES tkp = {0}; + LookupPrivilegeValue(NULL, SE_SHUTDOWN_NAME, &tkp.Privileges[0].Luid); + tkp.PrivilegeCount = 1; + tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + + // Get the shutdown privilege for this process. + AdjustTokenPrivileges(token, FALSE, &tkp, 0, + reinterpret_cast<PTOKEN_PRIVILEGES>(NULL), 0); + if (GetLastError() != ERROR_SUCCESS) { + LOG(ERROR) << "Unable to get shutdown privileges."; + return false; + } + + // Popup a dialog that will prompt to reboot using the default system message. + // TODO(robertshield): Add a localized, more specific string to the prompt. + RestartDialog(NULL, NULL, EWX_REBOOT | EWX_FORCEIFHUNG); + return true; +} + } // namespace int WINAPI wWinMain(HINSTANCE instance, HINSTANCE prev_instance, @@ -550,7 +586,15 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE prev_instance, prefs.get()); } + if (install_status == installer_util::UNINSTALL_REQUIRES_REBOOT) { + install_status = installer_util::UNINSTALL_SUCCESSFUL; +#if defined(CHROME_FRAME_BUILD) + ShowRebootDialog(); +#endif + } + CoUninitialize(); + BrowserDistribution* dist = BrowserDistribution::GetDistribution(); return dist->GetInstallReturnCode(install_status); } diff --git a/chrome/installer/setup/uninstall.cc b/chrome/installer/setup/uninstall.cc index 978c056..2ad8cf2 100644 --- a/chrome/installer/setup/uninstall.cc +++ b/chrome/installer/setup/uninstall.cc @@ -17,6 +17,7 @@ #include "chrome/installer/setup/install.h" #include "chrome/installer/setup/setup_constants.h" #include "chrome/installer/util/browser_distribution.h" +#include "chrome/installer/util/delete_after_reboot_helper.h" #include "chrome/installer/util/helper.h" #include "chrome/installer/util/install_util.h" #include "chrome/installer/util/logging_installer.h" @@ -137,18 +138,27 @@ bool DeleteEmptyParentDir(const std::wstring& path) { return ret; } -// Deletes all installed files of Chromium and Folders. Before deleting it +enum DeleteResult { + DELETE_SUCCEEDED, + DELETE_FAILED, + DELETE_REQUIRES_REBOOT +}; + +// Deletes all installed files of Chromium and Folders or schedules them for +// deletion on reboot if they are in use. Before deleting it // needs to move setup.exe in a temp folder because the current process -// is using that file. It returns false when it can not get the path to -// installation folder, in all other cases it returns true even in case -// of error (only logs the error). -bool DeleteFilesAndFolders(const std::wstring& exe_path, bool system_uninstall, - const installer::Version& installed_version, +// is using that file. +// Returns DELETE_SUCCEEDED if all files were successfully delete. +// Returns DELETE_FAILED if it could not get the path to the install dir. +// Returns DELETE_REQUIRES_REBOOT if the files were in use and so were +// scheduled for deletion on next reboot. +DeleteResult DeleteFilesAndFolders(const std::wstring& exe_path, + bool system_uninstall, const installer::Version& installed_version, std::wstring* local_state_path, bool delete_profile) { std::wstring install_path(installer::GetChromeInstallPath(system_uninstall)); if (install_path.empty()) { LOG(ERROR) << "Could not get installation destination path."; - return false; // Nothing else we can do for uninstall, so we return. + return DELETE_FAILED; // Nothing else we can do to uninstall, so we return. } else { LOG(INFO) << "install destination path: " << install_path; } @@ -179,27 +189,46 @@ bool DeleteFilesAndFolders(const std::wstring& exe_path, bool system_uninstall, file_util::CopyFile(user_local_file, path); } + DeleteResult result = DELETE_SUCCEEDED; + LOG(INFO) << "Deleting install path " << install_path; if (!file_util::Delete(install_path, true)) { LOG(ERROR) << "Failed to delete folder (1st try): " << install_path; +#if defined(CHROME_FRAME_BUILD) + // We don't try killing Chrome processes for Chrome Frame builds since + // that is unlikely to help. Instead, schedule files for deletion and + // return a value that will trigger a reboot prompt. + ScheduleDirectoryForDeletion(install_path.c_str()); + result = DELETE_REQUIRES_REBOOT; +#else // Try closing any running chrome processes and deleting files once again. CloseAllChromeProcesses(); - if (!file_util::Delete(install_path, true)) + if (!file_util::Delete(install_path, true)) { LOG(ERROR) << "Failed to delete folder (2nd try): " << install_path; + result = DELETE_FAILED; + } +#endif } if (delete_profile) { LOG(INFO) << "Deleting user profile" << user_local_state.value(); - if (!file_util::Delete(user_local_state, true)) - LOG(ERROR) << "Failed to delete user profle dir: " + if (!file_util::Delete(user_local_state, true)) { + LOG(ERROR) << "Failed to delete user profile dir: " << user_local_state.value(); +#if defined(CHROME_FRAME_BUILD) + ScheduleDirectoryForDeletion(user_local_state.value().c_str()); + result = DELETE_REQUIRES_REBOOT; +#else + result = DELETE_FAILED; +#endif + } DeleteEmptyParentDir(user_local_state.value()); } // Now check and delete if the parent directories are empty // For example Google\Chrome or Chromium DeleteEmptyParentDir(install_path); - return true; + return result; } // This method tries to delete a registry key and logs an error message @@ -368,7 +397,9 @@ installer_util::InstallStatus installer_setup::UninstallChrome( if (force_uninstall) { // Since --force-uninstall command line option is used, we are going to // do silent uninstall. Try to close all running Chrome instances. +#if !defined(CHROME_FRAME_BUILD) CloseAllChromeProcesses(); +#endif } else { // no --force-uninstall so lets show some UI dialog boxes. status = IsChromeActiveOrUserCancelled(system_uninstall); if (status != installer_util::UNINSTALL_CONFIRMED && @@ -469,9 +500,14 @@ installer_util::InstallStatus installer_setup::UninstallChrome( (cmd_line.HasSwitch(installer_util::switches::kDeleteProfile)); std::wstring local_state_path; ret = installer_util::UNINSTALL_SUCCESSFUL; - if (!DeleteFilesAndFolders(exe_path, system_uninstall, *installed_version, - &local_state_path, delete_profile)) + + DeleteResult delete_result = DeleteFilesAndFolders(exe_path, + system_uninstall, *installed_version, &local_state_path, delete_profile); + if (delete_result == DELETE_FAILED) { ret = installer_util::UNINSTALL_FAILED; + } else if (delete_result == DELETE_REQUIRES_REBOOT) { + ret = installer_util::UNINSTALL_REQUIRES_REBOOT; + } if (!force_uninstall) { LOG(INFO) << "Uninstallation complete. Launching Uninstall survey."; diff --git a/chrome/installer/util/delete_after_reboot_helper.cc b/chrome/installer/util/delete_after_reboot_helper.cc new file mode 100644 index 0000000..a54acb3 --- /dev/null +++ b/chrome/installer/util/delete_after_reboot_helper.cc @@ -0,0 +1,375 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// This file defines helper methods used to schedule files for deletion +// on next reboot. The code here is heavily borrowed and simplified from +// http://code.google.com/p/omaha/source/browse/trunk/common/file.cc and +// http://code.google.com/p/omaha/source/browse/trunk/common/utils.cc +// +// This implementation really is not fast, so do not use it where that will +// matter. + +#include "chrome/installer/util/delete_after_reboot_helper.h" + +#include <string> +#include <vector> + +#include "base/file_util.h" +#include "base/registry.h" +#include "base/string_util.h" + +// The moves-pending-reboot is a MULTISZ registry key in the HKLM part of the +// registry. +const wchar_t kSessionManagerKey[] = + L"SYSTEM\\CurrentControlSet\\Control\\Session Manager"; +const wchar_t kPendingFileRenameOps[] = L"PendingFileRenameOperations"; + +namespace { + +// Returns true if this directory name is 'safe' for deletion (doesn't contain +// "..", doesn't specify a drive root) +bool IsSafeDirectoryNameForDeletion(const wchar_t* dir_name) { + DCHECK(dir_name); + + // empty name isn't allowed + if (!(dir_name && *dir_name)) { + return false; + } + + // require a character other than \/:. after the last : + // disallow anything with ".." + bool ok = false; + for (const wchar_t* s = dir_name; *s; ++s) { + if (*s != L'\\' && *s != L'/' && *s != L':' && *s != L'.') { + ok = true; + } + if (*s == L'.' && s > dir_name && *(s - 1) == L'.') { + return false; + } + if (*s == L':') { + ok = false; + } + } + return ok; +} + +// Must only be called for regular files or directories that will be empty. +bool ScheduleFileSystemEntityForDeletion(const wchar_t* path) { + // Check if the file exists, return false if not. + WIN32_FILE_ATTRIBUTE_DATA attrs = {0}; + if (!::GetFileAttributesEx(path, ::GetFileExInfoStandard, &attrs)) { + LOG(ERROR) << path << " for deletion does not exist." << GetLastError(); + return false; + } + + DWORD flags = MOVEFILE_DELAY_UNTIL_REBOOT; + if (!file_util::DirectoryExists(path)) { + // This flag valid only for files + flags |= MOVEFILE_REPLACE_EXISTING; + } + + if (!::MoveFileEx(path, NULL, flags)) { + LOG(ERROR) << "Could not schedule " << path << " for deletion."; + return false; + } + + LOG(INFO) << "Scheduled for deletion: " << path; + return true; +} +} // end namespace + +bool ScheduleDirectoryForDeletion(const wchar_t* dir_name) { + if (!IsSafeDirectoryNameForDeletion(dir_name)) { + LOG(ERROR) << "Unsafe directory name for deletion: " << dir_name; + return false; + } + + // Make sure the directory exists (it is ok if it doesn't) + DWORD dir_attributes = ::GetFileAttributes(dir_name); + if (dir_attributes == INVALID_FILE_ATTRIBUTES) { + if (::GetLastError() == ERROR_FILE_NOT_FOUND) { + return true; // Ok if directory is missing + } else { + LOG(ERROR) << "Could not GetFileAttributes for " << dir_name; + return false; + } + } + // Confirm it is a directory + if (!(dir_attributes & FILE_ATTRIBUTE_DIRECTORY)) { + LOG(ERROR) << "Scheduled directory is not a directory: " << dir_name; + return false; + } + + // First schedule all the normal files for deletion. + { + bool success = true; + file_util::FileEnumerator file_enum(FilePath(dir_name), false, + file_util::FileEnumerator::FILES); + for (FilePath file = file_enum.Next(); !file.empty(); + file = file_enum.Next()) { + success = ScheduleFileSystemEntityForDeletion(file.value().c_str()); + if (!success) { + LOG(ERROR) << "Failed to schedule file for deletion: " << file.value(); + return false; + } + } + } + + // Then recurse to all the subdirectories. + { + bool success = true; + file_util::FileEnumerator dir_enum(FilePath(dir_name), false, + file_util::FileEnumerator::DIRECTORIES); + for (FilePath sub_dir = dir_enum.Next(); !sub_dir.empty(); + sub_dir = dir_enum.Next()) { + success = ScheduleDirectoryForDeletion(sub_dir.value().c_str()); + if (!success) { + LOG(ERROR) << "Failed to schedule subdirectory for deletion: " + << sub_dir.value(); + return false; + } + } + } + + // Now schedule the empty directory itself + if (!ScheduleFileSystemEntityForDeletion(dir_name)) { + LOG(ERROR) << "Failed to schedule directory for deletion: " << dir_name; + } + + return true; +} + +// Converts the strings found in |buffer| to a list of wstrings that is returned +// in |value|. +// |buffer| points to a series of pairs of null-terminated wchar_t strings +// followed by a terminating null character. +// |byte_count| is the length of |buffer| in bytes. +// |value| is a pointer to an empty vector of wstrings. On success, this vector +// contains all of the strings extracted from |buffer|. +// Returns S_OK on success, E_INVALIDARG if buffer does not meet tha above +// specification. +HRESULT MultiSZBytesToStringArray(const char* buffer, size_t byte_count, + std::vector<PendingMove>* value) { + DCHECK(buffer); + DCHECK(value); + DCHECK(value->empty()); + + DWORD data_len = byte_count / sizeof(wchar_t); + const wchar_t* data = reinterpret_cast<const wchar_t*>(buffer); + const wchar_t* data_end = data + data_len; + if (data_len > 1) { + // must be terminated by two null characters + if (data[data_len - 1] != 0 || data[data_len - 2] != 0) { + DLOG(ERROR) << "Invalid MULTI_SZ found."; + return E_INVALIDARG; + } + + // put null-terminated strings into arrays + while (data < data_end) { + std::wstring str_from(data); + data += str_from.length() + 1; + if (data < data_end) { + std::wstring str_to(data); + data += str_to.length() + 1; + value->push_back(std::make_pair(str_from, str_to)); + } + } + } + return S_OK; +} + +void StringArrayToMultiSZBytes(const std::vector<PendingMove>& strings, + std::vector<char>* buffer) { + DCHECK(buffer); + buffer->clear(); + + if (strings.size() == 0) { + // Leave buffer empty if we have no strings. + return; + } + + size_t total_wchars = 0; + { + std::vector<PendingMove>::const_iterator iter(strings.begin()); + for (; iter != strings.end(); ++iter) { + total_wchars += iter->first.length(); + total_wchars++; // Space for the null char. + total_wchars += iter->second.length(); + total_wchars++; // Space for the null char. + } + total_wchars++; // Space for the extra terminating null char. + } + + size_t total_length = total_wchars * sizeof(wchar_t); + buffer->resize(total_length); + wchar_t* write_pointer = reinterpret_cast<wchar_t*>(&((*buffer)[0])); + // Keep an end pointer around for sanity checking. + wchar_t* end_pointer = write_pointer + total_wchars; + + std::vector<PendingMove>::const_iterator copy_iter(strings.begin()); + for (; copy_iter != strings.end() && write_pointer < end_pointer; + copy_iter++) { + // First copy the source string. + size_t string_length = copy_iter->first.length() + 1; + memcpy(write_pointer, copy_iter->first.c_str(), + string_length * sizeof(wchar_t)); + write_pointer += string_length; + // Now copy the destination string. + string_length = copy_iter->second.length() + 1; + memcpy(write_pointer, copy_iter->second.c_str(), + string_length * sizeof(wchar_t)); + write_pointer += string_length; + + // We should never run off the end while in this loop. + DCHECK(write_pointer < end_pointer); + } + *write_pointer = L'\0'; // Explicitly set the final null char. + DCHECK(++write_pointer == end_pointer); +} + +std::wstring GetShortPathName(const wchar_t* path) { + std::wstring short_path; + DWORD length = GetShortPathName(path, WriteInto(&short_path, MAX_PATH), + MAX_PATH); + DLOG_IF(WARNING, length == 0) << __FUNCTION__ << " gle=" << GetLastError(); + short_path.resize(length); + return short_path; +} + +HRESULT GetPendingMovesValue( + std::vector<PendingMove>* pending_moves) { + DCHECK(pending_moves); + pending_moves->clear(); + + // Get the current value of the key + // If the Key is missing, that's totally acceptable. + RegKey session_manager_key(HKEY_LOCAL_MACHINE, kSessionManagerKey, + KEY_QUERY_VALUE); + HKEY session_manager_handle = session_manager_key.Handle(); + if (!session_manager_handle) { + return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); + } + + // The base::RegKey Read code squashes the return code from + // ReqQueryValueEx, we have to do things ourselves: + DWORD buffer_size = 0; + std::vector<char> buffer; + buffer.resize(1); + DWORD type; + DWORD result = RegQueryValueEx(session_manager_handle, kPendingFileRenameOps, + 0, &type, reinterpret_cast<BYTE*>(&buffer[0]), + &buffer_size); + + if (result == ERROR_FILE_NOT_FOUND) { + // No pending moves were found. + return HRESULT_FROM_WIN32(result); + } else if (result == ERROR_MORE_DATA) { + if (type != REG_MULTI_SZ) { + DLOG(ERROR) << "Found PendingRename value of unexpected type."; + return E_UNEXPECTED; + } + if (buffer_size % 2) { + // The buffer size should be an even number (since we expect wchar_ts). + // If this is not the case, fail here. + DLOG(ERROR) << "Corrupt PendingRename value."; + return E_UNEXPECTED; + } + + // There are pending file renames. Read them in. + buffer.resize(buffer_size); + result = RegQueryValueEx(session_manager_handle, kPendingFileRenameOps, + 0, &type, reinterpret_cast<LPBYTE>(&buffer[0]), + &buffer_size); + if (result != ERROR_SUCCESS) { + DLOG(ERROR) << "Failed to read from " << kPendingFileRenameOps; + return HRESULT_FROM_WIN32(result); + } + } else { + // That was unexpected. + DLOG(ERROR) << "Unexpected result from RegQueryValueEx: " << result; + return HRESULT_FROM_WIN32(result); + } + + // We now have a buffer of bytes that is actually a sequence of + // null-terminated wchar_t strings terminated by an additional null character. + // Stick this into a vector of strings for clarity. + HRESULT hr = MultiSZBytesToStringArray(&buffer[0], buffer.size(), + pending_moves); + return hr; +} + +bool MatchPendingDeletePath(const std::wstring& short_form_needle, + const std::wstring& reg_path) { + std::wstring match_path(reg_path); // Stores the path stored in each entry. + + // First chomp the prefix since that will mess up GetShortPathName. + std::wstring prefix(L"\\??\\"); + if (StartsWith(match_path, prefix, false)) { + match_path = match_path.substr(4); + } + + // Get the short path name of the entry. + std::wstring short_match_path(GetShortPathName(match_path.c_str())); + + // Now compare the paths. If it isn't one we're looking for, add it + // to the list to keep. + return StartsWith(short_match_path, short_form_needle, false); +} + +// Removes all pending moves for the given |directory| and any contained +// files or subdirectories. Returns true on success +bool RemoveFromMovesPendingReboot(const wchar_t* directory) { + DCHECK(directory); + std::vector<PendingMove> pending_moves; + HRESULT hr = GetPendingMovesValue(&pending_moves); + if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) { + // No pending moves, nothing to do. + return true; + } else if (FAILED(hr)) { + // Couldn't read the key or the key was corrupt. + return false; + } + + // Get the short form of |directory| and use that to match. + std::wstring short_directory(GetShortPathName(directory)); + + std::vector<PendingMove> strings_to_keep; + std::vector<PendingMove>::const_iterator iter(pending_moves.begin()); + for (; iter != pending_moves.end(); iter++) { + if (!MatchPendingDeletePath(short_directory, iter->first)) { + // This doesn't match the deletions we are looking for. Preserve + // this string pair, making sure that it is in fact a pair. + strings_to_keep.push_back(*iter); + } + } + + if (strings_to_keep.size() == pending_moves.size()) { + // Nothing to remove, return true. + return true; + } + + // Write the key back into a buffer. + RegKey session_manager_key(HKEY_LOCAL_MACHINE, kSessionManagerKey, + KEY_CREATE_SUB_KEY | KEY_SET_VALUE); + if (!session_manager_key.Handle()) { + // Couldn't open / create the key. + LOG(ERROR) << "Failed to open session manager key for writing."; + return false; + } + + if (strings_to_keep.size() > 1) { + std::vector<char> buffer; + StringArrayToMultiSZBytes(strings_to_keep, &buffer); + DCHECK(buffer.size() > 0); + if (buffer.size() > 0) { + return session_manager_key.WriteValue(kPendingFileRenameOps, &buffer[0], + buffer.size(), REG_MULTI_SZ); + } else { + return false; + } + } else { + // We have only the trailing NULL string. Don't bother writing that. + return session_manager_key.DeleteValue(kPendingFileRenameOps); + } +} diff --git a/chrome/installer/util/delete_after_reboot_helper.h b/chrome/installer/util/delete_after_reboot_helper.h new file mode 100644 index 0000000..064a634 --- /dev/null +++ b/chrome/installer/util/delete_after_reboot_helper.h @@ -0,0 +1,66 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// This file declares helper methods used to schedule files for deletion +// on next reboot. + +#ifndef CHROME_INSTALLER_UTIL_DELETE_AFTER_REBOOT_HELPER_H_ +#define CHROME_INSTALLER_UTIL_DELETE_AFTER_REBOOT_HELPER_H_ + +#include <string> +#include <vector> + +#include <windows.h> + +// Used by the unit tests. +extern const wchar_t kSessionManagerKey[]; +extern const wchar_t kPendingFileRenameOps[]; + +typedef std::pair<std::wstring, std::wstring> PendingMove; + +// Attempts to schedule the directory for deletion. +bool ScheduleDirectoryForDeletion(const wchar_t* dir_name); + +// Removes all pending moves that are registered for |directory| and all +// elements contained in |directory|. +bool RemoveFromMovesPendingReboot(const wchar_t* directory); + +// Retrieves the list of pending renames from the registry and returns a vector +// containing pairs of strings that represent the operations. If the list +// contains only deletes then every other element will be an empty string +// as per http://msdn.microsoft.com/en-us/library/aa365240(VS.85).aspx. +HRESULT GetPendingMovesValue(std::vector<PendingMove>* pending_moves); + +// This returns true if |short_form_needle| is contained in |reg_path| where +// |short_form_needle| is a file system path that has been shortened by +// GetShortPathName and |reg_path| is a path stored in the +// PendingFileRenameOperations key. +bool MatchPendingDeletePath(const std::wstring& short_form_needle, + const std::wstring& reg_path); + +// Converts the strings found in |buffer| to a list of PendingMoves that is +// returned in |value|. +// |buffer| points to a series of pairs of null-terminated wchar_t strings +// followed by a terminating null character. +// |byte_count| is the length of |buffer| in bytes. +// |value| is a pointer to an empty vector of PendingMoves (string pairs). +// On success, this vector contains all of the string pairs extracted from +// |buffer|. +// Returns S_OK on success, E_INVALIDARG if buffer does not meet the above +// specification. +HRESULT MultiSZBytesToStringArray(const char* buffer, size_t byte_count, + std::vector<PendingMove>* value); + +// The inverse of MultiSZBytesToStringArray, this function converts a list +// of string pairs into a byte array format suitable for writing to the +// kPendingFileRenameOps registry value. It concatenates the strings and +// appends an additional terminating null character. +void StringArrayToMultiSZBytes(const std::vector<PendingMove>& strings, + std::vector<char>* buffer); + +// A helper function for the win32 GetShortPathName that more conveniently +// returns a correctly sized wstring. +std::wstring GetShortPathName(const wchar_t* path); + +#endif // CHROME_INSTALLER_UTIL_DELETE_AFTER_REBOOT_HELPER_H_ diff --git a/chrome/installer/util/delete_after_reboot_helper_unittest.cc b/chrome/installer/util/delete_after_reboot_helper_unittest.cc new file mode 100644 index 0000000..114825c --- /dev/null +++ b/chrome/installer/util/delete_after_reboot_helper_unittest.cc @@ -0,0 +1,186 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <windows.h> +#include <shlobj.h> + +#include "base/file_util.h" +#include "base/registry.h" +#include "base/scoped_ptr.h" +#include "base/string_util.h" +#include "chrome/installer/util/delete_after_reboot_helper.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +// These tests exercise the Delete-After-Reboot code which requires +// modifications to HKLM. This will fail on Vista and above if the user +// is not an admin or if UAC is on. +// I tried using RegOverridePredefKey to test, but MoveFileEx ignore this +// even on 32 bit machines :-( As such, running this test may pollute +// your PendingFileRenameOperations value. +class DeleteAfterRebootHelperTest : public testing::Test { + protected: + virtual void SetUp() { + // Create a temporary directory for testing and fill it with some files. + std::wstring no_prefix; + file_util::CreateNewTempDirectory(no_prefix, &temp_dir_); + file_util::CreateTemporaryFileInDir(temp_dir_, &temp_file_); + + temp_subdir_ = temp_dir_.Append(L"subdir"); + file_util::CreateDirectory(temp_subdir_); + file_util::CreateTemporaryFileInDir(temp_subdir_, &temp_subdir_file_); + + // Copy the current pending moves and then clear it if we can: + if (IsUserAnAdmin()) { + GetPendingMovesValue(&original_pending_moves_); + } + } + virtual void TearDown() { + // Delete the temporary directory if it's still there. + file_util::Delete(temp_dir_, true); + + // Try and restore the pending moves value, if we have one. + if (IsUserAnAdmin() && original_pending_moves_.size() > 1) { + RegKey session_manager_key(HKEY_LOCAL_MACHINE, kSessionManagerKey, + KEY_CREATE_SUB_KEY | KEY_SET_VALUE); + if (!session_manager_key.Handle()) { + // Couldn't open / create the key. + DLOG(ERROR) << "Failed to open session manager key for writing."; + } + + std::vector<char> buffer; + StringArrayToMultiSZBytes(original_pending_moves_, &buffer); + session_manager_key.WriteValue(kPendingFileRenameOps, &buffer[0], + buffer.size(), REG_MULTI_SZ); + } + } + + // Compares two buffers of size len. Returns true if they are equal, + // false otherwise. Standard warnings about making sure the buffers + // are at least len chars long apply. + template<class Type> + bool CompareBuffers(Type* buf1, Type* buf2, int len) { + Type* comp1 = buf1; + Type* comp2 = buf2; + for (int i = 0; i < len; i++) { + if (*comp1 != *comp2) + return false; + comp1++; + comp2++; + } + return true; + } + + // Returns the size of the given list of wstrings in bytes, including + // null chars, plus an additional terminating null char. + // e.g. the length of all the strings * sizeof(wchar_t). + virtual size_t WStringPairListSize( + const std::vector<PendingMove>& string_list) { + size_t length = 0; + std::vector<PendingMove>::const_iterator iter(string_list.begin()); + for (; iter != string_list.end(); ++iter) { + length += iter->first.size() + 1; // +1 for the null char. + length += iter->second.size() + 1; // +1 for the null char. + } + length++; // for the additional null char. + return length * sizeof(wchar_t); + } + + std::vector<PendingMove> original_pending_moves_; + + FilePath temp_dir_; + FilePath temp_file_; + FilePath temp_subdir_; + FilePath temp_subdir_file_; +}; +} + +TEST_F(DeleteAfterRebootHelperTest, TestStringListToMultiSZConversions) { + struct StringTest { + wchar_t* test_name; + wchar_t* str; + DWORD length; + size_t count; + } tests[] = { + { L"basic", L"foo\0bar\0fee\0bee\0boo\0bong\0\0", 26 * sizeof(wchar_t), 3 }, + { L"empty", L"\0\0", 2 * sizeof(wchar_t), 1 }, + { L"deletes", L"foo\0\0bar\0\0bizz\0\0", 16 * sizeof(wchar_t), 3 }, + }; + + for (int i = 0; i < arraysize(tests); i++) { + std::vector<PendingMove> string_list; + EXPECT_TRUE(SUCCEEDED( + MultiSZBytesToStringArray(reinterpret_cast<char*>(tests[i].str), + tests[i].length, &string_list))) + << tests[i].test_name; + EXPECT_EQ(tests[i].count, string_list.size()) << tests[i].test_name; + std::vector<char> buffer; + buffer.resize(WStringPairListSize(string_list)); + StringArrayToMultiSZBytes(string_list, &buffer); + EXPECT_TRUE(CompareBuffers(&buffer[0], + reinterpret_cast<char*>(tests[i].str), + tests[i].length)) << tests[i].test_name; + } + + StringTest failures[] = + { L"malformed", reinterpret_cast<wchar_t*>("oddnumb\0\0"), 9, 1 }; + + for (int i = 0; i < arraysize(failures); i++) { + std::vector<PendingMove> string_list; + EXPECT_FALSE(SUCCEEDED( + MultiSZBytesToStringArray(reinterpret_cast<char*>(failures[i].str), + failures[i].length, &string_list))) + << failures[i].test_name; + } +} + + +TEST_F(DeleteAfterRebootHelperTest, TestFileDeletes) { + if (!IsUserAnAdmin()) { + return; + } + + EXPECT_TRUE(ScheduleDirectoryForDeletion(temp_dir_.value().c_str())); + + std::vector<PendingMove> pending_moves; + EXPECT_TRUE(SUCCEEDED(GetPendingMovesValue(&pending_moves))); + + // We should see, somewhere in this key, deletion writs for + // temp_file_, temp_subdir_file_, temp_subdir_ and temp_dir_ in that order. + EXPECT_TRUE(pending_moves.size() > 3); + + // Get the short form of temp_file_ and use that to match. + std::wstring short_temp_file(GetShortPathName(temp_file_.value().c_str())); + + // Scan for the first expected delete. + std::vector<PendingMove>::const_iterator iter(pending_moves.begin()); + for (; iter != pending_moves.end(); iter++) { + if (MatchPendingDeletePath(short_temp_file, iter->first)) + break; + } + + // Check that each of the deletes we expect are there in order. + FilePath expected_paths[] = + { temp_file_, temp_subdir_file_, temp_subdir_, temp_dir_ }; + for (int i = 0; i < arraysize(expected_paths); i++) { + EXPECT_FALSE(iter == pending_moves.end()); + if (iter != pending_moves.end()) { + std::wstring short_path_name( + GetShortPathName(expected_paths[i].value().c_str())); + EXPECT_TRUE(MatchPendingDeletePath(short_path_name, iter->first)); + iter++; + } + } + + // Test that we can remove the pending deletes. + EXPECT_TRUE(RemoveFromMovesPendingReboot(temp_dir_.value().c_str())); + HRESULT hr = GetPendingMovesValue(&pending_moves); + EXPECT_TRUE(hr == S_OK || hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)); + + std::vector<PendingMove>::const_iterator check_iter(pending_moves.begin()); + for (; check_iter != pending_moves.end(); ++check_iter) { + EXPECT_FALSE(MatchPendingDeletePath(short_temp_file, check_iter->first)); + } +} diff --git a/chrome/installer/util/util_constants.h b/chrome/installer/util/util_constants.h index 534d770..61f6805 100644 --- a/chrome/installer/util/util_constants.h +++ b/chrome/installer/util/util_constants.h @@ -39,7 +39,8 @@ enum InstallStatus { EULA_REJECTED, // EULA dialog was not accepted by user. EULA_ACCEPTED, // EULA dialog was accepted by user. EULA_ACCEPTED_OPT_IN, // EULA accepted wtih the crash optin selected. - INSTALL_DIR_IN_USE // Installation directory is in use by another process + INSTALL_DIR_IN_USE, // Installation directory is in use by another process + UNINSTALL_REQUIRES_REBOOT // Uninstallation required a reboot. }; namespace switches { |