Compare commits

..

No commits in common. "main" and "3.1.8" have entirely different histories.
main ... 3.1.8

28 changed files with 380 additions and 4586 deletions

View file

@ -1,22 +0,0 @@
#!/bin/bash
set -euo pipefail
website_directory=${1:?Usage: update_source.sh WEBSITE_DIRECTORY RELEASE_VERSION}
release_version=${2:?Usage: update_source.sh WEBSITE_DIRECTORY RELEASE_VERSION}
cd "$website_directory"
git config user.name 'GitHub Action'
git config user.email 'action@github.com'
python3 updatesource.py index.json
git add index.json
if git diff --staged --quiet; then
echo 'Website/index.json is already current.'
exit 0
fi
git commit -m "chore: update StikDebug source for $release_version"
git push origin main

View file

@ -3,20 +3,17 @@ name: Build Debug IPA
on:
push:
branches: [ "main" ]
tags: [ "[0-9]*" ]
pull_request:
branches: [ "main" ]
workflow_dispatch:
permissions:
contents: write
contents: read
jobs:
build:
name: Build IPA
runs-on: macos-latest
outputs:
version: ${{ steps.version.outputs.version }}
env:
UPLOAD_IPA: ${{ vars.UPLOAD_IPA || 'true' }}
@ -53,70 +50,10 @@ jobs:
zip -r StikDebug.ipa Payload
rm -rf Payload StikDebug.app
- name: 5. Name workflow artifact
id: artifact
if: env.UPLOAD_IPA == 'true'
run: |
file_name="StikDebug-${GITHUB_SHA::7}.ipa"
cp StikDebug.ipa "$file_name"
echo "file_name=$file_name" >> "$GITHUB_OUTPUT"
- name: 6. Store IPA as Workflow Artifact
- name: 5. Store IPA as Workflow Artifact
if: env.UPLOAD_IPA == 'true'
uses: actions/upload-artifact@v7
with:
path: ${{ steps.artifact.outputs.file_name }}
archive: false
name: StikDebug-Debug.ipa
path: StikDebug.ipa
retention-days: 90
- name: Read version
id: version
if: startsWith(github.ref, 'refs/tags/')
run: |
version=$(xcodebuild -project StikDebug.xcodeproj -scheme StikDebug -showBuildSettings | awk -F ' = ' '/ MARKETING_VERSION = / { print $2; exit }')
test -n "$version"
if [ "$GITHUB_REF_TYPE" = "tag" ]; then
test "${GITHUB_REF_NAME#v}" = "$version"
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Name release asset
if: startsWith(github.ref, 'refs/tags/')
run: mv StikDebug.ipa StikDebug-${{ steps.version.outputs.version }}.ipa
- name: Publish release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v3
with:
name: ${{ steps.version.outputs.version }}
tag_name: ${{ github.ref_name }}
target_commitish: ${{ github.sha }}
files: StikDebug-${{ steps.version.outputs.version }}.ipa
generate_release_notes: true
fail_on_unmatched_files: true
update-source:
name: Update website source
if: startsWith(github.ref, 'refs/tags/')
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout source
uses: actions/checkout@v6
- name: Checkout website
uses: actions/checkout@v6
with:
repository: StikDebug/Website
ref: main
path: Website
token: ${{ secrets.WEBSITE_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: '3.x'
- name: Update StikDebug source
run: .github/scripts/update_source.sh Website '${{ needs.build.outputs.version }}'

52
.github/workflows/updatesource.yml vendored Normal file
View file

@ -0,0 +1,52 @@
name: Update StikDebug Source
on:
release:
types: [published]
workflow_dispatch:
jobs:
update-source:
runs-on: ubuntu-latest
steps:
- name: Checkout Website-Stuff
uses: actions/checkout@v6
with:
repository: StephenDev0/Website-Stuff
ref: main
path: Website-Stuff
# A repository-scoped GITHUB_TOKEN cannot push to Website-Stuff.
token: ${{ secrets.WEBSITE_STUFF_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Update StikDebug source
id: update_source
working-directory: Website-Stuff
run: |
git config --global user.name 'GitHub Action'
git config --global user.email 'action@github.com'
python updatesource.py index.json
git add index.json
if git diff --staged --quiet; then
echo "changes=false" >> $GITHUB_OUTPUT
echo "No changes detected in index.json"
else
git commit -m "Update StikDebug source for latest release"
git push origin main
echo "changes=true" >> $GITHUB_OUTPUT
fi
- name: Create job summary
run: |
if [[ "${{ steps.update_source.outputs.changes }}" == "true" ]]; then
echo "## StikDebug source updated" >> $GITHUB_STEP_SUMMARY
echo "Website-Stuff/index.json was updated and pushed." >> $GITHUB_STEP_SUMMARY
else
echo "## StikDebug source already current" >> $GITHUB_STEP_SUMMARY
echo "No change to Website-Stuff/index.json was needed." >> $GITHUB_STEP_SUMMARY
fi

View file

@ -7,17 +7,24 @@
<p><i>An on-device debugger/JIT enabler for iOS versions 17.4+ powered by <a href="https://github.com/jkcoxson/idevice">idevice</a>.</i></p>
</div>
<h3 align="center">
<a href="https://discord.gg/ZnNcrRT3M8"><img src="https://img.shields.io/badge/Discord-join%20us-7289DA?logo=discord&logoColor=white&style=for-the-badge&labelColor=23272A" /></a>&nbsp;
<a href="https://github.com/StikDebug/StikDebug/blob/main/LICENSE"><img src="https://img.shields.io/github/license/StikDebug/StikDebug?label=License&color=5865F2&style=for-the-badge&labelColor=23272A" /></a>&nbsp;
<a href="https://github.com/StikDebug/StikDebug"><img src="https://img.shields.io/github/stars/StikDebug/StikDebug?label=Stars&color=FEE75C&style=for-the-badge&labelColor=23272A" /></a>&nbsp;
<a href="https://github.com/StikDebug/StikDebug/releases"><img src="https://img.shields.io/github/v/release/StikDebug/StikDebug?label=Latest&color=00BFFF&style=for-the-badge&labelColor=23272A" /></a>&nbsp;
<a href="https://deepwiki.com/StikDebug/StikDebug"><img src="https://img.shields.io/badge/Ask-DeepWiki-grey?style=for-the-badge&labelColor=23272A&color=007ec6" /></a>
<br />
</h3>
<h6 align="center">
<a href="https://discord.gg/ZnNcrRT3M8">
<img src="https://img.shields.io/badge/Discord-join%20us-7289DA?logo=discord&logoColor=white&style=for-the-badge&labelColor=23272A" />
</a>
<a href="https://github.com/StephenDev0/StikDebug/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/StephenDev0/StikDebug?label=License&color=5865F2&style=for-the-badge&labelColor=23272A" />
</a>
<a href="https://github.com/StephenDev0/StikDebug/stargazers">
<img src="https://img.shields.io/github/stars/StephenDev0/StikDebug?label=Stars&color=FEE75C&style=for-the-badge&labelColor=23272A" />
</a>
<a href="https://github.com/StephenDev0/StikDebug/releases">
<img src="https://img.shields.io/github/v/release/StephenDev0/StikDebug?label=Latest&color=00BFFF&style=for-the-badge&labelColor=23272A" />
</a>
<br />
</h6>
## Features
- **JIT:** Enable Just-In-Time compilation for sideloaded apps that have the `get-task-allow` entitlement.
- **JIT:** Enable Just In Time compilation for sideloaded apps that have the `get-task-allow` entitlement.
- **App Launching:** Launch every app installed on your device.
- **Console:** Live app and system logs.
- **Scripts:** Manage automation scripts (mainly used for iOS 26 JIT).
@ -30,11 +37,14 @@
> [!NOTE]
> **Notice:** StikDebug is no longer available on the App Store. Please use the official download methods below.
<h3 align="center">
<a href="https://altdirect.app/?url=https://stikdebug.xyz/index.json" target="_blank"><img src="https://altdirect.app/assets/png/AltSource_Blue.png" alt="Add AltSource" width="200"></a>
&nbsp;
<a href="https://github.com/StikDebug/StikDebug/releases/latest/download/StikDebug-3.1.15.ipa" target="_blank"><img src="https://altdirect.app/assets/png/Download_Blue.png" alt="Download .ipa" width="200"></a>
</h3>
<div align="center" style="display: flex; justify-content: center; align-items: center; gap: 16px; flex-wrap: wrap;">
<a href="https://stikstore.app/altdirect/?url=https://stikdebug.xyz/index.json" target="_blank">
<img src="https://github.com/stikstore/altdirect/blob/main/assets/png/AltSource_Blue.png" alt="Add AltSource" width="200">
</a>
<a href="https://github.com/StephenDev0/StikDebug/releases/download/3.1.6/StikDebug-3.1.6.ipa" target="_blank">
<img src="https://github.com/stikstore/altdirect/blob/main/assets/png/Download_Blue.png" alt="Download .ipa" width="200">
</a>
</div>
## Compatibility
@ -56,18 +66,18 @@ StikDebug enables **JIT** for sideloaded apps on iOS 17.4+ without needing a com
### Steps
1. **Obtain a pairing file**
- Detailed guide: [Pairing File Instructions](https://github.com/StikDebug/StikDebug-Guide/blob/main/pairing_file.md) (or ask in Discord).
- Detailed guide: [Pairing File Instructions](https://github.com/StephenDev0/StikDebug-Guide/blob/main/pairing_file.md) (or ask in Discord).
2. **Set up VPN**
- Launch LocalDevVPN and enable the VPN.
4. **Enable JIT for an app**
- Launch StikDebug and tap the `Enable JIT` button.
- Launch StikDebug and tapp the `Enable JIT` button.
- Select your sideloaded app from the list in StikDebug.
**Troubleshooting**
- "Connection dropped" or loopback errors → Check iOS version compatibility / beta warnings.
- Heartbeat errors → Ensure that the VPN is on and that you are connected to Wi-Fi. It may be a pairing file issue.
- Heartbeat errors → Ensure that the VPN is on and that you are connecected to Wi-Fi. It may be a pairing file issue.
- Pairing file issues → Replace file with device unlocked & trusted.
- Still stuck? Join the [Discord](https://discord.gg/ZnNcrRT3M8) with logs/screenshots.
@ -86,10 +96,6 @@ StikDebug enables **JIT** for sideloaded apps on iOS 17.4+ without needing a com
## Building from Source
> [!IMPORTANT]
> **Integrating StikDebug or StikJIT into your own project?**
> See the [StikJIT Integration Guide](https://github.com/StikDebug/StikJIT/blob/main/INTEGRATION.md) for integration instructions.
### Requirements
- macOS (latest recommended)
- Xcode 16+ (Xcode 26+ preferred for iOS 26+ support)
@ -100,7 +106,7 @@ StikDebug enables **JIT** for sideloaded apps on iOS 17.4+ without needing a com
### Steps
1. **Clone the repo**
```bash
git clone https://github.com/StikDebug/StikDebug.git
git clone https://github.com/StephenDev0/StikDebug.git
cd StikDebug
```
@ -140,11 +146,13 @@ To propose a new feature, open a feature request issue and provide:
### Code Contributions (Best Practices)
- Follow normal Swift and SwiftUI style.
- Write clear and easy-to-understand code.
- Write clear and easy to understand code.
- Keep your changes consistent with how the project is already set up.
- Make sure everything builds and works without errors.
We appreciate your time and effort in helping improve this project.
## Code Help
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/stephendev0/stikdebug)
## License
StikDebug is licensed under **AGPL-3.0**. See [`LICENSE`](LICENSE) for details.

View file

@ -482,7 +482,7 @@
"$(PROJECT_DIR)/StikDebug/idevice",
);
MACOSX_DEPLOYMENT_TARGET = 15.1;
MARKETING_VERSION = 3.1.15;
MARKETING_VERSION = 3.1.8;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.stikdebug;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -544,7 +544,7 @@
"$(PROJECT_DIR)/StikDebug/idevice",
);
MACOSX_DEPLOYMENT_TARGET = 15.1;
MARKETING_VERSION = 3.1.15;
MARKETING_VERSION = 3.1.8;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.stikdebug;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View file

@ -9,9 +9,8 @@ import UIKit
enum AppBootstrapper {
static func configure() {
IdeviceLogFileManager.shared.prepareForLogging()
registerDefaultSettings()
ensureKeepAliveSelection()
startConfiguredKeepAliveServices()
applyDocumentPickerCopyWorkaround()
}
@ -28,11 +27,11 @@ enum AppBootstrapper {
])
}
private static func ensureKeepAliveSelection() {
let defaults = UserDefaults.standard
if !defaults.bool(forKey: "keepAliveAudio"), !defaults.bool(forKey: "keepAliveLocation") {
defaults.set(true, forKey: "keepAliveAudio")
private static func startConfiguredKeepAliveServices() {
guard UserDefaults.standard.bool(forKey: "keepAliveAudio") else {
return
}
BackgroundAudioManager.shared.start()
}
private static func applyDocumentPickerCopyWorkaround() {

View file

@ -39,8 +39,7 @@ private enum IdeviceBridge {
}
let code = Int(ffiError.pointee.code)
let detail = string(from: ffiError.pointee.message) ?? fallback
let message = detail == fallback ? fallback : "\(fallback): \(detail)"
let message = string(from: ffiError.pointee.message) ?? fallback
idevice_error_free(ffiError)
return makeError(domain: domain, code: code, message: message)
}
@ -263,69 +262,31 @@ private enum IdeviceBridge {
}
extension JITEnableContext {
func isDeveloperDiskImageMounted() throws -> Bool {
func getMountedDeviceCount() throws -> Int {
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
var cryptexQuerySucceeded = false
var cryptexQueryError: NSError?
if DeveloperDiskImageService.usesCryptexDDI {
var installed: UnsafeMutablePointer<InstalledCryptexC>?
defer { cryptexd_free_installed_cryptex(installed) }
if let ffiError = cryptexd_installed_ddi(adapter, handshake, &installed) {
cryptexQueryError = IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to query installed DDI cryptex")
} else {
cryptexQuerySucceeded = true
if installed != nil {
return true
}
try IdeviceBridge.withConnectedClient(
fallback: "Failed to connect to image mounter",
missingClientMessage: "Image mounter client was not created",
connect: { image_mounter_connect_rsd(adapter, handshake, $0) },
cleanup: { image_mounter_free($0) }
) { client in
var devices: UnsafeMutablePointer<plist_t?>?
var deviceCount = 0
if let ffiError = image_mounter_copy_devices(client, &devices, &deviceCount) {
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to fetch mounted devices")
}
}
do {
return try IdeviceBridge.withConnectedClient(
fallback: "Failed to connect to image mounter",
missingClientMessage: "Image mounter client was not created",
connect: { image_mounter_connect_rsd(adapter, handshake, $0) },
cleanup: { image_mounter_free($0) }
) { client in
var devices: UnsafeMutablePointer<plist_t?>?
var deviceCount = 0
if let error = image_mounter_copy_devices(client, &devices, &deviceCount) {
throw IdeviceBridge.consumeFFIError(error, fallback: "Failed to fetch mounted devices")
if let devices {
for index in 0..<deviceCount {
plist_free(devices[index])
}
if let devices {
for index in 0..<deviceCount {
plist_free(devices[index])
}
idevice_data_free(
UnsafeMutableRawPointer(devices).assumingMemoryBound(to: UInt8.self),
UInt(deviceCount * MemoryLayout<plist_t?>.stride)
)
}
return deviceCount > 0
idevice_data_free(
UnsafeMutableRawPointer(devices).assumingMemoryBound(to: UInt8.self),
UInt(deviceCount * MemoryLayout<plist_t?>.stride)
)
}
} catch {
if cryptexQuerySucceeded {
return false
}
throw cryptexQueryError ?? (error as NSError)
}
}
}
func installCryptexDDI(from directoryPath: String) throws {
var assets: OpaquePointer?
if let ffiError = cryptex1_assets_load(directoryPath, &assets) {
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to load DDI cryptex assets")
}
guard let assets else {
throw IdeviceBridge.makeError(message: "DDI cryptex assets were not loaded")
}
defer { cryptex1_assets_free(assets) }
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
if let ffiError = cryptexd_install_ddi(adapter, handshake, assets, nil) {
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to install DDI cryptex")
return deviceCount
}
}
}
@ -346,7 +307,13 @@ extension JITEnableContext {
if let ffiError = lockdownd_get_value(lockdownClient, "UniqueChipID", nil, &uniqueChipIDPlist) {
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to query UniqueChipID")
}
defer { plist_free(uniqueChipIDPlist) }
defer {
if let uniqueChipIDPlist {
plist_free(uniqueChipIDPlist)
}
}
return try IdeviceBridge.uint64Value(from: uniqueChipIDPlist, fieldName: "UniqueChipID")
}
@ -356,10 +323,10 @@ extension JITEnableContext {
connect: { image_mounter_connect_rsd(adapter, handshake, $0) },
cleanup: { image_mounter_free($0) }
) { imageMounterClient in
let ffiError = imageData.withUnsafeBytes { imageBuffer in
trustcacheData.withUnsafeBytes { trustcacheBuffer in
manifestData.withUnsafeBytes { manifestBuffer in
image_mounter_mount_personalized_rsd(
let ffiError = imageData.withUnsafeBytes { imageBuffer -> UnsafeMutablePointer<IdeviceFfiError>? in
trustcacheData.withUnsafeBytes { trustcacheBuffer -> UnsafeMutablePointer<IdeviceFfiError>? in
manifestData.withUnsafeBytes { manifestBuffer -> UnsafeMutablePointer<IdeviceFfiError>? in
image_mounter_mount_personalized_with_callback_rsd(
imageMounterClient,
adapter,
handshake,
@ -370,7 +337,9 @@ extension JITEnableContext {
manifestBuffer.bindMemory(to: UInt8.self).baseAddress,
manifestData.count,
nil,
uniqueChipID
uniqueChipID,
progressCallback,
nil
)
}
}

View file

@ -17,37 +17,6 @@ typealias SyslogErrorHandler = (NSError?) -> Void
final class JITEnableContext {
static let shared = JITEnableContext()
private static func withCStringArray<R>(
_ strings: [String], _ body: (UnsafePointer<UnsafePointer<CChar>?>?, UInt) -> R
) -> R {
if strings.isEmpty {
return body(nil, 0)
}
var cStrings: [UnsafeMutablePointer<CChar>?] = strings.map { strdup($0) }
defer { cStrings.forEach { free($0) } }
return cStrings.withUnsafeBufferPointer { buffer in
buffer.baseAddress!.withMemoryRebound(to: UnsafePointer<CChar>?.self, capacity: buffer.count) { rebound in
body(rebound, UInt(buffer.count))
}
}
}
private static func mallocDebugEnvVars() -> [String] {
let defaults = UserDefaults.standard
let enableAll = defaults.bool(forKey: UserDefaults.Keys.mallocDebug)
let guardEdges = enableAll || defaults.bool(forKey: UserDefaults.Keys.mallocGuardEdges)
let scribble = enableAll || defaults.bool(forKey: UserDefaults.Keys.mallocScribble)
var envVars: [String] = []
if guardEdges {
envVars.append("MallocGuardEdges=1")
}
if scribble {
envVars.append("MallocScribble=1")
}
return envVars
}
private struct TunnelHandles {
var adapter: OpaquePointer?
var handshake: OpaquePointer?
@ -82,7 +51,11 @@ final class JITEnableContext {
var handshakeHandle: OpaquePointer? { handshake }
private init() {
var path = Array(IdeviceLogFileManager.logURL.path.utf8CString)
let logURL = FileManager.default
.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent("idevice_log.txt")
var path = Array(logURL.path.utf8CString)
path.withUnsafeMutableBufferPointer { buffer in
_ = idevice_init_logger(Info, Debug, buffer.baseAddress)
}
@ -117,8 +90,7 @@ final class JITEnableContext {
guard let ffiError else {
return makeError(fallback)
}
let detail = nsString(from: ffiError.pointee.message, fallback: fallback)
let message = detail == fallback ? fallback : "\(fallback): \(detail)"
let message = nsString(from: ffiError.pointee.message, fallback: fallback)
let error = makeError(message, code: Int(ffiError.pointee.code))
idevice_error_free(ffiError)
return error
@ -216,9 +188,7 @@ final class JITEnableContext {
tunnelLock.unlock()
if let waitSemaphore {
guard waitSemaphore.wait(timeout: .now() + .seconds(15)) == .success else {
throw makeError("Timed out waiting for the tunnel connection", code: -19)
}
waitSemaphore.wait()
waitSemaphore.signal()
}
@ -458,19 +428,22 @@ final class JITEnableContext {
private func withConnectedRemoteServer<T>(
_ body: (OpaquePointer) throws -> T
) throws -> T {
try withFreshDebugTunnel(hostname: "StikDebugRemoteServer") { adapter, handshake in
var remoteServer: OpaquePointer?
if let ffiError = remote_server_connect_rsd(adapter, handshake, &remoteServer) {
throw error(from: ffiError, fallback: "Failed to connect remote server")
}
guard let remoteServer else {
throw makeError("Remote server handle was not created")
}
defer { remote_server_free(remoteServer) }
return try body(remoteServer)
try ensureTunnel()
guard let adapter, let handshake else {
throw makeError("Tunnel is not connected")
}
var remoteServer: OpaquePointer?
if let ffiError = remote_server_connect_rsd(adapter, handshake, &remoteServer) {
throw error(from: ffiError, fallback: "Failed to connect remote server")
}
guard let remoteServer else {
throw makeError("Remote server handle was not created")
}
defer { remote_server_free(remoteServer) }
return try body(remoteServer)
}
private func withProcessControl<T>(
@ -628,9 +601,7 @@ final class JITEnableContext {
try withProcessControl(remoteServer: remoteServer) { processControl in
var pid: UInt64 = 0
let ffiError = bundleID.withCString { bundleID in
Self.withCStringArray(Self.mallocDebugEnvVars()) { envPtr, envCount in
process_control_launch_app(processControl, bundleID, envPtr, envCount, nil, 0, true, false, &pid)
}
process_control_launch_app(processControl, bundleID, nil, 0, nil, 0, true, false, &pid)
}
if let ffiError {

View file

@ -9,108 +9,74 @@ import idevice
final class MountingProgress: ObservableObject {
static let shared = MountingProgress()
@Published private(set) var mountProgress: Double = 0.0
@Published private(set) var mountingThread: Thread?
@Published private(set) var coolisMounted: Bool = false
private let mountCheckLock = NSLock()
private var mountCheckInProgress = false
private let mountLock = NSLock()
private var mountInProgress = false
private init() {}
func checkforMounted() {
guard TunnelManager.shared.isConnected else { return }
mountCheckLock.lock()
guard !mountCheckInProgress else {
mountCheckLock.unlock()
return
}
mountCheckInProgress = true
mountCheckLock.unlock()
DispatchQueue.global(qos: .utility).async {
let status = checkMountStatus()
self.mountCheckLock.lock()
self.mountCheckInProgress = false
self.mountCheckLock.unlock()
let mounted = isMounted()
DispatchQueue.main.async {
if status != .unreachable {
self.coolisMounted = status == .mounted
}
self.coolisMounted = mounted
}
}
}
func progressCallback(progress: size_t, total: size_t, context: UnsafeMutableRawPointer?) {
let percentage = Double(progress) / Double(total) * 100.0
DispatchQueue.main.async {
self.mountProgress = percentage
}
}
func pubMount() {
guard TunnelManager.shared.isConnected, DeveloperDiskImageService.filesAreReady else { return }
mountLock.lock()
guard !mountInProgress else {
mountLock.unlock()
return
}
mountInProgress = true
mountLock.unlock()
let thread = Thread { [weak self] in
self?.mount()
}
thread.qualityOfService = .background
thread.name = "mounting"
DispatchQueue.main.async {
self.mountingThread = thread
}
thread.start()
mount()
}
private func mount() {
switch checkMountStatus() {
case .mounted:
finishMount {
self.coolisMounted = true
}
return
case .unreachable:
finishMount()
return
case .notMounted:
break
}
guard isPairing() else {
finishMount()
return
}
let mountError = mountDeveloperDiskImage(from: DeveloperDiskImageService.directoryURL.path)
let mounted = mountError == nil || checkMountStatus() == .mounted
finishMount {
if mounted {
self.coolisMounted = true
self.checkforMounted()
} else if let mountError {
LogManager.shared.addErrorLog("Failed to mount DDI: \(mountError)")
showAlert(title: "DDI Mount Failed", message: mountError, showOk: true, showTryAgain: true) { shouldTryAgain in
if shouldTryAgain {
self.pubMount()
}
}
}
}
}
private func finishMount(_ completion: @escaping () -> Void = {}) {
let currentlyMounted = isMounted()
DispatchQueue.main.async {
self.mountingThread = nil
self.mountLock.lock()
self.mountInProgress = false
self.mountLock.unlock()
completion()
self.coolisMounted = currentlyMounted
}
guard isPairing(), !currentlyMounted else {
return
}
if let mountingThread {
mountingThread.cancel()
self.mountingThread = nil
}
let thread = Thread { [weak self] in
guard let self else { return }
let mountError = mountPersonalDDI(
imagePath: URL.documentsDirectory.appendingPathComponent("DDI/Image.dmg").path,
trustcachePath: URL.documentsDirectory.appendingPathComponent("DDI/Image.dmg.trustcache").path,
manifestPath: URL.documentsDirectory.appendingPathComponent("DDI/BuildManifest.plist").path
)
DispatchQueue.main.async {
if let mountError {
showAlert(title: "DDI Mount Failed", message: mountError, showOk: true, showTryAgain: true) { shouldTryAgain in
if shouldTryAgain {
self.mount()
}
}
} else {
self.coolisMounted = true
self.checkforMounted()
}
self.mountingThread = nil
}
}
thread.qualityOfService = .background
thread.name = "mounting"
thread.start()
mountingThread = thread
}
}

View file

@ -13,6 +13,10 @@ typealias RsdHandshakeHandle = OpaquePointer
typealias ImageMounterHandle = OpaquePointer
typealias LockdowndClientHandle = OpaquePointer
func progressCallback(progress: size_t, total: size_t, context: UnsafeMutableRawPointer?) {
MountingProgress.shared.progressCallback(progress: progress, total: total, context: context)
}
enum MountCheckResult {
case mounted
case notMounted
@ -25,25 +29,18 @@ func isMounted() -> Bool {
func checkMountStatus() -> MountCheckResult {
do {
return try JITEnableContext.shared.isDeveloperDiskImageMounted() ? .mounted : .notMounted
let result = try JITEnableContext.shared.getMountedDeviceCount()
return result > 0 ? .mounted : .notMounted
} catch {
LogManager.shared.addErrorLog("Failed to check DDI mount status: \(error.localizedDescription)")
return .unreachable
}
}
func mountDeveloperDiskImage(from directoryPath: String) -> String? {
func mountPersonalDDI(imagePath: String, trustcachePath: String, manifestPath: String) -> String? {
do {
if DeveloperDiskImageService.usesCryptexDDI {
try JITEnableContext.shared.installCryptexDDI(from: directoryPath)
} else {
try JITEnableContext.shared.mountPersonalDDI(
withImagePath: URL(fileURLWithPath: directoryPath).appendingPathComponent("Image.dmg").path,
trustcachePath: URL(fileURLWithPath: directoryPath).appendingPathComponent("Image.dmg.trustcache").path,
manifestPath: URL(fileURLWithPath: directoryPath).appendingPathComponent("BuildManifest.plist").path
)
}
try JITEnableContext.shared.mountPersonalDDI(withImagePath: imagePath, trustcachePath: trustcachePath, manifestPath: manifestPath)
} catch {
LogManager.shared.addErrorLog("Failed to mount DDI: \(error.localizedDescription)")
return error.localizedDescription
}
return nil

View file

@ -20,26 +20,13 @@ final class InstalledAppsViewModel: ObservableObject {
private let cacheKeyDebuggable = "cachedDebuggableApps"
private let cacheKeyNonDebuggable = "cachedNonDebuggableApps"
private let cacheKeySystem = "cachedSystemApps"
private var cancellables: Set<AnyCancellable> = []
private var refreshInProgress = false
private var refreshPending = false
init() {
loadCachedApps()
TunnelManager.shared.$isConnected
.removeDuplicates()
.filter { $0 }
.sink { [weak self] _ in self?.refreshAppLists() }
.store(in: &cancellables)
refreshAppLists()
}
func refreshAppLists() {
guard !refreshInProgress else {
refreshPending = true
return
}
refreshInProgress = true
isLoading = true
lastError = nil
@ -55,11 +42,6 @@ final class InstalledAppsViewModel: ObservableObject {
debuggable: debuggable,
hiddenSystem: hiddenSystem
)
self.cacheApps(
debuggable: debuggable,
nonDebuggable: classifiedApps.nonDebuggable,
system: classifiedApps.system
)
DispatchQueue.main.async {
self.apply(
@ -67,26 +49,22 @@ final class InstalledAppsViewModel: ObservableObject {
nonDebuggable: classifiedApps.nonDebuggable,
system: classifiedApps.system
)
self.finishRefresh()
self.isLoading = false
self.cacheApps(
debuggable: debuggable,
nonDebuggable: classifiedApps.nonDebuggable,
system: classifiedApps.system
)
}
} catch {
DispatchQueue.main.async {
self.isLoading = false
self.lastError = error.localizedDescription
self.finishRefresh()
}
}
}
}
private func finishRefresh() {
refreshInProgress = false
isLoading = false
guard refreshPending else { return }
refreshPending = false
refreshAppLists()
}
func displayName(for bundleID: String) -> String? {
debuggableApps[bundleID] ?? systemApps[bundleID] ?? nonDebuggableApps[bundleID]
}

View file

@ -11,6 +11,7 @@ final class BackgroundAudioManager {
private var engine = AVAudioEngine()
private var player = AVAudioPlayerNode()
private var isRunning = false
private var persistentEnabled = false
private var activityCount = 0
private var healthCheckTimer: Timer?
@ -29,7 +30,13 @@ final class BackgroundAudioManager {
)
}
func configurationDidChange() {
func start() {
persistentEnabled = true
refreshRunningState()
}
func stop() {
persistentEnabled = false
refreshRunningState()
}
@ -44,7 +51,7 @@ final class BackgroundAudioManager {
}
private func refreshRunningState() {
let shouldRun = activityCount > 0 && UserDefaults.standard.bool(forKey: "keepAliveAudio")
let shouldRun = persistentEnabled || (activityCount > 0 && UserDefaults.standard.bool(forKey: "keepAliveAudio"))
guard shouldRun != isRunning else {
if shouldRun {
recoverIfNeeded()

View file

@ -4,7 +4,6 @@
//
import CoreLocation
import UIKit
final class BackgroundLocationManager: NSObject, CLLocationManagerDelegate {
static let shared = BackgroundLocationManager()
@ -12,7 +11,6 @@ final class BackgroundLocationManager: NSObject, CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
private var isRunning = false
private var activityCount = 0
private var isRequestingAuthorization = false
private override init() {
super.init()
@ -23,44 +21,8 @@ final class BackgroundLocationManager: NSObject, CLLocationManagerDelegate {
locationManager.pausesLocationUpdatesAutomatically = false
}
func configurationDidChange() {
refreshRunningState()
}
func requestStart() {
activityCount += 1
refreshRunningState()
}
func requestStop() {
activityCount = max(activityCount - 1, 0)
refreshRunningState()
}
func requestAuthorizationIfNeeded() {
switch locationManager.authorizationStatus {
case .authorizedAlways, .authorizedWhenInUse:
return
case .notDetermined:
isRequestingAuthorization = true
locationManager.requestAlwaysAuthorization()
case .denied, .restricted:
restoreAudioFallback()
@unknown default:
restoreAudioFallback()
}
}
private func refreshRunningState() {
let shouldRun = activityCount > 0 && UserDefaults.standard.bool(forKey: "keepAliveLocation")
guard shouldRun != isRunning else { return }
isRunning = shouldRun
guard shouldRun else {
locationManager.stopUpdatingLocation()
return
}
func start() {
isRunning = true
switch locationManager.authorizationStatus {
case .authorizedAlways:
locationManager.startUpdatingLocation()
@ -68,54 +30,37 @@ final class BackgroundLocationManager: NSObject, CLLocationManagerDelegate {
locationManager.requestAlwaysAuthorization()
case .notDetermined:
locationManager.requestAlwaysAuthorization()
case .denied, .restricted:
restoreAudioFallback()
@unknown default:
restoreAudioFallback()
default:
break
}
}
func stop() {
isRunning = false
locationManager.stopUpdatingLocation()
}
func requestStart() {
activityCount += 1
if activityCount == 1, UserDefaults.standard.bool(forKey: "keepAliveLocation") {
start()
}
}
func requestStop() {
activityCount = max(activityCount - 1, 0)
if activityCount == 0 {
stop()
}
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
guard isRunning else { return }
switch manager.authorizationStatus {
case .authorizedAlways, .authorizedWhenInUse:
isRequestingAuthorization = false
if isRunning {
manager.startUpdatingLocation()
}
case .denied, .restricted:
if isRunning || isRequestingAuthorization {
isRequestingAuthorization = false
restoreAudioFallback()
}
case .notDetermined:
return
@unknown default:
if isRunning || isRequestingAuthorization {
isRequestingAuthorization = false
restoreAudioFallback()
}
}
}
private func restoreAudioFallback() {
let defaults = UserDefaults.standard
defaults.set(false, forKey: "keepAliveLocation")
defaults.set(true, forKey: "keepAliveAudio")
refreshRunningState()
BackgroundAudioManager.shared.configurationDidChange()
showAlert(
title: "Location Access Refused",
message: "Location access was refused. Open Settings to enable it.",
showOk: false,
showTryAgain: true,
primaryButtonText: "Settings"
) { openSettings in
guard openSettings,
let settingsURL = URL(string: UIApplication.openSettingsURLString) else {
return
}
UIApplication.shared.open(settingsURL)
manager.startUpdatingLocation()
default:
break
}
}

View file

@ -7,17 +7,6 @@ import Foundation
final class DeveloperDiskImageService {
static let shared = DeveloperDiskImageService()
static let directoryURL = URL.documentsDirectory.appendingPathComponent("DDI", isDirectory: true)
static var filesAreReady: Bool {
downloadItems.allSatisfy {
FileManager.default.fileExists(atPath: directoryURL.appendingPathComponent($0.fileName).path)
} && (try? String(contentsOf: mountMethodURL, encoding: .utf8)) == mountMethod.rawValue
}
static var usesCryptexDDI: Bool {
mountMethod == .cryptex
}
private let fileManager: FileManager
private let session: URLSession
@ -28,21 +17,13 @@ final class DeveloperDiskImageService {
}
func downloadMissingFiles() async throws {
let installedMethod = try? String(contentsOf: Self.mountMethodURL, encoding: .utf8)
let replaceFiles = installedMethod != Self.mountMethod.rawValue
if !Self.usesCryptexDDI {
try removeCryptexOnlyFiles()
}
for item in Self.downloadItems {
let destinationURL = Self.directoryURL.appendingPathComponent(item.fileName)
guard replaceFiles || !fileManager.fileExists(atPath: destinationURL.path) else {
let destinationURL = URL.documentsDirectory.appendingPathComponent(item.relativePath)
guard !fileManager.fileExists(atPath: destinationURL.path) else {
continue
}
try await downloadFile(from: item.urlString, to: destinationURL)
}
try Self.mountMethod.rawValue.write(to: Self.mountMethodURL, atomically: true, encoding: .utf8)
}
func downloadFile(from urlString: String, to destinationURL: URL) async throws {
@ -75,18 +56,8 @@ final class DeveloperDiskImageService {
var completedStages = 0.0
progressHandler?(0.0, "Removing existing DDI files...")
if fileManager.fileExists(atPath: Self.mountMethodURL.path) {
try fileManager.removeItem(at: Self.mountMethodURL)
}
if !Self.usesCryptexDDI {
try removeCryptexOnlyFiles()
}
let completionMarker = Self.directoryURL.appendingPathComponent("Image.dmg.root_hash")
if fileManager.fileExists(atPath: completionMarker.path) {
try fileManager.removeItem(at: completionMarker)
}
for item in Self.downloadItems {
let fileURL = Self.directoryURL.appendingPathComponent(item.fileName)
let fileURL = URL.documentsDirectory.appendingPathComponent(item.relativePath)
if fileManager.fileExists(atPath: fileURL.path) {
try fileManager.removeItem(at: fileURL)
}
@ -97,100 +68,37 @@ final class DeveloperDiskImageService {
for item in Self.downloadItems {
progressHandler?(completedStages / totalStages, "Downloading \(item.name)...")
let destinationURL = Self.directoryURL.appendingPathComponent(item.fileName)
let destinationURL = URL.documentsDirectory.appendingPathComponent(item.relativePath)
try await downloadFile(from: item.urlString, to: destinationURL)
completedStages += 1.0
progressHandler?(completedStages / totalStages, "\(item.name) ready")
}
try Self.mountMethod.rawValue.write(to: Self.mountMethodURL, atomically: true, encoding: .utf8)
progressHandler?(1.0, "DDI download complete.")
}
private func removeCryptexOnlyFiles() throws {
for fileName in Self.cryptexOnlyFileNames {
let fileURL = Self.directoryURL.appendingPathComponent(fileName)
if fileManager.fileExists(atPath: fileURL.path) {
try fileManager.removeItem(at: fileURL)
}
}
}
private static var mountMethod: DDIMountMethod {
let version = ProcessInfo.processInfo.operatingSystemVersion
return version.majorVersion > 26 || (version.majorVersion == 26 && version.minorVersion >= 4)
? .cryptex
: .personalized
}
private static let mountMethodURL = directoryURL.appendingPathComponent("MountMethod")
private static let cryptexOnlyFileNames = ["Image.dmg.cryptex_info", "Image.dmg.root_hash"]
private static var downloadItems: [DDIDownloadItem] {
switch mountMethod {
case .cryptex:
return cryptexDownloadItems
case .personalized:
return personalizedDownloadItems
}
}
private static let cryptexDownloadItems: [DDIDownloadItem] = [
private static let downloadItems: [DDIDownloadItem] = [
.init(
name: "Build Manifest",
fileName: "BuildManifest.plist",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/BuildManifest.plist"
),
.init(
name: "Image",
fileName: "Image.dmg",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg"
),
.init(
name: "TrustCache",
fileName: "Image.dmg.trustcache",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg.trustcache"
),
.init(
name: "Cryptex Info",
fileName: "Image.dmg.cryptex_info",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg.cryptex_info"
),
.init(
name: "Root Hash",
fileName: "Image.dmg.root_hash",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg.root_hash"
)
]
private static let personalizedDownloadItems: [DDIDownloadItem] = [
.init(
name: "Build Manifest",
fileName: "BuildManifest.plist",
relativePath: "DDI/BuildManifest.plist",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/BuildManifest.plist"
),
.init(
name: "Image",
fileName: "Image.dmg",
relativePath: "DDI/Image.dmg",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg"
),
.init(
name: "TrustCache",
fileName: "Image.dmg.trustcache",
relativePath: "DDI/Image.dmg.trustcache",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg.trustcache"
)
]
}
private enum DDIMountMethod: String {
case cryptex
case personalized
}
private struct DDIDownloadItem {
let name: String
let fileName: String
let relativePath: String
let urlString: String
}

View file

@ -1,66 +0,0 @@
//
// IdeviceLogFileManager.swift
// StikDebug
//
import Foundation
final class IdeviceLogFileManager {
static let shared = IdeviceLogFileManager()
static let logURL = URL.documentsDirectory.appendingPathComponent("idevice_log.txt")
private static let maximumSize: UInt64 = 20 * 1024 * 1024
private static let retainedSize: UInt64 = 10 * 1024 * 1024
private static let checkInterval: TimeInterval = 5
private let queue = DispatchQueue(label: "com.stik.ideviceLogFile", qos: .utility)
private var timer: DispatchSourceTimer?
private init() {}
func prepareForLogging() {
compactIfNeeded()
queue.async {
guard self.timer == nil else { return }
let timer = DispatchSource.makeTimerSource(queue: self.queue)
timer.schedule(deadline: .now() + Self.checkInterval, repeating: Self.checkInterval)
timer.setEventHandler { [weak self] in
self?.compactIfNeeded()
}
self.timer = timer
timer.resume()
}
}
private func compactIfNeeded() {
let fileManager = FileManager.default
guard let attributes = try? fileManager.attributesOfItem(atPath: Self.logURL.path),
let fileSize = attributes[.size] as? NSNumber,
fileSize.uint64Value > Self.maximumSize,
let handle = try? FileHandle(forUpdating: Self.logURL) else {
return
}
defer { try? handle.close() }
do {
let endOffset = try handle.seekToEnd()
let startOffset = endOffset > Self.retainedSize ? endOffset - Self.retainedSize : 0
try handle.seek(toOffset: startOffset)
var recentData = try handle.readToEnd() ?? Data()
if startOffset > 0, let newlineIndex = recentData.firstIndex(of: 0x0A) {
recentData.removeSubrange(recentData.startIndex...newlineIndex)
}
try handle.truncate(atOffset: 0)
try handle.seek(toOffset: 0)
try handle.write(contentsOf: recentData)
try handle.synchronize()
} catch {
return
}
}
}

View file

@ -1,68 +0,0 @@
import Foundation
import CoreLocation
import Combine
final class LocationSimulationSession: ObservableObject {
static let shared = LocationSimulationSession()
@Published private(set) var isActive = false
@Published private(set) var coordinate: CLLocationCoordinate2D?
@Published private(set) var routeCoordinates: [CLLocationCoordinate2D]?
private var resendTimer: Timer?
private var routeID: UUID?
private init() {}
func start(at coordinate: CLLocationCoordinate2D) {
self.coordinate = coordinate
if !isActive {
isActive = true
BackgroundAudioManager.shared.requestStart()
BackgroundLocationManager.shared.requestStart()
}
}
func startResending(at coordinate: CLLocationCoordinate2D, _ operation: @escaping () -> Void) {
start(at: coordinate)
resendTimer?.invalidate()
let timer = Timer(timeInterval: 4, repeats: true) { _ in operation() }
RunLoop.main.add(timer, forMode: .common)
resendTimer = timer
}
func startRoute(at coordinate: CLLocationCoordinate2D, coordinates: [CLLocationCoordinate2D]) -> UUID {
let routeID = UUID()
self.routeID = routeID
routeCoordinates = coordinates
start(at: coordinate)
return routeID
}
func isCurrentRoute(_ routeID: UUID) -> Bool {
self.routeID == routeID
}
func clearRoute() {
routeID = nil
routeCoordinates = nil
}
func pauseResending() {
resendTimer?.invalidate()
resendTimer = nil
}
func updateCoordinate(_ coordinate: CLLocationCoordinate2D) {
self.coordinate = coordinate
}
func stop() {
pauseResending()
clearRoute()
guard isActive else { return }
isActive = false
coordinate = nil
BackgroundAudioManager.shared.requestStop()
BackgroundLocationManager.shared.requestStop()
}
}

View file

@ -70,7 +70,12 @@ final class TunnelManager: ObservableObject {
}
private func mountDeveloperDiskImageIfNeeded() {
guard DeveloperDiskImageService.filesAreReady else { return }
let trustcachePath = URL.documentsDirectory.appendingPathComponent("DDI/Image.dmg.trustcache").path
guard FileManager.default.fileExists(atPath: trustcachePath),
!MountingProgress.shared.coolisMounted,
MountingProgress.shared.mountingThread == nil else {
return
}
MountingProgress.shared.pubMount()
}

View file

@ -14,17 +14,13 @@ extension ScriptStore {
appNames: [
"Amethyst",
"MeloNX",
"Melo",
"XeniOS",
"MeloCafé",
"MeloCafe",
"Manic EMU",
"Manic",
"DukeX",
"TachyonU",
"touchHLE",
"HyperHLE",
"Applesauce",
"RPCS3",
"AetherPS4",
"Husk"
],
resource: ScriptResource(resourceName: "universal", fileName: "universal.js")
),

View file

@ -7,14 +7,14 @@ import Foundation
import UniformTypeIdentifiers
enum PairingFileStore {
static let fileName = "pairingFile.plist"
static let fileName = "rp_pairing_file.plist"
static let supportedContentTypes: [UTType] = [
UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!,
UTType(filenameExtension: "mobiledevicepair", conformingTo: .data)!,
.propertyList
]
private static let legacyFileName = "rp_pairing_file.plist"
private static let legacyFileName = "pairingFile.plist"
static var url: URL {
directoryURL.appendingPathComponent(fileName)
@ -25,31 +25,31 @@ enum PairingFileStore {
let destination = url
try? fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
if let documentURL = documentSourceURL(fileManager: fileManager) {
if !fileManager.fileExists(atPath: destination.path) ||
!fileManager.contentsEqual(atPath: documentURL.path, andPath: destination.path) {
try? replaceItem(at: destination, with: documentURL, fileManager: fileManager)
protectPairingFile(at: destination, fileManager: fileManager)
}
guard !fileManager.fileExists(atPath: destination.path) else {
removeLegacyCopies(fileManager: fileManager)
return destination
}
if !fileManager.fileExists(atPath: destination.path) {
migrateLegacyCopy(to: destination, fileManager: fileManager)
}
migrateLegacyCopy(to: destination, fileManager: fileManager)
return destination
}
static func replace(with sourceURL: URL, fileManager: FileManager = .default) throws {
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
let destination = prepareURL(fileManager: fileManager)
let documentsDestination = documentsURL
if sourceURL.standardizedFileURL != documentsDestination.standardizedFileURL {
try replaceItem(at: documentsDestination, with: sourceURL, fileManager: fileManager)
let tempURL = directoryURL.appendingPathComponent(UUID().uuidString + ".tmp")
try? fileManager.removeItem(at: tempURL)
try fileManager.copyItem(at: sourceURL, to: tempURL)
defer { try? fileManager.removeItem(at: tempURL) }
if fileManager.fileExists(atPath: destination.path) {
_ = try fileManager.replaceItemAt(destination, withItemAt: tempURL)
} else {
try fileManager.moveItem(at: tempURL, to: destination)
}
try replaceItem(at: url, with: documentsDestination, fileManager: fileManager)
protectPairingFile(at: url, fileManager: fileManager)
removeLegacyCopies(fileManager: fileManager)
protectPairingFile(at: destination, fileManager: fileManager)
}
static func importFromPicker(_ sourceURL: URL, fileManager: FileManager = .default) throws {
@ -83,33 +83,19 @@ enum PairingFileStore {
private static var legacyURLs: [URL] {
[
directoryURL.appendingPathComponent(legacyFileName),
documentsURL,
URL.documentsDirectory.appendingPathComponent(fileName),
URL.documentsDirectory.appendingPathComponent(legacyFileName)
]
}
private static var documentsURL: URL {
URL.documentsDirectory.appendingPathComponent(fileName)
}
private static func documentSourceURL(fileManager: FileManager) -> URL? {
[documentsURL, URL.documentsDirectory.appendingPathComponent(legacyFileName)]
.filter { fileManager.fileExists(atPath: $0.path) }
.max {
let firstDate = (try? $0.resourceValues(forKeys: [.contentModificationDateKey]))?.contentModificationDate ?? .distantPast
let secondDate = (try? $1.resourceValues(forKeys: [.contentModificationDateKey]))?.contentModificationDate ?? .distantPast
return firstDate < secondDate
}
}
private static func migrateLegacyCopy(to destination: URL, fileManager: FileManager) {
for legacyURL in legacyURLs where fileManager.fileExists(atPath: legacyURL.path) {
do {
try replaceItem(at: destination, with: legacyURL, fileManager: fileManager)
try fileManager.moveItem(at: legacyURL, to: destination)
} catch {
if let data = try? Data(contentsOf: legacyURL) {
try? data.write(to: destination, options: .atomic)
try? fileManager.removeItem(at: legacyURL)
}
}
@ -118,21 +104,6 @@ enum PairingFileStore {
}
}
private static func replaceItem(at destination: URL, with source: URL, fileManager: FileManager) throws {
let temporary = destination
.deletingLastPathComponent()
.appendingPathComponent(UUID().uuidString + ".tmp")
try? fileManager.removeItem(at: temporary)
try fileManager.copyItem(at: source, to: temporary)
defer { try? fileManager.removeItem(at: temporary) }
if fileManager.fileExists(atPath: destination.path) {
_ = try fileManager.replaceItemAt(destination, withItemAt: temporary)
} else {
try fileManager.moveItem(at: temporary, to: destination)
}
}
private static func removeLegacyCopies(fileManager: FileManager) {
for legacyURL in legacyURLs where fileManager.fileExists(atPath: legacyURL.path) {
try? fileManager.removeItem(at: legacyURL)

View file

@ -15,9 +15,5 @@ extension UserDefaults {
static let defaultScriptName = "DefaultScriptName"
static let defaultScriptNameValue = ""
static let targetDeviceIP = "TunnelDeviceIP"
/// Attaches processes with MallocGuardEdges and MallocScribble.
static let mallocDebug = "enableMallocDebug"
static let mallocGuardEdges = "enableMallocGuardEdges"
static let mallocScribble = "enableMallocScribble"
}
}

View file

@ -266,7 +266,7 @@ struct ConsoleLogsView: View {
@ViewBuilder
private var exportMenuOption: some View {
let logURL = IdeviceLogFileManager.logURL
let logURL: URL = URL.documentsDirectory.appendingPathComponent("idevice_log.txt")
if FileManager.default.fileExists(atPath: logURL.path) {
ShareLink(
item: logURL,
@ -309,7 +309,7 @@ struct ConsoleLogsView: View {
guard !isLoadingLogs else { return }
isLoadingLogs = true
let logPath = IdeviceLogFileManager.logURL.path
let logPath = URL.documentsDirectory.appendingPathComponent("idevice_log.txt").path
guard FileManager.default.fileExists(atPath: logPath) else {
await MainActor.run {
@ -368,7 +368,7 @@ struct ConsoleLogsView: View {
guard !isLoadingLogs else { return }
isLoadingLogs = true
let logPath = IdeviceLogFileManager.logURL.path
let logPath = URL.documentsDirectory.appendingPathComponent("idevice_log.txt").path
let previousCount = lastProcessedLineCount
guard FileManager.default.fileExists(atPath: logPath) else {

View file

@ -12,14 +12,17 @@ struct HomeView: View {
@AppStorage("bundleID") private var bundleID: String = ""
@AppStorage(UserDefaults.Keys.confirmExternalJITRequests) private var confirmExternalJITRequests = true
@ObservedObject private var mounting = MountingProgress.shared
@State private var hasAppeared = false
@State private var pendingJITEnableConfiguration: JITEnableConfiguration?
@State private var isShowingPairingFilePicker = false
@State private var debugFeedback: DebugFeedback?
@State private var pendingExternalURLAction: HomeExternalAction?
@State private var pendingLiveContainerApp: LiveContainerApp?
@State private var scriptRunModel: RunJSViewModel?
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
private struct DebugFeedback: Identifiable {
let id = UUID()
let message: String
@ -27,18 +30,11 @@ struct HomeView: View {
let isWorking: Bool
}
private struct LiveContainerApp {
let bundleID: String
let name: String
}
var body: some View {
InstalledAppsListView(onSelectApp: { selectedBundle, selectedName in
if selectedName.localizedCaseInsensitiveContains("LiveContainer") {
pendingLiveContainerApp = LiveContainerApp(bundleID: selectedBundle, name: selectedName)
} else {
enableJITForSelectedApp(bundleID: selectedBundle, name: selectedName)
}
bundleID = selectedBundle
Haptics.medium()
startJITInBackground(bundleID: selectedBundle, displayName: selectedName)
}, showDoneButton: false, onImportPairingFile: { isShowingPairingFilePicker = true })
.overlay(alignment: .bottom) {
if let debugFeedback {
@ -49,6 +45,9 @@ struct HomeView: View {
}
.onAppear(perform: handleAppear)
.onReceive(NotificationCenter.default.publisher(for: .intentJSScriptReady), perform: handleScriptReadyNotification)
.onReceive(timer) { _ in
refreshMountStatusIfNeeded()
}
.onOpenURL { url in
handleExternalURL(url)
}
@ -75,28 +74,6 @@ struct HomeView: View {
} message: { action in
Text(action.message)
}
.alert(
"Enable JIT for LiveContainer?",
isPresented: Binding(
get: { pendingLiveContainerApp != nil },
set: { isPresented in
if !isPresented {
pendingLiveContainerApp = nil
}
}
),
presenting: pendingLiveContainerApp
) { app in
Button("Cancel", role: .cancel) {
pendingLiveContainerApp = nil
}
Button("Proceed") {
pendingLiveContainerApp = nil
enableJITForSelectedApp(bundleID: app.bundleID, name: app.name)
}
} message: { _ in
Text("StikDebug isn't meant to enable JIT for LiveContainer manually. Select StikDebug in LiveContainer settings, enable \"Run with JIT\" for your app, then open your app normally. It will automatically jump back here.")
}
.fileImporter(
isPresented: $isShowingPairingFilePicker,
allowedContentTypes: PairingFileStore.supportedContentTypes,
@ -115,14 +92,9 @@ struct HomeView: View {
}
}
private func enableJITForSelectedApp(bundleID selectedBundle: String, name: String) {
bundleID = selectedBundle
Haptics.medium()
startJITInBackground(bundleID: selectedBundle, displayName: name)
}
private func handleAppear() {
startTunnelInBackground()
MountingProgress.shared.checkforMounted()
hasAppeared = true
if let config = pendingJITEnableConfiguration {
@ -145,6 +117,13 @@ struct HomeView: View {
scriptRunModel = model
}
private func refreshMountStatusIfNeeded() {
guard mounting.mountingThread == nil, !mounting.coolisMounted else {
return
}
MountingProgress.shared.checkforMounted()
}
private func importPairingFile(_ result: Result<URL, Error>) {
switch result {
case .success(let url):

View file

@ -83,9 +83,7 @@ struct InstalledAppsListView: View {
handleLoadingChange(isLoading)
}
.onReceive(NotificationCenter.default.publisher(for: .pairingFileImported)) { _ in
if TunnelManager.shared.isConnected {
viewModel.refreshAppLists()
}
viewModel.refreshAppLists()
}
}

View file

@ -7,7 +7,6 @@
import SwiftUI
import Foundation
import CoreLocation
private enum ExternalLocationAction: Identifiable {
case simulate(URL, Double, Double)
@ -224,22 +223,7 @@ struct MainTabView: View {
DispatchQueue.main.async {
if code == 0 {
LocationSimulationSession.shared.clearRoute()
LocationSimulationSession.shared.startResending(
at: CLLocationCoordinate2D(
latitude: coordinate.latitude,
longitude: coordinate.longitude
)
) {
LocationSimulationCommandQueue.shared.async {
_ = simulate_location(
DeviceConnectionContext.targetIPAddress,
coordinate.latitude,
coordinate.longitude,
pairingFile.path
)
}
}
BackgroundLocationManager.shared.requestStart()
LogManager.shared.addInfoLog(
String(format: "Simulated location from URL: %.6f, %.6f", coordinate.latitude, coordinate.longitude)
)
@ -259,7 +243,7 @@ struct MainTabView: View {
let code = clear_simulated_location()
DispatchQueue.main.async {
if code == 0 {
LocationSimulationSession.shared.stop()
BackgroundLocationManager.shared.requestStop()
LogManager.shared.addInfoLog("Cleared simulated location from URL")
} else {
showAlert(

View file

@ -724,11 +724,11 @@ final class LocationSearchCompleter: NSObject, ObservableObject, MKLocalSearchCo
}
struct LocationSimulationView: View {
@ObservedObject private var simulationSession = LocationSimulationSession.shared
@State private var coordinate: CLLocationCoordinate2D?
@State private var position: MapCameraPosition = .userLocation(fallback: .automatic)
@State private var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
@State private var resendTimer: Timer?
@State private var routeLoadTask: Task<Void, Never>?
@State private var routeSpeedPrefetchTask: Task<Void, Never>?
@State private var routePlaybackTask: Task<Void, Never>?
@ -788,11 +788,11 @@ struct LocationSimulationView: View {
}
private var hasActiveSimulation: Bool {
simulationSession.isActive || simulatedCoordinate != nil || routePlaybackTask != nil
simulatedCoordinate != nil || routePlaybackTask != nil
}
private var isRouteRunning: Bool {
routePlaybackTask != nil || simulationSession.routeCoordinates != nil
routePlaybackTask != nil
}
private var hasRouteContext: Bool {
@ -801,12 +801,7 @@ struct LocationSimulationView: View {
routePlan != nil ||
isLoadingRoute ||
isPrefetchingRouteSpeeds ||
routePlaybackCoordinate != nil ||
simulationSession.routeCoordinates != nil
}
private var displayedRouteCoordinate: CLLocationCoordinate2D? {
simulationSession.routeCoordinates == nil ? routePlaybackCoordinate : simulationSession.coordinate
routePlaybackCoordinate != nil
}
private var routeSummaryText: String? {
@ -823,9 +818,6 @@ struct LocationSimulationView: View {
}
private var routeStatusText: String {
if simulationSession.routeCoordinates != nil {
return "Route simulation active."
}
if isLoadingRoute {
return "Calculating route…"
}
@ -900,8 +892,8 @@ struct LocationSimulationView: View {
Marker("End", coordinate: routeEndCoordinate)
.tint(.red)
}
if let displayedRouteCoordinate {
Marker("Current", coordinate: displayedRouteCoordinate)
if let routePlaybackCoordinate {
Marker("Current", coordinate: routePlaybackCoordinate)
.tint(.blue)
}
} else if let coordinate {
@ -1033,28 +1025,17 @@ struct LocationSimulationView: View {
}
.onAppear {
loadBookmarks()
if let routeCoordinates = simulationSession.routeCoordinates {
coordinate = nil
routePolyline = makeRoutePolyline(for: routeCoordinates)
routeStartSelection = routeCoordinates.first.map {
RouteSearchSelection(title: "Route Start", coordinate: $0)
}
routeEndSelection = routeCoordinates.last.map {
RouteSearchSelection(title: "Route End", coordinate: $0)
}
if let routePolyline {
position = .rect(routePolyline.boundingMapRect)
}
} else if let activeCoordinate = simulationSession.coordinate {
coordinate = activeCoordinate
simulatedCoordinate = activeCoordinate
}
}
.onDisappear {
routeLoadTask?.cancel()
routeLoadTask = nil
routeSpeedPrefetchTask?.cancel()
routeSpeedPrefetchTask = nil
cancelRoutePlayback(resetMarker: true)
stopResendLoop()
if backgroundTaskID != .invalid {
BackgroundLocationManager.shared.requestStop()
}
endBackgroundTask()
}
}
@ -1316,14 +1297,14 @@ struct LocationSimulationView: View {
) {
routePlaybackCoordinate = nil
beginBackgroundTask()
LocationSimulationSession.shared.clearRoute()
startResendLoop(with: coord)
BackgroundLocationManager.shared.requestStart()
}
}
private func simulateRoute() {
guard pairingExists,
let routePlan,
routePlan != nil,
let firstCoordinate = routePlaybackSamples.first?.coordinate,
!isBusy else {
return
@ -1338,13 +1319,10 @@ struct LocationSimulationView: View {
operation: { locationUpdateCode(for: firstCoordinate) }
) {
beginBackgroundTask()
let routeID = LocationSimulationSession.shared.startRoute(
at: firstCoordinate,
coordinates: routePlan.displayCoordinates
)
BackgroundLocationManager.shared.requestStart()
simulatedCoordinate = nil
routePlaybackCoordinate = firstCoordinate
startRoutePlayback(routeID: routeID)
startRoutePlayback()
}
}
@ -1378,14 +1356,13 @@ struct LocationSimulationView: View {
routeSpeedPrefetchTask = nil
cancelRoutePlayback(resetMarker: true)
stopResendLoop()
LocationSimulationSession.shared.clearRoute()
runLocationCommand(
errorTitle: "Clear Failed",
errorMessage: { code in "Could not clear simulated location (error \(code))." },
operation: clear_simulated_location
) {
endBackgroundTask()
LocationSimulationSession.shared.stop()
BackgroundLocationManager.shared.requestStop()
}
}
@ -1402,15 +1379,18 @@ struct LocationSimulationView: View {
private func startResendLoop(with coordinate: CLLocationCoordinate2D) {
simulatedCoordinate = coordinate
LocationSimulationSession.shared.startResending(at: coordinate) {
resendTimer?.invalidate()
resendTimer = Timer.scheduledTimer(withTimeInterval: 4, repeats: true) { _ in
guard let simulatedCoordinate else { return }
LocationSimulationCommandQueue.shared.async {
_ = locationUpdateCode(for: coordinate)
_ = locationUpdateCode(for: simulatedCoordinate)
}
}
}
private func stopResendLoop() {
LocationSimulationSession.shared.pauseResending()
resendTimer?.invalidate()
resendTimer = nil
simulatedCoordinate = nil
}
@ -1540,19 +1520,15 @@ struct LocationSimulationView: View {
}
}
private func startRoutePlayback(routeID: UUID) {
private func startRoutePlayback() {
routePlaybackTask = Task {
var lastSuccessfulCoordinate = routePlaybackSamples.first?.coordinate
for sample in routePlaybackSamples.dropFirst() {
try? await Task.sleep(for: .seconds(sample.delayFromPrevious))
guard !Task.isCancelled,
LocationSimulationSession.shared.isCurrentRoute(routeID) else {
return
}
guard !Task.isCancelled else { return }
let code = await sendLocationUpdate(for: sample.coordinate)
guard LocationSimulationSession.shared.isCurrentRoute(routeID) else { return }
guard code == 0 else {
await MainActor.run {
routePlaybackTask = nil
@ -1570,12 +1546,10 @@ struct LocationSimulationView: View {
lastSuccessfulCoordinate = sample.coordinate
await MainActor.run {
routePlaybackCoordinate = sample.coordinate
LocationSimulationSession.shared.updateCoordinate(sample.coordinate)
}
}
await MainActor.run {
guard LocationSimulationSession.shared.isCurrentRoute(routeID) else { return }
routePlaybackTask = nil
if let lastSuccessfulCoordinate {
routePlaybackCoordinate = lastSuccessfulCoordinate

View file

@ -7,8 +7,8 @@ import SwiftUI
import UIKit
private enum SettingsLinks {
static let githubStars = URL(string: "https://github.com/StikDebug/StikDebug")!
static let pairingFileGuide = URL(string: "https://github.com/StikDebug/StikDebug-Guide/blob/main/pairing_file.md")!
static let githubStars = URL(string: "https://github.com/StephenDev0/StikDebug/stargazers")!
static let pairingFileGuide = URL(string: "https://github.com/StephenDev0/StikDebug-Guide/blob/main/pairing_file.md")!
static let localDevVPN = URL(string: "https://apps.apple.com/us/app/localdevvpn/id6755608044")!
static let discord = URL(string: "https://discord.gg/qahjXNTDwS")!
}
@ -19,7 +19,6 @@ struct SettingsView: View {
@AppStorage("keepAliveAudio") private var keepAliveAudio = true
@AppStorage("keepAliveLocation") private var keepAliveLocation = true
@AppStorage(UserDefaults.Keys.targetDeviceIP) private var targetDeviceIP = DeviceConnectionContext.defaultTargetIPAddress
@AppStorage(UserDefaults.Keys.mallocDebug) private var mallocDebug = false
@State private var isShowingPairingFilePicker = false
@State private var isImportingFile = false
@ -95,7 +94,8 @@ struct SettingsView: View {
}
}
.onChange(of: keepAliveAudio) { _, enabled in
handleAudioKeepAliveChange(enabled)
if enabled { BackgroundAudioManager.shared.start() }
else { BackgroundAudioManager.shared.stop() }
}
Toggle(isOn: $keepAliveLocation) {
@ -106,7 +106,7 @@ struct SettingsView: View {
}
}
.onChange(of: keepAliveLocation) { _, enabled in
handleLocationKeepAliveChange(enabled)
if !enabled { BackgroundLocationManager.shared.stop() }
}
} header: {
@ -131,16 +131,6 @@ struct SettingsView: View {
}
}
Section("Debugging") {
Toggle(isOn: $mallocDebug) {
VStack(alignment: .leading, spacing: 2) {
Text("Malloc Debugging")
Text("Attaches processes with MallocGuardEdges and MallocScribble to detect memory corruption.")
.font(.caption).foregroundStyle(.secondary)
}
}
}
Section("Advanced") {
HStack {
Text("Target Device IP")
@ -241,25 +231,6 @@ struct SettingsView: View {
return "Version \(appVersion) • iOS \(UIDevice.current.systemVersion) • \(txmLabel)"
}
private func handleAudioKeepAliveChange(_ enabled: Bool) {
if !enabled, !keepAliveLocation {
keepAliveLocation = true
BackgroundLocationManager.shared.requestAuthorizationIfNeeded()
}
BackgroundAudioManager.shared.configurationDidChange()
BackgroundLocationManager.shared.configurationDidChange()
}
private func handleLocationKeepAliveChange(_ enabled: Bool) {
if enabled {
BackgroundLocationManager.shared.requestAuthorizationIfNeeded()
} else if !keepAliveAudio {
keepAliveAudio = true
}
BackgroundAudioManager.shared.configurationDidChange()
BackgroundLocationManager.shared.configurationDidChange()
}
// MARK: - Business Logic
private func openAppFolder() {

File diff suppressed because it is too large Load diff

Binary file not shown.