Archive for the ‘Sample Code’ category

NSWindow setResizable:

December 18, 2007

This article has been updated moved to a new location.

NSWorkspace Bug in – openFile: ?

December 7, 2007

EDITED TO ADD: I could reproduce this bug in a large project, but not isolate it in a smaller one. It is much likely for my code to have a bug then NSWorkspace. I’m still not 100% certain that this issue wasn’t my fault in some way I don’t understand. But no matter what caused the bug, the work-around I describe here has been working for me. Please let me know if you have this same issue as well, or any insight into what I could have done to cause it.

The Problem:
I need to launch a background-application from inside the application-support directory. The OS is Mac OS X 10.5.1 build 9B18.
[[NSWorkspace sharedWorkspace] openFile:path];
where path is the correct path to the application, in my case “/Users/user/Library/Application Support/IMLocation/IMLocationHelper.app.”, works about 3 out out 4 times, but intermittently fails. It also reports success without actually launching IMLocationHelper.app, but this is far more rare.

Details:
When openFile: explicitly fails, it prints, “LSOpenFromURLSpec() returned -600 for application (null) path /Users/user/Library/Application Support/IMLocation/IMLocationHelper.app.” to stderr.

Error -600 is procNotFound /*no eligible process with specified descriptor*/.

The path is correct, the application exists there. I verified this by
assert([[NSFileManager defaultManager] fileExistsAtPath:path]);
before calling openFile:.

Calling openFile: again immediately after a failure often works

Also, while(![[NSWorkspace sharedWorkspace] openFile:path]);
will terminate, meaning it reported success at some point, but it will not always have actually launched the application at path.

(EDITED TO ADD: At first I thought that, NSWorkspace was probably opening IMLocationHelper, but not blocking until it has finished being launched. But sleeping, to give IMLocationHelper time to finish launching, did not solve the problem.)

performSelectorOnMainThread did not solve anything ether.

I verify that IMLocationHelper has been launched by
assert([[ShellTask executeShellCommandSynchronously:@"ps -axww | grep IMLocationHelper | grep -v grep"] length] > 0);

