[Console-Log]: Added raw console logging in ErrorLog section (ladybug icon)

This commit is contained in:
Magesh K
2024-12-29 03:12:59 +05:30
parent 2e247f1773
commit 2e01116f1f
9 changed files with 616 additions and 17 deletions

View File

@@ -61,6 +61,12 @@
A8945AA62D059B6100D86CBE /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A8945AA52D059B6100D86CBE /* Roxas.framework */; }; A8945AA62D059B6100D86CBE /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A8945AA52D059B6100D86CBE /* Roxas.framework */; };
A8A543302D04F14400D72399 /* libfragmentzip.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A8A5432F2D04F0C100D72399 /* libfragmentzip.a */; }; A8A543302D04F14400D72399 /* libfragmentzip.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A8A5432F2D04F0C100D72399 /* libfragmentzip.a */; };
A8BB34E52D04EC8E000A8B4D /* minimuxer-helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A52D04DA1900F0F0F3 /* minimuxer-helpers.swift */; }; A8BB34E52D04EC8E000A8B4D /* minimuxer-helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = A809F6A52D04DA1900F0F0F3 /* minimuxer-helpers.swift */; };
A8C38C242D206A3A00E83DBD /* ConsoleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C1D2D206A3A00E83DBD /* ConsoleLogger.swift */; };
A8C38C262D206A3A00E83DBD /* ConsoleLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C1E2D206A3A00E83DBD /* ConsoleLog.swift */; };
A8C38C2A2D206AC100E83DBD /* OutputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C282D206AC100E83DBD /* OutputStream.swift */; };
A8C38C2C2D206AD900E83DBD /* AbstractClassError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C2B2D206AD900E83DBD /* AbstractClassError.swift */; };
A8C38C322D206B2500E83DBD /* FileOutputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C312D206B2500E83DBD /* FileOutputStream.swift */; };
A8C38C382D2084D000E83DBD /* ConsoleLogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C38C372D2084D000E83DBD /* ConsoleLogView.swift */; };
A8C6D50C2D1EE87600DF01F1 /* AltSign-Static in Frameworks */ = {isa = PBXBuildFile; productRef = A8C6D50B2D1EE87600DF01F1 /* AltSign-Static */; }; A8C6D50C2D1EE87600DF01F1 /* AltSign-Static in Frameworks */ = {isa = PBXBuildFile; productRef = A8C6D50B2D1EE87600DF01F1 /* AltSign-Static */; };
A8C6D5122D1EE8AF00DF01F1 /* AltSign-Static in Frameworks */ = {isa = PBXBuildFile; productRef = A8C6D5112D1EE8AF00DF01F1 /* AltSign-Static */; }; A8C6D5122D1EE8AF00DF01F1 /* AltSign-Static in Frameworks */ = {isa = PBXBuildFile; productRef = A8C6D5112D1EE8AF00DF01F1 /* AltSign-Static */; };
A8C6D5132D1EE8D700DF01F1 /* OpenSSL.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; }; A8C6D5132D1EE8D700DF01F1 /* OpenSSL.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A859ED5B2D1EE80D003DCC58 /* OpenSSL.xcframework */; };
@@ -624,6 +630,12 @@
A86202322D1F35640091187B /* AltStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStore.xcconfig; sourceTree = "<group>"; }; A86202322D1F35640091187B /* AltStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStore.xcconfig; sourceTree = "<group>"; };
A86202332D1F35640091187B /* AltStoreCore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStoreCore.xcconfig; sourceTree = "<group>"; }; A86202332D1F35640091187B /* AltStoreCore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltStoreCore.xcconfig; sourceTree = "<group>"; };
A8945AA52D059B6100D86CBE /* Roxas.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Roxas.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A8945AA52D059B6100D86CBE /* Roxas.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Roxas.framework; sourceTree = BUILT_PRODUCTS_DIR; };
A8C38C1D2D206A3A00E83DBD /* ConsoleLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogger.swift; sourceTree = "<group>"; };
A8C38C1E2D206A3A00E83DBD /* ConsoleLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLog.swift; sourceTree = "<group>"; };
A8C38C282D206AC100E83DBD /* OutputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputStream.swift; sourceTree = "<group>"; };
A8C38C2B2D206AD900E83DBD /* AbstractClassError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbstractClassError.swift; sourceTree = "<group>"; };
A8C38C312D206B2500E83DBD /* FileOutputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOutputStream.swift; sourceTree = "<group>"; };
A8C38C372D2084D000E83DBD /* ConsoleLogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogView.swift; sourceTree = "<group>"; };
A8D484D72D0CD306002C691D /* AltBackup.ipa */ = {isa = PBXFileReference; lastKnownFileType = file; path = AltBackup.ipa; sourceTree = "<group>"; }; A8D484D72D0CD306002C691D /* AltBackup.ipa */ = {isa = PBXFileReference; lastKnownFileType = file; path = AltBackup.ipa; sourceTree = "<group>"; };
A8F66C3C2D04D433009689E6 /* em_proxy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = em_proxy.h; sourceTree = "<group>"; }; A8F66C3C2D04D433009689E6 /* em_proxy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = em_proxy.h; sourceTree = "<group>"; };
A8F66C602D04D464009689E6 /* minimuxer.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = minimuxer.xcodeproj; sourceTree = "<group>"; }; A8F66C602D04D464009689E6 /* minimuxer.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = minimuxer.xcodeproj; sourceTree = "<group>"; };
@@ -1161,6 +1173,34 @@
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
A8C38C1C2D2068D100E83DBD /* Utils */ = {
isa = PBXGroup;
children = (
A8C38C272D206AA500E83DBD /* common */,
A8C38C202D206A3A00E83DBD /* iostreams */,
);
path = Utils;
sourceTree = "<group>";
};
A8C38C202D206A3A00E83DBD /* iostreams */ = {
isa = PBXGroup;
children = (
A8C38C1D2D206A3A00E83DBD /* ConsoleLogger.swift */,
A8C38C1E2D206A3A00E83DBD /* ConsoleLog.swift */,
);
path = iostreams;
sourceTree = "<group>";
};
A8C38C272D206AA500E83DBD /* common */ = {
isa = PBXGroup;
children = (
A8C38C282D206AC100E83DBD /* OutputStream.swift */,
A8C38C312D206B2500E83DBD /* FileOutputStream.swift */,
A8C38C2B2D206AD900E83DBD /* AbstractClassError.swift */,
);
path = common;
sourceTree = "<group>";
};
A8F66C072D04C025009689E6 /* SideStore */ = { A8F66C072D04C025009689E6 /* SideStore */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -1170,6 +1210,7 @@
B343F84D295F6323002B1159 /* em_proxy.xcodeproj */, B343F84D295F6323002B1159 /* em_proxy.xcodeproj */,
19104DB32909C06D00C49C7B /* EmotionalDamage */, 19104DB32909C06D00C49C7B /* EmotionalDamage */,
B343F886295F7F9B002B1159 /* libfragmentzip.xcodeproj */, B343F886295F7F9B002B1159 /* libfragmentzip.xcodeproj */,
A8C38C1C2D2068D100E83DBD /* Utils */,
); );
path = SideStore; path = SideStore;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -2095,6 +2136,7 @@
D589170128C7D93500E39C8B /* Error Log */ = { D589170128C7D93500E39C8B /* Error Log */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A8C38C372D2084D000E83DBD /* ConsoleLogView.swift */,
D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */, D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */,
D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */, D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */,
0EE7FDCC2BE9124400D1E390 /* ErrorDetailsViewController.swift */, 0EE7FDCC2BE9124400D1E390 /* ErrorDetailsViewController.swift */,
@@ -2838,6 +2880,7 @@
files = ( files = (
BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */, BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */,
BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */, BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */,
A8C38C2C2D206AD900E83DBD /* AbstractClassError.swift in Sources */,
BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */, BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */,
D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */, D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */,
BFD2478F2284C8F900981D42 /* Button.swift in Sources */, BFD2478F2284C8F900981D42 /* Button.swift in Sources */,
@@ -2865,11 +2908,14 @@
BFCCB51A245E3401001853EA /* VerifyAppOperation.swift in Sources */, BFCCB51A245E3401001853EA /* VerifyAppOperation.swift in Sources */,
BFF0B6982322CAB8007A79E1 /* InstructionsViewController.swift in Sources */, BFF0B6982322CAB8007A79E1 /* InstructionsViewController.swift in Sources */,
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */, BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */,
A8C38C242D206A3A00E83DBD /* ConsoleLogger.swift in Sources */,
A8C38C262D206A3A00E83DBD /* ConsoleLog.swift in Sources */,
D5935AED29C39DE300C157EF /* SourceComponents.swift in Sources */, D5935AED29C39DE300C157EF /* SourceComponents.swift in Sources */,
D5935AED29C39DE300C157EF /* SourceComponents.swift in Sources */, D5935AED29C39DE300C157EF /* SourceComponents.swift in Sources */,
A8FD917C2D0478D200322782 /* VerificationError.swift in Sources */, A8FD917C2D0478D200322782 /* VerificationError.swift in Sources */,
D5A0537329B91DB400997551 /* SourceDetailContentViewController.swift in Sources */, D5A0537329B91DB400997551 /* SourceDetailContentViewController.swift in Sources */,
BF770E5422BC044E002A40FE /* OperationContexts.swift in Sources */, BF770E5422BC044E002A40FE /* OperationContexts.swift in Sources */,
A8C38C322D206B2500E83DBD /* FileOutputStream.swift in Sources */,
BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */, BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */,
BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */, BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */,
BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */, BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */,
@@ -2929,12 +2975,14 @@
BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */, BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */,
A8FD917B2D0472DD00322782 /* DeprecatedAPIs.swift in Sources */, A8FD917B2D0472DD00322782 /* DeprecatedAPIs.swift in Sources */,
BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */, BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */,
A8C38C382D2084D000E83DBD /* ConsoleLogView.swift in Sources */,
BFF00D322501BDA100746320 /* BackgroundRefreshAppsOperation.swift in Sources */, BFF00D322501BDA100746320 /* BackgroundRefreshAppsOperation.swift in Sources */,
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */, BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */,
BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */, BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */,
0EE7FDC42BE8BC7900D1E390 /* ALTLocalizedError.swift in Sources */, 0EE7FDC42BE8BC7900D1E390 /* ALTLocalizedError.swift in Sources */,
BFF00D342501BDCF00746320 /* IntentHandler.swift in Sources */, BFF00D342501BDCF00746320 /* IntentHandler.swift in Sources */,
BFDBBD80246CB84F004ED2F3 /* RemoveAppBackupOperation.swift in Sources */, BFDBBD80246CB84F004ED2F3 /* RemoveAppBackupOperation.swift in Sources */,
A8C38C2A2D206AC100E83DBD /* OutputStream.swift in Sources */,
BFF0B6942321CB85007A79E1 /* AuthenticationViewController.swift in Sources */, BFF0B6942321CB85007A79E1 /* AuthenticationViewController.swift in Sources */,
0EE7FDCD2BE9124400D1E390 /* ErrorDetailsViewController.swift in Sources */, 0EE7FDCD2BE9124400D1E390 /* ErrorDetailsViewController.swift in Sources */,
D561AF822B21669400BF59C6 /* VerifyAppPledgeOperation.swift in Sources */, D561AF822B21669400BF59C6 /* VerifyAppPledgeOperation.swift in Sources */,

View File

@@ -41,8 +41,14 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
private let intentHandler = IntentHandler() private let intentHandler = IntentHandler()
private let viewAppIntentHandler = ViewAppIntentHandler() private let viewAppIntentHandler = ViewAppIntentHandler()
public let consoleLog = ConsoleLog()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{ {
// start logging to console immediately on startup
consoleLog.startCapturing()
// Override point for customization after application launch. // Override point for customization after application launch.
// UserDefaults.standard.setValue(true, forKey: "com.apple.CoreData.MigrationDebug") // UserDefaults.standard.setValue(true, forKey: "com.apple.CoreData.MigrationDebug")
// UserDefaults.standard.setValue(true, forKey: "com.apple.CoreData.SQLDebug") // UserDefaults.standard.setValue(true, forKey: "com.apple.CoreData.SQLDebug")
@@ -130,6 +136,11 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
default: return nil default: return nil
} }
} }
func applicationWillTerminate(_ application: UIApplication) {
// Stop console logging and clean up resources
consoleLog.stopCapturing()
}
} }
extension AppDelegate extension AppDelegate

