mirror of
https://github.com/xweiba/location-spoofer.git
synced 2026-09-26 16:41:54 +08:00
fix: 优化启动引导、坐标处理和日志管理
This commit is contained in:
+126
-4
@@ -1,3 +1,4 @@
|
||||
import CoreLocation
|
||||
import Foundation
|
||||
|
||||
struct RuntimeLogEntry: Codable, Identifiable, Equatable {
|
||||
@@ -49,11 +50,17 @@ enum RuntimeLogStore {
|
||||
private static let decoder = JSONDecoder()
|
||||
private static let encoder = JSONEncoder()
|
||||
private static let maximumBytes: UInt64 = 1_500_000
|
||||
static let retentionInterval: TimeInterval = 3 * 24 * 60 * 60
|
||||
private static let pruningInterval: TimeInterval = 60 * 60
|
||||
private static var lastPrunedAt: Date?
|
||||
|
||||
static func append(_ entry: RuntimeLogEntry) {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
do {
|
||||
let now = Date()
|
||||
try pruneExpiredLogsIfNeeded(now: now)
|
||||
guard entry.timestamp >= retentionCutoff(now: now) else { return }
|
||||
let url = try logURL(for: entry.source)
|
||||
try rotateIfNeeded(url)
|
||||
var data = try encoder.encode(entry)
|
||||
@@ -75,7 +82,8 @@ enum RuntimeLogStore {
|
||||
static func loadAll(limit: Int = 800) -> [RuntimeLogEntry] {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
let directory = AppGroup.containerURL.appendingPathComponent("RuntimeLogs", isDirectory: true)
|
||||
let directory = logDirectory
|
||||
try? pruneExpiredLogs(in: directory, now: Date())
|
||||
guard let urls = try? FileManager.default.contentsOfDirectory(
|
||||
at: directory,
|
||||
includingPropertiesForKeys: nil
|
||||
@@ -90,12 +98,12 @@ enum RuntimeLogStore {
|
||||
static func clearAll() {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
let directory = AppGroup.containerURL.appendingPathComponent("RuntimeLogs", isDirectory: true)
|
||||
try? FileManager.default.removeItem(at: directory)
|
||||
try? FileManager.default.removeItem(at: logDirectory)
|
||||
lastPrunedAt = nil
|
||||
}
|
||||
|
||||
private static func logURL(for source: String) throws -> URL {
|
||||
let directory = AppGroup.containerURL.appendingPathComponent("RuntimeLogs", isDirectory: true)
|
||||
let directory = logDirectory
|
||||
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
let process = (Bundle.main.bundleIdentifier ?? "unknown-process")
|
||||
.replacingOccurrences(of: "/", with: "-")
|
||||
@@ -103,6 +111,56 @@ enum RuntimeLogStore {
|
||||
return directory.appendingPathComponent("\(process)-\(safeSource).jsonl")
|
||||
}
|
||||
|
||||
private static var logDirectory: URL {
|
||||
AppGroup.containerURL.appendingPathComponent("RuntimeLogs", isDirectory: true)
|
||||
}
|
||||
|
||||
static func retentionCutoff(now: Date) -> Date {
|
||||
now.addingTimeInterval(-retentionInterval)
|
||||
}
|
||||
|
||||
private static func pruneExpiredLogsIfNeeded(now: Date) throws {
|
||||
if let lastPrunedAt,
|
||||
now >= lastPrunedAt,
|
||||
now.timeIntervalSince(lastPrunedAt) < pruningInterval {
|
||||
return
|
||||
}
|
||||
try pruneExpiredLogs(in: logDirectory, now: now)
|
||||
}
|
||||
|
||||
private static func pruneExpiredLogs(in directory: URL, now: Date) throws {
|
||||
guard FileManager.default.fileExists(atPath: directory.path) else {
|
||||
lastPrunedAt = now
|
||||
return
|
||||
}
|
||||
let urls = try FileManager.default.contentsOfDirectory(
|
||||
at: directory,
|
||||
includingPropertiesForKeys: nil
|
||||
).filter { $0.pathExtension == "jsonl" }
|
||||
|
||||
for url in urls {
|
||||
let entries = readEntries(url)
|
||||
let retained = retainedEntries(entries, now: now)
|
||||
guard retained.count != entries.count else { continue }
|
||||
guard !retained.isEmpty else {
|
||||
try FileManager.default.removeItem(at: url)
|
||||
continue
|
||||
}
|
||||
var data = Data()
|
||||
for entry in retained {
|
||||
data.append(try encoder.encode(entry))
|
||||
data.append(0x0A)
|
||||
}
|
||||
try data.write(to: url, options: .atomic)
|
||||
}
|
||||
lastPrunedAt = now
|
||||
}
|
||||
|
||||
static func retainedEntries(_ entries: [RuntimeLogEntry], now: Date) -> [RuntimeLogEntry] {
|
||||
let cutoff = retentionCutoff(now: now)
|
||||
return entries.filter { $0.timestamp >= cutoff }
|
||||
}
|
||||
|
||||
private static func rotateIfNeeded(_ url: URL) throws {
|
||||
guard let attributes = try? FileManager.default.attributesOfItem(atPath: url.path),
|
||||
let size = attributes[.size] as? NSNumber,
|
||||
@@ -162,3 +220,67 @@ enum RuntimeLogger {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Realtime-location diagnostics keep precise coordinates out of the persisted,
|
||||
/// exportable log. Exact values are printed only by DEBUG builds for local Xcode
|
||||
/// debugging.
|
||||
enum RealtimeLocationTrace {
|
||||
static func log(
|
||||
_ message: String,
|
||||
location: CLLocation,
|
||||
details: [String: String] = [:],
|
||||
level: RuntimeLogEntry.Level = .info
|
||||
) {
|
||||
var metadata = details
|
||||
metadata["样本时间"] = ISO8601DateFormatter().string(from: location.timestamp)
|
||||
metadata["样本年龄秒"] = format(Date().timeIntervalSince(location.timestamp))
|
||||
metadata["水平精度米"] = format(location.horizontalAccuracy)
|
||||
metadata["垂直精度米"] = format(location.verticalAccuracy)
|
||||
metadata["海拔米"] = format(location.altitude)
|
||||
metadata["坐标有效"] = String(CLLocationCoordinate2DIsValid(location.coordinate))
|
||||
persist(level, message: message, details: metadata)
|
||||
debugCoordinate(message, location: location, details: metadata)
|
||||
}
|
||||
|
||||
static func coordinate(
|
||||
_ message: String,
|
||||
coordinate: CLLocationCoordinate2D,
|
||||
details: [String: String] = [:]
|
||||
) {
|
||||
#if DEBUG
|
||||
let suffix = details.sorted { $0.key < $1.key }
|
||||
.map { "\($0.key)=\($0.value)" }
|
||||
.joined(separator: " ")
|
||||
let latitude = String(format: "%.8f", coordinate.latitude)
|
||||
let longitude = String(format: "%.8f", coordinate.longitude)
|
||||
let metadata = suffix.isEmpty ? "" : " \(suffix)"
|
||||
print("[RealtimeLocation] \(message) latitude=\(latitude) longitude=\(longitude)\(metadata)")
|
||||
#endif
|
||||
}
|
||||
|
||||
private static func persist(
|
||||
_ level: RuntimeLogEntry.Level,
|
||||
message: String,
|
||||
details: [String: String]
|
||||
) {
|
||||
switch level {
|
||||
case .debug: RuntimeLogger.debug("APP", "实时定位", message, details: details)
|
||||
case .info: RuntimeLogger.info("APP", "实时定位", message, details: details)
|
||||
case .warning: RuntimeLogger.warning("APP", "实时定位", message, details: details)
|
||||
case .error: RuntimeLogger.error("APP", "实时定位", message, details: details)
|
||||
}
|
||||
}
|
||||
|
||||
private static func debugCoordinate(
|
||||
_ message: String,
|
||||
location: CLLocation,
|
||||
details: [String: String]
|
||||
) {
|
||||
coordinate(message, coordinate: location.coordinate, details: details)
|
||||
}
|
||||
|
||||
private static func format(_ value: Double) -> String {
|
||||
guard value.isFinite else { return String(value) }
|
||||
return String(format: "%.3f", value)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user