Skip to content

Commit

Permalink
ensure uncommitted changes are also pushed before entering sleep; pre…
Browse files Browse the repository at this point in the history
…vent idle sleep if a sync is still in progress; allow multiple callbacks to wait simultaneously for uncommitted changes to finish pushing
  • Loading branch information
Zachary Schneirov committed Feb 25, 2010
1 parent 86b3857 commit 5329752
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 29 deletions.
1 change: 1 addition & 0 deletions NotationSyncServiceManager.m
Expand Up @@ -66,6 +66,7 @@ - (void)startSyncServices {
- (void)stopSyncServices {
[NSObject cancelPreviousPerformRequestsWithTarget:syncSessionController];
[syncSessionController invalidateReachabilityRefs];
[syncSessionController unregisterPowerChangeCallback];
[syncSessionController invalidateAllServices];
[syncSessionController setSyncDelegate:nil];
}
Expand Down
21 changes: 15 additions & 6 deletions SyncSessionController.h
Expand Up @@ -17,7 +17,7 @@

#import <Cocoa/Cocoa.h>
#include <SystemConfiguration/SystemConfiguration.h>

#import <IOKit/IOMessage.h>
#import "SyncServiceSessionProtocol.h"

@class NotationPrefs;
Expand All @@ -36,11 +36,15 @@ extern NSString *SyncSessionsChangedVisibleStatusNotification;
//shouldn't go in the SimplenoteSession class as we probably don't even want an instance hanging around if there's no network:
//do we have one reachableref for every service? maybe this class should manage a series of SyncServicePref objs instead
SCNetworkReachabilityRef reachableRef;

io_object_t deregisteringNotifier;
io_connect_t fRootPort;
IONotificationPortRef notifyPortRef;

BOOL isConnectedToNetwork;
BOOL isWaitingForUncommittedChanges;
id uncommittedWaitTarget;
SEL uncommittedWaitSelector;

NSString *lastUncomittedChangeResultMessage;
NSMutableSet *uncommittedWaitInvocations;
}

+ (NSArray*)allServiceNames;
Expand All @@ -55,13 +59,18 @@ extern NSString *SyncSessionsChangedVisibleStatusNotification;
- (void)invalidateSyncService:(NSString*)serviceName;
- (void)invalidateAllServices;

- (void)endDelayingSleepWithMessage:(void*)messageArgument;

- (void)disableService:(NSString*)serviceName;
- (void)initializeService:(NSString*)serviceName;
- (void)initializeAllServices;

- (void)schedulePushToAllInitializedSessionsForNote:(id <SynchronizedNote>)aNote;
- (NSArray*)activeSessions;

- (void)_registerPowerChangeCallbackIfNecessary;
- (void)unregisterPowerChangeCallback;

- (void)invalidateReachabilityRefs;

- (void)_updateMenuWithCurrentStatus:(NSMenu*)aMenu;
Expand All @@ -71,9 +80,9 @@ extern NSString *SyncSessionsChangedVisibleStatusNotification;
- (BOOL)hasErrors;
- (void)queueStatusNotification;

- (void)_invokeUncommittedCallback;
- (NSString*)changeCommittingErrorMessage;
- (void)invokeUncommmitedWaitCallbackIfNecessaryReturningError:(NSString*)errString;
- (BOOL)waitForUncommitedChangesWithTarget:(id)aTarget selector:(SEL)aSEL;
- (BOOL)waitForUncommitedChangesWithInvocation:(NSInvocation*)anInvocation;


@end
125 changes: 102 additions & 23 deletions SyncSessionController.m
Expand Up @@ -17,14 +17,19 @@

#import "SyncSessionController.h"
#import "NotationPrefs.h"
#import "InvocationRecorder.h"
#import "SyncServiceSessionProtocol.h"
#import "NotationDirectoryManager.h"
#import "SimplenoteSession.h"

//#import <IOKit/IOMessage.h>

NSString *SyncSessionsChangedVisibleStatusNotification = @"SSCVSN";

@implementation SyncSessionController

static void SNReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkConnectionFlags flags, void * info);
static void SleepCallBack(void *refcon, io_service_t y, natural_t messageType, void * messageArgument);

