blob: eb74c0e505f2e85a4d97e4d156eac38aa788ab53 [file] [log] [blame]
// Copyright 2007-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.
// ========================================================================
#include "omaha/worker/job.h"
#include <windows.h>
#include <winhttp.h>
#include <vector>
#include "omaha/common/debug.h"
#include "omaha/common/error.h"
#include "omaha/common/file.h"
#include "omaha/common/logging.h"
#include "omaha/common/path.h"
#include "omaha/common/sta_call.h"
#include "omaha/common/time.h"
#include "omaha/common/utils.h"
#include "omaha/common/vistautil.h"
#include "omaha/goopdate/command_line.h"
#include "omaha/goopdate/goopdate_utils.h"
#include "omaha/goopdate/resource.h"
#include "omaha/goopdate/update_response_data.h"
#include "omaha/worker/application_data.h"
#include "omaha/worker/application_manager.h"
#include "omaha/worker/download_manager.h"
#include "omaha/worker/install_manager.h"
#include "omaha/worker/job_observer.h"
#include "omaha/worker/ping.h"
#include "omaha/worker/ping_utils.h"
#include "omaha/worker/worker_event_logger.h"
#include "omaha/worker/worker_metrics.h"
namespace omaha {
// The caller retains ownership of ping. Delete this object before deleting
// the Ping.
Job::Job(bool is_update, Ping* ping)
: extra_code1_(0),
job_state_(JOBSTATE_START),
job_observer_(NULL),
user_explorer_pid_(0),
bytes_downloaded_(0),
bytes_total_(0),
ping_(ping),
is_update_(is_update),
is_offline_(false),
is_background_(false),
is_update_check_only_(false),
did_launch_cmd_fail_(false) {
CORE_LOG(L1, (_T("[Job::Job]")));
}
Job::~Job() {
CORE_LOG(L1, (_T("[Job::~Job][%s]"), GuidToString(app_data_.app_guid())));
}
void Job::ChangeState(JobState state, bool send_ping) {
CORE_LOG(L1, (_T("[Job::ChangeState moving job from %d to %d.]"),
job_state_, state));
const JobState previous_state = job_state_;
job_state_ = state;
// Ignore failures.
if (send_ping && job_state_ != JOBSTATE_START) {
SendStateChangePing(previous_state);
}
VERIFY1(SUCCEEDED(NotifyUI()));
}
void Job::NotifyDownloadStarted() {
ASSERT1(job_state_ == JOBSTATE_START);
ChangeState(JOBSTATE_DOWNLOADSTARTED, true);
}
void Job::NotifyDownloadComplete() {
ASSERT1(job_state_ == JOBSTATE_DOWNLOADSTARTED);
ChangeState(JOBSTATE_DOWNLOADCOMPLETED, true);
}
void Job::NotifyInstallStarted() {
ChangeState(JOBSTATE_INSTALLERSTARTED, true);
}
void Job::NotifyCompleted(const CompletionInfo& info) {
info_ = info;
if (!is_update_check_only_) {
WriteJobCompletedEvent(app_data().is_machine_app(), *this);
}
bool is_successful_self_update =
is_update_ &&
IsCompletionSuccess(info_) &&
::IsEqualGUID(kGoopdateGuid, app_data().app_guid());
if (is_successful_self_update) {
CORE_LOG(L2, (_T("[self-update successfully invoked; not sending ping]")));
}
// Always send a ping unless:
// - the state is successful self-update completed, or
// - this is an update check only job.
// The exception exists because in the first case we do not know the actual
// outcome of the job. The ping will be sent by the installer when setup
// completes the self-update.
// In the second case, update check only jobs do not actually complete.
// They are used as containers for the update check response and they
// are not downloaded nor installed.
bool send_ping = !(is_successful_self_update || is_update_check_only_);
ChangeState(JOBSTATE_COMPLETED, send_ping);
}
void Job::OnProgress(int bytes, int bytes_total,
int status, const TCHAR* status_text) {
UNREFERENCED_PARAMETER(status);
UNREFERENCED_PARAMETER(status_text);
ASSERT1(status == WINHTTP_CALLBACK_STATUS_READ_COMPLETE ||
status == WINHTTP_CALLBACK_STATUS_CONNECTING_TO_SERVER);
ASSERT1(job_state_ == JOBSTATE_DOWNLOADSTARTED);
bytes_downloaded_ = bytes;
bytes_total_ = bytes_total;
VERIFY1(SUCCEEDED(NotifyUI()));
}
HRESULT Job::NotifyUI() {
if (!job_observer_) {
return S_OK;
}
switch (job_state_) {
case JOBSTATE_DOWNLOADSTARTED: {
int pos = 0;
if (bytes_total_) {
pos = static_cast<int>(
(static_cast<double>(bytes_downloaded_) / bytes_total_) * 100);
}
job_observer_->OnDownloading(0, pos);
break;
}
case JOBSTATE_DOWNLOADCOMPLETED:
job_observer_->OnWaitingToInstall();
break;
case JOBSTATE_INSTALLERSTARTED:
job_observer_->OnInstalling();
break;
case JOBSTATE_COMPLETED:
return DoCompleteJob();
case JOBSTATE_START:
default:
ASSERT1(false);
return E_UNEXPECTED;
}
return S_OK;
}
HRESULT Job::ShouldRestartBrowser() const {
return !update_response_data_.success_url().IsEmpty();
}
HRESULT Job::DeleteJobDownloadDirectory() const {
CORE_LOG(L3, (_T("[DeleteJobDownloadDirectory]")));
ASSERT1(IsCompletionSuccess(info_));
// Do no delete the downloaded file if the installer is omaha.
if (::IsEqualGUID(app_data_.app_guid(), kGoopdateGuid)) {
CORE_LOG(L3, (_T("[Not deleting goopdate self update.]")));
return S_OK;
}
// In case of ondemand updates checks, it is possible that we do not have
// a downloaded file.
if (download_file_name_.IsEmpty() &&
is_update_check_only_) {
return S_OK;
}
ASSERT1(!download_file_name_.IsEmpty());
ASSERT1(!File::IsDirectory(download_file_name_));
CString path = GetDirectoryFromPath(download_file_name_);
HRESULT hr = DeleteDirectory(path);
if (FAILED(hr)) {
CORE_LOG(L3, (_T("[Download file directory delete failed.][%s][0x%08x]"),
path, hr));
}
return hr;
}
// The SUCCESS_ACTION_EXIT_SILENTLY_ON_LAUNCH_CMD case assumes that
// the launch command was run and did_launch_cmd_fail_ set correctly if
// launch_cmd_line_ is not empty.
CompletionCodes Job::CompletionStatusToCompletionCode(
JobCompletionStatus status) const {
CompletionCodes code(COMPLETION_CODE_ERROR);
switch (status) {
case COMPLETION_SUCCESS: {
if ((update_response_data_.success_action() ==
SUCCESS_ACTION_EXIT_SILENTLY) ||
(update_response_data_.success_action() ==
SUCCESS_ACTION_EXIT_SILENTLY_ON_LAUNCH_CMD &&
!launch_cmd_line_.IsEmpty())) {
// If launch failed, display the success UI anyway.
code = did_launch_cmd_fail_ ? COMPLETION_CODE_SUCCESS :
COMPLETION_CODE_SUCCESS_CLOSE_UI;
} else if (ShouldRestartBrowser()) {
if (app_data_.browser_type() == BROWSER_UNKNOWN ||
app_data_.browser_type() == BROWSER_DEFAULT ||
app_data_.browser_type() > BROWSER_MAX) {
code = update_response_data_.terminate_all_browsers() ?
COMPLETION_CODE_RESTART_ALL_BROWSERS_NOTICE_ONLY :
COMPLETION_CODE_RESTART_BROWSER_NOTICE_ONLY;
} else {
code = update_response_data_.terminate_all_browsers() ?
COMPLETION_CODE_RESTART_ALL_BROWSERS :
COMPLETION_CODE_RESTART_BROWSER;
}
} else {
code = COMPLETION_CODE_SUCCESS;
}
break;
}
case COMPLETION_SUCCESS_REBOOT_REQUIRED:
code = COMPLETION_CODE_REBOOT_NOTICE_ONLY;
break;
case COMPLETION_ERROR:
case COMPLETION_INSTALLER_ERROR_MSI:
case COMPLETION_INSTALLER_ERROR_SYSTEM:
case COMPLETION_INSTALLER_ERROR_OTHER:
code = COMPLETION_CODE_ERROR;
break;
case COMPLETION_CANCELLED:
code = COMPLETION_CODE_ERROR;
break;
default:
ASSERT1(false);
}
return code;
}
HRESULT Job::DoCompleteJob() {
CompletionCodes completion_code =
CompletionStatusToCompletionCode(info_.status);
if (IsCompletionSuccess(info_)) {
VERIFY1(SUCCEEDED(DeleteJobDownloadDirectory()));
}
OPT_LOG(L1, (_T("[job completed]")
_T("[status %d][completion code %d][error 0x%08x][text \"%s\"]"),
info_.status, completion_code, info_.error_code, info_.text));
if (job_observer_) {
job_observer_->OnComplete(completion_code,
info_.text,
info_.error_code);
}
return S_OK;
}
HRESULT Job::SendStateChangePing(JobState previous_state) {
Request request(app_data().is_machine_app());
// TODO(omaha): Will need to update this to determine if this is a product or
// component level job and create the ProductAppData appropriately.
AppRequestData app_request_data(app_data());
PingEvent::Types event_type = JobStateToEventType();
PingEvent::Results event_result =
ping_utils::CompletionStatusToPingEventResult(info().status);
// TODO(omaha): Remove this value when circular log buffer is implemented.
// If extra_code1 is not already used and there is an error, specify the state
// in which the error occurred in extra_code1.
const int extra_code1 = (!extra_code1_ && info().error_code) ?
kJobStateExtraCodeMask | previous_state :
extra_code1_;
PingEvent ping_event(event_type,
event_result,
info().error_code,
extra_code1,
app_data().previous_version());
app_request_data.AddPingEvent(ping_event);
AppRequest app_request(app_request_data);
request.AddAppRequest(app_request);
// Clear the extra code for the next state/ping.
extra_code1_ = 0;
if (event_result == COMPLETION_CANCELLED) {
// This ping is sending the last ping after the cancel it cannot
// be canceled.
Ping cancel_ping;
return cancel_ping.SendPing(&request);
} else {
ASSERT1(ping_);
return ping_->SendPing(&request);
}
}
PingEvent::Types Job::JobStateToEventType() const {
PingEvent::Types type = PingEvent::EVENT_UNKNOWN;
switch (job_state_) {
case JOBSTATE_START:
break;
case JOBSTATE_DOWNLOADSTARTED:
type = is_update_ ? PingEvent::EVENT_UPDATE_DOWNLOAD_START :
PingEvent::EVENT_INSTALL_DOWNLOAD_START;
break;
case JOBSTATE_DOWNLOADCOMPLETED:
type = is_update_ ? PingEvent::EVENT_UPDATE_DOWNLOAD_FINISH :
PingEvent::EVENT_INSTALL_DOWNLOAD_FINISH;
break;
case JOBSTATE_INSTALLERSTARTED:
type = is_update_ ? PingEvent::EVENT_UPDATE_INSTALLER_START :
PingEvent::EVENT_INSTALL_INSTALLER_START;
break;
case JOBSTATE_COMPLETED:
type = is_update_ ? PingEvent::EVENT_UPDATE_COMPLETE :
PingEvent::EVENT_INSTALL_COMPLETE;
break;
default:
break;
}
ASSERT1(PingEvent::EVENT_UNKNOWN != type);
return type;
}
void Job::RestartBrowsers() {
// CompletionStatusToCompletionCode does not set a restart completion code
// if the browser type is not a specific browser, so this method should never
// be called in those cases.
ASSERT1(app_data_.browser_type() != BROWSER_UNKNOWN &&
app_data_.browser_type() != BROWSER_DEFAULT &&
app_data_.browser_type() < BROWSER_MAX);
CORE_LOG(L3, (_T("[Job::RestartBrowsers]")));
TerminateBrowserResult browser_res;
TerminateBrowserResult default_res;
if (update_response_data_.terminate_all_browsers()) {
goopdate_utils::TerminateAllBrowsers(app_data_.browser_type(),
&browser_res,
&default_res);
} else {
goopdate_utils::TerminateBrowserProcesses(app_data_.browser_type(),
&browser_res,
&default_res);
}
BrowserType default_type = BROWSER_UNKNOWN;
HRESULT hr = GetDefaultBrowserType(&default_type);
if (FAILED(hr)) {
CORE_LOG(LE, (_T("[GetDefaultBrowserType failed][0x%08x]"), hr));
return;
}
BrowserType browser_type = BROWSER_UNKNOWN;
if (!goopdate_utils::GetBrowserToRestart(app_data_.browser_type(),
default_type,
browser_res,
default_res,
&browser_type)) {
CORE_LOG(LE, (_T("[GetBrowserToRestart returned false. Not launching.]")));
return;
}
ASSERT1(BROWSER_UNKNOWN != browser_type);
VERIFY1(SUCCEEDED(goopdate_utils::LaunchBrowser(
browser_type,
update_response_data_.success_url())));
}
void Job::LaunchBrowser(const CString& url) {
CORE_LOG(L3, (_T("[Job::LaunchBrowser]")));
BrowserType browser_type =
app_data_.browser_type() == BROWSER_UNKNOWN ?
BROWSER_DEFAULT :
app_data_.browser_type();
VERIFY1(SUCCEEDED(goopdate_utils::LaunchBrowser(browser_type, url)));
}
void Job::set_app_data(const AppData& app_data) {
app_data_ = app_data;
}
// On Vista with UAC on for an interactive install, LaunchCmdLine will run the
// command line at medium integrity even if the installer was running at high
// integrity.
HRESULT Job::LaunchCmdLine() {
CORE_LOG(L3, (_T("[Job::LaunchCmdLine][%s]"), launch_cmd_line_));
ASSERT1(!is_update_);
if (launch_cmd_line_.IsEmpty()) {
return S_OK;
}
// InstallerSuccessLaunchCmdLine should not be set if the install failed.
ASSERT1(IsCompletionSuccess(info_));
HRESULT hr = goopdate_utils::LaunchCmdLine(launch_cmd_line_);
if (FAILED(hr)) {
OPT_LOG(LE, (_T("[goopdate_utils::LaunchCmdLine failed][0x%x]"), hr));
did_launch_cmd_fail_ = true;
return hr;
}
return S_OK;
}
HRESULT Job::Download(DownloadManager* dl_manager) {
CORE_LOG(L2, (_T("[Job::DownloadJob][%s]"),
GuidToString(app_data_.app_guid())));
ASSERT1(dl_manager);
NotifyDownloadStarted();
HRESULT hr = dl_manager->DownloadFile(this);
if (FAILED(hr)) {
CORE_LOG(LW, (_T("[Job::DownloadJob DownloadFile failed][0x%08x][%s]"),
hr, GuidToString(app_data_.app_guid())));
CompletionInfo info = dl_manager->error_info();
if (!IsCompletionSuccess(info)) {
NotifyCompleted(info);
}
return hr;
}
NotifyDownloadComplete();
return S_OK;
}
HRESULT Job::Install() {
CORE_LOG(L2, (_T("[Job::InstallJob]")));
NotifyInstallStarted();
CompletionInfo completion_info;
AppData new_app_data;
HRESULT hr = DoInstall(&completion_info, &new_app_data);
// Do not return until after NotifyCompleted() has been called.
NotifyCompleted(completion_info);
app_data_ = new_app_data;
return hr;
}
HRESULT Job::DoInstall(CompletionInfo* completion_info,
AppData* new_app_data) {
CORE_LOG(L3, (_T("[Job::DoInstall]")));
ASSERT1(completion_info);
ASSERT1(new_app_data);
if (!is_update_) {
AppManager app_manager(app_data_.is_machine_app());
HRESULT hr = app_manager.WritePreInstallData(app_data_);
if (FAILED(hr)) {
CORE_LOG(LE, (_T("[AppManager::WritePreInstallData failed][0x%08x][%s]"),
hr, GuidToString(app_data_.app_guid())));
completion_info->status = COMPLETION_ERROR;
completion_info->error_code = hr;
completion_info->text.FormatMessage(IDS_INSTALL_FAILED, hr);
return hr;
}
}
InstallManager install_manager(app_data_.is_machine_app());
AppManager app_manager(app_data_.is_machine_app());
HRESULT hr = install_manager.InstallJob(this);
*completion_info = install_manager.error_info();
if (FAILED(hr)) {
CORE_LOG(LE, (_T("[InstallManager::InstallJob failed][0x%08x][%s]"),
hr, GuidToString(app_data_.app_guid())));
// If we failed the install job and the product wasn't registered, it's safe
// to delete the ClientState key. We need to remove it because it contains
// data like "ap", browsertype, language, etc. that need to be cleaned up in
// case user tries to install again in the future.
if (!is_update_ && !app_manager.IsProductRegistered(app_data_.app_guid())) {
// Need to set is_uninstalled to true or else we'll assert in
// RemoveClientState().
app_data_.set_is_uninstalled(true);
app_manager.RemoveClientState(app_data_);
}
return hr;
}
hr = UpdateJob();
if (FAILED(hr)) {
CORE_LOG(LE, (_T("[UpdateJob failed][0x%08x][%s]"),
hr, GuidToString(app_data_.app_guid())));
completion_info->status = COMPLETION_ERROR;
completion_info->error_code = hr;
completion_info->text.FormatMessage(IDS_INSTALL_FAILED, hr);
return hr;
}
hr = UpdateRegistry(new_app_data);
if (FAILED(hr)) {
CORE_LOG(LE, (_T("[UpdateRegistry failed][0x%08x][%s]"),
hr, GuidToString(app_data_.app_guid())));
completion_info->status = COMPLETION_ERROR;
completion_info->error_code = hr;
completion_info->text.FormatMessage(IDS_INSTALL_FAILED, hr);
return hr;
}
// We do not know whether Goopdate has succeeded because its installer has
// not completed.
if (!::IsEqualGUID(kGoopdateGuid, app_data_.app_guid())) {
app_manager.RecordSuccessfulInstall(app_data_.parent_app_guid(),
app_data_.app_guid(),
is_update_,
is_offline_);
}
return S_OK;
}
// Update the version and the language in the job using the values written by
// the installer.
HRESULT Job::UpdateJob() {
CORE_LOG(L2, (_T("[Job::UpdateJob]")));
AppManager app_manager(app_data_.is_machine_app());
AppData data;
HRESULT hr = app_manager.ReadAppDataFromStore(app_data_.parent_app_guid(),
app_data_.app_guid(),
&data);
ASSERT1(SUCCEEDED(hr));
if (SUCCEEDED(hr)) {
app_data_.set_version(data.version());
app_data_.set_language(data.language());
} else {
CORE_LOG(LW, (_T("[ReadApplicationData failed][0x%08x][%s]"),
hr, GuidToString(app_data_.app_guid())));
// Continue without the data from the registry.
}
return S_OK;
}
// Update the registry with the information from the job.
HRESULT Job::UpdateRegistry(AppData* new_app_data) {
CORE_LOG(L2, (_T("[Job::UpdateRegistry]")));
ASSERT1(new_app_data);
*new_app_data = app_data_;
AppManager app_manager(app_data_.is_machine_app());
// Update the client registry information and the client state with the
// information in this job.
HRESULT hr = app_manager.InitializeApplicationState(new_app_data);
if (FAILED(hr)) {
CORE_LOG(LE, (_T("[WriteAppParamsToRegistry failed][0x%08x][%s]"),
hr, GuidToString(new_app_data->app_guid())));
return hr;
}
return S_OK;
}
HRESULT Job::GetInstallerData(CString* installer_data) const {
ASSERT1(installer_data);
installer_data->Empty();
if (!app_data().encoded_installer_data().IsEmpty()) {
CString decoded_installer_data;
HRESULT hr = Utf8UrlEncodedStringToWideString(
app_data().encoded_installer_data(),
&decoded_installer_data);
ASSERT(SUCCEEDED(hr), (_T("[Utf8UrlEncodedStringToWideString][0x%x]"), hr));
if (FAILED(hr) || CString(decoded_installer_data).Trim().IsEmpty()) {
return GOOPDATE_E_INVALID_INSTALLER_DATA_IN_APPARGS;
}
*installer_data = decoded_installer_data;
return S_OK;
}
if (app_data().install_data_index().IsEmpty()) {
return S_OK;
}
CString data(response_data().GetInstallData(app_data().install_data_index()));
if (CString(data).Trim().IsEmpty()) {
return GOOPDATE_E_INVALID_INSTALL_DATA_INDEX;
}
*installer_data = data;
return S_OK;
}
bool IsCompletionStatusSuccess(JobCompletionStatus status) {
switch (status) {
case COMPLETION_SUCCESS:
case COMPLETION_SUCCESS_REBOOT_REQUIRED:
return true;
case COMPLETION_ERROR:
case COMPLETION_INSTALLER_ERROR_MSI:
case COMPLETION_INSTALLER_ERROR_SYSTEM:
case COMPLETION_INSTALLER_ERROR_OTHER:
case COMPLETION_CANCELLED:
default:
return false;
}
}
bool IsCompletionStatusInstallerError(JobCompletionStatus status) {
switch (status) {
case COMPLETION_INSTALLER_ERROR_MSI:
case COMPLETION_INSTALLER_ERROR_SYSTEM:
case COMPLETION_INSTALLER_ERROR_OTHER:
return true;
case COMPLETION_SUCCESS:
case COMPLETION_SUCCESS_REBOOT_REQUIRED:
case COMPLETION_ERROR:
case COMPLETION_CANCELLED:
default:
return false;
}
}
CString CompletionInfo::ToString() const {
CString error_code_string = FormatErrorCode(error_code);
// Get the format string for the status. Must have %s placeholder for code.
CString status_format;
switch (status) {
case COMPLETION_SUCCESS:
status_format = _T("Installer succeeded with code %s.");
break;
case COMPLETION_SUCCESS_REBOOT_REQUIRED:
status_format = _T("Installer succeeded but reboot required. Code %s.");
break;
case COMPLETION_ERROR:
status_format = _T("Omaha failed with error code %s.");
break;
case COMPLETION_INSTALLER_ERROR_MSI:
status_format = _T("Installer failed with MSI error %s.");
break;
case COMPLETION_INSTALLER_ERROR_SYSTEM:
status_format = _T("Installer failed with system error %s.");
break;
case COMPLETION_INSTALLER_ERROR_OTHER:
status_format = _T("Installer failed with error code %s.");
break;
case COMPLETION_CANCELLED:
status_format = _T("Operation canceled. Code %s.");
break;
default:
status_format = _T("Unknown status. Code %s.");
ASSERT1(false);
}
CString output;
output.Format(status_format, error_code_string);
if (!text.IsEmpty()) {
output.Append(_T(" ") + text);
}
return output;
}
} // namespace omaha