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