On 2013 Apr 19, at 12:37, Mike Abdullah <cocoa...@mikeabdullah.net> wrote:

> Why, what's wrong with cancelling a [auto]save?

That's a damned good question, Mike.  You're probably thinking that, hey, we 
lived without any autosaves from 1984 to 2011.  What's the big deal?  It turns 
out that you need to be really careful when playing around with an autosave 
that Cocoa has designated "not implicitly cancellable".  Search the internet 
for:

    deadlock in -[NSDocument performActivityWithSynchronousWaiting:usingBlock:]

to see some of the fun that people have had.

I have an app with a requirement similar to Steve's.  The app can do 
long-winded sequences of operations that take tens of seconds, and change the 
document.  So if I honored an autosave request during one these sequences, I'd 
have to interrupt the operations (which is tricky), save, and then save again 
at the end after the changes were done.

The solution: Upon receiving a non-cancellable autosave message while other 
operations are in progress, I stash the completion handler that Cocoa sends in 
the message, create an operation to "really autosave" later, and add it to my 
operation queue.

I had to do other stuff to deal with corner cases such as Revert, being in the 
Versions Browser, etc.  Below, I've snipped out a few of the relevant methods 
from my NSDocument (actually it's NSPersistentDocument, which adds even more to 
the mess) implementation.

Jerry

- 
(void)autosaveWithImplicitCancellability:(BOOL)autosavingIsImplicitlyCancellable
                         completionHandler:(void (^)(NSError 
*errorOrNil))completionHandler {
    if (autosavingIsImplicitlyCancellable) {
        // We can cancel this autosave if we want to.
        if (
            // If operations are currently in progress, cancel it.  This is
            // because we will save when our operations are complete.
            ([[[self operationQueue] operations] count] != 0)
            ||
            // Prevent unnecessary saves, in case the document is in a watched 
folder
            // that triggers a syncing mechanism.
            (![self isDocumentEdited])
            ) {
            // Cancel it.
            completionHandler([NSError errorWithDomain:NSCocoaErrorDomain
                                                  code:NSUserCancelledError
                                              userInfo:nil]) ;
            return ;
        }
    }
    
    NSMutableDictionary* info = [NSMutableDictionary dictionary] ;
    [info setValue:Block_copy(completionHandler) 
forKey:constKeyCompletionHandler] ;
    [info setObject:self forKey:constKeyDocument] ;
    // The following adds a task to my home-made main operation
    // queue which I wrote back in the Leopard days …
    NSArray* selectorNames = [NSArray arrayWithObject:@"reallyAutosave"] ;
    [[self operationQueue] queueGroup:@"Non-cancellable Autosave"
                                addon:NO
                        selectorNames:selectorNames
                                 info:info
                                block:NO
                                owner:self
                           doneThread:nil
                           doneTarget:nil
                         doneSelector:NULL
                         keepWithNext:NO] ;
    [self setSavingState:1] ;
}


// This is the method that runs (in the main thread) to fulfill "reqllyAutosave"

- (void)reallyAutosave_unsafe {
    NSDictionary* info = [self info] ;
    Bkmslf* bkmslf = [info objectForKey:constKeyDocument] ;
    
    void (^completionHandler)(NSError*) = [info 
objectForKey:constKeyCompletionHandler] ;
    
    [bkmslf reallyAutosaveWithCompletionHandler:completionHandler] ;
    // Note: completionHandler will be released by the receiver of the above 
message.
}

/*!
 @brief    Override of new asynchronous saving method invoked by Cocoa
 when running in Mac OS X 10.7 or later.
 */
- (void)saveToURL:(NSURL *)url
           ofType:(NSString *)typeName
 forSaveOperation:(NSSaveOperationType)saveOperation
completionHandler:(void (^)(NSError *errorOrNil))completionHandler {
    
    [self prepareForSaveOperation:saveOperation] ;
    
    // Despite my best efforts to play nice with autosave, it still occasionally
    // deadlocked on me, until I added this:
    if ([self savingState] > 1) {
        NSLog(@"Warning 089-0831  Cancelling save, now in state %ld, from %@ to 
avoid deadlock", (long)[self savingState], SSYDebugCaller()) ;
        completionHandler([NSError errorWithDomain:NSCocoaErrorDomain
                                              code:NSUserCancelledError
                                          userInfo:nil]) ;
        return ;
    }
    [self setSavingState:2] ;
    
    [super saveToURL:url
              ofType:typeName
    forSaveOperation:saveOperation
   completionHandler:completionHandler] ;
}

- (void)doHousekeepingAfterSaveNotification:(NSNotification*)note {
    [self setSavingState:0] ;

    ...
}


_______________________________________________

Cocoa-dev mailing list (Cocoa-dev@lists.apple.com)

Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com

Help/Unsubscribe/Update your Subscription:
https://lists.apple.com/mailman/options/cocoa-dev/archive%40mail-archive.com

This email sent to arch...@mail-archive.com

Reply via email to