mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-04-04 20:39:05 +08:00
Update README.md
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -310,3 +310,4 @@ __marimo__/
|
||||
*.resolved
|
||||
/VM
|
||||
.limd/
|
||||
/.swiftpm
|
||||
|
||||
58
AGENTS.md
58
AGENTS.md
@@ -1,4 +1,17 @@
|
||||
# AGENTS — vphone-cli
|
||||
# vphone-cli
|
||||
|
||||
Virtual iPhone boot tool using Apple's Virtualization.framework with PCC research VMs.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
- **Build:** `make build`
|
||||
- **Boot (GUI):** `make boot`
|
||||
- **Boot (DFU):** `make boot_dfu`
|
||||
- **All targets:** `make help`
|
||||
- **Python venv:** `make setup_venv` (installs to `.venv/`, activate with `source .venv/bin/activate`)
|
||||
- **Platform:** macOS 14+ (Sequoia), SIP/AMFI disabled
|
||||
- **Language:** Swift 6.0 (SwiftPM), private APIs via [Dynamic](https://github.com/mhdhejazi/Dynamic)
|
||||
- **Python deps:** `capstone`, `keystone-engine`, `pyimg4` (see `requirements.txt`)
|
||||
|
||||
## Project Overview
|
||||
|
||||
@@ -10,14 +23,17 @@ CLI tool that boots virtual iPhones (PV=3) via Apple's Virtualization.framework,
|
||||
Makefile # Single entry point — run `make help`
|
||||
|
||||
sources/
|
||||
├── vphone-objc/ # ObjC bridge for private Virtualization.framework APIs
|
||||
│ ├── include/VPhoneObjC.h
|
||||
│ └── VPhoneObjC.m
|
||||
└── vphone-cli/ # Swift executable
|
||||
├── VPhoneCLI.swift
|
||||
├── VPhoneVM.swift
|
||||
├── VPhoneHardwareModel.swift
|
||||
└── VPhoneVMWindow.swift
|
||||
├── vphone.entitlements # Private API entitlements (5 keys)
|
||||
└── vphone-cli/ # Swift 6.0 executable (pure Swift, no ObjC)
|
||||
├── main.swift # Entry point — NSApplication + AppDelegate
|
||||
├── VPhoneAppDelegate.swift # App lifecycle, SIGINT, VM start/stop
|
||||
├── VPhoneCLI.swift # ArgumentParser options (no execution logic)
|
||||
├── VPhoneVM.swift # @MainActor VM configuration and lifecycle
|
||||
├── VPhoneHardwareModel.swift # PV=3 hardware model via Dynamic
|
||||
├── VPhoneVMView.swift # Touch-enabled VZVirtualMachineView + helpers
|
||||
├── VPhoneWindowController.swift # @MainActor window management
|
||||
├── VPhoneError.swift # Error types
|
||||
└── MainActor+Isolated.swift # MainActor.isolated helper
|
||||
|
||||
scripts/
|
||||
├── patchers/ # Python patcher package
|
||||
@@ -38,17 +54,16 @@ scripts/
|
||||
├── setup_venv.sh # Creates Python venv with native keystone dylib
|
||||
└── setup_libimobiledevice.sh # Builds libimobiledevice toolchain from source
|
||||
|
||||
Research/ # Research notes and verification reports
|
||||
researchs/ # Component analysis and architecture docs
|
||||
```
|
||||
|
||||
### Key Patterns
|
||||
|
||||
- **Private API access:** All private Virtualization.framework calls go through the ObjC bridge (`VPhoneObjC`). Swift code never calls private APIs directly.
|
||||
- **Function naming:** ObjC bridge functions use the `VPhone` prefix (e.g., `VPhoneCreateHardwareModel`, `VPhoneConfigureSEP`).
|
||||
- **Private API access:** Private Virtualization.framework APIs are called via the [Dynamic](https://github.com/mhdhejazi/Dynamic) library (runtime method dispatch from pure Swift). No ObjC bridge needed.
|
||||
- **App lifecycle:** Explicit `main.swift` creates `NSApplication` + `VPhoneAppDelegate`. CLI args parsed before the run loop starts. AppDelegate drives VM start, window, and shutdown.
|
||||
- **Configuration:** CLI options parsed via `ArgumentParser`, converted to `VPhoneVM.Options` struct, then used to build `VZVirtualMachineConfiguration`.
|
||||
- **Error handling:** `VPhoneError` enum with `CustomStringConvertible` for user-facing messages.
|
||||
- **Window management:** `VPhoneWindowController` wraps `NSWindow` + `VZVirtualMachineView`. Touch input translated from mouse events to multi-touch via `VPhoneVMView`.
|
||||
- **Window management:** `VPhoneWindowController` wraps `NSWindow` + `VZVirtualMachineView`. Window size derived from configurable screen dimensions and scale factor. Touch input translated from mouse events to multi-touch via `VPhoneVMView`.
|
||||
|
||||
---
|
||||
|
||||
@@ -184,17 +199,13 @@ AVPBooter (ROM, PCC)
|
||||
|
||||
### Swift
|
||||
|
||||
- **Language:** Swift 6.0 (strict concurrency).
|
||||
- **Style:** Pragmatic, minimal. No unnecessary abstractions.
|
||||
- **Sections:** Use `// MARK: -` to organize code within files.
|
||||
- **Access control:** Default (internal). Only mark `private` when needed for clarity.
|
||||
- **Async:** Use `async/await` for VM lifecycle. `@MainActor` for UI and VM start operations.
|
||||
- **Concurrency:** `@MainActor` for VM and UI classes. `nonisolated` delegate methods use `MainActor.isolated {}` to hop back safely.
|
||||
- **Naming:** Types are `VPhone`-prefixed (`VPhoneVM`, `VPhoneWindowController`). Match Apple framework conventions.
|
||||
|
||||
### ObjC Bridge
|
||||
|
||||
- All functions are C-style (no ObjC classes exposed to Swift).
|
||||
- Return `nil`/`NULL` on failure — caller handles gracefully.
|
||||
- Header documents the private API being wrapped in each function's doc comment.
|
||||
- **Private APIs:** Use `Dynamic()` for runtime method dispatch. Touch objects use `NSClassFromString` + KVC to avoid designated initializer crashes.
|
||||
|
||||
### Shell Scripts
|
||||
|
||||
@@ -228,7 +239,12 @@ Creates a VM directory with:
|
||||
- Sparse disk image (default 64 GB)
|
||||
- SEP storage (512 KB flat file)
|
||||
- AVPBooter + AVPSEPBooter ROMs (copied from `/System/Library/Frameworks/Virtualization.framework/`)
|
||||
- NVRAM and machineIdentifier auto-created on first boot
|
||||
- machineIdentifier (created on first boot if missing, persisted for stable ECID)
|
||||
- NVRAM (created/overwritten each boot)
|
||||
|
||||
All paths are passed explicitly via CLI (`--rom`, `--disk`, `--nvram`, `--machine-id`, `--sep-storage`, `--sep-rom`). SEP coprocessor is always enabled.
|
||||
|
||||
Display is configurable via `--screen-width`, `--screen-height`, `--screen-ppi`, `--screen-scale` (defaults: 1290x2796 @ 460 PPI, scale 3.0).
|
||||
|
||||
Override defaults: `make vm_new VM_DIR=myvm DISK_SIZE=32`.
|
||||
|
||||
|
||||
16
CLAUDE.md
16
CLAUDE.md
@@ -1,16 +0,0 @@
|
||||
# vphone-cli
|
||||
|
||||
Virtual iPhone boot tool using Apple's Virtualization.framework with PCC research VMs.
|
||||
|
||||
See [AGENTS.md](./AGENTS.md) for project conventions, architecture, and design system.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
- **Build:** `make build`
|
||||
- **Boot (headless):** `make boot`
|
||||
- **Boot (DFU):** `make boot_dfu`
|
||||
- **All targets:** `make help`
|
||||
- **Python venv:** `make setup_venv` (installs to `.venv/`, activate with `source .venv/bin/activate`)
|
||||
- **Platform:** macOS 14+ (Sequoia), SIP/AMFI disabled
|
||||
- **Language:** Swift 5.10 (SwiftPM), ObjC bridge for private APIs
|
||||
- **Python deps:** `capstone`, `keystone-engine`, `pyimg4` (see `requirements.txt`)
|
||||
17
Makefile
17
Makefile
@@ -4,8 +4,8 @@
|
||||
|
||||
# ─── Configuration (override with make VAR=value) ─────────────────
|
||||
VM_DIR ?= vm
|
||||
CPU ?= 4
|
||||
MEMORY ?= 4096
|
||||
CPU ?= 8
|
||||
MEMORY ?= 8192
|
||||
DISK_SIZE ?= 64
|
||||
|
||||
# ─── Paths ────────────────────────────────────────────────────────
|
||||
@@ -18,7 +18,7 @@ IRECOVERY := $(LIMD_PREFIX)/bin/irecovery
|
||||
IDEVICERESTORE := $(LIMD_PREFIX)/bin/idevicerestore
|
||||
PYTHON := $(CURDIR)/$(VENV)/bin/python3
|
||||
|
||||
SWIFT_SOURCES := $(shell find sources -name '*.swift' -o -name '*.m' -o -name '*.h')
|
||||
SWIFT_SOURCES := $(shell find sources -name '*.swift')
|
||||
|
||||
# ─── Environment — prefer project-local binaries ────────────────
|
||||
export PATH := $(CURDIR)/$(LIMD_PREFIX)/bin:$(CURDIR)/$(VENV)/bin:$(CURDIR)/.build/release:$(PATH)
|
||||
@@ -39,7 +39,7 @@ help:
|
||||
@echo ""
|
||||
@echo "VM management:"
|
||||
@echo " make vm_new Create VM directory"
|
||||
@echo " make boot Boot VM (headless)"
|
||||
@echo " make boot Boot VM (GUI)"
|
||||
@echo " make boot_dfu Boot VM in DFU mode"
|
||||
@echo ""
|
||||
@echo "Firmware pipeline:"
|
||||
@@ -110,21 +110,18 @@ boot: build
|
||||
--rom ./AVPBooter.vresearch1.bin \
|
||||
--disk ./Disk.img \
|
||||
--nvram ./nvram.bin \
|
||||
--machine-id ./machineIdentifier.bin \
|
||||
--cpu $(CPU) --memory $(MEMORY) \
|
||||
--serial-log ./serial.log \
|
||||
--stop-on-panic --stop-on-fatal-error \
|
||||
--sep-rom ./AVPSEPBooter.vresearch1.bin \
|
||||
--sep-storage ./SEPStorage \
|
||||
--no-graphics
|
||||
--sep-storage ./SEPStorage
|
||||
|
||||
boot_dfu: build
|
||||
cd $(VM_DIR) && "$(CURDIR)/$(BINARY)" \
|
||||
--rom ./AVPBooter.vresearch1.bin \
|
||||
--disk ./Disk.img \
|
||||
--nvram ./nvram.bin \
|
||||
--machine-id ./machineIdentifier.bin \
|
||||
--cpu $(CPU) --memory $(MEMORY) \
|
||||
--serial-log ./serial.log \
|
||||
--stop-on-panic --stop-on-fatal-error \
|
||||
--sep-rom ./AVPSEPBooter.vresearch1.bin \
|
||||
--sep-storage ./SEPStorage \
|
||||
--no-graphics --dfu
|
||||
|
||||
@@ -1,40 +1,28 @@
|
||||
// swift-tools-version:5.10
|
||||
// swift-tools-version:6.0
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "vphone-cli",
|
||||
platforms: [
|
||||
.macOS(.v14),
|
||||
.macOS(.v15),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.1"),
|
||||
.package(url: "https://github.com/mhdhejazi/Dynamic", from: "1.2.0"),
|
||||
],
|
||||
targets: [
|
||||
// ObjC module: wraps private Virtualization.framework APIs
|
||||
.target(
|
||||
name: "VPhoneObjC",
|
||||
path: "sources/vphone-objc",
|
||||
publicHeadersPath: "include",
|
||||
linkerSettings: [
|
||||
.linkedFramework("Virtualization"),
|
||||
]
|
||||
),
|
||||
// Swift executable
|
||||
.executableTarget(
|
||||
name: "vphone-cli",
|
||||
dependencies: [
|
||||
"VPhoneObjC",
|
||||
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||
.product(name: "Dynamic", package: "Dynamic"),
|
||||
],
|
||||
path: "sources/vphone-cli",
|
||||
swiftSettings: [
|
||||
.unsafeFlags(["-parse-as-library"]),
|
||||
],
|
||||
linkerSettings: [
|
||||
.linkedFramework("Virtualization"),
|
||||
.linkedFramework("AppKit"),
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
11
README.md
11
README.md
@@ -6,10 +6,11 @@ Boot a virtual iPhone (iOS 26) via Apple's Virtualization.framework using PCC re
|
||||
|
||||
## Tested Environments
|
||||
|
||||
| Record | Host macOS | Host Hardware | iPhone IPSW | CloudOS IPSW |
|
||||
|--------|------------|---------------|-------------|--------------|
|
||||
| 1 | macOS 26.3 (Tahoe, Build 25D125) | MacBook Air, Apple M4 | `iPhone17,3_26.1_23B85_Restore.ipsw` | `PCC-CloudOS-26.1-23B85.ipsw` |
|
||||
| 2 | macOS 26.3 (Tahoe, Build 25D125) | MacBook Air, Apple M4 | `iPhone17,3_26.3_23D127_Restore.ipsw` | `PCC-CloudOS-26.1-23B85.ipsw` |
|
||||
| Host | iPhone | CloudOS |
|
||||
|------|--------|---------|
|
||||
| Mac16,12 26.3 | `17,3_26.1_23B85` | `26.1-23B85` |
|
||||
| Mac16,12 26.3 | `17,3_26.3_23D127` | `26.1-23B85` |
|
||||
| Mac16,12 26.3 | `17,3_26.3_23D127` | `26.3-23D128` |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -103,7 +104,7 @@ Run `make help` for the full list. Key targets:
|
||||
| `vm_new` | Create VM directory |
|
||||
| `fw_prepare` | Download/merge IPSWs |
|
||||
| `fw_patch` | Patch boot chain |
|
||||
| `boot` / `boot_dfu` | Boot VM (normal / DFU) |
|
||||
| `boot` / `boot_dfu` | Boot VM (GUI / DFU headless) |
|
||||
| `restore_get_shsh` | Fetch SHSH blob |
|
||||
| `restore` | Flash firmware |
|
||||
| `ramdisk_build` | Build SSH ramdisk |
|
||||
|
||||
119
researchs/jailbreak_patches.md
Normal file
119
researchs/jailbreak_patches.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# Jailbreak Patches vs Base Patches
|
||||
|
||||
Comparison of base boot-chain patches (`make fw_patch`) vs jailbreak-extended patches (`make fw_patch_jb`).
|
||||
|
||||
Base patches enable VM boot with signature bypass and SSV override.
|
||||
Jailbreak patches add code signing bypass, entitlement spoofing, task/VM security bypass,
|
||||
sandbox hook neutralization, and kernel arbitrary call (kcall10).
|
||||
|
||||
## iBSS
|
||||
|
||||
| # | Patch | Purpose | Base | JB |
|
||||
| --- | --------------------------------- | --------------------------------------- | :--: | :-: |
|
||||
| 1 | Serial labels (2x) | "Loaded iBSS" in serial log | Y | Y |
|
||||
| 2 | image4_validate_property_callback | Signature bypass (nop b.ne + mov x0,#0) | Y | Y |
|
||||
| 3 | Skip generate_nonce | Keep apnonce stable for SHSH | — | Y |
|
||||
|
||||
## iBEC
|
||||
|
||||
| # | Patch | Purpose | Base | JB |
|
||||
| --- | --------------------------------- | ------------------------------ | :--: | :-: |
|
||||
| 1 | Serial labels (2x) | "Loaded iBEC" in serial log | Y | Y |
|
||||
| 2 | image4_validate_property_callback | Signature bypass | Y | Y |
|
||||
| 3 | Boot-args redirect | `serial=3 -v debug=0x2014e %s` | Y | Y |
|
||||
|
||||
No additional JB patches for iBEC.
|
||||
|
||||
## LLB
|
||||
|
||||
| # | Patch | Purpose | Base | JB |
|
||||
| --- | --------------------------------- | ---------------------------------- | :--: | :-: |
|
||||
| 1 | Serial labels (2x) | "Loaded LLB" in serial log | Y | Y |
|
||||
| 2 | image4_validate_property_callback | Signature bypass | Y | Y |
|
||||
| 3 | Boot-args redirect | `serial=3 -v debug=0x2014e %s` | Y | Y |
|
||||
| 4 | Rootfs bypass (5 patches) | Allow edited rootfs loading | Y | Y |
|
||||
| 5 | Panic bypass | NOP cbnz after mov w8,#0x328 check | Y | Y |
|
||||
|
||||
No additional JB patches for LLB.
|
||||
|
||||
## TXM
|
||||
|
||||
| # | Patch | Purpose | Base | JB |
|
||||
| --- | ------------------------------------------ | --------------------------------- | :------------------------------------------: | :-: | --- |
|
||||
| 1 | Trustcache binary search bypass | `bl hash_cmp → mov x0,#0` | Y | Y |
|
||||
| 2 | CodeSignature selector 24 (3x mov x0,#0) | Bypass CS validation return paths | — | Y |
|
||||
| 3 | CodeSignature selector 24 | 0xA1 (2x nop) | Bypass CS error path | — | Y |
|
||||
| 4 | get-task-allow (selector 41 | 29) | `mov x0,#1` — allow get-task-allow | — | Y |
|
||||
| 5 | Selector 42 | 29 + shellcode | Branch to shellcode that sets flag + returns | — | Y |
|
||||
| 6 | com.apple.private.cs.debugger (selector 42 | 37) | `mov w0,#1` — allow debugger entitlement | — | Y |
|
||||
| 7 | Developer mode bypass | NOP developer mode enforcement | — | Y |
|
||||
|
||||
## Kernelcache
|
||||
|
||||
### Base patches (SSV + basic AMFI + sandbox)
|
||||
|
||||
| # | Patch | Function | Purpose | Base | JB |
|
||||
| ----- | ------------------------ | -------------------------------- | ------------------------------------- | :--: | :-: |
|
||||
| 1 | NOP panic | `_apfs_vfsop_mount` | Skip "root snapshot" panic | Y | Y |
|
||||
| 2 | NOP panic | `_authapfs_seal_is_broken` | Skip "root volume seal" panic | Y | Y |
|
||||
| 3 | NOP panic | `_bsd_init` | Skip "rootvp not authenticated" panic | Y | Y |
|
||||
| 4-5 | mov w0,#0; ret | `_proc_check_launch_constraints` | Bypass launch constraints | Y | Y |
|
||||
| 6-7 | mov x0,#1 (2x) | `PE_i_can_has_debugger` | Enable kernel debugger | Y | Y |
|
||||
| 8 | NOP | `_postValidation` | Skip AMFI post-validation | Y | Y |
|
||||
| 9 | cmp w0,w0 | `_postValidation` | Force comparison true | Y | Y |
|
||||
| 10-11 | mov w0,#1 (2x) | `_check_dyld_policy_internal` | Allow dyld loading | Y | Y |
|
||||
| 12 | mov w0,#0 | `_apfs_graft` | Allow APFS graft | Y | Y |
|
||||
| 13 | cmp x0,x0 | `_apfs_vfsop_mount` | Skip mount check | Y | Y |
|
||||
| 14 | mov w0,#0 | `_apfs_mount_upgrade_checks` | Allow mount upgrade | Y | Y |
|
||||
| 15 | mov w0,#0 | `_handle_fsioc_graft` | Allow fsioc graft | Y | Y |
|
||||
| 16-25 | mov x0,#0; ret (5 hooks) | Sandbox MACF ops table | Stub 5 sandbox hooks | Y | Y |
|
||||
|
||||
### Jailbreak-only kernel patches
|
||||
|
||||
| # | Patch | Function | Purpose | Base | JB |
|
||||
| --- | -------------------------- | ------------------------------------ | ------------------------------------------ | :--: | :-: |
|
||||
| 26 | Rewrite function | `AMFIIsCDHashInTrustCache` | Always return true + store hash | — | Y |
|
||||
| 27 | Shellcode + branch | `_cred_label_update_execve` | Set cs_flags (platform+entitlements) | — | Y |
|
||||
| 28 | cmp w0,w0 | `_postValidation` (additional) | Force validation pass | — | Y |
|
||||
| 29 | Shellcode + branch | `_syscallmask_apply_to_proc` | Patch zalloc_ro_mut for syscall mask | — | Y |
|
||||
| 30 | Shellcode + ops redirect | `_hook_cred_label_update_execve` | vnode_getattr ownership propagation + suid | — | Y |
|
||||
| 31 | mov x0,#0; ret (20+ hooks) | Sandbox MACF ops table (extended) | Stub remaining 20+ sandbox hooks | — | Y |
|
||||
| 32 | cmp xzr,xzr | `_task_conversion_eval_internal` | Allow task conversion | — | Y |
|
||||
| 33 | mov x0,#0; ret | `_proc_security_policy` | Bypass security policy | — | Y |
|
||||
| 34 | NOP (2x) | `_proc_pidinfo` | Allow pid 0 info | — | Y |
|
||||
| 35 | b (skip panic) | `_convert_port_to_map_with_flavor` | Skip kernel map panic | — | Y |
|
||||
| 36 | NOP | `_vm_fault_enter_prepare` | Skip fault check | — | Y |
|
||||
| 37 | b (skip check) | `_vm_map_protect` | Allow VM protect | — | Y |
|
||||
| 38 | NOP + mov x8,xzr | `___mac_mount` | Bypass MAC mount check | — | Y |
|
||||
| 39 | NOP | `_dounmount` | Allow unmount | — | Y |
|
||||
| 40 | mov x0,#0 | `_bsd_init` (2nd) | Skip auth at @%s:%d | — | Y |
|
||||
| 41 | NOP (2x) | `_spawn_validate_persona` | Skip persona validation | — | Y |
|
||||
| 42 | NOP | `_task_for_pid` | Allow task_for_pid | — | Y |
|
||||
| 43 | b (skip check) | `_load_dylinker` | Allow dylinker loading | — | Y |
|
||||
| 44 | cmp x0,x0 | `_shared_region_map_and_slide_setup` | Force shared region | — | Y |
|
||||
| 45 | NOP | `_verifyPermission` (NVRAM) | Allow NVRAM writes | — | Y |
|
||||
| 46 | b (skip check) | `_IOSecureBSDRoot` | Skip secure root check | — | Y |
|
||||
| 47 | Syscall 439 + shellcode | kcall10 (SYS_kas_info replacement) | Kernel arbitrary call from userspace | — | Y |
|
||||
| 48 | Zero out | `_thid_should_crash` | Prevent GUARD_TYPE_MACH_PORT crash | — | Y |
|
||||
|
||||
## CFW (cfw_install)
|
||||
|
||||
| # | Patch | Binary | Purpose | Base | JB |
|
||||
| --- | -------------------- | -------------------- | ------------------------------ | :--: | :-: |
|
||||
| 1 | /%s.gl → /AA.gl | seputil | Gigalocker UUID fix | Y | Y |
|
||||
| 2 | NOP cache validation | launchd_cache_loader | Allow modified launchd.plist | Y | Y |
|
||||
| 3 | mov x0,#1; ret | mobileactivationd | Activation bypass | Y | Y |
|
||||
| 4 | Plist injection | launchd.plist | bash/dropbear/trollvnc daemons | Y | Y |
|
||||
| 5 | b (skip jetsam) | launchd | Prevent jetsam panic on boot | — | Y |
|
||||
|
||||
## Summary
|
||||
|
||||
| Binary | Base | JB-only | Total |
|
||||
| ----------- | :----: | :------: | :------: |
|
||||
| iBSS | 2 | 1 | 3 |
|
||||
| iBEC | 3 | 0 | 3 |
|
||||
| LLB | 6 | 0 | 6 |
|
||||
| TXM | 1 | ~13 | ~14 |
|
||||
| Kernelcache | 25 | ~23+ | ~48+ |
|
||||
| CFW | 4 | 1 | 5 |
|
||||
| **Total** | **41** | **~38+** | **~79+** |
|
||||
251
researchs/keyboard_event_pipeline.md
Normal file
251
researchs/keyboard_event_pipeline.md
Normal file
@@ -0,0 +1,251 @@
|
||||
# Virtualization.framework Keyboard Event Pipeline
|
||||
|
||||
Reverse engineering findings for the keyboard event pipeline in Apple's
|
||||
Virtualization.framework (macOS 26.2, version 259.3.3.0.0). Documents how
|
||||
keyboard events flow from the macOS host to the virtual iPhone guest.
|
||||
|
||||
---
|
||||
|
||||
## Event Flow Architecture
|
||||
|
||||
There are two pipelines for sending keyboard events to the VM:
|
||||
|
||||
### Pipeline 1: _VZKeyEvent -> sendKeyEvents: (Standard Keys)
|
||||
|
||||
```
|
||||
_VZKeyEvent(type, keyCode)
|
||||
-> _VZKeyboard.sendKeyEvents:(NSArray<_VZKeyEvent>)
|
||||
-> table lookup: keyCode -> intermediate index
|
||||
-> pack: uint64_t = (index << 32) | is_key_down
|
||||
-> std::vector<uint64_t>
|
||||
-> [if type==2] sendKeyboardEventsHIDReport:keyboardID: (switch -> IOHIDEvent -> HID reports)
|
||||
-> [fallback] eventSender.sendKeyboardEvents:keyboardID: (VzCore C++ layer)
|
||||
```
|
||||
|
||||
### Pipeline 2: _processHIDReports (Raw HID Reports)
|
||||
|
||||
```
|
||||
Raw HID report bytes
|
||||
-> std::span<const unsigned char>{data_ptr, length}
|
||||
-> std::vector<std::span<...>>{begin, end, cap}
|
||||
-> VZVirtualMachine._processHIDReports:forDevice:deviceType:
|
||||
-> XpcEncoder::encode_data(span) -> xpc_data_create
|
||||
-> XPC to VMM process
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## _VZKeyEvent Structure
|
||||
|
||||
From IDA + LLDB inspection:
|
||||
|
||||
```c
|
||||
struct _VZKeyEvent { // sizeof = 0x18
|
||||
uint8_t isa[8]; // offset 0x00 -- ObjC isa pointer
|
||||
uint16_t _keyCode; // offset 0x08 -- Apple VK code (0x00-0xB2)
|
||||
uint8_t _pad[6]; // offset 0x0A -- padding
|
||||
int64_t _type; // offset 0x10 -- 0 = keyDown, 1 = keyUp
|
||||
};
|
||||
```
|
||||
|
||||
Initializer: `_VZKeyEvent(type: Int64, keyCode: UInt16)`
|
||||
|
||||
---
|
||||
|
||||
## _VZKeyboard Object Layout
|
||||
|
||||
From LLDB memory dump:
|
||||
|
||||
```
|
||||
+0x00: isa
|
||||
+0x08: _eventSender (weak, id<_VZHIDAdditions, _VZKeyboardEventSender>)
|
||||
+0x10: _deviceIdentifier (uint32_t) -- value 1 for first keyboard
|
||||
+0x18: type (int64_t) -- 0 for USB keyboard, 2 for type that tries HIDReport first
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Lookup Tables in sendKeyEvents:
|
||||
|
||||
Two tables indexed by Apple VK keyCode (0x00-0xB2, 179 entries x 8 bytes each):
|
||||
|
||||
**Table 1** (validity flags): All valid entries = `0x0000000100000000` (bit 32 set).
|
||||
Invalid entries = 0.
|
||||
|
||||
**Table 2** (intermediate indices): Maps Apple VK codes to internal indices (0x00-0x72).
|
||||
|
||||
The tables are OR'd: `combined = table1[vk] | table2[vk]`. Bit 32 check validates
|
||||
the entry. The lower 32 bits of combined become the intermediate index.
|
||||
|
||||
### Sample Table 2 Entries
|
||||
|
||||
| Apple VK | Key | Table2 (Index) | HID Page | HID Usage |
|
||||
|----------|---------|----------------|----------|-----------|
|
||||
| 0x00 | A | 0x00 | 7 | 0x04 |
|
||||
| 0x01 | S | 0x12 | 7 | 0x16 |
|
||||
| 0x24 | Return | 0x24 | 7 | 0x28 |
|
||||
| 0x31 | Space | 0x29 | 7 | 0x2C |
|
||||
| 0x35 | Escape | 0x25 | 7 | 0x29 |
|
||||
| 0x38 | Shift | 0x51 | 7 | 0xE1 |
|
||||
| 0x37 | Command | 0x53 | 7 | 0xE3 |
|
||||
|
||||
### Invalid VK Codes (both tables = 0, silently dropped)
|
||||
|
||||
0x48 (Volume Up), 0x49 (Volume Down), 0x4A (Mute), and many others.
|
||||
|
||||
---
|
||||
|
||||
## Packed Event Format (std::vector<uint64_t>)
|
||||
|
||||
Each element in the vector sent to `sendKeyboardEvents:keyboardID:`:
|
||||
|
||||
```
|
||||
bits 63:32 = intermediate_index (from table2, lower 32 bits of combined)
|
||||
bits 31:1 = 0
|
||||
bit 0 = is_key_down (1 = down, 0 = up)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## sendKeyboardEventsHIDReport Switch Statement
|
||||
|
||||
For type-2 keyboards, the intermediate index is mapped to
|
||||
`IOHIDEventCreateKeyboardEvent(page, usage)` via a large switch.
|
||||
|
||||
### Standard Keyboard Entries (HID Page 7)
|
||||
|
||||
| Index | HID Page | HID Usage | Meaning |
|
||||
|-----------|----------|-----------|------------------|
|
||||
| 0x00-0x19 | 7 | 4-29 | Letters a-z |
|
||||
| 0x1A-0x23 | 7 | 30-39 | Digits 1-0 |
|
||||
| 0x24 | 7 | 40 | Return |
|
||||
| 0x25 | 7 | 41 | Escape |
|
||||
| 0x29 | 7 | 44 | Space |
|
||||
| 0x48-0x4B | 7 | 79-82 | Arrow keys |
|
||||
| 0x50-0x53 | 7 | 224-227 | L-Ctrl/Shift/Alt/Cmd |
|
||||
|
||||
### Consumer / System Entries (Non-Standard Pages)
|
||||
|
||||
| Index | HID Page | HID Usage | Meaning |
|
||||
|-------|----------|-----------|---------------------------|
|
||||
| 0x6E | **12** | 671 | **Consumer Volume Down** |
|
||||
| 0x6F | **12** | 674 | **Consumer Volume Up** |
|
||||
| 0x70 | **12** | 207 | **Consumer Play/Pause** |
|
||||
| 0x71 | **12** | 545 | **Consumer Snapshot** |
|
||||
| 0x72 | **1** | 155 | **Generic Desktop Wake** |
|
||||
|
||||
**Home/Menu (Consumer page 0x0C, usage 0x40) has NO intermediate index.** It cannot
|
||||
be sent through Pipeline 1 at all.
|
||||
|
||||
---
|
||||
|
||||
## _processHIDReports Parameter Format
|
||||
|
||||
From IDA decompilation of
|
||||
`VZVirtualMachine._processHIDReports:forDevice:deviceType:` at 0x2301b2310.
|
||||
|
||||
The `void *` parameter is a **pointer to std::vector<std::span<const unsigned char>>**:
|
||||
|
||||
```
|
||||
Level 3 (outermost): std::vector (24 bytes, passed by pointer)
|
||||
+0x00: __begin_ (pointer to span array)
|
||||
+0x08: __end_ (pointer past last span)
|
||||
+0x10: __end_cap_ (capacity pointer)
|
||||
|
||||
Level 2: std::span (16 bytes per element in the array)
|
||||
+0x00: data_ptr (const unsigned char *)
|
||||
+0x08: length (size_t)
|
||||
|
||||
Level 1 (innermost): raw HID report bytes
|
||||
```
|
||||
|
||||
The function iterates spans in the vector:
|
||||
|
||||
```c
|
||||
begin = *vec; // vec->__begin_
|
||||
end = *(vec + 1); // vec->__end_
|
||||
for (span = begin; span != end; span += 16) {
|
||||
data_ptr = *(uint64_t*)span;
|
||||
length = *(uint64_t*)(span + 8);
|
||||
encoder.encode_data(data_ptr, length); // -> xpc_data_create
|
||||
}
|
||||
```
|
||||
|
||||
**deviceType**: 0 = keyboard, 1 = pointing device
|
||||
|
||||
**device**: device identifier (uint32_t, matches `_VZKeyboard._deviceIdentifier`)
|
||||
|
||||
---
|
||||
|
||||
## Crash Analysis: Why Raw Bytes Crashed
|
||||
|
||||
Passing raw `[0x40, 0x00]` as the `void*` parameter:
|
||||
|
||||
1. Function reads bytes as vector struct: begin = 0x0040 (first 8 bytes), end = garbage
|
||||
2. Dereferences begin as span pointer -> reads from address ~0x0040
|
||||
3. Gets garbage data_ptr (0x700420e) and garbage length (0x300000020 = 12GB)
|
||||
4. `xpc_data_create(0x700420e, 0x300000020)` -> EXC_BAD_ACCESS in memcpy
|
||||
|
||||
The three-level indirection (vector -> span -> bytes) must be constructed correctly
|
||||
or the framework will dereference invalid pointers.
|
||||
|
||||
---
|
||||
|
||||
## Swift Implementation Notes
|
||||
|
||||
### Accessing _VZKeyboard
|
||||
|
||||
```swift
|
||||
// Get keyboards array
|
||||
let arr = Dynamic(vm)._keyboards.asObject as? NSArray
|
||||
let keyboard = arr?.object(at: 0) as AnyObject
|
||||
|
||||
// _deviceIdentifier is an ivar, not a property -- use KVC
|
||||
(keyboard as? NSObject)?.value(forKey: "_deviceIdentifier") as? UInt32
|
||||
```
|
||||
|
||||
### Constructing std::vector<uint64_t> for sendKeyboardEvents
|
||||
|
||||
```swift
|
||||
let data = UnsafeMutablePointer<UInt64>.allocate(capacity: 1)
|
||||
data.pointee = (index << 32) | (isKeyDown ? 1 : 0)
|
||||
var vec = (data, data.advanced(by: 1), data.advanced(by: 1))
|
||||
withUnsafeMutablePointer(to: &vec) { vecPtr in
|
||||
Dynamic(vm).sendKeyboardEvents(UnsafeMutableRawPointer(vecPtr), keyboardID: deviceId)
|
||||
}
|
||||
```
|
||||
|
||||
### Constructing vector<span<unsigned char>> for _processHIDReports
|
||||
|
||||
```swift
|
||||
let reportPtr = UnsafeMutablePointer<UInt8>.allocate(capacity: N)
|
||||
// fill report bytes...
|
||||
|
||||
let spanPtr = UnsafeMutablePointer<Int>.allocate(capacity: 2)
|
||||
spanPtr[0] = Int(bitPattern: reportPtr) // data pointer
|
||||
spanPtr[1] = N // length
|
||||
|
||||
let vecPtr = UnsafeMutablePointer<Int>.allocate(capacity: 3)
|
||||
vecPtr[0] = Int(bitPattern: UnsafeRawPointer(spanPtr)) // begin
|
||||
vecPtr[1] = Int(bitPattern: UnsafeRawPointer(spanPtr).advanced(by: 16)) // end
|
||||
vecPtr[2] = vecPtr[1] // cap
|
||||
|
||||
Dynamic(vm)._processHIDReports(UnsafeRawPointer(vecPtr), forDevice: deviceId, deviceType: 0)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Source Files
|
||||
|
||||
- Class dumps: `/Users/qaq/Documents/GitHub/super-tart-vphone-private/Virtualization_26.2-class-dump/`
|
||||
- IDA database: dyld_shared_cache_arm64e with Virtualization.framework
|
||||
|
||||
### Key Functions Analyzed
|
||||
|
||||
| Function | Address |
|
||||
|----------|---------|
|
||||
| `-[_VZKeyboard sendKeyEvents:]` | 0x2301b2f54 |
|
||||
| `-[_VZKeyboard sendKeyboardEventsHIDReport:keyboardID:]` | 0x2301b3230 |
|
||||
| `-[VZVirtualMachine(_VZHIDAdditions) _processHIDReports:forDevice:deviceType:]` | 0x2301b2310 |
|
||||
| `-[VZVirtualMachineView _sendKeyEventsToVirtualMachine:]` | -- |
|
||||
| `-[_VZHIDEventMonitor getHIDReportsFromHIDEvent:]` | 0x2301b2af0 |
|
||||
@@ -29,8 +29,8 @@ Dependencies:
|
||||
|
||||
import os
|
||||
import plistlib
|
||||
import re
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from capstone import Cs, CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN
|
||||
@@ -228,10 +228,9 @@ def patch_seputil(filepath):
|
||||
def patch_launchd_cache_loader(filepath):
|
||||
"""NOP the cache validation check in launchd_cache_loader.
|
||||
|
||||
Anchor strategies (in order):
|
||||
1. Search for "unsecure_cache" substring, resolve to full null-terminated
|
||||
string start, find ADRP+ADD xref to it, NOP the nearby cbz/cbnz branch
|
||||
2. Verified known offset fallback
|
||||
Anchor strategy:
|
||||
Search for "unsecure_cache" substring, resolve to full null-terminated
|
||||
string start, find ADRP+ADD xref to it, NOP the nearby cbz/cbnz branch.
|
||||
|
||||
The binary checks boot-arg "launchd_unsecure_cache=" — if not found,
|
||||
it skips the unsecure path via a conditional branch. NOPping that branch
|
||||
@@ -243,7 +242,7 @@ def patch_launchd_cache_loader(filepath):
|
||||
text_sec = find_section(sections, "__TEXT,__text")
|
||||
if not text_sec:
|
||||
print(" [-] __TEXT,__text not found")
|
||||
return _launchd_cache_fallback(filepath, data)
|
||||
return False
|
||||
|
||||
text_va, text_size, text_foff = text_sec
|
||||
|
||||
@@ -318,9 +317,8 @@ def patch_launchd_cache_loader(filepath):
|
||||
print(f" [+] NOPped at 0x{branch_foff:X}")
|
||||
return True
|
||||
|
||||
# Strategy 2: Fallback to verified known offset
|
||||
print(" Dynamic anchor not found, trying verified fallback...")
|
||||
return _launchd_cache_fallback(filepath, data)
|
||||
print(" [-] Dynamic anchor not found — all strategies exhausted")
|
||||
return False
|
||||
|
||||
|
||||
def _find_cstring_start(data, match_off, section_foff):
|
||||
@@ -425,31 +423,6 @@ def _find_nearby_branch(data, ref_foff, text_foff, text_size):
|
||||
return -1
|
||||
|
||||
|
||||
def _launchd_cache_fallback(filepath, data):
|
||||
"""Fallback: verify known offset and NOP."""
|
||||
KNOWN_OFF = 0xB58
|
||||
|
||||
if KNOWN_OFF + 4 > len(data):
|
||||
print(f" [-] Known offset 0x{KNOWN_OFF:X} out of bounds")
|
||||
return False
|
||||
|
||||
insns = disasm_at(data, KNOWN_OFF, 1)
|
||||
if insns:
|
||||
mn = insns[0].mnemonic
|
||||
print(f" Fallback: {mn} {insns[0].op_str} at 0x{KNOWN_OFF:X}")
|
||||
|
||||
# Verify it's a branch-type instruction (expected for this patch)
|
||||
branch_types = {"cbz", "cbnz", "tbz", "tbnz", "b"}
|
||||
if mn not in branch_types and not mn.startswith("b."):
|
||||
print(f" [!] Warning: unexpected instruction type '{mn}' at known offset")
|
||||
print(f" Expected a conditional branch. Proceeding anyway.")
|
||||
|
||||
data[KNOWN_OFF : KNOWN_OFF + 4] = NOP
|
||||
open(filepath, "wb").write(data)
|
||||
print(f" [+] NOPped at 0x{KNOWN_OFF:X} (fallback)")
|
||||
return True
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
# 3. mobileactivationd — Hackivation bypass
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
@@ -461,7 +434,6 @@ def patch_mobileactivationd(filepath):
|
||||
Anchor strategies (in order):
|
||||
1. Search LC_SYMTAB for symbol containing "should_hactivate"
|
||||
2. Parse ObjC metadata: methnames -> selrefs -> method_list -> IMP
|
||||
3. Verified known offset fallback
|
||||
|
||||
The method determines if the device should self-activate (hackivation).
|
||||
Patching it to always return YES bypasses activation lock.
|
||||
@@ -481,15 +453,15 @@ def patch_mobileactivationd(filepath):
|
||||
if imp_foff < 0:
|
||||
imp_foff = _find_via_objc_metadata(data)
|
||||
|
||||
# Strategy 3: Fallback
|
||||
# All dynamic strategies exhausted
|
||||
if imp_foff < 0:
|
||||
print(" Dynamic anchor not found, trying verified fallback...")
|
||||
return _mobileactivationd_fallback(filepath, data)
|
||||
print(" [-] Dynamic anchor not found — all strategies exhausted")
|
||||
return False
|
||||
|
||||
# Verify the target looks like code
|
||||
if imp_foff + 8 > len(data):
|
||||
print(f" [-] IMP offset 0x{imp_foff:X} out of bounds")
|
||||
return _mobileactivationd_fallback(filepath, data)
|
||||
return False
|
||||
|
||||
insns = disasm_at(data, imp_foff, 4)
|
||||
if insns:
|
||||
@@ -602,26 +574,6 @@ def _find_via_objc_metadata(data):
|
||||
return -1
|
||||
|
||||
|
||||
def _mobileactivationd_fallback(filepath, data):
|
||||
"""Fallback: verify known offset and patch."""
|
||||
KNOWN_OFF = 0x2F5F84
|
||||
|
||||
if KNOWN_OFF + 8 > len(data):
|
||||
print(f" [-] Known offset 0x{KNOWN_OFF:X} out of bounds (size: {len(data)})")
|
||||
return False
|
||||
|
||||
insns = disasm_at(data, KNOWN_OFF, 4)
|
||||
if insns:
|
||||
print(f" Fallback: {insns[0].mnemonic} {insns[0].op_str} at 0x{KNOWN_OFF:X}")
|
||||
|
||||
data[KNOWN_OFF : KNOWN_OFF + 4] = MOV_X0_1
|
||||
data[KNOWN_OFF + 4 : KNOWN_OFF + 8] = RET
|
||||
|
||||
open(filepath, "wb").write(data)
|
||||
print(f" [+] Patched at 0x{KNOWN_OFF:X} (fallback): mov x0, #1; ret")
|
||||
return True
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
# BuildManifest parsing
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
@@ -660,7 +612,8 @@ def parse_cryptex_paths(manifest_path):
|
||||
def inject_daemons(plist_path, daemon_dir):
|
||||
"""Inject bash/dropbear/trollvnc entries into launchd.plist."""
|
||||
# Convert to XML first (macOS binary plist -> XML)
|
||||
os.system(f'plutil -convert xml1 "{plist_path}" 2>/dev/null')
|
||||
subprocess.run(["plutil", "-convert", "xml1", plist_path],
|
||||
capture_output=True)
|
||||
|
||||
with open(plist_path, "rb") as f:
|
||||
target = plistlib.load(f)
|
||||
|
||||
@@ -12,13 +12,13 @@ Dependencies: keystone-engine, capstone
|
||||
import struct, plistlib
|
||||
from collections import defaultdict
|
||||
from keystone import Ks, KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN as KS_MODE_LE
|
||||
from capstone import Cs, CS_ARCH_ARM64, CS_MODE_ARM
|
||||
from capstone import Cs, CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN
|
||||
from capstone.arm64_const import (ARM64_OP_REG, ARM64_OP_IMM,
|
||||
ARM64_REG_W0, ARM64_REG_X0, ARM64_REG_X8)
|
||||
|
||||
# ── Assembly / disassembly helpers ───────────────────────────────
|
||||
_ks = Ks(KS_ARCH_ARM64, KS_MODE_LE)
|
||||
_cs = Cs(CS_ARCH_ARM64, CS_MODE_ARM)
|
||||
_cs = Cs(CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN)
|
||||
_cs.detail = True
|
||||
|
||||
|
||||
|
||||
@@ -24,17 +24,14 @@ import glob
|
||||
import os
|
||||
import plistlib
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
# Ensure sibling modules (patch_firmware) are importable when run from any CWD
|
||||
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
if _SCRIPT_DIR not in sys.path:
|
||||
sys.path.insert(0, _SCRIPT_DIR)
|
||||
|
||||
from keystone import Ks, KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN as KS_MODE_LE
|
||||
from pyimg4 import IM4P
|
||||
|
||||
from fw_patch import (
|
||||
@@ -46,26 +43,6 @@ from fw_patch import (
|
||||
)
|
||||
from patchers.iboot import IBootPatcher
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
# ARM64 assembler
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
|
||||
_ks = Ks(KS_ARCH_ARM64, KS_MODE_LE)
|
||||
|
||||
|
||||
def asm(s, addr=0):
|
||||
"""Assemble an ARM64 instruction string to bytes."""
|
||||
enc, _ = _ks.asm(s, addr)
|
||||
if not enc:
|
||||
raise RuntimeError(f"asm failed: {s}")
|
||||
return bytes(enc)
|
||||
|
||||
|
||||
def asm_u32(s, addr=0):
|
||||
"""Assemble an ARM64 instruction and return as little-endian u32."""
|
||||
return struct.unpack("<I", asm(s, addr))[0]
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
# Configuration
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
@@ -74,11 +51,6 @@ OUTPUT_DIR = "Ramdisk"
|
||||
TEMP_DIR = "ramdisk_builder_temp"
|
||||
INPUT_DIR = "ramdisk_input"
|
||||
|
||||
# Default location to copy resources from
|
||||
CFW_DIR = os.path.expanduser(
|
||||
"~/Documents/GitHub/super-tart-vphone-private/CFW"
|
||||
)
|
||||
|
||||
# Ramdisk boot-args
|
||||
RAMDISK_BOOT_ARGS = b"serial=3 rd=md0 debug=0x2014e -v wdt=-1 %s"
|
||||
|
||||
@@ -298,7 +270,9 @@ def build_ramdisk(restore_dir, im4m_path, vm_dir, input_dir, output_dir, temp_di
|
||||
for pattern in SIGN_DIRS:
|
||||
for path in glob.glob(os.path.join(mountpoint, pattern)):
|
||||
if os.path.isfile(path) and not os.path.islink(path):
|
||||
if "Mach-O" in subprocess.getoutput(f'file "{path}"'):
|
||||
if "Mach-O" in subprocess.run(
|
||||
["file", path], capture_output=True, text=True,
|
||||
).stdout:
|
||||
subprocess.run(
|
||||
[ldid, "-S", "-M", f"-K{signcert}", path],
|
||||
capture_output=True,
|
||||
|
||||
103
sources/vphone-cli/VPhoneAppDelegate.swift
Normal file
103
sources/vphone-cli/VPhoneAppDelegate.swift
Normal file
@@ -0,0 +1,103 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
import Virtualization
|
||||
|
||||
class VPhoneAppDelegate: NSObject, NSApplicationDelegate {
|
||||
private let cli: VPhoneCLI
|
||||
private var vm: VPhoneVM?
|
||||
private var windowController: VPhoneWindowController?
|
||||
private var menuController: VPhoneMenuController?
|
||||
private var sigintSource: DispatchSourceSignal?
|
||||
|
||||
init(cli: VPhoneCLI) {
|
||||
self.cli = cli
|
||||
super.init()
|
||||
}
|
||||
|
||||
func applicationDidFinishLaunching(_: Notification) {
|
||||
NSApp.setActivationPolicy(cli.noGraphics ? .prohibited : .regular)
|
||||
|
||||
signal(SIGINT, SIG_IGN)
|
||||
let src = DispatchSource.makeSignalSource(signal: SIGINT, queue: .main)
|
||||
src.setEventHandler {
|
||||
print("\n[vphone] SIGINT — shutting down")
|
||||
NSApp.terminate(nil)
|
||||
}
|
||||
src.activate()
|
||||
sigintSource = src
|
||||
|
||||
Task { @MainActor in
|
||||
do {
|
||||
try await self.startVM()
|
||||
} catch {
|
||||
print("[vphone] Fatal: \(error)")
|
||||
NSApp.terminate(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func startVM() async throws {
|
||||
let romURL = URL(fileURLWithPath: cli.rom)
|
||||
guard FileManager.default.fileExists(atPath: romURL.path) else {
|
||||
throw VPhoneError.romNotFound(cli.rom)
|
||||
}
|
||||
|
||||
let diskURL = URL(fileURLWithPath: cli.disk)
|
||||
let nvramURL = URL(fileURLWithPath: cli.nvram)
|
||||
let machineIDURL = URL(fileURLWithPath: cli.machineId)
|
||||
let sepStorageURL = URL(fileURLWithPath: cli.sepStorage)
|
||||
let sepRomURL = URL(fileURLWithPath: cli.sepRom)
|
||||
|
||||
print("=== vphone-cli ===")
|
||||
print("ROM : \(cli.rom)")
|
||||
print("Disk : \(cli.disk)")
|
||||
print("NVRAM : \(cli.nvram)")
|
||||
print("MachID: \(cli.machineId)")
|
||||
print("CPU : \(cli.cpu)")
|
||||
print("Memory: \(cli.memory) MB")
|
||||
print("Screen: \(cli.screenWidth)x\(cli.screenHeight) @ \(cli.screenPpi) PPI (scale \(cli.screenScale)x)")
|
||||
print("SEP : enabled")
|
||||
print(" storage: \(cli.sepStorage)")
|
||||
print(" rom : \(cli.sepRom)")
|
||||
print("")
|
||||
|
||||
let options = VPhoneVM.Options(
|
||||
romURL: romURL,
|
||||
nvramURL: nvramURL,
|
||||
machineIDURL: machineIDURL,
|
||||
diskURL: diskURL,
|
||||
cpuCount: cli.cpu,
|
||||
memorySize: UInt64(cli.memory) * 1024 * 1024,
|
||||
sepStorageURL: sepStorageURL,
|
||||
sepRomURL: sepRomURL,
|
||||
screenWidth: cli.screenWidth,
|
||||
screenHeight: cli.screenHeight,
|
||||
screenPPI: cli.screenPpi,
|
||||
screenScale: cli.screenScale
|
||||
)
|
||||
|
||||
let vm = try VPhoneVM(options: options)
|
||||
self.vm = vm
|
||||
|
||||
try await vm.start(forceDFU: cli.dfu)
|
||||
|
||||
if !cli.noGraphics {
|
||||
let keyHelper = VPhoneKeyHelper(vm: vm.virtualMachine)
|
||||
let wc = VPhoneWindowController()
|
||||
wc.showWindow(
|
||||
for: vm.virtualMachine,
|
||||
screenWidth: cli.screenWidth,
|
||||
screenHeight: cli.screenHeight,
|
||||
screenScale: cli.screenScale,
|
||||
keyHelper: keyHelper
|
||||
)
|
||||
windowController = wc
|
||||
menuController = VPhoneMenuController(keyHelper: keyHelper)
|
||||
}
|
||||
}
|
||||
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool {
|
||||
!cli.noGraphics
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
import AppKit
|
||||
import ArgumentParser
|
||||
import Foundation
|
||||
import Virtualization
|
||||
|
||||
@main
|
||||
struct VPhoneCLI: AsyncParsableCommand {
|
||||
static var configuration = CommandConfiguration(
|
||||
struct VPhoneCLI: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "vphone-cli",
|
||||
abstract: "Boot a virtual iPhone (PV=3)",
|
||||
discussion: """
|
||||
@@ -31,100 +28,40 @@ struct VPhoneCLI: AsyncParsableCommand {
|
||||
@Option(help: "Path to NVRAM storage (created/overwritten)")
|
||||
var nvram: String = "nvram.bin"
|
||||
|
||||
@Option(help: "Path to machineIdentifier file (created if missing)")
|
||||
var machineId: String
|
||||
|
||||
@Option(help: "Number of CPU cores")
|
||||
var cpu: Int = 4
|
||||
var cpu: Int = 8
|
||||
|
||||
@Option(help: "Memory size in MB")
|
||||
var memory: Int = 4096
|
||||
|
||||
@Option(help: "Path to write serial console log file")
|
||||
var serialLog: String? = nil
|
||||
|
||||
@Flag(help: "Stop VM on guest panic")
|
||||
var stopOnPanic: Bool = false
|
||||
|
||||
@Flag(help: "Stop VM on fatal error")
|
||||
var stopOnFatalError: Bool = false
|
||||
|
||||
@Flag(help: "Skip SEP coprocessor setup")
|
||||
var skipSep: Bool = false
|
||||
var memory: Int = 8192
|
||||
|
||||
@Option(help: "Path to SEP storage file (created if missing)")
|
||||
var sepStorage: String? = nil
|
||||
var sepStorage: String
|
||||
|
||||
@Option(help: "Path to SEP ROM binary")
|
||||
var sepRom: String? = nil
|
||||
var sepRom: String
|
||||
|
||||
@Flag(help: "Boot into DFU mode")
|
||||
var dfu: Bool = false
|
||||
|
||||
@Option(help: "Display width in pixels (default: 1290)")
|
||||
var screenWidth: Int = 1290
|
||||
|
||||
@Option(help: "Display height in pixels (default: 2796)")
|
||||
var screenHeight: Int = 2796
|
||||
|
||||
@Option(help: "Display pixels per inch (default: 460)")
|
||||
var screenPpi: Int = 460
|
||||
|
||||
@Option(help: "Window scale divisor (default: 3.0)")
|
||||
var screenScale: Double = 3.0
|
||||
|
||||
@Flag(help: "Run without GUI (headless)")
|
||||
var noGraphics: Bool = false
|
||||
|
||||
@MainActor
|
||||
mutating func run() async throws {
|
||||
let romURL = URL(fileURLWithPath: rom)
|
||||
guard FileManager.default.fileExists(atPath: romURL.path) else {
|
||||
throw VPhoneError.romNotFound(rom)
|
||||
}
|
||||
|
||||
let diskURL = URL(fileURLWithPath: disk)
|
||||
let nvramURL = URL(fileURLWithPath: nvram)
|
||||
|
||||
print("=== vphone-cli ===")
|
||||
print("ROM : \(rom)")
|
||||
print("Disk : \(disk)")
|
||||
print("NVRAM : \(nvram)")
|
||||
print("CPU : \(cpu)")
|
||||
print("Memory: \(memory) MB")
|
||||
let sepStorageURL = sepStorage.map { URL(fileURLWithPath: $0) }
|
||||
let sepRomURL = sepRom.map { URL(fileURLWithPath: $0) }
|
||||
|
||||
print("SEP : \(skipSep ? "skipped" : "enabled")")
|
||||
if !skipSep {
|
||||
print(" storage: \(sepStorage ?? "(auto)")")
|
||||
if let r = sepRom { print(" rom : \(r)") }
|
||||
}
|
||||
print("")
|
||||
|
||||
let options = VPhoneVM.Options(
|
||||
romURL: romURL,
|
||||
nvramURL: nvramURL,
|
||||
diskURL: diskURL,
|
||||
cpuCount: cpu,
|
||||
memorySize: UInt64(memory) * 1024 * 1024,
|
||||
skipSEP: skipSep,
|
||||
sepStorageURL: sepStorageURL,
|
||||
sepRomURL: sepRomURL,
|
||||
serialLogPath: serialLog,
|
||||
stopOnPanic: stopOnPanic,
|
||||
stopOnFatalError: stopOnFatalError
|
||||
)
|
||||
|
||||
let vm = try VPhoneVM(options: options)
|
||||
|
||||
// Handle Ctrl+C
|
||||
signal(SIGINT, SIG_IGN)
|
||||
let sigintSrc = DispatchSource.makeSignalSource(signal: SIGINT)
|
||||
sigintSrc.setEventHandler {
|
||||
print("\n[vphone] SIGINT — shutting down")
|
||||
vm.stopConsoleCapture()
|
||||
Foundation.exit(0)
|
||||
}
|
||||
sigintSrc.activate()
|
||||
|
||||
// Start VM
|
||||
try await vm.start(forceDFU: dfu, stopOnPanic: stopOnPanic, stopOnFatalError: stopOnFatalError)
|
||||
|
||||
if noGraphics {
|
||||
// Headless: just wait
|
||||
NSApplication.shared.setActivationPolicy(.prohibited)
|
||||
await vm.waitUntilStopped()
|
||||
} else {
|
||||
// GUI: show VM window with touch support
|
||||
let windowController = VPhoneWindowController()
|
||||
windowController.showWindow(for: vm.virtualMachine)
|
||||
await vm.waitUntilStopped()
|
||||
}
|
||||
}
|
||||
/// Execution is driven by VPhoneAppDelegate; main.swift calls parseOrExit()
|
||||
/// and hands the parsed options to the delegate.
|
||||
mutating func run() throws {}
|
||||
}
|
||||
|
||||
24
sources/vphone-cli/VPhoneError.swift
Normal file
24
sources/vphone-cli/VPhoneError.swift
Normal file
@@ -0,0 +1,24 @@
|
||||
import Foundation
|
||||
|
||||
enum VPhoneError: Error, CustomStringConvertible {
|
||||
case hardwareModelNotSupported
|
||||
case romNotFound(String)
|
||||
case diskNotFound(String)
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .hardwareModelNotSupported:
|
||||
"""
|
||||
PV=3 hardware model not supported. Check:
|
||||
1. macOS >= 15.0 (Sequoia)
|
||||
2. Signed with com.apple.private.virtualization + \
|
||||
com.apple.private.virtualization.security-research
|
||||
3. SIP/AMFI disabled
|
||||
"""
|
||||
case let .romNotFound(p):
|
||||
"ROM not found: \(p)"
|
||||
case let .diskNotFound(p):
|
||||
"Disk image not found: \(p)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import Dynamic
|
||||
import Foundation
|
||||
import Virtualization
|
||||
import VPhoneObjC
|
||||
|
||||
/// Wrapper around the ObjC private API call to create a PV=3 hardware model.
|
||||
/// Creates a PV=3 hardware model via private _VZMacHardwareModelDescriptor.
|
||||
///
|
||||
/// The Virtualization.framework checks:
|
||||
/// default_configuration_for_platform_version(3) validity byte =
|
||||
@@ -13,9 +13,17 @@ import VPhoneObjC
|
||||
/// Minimum host OS for PV=3: macOS 15.0 (Sequoia)
|
||||
///
|
||||
enum VPhoneHardware {
|
||||
/// Create a PV=3 VZMacHardwareModel. Throws if isSupported is false.
|
||||
static func createModel() throws -> VZMacHardwareModel {
|
||||
let model = VPhoneCreateHardwareModel()
|
||||
// platformVersion=3, boardID=0x90, ISA=2 matches vresearch101
|
||||
let desc = Dynamic._VZMacHardwareModelDescriptor()
|
||||
desc.setPlatformVersion(NSNumber(value: UInt32(3)))
|
||||
desc.setBoardID(NSNumber(value: UInt32(0x90)))
|
||||
desc.setISA(NSNumber(value: Int64(2)))
|
||||
|
||||
let model = Dynamic.VZMacHardwareModel
|
||||
._hardwareModelWithDescriptor(desc.asObject)
|
||||
.asObject as! VZMacHardwareModel
|
||||
|
||||
guard model.isSupported else {
|
||||
throw VPhoneError.hardwareModelNotSupported
|
||||
}
|
||||
|
||||
273
sources/vphone-cli/VPhoneKeyHelper.swift
Normal file
273
sources/vphone-cli/VPhoneKeyHelper.swift
Normal file
@@ -0,0 +1,273 @@
|
||||
import AppKit
|
||||
import Dynamic
|
||||
import Foundation
|
||||
import Virtualization
|
||||
|
||||
// MARK: - Key Helper
|
||||
|
||||
@MainActor
|
||||
class VPhoneKeyHelper {
|
||||
private let vm: VZVirtualMachine
|
||||
|
||||
/// First _VZKeyboard from the VM's internal keyboard array.
|
||||
private var firstKeyboard: AnyObject? {
|
||||
guard let arr = Dynamic(vm)._keyboards.asObject as? NSArray, arr.count > 0 else { return nil }
|
||||
return arr.object(at: 0) as AnyObject
|
||||
}
|
||||
|
||||
/// Get _deviceIdentifier from _VZKeyboard via KVC (it's an ivar, not a property).
|
||||
private func keyboardDeviceId(_ keyboard: AnyObject) -> UInt32 {
|
||||
if let obj = keyboard as? NSObject,
|
||||
let val = obj.value(forKey: "_deviceIdentifier") as? UInt32
|
||||
{
|
||||
return val
|
||||
}
|
||||
print("[keys] WARNING: Could not read _deviceIdentifier, defaulting to 1")
|
||||
return 1
|
||||
}
|
||||
|
||||
init(vm: VZVirtualMachine) {
|
||||
self.vm = vm
|
||||
}
|
||||
|
||||
// MARK: - Send Key via _VZKeyEvent
|
||||
|
||||
/// Send key down + up through _VZKeyEvent → _VZKeyboard.sendKeyEvents: pipeline.
|
||||
private func sendKeyPress(keyCode: UInt16) {
|
||||
guard let keyboard = firstKeyboard else {
|
||||
print("[keys] No keyboard found")
|
||||
return
|
||||
}
|
||||
|
||||
let down = Dynamic._VZKeyEvent(type: 0, keyCode: keyCode)
|
||||
let up = Dynamic._VZKeyEvent(type: 1, keyCode: keyCode)
|
||||
|
||||
guard let downObj = down.asAnyObject, let upObj = up.asAnyObject else {
|
||||
print("[keys] Failed to create _VZKeyEvent")
|
||||
return
|
||||
}
|
||||
|
||||
Dynamic(keyboard).sendKeyEvents([downObj, upObj] as NSArray)
|
||||
print("[keys] Sent VK 0x\(String(keyCode, radix: 16)) (down+up)")
|
||||
}
|
||||
|
||||
// MARK: - Fn+Key Combos (iOS Full Keyboard Access)
|
||||
|
||||
/// Send modifier+key combo via _VZKeyEvent (mod down → key down → key up → mod up).
|
||||
private func sendVKCombo(modifierVK: UInt16, keyVK: UInt16) {
|
||||
guard let keyboard = firstKeyboard else {
|
||||
print("[keys] No keyboard found")
|
||||
return
|
||||
}
|
||||
|
||||
var events: [AnyObject] = []
|
||||
if let obj = Dynamic._VZKeyEvent(type: 0, keyCode: modifierVK).asAnyObject { events.append(obj) }
|
||||
if let obj = Dynamic._VZKeyEvent(type: 0, keyCode: keyVK).asAnyObject { events.append(obj) }
|
||||
if let obj = Dynamic._VZKeyEvent(type: 1, keyCode: keyVK).asAnyObject { events.append(obj) }
|
||||
if let obj = Dynamic._VZKeyEvent(type: 1, keyCode: modifierVK).asAnyObject { events.append(obj) }
|
||||
|
||||
print("[keys] events: \(events)")
|
||||
Dynamic(keyboard).sendKeyEvents(events as NSArray)
|
||||
print("[keys] VK combo: 0x\(String(modifierVK, radix: 16))+0x\(String(keyVK, radix: 16))")
|
||||
}
|
||||
|
||||
// MARK: - Vector Injection (for keys with no VK code)
|
||||
|
||||
/// Bypass _VZKeyEvent by calling sendKeyboardEvents:keyboardID: directly
|
||||
/// with a crafted std::vector<uint64_t>. Packed: (intermediate_index << 32) | is_key_down.
|
||||
private func sendRawKeyPress(index: UInt64) {
|
||||
guard let keyboard = firstKeyboard else {
|
||||
print("[keys] No keyboard found")
|
||||
return
|
||||
}
|
||||
let deviceId = keyboardDeviceId(keyboard)
|
||||
|
||||
sendRawKeyEvent(index: index, isKeyDown: true, deviceId: deviceId)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [self] in
|
||||
sendRawKeyEvent(index: index, isKeyDown: false, deviceId: deviceId)
|
||||
}
|
||||
}
|
||||
|
||||
private func sendRawKeyEvent(index: UInt64, isKeyDown: Bool, deviceId: UInt32) {
|
||||
let packed = (index << 32) | (isKeyDown ? 1 : 0)
|
||||
|
||||
let data = UnsafeMutablePointer<UInt64>.allocate(capacity: 1)
|
||||
defer { data.deallocate() }
|
||||
data.pointee = packed
|
||||
|
||||
var vec = (data, data.advanced(by: 1), data.advanced(by: 1))
|
||||
withUnsafeMutablePointer(to: &vec) { vecPtr in
|
||||
_ = Dynamic(vm).sendKeyboardEvents(UnsafeMutableRawPointer(vecPtr), keyboardID: deviceId)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Named Key Actions
|
||||
|
||||
/// iOS hardware keyboard shortcuts (Cmd-based, Fn has no table entry in _VZKeyEvent)
|
||||
func sendHome() {
|
||||
sendVKCombo(modifierVK: 0x37, keyVK: 0x04)
|
||||
} // Cmd+H → Home Screen
|
||||
func sendSpotlight() {
|
||||
sendVKCombo(modifierVK: 0x37, keyVK: 0x31)
|
||||
} // Cmd+Space → Spotlight
|
||||
|
||||
/// Standard keyboard keys
|
||||
func sendReturn() {
|
||||
sendKeyPress(keyCode: 0x24)
|
||||
}
|
||||
|
||||
func sendEscape() {
|
||||
sendKeyPress(keyCode: 0x35)
|
||||
}
|
||||
|
||||
func sendSpace() {
|
||||
sendKeyPress(keyCode: 0x31)
|
||||
}
|
||||
|
||||
func sendTab() {
|
||||
sendKeyPress(keyCode: 0x30)
|
||||
}
|
||||
|
||||
func sendDeleteKey() {
|
||||
sendKeyPress(keyCode: 0x33)
|
||||
}
|
||||
|
||||
func sendArrowUp() {
|
||||
sendKeyPress(keyCode: 0x7E)
|
||||
}
|
||||
|
||||
func sendArrowDown() {
|
||||
sendKeyPress(keyCode: 0x7D)
|
||||
}
|
||||
|
||||
func sendArrowLeft() {
|
||||
sendKeyPress(keyCode: 0x7B)
|
||||
}
|
||||
|
||||
func sendArrowRight() {
|
||||
sendKeyPress(keyCode: 0x7C)
|
||||
}
|
||||
|
||||
func sendShift() {
|
||||
sendKeyPress(keyCode: 0x38)
|
||||
}
|
||||
|
||||
func sendCommand() {
|
||||
sendKeyPress(keyCode: 0x37)
|
||||
}
|
||||
|
||||
/// Volume (Apple VK codes)
|
||||
func sendVolumeUp() {
|
||||
sendKeyPress(keyCode: 0x48)
|
||||
}
|
||||
|
||||
func sendVolumeDown() {
|
||||
sendKeyPress(keyCode: 0x49)
|
||||
}
|
||||
|
||||
/// Power — no VK code, use vector injection (intermediate index 0x72 = System Wake)
|
||||
func sendPower() {
|
||||
sendRawKeyPress(index: 0x72)
|
||||
}
|
||||
|
||||
// MARK: - Type ASCII from Clipboard
|
||||
|
||||
func typeFromClipboard() {
|
||||
guard let string = NSPasteboard.general.string(forType: .string) else {
|
||||
print("[keys] Clipboard has no string")
|
||||
return
|
||||
}
|
||||
print("[keys] Typing \(string.count) characters from clipboard")
|
||||
typeString(string)
|
||||
}
|
||||
|
||||
func typeString(_ string: String) {
|
||||
guard let keyboard = firstKeyboard else {
|
||||
print("[keys] No keyboard found")
|
||||
return
|
||||
}
|
||||
|
||||
var delay: TimeInterval = 0
|
||||
let interval: TimeInterval = 0.02
|
||||
|
||||
for char in string {
|
||||
guard let (keyCode, needsShift) = asciiToVK(char) else {
|
||||
print("[keys] Skipping unsupported char: '\(char)'")
|
||||
continue
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
||||
var events: [AnyObject] = []
|
||||
if needsShift {
|
||||
if let obj = Dynamic._VZKeyEvent(type: 0, keyCode: UInt16(0x38)).asAnyObject { events.append(obj) }
|
||||
}
|
||||
if let obj = Dynamic._VZKeyEvent(type: 0, keyCode: keyCode).asAnyObject { events.append(obj) }
|
||||
if let obj = Dynamic._VZKeyEvent(type: 1, keyCode: keyCode).asAnyObject { events.append(obj) }
|
||||
if needsShift {
|
||||
if let obj = Dynamic._VZKeyEvent(type: 1, keyCode: UInt16(0x38)).asAnyObject { events.append(obj) }
|
||||
}
|
||||
Dynamic(keyboard).sendKeyEvents(events as NSArray)
|
||||
}
|
||||
|
||||
delay += interval
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ASCII → Apple VK Code (US Layout)
|
||||
|
||||
private func asciiToVK(_ char: Character) -> (UInt16, Bool)? {
|
||||
switch char {
|
||||
case "a": (0x00, false) case "b": (0x0B, false)
|
||||
case "c": (0x08, false) case "d": (0x02, false)
|
||||
case "e": (0x0E, false) case "f": (0x03, false)
|
||||
case "g": (0x05, false) case "h": (0x04, false)
|
||||
case "i": (0x22, false) case "j": (0x26, false)
|
||||
case "k": (0x28, false) case "l": (0x25, false)
|
||||
case "m": (0x2E, false) case "n": (0x2D, false)
|
||||
case "o": (0x1F, false) case "p": (0x23, false)
|
||||
case "q": (0x0C, false) case "r": (0x0F, false)
|
||||
case "s": (0x01, false) case "t": (0x11, false)
|
||||
case "u": (0x20, false) case "v": (0x09, false)
|
||||
case "w": (0x0D, false) case "x": (0x07, false)
|
||||
case "y": (0x10, false) case "z": (0x06, false)
|
||||
case "A": (0x00, true) case "B": (0x0B, true)
|
||||
case "C": (0x08, true) case "D": (0x02, true)
|
||||
case "E": (0x0E, true) case "F": (0x03, true)
|
||||
case "G": (0x05, true) case "H": (0x04, true)
|
||||
case "I": (0x22, true) case "J": (0x26, true)
|
||||
case "K": (0x28, true) case "L": (0x25, true)
|
||||
case "M": (0x2E, true) case "N": (0x2D, true)
|
||||
case "O": (0x1F, true) case "P": (0x23, true)
|
||||
case "Q": (0x0C, true) case "R": (0x0F, true)
|
||||
case "S": (0x01, true) case "T": (0x11, true)
|
||||
case "U": (0x20, true) case "V": (0x09, true)
|
||||
case "W": (0x0D, true) case "X": (0x07, true)
|
||||
case "Y": (0x10, true) case "Z": (0x06, true)
|
||||
case "0": (0x1D, false) case "1": (0x12, false)
|
||||
case "2": (0x13, false) case "3": (0x14, false)
|
||||
case "4": (0x15, false) case "5": (0x17, false)
|
||||
case "6": (0x16, false) case "7": (0x1A, false)
|
||||
case "8": (0x1C, false) case "9": (0x19, false)
|
||||
case "-": (0x1B, false) case "=": (0x18, false)
|
||||
case "[": (0x21, false) case "]": (0x1E, false)
|
||||
case "\\": (0x2A, false) case ";": (0x29, false)
|
||||
case "'": (0x27, false) case ",": (0x2B, false)
|
||||
case ".": (0x2F, false) case "/": (0x2C, false)
|
||||
case "`": (0x32, false)
|
||||
case "!": (0x12, true) case "@": (0x13, true)
|
||||
case "#": (0x14, true) case "$": (0x15, true)
|
||||
case "%": (0x17, true) case "^": (0x16, true)
|
||||
case "&": (0x1A, true) case "*": (0x1C, true)
|
||||
case "(": (0x19, true) case ")": (0x1D, true)
|
||||
case "_": (0x1B, true) case "+": (0x18, true)
|
||||
case "{": (0x21, true) case "}": (0x1E, true)
|
||||
case "|": (0x2A, true) case ":": (0x29, true)
|
||||
case "\"": (0x27, true) case "<": (0x2B, true)
|
||||
case ">": (0x2F, true) case "?": (0x2C, true)
|
||||
case "~": (0x32, true)
|
||||
case " ": (0x31, false) case "\t": (0x30, false)
|
||||
case "\n": (0x24, false) case "\r": (0x24, false)
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
}
|
||||
141
sources/vphone-cli/VPhoneMenuController.swift
Normal file
141
sources/vphone-cli/VPhoneMenuController.swift
Normal file
@@ -0,0 +1,141 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
// MARK: - Menu Controller
|
||||
|
||||
@MainActor
|
||||
class VPhoneMenuController {
|
||||
private let keyHelper: VPhoneKeyHelper
|
||||
|
||||
init(keyHelper: VPhoneKeyHelper) {
|
||||
self.keyHelper = keyHelper
|
||||
setupMenuBar()
|
||||
}
|
||||
|
||||
// MARK: - Menu Bar Setup
|
||||
|
||||
private func setupMenuBar() {
|
||||
let mainMenu = NSMenu()
|
||||
|
||||
// App menu
|
||||
let appMenuItem = NSMenuItem()
|
||||
let appMenu = NSMenu(title: "vphone")
|
||||
appMenu.addItem(withTitle: "Quit vphone", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
|
||||
appMenuItem.submenu = appMenu
|
||||
mainMenu.addItem(appMenuItem)
|
||||
|
||||
// Keys menu — NO key equivalents to avoid intercepting VM keyboard input
|
||||
let keysMenuItem = NSMenuItem()
|
||||
let keysMenu = NSMenu(title: "Keys")
|
||||
|
||||
// iOS hardware keyboard shortcuts
|
||||
keysMenu.addItem(makeItem("Home Screen (Cmd+H)", action: #selector(sendHome)))
|
||||
keysMenu.addItem(makeItem("Spotlight (Cmd+Space)", action: #selector(sendSpotlight)))
|
||||
keysMenu.addItem(NSMenuItem.separator())
|
||||
keysMenu.addItem(makeItem("Return", action: #selector(sendReturn)))
|
||||
keysMenu.addItem(makeItem("Escape", action: #selector(sendEscape)))
|
||||
keysMenu.addItem(makeItem("Space", action: #selector(sendSpace)))
|
||||
keysMenu.addItem(makeItem("Tab", action: #selector(sendTab)))
|
||||
keysMenu.addItem(makeItem("Delete", action: #selector(sendDeleteKey)))
|
||||
keysMenu.addItem(NSMenuItem.separator())
|
||||
keysMenu.addItem(makeItem("Arrow Up", action: #selector(sendArrowUp)))
|
||||
keysMenu.addItem(makeItem("Arrow Down", action: #selector(sendArrowDown)))
|
||||
keysMenu.addItem(makeItem("Arrow Left", action: #selector(sendArrowLeft)))
|
||||
keysMenu.addItem(makeItem("Arrow Right", action: #selector(sendArrowRight)))
|
||||
keysMenu.addItem(NSMenuItem.separator())
|
||||
keysMenu.addItem(makeItem("Power", action: #selector(sendPower)))
|
||||
keysMenu.addItem(makeItem("Volume Up", action: #selector(sendVolumeUp)))
|
||||
keysMenu.addItem(makeItem("Volume Down", action: #selector(sendVolumeDown)))
|
||||
keysMenu.addItem(NSMenuItem.separator())
|
||||
keysMenu.addItem(makeItem("Shift (tap)", action: #selector(sendShift)))
|
||||
keysMenu.addItem(makeItem("Command (tap)", action: #selector(sendCommand)))
|
||||
|
||||
keysMenuItem.submenu = keysMenu
|
||||
mainMenu.addItem(keysMenuItem)
|
||||
|
||||
// Type menu
|
||||
let typeMenuItem = NSMenuItem()
|
||||
let typeMenu = NSMenu(title: "Type")
|
||||
typeMenu.addItem(makeItem("Type ASCII from Clipboard", action: #selector(typeFromClipboard)))
|
||||
typeMenuItem.submenu = typeMenu
|
||||
mainMenu.addItem(typeMenuItem)
|
||||
|
||||
NSApp.mainMenu = mainMenu
|
||||
}
|
||||
|
||||
private func makeItem(_ title: String, action: Selector) -> NSMenuItem {
|
||||
let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
|
||||
item.target = self
|
||||
return item
|
||||
}
|
||||
|
||||
// MARK: - Menu Actions (delegate to helper)
|
||||
|
||||
@objc private func sendHome() {
|
||||
keyHelper.sendHome()
|
||||
}
|
||||
|
||||
@objc private func sendSpotlight() {
|
||||
keyHelper.sendSpotlight()
|
||||
}
|
||||
|
||||
@objc private func sendReturn() {
|
||||
keyHelper.sendReturn()
|
||||
}
|
||||
|
||||
@objc private func sendEscape() {
|
||||
keyHelper.sendEscape()
|
||||
}
|
||||
|
||||
@objc private func sendSpace() {
|
||||
keyHelper.sendSpace()
|
||||
}
|
||||
|
||||
@objc private func sendTab() {
|
||||
keyHelper.sendTab()
|
||||
}
|
||||
|
||||
@objc private func sendDeleteKey() {
|
||||
keyHelper.sendDeleteKey()
|
||||
}
|
||||
|
||||
@objc private func sendArrowUp() {
|
||||
keyHelper.sendArrowUp()
|
||||
}
|
||||
|
||||
@objc private func sendArrowDown() {
|
||||
keyHelper.sendArrowDown()
|
||||
}
|
||||
|
||||
@objc private func sendArrowLeft() {
|
||||
keyHelper.sendArrowLeft()
|
||||
}
|
||||
|
||||
@objc private func sendArrowRight() {
|
||||
keyHelper.sendArrowRight()
|
||||
}
|
||||
|
||||
@objc private func sendPower() {
|
||||
keyHelper.sendPower()
|
||||
}
|
||||
|
||||
@objc private func sendVolumeUp() {
|
||||
keyHelper.sendVolumeUp()
|
||||
}
|
||||
|
||||
@objc private func sendVolumeDown() {
|
||||
keyHelper.sendVolumeDown()
|
||||
}
|
||||
|
||||
@objc private func sendShift() {
|
||||
keyHelper.sendShift()
|
||||
}
|
||||
|
||||
@objc private func sendCommand() {
|
||||
keyHelper.sendCommand()
|
||||
}
|
||||
|
||||
@objc private func typeFromClipboard() {
|
||||
keyHelper.typeFromClipboard()
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,27 @@
|
||||
import Dynamic
|
||||
import Foundation
|
||||
import Virtualization
|
||||
import VPhoneObjC
|
||||
|
||||
/// Minimal VM for booting a vphone (virtual iPhone) in DFU mode.
|
||||
@MainActor
|
||||
class VPhoneVM: NSObject, VZVirtualMachineDelegate {
|
||||
let virtualMachine: VZVirtualMachine
|
||||
private var done = false
|
||||
|
||||
struct Options {
|
||||
var romURL: URL
|
||||
var nvramURL: URL
|
||||
var machineIDURL: URL
|
||||
var diskURL: URL
|
||||
var cpuCount: Int = 4
|
||||
var memorySize: UInt64 = 4 * 1024 * 1024 * 1024
|
||||
var skipSEP: Bool = true
|
||||
var sepStorageURL: URL?
|
||||
var sepRomURL: URL?
|
||||
var serialLogPath: String? = nil
|
||||
var stopOnPanic: Bool = false
|
||||
var stopOnFatalError: Bool = false
|
||||
var cpuCount: Int = 8
|
||||
var memorySize: UInt64 = 8 * 1024 * 1024 * 1024
|
||||
var sepStorageURL: URL
|
||||
var sepRomURL: URL
|
||||
var screenWidth: Int = 1290
|
||||
var screenHeight: Int = 2796
|
||||
var screenPPI: Int = 460
|
||||
var screenScale: Double = 3.0
|
||||
}
|
||||
|
||||
private var consoleLogFileHandle: FileHandle?
|
||||
|
||||
init(options: Options) throws {
|
||||
// --- Hardware model (PV=3) ---
|
||||
let hwModel = try VPhoneHardware.createModel()
|
||||
@@ -31,18 +30,17 @@ class VPhoneVM: NSObject, VZVirtualMachineDelegate {
|
||||
// --- Platform ---
|
||||
let platform = VZMacPlatformConfiguration()
|
||||
|
||||
// Persist machineIdentifier for stable ECID (same as vrevm)
|
||||
let machineIDPath = options.nvramURL.deletingLastPathComponent()
|
||||
.appendingPathComponent("machineIdentifier.bin")
|
||||
if let savedData = try? Data(contentsOf: machineIDPath),
|
||||
let savedID = VZMacMachineIdentifier(dataRepresentation: savedData) {
|
||||
// Persist machineIdentifier for stable ECID
|
||||
if let savedData = try? Data(contentsOf: options.machineIDURL),
|
||||
let savedID = VZMacMachineIdentifier(dataRepresentation: savedData)
|
||||
{
|
||||
platform.machineIdentifier = savedID
|
||||
print("[vphone] Loaded machineIdentifier (ECID stable)")
|
||||
} else {
|
||||
let newID = VZMacMachineIdentifier()
|
||||
platform.machineIdentifier = newID
|
||||
try newID.dataRepresentation.write(to: machineIDPath)
|
||||
print("[vphone] Created new machineIdentifier -> \(machineIDPath.lastPathComponent)")
|
||||
try newID.dataRepresentation.write(to: options.machineIDURL)
|
||||
print("[vphone] Created new machineIdentifier -> \(options.machineIDURL.lastPathComponent)")
|
||||
}
|
||||
|
||||
let auxStorage = try VZMacAuxiliaryStorage(
|
||||
@@ -52,19 +50,19 @@ class VPhoneVM: NSObject, VZVirtualMachineDelegate {
|
||||
)
|
||||
platform.auxiliaryStorage = auxStorage
|
||||
platform.hardwareModel = hwModel
|
||||
// platformFusing = prod (same as vrevm config)
|
||||
|
||||
// Set NVRAM boot-args to enable serial output (same as vrevm restore)
|
||||
// Set NVRAM boot-args to enable serial output
|
||||
let bootArgs = "serial=3 debug=0x104c04"
|
||||
if let bootArgsData = bootArgs.data(using: .utf8) {
|
||||
if VPhoneSetNVRAMVariable(auxStorage, "boot-args", bootArgsData) {
|
||||
print("[vphone] NVRAM boot-args: \(bootArgs)")
|
||||
}
|
||||
let ok = Dynamic(auxStorage)
|
||||
._setDataValue(bootArgsData, forNVRAMVariableNamed: "boot-args", error: nil)
|
||||
.asBool ?? false
|
||||
if ok { print("[vphone] NVRAM boot-args: \(bootArgs)") }
|
||||
}
|
||||
|
||||
// --- Boot loader with custom ROM ---
|
||||
let bootloader = VZMacOSBootLoader()
|
||||
VPhoneSetBootLoaderROMURL(bootloader, options.romURL)
|
||||
Dynamic(bootloader)._setROMURL(options.romURL)
|
||||
|
||||
// --- VM Configuration ---
|
||||
let config = VZVirtualMachineConfiguration()
|
||||
@@ -73,63 +71,65 @@ class VPhoneVM: NSObject, VZVirtualMachineDelegate {
|
||||
config.cpuCount = max(options.cpuCount, VZVirtualMachineConfiguration.minimumAllowedCPUCount)
|
||||
config.memorySize = max(options.memorySize, VZVirtualMachineConfiguration.minimumAllowedMemorySize)
|
||||
|
||||
// Display (vresearch101: 1290x2796 @ 460 PPI — matches vrevm)
|
||||
// Display
|
||||
let gfx = VZMacGraphicsDeviceConfiguration()
|
||||
gfx.displays = [
|
||||
VZMacGraphicsDisplayConfiguration(widthInPixels: 1290, heightInPixels: 2796, pixelsPerInch: 460),
|
||||
VZMacGraphicsDisplayConfiguration(
|
||||
widthInPixels: options.screenWidth, heightInPixels: options.screenHeight,
|
||||
pixelsPerInch: options.screenPPI
|
||||
),
|
||||
]
|
||||
config.graphicsDevices = [gfx]
|
||||
|
||||
// Audio
|
||||
let afg = VZVirtioSoundDeviceConfiguration()
|
||||
let inputAudioStreamConfiguration = VZVirtioSoundDeviceInputStreamConfiguration()
|
||||
let outputAudioStreamConfiguration = VZVirtioSoundDeviceOutputStreamConfiguration()
|
||||
inputAudioStreamConfiguration.source = VZHostAudioInputStreamSource()
|
||||
outputAudioStreamConfiguration.sink = VZHostAudioOutputStreamSink()
|
||||
afg.streams = [inputAudioStreamConfiguration, outputAudioStreamConfiguration]
|
||||
config.audioDevices = [afg]
|
||||
|
||||
// Storage
|
||||
if FileManager.default.fileExists(atPath: options.diskURL.path) {
|
||||
let attachment = try VZDiskImageStorageDeviceAttachment(url: options.diskURL, readOnly: false)
|
||||
config.storageDevices = [VZVirtioBlockDeviceConfiguration(attachment: attachment)]
|
||||
guard FileManager.default.fileExists(atPath: options.diskURL.path) else {
|
||||
throw VPhoneError.diskNotFound(options.diskURL.path)
|
||||
}
|
||||
let attachment = try VZDiskImageStorageDeviceAttachment(url: options.diskURL, readOnly: false)
|
||||
config.storageDevices = [VZVirtioBlockDeviceConfiguration(attachment: attachment)]
|
||||
|
||||
// Network (shared NAT)
|
||||
let net = VZVirtioNetworkDeviceConfiguration()
|
||||
net.attachment = VZNATNetworkDeviceAttachment()
|
||||
config.networkDevices = [net]
|
||||
|
||||
// Serial port (PL011 UART — always configured)
|
||||
// Connect host stdin/stdout directly for interactive serial console
|
||||
do {
|
||||
if let serialPort = VPhoneCreatePL011SerialPort() {
|
||||
serialPort.attachment = VZFileHandleSerialPortAttachment(
|
||||
fileHandleForReading: FileHandle.standardInput,
|
||||
fileHandleForWriting: FileHandle.standardOutput
|
||||
)
|
||||
config.serialPorts = [serialPort]
|
||||
print("[vphone] PL011 serial port attached (interactive)")
|
||||
}
|
||||
|
||||
// Set up log file if requested
|
||||
if let logPath = options.serialLogPath {
|
||||
let logURL = URL(fileURLWithPath: logPath)
|
||||
FileManager.default.createFile(atPath: logURL.path, contents: nil)
|
||||
self.consoleLogFileHandle = FileHandle(forWritingAtPath: logURL.path)
|
||||
print("[vphone] Serial log: \(logPath)")
|
||||
}
|
||||
// Serial port (PL011 UART — interactive stdin/stdout)
|
||||
if let serialPort = Dynamic._VZPL011SerialPortConfiguration().asObject as? VZSerialPortConfiguration {
|
||||
serialPort.attachment = VZFileHandleSerialPortAttachment(
|
||||
fileHandleForReading: FileHandle.standardInput,
|
||||
fileHandleForWriting: FileHandle.standardOutput
|
||||
)
|
||||
config.serialPorts = [serialPort]
|
||||
print("[vphone] PL011 serial port attached (interactive)")
|
||||
}
|
||||
|
||||
// Multi-touch (USB touch screen for VNC click support)
|
||||
VPhoneConfigureMultiTouch(config)
|
||||
// Multi-touch (USB touch screen)
|
||||
if let obj = Dynamic._VZUSBTouchScreenConfiguration().asObject {
|
||||
Dynamic(config)._setMultiTouchDevices([obj])
|
||||
print("[vphone] USB touch screen configured")
|
||||
}
|
||||
|
||||
// GDB debug stub (default init, system-assigned port — same as vrevm)
|
||||
VPhoneSetGDBDebugStubDefault(config)
|
||||
config.keyboards = [VZUSBKeyboardConfiguration()]
|
||||
|
||||
// GDB debug stub (default init, system-assigned port)
|
||||
Dynamic(config)._setDebugStub(Dynamic._VZGDBDebugStubConfiguration().asObject)
|
||||
|
||||
// Coprocessors
|
||||
if options.skipSEP {
|
||||
print("[vphone] SKIP_SEP=1 — no coprocessor")
|
||||
} else if let sepStorageURL = options.sepStorageURL {
|
||||
VPhoneConfigureSEP(config, sepStorageURL, options.sepRomURL)
|
||||
print("[vphone] SEP coprocessor enabled (storage: \(sepStorageURL.path))")
|
||||
} else {
|
||||
// Create default SEP storage next to NVRAM
|
||||
let defaultSEPURL = options.nvramURL.deletingLastPathComponent()
|
||||
.appendingPathComponent("sep_storage.bin")
|
||||
VPhoneConfigureSEP(config, defaultSEPURL, options.sepRomURL)
|
||||
print("[vphone] SEP coprocessor enabled (storage: \(defaultSEPURL.path))")
|
||||
let sepConfig = Dynamic._VZSEPCoprocessorConfiguration(storageURL: options.sepStorageURL)
|
||||
sepConfig.setRomBinaryURL(options.sepRomURL)
|
||||
sepConfig.setDebugStub(Dynamic._VZGDBDebugStubConfiguration().asObject)
|
||||
if let sepObj = sepConfig.asObject {
|
||||
Dynamic(config)._setCoprocessors([sepObj])
|
||||
print("[vphone] SEP coprocessor enabled (storage: \(options.sepStorageURL.path))")
|
||||
}
|
||||
|
||||
// Validate
|
||||
@@ -141,12 +141,14 @@ class VPhoneVM: NSObject, VZVirtualMachineDelegate {
|
||||
virtualMachine.delegate = self
|
||||
}
|
||||
|
||||
// MARK: - DFU start
|
||||
// MARK: - Start
|
||||
|
||||
@MainActor
|
||||
func start(forceDFU: Bool, stopOnPanic: Bool, stopOnFatalError: Bool) async throws {
|
||||
func start(forceDFU: Bool) async throws {
|
||||
let opts = VZMacOSVirtualMachineStartOptions()
|
||||
VPhoneConfigureStartOptions(opts, forceDFU, stopOnPanic, stopOnFatalError)
|
||||
Dynamic(opts)._setForceDFU(forceDFU)
|
||||
Dynamic(opts)._setStopInIBootStage1(false)
|
||||
Dynamic(opts)._setStopInIBootStage2(false)
|
||||
print("[vphone] Starting\(forceDFU ? " DFU" : "")...")
|
||||
try await virtualMachine.start(options: opts)
|
||||
if forceDFU {
|
||||
@@ -156,57 +158,23 @@ class VPhoneVM: NSObject, VZVirtualMachineDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Wait
|
||||
|
||||
func waitUntilStopped() async {
|
||||
while !done {
|
||||
try? await Task.sleep(nanoseconds: 500_000_000)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Delegate
|
||||
|
||||
func guestDidStop(_: VZVirtualMachine) {
|
||||
// VZ delivers delegate callbacks via dispatch source on the main queue.
|
||||
|
||||
nonisolated func guestDidStop(_: VZVirtualMachine) {
|
||||
print("[vphone] Guest stopped")
|
||||
done = true
|
||||
exit(EXIT_SUCCESS)
|
||||
}
|
||||
|
||||
func virtualMachine(_: VZVirtualMachine, didStopWithError error: Error) {
|
||||
nonisolated func virtualMachine(_: VZVirtualMachine, didStopWithError error: Error) {
|
||||
print("[vphone] Stopped with error: \(error)")
|
||||
done = true
|
||||
exit(EXIT_FAILURE)
|
||||
}
|
||||
|
||||
func virtualMachine(_: VZVirtualMachine, networkDevice _: VZNetworkDevice,
|
||||
attachmentWasDisconnectedWithError error: Error)
|
||||
nonisolated func virtualMachine(_: VZVirtualMachine, networkDevice _: VZNetworkDevice,
|
||||
attachmentWasDisconnectedWithError error: Error)
|
||||
{
|
||||
print("[vphone] Network error: \(error)")
|
||||
}
|
||||
|
||||
// MARK: - Cleanup
|
||||
|
||||
func stopConsoleCapture() {
|
||||
consoleLogFileHandle?.closeFile()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Errors
|
||||
|
||||
enum VPhoneError: Error, CustomStringConvertible {
|
||||
case hardwareModelNotSupported
|
||||
case romNotFound(String)
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .hardwareModelNotSupported:
|
||||
"""
|
||||
PV=3 hardware model not supported. Check:
|
||||
1. macOS >= 15.0 (Sequoia)
|
||||
2. Signed with com.apple.private.virtualization + \
|
||||
com.apple.private.virtualization.security-research
|
||||
3. SIP/AMFI disabled
|
||||
"""
|
||||
case let .romNotFound(p):
|
||||
"ROM not found: \(p)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
sources/vphone-cli/VPhoneVMView.swift
Normal file
16
sources/vphone-cli/VPhoneVMView.swift
Normal file
@@ -0,0 +1,16 @@
|
||||
import AppKit
|
||||
import Dynamic
|
||||
import Foundation
|
||||
import Virtualization
|
||||
|
||||
class VPhoneVMView: VZVirtualMachineView {
|
||||
var keyHelper: VPhoneKeyHelper?
|
||||
|
||||
override func rightMouseDown(with _: NSEvent) {
|
||||
guard let keyHelper else {
|
||||
print("[keys] keyHelper was not set, no way home!")
|
||||
return
|
||||
}
|
||||
keyHelper.sendHome()
|
||||
}
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
import Virtualization
|
||||
import VPhoneObjC
|
||||
|
||||
// MARK: - Touch-enabled VZVirtualMachineView
|
||||
|
||||
struct NormalizedResult {
|
||||
var point: CGPoint
|
||||
var isInvalid: Bool
|
||||
}
|
||||
|
||||
class VPhoneVMView: VZVirtualMachineView {
|
||||
var currentTouchSwipeAim: Int64 = 0
|
||||
|
||||
// 1. Mouse dragged -> touch phase 1 (moving)
|
||||
override func mouseDragged(with event: NSEvent) {
|
||||
handleMouseDragged(event)
|
||||
super.mouseDragged(with: event)
|
||||
}
|
||||
|
||||
private func handleMouseDragged(_ event: NSEvent) {
|
||||
guard let vm = self.virtualMachine,
|
||||
let devices = VPhoneGetMultiTouchDevices(vm),
|
||||
devices.count > 0 else { return }
|
||||
|
||||
let normalized = normalizeCoordinate(event.locationInWindow)
|
||||
let swipeAim = self.currentTouchSwipeAim
|
||||
|
||||
guard let touch = VPhoneCreateTouch(0, 1, normalized.point, Int(swipeAim), event.timestamp) else { return }
|
||||
guard let touchEvent = VPhoneCreateMultiTouchEvent([touch]) else { return }
|
||||
|
||||
let device = devices[0]
|
||||
VPhoneSendMultiTouchEvents(device, [touchEvent])
|
||||
}
|
||||
|
||||
// 2. Mouse down -> touch phase 0 (began)
|
||||
override func mouseDown(with event: NSEvent) {
|
||||
handleMouseDown(event)
|
||||
super.mouseDown(with: event)
|
||||
}
|
||||
|
||||
private func handleMouseDown(_ event: NSEvent) {
|
||||
guard let vm = self.virtualMachine,
|
||||
let devices = VPhoneGetMultiTouchDevices(vm),
|
||||
devices.count > 0 else { return }
|
||||
|
||||
let normalized = normalizeCoordinate(event.locationInWindow)
|
||||
let localPoint = self.convert(event.locationInWindow, from: nil)
|
||||
let edgeResult = hitTestEdge(at: localPoint)
|
||||
self.currentTouchSwipeAim = Int64(edgeResult)
|
||||
|
||||
guard let touch = VPhoneCreateTouch(0, 0, normalized.point, edgeResult, event.timestamp) else { return }
|
||||
guard let touchEvent = VPhoneCreateMultiTouchEvent([touch]) else { return }
|
||||
|
||||
let device = devices[0]
|
||||
VPhoneSendMultiTouchEvents(device, [touchEvent])
|
||||
}
|
||||
|
||||
// 3. Right mouse down -> two-finger touch began
|
||||
override func rightMouseDown(with event: NSEvent) {
|
||||
handleRightMouseDown(event)
|
||||
super.rightMouseDown(with: event)
|
||||
}
|
||||
|
||||
private func handleRightMouseDown(_ event: NSEvent) {
|
||||
guard let vm = self.virtualMachine,
|
||||
let devices = VPhoneGetMultiTouchDevices(vm),
|
||||
devices.count > 0 else { return }
|
||||
|
||||
let normalized = normalizeCoordinate(event.locationInWindow)
|
||||
guard !normalized.isInvalid else { return }
|
||||
|
||||
let localPoint = self.convert(event.locationInWindow, from: nil)
|
||||
let edgeResult = hitTestEdge(at: localPoint)
|
||||
self.currentTouchSwipeAim = Int64(edgeResult)
|
||||
|
||||
guard let touch = VPhoneCreateTouch(0, 0, normalized.point, edgeResult, event.timestamp),
|
||||
let touch2 = VPhoneCreateTouch(1, 0, normalized.point, edgeResult, event.timestamp) else { return }
|
||||
guard let touchEvent = VPhoneCreateMultiTouchEvent([touch, touch2]) else { return }
|
||||
|
||||
let device = devices[0]
|
||||
VPhoneSendMultiTouchEvents(device, [touchEvent])
|
||||
}
|
||||
|
||||
// 4. Mouse up -> touch phase 3 (ended)
|
||||
override func mouseUp(with event: NSEvent) {
|
||||
handleMouseUp(event)
|
||||
super.mouseUp(with: event)
|
||||
}
|
||||
|
||||
private func handleMouseUp(_ event: NSEvent) {
|
||||
guard let vm = self.virtualMachine,
|
||||
let devices = VPhoneGetMultiTouchDevices(vm),
|
||||
devices.count > 0 else { return }
|
||||
|
||||
let normalized = normalizeCoordinate(event.locationInWindow)
|
||||
let swipeAim = self.currentTouchSwipeAim
|
||||
|
||||
guard let touch = VPhoneCreateTouch(0, 3, normalized.point, Int(swipeAim), event.timestamp) else { return }
|
||||
guard let touchEvent = VPhoneCreateMultiTouchEvent([touch]) else { return }
|
||||
|
||||
let device = devices[0]
|
||||
VPhoneSendMultiTouchEvents(device, [touchEvent])
|
||||
}
|
||||
|
||||
// 5. Right mouse up -> two-finger touch ended
|
||||
override func rightMouseUp(with event: NSEvent) {
|
||||
handleRightMouseUp(event)
|
||||
super.rightMouseUp(with: event)
|
||||
}
|
||||
|
||||
private func handleRightMouseUp(_ event: NSEvent) {
|
||||
guard let vm = self.virtualMachine,
|
||||
let devices = VPhoneGetMultiTouchDevices(vm),
|
||||
devices.count > 0 else { return }
|
||||
|
||||
let normalized = normalizeCoordinate(event.locationInWindow)
|
||||
guard !normalized.isInvalid else { return }
|
||||
|
||||
let swipeAim = self.currentTouchSwipeAim
|
||||
|
||||
guard let touch = VPhoneCreateTouch(0, 3, normalized.point, Int(swipeAim), event.timestamp),
|
||||
let touch2 = VPhoneCreateTouch(1, 3, normalized.point, Int(swipeAim), event.timestamp) else { return }
|
||||
guard let touchEvent = VPhoneCreateMultiTouchEvent([touch, touch2]) else { return }
|
||||
|
||||
let device = devices[0]
|
||||
VPhoneSendMultiTouchEvents(device, [touchEvent])
|
||||
}
|
||||
|
||||
// MARK: - Coordinate normalization
|
||||
|
||||
func normalizeCoordinate(_ point: CGPoint) -> NormalizedResult {
|
||||
let bounds = self.bounds
|
||||
|
||||
if bounds.size.width <= 0 || bounds.size.height <= 0 {
|
||||
return NormalizedResult(point: .zero, isInvalid: true)
|
||||
}
|
||||
|
||||
let localPoint = self.convert(point, from: nil)
|
||||
|
||||
var nx = Double(localPoint.x / bounds.size.width)
|
||||
var ny = Double(localPoint.y / bounds.size.height)
|
||||
|
||||
nx = max(0.0, min(1.0, nx))
|
||||
ny = max(0.0, min(1.0, ny))
|
||||
|
||||
if !self.isFlipped {
|
||||
ny = 1.0 - ny
|
||||
}
|
||||
|
||||
return NormalizedResult(point: CGPoint(x: nx, y: ny), isInvalid: false)
|
||||
}
|
||||
|
||||
// MARK: - Edge detection for swipe aim
|
||||
|
||||
func hitTestEdge(at point: CGPoint) -> Int {
|
||||
let bounds = self.bounds
|
||||
let width = bounds.size.width
|
||||
let height = bounds.size.height
|
||||
|
||||
let distLeft = point.x
|
||||
let distRight = width - point.x
|
||||
|
||||
var minDist: Double
|
||||
var edgeCode: Int
|
||||
|
||||
if distRight < distLeft {
|
||||
minDist = distRight
|
||||
edgeCode = 4 // Right
|
||||
} else {
|
||||
minDist = distLeft
|
||||
edgeCode = 8 // Left
|
||||
}
|
||||
|
||||
let topCode = self.isFlipped ? 2 : 1
|
||||
let bottomCode = self.isFlipped ? 1 : 2
|
||||
|
||||
let distTop = point.y
|
||||
if distTop < minDist {
|
||||
minDist = distTop
|
||||
edgeCode = topCode
|
||||
}
|
||||
|
||||
let distBottom = height - point.y
|
||||
if distBottom < minDist {
|
||||
minDist = distBottom
|
||||
edgeCode = bottomCode
|
||||
}
|
||||
|
||||
return minDist < 32.0 ? edgeCode : 0
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Window management
|
||||
|
||||
class VPhoneWindowController {
|
||||
private var windowController: NSWindowController?
|
||||
|
||||
@MainActor
|
||||
func showWindow(for vm: VZVirtualMachine) {
|
||||
let vmView: NSView
|
||||
if #available(macOS 16.0, *) {
|
||||
let view = VZVirtualMachineView()
|
||||
view.virtualMachine = vm
|
||||
view.capturesSystemKeys = true
|
||||
vmView = view
|
||||
} else {
|
||||
let view = VPhoneVMView()
|
||||
view.virtualMachine = vm
|
||||
view.capturesSystemKeys = true
|
||||
vmView = view
|
||||
}
|
||||
|
||||
let pixelWidth: CGFloat = 1179
|
||||
let pixelHeight: CGFloat = 2556
|
||||
let windowSize = NSSize(width: pixelWidth, height: pixelHeight)
|
||||
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(origin: .zero, size: windowSize),
|
||||
styleMask: [.titled, .closable, .resizable, .miniaturizable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
|
||||
window.contentAspectRatio = windowSize
|
||||
window.title = "vphone"
|
||||
window.contentView = vmView
|
||||
window.center()
|
||||
|
||||
let controller = NSWindowController(window: window)
|
||||
controller.showWindow(nil)
|
||||
self.windowController = controller
|
||||
|
||||
if NSApp == nil {
|
||||
_ = NSApplication.shared
|
||||
}
|
||||
NSApp.setActivationPolicy(.regular)
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
|
||||
func close() {
|
||||
DispatchQueue.main.async {
|
||||
self.windowController?.close()
|
||||
self.windowController = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
38
sources/vphone-cli/VPhoneWindowController.swift
Normal file
38
sources/vphone-cli/VPhoneWindowController.swift
Normal file
@@ -0,0 +1,38 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
import Virtualization
|
||||
|
||||
@MainActor
|
||||
class VPhoneWindowController {
|
||||
private var windowController: NSWindowController?
|
||||
|
||||
func showWindow(for vm: VZVirtualMachine, screenWidth: Int, screenHeight: Int, screenScale: Double, keyHelper: VPhoneKeyHelper) {
|
||||
let view = VPhoneVMView()
|
||||
view.virtualMachine = vm
|
||||
view.capturesSystemKeys = true
|
||||
view.keyHelper = keyHelper
|
||||
let vmView: NSView = view
|
||||
|
||||
let scale = CGFloat(screenScale)
|
||||
let windowSize = NSSize(width: CGFloat(screenWidth) / scale, height: CGFloat(screenHeight) / scale)
|
||||
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(origin: .zero, size: windowSize),
|
||||
styleMask: [.titled, .closable, .resizable, .miniaturizable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
|
||||
window.contentAspectRatio = windowSize
|
||||
window.title = "vphone"
|
||||
window.contentView = vmView
|
||||
window.center()
|
||||
|
||||
let controller = NSWindowController(window: window)
|
||||
controller.showWindow(nil)
|
||||
windowController = controller
|
||||
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
}
|
||||
11
sources/vphone-cli/main.swift
Normal file
11
sources/vphone-cli/main.swift
Normal file
@@ -0,0 +1,11 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
/// Parse CLI arguments before NSApplication starts so that bad-arg errors
|
||||
/// print cleanly to the terminal without a run loop ever starting.
|
||||
let cli = VPhoneCLI.parseOrExit()
|
||||
|
||||
let app = NSApplication.shared
|
||||
let delegate = VPhoneAppDelegate(cli: cli)
|
||||
app.delegate = delegate
|
||||
app.run()
|
||||
@@ -1,256 +0,0 @@
|
||||
// VPhoneObjC.m — ObjC wrappers for private Virtualization.framework APIs
|
||||
#import "VPhoneObjC.h"
|
||||
#import <objc/message.h>
|
||||
|
||||
// Private class forward declarations
|
||||
@interface _VZMacHardwareModelDescriptor : NSObject
|
||||
- (instancetype)init;
|
||||
- (void)setPlatformVersion:(unsigned int)version;
|
||||
- (void)setISA:(long long)isa;
|
||||
- (void)setBoardID:(unsigned int)boardID;
|
||||
@end
|
||||
|
||||
@interface VZMacHardwareModel (Private)
|
||||
+ (instancetype)_hardwareModelWithDescriptor:(id)descriptor;
|
||||
@end
|
||||
|
||||
@interface VZMacOSVirtualMachineStartOptions (Private)
|
||||
- (void)_setForceDFU:(BOOL)force;
|
||||
- (void)_setPanicAction:(BOOL)stop;
|
||||
- (void)_setFatalErrorAction:(BOOL)stop;
|
||||
- (void)_setStopInIBootStage1:(BOOL)stop;
|
||||
- (void)_setStopInIBootStage2:(BOOL)stop;
|
||||
@end
|
||||
|
||||
@interface VZMacOSBootLoader (Private)
|
||||
- (void)_setROMURL:(NSURL *)url;
|
||||
@end
|
||||
|
||||
@interface VZVirtualMachineConfiguration (Private)
|
||||
- (void)_setDebugStub:(id)stub;
|
||||
- (void)_setPanicDevice:(id)device;
|
||||
- (void)_setCoprocessors:(NSArray *)coprocessors;
|
||||
- (void)_setMultiTouchDevices:(NSArray *)devices;
|
||||
@end
|
||||
|
||||
@interface VZMacPlatformConfiguration (Private)
|
||||
- (void)_setProductionModeEnabled:(BOOL)enabled;
|
||||
@end
|
||||
|
||||
// --- Implementation ---
|
||||
|
||||
VZMacHardwareModel *VPhoneCreateHardwareModel(void) {
|
||||
// Create descriptor with PV=3, ISA=2, boardID=0x90 (matches vrevm vresearch101)
|
||||
_VZMacHardwareModelDescriptor *desc = [[_VZMacHardwareModelDescriptor alloc] init];
|
||||
[desc setPlatformVersion:3];
|
||||
[desc setBoardID:0x90];
|
||||
[desc setISA:2];
|
||||
|
||||
VZMacHardwareModel *model = [VZMacHardwareModel _hardwareModelWithDescriptor:desc];
|
||||
return model;
|
||||
}
|
||||
|
||||
void VPhoneSetBootLoaderROMURL(VZMacOSBootLoader *bootloader, NSURL *romURL) {
|
||||
[bootloader _setROMURL:romURL];
|
||||
}
|
||||
|
||||
void VPhoneConfigureStartOptions(VZMacOSVirtualMachineStartOptions *opts,
|
||||
BOOL forceDFU,
|
||||
BOOL stopOnPanic,
|
||||
BOOL stopOnFatalError) {
|
||||
[opts _setForceDFU:forceDFU];
|
||||
[opts _setStopInIBootStage1:NO];
|
||||
[opts _setStopInIBootStage2:NO];
|
||||
// Note: _setPanicAction: / _setFatalErrorAction: don't exist on
|
||||
// VZMacOSVirtualMachineStartOptions. Panic handling is done via
|
||||
// _VZPvPanicDeviceConfiguration set on VZVirtualMachineConfiguration.
|
||||
}
|
||||
|
||||
void VPhoneSetGDBDebugStub(VZVirtualMachineConfiguration *config, NSInteger port) {
|
||||
Class stubClass = NSClassFromString(@"_VZGDBDebugStubConfiguration");
|
||||
if (!stubClass) {
|
||||
NSLog(@"[vphone] WARNING: _VZGDBDebugStubConfiguration not found");
|
||||
return;
|
||||
}
|
||||
// Use objc_msgSend to call initWithPort: with an NSInteger argument
|
||||
id (*initWithPort)(id, SEL, NSInteger) = (id (*)(id, SEL, NSInteger))objc_msgSend;
|
||||
id stub = initWithPort([stubClass alloc], NSSelectorFromString(@"initWithPort:"), port);
|
||||
[config _setDebugStub:stub];
|
||||
}
|
||||
|
||||
void VPhoneSetPanicDevice(VZVirtualMachineConfiguration *config) {
|
||||
Class panicClass = NSClassFromString(@"_VZPvPanicDeviceConfiguration");
|
||||
if (!panicClass) {
|
||||
NSLog(@"[vphone] WARNING: _VZPvPanicDeviceConfiguration not found");
|
||||
return;
|
||||
}
|
||||
id device = [[panicClass alloc] init];
|
||||
[config _setPanicDevice:device];
|
||||
}
|
||||
|
||||
void VPhoneSetCoprocessors(VZVirtualMachineConfiguration *config, NSArray *coprocessors) {
|
||||
[config _setCoprocessors:coprocessors];
|
||||
}
|
||||
|
||||
void VPhoneDisableProductionMode(VZMacPlatformConfiguration *platform) {
|
||||
[platform _setProductionModeEnabled:NO];
|
||||
}
|
||||
|
||||
// --- NVRAM ---
|
||||
|
||||
@interface VZMacAuxiliaryStorage (Private)
|
||||
- (BOOL)_setDataValue:(NSData *)value forNVRAMVariableNamed:(NSString *)name error:(NSError **)error;
|
||||
@end
|
||||
|
||||
BOOL VPhoneSetNVRAMVariable(VZMacAuxiliaryStorage *auxStorage, NSString *name, NSData *value) {
|
||||
NSError *error = nil;
|
||||
BOOL ok = [auxStorage _setDataValue:value forNVRAMVariableNamed:name error:&error];
|
||||
if (!ok) {
|
||||
NSLog(@"[vphone] NVRAM set '%@' failed: %@", name, error);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
// --- PL011 Serial Port ---
|
||||
|
||||
@interface _VZPL011SerialPortConfiguration : VZSerialPortConfiguration
|
||||
@end
|
||||
|
||||
VZSerialPortConfiguration *VPhoneCreatePL011SerialPort(void) {
|
||||
Class cls = NSClassFromString(@"_VZPL011SerialPortConfiguration");
|
||||
if (!cls) {
|
||||
NSLog(@"[vphone] WARNING: _VZPL011SerialPortConfiguration not found");
|
||||
return nil;
|
||||
}
|
||||
return [[cls alloc] init];
|
||||
}
|
||||
|
||||
// --- SEP Coprocessor ---
|
||||
|
||||
@interface _VZSEPCoprocessorConfiguration : NSObject
|
||||
- (instancetype)initWithStorageURL:(NSURL *)url;
|
||||
- (void)setRomBinaryURL:(NSURL *)url;
|
||||
- (void)setDebugStub:(id)stub;
|
||||
@end
|
||||
|
||||
id VPhoneCreateSEPCoprocessorConfig(NSURL *storageURL) {
|
||||
Class cls = NSClassFromString(@"_VZSEPCoprocessorConfiguration");
|
||||
if (!cls) {
|
||||
NSLog(@"[vphone] WARNING: _VZSEPCoprocessorConfiguration not found");
|
||||
return nil;
|
||||
}
|
||||
_VZSEPCoprocessorConfiguration *config = [[cls alloc] initWithStorageURL:storageURL];
|
||||
return config;
|
||||
}
|
||||
|
||||
void VPhoneSetSEPRomBinaryURL(id sepConfig, NSURL *romURL) {
|
||||
if ([sepConfig respondsToSelector:@selector(setRomBinaryURL:)]) {
|
||||
[sepConfig performSelector:@selector(setRomBinaryURL:) withObject:romURL];
|
||||
}
|
||||
}
|
||||
|
||||
void VPhoneConfigureSEP(VZVirtualMachineConfiguration *config,
|
||||
NSURL *sepStorageURL,
|
||||
NSURL *sepRomURL) {
|
||||
id sepConfig = VPhoneCreateSEPCoprocessorConfig(sepStorageURL);
|
||||
if (!sepConfig) {
|
||||
NSLog(@"[vphone] Failed to create SEP coprocessor config");
|
||||
return;
|
||||
}
|
||||
if (sepRomURL) {
|
||||
VPhoneSetSEPRomBinaryURL(sepConfig, sepRomURL);
|
||||
}
|
||||
// Set debug stub on SEP (same as vrevm)
|
||||
Class stubClass = NSClassFromString(@"_VZGDBDebugStubConfiguration");
|
||||
if (stubClass) {
|
||||
id sepDebugStub = [[stubClass alloc] init];
|
||||
[sepConfig performSelector:@selector(setDebugStub:) withObject:sepDebugStub];
|
||||
}
|
||||
[config _setCoprocessors:@[sepConfig]];
|
||||
NSLog(@"[vphone] SEP coprocessor configured (storage: %@)", sepStorageURL.path);
|
||||
}
|
||||
|
||||
void VPhoneSetGDBDebugStubDefault(VZVirtualMachineConfiguration *config) {
|
||||
Class stubClass = NSClassFromString(@"_VZGDBDebugStubConfiguration");
|
||||
if (!stubClass) {
|
||||
NSLog(@"[vphone] WARNING: _VZGDBDebugStubConfiguration not found");
|
||||
return;
|
||||
}
|
||||
id stub = [[stubClass alloc] init]; // default init, no specific port (same as vrevm)
|
||||
[config _setDebugStub:stub];
|
||||
}
|
||||
|
||||
// --- Multi-Touch (VNC click fix) ---
|
||||
|
||||
@interface _VZMultiTouchDeviceConfiguration : NSObject <NSCopying>
|
||||
@end
|
||||
|
||||
@interface _VZUSBTouchScreenConfiguration : _VZMultiTouchDeviceConfiguration
|
||||
- (instancetype)init;
|
||||
@end
|
||||
|
||||
void VPhoneConfigureMultiTouch(VZVirtualMachineConfiguration *config) {
|
||||
Class cls = NSClassFromString(@"_VZUSBTouchScreenConfiguration");
|
||||
if (!cls) {
|
||||
NSLog(@"[vphone] WARNING: _VZUSBTouchScreenConfiguration not found");
|
||||
return;
|
||||
}
|
||||
id touchConfig = [[cls alloc] init];
|
||||
[config _setMultiTouchDevices:@[touchConfig]];
|
||||
NSLog(@"[vphone] USB touch screen configured");
|
||||
}
|
||||
|
||||
// VZTouchHelper: create _VZTouch using KVC to avoid crash in initWithView:...
|
||||
// The _VZTouch initializer does a struct copy (objc_copyStruct) that causes
|
||||
// EXC_BAD_ACCESS (SIGBUS) when called from Swift Dynamic framework.
|
||||
// Using alloc+init then KVC setValue:forKey: bypasses the problematic initializer.
|
||||
id VPhoneCreateTouch(NSInteger index,
|
||||
NSInteger phase,
|
||||
CGPoint location,
|
||||
NSInteger swipeAim,
|
||||
NSTimeInterval timestamp) {
|
||||
Class touchClass = NSClassFromString(@"_VZTouch");
|
||||
if (!touchClass) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
id touch = [[touchClass alloc] init];
|
||||
|
||||
[touch setValue:@((unsigned char)index) forKey:@"_index"];
|
||||
[touch setValue:@(phase) forKey:@"_phase"];
|
||||
[touch setValue:@(swipeAim) forKey:@"_swipeAim"];
|
||||
[touch setValue:@(timestamp) forKey:@"_timestamp"];
|
||||
[touch setValue:[NSValue valueWithPoint:location] forKey:@"_location"];
|
||||
|
||||
return touch;
|
||||
}
|
||||
|
||||
id VPhoneCreateMultiTouchEvent(NSArray *touches) {
|
||||
Class cls = NSClassFromString(@"_VZMultiTouchEvent");
|
||||
if (!cls) {
|
||||
return nil;
|
||||
}
|
||||
// _VZMultiTouchEvent initWithTouches:
|
||||
SEL sel = NSSelectorFromString(@"initWithTouches:");
|
||||
id event = [cls alloc];
|
||||
id (*initWithTouches)(id, SEL, NSArray *) = (id (*)(id, SEL, NSArray *))objc_msgSend;
|
||||
return initWithTouches(event, sel, touches);
|
||||
}
|
||||
|
||||
NSArray *VPhoneGetMultiTouchDevices(VZVirtualMachine *vm) {
|
||||
SEL sel = NSSelectorFromString(@"_multiTouchDevices");
|
||||
if (![vm respondsToSelector:sel]) {
|
||||
return nil;
|
||||
}
|
||||
NSArray * (*getter)(id, SEL) = (NSArray * (*)(id, SEL))objc_msgSend;
|
||||
return getter(vm, sel);
|
||||
}
|
||||
|
||||
void VPhoneSendMultiTouchEvents(id multiTouchDevice, NSArray *events) {
|
||||
SEL sel = NSSelectorFromString(@"sendMultiTouchEvents:");
|
||||
if (![multiTouchDevice respondsToSelector:sel]) {
|
||||
return;
|
||||
}
|
||||
void (*send)(id, SEL, NSArray *) = (void (*)(id, SEL, NSArray *))objc_msgSend;
|
||||
send(multiTouchDevice, sel, events);
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
// VPhoneObjC.h — ObjC wrappers for private Virtualization.framework APIs
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Virtualization/Virtualization.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// Create a PV=3 (vphone) VZMacHardwareModel using private _VZMacHardwareModelDescriptor.
|
||||
VZMacHardwareModel *VPhoneCreateHardwareModel(void);
|
||||
|
||||
/// Set _setROMURL: on a VZMacOSBootLoader.
|
||||
void VPhoneSetBootLoaderROMURL(VZMacOSBootLoader *bootloader, NSURL *romURL);
|
||||
|
||||
/// Configure VZMacOSVirtualMachineStartOptions.
|
||||
/// Sets _setForceDFU:, _setPanicAction:, _setFatalErrorAction:
|
||||
void VPhoneConfigureStartOptions(VZMacOSVirtualMachineStartOptions *opts,
|
||||
BOOL forceDFU,
|
||||
BOOL stopOnPanic,
|
||||
BOOL stopOnFatalError);
|
||||
|
||||
/// Set _setDebugStub: with a _VZGDBDebugStubConfiguration on the VM config (specific port).
|
||||
void VPhoneSetGDBDebugStub(VZVirtualMachineConfiguration *config, NSInteger port);
|
||||
|
||||
/// Set _setDebugStub: with default _VZGDBDebugStubConfiguration (system-assigned port, same as vrevm).
|
||||
void VPhoneSetGDBDebugStubDefault(VZVirtualMachineConfiguration *config);
|
||||
|
||||
/// Set _VZPvPanicDeviceConfiguration on the VM config.
|
||||
void VPhoneSetPanicDevice(VZVirtualMachineConfiguration *config);
|
||||
|
||||
/// Set _setCoprocessors: on the VM config (empty array = no coprocessors).
|
||||
void VPhoneSetCoprocessors(VZVirtualMachineConfiguration *config, NSArray *coprocessors);
|
||||
|
||||
/// Set _setProductionModeEnabled:NO on VZMacPlatformConfiguration.
|
||||
void VPhoneDisableProductionMode(VZMacPlatformConfiguration *platform);
|
||||
|
||||
/// Create a _VZSEPCoprocessorConfiguration with the given storage URL.
|
||||
/// Returns the config object, or nil on failure.
|
||||
id _Nullable VPhoneCreateSEPCoprocessorConfig(NSURL *storageURL);
|
||||
|
||||
/// Set romBinaryURL on a _VZSEPCoprocessorConfiguration.
|
||||
void VPhoneSetSEPRomBinaryURL(id sepConfig, NSURL *romURL);
|
||||
|
||||
/// Configure SEP coprocessor on the VM config.
|
||||
/// Creates storage at sepStorageURL, optionally sets sepRomURL, and calls _setCoprocessors:.
|
||||
void VPhoneConfigureSEP(VZVirtualMachineConfiguration *config,
|
||||
NSURL *sepStorageURL,
|
||||
NSURL *_Nullable sepRomURL);
|
||||
|
||||
/// Set an NVRAM variable on VZMacAuxiliaryStorage using the private _setDataValue API.
|
||||
/// Returns YES on success.
|
||||
BOOL VPhoneSetNVRAMVariable(VZMacAuxiliaryStorage *auxStorage, NSString *name, NSData *value);
|
||||
|
||||
/// Create a _VZPL011SerialPortConfiguration (ARM PL011 UART serial port).
|
||||
/// Returns nil if the private class is unavailable.
|
||||
VZSerialPortConfiguration *_Nullable VPhoneCreatePL011SerialPort(void);
|
||||
|
||||
// --- Multi-Touch (VNC click fix) ---
|
||||
|
||||
/// Configure _VZUSBTouchScreenConfiguration on the VM config.
|
||||
/// Must be called before VM starts to enable touch input.
|
||||
void VPhoneConfigureMultiTouch(VZVirtualMachineConfiguration *config);
|
||||
|
||||
/// Create a _VZTouch object using KVC (avoids crash in _VZTouch initWithView:...).
|
||||
/// Returns nil if the _VZTouch class is unavailable.
|
||||
id _Nullable VPhoneCreateTouch(NSInteger index,
|
||||
NSInteger phase,
|
||||
CGPoint location,
|
||||
NSInteger swipeAim,
|
||||
NSTimeInterval timestamp);
|
||||
|
||||
/// Create a _VZMultiTouchEvent from an array of _VZTouch objects.
|
||||
id _Nullable VPhoneCreateMultiTouchEvent(NSArray *touches);
|
||||
|
||||
/// Get the _multiTouchDevices array from a running VZVirtualMachine.
|
||||
NSArray *_Nullable VPhoneGetMultiTouchDevices(VZVirtualMachine *vm);
|
||||
|
||||
/// Send multi-touch events to a multi-touch device.
|
||||
void VPhoneSendMultiTouchEvents(id multiTouchDevice, NSArray *events);
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
Reference in New Issue
Block a user