Search code examples
objective-ccocoamacosscreensaver

Mac Screensaver with WebView crashes


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.


Solution

  • 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".