blob: 32fbc9f4f6c2999f9a73f75a6b7be05cb655d8b0 [file] [log] [blame]
// Copyright 2012 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/mac/relauncher.h"
#import <AppKit/AppKit.h>
#include <AvailabilityMacros.h>
#include <crt_externs.h>
#include <dlfcn.h>
#include <stddef.h>
#include <string.h>
#include <sys/event.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <string>
#include <vector>
#include "base/files/file_util.h"
#include "base/files/scoped_file.h"
#include "base/logging.h"
#include "base/mac/mac_logging.h"
#include "base/mac/scoped_nsautorelease_pool.h"
#include "base/mac/scoped_nsobject.h"
#include "base/path_service.h"
#include "base/posix/eintr_wrapper.h"
#include "base/process/launch.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/browser/mac/install_from_dmg.h"
#include "chrome/common/chrome_switches.h"
#include "content/public/common/content_paths.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/main_function_params.h"
namespace mac_relauncher {
namespace {
// The "magic" file descriptor that the relauncher process' write side of the
// pipe shows up on. Chosen to avoid conflicting with stdin, stdout, and
// stderr.
const int kRelauncherSyncFD = STDERR_FILENO + 1;
// The argument separating arguments intended for the relauncher process from
// those intended for the relaunched process. "---" is chosen instead of "--"
// because CommandLine interprets "--" as meaning "end of switches", but
// for many purposes, the relauncher process' CommandLine ought to interpret
// arguments intended for the relaunched process, to get the correct settings
// for such things as logging and the user-data-dir in case it affects crash
// reporting.
const char kRelauncherArgSeparator[] = "---";
// When this argument is supplied to the relauncher process, it will launch
// the relaunched process without bringing it to the foreground.
const char kRelauncherBackgroundArg[] = "--background";
// The beginning of the "process serial number" argument that Launch Services
// sometimes inserts into command lines. A process serial number is only valid
// for a single process, so any PSN arguments will be stripped from command
// lines during relaunch to avoid confusion.
const char kPSNArg[] = "-psn_";
// Returns the "type" argument identifying a relauncher process
// ("--type=relauncher").
std::string RelauncherTypeArg() {
return base::StringPrintf("--%s=%s",
switches::kProcessType,
switches::kRelauncherProcess);
}
} // namespace
bool RelaunchApp(const std::vector<std::string>& args) {
// Use the currently-running application's helper process. The automatic
// update feature is careful to leave the currently-running version alone,
// so this is safe even if the relaunch is the result of an update having
// been applied. In fact, it's safer than using the updated version of the
// helper process, because there's no guarantee that the updated version's
// relauncher implementation will be compatible with the running version's.
base::FilePath child_path;
if (!base::PathService::Get(content::CHILD_PROCESS_EXE, &child_path)) {
LOG(ERROR) << "No CHILD_PROCESS_EXE";
return false;
}
std::vector<std::string> relauncher_args;
return RelaunchAppWithHelper(child_path.value(), relauncher_args, args);
}
bool RelaunchAppWithHelper(const std::string& helper,
const std::vector<std::string>& relauncher_args,
const std::vector<std::string>& args) {
std::vector<std::string> relaunch_args;
relaunch_args.push_back(helper);
relaunch_args.push_back(RelauncherTypeArg());
// If this application isn't in the foreground, the relaunched one shouldn't
// be either.
if (![NSApp isActive]) {
relaunch_args.push_back(kRelauncherBackgroundArg);
}
relaunch_args.insert(relaunch_args.end(),
relauncher_args.begin(), relauncher_args.end());
relaunch_args.push_back(kRelauncherArgSeparator);
// When using the CommandLine interface, -psn_ may have been rewritten as
// --psn_. Look for both.
const char alt_psn_arg[] = "--psn_";
for (size_t index = 0; index < args.size(); ++index) {
// Strip any -psn_ arguments, as they apply to a specific process.
if (args[index].compare(0, strlen(kPSNArg), kPSNArg) != 0 &&
args[index].compare(0, strlen(alt_psn_arg), alt_psn_arg) != 0) {
relaunch_args.push_back(args[index]);
}
}
int pipe_fds[2];
if (HANDLE_EINTR(pipe(pipe_fds)) != 0) {
PLOG(ERROR) << "pipe";
return false;
}
// The parent process will only use pipe_read_fd as the read side of the
// pipe. It can close the write side as soon as the relauncher process has
// forked off. The relauncher process will only use pipe_write_fd as the
// write side of the pipe. In that process, the read side will be closed by
// base::LaunchApp because it won't be present in fds_to_remap, and the write
// side will be remapped to kRelauncherSyncFD by fds_to_remap.
base::ScopedFD pipe_read_fd(pipe_fds[0]);
base::ScopedFD pipe_write_fd(pipe_fds[1]);
// Make sure kRelauncherSyncFD is a safe value. base::LaunchProcess will
// preserve these three FDs in forked processes, so kRelauncherSyncFD should
// not conflict with them.
static_assert(kRelauncherSyncFD != STDIN_FILENO &&
kRelauncherSyncFD != STDOUT_FILENO &&
kRelauncherSyncFD != STDERR_FILENO,
"kRelauncherSyncFD must not conflict with stdio fds");
base::LaunchOptions options;
options.fds_to_remap.push_back(
std::make_pair(pipe_write_fd.get(), kRelauncherSyncFD));
if (!base::LaunchProcess(relaunch_args, options).IsValid()) {
LOG(ERROR) << "base::LaunchProcess failed";
return false;
}
// The relauncher process is now starting up, or has started up. The
// original parent process continues.
pipe_write_fd.reset(); // close(pipe_fds[1]);
// Synchronize with the relauncher process.
char read_char;
int read_result = HANDLE_EINTR(read(pipe_read_fd.get(), &read_char, 1));
if (read_result != 1) {
if (read_result < 0) {
PLOG(ERROR) << "read";
} else {
LOG(ERROR) << "read: unexpected result " << read_result;
}
return false;
}
// Since a byte has been successfully read from the relauncher process, it's
// guaranteed to have set up its kqueue monitoring this process for exit.
// It's safe to exit now.
return true;
}
namespace {
// In the relauncher process, performs the necessary synchronization steps
// with the parent by setting up a kqueue to watch for it to exit, writing a
// byte to the pipe, and then waiting for the exit notification on the kqueue.
// If anything fails, this logs a message and returns immediately. In those
// situations, it can be assumed that something went wrong with the parent
// process and the best recovery approach is to attempt relaunch anyway.
void RelauncherSynchronizeWithParent() {
base::ScopedFD relauncher_sync_fd(kRelauncherSyncFD);
int parent_pid = getppid();
// PID 1 identifies init. launchd, that is. launchd never starts the
// relauncher process directly, having this parent_pid means that the parent
// already exited and launchd "inherited" the relauncher as its child.
// There's no reason to synchronize with launchd.
if (parent_pid == 1) {
LOG(ERROR) << "unexpected parent_pid";
return;
}
// Set up a kqueue to monitor the parent process for exit.
base::ScopedFD kq(kqueue());
if (!kq.is_valid()) {
PLOG(ERROR) << "kqueue";
return;
}
struct kevent change = { 0 };
EV_SET(&change, parent_pid, EVFILT_PROC, EV_ADD, NOTE_EXIT, 0, NULL);
if (kevent(kq.get(), &change, 1, NULL, 0, NULL) == -1) {
PLOG(ERROR) << "kevent (add)";
return;
}
// Write a '\0' character to the pipe.
if (HANDLE_EINTR(write(relauncher_sync_fd.get(), "", 1)) != 1) {
PLOG(ERROR) << "write";
return;
}
// Up until now, the parent process was blocked in a read waiting for the
// write above to complete. The parent process is now free to exit. Wait for
// that to happen.
struct kevent event;
int events = kevent(kq.get(), NULL, 0, &event, 1, NULL);
if (events != 1) {
if (events < 0) {
PLOG(ERROR) << "kevent (monitor)";
} else {
LOG(ERROR) << "kevent (monitor): unexpected result " << events;
}
return;
}
if (event.filter != EVFILT_PROC ||
event.fflags != NOTE_EXIT ||
event.ident != static_cast<uintptr_t>(parent_pid)) {
LOG(ERROR) << "kevent (monitor): unexpected event, filter " << event.filter
<< ", fflags " << event.fflags << ", ident " << event.ident;
return;
}
}
} // namespace
namespace internal {
int RelauncherMain(const content::MainFunctionParams& main_parameters) {
base::mac::ScopedNSAutoreleasePool pool;
// CommandLine rearranges the order of the arguments returned by
// main_parameters.argv(), rendering it impossible to determine which
// arguments originally came before kRelauncherArgSeparator and which came
// after. It's crucial to distinguish between these because only those
// after the separator should be given to the relaunched process; it's also
// important to not treat the path to the relaunched process as a "loose"
// argument. NXArgc and NXArgv are pointers to the original argc and argv as
// passed to main(), so use those. Access them through _NSGetArgc and
// _NSGetArgv because NXArgc and NXArgv are normally only available to a
// main executable via crt1.o and this code will run from a dylib, and
// because of http://crbug.com/139902.
const int* argcp = _NSGetArgc();
if (!argcp) {
NOTREACHED();
return 1;
}
int argc = *argcp;
const char* const* const* argvp = _NSGetArgv();
if (!argvp) {
NOTREACHED();
return 1;
}
const char* const* argv = *argvp;
if (argc < 4 || RelauncherTypeArg() != argv[1]) {
LOG(ERROR) << "relauncher process invoked with unexpected arguments";
return 1;
}
RelauncherSynchronizeWithParent();
// The capacity for relaunch_args is 4 less than argc, because it
// won't contain the argv[0] of the relauncher process, the
// RelauncherTypeArg() at argv[1], kRelauncherArgSeparator, or the
// executable path of the process to be launched.
base::scoped_nsobject<NSMutableArray> relaunch_args(
[[NSMutableArray alloc] initWithCapacity:argc - 4]);
// Figure out what to execute, what arguments to pass it, and whether to
// start it in the background.
bool background = false;
bool in_relaunch_args = false;
std::string dmg_bsd_device_name;
bool seen_relaunch_executable = false;
std::string relaunch_executable;
const std::string relauncher_arg_separator(kRelauncherArgSeparator);
const std::string relauncher_dmg_device_arg =
base::StringPrintf("--%s=", switches::kRelauncherProcessDMGDevice);
for (int argv_index = 2; argv_index < argc; ++argv_index) {
const std::string arg(argv[argv_index]);
// Strip any -psn_ arguments, as they apply to a specific process.
if (arg.compare(0, strlen(kPSNArg), kPSNArg) == 0) {
continue;
}
if (!in_relaunch_args) {
if (arg == relauncher_arg_separator) {
in_relaunch_args = true;
} else if (arg == kRelauncherBackgroundArg) {
background = true;
} else if (arg.compare(0,
relauncher_dmg_device_arg.size(),
relauncher_dmg_device_arg) == 0) {
dmg_bsd_device_name.assign(
arg.substr(relauncher_dmg_device_arg.size()));
}
} else {
if (!seen_relaunch_executable) {
// The first argument after kRelauncherBackgroundArg is the path to
// the executable file or .app bundle directory. The Launch Services
// interface wants this separate from the rest of the arguments. In
// the relaunched process, this path will still be visible at argv[0].
relaunch_executable.assign(arg);
seen_relaunch_executable = true;
} else {
NSString* arg_string = base::SysUTF8ToNSString(arg);
if (!arg_string) {
LOG(ERROR) << "base::SysUTF8ToNSString failed for " << arg;
return 1;
}
[relaunch_args addObject:arg_string];
}
}
}
if (!seen_relaunch_executable) {
LOG(ERROR) << "nothing to relaunch";
return 1;
}
NSString* path = base::SysUTF8ToNSString(relaunch_executable);
base::scoped_nsobject<NSURL> url([[NSURL alloc] initFileURLWithPath:path]);
NSDictionary* configuration =
@{NSWorkspaceLaunchConfigurationArguments : (relaunch_args.get())};
NSRunningApplication *application = [[NSWorkspace sharedWorkspace]
launchApplicationAtURL:url
options:NSWorkspaceLaunchDefault |
NSWorkspaceLaunchWithErrorPresentation |
(background ? NSWorkspaceLaunchWithoutActivation
: 0) |
NSWorkspaceLaunchNewInstance
configuration:configuration
error:nil];
if (!application) {
LOG(ERROR) << "Failed to relaunch " << relaunch_executable;
return 1;
}
// The application should have relaunched (or is in the process of
// relaunching). From this point on, only clean-up tasks should occur, and
// failures are tolerable.
if (!dmg_bsd_device_name.empty()) {
EjectAndTrashDiskImage(dmg_bsd_device_name);
}
return 0;
}
} // namespace internal
} // namespace mac_relauncher