1// Copyright (c) 2011 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/command_line.h"
13#include "base/file_path.h"
14#include "base/file_util.h"
15#include "base/mac/foundation_util.h"
16#include "base/mac/mac_util.h"
17#include "base/mac/scoped_nsautorelease_pool.h"
18#include "base/memory/scoped_nsobject.h"
19#include "base/path_service.h"
20#include "base/process_util.h"
21#include "base/stringprintf.h"
22#include "base/string_util.h"
23#include "base/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/launchd_mac.h"
30#include "content/common/child_process_host.h"
31
32using ::base::files::FilePathWatcher;
33
34namespace {
35
36#define kServiceProcessSessionType "Background"
37
38CFStringRef CopyServiceProcessLaunchDName() {
39  base::mac::ScopedNSAutoreleasePool pool;
40  NSBundle* bundle = base::mac::MainAppBundle();
41  return CFStringCreateCopy(kCFAllocatorDefault,
42                            base::mac::NSToCFCast([bundle bundleIdentifier]));
43}
44
45NSString* GetServiceProcessLaunchDLabel() {
46  scoped_nsobject<NSString> name(
47      base::mac::CFToNSCast(CopyServiceProcessLaunchDName()));
48  NSString *label = [name stringByAppendingString:@".service_process"];
49  FilePath user_data_dir;
50  PathService::Get(chrome::DIR_USER_DATA, &user_data_dir);
51  std::string user_data_dir_path = user_data_dir.value();
52  NSString *ns_path = base::SysUTF8ToNSString(user_data_dir_path);
53  ns_path = [ns_path stringByReplacingOccurrencesOfString:@" "
54                                               withString:@"_"];
55  label = [label stringByAppendingString:ns_path];
56  return label;
57}
58
59NSString* GetServiceProcessLaunchDSocketKey() {
60  return @"ServiceProcessSocket";
61}
62
63NSString* GetServiceProcessLaunchDSocketEnvVar() {
64  NSString *label = GetServiceProcessLaunchDLabel();
65  NSString *env_var = [label stringByReplacingOccurrencesOfString:@"."
66                                                       withString:@"_"];
67  env_var = [env_var stringByAppendingString:@"_SOCKET"];
68  env_var = [env_var uppercaseString];
69  return env_var;
70}
71
72bool GetParentFSRef(const FSRef& child, FSRef* parent) {
73  return FSGetCatalogInfo(&child, 0, NULL, NULL, NULL, parent) == noErr;
74}
75
76bool RemoveFromLaunchd() {
77  // We're killing a file.
78  base::ThreadRestrictions::AssertIOAllowed();
79  base::mac::ScopedCFTypeRef<CFStringRef> name(CopyServiceProcessLaunchDName());
80  return Launchd::GetInstance()->DeletePlist(Launchd::User,
81                                             Launchd::Agent,
82                                             name);
83}
84
85class ExecFilePathWatcherDelegate : public FilePathWatcher::Delegate {
86 public:
87  ExecFilePathWatcherDelegate() { }
88  virtual ~ExecFilePathWatcherDelegate() { }
89
90  bool Init(const FilePath& path);
91  virtual void OnFilePathChanged(const FilePath& path) OVERRIDE;
92
93 private:
94  FSRef executable_fsref_;
95};
96
97}  // namespace
98
99// Gets the name of the service process IPC channel.
100IPC::ChannelHandle GetServiceProcessChannel() {
101  base::mac::ScopedNSAutoreleasePool pool;
102  std::string socket_path;
103  scoped_nsobject<NSDictionary> dictionary(
104      base::mac::CFToNSCast(Launchd::GetInstance()->CopyExports()));
105  NSString *ns_socket_path =
106      [dictionary objectForKey:GetServiceProcessLaunchDSocketEnvVar()];
107  if (ns_socket_path) {
108    socket_path = base::SysNSStringToUTF8(ns_socket_path);
109  }
110  return IPC::ChannelHandle(socket_path);
111}
112
113bool ForceServiceProcessShutdown(const std::string& /* version */,
114                                 base::ProcessId /* process_id */) {
115  base::mac::ScopedNSAutoreleasePool pool;
116  CFStringRef label = base::mac::NSToCFCast(GetServiceProcessLaunchDLabel());
117  CFErrorRef err = NULL;
118  bool ret = Launchd::GetInstance()->RemoveJob(label, &err);
119  if (!ret) {
120    LOG(ERROR) << "ForceServiceProcessShutdown: " << err;
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  scoped_nsobject<NSDictionary> launchd_conf(base::mac::CFToNSCast(
130      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          LOG(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        LOG(ERROR) << "Unable to get bundle at: "
157                   << reinterpret_cast<CFStringRef>(bundle_path);
158      }
159    } else {
160      LOG(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
178  if (!dict) {
179    LOG(ERROR) << "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  CHECK(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  CHECK_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  scoped_ptr<Version> service_version(Version::GetVersionFromString(version));
207  bool ready = true;
208  if (!service_version.get()) {
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() << "Failed to get current file version";
216      ready = true;
217    }
218    else {
219      scoped_ptr<Version> running_version(Version::GetVersionFromString(
220          version_info.Version()));
221      if (!running_version.get()) {
222        // Our own version is invalid. This is an error case. Pretend that we
223        // are out of date.
224        NOTREACHED() << "Failed to parse version info";
225        ready = true;
226      } else if (running_version->CompareTo(*service_version) > 0) {
227        ready = false;
228      } else {
229        ready = true;
230      }
231    }
232  }
233  if (!ready) {
234    ForceServiceProcessShutdown(version, pid);
235  }
236  return ready;
237}
238
239CFDictionaryRef CreateServiceProcessLaunchdPlist(CommandLine* cmd_line,
240                                                 bool for_auto_launch) {
241  base::mac::ScopedNSAutoreleasePool pool;
242
243  NSString *program =
244      base::SysUTF8ToNSString(cmd_line->GetProgram().value());
245
246  std::vector<std::string> args = cmd_line->argv();
247  NSMutableArray *ns_args = [NSMutableArray arrayWithCapacity:args.size()];
248
249  for (std::vector<std::string>::iterator iter = args.begin();
250       iter < args.end();
251       ++iter) {
252    [ns_args addObject:base::SysUTF8ToNSString(*iter)];
253  }
254
255  NSDictionary *socket =
256      [NSDictionary dictionaryWithObject:GetServiceProcessLaunchDSocketEnvVar()
257                                  forKey:@ LAUNCH_JOBSOCKETKEY_SECUREWITHKEY];
258  NSDictionary *sockets =
259      [NSDictionary dictionaryWithObject:socket
260                                  forKey:GetServiceProcessLaunchDSocketKey()];
261
262  // See the man page for launchd.plist.
263  NSMutableDictionary *launchd_plist =
264      [[NSMutableDictionary alloc] initWithObjectsAndKeys:
265        GetServiceProcessLaunchDLabel(), @ LAUNCH_JOBKEY_LABEL,
266        program, @ LAUNCH_JOBKEY_PROGRAM,
267        ns_args, @ LAUNCH_JOBKEY_PROGRAMARGUMENTS,
268        sockets, @ LAUNCH_JOBKEY_SOCKETS,
269        nil];
270
271  if (for_auto_launch) {
272    // We want the service process to be able to exit if there are no services
273    // enabled. With a value of NO in the SuccessfulExit key, launchd will
274    // relaunch the service automatically in any other case than exiting
275    // cleanly with a 0 return code.
276    NSDictionary *keep_alive =
277      [NSDictionary
278        dictionaryWithObject:[NSNumber numberWithBool:NO]
279                      forKey:@ LAUNCH_JOBKEY_KEEPALIVE_SUCCESSFULEXIT];
280    NSDictionary *auto_launchd_plist =
281      [[NSDictionary alloc] initWithObjectsAndKeys:
282        [NSNumber numberWithBool:YES], @ LAUNCH_JOBKEY_RUNATLOAD,
283        keep_alive, @ LAUNCH_JOBKEY_KEEPALIVE,
284        @ kServiceProcessSessionType, @ LAUNCH_JOBKEY_LIMITLOADTOSESSIONTYPE,
285        nil];
286    [launchd_plist addEntriesFromDictionary:auto_launchd_plist];
287  }
288  return reinterpret_cast<CFDictionaryRef>(launchd_plist);
289}
290
291// Writes the launchd property list into the user's LaunchAgents directory,
292// creating that directory if needed. This will cause the service process to be
293// auto launched on the next user login.
294bool ServiceProcessState::AddToAutoRun() {
295  // We're creating directories and writing a file.
296  base::ThreadRestrictions::AssertIOAllowed();
297  DCHECK(autorun_command_line_.get());
298  base::mac::ScopedCFTypeRef<CFStringRef> name(CopyServiceProcessLaunchDName());
299  base::mac::ScopedCFTypeRef<CFDictionaryRef> plist(
300      CreateServiceProcessLaunchdPlist(autorun_command_line_.get(), true));
301  return Launchd::GetInstance()->WritePlistToFile(Launchd::User,
302                                                  Launchd::Agent,
303                                                  name,
304                                                  plist);
305}
306
307bool ServiceProcessState::RemoveFromAutoRun() {
308  return RemoveFromLaunchd();
309}
310
311bool ServiceProcessState::StateData::WatchExecutable() {
312  base::mac::ScopedNSAutoreleasePool pool;
313  NSDictionary* ns_launchd_conf = base::mac::CFToNSCast(launchd_conf_);
314  NSString* exe_path = [ns_launchd_conf objectForKey:@ LAUNCH_JOBKEY_PROGRAM];
315  if (!exe_path) {
316    LOG(ERROR) << "No " LAUNCH_JOBKEY_PROGRAM;
317    return false;
318  }
319
320  FilePath executable_path = FilePath([exe_path fileSystemRepresentation]);
321  scoped_ptr<ExecFilePathWatcherDelegate> delegate(
322      new ExecFilePathWatcherDelegate);
323  if (!delegate->Init(executable_path)) {
324    LOG(ERROR) << "executable_watcher_.Init " << executable_path.value();
325    return false;
326  }
327  if (!executable_watcher_.Watch(executable_path, delegate.release())) {
328    LOG(ERROR) << "executable_watcher_.watch " << executable_path.value();
329    return false;
330  }
331  return true;
332}
333
334bool ExecFilePathWatcherDelegate::Init(const FilePath& path) {
335  return base::mac::FSRefFromPath(path.value(), &executable_fsref_);
336}
337
338void ExecFilePathWatcherDelegate::OnFilePathChanged(const FilePath& path) {
339  base::mac::ScopedNSAutoreleasePool pool;
340  bool needs_shutdown = false;
341  bool needs_restart = false;
342  bool good_bundle = false;
343
344  FSRef macos_fsref;
345  if (GetParentFSRef(executable_fsref_, &macos_fsref)) {
346    FSRef contents_fsref;
347    if (GetParentFSRef(macos_fsref, &contents_fsref)) {
348      FSRef bundle_fsref;
349      if (GetParentFSRef(contents_fsref, &bundle_fsref)) {
350        base::mac::ScopedCFTypeRef<CFURLRef> bundle_url(
351            CFURLCreateFromFSRef(kCFAllocatorDefault, &bundle_fsref));
352        if (bundle_url.get()) {
353          base::mac::ScopedCFTypeRef<CFBundleRef> bundle(
354              CFBundleCreate(kCFAllocatorDefault, bundle_url));
355          // Check to see if the bundle still has a minimal structure.
356          good_bundle = CFBundleGetIdentifier(bundle) != NULL;
357        }
358      }
359    }
360  }
361  if (!good_bundle) {
362    needs_shutdown = true;
363  } else {
364    Boolean in_trash;
365    OSErr err = FSDetermineIfRefIsEnclosedByFolder(kOnAppropriateDisk,
366                                                   kTrashFolderType,
367                                                   &executable_fsref_,
368                                                   &in_trash);
369    if (err == noErr && in_trash) {
370      needs_shutdown = true;
371    } else {
372      bool was_moved = true;
373      FSRef path_ref;
374      if (base::mac::FSRefFromPath(path.value(), &path_ref)) {
375        if (FSCompareFSRefs(&path_ref, &executable_fsref_) == noErr) {
376          was_moved = false;
377        }
378      }
379      if (was_moved) {
380        needs_restart = true;
381      }
382    }
383  }
384  if (needs_shutdown || needs_restart) {
385    // First deal with the plist.
386    base::mac::ScopedCFTypeRef<CFStringRef> name(
387        CopyServiceProcessLaunchDName());
388    if (needs_restart) {
389      base::mac::ScopedCFTypeRef<CFMutableDictionaryRef> plist(
390         Launchd::GetInstance()->CreatePlistFromFile(Launchd::User,
391                                                     Launchd::Agent,
392                                                     name));
393      if (plist.get()) {
394        NSMutableDictionary* ns_plist = base::mac::CFToNSCast(plist);
395        std::string new_path = base::mac::PathFromFSRef(executable_fsref_);
396        NSString* ns_new_path = base::SysUTF8ToNSString(new_path);
397        [ns_plist setObject:ns_new_path forKey:@ LAUNCH_JOBKEY_PROGRAM];
398        scoped_nsobject<NSMutableArray> args(
399            [[ns_plist objectForKey:@ LAUNCH_JOBKEY_PROGRAMARGUMENTS]
400             mutableCopy]);
401        [args replaceObjectAtIndex:0 withObject:ns_new_path];
402        [ns_plist setObject:args forKey:@ LAUNCH_JOBKEY_PROGRAMARGUMENTS];
403        if (!Launchd::GetInstance()->WritePlistToFile(Launchd::User,
404                                                      Launchd::Agent,
405                                                      name,
406                                                      plist)) {
407          LOG(ERROR) << "Unable to rewrite plist.";
408          needs_shutdown = true;
409        }
410      } else {
411        LOG(ERROR) << "Unable to read plist.";
412        needs_shutdown = true;
413      }
414    }
415    if (needs_shutdown) {
416      if (!RemoveFromLaunchd()) {
417        LOG(ERROR) << "Unable to RemoveFromLaunchd.";
418      }
419    }
420
421    // Then deal with the process.
422    CFStringRef session_type = CFSTR(kServiceProcessSessionType);
423    if (needs_restart) {
424      if (!Launchd::GetInstance()->RestartJob(Launchd::User,
425                                              Launchd::Agent,
426                                              name,
427                                              session_type)) {
428        LOG(ERROR) << "RestartLaunchdJob";
429        needs_shutdown = true;
430      }
431    }
432    if (needs_shutdown) {
433      CFStringRef label =
434          base::mac::NSToCFCast(GetServiceProcessLaunchDLabel());
435      CFErrorRef err = NULL;
436      if (!Launchd::GetInstance()->RemoveJob(label, &err)) {
437        base::mac::ScopedCFTypeRef<CFErrorRef> scoped_err(err);
438        LOG(ERROR) << "RemoveJob " << err;
439        // Exiting with zero, so launchd doesn't restart the process.
440        exit(0);
441      }
442    }
443  }
444}
445