[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

@@ -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()
}
}