- (id)initWithSyncDelegate:(id)aSyncDelegate notationPrefs:(NotationPrefs*)prefs {
if ([super init]) {
Expand Down Expand Up @@ -88,6 +93,62 @@ static void SNReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkCon
[[NSNotificationCenter defaultCenter] postNotificationName:SyncPrefsDidChangeNotification object:nil];
}

static void SleepCallBack(void *refcon, io_service_t y, natural_t messageType, void * messageArgument) {

SyncSessionController *self = (SyncSessionController*)refcon;
InvocationRecorder *invRecorder = nil;

switch (messageType) {
case kIOMessageSystemWillSleep:
[[(invRecorder = [InvocationRecorder invocationRecorder]) prepareWithInvocationTarget:self] endDelayingSleepWithMessage:messageArgument];

if (![self waitForUncommitedChangesWithInvocation:[invRecorder invocation]]) {
//if we don't have to wait, then do not delay sleep
[self endDelayingSleepWithMessage:messageArgument];
} else {
NSLog(@"delaying sleep for uncommitted changes");
}
break;
case kIOMessageCanSystemSleep:
//pevent idle sleep if a session is currently running
if ([self hasRunningSessions]) {
IOCancelPowerChange(self->fRootPort, (long)messageArgument);
} else {
IOAllowPowerChange(self->fRootPort, (long)messageArgument);
}
break;
case kIOMessageSystemHasPoweredOn:
//after waking from sleep, probably don't need to do anything as the network reachability check ought to run later, anyway
break;
}
}

- (void)endDelayingSleepWithMessage:(void*)messageArgument {
//NSLog(@"allow powerchange under port %X for '%d'", fRootPort, (long)messageArgument);
IOAllowPowerChange(fRootPort, (long)messageArgument);
}
- (void)_registerPowerChangeCallbackIfNecessary {
if (!notifyPortRef) {
if ((fRootPort = IORegisterForSystemPower((void*)self, &notifyPortRef, SleepCallBack, &deregisteringNotifier))) {
CFRunLoopAddSource(CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(notifyPortRef), kCFRunLoopCommonModes);
//NSLog(@"registered for power change under port %X", fRootPort);
} else {
NSLog(@"error: IORegisterForSystemPower");
}
}
}
- (void)unregisterPowerChangeCallback {
if (notifyPortRef) {
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(notifyPortRef), kCFRunLoopCommonModes);

IODeregisterForSystemPower(&deregisteringNotifier);
IOServiceClose(fRootPort);
IONotificationPortDestroy(notifyPortRef);
//NSLog(@"unregistered for power change under port %X", fRootPort);
fRootPort = 0;
notifyPortRef = NULL;
}
}

- (id<SyncServiceSession>)_sessionForSyncService:(NSString*)serviceName {
//map names to sync service sessions, creating them if necessary
Expand Down Expand Up @@ -119,9 +180,9 @@ static void SNReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkCon
//init and return other services here
} */ else {
NSLog(@"%s: unknown service named '%@'", _cmd, serviceName);
}
} */ else {
NSLog(@"%s: unknown service named '%@'", _cmd, serviceName);
}
}
return session;
}
Expand All @@ -134,6 +195,8 @@ - (void)invalidateSyncService:(NSString*)serviceName {

[[syncServiceTimers objectForKey:serviceName] invalidate];
[syncServiceTimers removeObjectForKey:serviceName];

//can't unregister power-change-callback here because network interruptions could extend sleep via dissociating the notifier
}

- (void)initializeService:(NSString*)serviceName {
Expand All @@ -148,6 +211,8 @@ - (void)initializeService:(NSString*)serviceName {
[syncServiceTimers setObject:timer forKey:serviceName];
}

[self _registerPowerChangeCallbackIfNecessary];

//start syncing now
[session startFetchingListForFullSync];
}
Expand Down Expand Up @@ -201,8 +266,9 @@ - (void)invalidateReachabilityRefs {
if (reachableRef) {
SCNetworkReachabilityUnscheduleFromRunLoop(reachableRef, CFRunLoopGetCurrent(),kCFRunLoopDefaultMode);
CFRelease(reachableRef);
reachableRef = nil;
reachableRef = NULL;
}
isConnectedToNetwork = YES;
}

- (void)menuNeedsUpdate:(NSMenu *)menu {
Expand Down Expand Up @@ -283,8 +349,11 @@ - (void)_updateMenuWithCurrentStatus:(NSMenu*)aMenu {
action:nil keyEquivalent:@""] autorelease];
} else if (isEnabled) {

NSString *title = isConnectedToNetwork ? NSLocalizedString(@"Session could not be created", nil) :
NSLocalizedString(@"Internet unavailable.", @"message to report when sync service is not reachable over internet");
NSString *title = NSLocalizedString(@"Session could not be created", nil);
if (!isConnectedToNetwork) {
title = //[class respondsToSelector:@selector(localizedNetworkDiagnosticMessage)] ? [class localizedNetworkDiagnosticMessage] :
NSLocalizedString(@"Internet unavailable.", @"message to report when sync service is not reachable over internet");
}
badItem = [[[NSMenuItem alloc] initWithTitle:title action:nil keyEquivalent:@""] autorelease];
}
[badItem setEnabled:NO];
Expand Down Expand Up @@ -339,35 +408,39 @@ - (void)queueStatusNotification {
[[NSNotificationQueue defaultQueue] enqueueNotification:aNote postingStyle:NSPostWhenIdle coalesceMask:NSNotificationCoalescingOnName forModes:nil];
}

