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:
@@ -61,6 +61,12 @@
|
||||
A8945AA62D059B6100D86CBE /* Roxas.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A8945AA52D059B6100D86CBE /* Roxas.framework */; };
|
||||
A8A543302D04F14400D72399 /* libfragmentzip.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A8A5432F2D04F0C100D72399 /* libfragmentzip.a */; };
|
||||
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 */; };
|
||||
A8C6D5122D1EE8AF00DF01F1 /* AltSign-Static in Frameworks */ = {isa = PBXBuildFile; productRef = A8C6D5112D1EE8AF00DF01F1 /* AltSign-Static */; };
|
||||
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>"; };
|
||||
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; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -1161,6 +1173,34 @@
|
||||
name = Products;
|
||||
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 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1170,6 +1210,7 @@
|
||||
B343F84D295F6323002B1159 /* em_proxy.xcodeproj */,
|
||||
19104DB32909C06D00C49C7B /* EmotionalDamage */,
|
||||
B343F886295F7F9B002B1159 /* libfragmentzip.xcodeproj */,
|
||||
A8C38C1C2D2068D100E83DBD /* Utils */,
|
||||
);
|
||||
path = SideStore;
|
||||
sourceTree = "<group>";
|
||||
@@ -2095,6 +2136,7 @@
|
||||
D589170128C7D93500E39C8B /* Error Log */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A8C38C372D2084D000E83DBD /* ConsoleLogView.swift */,
|
||||
D57FE84328C7DB7100216002 /* ErrorLogViewController.swift */,
|
||||
D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */,
|
||||
0EE7FDCC2BE9124400D1E390 /* ErrorDetailsViewController.swift */,
|
||||
@@ -2838,6 +2880,7 @@
|
||||
files = (
|
||||
BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */,
|
||||
BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */,
|
||||
A8C38C2C2D206AD900E83DBD /* AbstractClassError.swift in Sources */,
|
||||
BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */,
|
||||
D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */,
|
||||
BFD2478F2284C8F900981D42 /* Button.swift in Sources */,
|
||||
@@ -2865,11 +2908,14 @@
|
||||
BFCCB51A245E3401001853EA /* VerifyAppOperation.swift in Sources */,
|
||||
BFF0B6982322CAB8007A79E1 /* InstructionsViewController.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 */,
|
||||
A8FD917C2D0478D200322782 /* VerificationError.swift in Sources */,
|
||||
D5A0537329B91DB400997551 /* SourceDetailContentViewController.swift in Sources */,
|
||||
BF770E5422BC044E002A40FE /* OperationContexts.swift in Sources */,
|
||||
A8C38C322D206B2500E83DBD /* FileOutputStream.swift in Sources */,
|
||||
BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */,
|
||||
BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */,
|
||||
BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */,
|
||||
@@ -2929,12 +2975,14 @@
|
||||
BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */,
|
||||
A8FD917B2D0472DD00322782 /* DeprecatedAPIs.swift in Sources */,
|
||||
BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */,
|
||||
A8C38C382D2084D000E83DBD /* ConsoleLogView.swift in Sources */,
|
||||
BFF00D322501BDA100746320 /* BackgroundRefreshAppsOperation.swift in Sources */,
|
||||
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */,
|
||||
BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */,
|
||||
0EE7FDC42BE8BC7900D1E390 /* ALTLocalizedError.swift in Sources */,
|
||||
BFF00D342501BDCF00746320 /* IntentHandler.swift in Sources */,
|
||||
BFDBBD80246CB84F004ED2F3 /* RemoveAppBackupOperation.swift in Sources */,
|
||||
A8C38C2A2D206AC100E83DBD /* OutputStream.swift in Sources */,
|
||||
BFF0B6942321CB85007A79E1 /* AuthenticationViewController.swift in Sources */,
|
||||
0EE7FDCD2BE9124400D1E390 /* ErrorDetailsViewController.swift in Sources */,
|
||||
D561AF822B21669400BF59C6 /* VerifyAppPledgeOperation.swift in Sources */,
|
||||
|
||||
@@ -41,8 +41,14 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
private let intentHandler = IntentHandler()
|
||||
private let viewAppIntentHandler = ViewAppIntentHandler()
|
||||
|
||||
public let consoleLog = ConsoleLog()
|
||||
|
||||
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.
|
||||
// UserDefaults.standard.setValue(true, forKey: "com.apple.CoreData.MigrationDebug")
|
||||
// UserDefaults.standard.setValue(true, forKey: "com.apple.CoreData.SQLDebug")
|
||||
@@ -130,6 +136,11 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Stop console logging and clean up resources
|
||||
consoleLog.stopCapturing()
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDelegate
|
||||
|
||||
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,13 +217,122 @@ private extension ErrorLogViewController
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func showMinimuxerLogs(_ sender: UIBarButtonItem)
|
||||
{
|
||||
// Show minimuxer.log
|
||||
|
||||
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.dataSource = self
|
||||
let navigationController = UINavigationController(rootViewController: previewController)
|
||||
present(navigationController, animated: true, completion: nil)
|
||||
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)
|
||||
@@ -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 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, matching: predicate)
|
||||
.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.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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
12
SideStore/Utils/common/AbstractClassError.swift
Normal file
12
SideStore/Utils/common/AbstractClassError.swift
Normal 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
|
||||
}
|
||||
29
SideStore/Utils/common/FileOutputStream.swift
Normal file
29
SideStore/Utils/common/FileOutputStream.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
15
SideStore/Utils/common/OutputStream.swift
Normal file
15
SideStore/Utils/common/OutputStream.swift
Normal 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()
|
||||
}
|
||||
73
SideStore/Utils/iostreams/ConsoleLog.swift
Normal file
73
SideStore/Utils/iostreams/ConsoleLog.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
|
||||
166
SideStore/Utils/iostreams/ConsoleLogger.swift
Normal file
166
SideStore/Utils/iostreams/ConsoleLogger.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user