mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-15 12:02:14 +00:00
Compare commits
46 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
734361bb9d | ||
|
|
cd5e609387 | ||
|
|
e4665d74b5 | ||
|
|
300ebff50b | ||
|
|
775fba15f3 | ||
|
|
871f1461d9 | ||
|
|
4bdfc92aa7 | ||
|
|
4b3c8d6e88 | ||
| c03abcaa44 | |||
|
|
d4306e724b | ||
|
|
50766f5ac5 | ||
|
|
e7ef9491e2 | ||
|
|
6a078be17e | ||
|
|
2d09a405de | ||
|
|
f50353c643 | ||
|
|
cbaf467b66 | ||
|
|
534b6daa3f | ||
|
|
5e3e1bc91e | ||
|
|
3abf9d5b19 | ||
|
|
e85e0b3d47 | ||
|
|
f76672838b | ||
|
|
94bc9e8cf3 | ||
|
|
c5bad70d37 | ||
|
|
b916fe51cc | ||
|
|
b3e3096b63 | ||
|
|
8f0439cd0c | ||
|
|
460fb235c2 | ||
|
|
f5d31fe2b7 | ||
|
|
70d31322e7 | ||
|
|
66e359c7e2 | ||
|
|
ae460a7a5d | ||
|
|
8fdfb193e0 | ||
|
|
2354795117 | ||
|
|
a8a4892a8c | ||
|
|
cf4108fc2c | ||
| 30cfa58785 | |||
| 15d99027c0 | |||
| 38c0ca1348 | |||
| e160c9e90a | |||
|
|
f928790e7d | ||
|
|
30a147aaaf | ||
|
|
c7e08e64fd | ||
|
|
85fb1350a4 | ||
|
|
ff030b809a | ||
|
|
22e34051ff | ||
|
|
ea42d847d1 |
28 changed files with 4582 additions and 373 deletions
22
.github/scripts/update_source.sh
vendored
Executable file
22
.github/scripts/update_source.sh
vendored
Executable 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
|
||||
71
.github/workflows/build_ipa.yml
vendored
71
.github/workflows/build_ipa.yml
vendored
|
|
@ -3,17 +3,20 @@ name: Build Debug IPA
|
|||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
tags: [ "[0-9]*" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build IPA
|
||||
runs-on: macos-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
env:
|
||||
UPLOAD_IPA: ${{ vars.UPLOAD_IPA || 'true' }}
|
||||
|
||||
|
|
@ -50,10 +53,70 @@ jobs:
|
|||
zip -r StikDebug.ipa Payload
|
||||
rm -rf Payload StikDebug.app
|
||||
|
||||
- name: 5. Store IPA as Workflow Artifact
|
||||
- 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
|
||||
if: env.UPLOAD_IPA == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: StikDebug-Debug.ipa
|
||||
path: StikDebug.ipa
|
||||
path: ${{ steps.artifact.outputs.file_name }}
|
||||
archive: false
|
||||
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
52
.github/workflows/updatesource.yml
vendored
|
|
@ -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
|
||||
53
README.md
53
README.md
|
|
@ -7,24 +7,16 @@
|
|||
<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 />
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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).
|
||||
|
|
@ -37,14 +29,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.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>
|
||||
<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>
|
||||
|
||||
<a href="https://github.com/StikDebug/StikDebug/releases/latest/download/StikDebug-3.1.14.ipa" target="_blank"><img src="https://altdirect.app/assets/png/Download_Blue.png" alt="Download .ipa" width="200"></a>
|
||||
</h3>
|
||||
|
||||
## Compatibility
|
||||
|
||||
|
|
@ -66,18 +55,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 +85,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 +99,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 +139,15 @@ 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
|
||||
[](https://deepwiki.com/stephendev0/stikdebug)
|
||||
|
||||
[](https://deepwiki.com/StikDebug/StikDebug)
|
||||
|
||||
## License
|
||||
StikDebug is licensed under **AGPL-3.0**. See [`LICENSE`](LICENSE) for details.
|
||||
|
|
|
|||
|
|
@ -482,7 +482,7 @@
|
|||
"$(PROJECT_DIR)/StikDebug/idevice",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.1;
|
||||
MARKETING_VERSION = 3.1.8;
|
||||
MARKETING_VERSION = 3.1.14;
|
||||
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.8;
|
||||
MARKETING_VERSION = 3.1.14;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.stik.stikdebug;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
66
StikDebug/Services/IdeviceLogFileManager.swift
Normal file
66
StikDebug/Services/IdeviceLogFileManager.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
68
StikDebug/Services/LocationSimulationSession.swift
Normal file
68
StikDebug/Services/LocationSimulationSession.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
),
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@ import Foundation
|
|||
import UniformTypeIdentifiers
|
||||
|
||||
enum PairingFileStore {
|
||||
static let fileName = "rp_pairing_file.plist"
|
||||
static let fileName = "pairingFile.plist"
|
||||
static let supportedContentTypes: [UTType] = [
|
||||
UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!,
|
||||
UTType(filenameExtension: "mobiledevicepair", conformingTo: .data)!,
|
||||
.propertyList
|
||||
]
|
||||
|
||||
private static let legacyFileName = "pairingFile.plist"
|
||||
private static let legacyFileName = "rp_pairing_file.plist"
|
||||
|
||||
static var url: URL {
|
||||
directoryURL.appendingPathComponent(fileName)
|
||||
|
|
@ -25,31 +25,31 @@ enum PairingFileStore {
|
|||
let destination = url
|
||||
try? fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
|
||||
|
||||
guard !fileManager.fileExists(atPath: destination.path) else {
|
||||
removeLegacyCopies(fileManager: fileManager)
|
||||
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)
|
||||
}
|
||||
return destination
|
||||
}
|
||||
|
||||
migrateLegacyCopy(to: destination, fileManager: fileManager)
|
||||
if !fileManager.fileExists(atPath: destination.path) {
|
||||
migrateLegacyCopy(to: destination, fileManager: fileManager)
|
||||
}
|
||||
return destination
|
||||
}
|
||||
|
||||
static func replace(with sourceURL: URL, fileManager: FileManager = .default) throws {
|
||||
let destination = prepareURL(fileManager: fileManager)
|
||||
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
|
||||
|
||||
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)
|
||||
let documentsDestination = documentsURL
|
||||
if sourceURL.standardizedFileURL != documentsDestination.standardizedFileURL {
|
||||
try replaceItem(at: documentsDestination, with: sourceURL, fileManager: fileManager)
|
||||
}
|
||||
|
||||
removeLegacyCopies(fileManager: fileManager)
|
||||
protectPairingFile(at: destination, fileManager: fileManager)
|
||||
try replaceItem(at: url, with: documentsDestination, fileManager: fileManager)
|
||||
protectPairingFile(at: url, fileManager: fileManager)
|
||||
}
|
||||
|
||||
static func importFromPicker(_ sourceURL: URL, fileManager: FileManager = .default) throws {
|
||||
|
|
@ -83,19 +83,33 @@ enum PairingFileStore {
|
|||
|
||||
private static var legacyURLs: [URL] {
|
||||
[
|
||||
URL.documentsDirectory.appendingPathComponent(fileName),
|
||||
directoryURL.appendingPathComponent(legacyFileName),
|
||||
documentsURL,
|
||||
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 fileManager.moveItem(at: legacyURL, to: destination)
|
||||
try replaceItem(at: destination, with: legacyURL, fileManager: fileManager)
|
||||
} catch {
|
||||
if let data = try? Data(contentsOf: legacyURL) {
|
||||
try? data.write(to: destination, options: .atomic)
|
||||
try? fileManager.removeItem(at: legacyURL)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -104,6 +118,21 @@ 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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -83,7 +83,9 @@ struct InstalledAppsListView: View {
|
|||
handleLoadingChange(isLoading)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .pairingFileImported)) { _ in
|
||||
viewModel.refreshAppLists()
|
||||
if TunnelManager.shared.isConnected {
|
||||
viewModel.refreshAppLists()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
Loading…
Add table
Add a link
Reference in a new issue