Merge remote-tracking branch 'upstream/main'

This commit is contained in:
Huge_Black 2025-03-31 10:20:05 +08:00
commit a49c7adc7e
15 changed files with 611 additions and 88 deletions

View file

@ -1,6 +1,10 @@
# StikJIT
A **work-in-progress** on-device JIT enabler for iOS, powered by [`idevice`](https://github.com/jkcoxson/idevice).
[![GitHub Release](https://img.shields.io/github/v/release/0-Blu/StikJIT?include_prereleases)](https://github.com/0-Blu/StikJIT/releases)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/0-Blu/StikJIT/pulls)
[![GitHub License](https://img.shields.io/github/license/0-Blu/StikJIT?color=%23C96FAD&cache=none)](https://github.com/0-Blu/StikJIT/blob/main/LICENSE)
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/0-Blu/StikJIT/total)
## Requirements
The [SideStore VPN](https://github.com/SideStore/SideStore/releases/download/0.1.1/SideStore.conf) is required. This allows the device to connect to itself.
@ -9,10 +13,7 @@ The [SideStore VPN](https://github.com/SideStore/SideStore/releases/download/0.1
- On-device Just-In-Time (JIT) compilation for supported apps via [`idevice`](https://github.com/jkcoxson/idevice).
- Seamless integration with [`em_proxy`](https://github.com/SideStore/em_proxy).
- Native UI for managing JIT-enabling.
- No data collection—ensuring full privacy.
## Privacy Policy
StikJIT **does not collect or store any user data.**
- No data collection—ensuring full privacy.
## Compiling Instructions
@ -28,18 +29,7 @@ StikJIT **does not collect or store any user data.**
3. **Build and Run:**
- Connect your iOS device.
- Select your device in Xcode.
- Build and run the project.
## TODO List
- [X] Integrate [`em_proxy`](https://github.com/SideStore/em_proxy)
- [X] Compile [`idevice`](https://github.com/jkcoxson/idevice/tree/master/ffi)
- [X] Implement heartbeat
- [X] Mount the developer image
- [X] Retrieve and filter installed apps by `get-task-allow`
- [X] Enable JIT for selected apps
- [X] Design and implement a user-friendly UI *(Done for now)*
- [ ] Write comprehensive documentation
- [ ] Prepare and release the initial version
- Build and run the project.
## License
StikJIT is licensed under **AGPL-3.0**. See [`LICENSE`](LICENSE) for details.

View file

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

View file

@ -1,6 +1,7 @@
{
"images" : [
{
"filename" : "StikJIT.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
@ -12,6 +13,7 @@
"value" : "dark"
}
],
"filename" : "StikJIT_Dark.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

View file

@ -51,6 +51,10 @@ struct HeartbeatApp: App {
}
return
}
var log_path = URL.documentsDirectory.appendingPathComponent("idevice_log.txt").path.utf8CString
log_path.withUnsafeMutableBufferPointer { buffer in
idevice_init_logger(Debug, Debug, buffer.baseAddress)
}
startProxy() { result, error in
if result {
checkVPNConnection() { result, vpn_error in

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

@ -11,85 +11,108 @@ struct InstalledAppsListView: View {
@StateObject private var viewModel = InstalledAppsViewModel()
@State private var appIcons: [String: UIImage] = [:]
@Environment(\.dismiss) private var dismiss
@Environment(\.colorScheme) private var colorScheme
@AppStorage("recentApps") var recentApps: [String] = []
var onSelectApp: (String) -> Void
var body: some View {
NavigationView {
ScrollView {
LazyVStack(spacing: 0) {
ForEach(viewModel.apps.sorted(by: { $0.key < $1.key }), id: \.key) { bundleID, appName in
Button(action: {
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
onSelectApp(bundleID)
}
}) {
HStack(spacing: 16) {
// App Icon
if let image = appIcons[bundleID] {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.cornerRadius(12)
.shadow(color: colorScheme == .dark ? Color.black.opacity(0.2) : Color.gray.opacity(0.2),
radius: 3, x: 0, y: 1)
} else {
RoundedRectangle(cornerRadius: 12)
.fill(Color(UIColor.systemGray5))
.frame(width: 60, height: 60)
.overlay(
Image(systemName: "app")
.font(.system(size: 26))
.foregroundColor(.gray)
)
.shadow(color: Color.black.opacity(0.1), radius: 2, x: 0, y: 1)
.onAppear {
loadAppIcon(for: bundleID)
List {
if !recentApps.isEmpty {
Section {
ForEach(recentApps, id: \.self) { bundleID in
AppButton(bundleID: bundleID, appName: viewModel.apps[bundleID] ?? "", recentApps: $recentApps, appIcons: $appIcons, onSelectApp: onSelectApp)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
withAnimation {
recentApps.removeAll(where: { $0 == bundleID })
}
} label: {
Label("Delete", systemImage: "trash")
}
}
// App Name and Bundle ID
VStack(alignment: .leading, spacing: 4) {
Text(appName)
.font(.system(size: 18, weight: .semibold))
.foregroundColor(Color.blue)
Text(bundleID)
.font(.system(size: 15))
.foregroundColor(Color.gray)
.lineLimit(1)
}
Spacer()
}
.padding(.vertical, 12)
.padding(.horizontal, 20)
.contentShape(Rectangle())
}
.buttonStyle(PlainButtonStyle())
if appName != viewModel.apps.sorted(by: { $0.key < $1.key }).last?.key {
Divider()
.padding(.leading, 96)
.padding(.trailing, 20)
.opacity(0.4)
}
} header: {
Text("Recents")
}
}
Section {
ForEach(viewModel.apps.sorted(by: { $0.key < $1.key }), id: \.key) { bundleID, appName in
AppButton(bundleID: bundleID, appName: appName, recentApps: $recentApps, appIcons: $appIcons, onSelectApp: onSelectApp)
}
} header: {
if !recentApps.isEmpty {
Text("All applications")
} else {
EmptyView()
}
}
.background(Color(UIColor.systemBackground))
}
.background(Color(UIColor.systemGroupedBackground).edgesIgnoringSafeArea(.all))
.listStyle(.plain)
.navigationTitle("Installed Apps")
.navigationBarItems(leading: Button("Done") {
dismiss()
.toolbar(content: {
Button("Done") {
dismiss()
}
})
}
.background(Color(UIColor.systemGroupedBackground).edgesIgnoringSafeArea(.all))
}
}
struct AppButton: View {
@State var bundleID: String
@State var appName: String
@Binding var recentApps: [String]
@Binding var appIcons: [String: UIImage]
var onSelectApp: (String) -> Void
@Environment(\.colorScheme) private var colorScheme
var body: some View {
Button(action: {
recentApps.removeAll(where: { $0 == bundleID })
recentApps.insert(bundleID, at: 0)
if recentApps.count > 3 {
recentApps = Array(recentApps.prefix(3))
}
onSelectApp(bundleID)
}) {
HStack(spacing: 16) {
if let image = appIcons[bundleID] {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.cornerRadius(12)
.shadow(color: colorScheme == .dark ? Color.black.opacity(0.2) : Color.gray.opacity(0.2), radius: 3, x: 0, y: 1)
} else {
RoundedRectangle(cornerRadius: 12)
.fill(Color(UIColor.systemGray5))
.frame(width: 60, height: 60)
.overlay(
Image(systemName: "app")
.font(.system(size: 26))
.foregroundColor(.gray)
)
.shadow(color: Color.black.opacity(0.1), radius: 2, x: 0, y: 1)
.onAppear {
loadAppIcon(for: bundleID)
}
}
VStack(alignment: .leading, spacing: 4) {
Text(appName)
.font(.system(size: 18, weight: .semibold))
.foregroundColor(Color(.label))
Text(bundleID)
.font(.system(size: 15))
.foregroundColor(Color.gray)
.lineLimit(1)
}
Spacer()
}
.font(.system(size: 17, weight: .regular))
.foregroundColor(.blue))
}
}
// Helper method to load app icon
private func loadAppIcon(for bundleID: String) {
AppStoreIconFetcher.getIcon(for: bundleID) { image in
if let image = image {
@ -103,6 +126,26 @@ struct InstalledAppsListView: View {
}
}
extension Array: @retroactive RawRepresentable where Element: Codable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode([Element].self, from: data)
else {
return nil
}
self = result
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return "[]"
}
return result
}
}
#Preview {
InstalledAppsListView { _ in }
}

View file

@ -23,9 +23,11 @@ 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",
"Stephen": "https://github.com/0-Blu.png",
"jkcoxson": "https://github.com/jkcoxson.png",
"Stossy11": "https://github.com/Stossy11.png",
"Neo": "https://github.com/neoarz.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);
}

View file

@ -14,7 +14,6 @@
void startHeartbeat(IdevicePairingFile* pairing_file, TcpProviderHandle** provider, int* heartbeatSessionId, HeartbeatCompletionHandlerC completion, LogFuncC logger) {
int currentSessionId = *heartbeatSessionId;
logger("DEBUG: Initializing logger...");
idevice_init_logger(Debug, Disabled, NULL);
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));

Binary file not shown.