diff options
Diffstat (limited to 'chromium/chrome/common/mac/staging_watcher.mm')
-rw-r--r-- | chromium/chrome/common/mac/staging_watcher.mm | 191 |
1 files changed, 191 insertions, 0 deletions
diff --git a/chromium/chrome/common/mac/staging_watcher.mm b/chromium/chrome/common/mac/staging_watcher.mm new file mode 100644 index 00000000000..12fed3c9e0e --- /dev/null +++ b/chromium/chrome/common/mac/staging_watcher.mm @@ -0,0 +1,191 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/common/mac/staging_watcher.h" + +#include "base/mac/bundle_locations.h" +#include "base/mac/foundation_util.h" +#include "base/mac/mac_util.h" +#include "base/mac/scoped_block.h" +#include "base/mac/scoped_nsobject.h" + +// Best documentation / Is unofficial documentation +// +// Required reading for CFPreferences/NSUserDefaults is at +// <http://dscoder.com/defaults.html>, a post by David "Catfish Man" Smith, who +// re-wrote NSUserDefaults for 10.12 and iPad Classroom support. It is important +// to note that KVO only notifies for changes made by other programs starting +// with that rewrite in 10.12. In macOS 10.11 and earlier, polling is the only +// option. Note that NSUserDefaultsDidChangeNotification never notifies about +// changes made by other programs, not even in 10.12 and later. +// +// On the other hand, KVO notification was broken in the NSUserDefaults rewrite +// for 10.14; see: +// - https://twitter.com/Catfish_Man/status/1116185288257105925 +// - rdar://49812220 +// The bug is that a change from "no value" to "value present" doesn't notify. +// To work around that, a default is registered to ensure that there never is +// a "no value" situation. + +namespace { + +NSString* const kStagingKey = @"UpdatePending"; + +} // namespace + +@interface CrStagingKeyWatcher () { + base::scoped_nsobject<NSUserDefaults> defaults_; + NSTimeInterval pollingTime_; + base::scoped_nsobject<NSTimer> pollingTimer_; + BOOL observing_; + base::mac::ScopedBlock<StagingKeyChangedObserver> callback_; + BOOL lastStagingKeyValue_; + + BOOL lastWaitWasBlockedForTesting_; +} + ++ (NSString*)stagingLocationWithUserDefaults:(NSUserDefaults*)defaults; + +@end + +@implementation CrStagingKeyWatcher + +- (instancetype)initWithPollingTime:(NSTimeInterval)pollingTime { + return [self initWithUserDefaults:[NSUserDefaults standardUserDefaults] + pollingTime:pollingTime + disableKVOForTesting:NO]; +} + +- (instancetype)initWithUserDefaults:(NSUserDefaults*)defaults + pollingTime:(NSTimeInterval)pollingTime + disableKVOForTesting:(BOOL)disableKVOForTesting { + if ((self = [super init])) { + pollingTime_ = pollingTime; + defaults_.reset(defaults, base::scoped_policy::RETAIN); + [defaults_ registerDefaults:@{kStagingKey : @[]}]; + lastStagingKeyValue_ = [self isStagingKeySet]; + if (base::mac::IsAtLeastOS10_12() && !disableKVOForTesting) { + // If a change is made in another process (which is the use case here), + // the prior value is never provided in the observation callback change + // dictionary, whether or not NSKeyValueObservingOptionPrior is specified. + // Therefore, pass in 0 for the NSKeyValueObservingOptions and rely on + // keeping the previous value in |lastStagingKeyValue_|. + [defaults_ addObserver:self + forKeyPath:kStagingKey + options:0 + context:nullptr]; + observing_ = YES; + } + } + return self; +} + ++ (NSString*)stagingLocationWithUserDefaults:(NSUserDefaults*)defaults { + NSDictionary<NSString*, id>* stagedPathPairs = + [defaults dictionaryForKey:kStagingKey]; + if (!stagedPathPairs) + return nil; + + NSString* appPath = [base::mac::OuterBundle() bundlePath]; + + return base::mac::ObjCCast<NSString>([stagedPathPairs objectForKey:appPath]); +} + +- (BOOL)isStagingKeySet { + return [self stagingLocation] != nil; +} + ++ (BOOL)isStagingKeySet { + return [self stagingLocation] != nil; +} + +- (NSString*)stagingLocation { + return [CrStagingKeyWatcher stagingLocationWithUserDefaults:defaults_]; +} + ++ (NSString*)stagingLocation { + return [self + stagingLocationWithUserDefaults:[NSUserDefaults standardUserDefaults]]; +} + +- (void)waitForStagingKeyToClear { + if (![self isStagingKeySet]) { + lastWaitWasBlockedForTesting_ = NO; + return; + } + + NSRunLoop* runloop = [NSRunLoop currentRunLoop]; + if (observing_) { + callback_.reset( + ^(BOOL stagingKeySet) { + CFRunLoopStop([runloop getCFRunLoop]); + }, + base::scoped_policy::RETAIN); + + while ([self isStagingKeySet] && [runloop runMode:NSDefaultRunLoopMode + beforeDate:[NSDate distantFuture]]) { + /* run! */ + } + } else { + while ([self isStagingKeySet] && + [runloop + runMode:NSDefaultRunLoopMode + beforeDate:[NSDate dateWithTimeIntervalSinceNow:pollingTime_]]) { + /* run! */ + } + } + + lastWaitWasBlockedForTesting_ = YES; +} + +- (void)setStagingKeyChangedObserver:(StagingKeyChangedObserver)block { + callback_.reset(block, base::scoped_policy::RETAIN); + + if (observing_) { + // Nothing to be done; the observation is already started. + } else { + pollingTimer_.reset( + [NSTimer scheduledTimerWithTimeInterval:pollingTime_ + target:self + selector:@selector(timerFired:) + userInfo:nil + repeats:YES], + base::scoped_policy::RETAIN); + } +} + +- (void)timerFired:(NSTimer*)timer { + [self observeValueForKeyPath:nil ofObject:nil change:nil context:nil]; +} + +- (void)dealloc { + if (observing_) + [defaults_ removeObserver:self forKeyPath:kStagingKey context:nullptr]; + if (pollingTimer_) + [pollingTimer_ invalidate]; + + [super dealloc]; +} + +- (BOOL)lastWaitWasBlockedForTesting { + return lastWaitWasBlockedForTesting_; +} + ++ (NSString*)stagingKeyForTesting { + return kStagingKey; +} + +- (void)observeValueForKeyPath:(NSString*)keyPath + ofObject:(id)object + change:(NSDictionary*)change + context:(void*)context { + BOOL isStagingKeySet = [self isStagingKeySet]; + if (isStagingKeySet == lastStagingKeyValue_) + return; + + lastStagingKeyValue_ = isStagingKeySet; + callback_.get()([self isStagingKeySet]); +} + +@end |