Compare commits

...

44 commits
3.1.9 ... main

Author SHA1 Message Date
CelloSerenity
1256c52803
Fix DeepWiki embed
Some checks are pending
Build Debug IPA / Build IPA (push) Waiting to run
Build Debug IPA / Update website source (push) Blocked by required conditions
2026-10-15 11:41:27 -04:00
CelloSerenity
3422fc4725
3.1.15 2026-10-15 10:30:45 -04:00
CelloSerenity
734361bb9d fix: handle legacy and cryptex DDI service discovery 2026-10-14 23:26:00 -04:00
CelloSerenity
cd5e609387
Update README.md 2026-10-12 06:56:38 -06:00
CelloSerenity
e4665d74b5
Update SettingsView.swift 2026-10-12 06:56:17 -06:00
CelloSerenity
300ebff50b
3.1.14 2026-10-11 21:13:08 -06:00
CelloSerenity
775fba15f3
Update AutoScriptAssignments.swift 2026-10-10 17:53:12 -06:00
CelloSerenity
871f1461d9
Update README.md 2026-10-04 14:09:38 -06:00
CelloSerenity
4bdfc92aa7
3.1.13 2026-09-28 08:29:15 -06:00
CelloSerenity
4b3c8d6e88
Rename 'MeloCafe' to 'MeloCafé' 2026-09-28 08:24:29 -06:00
c03abcaa44
Update AutoScriptAssignments.swift 2026-09-28 07:32:03 -04:00
CelloSerenity
d4306e724b
3.1.12 ReadMe 2026-09-27 15:26:08 -06:00
CelloSerenity
50766f5ac5
3.1.12 2026-09-27 14:55:57 -06:00
CelloSerenity
e7ef9491e2 fix: preserve background activity sessions 2026-09-27 14:52:35 -06:00
CelloSerenity
6a078be17e fix: require a background keep alive 2026-09-27 13:54:35 -06:00
CelloSerenity
2d09a405de fix: limit idevice log file size 2026-09-27 13:46:33 -06:00
CelloSerenity
f50353c643 perf: reduce redundant device queries 2026-09-27 13:26:07 -06:00
CelloSerenity
cbaf467b66 fix: restore personalized DDI mounting 2026-09-27 13:16:09 -06:00
CelloSerenity
534b6daa3f
Update AutoScriptAssignments.swift 2026-09-25 17:49:59 -06:00
CelloSerenity
5e3e1bc91e
3.1.11 2026-09-23 17:22:17 -06:00
CelloSerenity
3abf9d5b19 feat: warn before manually enabling LiveContainer JIT 2026-09-23 17:09:48 -06:00
CelloSerenity
e85e0b3d47 feat: smooth cryptex DDI migration 2026-09-23 17:09:26 -06:00
CelloSerenity
f76672838b feat: migrate to cryptex DDI 2026-09-22 10:26:20 -06:00
levi
94bc9e8cf3
Add Malloc Debugging (#460)
* added malloc implementation

* update malloc debugging label in settings

---------

Co-authored-by: Leviidev <Leviidev@users.noreply.github.com>
2026-09-09 08:52:58 -04:00
CelloSerenity
c5bad70d37 ci: integrate source update into build workflow 2026-08-27 11:38:07 -06:00
Stephen
b916fe51cc fix: prevent stalled tunnel connections from exhausting threads 2026-08-27 09:18:18 -04:00
CelloSerenity
b3e3096b63
chore: update actions dep because node 24 2026-08-26 22:16:28 -06:00
CelloSerenity
8f0439cd0c
Update AutoScriptAssignments.swift 2026-08-26 22:05:53 -06:00
CelloSerenity
460fb235c2
Fix uncomfortable blue line 2026-08-25 16:32:41 -06:00
Stephen
f5d31fe2b7 Bump version to 3.1.10 2026-08-25 12:42:58 -04:00
Stephen
70d31322e7 Fix DDI mount watchdog crash (hopefully) 2026-08-25 11:08:54 -04:00
CelloSerenity
66e359c7e2
Update README.md 2026-08-19 16:09:45 -06:00
CelloSerenity
ae460a7a5d
Fix extra line 2026-08-07 15:07:15 -06:00
CelloSerenity
8fdfb193e0
Fix image links in README.md 2026-08-07 15:00:40 -06:00
CelloSerenity
2354795117
Update AutoScriptAssignments.swift 2026-08-05 16:15:47 -06:00
CelloSerenity
a8a4892a8c fix: target Website source workflow 2026-08-03 18:16:14 -06:00
CelloSerenity
cf4108fc2c ci: update website source workflow 2026-08-03 18:13:31 -06:00
30cfa58785
Update download links and images in README.md 2026-08-03 14:27:51 -04:00
15d99027c0
Update GitHub URLs in SettingsView.swift 2026-08-03 12:44:37 -04:00
38c0ca1348
Update links to reflect new GitHub repository 2026-08-03 12:34:07 -04:00
e160c9e90a
Update AutoScriptAssignments.swift 2026-08-03 11:58:19 -04:00
CelloSerenity
f928790e7d
Update AutoScriptAssignments.swift 2026-08-01 16:53:07 -06:00
CelloSerenity
30a147aaaf ci: wait for tagged IPA builds 2026-08-01 14:28:22 -06:00
CelloSerenity
c7e08e64fd
Upgrade action-gh-release to version 3 2026-08-01 14:13:29 -06:00
27 changed files with 4501 additions and 365 deletions

22
.github/scripts/update_source.sh vendored Executable file
View file

@ -0,0 +1,22 @@
#!/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

@ -7,12 +7,6 @@ on:
pull_request:
branches: [ "main" ]
workflow_dispatch:
inputs:
create_release:
description: Create a GitHub release from this ref
required: false
default: false
type: boolean
permissions:
contents: write
@ -21,6 +15,8 @@ jobs:
build:
name: Build IPA
runs-on: macos-latest
outputs:
version: ${{ steps.version.outputs.version }}
env:
UPLOAD_IPA: ${{ vars.UPLOAD_IPA || 'true' }}
@ -59,14 +55,14 @@ jobs:
- name: 5. Name workflow artifact
id: artifact
if: env.UPLOAD_IPA == 'true' && (github.event_name == 'push' || github.event_name == 'pull_request')
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
if: env.UPLOAD_IPA == 'true' && (github.event_name == 'push' || github.event_name == 'pull_request')
if: env.UPLOAD_IPA == 'true'
uses: actions/upload-artifact@v7
with:
path: ${{ steps.artifact.outputs.file_name }}
@ -75,7 +71,7 @@ jobs:
- name: Read version
id: version
if: startsWith(github.ref, 'refs/tags/') || inputs.create_release
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"
@ -85,16 +81,42 @@ jobs:
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Name release asset
if: startsWith(github.ref, 'refs/tags/') || inputs.create_release
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/') || inputs.create_release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v3
with:
name: StikDebug ${{ steps.version.outputs.version }}
tag_name: ${{ github.ref_type == 'tag' && github.ref_name || steps.version.outputs.version }}
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 }}'

