Merge pull request #31 from neoarz/main

Add logger view (please review and test before merging)
This commit is contained in:
Stephen 2025-03-30 15:40:54 -04:00 • committed by GitHub
commit ab67f379e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 489 additions and 4 deletions

View file

@ -655,4 +655,4 @@
/* End XCConfigurationList section */
};
rootObject = DC6F1D2F2D94EADD0071B2B6 /* Project object */;
}
}

View file

@ -0,0 +1,91 @@
//
// LogManager.swift
// StikJIT
//
// Created by neoarz on 3/29/25.
//
import Foundation
class LogManager: ObservableObject {
static let shared = LogManager()
@Published var logs: [LogEntry] = []
@Published var errorCount: Int = 0
struct LogEntry: Identifiable {
let id = UUID()
let timestamp: Date
let type: LogType
let message: String
enum LogType: String {
case info = "INFO"
case error = "ERROR"
case debug = "DEBUG"
case warning = "WARNING"
}
}
private init() {
// Add initial system info logs
addInfoLog("StikJIT starting up")
addInfoLog("Initializing environment")
}
func addLog(message: String, type: LogEntry.LogType) {
//clean dumb stuff
var cleanMessage = message
// Clean up common prefixes that match the log type
let prefixesToRemove = [
"Info: ", "INFO: ", "Information: ",
"Error: ", "ERROR: ", "ERR: ",
"Debug: ", "DEBUG: ", "DBG: ",
"Warning: ", "WARN: ", "WARNING: "
]
for prefix in prefixesToRemove {
if cleanMessage.hasPrefix(prefix) {
cleanMessage = String(cleanMessage.dropFirst(prefix.count))
break
}
}
DispatchQueue.main.async {
self.logs.append(LogEntry(timestamp: Date(), type: type, message: cleanMessage))
if type == .error {
self.errorCount += 1
}
// Keep log size manageable
if self.logs.count > 1000 {
self.logs.removeFirst(100)
}
}
}
func addInfoLog(_ message: String) {
addLog(message: message, type: .info)
}
func addErrorLog(_ message: String) {
addLog(message: message, type: .error)
}
func addDebugLog(_ message: String) {
addLog(message: message, type: .debug)
}
func addWarningLog(_ message: String) {
addLog(message: message, type: .warning)
}
func clearLogs() {
DispatchQueue.main.async {
self.logs.removeAll()
self.errorCount = 0
}
}
}

View file

@ -0,0 +1,32 @@
//
// LogManagerBridge.swift
// StikJIT
//
// Created by neoarz on 3/29/25.
//
import Foundation
//objc bridge for logs
@objc class LogManagerBridge: NSObject {
@objc static let shared = LogManagerBridge()
private override init() {
super.init()
}
@objc func addInfoLog(_ message: String) {
LogManager.shared.addInfoLog(message)
}
@objc func addErrorLog(_ message: String) {
LogManager.shared.addErrorLog(message)
}
@objc func addDebugLog(_ message: String) {
LogManager.shared.addDebugLog(message)
}
@objc func addWarningLog(_ message: String) {
LogManager.shared.addWarningLog(message)
}
}

View file

