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.6" have entirely different histories.
34 changed files with 519 additions and 4752 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
|
|
||||||
75
.github/workflows/build_ipa.yml
vendored
75
.github/workflows/build_ipa.yml
vendored
|
|
@ -3,20 +3,17 @@ name: Build Debug IPA
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
tags: [ "[0-9]*" ]
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build IPA
|
name: Build IPA
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
outputs:
|
|
||||||
version: ${{ steps.version.outputs.version }}
|
|
||||||
env:
|
env:
|
||||||
UPLOAD_IPA: ${{ vars.UPLOAD_IPA || 'true' }}
|
UPLOAD_IPA: ${{ vars.UPLOAD_IPA || 'true' }}
|
||||||
|
|
||||||
|
|
@ -27,7 +24,7 @@ jobs:
|
||||||
- name: 2. Configure Xcode Environment
|
- name: 2. Configure Xcode Environment
|
||||||
uses: maxim-lobanov/setup-xcode@v1
|
uses: maxim-lobanov/setup-xcode@v1
|
||||||
with:
|
with:
|
||||||
xcode-version: '26.6'
|
xcode-version: '26.0.1'
|
||||||
|
|
||||||
- name: 3. Compile Project Archive
|
- name: 3. Compile Project Archive
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -53,70 +50,10 @@ jobs:
|
||||||
zip -r StikDebug.ipa Payload
|
zip -r StikDebug.ipa Payload
|
||||||
rm -rf Payload StikDebug.app
|
rm -rf Payload StikDebug.app
|
||||||
|
|
||||||
- name: 5. Name workflow artifact
|
- name: 5. Store IPA as Workflow Artifact
|
||||||
id: artifact
|
|
||||||
if: env.UPLOAD_IPA == 'true'
|
if: env.UPLOAD_IPA == 'true'
|
||||||
run: |
|
uses: actions/upload-artifact@v4
|
||||||
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:
|
with:
|
||||||
path: ${{ steps.artifact.outputs.file_name }}
|
name: StikDebug-Debug.ipa
|
||||||
archive: false
|
path: StikDebug.ipa
|
||||||
retention-days: 90
|
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 }}'
|
|
||||||
|
|
|
||||||
104
.github/workflows/updatesource.yml
vendored
Normal file
104
.github/workflows/updatesource.yml
vendored
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
name: Update StikJIT Source
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# Add permissions block to ensure the workflow can push changes
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-source:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
repository: 0-Blu/StikJIT
|
||||||
|
ref: main
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install requests
|
||||||
|
|
||||||
|
- name: Record job start time
|
||||||
|
id: job_start_time
|
||||||
|
run: echo "start_time=$(date +%s)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Debug repository information
|
||||||
|
run: |
|
||||||
|
echo "Repository: 0-Blu/StikJIT"
|
||||||
|
echo "Event name: $GITHUB_EVENT_NAME"
|
||||||
|
echo "Current directory: $(pwd)"
|
||||||
|
echo "Repository contents:"
|
||||||
|
ls -la
|
||||||
|
|
||||||
|
- name: Update StikJIT source
|
||||||
|
id: update_source
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
# Configure git
|
||||||
|
git config --global user.name 'GitHub Action'
|
||||||
|
git config --global user.email 'action@github.com'
|
||||||
|
|
||||||
|
# Run the update script
|
||||||
|
python update_json.py
|
||||||
|
|
||||||
|
# Check for changes and commit if needed
|
||||||
|
git add repo.json
|
||||||
|
if git diff --staged --quiet; then
|
||||||
|
echo "changes=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "No changes detected in repo.json"
|
||||||
|
else
|
||||||
|
echo "Changes detected in repo.json:"
|
||||||
|
git diff --staged
|
||||||
|
git commit -m "Updated source with latest StikJIT release"
|
||||||
|
# Push to main branch of original repository
|
||||||
|
git push origin main
|
||||||
|
echo "changes=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Calculate job duration
|
||||||
|
id: duration
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
end_time=$(date +%s)
|
||||||
|
duration=$((end_time - ${{ steps.job_start_time.outputs.start_time }}))
|
||||||
|
echo "duration=$duration seconds" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Create job summary
|
||||||
|
run: |
|
||||||
|
if [[ "${{ steps.update_source.outputs.changes }}" == "true" ]]; then
|
||||||
|
echo "## Update StikJIT Source Summary 🚀" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "✅ Changes Detected and Pushed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "The repo.json file has been updated with the latest release information." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "## Update StikJIT Source Summary 🚀" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "🔍 No Changes Detected" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "The repo.json file is up to date. No changes were necessary." >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "This could be because:" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- The release version is already in the repo.json file" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- The release doesn't contain an IPA file" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- The release tag doesn't match the expected version format" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "🕐 Execution Time" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "This job took ${{ steps.duration.outputs.duration }} to complete." >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "📆 Next Scheduled Run" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "The next scheduled run will be triggered when a new release is published." >> $GITHUB_STEP_SUMMARY
|
||||||
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>
|
<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>
|
</div>
|
||||||
|
|
||||||
<h3 align="center">
|
<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://discord.gg/ZnNcrRT3M8">
|
||||||
<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>
|
<img src="https://img.shields.io/badge/Discord-join%20us-7289DA?logo=discord&logoColor=white&style=for-the-badge&labelColor=23272A" />
|
||||||
<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>
|
||||||
<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://github.com/StephenDev0/StikDebug/blob/main/LICENSE">
|
||||||
<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>
|
<img src="https://img.shields.io/github/license/StephenDev0/StikDebug?label=License&color=5865F2&style=for-the-badge&labelColor=23272A" />
|
||||||
<br />
|
</a>
|
||||||
</h3>
|
<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
|
## 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.
|
- **App Launching:** Launch every app installed on your device.
|
||||||
- **Console:** Live app and system logs.
|
- **Console:** Live app and system logs.
|
||||||
- **Scripts:** Manage automation scripts (mainly used for iOS 26 JIT).
|
- **Scripts:** Manage automation scripts (mainly used for iOS 26 JIT).
|
||||||
|
|
@ -30,11 +37,14 @@
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> **Notice:** StikDebug is no longer available on the App Store. Please use the official download methods below.
|
> **Notice:** StikDebug is no longer available on the App Store. Please use the official download methods below.
|
||||||
|
|
||||||
<h3 align="center">
|
<div align="center" style="display: flex; justify-content: center; align-items: center; gap: 16px; flex-wrap: wrap;">
|
||||||
<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://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 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>
|
</a>
|
||||||
</h3>
|
<a href="https://github.com/StephenDev0/StikDebug/releases/download/3.1.5/StikDebug-3.1.5.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
|
## Compatibility
|
||||||
|
|
||||||
|
|
@ -56,18 +66,18 @@ StikDebug enables **JIT** for sideloaded apps on iOS 17.4+ without needing a com
|
||||||
|
|
||||||
### Steps
|
### Steps
|
||||||
1. **Obtain a pairing file**
|
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**
|
2. **Set up VPN**
|
||||||
- Launch LocalDevVPN and enable the VPN.
|
- Launch LocalDevVPN and enable the VPN.
|
||||||
|
|
||||||
4. **Enable JIT for an app**
|
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.
|
- Select your sideloaded app from the list in StikDebug.
|
||||||
|
|
||||||
**Troubleshooting**
|
**Troubleshooting**
|
||||||
- "Connection dropped" or loopback errors → Check iOS version compatibility / beta warnings.
|
- "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.
|
- Pairing file issues → Replace file with device unlocked & trusted.
|
||||||
- Still stuck? Join the [Discord](https://discord.gg/ZnNcrRT3M8) with logs/screenshots.
|
- 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
|
## 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
|
### Requirements
|
||||||
- macOS (latest recommended)
|
- macOS (latest recommended)
|
||||||
- Xcode 16+ (Xcode 26+ preferred for iOS 26+ support)
|
- 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
|
### Steps
|
||||||
1. **Clone the repo**
|
1. **Clone the repo**
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/StikDebug/StikDebug.git
|
git clone https://github.com/StephenDev0/StikDebug.git
|
||||||
cd StikDebug
|
cd StikDebug
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -140,11 +146,13 @@ To propose a new feature, open a feature request issue and provide:
|
||||||
|
|
||||||
### Code Contributions (Best Practices)
|
### Code Contributions (Best Practices)
|
||||||
- Follow normal Swift and SwiftUI style.
|
- 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.
|
- Keep your changes consistent with how the project is already set up.
|
||||||
- Make sure everything builds and works without errors.
|
- Make sure everything builds and works without errors.
|
||||||
|
|
||||||
We appreciate your time and effort in helping improve this project.
|
We appreciate your time and effort in helping improve this project.
|
||||||
|
|
||||||
|
## Code Help
|
||||||
|
[](https://deepwiki.com/stephendev0/stikdebug)
|
||||||
## License
|
## License
|
||||||
StikDebug is licensed under **AGPL-3.0**. See [`LICENSE`](LICENSE) for details.
|
StikDebug is licensed under **AGPL-3.0**. See [`LICENSE`](LICENSE) for details.
|
||||||
|
|
|
||||||
|
|
@ -482,7 +482,7 @@
|
||||||
"$(PROJECT_DIR)/StikDebug/idevice",
|
"$(PROJECT_DIR)/StikDebug/idevice",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.1;
|
MACOSX_DEPLOYMENT_TARGET = 15.1;
|
||||||
MARKETING_VERSION = 3.1.15;
|
MARKETING_VERSION = 3.1.6;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.stik.stikdebug;
|
PRODUCT_BUNDLE_IDENTIFIER = com.stik.stikdebug;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|
@ -544,7 +544,7 @@
|
||||||
"$(PROJECT_DIR)/StikDebug/idevice",
|
"$(PROJECT_DIR)/StikDebug/idevice",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 15.1;
|
MACOSX_DEPLOYMENT_TARGET = 15.1;
|
||||||
MARKETING_VERSION = 3.1.15;
|
MARKETING_VERSION = 3.1.6;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.stik.stikdebug;
|
PRODUCT_BUNDLE_IDENTIFIER = com.stik.stikdebug;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,8 @@ import UIKit
|
||||||
|
|
||||||
enum AppBootstrapper {
|
enum AppBootstrapper {
|
||||||
static func configure() {
|
static func configure() {
|
||||||
IdeviceLogFileManager.shared.prepareForLogging()
|
|
||||||
registerDefaultSettings()
|
registerDefaultSettings()
|
||||||
ensureKeepAliveSelection()
|
startConfiguredKeepAliveServices()
|
||||||
applyDocumentPickerCopyWorkaround()
|
applyDocumentPickerCopyWorkaround()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -28,11 +27,11 @@ enum AppBootstrapper {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func ensureKeepAliveSelection() {
|
private static func startConfiguredKeepAliveServices() {
|
||||||
let defaults = UserDefaults.standard
|
guard UserDefaults.standard.bool(forKey: "keepAliveAudio") else {
|
||||||
if !defaults.bool(forKey: "keepAliveAudio"), !defaults.bool(forKey: "keepAliveLocation") {
|
return
|
||||||
defaults.set(true, forKey: "keepAliveAudio")
|
|
||||||
}
|
}
|
||||||
|
BackgroundAudioManager.shared.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func applyDocumentPickerCopyWorkaround() {
|
private static func applyDocumentPickerCopyWorkaround() {
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ struct StikDebugApp: App {
|
||||||
private func downloadMissingDeveloperDiskImageFiles() async {
|
private func downloadMissingDeveloperDiskImageFiles() async {
|
||||||
do {
|
do {
|
||||||
try await DeveloperDiskImageService.shared.downloadMissingFiles()
|
try await DeveloperDiskImageService.shared.downloadMissingFiles()
|
||||||
MountingProgress.shared.pubMount()
|
|
||||||
} catch {
|
} catch {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
showAlert(
|
showAlert(
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,6 @@ enum DeviceConnectionContext {
|
||||||
static let defaultTargetIPAddress = "10.7.0.1"
|
static let defaultTargetIPAddress = "10.7.0.1"
|
||||||
|
|
||||||
static var targetIPAddress: String {
|
static var targetIPAddress: String {
|
||||||
let stored = UserDefaults.standard
|
defaultTargetIPAddress
|
||||||
.string(forKey: UserDefaults.Keys.targetDeviceIP)?
|
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
guard let stored, !stored.isEmpty else {
|
|
||||||
return defaultTargetIPAddress
|
|
||||||
}
|
|
||||||
return stored
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,7 @@ private enum IdeviceBridge {
|
||||||
}
|
}
|
||||||
|
|
||||||
let code = Int(ffiError.pointee.code)
|
let code = Int(ffiError.pointee.code)
|
||||||
let detail = string(from: ffiError.pointee.message) ?? fallback
|
let message = string(from: ffiError.pointee.message) ?? fallback
|
||||||
let message = detail == fallback ? fallback : "\(fallback): \(detail)"
|
|
||||||
idevice_error_free(ffiError)
|
idevice_error_free(ffiError)
|
||||||
return makeError(domain: domain, code: code, message: message)
|
return makeError(domain: domain, code: code, message: message)
|
||||||
}
|
}
|
||||||
|
|
@ -263,69 +262,31 @@ private enum IdeviceBridge {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension JITEnableContext {
|
extension JITEnableContext {
|
||||||
func isDeveloperDiskImageMounted() throws -> Bool {
|
func getMountedDeviceCount() throws -> Int {
|
||||||
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
try IdeviceBridge.withTunnelHandles(for: self) { adapter, handshake in
|
||||||
var cryptexQuerySucceeded = false
|
try IdeviceBridge.withConnectedClient(
|
||||||
var cryptexQueryError: NSError?
|
fallback: "Failed to connect to image mounter",
|
||||||
if DeveloperDiskImageService.usesCryptexDDI {
|
missingClientMessage: "Image mounter client was not created",
|
||||||
var installed: UnsafeMutablePointer<InstalledCryptexC>?
|
connect: { image_mounter_connect_rsd(adapter, handshake, $0) },
|
||||||
defer { cryptexd_free_installed_cryptex(installed) }
|
cleanup: { image_mounter_free($0) }
|
||||||
if let ffiError = cryptexd_installed_ddi(adapter, handshake, &installed) {
|
) { client in
|
||||||
cryptexQueryError = IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to query installed DDI cryptex")
|
var devices: UnsafeMutablePointer<plist_t?>?
|
||||||
} else {
|
var deviceCount = 0
|
||||||
cryptexQuerySucceeded = true
|
if let ffiError = image_mounter_copy_devices(client, &devices, &deviceCount) {
|
||||||
if installed != nil {
|
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to fetch mounted devices")
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
if let devices {
|
||||||
return try IdeviceBridge.withConnectedClient(
|
for index in 0..<deviceCount {
|
||||||
fallback: "Failed to connect to image mounter",
|
plist_free(devices[index])
|
||||||
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 {
|
idevice_data_free(
|
||||||
for index in 0..<deviceCount {
|
UnsafeMutableRawPointer(devices).assumingMemoryBound(to: UInt8.self),
|
||||||
plist_free(devices[index])
|
UInt(deviceCount * MemoryLayout<plist_t?>.stride)
|
||||||
}
|
)
|
||||||
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 {
|
return deviceCount
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -346,7 +307,13 @@ extension JITEnableContext {
|
||||||
if let ffiError = lockdownd_get_value(lockdownClient, "UniqueChipID", nil, &uniqueChipIDPlist) {
|
if let ffiError = lockdownd_get_value(lockdownClient, "UniqueChipID", nil, &uniqueChipIDPlist) {
|
||||||
throw IdeviceBridge.consumeFFIError(ffiError, fallback: "Failed to query UniqueChipID")
|
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")
|
return try IdeviceBridge.uint64Value(from: uniqueChipIDPlist, fieldName: "UniqueChipID")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -356,10 +323,10 @@ extension JITEnableContext {
|
||||||
connect: { image_mounter_connect_rsd(adapter, handshake, $0) },
|
connect: { image_mounter_connect_rsd(adapter, handshake, $0) },
|
||||||
cleanup: { image_mounter_free($0) }
|
cleanup: { image_mounter_free($0) }
|
||||||
) { imageMounterClient in
|
) { imageMounterClient in
|
||||||
let ffiError = imageData.withUnsafeBytes { imageBuffer in
|
let ffiError = imageData.withUnsafeBytes { imageBuffer -> UnsafeMutablePointer<IdeviceFfiError>? in
|
||||||
trustcacheData.withUnsafeBytes { trustcacheBuffer in
|
trustcacheData.withUnsafeBytes { trustcacheBuffer -> UnsafeMutablePointer<IdeviceFfiError>? in
|
||||||
manifestData.withUnsafeBytes { manifestBuffer in
|
manifestData.withUnsafeBytes { manifestBuffer -> UnsafeMutablePointer<IdeviceFfiError>? in
|
||||||
image_mounter_mount_personalized_rsd(
|
image_mounter_mount_personalized_with_callback_rsd(
|
||||||
imageMounterClient,
|
imageMounterClient,
|
||||||
adapter,
|
adapter,
|
||||||
handshake,
|
handshake,
|
||||||
|
|
@ -370,7 +337,9 @@ extension JITEnableContext {
|
||||||
manifestBuffer.bindMemory(to: UInt8.self).baseAddress,
|
manifestBuffer.bindMemory(to: UInt8.self).baseAddress,
|
||||||
manifestData.count,
|
manifestData.count,
|
||||||
nil,
|
nil,
|
||||||
uniqueChipID
|
uniqueChipID,
|
||||||
|
progressCallback,
|
||||||
|
nil
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,37 +17,6 @@ typealias SyslogErrorHandler = (NSError?) -> Void
|
||||||
final class JITEnableContext {
|
final class JITEnableContext {
|
||||||
static let shared = 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 {
|
private struct TunnelHandles {
|
||||||
var adapter: OpaquePointer?
|
var adapter: OpaquePointer?
|
||||||
var handshake: OpaquePointer?
|
var handshake: OpaquePointer?
|
||||||
|
|
@ -82,7 +51,11 @@ final class JITEnableContext {
|
||||||
var handshakeHandle: OpaquePointer? { handshake }
|
var handshakeHandle: OpaquePointer? { handshake }
|
||||||
|
|
||||||
private init() {
|
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
|
path.withUnsafeMutableBufferPointer { buffer in
|
||||||
_ = idevice_init_logger(Info, Debug, buffer.baseAddress)
|
_ = idevice_init_logger(Info, Debug, buffer.baseAddress)
|
||||||
}
|
}
|
||||||
|
|
@ -117,8 +90,7 @@ final class JITEnableContext {
|
||||||
guard let ffiError else {
|
guard let ffiError else {
|
||||||
return makeError(fallback)
|
return makeError(fallback)
|
||||||
}
|
}
|
||||||
let detail = nsString(from: ffiError.pointee.message, fallback: fallback)
|
let message = nsString(from: ffiError.pointee.message, fallback: fallback)
|
||||||
let message = detail == fallback ? fallback : "\(fallback): \(detail)"
|
|
||||||
let error = makeError(message, code: Int(ffiError.pointee.code))
|
let error = makeError(message, code: Int(ffiError.pointee.code))
|
||||||
idevice_error_free(ffiError)
|
idevice_error_free(ffiError)
|
||||||
return error
|
return error
|
||||||
|
|
@ -216,9 +188,7 @@ final class JITEnableContext {
|
||||||
tunnelLock.unlock()
|
tunnelLock.unlock()
|
||||||
|
|
||||||
if let waitSemaphore {
|
if let waitSemaphore {
|
||||||
guard waitSemaphore.wait(timeout: .now() + .seconds(15)) == .success else {
|
waitSemaphore.wait()
|
||||||
throw makeError("Timed out waiting for the tunnel connection", code: -19)
|
|
||||||
}
|
|
||||||
waitSemaphore.signal()
|
waitSemaphore.signal()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -458,19 +428,22 @@ final class JITEnableContext {
|
||||||
private func withConnectedRemoteServer<T>(
|
private func withConnectedRemoteServer<T>(
|
||||||
_ body: (OpaquePointer) throws -> T
|
_ body: (OpaquePointer) throws -> T
|
||||||
) throws -> T {
|
) throws -> T {
|
||||||
try withFreshDebugTunnel(hostname: "StikDebugRemoteServer") { adapter, handshake in
|
try ensureTunnel()
|
||||||
var remoteServer: OpaquePointer?
|
guard let adapter, let handshake else {
|
||||||
if let ffiError = remote_server_connect_rsd(adapter, handshake, &remoteServer) {
|
throw makeError("Tunnel is not connected")
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>(
|
private func withProcessControl<T>(
|
||||||
|
|
@ -628,9 +601,7 @@ final class JITEnableContext {
|
||||||
try withProcessControl(remoteServer: remoteServer) { processControl in
|
try withProcessControl(remoteServer: remoteServer) { processControl in
|
||||||
var pid: UInt64 = 0
|
var pid: UInt64 = 0
|
||||||
let ffiError = bundleID.withCString { bundleID in
|
let ffiError = bundleID.withCString { bundleID in
|
||||||
Self.withCStringArray(Self.mallocDebugEnvVars()) { envPtr, envCount in
|
process_control_launch_app(processControl, bundleID, nil, 0, nil, 0, true, false, &pid)
|
||||||
process_control_launch_app(processControl, bundleID, envPtr, envCount, nil, 0, true, false, &pid)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let ffiError {
|
if let ffiError {
|
||||||
|
|
@ -671,46 +642,6 @@ final class JITEnableContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func relaunchApp(_ bundleID: String, logger: LogFunc? = nil) -> Bool {
|
|
||||||
do {
|
|
||||||
let pid = try withConnectedRemoteServer { remoteServer in
|
|
||||||
try relaunchApp(bundleID, remoteServer: remoteServer)
|
|
||||||
}
|
|
||||||
|
|
||||||
emitLog("Returned to \(bundleID) (PID \(pid))", logger: logger)
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
emitLog("Failed to return to \(bundleID): \(error.localizedDescription)", logger: logger)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func relaunchApp(_ bundleID: String, usingRemoteServer remoteServer: OpaquePointer, logger: LogFunc? = nil) -> Bool {
|
|
||||||
do {
|
|
||||||
let pid = try relaunchApp(bundleID, remoteServer: remoteServer)
|
|
||||||
emitLog("Returned to \(bundleID) (PID \(pid))", logger: logger)
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
emitLog("Failed to return to \(bundleID): \(error.localizedDescription)", logger: logger)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func relaunchApp(_ bundleID: String, remoteServer: OpaquePointer) throws -> UInt64 {
|
|
||||||
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, false, false, &pid)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let ffiError {
|
|
||||||
throw error(from: ffiError, fallback: "Failed to return to app")
|
|
||||||
}
|
|
||||||
|
|
||||||
return pid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func startSyslogRelay(handler: @escaping SyslogLineHandler, onError: @escaping SyslogErrorHandler) {
|
func startSyslogRelay(handler: @escaping SyslogLineHandler, onError: @escaping SyslogErrorHandler) {
|
||||||
do {
|
do {
|
||||||
try ensureTunnel()
|
try ensureTunnel()
|
||||||
|
|
|
||||||
|
|
@ -9,108 +9,74 @@ import idevice
|
||||||
final class MountingProgress: ObservableObject {
|
final class MountingProgress: ObservableObject {
|
||||||
static let shared = MountingProgress()
|
static let shared = MountingProgress()
|
||||||
|
|
||||||
|
@Published private(set) var mountProgress: Double = 0.0
|
||||||
@Published private(set) var mountingThread: Thread?
|
@Published private(set) var mountingThread: Thread?
|
||||||
@Published private(set) var coolisMounted: Bool = false
|
@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() {}
|
private init() {}
|
||||||
|
|
||||||
func checkforMounted() {
|
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 {
|
DispatchQueue.global(qos: .utility).async {
|
||||||
let status = checkMountStatus()
|
let mounted = isMounted()
|
||||||
|
|
||||||
self.mountCheckLock.lock()
|
|
||||||
self.mountCheckInProgress = false
|
|
||||||
self.mountCheckLock.unlock()
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if status != .unreachable {
|
self.coolisMounted = mounted
|
||||||
self.coolisMounted = status == .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() {
|
func pubMount() {
|
||||||
guard TunnelManager.shared.isConnected, DeveloperDiskImageService.filesAreReady else { return }
|
mount()
|
||||||
|
|
||||||
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() {
|
private func mount() {
|
||||||
switch checkMountStatus() {
|
let currentlyMounted = isMounted()
|
||||||
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 = {}) {
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.mountingThread = nil
|
self.coolisMounted = currentlyMounted
|
||||||
self.mountLock.lock()
|
|
||||||
self.mountInProgress = false
|
|
||||||
self.mountLock.unlock()
|
|
||||||
completion()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 ImageMounterHandle = OpaquePointer
|
||||||
typealias LockdowndClientHandle = 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 {
|
enum MountCheckResult {
|
||||||
case mounted
|
case mounted
|
||||||
case notMounted
|
case notMounted
|
||||||
|
|
@ -25,25 +29,18 @@ func isMounted() -> Bool {
|
||||||
|
|
||||||
func checkMountStatus() -> MountCheckResult {
|
func checkMountStatus() -> MountCheckResult {
|
||||||
do {
|
do {
|
||||||
return try JITEnableContext.shared.isDeveloperDiskImageMounted() ? .mounted : .notMounted
|
let result = try JITEnableContext.shared.getMountedDeviceCount()
|
||||||
|
return result > 0 ? .mounted : .notMounted
|
||||||
} catch {
|
} catch {
|
||||||
LogManager.shared.addErrorLog("Failed to check DDI mount status: \(error.localizedDescription)")
|
|
||||||
return .unreachable
|
return .unreachable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mountDeveloperDiskImage(from directoryPath: String) -> String? {
|
func mountPersonalDDI(imagePath: String, trustcachePath: String, manifestPath: String) -> String? {
|
||||||
do {
|
do {
|
||||||
if DeveloperDiskImageService.usesCryptexDDI {
|
try JITEnableContext.shared.mountPersonalDDI(withImagePath: imagePath, trustcachePath: trustcachePath, manifestPath: manifestPath)
|
||||||
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 {
|
} catch {
|
||||||
|
LogManager.shared.addErrorLog("Failed to mount DDI: \(error.localizedDescription)")
|
||||||
return error.localizedDescription
|
return error.localizedDescription
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -20,26 +20,13 @@ final class InstalledAppsViewModel: ObservableObject {
|
||||||
private let cacheKeyDebuggable = "cachedDebuggableApps"
|
private let cacheKeyDebuggable = "cachedDebuggableApps"
|
||||||
private let cacheKeyNonDebuggable = "cachedNonDebuggableApps"
|
private let cacheKeyNonDebuggable = "cachedNonDebuggableApps"
|
||||||
private let cacheKeySystem = "cachedSystemApps"
|
private let cacheKeySystem = "cachedSystemApps"
|
||||||
private var cancellables: Set<AnyCancellable> = []
|
|
||||||
private var refreshInProgress = false
|
|
||||||
private var refreshPending = false
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
loadCachedApps()
|
loadCachedApps()
|
||||||
TunnelManager.shared.$isConnected
|
refreshAppLists()
|
||||||
.removeDuplicates()
|
|
||||||
.filter { $0 }
|
|
||||||
.sink { [weak self] _ in self?.refreshAppLists() }
|
|
||||||
.store(in: &cancellables)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshAppLists() {
|
func refreshAppLists() {
|
||||||
guard !refreshInProgress else {
|
|
||||||
refreshPending = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshInProgress = true
|
|
||||||
isLoading = true
|
isLoading = true
|
||||||
lastError = nil
|
lastError = nil
|
||||||
|
|
||||||
|
|
@ -55,11 +42,6 @@ final class InstalledAppsViewModel: ObservableObject {
|
||||||
debuggable: debuggable,
|
debuggable: debuggable,
|
||||||
hiddenSystem: hiddenSystem
|
hiddenSystem: hiddenSystem
|
||||||
)
|
)
|
||||||
self.cacheApps(
|
|
||||||
debuggable: debuggable,
|
|
||||||
nonDebuggable: classifiedApps.nonDebuggable,
|
|
||||||
system: classifiedApps.system
|
|
||||||
)
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.apply(
|
self.apply(
|
||||||
|
|
@ -67,26 +49,22 @@ final class InstalledAppsViewModel: ObservableObject {
|
||||||
nonDebuggable: classifiedApps.nonDebuggable,
|
nonDebuggable: classifiedApps.nonDebuggable,
|
||||||
system: classifiedApps.system
|
system: classifiedApps.system
|
||||||
)
|
)
|
||||||
self.finishRefresh()
|
self.isLoading = false
|
||||||
|
self.cacheApps(
|
||||||
|
debuggable: debuggable,
|
||||||
|
nonDebuggable: classifiedApps.nonDebuggable,
|
||||||
|
system: classifiedApps.system
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
self.isLoading = false
|
||||||
self.lastError = error.localizedDescription
|
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? {
|
func displayName(for bundleID: String) -> String? {
|
||||||
debuggableApps[bundleID] ?? systemApps[bundleID] ?? nonDebuggableApps[bundleID]
|
debuggableApps[bundleID] ?? systemApps[bundleID] ?? nonDebuggableApps[bundleID]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,17 +22,12 @@ final class RunJSViewModel: ObservableObject, Identifiable, @unchecked Sendable
|
||||||
var debugProxy: OpaquePointer?
|
var debugProxy: OpaquePointer?
|
||||||
var remoteServer: OpaquePointer?
|
var remoteServer: OpaquePointer?
|
||||||
var semaphore: DispatchSemaphore?
|
var semaphore: DispatchSemaphore?
|
||||||
let resumeBundleID: String?
|
|
||||||
|
init(pid: Int, debugProxy: OpaquePointer?, remoteServer: OpaquePointer?, semaphore: DispatchSemaphore?) {
|
||||||
private let resumeLock = NSLock()
|
|
||||||
private var hasResumedApp = false
|
|
||||||
|
|
||||||
init(pid: Int, debugProxy: OpaquePointer?, remoteServer: OpaquePointer?, semaphore: DispatchSemaphore?, resumeBundleID: String? = nil) {
|
|
||||||
self.pid = pid
|
self.pid = pid
|
||||||
self.debugProxy = debugProxy
|
self.debugProxy = debugProxy
|
||||||
self.remoteServer = remoteServer
|
self.remoteServer = remoteServer
|
||||||
self.semaphore = semaphore
|
self.semaphore = semaphore
|
||||||
self.resumeBundleID = resumeBundleID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runScript(path: URL, scriptName: String? = nil) throws {
|
func runScript(path: URL, scriptName: String? = nil) throws {
|
||||||
|
|
@ -45,8 +40,6 @@ final class RunJSViewModel: ObservableObject, Identifiable, @unchecked Sendable
|
||||||
self.scriptName = displayName
|
self.scriptName = displayName
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = resumeTargetApp()
|
|
||||||
|
|
||||||
guard let scriptContent = String(data: data, encoding: .utf8) else {
|
guard let scriptContent = String(data: data, encoding: .utf8) else {
|
||||||
throw CocoaError(.fileReadCorruptFile)
|
throw CocoaError(.fileReadCorruptFile)
|
||||||
}
|
}
|
||||||
|
|
@ -64,11 +57,8 @@ final class RunJSViewModel: ObservableObject, Identifiable, @unchecked Sendable
|
||||||
self.context?.exception = JSValue(object: "Script execution is interrupted by StikDebug.", in: self.context!)
|
self.context?.exception = JSValue(object: "Script execution is interrupted by StikDebug.", in: self.context!)
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = handleJSContextSendDebugCommand(self.context, commandStr, self.debugProxy) ?? ""
|
return handleJSContextSendDebugCommand(self.context, commandStr, self.debugProxy) ?? ""
|
||||||
|
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let logFunction: @convention(block) (String) -> Void = { logStr in
|
let logFunction: @convention(block) (String) -> Void = { logStr in
|
||||||
|
|
@ -88,13 +78,8 @@ final class RunJSViewModel: ObservableObject, Identifiable, @unchecked Sendable
|
||||||
let hasTXMFunction: @convention(block) () -> Bool = {
|
let hasTXMFunction: @convention(block) () -> Bool = {
|
||||||
return ProcessInfo.processInfo.hasTXM
|
return ProcessInfo.processInfo.hasTXM
|
||||||
}
|
}
|
||||||
|
|
||||||
let resumeAppFunction: @convention(block) () -> String = {
|
|
||||||
return self.resumeTargetApp()
|
|
||||||
}
|
|
||||||
|
|
||||||
context = JSContext()
|
context = JSContext()
|
||||||
context?.setObject(resumeAppFunction, forKeyedSubscript: "resume_app" as NSString)
|
|
||||||
context?.setObject(hasTXMFunction, forKeyedSubscript: "hasTXM" as NSString)
|
context?.setObject(hasTXMFunction, forKeyedSubscript: "hasTXM" as NSString)
|
||||||
context?.setObject(getPidFunction, forKeyedSubscript: "get_pid" as NSString)
|
context?.setObject(getPidFunction, forKeyedSubscript: "get_pid" as NSString)
|
||||||
context?.setObject(sendCommandFunction, forKeyedSubscript: "send_command" as NSString)
|
context?.setObject(sendCommandFunction, forKeyedSubscript: "send_command" as NSString)
|
||||||
|
|
@ -116,40 +101,6 @@ final class RunJSViewModel: ObservableObject, Identifiable, @unchecked Sendable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
private func resumeTargetApp() -> String {
|
|
||||||
resumeLock.lock()
|
|
||||||
if hasResumedApp {
|
|
||||||
resumeLock.unlock()
|
|
||||||
return "OK"
|
|
||||||
}
|
|
||||||
guard let resumeBundleID else {
|
|
||||||
resumeLock.unlock()
|
|
||||||
return "no app to return to for this session"
|
|
||||||
}
|
|
||||||
hasResumedApp = true
|
|
||||||
resumeLock.unlock()
|
|
||||||
|
|
||||||
appendLog("Returning to \(resumeBundleID)...")
|
|
||||||
let success: Bool
|
|
||||||
if let remoteServer {
|
|
||||||
success = JITEnableContext.shared.relaunchApp(resumeBundleID, usingRemoteServer: remoteServer)
|
|
||||||
} else {
|
|
||||||
success = JITEnableContext.shared.relaunchApp(resumeBundleID)
|
|
||||||
}
|
|
||||||
guard success else {
|
|
||||||
appendLog("Failed to return to \(resumeBundleID) — continuing in the foreground.")
|
|
||||||
return "failed to return to \(resumeBundleID)"
|
|
||||||
}
|
|
||||||
return "OK"
|
|
||||||
}
|
|
||||||
|
|
||||||
private func appendLog(_ message: String) {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.logs.append(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func captureScreenshot(named preferredName: String?) -> String {
|
private func captureScreenshot(named preferredName: String?) -> String {
|
||||||
if executionInterrupted {
|
if executionInterrupted {
|
||||||
raiseException("Script execution is interrupted by StikDebug.")
|
raiseException("Script execution is interrupted by StikDebug.")
|
||||||
|
|
|
||||||
|
|
@ -201,7 +201,21 @@ struct ScriptListView: View {
|
||||||
// MARK: - File Ops
|
// MARK: - File Ops
|
||||||
|
|
||||||
private func scriptsDirectory() throws -> URL {
|
private func scriptsDirectory() throws -> URL {
|
||||||
try ScriptStore.prepareDirectory()
|
let directory = try ScriptStore.prepareDirectory()
|
||||||
|
try ensureEditorScripts(in: directory)
|
||||||
|
return directory
|
||||||
|
}
|
||||||
|
|
||||||
|
private func ensureEditorScripts(in directory: URL) throws {
|
||||||
|
let fm = FileManager.default
|
||||||
|
let screenshotURL = directory.appendingPathComponent("screenshot-demo.js")
|
||||||
|
if !fm.fileExists(atPath: screenshotURL.path) {
|
||||||
|
try screenshotDemoScript.write(to: screenshotURL, atomically: true, encoding: .utf8)
|
||||||
|
}
|
||||||
|
let standaloneURL = directory.appendingPathComponent("screenshot-capture.js")
|
||||||
|
if !fm.fileExists(atPath: standaloneURL.path) {
|
||||||
|
try screenshotCaptureScript.write(to: standaloneURL, atomically: true, encoding: .utf8)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadScripts() {
|
private func loadScripts() {
|
||||||
|
|
@ -317,3 +331,58 @@ struct ScriptListView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Script content stubs
|
||||||
|
|
||||||
|
private let screenshotDemoScript = """
|
||||||
|
// Screenshot Demo Script
|
||||||
|
// Attaches to the target, captures a PNG screenshot, and detaches.
|
||||||
|
|
||||||
|
function takeScreenshotDemo() {
|
||||||
|
log("[ScreenshotDemo] Starting demo");
|
||||||
|
|
||||||
|
const pid = get_pid();
|
||||||
|
log(`[ScreenshotDemo] Target PID: ${pid}`);
|
||||||
|
|
||||||
|
const attachResponse = send_command(`vAttach;${pid.toString(16)}`);
|
||||||
|
log(`[ScreenshotDemo] attach_response = ${attachResponse}`);
|
||||||
|
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||||
|
const fileName = `screenshot-${timestamp}.png`;
|
||||||
|
const savedPath = take_screenshot(fileName);
|
||||||
|
|
||||||
|
if (savedPath && savedPath.length > 0) {
|
||||||
|
log(`[ScreenshotDemo] Screenshot saved to ${savedPath}`);
|
||||||
|
} else {
|
||||||
|
log("[ScreenshotDemo] Device did not report a saved path.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const detachResponse = send_command("D");
|
||||||
|
log(`[ScreenshotDemo] detach_response = ${detachResponse}`);
|
||||||
|
log("[ScreenshotDemo] Demo complete.");
|
||||||
|
}
|
||||||
|
|
||||||
|
takeScreenshotDemo();
|
||||||
|
"""
|
||||||
|
|
||||||
|
private let screenshotCaptureScript = """
|
||||||
|
// Screenshot Capture Script
|
||||||
|
// Takes a screenshot without sending any debugserver commands.
|
||||||
|
|
||||||
|
function captureScreenshot() {
|
||||||
|
log("[ScreenshotCapture] Requesting screenshot without attaching…");
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||||
|
const fileName = `standalone-${timestamp}.png`;
|
||||||
|
const savedPath = take_screenshot(fileName);
|
||||||
|
|
||||||
|
if (savedPath && savedPath.length > 0) {
|
||||||
|
log(`[ScreenshotCapture] Screenshot saved to ${savedPath}`);
|
||||||
|
} else {
|
||||||
|
log("[ScreenshotCapture] Device did not report a saved path.");
|
||||||
|
}
|
||||||
|
|
||||||
|
log("[ScreenshotCapture] Done.");
|
||||||
|
}
|
||||||
|
|
||||||
|
captureScreenshot();
|
||||||
|
"""
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ final class BackgroundAudioManager {
|
||||||
private var engine = AVAudioEngine()
|
private var engine = AVAudioEngine()
|
||||||
private var player = AVAudioPlayerNode()
|
private var player = AVAudioPlayerNode()
|
||||||
private var isRunning = false
|
private var isRunning = false
|
||||||
|
private var persistentEnabled = false
|
||||||
private var activityCount = 0
|
private var activityCount = 0
|
||||||
private var healthCheckTimer: Timer?
|
private var healthCheckTimer: Timer?
|
||||||
|
|
||||||
|
|
@ -29,7 +30,13 @@ final class BackgroundAudioManager {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func configurationDidChange() {
|
func start() {
|
||||||
|
persistentEnabled = true
|
||||||
|
refreshRunningState()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stop() {
|
||||||
|
persistentEnabled = false
|
||||||
refreshRunningState()
|
refreshRunningState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,7 +51,7 @@ final class BackgroundAudioManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func refreshRunningState() {
|
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 {
|
guard shouldRun != isRunning else {
|
||||||
if shouldRun {
|
if shouldRun {
|
||||||
recoverIfNeeded()
|
recoverIfNeeded()
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import CoreLocation
|
import CoreLocation
|
||||||
import UIKit
|
|
||||||
|
|
||||||
final class BackgroundLocationManager: NSObject, CLLocationManagerDelegate {
|
final class BackgroundLocationManager: NSObject, CLLocationManagerDelegate {
|
||||||
static let shared = BackgroundLocationManager()
|
static let shared = BackgroundLocationManager()
|
||||||
|
|
@ -12,7 +11,6 @@ final class BackgroundLocationManager: NSObject, CLLocationManagerDelegate {
|
||||||
private let locationManager = CLLocationManager()
|
private let locationManager = CLLocationManager()
|
||||||
private var isRunning = false
|
private var isRunning = false
|
||||||
private var activityCount = 0
|
private var activityCount = 0
|
||||||
private var isRequestingAuthorization = false
|
|
||||||
|
|
||||||
private override init() {
|
private override init() {
|
||||||
super.init()
|
super.init()
|
||||||
|
|
@ -23,44 +21,8 @@ final class BackgroundLocationManager: NSObject, CLLocationManagerDelegate {
|
||||||
locationManager.pausesLocationUpdatesAutomatically = false
|
locationManager.pausesLocationUpdatesAutomatically = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func configurationDidChange() {
|
func start() {
|
||||||
refreshRunningState()
|
isRunning = true
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
switch locationManager.authorizationStatus {
|
||||||
case .authorizedAlways:
|
case .authorizedAlways:
|
||||||
locationManager.startUpdatingLocation()
|
locationManager.startUpdatingLocation()
|
||||||
|
|
@ -68,54 +30,37 @@ final class BackgroundLocationManager: NSObject, CLLocationManagerDelegate {
|
||||||
locationManager.requestAlwaysAuthorization()
|
locationManager.requestAlwaysAuthorization()
|
||||||
case .notDetermined:
|
case .notDetermined:
|
||||||
locationManager.requestAlwaysAuthorization()
|
locationManager.requestAlwaysAuthorization()
|
||||||
case .denied, .restricted:
|
default:
|
||||||
restoreAudioFallback()
|
break
|
||||||
@unknown default:
|
}
|
||||||
restoreAudioFallback()
|
}
|
||||||
|
|
||||||
|
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) {
|
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||||
|
guard isRunning else { return }
|
||||||
switch manager.authorizationStatus {
|
switch manager.authorizationStatus {
|
||||||
case .authorizedAlways, .authorizedWhenInUse:
|
case .authorizedAlways, .authorizedWhenInUse:
|
||||||
isRequestingAuthorization = false
|
manager.startUpdatingLocation()
|
||||||
if isRunning {
|
default:
|
||||||
manager.startUpdatingLocation()
|
break
|
||||||
}
|
|
||||||
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,17 +7,6 @@ import Foundation
|
||||||
|
|
||||||
final class DeveloperDiskImageService {
|
final class DeveloperDiskImageService {
|
||||||
static let shared = 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 fileManager: FileManager
|
||||||
private let session: URLSession
|
private let session: URLSession
|
||||||
|
|
@ -28,21 +17,13 @@ final class DeveloperDiskImageService {
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadMissingFiles() async throws {
|
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 {
|
for item in Self.downloadItems {
|
||||||
let destinationURL = Self.directoryURL.appendingPathComponent(item.fileName)
|
let destinationURL = URL.documentsDirectory.appendingPathComponent(item.relativePath)
|
||||||
guard replaceFiles || !fileManager.fileExists(atPath: destinationURL.path) else {
|
guard !fileManager.fileExists(atPath: destinationURL.path) else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
try await downloadFile(from: item.urlString, to: destinationURL)
|
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 {
|
func downloadFile(from urlString: String, to destinationURL: URL) async throws {
|
||||||
|
|
@ -75,18 +56,8 @@ final class DeveloperDiskImageService {
|
||||||
var completedStages = 0.0
|
var completedStages = 0.0
|
||||||
|
|
||||||
progressHandler?(0.0, "Removing existing DDI files...")
|
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 {
|
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) {
|
if fileManager.fileExists(atPath: fileURL.path) {
|
||||||
try fileManager.removeItem(at: fileURL)
|
try fileManager.removeItem(at: fileURL)
|
||||||
}
|
}
|
||||||
|
|
@ -97,100 +68,37 @@ final class DeveloperDiskImageService {
|
||||||
|
|
||||||
for item in Self.downloadItems {
|
for item in Self.downloadItems {
|
||||||
progressHandler?(completedStages / totalStages, "Downloading \(item.name)...")
|
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)
|
try await downloadFile(from: item.urlString, to: destinationURL)
|
||||||
completedStages += 1.0
|
completedStages += 1.0
|
||||||
progressHandler?(completedStages / totalStages, "\(item.name) ready")
|
progressHandler?(completedStages / totalStages, "\(item.name) ready")
|
||||||
}
|
}
|
||||||
|
|
||||||
try Self.mountMethod.rawValue.write(to: Self.mountMethodURL, atomically: true, encoding: .utf8)
|
|
||||||
|
|
||||||
progressHandler?(1.0, "DDI download complete.")
|
progressHandler?(1.0, "DDI download complete.")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func removeCryptexOnlyFiles() throws {
|
private static let downloadItems: [DDIDownloadItem] = [
|
||||||
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(
|
.init(
|
||||||
name: "Build Manifest",
|
name: "Build Manifest",
|
||||||
fileName: "BuildManifest.plist",
|
relativePath: "DDI/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"
|
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/BuildManifest.plist"
|
||||||
),
|
),
|
||||||
.init(
|
.init(
|
||||||
name: "Image",
|
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"
|
urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg"
|
||||||
),
|
),
|
||||||
.init(
|
.init(
|
||||||
name: "TrustCache",
|
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"
|
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 {
|
private struct DDIDownloadItem {
|
||||||
let name: String
|
let name: String
|
||||||
let fileName: String
|
let relativePath: String
|
||||||
let urlString: 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() {
|
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()
|
MountingProgress.shared.pubMount()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,11 +103,16 @@ final class TunnelManager: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleInvalidPairingFile() {
|
private func handleInvalidPairingFile() {
|
||||||
LogManager.shared.addInfoLog("Pairing file reported invalid; keeping existing file")
|
do {
|
||||||
|
try PairingFileStore.remove()
|
||||||
|
LogManager.shared.addInfoLog("Removed invalid pairing file")
|
||||||
|
} catch {
|
||||||
|
LogManager.shared.addErrorLog("Failed to remove invalid pairing file: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
|
||||||
showAlert(
|
showAlert(
|
||||||
title: "Invalid Pairing File",
|
title: "Invalid Pairing File",
|
||||||
message: "The pairing file may be invalid or expired. You can import a new pairing file to replace it.",
|
message: "The pairing file is invalid or expired. Please select a new pairing file.",
|
||||||
showOk: true,
|
showOk: true,
|
||||||
showTryAgain: false,
|
showTryAgain: false,
|
||||||
primaryButtonText: "Select New File"
|
primaryButtonText: "Select New File"
|
||||||
|
|
|
||||||
|
|
@ -14,17 +14,12 @@ extension ScriptStore {
|
||||||
appNames: [
|
appNames: [
|
||||||
"Amethyst",
|
"Amethyst",
|
||||||
"MeloNX",
|
"MeloNX",
|
||||||
|
"Melo",
|
||||||
"XeniOS",
|
"XeniOS",
|
||||||
"MeloCafé",
|
"MeloCafe",
|
||||||
"Manic EMU",
|
"Manic EMU",
|
||||||
|
"Manic",
|
||||||
"DukeX",
|
"DukeX",
|
||||||
"TachyonU",
|
|
||||||
"touchHLE",
|
|
||||||
"HyperHLE",
|
|
||||||
"Applesauce",
|
|
||||||
"RPCS3",
|
|
||||||
"AetherPS4",
|
|
||||||
"Husk"
|
|
||||||
],
|
],
|
||||||
resource: ScriptResource(resourceName: "universal", fileName: "universal.js")
|
resource: ScriptResource(resourceName: "universal", fileName: "universal.js")
|
||||||
),
|
),
|
||||||
|
|
@ -41,7 +36,7 @@ extension ScriptStore {
|
||||||
"Flycast",
|
"Flycast",
|
||||||
"ARMSX2 iOS"
|
"ARMSX2 iOS"
|
||||||
],
|
],
|
||||||
resource: ScriptResource(resourceName: "legacy", fileName: "legacy.js")
|
resource: ScriptResource(resourceName: "UTM-Dolphin", fileName: "UTM-Dolphin.js")
|
||||||
),
|
),
|
||||||
AutoScriptAssignment(
|
AutoScriptAssignment(
|
||||||
appNames: [
|
appNames: [
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,14 @@ import Foundation
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
enum PairingFileStore {
|
enum PairingFileStore {
|
||||||
static let fileName = "pairingFile.plist"
|
static let fileName = "rp_pairing_file.plist"
|
||||||
static let supportedContentTypes: [UTType] = [
|
static let supportedContentTypes: [UTType] = [
|
||||||
UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!,
|
UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!,
|
||||||
UTType(filenameExtension: "mobiledevicepair", conformingTo: .data)!,
|
UTType(filenameExtension: "mobiledevicepair", conformingTo: .data)!,
|
||||||
.propertyList
|
.propertyList
|
||||||
]
|
]
|
||||||
|
|
||||||
private static let legacyFileName = "rp_pairing_file.plist"
|
private static let legacyFileName = "pairingFile.plist"
|
||||||
|
|
||||||
static var url: URL {
|
static var url: URL {
|
||||||
directoryURL.appendingPathComponent(fileName)
|
directoryURL.appendingPathComponent(fileName)
|
||||||
|
|
@ -25,31 +25,24 @@ enum PairingFileStore {
|
||||||
let destination = url
|
let destination = url
|
||||||
try? fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
|
try? fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
|
||||||
|
|
||||||
if let documentURL = documentSourceURL(fileManager: fileManager) {
|
guard !fileManager.fileExists(atPath: destination.path) else {
|
||||||
if !fileManager.fileExists(atPath: destination.path) ||
|
removeLegacyCopies(fileManager: fileManager)
|
||||||
!fileManager.contentsEqual(atPath: documentURL.path, andPath: destination.path) {
|
|
||||||
try? replaceItem(at: destination, with: documentURL, fileManager: fileManager)
|
|
||||||
protectPairingFile(at: destination, fileManager: fileManager)
|
|
||||||
}
|
|
||||||
return destination
|
return destination
|
||||||
}
|
}
|
||||||
|
|
||||||
if !fileManager.fileExists(atPath: destination.path) {
|
migrateLegacyCopy(to: destination, fileManager: fileManager)
|
||||||
migrateLegacyCopy(to: destination, fileManager: fileManager)
|
|
||||||
}
|
|
||||||
return destination
|
return destination
|
||||||
}
|
}
|
||||||
|
|
||||||
static func replace(with sourceURL: URL, fileManager: FileManager = .default) throws {
|
static func replace(with sourceURL: URL, fileManager: FileManager = .default) throws {
|
||||||
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
|
let destination = prepareURL(fileManager: fileManager)
|
||||||
|
if fileManager.fileExists(atPath: destination.path) {
|
||||||
let documentsDestination = documentsURL
|
try fileManager.removeItem(at: destination)
|
||||||
if sourceURL.standardizedFileURL != documentsDestination.standardizedFileURL {
|
|
||||||
try replaceItem(at: documentsDestination, with: sourceURL, fileManager: fileManager)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try replaceItem(at: url, with: documentsDestination, fileManager: fileManager)
|
removeLegacyCopies(fileManager: fileManager)
|
||||||
protectPairingFile(at: url, fileManager: fileManager)
|
try fileManager.copyItem(at: sourceURL, to: destination)
|
||||||
|
protectPairingFile(at: destination, fileManager: fileManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func importFromPicker(_ sourceURL: URL, fileManager: FileManager = .default) throws {
|
static func importFromPicker(_ sourceURL: URL, fileManager: FileManager = .default) throws {
|
||||||
|
|
@ -83,33 +76,19 @@ enum PairingFileStore {
|
||||||
|
|
||||||
private static var legacyURLs: [URL] {
|
private static var legacyURLs: [URL] {
|
||||||
[
|
[
|
||||||
directoryURL.appendingPathComponent(legacyFileName),
|
URL.documentsDirectory.appendingPathComponent(fileName),
|
||||||
documentsURL,
|
|
||||||
URL.documentsDirectory.appendingPathComponent(legacyFileName)
|
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) {
|
private static func migrateLegacyCopy(to destination: URL, fileManager: FileManager) {
|
||||||
for legacyURL in legacyURLs where fileManager.fileExists(atPath: legacyURL.path) {
|
for legacyURL in legacyURLs where fileManager.fileExists(atPath: legacyURL.path) {
|
||||||
do {
|
do {
|
||||||
try replaceItem(at: destination, with: legacyURL, fileManager: fileManager)
|
try fileManager.moveItem(at: legacyURL, to: destination)
|
||||||
} catch {
|
} catch {
|
||||||
if let data = try? Data(contentsOf: legacyURL) {
|
if let data = try? Data(contentsOf: legacyURL) {
|
||||||
try? data.write(to: destination, options: .atomic)
|
try? data.write(to: destination, options: .atomic)
|
||||||
|
try? fileManager.removeItem(at: legacyURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,21 +97,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) {
|
private static func removeLegacyCopies(fileManager: FileManager) {
|
||||||
for legacyURL in legacyURLs where fileManager.fileExists(atPath: legacyURL.path) {
|
for legacyURL in legacyURLs where fileManager.fileExists(atPath: legacyURL.path) {
|
||||||
try? fileManager.removeItem(at: legacyURL)
|
try? fileManager.removeItem(at: legacyURL)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ enum ScriptStore {
|
||||||
ScriptResource(resourceName: "maciOS", fileName: "maciOS.js"),
|
ScriptResource(resourceName: "maciOS", fileName: "maciOS.js"),
|
||||||
ScriptResource(resourceName: "universal", fileName: "universal.js"),
|
ScriptResource(resourceName: "universal", fileName: "universal.js"),
|
||||||
ScriptResource(resourceName: "Geode", fileName: "Geode.js"),
|
ScriptResource(resourceName: "Geode", fileName: "Geode.js"),
|
||||||
ScriptResource(resourceName: "legacy", fileName: "legacy.js")
|
ScriptResource(resourceName: "UTM-Dolphin", fileName: "UTM-Dolphin.js")
|
||||||
]
|
]
|
||||||
|
|
||||||
static var directoryURL: URL {
|
static var directoryURL: URL {
|
||||||
|
|
@ -54,16 +54,6 @@ enum ScriptStore {
|
||||||
return directory.appendingPathComponent(scriptName)
|
return directory.appendingPathComponent(scriptName)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func script(named scriptName: String, fileManager: FileManager = .default) -> (data: Data, name: String)? {
|
|
||||||
guard let normalizedName = normalizedScriptFileName(scriptName),
|
|
||||||
let scriptURL = try? scriptURL(named: normalizedName, fileManager: fileManager),
|
|
||||||
let data = try? Data(contentsOf: scriptURL) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return (data, normalizedName)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func assignedScriptName(for bundleID: String, defaults: UserDefaults = .standard) -> String? {
|
static func assignedScriptName(for bundleID: String, defaults: UserDefaults = .standard) -> String? {
|
||||||
assignedScriptMap(defaults: defaults)[bundleID]
|
assignedScriptMap(defaults: defaults)[bundleID]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,5 @@ extension UserDefaults {
|
||||||
static let bundleScriptMap = "BundleScriptMap"
|
static let bundleScriptMap = "BundleScriptMap"
|
||||||
static let defaultScriptName = "DefaultScriptName"
|
static let defaultScriptName = "DefaultScriptName"
|
||||||
static let defaultScriptNameValue = ""
|
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
|
@ViewBuilder
|
||||||
private var exportMenuOption: some View {
|
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) {
|
if FileManager.default.fileExists(atPath: logURL.path) {
|
||||||
ShareLink(
|
ShareLink(
|
||||||
item: logURL,
|
item: logURL,
|
||||||
|
|
@ -309,7 +309,7 @@ struct ConsoleLogsView: View {
|
||||||
guard !isLoadingLogs else { return }
|
guard !isLoadingLogs else { return }
|
||||||
isLoadingLogs = true
|
isLoadingLogs = true
|
||||||
|
|
||||||
let logPath = IdeviceLogFileManager.logURL.path
|
let logPath = URL.documentsDirectory.appendingPathComponent("idevice_log.txt").path
|
||||||
|
|
||||||
guard FileManager.default.fileExists(atPath: logPath) else {
|
guard FileManager.default.fileExists(atPath: logPath) else {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
|
|
@ -368,7 +368,7 @@ struct ConsoleLogsView: View {
|
||||||
guard !isLoadingLogs else { return }
|
guard !isLoadingLogs else { return }
|
||||||
isLoadingLogs = true
|
isLoadingLogs = true
|
||||||
|
|
||||||
let logPath = IdeviceLogFileManager.logURL.path
|
let logPath = URL.documentsDirectory.appendingPathComponent("idevice_log.txt").path
|
||||||
let previousCount = lastProcessedLineCount
|
let previousCount = lastProcessedLineCount
|
||||||
|
|
||||||
guard FileManager.default.fileExists(atPath: logPath) else {
|
guard FileManager.default.fileExists(atPath: logPath) else {
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,17 @@ struct HomeView: View {
|
||||||
@AppStorage("bundleID") private var bundleID: String = ""
|
@AppStorage("bundleID") private var bundleID: String = ""
|
||||||
@AppStorage(UserDefaults.Keys.confirmExternalJITRequests) private var confirmExternalJITRequests = true
|
@AppStorage(UserDefaults.Keys.confirmExternalJITRequests) private var confirmExternalJITRequests = true
|
||||||
|
|
||||||
|
@ObservedObject private var mounting = MountingProgress.shared
|
||||||
|
|
||||||
@State private var hasAppeared = false
|
@State private var hasAppeared = false
|
||||||
@State private var pendingJITEnableConfiguration: JITEnableConfiguration?
|
@State private var pendingJITEnableConfiguration: JITEnableConfiguration?
|
||||||
@State private var isShowingPairingFilePicker = false
|
@State private var isShowingPairingFilePicker = false
|
||||||
@State private var debugFeedback: DebugFeedback?
|
@State private var debugFeedback: DebugFeedback?
|
||||||
@State private var pendingExternalURLAction: HomeExternalAction?
|
@State private var pendingExternalURLAction: HomeExternalAction?
|
||||||
@State private var pendingLiveContainerApp: LiveContainerApp?
|
|
||||||
@State private var scriptRunModel: RunJSViewModel?
|
@State private var scriptRunModel: RunJSViewModel?
|
||||||
|
|
||||||
|
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||||
|
|
||||||
private struct DebugFeedback: Identifiable {
|
private struct DebugFeedback: Identifiable {
|
||||||
let id = UUID()
|
let id = UUID()
|
||||||
let message: String
|
let message: String
|
||||||
|
|
@ -27,18 +30,11 @@ struct HomeView: View {
|
||||||
let isWorking: Bool
|
let isWorking: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct LiveContainerApp {
|
|
||||||
let bundleID: String
|
|
||||||
let name: String
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
InstalledAppsListView(onSelectApp: { selectedBundle, selectedName in
|
InstalledAppsListView(onSelectApp: { selectedBundle, selectedName in
|
||||||
if selectedName.localizedCaseInsensitiveContains("LiveContainer") {
|
bundleID = selectedBundle
|
||||||
pendingLiveContainerApp = LiveContainerApp(bundleID: selectedBundle, name: selectedName)
|
Haptics.medium()
|
||||||
} else {
|
startJITInBackground(bundleID: selectedBundle, displayName: selectedName)
|
||||||
enableJITForSelectedApp(bundleID: selectedBundle, name: selectedName)
|
|
||||||
}
|
|
||||||
}, showDoneButton: false, onImportPairingFile: { isShowingPairingFilePicker = true })
|
}, showDoneButton: false, onImportPairingFile: { isShowingPairingFilePicker = true })
|
||||||
.overlay(alignment: .bottom) {
|
.overlay(alignment: .bottom) {
|
||||||
if let debugFeedback {
|
if let debugFeedback {
|
||||||
|
|
@ -49,6 +45,9 @@ struct HomeView: View {
|
||||||
}
|
}
|
||||||
.onAppear(perform: handleAppear)
|
.onAppear(perform: handleAppear)
|
||||||
.onReceive(NotificationCenter.default.publisher(for: .intentJSScriptReady), perform: handleScriptReadyNotification)
|
.onReceive(NotificationCenter.default.publisher(for: .intentJSScriptReady), perform: handleScriptReadyNotification)
|
||||||
|
.onReceive(timer) { _ in
|
||||||
|
refreshMountStatusIfNeeded()
|
||||||
|
}
|
||||||
.onOpenURL { url in
|
.onOpenURL { url in
|
||||||
handleExternalURL(url)
|
handleExternalURL(url)
|
||||||
}
|
}
|
||||||
|
|
@ -75,28 +74,6 @@ struct HomeView: View {
|
||||||
} message: { action in
|
} message: { action in
|
||||||
Text(action.message)
|
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(
|
.fileImporter(
|
||||||
isPresented: $isShowingPairingFilePicker,
|
isPresented: $isShowingPairingFilePicker,
|
||||||
allowedContentTypes: PairingFileStore.supportedContentTypes,
|
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() {
|
private func handleAppear() {
|
||||||
startTunnelInBackground()
|
startTunnelInBackground()
|
||||||
|
MountingProgress.shared.checkforMounted()
|
||||||
hasAppeared = true
|
hasAppeared = true
|
||||||
|
|
||||||
if let config = pendingJITEnableConfiguration {
|
if let config = pendingJITEnableConfiguration {
|
||||||
|
|
@ -145,6 +117,13 @@ struct HomeView: View {
|
||||||
scriptRunModel = model
|
scriptRunModel = model
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func refreshMountStatusIfNeeded() {
|
||||||
|
guard mounting.mountingThread == nil, !mounting.coolisMounted else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
MountingProgress.shared.checkforMounted()
|
||||||
|
}
|
||||||
|
|
||||||
private func importPairingFile(_ result: Result<URL, Error>) {
|
private func importPairingFile(_ result: Result<URL, Error>) {
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let url):
|
case .success(let url):
|
||||||
|
|
@ -196,14 +175,6 @@ struct HomeView: View {
|
||||||
}
|
}
|
||||||
if let scriptName = queryValue(["script-name", "scriptName", "script_name"], in: components) {
|
if let scriptName = queryValue(["script-name", "scriptName", "script_name"], in: components) {
|
||||||
config.scriptName = scriptName
|
config.scriptName = scriptName
|
||||||
if config.scriptData == nil {
|
|
||||||
if let namedScript = ScriptStore.script(named: scriptName) {
|
|
||||||
config.scriptData = namedScript.data
|
|
||||||
config.scriptName = namedScript.name
|
|
||||||
} else {
|
|
||||||
LogManager.shared.addWarningLog("Script \(scriptName) was not found in the scripts folder")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if config.scriptData == nil, let bundleID = config.bundleID,
|
if config.scriptData == nil, let bundleID = config.bundleID,
|
||||||
let scriptInfo = ScriptStore.preferredScript(for: bundleID) {
|
let scriptInfo = ScriptStore.preferredScript(for: bundleID) {
|
||||||
|
|
@ -288,13 +259,12 @@ struct HomeView: View {
|
||||||
.accessibilityLabel(feedback.message)
|
.accessibilityLabel(feedback.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getJsCallback(_ script: Data, name: String? = nil, resumeBundleID: String? = nil) -> DebugAppCallback {
|
private func getJsCallback(_ script: Data, name: String? = nil) -> DebugAppCallback {
|
||||||
return { pid, debugProxyHandle, remoteServerHandle, semaphore in
|
return { pid, debugProxyHandle, remoteServerHandle, semaphore in
|
||||||
let model = RunJSViewModel(pid: Int(pid),
|
let model = RunJSViewModel(pid: Int(pid),
|
||||||
debugProxy: debugProxyHandle,
|
debugProxy: debugProxyHandle,
|
||||||
remoteServer: remoteServerHandle,
|
remoteServer: remoteServerHandle,
|
||||||
semaphore: semaphore,
|
semaphore: semaphore)
|
||||||
resumeBundleID: resumeBundleID)
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
scriptRunModel = model
|
scriptRunModel = model
|
||||||
|
|
@ -329,18 +299,8 @@ struct HomeView: View {
|
||||||
let keepAliveLease = DebugKeepAliveLease()
|
let keepAliveLease = DebugKeepAliveLease()
|
||||||
defer { keepAliveLease.invalidate() }
|
defer { keepAliveLease.invalidate() }
|
||||||
|
|
||||||
if triggeredByURLScheme, !waitForJITPrerequisites() {
|
if triggeredByURLScheme {
|
||||||
DispatchQueue.main.async {
|
sleep(1)
|
||||||
withAnimation {
|
|
||||||
debugFeedback = nil
|
|
||||||
}
|
|
||||||
showAlert(
|
|
||||||
title: "Failed to Enable JIT".localized,
|
|
||||||
message: "The device connection or Developer Disk Image wasn't ready in time. Open StikDebug directly, wait for it to finish connecting, then try again.".localized,
|
|
||||||
showOk: true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let finishProcessing: (Bool, String?) -> Void = { success, detail in
|
let finishProcessing: (Bool, String?) -> Void = { success, detail in
|
||||||
|
|
@ -378,11 +338,9 @@ struct HomeView: View {
|
||||||
scriptData = preferred.data
|
scriptData = preferred.data
|
||||||
}
|
}
|
||||||
|
|
||||||
let resumeBundleID = pid == nil ? nil : bundleID
|
|
||||||
|
|
||||||
var callback: DebugAppCallback? = nil
|
var callback: DebugAppCallback? = nil
|
||||||
if ProcessInfo.processInfo.hasTXM, let sd = scriptData {
|
if ProcessInfo.processInfo.hasTXM, let sd = scriptData {
|
||||||
callback = getJsCallback(sd, name: scriptName ?? bundleID ?? "Script", resumeBundleID: resumeBundleID)
|
callback = getJsCallback(sd, name: scriptName ?? bundleID ?? "Script")
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastDebugMessage: String?
|
var lastDebugMessage: String?
|
||||||
|
|
@ -415,17 +373,6 @@ struct HomeView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func waitForJITPrerequisites(timeout: TimeInterval = 20) -> Bool {
|
|
||||||
let deadline = Date().addingTimeInterval(timeout)
|
|
||||||
while Date() < deadline {
|
|
||||||
if TunnelManager.shared.isConnected && MountingProgress.shared.coolisMounted {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
usleep(250_000)
|
|
||||||
}
|
|
||||||
return TunnelManager.shared.isConnected && MountingProgress.shared.coolisMounted
|
|
||||||
}
|
|
||||||
|
|
||||||
private func base64URLToBase64(_ base64url: String) -> String {
|
private func base64URLToBase64(_ base64url: String) -> String {
|
||||||
var base64 = base64url.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
|
var base64 = base64url.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
|
||||||
let pad = 4 - (base64.count % 4)
|
let pad = 4 - (base64.count % 4)
|
||||||
|
|
|
||||||
|
|
@ -83,9 +83,7 @@ struct InstalledAppsListView: View {
|
||||||
handleLoadingChange(isLoading)
|
handleLoadingChange(isLoading)
|
||||||
}
|
}
|
||||||
.onReceive(NotificationCenter.default.publisher(for: .pairingFileImported)) { _ in
|
.onReceive(NotificationCenter.default.publisher(for: .pairingFileImported)) { _ in
|
||||||
if TunnelManager.shared.isConnected {
|
viewModel.refreshAppLists()
|
||||||
viewModel.refreshAppLists()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Foundation
|
import Foundation
|
||||||
import CoreLocation
|
|
||||||
|
|
||||||
private enum ExternalLocationAction: Identifiable {
|
private enum ExternalLocationAction: Identifiable {
|
||||||
case simulate(URL, Double, Double)
|
case simulate(URL, Double, Double)
|
||||||
|
|
@ -224,22 +223,7 @@ struct MainTabView: View {
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if code == 0 {
|
if code == 0 {
|
||||||
LocationSimulationSession.shared.clearRoute()
|
BackgroundLocationManager.shared.requestStart()
|
||||||
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(
|
LogManager.shared.addInfoLog(
|
||||||
String(format: "Simulated location from URL: %.6f, %.6f", coordinate.latitude, coordinate.longitude)
|
String(format: "Simulated location from URL: %.6f, %.6f", coordinate.latitude, coordinate.longitude)
|
||||||
)
|
)
|
||||||
|
|
@ -259,7 +243,7 @@ struct MainTabView: View {
|
||||||
let code = clear_simulated_location()
|
let code = clear_simulated_location()
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if code == 0 {
|
if code == 0 {
|
||||||
LocationSimulationSession.shared.stop()
|
BackgroundLocationManager.shared.requestStop()
|
||||||
LogManager.shared.addInfoLog("Cleared simulated location from URL")
|
LogManager.shared.addInfoLog("Cleared simulated location from URL")
|
||||||
} else {
|
} else {
|
||||||
showAlert(
|
showAlert(
|
||||||
|
|
|
||||||
|
|
@ -724,11 +724,11 @@ final class LocationSearchCompleter: NSObject, ObservableObject, MKLocalSearchCo
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LocationSimulationView: View {
|
struct LocationSimulationView: View {
|
||||||
@ObservedObject private var simulationSession = LocationSimulationSession.shared
|
|
||||||
@State private var coordinate: CLLocationCoordinate2D?
|
@State private var coordinate: CLLocationCoordinate2D?
|
||||||
@State private var position: MapCameraPosition = .userLocation(fallback: .automatic)
|
@State private var position: MapCameraPosition = .userLocation(fallback: .automatic)
|
||||||
|
|
||||||
@State private var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
|
@State private var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
|
||||||
|
@State private var resendTimer: Timer?
|
||||||
@State private var routeLoadTask: Task<Void, Never>?
|
@State private var routeLoadTask: Task<Void, Never>?
|
||||||
@State private var routeSpeedPrefetchTask: Task<Void, Never>?
|
@State private var routeSpeedPrefetchTask: Task<Void, Never>?
|
||||||
@State private var routePlaybackTask: Task<Void, Never>?
|
@State private var routePlaybackTask: Task<Void, Never>?
|
||||||
|
|
@ -788,11 +788,11 @@ struct LocationSimulationView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var hasActiveSimulation: Bool {
|
private var hasActiveSimulation: Bool {
|
||||||
simulationSession.isActive || simulatedCoordinate != nil || routePlaybackTask != nil
|
simulatedCoordinate != nil || routePlaybackTask != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isRouteRunning: Bool {
|
private var isRouteRunning: Bool {
|
||||||
routePlaybackTask != nil || simulationSession.routeCoordinates != nil
|
routePlaybackTask != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private var hasRouteContext: Bool {
|
private var hasRouteContext: Bool {
|
||||||
|
|
@ -801,12 +801,7 @@ struct LocationSimulationView: View {
|
||||||
routePlan != nil ||
|
routePlan != nil ||
|
||||||
isLoadingRoute ||
|
isLoadingRoute ||
|
||||||
isPrefetchingRouteSpeeds ||
|
isPrefetchingRouteSpeeds ||
|
||||||
routePlaybackCoordinate != nil ||
|
routePlaybackCoordinate != nil
|
||||||
simulationSession.routeCoordinates != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private var displayedRouteCoordinate: CLLocationCoordinate2D? {
|
|
||||||
simulationSession.routeCoordinates == nil ? routePlaybackCoordinate : simulationSession.coordinate
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var routeSummaryText: String? {
|
private var routeSummaryText: String? {
|
||||||
|
|
@ -823,9 +818,6 @@ struct LocationSimulationView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var routeStatusText: String {
|
private var routeStatusText: String {
|
||||||
if simulationSession.routeCoordinates != nil {
|
|
||||||
return "Route simulation active."
|
|
||||||
}
|
|
||||||
if isLoadingRoute {
|
if isLoadingRoute {
|
||||||
return "Calculating route…"
|
return "Calculating route…"
|
||||||
}
|
}
|
||||||
|
|
@ -900,8 +892,8 @@ struct LocationSimulationView: View {
|
||||||
Marker("End", coordinate: routeEndCoordinate)
|
Marker("End", coordinate: routeEndCoordinate)
|
||||||
.tint(.red)
|
.tint(.red)
|
||||||
}
|
}
|
||||||
if let displayedRouteCoordinate {
|
if let routePlaybackCoordinate {
|
||||||
Marker("Current", coordinate: displayedRouteCoordinate)
|
Marker("Current", coordinate: routePlaybackCoordinate)
|
||||||
.tint(.blue)
|
.tint(.blue)
|
||||||
}
|
}
|
||||||
} else if let coordinate {
|
} else if let coordinate {
|
||||||
|
|
@ -1033,28 +1025,17 @@ struct LocationSimulationView: View {
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
loadBookmarks()
|
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 {
|
.onDisappear {
|
||||||
routeLoadTask?.cancel()
|
routeLoadTask?.cancel()
|
||||||
routeLoadTask = nil
|
routeLoadTask = nil
|
||||||
routeSpeedPrefetchTask?.cancel()
|
routeSpeedPrefetchTask?.cancel()
|
||||||
routeSpeedPrefetchTask = nil
|
routeSpeedPrefetchTask = nil
|
||||||
|
cancelRoutePlayback(resetMarker: true)
|
||||||
|
stopResendLoop()
|
||||||
|
if backgroundTaskID != .invalid {
|
||||||
|
BackgroundLocationManager.shared.requestStop()
|
||||||
|
}
|
||||||
endBackgroundTask()
|
endBackgroundTask()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1316,14 +1297,14 @@ struct LocationSimulationView: View {
|
||||||
) {
|
) {
|
||||||
routePlaybackCoordinate = nil
|
routePlaybackCoordinate = nil
|
||||||
beginBackgroundTask()
|
beginBackgroundTask()
|
||||||
LocationSimulationSession.shared.clearRoute()
|
|
||||||
startResendLoop(with: coord)
|
startResendLoop(with: coord)
|
||||||
|
BackgroundLocationManager.shared.requestStart()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func simulateRoute() {
|
private func simulateRoute() {
|
||||||
guard pairingExists,
|
guard pairingExists,
|
||||||
let routePlan,
|
routePlan != nil,
|
||||||
let firstCoordinate = routePlaybackSamples.first?.coordinate,
|
let firstCoordinate = routePlaybackSamples.first?.coordinate,
|
||||||
!isBusy else {
|
!isBusy else {
|
||||||
return
|
return
|
||||||
|
|
@ -1338,13 +1319,10 @@ struct LocationSimulationView: View {
|
||||||
operation: { locationUpdateCode(for: firstCoordinate) }
|
operation: { locationUpdateCode(for: firstCoordinate) }
|
||||||
) {
|
) {
|
||||||
beginBackgroundTask()
|
beginBackgroundTask()
|
||||||
let routeID = LocationSimulationSession.shared.startRoute(
|
BackgroundLocationManager.shared.requestStart()
|
||||||
at: firstCoordinate,
|
|
||||||
coordinates: routePlan.displayCoordinates
|
|
||||||
)
|
|
||||||
simulatedCoordinate = nil
|
simulatedCoordinate = nil
|
||||||
routePlaybackCoordinate = firstCoordinate
|
routePlaybackCoordinate = firstCoordinate
|
||||||
startRoutePlayback(routeID: routeID)
|
startRoutePlayback()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1378,14 +1356,13 @@ struct LocationSimulationView: View {
|
||||||
routeSpeedPrefetchTask = nil
|
routeSpeedPrefetchTask = nil
|
||||||
cancelRoutePlayback(resetMarker: true)
|
cancelRoutePlayback(resetMarker: true)
|
||||||
stopResendLoop()
|
stopResendLoop()
|
||||||
LocationSimulationSession.shared.clearRoute()
|
|
||||||
runLocationCommand(
|
runLocationCommand(
|
||||||
errorTitle: "Clear Failed",
|
errorTitle: "Clear Failed",
|
||||||
errorMessage: { code in "Could not clear simulated location (error \(code))." },
|
errorMessage: { code in "Could not clear simulated location (error \(code))." },
|
||||||
operation: clear_simulated_location
|
operation: clear_simulated_location
|
||||||
) {
|
) {
|
||||||
endBackgroundTask()
|
endBackgroundTask()
|
||||||
LocationSimulationSession.shared.stop()
|
BackgroundLocationManager.shared.requestStop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1402,15 +1379,18 @@ struct LocationSimulationView: View {
|
||||||
|
|
||||||
private func startResendLoop(with coordinate: CLLocationCoordinate2D) {
|
private func startResendLoop(with coordinate: CLLocationCoordinate2D) {
|
||||||
simulatedCoordinate = coordinate
|
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 {
|
LocationSimulationCommandQueue.shared.async {
|
||||||
_ = locationUpdateCode(for: coordinate)
|
_ = locationUpdateCode(for: simulatedCoordinate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func stopResendLoop() {
|
private func stopResendLoop() {
|
||||||
LocationSimulationSession.shared.pauseResending()
|
resendTimer?.invalidate()
|
||||||
|
resendTimer = nil
|
||||||
simulatedCoordinate = nil
|
simulatedCoordinate = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1540,19 +1520,15 @@ struct LocationSimulationView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startRoutePlayback(routeID: UUID) {
|
private func startRoutePlayback() {
|
||||||
routePlaybackTask = Task {
|
routePlaybackTask = Task {
|
||||||
var lastSuccessfulCoordinate = routePlaybackSamples.first?.coordinate
|
var lastSuccessfulCoordinate = routePlaybackSamples.first?.coordinate
|
||||||
|
|
||||||
for sample in routePlaybackSamples.dropFirst() {
|
for sample in routePlaybackSamples.dropFirst() {
|
||||||
try? await Task.sleep(for: .seconds(sample.delayFromPrevious))
|
try? await Task.sleep(for: .seconds(sample.delayFromPrevious))
|
||||||
guard !Task.isCancelled,
|
guard !Task.isCancelled else { return }
|
||||||
LocationSimulationSession.shared.isCurrentRoute(routeID) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let code = await sendLocationUpdate(for: sample.coordinate)
|
let code = await sendLocationUpdate(for: sample.coordinate)
|
||||||
guard LocationSimulationSession.shared.isCurrentRoute(routeID) else { return }
|
|
||||||
guard code == 0 else {
|
guard code == 0 else {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
routePlaybackTask = nil
|
routePlaybackTask = nil
|
||||||
|
|
@ -1570,12 +1546,10 @@ struct LocationSimulationView: View {
|
||||||
lastSuccessfulCoordinate = sample.coordinate
|
lastSuccessfulCoordinate = sample.coordinate
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
routePlaybackCoordinate = sample.coordinate
|
routePlaybackCoordinate = sample.coordinate
|
||||||
LocationSimulationSession.shared.updateCoordinate(sample.coordinate)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
guard LocationSimulationSession.shared.isCurrentRoute(routeID) else { return }
|
|
||||||
routePlaybackTask = nil
|
routePlaybackTask = nil
|
||||||
if let lastSuccessfulCoordinate {
|
if let lastSuccessfulCoordinate {
|
||||||
routePlaybackCoordinate = lastSuccessfulCoordinate
|
routePlaybackCoordinate = lastSuccessfulCoordinate
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
private enum SettingsLinks {
|
private enum SettingsLinks {
|
||||||
static let githubStars = URL(string: "https://github.com/StikDebug/StikDebug")!
|
static let githubStars = URL(string: "https://github.com/StephenDev0/StikDebug/stargazers")!
|
||||||
static let pairingFileGuide = URL(string: "https://github.com/StikDebug/StikDebug-Guide/blob/main/pairing_file.md")!
|
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 localDevVPN = URL(string: "https://apps.apple.com/us/app/localdevvpn/id6755608044")!
|
||||||
static let discord = URL(string: "https://discord.gg/qahjXNTDwS")!
|
static let discord = URL(string: "https://discord.gg/qahjXNTDwS")!
|
||||||
}
|
}
|
||||||
|
|
@ -18,8 +18,6 @@ struct SettingsView: View {
|
||||||
@AppStorage(UserDefaults.Keys.confirmExternalJITRequests) private var confirmExternalJITRequests = true
|
@AppStorage(UserDefaults.Keys.confirmExternalJITRequests) private var confirmExternalJITRequests = true
|
||||||
@AppStorage("keepAliveAudio") private var keepAliveAudio = true
|
@AppStorage("keepAliveAudio") private var keepAliveAudio = true
|
||||||
@AppStorage("keepAliveLocation") private var keepAliveLocation = 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 isShowingPairingFilePicker = false
|
||||||
@State private var isImportingFile = false
|
@State private var isImportingFile = false
|
||||||
|
|
@ -95,7 +93,8 @@ struct SettingsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: keepAliveAudio) { _, enabled in
|
.onChange(of: keepAliveAudio) { _, enabled in
|
||||||
handleAudioKeepAliveChange(enabled)
|
if enabled { BackgroundAudioManager.shared.start() }
|
||||||
|
else { BackgroundAudioManager.shared.stop() }
|
||||||
}
|
}
|
||||||
|
|
||||||
Toggle(isOn: $keepAliveLocation) {
|
Toggle(isOn: $keepAliveLocation) {
|
||||||
|
|
@ -106,7 +105,7 @@ struct SettingsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: keepAliveLocation) { _, enabled in
|
.onChange(of: keepAliveLocation) { _, enabled in
|
||||||
handleLocationKeepAliveChange(enabled)
|
if !enabled { BackgroundLocationManager.shared.stop() }
|
||||||
}
|
}
|
||||||
|
|
||||||
} header: {
|
} header: {
|
||||||
|
|
@ -131,27 +130,12 @@ 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") {
|
Section("Advanced") {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Target Device IP")
|
Text("Target Device IP")
|
||||||
Spacer()
|
Spacer()
|
||||||
TextField(DeviceConnectionContext.defaultTargetIPAddress, text: $targetDeviceIP)
|
Text(DeviceConnectionContext.defaultTargetIPAddress)
|
||||||
.multilineTextAlignment(.trailing)
|
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.textInputAutocapitalization(.never)
|
|
||||||
.autocorrectionDisabled(true)
|
|
||||||
.keyboardType(.numbersAndPunctuation)
|
|
||||||
.frame(maxWidth: 160)
|
|
||||||
}
|
}
|
||||||
Button { openAppFolder() } label: {
|
Button { openAppFolder() } label: {
|
||||||
Label("App Folder", systemImage: "folder")
|
Label("App Folder", systemImage: "folder")
|
||||||
|
|
@ -241,25 +225,6 @@ struct SettingsView: View {
|
||||||
return "Version \(appVersion) • iOS \(UIDevice.current.systemVersion) • \(txmLabel)"
|
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
|
// MARK: - Business Logic
|
||||||
|
|
||||||
private func openAppFolder() {
|
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