- (void)_invokeUncommittedCallback {
isWaitingForUncommittedChanges = NO;
[uncommittedWaitTarget performSelector:uncommittedWaitSelector];
- (NSString*)changeCommittingErrorMessage {
return lastUncomittedChangeResultMessage;
}

- (void)invokeUncommmitedWaitCallbackIfNecessaryReturningError:(NSString*)errString {
if (isWaitingForUncommittedChanges) {
if ([errString length]) {
if ([uncommittedWaitInvocations count]) {
[lastUncomittedChangeResultMessage autorelease];
lastUncomittedChangeResultMessage = [errString copy];
if ([errString length] || ![self hasRunningSessions]) {
//fail on the first occur that occurs; currently doesn't provide an opportunity for continuing to sync with other non-failed svcs
NSRunAlertPanel(NSLocalizedString(@"Changes could not be uploaded.", nil), errString, @"Quit", nil, nil);
[self _invokeUncommittedCallback];
} else if (![self hasRunningSessions]) {
[self _invokeUncommittedCallback];
[uncommittedWaitInvocations makeObjectsPerformSelector:@selector(invoke)];
[uncommittedWaitInvocations removeAllObjects];
}
}
}

- (BOOL)waitForUncommitedChangesWithTarget:(id)aTarget selector:(SEL)aSEL {

- (BOOL)waitForUncommitedChangesWithInvocation:(NSInvocation*)anInvocation {
// push any uncommitted notes for all sessions, so that those will then be running
// if we didn't have to push for any of the sessions AND none of the sessions are running, then return right away; there are no changes left to send

// syncDelegate invokes selector on target when any currently running sessions have stopped and no sessions have any more uncommited notes
// syncDelegate invokes anInvocation when any currently running sessions have stopped and no sessions have any more uncommited notes
// it must call invokeUncommmitedWaitCallbackIfNecessary from -syncSession:didStopWithError:

if (isWaitingForUncommittedChanges) return YES; //we're already waiting
NSAssert(anInvocation != nil, @"cannot wait without an ending invocation");
ComparableInvocation *cInvocation = [[[ComparableInvocation alloc] initWithInvocation: anInvocation] autorelease];
if ([uncommittedWaitInvocations containsObject:cInvocation]) {
NSLog(@"%s: already waiting for %@", _cmd, anInvocation);
return YES; //we're already waiting for this invocation
}

NSAssert([aTarget respondsToSelector:aSEL], @"target doesn't respond to callback");
uncommittedWaitTarget = aTarget;
uncommittedWaitSelector = aSEL;
[lastUncomittedChangeResultMessage release];
lastUncomittedChangeResultMessage = nil;

BOOL willNeedToWait = NO;
NSArray *sessions = [syncServiceSessions allValues];
Expand All @@ -377,7 +450,8 @@ - (BOOL)waitForUncommitedChangesWithTarget:(id)aTarget selector:(SEL)aSEL {
if ([session hasUnsyncedChanges]) {
if (!(![session pushSyncServiceChanges] && ![session isRunning])) {
willNeedToWait = YES;
isWaitingForUncommittedChanges = YES;
if (!uncommittedWaitInvocations) uncommittedWaitInvocations = [[NSMutableSet alloc] initWithCapacity:1];
[uncommittedWaitInvocations addObject:cInvocation];
}
}
}
Expand All @@ -388,12 +462,17 @@ - (BOOL)waitForUncommitedChangesWithTarget:(id)aTarget selector:(SEL)aSEL {

- (void)dealloc {

if (reachableRef) CFRelease(reachableRef);
[self invalidateReachabilityRefs];
[self unregisterPowerChangeCallback];

[statusMenu release];
[syncServiceTimers release];
[syncServiceSessions release];
syncServiceSessions = nil;
[notationPrefs release];
[uncommittedWaitInvocations release];
uncommittedWaitInvocations = nil;
[lastUncomittedChangeResultMessage release];

[super dealloc];
}
Expand Down

0 comments on commit 5329752

Please sign in to comment.