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 */; };
|
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 */,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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 Nuke
|
||||||
|
|
||||||
import QuickLook
|
import QuickLook
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
final class ErrorLogViewController: UITableViewController, QLPreviewControllerDelegate
|
final class ErrorLogViewController: UITableViewController, QLPreviewControllerDelegate
|
||||||
{
|
{
|
||||||
@@ -216,13 +217,122 @@ private extension ErrorLogViewController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func showMinimuxerLogs(_ sender: UIBarButtonItem)
|
|
||||||
{
|
enum LogView: String {
|
||||||
// Show minimuxer.log
|
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()
|
let previewController = QLPreviewController()
|
||||||
previewController.dataSource = self
|
previewController.restorationIdentifier = self.rawValue
|
||||||
let navigationController = UINavigationController(rootViewController: previewController)
|
previewController.dataSource = dataSource
|
||||||
present(navigationController, animated: true, completion: nil)
|
|
||||||
|
// 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)
|
||||||
@@ -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)
|
||||||
|
// .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)
|
let entries = try store.getEntries(at: position)
|
||||||
.compactMap { $0 as? OSLogEntryLog }
|
|
||||||
.map { "[\($0.date.formatted())] [\($0.category)] [\($0.level.localizedName)] \($0.composedMessage)" }
|
|
||||||
|
|
||||||
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()
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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