View file

@ -1,52 +0,0 @@
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,24 +7,17 @@
<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>
<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>
<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>
## 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).
@ -37,14 +30,11 @@
> [!NOTE]
> **Notice:** StikDebug is no longer available on the App Store. Please use the official download methods below.
<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.9/StikDebug-3.1.9.ipa" target="_blank">
<img src="https://github.com/stikstore/altdirect/blob/main/assets/png/Download_Blue.png" alt="Download .ipa" width="200">
</a>
</div>
<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>
## Compatibility
@ -66,18 +56,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/StephenDev0/StikDebug-Guide/blob/main/pairing_file.md) (or ask in Discord).
- Detailed guide: [Pairing File Instructions](https://github.com/StikDebug/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 tapp the `Enable JIT` button.
- Launch StikDebug and tap 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 connecected to Wi-Fi. It may be a pairing file issue.
- Heartbeat errors → Ensure that the VPN is on and that you are connected 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.
@ -96,6 +86,10 @@ 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)
@ -106,7 +100,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/StephenDev0/StikDebug.git
git clone https://github.com/StikDebug/StikDebug.git
cd StikDebug
```
@ -146,13 +140,11 @@ 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.9;
MARKETING_VERSION = 3.1.15;
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.9;
MARKETING_VERSION = 3.1.15;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.stikdebug;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View file

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

View file

@ -39,7 +39,8 @@ private enum IdeviceBridge {
}
let code = Int(ffiError.pointee.code)
let message = string(from: ffiError.pointee.message) ?? fallback
let detail = string(from: ffiError.pointee.message) ?? fallback
let message = detail == fallback ? fallback : "\(fallback): \(detail)"
idevice_error_free(ffiError)
return makeError(domain: domain, code: code, message: message)
}
@ -262,31 +263,69 @@ private enum IdeviceBridge {
}
extension JITEnableContext {
func getMountedDeviceCount() throws -> Int {
func isDeveloperDiskImageMounted() throws -> Bool {
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
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")
}
if let devices {
for index in 0..<deviceCount {
plist_free(devices[index])
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
}
idevice_data_free(
UnsafeMutableRawPointer(devices).assumingMemoryBound(to: UInt8.self),
UInt(deviceCount * MemoryLayout<plist_t?>.stride)
)
}
}
return deviceCount
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])
}
idevice_data_free(
UnsafeMutableRawPointer(devices).assumingMemoryBound(to: UInt8.self),
UInt(deviceCount * MemoryLayout<plist_t?>.stride)
)
}
return deviceCount > 0
}
} 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")
}
}
}
@ -307,13 +346,7 @@ extension JITEnableContext {
if let ffiError = lockdownd_get_value(lockdownClient, "UniqueChipID", nil, &uniqueChipIDPlist) {
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to query UniqueChipID")
}
defer {
if let uniqueChipIDPlist {
plist_free(uniqueChipIDPlist)
}
}
defer { plist_free(uniqueChipIDPlist) }
return try IdeviceBridge.uint64Value(from: uniqueChipIDPlist, fieldName: "UniqueChipID")
}
@ -323,10 +356,10 @@ extension JITEnableContext {
connect: { image_mounter_connect_rsd(adapter, handshake, $0) },
cleanup: { image_mounter_free($0) }
) { imageMounterClient in
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(
let ffiError = imageData.withUnsafeBytes { imageBuffer in
trustcacheData.withUnsafeBytes { trustcacheBuffer in
manifestData.withUnsafeBytes { manifestBuffer in
image_mounter_mount_personalized_rsd(
imageMounterClient,
adapter,
handshake,
@ -337,9 +370,7 @@ extension JITEnableContext {
manifestBuffer.bindMemory(to: UInt8.self).baseAddress,
manifestData.count,
nil,
uniqueChipID,
progressCallback,
nil
uniqueChipID
)
}
}

View file

@ -17,6 +17,37 @@ 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?
@ -51,11 +82,7 @@ final class JITEnableContext {
var handshakeHandle: OpaquePointer? { handshake }
private init() {
let logURL = FileManager.default
.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent("idevice_log.txt")
var path = Array(logURL.path.utf8CString)
var path = Array(IdeviceLogFileManager.logURL.path.utf8CString)
path.withUnsafeMutableBufferPointer { buffer in
_ = idevice_init_logger(Info, Debug, buffer.baseAddress)
}
@ -90,7 +117,8 @@ final class JITEnableContext {
guard let ffiError else {
return makeError(fallback)
}
let message = nsString(from: ffiError.pointee.message, fallback: fallback)
let detail = nsString(from: ffiError.pointee.message, fallback: fallback)
let message = detail == fallback ? fallback : "\(fallback): \(detail)"
let error = makeError(message, code: Int(ffiError.pointee.code))
idevice_error_free(ffiError)
return error
@ -188,7 +216,9 @@ final class JITEnableContext {
tunnelLock.unlock()
if let waitSemaphore {
waitSemaphore.wait()
guard waitSemaphore.wait(timeout: .now() + .seconds(15)) == .success else {
throw makeError("Timed out waiting for the tunnel connection", code: -19)
}
waitSemaphore.signal()
}
@ -428,22 +458,19 @@ final class JITEnableContext {
private func withConnectedRemoteServer<T>(
_ body: (OpaquePointer) throws -> T
) throws -> T {
try ensureTunnel()
guard let adapter, let handshake else {
throw makeError("Tunnel is not connected")
}
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")
}
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")
}
guard let remoteServer else {
throw makeError("Remote server handle was not created")
defer { remote_server_free(remoteServer) }
return try body(remoteServer)
}
defer { remote_server_free(remoteServer) }
return try body(remoteServer)
}
private func withProcessControl<T>(
@ -601,7 +628,9 @@ final class JITEnableContext {
try withProcessControl(remoteServer: remoteServer) { processControl in
var pid: UInt64 = 0
let ffiError = bundleID.withCString { bundleID in
process_control_launch_app(processControl, bundleID, nil, 0, nil, 0, true, false, &pid)
Self.withCStringArray(Self.mallocDebugEnvVars()) { envPtr, envCount in
process_control_launch_app(processControl, bundleID, envPtr, envCount, nil, 0, true, false, &pid)
}
}
if let ffiError {

View file

@ -9,74 +9,108 @@ 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() {
DispatchQueue.global(qos: .utility).async {
let mounted = isMounted()
DispatchQueue.main.async {
self.coolisMounted = mounted
}
}
}
guard TunnelManager.shared.isConnected else { return }
func progressCallback(progress: size_t, total: size_t, context: UnsafeMutableRawPointer?) {
let percentage = Double(progress) / Double(total) * 100.0
DispatchQueue.main.async {
self.mountProgress = percentage
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()
DispatchQueue.main.async {
if status != .unreachable {
self.coolisMounted = status == .mounted
}
}
}
}
func pubMount() {
mount()
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()
}
private func mount() {
let currentlyMounted = isMounted()
DispatchQueue.main.async {
self.coolisMounted = currentlyMounted
switch checkMountStatus() {
case .mounted:
finishMount {
self.coolisMounted = true
}
return
case .unreachable:
finishMount()
return
case .notMounted:
break
}
guard isPairing(), !currentlyMounted else {
guard isPairing() else {
finishMount()
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()
}
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()
}
} else {
self.coolisMounted = true
self.checkforMounted()
}
self.mountingThread = nil
}
}
}
thread.qualityOfService = .background
thread.name = "mounting"
thread.start()
mountingThread = thread
private func finishMount(_ completion: @escaping () -> Void = {}) {
DispatchQueue.main.async {
self.mountingThread = nil
self.mountLock.lock()
self.mountInProgress = false
self.mountLock.unlock()
completion()
}
}
}

View file

@ -13,10 +13,6 @@ 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
@ -29,18 +25,25 @@ func isMounted() -> Bool {
func checkMountStatus() -> MountCheckResult {
do {
let result = try JITEnableContext.shared.getMountedDeviceCount()
return result > 0 ? .mounted : .notMounted
return try JITEnableContext.shared.isDeveloperDiskImageMounted() ? .mounted : .notMounted
} catch {
LogManager.shared.addErrorLog("Failed to check DDI mount status: \(error.localizedDescription)")
return .unreachable
}
}
func mountPersonalDDI(imagePath: String, trustcachePath: String, manifestPath: String) -> String? {
func mountDeveloperDiskImage(from directoryPath: String) -> String? {
do {
try JITEnableContext.shared.mountPersonalDDI(withImagePath: imagePath, trustcachePath: trustcachePath, manifestPath: manifestPath)
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
)
}
} catch {
LogManager.shared.addErrorLog("Failed to mount DDI: \(error.localizedDescription)")
return error.localizedDescription
}
return nil

View file

@ -20,13 +20,26 @@ 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()
refreshAppLists()
TunnelManager.shared.$isConnected
.removeDuplicates()
.filter { $0 }
.sink { [weak self] _ in self?.refreshAppLists() }
.store(in: &cancellables)
}
func refreshAppLists() {
guard !refreshInProgress else {
refreshPending = true
return
}
refreshInProgress = true
isLoading = true
lastError = nil
@ -42,6 +55,11 @@ final class InstalledAppsViewModel: ObservableObject {
debuggable: debuggable,
hiddenSystem: hiddenSystem
)
self.cacheApps(
debuggable: debuggable,
nonDebuggable: classifiedApps.nonDebuggable,
system: classifiedApps.system
)
DispatchQueue.main.async {
self.apply(
@ -49,22 +67,26 @@ final class InstalledAppsViewModel: ObservableObject {
nonDebuggable: classifiedApps.nonDebuggable,
system: classifiedApps.system
)
self.isLoading = false
self.cacheApps(
debuggable: debuggable,
nonDebuggable: classifiedApps.nonDebuggable,
system: classifiedApps.system
)
self.finishRefresh()
}
} 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,7 +11,6 @@ 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?
@ -30,13 +29,7 @@ final class BackgroundAudioManager {
)
}
func start() {
persistentEnabled = true
refreshRunningState()
}
func stop() {
persistentEnabled = false
func configurationDidChange() {
refreshRunningState()
}
@ -51,7 +44,7 @@ final class BackgroundAudioManager {
}
private func refreshRunningState() {
let shouldRun = persistentEnabled || (activityCount > 0 && UserDefaults.standard.bool(forKey: "keepAliveAudio"))
let shouldRun = activityCount > 0 && UserDefaults.standard.bool(forKey: "keepAliveAudio")
guard shouldRun != isRunning else {
if shouldRun {
recoverIfNeeded()

View file

@ -4,6 +4,7 @@
//
import CoreLocation
import UIKit
final class BackgroundLocationManager: NSObject, CLLocationManagerDelegate {
static let shared = BackgroundLocationManager()
@ -11,6 +12,7 @@ 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()
@ -21,8 +23,44 @@ final class BackgroundLocationManager: NSObject, CLLocationManagerDelegate {
locationManager.pausesLocationUpdatesAutomatically = false
}
func start() {
isRunning = true
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
}
switch locationManager.authorizationStatus {
case .authorizedAlways:
locationManager.startUpdatingLocation()
@ -30,37 +68,54 @@ final class BackgroundLocationManager: NSObject, CLLocationManagerDelegate {
locationManager.requestAlwaysAuthorization()
case .notDetermined:
locationManager.requestAlwaysAuthorization()
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()
case .denied, .restricted:
restoreAudioFallback()
@unknown default:
restoreAudioFallback()
}
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
guard isRunning else { return }
switch manager.authorizationStatus {
case .authorizedAlways, .authorizedWhenInUse:
manager.startUpdatingLocation()
default:
break
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)
}
}

View file

@ -7,6 +7,17 @@ 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
@ -17,13 +28,21 @@ 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 = URL.documentsDirectory.appendingPathComponent(item.relativePath)
guard !fileManager.fileExists(atPath: destinationURL.path) else {
let destinationURL = Self.directoryURL.appendingPathComponent(item.fileName)
guard replaceFiles || !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 {
@ -56,8 +75,18 @@ 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 = URL.documentsDirectory.appendingPathComponent(item.relativePath)
let fileURL = Self.directoryURL.appendingPathComponent(item.fileName)
if fileManager.fileExists(atPath: fileURL.path) {
try fileManager.removeItem(at: fileURL)
}
@ -68,37 +97,100 @@ final class DeveloperDiskImageService {
for item in Self.downloadItems {
progressHandler?(completedStages / totalStages, "Downloading \(item.name)...")
let destinationURL = URL.documentsDirectory.appendingPathComponent(item.relativePath)
let destinationURL = Self.directoryURL.appendingPathComponent(item.fileName)
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 static let downloadItems: [DDIDownloadItem] = [
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] = [
.init(
name: "Build Manifest",
relativePath: "DDI/BuildManifest.plist",
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",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/BuildManifest.plist"
),
.init(
name: "Image",
relativePath: "DDI/Image.dmg",
fileName: "Image.dmg",
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg"
),
.init(
name: "TrustCache",
relativePath: "DDI/Image.dmg.trustcache",
fileName: "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 relativePath: String
let fileName: String
let urlString: String
}

View file

@ -0,0 +1,66 @@
//
// 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

@ -0,0 +1,68 @@
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,12 +70,7 @@ final class TunnelManager: ObservableObject {
}
private func mountDeveloperDiskImageIfNeeded() {
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
}
guard DeveloperDiskImageService.filesAreReady else { return }
MountingProgress.shared.pubMount()
}

View file

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

View file

@ -15,5 +15,9 @@ 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: URL = URL.documentsDirectory.appendingPathComponent("idevice_log.txt")
let logURL = IdeviceLogFileManager.logURL
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 = URL.documentsDirectory.appendingPathComponent("idevice_log.txt").path
let logPath = IdeviceLogFileManager.logURL.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 = URL.documentsDirectory.appendingPathComponent("idevice_log.txt").path
let logPath = IdeviceLogFileManager.logURL.path
let previousCount = lastProcessedLineCount
guard FileManager.default.fileExists(atPath: logPath) else {

View file

@ -12,17 +12,14 @@ 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
@ -30,11 +27,18 @@ struct HomeView: View {
let isWorking: Bool
}
private struct LiveContainerApp {
let bundleID: String
let name: String
}
var body: some View {
InstalledAppsListView(onSelectApp: { selectedBundle, selectedName in
bundleID = selectedBundle
Haptics.medium()
startJITInBackground(bundleID: selectedBundle, displayName: selectedName)
if selectedName.localizedCaseInsensitiveContains("LiveContainer") {
pendingLiveContainerApp = LiveContainerApp(bundleID: selectedBundle, name: selectedName)
} else {
enableJITForSelectedApp(bundleID: selectedBundle, name: selectedName)
}
}, showDoneButton: false, onImportPairingFile: { isShowingPairingFilePicker = true })
.overlay(alignment: .bottom) {
if let debugFeedback {
@ -45,9 +49,6 @@ struct HomeView: View {
}
.onAppear(perform: handleAppear)
.onReceive(NotificationCenter.default.publisher(for: .intentJSScriptReady), perform: handleScriptReadyNotification)
.onReceive(timer) { _ in
refreshMountStatusIfNeeded()
}
.onOpenURL { url in
handleExternalURL(url)
}
@ -74,6 +75,28 @@ 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,
@ -92,9 +115,14 @@ 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 {
@ -117,13 +145,6 @@ 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,7 +83,9 @@ struct InstalledAppsListView: View {
handleLoadingChange(isLoading)
}
.onReceive(NotificationCenter.default.publisher(for: .pairingFileImported)) { _ in
viewModel.refreshAppLists()
if TunnelManager.shared.isConnected {
viewModel.refreshAppLists()
}
}
}

View file

@ -7,6 +7,7 @@
import SwiftUI
import Foundation
import CoreLocation
private enum ExternalLocationAction: Identifiable {
case simulate(URL, Double, Double)
@ -223,7 +224,22 @@ struct MainTabView: View {
DispatchQueue.main.async {
if code == 0 {
BackgroundLocationManager.shared.requestStart()
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
)
}
}
LogManager.shared.addInfoLog(
String(format: "Simulated location from URL: %.6f, %.6f", coordinate.latitude, coordinate.longitude)
)
@ -243,7 +259,7 @@ struct MainTabView: View {
let code = clear_simulated_location()
DispatchQueue.main.async {
if code == 0 {
BackgroundLocationManager.shared.requestStop()
LocationSimulationSession.shared.stop()
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 {
simulatedCoordinate != nil || routePlaybackTask != nil
simulationSession.isActive || simulatedCoordinate != nil || routePlaybackTask != nil
}
private var isRouteRunning: Bool {
routePlaybackTask != nil
routePlaybackTask != nil || simulationSession.routeCoordinates != nil
}
private var hasRouteContext: Bool {
@ -801,7 +801,12 @@ struct LocationSimulationView: View {
routePlan != nil ||
isLoadingRoute ||
isPrefetchingRouteSpeeds ||
routePlaybackCoordinate != nil
routePlaybackCoordinate != nil ||
simulationSession.routeCoordinates != nil
}
private var displayedRouteCoordinate: CLLocationCoordinate2D? {
simulationSession.routeCoordinates == nil ? routePlaybackCoordinate : simulationSession.coordinate
}
private var routeSummaryText: String? {
@ -818,6 +823,9 @@ struct LocationSimulationView: View {
}
private var routeStatusText: String {
if simulationSession.routeCoordinates != nil {
return "Route simulation active."
}
if isLoadingRoute {
return "Calculating route…"
}
@ -892,8 +900,8 @@ struct LocationSimulationView: View {
Marker("End", coordinate: routeEndCoordinate)
.tint(.red)
}
if let routePlaybackCoordinate {
Marker("Current", coordinate: routePlaybackCoordinate)
if let displayedRouteCoordinate {
Marker("Current", coordinate: displayedRouteCoordinate)
.tint(.blue)
}
} else if let coordinate {
@ -1025,17 +1033,28 @@ 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()
}
}
@ -1297,14 +1316,14 @@ struct LocationSimulationView: View {
) {
routePlaybackCoordinate = nil
beginBackgroundTask()
LocationSimulationSession.shared.clearRoute()
startResendLoop(with: coord)
BackgroundLocationManager.shared.requestStart()
}
}
private func simulateRoute() {
guard pairingExists,
routePlan != nil,
let routePlan,
let firstCoordinate = routePlaybackSamples.first?.coordinate,
!isBusy else {
return
@ -1319,10 +1338,13 @@ struct LocationSimulationView: View {
operation: { locationUpdateCode(for: firstCoordinate) }
) {
beginBackgroundTask()
BackgroundLocationManager.shared.requestStart()
let routeID = LocationSimulationSession.shared.startRoute(
at: firstCoordinate,
coordinates: routePlan.displayCoordinates
)
simulatedCoordinate = nil
routePlaybackCoordinate = firstCoordinate
startRoutePlayback()
startRoutePlayback(routeID: routeID)
}
}
@ -1356,13 +1378,14 @@ 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()
BackgroundLocationManager.shared.requestStop()
LocationSimulationSession.shared.stop()
}
}
@ -1379,18 +1402,15 @@ struct LocationSimulationView: View {
private func startResendLoop(with coordinate: CLLocationCoordinate2D) {
simulatedCoordinate = coordinate
resendTimer?.invalidate()
resendTimer = Timer.scheduledTimer(withTimeInterval: 4, repeats: true) { _ in
guard let simulatedCoordinate else { return }
LocationSimulationSession.shared.startResending(at: coordinate) {
LocationSimulationCommandQueue.shared.async {
_ = locationUpdateCode(for: simulatedCoordinate)
_ = locationUpdateCode(for: coordinate)
}
}
}
private func stopResendLoop() {
resendTimer?.invalidate()
resendTimer = nil
LocationSimulationSession.shared.pauseResending()
simulatedCoordinate = nil
}
@ -1520,15 +1540,19 @@ struct LocationSimulationView: View {
}
}
private func startRoutePlayback() {
private func startRoutePlayback(routeID: UUID) {
routePlaybackTask = Task {
var lastSuccessfulCoordinate = routePlaybackSamples.first?.coordinate
for sample in routePlaybackSamples.dropFirst() {
try? await Task.sleep(for: .seconds(sample.delayFromPrevious))
guard !Task.isCancelled else { return }
guard !Task.isCancelled,
LocationSimulationSession.shared.isCurrentRoute(routeID) 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
@ -1546,10 +1570,12 @@ 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/StephenDev0/StikDebug/stargazers")!
static let pairingFileGuide = URL(string: "https://github.com/StephenDev0/StikDebug-Guide/blob/main/pairing_file.md")!
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 localDevVPN = URL(string: "https://apps.apple.com/us/app/localdevvpn/id6755608044")!
static let discord = URL(string: "https://discord.gg/qahjXNTDwS")!
}
@ -19,6 +19,7 @@ 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
@ -94,8 +95,7 @@ struct SettingsView: View {
}
}
.onChange(of: keepAliveAudio) { _, enabled in
if enabled { BackgroundAudioManager.shared.start() }
else { BackgroundAudioManager.shared.stop() }
handleAudioKeepAliveChange(enabled)
}
Toggle(isOn: $keepAliveLocation) {
@ -106,7 +106,7 @@ struct SettingsView: View {
}
}
.onChange(of: keepAliveLocation) { _, enabled in
if !enabled { BackgroundLocationManager.shared.stop() }
handleLocationKeepAliveChange(enabled)
}
} header: {
@ -131,6 +131,16 @@ 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")
@ -231,6 +241,25 @@ 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.