• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
31using ::base::FilePathWatcher;
32
33namespace {
34
35#define kServiceProcessSessionType "Aqua"
36
37CFStringRef CopyServiceProcessLaunchDName() {
38  base::mac::ScopedNSAutoreleasePool pool;
39  NSBundle* bundle = base::mac::FrameworkBundle();
40  return CFStringCreateCopy(kCFAllocatorDefault,
41                            base::mac::NSToCFCast([bundle bundleIdentifier]));
42}
43
44NSString* 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
58NSString* GetServiceProcessLaunchDSocketKey() {
59  return @"ServiceProcessSocket";
60}
61
62bool GetParentFSRef(const FSRef& child, FSRef* parent) {
63  return FSGetCatalogInfo(&child, 0, NULL, NULL, NULL, parent) == noErr;
64}
65
66bool 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
75class 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
89NSString* 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.
99IPC::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
112bool 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
126bool 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
173bool 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
187IPC::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
200bool 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
238CFDictionaryRef 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.
293bool 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
306bool ServiceProcessState::RemoveFromAutoRun() {
307  return RemoveFromLaunchd();
308}
309
310bool 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
338bool ExecFilePathWatcherCallback::Init(const base::FilePath& path) {
339  return base::mac::FSRefFromPath(path.value(), &executable_fsref_);
340}
341
342void 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