mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-16 20:30:30 +00:00
* Rewrite parts of the app in Swift and fix iOS 26.4 issues (WIP) * Add more PID stuff * Improve location simulation * whoops
225 lines
6.6 KiB
Swift
225 lines
6.6 KiB
Swift
//
|
|
// SystemLogStream.swift
|
|
// StikJIT
|
|
//
|
|
// Created by Stephen on 09/21/2025.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
@MainActor
|
|
final class SystemLogStream: ObservableObject {
|
|
struct Entry: Identifiable {
|
|
let id = UUID()
|
|
let timestamp: Date
|
|
let message: String
|
|
let raw: String
|
|
}
|
|
|
|
@Published private(set) var entries: [Entry] = []
|
|
@Published var lastError: String? = nil
|
|
@Published private(set) var isStreaming: Bool = false
|
|
@Published private(set) var isPaused: Bool = false
|
|
@Published var updateInterval: TimeInterval = 0 {
|
|
didSet {
|
|
if updateInterval < 0 { updateInterval = 0 }
|
|
flushTimer?.invalidate()
|
|
flushTimer = nil
|
|
if !isPaused && !pendingEntries.isEmpty {
|
|
if updateInterval == 0 {
|
|
flushAllPending()
|
|
} else {
|
|
scheduleFlushIfNeeded()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private let maxEntries = 1500
|
|
private var pendingEntries: [Entry] = []
|
|
private var flushTimer: Timer?
|
|
private var retryTimer: Timer?
|
|
private var batchTimer: Timer?
|
|
private static let batchInterval: TimeInterval = 0.1
|
|
|
|
func start() {
|
|
retryTimer?.invalidate()
|
|
retryTimer = nil
|
|
guard !isStreaming else {
|
|
if isPaused { resume() }
|
|
return
|
|
}
|
|
isStreaming = true
|
|
isPaused = false
|
|
lastError = nil
|
|
startBatchTimer()
|
|
|
|
JITEnableContext.shared.startSyslogRelay(handler: { [weak self] line in
|
|
Task { @MainActor [weak self] in
|
|
self?.handleLine(line)
|
|
}
|
|
}, onError: { [weak self] error in
|
|
Task { @MainActor [weak self] in
|
|
self?.handleError(error)
|
|
}
|
|
})
|
|
}
|
|
|
|
func stop() {
|
|
guard isStreaming else { return }
|
|
isStreaming = false
|
|
isPaused = false
|
|
JITEnableContext.shared.stopSyslogRelay()
|
|
flushTimer?.invalidate()
|
|
flushTimer = nil
|
|
batchTimer?.invalidate()
|
|
batchTimer = nil
|
|
pendingEntries.removeAll()
|
|
retryTimer?.invalidate()
|
|
retryTimer = nil
|
|
}
|
|
|
|
func clear() {
|
|
entries.removeAll()
|
|
pendingEntries.removeAll()
|
|
flushTimer?.invalidate()
|
|
flushTimer = nil
|
|
retryTimer?.invalidate()
|
|
retryTimer = nil
|
|
}
|
|
|
|
func togglePause() {
|
|
isPaused ? resume() : pause()
|
|
}
|
|
|
|
func pause() {
|
|
guard !isPaused else { return }
|
|
isPaused = true
|
|
flushTimer?.invalidate()
|
|
flushTimer = nil
|
|
}
|
|
|
|
func resume() {
|
|
guard isPaused else { return }
|
|
isPaused = false
|
|
if updateInterval == 0 {
|
|
flushAllPending()
|
|
} else {
|
|
scheduleFlushIfNeeded()
|
|
}
|
|
}
|
|
|
|
private func handleLine(_ line: String) {
|
|
guard isStreaming else { return }
|
|
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !trimmed.isEmpty else { return }
|
|
|
|
let entry = Entry(timestamp: Date(), message: prettify(line: trimmed), raw: trimmed)
|
|
pendingEntries.append(entry)
|
|
if pendingEntries.count > maxEntries {
|
|
pendingEntries.removeFirst(pendingEntries.count - maxEntries)
|
|
}
|
|
// Batching is handled by batchTimer / scheduleFlushIfNeeded
|
|
if updateInterval > 0 {
|
|
scheduleFlushIfNeeded()
|
|
}
|
|
}
|
|
|
|
private func handleError(_ error: NSError?) {
|
|
isStreaming = false
|
|
isPaused = false
|
|
flushTimer?.invalidate()
|
|
flushTimer = nil
|
|
batchTimer?.invalidate()
|
|
batchTimer = nil
|
|
lastError = error?.localizedDescription ?? "System log stream stopped"
|
|
scheduleAutoRetry()
|
|
}
|
|
|
|
private func startBatchTimer() {
|
|
batchTimer?.invalidate()
|
|
batchTimer = Timer.scheduledTimer(withTimeInterval: Self.batchInterval, repeats: true) { [weak self] _ in
|
|
Task { @MainActor [weak self] in
|
|
guard let self else { return }
|
|
guard !self.isPaused, self.updateInterval == 0, !self.pendingEntries.isEmpty else { return }
|
|
self.flushAllPending()
|
|
}
|
|
}
|
|
if let batchTimer {
|
|
RunLoop.main.add(batchTimer, forMode: .common)
|
|
}
|
|
}
|
|
|
|
private func flushAllPending() {
|
|
guard !pendingEntries.isEmpty else { return }
|
|
entries.append(contentsOf: pendingEntries)
|
|
pendingEntries.removeAll()
|
|
if entries.count > maxEntries {
|
|
entries.removeFirst(entries.count - maxEntries)
|
|
}
|
|
}
|
|
|
|
private func prettify(line: String) -> String {
|
|
if let range = line.range(of: ": ") {
|
|
let messagePart = line[range.upperBound...]
|
|
return String(messagePart)
|
|
}
|
|
return line
|
|
}
|
|
|
|
func concatenatedLog(limit: Int = 500) -> String {
|
|
let slice = entries.suffix(limit)
|
|
return slice.map { "[\(DateFormatter.consoleFormatter.string(from: $0.timestamp))] \($0.raw)" }
|
|
.joined(separator: "\n")
|
|
}
|
|
|
|
private func scheduleFlushIfNeeded() {
|
|
guard !isPaused else { return }
|
|
guard !pendingEntries.isEmpty else { return }
|
|
guard flushTimer == nil else { return }
|
|
guard updateInterval > 0 else {
|
|
// Live mode uses batchTimer instead
|
|
return
|
|
}
|
|
|
|
flushTimer = Timer.scheduledTimer(withTimeInterval: updateInterval, repeats: false) { [weak self] _ in
|
|
Task { @MainActor [weak self] in
|
|
guard let self else { return }
|
|
self.flushTimer = nil
|
|
guard !self.isPaused else { return }
|
|
self.flushAllPending()
|
|
if !self.pendingEntries.isEmpty {
|
|
self.scheduleFlushIfNeeded()
|
|
}
|
|
}
|
|
}
|
|
|
|
if let flushTimer {
|
|
RunLoop.main.add(flushTimer, forMode: .common)
|
|
}
|
|
}
|
|
|
|
private func scheduleAutoRetry() {
|
|
guard !isStreaming else { return }
|
|
guard retryTimer == nil else { return }
|
|
let timer = Timer(timeInterval: 2.0, repeats: false) { [weak self] _ in
|
|
Task { @MainActor [weak self] in
|
|
guard let self else { return }
|
|
self.retryTimer = nil
|
|
if !self.isStreaming && !self.isPaused {
|
|
self.start()
|
|
}
|
|
}
|
|
}
|
|
retryTimer = timer
|
|
RunLoop.main.add(timer, forMode: .common)
|
|
}
|
|
}
|
|
|
|
private extension DateFormatter {
|
|
static let consoleFormatter: DateFormatter = {
|
|
let formatter = DateFormatter()
|
|
formatter.dateFormat = "HH:mm:ss"
|
|
return formatter
|
|
}()
|
|
}
|