View 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)
}
}

View File

@@ -16,6 +16,7 @@ import Roxas
import Nuke import Nuke
import QuickLook import QuickLook
import SwiftUI
final class ErrorLogViewController: UITableViewController, QLPreviewControllerDelegate final class ErrorLogViewController: UITableViewController, QLPreviewControllerDelegate
{ {
@@ -216,15 +217,124 @@ private extension ErrorLogViewController
} }
} }
@IBAction func showMinimuxerLogs(_ sender: UIBarButtonItem)
{ enum LogView: String {
// Show minimuxer.log case consoleLog = "console-log"
let previewController = QLPreviewController() case minimuxerLog = "minimuxer-log"
previewController.dataSource = self
let navigationController = UINavigationController(rootViewController: previewController) // // This class will manage the QLPreviewController and the timer.
present(navigationController, animated: true, completion: nil) // 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) @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) 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. // All logs since the app launched.
let position = store.position(timeIntervalSinceLatestBoot: 0) let position = store.position(timeIntervalSinceLatestBoot: 0)
let predicate = NSPredicate(format: "subsystem == %@", Logger.altstoreSubsystem) // let predicate = NSPredicate(format: "subsystem == %@", Logger.altstoreSubsystem)
//
let entries = try store.getEntries(at: position, matching: predicate) // let entries = try store.getEntries(at: position, matching: predicate)
.compactMap { $0 as? OSLogEntryLog } // .compactMap { $0 as? OSLogEntryLog }
.map { "[\($0.date.formatted())] [\($0.category)] [\($0.level.localizedName)] \($0.composedMessage)" } // .map { "[\($0.date.formatted())] [\($0.category)] [\($0.level.localizedName)] \($0.composedMessage)" }
//
let outputText = entries.joined(separator: "\n") // 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() let outputDirectory = FileManager.default.uniqueTemporaryURL()
try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true) try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true)
@@ -412,9 +530,13 @@ extension ErrorLogViewController: QLPreviewControllerDataSource {
return 1 return 1
} }
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem
let fileURL = FileManager.default.documentsDirectory.appendingPathComponent("minimuxer.log") {
return fileURL as QLPreviewItem guard let identifier = controller.restorationIdentifier,
let logView = LogView(rawValue: identifier) else {
fatalError("Invalid restorationIdentifier")
}
return logView.getLogPath() as QLPreviewItem
} }
} }

