Skip to content

Commit

Permalink
ref: Remove a possible race condition related to reading previous app…
Browse files Browse the repository at this point in the history
… state (#2250)
  • Loading branch information
kevinrenskers authored Oct 5, 2022
1 parent db5f62a commit fbeb49a
Show file tree
Hide file tree
Showing 11 changed files with 104 additions and 23 deletions.
7 changes: 6 additions & 1 deletion Sources/Sentry/SentryAppStateManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ - (SentryAppState *)buildCurrentAppState
systemBootTimestamp:self.sysctl.systemBootTimestamp];
}

- (SentryAppState *)loadPreviousAppState
{
return [self.fileManager readPreviousAppState];
}

- (SentryAppState *)loadCurrentAppState
{
return [self.fileManager readAppState];
Expand All @@ -67,7 +72,7 @@ - (void)storeCurrentAppState
[self.fileManager storeAppState:[self buildCurrentAppState]];
}

- (void)removeCurrentAppState
- (void)deleteAppState
{
[self.fileManager deleteAppState];
}
Expand Down
68 changes: 55 additions & 13 deletions Sources/Sentry/SentryFileManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
@property (nonatomic, copy) NSString *currentSessionFilePath;
@property (nonatomic, copy) NSString *crashedSessionFilePath;
@property (nonatomic, copy) NSString *lastInForegroundFilePath;
@property (nonatomic, copy) NSString *previousAppStateFilePath;
@property (nonatomic, copy) NSString *appStateFilePath;
@property (nonatomic, copy) NSString *timezoneOffsetFilePath;
@property (nonatomic, assign) NSUInteger currentFileCounter;
Expand Down Expand Up @@ -65,6 +66,8 @@ - (nullable instancetype)initWithOptions:(SentryOptions *)options
self.lastInForegroundFilePath =
[self.sentryPath stringByAppendingPathComponent:@"lastInForeground.timestamp"];

self.previousAppStateFilePath =
[self.sentryPath stringByAppendingPathComponent:@"previous.app.state"];
self.appStateFilePath = [self.sentryPath stringByAppendingPathComponent:@"app.state"];
self.timezoneOffsetFilePath =
[self.sentryPath stringByAppendingPathComponent:@"timezone.offset"];
Expand Down Expand Up @@ -395,32 +398,71 @@ - (void)storeAppState:(SentryAppState *)appState
}
}

- (void)moveAppStateToPreviousAppState
{
@synchronized(self.appStateFilePath) {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error = nil;
[fileManager moveItemAtPath:self.appStateFilePath
toPath:self.previousAppStateFilePath
error:&error];

// We don't want to log an error if the file doesn't exist.
if (nil != error && error.code != NSFileNoSuchFileError) {
[SentryLog
logWithMessage:[NSString
stringWithFormat:
@"Failed to move app state to previous app state: %@", error]
andLevel:kSentryLevelError];
}
}
}

- (SentryAppState *_Nullable)readAppState
{
@synchronized(self.appStateFilePath) {
return [self readAppStateFrom:self.appStateFilePath];
}
}

- (SentryAppState *_Nullable)readPreviousAppState
{
@synchronized(self.previousAppStateFilePath) {
return [self readAppStateFrom:self.previousAppStateFilePath];
}
}

- (SentryAppState *_Nullable)readAppStateFrom:(NSString *)path
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSData *currentData = nil;
@synchronized(self.appStateFilePath) {
currentData = [fileManager contentsAtPath:self.appStateFilePath];
if (nil == currentData) {
return nil;
}
currentData = [fileManager contentsAtPath:path];
if (nil == currentData) {
return nil;
}
return [SentrySerialization appStateWithData:currentData];
}

- (void)deleteAppState
{
@synchronized(self.appStateFilePath) {
[self deleteAppStateFrom:self.appStateFilePath];
[self deleteAppStateFrom:self.previousAppStateFilePath];
}
}

- (void)deleteAppStateFrom:(NSString *)path
{
NSError *error = nil;
NSFileManager *fileManager = [NSFileManager defaultManager];
@synchronized(self.appStateFilePath) {
[fileManager removeItemAtPath:self.appStateFilePath error:&error];
[fileManager removeItemAtPath:path error:&error];

// We don't want to log an error if the file doesn't exist.
if (nil != error && error.code != NSFileNoSuchFileError) {
[SentryLog
logWithMessage:[NSString stringWithFormat:@"Failed to delete app state %@", error]
andLevel:kSentryLevelError];
}
// We don't want to log an error if the file doesn't exist.
if (nil != error && error.code != NSFileNoSuchFileError) {
[SentryLog
logWithMessage:[NSString stringWithFormat:@"Failed to delete app state from %@: %@",
path, error]
andLevel:kSentryLevelError];
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/Sentry/SentryOutOfMemoryLogic.m
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ - (BOOL)isOOM
}

#if SENTRY_HAS_UIKIT
SentryAppState *previousAppState = [self.appStateManager loadCurrentAppState];
SentryAppState *previousAppState = [self.appStateManager loadPreviousAppState];
SentryAppState *currentAppState = [self.appStateManager buildCurrentAppState];

// If there is no previous app state, we can't do anything.
Expand Down
2 changes: 1 addition & 1 deletion Sources/Sentry/SentryOutOfMemoryTracker.m
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ - (void)stop
removeObserver:self
name:SentryNSNotificationCenterWrapper.willTerminateNotificationName
object:nil];
[self.appStateManager removeCurrentAppState];
[self.appStateManager deleteAppState];
#endif
}

Expand Down
3 changes: 3 additions & 0 deletions Sources/Sentry/SentrySDK.m
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,10 @@ + (void)startWithOptionsObject:(SentryOptions *)options
startInvocations++;

