blob: 610e017808e6e06bee2ff400f7645517ef6d9c18 [file] [log] [blame]
// Copyright 2009 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========================================================================
//
// This file implements a Custom Action DLL for standalone MSI installers that
// uninstall Omaha-managed products by exposing a UninstallOmahaProduct
// method.
//
// The custom action makes the following assumptions:
// 1) The software to be uninstalled has a corresponding Omaha key under
// kOmahaClientsKey\{AppGuid}
//
// 2) {AppGuid} and any extra flags needed to cause the product's uninstall
// to execute silently are passed in via the CustomActionData property (if
// using Wix, set a Property of type 'immediate' with the same name as your
// 'deferred' custom action - this causes the value to get passed in as
// the CustomActionData property here.)
// The expected format for the CustomActionData is:
// {AppGuid}|<uninstall flags>
// For example:
// {8BA986DA-5100-405E-AA35-86F34A02ACBF}|--force-uninstall
//
// 3) The app's "Clients" key contains a 'name' string value that is used as
// the key to the list of Windows uninstall shortcuts that reside in
// kUninstallKey.
//
// 4) The program to be uninstalled has made its registrations under HKLM.
//
// TODO(robertshield): Make 4) an argument, don't assume HKLM.
// TODO(robertshield): Make this work for non-Omaha-managed products as well.
#include <Windows.h>
#include <Msi.h>
#include <MsiQuery.h>
#include <Shlwapi.h>
#include <string>
#include <vector>
const wchar_t kOmahaClientsKey[] = L"Software\\Google\\Update\\Clients\\";
const wchar_t kOmahaProductName[] = L"name";
const wchar_t kUninstallKey[] =
L"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\";
const wchar_t kUninstallCmdName[] = L"UninstallString";
// This is how long (in ms) we wait for the uninstall command to complete.
const DWORD kUninstallCmdTimeoutMs = 120 * 1000;
static void MsiLogInfo(MSIHANDLE install, const std::wstring& msg) {
// Note that PMSIHANDLES clean up after themselves.
PMSIHANDLE record = ::MsiCreateRecord(2);
std::wstring log_msg(L"UNINSTALL LOG: ");
log_msg += msg;
::MsiRecordSetString(record, 0, log_msg.c_str());
::MsiProcessMessage(install, INSTALLMESSAGE(INSTALLMESSAGE_INFO), record);
}
// Retrieve the named property from the database and stuff it in
// return_value. Returns true on success, false otherwise.
//
// Note that in practice, if this is run as a deferred custom action, you can
// only query for CustomActionData and a few others as per
// http://msdn.microsoft.com/en-us/library/aa370543(VS.85).aspx.
static bool GetMsiProperty(MSIHANDLE msi_handle,
const std::wstring& name,
std::wstring* return_value) {
if (!return_value) {
return false;
}
DWORD size = 1024;
return_value->resize(size + 1);
UINT result = ::MsiGetProperty(msi_handle, name.c_str(), &(*return_value)[0],
&size);
if (result == ERROR_MORE_DATA) {
return_value->resize(size + 1);
result = ::MsiGetProperty(msi_handle, name.c_str(), &(*return_value)[0],
&size);
}
// Resize the string down to the actual number of characters copied.
return_value->resize(size + 1);
if (result != ERROR_SUCCESS) {
std::wstring msg(L"Failed to retrieve property: ");
msg += name;
MsiLogInfo(msi_handle, msg);
}
return (result == ERROR_SUCCESS);
}
// Reads in the registry value called value_name residing at key_path under
// root_key and stuffs it in value. Returns true on success, false
// otherwise.
static bool ReadRegStringValue(MSIHANDLE msi_handle,
HKEY root_key,
const std::wstring& key_path,
const std::wstring& value_name,
std::wstring* value) {
if (!value) {
return false;
}
LSTATUS result;
DWORD size = 0;
result = ::SHRegGetValue(root_key, key_path.c_str(), value_name.c_str(),
SRRF_RT_REG_SZ, NULL, NULL, &size);
std::vector<BYTE> buffer;
// Note that since we passed NULL in as the buffer, SHRegGetValue will
// have returned ERROR_SUCCESS if the key exists.
if (result == ERROR_SUCCESS && size > 0) {
buffer.resize(size);
result = ::SHRegGetValue(root_key, key_path.c_str(), value_name.c_str(),
SRRF_RT_REG_SZ, NULL, &buffer[0], &size);
}
if (result != ERROR_SUCCESS || buffer.empty()) {
std::wstring error_message(L"Failed to read reg value: ");
error_message += key_path;
error_message += L"\\";
error_message += value_name;
MsiLogInfo(msi_handle, error_message);
} else {
size_t string_length = size / sizeof(wchar_t);
// Note that we must subtract one from the length to account for the extra
// null terminator added by SHRegValue().
value->assign(reinterpret_cast<wchar_t*>(&buffer[0]), string_length - 1);
}
return (result == ERROR_SUCCESS);
}
// Runs the command given in cmd_line and waits kUninstallCmdTimeoutMs for it
// to complete and places the exit code in exit_code. Tries to kill the
// process if it hasn't completed before the timeout. Returns true on successful
// completion of the command, false otherwise.
static bool LaunchAppAndReturnExitCode(MSIHANDLE msi_handle,
const std::wstring& cmd_line,
DWORD* exit_code) {
if (!exit_code) {
return false;
}
bool is_successful = false;
STARTUPINFO startup_info = {0};
startup_info.cb = sizeof(startup_info);
PROCESS_INFORMATION process_info = {0};
if (::CreateProcess(NULL,
const_cast<wchar_t*>(cmd_line.c_str()), NULL, NULL,
FALSE, 0, NULL, NULL,
&startup_info, &process_info)) {
DWORD wait_result = ::WaitForSingleObject(process_info.hProcess,
kUninstallCmdTimeoutMs);
if (wait_result == WAIT_TIMEOUT) {
// Looks like our uninstall process is hung, try to kill it.
::TerminateProcess(process_info.hProcess, 0);
} else if (wait_result == WAIT_OBJECT_0) {
if (::GetExitCodeProcess(process_info.hProcess, exit_code)) {
if (*exit_code != STILL_ACTIVE)
is_successful = true;
}
} else {
std::wstring error_message(L"Error waiting for process exit: ");
error_message += cmd_line;
MsiLogInfo(msi_handle, error_message);
}
// Don't leak the handles.
::CloseHandle(process_info.hThread);
::CloseHandle(process_info.hProcess);
} else {
std::wstring error_message(L"Failed to CreateProcess ");
error_message += cmd_line;
MsiLogInfo(msi_handle, error_message);
}
return is_successful;
}
static bool ParseCustomActionData(const std::wstring& custom_action_data,
std::wstring* app_id,
std::wstring* additional_uninstall_args) {
if (!app_id || !additional_uninstall_args) {
return false;
}
bool result = false;
size_t separator_pos = custom_action_data.find(L'|');
if (separator_pos != std::wstring::npos) {
*app_id = custom_action_data.substr(0, separator_pos);
*additional_uninstall_args = custom_action_data.substr(separator_pos + 1);
result = true;
}
return result;
}
extern "C" UINT __stdcall UninstallOmahaProduct(MSIHANDLE msi_handle) {
DWORD result = ERROR_INSTALL_FAILURE;
// Get the app id we're interested in as well as the product uninstallation
// parameters.
bool valid_ca_data = false;
std::wstring custom_action_data;
std::wstring app_id;
std::wstring additional_uninstall_args;
if (GetMsiProperty(msi_handle, L"CustomActionData", &custom_action_data)) {
valid_ca_data = ParseCustomActionData(custom_action_data, &app_id,
&additional_uninstall_args);
}
std::wstring uninstall_cmd;
if (valid_ca_data) {
// Use the app id to look up the product name...
std::wstring product_name;
std::wstring omaha_client_key(kOmahaClientsKey);
omaha_client_key += app_id;
std::wstring product_msg(L"Looking for product name in: ");
product_msg += omaha_client_key;
MsiLogInfo(msi_handle, product_msg);
if (ReadRegStringValue(msi_handle, HKEY_LOCAL_MACHINE,
omaha_client_key.c_str(), kOmahaProductName,
&product_name)) {
std::wstring uninstall_key_path(kUninstallKey);
uninstall_key_path += product_name;
std::wstring key_msg(L"Looking for uninstall key: ");
key_msg += uninstall_key_path;
MsiLogInfo(msi_handle, key_msg);
// ... and then use product name to look up the uninstall command line.
if (ReadRegStringValue(msi_handle, HKEY_LOCAL_MACHINE,
uninstall_key_path.c_str(), kUninstallCmdName,
&uninstall_cmd)) {
result = ERROR_SUCCESS;
}
}
}
if (result == ERROR_SUCCESS) {
// Append the necessary flags to keep the uninstall silent.
uninstall_cmd += L" ";
uninstall_cmd += additional_uninstall_args;
std::wstring uninstall_msg(L"Found uninstall command, executing: ");
uninstall_msg += uninstall_cmd;
MsiLogInfo(msi_handle, uninstall_msg);
// We've found the uninstall command. Now run it and wait for the response.
DWORD exit_code;
if (LaunchAppAndReturnExitCode(msi_handle, uninstall_cmd, &exit_code)) {
// TODO(robertshield): See about doing something based on the return code.
} else {
result = ERROR_INSTALL_FAILURE;
}
}
return result;
}