Skip to content

Commit dac0f63

Browse files
sharvilshahmlw
andauthored
sync: support batched mode for sync state and handle clean sync (#944)
Support batched mode for sync state and handle clean sync Fixes SNT-357 --------- Co-authored-by: Matt W <436037+mlw@users.noreply.github.com>
1 parent 4b3ae39 commit dac0f63

11 files changed

Lines changed: 627 additions & 136 deletions

Source/common/SNTConfigBundle.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,11 @@
5656
- (void)fullSyncInterval:(void (^)(NSUInteger))block;
5757
- (void)pushNotificationsFullSyncInterval:(void (^)(NSUInteger))block;
5858

59+
///
60+
/// When set, signals the daemon to clear persisted sync state before
61+
/// applying the rest of the bundle. Set only by PostflightConfigBundle
62+
/// when the in-flight sync was Clean or CleanAll.
63+
///
64+
- (void)clearSyncStateBeforeApply:(void (^)(BOOL))block;
65+
5966
@end

Source/common/SNTConfigBundle.mm

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ @interface SNTConfigBundle ()
5252
@property NSArray<SNTCELFallbackRule*>* celFallbackRules;
5353
@property NSNumber* fullSyncInterval;
5454
@property NSNumber* pushNotificationsFullSyncInterval;
55+
@property NSNumber* clearSyncStateBeforeApply;
5556
@end
5657

5758
@implementation SNTConfigBundle
@@ -92,6 +93,7 @@ - (void)encodeWithCoder:(NSCoder*)coder {
9293
ENCODE(coder, celFallbackRules);
9394
ENCODE(coder, fullSyncInterval);
9495
ENCODE(coder, pushNotificationsFullSyncInterval);
96+
ENCODE(coder, clearSyncStateBeforeApply);
9597
}
9698

9799
- (instancetype)initWithCoder:(NSCoder*)decoder {
@@ -128,6 +130,7 @@ - (instancetype)initWithCoder:(NSCoder*)decoder {
128130
DECODE_ARRAY(decoder, celFallbackRules, SNTCELFallbackRule);
129131
DECODE(decoder, fullSyncInterval, NSNumber);
130132
DECODE(decoder, pushNotificationsFullSyncInterval, NSNumber);
133+
DECODE(decoder, clearSyncStateBeforeApply, NSNumber);
131134
}
132135
return self;
133136
}
@@ -317,4 +320,10 @@ - (void)pushNotificationsFullSyncInterval:(void (^)(NSUInteger))block {
317320
}
318321
}
319322

323+
- (void)clearSyncStateBeforeApply:(void (^)(BOOL))block {
324+
if (self.clearSyncStateBeforeApply) {
325+
block([self.clearSyncStateBeforeApply boolValue]);
326+
}
327+
}
328+
320329
@end

Source/common/SNTConfigBundleTest.mm

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ @interface SNTConfigBundle (Testing)
5353
@property NSArray<SNTCELFallbackRule*>* celFallbackRules;
5454
@property NSNumber* fullSyncInterval;
5555
@property NSNumber* pushNotificationsFullSyncInterval;
56+
@property NSNumber* clearSyncStateBeforeApply;
5657
@end
5758

5859
@interface SNTConfigBundleTest : XCTestCase
@@ -372,4 +373,46 @@ - (void)testGettersWithoutValues {
372373
}];
373374
}
374375

