Commit 35b27074 authored by Avi Drissman's avatar Avi Drissman Committed by Commit Bot

[Mac Updater] Add a staging-key-changed callback to the staging watcher.

Also do some cleanup of naming, and use CFRunLoop calls rather
than NSPorts to stop running NSRunLoops.

BUG=496298

Change-Id: Iaa862268ae2a0b40e2eedfbc482da2cf4c69e4e1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1540099
Auto-Submit: Avi Drissman <avi@chromium.org>
Reviewed-by: default avatarMark Mentovai <mark@chromium.org>
Commit-Queue: Avi Drissman <avi@chromium.org>
Cr-Commit-Position: refs/heads/master@{#644849}
parent 8d68867d
...@@ -365,8 +365,8 @@ int RelauncherMain(const content::MainFunctionParams& main_parameters) { ...@@ -365,8 +365,8 @@ int RelauncherMain(const content::MainFunctionParams& main_parameters) {
// If an update is staged but not yet installed, wait for it to be installed. // If an update is staged but not yet installed, wait for it to be installed.
if (wait_for_staged_update) { if (wait_for_staged_update) {
base::scoped_nsobject<CrStagingKeyWatcher> watcher( base::scoped_nsobject<CrStagingKeyWatcher> watcher(
[[CrStagingKeyWatcher alloc] init]); [[CrStagingKeyWatcher alloc] initWithPollingTime:0.5]);
[watcher waitForStagingKey]; [watcher waitForStagingKeyToClear];
} }
NSString* path = base::SysUTF8ToNSString(relaunch_executable); NSString* path = base::SysUTF8ToNSString(relaunch_executable);
......
...@@ -12,24 +12,50 @@ ...@@ -12,24 +12,50 @@
// this state of "update pending" is indicated outside of Keystone by a key in // this state of "update pending" is indicated outside of Keystone by a key in
// the CFPreferences. // the CFPreferences.
using StagingKeyChangedObserver = void (^)(BOOL stagingKeySet);
// An object to observe the staging key. It can be used in either of two ways:
// 1. To wait for the staging key to clear.
// 2. To notify when the staging key changes state.
@interface CrStagingKeyWatcher : NSObject @interface CrStagingKeyWatcher : NSObject
- (instancetype)init; // On macOS 10.11 and earlier, polling is used, and |pollingTime| specifies the
// frequency of the polling. On macOS 10.12 and later, no polling is performed
// and |pollingTime| is ignored.
- (instancetype)initWithPollingTime:(NSTimeInterval)pollingTime;
// Returns a boolean indicating whether or not the staging key is set.
- (BOOL)isStagingKeySet;
// Sleeps until the staging key is clear. If there is no staging key set, // Sleeps until the staging key is clear. If there is no staging key set,
// returns immediately. // returns immediately.
- (void)waitForStagingKey; - (void)waitForStagingKeyToClear;
// Sets a block to be called when the staging key changes, and starts observing.
// Only one observer may be set for a given CrStagingKeyWatcher; calling this
// method again replaces the current observer.
- (void)setStagingKeyChangedObserver:(StagingKeyChangedObserver)block;
@end @end
@interface CrStagingKeyWatcher (TestingInterface) @interface CrStagingKeyWatcher (TestingInterface)
- (instancetype)initWithUserDefaults:(NSUserDefaults*)defaults; // The designated initializer. Allows a non-default NSUserDefaults to be
// specified.
- (instancetype)initWithUserDefaults:(NSUserDefaults*)defaults
pollingTime:(NSTimeInterval)pollingTime;
// KVO works for NSUserDefaults on 10.12 and later. This method turns off the
// use of KVO to allow the macOS 10.11 and earlier code path to be tested on
// 10.12 and later.
- (void)disableKVOForTesting; - (void)disableKVOForTesting;
// Returns whether the last call to -waitForStagingKeyToClear blocked or
// returned immediately.
- (BOOL)lastWaitWasBlockedForTesting; - (BOOL)lastWaitWasBlockedForTesting;
// Returns the NSUserDefaults key that is used to indicate staging. The value to
// be used is an array of strings, each string being a file path to the bundle.
+ (NSString*)stagingKeyForTesting; + (NSString*)stagingKeyForTesting;
@end @end
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
#include "base/mac/bundle_locations.h" #include "base/mac/bundle_locations.h"
#include "base/mac/mac_util.h" #include "base/mac/mac_util.h"
#include "base/mac/scoped_block.h"
#include "base/mac/scoped_nsobject.h" #include "base/mac/scoped_nsobject.h"
// Best documentation / Is unofficial documentation // Best documentation / Is unofficial documentation
...@@ -26,8 +27,11 @@ NSString* const kStagingKey = @"UpdatePending"; ...@@ -26,8 +27,11 @@ NSString* const kStagingKey = @"UpdatePending";
@interface CrStagingKeyWatcher () { @interface CrStagingKeyWatcher () {
base::scoped_nsobject<NSUserDefaults> defaults_; base::scoped_nsobject<NSUserDefaults> defaults_;
NSTimeInterval pollingTime_;
base::scoped_nsobject<NSTimer> pollingTimer_;
BOOL observing_; BOOL observing_;
base::scoped_nsobject<NSPort> wakePort_; base::mac::ScopedBlock<StagingKeyChangedObserver> callback_;
BOOL lastStagingKeyValue_;
BOOL kvoDisabledForTesting_; BOOL kvoDisabledForTesting_;
BOOL lastWaitWasBlockedForTesting_; BOOL lastWaitWasBlockedForTesting_;
...@@ -37,14 +41,23 @@ NSString* const kStagingKey = @"UpdatePending"; ...@@ -37,14 +41,23 @@ NSString* const kStagingKey = @"UpdatePending";
@implementation CrStagingKeyWatcher @implementation CrStagingKeyWatcher
- (instancetype)init { - (instancetype)initWithPollingTime:(NSTimeInterval)pollingTime {
return [self initWithUserDefaults:[NSUserDefaults standardUserDefaults]]; return [self initWithUserDefaults:[NSUserDefaults standardUserDefaults]
pollingTime:pollingTime];
} }
- (instancetype)initWithUserDefaults:(NSUserDefaults*)defaults { - (instancetype)initWithUserDefaults:(NSUserDefaults*)defaults
pollingTime:(NSTimeInterval)pollingTime {
if ((self = [super init])) { if ((self = [super init])) {
pollingTime_ = pollingTime;
defaults_.reset(defaults, base::scoped_policy::RETAIN); defaults_.reset(defaults, base::scoped_policy::RETAIN);
lastStagingKeyValue_ = [self isStagingKeySet];
if (base::mac::IsAtLeastOS10_12() && !kvoDisabledForTesting_) { if (base::mac::IsAtLeastOS10_12() && !kvoDisabledForTesting_) {
// 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 [defaults_ addObserver:self
forKeyPath:kStagingKey forKeyPath:kStagingKey
options:0 options:0
...@@ -55,7 +68,7 @@ NSString* const kStagingKey = @"UpdatePending"; ...@@ -55,7 +68,7 @@ NSString* const kStagingKey = @"UpdatePending";
return self; return self;
} }
- (BOOL)shouldWait { - (BOOL)isStagingKeySet {
NSArray<NSString*>* paths = [defaults_ stringArrayForKey:kStagingKey]; NSArray<NSString*>* paths = [defaults_ stringArrayForKey:kStagingKey];
if (!paths) if (!paths)
return NO; return NO;
...@@ -65,28 +78,29 @@ NSString* const kStagingKey = @"UpdatePending"; ...@@ -65,28 +78,29 @@ NSString* const kStagingKey = @"UpdatePending";
return [paths containsObject:appPath]; return [paths containsObject:appPath];
} }
- (void)waitForStagingKey { - (void)waitForStagingKeyToClear {
if (![self shouldWait]) { if (![self isStagingKeySet]) {
lastWaitWasBlockedForTesting_ = NO; lastWaitWasBlockedForTesting_ = NO;
return; return;
} }
NSRunLoop* runloop = [NSRunLoop currentRunLoop]; NSRunLoop* runloop = [NSRunLoop currentRunLoop];
if (observing_) { if (observing_) {
wakePort_.reset([NSPort port], base::scoped_policy::RETAIN); callback_.reset(
[runloop addPort:wakePort_ forMode:NSDefaultRunLoopMode]; ^(BOOL stagingKeySet) {
CFRunLoopStop([runloop getCFRunLoop]);
while ([self shouldWait] && [runloop runMode:NSDefaultRunLoopMode },
beforeDate:[NSDate distantFuture]]) { base::scoped_policy::RETAIN);
while ([self isStagingKeySet] && [runloop runMode:NSDefaultRunLoopMode
beforeDate:[NSDate distantFuture]]) {
/* run! */ /* run! */
} }
} else { } else {
const NSTimeInterval kPollingTime = 0.5; while ([self isStagingKeySet] &&
while ([self shouldWait] &&
[runloop [runloop
runMode:NSDefaultRunLoopMode runMode:NSDefaultRunLoopMode
beforeDate:[NSDate dateWithTimeIntervalSinceNow:kPollingTime]]) { beforeDate:[NSDate dateWithTimeIntervalSinceNow:pollingTime_]]) {
/* run! */ /* run! */
} }
} }
...@@ -94,11 +108,31 @@ NSString* const kStagingKey = @"UpdatePending"; ...@@ -94,11 +108,31 @@ NSString* const kStagingKey = @"UpdatePending";
lastWaitWasBlockedForTesting_ = YES; 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 { - (void)dealloc {
if (observing_) if (observing_)
[defaults_ removeObserver:self forKeyPath:kStagingKey context:nullptr]; [defaults_ removeObserver:self forKeyPath:kStagingKey context:nullptr];
if (wakePort_) if (pollingTimer_)
[wakePort_ invalidate]; [pollingTimer_ invalidate];
[super dealloc]; [super dealloc];
} }
...@@ -119,7 +153,12 @@ NSString* const kStagingKey = @"UpdatePending"; ...@@ -119,7 +153,12 @@ NSString* const kStagingKey = @"UpdatePending";
ofObject:(id)object ofObject:(id)object
change:(NSDictionary*)change change:(NSDictionary*)change
context:(void*)context { context:(void*)context {
[wakePort_ sendBeforeDate:[NSDate date] components:nil from:nil reserved:0]; BOOL isStagingKeySet = [self isStagingKeySet];
if (isStagingKeySet == lastStagingKeyValue_)
return;
lastStagingKeyValue_ = isStagingKeySet;
callback_.get()([self isStagingKeySet]);
} }
@end @end
...@@ -34,13 +34,14 @@ class StagingKeyWatcherTest : public testing::TestWithParam<KVOOrNot> { ...@@ -34,13 +34,14 @@ class StagingKeyWatcherTest : public testing::TestWithParam<KVOOrNot> {
[defaults_ removeObjectForKey:[CrStagingKeyWatcher stagingKeyForTesting]]; [defaults_ removeObjectForKey:[CrStagingKeyWatcher stagingKeyForTesting]];
} }
CrStagingKeyWatcher* CreateKeyWatcher() { base::scoped_nsobject<CrStagingKeyWatcher> CreateKeyWatcher() {
keyWatcher_.reset( base::scoped_nsobject<CrStagingKeyWatcher> keyWatcher(
[[CrStagingKeyWatcher alloc] initWithUserDefaults:defaults_]); [[CrStagingKeyWatcher alloc] initWithUserDefaults:defaults_
pollingTime:0.5]);
if (GetParam() == KVOOrNot::kDontUseKVO) if (GetParam() == KVOOrNot::kDontUseKVO)
[keyWatcher_ disableKVOForTesting]; [keyWatcher disableKVOForTesting];
return keyWatcher_; return keyWatcher;
} }
void SetDefaultsValue(id value) { void SetDefaultsValue(id value) {
...@@ -56,8 +57,18 @@ class StagingKeyWatcherTest : public testing::TestWithParam<KVOOrNot> { ...@@ -56,8 +57,18 @@ class StagingKeyWatcherTest : public testing::TestWithParam<KVOOrNot> {
]]; ]];
} }
void SetDefaultsValueInSeparateProcess() {
NSString* appPath = [base::mac::OuterBundle() bundlePath];
[NSTask launchedTaskWithLaunchPath:@"/usr/bin/defaults"
arguments:@[
@"write", testingBundleID_.get(),
[CrStagingKeyWatcher stagingKeyForTesting],
@"-array", appPath
]];
}
private: private:
base::scoped_nsobject<CrStagingKeyWatcher> keyWatcher_;
base::scoped_nsobject<NSString> testingBundleID_; base::scoped_nsobject<NSString> testingBundleID_;
base::scoped_nsobject<NSUserDefaults> defaults_; base::scoped_nsobject<NSUserDefaults> defaults_;
}; };
...@@ -70,32 +81,32 @@ INSTANTIATE_TEST_SUITE_P(KVOandNot, ...@@ -70,32 +81,32 @@ INSTANTIATE_TEST_SUITE_P(KVOandNot,
} // namespace } // namespace
TEST_P(StagingKeyWatcherTest, NoBlockingWhenNoKey) { TEST_P(StagingKeyWatcherTest, NoBlockingWhenNoKey) {
CrStagingKeyWatcher* watcher = CreateKeyWatcher(); base::scoped_nsobject<CrStagingKeyWatcher> watcher = CreateKeyWatcher();
[watcher waitForStagingKey]; [watcher waitForStagingKeyToClear];
ASSERT_FALSE([watcher lastWaitWasBlockedForTesting]); ASSERT_FALSE([watcher lastWaitWasBlockedForTesting]);
} }
TEST_P(StagingKeyWatcherTest, NoBlockingWhenWrongKeyType) { TEST_P(StagingKeyWatcherTest, NoBlockingWhenWrongKeyType) {
SetDefaultsValue(@"this is not an string array"); SetDefaultsValue(@"this is not an string array");
CrStagingKeyWatcher* watcher = CreateKeyWatcher(); base::scoped_nsobject<CrStagingKeyWatcher> watcher = CreateKeyWatcher();
[watcher waitForStagingKey]; [watcher waitForStagingKeyToClear];
ASSERT_FALSE([watcher lastWaitWasBlockedForTesting]); ASSERT_FALSE([watcher lastWaitWasBlockedForTesting]);
} }
TEST_P(StagingKeyWatcherTest, NoBlockingWhenWrongArrayType) { TEST_P(StagingKeyWatcherTest, NoBlockingWhenWrongArrayType) {
SetDefaultsValue(@[ @3, @1, @4, @1, @5 ]); SetDefaultsValue(@[ @3, @1, @4, @1, @5 ]);
CrStagingKeyWatcher* watcher = CreateKeyWatcher(); base::scoped_nsobject<CrStagingKeyWatcher> watcher = CreateKeyWatcher();
[watcher waitForStagingKey]; [watcher waitForStagingKeyToClear];
ASSERT_FALSE([watcher lastWaitWasBlockedForTesting]); ASSERT_FALSE([watcher lastWaitWasBlockedForTesting]);
} }
TEST_P(StagingKeyWatcherTest, NoBlockingWhenEmptyArray) { TEST_P(StagingKeyWatcherTest, NoBlockingWhenEmptyArray) {
SetDefaultsValue(@[]); SetDefaultsValue(@[]);
CrStagingKeyWatcher* watcher = CreateKeyWatcher(); base::scoped_nsobject<CrStagingKeyWatcher> watcher = CreateKeyWatcher();
[watcher waitForStagingKey]; [watcher waitForStagingKeyToClear];
ASSERT_FALSE([watcher lastWaitWasBlockedForTesting]); ASSERT_FALSE([watcher lastWaitWasBlockedForTesting]);
} }
...@@ -111,7 +122,44 @@ TEST_P(StagingKeyWatcherTest, BlockFunctionality) { ...@@ -111,7 +122,44 @@ TEST_P(StagingKeyWatcherTest, BlockFunctionality) {
ClearDefaultsValueInSeparateProcess(); ClearDefaultsValueInSeparateProcess();
}); });
CrStagingKeyWatcher* watcher = CreateKeyWatcher(); base::scoped_nsobject<CrStagingKeyWatcher> watcher = CreateKeyWatcher();
[watcher waitForStagingKey]; [watcher waitForStagingKeyToClear];
ASSERT_TRUE([watcher lastWaitWasBlockedForTesting]); ASSERT_TRUE([watcher lastWaitWasBlockedForTesting]);
} }
TEST_P(StagingKeyWatcherTest, CallbackOnKeySet) {
// The staging key begins not set.
base::scoped_nsobject<CrStagingKeyWatcher> watcher = CreateKeyWatcher();
NSRunLoop* runloop = [NSRunLoop currentRunLoop];
[watcher setStagingKeyChangedObserver:^(BOOL stagingKeySet) {
CFRunLoopStop([runloop getCFRunLoop]);
}];
SetDefaultsValueInSeparateProcess();
while (![watcher isStagingKeySet] &&
[runloop runMode:NSDefaultRunLoopMode
beforeDate:[NSDate distantFuture]]) {
/* run! */
}
}
TEST_P(StagingKeyWatcherTest, CallbackOnKeyUnset) {
NSString* appPath = [base::mac::OuterBundle() bundlePath];
SetDefaultsValue(@[ appPath ]);
base::scoped_nsobject<CrStagingKeyWatcher> watcher = CreateKeyWatcher();
NSRunLoop* runloop = [NSRunLoop currentRunLoop];
[watcher setStagingKeyChangedObserver:^(BOOL stagingKeySet) {
CFRunLoopStop([runloop getCFRunLoop]);
}];
ClearDefaultsValueInSeparateProcess();
while ([watcher isStagingKeySet] &&
[runloop runMode:NSDefaultRunLoopMode
beforeDate:[NSDate distantFuture]]) {
/* run! */
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment