views:

267

answers:

4

Hy everybody,

I have a screensaver made with obj-c and cocoa. Everything works fine under OsX 10.6.2 except the following. Within my screensaver I have a WebView with some application running. When I try to call my objective-c app (the screensaver) via javascript, I get an error and the screensaver and the system preferences panel crash.

System Preferences[86666] * Terminating app due to uncaught exception 'NSInvalidArgumentException'

reason: '-[NSCFArray drain]: unrecognized selector sent to instance 0x20049b1e0'

* Call stack at first throw:(
0 CoreFoundation 0x00007fff8123a444 __exceptionPreprocess + 180
1 libobjc.A.dylib 0x00007fff81f130f3 objc_exception_throw + 45
2 CoreFoundation 0x00007fff812931c0 +[NSObject(NSObject) doesNotRecognizeSelector:] + 0
3 CoreFoundation 0x00007fff8120d08f forwarding + 751
4 CoreFoundation 0x00007fff812091d8 _CF_forwarding_prep_0 + 232 5 WebCore 0x00007fff847adee0 _ZN3JSC8Bindings12ObjcInstance10virtualEndEv + 48
6 WebCore 0x00007fff8470d71d _ZN3JSC16RuntimeObjectImp18getOwnPropertySlotEPNS_9ExecStateERKNS_10IdentifierERNS_12PropertySlotE + 397
7 JavaScriptCore 0x00007fff80862b66 NK3JSC7JSValue3getEPNS_9ExecStateERKNS_10IdentifierERNS_12PropertySlotE + 486
)

I know this looks like some memory leak, but as you will see in the code, I really have nearly no objects allocated.

This only happens, when I start the screensaver with the "Test" button from the screensaver system prefs. When I start the screensaver via terminal or if it starts automatically, the same action (calling obj-c from javascript) works fine.

Maybe someone has any idea, where the error could come from. Here is some code from the implementation:

@implementation ScreensaverView

