From 0111e6d089afde4fa6f5d67e5fda7476146bb388 Mon Sep 17 00:00:00 2001 From: Allan Odgaard Date: Mon, 8 Apr 2013 16:35:57 +0700 Subject: [PATCH] Rework bundle update scheduling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now store the time of last check in user defaults instead of via extended attributes on the source(s) and do a check every third hour, except of errors, where we retry after 30 minutes. We now also check for bundle updates even if the sources hasn’t been updated. This is incase we previously updated a source but failed to then update bundles, these will now be updated when we retry, rather than next time the source index has updates. Overall the code should be simpler and thus more robust. The text in the Bundles preferences page has also been improved slightly in that it will now tell the user if there was a problem updating the bundles, although for the specifics, the user will need to grab the log. --- .../BundlesManager/src/BundlesManager.h | 1 + .../BundlesManager/src/BundlesManager.mm | 144 ++++++++++-------- 2 files changed, 78 insertions(+), 67 deletions(-) diff --git a/Frameworks/BundlesManager/src/BundlesManager.h b/Frameworks/BundlesManager/src/BundlesManager.h index a49f689b..d2b38c5b 100644 --- a/Frameworks/BundlesManager/src/BundlesManager.h +++ b/Frameworks/BundlesManager/src/BundlesManager.h @@ -12,6 +12,7 @@ PUBLIC @interface BundlesManager : NSObject @property (nonatomic, readonly) BOOL isBusy; @property (nonatomic, readonly) BOOL determinateProgress; @property (nonatomic, readonly) CGFloat progress; +@property (nonatomic, readonly) NSDate* lastUpdateCheck; - (NSUInteger)numberOfBundles; - (bundles_db::bundle_ptr const&)bundleAtIndex:(NSUInteger)anIndex; diff --git a/Frameworks/BundlesManager/src/BundlesManager.mm b/Frameworks/BundlesManager/src/BundlesManager.mm index 53025861..043ea432 100644 --- a/Frameworks/BundlesManager/src/BundlesManager.mm +++ b/Frameworks/BundlesManager/src/BundlesManager.mm @@ -37,6 +37,7 @@ static double const kPollInterval = 3*60*60; @property (nonatomic) BOOL isBusy; @property (nonatomic) BOOL determinateProgress; @property (nonatomic) CGFloat progress; +@property (nonatomic) NSDate* lastUpdateCheck; @property (nonatomic) NSTimer* updateTimer; @property (nonatomic) BOOL needsCreateBundlesIndex; @@ -70,15 +71,77 @@ static double const kPollInterval = 3*60*60; if(_autoUpdateBundles == flag) return; - if(_autoUpdateBundles = flag) + _autoUpdateBundles = flag; + [self setupUpdateTimer]; +} + +- (void)setupUpdateTimer +{ + [self.updateTimer invalidate]; + self.updateTimer = nil; + if(!_autoUpdateBundles) + return; + + NSDate* lastCheck = [[NSUserDefaults standardUserDefaults] objectForKey:kUserDefaultsLastBundleUpdateCheckKey] ?: [NSDate distantPast]; + NSDate* nextCheck = [lastCheck dateByAddingTimeInterval:kPollInterval]; + NSTimeInterval checkAfterSeconds = std::max(1, [nextCheck timeIntervalSinceNow]); + D(DBF_BundlesManager, bug("perform next check in %.1f hours\n", checkAfterSeconds/60/60);); + self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:checkAfterSeconds target:self selector:@selector(didFireUpdateTimer:) userInfo:nil repeats:NO]; +} + +- (void)didFireUpdateTimer:(NSTimer*)aTimer +{ + self.isBusy = YES; + + std::vector sources; + for(auto source : sourceList) { - [self updateSources:nil]; - } - else - { - [self.updateTimer invalidate]; - self.updateTimer = nil; + if(!source->disabled()) + sources.push_back(source); } + + self.activityText = @"Updating sources…"; + [self updateSources:sources completionHandler:^(std::vector const& failedSources){ + std::vector outdatedBundles; + if(![[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsDisableBundleUpdatesKey]) + { + for(auto installedBundle : bundlesIndex) + { + if(!installedBundle->has_update()) + continue; + + for(auto bundle : bundles_db::dependencies(bundlesIndex, installedBundle, false, false)) + { + if((bundle->has_update() || !bundle->installed()) && installing.find(bundle->uuid()) == installing.end()) + outdatedBundles.push_back(bundle); + } + } + } + + self.activityText = @"Updating bundles…"; + [self installBundles:outdatedBundles completionHandler:^(std::vector const& failedBundles){ + NSDate* lastCheck = [NSDate date]; + if(failedSources.empty() && failedBundles.empty()) + { + self.activityText = nil; + } + else + { + self.activityText = @"Error updating bundles, will retry later."; + lastCheck = [lastCheck dateByAddingTimeInterval:-(kPollInterval - 30*60)]; // retry in 30 minutes + + for(auto source : failedSources) + fprintf(stderr, "*** error downloading ‘%s’\n", source->url().c_str()); + for(auto bundle : failedBundles) + fprintf(stderr, "*** error downloading ‘%s’\n", bundle->url().c_str()); + } + + self.isBusy = NO; + + [[NSUserDefaults standardUserDefaults] setObject:lastCheck forKey:kUserDefaultsLastBundleUpdateCheckKey]; + [self setupUpdateTimer]; + }]; + }]; } - (void)installBundleItemsAtPaths:(NSArray*)somePaths @@ -177,60 +240,18 @@ static double const kPollInterval = 3*60*60; }); } -- (void)updateInstalledBundles +- (void)updateSources:(std::vector const&)sourcesReference completionHandler:(void(^)(std::vector const& ))callback { - std::vector bundles; - for(auto installedBundle : bundlesIndex) + D(DBF_BundlesManager, bug("\n");); + __block std::vector failedSources; + if(sourcesReference.empty()) { - if(!installedBundle->has_update()) - continue; - - for(auto bundle : bundles_db::dependencies(bundlesIndex, installedBundle, false, false)) - { - if((bundle->has_update() || !bundle->installed()) && installing.find(bundle->uuid()) == installing.end()) - bundles.push_back(bundle); - } - } - - if(!bundles.empty()) - { - self.activityText = @"Updating bundles…"; - self.isBusy = YES; - [self installBundles:bundles completionHandler:^(std::vector const& failedBundles){ - self.activityText = @"Bundles updated"; - self.isBusy = NO; - }]; - } -} - -- (void)updateSources:(id)sender -{ - D(DBF_BundlesManager, bug("busy %s\n", BSTR(self.isBusy));); - if(self.isBusy || sourceList.empty()) - return; - - std::vector sources; - NSDate* earliest = [NSDate distantFuture]; - for(auto source : sourceList) - { - if(!source->disabled() && source->needs_update(kPollInterval)) - sources.push_back(source); - earliest = [earliest earlierDate:CFBridgingRelease(CFDateCreate(kCFAllocatorDefault, source->last_check().value()))]; - } - - if(sources.empty()) - { - [self.updateTimer invalidate]; - self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:MIN(kPollInterval + [earliest timeIntervalSinceNow], kPollInterval) target:self selector:@selector(updateSources:) userInfo:nil repeats:NO]; - self.activityText = [NSString stringWithFormat:@"Last check: %@", [earliest humanReadableTimeElapsed]]; + callback(failedSources); return; } - self.activityText = @"Updating sources…"; - self.isBusy = YES; - + auto sources = sourcesReference; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ - __block std::vector failedSources; dispatch_apply(sources.size(), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^(size_t i){ D(DBF_BundlesManager, bug("Updating ‘%s’…\n", sources[i]->url().c_str());); if(!bundles_db::update(sources[i])) @@ -238,21 +259,10 @@ static double const kPollInterval = 3*60*60; }); dispatch_async(dispatch_get_main_queue(), ^{ - for(auto source : failedSources) - fprintf(stderr, "*** error downloading ‘%s’\n", source->url().c_str()); - bundlesIndex = bundles_db::index(kInstallDirectory); // trigger reload for table view in Preferences → Bundles [[NSNotificationCenter defaultCenter] postNotificationName:BundlesManagerBundlesDidChangeNotification object:self]; - - self.activityText = @"Sources updated"; - self.isBusy = NO; - - if(![[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsDisableBundleUpdatesKey]) - [self updateInstalledBundles]; - - [self.updateTimer invalidate]; - self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:(failedSources.empty() ? kPollInterval : 20*60*60) target:self selector:@selector(updateSources:) userInfo:nil repeats:NO]; + callback(failedSources); }); }); }