Home | History | Annotate | Download | only in common
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "chrome/common/service_process_util_posix.h"
      6 
      7 #import <Foundation/Foundation.h>
      8 #include <launch.h>
      9 
     10 #include <vector>
     11 
     12 #include "base/bind.h"
     13 #include "base/command_line.h"
     14 #include "base/files/file_path.h"
     15 #include "base/mac/bundle_locations.h"
     16 #include "base/mac/foundation_util.h"
     17 #include "base/mac/mac_util.h"
     18 #include "base/mac/scoped_nsautorelease_pool.h"
     19 #include "base/mac/scoped_nsobject.h"
     20 #include "base/path_service.h"
     21 #include "base/strings/string_util.h"
     22 #include "base/strings/stringprintf.h"
     23 #include "base/strings/sys_string_conversions.h"
     24 #include "base/threading/thread_restrictions.h"
     25 #include "base/version.h"
     26 #include "chrome/common/chrome_paths.h"
     27 #include "chrome/common/chrome_switches.h"
     28 #include "chrome/common/chrome_version_info.h"
     29 #include "chrome/common/mac/launchd.h"
     30 
     31 using ::base::FilePathWatcher;
     32 
     33 namespace {
     34 
     35 #define kServiceProcessSessionType "Aqua"
     36 
     37 CFStringRef CopyServiceProcessLaunchDName() {
     38   base::mac::ScopedNSAutoreleasePool pool;
     39   NSBundle* bundle = base::mac::FrameworkBundle();
     40   return CFStringCreateCopy(kCFAllocatorDefault,
     41                             base::mac::NSToCFCast([bundle bundleIdentifier]));
     42 }
     43 
     44 NSString* GetServiceProcessLaunchDLabel() {
     45   base::scoped_nsobject<NSString> name(
     46       base::mac::CFToNSCast(CopyServiceProcessLaunchDName()));
     47   NSString *label = [name stringByAppendingString:@".service_process"];
     48   base::FilePath user_data_dir;
     49   PathService::Get(chrome::DIR_USER_DATA, &user_data_dir);
     50   std::string user_data_dir_path = user_data_dir.value();
     51   NSString *ns_path = base::SysUTF8ToNSString(user_data_dir_path);
     52   ns_path = [ns_path stringByReplacingOccurrencesOfString:@" "
     53                                                withString:@"_"];
     54   label = [label stringByAppendingString:ns_path];
     55   return label;
     56 }
     57 
     58 NSString* GetServiceProcessLaunchDSocketKey() {
     59   return @"ServiceProcessSocket";
     60 }
     61 
     62 bool GetParentFSRef(const FSRef& child, FSRef* parent) {
     63   return FSGetCatalogInfo(&child, 0, NULL, NULL, NULL, parent) == noErr;
     64 }
     65 
     66 bool RemoveFromLaunchd() {
     67   // We're killing a file.
     68   base::ThreadRestrictions::AssertIOAllowed();
     69   base::ScopedCFTypeRef<CFStringRef> name(CopyServiceProcessLaunchDName());
     70   return Launchd::GetInstance()->DeletePlist(Launchd::User,
     71                                              Launchd::Agent,
     72                                              name);
     73 }
     74 
     75 class ExecFilePathWatcherCallback {
     76  public:
     77   ExecFilePathWatcherCallback() {}
     78   ~ExecFilePathWatcherCallback() {}
     79 
     80   bool Init(const base::FilePath& path);
     81   void NotifyPathChanged(const base::FilePath& path, bool error);
     82 
     83  private:
     84   FSRef executable_fsref_;
     85 };
     86 
     87 }  // namespace
     88 
     89 NSString* GetServiceProcessLaunchDSocketEnvVar() {
     90   NSString *label = GetServiceProcessLaunchDLabel();
     91   NSString *env_var = [label stringByReplacingOccurrencesOfString:@"."
     92                                                        withString:@"_"];
     93   env_var = [env_var stringByAppendingString:@"_SOCKET"];
     94   env_var = [env_var uppercaseString];
     95   return env_var;
     96 }
     97 
     98 // Gets the name of the service process IPC channel.
     99 IPC::ChannelHandle GetServiceProcessChannel() {
    100   base::mac::ScopedNSAutoreleasePool pool;
    101   std::string socket_path;
    102   base::scoped_nsobject<NSDictionary> dictionary(
    103       base::mac::CFToNSCast(Launchd::GetInstance()->CopyExports()));
    104   NSString *ns_socket_path =
    105       [dictionary objectForKey:GetServiceProcessLaunchDSocketEnvVar()];
    106   if (ns_socket_path) {
    107     socket_path = base::SysNSStringToUTF8(ns_socket_path);
    108   }
    109   return IPC::ChannelHandle(socket_path);
    110 }
    111 
    112 bool ForceServiceProcessShutdown(const std::string& /* version */,
    113                                  base::ProcessId /* process_id */) {
    114   base::mac::ScopedNSAutoreleasePool pool;
    115   CFStringRef label = base::mac::NSToCFCast(GetServiceProcessLaunchDLabel());
    116   CFErrorRef err = NULL;
    117   bool ret = Launchd::GetInstance()->RemoveJob(label, &err);
    118   if (!ret) {
    119     DLOG(ERROR) << "ForceServiceProcessShutdown: " << err << " "
    120                 << base::SysCFStringRefToUTF8(label);
    121     CFRelease(err);
    122   }
    123   return ret;
    124 }
    125 
    126 bool GetServiceProcessData(std::string* version, base::ProcessId* pid) {
    127   base::mac::ScopedNSAutoreleasePool pool;
    128   CFStringRef label = base::mac::NSToCFCast(GetServiceProcessLaunchDLabel());
    129   base::scoped_nsobject<NSDictionary> launchd_conf(
    130       base::mac::CFToNSCast(Launchd::GetInstance()->CopyJobDictionary(label)));
    131   if (!launchd_conf.get()) {
    132     return false;
    133   }
    134   // Anything past here will return true in that there does appear
    135   // to be a service process of some sort registered with launchd.
    136   if (version) {
    137     *version = "0";
    138     NSString *exe_path = [launchd_conf objectForKey:@ LAUNCH_JOBKEY_PROGRAM];
    139     if (exe_path) {
    140       NSString *bundle_path = [[[exe_path stringByDeletingLastPathComponent]
    141                                 stringByDeletingLastPathComponent]
    142                                stringByDeletingLastPathComponent];
    143       NSBundle *bundle = [NSBundle bundleWithPath:bundle_path];
    144       if (bundle) {
    145         NSString *ns_version =
    146             [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
    147         if (ns_version) {
    148           *version = base::SysNSStringToUTF8(ns_version);
    149         } else {
    150           DLOG(ERROR) << "Unable to get version at: "
    151                       << reinterpret_cast<CFStringRef>(bundle_path);
    152         }
    153       } else {
    154         // The bundle has been deleted out from underneath the registered
    155         // job.
    156         DLOG(ERROR) << "Unable to get bundle at: "
    157                     << reinterpret_cast<CFStringRef>(bundle_path);
    158       }
    159     } else {
    160       DLOG(ERROR) << "Unable to get executable path for service process";
    161     }
    162   }
    163   if (pid) {
    164     *pid = -1;
    165     NSNumber* ns_pid = [launchd_conf objectForKey:@ LAUNCH_JOBKEY_PID];
    166     if (ns_pid) {
    167      *pid = [ns_pid intValue];
    168     }
    169   }
    170   return true;
    171 }
    172 
    173 bool ServiceProcessState::Initialize() {
    174   CFErrorRef err = NULL;
    175   CFDictionaryRef dict =
    176       Launchd::GetInstance()->CopyDictionaryByCheckingIn(&err);
    177   if (!dict) {
    178     DLOG(ERROR) << "ServiceProcess must be launched by launchd. "
    179                 << "CopyLaunchdDictionaryByCheckingIn: " << err;
    180     CFRelease(err);
    181     return false;
    182   }
    183   state_->launchd_conf_.reset(dict);
    184   return true;
    185 }
    186 
    187 IPC::ChannelHandle ServiceProcessState::GetServiceProcessChannel() {
    188   DCHECK(state_);
    189   NSDictionary *ns_launchd_conf = base::mac::CFToNSCast(state_->launchd_conf_);
    190   NSDictionary* socket_dict =
    191       [ns_launchd_conf objectForKey:@ LAUNCH_JOBKEY_SOCKETS];
    192   NSArray* sockets =
    193       [socket_dict objectForKey:GetServiceProcessLaunchDSocketKey()];
    194   DCHECK_EQ([sockets count], 1U);
    195   int socket = [[sockets objectAtIndex:0] intValue];
    196   base::FileDescriptor fd(socket, false);
    197   return IPC::ChannelHandle(std::string(), fd);
    198 }
    199 
    200 bool CheckServiceProcessReady() {
    201   std::string version;
    202   pid_t pid;
    203   if (!GetServiceProcessData(&version, &pid)) {
    204     return false;
    205   }
    206   Version service_version(version);
    207   bool ready = true;
    208   if (!service_version.IsValid()) {
    209     ready = false;
    210   } else {
    211     chrome::VersionInfo version_info;
    212     if (!version_info.is_valid()) {
    213       // Our own version is invalid. This is an error case. Pretend that we
    214       // are out of date.
    215       NOTREACHED();
    216       ready = true;
    217     }
    218     else {
    219       Version running_version(version_info.Version());
    220       if (!running_version.IsValid()) {
    221         // Our own version is invalid. This is an error case. Pretend that we
    222         // are out of date.
    223         NOTREACHED();
    224         ready = true;
    225       } else if (running_version.CompareTo(service_version) > 0) {
    226         ready = false;
    227       } else {
    228         ready = true;
    229       }
    230     }
    231   }
    232   if (!ready) {
    233     ForceServiceProcessShutdown(version, pid);
    234   }
    235   return ready;
    236 }
    237 
    238 CFDictionaryRef CreateServiceProcessLaunchdPlist(CommandLine* cmd_line,
    239                                                  bool for_auto_launch) {
    240   base::mac::ScopedNSAutoreleasePool pool;
    241 
    242   NSString *program =
    243       base::SysUTF8ToNSString(cmd_line->GetProgram().value());
    244 
    245   std::vector<std::string> args = cmd_line->argv();
    246   NSMutableArray *ns_args = [NSMutableArray arrayWithCapacity:args.size()];
    247 
    248   for (std::vector<std::string>::iterator iter = args.begin();
    249        iter < args.end();
    250        ++iter) {
    251     [ns_args addObject:base::SysUTF8ToNSString(*iter)];
    252   }
    253 
    254   NSDictionary *socket =
    255       [NSDictionary dictionaryWithObject:GetServiceProcessLaunchDSocketEnvVar()
    256                                   forKey:@ LAUNCH_JOBSOCKETKEY_SECUREWITHKEY];
    257   NSDictionary *sockets =
    258       [NSDictionary dictionaryWithObject:socket
    259                                   forKey:GetServiceProcessLaunchDSocketKey()];
    260 
    261   // See the man page for launchd.plist.
    262   NSMutableDictionary *launchd_plist =
    263       [[NSMutableDictionary alloc] initWithObjectsAndKeys:
    264         GetServiceProcessLaunchDLabel(), @ LAUNCH_JOBKEY_LABEL,
    265         program, @ LAUNCH_JOBKEY_PROGRAM,
    266         ns_args, @ LAUNCH_JOBKEY_PROGRAMARGUMENTS,
    267         sockets, @ LAUNCH_JOBKEY_SOCKETS,
    268         nil];
    269 
    270   if (for_auto_launch) {
    271     // We want the service process to be able to exit if there are no services
    272     // enabled. With a value of NO in the SuccessfulExit key, launchd will
    273     // relaunch the service automatically in any other case than exiting
    274     // cleanly with a 0 return code.
    275     NSDictionary *keep_alive =
    276       [NSDictionary
    277         dictionaryWithObject:[NSNumber numberWithBool:NO]
    278                       forKey:@ LAUNCH_JOBKEY_KEEPALIVE_SUCCESSFULEXIT];
    279     NSDictionary *auto_launchd_plist =
    280       [[NSDictionary alloc] initWithObjectsAndKeys:
    281         [NSNumber numberWithBool:YES], @ LAUNCH_JOBKEY_RUNATLOAD,
    282         keep_alive, @ LAUNCH_JOBKEY_KEEPALIVE,
    283         @ kServiceProcessSessionType, @ LAUNCH_JOBKEY_LIMITLOADTOSESSIONTYPE,
    284         nil];
    285     [launchd_plist addEntriesFromDictionary:auto_launchd_plist];
    286   }
    287   return reinterpret_cast<CFDictionaryRef>(launchd_plist);
    288 }
    289 
    290 // Writes the launchd property list into the user's LaunchAgents directory,
    291 // creating that directory if needed. This will cause the service process to be
    292 // auto launched on the next user login.
    293 bool ServiceProcessState::AddToAutoRun() {
    294   // We're creating directories and writing a file.
    295   base::ThreadRestrictions::AssertIOAllowed();
    296   DCHECK(autorun_command_line_.get());
    297   base::ScopedCFTypeRef<CFStringRef> name(CopyServiceProcessLaunchDName());
    298   base::ScopedCFTypeRef<CFDictionaryRef> plist(
    299       CreateServiceProcessLaunchdPlist(autorun_command_line_.get(), true));
    300   return Launchd::GetInstance()->WritePlistToFile(Launchd::User,
    301                                                   Launchd::Agent,
    302                                                   name,
    303                                                   plist);
    304 }
    305 
    306 bool ServiceProcessState::RemoveFromAutoRun() {
    307   return RemoveFromLaunchd();
    308 }
    309 
    310 bool ServiceProcessState::StateData::WatchExecutable() {
    311   base::mac::ScopedNSAutoreleasePool pool;
    312   NSDictionary* ns_launchd_conf = base::mac::CFToNSCast(launchd_conf_);
    313   NSString* exe_path = [ns_launchd_conf objectForKey:@ LAUNCH_JOBKEY_PROGRAM];
    314   if (!exe_path) {
    315     DLOG(ERROR) << "No " LAUNCH_JOBKEY_PROGRAM;
    316     return false;
    317   }
    318 
    319   base::FilePath executable_path =
    320       base::FilePath([exe_path fileSystemRepresentation]);
    321   scoped_ptr<ExecFilePathWatcherCallback> callback(
    322       new ExecFilePathWatcherCallback);
    323   if (!callback->Init(executable_path)) {
    324     DLOG(ERROR) << "executable_watcher_.Init " << executable_path.value();
    325     return false;
    326   }
    327   if (!executable_watcher_.Watch(
    328           executable_path,
    329           false,
    330           base::Bind(&ExecFilePathWatcherCallback::NotifyPathChanged,
    331                      base::Owned(callback.release())))) {
    332     DLOG(ERROR) << "executable_watcher_.watch " << executable_path.value();
    333     return false;
    334   }
    335   return true;
    336 }
    337 
    338 bool ExecFilePathWatcherCallback::Init(const base::FilePath& path) {
    339   return base::mac::FSRefFromPath(path.value(), &executable_fsref_);
    340 }
    341 
    342 void ExecFilePathWatcherCallback::NotifyPathChanged(const base::FilePath& path,
    343                                                     bool error) {
    344   if (error) {
    345     NOTREACHED();  // TODO(darin): Do something smarter?
    346     return;
    347   }
    348 
    349   base::mac::ScopedNSAutoreleasePool pool;
    350   bool needs_shutdown = false;
    351   bool needs_restart = false;
    352   bool good_bundle = false;
    353 
    354   FSRef macos_fsref;
    355   if (GetParentFSRef(executable_fsref_, &macos_fsref)) {
    356     FSRef contents_fsref;
    357     if (GetParentFSRef(macos_fsref, &contents_fsref)) {
    358       FSRef bundle_fsref;
    359       if (GetParentFSRef(contents_fsref, &bundle_fsref)) {
    360         base::ScopedCFTypeRef<CFURLRef> bundle_url(
    361             CFURLCreateFromFSRef(kCFAllocatorDefault, &bundle_fsref));
    362         if (bundle_url.get()) {
    363           base::ScopedCFTypeRef<CFBundleRef> bundle(
    364               CFBundleCreate(kCFAllocatorDefault, bundle_url));
    365           // Check to see if the bundle still has a minimal structure.
    366           good_bundle = CFBundleGetIdentifier(bundle) != NULL;
    367         }
    368       }
    369     }
    370   }
    371   if (!good_bundle) {
    372     needs_shutdown = true;
    373   } else {
    374     Boolean in_trash;
    375     OSErr err = FSDetermineIfRefIsEnclosedByFolder(kOnAppropriateDisk,
    376                                                    kTrashFolderType,
    377                                                    &executable_fsref_,
    378                                                    &in_trash);
    379     if (err == noErr && in_trash) {
    380       needs_shutdown = true;
    381     } else {
    382       bool was_moved = true;
    383       FSRef path_ref;
    384       if (base::mac::FSRefFromPath(path.value(), &path_ref)) {
    385         if (FSCompareFSRefs(&path_ref, &executable_fsref_) == noErr) {
    386           was_moved = false;
    387         }
    388       }
    389       if (was_moved) {
    390         needs_restart = true;
    391       }
    392     }
    393   }
    394   if (needs_shutdown || needs_restart) {
    395     // First deal with the plist.
    396     base::ScopedCFTypeRef<CFStringRef> name(CopyServiceProcessLaunchDName());
    397     if (needs_restart) {
    398       base::ScopedCFTypeRef<CFMutableDictionaryRef> plist(
    399           Launchd::GetInstance()->CreatePlistFromFile(
    400               Launchd::User, Launchd::Agent, name));
    401       if (plist.get()) {
    402         NSMutableDictionary* ns_plist = base::mac::CFToNSCast(plist);
    403         std::string new_path = base::mac::PathFromFSRef(executable_fsref_);
    404         NSString* ns_new_path = base::SysUTF8ToNSString(new_path);
    405         [ns_plist setObject:ns_new_path forKey:@ LAUNCH_JOBKEY_PROGRAM];
    406         base::scoped_nsobject<NSMutableArray> args([[ns_plist
    407             objectForKey:@LAUNCH_JOBKEY_PROGRAMARGUMENTS] mutableCopy]);
    408         [args replaceObjectAtIndex:0 withObject:ns_new_path];
    409         [ns_plist setObject:args forKey:@ LAUNCH_JOBKEY_PROGRAMARGUMENTS];
    410         if (!Launchd::GetInstance()->WritePlistToFile(Launchd::User,
    411                                                       Launchd::Agent,
    412                                                       name,
    413                                                       plist)) {
    414           DLOG(ERROR) << "Unable to rewrite plist.";
    415           needs_shutdown = true;
    416         }
    417       } else {
    418         DLOG(ERROR) << "Unable to read plist.";
    419         needs_shutdown = true;
    420       }
    421     }
    422     if (needs_shutdown) {
    423       if (!RemoveFromLaunchd()) {
    424         DLOG(ERROR) << "Unable to RemoveFromLaunchd.";
    425       }
    426     }
    427 
    428     // Then deal with the process.
    429     CFStringRef session_type = CFSTR(kServiceProcessSessionType);
    430     if (needs_restart) {
    431       if (!Launchd::GetInstance()->RestartJob(Launchd::User,
    432                                               Launchd::Agent,
    433                                               name,
    434                                               session_type)) {
    435         DLOG(ERROR) << "RestartLaunchdJob";
    436         needs_shutdown = true;
    437       }
    438     }
    439     if (needs_shutdown) {
    440       CFStringRef label =
    441           base::mac::NSToCFCast(GetServiceProcessLaunchDLabel());
    442       CFErrorRef err = NULL;
    443       if (!Launchd::GetInstance()->RemoveJob(label, &err)) {
    444         base::ScopedCFTypeRef<CFErrorRef> scoped_err(err);
    445         DLOG(ERROR) << "RemoveJob " << err;
    446         // Exiting with zero, so launchd doesn't restart the process.
    447         exit(0);
    448       }
    449     }
    450   }
    451 }
    452