@ -0,0 +1,303 @@
//
// ConsoleLogsView.swift
// StikJIT
//
// Created by neoarz on 3/29/25.
//
import SwiftUI
import UIKit
struct ConsoleLogsView: View {
@Environment(\.dismiss) private var dismiss
@StateObject private var logManager = LogManager.shared
@State private var autoScroll = true
@State private var scrollView: ScrollViewProxy? = nil
// Alert handlin
@State private var showingExportAlert = false
@State private var showingCopyAlert = false
@State private var alertMessage = ""
@State private var alertTitle = ""
@State private var isError = false
var body: some View {
NavigationView {
ZStack {
Color.black
.edgesIgnoringSafeArea(.all)
VStack(spacing: 0) {
// Terminal logs area (made it look somewhat like feathers implementation)
ScrollViewReader { proxy in
ScrollView {
VStack(spacing: 0) {
// Device Information (all thw way on top)
ForEach(["Version: \(UIDevice.current.systemVersion)",
"Name: \(UIDevice.current.name)",
"Model: \(UIDevice.current.model)",
"StikJIT Version: App Version: 1.0"], id: \.self) { info in
Text("[\(timeString())] ℹ️ \(info)")
.font(.system(size: 11, design: .monospaced))
.foregroundColor(.white)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 2)
.padding(.horizontal, 4)
}
Spacer()
// Log entries
ForEach(logManager.logs) { logEntry in
Text(AttributedString(createLogAttributedString(logEntry)))
.font(.system(size: 11, design: .monospaced))
.textSelection(.enabled)
.lineLimit(nil)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 1)
.padding(.horizontal, 4)
.id(logEntry.id)
}
}
}
.onAppear {
scrollView = proxy
}
.onChange(of: logManager.logs.count) {
if autoScroll, let lastLog = logManager.logs.last {
proxy.scrollTo(lastLog.id, anchor: .bottom)
}
}
}
Spacer()
VStack(spacing: 16) {
// Error count with red theme
HStack {
Text("\(logManager.errorCount) Critical Errors.")
.font(.headline)
.foregroundColor(.white)
.padding(.vertical, 12)
.frame(maxWidth: .infinity)
.background(Color.red)
.cornerRadius(10)
}
.padding(.horizontal)
// Action buttons with dark background
VStack(spacing: 1) {
// Export button
Button(action: {
// Create logs content with device information
var logsContent = "=== DEVICE INFORMATION ===\n"
logsContent += "Version: \(UIDevice.current.systemVersion)\n"
logsContent += "Name: \(UIDevice.current.name)\n"
logsContent += "Model: \(UIDevice.current.model)\n"
logsContent += "StikJIT Version: App Version: 1.0\n\n"
logsContent += "=== LOG ENTRIES ===\n"
// Add all log entries with proper formatting
logsContent += logManager.logs.map {
"[\(formatTime(date: $0.timestamp))] [\($0.type.rawValue)] \($0.message)"
}.joined(separator: "\n")
// Save to document directory (accessible in Files app)
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
let timestamp = dateFormatter.string(from: Date())
let fileURL = documentsDirectory.appendingPathComponent("StikJIT_Logs_\(timestamp).txt")
do {
// Write the logs to the file
try logsContent.write(to: fileURL, atomically: true, encoding: .utf8)
// Set alert variables and show the alert
alertTitle = "Logs Exported"
alertMessage = "Logs have been saved to Files app in StikJIT folder."
isError = false
showingExportAlert = true
} catch {
// Set error alert variables and show the alert
alertTitle = "Export Failed"
alertMessage = "Failed to save logs: \(error.localizedDescription)"
isError = true
showingExportAlert = true
}
}) {
HStack {
Text("Export Logs")
.foregroundColor(.blue)
Spacer()
Image(systemName: "square.and.arrow.down")
.foregroundColor(.gray)
}
.padding(.vertical, 14)
.padding(.horizontal, 20)
.contentShape(Rectangle())
}
.background(Color(red: 0.1, green: 0.1, blue: 0.1))
Divider()
.background(Color(red: 0.15, green: 0.15, blue: 0.15))
// Copy button
Button(action: {
// Create logs content with device information
var logsContent = "=== DEVICE INFORMATION ===\n"
logsContent += "Version: \(UIDevice.current.systemVersion)\n"
logsContent += "Name: \(UIDevice.current.name)\n"
logsContent += "Model: \(UIDevice.current.model)\n"
logsContent += "StikJIT Version: App Version: 1.0\n\n"
logsContent += "=== LOG ENTRIES ===\n"
// Add all log entries with proper formatting
logsContent += logManager.logs.map {
"[\(formatTime(date: $0.timestamp))] [\($0.type.rawValue)] \($0.message)"
}.joined(separator: "\n")
// Copy to clipboard
UIPasteboard.general.string = logsContent
// Show success alert using SwiftUI alert
alertTitle = "Logs Copied"
alertMessage = "Logs have been copied to clipboard."
isError = false
showingCopyAlert = true
}) {
HStack {
Text("Copy Logs")
.foregroundColor(.blue)
Spacer()
Image(systemName: "doc.on.doc")
.foregroundColor(.gray)
}
.padding(.vertical, 14)
.padding(.horizontal, 20)
.contentShape(Rectangle())
}
.background(Color(red: 0.1, green: 0.1, blue: 0.1))
}
.cornerRadius(10)
.padding(.horizontal)
.padding(.bottom, 20)
}
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
Text("Console Logs")
.font(.headline)
.foregroundColor(.white)
}
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
dismiss()
}) {
HStack(spacing: 2) {
Image(systemName: "chevron.left")
.font(.system(size: 16, weight: .semibold))
Text("Settings")
.fontWeight(.regular)
}
.foregroundColor(.blue)
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
logManager.clearLogs()
}) {
Text("Clear")
.foregroundColor(.blue)
}
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
.alert(alertTitle, isPresented: $showingExportAlert) {
Button("OK", role: .cancel) { }
} message: {
Text(alertMessage)
}
.alert(alertTitle, isPresented: $showingCopyAlert) {
Button("OK", role: .cancel) { }
} message: {
Text(alertMessage)
}
}
// Creates an NSAttributedString that combines timestamp, type, and message
// with proper styling for each component
private func createLogAttributedString(_ logEntry: LogManager.LogEntry) -> NSAttributedString {
let fullString = NSMutableAttributedString()
// Timestamp part
let timestampString = "[\(formatTime(date: logEntry.timestamp))]"
let timestampAttr = NSAttributedString(
string: timestampString,
attributes: [.foregroundColor: UIColor.gray]
)
fullString.append(timestampAttr)
fullString.append(NSAttributedString(string: " "))
// Log type part
let typeString = "[\(logEntry.type.rawValue)]"
let typeColor = UIColor(colorForLogType(logEntry.type))
let typeAttr = NSAttributedString(
string: typeString,
attributes: [.foregroundColor: typeColor]
)
fullString.append(typeAttr)
fullString.append(NSAttributedString(string: " "))
// Message part
let messageAttr = NSAttributedString(
string: logEntry.message,
attributes: [.foregroundColor: UIColor.white]
)
fullString.append(messageAttr)
return fullString
}
// Helper to display current time
private func timeString() -> String {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
return formatter.string(from: Date())
}
// Helper to format Date objects to time strings
private func formatTime(date: Date) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
return formatter.string(from: date)
}
// Return color based on log type
private func colorForLogType(_ type: LogManager.LogEntry.LogType) -> Color {
switch type {
case .info:
return .green
case .error:
return .red
case .debug:
return .blue
case .warning:
return .orange
}
}
}
struct ConsoleLogsView_Previews: PreviewProvider {
static var previews: some View {
ConsoleLogsView()
}
}