View File

@@ -0,0 +1,12 @@
//
// OutputStream.swift
// AltStore
//
// Created by Magesh K on 28/12/24.
// Copyright © 2024 SideStore. All rights reserved.
//
public enum AbstractClassError: Error {
case abstractInitializerInvoked
case abstractMethodInvoked
}

View File

@@ -0,0 +1,29 @@
//
// FileOutputStream.swift
// AltStore
//
// Created by Magesh K on 28/12/24.
// Copyright © 2024 SideStore. All rights reserved.
//
import Foundation
public class FileOutputStream: OutputStream {
private let fileHandle: FileHandle
init(_ fileHandle: FileHandle) {
self.fileHandle = fileHandle
}
public func write(_ data: Data) {
fileHandle.write(data)
}
public func flush() {
fileHandle.synchronizeFile()
}
public func close() {
fileHandle.closeFile()
}
}

View File

@@ -0,0 +1,15 @@
//
// OutputStream.swift
// AltStore
//
// Created by Magesh K on 28/12/24.
// Copyright © 2024 SideStore. All rights reserved.
//
import Foundation
public protocol OutputStream {
func write(_ data: Data)
func flush()
func close()
}

View File

@@ -0,0 +1,73 @@
//
// ConsoleLog.swift
// AltStore
//
// Created by Magesh K on 25/11/24.
// Copyright © 2024 SideStore. All rights reserved.
//
//
import Foundation
class ConsoleLog {
private static let CONSOLE_LOGS_DIRECTORY = "ConsoleLogs"
private static let CONSOLE_LOG_NAME_PREFIX = "console"
private static let CONSOLE_LOG_EXTN = ".log"
private lazy var consoleLogger: ConsoleLogger = {
let logFileHandle = createLogFileHandle()
let fileOutputStream = FileOutputStream(logFileHandle)
return UnBufferedConsoleLogger(stream: fileOutputStream)
}()
private lazy var consoleLogsDir: URL = {
// create a directory for console logs
let docsDir = FileManager.default.documentsDirectory
let consoleLogsDir = docsDir.appendingPathComponent(ConsoleLog.CONSOLE_LOGS_DIRECTORY)
if !FileManager.default.fileExists(atPath: consoleLogsDir.path) {
try! FileManager.default.createDirectory(at: consoleLogsDir, withIntermediateDirectories: true, attributes: nil)
}
return consoleLogsDir
}()
public lazy var logName: String = {
logFileURL.lastPathComponent
}()
public lazy var logFileURL: URL = {
// get current timestamp
let currentTime = Date()
let dateTimeStamp = ConsoleLog.getDateInTimeStamp(date: currentTime)
// create a log file with the current timestamp
let logName = "\(ConsoleLog.CONSOLE_LOG_NAME_PREFIX)-\(dateTimeStamp)\(ConsoleLog.CONSOLE_LOG_EXTN)"
let logFileURL = consoleLogsDir.appendingPathComponent(logName)
return logFileURL
}()
private func createLogFileHandle() -> FileHandle {
if !FileManager.default.fileExists(atPath: logFileURL.path) {
FileManager.default.createFile(atPath: logFileURL.path, contents: nil, attributes: nil)
}
// return the file handle
return try! FileHandle(forWritingTo: logFileURL)
}
private static func getDateInTimeStamp(date: Date) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "yyyyMMdd_HHmmss" // Format: 20241228_142345
return formatter.string(from: date)
}
func startCapturing() {
consoleLogger.startCapturing()
}
func stopCapturing() {
consoleLogger.stopCapturing()
}
}

