Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 23 additions & 37 deletions Sources/TokiUsageReaders/HermesReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ public struct HermesReader: TokenReader {
}

public func readUsage(from startDate: Date, to endDate: Date) async throws -> RawTokenUsage {
if let database = try openDatabase() {
defer { sqlite3_close(database) }
let observations = try readSessionObservations(from: database)
if let observations = try readDatabaseSnapshot({ database in
try readSessionObservations(from: database)
}) {
try await usageLedger.refresh(
observations: observations,
observedAt: now())
Expand All @@ -45,25 +45,29 @@ public struct HermesReader: TokenReader {
}

public func coverageStatus() throws -> HermesUsageCoverageStatus {
guard let database = try openDatabase() else {
return HermesUsageCoverageStatus(unmeteredMainAPICallCount: 0)
}
defer { sqlite3_close(database) }
return try readSessionModelUsage(from: database).coverage
try readDatabaseSnapshot { database in
try readSessionModelUsage(from: database).coverage
} ?? HermesUsageCoverageStatus(unmeteredMainAPICallCount: 0)
}

private func openDatabase() throws -> OpaquePointer? {
guard FileManager.default.fileExists(atPath: dbPath) else { return nil }

var database: OpaquePointer?
guard sqlite3_open_v2(dbPath, &database, SQLITE_OPEN_READONLY, nil) == SQLITE_OK else {
let error = HermesSQLiteError(operation: "open", database: database)
sqlite3_close(database)
throw error
private func readDatabaseSnapshot<Value>(
_ read: (OpaquePointer) throws -> Value) throws -> Value? {
for attempt in 0..<2 {
guard let connection = try HermesSQLiteConnection.open(atPath: dbPath) else {
return nil
}
let value = try read(connection.database)
if connection.isSourceStateCurrent {
return value
}
guard attempt == 0 else {
throw HermesSQLiteError(
operation: "read snapshot",
message: "database changed during read",
code: SQLITE_BUSY)
}
}

sqlite3_busy_timeout(database, 2000)
return database
return nil
}

private func readSessionObservations(from database: OpaquePointer) throws -> [HermesSessionObservation] {
Expand Down Expand Up @@ -430,24 +434,6 @@ private struct HermesSessionUsageRow {
}
}

private struct HermesSQLiteError: LocalizedError {
let operation: String
let message: String

init(operation: String, database: OpaquePointer?) {
self.operation = operation
if let database, let errorMessage = sqlite3_errmsg(database) {
message = String(cString: errorMessage)
} else {
message = "unknown SQLite error"
}
}

var errorDescription: String? {
"Hermes SQLite \(operation) failed: \(message)"
}
}

private let hermesSQLiteTransient = unsafeBitCast(-1, to: sqlite3_destructor_type.self)

private func hermesSQLiteText(_ statement: OpaquePointer?, at index: Int32) -> String {
Expand Down
198 changes: 198 additions & 0 deletions Sources/TokiUsageReaders/HermesSQLiteConnection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import Foundation

#if os(Linux)
import CSQLite
#else
import SQLite3
#endif

final class HermesSQLiteConnection {
let database: OpaquePointer

private let immutableSnapshot: HermesDatabaseSourceSnapshot?

private init(
database: OpaquePointer,
immutableSnapshot: HermesDatabaseSourceSnapshot?) {
self.database = database
self.immutableSnapshot = immutableSnapshot
}

deinit {
sqlite3_close(database)
}

var isSourceStateCurrent: Bool {
immutableSnapshot?.isCurrent() ?? true
}

var isUsingImmutableSnapshot: Bool {
immutableSnapshot != nil
}

static func open(
atPath path: String,
fileManager: FileManager = .default) throws -> HermesSQLiteConnection? {
guard fileManager.fileExists(atPath: path) else { return nil }

do {
return try openValidatedDatabase(
path: path,
flags: SQLITE_OPEN_READONLY,
immutableSnapshot: nil)
} catch let error as HermesSQLiteError {
let databaseURL = URL(fileURLWithPath: path)
guard error.code == SQLITE_CANTOPEN,
let snapshot = HermesDatabaseSourceSnapshot.captureForImmutableFallback(
databaseURL: databaseURL,
fileManager: fileManager) else {
Comment thread
choi138 marked this conversation as resolved.
Outdated
throw error
}

return try openValidatedDatabase(
path: immutableDatabaseURI(for: databaseURL),
flags: SQLITE_OPEN_READONLY | SQLITE_OPEN_URI,
immutableSnapshot: snapshot)
}
}

private static func openValidatedDatabase(
path: String,
flags: Int32,
immutableSnapshot: HermesDatabaseSourceSnapshot?) throws -> HermesSQLiteConnection {
var database: OpaquePointer?
let openStatus = sqlite3_open_v2(path, &database, flags, nil)
guard openStatus == SQLITE_OK, let database else {
let error = HermesSQLiteError(
operation: "open",
database: database,
code: openStatus)
sqlite3_close(database)
throw error
}

sqlite3_busy_timeout(database, 2000)
let probeStatus = probeDatabase(database)
guard probeStatus == SQLITE_OK else {
let error = HermesSQLiteError(
operation: "probe",
database: database,
code: probeStatus)
sqlite3_close(database)
throw error
}

return HermesSQLiteConnection(
database: database,
immutableSnapshot: immutableSnapshot)
}

private static func probeDatabase(_ database: OpaquePointer) -> Int32 {
var statement: OpaquePointer?
let prepareStatus = sqlite3_prepare_v2(
database,
"PRAGMA schema_version",
-1,
&statement,
nil)
guard prepareStatus == SQLITE_OK else { return prepareStatus }
defer { sqlite3_finalize(statement) }

let stepStatus = sqlite3_step(statement)
guard stepStatus == SQLITE_ROW || stepStatus == SQLITE_DONE else {
return stepStatus
}
return SQLITE_OK
}

private static func immutableDatabaseURI(for databaseURL: URL) -> String {
"\(databaseURL.absoluteString)?mode=ro&immutable=1"
}
}

struct HermesDatabaseSourceSnapshot: Equatable {
let databaseURL: URL
let databaseSignature: HermesDatabaseFileSignature

static func captureForImmutableFallback(
databaseURL: URL,
fileManager: FileManager = .default) -> HermesDatabaseSourceSnapshot? {
guard !hasSQLiteSidecars(databaseURL: databaseURL, fileManager: fileManager),
let databaseSignature = HermesDatabaseFileSignature.capture(
at: databaseURL,
fileManager: fileManager),
!hasSQLiteSidecars(databaseURL: databaseURL, fileManager: fileManager) else {
return nil
}
return HermesDatabaseSourceSnapshot(
databaseURL: databaseURL,
databaseSignature: databaseSignature)
}

func isCurrent(fileManager: FileManager = .default) -> Bool {
guard let currentSnapshot = Self.captureForImmutableFallback(
databaseURL: databaseURL,
fileManager: fileManager) else {
return false
}
return currentSnapshot == self
}

private static func hasSQLiteSidecars(
databaseURL: URL,
fileManager: FileManager) -> Bool {
let path = databaseURL.path
return fileManager.fileExists(atPath: "\(path)-wal")
|| fileManager.fileExists(atPath: "\(path)-shm")
Comment thread
choi138 marked this conversation as resolved.
}
}

struct HermesDatabaseFileSignature: Equatable {
let systemNumber: UInt64?
let fileNumber: UInt64?
let size: UInt64
let modificationDate: Date?

static func capture(
at fileURL: URL,
fileManager: FileManager = .default) -> HermesDatabaseFileSignature? {
guard let attributes = try? fileManager.attributesOfItem(atPath: fileURL.path),
let size = (attributes[.size] as? NSNumber)?.uint64Value else {
return nil
}
return HermesDatabaseFileSignature(
systemNumber: (attributes[.systemNumber] as? NSNumber)?.uint64Value,
fileNumber: (attributes[.systemFileNumber] as? NSNumber)?.uint64Value,
size: size,
modificationDate: attributes[.modificationDate] as? Date)
}
}

struct HermesSQLiteError: LocalizedError {
let operation: String
let message: String
let code: Int32

init(
operation: String,
database: OpaquePointer?,
code: Int32? = nil) {
self.operation = operation
self.code = code ?? database.map(sqlite3_errcode) ?? SQLITE_ERROR
if let database, let errorMessage = sqlite3_errmsg(database) {
message = String(cString: errorMessage)
} else {
message = "unknown SQLite error"
}
}

init(operation: String, message: String, code: Int32) {
self.operation = operation
self.message = message
self.code = code
}

var errorDescription: String? {
"Hermes SQLite \(operation) failed: \(message)"
}
}
Loading
Loading