原文连接:
Whether you're applying new preferences, installing a new version of your app, or perhaps something more unusual, there may come a time in your life where you say to yourself "I really wish I could automatically restart my application."
Your first instinct would be to check the documentation for NSApplication
to see if there was an easy-to-use -relaunch:
method that would allow you to do this. Failing this — and sadly, this does fail — you might poke around in NSWorkspace
, or even some of the LaunchServices APIs, hoping to find something that will allow you to easily restart your application. Alas, despite the fact that the task does not seem complex, there is no built-in functionality in Mac OS X that allows you to restart the current application.
So let's write some.
The Background
The fatal problem with restarting an application is that once your application quits, it loses control: there is no way to perform operations from your application after your application has already exited (it makes perfect sense, it's just a bit inconvenient). Therefore, in order to facilitate an application restart, some outside agent needs to be there to launch the application again once the application has quit; we can't do this without help.
LaunchServices, particularly launchd
, sounds like a good place to start, since it's a system daemon that facilitates the launching of apps. Unfortunately, LaunchServices is Apple's domain, and we can't just add features to it. For that reason, we will instead create our own one-shot daemon. Here's how it works.
When your application receives a relaunch request, it will launch a small daemon within the application bundle. That daemon will then wait until the application quits, relaunch the application, and then quit itself. Relatively straightforward, right?
We'll be implementing this as a category on NSApplication
, so we can relaunch our application by simply doing this:
[NSApp relaunch:nil];
The Code
I'm going to assume you have an existing application to which you want to add relaunch capabilities, so we'll assume you've already got an Xcode project ready to go.
The first thing you'll want to do is add a new target for your relaunch daemon. From the Project menu, choose "New Target...", and then under Cocoa, choose "Shell Tool." Name your target "relaunch," then click Finish.
Expand the Targets group in the sidebar, and double click the target for your application. Drag the relaunch target into the "Direct Dependencies" table view on the General tab of the Info window. This will ensure that whenever you go to compile your application, the relaunch daemon will be compiled as well.
Since we'll be using Cocoa code within our shell tool, we need to make sure our tool will link against the Cocoa framework. Expand the disclosure triangle next to your relaunch target, and then the one next to "Link Binary With Libraries." If by some chance Cocoa.framework
is already listed there, you're all set, but most likely it won't be. From the Frameworks → Linked Frameworks group (above), drag the Cocoa.framework
entry down to the "Link Binary With Libraries" group under your relaunch target. This will allow you to use Cocoa code. (Note that you can also drag in the framework from /System/Library/Frameworks/Cocoa.framework
, but this is a bit faster).
Finally, expand the Products group, and drag the relaunch
build product into the "Copy Bundle Resources" group under your main application's target. This will tell Xcode to copy your relaunch daemon executable into your main application's Resources directory after it's compiled. When all is said and done, your targets should look a bit like this:
Now, we're going to create a new file called relaunch.m
. For this, I use the Objective-C Class template, and just opt not to create a header file. Make sure this gets added into only the relaunch target, like so:
Get rid of the implementation definition in your relaunch.m
file, and instead, insert the template for a C main method. Be sure to import the Cocoa headers! When you're done, your relaunch.m
file should look something like this:
// relaunch.m
#import <Cocoa/Cocoa.h>
#pragma mark Main method
int main(int argc, char *argv[])
{
}
Since this application will be running without a run loop, we need to create an autorelease pool, as is standard procedure with non-app Cocoa projects.
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// wait a sec, to be safe
sleep(1);
// blah blah blah, more code!
[pool drain];
You'll notice I added a one-second sleep into the project. This is just to ensure that the application has indeed quit before we attempt to launch it again. Depending on how long your application takes to terminate ordinarily, you may want to increase this to two, three, or even more seconds.
We're going to assume that the path to the application we want to launch was passed in as the first command line parameter to our relaunch daemon, so we'll obtain an NSString
from that usingNSString
's +stringWithCString:encoding:
method. Once we've got this, we'll useNSWorkspace
's -openFile:
method to launch our application, making sure to call -stringByExpandingTildeInPath
on our path in case we're provided a user-relative path.
NSString *appPath = [NSString stringWithCString:argv[1] encoding:NSUTF8StringEncoding];
BOOL success = [[NSWorkspace sharedWorkspace] openFile:[appPath stringByExpandingTildeInPath]];
Finally, we return, draining our autorelease pool and the process, and using the success of the application launch to return either 0 (success) or 1 (error).
[pool drain];
return (success) ? 0 : 1;
All together, our relaunch tool looks something like this (I also added in a log to the console if the application failed to launch properly):
// relaunch.m
#import <Cocoa/Cocoa.h>
#pragma mark Main method
int main(int argc, char *argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// wait a sec, to be safe
sleep(1);
NSString *appPath = [NSString stringWithCString:argv[1] encoding:NSUTF8StringEncoding];
BOOL success = [[NSWorkspace sharedWorkspace] openFile:[appPath stringByExpandingTildeInPath]];
if (!success)
NSLog(@"Error: could not relaunch application at %@", appPath);
[pool drain];
return (success) ? 0 : 1;
}
Pretty straightforward, right?
The next order of business is creating our NSApplication
category. For this, I once again use the Objective-C Class template (with header file, this time). Create a new file calledNSApplication+Relaunch.m
, and make sure you add it to your main application target this time, not your relaunch tool.
Modify your header to specify instead the interface for a category called Relaunch
, and add a signature for a method called -relaunch:
. You may also consider adding a preprocessor constant (which I chose to call NSApplicationRelaunchDaemon
) that maps to the name of your relaunch tool, making it easy for you to change this in the future. You can, of course, opt not to do this, but it's at least somewhat good practice.
// NSApplication+Relaunch.h
#import <Cocoa/Cocoa.h>
#define NSApplicationRelaunchDaemon @"relaunch"
@interface NSApplication (Relaunch)
- (void)relaunch:(id)sender;
@end
Finally, switch over to NSApplication+Relaunch.m
, fix the implementation symbol (change fromNSApplication_Relaunch
to NSApplication (Relaunch)
), an implement the relaunch:
method. Essentially all you need to do is get the path to the daemon, start it using NSTask
(passing the path to the current application on the command line), and then terminate the application (using self
, since we're implementing this within NSApplication
). It's relatively trivial, so I'll just give you the final code block:
// NSApplication+Relaunch.m
#import "NSApplication+Relaunch.h"
@implementation NSApplication (Relaunch)
- (void)relaunch:(id)sender
{
NSString *daemonPath = [[NSBundle mainBundle] pathForResource:NSApplicationRelaunchDaemon ofType:nil];
[NSTask launchedTaskWithLaunchPath:daemonPath arguments:[NSArray arrayWithObject:[[NSBundle mainBundle] bundlePath]]];
[self terminate:sender];
}
@end
And for most basic purposes, that's really all there is to it! Calling the relaunch:
method of NSApp
will begin the relaunch cycle.
Beefing it Up
Now while this will be sufficient for most uses, this implementation does ignore a few concerns, particularly what happens if your application displays alerts or requires subsequent user interaction in order to terminate (ie, an unsaved changes warning for document-based applications). Since most users do not have ninja-like reflexes, it will likely take more than one second for them to respond, meaning the relaunch will fail. Even if you increase the delay, it will most likely cause problems — in general, it's totally impractical to use the above solution if there exists a case in which your application would not quit immediately.
In order to combat this, we're going to take a cue from Sparkle: get the PID (process ID) of the application, and wait until that process ends before relaunching the application. This eliminates our need to use sleep(1)
, and also clears us for use in almost every use case.
The first order of business is to modify our Relaunch
category to pass the process ID as the second parameter. All we need to do is replace this line…
[NSTask launchedTaskWithLaunchPath:daemonPath arguments:[NSArray arrayWithObject:[[NSBundle mainBundle] bundlePath]]];
… with this line.
[NSTask launchedTaskWithLaunchPath:daemonPath arguments:[NSArray arrayWithObjects:[[NSBundle mainBundle] bundlePath], [NSString stringWithFormat:@"%d", [[NSProcessInfo processInfo] processIdentifier]], nil]];
Now, in our relaunch daemon, we can replace the arbitrary sleep()
call with a loop which checks if the process ID exists, sleeping in one-second intervals until the condition changes. A working loop would look a little bit like this:
pid_t parentPID = atoi(argv[2]);
ProcessSerialNumber psn;
while (GetProcessForPID(parentPID, &psn) != procNotFound)
sleep(1);
If you're a bit shaky on the Process Manager functions, don't worry about it — everything I needed I learned from exploring the Sparkle source, I can't honestly say I have a strong command of it myself. All you need to know is that we first convert the process ID into a native representation, then attempt to get a reference to the process based on PID; when that fails, (ie, making process != procNotFound
false), the loop exits, and execution continues. When all is said and done, the final relaunch.m
looks like this:
// relaunch.m
#import <Cocoa/Cocoa.h>
#pragma mark Main method
int main(int argc, char *argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
pid_t parentPID = atoi(argv[2]);
ProcessSerialNumber psn;
while (GetProcessForPID(parentPID, &psn) != procNotFound)
sleep(1);
NSString *appPath = [NSString stringWithCString:argv[1] encoding:NSUTF8StringEncoding];
BOOL success = [[NSWorkspace sharedWorkspace] openFile:[appPath stringByExpandingTildeInPath]];
if (!success)
NSLog(@"Error: could not relaunch application at %@", appPath);
[pool drain];
return (success) ? 0 : 1;
}
And the category file looks like this:
// NSApplication+Relaunch.m
#import "NSApplication+Relaunch.h"
@implementation NSApplication (Relaunch)
- (void)relaunch:(id)sender
{
NSString *daemonPath = [[NSBundle mainBundle] pathForResource:NSApplicationRelaunchDaemon ofType:nil];
[NSTask launchedTaskWithLaunchPath:daemonPath arguments:[NSArray arrayWithObjects:[[NSBundle mainBundle] bundlePath], [NSString stringWithFormat:@"%d", [[NSProcessInfo processInfo] processIdentifier]], nil]];
[self terminate:sender];
}
Our relaunch daemon will now wait until the main application finishes terminating before relaunching the application.
Conclusion
Relaunching an application isn't difficult, it just requires a bit of ingenuity. Using the code snippets above, you'll be set to automatically relaunch your application in just about any situation. The only time you may need to modify this code is if you're restarting in order to install an application update: in this case, you'll want to copy your relaunch daemon to a temporary directory before launching it, and then instruct the daemon to delete itself before exiting (another tip from Sparkle).
Per usual, I've packaged the code in this tutorial into an Xcode project and NSApplication
category that you can use in your own application. I've also included a pre-compiled copy of the relaunch daemon, so all you really need to do is add the relaunch tool to your app's Resources directory, and import the NSApplication
category — no need to bother with the relaunch
source/target/etc. unless you want to. Happy relaunching!