Problem Using AppleScript with Cocoa

Thursday, 2006-11-23; 23:56:00



Rrrrr... this problem has been annoying me for a few days, so I thought I'd put it out there in the open.

TuneTagger is a multithreaded application, since the interface needs to operate independently of some of the processes that power it (especially since internet requests can take a while to respond). I recently found out that running AppleScripts through an NSAppleScript object is not in fact threadsafe, which was causing the vast majority of crashes that I was previously having with TuneTagger. So I created an EPAppleScript object that handles this.

Here is a portion of code from a method in EPLookupHelper that runs on a separate thread (i.e.: not the main thread) every time it is called:

        // album artwork is also not included with the iTunes notifications, so we also have to retrieve this via AppleScript
        EPAppleScript *getArtworkScript = [[EPAppleScript alloc] initWithSource:
                @"tell application \"iTunes\"\ndata of artwork 1 of current track\nend tell"
                                                        andAppControllerInstance:appControllerInstance];
        NSDictionary *errorDict;
        NSAppleEventDescriptor *scriptResult = [getArtworkScript executeAppleScriptNowUsingErrorDict:&errorDict];
        NSImage *theArtwork;
        if (errorDict == nil) {
                theArtwork = [[NSImage alloc] initWithData:[scriptResult data]];
        } else {
                theArtwork = [[NSImage alloc] initWithSize:NSMakeSize(0.0,0.0)];
        }
        [getArtworkScript release];
        
                        
        // now copy the track info dict received from the notification and add in the artwork and database ID
        NSMutableDictionary *newTrackInfoDictModified = [[NSMutableDictionary alloc] initWithDictionary:newTrackInfoDict];
        [newTrackInfoDictModified setObject:[NSNumber numberWithInt:currentDatabaseID] forKey:@"Database ID"];
        [newTrackInfoDictModified setObject:theArtwork forKey:@"Artwork"];


The following is the code from the EPAppleScript object. Note that globalErrorDict, eventDescriptor, and appController are all global variables for EPAppleScript.

     
        - (void)executeAppleScriptNowUsingErrorDict:(NSDictionary **)errorDict
        {
                if ([NSThread currentThread] == [appController mainThread]) {
                        NSLog(@"EPAppleScript object already on main thread -- executing AppleScript normally");        
                        [self executeAndReturnEventDescriptorInGlobalVariable];
                } else {
                        [self performSelectorOnMainThread:@selector(executeAndReturnEventDescriptorInGlobalVariable) withObject:nil waitUntilDone:YES];
                }
                if (globalErrorDict != nil) {
                        // UPDATE: the following is the buggy line
                        errorDict = &globalErrorDict;

                        // this is the correct line:
                        // (*errorDict) = globalErrorDict

                        [(*errorDict) retain];
                        NSLog(@"errorDict: %@, retain count: %d",(*errorDict),[(*errorDict) retainCount]);
                }
                [eventDescriptor autorelease];
                return eventDescriptor;
        }
        
        - (void)executeAndReturnEventDescriptorInGlobalVariable {
                NSDictionary *tempDict = nil;
                eventDescriptor = [self executeAndReturnError:&tempDict];
                if (tempDict != nil) globalErrorDict = [tempDict copy];
                [eventDescriptor retain];
        }


I request the album artwork data in the top excerpt of code (which is on a secondary thread). This is unignorable, so it eventually executes the AppleScript. EPAppleScript checks whether it's running on a separate thread or not, and if it is, it requests the main thread to run the AppleScript (since NSAppleScript is not thread-safe). Then it deposits the NSAppleEventDescriptor result into a global variable as well as any error dictionary into a global variable, so that if the script was called from a secondary thread (and therefore needed to be run on the main thread), the EPAppleScript object can still access the results of the AppleScript.

Here's the problem, though: in executeAppleScriptNowUsingErrorDict:, EPAppleScript successfully logs the error dictionary (the retain count is 2 at that point, as well). However, when the code returns to EPLookupHelper and tries to access the error dictionary, it can't! (Note that at the point the EPAppleScript does the logging, the code is running on the same thread as the code from EPLookupHelper.) The error dictionary is always nil in EPLookupHelper, and so when it places the NSImage artwork into an NSDictionary object, it raises an exception since you can't add nil to a dictionary object.

But it shouldn't be nil if the AppleScript returns an error, and I don't know why!

Also: this method works with the eventDescriptor! It gets back successfully to the code in EPLookupHelper in other instances; the only difference between the eventDescriptor and the errorDict is that the eventDescriptor is a return value, while the errorDict is passed as a pointer and returned indirectly.

I suspect that it's something to do with multithreading (since I'm still a bit of a novice at it), but maybe it's something really, really stupid. Anyone have any ideas?

Please note: I know workarounds to this problem -- I could shift error detection to the AppleScript so that it doesn't ever throw errors, but that's not what I want. I could also probably return both the eventDescriptor and the errorDict in an NSArray or something. But it's kind of a matter of principle: I want to know exactly why this specific code is crashing. It just bugs me. Rrrr.


Technological Supernova   Software Development   Older   Newer   Post a Comment