mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-15 20:10:30 +00:00
Compare commits
No commits in common. "main" and "3.1.8" have entirely different histories.
28 changed files with 380 additions and 4586 deletions
22
.github/scripts/update_source.sh
vendored
22
.github/scripts/update_source.sh
vendored
|
|
@ -1,22 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
website_directory=${1:?Usage: update_source.sh WEBSITE_DIRECTORY RELEASE_VERSION}
|
||||
release_version=${2:?Usage: update_source.sh WEBSITE_DIRECTORY RELEASE_VERSION}
|
||||
|
||||
cd "$website_directory"
|
||||
|
||||
git config user.name 'GitHub Action'
|
||||
git config user.email 'action@github.com'
|
||||
|
||||
python3 updatesource.py index.json
|
||||
git add index.json
|
||||
|
||||
if git diff --staged --quiet; then
|
||||
echo 'Website/index.json is already current.'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git commit -m "chore: update StikDebug source for $release_version"
|
||||
git push origin main
|
||||
71
.github/workflows/build_ipa.yml
vendored
71
.github/workflows/build_ipa.yml
vendored
|
|
@ -3,20 +3,17 @@ name: Build Debug IPA
|
|||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
tags: [ "[0-9]*" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build IPA
|
||||
runs-on: macos-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
env:
|
||||
UPLOAD_IPA: ${{ vars.UPLOAD_IPA || 'true' }}
|
||||
|
||||
|
|
@ -53,70 +50,10 @@ jobs:
|
|||
zip -r StikDebug.ipa Payload
|
||||
rm -rf Payload StikDebug.app
|
||||
|
||||
- name: 5. Name workflow artifact
|
||||
id: artifact
|
||||
if: env.UPLOAD_IPA == 'true'
|
||||
run: |
|
||||
file_name="StikDebug-${GITHUB_SHA::7}.ipa"
|
||||
cp StikDebug.ipa "$file_name"
|
||||
echo "file_name=$file_name" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 6. Store IPA as Workflow Artifact
|
||||
- name: 5. Store IPA as Workflow Artifact
|
||||
if: env.UPLOAD_IPA == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: ${{ steps.artifact.outputs.file_name }}
|
||||
archive: false
|
||||
name: StikDebug-Debug.ipa
|
||||
path: StikDebug.ipa
|
||||
retention-days: 90
|
||||
|
||||
- name: Read version
|
||||
id: version
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
version=$(xcodebuild -project StikDebug.xcodeproj -scheme StikDebug -showBuildSettings | awk -F ' = ' '/ MARKETING_VERSION = / { print $2; exit }')
|
||||
test -n "$version"
|
||||
if [ "$GITHUB_REF_TYPE" = "tag" ]; then
|
||||
test "${GITHUB_REF_NAME#v}" = "$version"
|
||||
fi
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Name release asset
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: mv StikDebug.ipa StikDebug-${{ steps.version.outputs.version }}.ipa
|
||||
|
||||
- name: Publish release
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
name: ${{ steps.version.outputs.version }}
|
||||
tag_name: ${{ github.ref_name }}
|
||||
target_commitish: ${{ github.sha }}
|
||||
files: StikDebug-${{ steps.version.outputs.version }}.ipa
|
||||
generate_release_notes: true
|
||||
fail_on_unmatched_files: true
|
||||
|
||||
update-source:
|
||||
name: Update website source
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Checkout website
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: StikDebug/Website
|
||||
ref: main
|
||||
path: Website
|
||||
token: ${{ secrets.WEBSITE_TOKEN }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Update StikDebug source
|
||||
run: .github/scripts/update_source.sh Website '${{ needs.build.outputs.version }}'
|
||||
|
|
|
|||
52
.github/workflows/updatesource.yml
vendored
Normal file
52
.github/workflows/updatesource.yml
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
name: Update StikDebug Source
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update-source:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Website-Stuff
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: StephenDev0/Website-Stuff
|
||||
ref: main
|
||||
path: Website-Stuff
|
||||
# A repository-scoped GITHUB_TOKEN cannot push to Website-Stuff.
|
||||
token: ${{ secrets.WEBSITE_STUFF_TOKEN }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Update StikDebug source
|
||||
id: update_source
|
||||
working-directory: Website-Stuff
|
||||
run: |
|
||||
git config --global user.name 'GitHub Action'
|
||||
git config --global user.email 'action@github.com'
|
||||
|
||||
python updatesource.py index.json
|
||||
git add index.json
|
||||
if git diff --staged --quiet; then
|
||||
echo "changes=false" >> $GITHUB_OUTPUT
|
||||
echo "No changes detected in index.json"
|
||||
else
|
||||
git commit -m "Update StikDebug source for latest release"
|
||||
git push origin main
|
||||
echo "changes=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Create job summary
|
||||
run: |
|
||||
if [[ "${{ steps.update_source.outputs.changes }}" == "true" ]]; then
|
||||
echo "## StikDebug source updated" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Website-Stuff/index.json was updated and pushed." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "## StikDebug source already current" >> $GITHUB_STEP_SUMMARY
|
||||
echo "No change to Website-Stuff/index.json was needed." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
54
README.md
54
README.md
|
|
@ -7,17 +7,24 @@
|
|||
<p><i>An on-device debugger/JIT enabler for iOS versions 17.4+ powered by <a href="https://github.com/jkcoxson/idevice">idevice</a>.</i></p>
|
||||
</div>
|
||||
|
||||
<h3 align="center">
|
||||
<a href="https://discord.gg/ZnNcrRT3M8"><img src="https://img.shields.io/badge/Discord-join%20us-7289DA?logo=discord&logoColor=white&style=for-the-badge&labelColor=23272A" /></a>
|
||||
<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>
|
||||
<a href="https://deepwiki.com/StikDebug/StikDebug"><img src="https://img.shields.io/badge/Ask-DeepWiki-grey?style=for-the-badge&labelColor=23272A&color=007ec6" /></a>
|
||||
<br />
|
||||
</h3>
|
||||
<h6 align="center">
|
||||
<a href="https://discord.gg/ZnNcrRT3M8">
|
||||
<img src="https://img.shields.io/badge/Discord-join%20us-7289DA?logo=discord&logoColor=white&style=for-the-badge&labelColor=23272A" />
|
||||
</a>
|
||||
<a href="https://github.com/StephenDev0/StikDebug/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/StephenDev0/StikDebug?label=License&color=5865F2&style=for-the-badge&labelColor=23272A" />
|
||||
</a>
|
||||
<a href="https://github.com/StephenDev0/StikDebug/stargazers">
|
||||
<img src="https://img.shields.io/github/stars/StephenDev0/StikDebug?label=Stars&color=FEE75C&style=for-the-badge&labelColor=23272A" />
|
||||
</a>
|
||||
<a href="https://github.com/StephenDev0/StikDebug/releases">
|
||||
<img src="https://img.shields.io/github/v/release/StephenDev0/StikDebug?label=Latest&color=00BFFF&style=for-the-badge&labelColor=23272A" />
|
||||
</a>
|
||||
<br />
|
||||
</h6>
|
||||
|
||||
## Features
|
||||
- **JIT:** Enable Just-In-Time compilation for sideloaded apps that have the `get-task-allow` entitlement.
|
||||
- **JIT:** Enable Just In Time compilation for sideloaded apps that have the `get-task-allow` entitlement.
|
||||
- **App Launching:** Launch every app installed on your device.
|
||||
- **Console:** Live app and system logs.
|
||||
- **Scripts:** Manage automation scripts (mainly used for iOS 26 JIT).
|
||||
|
|
@ -30,11 +37,14 @@
|
|||
> [!NOTE]
|
||||
> **Notice:** StikDebug is no longer available on the App Store. Please use the official download methods below.
|
||||
|
||||
<h3 align="center">
|
||||
<a href="https://altdirect.app/?url=https://stikdebug.xyz/index.json" target="_blank"><img src="https://altdirect.app/assets/png/AltSource_Blue.png" alt="Add AltSource" width="200"></a>
|
||||
|
||||
<a href="https://github.com/StikDebug/StikDebug/releases/latest/download/StikDebug-3.1.15.ipa" target="_blank"><img src="https://altdirect.app/assets/png/Download_Blue.png" alt="Download .ipa" width="200"></a>
|
||||
</h3>
|
||||
<div align="center" style="display: flex; justify-content: center; align-items: center; gap: 16px; flex-wrap: wrap;">
|
||||
<a href="https://stikstore.app/altdirect/?url=https://stikdebug.xyz/index.json" target="_blank">
|
||||
<img src="https://github.com/stikstore/altdirect/blob/main/assets/png/AltSource_Blue.png" alt="Add AltSource" width="200">
|
||||
</a>
|
||||
<a href="https://github.com/StephenDev0/StikDebug/releases/download/3.1.6/StikDebug-3.1.6.ipa" target="_blank">
|
||||
<img src="https://github.com/stikstore/altdirect/blob/main/assets/png/Download_Blue.png" alt="Download .ipa" width="200">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## Compatibility
|
||||
|
||||
|
|
@ -56,18 +66,18 @@ StikDebug enables **JIT** for sideloaded apps on iOS 17.4+ without needing a com
|
|||
|
||||
### Steps
|
||||
1. **Obtain a pairing file**
|
||||
- Detailed guide: [Pairing File Instructions](https://github.com/StikDebug/StikDebug-Guide/blob/main/pairing_file.md) (or ask in Discord).
|
||||
- Detailed guide: [Pairing File Instructions](https://github.com/StephenDev0/StikDebug-Guide/blob/main/pairing_file.md) (or ask in Discord).
|
||||
|
||||
2. **Set up VPN**
|
||||
- Launch LocalDevVPN and enable the VPN.
|
||||
|
||||
4. **Enable JIT for an app**
|
||||
- Launch StikDebug and tap the `Enable JIT` button.
|
||||
- Launch StikDebug and tapp the `Enable JIT` button.
|
||||
- Select your sideloaded app from the list in StikDebug.
|
||||
|
||||
**Troubleshooting**
|
||||
- "Connection dropped" or loopback errors → Check iOS version compatibility / beta warnings.
|
||||
- Heartbeat errors → Ensure that the VPN is on and that you are connected to Wi-Fi. It may be a pairing file issue.
|
||||
- Heartbeat errors → Ensure that the VPN is on and that you are connecected to Wi-Fi. It may be a pairing file issue.
|
||||
- Pairing file issues → Replace file with device unlocked & trusted.
|
||||
- Still stuck? Join the [Discord](https://discord.gg/ZnNcrRT3M8) with logs/screenshots.
|
||||
|
||||
|
|
@ -86,10 +96,6 @@ StikDebug enables **JIT** for sideloaded apps on iOS 17.4+ without needing a com
|
|||
|
||||
## Building from Source
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Integrating StikDebug or StikJIT into your own project?**
|
||||
> See the [StikJIT Integration Guide](https://github.com/StikDebug/StikJIT/blob/main/INTEGRATION.md) for integration instructions.
|
||||
|
||||
### Requirements
|
||||
- macOS (latest recommended)
|
||||
- Xcode 16+ (Xcode 26+ preferred for iOS 26+ support)
|
||||
|
|
@ -100,7 +106,7 @@ StikDebug enables **JIT** for sideloaded apps on iOS 17.4+ without needing a com
|
|||
### Steps
|
||||
1. **Clone the repo**
|
||||
```bash
|
||||
git clone https://github.com/StikDebug/StikDebug.git
|
||||
git clone https://github.com/StephenDev0/StikDebug.git
|
||||
cd StikDebug
|
||||
```
|
||||
|
||||
|
|
@ -140,11 +146,13 @@ To propose a new feature, open a feature request issue and provide:
|
|||
|
||||
### Code Contributions (Best Practices)
|
||||
- Follow normal Swift and SwiftUI style.
|
||||
- Write clear and easy-to-understand code.
|
||||
- Write clear and easy to understand code.
|
||||
- Keep your changes consistent with how the project is already set up.
|
||||
- Make sure everything builds and works without errors.
|
||||
|
||||
We appreciate your time and effort in helping improve this project.
|
||||
|
||||
## Code Help
|
||||
[](https://deepwiki.com/stephendev0/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.15;
|
||||
MARKETING_VERSION = 3.1.8;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.stik.stikdebug;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -544,7 +544,7 @@
|
|||
"$(PROJECT_DIR)/StikDebug/idevice",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.1;
|
||||
MARKETING_VERSION = 3.1.15;
|
||||
MARKETING_VERSION = 3.1.8;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.stik.stikdebug;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
|
|||
|
|
@ -9,9 +9,8 @@ import UIKit
|
|||
|
||||
enum AppBootstrapper {
|
||||
static func configure() {
|
||||
IdeviceLogFileManager.shared.prepareForLogging()
|
||||
registerDefaultSettings()
|
||||
ensureKeepAliveSelection()
|
||||
startConfiguredKeepAliveServices()
|
||||
applyDocumentPickerCopyWorkaround()
|
||||
}
|
||||
|
||||
|
|
@ -28,11 +27,11 @@ enum AppBootstrapper {
|
|||
])
|
||||
}
|
||||
|
||||
private static func ensureKeepAliveSelection() {
|
||||
let defaults = UserDefaults.standard
|
||||
if !defaults.bool(forKey: "keepAliveAudio"), !defaults.bool(forKey: "keepAliveLocation") {
|
||||
defaults.set(true, forKey: "keepAliveAudio")
|
||||
private static func startConfiguredKeepAliveServices() {
|
||||
guard UserDefaults.standard.bool(forKey: "keepAliveAudio") else {
|
||||
return
|
||||
}
|
||||
BackgroundAudioManager.shared.start()
|
||||
}
|
||||
|
||||
private static func applyDocumentPickerCopyWorkaround() {
|
||||
|
|
|
|||
|
|
@ -39,8 +39,7 @@ private enum IdeviceBridge {
|
|||
}
|
||||
|
||||
let code = Int(ffiError.pointee.code)
|
||||
let detail = string(from: ffiError.pointee.message) ?? fallback
|
||||
let message = detail == fallback ? fallback : "\(fallback): \(detail)"
|
||||
let message = string(from: ffiError.pointee.message) ?? fallback
|
||||
idevice_error_free(ffiError)
|
||||
return makeError(domain: domain, code: code, message: message)
|
||||
}
|
||||
|
|
@ -263,69 +262,31 @@ private enum IdeviceBridge {
|
|||
}
|
||||
|
||||
extension JITEnableContext {
|
||||
func isDeveloperDiskImageMounted() throws -> Bool {
|
||||
func getMountedDeviceCount() throws -> Int {
|
||||
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
||||
var cryptexQuerySucceeded = false
|
||||
var cryptexQueryError: NSError?
|
||||
if DeveloperDiskImageService.usesCryptexDDI {
|
||||
var installed: UnsafeMutablePointer<InstalledCryptexC>?
|
||||
defer { cryptexd_free_installed_cryptex(installed) }
|
||||
if let ffiError = cryptexd_installed_ddi(adapter, handshake, &installed) {
|
||||
cryptexQueryError = IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to query installed DDI cryptex")
|
||||
} else {
|
||||
cryptexQuerySucceeded = true
|
||||
if installed != nil {
|
||||
return true
|
||||
}
|
||||
try IdeviceBridge.withConnectedClient(
|
||||
fallback: "Failed to connect to image mounter",
|
||||
missingClientMessage: "Image mounter client was not created",
|
||||
connect: { image_mounter_connect_rsd(adapter, handshake, $0) },
|
||||
cleanup: { image_mounter_free($0) }
|
||||
) { client in
|
||||
var devices: UnsafeMutablePointer<plist_t?>?
|
||||
var deviceCount = 0
|
||||
if let ffiError = image_mounter_copy_devices(client, &devices, &deviceCount) {
|
||||
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to fetch mounted devices")
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
return try IdeviceBridge.withConnectedClient(
|
||||
fallback: "Failed to connect to image mounter",
|
||||
missingClientMessage: "Image mounter client was not created",
|
||||
connect: { image_mounter_connect_rsd(adapter, handshake, $0) },
|
||||
cleanup: { image_mounter_free($0) }
|
||||
) { client in
|
||||
var devices: UnsafeMutablePointer<plist_t?>?
|
||||
var deviceCount = 0
|
||||
if let error = image_mounter_copy_devices(client, &devices, &deviceCount) {
|
||||
throw IdeviceBridge.consumeFFIError(error, fallback: "Failed to fetch mounted devices")
|
||||
if let devices {
|
||||
for index in 0..<deviceCount {
|
||||
plist_free(devices[index])
|
||||
}
|
||||
if let devices {
|
||||
for index in 0..<deviceCount {
|
||||
plist_free(devices[index])
|
||||
}
|
||||
idevice_data_free(
|
||||
UnsafeMutableRawPointer(devices).assumingMemoryBound(to: UInt8.self),
|
||||
UInt(deviceCount * MemoryLayout<plist_t?>.stride)
|
||||
)
|
||||
}
|
||||
return deviceCount > 0
|
||||
idevice_data_free(
|
||||
UnsafeMutableRawPointer(devices).assumingMemoryBound(to: UInt8.self),
|
||||
UInt(deviceCount * MemoryLayout<plist_t?>.stride)
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
if cryptexQuerySucceeded {
|
||||
return false
|
||||
}
|
||||
throw cryptexQueryError ?? (error as NSError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func installCryptexDDI(from directoryPath: String) throws {
|
||||
var assets: OpaquePointer?
|
||||
if let ffiError = cryptex1_assets_load(directoryPath, &assets) {
|
||||
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to load DDI cryptex assets")
|
||||
}
|
||||
|
||||
guard let assets else {
|
||||
throw IdeviceBridge.makeError(message: "DDI cryptex assets were not loaded")
|
||||
}
|
||||
defer { cryptex1_assets_free(assets) }
|
||||
|
||||
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
||||
if let ffiError = cryptexd_install_ddi(adapter, handshake, assets, nil) {
|
||||
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to install DDI cryptex")
|
||||
return deviceCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -346,7 +307,13 @@ extension JITEnableContext {
|
|||
if let ffiError = lockdownd_get_value(lockdownClient, "UniqueChipID", nil, &uniqueChipIDPlist) {
|
||||
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to query UniqueChipID")
|
||||
}
|
||||
defer { plist_free(uniqueChipIDPlist) }
|
||||
|
||||
defer {
|
||||
if let uniqueChipIDPlist {
|
||||
plist_free(uniqueChipIDPlist)
|
||||
}
|
||||
}
|
||||
|
||||
return try IdeviceBridge.uint64Value(from: uniqueChipIDPlist, fieldName: "UniqueChipID")
|
||||
}
|
||||
|
||||
|
|
@ -356,10 +323,10 @@ extension JITEnableContext {
|
|||
connect: { image_mounter_connect_rsd(adapter, handshake, $0) },
|
||||
cleanup: { image_mounter_free($0) }
|
||||
) { imageMounterClient in
|
||||
let ffiError = imageData.withUnsafeBytes { imageBuffer in
|
||||
trustcacheData.withUnsafeBytes { trustcacheBuffer in
|
||||
manifestData.withUnsafeBytes { manifestBuffer in
|
||||
image_mounter_mount_personalized_rsd(
|
||||
let ffiError = imageData.withUnsafeBytes { imageBuffer -> UnsafeMutablePointer<IdeviceFfiError>? in
|
||||
trustcacheData.withUnsafeBytes { trustcacheBuffer -> UnsafeMutablePointer<IdeviceFfiError>? in
|
||||
manifestData.withUnsafeBytes { manifestBuffer -> UnsafeMutablePointer<IdeviceFfiError>? in
|
||||
image_mounter_mount_personalized_with_callback_rsd(
|
||||
imageMounterClient,
|
||||
adapter,
|
||||
handshake,
|
||||
|
|
@ -370,7 +337,9 @@ extension JITEnableContext {
|
|||
manifestBuffer.bindMemory(to: UInt8.self).baseAddress,
|
||||
manifestData.count,
|
||||
nil,
|
||||
uniqueChipID
|
||||
uniqueChipID,
|
||||
progressCallback,
|
||||
nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,37 +17,6 @@ typealias SyslogErrorHandler = (NSError?) -> Void
|
|||
final class JITEnableContext {
|
||||
static let shared = JITEnableContext()
|
||||
|
||||
private static func withCStringArray<R>(
|
||||
_ strings: [String], _ body: (UnsafePointer<UnsafePointer<CChar>?>?, UInt) -> R
|
||||
) -> R {
|
||||
if strings.isEmpty {
|
||||
return body(nil, 0)
|
||||
}
|
||||
var cStrings: [UnsafeMutablePointer<CChar>?] = strings.map { strdup($0) }
|
||||
defer { cStrings.forEach { free($0) } }
|
||||
return cStrings.withUnsafeBufferPointer { buffer in
|
||||
buffer.baseAddress!.withMemoryRebound(to: UnsafePointer<CChar>?.self, capacity: buffer.count) { rebound in
|
||||
body(rebound, UInt(buffer.count))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func mallocDebugEnvVars() -> [String] {
|
||||
let defaults = UserDefaults.standard
|
||||
let enableAll = defaults.bool(forKey: UserDefaults.Keys.mallocDebug)
|
||||
let guardEdges = enableAll || defaults.bool(forKey: UserDefaults.Keys.mallocGuardEdges)
|
||||
let scribble = enableAll || defaults.bool(forKey: UserDefaults.Keys.mallocScribble)
|
||||
|
||||
var envVars: [String] = []
|
||||
if guardEdges {
|
||||
envVars.append("MallocGuardEdges=1")
|
||||
}
|
||||
if scribble {
|
||||
envVars.append("MallocScribble=1")
|
||||
}
|
||||
return envVars
|
||||
}
|
||||
|
||||
private struct TunnelHandles {
|
||||
var adapter: OpaquePointer?
|
||||
var handshake: OpaquePointer?
|
||||
|
|
@ -82,7 +51,11 @@ final class JITEnableContext {
|
|||
var handshakeHandle: OpaquePointer? { handshake }
|
||||
|
||||
private init() {
|
||||
var path = Array(IdeviceLogFileManager.logURL.path.utf8CString)
|
||||
let logURL = FileManager.default
|
||||
.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
.appendingPathComponent("idevice_log.txt")
|
||||
|
||||
var path = Array(logURL.path.utf8CString)
|
||||
path.withUnsafeMutableBufferPointer { buffer in
|
||||
_ = idevice_init_logger(Info, Debug, buffer.baseAddress)
|
||||
}
|
||||
|
|
@ -117,8 +90,7 @@ final class JITEnableContext {
|
|||
guard let ffiError else {
|
||||
return makeError(fallback)
|
||||
}
|
||||
let detail = nsString(from: ffiError.pointee.message, fallback: fallback)
|
||||
let message = detail == fallback ? fallback : "\(fallback): \(detail)"
|
||||
let message = nsString(from: ffiError.pointee.message, fallback: fallback)
|
||||
let error = makeError(message, code: Int(ffiError.pointee.code))
|
||||
idevice_error_free(ffiError)
|
||||
return error
|
||||
|
|
@ -216,9 +188,7 @@ final class JITEnableContext {
|
|||
tunnelLock.unlock()
|
||||
|
||||
if let waitSemaphore {
|
||||
guard waitSemaphore.wait(timeout: .now() + .seconds(15)) == .success else {
|
||||
throw makeError("Timed out waiting for the tunnel connection", code: -19)
|
||||
}
|
||||
waitSemaphore.wait()
|
||||
waitSemaphore.signal()
|
||||
}
|
||||
|
||||
|
|
@ -458,19 +428,22 @@ final class JITEnableContext {
|
|||
private func withConnectedRemoteServer<T>(
|
||||
_ body: (OpaquePointer) throws -> T
|
||||
) throws -> T {
|
||||
try withFreshDebugTunnel(hostname: "StikDebugRemoteServer") { adapter, handshake in
|
||||
var remoteServer: OpaquePointer?
|
||||
if let ffiError = remote_server_connect_rsd(adapter, handshake, &remoteServer) {
|
||||
throw error(from: ffiError, fallback: "Failed to connect remote server")
|
||||
}
|
||||
|
||||
guard let remoteServer else {
|
||||
throw makeError("Remote server handle was not created")
|
||||
}
|
||||
|
||||
defer { remote_server_free(remoteServer) }
|
||||
return try body(remoteServer)
|
||||
try ensureTunnel()
|
||||
guard let adapter, let handshake else {
|
||||
throw makeError("Tunnel is not connected")
|
||||
}
|
||||
|
||||
var remoteServer: OpaquePointer?
|
||||
if let ffiError = remote_server_connect_rsd(adapter, handshake, &remoteServer) {
|
||||
throw error(from: ffiError, fallback: "Failed to connect remote server")
|
||||
}
|
||||
|
||||
guard let remoteServer else {
|
||||
throw makeError("Remote server handle was not created")
|
||||
}
|
||||
|
||||
defer { remote_server_free(remoteServer) }
|
||||
return try body(remoteServer)
|
||||
}
|
||||
|
||||
private func withProcessControl<T>(
|
||||
|
|
@ -628,9 +601,7 @@ final class JITEnableContext {
|
|||
try withProcessControl(remoteServer: remoteServer) { processControl in
|
||||
var pid: UInt64 = 0
|
||||
let ffiError = bundleID.withCString { bundleID in
|
||||
Self.withCStringArray(Self.mallocDebugEnvVars()) { envPtr, envCount in
|
||||
process_control_launch_app(processControl, bundleID, envPtr, envCount, nil, 0, true, false, &pid)
|
||||
}
|
||||
process_control_launch_app(processControl, bundleID, nil, 0, nil, 0, true, false, &pid)
|
||||
}
|
||||
|
||||
if let ffiError {
|
||||
|
|
|
|||
|
|
@ -9,108 +9,74 @@ import idevice
|
|||
final class MountingProgress: ObservableObject {
|
||||
static let shared = MountingProgress()
|
||||
|
||||
@Published private(set) var mountProgress: Double = 0.0
|
||||
@Published private(set) var mountingThread: Thread?
|
||||
@Published private(set) var coolisMounted: Bool = false
|
||||
|
||||
private let mountCheckLock = NSLock()
|
||||
private var mountCheckInProgress = false
|
||||
private let mountLock = NSLock()
|
||||
private var mountInProgress = false
|
||||
|
||||
private init() {}
|
||||
|
||||
func checkforMounted() {
|
||||
guard TunnelManager.shared.isConnected else { return }
|
||||
|
||||
mountCheckLock.lock()
|
||||
guard !mountCheckInProgress else {
|
||||
mountCheckLock.unlock()
|
||||
return
|
||||
}
|
||||
mountCheckInProgress = true
|
||||
mountCheckLock.unlock()
|
||||
|
||||
DispatchQueue.global(qos: .utility).async {
|
||||
let status = checkMountStatus()
|
||||
|
||||
self.mountCheckLock.lock()
|
||||
self.mountCheckInProgress = false
|
||||
self.mountCheckLock.unlock()
|
||||
|
||||
let mounted = isMounted()
|
||||
DispatchQueue.main.async {
|
||||
if status != .unreachable {
|
||||
self.coolisMounted = status == .mounted
|
||||
}
|
||||
self.coolisMounted = mounted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func progressCallback(progress: size_t, total: size_t, context: UnsafeMutableRawPointer?) {
|
||||
let percentage = Double(progress) / Double(total) * 100.0
|
||||
DispatchQueue.main.async {
|
||||
self.mountProgress = percentage
|
||||
}
|
||||
}
|
||||
|
||||
func pubMount() {
|
||||
guard TunnelManager.shared.isConnected, DeveloperDiskImageService.filesAreReady else { return }
|
||||
|
||||
mountLock.lock()
|
||||
guard !mountInProgress else {
|
||||
mountLock.unlock()
|
||||
return
|
||||
}
|
||||
mountInProgress = true
|
||||
mountLock.unlock()
|
||||
|
||||
let thread = Thread { [weak self] in
|
||||
self?.mount()
|
||||
}
|
||||
thread.qualityOfService = .background
|
||||
thread.name = "mounting"
|
||||
DispatchQueue.main.async {
|
||||
self.mountingThread = thread
|
||||
}
|
||||
thread.start()
|
||||
mount()
|
||||
}
|
||||
|
||||
private func mount() {
|
||||
switch checkMountStatus() {
|
||||
case .mounted:
|
||||
finishMount {
|
||||
self.coolisMounted = true
|
||||
}
|
||||
return
|
||||
case .unreachable:
|
||||
finishMount()
|
||||
return
|
||||
case .notMounted:
|
||||
break
|
||||
}
|
||||
|
||||
guard isPairing() else {
|
||||
finishMount()
|
||||
return
|
||||
}
|
||||
|
||||
let mountError = mountDeveloperDiskImage(from: DeveloperDiskImageService.directoryURL.path)
|
||||
let mounted = mountError == nil || checkMountStatus() == .mounted
|
||||
finishMount {
|
||||
if mounted {
|
||||
self.coolisMounted = true
|
||||
self.checkforMounted()
|
||||
} else if let mountError {
|
||||
LogManager.shared.addErrorLog("Failed to mount DDI: \(mountError)")
|
||||
showAlert(title: "DDI Mount Failed", message: mountError, showOk: true, showTryAgain: true) { shouldTryAgain in
|
||||
if shouldTryAgain {
|
||||
self.pubMount()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func finishMount(_ completion: @escaping () -> Void = {}) {
|
||||
let currentlyMounted = isMounted()
|
||||
DispatchQueue.main.async {
|
||||
self.mountingThread = nil
|
||||
self.mountLock.lock()
|
||||
self.mountInProgress = false
|
||||
self.mountLock.unlock()
|
||||
completion()
|
||||
self.coolisMounted = currentlyMounted
|
||||
}
|
||||
|
||||
guard isPairing(), !currentlyMounted else {
|
||||
return
|
||||
}
|
||||
|
||||
if let mountingThread {
|
||||
mountingThread.cancel()
|
||||
self.mountingThread = nil
|
||||
}
|
||||
|
||||
let thread = Thread { [weak self] in
|
||||
guard let self else { return }
|
||||
let mountError = mountPersonalDDI(
|
||||
imagePath: URL.documentsDirectory.appendingPathComponent("DDI/Image.dmg").path,
|
||||
trustcachePath: URL.documentsDirectory.appendingPathComponent("DDI/Image.dmg.trustcache").path,
|
||||
manifestPath: URL.documentsDirectory.appendingPathComponent("DDI/BuildManifest.plist").path
|
||||
)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if let mountError {
|
||||
showAlert(title: "DDI Mount Failed", message: mountError, showOk: true, showTryAgain: true) { shouldTryAgain in
|
||||
if shouldTryAgain {
|
||||
self.mount()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.coolisMounted = true
|
||||
self.checkforMounted()
|
||||
}
|
||||
self.mountingThread = nil
|
||||
}
|
||||
}
|
||||
|
||||
thread.qualityOfService = .background
|
||||
thread.name = "mounting"
|
||||
thread.start()
|
||||
mountingThread = thread
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ typealias RsdHandshakeHandle = OpaquePointer
|
|||
typealias ImageMounterHandle = OpaquePointer
|
||||
typealias LockdowndClientHandle = OpaquePointer
|
||||
|
||||
func progressCallback(progress: size_t, total: size_t, context: UnsafeMutableRawPointer?) {
|
||||
MountingProgress.shared.progressCallback(progress: progress, total: total, context: context)
|
||||
}
|
||||
|
||||
enum MountCheckResult {
|
||||
case mounted
|
||||
case notMounted
|
||||
|
|
@ -25,25 +29,18 @@ func isMounted() -> Bool {
|
|||
|
||||
func checkMountStatus() -> MountCheckResult {
|
||||
do {
|
||||
return try JITEnableContext.shared.isDeveloperDiskImageMounted() ? .mounted : .notMounted
|
||||
let result = try JITEnableContext.shared.getMountedDeviceCount()
|
||||
return result > 0 ? .mounted : .notMounted
|
||||
} catch {
|
||||
LogManager.shared.addErrorLog("Failed to check DDI mount status: \(error.localizedDescription)")
|
||||
return .unreachable
|
||||
}
|
||||
}
|
||||
|
||||
func mountDeveloperDiskImage(from directoryPath: String) -> String? {
|
||||
func mountPersonalDDI(imagePath: String, trustcachePath: String, manifestPath: String) -> String? {
|
||||
do {
|
||||
if DeveloperDiskImageService.usesCryptexDDI {
|
||||
try JITEnableContext.shared.installCryptexDDI(from: directoryPath)
|
||||
} else {
|
||||
try JITEnableContext.shared.mountPersonalDDI(
|
||||
withImagePath: URL(fileURLWithPath: directoryPath).appendingPathComponent("Image.dmg").path,
|
||||
trustcachePath: URL(fileURLWithPath: directoryPath).appendingPathComponent("Image.dmg.trustcache").path,
|
||||
manifestPath: URL(fileURLWithPath: directoryPath).appendingPathComponent("BuildManifest.plist").path
|
||||
)
|
||||
}
|
||||
try JITEnableContext.shared.mountPersonalDDI(withImagePath: imagePath, trustcachePath: trustcachePath, manifestPath: manifestPath)
|
||||
} catch {
|
||||
LogManager.shared.addErrorLog("Failed to mount DDI: \(error.localizedDescription)")
|
||||
return error.localizedDescription
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -20,26 +20,13 @@ final class InstalledAppsViewModel: ObservableObject {
|
|||
private let cacheKeyDebuggable = "cachedDebuggableApps"
|
||||
private let cacheKeyNonDebuggable = "cachedNonDebuggableApps"
|
||||
private let cacheKeySystem = "cachedSystemApps"
|
||||
private var cancellables: Set<AnyCancellable> = []
|
||||
private var refreshInProgress = false
|
||||
private var refreshPending = false
|
||||
|
||||
init() {
|
||||
loadCachedApps()
|
||||
TunnelManager.shared.$isConnected
|
||||
.removeDuplicates()
|
||||
.filter { $0 }
|
||||
.sink { [weak self] _ in self?.refreshAppLists() }
|
||||
.store(in: &cancellables)
|
||||
refreshAppLists()
|
||||
}
|
||||
|
||||
func refreshAppLists() {
|
||||
guard !refreshInProgress else {
|
||||
refreshPending = true
|
||||
return
|
||||
}
|
||||
|
||||
refreshInProgress = true
|
||||
isLoading = true
|
||||
lastError = nil
|
||||
|
||||
|
|
@ -55,11 +42,6 @@ final class InstalledAppsViewModel: ObservableObject {
|
|||
debuggable: debuggable,
|
||||
hiddenSystem: hiddenSystem
|
||||
)
|
||||
self.cacheApps(
|
||||
debuggable: debuggable,
|
||||
nonDebuggable: classifiedApps.nonDebuggable,
|
||||
system: classifiedApps.system
|
||||
)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.apply(
|
||||
|
|
@ -67,26 +49,22 @@ final class InstalledAppsViewModel: ObservableObject {
|
|||
nonDebuggable: classifiedApps.nonDebuggable,
|
||||
system: classifiedApps.system
|
||||
)
|
||||
self.finishRefresh()
|
||||
self.isLoading = false
|
||||
self.cacheApps(
|
||||
debuggable: debuggable,
|
||||
nonDebuggable: classifiedApps.nonDebuggable,
|
||||
system: classifiedApps.system
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
self.lastError = error.localizedDescription
|
||||
self.finishRefresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func finishRefresh() {
|
||||
refreshInProgress = false
|
||||
isLoading = false
|
||||
|
||||
guard refreshPending else { return }
|
||||
refreshPending = false
|
||||
refreshAppLists()
|
||||
}
|
||||
|
||||
func displayName(for bundleID: String) -> String? {
|
||||
debuggableApps[bundleID] ?? systemApps[bundleID] ?? nonDebuggableApps[bundleID]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ final class BackgroundAudioManager {
|
|||
private var engine = AVAudioEngine()
|
||||
private var player = AVAudioPlayerNode()
|
||||
private var isRunning = false
|
||||
private var persistentEnabled = false
|
||||
private var activityCount = 0
|
||||
private var healthCheckTimer: Timer?
|
||||
|
||||
|
|
@ -29,7 +30,13 @@ final class BackgroundAudioManager {
|
|||
)
|
||||
}
|
||||
|
||||
func configurationDidChange() {
|
||||
func start() {
|
||||
persistentEnabled = true
|
||||
refreshRunningState()
|
||||
}
|
||||
|
||||
func stop() {
|
||||
persistentEnabled = false
|
||||
refreshRunningState()
|
||||
}
|
||||
|
||||
|
|
@ -44,7 +51,7 @@ final class BackgroundAudioManager {
|
|||
}
|
||||
|
||||
private func refreshRunningState() {
|
||||
let shouldRun = activityCount > 0 && UserDefaults.standard.bool(forKey: "keepAliveAudio")
|
||||
let shouldRun = persistentEnabled || (activityCount > 0 && UserDefaults.standard.bool(forKey: "keepAliveAudio"))
|
||||
guard shouldRun != isRunning else {
|
||||
if shouldRun {
|
||||
recoverIfNeeded()
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
//
|
||||
|
||||
import CoreLocation
|
||||
import UIKit
|
||||
|
||||
final class BackgroundLocationManager: NSObject, CLLocationManagerDelegate {
|
||||
static let shared = BackgroundLocationManager()
|
||||
|
|
@ -12,7 +11,6 @@ final class BackgroundLocationManager: NSObject, CLLocationManagerDelegate {
|
|||
private let locationManager = CLLocationManager()
|
||||
private var isRunning = false
|
||||
private var activityCount = 0
|
||||
private var isRequestingAuthorization = false
|
||||
|
||||
private override init() {
|
||||
super.init()
|
||||
|
|
@ -23,44 +21,8 @@ final class BackgroundLocationManager: NSObject, CLLocationManagerDelegate {
|
|||
locationManager.pausesLocationUpdatesAutomatically = false
|
||||
}
|
||||
|
||||
func configurationDidChange() {
|
||||
refreshRunningState()
|
||||
}
|
||||
|
||||
func requestStart() {
|
||||
activityCount += 1
|
||||
refreshRunningState()
|
||||
}
|
||||
|
||||
func requestStop() {
|
||||
activityCount = max(activityCount - 1, 0)
|
||||
refreshRunningState()
|
||||
}
|
||||
|
||||
func requestAuthorizationIfNeeded() {
|
||||
switch locationManager.authorizationStatus {
|
||||
case .authorizedAlways, .authorizedWhenInUse:
|
||||
return
|
||||
case .notDetermined:
|
||||
isRequestingAuthorization = true
|
||||
locationManager.requestAlwaysAuthorization()
|
||||
case .denied, .restricted:
|
||||
restoreAudioFallback()
|
||||
@unknown default:
|
||||
restoreAudioFallback()
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshRunningState() {
|
||||
let shouldRun = activityCount > 0 && UserDefaults.standard.bool(forKey: "keepAliveLocation")
|
||||
guard shouldRun != isRunning else { return }
|
||||
|
||||
isRunning = shouldRun
|
||||
guard shouldRun else {
|
||||
locationManager.stopUpdatingLocation()
|
||||
return
|
||||
}
|
||||
|
||||
func start() {
|
||||
isRunning = true
|
||||
switch locationManager.authorizationStatus {
|
||||
case .authorizedAlways:
|
||||
locationManager.startUpdatingLocation()
|
||||
|
|
@ -68,54 +30,37 @@ final class BackgroundLocationManager: NSObject, CLLocationManagerDelegate {
|
|||
locationManager.requestAlwaysAuthorization()
|
||||
case .notDetermined:
|
||||
locationManager.requestAlwaysAuthorization()
|
||||
case .denied, .restricted:
|
||||
restoreAudioFallback()
|
||||
@unknown default:
|
||||
restoreAudioFallback()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
isRunning = false
|
||||
locationManager.stopUpdatingLocation()
|
||||
}
|
||||
|
||||
func requestStart() {
|
||||
activityCount += 1
|
||||
if activityCount == 1, UserDefaults.standard.bool(forKey: "keepAliveLocation") {
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
func requestStop() {
|
||||
activityCount = max(activityCount - 1, 0)
|
||||
if activityCount == 0 {
|
||||
stop()
|
||||
}
|
||||
}
|
||||
|
||||
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||
guard isRunning else { return }
|
||||
switch manager.authorizationStatus {
|
||||
case .authorizedAlways, .authorizedWhenInUse:
|
||||
isRequestingAuthorization = false
|
||||
if isRunning {
|
||||
manager.startUpdatingLocation()
|
||||
}
|
||||
case .denied, .restricted:
|
||||
if isRunning || isRequestingAuthorization {
|
||||
isRequestingAuthorization = false
|
||||
restoreAudioFallback()
|
||||
}
|
||||
case .notDetermined:
|
||||
return
|
||||
@unknown default:
|
||||
if isRunning || isRequestingAuthorization {
|
||||
isRequestingAuthorization = false
|
||||
restoreAudioFallback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func restoreAudioFallback() {
|
||||
let defaults = UserDefaults.standard
|
||||
defaults.set(false, forKey: "keepAliveLocation")
|
||||
defaults.set(true, forKey: "keepAliveAudio")
|
||||
refreshRunningState()
|
||||
BackgroundAudioManager.shared.configurationDidChange()
|
||||
|
||||
showAlert(
|
||||
title: "Location Access Refused",
|
||||
message: "Location access was refused. Open Settings to enable it.",
|
||||
showOk: false,
|
||||
showTryAgain: true,
|
||||
primaryButtonText: "Settings"
|
||||
) { openSettings in
|
||||
guard openSettings,
|
||||
let settingsURL = URL(string: UIApplication.openSettingsURLString) else {
|
||||
return
|
||||
}
|
||||
UIApplication.shared.open(settingsURL)
|
||||
manager.startUpdatingLocation()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,17 +7,6 @@ import Foundation
|
|||
|
||||
final class DeveloperDiskImageService {
|
||||
static let shared = DeveloperDiskImageService()
|
||||
static let directoryURL = URL.documentsDirectory.appendingPathComponent("DDI", isDirectory: true)
|
||||
|
||||
static var filesAreReady: Bool {
|
||||
downloadItems.allSatisfy {
|
||||
FileManager.default.fileExists(atPath: directoryURL.appendingPathComponent($0.fileName).path)
|
||||
} && (try? String(contentsOf: mountMethodURL, encoding: .utf8)) == mountMethod.rawValue
|
||||
}
|
||||
|
||||
static var usesCryptexDDI: Bool {
|
||||
mountMethod == .cryptex
|
||||
}
|
||||
|
||||
private let fileManager: FileManager
|
||||
private let session: URLSession
|
||||
|
|
@ -28,21 +17,13 @@ final class DeveloperDiskImageService {
|
|||
}
|
||||
|
||||
func downloadMissingFiles() async throws {
|
||||
let installedMethod = try? String(contentsOf: Self.mountMethodURL, encoding: .utf8)
|
||||
let replaceFiles = installedMethod != Self.mountMethod.rawValue
|
||||
|
||||
if !Self.usesCryptexDDI {
|
||||
try removeCryptexOnlyFiles()
|
||||
}
|
||||
|
||||
for item in Self.downloadItems {
|
||||
let destinationURL = Self.directoryURL.appendingPathComponent(item.fileName)
|
||||
guard replaceFiles || !fileManager.fileExists(atPath: destinationURL.path) else {
|
||||
let destinationURL = URL.documentsDirectory.appendingPathComponent(item.relativePath)
|
||||
guard !fileManager.fileExists(atPath: destinationURL.path) else {
|
||||
continue
|
||||
}
|
||||
try await downloadFile(from: item.urlString, to: destinationURL)
|
||||
}
|
||||
try Self.mountMethod.rawValue.write(to: Self.mountMethodURL, atomically: true, encoding: .utf8)
|
||||
}
|
||||
|
||||
func downloadFile(from urlString: String, to destinationURL: URL) async throws {
|
||||
|
|
@ -75,18 +56,8 @@ final class DeveloperDiskImageService {
|
|||
var completedStages = 0.0
|
||||
|
||||
progressHandler?(0.0, "Removing existing DDI files...")
|
||||
if fileManager.fileExists(atPath: Self.mountMethodURL.path) {
|
||||
try fileManager.removeItem(at: Self.mountMethodURL)
|
||||
}
|
||||
if !Self.usesCryptexDDI {
|
||||
try removeCryptexOnlyFiles()
|
||||
}
|
||||
let completionMarker = Self.directoryURL.appendingPathComponent("Image.dmg.root_hash")
|
||||
if fileManager.fileExists(atPath: completionMarker.path) {
|
||||
try fileManager.removeItem(at: completionMarker)
|
||||
}
|
||||
for item in Self.downloadItems {
|
||||
let fileURL = Self.directoryURL.appendingPathComponent(item.fileName)
|
||||
let fileURL = URL.documentsDirectory.appendingPathComponent(item.relativePath)
|
||||
if fileManager.fileExists(atPath: fileURL.path) {
|
||||
try fileManager.removeItem(at: fileURL)
|
||||
}
|
||||
|
|
@ -97,100 +68,37 @@ final class DeveloperDiskImageService {
|
|||
|
||||
for item in Self.downloadItems {
|
||||
progressHandler?(completedStages / totalStages, "Downloading \(item.name)...")
|
||||
let destinationURL = Self.directoryURL.appendingPathComponent(item.fileName)
|
||||
let destinationURL = URL.documentsDirectory.appendingPathComponent(item.relativePath)
|
||||
try await downloadFile(from: item.urlString, to: destinationURL)
|
||||
completedStages += 1.0
|
||||
progressHandler?(completedStages / totalStages, "\(item.name) ready")
|
||||
}
|
||||
|
||||
try Self.mountMethod.rawValue.write(to: Self.mountMethodURL, atomically: true, encoding: .utf8)
|
||||
|
||||
progressHandler?(1.0, "DDI download complete.")
|
||||
}
|
||||
|
||||
private func removeCryptexOnlyFiles() throws {
|
||||
for fileName in Self.cryptexOnlyFileNames {
|
||||
let fileURL = Self.directoryURL.appendingPathComponent(fileName)
|
||||
if fileManager.fileExists(atPath: fileURL.path) {
|
||||
try fileManager.removeItem(at: fileURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static var mountMethod: DDIMountMethod {
|
||||
let version = ProcessInfo.processInfo.operatingSystemVersion
|
||||
return version.majorVersion > 26 || (version.majorVersion == 26 && version.minorVersion >= 4)
|
||||
? .cryptex
|
||||
: .personalized
|
||||
}
|
||||
|
||||
private static let mountMethodURL = directoryURL.appendingPathComponent("MountMethod")
|
||||
private static let cryptexOnlyFileNames = ["Image.dmg.cryptex_info", "Image.dmg.root_hash"]
|
||||
|
||||
private static var downloadItems: [DDIDownloadItem] {
|
||||
switch mountMethod {
|
||||
case .cryptex:
|
||||
return cryptexDownloadItems
|
||||
case .personalized:
|
||||
return personalizedDownloadItems
|
||||
}
|
||||
}
|
||||
|
||||
private static let cryptexDownloadItems: [DDIDownloadItem] = [
|
||||
private static let downloadItems: [DDIDownloadItem] = [
|
||||
.init(
|
||||
name: "Build Manifest",
|
||||
fileName: "BuildManifest.plist",
|
||||
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/BuildManifest.plist"
|
||||
),
|
||||
.init(
|
||||
name: "Image",
|
||||
fileName: "Image.dmg",
|
||||
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg"
|
||||
),
|
||||
.init(
|
||||
name: "TrustCache",
|
||||
fileName: "Image.dmg.trustcache",
|
||||
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg.trustcache"
|
||||
),
|
||||
.init(
|
||||
name: "Cryptex Info",
|
||||
fileName: "Image.dmg.cryptex_info",
|
||||
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg.cryptex_info"
|
||||
),
|
||||
.init(
|
||||
name: "Root Hash",
|
||||
fileName: "Image.dmg.root_hash",
|
||||
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Cryptex/Image.dmg.root_hash"
|
||||
)
|
||||
]
|
||||
|
||||
private static let personalizedDownloadItems: [DDIDownloadItem] = [
|
||||
.init(
|
||||
name: "Build Manifest",
|
||||
fileName: "BuildManifest.plist",
|
||||
relativePath: "DDI/BuildManifest.plist",
|
||||
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/BuildManifest.plist"
|
||||
),
|
||||
.init(
|
||||
name: "Image",
|
||||
fileName: "Image.dmg",
|
||||
relativePath: "DDI/Image.dmg",
|
||||
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg"
|
||||
),
|
||||
.init(
|
||||
name: "TrustCache",
|
||||
fileName: "Image.dmg.trustcache",
|
||||
relativePath: "DDI/Image.dmg.trustcache",
|
||||
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg.trustcache"
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
private enum DDIMountMethod: String {
|
||||
case cryptex
|
||||
case personalized
|
||||
}
|
||||
|
||||
private struct DDIDownloadItem {
|
||||
let name: String
|
||||
let fileName: String
|
||||
let relativePath: String
|
||||
let urlString: String
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
//
|
||||
// IdeviceLogFileManager.swift
|
||||
// StikDebug
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
final class IdeviceLogFileManager {
|
||||
static let shared = IdeviceLogFileManager()
|
||||
static let logURL = URL.documentsDirectory.appendingPathComponent("idevice_log.txt")
|
||||
|
||||
private static let maximumSize: UInt64 = 20 * 1024 * 1024
|
||||
private static let retainedSize: UInt64 = 10 * 1024 * 1024
|
||||
private static let checkInterval: TimeInterval = 5
|
||||
|
||||
private let queue = DispatchQueue(label: "com.stik.ideviceLogFile", qos: .utility)
|
||||
private var timer: DispatchSourceTimer?
|
||||
|
||||
private init() {}
|
||||
|
||||
func prepareForLogging() {
|
||||
compactIfNeeded()
|
||||
|
||||
queue.async {
|
||||
guard self.timer == nil else { return }
|
||||
|
||||
let timer = DispatchSource.makeTimerSource(queue: self.queue)
|
||||
timer.schedule(deadline: .now() + Self.checkInterval, repeating: Self.checkInterval)
|
||||
timer.setEventHandler { [weak self] in
|
||||
self?.compactIfNeeded()
|
||||
}
|
||||
self.timer = timer
|
||||
timer.resume()
|
||||
}
|
||||
}
|
||||
|
||||
private func compactIfNeeded() {
|
||||
let fileManager = FileManager.default
|
||||
guard let attributes = try? fileManager.attributesOfItem(atPath: Self.logURL.path),
|
||||
let fileSize = attributes[.size] as? NSNumber,
|
||||
fileSize.uint64Value > Self.maximumSize,
|
||||
let handle = try? FileHandle(forUpdating: Self.logURL) else {
|
||||
return
|
||||
}
|
||||
|
||||
defer { try? handle.close() }
|
||||
|
||||
do {
|
||||
let endOffset = try handle.seekToEnd()
|
||||
let startOffset = endOffset > Self.retainedSize ? endOffset - Self.retainedSize : 0
|
||||
try handle.seek(toOffset: startOffset)
|
||||
var recentData = try handle.readToEnd() ?? Data()
|
||||
|
||||
if startOffset > 0, let newlineIndex = recentData.firstIndex(of: 0x0A) {
|
||||
recentData.removeSubrange(recentData.startIndex...newlineIndex)
|
||||
}
|
||||
|
||||
try handle.truncate(atOffset: 0)
|
||||
try handle.seek(toOffset: 0)
|
||||
try handle.write(contentsOf: recentData)
|
||||
try handle.synchronize()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
import Foundation
|
||||
import CoreLocation
|
||||
import Combine
|
||||
|
||||
final class LocationSimulationSession: ObservableObject {
|
||||
static let shared = LocationSimulationSession()
|
||||
|
||||
@Published private(set) var isActive = false
|
||||
@Published private(set) var coordinate: CLLocationCoordinate2D?
|
||||
@Published private(set) var routeCoordinates: [CLLocationCoordinate2D]?
|
||||
private var resendTimer: Timer?
|
||||
private var routeID: UUID?
|
||||
|
||||
private init() {}
|
||||
|
||||
func start(at coordinate: CLLocationCoordinate2D) {
|
||||
self.coordinate = coordinate
|
||||
if !isActive {
|
||||
isActive = true
|
||||
BackgroundAudioManager.shared.requestStart()
|
||||
BackgroundLocationManager.shared.requestStart()
|
||||
}
|
||||
}
|
||||
|
||||
func startResending(at coordinate: CLLocationCoordinate2D, _ operation: @escaping () -> Void) {
|
||||
start(at: coordinate)
|
||||
resendTimer?.invalidate()
|
||||
let timer = Timer(timeInterval: 4, repeats: true) { _ in operation() }
|
||||
RunLoop.main.add(timer, forMode: .common)
|
||||
resendTimer = timer
|
||||
}
|
||||
|
||||
func startRoute(at coordinate: CLLocationCoordinate2D, coordinates: [CLLocationCoordinate2D]) -> UUID {
|
||||
let routeID = UUID()
|
||||
self.routeID = routeID
|
||||
routeCoordinates = coordinates
|
||||
start(at: coordinate)
|
||||
return routeID
|
||||
}
|
||||
|
||||
func isCurrentRoute(_ routeID: UUID) -> Bool {
|
||||
self.routeID == routeID
|
||||
}
|
||||
|
||||
func clearRoute() {
|
||||
routeID = nil
|
||||
routeCoordinates = nil
|
||||
}
|
||||
|
||||
func pauseResending() {
|
||||
resendTimer?.invalidate()
|
||||
resendTimer = nil
|
||||
}
|
||||
|
||||
func updateCoordinate(_ coordinate: CLLocationCoordinate2D) {
|
||||
self.coordinate = coordinate
|
||||
}
|
||||
|
||||
func stop() {
|
||||
pauseResending()
|
||||
clearRoute()
|
||||
guard isActive else { return }
|
||||
isActive = false
|
||||
coordinate = nil
|
||||
BackgroundAudioManager.shared.requestStop()
|
||||
BackgroundLocationManager.shared.requestStop()
|
||||
}
|
||||
}
|
||||
|
|
@ -70,7 +70,12 @@ final class TunnelManager: ObservableObject {
|
|||
}
|
||||
|
||||
private func mountDeveloperDiskImageIfNeeded() {
|
||||
guard DeveloperDiskImageService.filesAreReady else { return }
|
||||
let trustcachePath = URL.documentsDirectory.appendingPathComponent("DDI/Image.dmg.trustcache").path
|
||||
guard FileManager.default.fileExists(atPath: trustcachePath),
|
||||
!MountingProgress.shared.coolisMounted,
|
||||
MountingProgress.shared.mountingThread == nil else {
|
||||
return
|
||||
}
|
||||
MountingProgress.shared.pubMount()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,17 +14,13 @@ extension ScriptStore {
|
|||
appNames: [
|
||||
"Amethyst",
|
||||
"MeloNX",
|
||||
"Melo",
|
||||
"XeniOS",
|
||||
"MeloCafé",
|
||||
"MeloCafe",
|
||||
"Manic EMU",
|
||||
"Manic",
|
||||
"DukeX",
|
||||
"TachyonU",
|
||||
"touchHLE",
|
||||
"HyperHLE",
|
||||
"Applesauce",
|
||||
"RPCS3",
|
||||
"AetherPS4",
|
||||
"Husk"
|
||||
],
|
||||
resource: ScriptResource(resourceName: "universal", fileName: "universal.js")
|
||||
),
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@ import Foundation
|
|||
import UniformTypeIdentifiers
|
||||
|
||||
enum PairingFileStore {
|
||||
static let fileName = "pairingFile.plist"
|
||||
static let fileName = "rp_pairing_file.plist"
|
||||
static let supportedContentTypes: [UTType] = [
|
||||
UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!,
|
||||
UTType(filenameExtension: "mobiledevicepair", conformingTo: .data)!,
|
||||
.propertyList
|
||||
]
|
||||
|
||||
private static let legacyFileName = "rp_pairing_file.plist"
|
||||
private static let legacyFileName = "pairingFile.plist"
|
||||
|
||||
static var url: URL {
|
||||
directoryURL.appendingPathComponent(fileName)
|
||||
|
|
@ -25,31 +25,31 @@ enum PairingFileStore {
|
|||
let destination = url
|
||||
try? fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
|
||||
|
||||
if let documentURL = documentSourceURL(fileManager: fileManager) {
|
||||
if !fileManager.fileExists(atPath: destination.path) ||
|
||||
!fileManager.contentsEqual(atPath: documentURL.path, andPath: destination.path) {
|
||||
try? replaceItem(at: destination, with: documentURL, fileManager: fileManager)
|
||||
protectPairingFile(at: destination, fileManager: fileManager)
|
||||
}
|
||||
guard !fileManager.fileExists(atPath: destination.path) else {
|
||||
removeLegacyCopies(fileManager: fileManager)
|
||||
return destination
|
||||
}
|
||||
|
||||
if !fileManager.fileExists(atPath: destination.path) {
|
||||
migrateLegacyCopy(to: destination, fileManager: fileManager)
|
||||
}
|
||||
migrateLegacyCopy(to: destination, fileManager: fileManager)
|
||||
return destination
|
||||
}
|
||||
|
||||
static func replace(with sourceURL: URL, fileManager: FileManager = .default) throws {
|
||||
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
|
||||
let destination = prepareURL(fileManager: fileManager)
|
||||
|
||||
let documentsDestination = documentsURL
|
||||
if sourceURL.standardizedFileURL != documentsDestination.standardizedFileURL {
|
||||
try replaceItem(at: documentsDestination, with: sourceURL, fileManager: fileManager)
|
||||
let tempURL = directoryURL.appendingPathComponent(UUID().uuidString + ".tmp")
|
||||
try? fileManager.removeItem(at: tempURL)
|
||||
try fileManager.copyItem(at: sourceURL, to: tempURL)
|
||||
defer { try? fileManager.removeItem(at: tempURL) }
|
||||
|
||||
if fileManager.fileExists(atPath: destination.path) {
|
||||
_ = try fileManager.replaceItemAt(destination, withItemAt: tempURL)
|
||||
} else {
|
||||
try fileManager.moveItem(at: tempURL, to: destination)
|
||||
}
|
||||
|
||||
try replaceItem(at: url, with: documentsDestination, fileManager: fileManager)
|
||||
protectPairingFile(at: url, fileManager: fileManager)
|
||||
removeLegacyCopies(fileManager: fileManager)
|
||||
protectPairingFile(at: destination, fileManager: fileManager)
|
||||
}
|
||||
|
||||
static func importFromPicker(_ sourceURL: URL, fileManager: FileManager = .default) throws {
|
||||
|
|
@ -83,33 +83,19 @@ enum PairingFileStore {
|
|||
|
||||
private static var legacyURLs: [URL] {
|
||||
[
|
||||
directoryURL.appendingPathComponent(legacyFileName),
|
||||
documentsURL,
|
||||
URL.documentsDirectory.appendingPathComponent(fileName),
|
||||
URL.documentsDirectory.appendingPathComponent(legacyFileName)
|
||||
]
|
||||
}
|
||||
|
||||
private static var documentsURL: URL {
|
||||
URL.documentsDirectory.appendingPathComponent(fileName)
|
||||
}
|
||||
|
||||
private static func documentSourceURL(fileManager: FileManager) -> URL? {
|
||||
[documentsURL, URL.documentsDirectory.appendingPathComponent(legacyFileName)]
|
||||
.filter { fileManager.fileExists(atPath: $0.path) }
|
||||
.max {
|
||||
let firstDate = (try? $0.resourceValues(forKeys: [.contentModificationDateKey]))?.contentModificationDate ?? .distantPast
|
||||
let secondDate = (try? $1.resourceValues(forKeys: [.contentModificationDateKey]))?.contentModificationDate ?? .distantPast
|
||||
return firstDate < secondDate
|
||||
}
|
||||
}
|
||||
|
||||
private static func migrateLegacyCopy(to destination: URL, fileManager: FileManager) {
|
||||
for legacyURL in legacyURLs where fileManager.fileExists(atPath: legacyURL.path) {
|
||||
do {
|
||||
try replaceItem(at: destination, with: legacyURL, fileManager: fileManager)
|
||||
try fileManager.moveItem(at: legacyURL, to: destination)
|
||||
} catch {
|
||||
if let data = try? Data(contentsOf: legacyURL) {
|
||||
try? data.write(to: destination, options: .atomic)
|
||||
try? fileManager.removeItem(at: legacyURL)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -118,21 +104,6 @@ enum PairingFileStore {
|
|||
}
|
||||
}
|
||||
|
||||
private static func replaceItem(at destination: URL, with source: URL, fileManager: FileManager) throws {
|
||||
let temporary = destination
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent(UUID().uuidString + ".tmp")
|
||||
try? fileManager.removeItem(at: temporary)
|
||||
try fileManager.copyItem(at: source, to: temporary)
|
||||
defer { try? fileManager.removeItem(at: temporary) }
|
||||
|
||||
if fileManager.fileExists(atPath: destination.path) {
|
||||
_ = try fileManager.replaceItemAt(destination, withItemAt: temporary)
|
||||
} else {
|
||||
try fileManager.moveItem(at: temporary, to: destination)
|
||||
}
|
||||
}
|
||||
|
||||
private static func removeLegacyCopies(fileManager: FileManager) {
|
||||
for legacyURL in legacyURLs where fileManager.fileExists(atPath: legacyURL.path) {
|
||||
try? fileManager.removeItem(at: legacyURL)
|
||||
|
|
|
|||
|
|
@ -15,9 +15,5 @@ extension UserDefaults {
|
|||
static let defaultScriptName = "DefaultScriptName"
|
||||
static let defaultScriptNameValue = ""
|
||||
static let targetDeviceIP = "TunnelDeviceIP"
|
||||
/// Attaches processes with MallocGuardEdges and MallocScribble.
|
||||
static let mallocDebug = "enableMallocDebug"
|
||||
static let mallocGuardEdges = "enableMallocGuardEdges"
|
||||
static let mallocScribble = "enableMallocScribble"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -266,7 +266,7 @@ struct ConsoleLogsView: View {
|
|||
|
||||
@ViewBuilder
|
||||
private var exportMenuOption: some View {
|
||||
let logURL = IdeviceLogFileManager.logURL
|
||||
let logURL: URL = URL.documentsDirectory.appendingPathComponent("idevice_log.txt")
|
||||
if FileManager.default.fileExists(atPath: logURL.path) {
|
||||
ShareLink(
|
||||
item: logURL,
|
||||
|
|
@ -309,7 +309,7 @@ struct ConsoleLogsView: View {
|
|||
guard !isLoadingLogs else { return }
|
||||
isLoadingLogs = true
|
||||
|
||||
let logPath = IdeviceLogFileManager.logURL.path
|
||||
let logPath = URL.documentsDirectory.appendingPathComponent("idevice_log.txt").path
|
||||
|
||||
guard FileManager.default.fileExists(atPath: logPath) else {
|
||||
await MainActor.run {
|
||||
|
|
@ -368,7 +368,7 @@ struct ConsoleLogsView: View {
|
|||
guard !isLoadingLogs else { return }
|
||||
isLoadingLogs = true
|
||||
|
||||
let logPath = IdeviceLogFileManager.logURL.path
|
||||
let logPath = URL.documentsDirectory.appendingPathComponent("idevice_log.txt").path
|
||||
let previousCount = lastProcessedLineCount
|
||||
|
||||
guard FileManager.default.fileExists(atPath: logPath) else {
|
||||
|
|
|
|||
|
|
@ -12,14 +12,17 @@ struct HomeView: View {
|
|||
@AppStorage("bundleID") private var bundleID: String = ""
|
||||
@AppStorage(UserDefaults.Keys.confirmExternalJITRequests) private var confirmExternalJITRequests = true
|
||||
|
||||
@ObservedObject private var mounting = MountingProgress.shared
|
||||
|
||||
@State private var hasAppeared = false
|
||||
@State private var pendingJITEnableConfiguration: JITEnableConfiguration?
|
||||
@State private var isShowingPairingFilePicker = false
|
||||
@State private var debugFeedback: DebugFeedback?
|
||||
@State private var pendingExternalURLAction: HomeExternalAction?
|
||||
@State private var pendingLiveContainerApp: LiveContainerApp?
|
||||
@State private var scriptRunModel: RunJSViewModel?
|
||||
|
||||
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||
|
||||
private struct DebugFeedback: Identifiable {
|
||||
let id = UUID()
|
||||
let message: String
|
||||
|
|
@ -27,18 +30,11 @@ struct HomeView: View {
|
|||
let isWorking: Bool
|
||||
}
|
||||
|
||||
private struct LiveContainerApp {
|
||||
let bundleID: String
|
||||
let name: String
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
InstalledAppsListView(onSelectApp: { selectedBundle, selectedName in
|
||||
if selectedName.localizedCaseInsensitiveContains("LiveContainer") {
|
||||
pendingLiveContainerApp = LiveContainerApp(bundleID: selectedBundle, name: selectedName)
|
||||
} else {
|
||||
enableJITForSelectedApp(bundleID: selectedBundle, name: selectedName)
|
||||
}
|
||||
bundleID = selectedBundle
|
||||
Haptics.medium()
|
||||
startJITInBackground(bundleID: selectedBundle, displayName: selectedName)
|
||||
}, showDoneButton: false, onImportPairingFile: { isShowingPairingFilePicker = true })
|
||||
.overlay(alignment: .bottom) {
|
||||
if let debugFeedback {
|
||||
|
|
@ -49,6 +45,9 @@ struct HomeView: View {
|
|||
}
|
||||
.onAppear(perform: handleAppear)
|
||||
.onReceive(NotificationCenter.default.publisher(for: .intentJSScriptReady), perform: handleScriptReadyNotification)
|
||||
.onReceive(timer) { _ in
|
||||
refreshMountStatusIfNeeded()
|
||||
}
|
||||
.onOpenURL { url in
|
||||
handleExternalURL(url)
|
||||
}
|
||||
|
|
@ -75,28 +74,6 @@ struct HomeView: View {
|
|||
} message: { action in
|
||||
Text(action.message)
|
||||
}
|
||||
.alert(
|
||||
"Enable JIT for LiveContainer?",
|
||||
isPresented: Binding(
|
||||
get: { pendingLiveContainerApp != nil },
|
||||
set: { isPresented in
|
||||
if !isPresented {
|
||||
pendingLiveContainerApp = nil
|
||||
}
|
||||
}
|
||||
),
|
||||
presenting: pendingLiveContainerApp
|
||||
) { app in
|
||||
Button("Cancel", role: .cancel) {
|
||||
pendingLiveContainerApp = nil
|
||||
}
|
||||
Button("Proceed") {
|
||||
pendingLiveContainerApp = nil
|
||||
enableJITForSelectedApp(bundleID: app.bundleID, name: app.name)
|
||||
}
|
||||
} message: { _ in
|
||||
Text("StikDebug isn't meant to enable JIT for LiveContainer manually. Select StikDebug in LiveContainer settings, enable \"Run with JIT\" for your app, then open your app normally. It will automatically jump back here.")
|
||||
}
|
||||
.fileImporter(
|
||||
isPresented: $isShowingPairingFilePicker,
|
||||
allowedContentTypes: PairingFileStore.supportedContentTypes,
|
||||
|
|
@ -115,14 +92,9 @@ struct HomeView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func enableJITForSelectedApp(bundleID selectedBundle: String, name: String) {
|
||||
bundleID = selectedBundle
|
||||
Haptics.medium()
|
||||
startJITInBackground(bundleID: selectedBundle, displayName: name)
|
||||
}
|
||||
|
||||
private func handleAppear() {
|
||||
startTunnelInBackground()
|
||||
MountingProgress.shared.checkforMounted()
|
||||
hasAppeared = true
|
||||
|
||||
if let config = pendingJITEnableConfiguration {
|
||||
|
|
@ -145,6 +117,13 @@ struct HomeView: View {
|
|||
scriptRunModel = model
|
||||
}
|
||||
|
||||
private func refreshMountStatusIfNeeded() {
|
||||
guard mounting.mountingThread == nil, !mounting.coolisMounted else {
|
||||
return
|
||||
}
|
||||
MountingProgress.shared.checkforMounted()
|
||||
}
|
||||
|
||||
private func importPairingFile(_ result: Result<URL, Error>) {
|
||||
switch result {
|
||||
case .success(let url):
|
||||
|
|
|
|||
|
|
@ -83,9 +83,7 @@ struct InstalledAppsListView: View {
|
|||
handleLoadingChange(isLoading)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .pairingFileImported)) { _ in
|
||||
if TunnelManager.shared.isConnected {
|
||||
viewModel.refreshAppLists()
|
||||
}
|
||||
viewModel.refreshAppLists()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
import CoreLocation
|
||||
|
||||
private enum ExternalLocationAction: Identifiable {
|
||||
case simulate(URL, Double, Double)
|
||||
|
|
@ -224,22 +223,7 @@ struct MainTabView: View {
|
|||
|
||||
DispatchQueue.main.async {
|
||||
if code == 0 {
|
||||
LocationSimulationSession.shared.clearRoute()
|
||||
LocationSimulationSession.shared.startResending(
|
||||
at: CLLocationCoordinate2D(
|
||||
latitude: coordinate.latitude,
|
||||
longitude: coordinate.longitude
|
||||
)
|
||||
) {
|
||||
LocationSimulationCommandQueue.shared.async {
|
||||
_ = simulate_location(
|
||||
DeviceConnectionContext.targetIPAddress,
|
||||
coordinate.latitude,
|
||||
coordinate.longitude,
|
||||
pairingFile.path
|
||||
)
|
||||
}
|
||||
}
|
||||
BackgroundLocationManager.shared.requestStart()
|
||||
LogManager.shared.addInfoLog(
|
||||
String(format: "Simulated location from URL: %.6f, %.6f", coordinate.latitude, coordinate.longitude)
|
||||
)
|
||||
|
|
@ -259,7 +243,7 @@ struct MainTabView: View {
|
|||
let code = clear_simulated_location()
|
||||
DispatchQueue.main.async {
|
||||
if code == 0 {
|
||||
LocationSimulationSession.shared.stop()
|
||||
BackgroundLocationManager.shared.requestStop()
|
||||
LogManager.shared.addInfoLog("Cleared simulated location from URL")
|
||||
} else {
|
||||
showAlert(
|
||||
|
|
|
|||
|
|
@ -724,11 +724,11 @@ final class LocationSearchCompleter: NSObject, ObservableObject, MKLocalSearchCo
|
|||
}
|
||||
|
||||
struct LocationSimulationView: View {
|
||||
@ObservedObject private var simulationSession = LocationSimulationSession.shared
|
||||
@State private var coordinate: CLLocationCoordinate2D?
|
||||
@State private var position: MapCameraPosition = .userLocation(fallback: .automatic)
|
||||
|
||||
@State private var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
|
||||
@State private var resendTimer: Timer?
|
||||
@State private var routeLoadTask: Task<Void, Never>?
|
||||
@State private var routeSpeedPrefetchTask: Task<Void, Never>?
|
||||
@State private var routePlaybackTask: Task<Void, Never>?
|
||||
|
|
@ -788,11 +788,11 @@ struct LocationSimulationView: View {
|
|||
}
|
||||
|
||||
private var hasActiveSimulation: Bool {
|
||||
simulationSession.isActive || simulatedCoordinate != nil || routePlaybackTask != nil
|
||||
simulatedCoordinate != nil || routePlaybackTask != nil
|
||||
}
|
||||
|
||||
private var isRouteRunning: Bool {
|
||||
routePlaybackTask != nil || simulationSession.routeCoordinates != nil
|
||||
routePlaybackTask != nil
|
||||
}
|
||||
|
||||
private var hasRouteContext: Bool {
|
||||
|
|
@ -801,12 +801,7 @@ struct LocationSimulationView: View {
|
|||
routePlan != nil ||
|
||||
isLoadingRoute ||
|
||||
isPrefetchingRouteSpeeds ||
|
||||
routePlaybackCoordinate != nil ||
|
||||
simulationSession.routeCoordinates != nil
|
||||
}
|
||||
|
||||
private var displayedRouteCoordinate: CLLocationCoordinate2D? {
|
||||
simulationSession.routeCoordinates == nil ? routePlaybackCoordinate : simulationSession.coordinate
|
||||
routePlaybackCoordinate != nil
|
||||
}
|
||||
|
||||
private var routeSummaryText: String? {
|
||||
|
|
@ -823,9 +818,6 @@ struct LocationSimulationView: View {
|
|||
}
|
||||
|
||||
private var routeStatusText: String {
|
||||
if simulationSession.routeCoordinates != nil {
|
||||
return "Route simulation active."
|
||||
}
|
||||
if isLoadingRoute {
|
||||
return "Calculating route…"
|
||||
}
|
||||
|
|
@ -900,8 +892,8 @@ struct LocationSimulationView: View {
|
|||
Marker("End", coordinate: routeEndCoordinate)
|
||||
.tint(.red)
|
||||
}
|
||||
if let displayedRouteCoordinate {
|
||||
Marker("Current", coordinate: displayedRouteCoordinate)
|
||||
if let routePlaybackCoordinate {
|
||||
Marker("Current", coordinate: routePlaybackCoordinate)
|
||||
.tint(.blue)
|
||||
}
|
||||
} else if let coordinate {
|
||||
|
|
@ -1033,28 +1025,17 @@ struct LocationSimulationView: View {
|
|||
}
|
||||
.onAppear {
|
||||
loadBookmarks()
|
||||
if let routeCoordinates = simulationSession.routeCoordinates {
|
||||
coordinate = nil
|
||||
routePolyline = makeRoutePolyline(for: routeCoordinates)
|
||||
routeStartSelection = routeCoordinates.first.map {
|
||||
RouteSearchSelection(title: "Route Start", coordinate: $0)
|
||||
}
|
||||
routeEndSelection = routeCoordinates.last.map {
|
||||
RouteSearchSelection(title: "Route End", coordinate: $0)
|
||||
}
|
||||
if let routePolyline {
|
||||
position = .rect(routePolyline.boundingMapRect)
|
||||
}
|
||||
} else if let activeCoordinate = simulationSession.coordinate {
|
||||
coordinate = activeCoordinate
|
||||
simulatedCoordinate = activeCoordinate
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
routeLoadTask?.cancel()
|
||||
routeLoadTask = nil
|
||||
routeSpeedPrefetchTask?.cancel()
|
||||
routeSpeedPrefetchTask = nil
|
||||
cancelRoutePlayback(resetMarker: true)
|
||||
stopResendLoop()
|
||||
if backgroundTaskID != .invalid {
|
||||
BackgroundLocationManager.shared.requestStop()
|
||||
}
|
||||
endBackgroundTask()
|
||||
}
|
||||
}
|
||||
|
|
@ -1316,14 +1297,14 @@ struct LocationSimulationView: View {
|
|||
) {
|
||||
routePlaybackCoordinate = nil
|
||||
beginBackgroundTask()
|
||||
LocationSimulationSession.shared.clearRoute()
|
||||
startResendLoop(with: coord)
|
||||
BackgroundLocationManager.shared.requestStart()
|
||||
}
|
||||
}
|
||||
|
||||
private func simulateRoute() {
|
||||
guard pairingExists,
|
||||
let routePlan,
|
||||
routePlan != nil,
|
||||
let firstCoordinate = routePlaybackSamples.first?.coordinate,
|
||||
!isBusy else {
|
||||
return
|
||||
|
|
@ -1338,13 +1319,10 @@ struct LocationSimulationView: View {
|
|||
operation: { locationUpdateCode(for: firstCoordinate) }
|
||||
) {
|
||||
beginBackgroundTask()
|
||||
let routeID = LocationSimulationSession.shared.startRoute(
|
||||
at: firstCoordinate,
|
||||
coordinates: routePlan.displayCoordinates
|
||||
)
|
||||
BackgroundLocationManager.shared.requestStart()
|
||||
simulatedCoordinate = nil
|
||||
routePlaybackCoordinate = firstCoordinate
|
||||
startRoutePlayback(routeID: routeID)
|
||||
startRoutePlayback()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1378,14 +1356,13 @@ struct LocationSimulationView: View {
|
|||
routeSpeedPrefetchTask = nil
|
||||
cancelRoutePlayback(resetMarker: true)
|
||||
stopResendLoop()
|
||||
LocationSimulationSession.shared.clearRoute()
|
||||
runLocationCommand(
|
||||
errorTitle: "Clear Failed",
|
||||
errorMessage: { code in "Could not clear simulated location (error \(code))." },
|
||||
operation: clear_simulated_location
|
||||
) {
|
||||
endBackgroundTask()
|
||||
LocationSimulationSession.shared.stop()
|
||||
BackgroundLocationManager.shared.requestStop()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1402,15 +1379,18 @@ struct LocationSimulationView: View {
|
|||
|
||||
private func startResendLoop(with coordinate: CLLocationCoordinate2D) {
|
||||
simulatedCoordinate = coordinate
|
||||
LocationSimulationSession.shared.startResending(at: coordinate) {
|
||||
resendTimer?.invalidate()
|
||||
resendTimer = Timer.scheduledTimer(withTimeInterval: 4, repeats: true) { _ in
|
||||
guard let simulatedCoordinate else { return }
|
||||
LocationSimulationCommandQueue.shared.async {
|
||||
_ = locationUpdateCode(for: coordinate)
|
||||
_ = locationUpdateCode(for: simulatedCoordinate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func stopResendLoop() {
|
||||
LocationSimulationSession.shared.pauseResending()
|
||||
resendTimer?.invalidate()
|
||||
resendTimer = nil
|
||||
simulatedCoordinate = nil
|
||||
}
|
||||
|
||||
|
|
@ -1540,19 +1520,15 @@ struct LocationSimulationView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func startRoutePlayback(routeID: UUID) {
|
||||
private func startRoutePlayback() {
|
||||
routePlaybackTask = Task {
|
||||
var lastSuccessfulCoordinate = routePlaybackSamples.first?.coordinate
|
||||
|
||||
for sample in routePlaybackSamples.dropFirst() {
|
||||
try? await Task.sleep(for: .seconds(sample.delayFromPrevious))
|
||||
guard !Task.isCancelled,
|
||||
LocationSimulationSession.shared.isCurrentRoute(routeID) else {
|
||||
return
|
||||
}
|
||||
guard !Task.isCancelled else { return }
|
||||
|
||||
let code = await sendLocationUpdate(for: sample.coordinate)
|
||||
guard LocationSimulationSession.shared.isCurrentRoute(routeID) else { return }
|
||||
guard code == 0 else {
|
||||
await MainActor.run {
|
||||
routePlaybackTask = nil
|
||||
|
|
@ -1570,12 +1546,10 @@ struct LocationSimulationView: View {
|
|||
lastSuccessfulCoordinate = sample.coordinate
|
||||
await MainActor.run {
|
||||
routePlaybackCoordinate = sample.coordinate
|
||||
LocationSimulationSession.shared.updateCoordinate(sample.coordinate)
|
||||
}
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
guard LocationSimulationSession.shared.isCurrentRoute(routeID) else { return }
|
||||
routePlaybackTask = nil
|
||||
if let lastSuccessfulCoordinate {
|
||||
routePlaybackCoordinate = lastSuccessfulCoordinate
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import SwiftUI
|
|||
import UIKit
|
||||
|
||||
private enum SettingsLinks {
|
||||
static let githubStars = URL(string: "https://github.com/StikDebug/StikDebug")!
|
||||
static let pairingFileGuide = URL(string: "https://github.com/StikDebug/StikDebug-Guide/blob/main/pairing_file.md")!
|
||||
static let githubStars = URL(string: "https://github.com/StephenDev0/StikDebug/stargazers")!
|
||||
static let pairingFileGuide = URL(string: "https://github.com/StephenDev0/StikDebug-Guide/blob/main/pairing_file.md")!
|
||||
static let localDevVPN = URL(string: "https://apps.apple.com/us/app/localdevvpn/id6755608044")!
|
||||
static let discord = URL(string: "https://discord.gg/qahjXNTDwS")!
|
||||
}
|
||||
|
|
@ -19,7 +19,6 @@ struct SettingsView: View {
|
|||
@AppStorage("keepAliveAudio") private var keepAliveAudio = true
|
||||
@AppStorage("keepAliveLocation") private var keepAliveLocation = true
|
||||
@AppStorage(UserDefaults.Keys.targetDeviceIP) private var targetDeviceIP = DeviceConnectionContext.defaultTargetIPAddress
|
||||
@AppStorage(UserDefaults.Keys.mallocDebug) private var mallocDebug = false
|
||||
|
||||
@State private var isShowingPairingFilePicker = false
|
||||
@State private var isImportingFile = false
|
||||
|
|
@ -95,7 +94,8 @@ struct SettingsView: View {
|
|||
}
|
||||
}
|
||||
.onChange(of: keepAliveAudio) { _, enabled in
|
||||
handleAudioKeepAliveChange(enabled)
|
||||
if enabled { BackgroundAudioManager.shared.start() }
|
||||
else { BackgroundAudioManager.shared.stop() }
|
||||
}
|
||||
|
||||
Toggle(isOn: $keepAliveLocation) {
|
||||
|
|
@ -106,7 +106,7 @@ struct SettingsView: View {
|
|||
}
|
||||
}
|
||||
.onChange(of: keepAliveLocation) { _, enabled in
|
||||
handleLocationKeepAliveChange(enabled)
|
||||
if !enabled { BackgroundLocationManager.shared.stop() }
|
||||
}
|
||||
|
||||
} header: {
|
||||
|
|
@ -131,16 +131,6 @@ struct SettingsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
Section("Debugging") {
|
||||
Toggle(isOn: $mallocDebug) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Malloc Debugging")
|
||||
Text("Attaches processes with MallocGuardEdges and MallocScribble to detect memory corruption.")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("Advanced") {
|
||||
HStack {
|
||||
Text("Target Device IP")
|
||||
|
|
@ -241,25 +231,6 @@ struct SettingsView: View {
|
|||
return "Version \(appVersion) • iOS \(UIDevice.current.systemVersion) • \(txmLabel)"
|
||||
}
|
||||
|
||||
private func handleAudioKeepAliveChange(_ enabled: Bool) {
|
||||
if !enabled, !keepAliveLocation {
|
||||
keepAliveLocation = true
|
||||
BackgroundLocationManager.shared.requestAuthorizationIfNeeded()
|
||||
}
|
||||
BackgroundAudioManager.shared.configurationDidChange()
|
||||
BackgroundLocationManager.shared.configurationDidChange()
|
||||
}
|
||||
|
||||
private func handleLocationKeepAliveChange(_ enabled: Bool) {
|
||||
if enabled {
|
||||
BackgroundLocationManager.shared.requestAuthorizationIfNeeded()
|
||||
} else if !keepAliveAudio {
|
||||
keepAliveAudio = true
|
||||
}
|
||||
BackgroundAudioManager.shared.configurationDidChange()
|
||||
BackgroundLocationManager.shared.configurationDidChange()
|
||||
}
|
||||
|
||||
// MARK: - Business Logic
|
||||
|
||||
private func openAppFolder() {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue