blob: f24c53ef6610e63db6c3bcabbd1d05f727d02719 [file] [log] [blame]
// Copyright 2011 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 "chrome/browser/first_run/upgrade_util_mac.h"
#import <AppKit/AppKit.h>
#include <libproc.h>
#include <unistd.h>
#include <set>
#include <vector>
#include "base/command_line.h"
#include "base/mac/authorization_util.h"
#include "base/mac/foundation_util.h"
#include "base/mac/scoped_authorizationref.h"
#import "base/mac/scoped_nsobject.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_number_conversions.h"
#include "chrome/browser/mac/relauncher.h"
#include "chrome/common/mac/staging_watcher.h"
#include "chrome/grit/chromium_strings.h"
#include "components/strings/grit/components_strings.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/l10n_util_mac.h"
namespace upgrade_util {
namespace {
// Get the uid and executable path for a pid. Returns true iff successful.
// |path_buffer| must be of PROC_PIDPATHINFO_MAXSIZE length.
bool GetUIDAndPathOfPID(pid_t pid, char* path_buffer, uid_t* out_uid) {
struct proc_bsdshortinfo info;
int error = proc_pidinfo(pid, PROC_PIDT_SHORTBSDINFO, 0, &info, sizeof(info));
if (error <= 0)
return false;
error = proc_pidpath(pid, path_buffer, PROC_PIDPATHINFO_MAXSIZE);
if (error <= 0)
return false;
*out_uid = info.pbsi_uid;
return true;
}
struct ThisAndOtherUserPids {
std::set<pid_t> this_user;
std::set<pid_t> other_user;
};
ThisAndOtherUserPids GetPidsOfOtherInstancesOfThisBinary() {
ThisAndOtherUserPids pids;
// Get list of all processes.
int pid_array_size_needed = proc_listallpids(nullptr, 0);
if (pid_array_size_needed <= 0)
return pids;
std::vector<pid_t> pid_array(pid_array_size_needed * 4); // slack
int pid_count = proc_listallpids(pid_array.data(),
pid_array.size() * sizeof(pid_array[0]));
if (pid_count <= 0)
return pids;
pid_array.resize(pid_count);
// Get info about this process.
const pid_t this_pid = getpid();
uid_t this_uid;
char this_path[PROC_PIDPATHINFO_MAXSIZE];
if (!GetUIDAndPathOfPID(this_pid, this_path, &this_uid))
return pids;
// Compare all other processes to this one.
for (pid_t pid : pid_array) {
if (pid == this_pid)
continue;
uid_t uid;
char path[PROC_PIDPATHINFO_MAXSIZE];
if (!GetUIDAndPathOfPID(pid, path, &uid))
continue;
if (strcmp(path, this_path) != 0)
continue;
if (uid == this_uid)
pids.this_user.insert(pid);
else
pids.other_user.insert(pid);
}
return pids;
}
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
//
// ShouldContinueToRelaunchForUpgrade() can complete in quite a few different
// ways. This allows analysis of what's happening in the field.
enum class OtherInstancesCheckResult {
kOldStyleUpdate = 0,
kNoOtherInstancesAtStart = 1,
kUserDidNotAuthorize = 2,
kInitialExecuteAndWaitFailed = 3,
kSecondaryExecuteAndWaitFailed = 4,
kShowedNoDialogsNoInstancesExistAtExit = 5,
kShowedNoDialogsInstancesExistAtExit = 6,
kShowedOtherUserDialogNoInstancesExistAtExit = 7,
kShowedOtherUserDialogInstancesExistAtExit = 8,
kShowedThisUserDialogNoInstancesExistAtExit = 9,
kShowedThisUserDialogInstancesExistAtExit = 10,
kShowedBothDialogsNoInstancesExistAtExit = 11,
kShowedBothDialogsInstancesExistAtExit = 12,
kMaxValue = kShowedBothDialogsInstancesExistAtExit,
};
void LogInstanceCheckResult(OtherInstancesCheckResult result) {
UMA_HISTOGRAM_ENUMERATION("OSX.OtherInstancesCheckResult", result);
}
void LogInstanceCheckResult(bool showed_this_user_dialog,
bool showed_other_user_dialog,
bool other_instances_exist) {
int uma_value = showed_this_user_dialog << 2 | showed_other_user_dialog << 1 |
other_instances_exist << 0;
uma_value += static_cast<int>(
OtherInstancesCheckResult::kShowedNoDialogsNoInstancesExistAtExit);
LogInstanceCheckResult(static_cast<OtherInstancesCheckResult>(uma_value));
}
// Sends a termination signal to a set of processes. Returns true if the kill
// command was successfully executed. This says nothing about whether the kill
// command succeeded in actually killing anything.
enum class SignalType { kSoftKill, kHardKill };
bool SendKillSignalToPids(AuthorizationRef authorization,
SignalType signal_type,
std::set<pid_t> pids) {
DCHECK(!pids.empty());
// ExecuteWithPrivilegesAndWait() requires a helper tool that returns
// its pid via stdout. Using the inherent abilities of /bin/sh
// simplifies things.
static constexpr char shell_path[] = "/bin/sh";
static constexpr char shell_execute_command[] = "-c";
std::string command_string = "echo $$; exec /bin/kill ";
if (signal_type == SignalType::kSoftKill)
command_string += "-TERM";
else
command_string += "-KILL";
for (pid_t pid : pids) {
command_string += " ";
command_string += base::NumberToString(pid);
}
const char* command_string_c = command_string.c_str();
const char* arguments[] = {shell_execute_command, command_string_c, nullptr};
OSStatus status = base::mac::ExecuteWithPrivilegesAndWait(
authorization, shell_path, kAuthorizationFlagDefaults, arguments,
nullptr, // pipe
nullptr); // exit_status; no point in checking, as the result is checked
// later by enumerating processes.
return status == errAuthorizationSuccess;
}
} // namespace
ThisAndOtherUserCounts GetCountOfOtherInstancesOfThisBinary() {
ThisAndOtherUserPids pids = GetPidsOfOtherInstancesOfThisBinary();
return ThisAndOtherUserCounts{pids.this_user.size(), pids.other_user.size()};
}
bool ShouldContinueToRelaunchForUpgrade() {
// If this isn't a new-style update, and the staging key isn't set, don't
// block at all.
if (![CrStagingKeyWatcher isStagingKeySet]) {
LogInstanceCheckResult(OtherInstancesCheckResult::kOldStyleUpdate);
return true;
}
ThisAndOtherUserPids pids = GetPidsOfOtherInstancesOfThisBinary();
if (pids.this_user.empty() && pids.other_user.empty()) {
LogInstanceCheckResult(OtherInstancesCheckResult::kNoOtherInstancesAtStart);
return true;
}
// If there are both other-user and same-user instances, start with the auth
// dialog, as the user might not be able to auth. It would be weird to have
// the user kill all their own instances, and then hit a dialog they can't
// answer, and be left having quit a lot of stuff.
bool showed_this_user_dialog = false;
bool showed_other_user_dialog = false;
if (!pids.other_user.empty()) {
showed_other_user_dialog = true;
NSString* prompt = l10n_util::GetNSString(
IDS_UPDATE_OTHER_INSTANCES_OTHER_USER_AUTHENTICATION_PROMPT);
base::mac::ScopedAuthorizationRef authorization(
base::mac::AuthorizationCreateToRunAsRoot(
base::mac::NSToCFCast(prompt)));
if (authorization.get()) {
// A simple kill -TERM works if no page has a beforeunload handler; kill
// -KILL is required otherwise. Therefore, do a kill -TERM first, wait a
// few seconds, and then kill -KILL stragglers.
bool success = SendKillSignalToPids(authorization, SignalType::kSoftKill,
pids.other_user);
if (!success) {
// Killing other instances failed; installation cannot proceed.
LogInstanceCheckResult(
OtherInstancesCheckResult::kInitialExecuteAndWaitFailed);
return false;
}
// Three seconds is arbitrary; the intent is to give the other Chrome
// instances time to close themselves down safely, if they are able to.
// TODO(avi): Be smarter here; perhaps a kevent watcher (with timeout) on
// the pids, as to not wait as long if they all close quickly?
sleep(3);
pids = GetPidsOfOtherInstancesOfThisBinary();
if (!pids.other_user.empty()) {
success = SendKillSignalToPids(authorization, SignalType::kHardKill,
pids.other_user);
if (!success) {
// Killing other instances failed; installation cannot proceed.
LogInstanceCheckResult(
OtherInstancesCheckResult::kSecondaryExecuteAndWaitFailed);
return false;
}
// Also arbitrary, though less time is needed for a hard kill.
// TODO(avi): Use a watcher as above.
sleep(1);
}
} else {
// There was no auth to kill other-user instances. At this point,
// installation can't proceed, so don't ask the users to kill their own
// instances.
LogInstanceCheckResult(OtherInstancesCheckResult::kUserDidNotAuthorize);
return false;
}
}
if (!pids.this_user.empty()) {
showed_this_user_dialog = true;
@autoreleasepool {
NSString* title = l10n_util::GetNSString(
IDS_UPDATE_OTHER_INSTANCES_SAME_USER_DIALOG_TITLE);
NSString* error = l10n_util::GetNSString(
IDS_UPDATE_OTHER_INSTANCES_SAME_USER_DIALOG_MESSAGE);
NSString* ok = l10n_util::GetNSString(IDS_OK);
NSAlert* alert = [[[NSAlert alloc] init] autorelease];
[alert setAlertStyle:NSWarningAlertStyle];
[alert setMessageText:title];
[alert setInformativeText:error];
[alert addButtonWithTitle:ok];
[alert runModal];
}
}
// Count the processes again, as the non-user processes may have been killed
// (in the other-user case), or the user may have switched away and killed
// their own processes while the same-user dialog was up.
pids = GetPidsOfOtherInstancesOfThisBinary();
bool other_instances_exist =
!pids.this_user.empty() || !pids.other_user.empty();
LogInstanceCheckResult(showed_this_user_dialog, showed_other_user_dialog,
other_instances_exist);
return !other_instances_exist;
}
bool RelaunchChromeBrowser(const base::CommandLine& command_line) {
upgrade_util::ThisAndOtherUserCounts counts =
upgrade_util::GetCountOfOtherInstancesOfThisBinary();
const int other_instances = counts.this_user_count + counts.other_user_count;
const bool wait_for_staged_update = (other_instances == 0);
return mac_relauncher::RelaunchApp(command_line.argv(),
wait_for_staged_update);
}
} // namespace upgrade_util