- (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview {

    self = [super initWithFrame:frame isPreview:isPreview];

    if (self) {

        [self setAnimationTimeInterval:-1];
        [self setAutoresizesSubviews:YES];

        // ::::::::::::::::::::::: Init stuff ::::::::::::::::::    

        // init 
        quitFlag = false;
        previewMode = isPreview;

        // find out the path the screensaver bundle
        pMainBundle = [NSBundle bundleForClass:[self class]];
        pBundlePath = [pMainBundle bundlePath];

        // read Info.plist
        infoDict = [pMainBundle infoDictionary];
    }

    return self;
}

- (void)startAnimation
{   
    [super startAnimation];

    // combine: bundle path + filename for screensaver file 
    NSString *pathToScreensaver = [NSString stringWithString:pBundlePath];
    NSString *valueScreensaverFile;

    if(!previewMode)
    {
        valueScreensaverFile = [infoDict objectForKey:@"ScreensaverFile"];
    }
    else 
    {
        valueScreensaverFile = [infoDict objectForKey:@"PreviewFile"];
    }

    // add filename to bundle path
    pathToScreensaver = [pathToScreensaver stringByAppendingString:valueScreensaverFile];

    // complete NSURL to the screensaver file
    NSURL *screensaverUrl = [NSURL fileURLWithPath: pathToScreensaver];

    webView = [WebView alloc];
    [webView initWithFrame:[self frame]];
    [webView setDrawsBackground:NO];

    // delegation policy for interactive mode
    [webView setPolicyDelegate: self];
    [webView setUIDelegate:self];

    // load screensaver
    [[webView mainFrame] loadRequest:[NSURLRequest requestWithURL:screensaverUrl]];

    scriptObject = [webView windowScriptObject];
    [scriptObject setValue:self forKey:@"screensaver"];

    [self addSubview:webView];
}

- (void)stopAnimation
{   
    [[webView mainFrame] stopLoading];
    [webView removeFromSuperview];
    [webView release];
    [super stopAnimation];
}

+ (BOOL)isSelectorExcludedFromWebScript:(SEL)selector 
{       
    if (selector == @selector(quitScreenSaver)) {
        return NO;
    }

    if(selector == @selector(gotoUrl:) ){
        return NO;
    }

    return YES;
}

+(NSString *)webScriptNameForSelector:(SEL)selector
{   
    if(selector == @selector(quitScreenSaver))
    {
        return @"quitNoOpen";
    }

    if(selector == @selector(gotoUrl:))
    {
        return @"openAndQuit";
    }

    return nil;
}

- (void) quitScreenSaver
{
    quitFlag = true;
    [super stopAnimation];
}

- (void) gotoUrl:(NSString *) destinationURL 
{   
    if(destinationURL == NULL)
    {
        return;
    }

    NSString * path    = destinationURL;
    NSURL    * fileURL = [NSURL URLWithString:path];
    [[ NSWorkspace sharedWorkspace ] openURL:fileURL];
    [self quitScreenSaver];
}

@end

I hope that's enough code for you to see some problems / solutions. I would really appreciaty any answers.

A: 

Just to troubleshoot, did you try not releasing the WebView?

Also, maybe set the WebView's delegates to nil before releasing it first?

Ken Aspeslagh
Just tried that. The system prefs panel still crashes. And I think the WebView has no delegates.
dalind
In the sample code you posted, you're setting two different delegates on the webview.
Ken Aspeslagh
You're right. I have deleted these but the crash still occurs.
dalind
A: 

One thing to be aware of is that when you start the screensaver via the "Test" button in System Prefs, you actually have 2 instances of your screensaver view running in the same process' address space on different threads. One (with isPreview==YES) is the little preview in the SysPrefs window (which continues running even when the full-screen version is started), and the other one is the full-screen version. They are both running in the SysPrefs.app process. So, you have to be careful to check all Notifications/etc. to see if they are coming from the view instance you expect.

I don't see any obvious problems from a quick glance at the code you posted, but it may be somewhere else. Do you use Notifications anywhere?

I put a similar webview-in-a-screensaver project on github at http://github.com/kelan/WikiWalker, where I initially had some similar problems (though I wasn't using any javascript stuff). It's not perfect code, but might help. I also did some tricks to forward notifications to the main thread (for drawing) in a . See the "Threaded Notification Support" parts of WWScreenSaverView.{h,m}.

Kelan
I don't use notifications anywhere.I checked the instances of my different objects (WebView, ScriptObject, self), but the instance, where the unrecognized selector is sent to, is something completely different. Is there a way to find out, which instances of objects are running and to get the name (e.g.: 0x200044820)?The scriptobject is provided for JavaScript correctly, but afterwards, the sysPrefs crash.
dalind
A: 

Something to try:

  • Open up a terminal window and enter the following line to run System Preferences with NSZombieEnabled:

env NSZombieEnabled=YES "/Applications/System Preferences.app/Contents/MacOS/System Preferences"

  • Perform the steps that lead to the crash.

  • Run the Console app, set the filter in the upper right to "System Preferences", and look for NSZombie messages.

Hope this helps!

tedge
I tried that 2 times, but there are no NSZombie messages in my console. The error message is still "***Terminating app due to uncaught exception 'NSInvalidArgumentException' - Unrecognized selector sent to instances 0x200472000***" . That instance number is non of my objects I think, at least not my mainview, webview or scriptobject.
dalind
+2  A: 

Somehow an NSCFArray (NSMutableArray) is being sent a "drain" message that's meant for an NSAutoreleasePool.

You might be able to get a bit more info on what the array is by implementing the drain method for NSMutableArray, so you can trap the now-recognized selector and print out the contents of the array object. Try adding this somewhere in your code:

@interface NSMutableArray (drain)

- (void) drain;

@end

@implementation NSMutableArray (drain)

- (void) drain
{
   NSLog(@"drain message received by object: %@", self);
}

@end

If you don't see any messages show up in the Console, try changing the "NSMutableArray" in the above code to "NSObject".

tedge
With overriding NSObject drain, the system prefs panel crashes no longer. The drain message comes from some strange preview image, most of the time a different one. Everything works great now.Thank you!
dalind
Vote up because this is an answer I think one can always need if some strange objects kill your app, especially if the app gets executed by another one.
dalind