A Workaround:
[[NSTask launchedTaskWithLaunchPath:@"/usr/bin/open" arguments:[NSArray arrayWithObject:path] waitUntilExit];.

This calls the open command to launch the application. I have not been able to make it fail when path exists.

An AppleScript Quine

November 3, 2007

A few days ago, out of morbid curiosity, I went looking for a a quine written in AppleScript. To my surprise, I couldn’t find one.

I’d never actually written a quine before, so this was a good ‘excuse’ to make one (and brush up on some computability theory — use it or lose it).

Well, here it is. When run it will make Script Editor create a new window containing the source code.

This might seem like a strange way of printing. But AppleScript doesn’t have a printf function per-se. I thought about using display dialog to show the source, but the text wouldn’t be copyable, making bootstrapping difficult. Letting the source-string be the result of evaluating the script wasn’t a good solution ether, because Script Editor puts an extra “” around any result that’s a string. Using osascript to run the script could work, but I think it would be breaking the “spirit” of AppleScript to force the quine to be run from the command-line.

Unfortunately, I encountered what appear to be Leopard bugs in Script Editor. (Update 2008-10-11: this problem seems to be fixed.)
When bootstrapping/testing, the result window would sometimes be filled with…something inexplainable.For example

«handler ASCII character of item i of x»
quote pi 40 of string_from_ASCII_numbers(set_d_to)
100
quine
((5 blank lines))

I don’t know why, I don’t know how. But it’s a troubling inditement of AppleScript support in 10.5.0.
I could reproduce a problem like this:
* quit Script Editor
* open the quine in Script Editor, and run it.
* run the resulting window (“Untitled”)
* run the resulting window (“Untitled 2”)

* around “Untitled” 5 or 6, the result was “100”.
* keep iterating like this, and see what unpredictability awaits (the most iterations I could get before Script Editor crashed was 12).
In Tiger, the problem couldn’t be reproduced AFAIK.

I hope this regression is an isolated incident. It’s just one more reason I’m not fond of AppleScript.

Interfacing With (Hacking) iChat in Leopard

November 1, 2007

Before you consider using any of the unsupported hacks I’m about to discuss, check to see if existing frameworks , or iChat’s AppleScript interface, will do what you need. Any software update can break unsupported code in unpredictable ways at any time. When Leopard came out, all the hacks I had been using to interface with iChat broke — causing a deadlock, that was harder to track down then a crash. Hacks can have catastrophic consequences.

(I’m not going to go in-depth into the hacks I was using under Tiger. But for the benefit of people who’s code has suddenly broken: The FZDaemon/FZDaemonListener protocols have been significantly changed in Leopard. Every function in FZDaemon is now oneway void for example. My attempts to get a connection to com.apple.iChatAgent were deadlocking. The same approach will not work on both Leopard and Tiger, as far as I can tell. The good news is that the Leopard APIs are a lot cleaner.)

Reverse Engineering iChat and InstantMessage.framework

The first thing I did was check to see if Leopard introduced a supported way of doing what I needed. There were some, like the IMMyStatusChangedNotification, but still no way to set the user’s status message. After reading, I knew what frameworks I needed to poke at. I also perused the iChat, and iChatAgent bundles for further clues.

I used class-dump, otx and strings on: iChat, iChatAgent, InstantMessage.framework, and IMUtils.framework (inside InstantMessage.framework). This gave me private interface declarations, disassembled code, and portentous strings to pour over.

I also tried using gdb on iChat, to see just how it was communicating with iChatAgent. This was not very productive for me at all. I got much further by statically analyzing disassembled code and interfaces. GDB can be a great tool, but you have to know a lot about what you are looking for before you can find it — otherwise you don’t know where to set breakpoints. I’ve noticed myself using gdb less and less over the last few years. (As silly as it sounds, gdb just hasn’t been as much fun after the intel switch, now that all the assembler is x86 goobely-garb.)

Trying things out in my own “tester” project really helped me puzzle out exactly what was going on, why, and how. I had to explicitly add the IMUtils.framework to the “Linked Frameworks” group to get some stuff to link.

I learned a lot by listening to any - (NSNotificationCenter*) notificationCenter that an object exposed. For example,

[[IMService notificationCenter] addObserver:self selector:@selector(iChatNotification:) name:nil object:nil];

where,
- (void) iChatNotification:(NSNotification*)notification{
NSLog(@"iChatNotification: %@", notification);
}

Will print every notification passing through the IMService notification center. Passing in a name: or object: parameter filters what notifications your method receives. Different classes often share the same notification center, so I would verify that one was distinct before printing all it’s notifications.

It turns out that the public [IMService notificationCenter] sends an undocumented notification named “IMMyInfoChangedNotification” when the user’s status message changes. However, the documented IMMyStatusChangedNotification appears to always be sent in such cases — I could not contrive an example where the status message was changed, and it was not sent.

InstantMessagePrivate.h will let you use access private interfaces in InstantMessage.framework. It is a refinement o the class-dump of InstantMessage.framework.

IMServiceAgentImpl is the most feature-rich class for controlling iChat (see also: IMServiceAgent). [IMServiceAgentImpl sharedAgent] will give you the shared instance. Some of the most useful methods are:
- (void)setMyStatus:(IMPersonStatus)statusType message:(NSString*)statusMessage;
- (NSString*) myStatusMessage;
- (NSData*) myPictureData;
- (void) setMyPictureData:(NSData*)newPictureData;
- (NSString*) myProfile;
- (void) setMyProfile:(NSString*)newProfile;
- (NSArray*)myAvailableMessages;
- (NSArray*)myAwayMessages;

Accessor methods will not work correctly, unless you have a connection to the iChatAgent daemon.
[[IMServiceAgentImpl sharedAgent] connectWithLaunch:YES];
[[IMServiceAgentImpl sharedAgent] _blockUntilConnected];
ensures that there is a connection to the daemon. Only calling connectWithLaunch: or _blockUntilConnected didn’t do it for me, I had to call both in order. There may be a better way to get a connection, but I’m not aware of it

Unfortunately, I have not yet figured out how to get iChat to set the status to invisible. Right now I’m concentrating to getting IMLocation working on Leopard, so I’ll look into this later.

iChat’s a damn big program for “just a chat client”, It’s executable and class-dump are actually about 2x as big as Safari’s.

InstantMessagePrivate.h

Getting Mac OS X Version Information At Runtime

October 31, 2007

This article has been updated, and republished here.

Better AppleScript Through Expressions

October 13, 2007

When writing a short AppleScript to complete a specific task, do not write a procedure that checks if A exists, then operates on it if it does. Instead, write an expression that operates on A, and ignore any exceptions caused by A not existing. Surrounding the statement in a try-block will ignore any exceptions it generates. The where and whose commands can help. This almost always leads to cleaner, and faster, AppleScripts.

Here is an example, a re-write of code in EyeTV version 2.5 (if you have it, you can find this script at EyeTV.app/Contents/Resources/English.lproj/EyeTV\ Help/openreadme.scpt).

When run, this opens a readme inside the EyeTV application bundle, iff the EyeTV application is running. I did not write this, and I do not know exactly how it is used, or what constraints it was written under — just what it does.

on run
set readmePath to "Contents:Resources:English.lproj:EyeTV Help:eyetvreadme.rtfd"

tell application "Finder"
 set allApps to every application process
 
 repeat with i from 1 to number of items in allApps
  set thisApp to item i of allApps
  
  if creator type of thisApp is "EyTV" then
   open file (((application file of thisApp) as string) & readmePath)
   exit repeat
  end if
 end repeat
end tell
end run

Re-writing the code by making it an expression that assumes the correct application process exists has a dramatic difference.

on run
try
 tell application "Finder"
  set theEyeTVProcessBundle to file of first item of (every application process where creator type is "EyTV") as string
  open (theEyeTVProcessBundle & "Contents:Resources:English.lproj:EyeTV Help:eyetvreadme.rtfd") as alias
 end tell
end try
end run

We no longer need if or repeat blocks — they are procedural constructs that were used to find the correct application process, and guard against trying to open a file that does not exist. (I also removed the variable readmePath because I think the a path to “eyetvreadme.rtfd” is clearly a readme-path).

But it gets better. We could try removing the run and try blocks as well, and just ignore any errors the interpreter complains about. That brings the the script down to four lines. We can make it one-line: tell application "Finder" to open ((file of first item of (every application process where creator type is "EyTV") as string) & "Contents:Resources:English.lproj:EyeTV Help:eyetvreadme.rtfd") as alias, but I think that makes the script too hard to understand, because the name “theEyeTVProcessBundle” documents the long expression needed to reliably find the application bundle of the running process.

One line or four lines, that’s still a big reduction from 13 lines. Now it’s short enough to embed directly in Cocoa code with NSAppleScript.

Here’s another example. This is from IMLocation‘s uninstaller code, it It removes the IMLocationHelper from the list of items that will be opened at login. Here’s the original code:

tell application "System Events"
 if (login item "IMLocationHelper") exists then
  delete login item "IMLocationHelper"
 end if
end tell

Applying the approach of re-writing the procedure as an expression

try
 tell application "System Events" to delete (login item "IMLocationHelper")
end try

Taking things a step further, by removing the try-block, and just ignoring errors when running the one-line script: tell application "System Events" to delete (get login item "IMLocationHelper"). This makes a big difference in the clarity of the surrounding Cocoa code.

AppleScript is a pretty poor language on it’s own. Usually I see it being used to get some specific thing done. In other words, generally AppleScripts are essentially expressions, used to get or set a single value; rarely are they sub-programs. This is whyExpressions are such a good fit.

A big thanks to Uli Kusterer for correcting my doubleplusungood code! (details in comments)

NSApplicationName Inconsistencies

October 11, 2007

This article has been updated, and moved here.