376+
- (void)testClearSyncStateBeforeApplyRoundTrips {
377+
// When set to YES, the round-trip preserves the value and the accessor block fires with YES.
378+
SNTConfigBundle* set = [[SNTConfigBundle alloc] init];
379+
set.clearSyncStateBeforeApply = @(YES);
380+
381+
NSError* error = nil;
382+
NSData* setData = [NSKeyedArchiver archivedDataWithRootObject:set
383+
requiringSecureCoding:YES
384+
error:&error];
385+
XCTAssertNil(error);
386+
SNTConfigBundle* setDecoded = [NSKeyedUnarchiver unarchivedObjectOfClass:[SNTConfigBundle class]
387+
fromData:setData
388+
error:&error];
389+
XCTAssertNil(error);
390+
391+
__block BOOL setFired = NO;
392+
__block BOOL setValue = NO;
393+
[setDecoded clearSyncStateBeforeApply:^(BOOL v) {
394+
setFired = YES;
395+
setValue = v;
396+
}];
397+
XCTAssertTrue(setFired);
398+
XCTAssertEqual(setValue, YES);
399+
400+
// When left unset, the round-trip preserves the unset state and the accessor block does not fire.
401+
SNTConfigBundle* unset = [[SNTConfigBundle alloc] init];
402+
NSData* unsetData = [NSKeyedArchiver archivedDataWithRootObject:unset
403+
requiringSecureCoding:YES
404+
error:&error];
405+
XCTAssertNil(error);
406+
SNTConfigBundle* unsetDecoded = [NSKeyedUnarchiver unarchivedObjectOfClass:[SNTConfigBundle class]
407+
fromData:unsetData
408+
error:&error];
409+
XCTAssertNil(error);
410+
411+
__block BOOL unsetFired = NO;
412+
[unsetDecoded clearSyncStateBeforeApply:^(BOOL v) {
413+
unsetFired = YES;
414+
}];
415+
XCTAssertFalse(unsetFired);
416+
}
417+
375418
@end

Source/common/SNTConfigurator.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1152,6 +1152,24 @@ extern NSString* _Nonnull const kEnableMenuItemUserOverride;
11521152
///
11531153
- (void)clearSyncState;
11541154

1155+
///
1156+
/// Buffer a series of sync-state mutations into a single atomic commit.
1157+
///
1158+
/// Inside the block, calls to `updateSyncStateForKey:value:` and
1159+
/// `clearSyncState` write only to an in-memory working dictionary —
1160+
/// they do not fire KVO and do not write to disk. When the block
1161+
/// returns, the working dictionary is assigned to `syncState` (single
1162+
/// KVO fire) and saved to disk (single `writeToFile:atomically:YES`).
1163+
///
1164+
/// Returns `YES` when both the in-memory commit and the disk write
1165+
/// succeed. Returns `NO` when nested inside another batch (asserts in
1166+
/// debug, logs and skips in release) or when the disk write fails. The
1167+
/// block is not expected to raise exceptions; if one escapes it will
1168+
/// crash the daemon, by design. Callers should gate any post-commit
1169+
/// work that depends on durable state on the return value.
1170+
///
1171+
- (BOOL)performSyncStateBatch:(nonnull void(NS_NOESCAPE ^)(void))block;
1172+
11551173
///
11561174
/// Validate the configuration profile.
11571175
///

Source/common/SNTConfigurator.mm

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ @interface SNTConfigurator ()
8282
@property(atomic) NSMutableDictionary* configState;
8383
@property(atomic) NSDictionary* state;
8484

85+
/// Working dictionary for an in-progress `performSyncStateBatch:` transaction.
86+
/// Non-nil only while inside the batch's block. Always accessed on the main
87+
/// thread, matching the convention enforced by `updateSyncStateForKey:value:`.
88+
@property(nonatomic) NSMutableDictionary* batchedSyncState;
89+
8590
@property(readonly, nonatomic) NSString* syncStateFilePath;
8691
@property(readonly, nonatomic) NSString* stateFilePath;
8792

