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.
5 #include "chrome/common/service_process_util_posix.h"
7 #import <Foundation/Foundation.h>
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/metrics/histogram_macros.h"
21 #include "base/path_service.h"
22 #include "base/strings/string_util.h"
23 #include "base/strings/stringprintf.h"
24 #include "base/strings/sys_string_conversions.h"
25 #include "base/threading/thread_restrictions.h"
26 #include "base/version.h"
27 #include "chrome/common/chrome_paths.h"
28 #include "chrome/common/chrome_switches.h"
29 #include "chrome/common/chrome_version_info.h"
30 #include "chrome/common/mac/launchd.h"
31 #include "ipc/unix_domain_socket_util.h"
33 using ::base::FilePathWatcher;
37 #define kServiceProcessSessionType "Aqua"
39 CFStringRef CopyServiceProcessLaunchDName() {
40 base::mac::ScopedNSAutoreleasePool pool;
41 NSBundle* bundle = base::mac::FrameworkBundle();
42 return CFStringCreateCopy(kCFAllocatorDefault,
43 base::mac::NSToCFCast([bundle bundleIdentifier]));
46 NSString* GetServiceProcessLaunchDLabel() {
47 base::scoped_nsobject<NSString> name(
48 base::mac::CFToNSCast(CopyServiceProcessLaunchDName()));
49 NSString* label = [name stringByAppendingString:@".service_process"];
50 base::FilePath user_data_dir;
51 PathService::Get(chrome::DIR_USER_DATA, &user_data_dir);
52 std::string user_data_dir_path = user_data_dir.value();
53 NSString* ns_path = base::SysUTF8ToNSString(user_data_dir_path);
54 ns_path = [ns_path stringByReplacingOccurrencesOfString:@" "
56 label = [label stringByAppendingString:ns_path];
60 NSString* GetServiceProcessLaunchDSocketKey() {
61 return @"ServiceProcessSocket";
64 bool GetParentFSRef(const FSRef& child, FSRef* parent) {
65 return FSGetCatalogInfo(&child, 0, NULL, NULL, NULL, parent) == noErr;
68 bool RemoveFromLaunchd() {
69 // We're killing a file.
70 base::ThreadRestrictions::AssertIOAllowed();
71 base::ScopedCFTypeRef<CFStringRef> name(CopyServiceProcessLaunchDName());
72 return Launchd::GetInstance()->DeletePlist(Launchd::User,
77 class ExecFilePathWatcherCallback {
79 ExecFilePathWatcherCallback() {}
80 ~ExecFilePathWatcherCallback() {}
82 bool Init(const base::FilePath& path);
83 void NotifyPathChanged(const base::FilePath& path, bool error);
86 FSRef executable_fsref_;
89 base::FilePath GetServiceProcessSocketName() {
90 base::FilePath socket_name;
91 PathService::Get(base::DIR_TEMP, &socket_name);
92 std::string pipe_name = GetServiceProcessScopedName("srv");
93 socket_name = socket_name.Append(pipe_name);
94 UMA_HISTOGRAM_CUSTOM_COUNTS("CloudPrint.ServiceProcessSocketLength",
95 socket_name.value().size(), 75, 124, 50);
96 if (socket_name.value().size() < IPC::kMaxSocketNameLength)
98 // Fallback to /tmp if $TMPDIR is too long.
99 // TODO(vitalybuka): Investigate how often we get there.
100 // See http://crbug.com/466644
101 return base::FilePath("/tmp").Append(pipe_name);
106 IPC::ChannelHandle GetServiceProcessChannel() {
107 base::FilePath socket_name = GetServiceProcessSocketName();
108 VLOG(1) << "ServiceProcessChannel: " << socket_name.value();
109 return IPC::ChannelHandle(socket_name.value());
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);
119 DLOG(ERROR) << "ForceServiceProcessShutdown: " << err << " "
120 << base::SysCFStringRefToUTF8(label);
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()) {
134 // Anything past here will return true in that there does appear
135 // to be a service process of some sort registered with launchd.
138 NSString* exe_path = [launchd_conf objectForKey:@ LAUNCH_JOBKEY_PROGRAM];
140 NSString* bundle_path = [[[exe_path stringByDeletingLastPathComponent]
141 stringByDeletingLastPathComponent]
142 stringByDeletingLastPathComponent];
143 NSBundle* bundle = [NSBundle bundleWithPath:bundle_path];
145 NSString* ns_version =
146 [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
148 *version = base::SysNSStringToUTF8(ns_version);
150 DLOG(ERROR) << "Unable to get version at: "
151 << reinterpret_cast<CFStringRef>(bundle_path);
154 // The bundle has been deleted out from underneath the registered
156 DLOG(ERROR) << "Unable to get bundle at: "
157 << reinterpret_cast<CFStringRef>(bundle_path);
160 DLOG(ERROR) << "Unable to get executable path for service process";
165 NSNumber* ns_pid = [launchd_conf objectForKey:@ LAUNCH_JOBKEY_PID];
167 *pid = [ns_pid intValue];
173 bool ServiceProcessState::Initialize() {
174 CFErrorRef err = NULL;
175 CFDictionaryRef dict =
176 Launchd::GetInstance()->CopyDictionaryByCheckingIn(&err);
178 DLOG(ERROR) << "ServiceProcess must be launched by launchd. "
179 << "CopyLaunchdDictionaryByCheckingIn: " << err;
183 state_->launchd_conf.reset(dict);
187 IPC::ChannelHandle ServiceProcessState::GetServiceProcessChannel() {
189 NSDictionary* ns_launchd_conf = base::mac::CFToNSCast(state_->launchd_conf);
190 NSDictionary* socket_dict =
191 [ns_launchd_conf objectForKey:@ LAUNCH_JOBKEY_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);
200 bool CheckServiceProcessReady() {
203 if (!GetServiceProcessData(&version, &pid)) {
206 Version service_version(version);
208 if (!service_version.IsValid()) {
211 chrome::VersionInfo version_info;
212 Version running_version(version_info.Version());
213 if (!running_version.IsValid()) {
214 // Our own version is invalid. This is an error case. Pretend that we
218 } else if (running_version.CompareTo(service_version) > 0) {
225 ForceServiceProcessShutdown(version, pid);
230 CFDictionaryRef CreateServiceProcessLaunchdPlist(base::CommandLine* cmd_line,
231 bool for_auto_launch) {
232 base::mac::ScopedNSAutoreleasePool pool;
235 base::SysUTF8ToNSString(cmd_line->GetProgram().value());
237 std::vector<std::string> args = cmd_line->argv();
238 NSMutableArray* ns_args = [NSMutableArray arrayWithCapacity:args.size()];
240 for (std::vector<std::string>::iterator iter = args.begin();
243 [ns_args addObject:base::SysUTF8ToNSString(*iter)];
246 NSString* socket_name =
247 base::SysUTF8ToNSString(GetServiceProcessSocketName().value());
249 NSDictionary* socket =
250 [NSDictionary dictionaryWithObject:socket_name
251 forKey:@LAUNCH_JOBSOCKETKEY_PATHNAME];
252 NSDictionary* sockets =
253 [NSDictionary dictionaryWithObject:socket
254 forKey:GetServiceProcessLaunchDSocketKey()];
256 // See the man page for launchd.plist.
257 NSMutableDictionary* launchd_plist =
258 [[NSMutableDictionary alloc] initWithObjectsAndKeys:
259 GetServiceProcessLaunchDLabel(), @LAUNCH_JOBKEY_LABEL,
260 program, @LAUNCH_JOBKEY_PROGRAM,
261 ns_args, @LAUNCH_JOBKEY_PROGRAMARGUMENTS,
262 sockets, @LAUNCH_JOBKEY_SOCKETS,
265 if (for_auto_launch) {
266 // We want the service process to be able to exit if there are no services
267 // enabled. With a value of NO in the SuccessfulExit key, launchd will
268 // relaunch the service automatically in any other case than exiting
269 // cleanly with a 0 return code.
270 NSDictionary* keep_alive =
272 dictionaryWithObject:[NSNumber numberWithBool:NO]
273 forKey:@LAUNCH_JOBKEY_KEEPALIVE_SUCCESSFULEXIT];
274 NSDictionary* auto_launchd_plist =
275 [[NSDictionary alloc] initWithObjectsAndKeys:
276 [NSNumber numberWithBool:YES], @LAUNCH_JOBKEY_RUNATLOAD,
277 keep_alive, @LAUNCH_JOBKEY_KEEPALIVE,
278 @kServiceProcessSessionType, @LAUNCH_JOBKEY_LIMITLOADTOSESSIONTYPE,
280 [launchd_plist addEntriesFromDictionary:auto_launchd_plist];
282 return reinterpret_cast<CFDictionaryRef>(launchd_plist);
285 // Writes the launchd property list into the user's LaunchAgents directory,
286 // creating that directory if needed. This will cause the service process to be
287 // auto launched on the next user login.
288 bool ServiceProcessState::AddToAutoRun() {
289 // We're creating directories and writing a file.
290 base::ThreadRestrictions::AssertIOAllowed();
291 DCHECK(autorun_command_line_.get());
292 base::ScopedCFTypeRef<CFStringRef> name(CopyServiceProcessLaunchDName());
293 base::ScopedCFTypeRef<CFDictionaryRef> plist(
294 CreateServiceProcessLaunchdPlist(autorun_command_line_.get(), true));
295 return Launchd::GetInstance()->WritePlistToFile(Launchd::User,
301 bool ServiceProcessState::RemoveFromAutoRun() {
302 return RemoveFromLaunchd();
305 bool ServiceProcessState::StateData::WatchExecutable() {
306 base::mac::ScopedNSAutoreleasePool pool;
307 NSDictionary* ns_launchd_conf = base::mac::CFToNSCast(launchd_conf);
308 NSString* exe_path = [ns_launchd_conf objectForKey:@ LAUNCH_JOBKEY_PROGRAM];
310 DLOG(ERROR) << "No " LAUNCH_JOBKEY_PROGRAM;
314 base::FilePath executable_path =
315 base::FilePath([exe_path fileSystemRepresentation]);
316 scoped_ptr<ExecFilePathWatcherCallback> callback(
317 new ExecFilePathWatcherCallback);
318 if (!callback->Init(executable_path)) {
319 DLOG(ERROR) << "executable_watcher.Init " << executable_path.value();
322 if (!executable_watcher.Watch(
325 base::Bind(&ExecFilePathWatcherCallback::NotifyPathChanged,
326 base::Owned(callback.release())))) {
327 DLOG(ERROR) << "executable_watcher.watch " << executable_path.value();
333 bool ExecFilePathWatcherCallback::Init(const base::FilePath& path) {
334 return base::mac::FSRefFromPath(path.value(), &executable_fsref_);
337 void ExecFilePathWatcherCallback::NotifyPathChanged(const base::FilePath& path,
340 NOTREACHED(); // TODO(darin): Do something smarter?
344 base::mac::ScopedNSAutoreleasePool pool;
345 bool needs_shutdown = false;
346 bool needs_restart = false;
347 bool good_bundle = false;
350 if (GetParentFSRef(executable_fsref_, &macos_fsref)) {
351 FSRef contents_fsref;
352 if (GetParentFSRef(macos_fsref, &contents_fsref)) {
354 if (GetParentFSRef(contents_fsref, &bundle_fsref)) {
355 base::ScopedCFTypeRef<CFURLRef> bundle_url(
356 CFURLCreateFromFSRef(kCFAllocatorDefault, &bundle_fsref));
357 if (bundle_url.get()) {
358 base::ScopedCFTypeRef<CFBundleRef> bundle(
359 CFBundleCreate(kCFAllocatorDefault, bundle_url));
360 // Check to see if the bundle still has a minimal structure.
361 good_bundle = CFBundleGetIdentifier(bundle) != NULL;
367 needs_shutdown = true;
370 OSErr err = FSDetermineIfRefIsEnclosedByFolder(kOnAppropriateDisk,
374 if (err == noErr && in_trash) {
375 needs_shutdown = true;
377 bool was_moved = true;
379 if (base::mac::FSRefFromPath(path.value(), &path_ref)) {
380 if (FSCompareFSRefs(&path_ref, &executable_fsref_) == noErr) {
385 needs_restart = true;
389 if (needs_shutdown || needs_restart) {
390 // First deal with the plist.
391 base::ScopedCFTypeRef<CFStringRef> name(CopyServiceProcessLaunchDName());
393 base::ScopedCFTypeRef<CFMutableDictionaryRef> plist(
394 Launchd::GetInstance()->CreatePlistFromFile(
395 Launchd::User, Launchd::Agent, name));
397 NSMutableDictionary* ns_plist = base::mac::CFToNSCast(plist);
398 std::string new_path = base::mac::PathFromFSRef(executable_fsref_);
399 NSString* ns_new_path = base::SysUTF8ToNSString(new_path);
400 [ns_plist setObject:ns_new_path forKey:@ LAUNCH_JOBKEY_PROGRAM];
401 base::scoped_nsobject<NSMutableArray> args([[ns_plist
402 objectForKey:@LAUNCH_JOBKEY_PROGRAMARGUMENTS] mutableCopy]);
403 [args replaceObjectAtIndex:0 withObject:ns_new_path];
404 [ns_plist setObject:args forKey:@ LAUNCH_JOBKEY_PROGRAMARGUMENTS];
405 if (!Launchd::GetInstance()->WritePlistToFile(Launchd::User,
409 DLOG(ERROR) << "Unable to rewrite plist.";
410 needs_shutdown = true;
413 DLOG(ERROR) << "Unable to read plist.";
414 needs_shutdown = true;
417 if (needs_shutdown) {
418 if (!RemoveFromLaunchd()) {
419 DLOG(ERROR) << "Unable to RemoveFromLaunchd.";
423 // Then deal with the process.
424 CFStringRef session_type = CFSTR(kServiceProcessSessionType);
426 if (!Launchd::GetInstance()->RestartJob(Launchd::User,
430 DLOG(ERROR) << "RestartLaunchdJob";
431 needs_shutdown = true;
434 if (needs_shutdown) {
436 base::mac::NSToCFCast(GetServiceProcessLaunchDLabel());
437 CFErrorRef err = NULL;
438 if (!Launchd::GetInstance()->RemoveJob(label, &err)) {
439 base::ScopedCFTypeRef<CFErrorRef> scoped_err(err);
440 DLOG(ERROR) << "RemoveJob " << err;
441 // Exiting with zero, so launchd doesn't restart the process.