|
IntroductionAfter programming Mac OS X for a while, you will inevitably come across situations where it's necessary to create a suite of cooperating processes. For example:
Once you have multiple processes you inevitably run into the issue of process lifetime: that is, one process needs to know whether another process is running. This technote explains various techniques that you can use to be notified when a process launches or terminates. It is split into two main sections. Observing Processes That You Started shows how to monitor a process that you launched, while Observing Arbitrary Processes shows how to monitor a process that you didn't launch. Finally, On Process Serial Numbers contains some important information about the process serial number based APIs discussed in this technote. But first, let's start with a discussion of an alternative approach that offers a number of key advantages. IMPORTANT: All of the techniques discussed here notify you when things change. It is possible to get the same information by polling the process list, but polling is generally a bad idea (it consumes CPU time, reduces battery life, increase the working set of your process, and increases your latency in responding to an event). The Service-Oriented AlternativeOne of the most common reasons for monitoring the lifecycle of a process is that the process provides some service to you. For example, consider a movie transcoding application that sublaunches a worker process to do the actual transcoding. The main application needs to monitor the state of the worker process so that it can relaunch it if it quits unexpectedly. You can avoid this requirement by rethinking your approach. Rather than explicitly managing the state of your helper process, reimagine it as a service that your application calls upon. You can then use launchd to manage that service; it will take care of all the nitty-gritty details of launching and terminating the process that provides that service. A full discussion of this service-oriented approach is outside the scope of this technote. For more information you should read up on Observing Processes That You StartedThere are many different techniques for monitoring the lifetime of a process that you started. Each technique has a number of pros and cons. Read the following sections to determine which is most appropriate for your circumstances. NSTaskNSTask makes it easy to launch a helper process and wait for it to terminate. You can wait synchronously (using - (IBAction)testNSTaskSync:(id)sender { NSTask * syncTask; syncTask = [NSTask launchedTaskWithLaunchPath:@"/bin/sleep" arguments:[NSArray arrayWithObject:@"1"] ]; [syncTask waitUntilExit]; } - (IBAction)testNSTaskAsync:(id)sender { task = [[NSTask alloc] init]; [task setLaunchPath:@"/bin/sleep"]; [task setArguments:[NSArray arrayWithObject:@"1"]]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(taskExited:) name:NSTaskDidTerminateNotification object:task ]; [task launch]; // Execution continues in -taskExited:, below. } - (void)taskExited:(NSNotification *)note { // You've been notified! [[NSNotificationCenter defaultCenter] removeObserver:self name:NSTaskDidTerminateNotification object:task ]; [task release]; task = nil; } Application Died EventsIf you launch an application using a process serial number based API, you can learn about its termination by registering for the IMPORTANT: This event is only delivered for applications that you launched. Listing 3 shows how to register for and handle an application died event. - (IBAction)testApplicationDied:(id)sender { NSURL * url; static BOOL sHaveInstalledAppDiedHandler; if ( ! sHaveInstalledAppDiedHandler ) { (void) AEInstallEventHandler( kCoreEventClass, kAEApplicationDied, (AEEventHandlerUPP) AppDiedHandler, (SRefCon) self, false ); sHaveInstalledAppDiedHandler = YES; } url = [NSURL fileURLWithPath:@"/Applications/TextEdit.app"]; (void) LSOpenCFURLRef( (CFURLRef) url, NULL); // Execution continues in AppDiedHandler, below. } static OSErr AppDiedHandler( const AppleEvent * theAppleEvent, AppleEvent * reply, SRefCon handlerRefcon ) { SInt32 errFromEvent; ProcessSerialNumber psn; DescType junkType; Size junkSize; (void) AEGetParamPtr( theAppleEvent, keyErrorNumber, typeSInt32, &junkType, &errFromEvent, sizeof(errFromEvent), &junkSize ); (void) AEGetParamPtr( theAppleEvent, keyProcessSerialNumber, typeProcessSerialNumber, &junkType, &psn, sizeof(psn), &junkSize ); // You've been notified! NSLog( @"died %lu.%lu %d", (unsigned long) psn.highLongOfPSN, (unsigned long) psn.lowLongOfPSN, (int) errFromEvent ); return noErr; } IMPORTANT: Application died events are based on serial numbers, a fact which has a number of important consequences. See On Process Serial Numbers for details. The UNIX WayMac OS X's BSD subsystem has two fundamental APIs for starting new processes:
In both cases the resulting process is a child of the current process. There are two traditional UNIX ways to learn about the death of a child process:
Waiting synchronously is appropriate in many situations. For example, if the parent process can make no progress until the child is done, it's reasonable to wait synchronously. Listing 4 shows an example of how to fork, then exec, then wait. extern char **environ; - (IBAction)testWaitPID:(id)sender { pid_t pid; char * args[3] = { "/bin/sleep", "1", NULL }; pid_t waitResult; int status; // I used fork/exec rather than posix_spawn because I would like this // code to be compatible with 10.4.x. pid = fork(); switch (pid) { case 0: // child (void) execve(args[0], args, environ); _exit(EXIT_FAILURE); break; case -1: // error break; default: // parent break; } if (pid >= 0) { do { waitResult = waitpid(pid, &status, 0); } while ( (waitResult == -1) && (errno == EINTR) ); } } On the other hand there are circumstances where waiting synchronously is a really bad idea. For example, if you're running on the main thread of an application and the child process might operate for an extended period of time, you don't want to lock up your application's user interface waiting for the child to quit. In cases like this, you can wait asynchronously by listening for the IMPORTANT: If you use the Listening for a signal can be tricky because of the wacky execution environment associated with signal handlers. Specifically, if you install a signal handler (using signal or sigaction), you must be very careful about what you do in that handler. Very few functions are safe to call from a signal handler. For example, it is not safe to allocate memory using The functions that are safe from a signal handler (the async-signal safe functions) are listed on the sigaction man page. In most cases you must take extra steps to redirect incoming signals to a more sensible environment. There are two standard techniques for doing this:
IMPORTANT: The kqueue technique requires Mac OS X 10.5 or later because it uses CFFileDescriptor. UNIX AlternativesThere are numerous pitfalls associated with handing the There are various techniques to avoid all of this messing around with Listing 5 shows an example of this technique. - (IBAction)testSocketPair:(id)sender { int fds[2]; int remoteSocket; int localSocket; CFSocketContext context = { 0, self, NULL, NULL, NULL }; CFRunLoopSourceRef rls; char * args[3] = { "/bin/sleep", "1", NULL } ; // Create a socket pair and wrap the local end up in a CFSocket. (void) socketpair(AF_UNIX, SOCK_STREAM, 0, fds); remoteSocket = fds[0]; localSocket = fds[1]; socket = CFSocketCreateWithNative( NULL, localSocket, kCFSocketDataCallBack, SocketClosedSocketCallBack, &context ); CFSocketSetSocketFlags( socket, kCFSocketAutomaticallyReenableReadCallBack | kCFSocketCloseOnInvalidate ); // Add the CFSocket to our runloop. rls = CFSocketCreateRunLoopSource(NULL, socket, 0); CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, kCFRunLoopDefaultMode); CFRelease(rls); // fork and exec the child process. childPID = fork(); switch (childPID) { case 0: // child (void) execve(args[0], args, environ); _exit(EXIT_FAILURE); break; case -1: // error break; default: // parent break; } // Close our reference to the remote socket. The only reference remaining // is the one in the child. When that dies, the socket will become readable. (void) close(remoteSocket); // Execution continues in SocketClosedSocketCallBack, below. } static void SocketClosedSocketCallBack( CFSocketRef s, CFSocketCallBackType type, CFDataRef address, const void * data, void * info ) { int waitResult; int status; // Reap the child. do { waitResult = waitpid( ((AppDelegate *) info)->childPID, &status, 0); } while ( (waitResult == -1) && (errno == EINTR) ); // You've been notified! } Observing Arbitrary ProcessesThere are fewer options available if you want monitor the status of a process that you did not launch. However, the facilities that are available should be enough to meet most needs. Again, the right choice of API depends on your circumstances. Read the following sections to understand which API is appropriate and when. NSWorkspaceNSWorkspace provides a very easy way for you to learn about applications being launched and quit. To register for these notifications you must:
When you get a notification, the user info dictionary contains information about the affected process. The keys for that dictionary are listed in Listing 6 shows an example of how to use NSWorkspace to learn application launch and termination. - (IBAction)testNSWorkspace:(id)sender { NSNotificationCenter * center; NSLog(@"-[AppDelegate testNSWorkspace:]"); // Get the custom notification center. center = [[NSWorkspace sharedWorkspace] notificationCenter]; // Install the notifications. [center addObserver:self selector:@selector(appLaunched:) name:NSWorkspaceDidLaunchApplicationNotification object:nil ]; [center addObserver:self selector:@selector(appTerminated:) name:NSWorkspaceDidTerminateApplicationNotification object:nil ]; // Execution continues in -appLaunched: and -appTerminated:, below. } - (void)appLaunched:(NSNotification *)note { NSLog(@"launched %@\n", [[note userInfo] objectForKey:@"NSApplicationName"]); // You've been notified! } - (void)appTerminated:(NSNotification *)note { NSLog(@"terminated %@\n", [[note userInfo] objectForKey:@"NSApplicationName"]); // You've been notified! } IMPORTANT: NSWorkspace is a process serial number based API, a fact which has a number of important consequences. See On Process Serial Numbers for details. Carbon Event ManagerCarbon Event Manager sends a number of events related to process management. Specifically, the When your event handler is called the IMPORTANT: By the time your application has received the - (IBAction)testCarbonEvents:(id)sender { static EventHandlerRef sCarbonEventsRef = NULL; static const EventTypeSpec kEvents[] = { { kEventClassApplication, kEventAppLaunched }, { kEventClassApplication, kEventAppTerminated } }; if (sCarbonEventsRef == NULL) { (void) InstallEventHandler( GetApplicationEventTarget(), (EventHandlerUPP) CarbonEventHandler, GetEventTypeCount(kEvents), kEvents, self, &sCarbonEventsRef ); } // Execution continues in CarbonEventHandler, below. } static OSStatus CarbonEventHandler( EventHandlerCallRef inHandlerCallRef, EventRef inEvent, void * inUserData ) { ProcessSerialNumber psn; (void) GetEventParameter( inEvent, kEventParamProcessID, typeProcessSerialNumber, NULL, sizeof(psn), NULL, &psn ); switch ( GetEventKind(inEvent) ) { case kEventAppLaunched: NSLog( @"launched %u.%u", (unsigned int) psn.highLongOfPSN, (unsigned int) psn.lowLongOfPSN ); // You've been notified! break; case kEventAppTerminated: NSLog( @"terminated %u.%u", (unsigned int) psn.highLongOfPSN, (unsigned int) psn.lowLongOfPSN ); // You've been notified! break; default: assert(false); } return noErr; } IMPORTANT: These Carbon events are based on process serial numbers, a fact which has a number of important consequences. See On Process Serial Numbers for details. kqueuesBoth NSWorkspace and Carbon events only work within a single GUI login context. If you're writing a program that does not run within a GUI login context (a daemon perhaps), or you need to monitor a process in a different context from the one in which you're running, you will need to consider alternatives. One such alternative is the kqueue Listing 8 is a simplistic example of how you can use kqueues to watch for the termination of a specific process. static pid_t gTargetPID = -1; // We assume that some other code sets up gTargetPID. - (IBAction)testNoteExit:(id)sender { FILE * f; int kq; struct kevent changes; CFFileDescriptorContext context = { 0, self, NULL, NULL, NULL }; CFRunLoopSourceRef rls; // Create the kqueue and set it up to watch for SIGCHLD. Use the // new-in-10.5 EV_RECEIPT flag to ensure that we get what we expect. kq = kqueue(); EV_SET(&changes, gTargetPID, EVFILT_PROC, EV_ADD | EV_RECEIPT, NOTE_EXIT, 0, NULL); (void) kevent(kq, &changes, 1, &changes, 1, NULL); // Wrap the kqueue in a CFFileDescriptor (new in Mac OS X 10.5!). Then // create a run-loop source from the CFFileDescriptor and add that to the // runloop. noteExitKQueueRef = CFFileDescriptorCreate(NULL, kq, true, NoteExitKQueueCallback, &context); rls = CFFileDescriptorCreateRunLoopSource(NULL, noteExitKQueueRef, 0); CFRunLoopAddSource(CFRunLoopGetCurrent(), rls, kCFRunLoopDefaultMode); CFRelease(rls); CFFileDescriptorEnableCallBacks(noteExitKQueueRef, kCFFileDescriptorReadCallBack); // Execution continues in NoteExitKQueueCallback, below. } static void NoteExitKQueueCallback( CFFileDescriptorRef f, CFOptionFlags callBackTypes, void * info ) { struct kevent event; (void) kevent( CFFileDescriptorGetNativeDescriptor(f), NULL, 0, &event, 1, NULL); NSLog(@"terminated %d", (int) (pid_t) event.ident); // You've been notified! } On Process Serial NumbersMac OS X has a number of high-level APIs for process management that work in terms of process serial numbers (of type
See Technical Note TN2083, 'Daemons and Agents' for more information about execution contexts and their effect on high-level APIs. Further ReadingDocument Revision History
Posted: 2008-09-10 |
|