@@ -1960,6 +1965,10 @@ - (NSDictionary*)extraMetricLabels {
19601965
///
19611966
- (void)updateSyncStateForKey:(NSString*)key value:(id)value {
19621967
void (^block)(void) = ^{
1968+
if (self.batchedSyncState) {
1969+
self.batchedSyncState[key] = value;
1970+
return;
1971+
}
19631972
NSMutableDictionary* syncState = self.syncState.mutableCopy;
19641973
syncState[key] = value;
19651974
self.syncState = syncState;
@@ -1974,6 +1983,28 @@ - (void)updateSyncStateForKey:(NSString*)key value:(id)value {
19741983
}
19751984
}
19761985

1986+
- (BOOL)performSyncStateBatch:(void(NS_NOESCAPE ^)(void))block {
1987+
__block BOOL committed = NO;
1988+
void (^run)(void) = ^{
1989+
if (self.batchedSyncState != nil) {
1990+
NSAssert(NO, @"Sync-state batches do not nest");
1991+
LOGE(@"Sync-state batches do not nest; skipping nested batch");
1992+
return;
1993+
}
1994+
self.batchedSyncState = self.syncState.mutableCopy;
1995+
block();
1996+
self.syncState = self.batchedSyncState;
1997+
self.batchedSyncState = nil;
1998+
committed = [self saveSyncStateToDisk];
1999+
};
2000+
if ([NSThread isMainThread]) {
2001+
run();
2002+
} else {
2003+
dispatch_sync(dispatch_get_main_queue(), run);
2004+
}
2005+
return committed;
2006+
}
2007+
19772008
///
19782009
/// Read the saved syncState.
19792010
///
@@ -2043,27 +2074,47 @@ - (BOOL)migrateDeprecatedSyncStateKeys {
20432074
}
20442075

20452076
///
2046-
/// Saves the current effective syncState to disk.
2077+
/// Saves the current effective syncState to disk. Returns YES if the write
2078+
/// succeeded; NO if the authorizer denied the operation or the underlying
2079+
/// file write failed.
20472080
///
2048-
- (void)saveSyncStateToDisk {
2081+
- (BOOL)saveSyncStateToDisk {
20492082
if (!self.syncStateAccessAuthorizerBlock()) {
2050-
return;
2083+
return NO;
20512084
}
20522085

20532086
NSMutableDictionary* syncState = self.syncState.mutableCopy;
20542087
syncState[kAllowedPathRegexKey] = [syncState[kAllowedPathRegexKey] pattern];
20552088
syncState[kBlockedPathRegexKey] = [syncState[kBlockedPathRegexKey] pattern];
2056-
[syncState writeToFile:self.syncStateFilePath atomically:YES];
2089+
if (![syncState writeToFile:self.syncStateFilePath atomically:YES]) {
2090+
LOGE(@"Failed to write sync state to %@", self.syncStateFilePath);
2091+
return NO;
2092+
}
20572093
[[NSFileManager defaultManager] setAttributes:@{NSFilePosixPermissions : @0600}
20582094
ofItemAtPath:self.syncStateFilePath
20592095
error:NULL];
2096+
return YES;
20602097
}
20612098

20622099
- (void)clearSyncState {
2063-
self.syncState = [NSMutableDictionary dictionary];
2064-
// TODO: Start a timer to flush the state to disk. On startup, Santa should
2065-
// check for the presence of the state file and, if no SyncBaseURL is
2066-
// configured, start the timer to clear sync state and flush to disk.
2100+
// Intentionally not gated on `syncStateAccessAuthorizerBlock`: the authorizer
2101+
// requires `syncBaseURL != nil`, but the SNTSyncdQueue caller invokes this
2102+
// method precisely *because* `syncBaseURL` went to nil. Gating here would
2103+
// make the cleanup unreachable in production. Disk removal still requires
2104+
// root, which santad already has.
2105+
void (^block)(void) = ^{
2106+
if (self.batchedSyncState) {
2107+
[self.batchedSyncState removeAllObjects];
2108+
return;
2109+
}
2110+
self.syncState = [NSMutableDictionary dictionary];
2111+
[[NSFileManager defaultManager] removeItemAtPath:self.syncStateFilePath error:NULL];
2112+
};
2113+
if ([NSThread isMainThread]) {
2114+
block();
2115+
} else {
2116+
dispatch_sync(dispatch_get_main_queue(), block);
2117+
}
20672118
}
20682119

20692120
- (NSArray*)entitlementsPrefixFilter {

0 commit comments

Comments
 (0)