2fas-ios/TwoFAS/Storage/Log/LogHandler.swift
2024-03-26 18:29:49 +01:00

168 lines
5.2 KiB
Swift

//
// This file is part of the 2FAS iOS app (https://github.com/twofas/2fas-ios)
// Copyright © 2023 Two Factor Authentication Service, Inc.
// Contributed by Zbigniew Cisiński. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>
//
import Foundation
import CoreData
#if os(iOS)
import Common
#elseif os(watchOS)
import CommonWatch
#endif
public final class LogHandler: LogStorageHandling {
private struct CachedEntry {
let content: String
let timestamp: Date
let module: Int
let severity: Int
}
private let maxEntries: Int = 10000
private let checkEvery: Int = 300
private let saveEvery: Int = 10
private var checkCounter: Int = 0
private var saveCounter: Int = 0
private var zoneSaveCounter: Int = 0
private var inZone = false
private let context: NSManagedObjectContext
private let queue = DispatchQueue(
label: "com.2fas.logHandlerQueue",
attributes: .concurrent
)
init(coreDataStack: CoreDataStack) {
context = coreDataStack.createBackgroundContext()
context.automaticallyMergesChangesFromParent = true
#if os(iOS)
NotificationCenter.default.addObserver(
self,
selector: #selector(save),
name: UIApplication.willResignActiveNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(save),
name: UIApplication.willTerminateNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(save),
name: UIApplication.didEnterBackgroundNotification,
object: nil
)
#endif
}
public func markZoneStart() {
inZone = true
}
public func markZoneEnd() {
inZone = false
saveCounter += zoneSaveCounter
zoneSaveCounter = 0
checkSave()
}
public func store(content: String, timestamp: Date, module: Int, severity: Int) {
queue.async(flags: .barrier) { [weak self] in
guard let self else { return }
self.context.performAndWait {
LogEntryEntity.create(
on: self.context,
content: content,
timestamp: timestamp,
module: module,
severity: severity
)
}
self.checkCounter += 1
if self.inZone {
self.zoneSaveCounter += 1
} else {
self.saveCounter += 1
self.checkSave()
self.checkCleanup()
}
}
}
public func listAll() -> [LogEntry] {
queue.sync {
LogEntryEntity.listAll(on: context, ascending: true)
.map { entity in
LogEntry(
content: entity.content,
timestamp: entity.timestamp,
module: LogModule(rawValue: Int(entity.module)) ?? .unknown,
severity: LogSeverity(rawValue: Int(entity.severity)) ?? .unknown
)
}
}
}
private func checkSave() {
guard saveCounter >= saveEvery else { return }
save()
}
@objc(save)
private func save() {
queue.async(flags: .barrier) { [weak self] in
self?.saveCounter = 0
self?.zoneSaveCounter = 0
if self?.context.hasChanges == true {
self?.context.performAndWait { [weak self] in
do {
try self?.context.save()
} catch {
Log("Error while saving context in LogStorage: \(error)")
}
}
}
}
}
private func checkCleanup() {
guard checkCounter >= checkEvery else { return }
checkCounter = 0
queue.async(flags: .barrier) { [weak self] in
self?.context.performAndWait { [weak self] in
guard let self else { return }
let context = self.context
guard LogEntryEntity.count(on: context) > self.maxEntries else { return }
let all = LogEntryEntity.listAll(on: context, quickFetch: true, ascending: false)
let forRemoval = Array(all[self.maxEntries...])
LogEntryEntity.remove(on: context, objects: forRemoval)
}
}
}
}