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