Spin Control and non-GUI NSApplication programs

I've been running /Developer/Applications/Performance Tools/Spin Control.app continuously of late. I realized that the one thing better than Activity Monitor's three-click sampling of a hung application (one click on the Activity Monitor icon, one click on the errant process, one click on "Sample Process") would be something that just ran in the background, recognized a hung application, and sampled it for me. And that's what Apple's Spin Control is.

Every time Address Book crashes (there's a problem involving the interaction between the Large Type menu option on phone numbers and tool tips), I get an entry. Every time Safari decides it's going to ignore me for a few seconds, I get an entry. (I'd heard other people complain about Safari, but it's only recently I've used Safari enough to notice; I prefer Camino, in the main.) Mail shows up from time to time, too.

Anyway, the only bit of my code that shows up in Spin Control is the little command-line tool I wrote to pretend to be ispell(1) but work by calling NSSpellChecker. The idea being to have a fairly portable (to other Unixes) way of offering spelling checking in Java applications, in a way that uses the same dictionaries and algorithms as native components.

I surfed the web in vain for a while. I was sure there must be a way to tell Mac OS that my program isn't really a GUI application; it just has to start up NSApplication because NSSpellChecker aborts otherwise. Something to do with LSBackgroundOnly or LSUIElement, I thought.

It turns out that there's not much you can do about this configuration at run-time. Especially not if you're just a command-line program (as opposed to something launched from an application bundle). So what's the answer? Well, my program wasn't getting a Dock icon, so all I needed to do was make sure we respond to whatever heartbeat it is that Spin Control is using. I still don't know what that is, exactly. But I did guess that [NSApplication run] would make the problem go away.

So my solution is to detach a new thread to run the real "main" program, and main itself then goes straight into the NSApplication run loop.

Here's the full code:

#include <iostream>
#include <string>

#import <AppKit/AppKit.h>

* Creates and releases an NSAutoreleasePool in its scope. Having to manually
* release an autorelease pool on every exit point from a method is silly, but
* Objective-C++ lets us fix this.
class ScopedAutoReleasePool {
ScopedAutoReleasePool() {
m_pool = [[NSAutoreleasePool alloc] init];

~ScopedAutoReleasePool() {
[m_pool release];

NSAutoreleasePool* m_pool;

* Allows NSString to be output like std::string or a const char*.
std::ostream& operator<<(std::ostream& os, NSString* rhs) {
os << [rhs UTF8String];
return os;

@interface Ispell : NSObject
+ (void) ispellRunLoop:(id) unusedParameter;

@implementation Ispell

NSSpellChecker* checker;

+ (void) showSuggestionsForWord:(NSString*) word {
ScopedAutoReleasePool pool;
std::ostream& os = std::cout;

NSArray* guesses = [checker guessesForWord:word];
if ([guesses count] == 0) {
os << "# " << word << " 0\n";

os << "& " << word << " " << [guesses count] << " 0: ";
for (size_t i = 0; i < [guesses count]; ++i) {
if (i > 0) {
os << ", ";
NSString* guess = [guesses objectAtIndex:i];
os << guess;
os << "\n";

+ (bool) isCorrect:(NSString*) word {
ScopedAutoReleasePool pool;
NSRange range = [checker checkSpellingOfString:word startingAt:0];
bool isCorrect = (range.length == 0);
return isCorrect;

+ (void) ispellRunLoop:(id) unusedParameter {
(void) unusedParameter;

// Each thread needs its own pool, and we don't get one for free.
ScopedAutoReleasePool pool;
checker = [NSSpellChecker sharedSpellChecker];

std::ostream& os(std::cout);
os << "@(#) International Ispell 3.1.20 (but really NSSpellChecker)\n";
while (true) {
ScopedAutoReleasePool pool;
std::string line;
getline(std::cin, line);
if (line.length() == 0) {
// We're done.
[[NSApplication sharedApplication] terminate:nil];
} else if (line == "!") {
// set terse mode; ignore.
} else if (line[0] == '^') {
std::string word(line.substr(1));
NSString* string = [NSString stringWithUTF8String:word.c_str()];
if ([Ispell isCorrect:string] == false) {
[Ispell showSuggestionsForWord:string];
os << std::endl;
} else if (line[0] == '*') {
std::string word(line.substr(1));
// FIXME: insert word into dictionary.
} else {


int main(int /*argc*/, char* /*argv*/[]) {
ScopedAutoReleasePool pool;

// Pretend to be ispell in a new thread, so we can enter the normal event
// loop and prevent Spin Control from mistakenly diagnosing us as a hung
// GUI application.
[NSThread detachNewThreadSelector:@selector(ispellRunLoop:)
toTarget:[Ispell class]

[[NSApplication sharedApplication] run];

[When AppKit methods return nil... is an update to this article explaining the race condition in this code.]