[SentryLog configure:options.debug diagnosticLevel:options.diagnosticLevel];

SentryClient *newClient = [[SentryClient alloc] initWithOptions:options];
[newClient.fileManager moveAppStateToPreviousAppState];

// The Hub needs to be initialized with a client so that closing a session
// can happen.
[SentrySDK setCurrentHub:[[SentryHub alloc] initWithClient:newClient andScope:nil]];
Expand Down
4 changes: 3 additions & 1 deletion Sources/Sentry/include/SentryAppStateManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ SENTRY_NO_INIT
*/
- (SentryAppState *)buildCurrentAppState;

- (SentryAppState *)loadPreviousAppState;

- (SentryAppState *)loadCurrentAppState;

- (void)storeCurrentAppState;

- (void)removeCurrentAppState;
- (void)deleteAppState;

- (void)updateAppState:(void (^)(SentryAppState *))block;

Expand Down
2 changes: 2 additions & 0 deletions Sources/Sentry/include/SentryFileManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ SENTRY_NO_INIT
- (NSString *)storeDictionary:(NSDictionary *)dictionary toPath:(NSString *)path;

- (void)storeAppState:(SentryAppState *)appState;
- (void)moveAppStateToPreviousAppState;
- (SentryAppState *_Nullable)readAppState;
- (SentryAppState *_Nullable)readPreviousAppState;
- (void)deleteAppState;

- (NSNumber *_Nullable)readTimezoneOffset;
Expand Down
18 changes: 17 additions & 1 deletion Tests/SentryTests/Helper/SentryFileManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,15 @@ class SentryFileManagerTests: XCTestCase {
sut.deleteAppState()
XCTAssertNil(sut.readAppState())
}

func testDeletePreviousAppState() {
sut.store(TestData.appState)
sut.moveAppStateToPreviousAppState()
sut.deleteAppState()

XCTAssertNil(sut.readAppState())
XCTAssertNil(sut.readPreviousAppState())
}

func testStore_WhenFileImmutable_AppStateIsNotOverwritten() {
sut.store(TestData.appState)
Expand Down Expand Up @@ -471,6 +480,14 @@ class SentryFileManagerTests: XCTestCase {
XCTAssertNotNil(sut.readAppState())
}

func testMoveAppStateAndReadPreviousAppState() {
sut.store(TestData.appState)
sut.moveAppStateToPreviousAppState()

let actual = sut.readPreviousAppState()
XCTAssertEqual(TestData.appState, actual)
}

func testStoreAndReadTimezoneOffset() {
sut.storeTimezoneOffset(7_200)
XCTAssertEqual(sut.readTimezoneOffset(), 7_200)
Expand Down Expand Up @@ -600,7 +617,6 @@ class SentryFileManagerTests: XCTestCase {
private func assertValidAppStateStored() {
let actual = sut.readAppState()
XCTAssertEqual(TestData.appState, actual)

}

private func advanceTime(bySeconds: TimeInterval) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ class SentryOutOfMemoryTrackerTests: NotificationCenterTestCase {
sut.start()
goToForeground()

fixture.fileManager.moveAppStateToPreviousAppState()
sut.start()
assertOOMEventSent()
}
Expand All @@ -208,7 +209,8 @@ class SentryOutOfMemoryTrackerTests: NotificationCenterTestCase {
func testAppOOM_WithOnlyHybridSdkDidBecomeActive() {
sut.start()
hybridSdkDidBecomeActive()


fixture.fileManager.moveAppStateToPreviousAppState()
sut.start()
assertOOMEventSent()
}
Expand All @@ -217,7 +219,8 @@ class SentryOutOfMemoryTrackerTests: NotificationCenterTestCase {
sut.start()
goToForeground()
hybridSdkDidBecomeActive()


fixture.fileManager.moveAppStateToPreviousAppState()
sut.start()
assertOOMEventSent()
}
Expand All @@ -226,7 +229,8 @@ class SentryOutOfMemoryTrackerTests: NotificationCenterTestCase {
sut.start()
hybridSdkDidBecomeActive()
goToForeground()


fixture.fileManager.moveAppStateToPreviousAppState()
sut.start()
assertOOMEventSent()
}
Expand All @@ -253,15 +257,15 @@ class SentryOutOfMemoryTrackerTests: NotificationCenterTestCase {
func testStop_StopsObserving_NoMoreFileManagerInvocations() throws {
let fileManager = try TestFileManager(options: Options(), andCurrentDateProvider: TestCurrentDateProvider())
sut = fixture.getSut(fileManager: fileManager)

sut.start()
sut.stop()

hybridSdkDidBecomeActive()
goToForeground()
terminateApp()

XCTAssertEqual(1, fileManager.readAppStateInvocations.count)
XCTAssertEqual(1, fileManager.readPreviousAppStateInvocations.count)
}

private func givenPreviousAppState(appState: SentryAppState) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ class SentryCrashIntegrationTests: NotificationCenterTestCase {
let appState = SentryAppState(releaseName: TestData.appState.releaseName, osVersion: UIDevice.current.systemVersion, vendorId: UIDevice.current.identifierForVendor?.uuidString ?? "", isDebugging: false, systemBootTimestamp: fixture.currentDateProvider.date())
appState.isActive = true
fixture.fileManager.store(appState)
fixture.fileManager.moveAppStateToPreviousAppState()
}
#endif

Expand Down
6 changes: 6 additions & 0 deletions Tests/SentryTests/TestClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,10 @@ class TestFileManager: SentryFileManager {
readAppStateInvocations.record(Void())
return nil
}

var readPreviousAppStateInvocations = Invocations<Void>()
override func readPreviousAppState() -> SentryAppState? {
readPreviousAppStateInvocations.record(Void())
return nil
}
}

0 comments on commit fbeb49a

Please sign in to comment.