View File

@@ -0,0 +1,166 @@
//
// ConsoleCapture.swift
// AltStore
//
// Created by Magesh K on 25/11/24.
// Copyright © 2024 SideStore. All rights reserved.
//
import Foundation
protocol ConsoleLogger{
func startCapturing()
func stopCapturing()
}
public class AbstractConsoleLogger<T: OutputStream>: ConsoleLogger{
var outPipe: Pipe?
var errPipe: Pipe?
var outputHandle: FileHandle?
var errorHandle: FileHandle?
var originalStdout: Int32?
var originalStderr: Int32?
let ostream: T
let writeQueue = DispatchQueue(label: "async-write-queue")
public init(stream: T) throws {
// Since swift doesn't support compile time abstract classes Instantiation checking,
// we are using runtime check to prevent direct instantiation :(
if Self.self === AbstractConsoleLogger.self {
throw AbstractClassError.abstractInitializerInvoked
}
self.ostream = stream
}
deinit {
stopCapturing()
}
public func startCapturing() { // made it public coz, let client ask for capturing
// if already initialized within current instance, bail out
guard outPipe == nil, errPipe == nil else {
return
}
// Create new pipes for stdout and stderr
self.outPipe = Pipe()
self.errPipe = Pipe()
outputHandle = self.outPipe?.fileHandleForReading
errorHandle = self.errPipe?.fileHandleForReading
// Store original file descriptors
originalStdout = dup(STDOUT_FILENO)
originalStderr = dup(STDERR_FILENO)
// Redirect stdout and stderr to our pipes
dup2(self.outPipe?.fileHandleForWriting.fileDescriptor ?? -1, STDOUT_FILENO)
dup2(self.errPipe?.fileHandleForWriting.fileDescriptor ?? -1, STDERR_FILENO)
// Setup readability handlers for raw data
setupReadabilityHandler(for: outputHandle, isError: false)
setupReadabilityHandler(for: errorHandle, isError: true)
}
private func setupReadabilityHandler(for handle: FileHandle?, isError: Bool) {
handle?.readabilityHandler = { [weak self] handle in
let data = handle.availableData
if !data.isEmpty {
self?.writeQueue.async {
try? self?.writeData(data)
}
// Forward to original std stream
if let originalFD = isError ? self?.originalStderr : self?.originalStdout {
data.withUnsafeBytes { (bufferPointer) -> Void in
if let baseAddress = bufferPointer.baseAddress, bufferPointer.count > 0 {
write(originalFD, baseAddress, bufferPointer.count)
}
}
}
}
}
}
func writeData(_ data: Data) throws {
throw AbstractClassError.abstractMethodInvoked
}
func stopCapturing() {
ostream.close()
// Restore original stdout and stderr
if let stdout = originalStdout {
dup2(stdout, STDOUT_FILENO)
close(stdout)
}
if let stderr = originalStderr {
dup2(stderr, STDERR_FILENO)
close(stderr)
}
// Clean up
outPipe?.fileHandleForReading.readabilityHandler = nil
errPipe?.fileHandleForReading.readabilityHandler = nil
outPipe = nil
errPipe = nil
outputHandle = nil
errorHandle = nil
originalStdout = nil
originalStderr = nil
}
}
public class UnBufferedConsoleLogger<T: OutputStream>: AbstractConsoleLogger<T> {
required override init(stream: T) {
// cannot throw abstractInitializerInvoked, so need to override else client needs to handle it unnecessarily
try! super.init(stream: stream)
}
override func writeData(_ data: Data) throws {
// directly write data to the stream without buffering
ostream.write(data)
}
}
public class BufferedConsoleLogger<T: OutputStream>: AbstractConsoleLogger<T> {
// Buffer size (bytes) and storage
private let maxBufferSize: Int
private var bufferedData = Data()
required init(stream: T, bufferSize: Int = 1024) {
self.maxBufferSize = bufferSize
try! super.init(stream: stream)
}
override func writeData(_ data: Data) throws {
// Append data to buffer
self.bufferedData.append(data)
// Check if the buffer is full and flush
if self.bufferedData.count >= self.maxBufferSize {
self.flushBuffer()
}
}
private func flushBuffer() {
// Write all buffered data to the stream
ostream.write(bufferedData)
bufferedData.removeAll()
}
override func stopCapturing() {
// Flush buffer and close the file handles first
flushBuffer()
super.stopCapturing()
}
}