mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
[Console-Log]: Added raw console logging in ErrorLog section (ladybug icon)
This commit is contained in:
123
AltStore/Settings/Error Log/ConsoleLogView.swift
Normal file
123
AltStore/Settings/Error Log/ConsoleLogView.swift
Normal file
@@ -0,0 +1,123 @@
|
||||
//
|
||||
// ConsoleLogView.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Magesh K on 29/12/24.
|
||||
// Copyright © 2024 SideStore. All rights reserved.
|
||||
//
|
||||
import SwiftUI
|
||||
|
||||
class ConsoleLogViewModel: ObservableObject {
|
||||
@Published var logLines: [String] = []
|
||||
|
||||
private var fileWatcher: DispatchSourceFileSystemObject?
|
||||
|
||||
private let backgroundQueue = DispatchQueue(label: "com.myapp.backgroundQueue", qos: .background)
|
||||
private var logURL: URL
|
||||
|
||||
init(logURL: URL) {
|
||||
self.logURL = logURL
|
||||
startFileWatcher() // Start monitoring the log file for changes
|
||||
reloadLogData() // Load initial log data
|
||||
}
|
||||
|
||||
private func startFileWatcher() {
|
||||
let fileDescriptor = open(logURL.path, O_RDONLY)
|
||||
guard fileDescriptor != -1 else {
|
||||
print("Unable to open file for reading.")
|
||||
return
|
||||
}
|
||||
|
||||
fileWatcher = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fileDescriptor, eventMask: .write, queue: backgroundQueue)
|
||||
fileWatcher?.setEventHandler {
|
||||
self.reloadLogData()
|
||||
}
|
||||
fileWatcher?.resume()
|
||||
}
|
||||
|
||||
private func reloadLogData() {
|
||||
if let fileContents = try? String(contentsOf: logURL) {
|
||||
let lines = fileContents.split(whereSeparator: \.isNewline).map { String($0) }
|
||||
DispatchQueue.main.async {
|
||||
self.logLines = lines
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
fileWatcher?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public struct ConsoleLogView: View {
|
||||
|
||||
@ObservedObject var viewModel: ConsoleLogViewModel
|
||||
@State private var isAtBottom: Bool = true
|
||||
// private let linesToShow: Int = 100 // Number of lines to show at once
|
||||
@State private var scrollToBottom: Bool = false // State variable to trigger scroll
|
||||
|
||||
init(logURL: URL) {
|
||||
self.viewModel = ConsoleLogViewModel(logURL: logURL)
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
VStack {
|
||||
// Custom Header Bar (similar to QuickLook's preview screen)
|
||||
HStack {
|
||||
Text("Console Log")
|
||||
.font(.system(size: 22, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
Spacer()
|
||||
SwiftUI.Button(action: {
|
||||
scrollToBottom.toggle()
|
||||
}) {
|
||||
Image(systemName: "ellipsis")
|
||||
.foregroundColor(.white)
|
||||
.imageScale(.large)
|
||||
}
|
||||
}
|
||||
.padding(15)
|
||||
.padding(.top, 5)
|
||||
.padding(.bottom, 2.5)
|
||||
.background(Color.black.opacity(0.9))
|
||||
.overlay(
|
||||
Rectangle()
|
||||
.frame(height: 1)
|
||||
.foregroundColor(Color.gray.opacity(0.2)), alignment: .bottom
|
||||
)
|
||||
|
||||
// Main Log Display (scrollable area)
|
||||
ScrollView(.vertical) {
|
||||
ScrollViewReader { scrollViewProxy in
|
||||
LazyVStack(alignment: .leading, spacing: 4) {
|
||||
ForEach(viewModel.logLines.indices, id: \.self) { index in
|
||||
Text(viewModel.logLines[index])
|
||||
.font(.system(size: 12, design: .monospaced))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
.onChange(of: scrollToBottom) { _ in
|
||||
scrollToBottomIfNeeded(scrollViewProxy: scrollViewProxy)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(Color.black) // Set background color to mimic QL's dark theme
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
|
||||
// Scroll to the last index (bottom) only if logLines is not empty
|
||||
private func scrollToBottomIfNeeded(scrollViewProxy: ScrollViewProxy) {
|
||||
// Ensure we have log data before attempting to scroll
|
||||
guard !viewModel.logLines.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
let last = viewModel.logLines.count - 1
|
||||
let lastIdx = viewModel.logLines.indices.last
|
||||
assert(last == lastIdx)
|
||||
// scrollViewProxy.scrollTo(lastIdx, anchor: .bottom)
|
||||
scrollViewProxy.scrollTo(last, anchor: .bottom)
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import Roxas
|
||||
import Nuke
|
||||
|
||||
import QuickLook
|
||||
import SwiftUI
|
||||
|
||||
final class ErrorLogViewController: UITableViewController, QLPreviewControllerDelegate
|
||||
{
|
||||
@@ -216,15 +217,124 @@ private extension ErrorLogViewController
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func showMinimuxerLogs(_ sender: UIBarButtonItem)
|
||||
{
|
||||
// Show minimuxer.log
|
||||
let previewController = QLPreviewController()
|
||||
previewController.dataSource = self
|
||||
let navigationController = UINavigationController(rootViewController: previewController)
|
||||
present(navigationController, animated: true, completion: nil)
|
||||
|
||||
enum LogView: String {
|
||||
case consoleLog = "console-log"
|
||||
case minimuxerLog = "minimuxer-log"
|
||||
|
||||
// // This class will manage the QLPreviewController and the timer.
|
||||
// private class LogViewManager {
|
||||
// var previewController: QLPreviewController
|
||||
// var refreshTimer: Timer?
|
||||
//
|
||||
// init(previewController: QLPreviewController) {
|
||||
// self.previewController = previewController
|
||||
// }
|
||||
//
|
||||
// // Start refreshing the preview controller every second
|
||||
// func startRefreshing() {
|
||||
// refreshTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(refreshPreview), userInfo: nil, repeats: true)
|
||||
// }
|
||||
//
|
||||
// @objc private func refreshPreview() {
|
||||
// previewController.reloadData()
|
||||
// }
|
||||
//
|
||||
// // Stop the timer to prevent leaks
|
||||
// func stopRefreshing() {
|
||||
// refreshTimer?.invalidate()
|
||||
// refreshTimer = nil
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Method to get the QLPreviewController for this log type
|
||||
// func getViewController(_ dataSource: QLPreviewControllerDataSource) -> QLPreviewController {
|
||||
// let previewController = QLPreviewController()
|
||||
// previewController.restorationIdentifier = self.rawValue
|
||||
// previewController.dataSource = dataSource
|
||||
//
|
||||
// // Create LogViewManager and start refreshing
|
||||
// let manager = LogViewManager(previewController: previewController)
|
||||
// manager.startRefreshing()
|
||||
//
|
||||
// return previewController
|
||||
// }
|
||||
|
||||
// This class will manage the QLPreviewController and the timer.
|
||||
private class LogViewManager {
|
||||
var previewController: QLPreviewController
|
||||
var refreshTimer: Timer?
|
||||
var logView: LogView
|
||||
|
||||
init(previewController: QLPreviewController, logView: LogView) {
|
||||
self.previewController = previewController
|
||||
self.logView = logView
|
||||
}
|
||||
|
||||
// Start refreshing the preview controller every second
|
||||
func startRefreshing() {
|
||||
refreshTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(refreshPreview), userInfo: nil, repeats: true)
|
||||
}
|
||||
|
||||
@objc private func refreshPreview() {
|
||||
previewController.reloadData()
|
||||
}
|
||||
|
||||
// Stop the timer to prevent leaks
|
||||
func stopRefreshing() {
|
||||
refreshTimer?.invalidate()
|
||||
refreshTimer = nil
|
||||
}
|
||||
|
||||
func updateLogPath() {
|
||||
// Force the QLPreviewController to reload by changing the file path
|
||||
previewController.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
// Method to get the QLPreviewController for this log type
|
||||
func getViewController(_ dataSource: QLPreviewControllerDataSource) -> QLPreviewController {
|
||||
let previewController = QLPreviewController()
|
||||
previewController.restorationIdentifier = self.rawValue
|
||||
previewController.dataSource = dataSource
|
||||
|
||||
// Create LogViewManager and start refreshing
|
||||
let manager = LogViewManager(previewController: previewController, logView: self)
|
||||
manager.startRefreshing()
|
||||
|
||||
return previewController
|
||||
}
|
||||
|
||||
func getLogPath() -> URL {
|
||||
switch self {
|
||||
case .consoleLog:
|
||||
let appDelegate = UIApplication.shared.delegate as! AppDelegate
|
||||
return appDelegate.consoleLog.logFileURL
|
||||
case .minimuxerLog:
|
||||
return FileManager.default.documentsDirectory.appendingPathComponent("minimuxer.log")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func showMinimuxerLogs(_ sender: UIBarButtonItem) {
|
||||
// Create the SwiftUI ConsoleLogView with the URL
|
||||
let consoleLogView = ConsoleLogView(logURL: (UIApplication.shared.delegate as! AppDelegate).consoleLog.logFileURL)
|
||||
|
||||
// Create the UIHostingController
|
||||
let consoleLogController = UIHostingController(rootView: consoleLogView)
|
||||
|
||||
// Configure the bottom sheet presentation
|
||||
consoleLogController.modalPresentationStyle = .pageSheet
|
||||
if let sheet = consoleLogController.sheetPresentationController {
|
||||
sheet.detents = [.medium(), .large()] // You can adjust the size of the sheet (medium/large)
|
||||
sheet.prefersGrabberVisible = true // Optional: Shows a grabber at the top of the sheet
|
||||
sheet.selectedDetentIdentifier = .large // Default size when presented
|
||||
}
|
||||
|
||||
// Present the bottom sheet
|
||||
present(consoleLogController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@IBAction func clearLoggedErrors(_ sender: UIBarButtonItem)
|
||||
{
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to clear the error log?", comment: ""), message: nil, preferredStyle: .actionSheet)
|
||||
@@ -297,13 +407,21 @@ private extension ErrorLogViewController
|
||||
|
||||
// All logs since the app launched.
|
||||
let position = store.position(timeIntervalSinceLatestBoot: 0)
|
||||
let predicate = NSPredicate(format: "subsystem == %@", Logger.altstoreSubsystem)
|
||||
|
||||
let entries = try store.getEntries(at: position, matching: predicate)
|
||||
.compactMap { $0 as? OSLogEntryLog }
|
||||
.map { "[\($0.date.formatted())] [\($0.category)] [\($0.level.localizedName)] \($0.composedMessage)" }
|
||||
|
||||
let outputText = entries.joined(separator: "\n")
|
||||
// let predicate = NSPredicate(format: "subsystem == %@", Logger.altstoreSubsystem)
|
||||
//
|
||||
// let entries = try store.getEntries(at: position, matching: predicate)
|
||||
// .compactMap { $0 as? OSLogEntryLog }
|
||||
// .map { "[\($0.date.formatted())] [\($0.category)] [\($0.level.localizedName)] \($0.composedMessage)" }
|
||||
//
|
||||
// Remove the predicate to get all log entries
|
||||
// let entries = try store.getEntries(at: position)
|
||||
// .compactMap { $0 as? OSLogEntryLog }
|
||||
// .map { "[\($0.date.formatted())] [\($0.category)] [\($0.level.localizedName)] \($0.composedMessage)" }
|
||||
|
||||
let entries = try store.getEntries(at: position)
|
||||
|
||||
// let outputText = entries.joined(separator: "\n")
|
||||
let outputText = entries.map { $0.description }.joined(separator: "\n")
|
||||
|
||||
let outputDirectory = FileManager.default.uniqueTemporaryURL()
|
||||
try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true)
|
||||
@@ -412,9 +530,13 @@ extension ErrorLogViewController: QLPreviewControllerDataSource {
|
||||
return 1
|
||||
}
|
||||
|
||||
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
|
||||
let fileURL = FileManager.default.documentsDirectory.appendingPathComponent("minimuxer.log")
|
||||
return fileURL as QLPreviewItem
|
||||
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem
|
||||
{
|
||||
guard let identifier = controller.restorationIdentifier,
|
||||
let logView = LogView(rawValue: identifier) else {
|
||||
fatalError("Invalid restorationIdentifier")
|
||||
}
|
||||
return logView.getLogPath() as QLPreviewItem
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user