View file

@ -259,11 +259,20 @@ struct HomeView: View {
private func startJITInBackground(with bundleID: String) {
isProcessing = true
// Add log message
LogManager.shared.addInfoLog("Starting JIT for \(bundleID)")
DispatchQueue.global(qos: .background).async {
JITEnableContext.shared().debugApp(withBundleID: bundleID, logger: nil)
JITEnableContext.shared().debugApp(withBundleID: bundleID, logger: { message in
if let message = message {
// Log messages from the JIT process
LogManager.shared.addInfoLog(message)
}
})
DispatchQueue.main.async {
LogManager.shared.addInfoLog("JIT process completed for \(bundleID)")
isProcessing = false
}
}

View file

@ -23,6 +23,8 @@ struct SettingsView: View {
@State private var mounted = false
@State private var showingConsoleLogsView = false
// Developer profile image URLs
private let developerProfiles: [String: String] = [
"Blu": "https://github.com/0-Blu.png",
@ -361,8 +363,32 @@ struct SettingsView: View {
.padding(.vertical, 20)
.padding(.horizontal, 16)
}
.padding(.bottom, 16)
// Version text - now outside of any card, as standalone text at the bottom
// Move System Logs section here (right after About card, before version)
SettingsCard {
Button(action: {
showingConsoleLogsView = true
}) {
HStack {
Text("System Logs")
.font(.headline)
.foregroundColor(.primary)
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.secondary)
}
.contentShape(Rectangle())
.padding(.vertical, 16)
.padding(.horizontal, 16)
}
.buttonStyle(PlainButtonStyle())
}
.padding(.bottom, 16)
// Version info should now come after System Logs
HStack {
Spacer()
Text("Version 1.0 • iOS \(UIDevice.current.systemVersion)")
@ -376,6 +402,11 @@ struct SettingsView: View {
.padding(.horizontal, 16)
.padding(.vertical, 20)
}
// Add this sheet at the end of the ZStack, before the final closing bracket
.sheet(isPresented: $showingConsoleLogsView) {
ConsoleLogsView()
}
}
.fileImporter(
isPresented: $isShowingPairingFilePicker,
@ -655,3 +686,10 @@ struct CollaboratorRow: View {
}
}
}
// Define these in a separate file if they conflict
struct ConsoleLogsView_Preview: PreviewProvider {
static var previews: some View {
ConsoleLogsView()
}
}

View file

@ -13,6 +13,7 @@
#include "applist.h"
#include "JITEnableContext.h"
#import "StikJIT-Swift.h" // This imports the Swift files into Objective-C
JITEnableContext* sharedJITContext = nil;
@ -41,6 +42,17 @@ JITEnableContext* sharedJITContext = nil;
NSString *message = [[NSString alloc] initWithFormat:formatStr arguments:args];
NSLog(@"%@", message);
// Add to log manager
if ([message containsString:@"ERROR"] || [message containsString:@"Error"]) {
[[LogManagerBridge shared] addErrorLog:message];
} else if ([message containsString:@"WARNING"] || [message containsString:@"Warning"]) {
[[LogManagerBridge shared] addWarningLog:message];
} else if ([message containsString:@"DEBUG"]) {
[[LogManagerBridge shared] addDebugLog:message];
} else {
[[LogManagerBridge shared] addInfoLog:message];
}
if(logger) {
logger(message);
}