Clean up location passthrough PR: consistent formatting and logging

This commit is contained in:
Lakr
2026-03-02 19:28:53 +08:00
parent c0f0efa492
commit 8c7d9911a2
27 changed files with 1205 additions and 905 deletions

2
.gitignore vendored
View File

@@ -317,3 +317,5 @@ __marimo__/
TODO.md
/references/
scripts/vphoned/vphoned
sources/vphone-cli/VPhoneBuildInfo.swift
setup_logs/

View File

@@ -35,14 +35,46 @@ sources/
├── 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
├── VPhoneBuildInfo.swift # Auto-generated build-time commit hash
│ # VM core
├── VPhoneVirtualMachine.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
├── VPhoneVirtualMachineView.swift # Touch-enabled VZVirtualMachineView + helpers
├── VPhoneError.swift # Error types
└── MainActor+Isolated.swift # MainActor.isolated helper
│ # Guest daemon client (vsock)
├── VPhoneControl.swift # Host-side vsock client for vphoned (length-prefixed JSON)
│ # Window & UI
├── VPhoneWindowController.swift # @MainActor VM window management + toolbar
├── VPhoneKeyHelper.swift # Keyboard/hardware key event dispatch to VM
├── VPhoneLocationProvider.swift # CoreLocation → guest forwarding over vsock
│ # Menu bar (extensions on VPhoneMenuController)
├── VPhoneMenuController.swift # Menu bar controller (builds Keys, Type, Location, Connect)
├── VPhoneMenuKeys.swift # Keys menu — home, power, volume, spotlight buttons
├── VPhoneMenuType.swift # Type menu — paste ASCII text to guest
├── VPhoneMenuLocation.swift # Location menu — host location sync toggle
├── VPhoneMenuConnect.swift # Connect menu — devmode, ping, version, file browser
│ # File browser (SwiftUI)
├── VPhoneFileWindowController.swift # File browser window (NSHostingController)
├── VPhoneFileBrowserView.swift # SwiftUI file browser with search + drag-drop
├── VPhoneFileBrowserModel.swift # @Observable file browser state + transfers
└── VPhoneRemoteFile.swift # Remote file data model (path, size, permissions)
scripts/
├── vphoned/ # Guest daemon (Objective-C, runs inside iOS VM)
│ ├── vphoned.m # Main — vsock listener, message dispatch, auto-update
│ ├── vphoned_protocol.{h,m} # Length-prefixed JSON framing (shared with host)
│ ├── vphoned_hid.{h,m} # HID event injection (IOHIDEvent)
│ ├── vphoned_devmode.{h,m} # Developer Mode query/enable via XPC
│ ├── vphoned_location.{h,m} # CLLocationManager spoofing
│ ├── vphoned_files.{h,m} # File operations (list, get, put, mkdir, delete, rename)
│ ├── vphoned.plist # LaunchDaemon plist
│ ├── entitlements.plist # Guest entitlements
│ └── signcert.p12 # Signing certificate for re-signing
├── patchers/ # Python patcher package
│ ├── iboot.py # Dynamic iBoot patcher (iBSS/iBEC/LLB)
│ ├── iboot_jb.py # JB extension iBoot patcher (nonce skip)
@@ -76,9 +108,14 @@ researchs/
- **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`.
- **Configuration:** CLI options parsed via `ArgumentParser`, converted to `VPhoneVirtualMachine.Options` struct, then used to build `VZVirtualMachineConfiguration`.
- **Error handling:** `VPhoneError` enum with `CustomStringConvertible` for user-facing messages.
- **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`.
- **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 `VPhoneVirtualMachineView`.
- **Guest daemon (vphoned):** ObjC daemon running inside the iOS VM as a LaunchDaemon. Communicates with host over vsock port 1337 using length-prefixed JSON (`[uint32 BE length][UTF-8 JSON]`). Handles HID injection, developer mode, location spoofing, and file operations. Host side is `VPhoneControl` which auto-reconnects and supports binary auto-update on connect.
- **Control protocol:** All commands use async request-response via `VPhoneControl.sendRequest()` with pending request tracking. Menu actions (`VPhoneMenuConnect`) await responses and show results as `NSAlert` sheets on the VM window.
- **Menu system:** `VPhoneMenuController` owns the menu bar, built from extensions in separate files per menu (Keys, Type, Location, Connect). Each extension has its own `build*Menu()` method.
- **File browser:** SwiftUI-based (`VPhoneFileBrowserView` + `VPhoneFileBrowserModel`) hosted in a separate `NSWindow` via `NSHostingController`. Supports search, sort, upload/download, drag-drop. File operations go through `VPhoneControl` async APIs.
- **Location sync:** `VPhoneLocationProvider` wraps `CLLocationManager`, forwards host Mac's GPS coordinates to the guest over vsock when toggled from the Location menu.
---
@@ -255,8 +292,9 @@ AVPBooter (ROM, PCC)
- **Sections:** Use `// MARK: -` to organize code within files.
- **Access control:** Default (internal). Only mark `private` when needed for clarity.
- **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.
- **Naming:** Types are `VPhone`-prefixed (`VPhoneVirtualMachine`, `VPhoneWindowController`). Match Apple framework conventions.
- **Private APIs:** Use `Dynamic()` for runtime method dispatch. Touch objects use `NSClassFromString` + KVC to avoid designated initializer crashes.
- **NSWindow `isReleasedWhenClosed`:** Always set `window.isReleasedWhenClosed = false` for programmatically created windows managed by an `NSWindowController`. The default is `true`, which causes the window to be released on close while `NSWindowController` and `_NSWindowTransformAnimation` still hold references — `objc_release` crashes on a dangling pointer during CA transaction commit. Nib-loaded windows handled by `NSWindowController` get this set automatically, but programmatic windows do not.
### Shell Scripts

View File

@@ -9,6 +9,10 @@ MEMORY ?= 8192
DISK_SIZE ?= 64
CFW_INPUT ?= cfw_input
# ─── Build info ──────────────────────────────────────────────────
GIT_HASH := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
BUILD_INFO := sources/vphone-cli/VPhoneBuildInfo.swift
# ─── Paths ────────────────────────────────────────────────────────
SCRIPTS := scripts
BINARY := .build/release/vphone-cli
@@ -91,7 +95,9 @@ setup_libimobiledevice:
build: $(BINARY)
$(BINARY): $(SWIFT_SOURCES) Package.swift $(ENTITLEMENTS)
@echo "=== Building vphone-cli ==="
@echo "=== Building vphone-cli ($(GIT_HASH)) ==="
@echo '// Auto-generated — do not edit' > $(BUILD_INFO)
@echo 'enum VPhoneBuildInfo { static let commitHash = "$(GIT_HASH)" }' >> $(BUILD_INFO)
swift build -c release 2>&1 | tail -5
@echo ""
@echo "=== Signing with entitlements ==="
@@ -114,15 +120,21 @@ clean:
swift package clean
rm -rf .build
rm -f $(SCRIPTS)/vphoned/vphoned
rm -f $(BUILD_INFO)
# Cross-compile vphoned daemon for iOS arm64 (installed into VM by cfw_install)
.PHONY: vphoned
vphoned: $(SCRIPTS)/vphoned/vphoned
$(SCRIPTS)/vphoned/vphoned: $(SCRIPTS)/vphoned/vphoned.m
VPHONED_SRCS := $(addprefix $(SCRIPTS)/vphoned/, \
vphoned.m vphoned_protocol.m vphoned_hid.m \
vphoned_devmode.m vphoned_location.m vphoned_files.m)
$(SCRIPTS)/vphoned/vphoned: $(VPHONED_SRCS)
@echo "=== Building vphoned (arm64, iphoneos) ==="
xcrun -sdk iphoneos clang -arch arm64 -Os -fobjc-arc \
-o $@ $< -framework Foundation
-I$(SCRIPTS)/vphoned \
-DVPHONED_BUILD_HASH='"$(GIT_HASH)"' \
-o $@ $(VPHONED_SRCS) -framework Foundation
@echo " built OK"
# Sign vphoned with entitlements using cfw_input tools (requires make cfw_install to have unpacked cfw_input)

View File

@@ -9,39 +9,33 @@
* it to CACHE_PATH and exit launchd restarts us, and the bootstrap
* code in main() exec's the cached binary.
*
* Capabilities:
* hid inject HID events (Home, Power, Lock, Unlock) via IOKit
* devmode enable developer mode via AMFI XPC
*
* Protocol:
* Each message: [uint32 big-endian length][UTF-8 JSON payload]
* Every JSON object carries "v" (protocol version), "t" (type),
* and optionally "id" (request ID, echoed in responses).
*
* Build:
* xcrun -sdk iphoneos clang -arch arm64 -Os -fobjc-arc \
* -o vphoned vphoned.m -framework Foundation
* make vphoned
*/
#import <Foundation/Foundation.h>
#include <CommonCrypto/CommonDigest.h>
#include <dispatch/dispatch.h>
#include <dlfcn.h>
#include <mach/mach_time.h>
#include <mach-o/dyld.h>
#include <objc/runtime.h>
#include <objc/message.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <unistd.h>
#import "vphoned_protocol.h"
#import "vphoned_hid.h"
#import "vphoned_devmode.h"
#import "vphoned_location.h"
#import "vphoned_files.h"
#ifndef AF_VSOCK
#define AF_VSOCK 40
#endif
#define VMADDR_CID_ANY 0xFFFFFFFF
#define VPHONED_PORT 1337
#define PROTOCOL_VERSION 1
#ifndef VPHONED_BUILD_HASH
#define VPHONED_BUILD_HASH "unknown"
#endif
#define INSTALL_PATH "/usr/bin/vphoned"
#define CACHE_PATH "/var/root/Library/Caches/vphoned"
@@ -86,729 +80,9 @@ static const char *self_executable_path(void) {
return path;
}
// MARK: - IOKit (matches TrollVNC's STHIDEventGenerator)
typedef void *IOHIDEventSystemClientRef;
typedef void *IOHIDEventRef;
static IOHIDEventSystemClientRef (*pCreate)(CFAllocatorRef);
static IOHIDEventRef (*pKeyboard)(CFAllocatorRef, uint64_t,
uint32_t, uint32_t, int, int);
static void (*pSetSender)(IOHIDEventRef, uint64_t);
static void (*pDispatch)(IOHIDEventSystemClientRef, IOHIDEventRef);
static IOHIDEventSystemClientRef gClient;
static dispatch_queue_t gHIDQueue;
static BOOL load_iokit(void) {
void *h = dlopen("/System/Library/Frameworks/IOKit.framework/IOKit", RTLD_NOW);
if (!h) { NSLog(@"vphoned: dlopen IOKit failed"); return NO; }
pCreate = dlsym(h, "IOHIDEventSystemClientCreate");
pKeyboard = dlsym(h, "IOHIDEventCreateKeyboardEvent");
pSetSender = dlsym(h, "IOHIDEventSetSenderID");
pDispatch = dlsym(h, "IOHIDEventSystemClientDispatchEvent");
if (!pCreate || !pKeyboard || !pSetSender || !pDispatch) {
NSLog(@"vphoned: missing IOKit symbols");
return NO;
}
gClient = pCreate(kCFAllocatorDefault);
if (!gClient) { NSLog(@"vphoned: IOHIDEventSystemClientCreate returned NULL"); return NO; }
dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(
DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0);
gHIDQueue = dispatch_queue_create("com.vphone.vphoned.hid", attr);
NSLog(@"vphoned: IOKit loaded");
return YES;
}
static void send_hid_event(IOHIDEventRef event) {
IOHIDEventRef strong = (IOHIDEventRef)CFRetain(event);
dispatch_async(gHIDQueue, ^{
pSetSender(strong, 0x8000000817319372);
pDispatch(gClient, strong);
CFRelease(strong);
});
}
static void press(uint32_t page, uint32_t usage) {
IOHIDEventRef down = pKeyboard(kCFAllocatorDefault, mach_absolute_time(),
page, usage, 1, 0);
if (!down) return;
send_hid_event(down);
CFRelease(down);
usleep(100000);
IOHIDEventRef up = pKeyboard(kCFAllocatorDefault, mach_absolute_time(),
page, usage, 0, 0);
if (!up) return;
send_hid_event(up);
CFRelease(up);
}
// MARK: - Developer Mode (AMFI XPC)
//
// Talks to com.apple.amfi.xpc to query / arm developer mode.
// Reference: TrollStore RootHelper/devmode.m
// Requires entitlement: com.apple.private.amfi.developer-mode-control
// XPC functions resolved via dlsym to avoid iOS SDK availability
// guards (xpc_connection_create_mach_service is marked unavailable
// on iOS but works at runtime with the right entitlements).
typedef void *xpc_conn_t; // opaque, avoids typedef conflict with SDK
typedef void *xpc_obj_t;
static xpc_conn_t (*pXpcCreateMach)(const char *, dispatch_queue_t, uint64_t);
static void (*pXpcSetHandler)(xpc_conn_t, void (^)(xpc_obj_t));
static void (*pXpcResume)(xpc_conn_t);
static void (*pXpcCancel)(xpc_conn_t);
static xpc_obj_t (*pXpcSendSync)(xpc_conn_t, xpc_obj_t);
static xpc_obj_t (*pXpcDictGet)(xpc_obj_t, const char *);
static xpc_obj_t (*pCFToXPC)(CFTypeRef);
static CFTypeRef (*pXPCToCF)(xpc_obj_t);
static BOOL load_xpc(void) {
void *libxpc = dlopen("/usr/lib/system/libxpc.dylib", RTLD_NOW);
if (!libxpc) { NSLog(@"vphoned: dlopen libxpc failed"); return NO; }
void *libcf = dlopen("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", RTLD_NOW);
if (!libcf) { NSLog(@"vphoned: dlopen CoreFoundation failed"); return NO; }
pXpcCreateMach = dlsym(libxpc, "xpc_connection_create_mach_service");
pXpcSetHandler = dlsym(libxpc, "xpc_connection_set_event_handler");
pXpcResume = dlsym(libxpc, "xpc_connection_resume");
pXpcCancel = dlsym(libxpc, "xpc_connection_cancel");
pXpcSendSync = dlsym(libxpc, "xpc_connection_send_message_with_reply_sync");
pXpcDictGet = dlsym(libxpc, "xpc_dictionary_get_value");
pCFToXPC = dlsym(libcf, "_CFXPCCreateXPCMessageWithCFObject");
pXPCToCF = dlsym(libcf, "_CFXPCCreateCFObjectFromXPCMessage");
if (!pXpcCreateMach || !pXpcSetHandler || !pXpcResume || !pXpcCancel ||
!pXpcSendSync || !pXpcDictGet || !pCFToXPC || !pXPCToCF) {
NSLog(@"vphoned: missing XPC/CF symbols");
return NO;
}
NSLog(@"vphoned: XPC loaded");
return YES;
}
typedef enum {
kAMFIActionArm = 0, // arm developer mode (prompts on next reboot)
kAMFIActionDisable = 1, // disable developer mode immediately
kAMFIActionStatus = 2, // query: {success, status, armed}
} AMFIXPCAction;
static NSDictionary *amfi_send(AMFIXPCAction action) {
xpc_conn_t conn = pXpcCreateMach("com.apple.amfi.xpc", NULL, 0);
if (!conn) {
NSLog(@"vphoned: amfi xpc connection failed");
return nil;
}
pXpcSetHandler(conn, ^(xpc_obj_t event) {});
pXpcResume(conn);
xpc_obj_t msg = pCFToXPC((__bridge CFDictionaryRef)@{@"action": @(action)});
xpc_obj_t reply = pXpcSendSync(conn, msg);
pXpcCancel(conn);
if (!reply) {
NSLog(@"vphoned: amfi xpc no reply");
return nil;
}
xpc_obj_t cfReply = pXpcDictGet(reply, "cfreply");
if (!cfReply) {
NSLog(@"vphoned: amfi xpc no cfreply");
return nil;
}
NSDictionary *dict = (__bridge_transfer NSDictionary *)pXPCToCF(cfReply);
NSLog(@"vphoned: amfi reply: %@", dict);
return dict;
}
static BOOL devmode_status(void) {
NSDictionary *reply = amfi_send(kAMFIActionStatus);
if (!reply) return NO;
NSNumber *success = reply[@"success"];
if (!success || ![success boolValue]) return NO;
NSNumber *status = reply[@"status"];
return [status boolValue];
}
static BOOL devmode_arm(BOOL *alreadyEnabled) {
BOOL enabled = devmode_status();
if (alreadyEnabled) *alreadyEnabled = enabled;
if (enabled) return YES;
NSDictionary *reply = amfi_send(kAMFIActionArm);
if (!reply) return NO;
NSNumber *success = reply[@"success"];
return success && [success boolValue];
}
// MARK: - CoreLocation Simulation
static id gSimManager = nil;
static SEL gSetLocationSel = NULL;
static SEL gClearLocationsSel = NULL;
static SEL gFlushSel = NULL;
static SEL gStartSimSel = NULL;
static BOOL gLocationLoaded = NO;
static BOOL load_corelocation(void) {
void *h = dlopen("/System/Library/Frameworks/CoreLocation.framework/CoreLocation", RTLD_NOW);
if (!h) { NSLog(@"vphoned: dlopen CoreLocation failed"); return NO; }
Class cls = NSClassFromString(@"CLSimulationManager");
if (!cls) { NSLog(@"vphoned: CLSimulationManager not found"); return NO; }
gSimManager = [[cls alloc] init];
if (!gSimManager) { NSLog(@"vphoned: CLSimulationManager alloc/init failed"); return NO; }
// Probe available selectors for setting location
SEL candidates[] = {
NSSelectorFromString(@"setSimulatedLocation:"),
NSSelectorFromString(@"appendSimulatedLocation:"),
NSSelectorFromString(@"setLocation:"),
};
for (int i = 0; i < 3; i++) {
if ([gSimManager respondsToSelector:candidates[i]]) {
gSetLocationSel = candidates[i];
break;
}
}
if (!gSetLocationSel) {
NSLog(@"vphoned: no set-location selector found, dumping methods:");
unsigned int count = 0;
Method *methods = class_copyMethodList([gSimManager class], &count);
for (unsigned int i = 0; i < count; i++) {
NSLog(@" %s", sel_getName(method_getName(methods[i])));
}
free(methods);
return NO;
}
// Probe clear selector
SEL clearCandidates[] = {
NSSelectorFromString(@"clearSimulatedLocations"),
NSSelectorFromString(@"stopLocationSimulation"),
};
for (int i = 0; i < 2; i++) {
if ([gSimManager respondsToSelector:clearCandidates[i]]) {
gClearLocationsSel = clearCandidates[i];
break;
}
}
// Probe flush selector
SEL flushCandidates[] = {
NSSelectorFromString(@"flush"),
NSSelectorFromString(@"flushSimulatedLocations"),
};
for (int i = 0; i < 2; i++) {
if ([gSimManager respondsToSelector:flushCandidates[i]]) {
gFlushSel = flushCandidates[i];
break;
}
}
// Probe startLocationSimulation selector
SEL startCandidates[] = {
NSSelectorFromString(@"startLocationSimulation"),
NSSelectorFromString(@"startSimulation"),
};
for (int i = 0; i < 2; i++) {
if ([gSimManager respondsToSelector:startCandidates[i]]) {
gStartSimSel = startCandidates[i];
break;
}
}
// Start simulation session if available
if (gStartSimSel) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[gSimManager performSelector:gStartSimSel];
#pragma clang diagnostic pop
}
NSLog(@"vphoned: CoreLocation simulation loaded (set=%s, clear=%s, flush=%s, start=%s)",
sel_getName(gSetLocationSel),
gClearLocationsSel ? sel_getName(gClearLocationsSel) : "(none)",
gFlushSel ? sel_getName(gFlushSel) : "(none)",
gStartSimSel ? sel_getName(gStartSimSel) : "(none)");
gLocationLoaded = YES;
return YES;
}
static void simulate_location(double lat, double lon, double alt,
double hacc, double vacc,
double speed, double course) {
if (!gLocationLoaded || !gSimManager || !gSetLocationSel) return;
@try {
// CLLocationCoordinate2D is {double latitude, double longitude}
typedef struct { double latitude; double longitude; } CLCoord2D;
CLCoord2D coord = {lat, lon};
// Use full CLLocation init including speed and course
Class locClass = NSClassFromString(@"CLLocation");
id locInst = [locClass alloc];
SEL initSel = NSSelectorFromString(
@"initWithCoordinate:altitude:horizontalAccuracy:verticalAccuracy:course:speed:timestamp:");
if (![locInst respondsToSelector:initSel]) {
// Fallback to simpler init
initSel = NSSelectorFromString(
@"initWithCoordinate:altitude:horizontalAccuracy:verticalAccuracy:timestamp:");
typedef id (*InitFunc5)(id, SEL, CLCoord2D, double, double, double, id);
id location = ((InitFunc5)objc_msgSend)(locInst, initSel, coord, alt, hacc, vacc, [NSDate date]);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[gSimManager performSelector:gSetLocationSel withObject:location];
if (gFlushSel) [gSimManager performSelector:gFlushSel];
#pragma clang diagnostic pop
NSLog(@"vphoned: simulate_location lat=%.6f lon=%.6f (fallback init) sel=%s%s",
lat, lon, sel_getName(gSetLocationSel),
gFlushSel ? " (flushed)" : "");
return;
}
typedef id (*InitFunc7)(id, SEL, CLCoord2D, double, double, double, double, double, id);
id location = ((InitFunc7)objc_msgSend)(locInst, initSel, coord, alt, hacc, vacc, course, speed, [NSDate date]);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[gSimManager performSelector:gSetLocationSel withObject:location];
if (gFlushSel) {
[gSimManager performSelector:gFlushSel];
}
#pragma clang diagnostic pop
NSLog(@"vphoned: simulate_location lat=%.6f lon=%.6f alt=%.1f spd=%.1f crs=%.1f sel=%s%s",
lat, lon, alt, speed, course, sel_getName(gSetLocationSel),
gFlushSel ? " (flushed)" : "");
} @catch (NSException *e) {
NSLog(@"vphoned: simulate_location exception: %@", e);
}
}
static void clear_simulated_location(void) {
if (!gLocationLoaded || !gSimManager) return;
@try {
if (gClearLocationsSel) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[gSimManager performSelector:gClearLocationsSel];
#pragma clang diagnostic pop
NSLog(@"vphoned: cleared simulated location");
}
} @catch (NSException *e) {
NSLog(@"vphoned: clear_simulated_location exception: %@", e);
}
}
// MARK: - Protocol Framing
static BOOL read_fully(int fd, void *buf, size_t count) {
size_t offset = 0;
while (offset < count) {
ssize_t n = read(fd, (uint8_t *)buf + offset, count - offset);
if (n <= 0) return NO;
offset += n;
}
return YES;
}
static BOOL write_fully(int fd, const void *buf, size_t count) {
size_t offset = 0;
while (offset < count) {
ssize_t n = write(fd, (const uint8_t *)buf + offset, count - offset);
if (n <= 0) return NO;
offset += n;
}
return YES;
}
static NSDictionary *read_message(int fd) {
uint32_t header = 0;
if (!read_fully(fd, &header, 4)) return nil;
uint32_t length = ntohl(header);
if (length == 0 || length > 4 * 1024 * 1024) return nil;
NSMutableData *payload = [NSMutableData dataWithLength:length];
if (!read_fully(fd, payload.mutableBytes, length)) return nil;
NSError *err = nil;
id obj = [NSJSONSerialization JSONObjectWithData:payload options:0 error:&err];
if (![obj isKindOfClass:[NSDictionary class]]) return nil;
return obj;
}
static BOOL write_message(int fd, NSDictionary *dict) {
NSError *err = nil;
NSData *json = [NSJSONSerialization dataWithJSONObject:dict options:0 error:&err];
if (!json) return NO;
uint32_t header = htonl((uint32_t)json.length);
if (write(fd, &header, 4) != 4) return NO;
if (write(fd, json.bytes, json.length) != (ssize_t)json.length) return NO;
return YES;
}
// MARK: - Response Helper
/// Build a response dict with protocol version, type, and optional request ID echo.
static NSMutableDictionary *make_response(NSString *type, id reqId) {
NSMutableDictionary *r = [@{@"v": @PROTOCOL_VERSION, @"t": type} mutableCopy];
if (reqId) r[@"id"] = reqId;
return r;
}
// MARK: - File Operations
//
// Handle file_list, file_get, file_put, file_mkdir, file_delete, file_rename.
// file_get and file_put perform inline binary I/O on the socket, so they
// need the fd directly (can't use the simple return-dict pattern).
/// Handle a file command. Returns a response dict, or nil if the response
/// was already written inline (file_get with streaming data).
static NSDictionary *handle_file_command(int fd, NSDictionary *msg) {
NSString *type = msg[@"t"];
id reqId = msg[@"id"];
// -- file_list: list directory contents --
if ([type isEqualToString:@"file_list"]) {
NSString *path = msg[@"path"];
if (!path) {
NSMutableDictionary *r = make_response(@"err", reqId);
r[@"msg"] = @"missing path";
return r;
}
NSFileManager *fm = [NSFileManager defaultManager];
NSError *err = nil;
NSArray *contents = [fm contentsOfDirectoryAtPath:path error:&err];
if (!contents) {
NSMutableDictionary *r = make_response(@"err", reqId);
r[@"msg"] = err.localizedDescription ?: @"list failed";
return r;
}
NSMutableArray *entries = [NSMutableArray arrayWithCapacity:contents.count];
for (NSString *name in contents) {
NSString *full = [path stringByAppendingPathComponent:name];
NSDictionary *attrs = [fm attributesOfItemAtPath:full error:nil];
if (!attrs) continue;
NSString *fileType = attrs[NSFileType];
NSString *typeStr = @"file";
if ([fileType isEqualToString:NSFileTypeDirectory]) typeStr = @"dir";
else if ([fileType isEqualToString:NSFileTypeSymbolicLink]) typeStr = @"link";
NSNumber *size = attrs[NSFileSize] ?: @0;
NSDate *mtime = attrs[NSFileModificationDate];
NSNumber *posixPerms = attrs[NSFilePosixPermissions];
[entries addObject:@{
@"name": name,
@"type": typeStr,
@"size": size,
@"perm": [NSString stringWithFormat:@"%lo", [posixPerms unsignedLongValue]],
@"mtime": @(mtime ? [mtime timeIntervalSince1970] : 0),
}];
}
NSMutableDictionary *r = make_response(@"ok", reqId);
r[@"entries"] = entries;
return r;
}
// -- file_get: download file from guest to host --
if ([type isEqualToString:@"file_get"]) {
NSString *path = msg[@"path"];
if (!path) {
NSMutableDictionary *r = make_response(@"err", reqId);
r[@"msg"] = @"missing path";
return r;
}
struct stat st;
if (stat([path fileSystemRepresentation], &st) != 0) {
NSMutableDictionary *r = make_response(@"err", reqId);
r[@"msg"] = [NSString stringWithFormat:@"stat failed: %s", strerror(errno)];
return r;
}
if (!S_ISREG(st.st_mode)) {
NSMutableDictionary *r = make_response(@"err", reqId);
r[@"msg"] = @"not a regular file";
return r;
}
int fileFd = open([path fileSystemRepresentation], O_RDONLY);
if (fileFd < 0) {
NSMutableDictionary *r = make_response(@"err", reqId);
r[@"msg"] = [NSString stringWithFormat:@"open failed: %s", strerror(errno)];
return r;
}
// Send header with file size
NSMutableDictionary *header = make_response(@"file_data", reqId);
header[@"size"] = @((unsigned long long)st.st_size);
if (!write_message(fd, header)) {
close(fileFd);
return nil;
}
// Stream file data in chunks
uint8_t buf[32768];
ssize_t n;
while ((n = read(fileFd, buf, sizeof(buf))) > 0) {
if (!write_fully(fd, buf, (size_t)n)) {
NSLog(@"vphoned: file_get write failed for %@", path);
close(fileFd);
return nil;
}
}
close(fileFd);
return nil; // Response already written inline
}
// -- file_put: upload file from host to guest --
if ([type isEqualToString:@"file_put"]) {
NSString *path = msg[@"path"];
NSUInteger size = [msg[@"size"] unsignedIntegerValue];
NSString *perm = msg[@"perm"];
if (!path) {
// Must still drain the raw bytes to keep protocol in sync
if (size > 0) {
uint8_t drain[32768];
NSUInteger remaining = size;
while (remaining > 0) {
size_t chunk = remaining < sizeof(drain) ? remaining : sizeof(drain);
if (!read_fully(fd, drain, chunk)) break;
remaining -= chunk;
}
}
NSMutableDictionary *r = make_response(@"err", reqId);
r[@"msg"] = @"missing path";
return r;
}
// Create parent directories if needed
NSString *parent = [path stringByDeletingLastPathComponent];
[[NSFileManager defaultManager] createDirectoryAtPath:parent
withIntermediateDirectories:YES
attributes:nil
error:nil];
// Write to temp file, then rename (atomic, same pattern as receive_update)
char tmp_path[PATH_MAX];
snprintf(tmp_path, sizeof(tmp_path), "%s.XXXXXX", [path fileSystemRepresentation]);
int tmp_fd = mkstemp(tmp_path);
if (tmp_fd < 0) {
// Drain bytes
uint8_t drain[32768];
NSUInteger remaining = size;
while (remaining > 0) {
size_t chunk = remaining < sizeof(drain) ? remaining : sizeof(drain);
if (!read_fully(fd, drain, chunk)) break;
remaining -= chunk;
}
NSMutableDictionary *r = make_response(@"err", reqId);
r[@"msg"] = [NSString stringWithFormat:@"mkstemp failed: %s", strerror(errno)];
return r;
}
uint8_t buf[32768];
NSUInteger remaining = size;
BOOL ok = YES;
while (remaining > 0) {
size_t chunk = remaining < sizeof(buf) ? remaining : sizeof(buf);
if (!read_fully(fd, buf, chunk)) { ok = NO; break; }
if (write(tmp_fd, buf, chunk) != (ssize_t)chunk) { ok = NO; break; }
remaining -= chunk;
}
close(tmp_fd);
if (!ok) {
unlink(tmp_path);
NSMutableDictionary *r = make_response(@"err", reqId);
r[@"msg"] = @"file transfer failed";
return r;
}
// Set permissions
if (perm) {
unsigned long mode = strtoul([perm UTF8String], NULL, 8);
chmod(tmp_path, (mode_t)mode);
} else {
chmod(tmp_path, 0644);
}
if (rename(tmp_path, [path fileSystemRepresentation]) != 0) {
unlink(tmp_path);
NSMutableDictionary *r = make_response(@"err", reqId);
r[@"msg"] = [NSString stringWithFormat:@"rename failed: %s", strerror(errno)];
return r;
}
NSLog(@"vphoned: file_put %@ (%lu bytes)", path, (unsigned long)size);
return make_response(@"ok", reqId);
}
// -- file_mkdir --
if ([type isEqualToString:@"file_mkdir"]) {
NSString *path = msg[@"path"];
if (!path) {
NSMutableDictionary *r = make_response(@"err", reqId);
r[@"msg"] = @"missing path";
return r;
}
NSError *err = nil;
if (![[NSFileManager defaultManager] createDirectoryAtPath:path
withIntermediateDirectories:YES
attributes:nil
error:&err]) {
NSMutableDictionary *r = make_response(@"err", reqId);
r[@"msg"] = err.localizedDescription ?: @"mkdir failed";
return r;
}
return make_response(@"ok", reqId);
}
// -- file_delete --
if ([type isEqualToString:@"file_delete"]) {
NSString *path = msg[@"path"];
if (!path) {
NSMutableDictionary *r = make_response(@"err", reqId);
r[@"msg"] = @"missing path";
return r;
}
NSError *err = nil;
if (![[NSFileManager defaultManager] removeItemAtPath:path error:&err]) {
NSMutableDictionary *r = make_response(@"err", reqId);
r[@"msg"] = err.localizedDescription ?: @"delete failed";
return r;
}
return make_response(@"ok", reqId);
}
// -- file_rename --
if ([type isEqualToString:@"file_rename"]) {
NSString *from = msg[@"from"];
NSString *to = msg[@"to"];
if (!from || !to) {
NSMutableDictionary *r = make_response(@"err", reqId);
r[@"msg"] = @"missing from/to";
return r;
}
NSError *err = nil;
if (![[NSFileManager defaultManager] moveItemAtPath:from toPath:to error:&err]) {
NSMutableDictionary *r = make_response(@"err", reqId);
r[@"msg"] = err.localizedDescription ?: @"rename failed";
return r;
}
return make_response(@"ok", reqId);
}
NSMutableDictionary *r = make_response(@"err", reqId);
r[@"msg"] = [NSString stringWithFormat:@"unknown file command: %@", type];
return r;
}
// MARK: - Command Dispatch
static NSDictionary *handle_command(NSDictionary *msg) {
NSString *type = msg[@"t"];
id reqId = msg[@"id"];
if ([type isEqualToString:@"hid"]) {
uint32_t page = [msg[@"page"] unsignedIntValue];
uint32_t usage = [msg[@"usage"] unsignedIntValue];
NSNumber *downVal = msg[@"down"];
if (downVal != nil) {
// Single down or up event (for modifier combos)
IOHIDEventRef ev = pKeyboard(kCFAllocatorDefault, mach_absolute_time(),
page, usage, [downVal boolValue] ? 1 : 0, 0);
if (ev) { send_hid_event(ev); CFRelease(ev); }
} else {
// Full press (down + 100ms + up)
press(page, usage);
}
return make_response(@"ok", reqId);
}
if ([type isEqualToString:@"devmode"]) {
if (!pXpcCreateMach) {
NSMutableDictionary *r = make_response(@"err", reqId);
r[@"msg"] = @"XPC not available";
return r;
}
NSString *action = msg[@"action"];
if ([action isEqualToString:@"status"]) {
BOOL enabled = devmode_status();
NSMutableDictionary *r = make_response(@"ok", reqId);
r[@"enabled"] = @(enabled);
return r;
}
if ([action isEqualToString:@"enable"]) {
BOOL alreadyEnabled = NO;
BOOL ok = devmode_arm(&alreadyEnabled);
NSMutableDictionary *r = make_response(ok ? @"ok" : @"err", reqId);
if (ok) {
r[@"already_enabled"] = @(alreadyEnabled);
r[@"msg"] = alreadyEnabled
? @"developer mode already enabled"
: @"developer mode armed, reboot to activate";
} else {
r[@"msg"] = @"failed to arm developer mode";
}
return r;
}
NSMutableDictionary *r = make_response(@"err", reqId);
r[@"msg"] = [NSString stringWithFormat:@"unknown devmode action: %@", action];
return r;
}
if ([type isEqualToString:@"ping"]) {
return make_response(@"pong", reqId);
}
if ([type isEqualToString:@"location"]) {
double lat = [msg[@"lat"] doubleValue];
double lon = [msg[@"lon"] doubleValue];
double alt = [msg[@"alt"] doubleValue];
double hacc = [msg[@"hacc"] doubleValue];
double vacc = [msg[@"vacc"] doubleValue];
double speed = [msg[@"speed"] doubleValue];
double course = [msg[@"course"] doubleValue];
simulate_location(lat, lon, alt, hacc, vacc, speed, course);
return make_response(@"ok", reqId);
}
if ([type isEqualToString:@"location_stop"]) {
clear_simulated_location();
return make_response(@"ok", reqId);
}
NSMutableDictionary *r = make_response(@"err", reqId);
r[@"msg"] = [NSString stringWithFormat:@"unknown type: %@", type];
return r;
}
// MARK: - Auto-update
/// Receive raw binary from host, write to CACHE_PATH, chmod +x.
/// Returns YES on success.
static BOOL receive_update(int fd, NSUInteger size) {
mkdir(CACHE_DIR, 0755);
@@ -823,7 +97,7 @@ static BOOL receive_update(int fd, NSUInteger size) {
NSUInteger remaining = size;
while (remaining > 0) {
size_t chunk = remaining < sizeof(buf) ? remaining : sizeof(buf);
if (!read_fully(fd, buf, chunk)) {
if (!vp_read_fully(fd, buf, chunk)) {
NSLog(@"vphoned: update read failed at %lu/%lu",
(unsigned long)(size - remaining), (unsigned long)size);
close(tmp_fd);
@@ -851,13 +125,95 @@ static BOOL receive_update(int fd, NSUInteger size) {
return YES;
}
// MARK: - Command Dispatch
static NSDictionary *handle_command(NSDictionary *msg) {
NSString *type = msg[@"t"];
id reqId = msg[@"id"];
if ([type isEqualToString:@"hid"]) {
uint32_t page = [msg[@"page"] unsignedIntValue];
uint32_t usage = [msg[@"usage"] unsignedIntValue];
NSNumber *downVal = msg[@"down"];
if (downVal != nil) {
vp_hid_key(page, usage, [downVal boolValue]);
} else {
vp_hid_press(page, usage);
}
return vp_make_response(@"ok", reqId);
}
if ([type isEqualToString:@"devmode"]) {
if (!vp_devmode_available()) {
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = @"XPC not available";
return r;
}
NSString *action = msg[@"action"];
if ([action isEqualToString:@"status"]) {
BOOL enabled = vp_devmode_status();
NSMutableDictionary *r = vp_make_response(@"ok", reqId);
r[@"enabled"] = @(enabled);
return r;
}
if ([action isEqualToString:@"enable"]) {
BOOL alreadyEnabled = NO;
BOOL ok = vp_devmode_arm(&alreadyEnabled);
NSMutableDictionary *r = vp_make_response(ok ? @"ok" : @"err", reqId);
if (ok) {
r[@"already_enabled"] = @(alreadyEnabled);
r[@"msg"] = alreadyEnabled
? @"developer mode already enabled"
: @"developer mode armed, reboot to activate";
} else {
r[@"msg"] = @"failed to arm developer mode";
}
return r;
}
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = [NSString stringWithFormat:@"unknown devmode action: %@", action];
return r;
}
if ([type isEqualToString:@"ping"]) {
return vp_make_response(@"pong", reqId);
}
if ([type isEqualToString:@"location"]) {
double lat = [msg[@"lat"] doubleValue];
double lon = [msg[@"lon"] doubleValue];
double alt = [msg[@"alt"] doubleValue];
double hacc = [msg[@"hacc"] doubleValue];
double vacc = [msg[@"vacc"] doubleValue];
double speed = [msg[@"speed"] doubleValue];
double course = [msg[@"course"] doubleValue];
vp_location_simulate(lat, lon, alt, hacc, vacc, speed, course);
return vp_make_response(@"ok", reqId);
}
if ([type isEqualToString:@"location_stop"]) {
vp_location_clear();
return vp_make_response(@"ok", reqId);
}
if ([type isEqualToString:@"version"]) {
NSMutableDictionary *r = vp_make_response(@"version", reqId);
r[@"hash"] = @VPHONED_BUILD_HASH;
return r;
}
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = [NSString stringWithFormat:@"unknown type: %@", type];
return r;
}
// MARK: - Client Session
/// Returns YES if daemon should exit for restart (after update).
static BOOL handle_client(int fd) {
BOOL should_restart = NO;
@autoreleasepool {
NSDictionary *hello = read_message(fd);
NSDictionary *hello = vp_read_message(fd);
if (!hello) { close(fd); return NO; }
NSInteger version = [hello[@"v"] integerValue];
@@ -872,7 +228,7 @@ static BOOL handle_client(int fd) {
if (version != PROTOCOL_VERSION) {
NSLog(@"vphoned: version mismatch (client v%ld, daemon v%d)",
(long)version, PROTOCOL_VERSION);
write_message(fd, @{@"v": @PROTOCOL_VERSION, @"t": @"err",
vp_write_message(fd, @{@"v": @PROTOCOL_VERSION, @"t": @"err",
@"msg": @"version mismatch"});
close(fd);
return NO;
@@ -892,20 +248,24 @@ static BOOL handle_client(int fd) {
}
}
// Build capabilities list
NSMutableArray *caps = [NSMutableArray arrayWithObjects:@"hid", @"devmode", @"file", nil];
if (vp_location_available()) [caps addObject:@"location"];
NSMutableDictionary *helloResp = [@{
@"v": @PROTOCOL_VERSION,
@"t": @"hello",
@"name": @"vphoned",
@"caps": gLocationLoaded ? @[@"hid", @"devmode", @"file", @"location"] : @[@"hid", @"devmode", @"file"],
@"caps": caps,
} mutableCopy];
if (needUpdate) helloResp[@"need_update"] = @YES;
if (!write_message(fd, helloResp)) { close(fd); return NO; }
if (!vp_write_message(fd, helloResp)) { close(fd); return NO; }
NSLog(@"vphoned: client connected (v%d)%s",
PROTOCOL_VERSION, needUpdate ? " [update pending]" : "");
NSDictionary *msg;
while ((msg = read_message(fd)) != nil) {
while ((msg = vp_read_message(fd)) != nil) {
@autoreleasepool {
NSString *t = msg[@"t"];
NSLog(@"vphoned: recv cmd: %@", t);
@@ -915,28 +275,28 @@ static BOOL handle_client(int fd) {
id reqId = msg[@"id"];
NSLog(@"vphoned: receiving update (%lu bytes)", (unsigned long)size);
if (size > 0 && size < 10 * 1024 * 1024 && receive_update(fd, size)) {
NSMutableDictionary *r = make_response(@"ok", reqId);
NSMutableDictionary *r = vp_make_response(@"ok", reqId);
r[@"msg"] = @"updated, restarting";
write_message(fd, r);
vp_write_message(fd, r);
should_restart = YES;
break;
} else {
NSMutableDictionary *r = make_response(@"err", reqId);
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = @"update failed";
write_message(fd, r);
vp_write_message(fd, r);
}
continue;
}
// File operations (need fd for inline binary transfer)
if ([t hasPrefix:@"file_"]) {
NSDictionary *resp = handle_file_command(fd, msg);
if (resp && !write_message(fd, resp)) break;
NSDictionary *resp = vp_handle_file_command(fd, msg);
if (resp && !vp_write_message(fd, resp)) break;
continue;
}
NSDictionary *resp = handle_command(msg);
if (resp && !write_message(fd, resp)) break;
if (resp && !vp_write_message(fd, resp)) break;
}
}
@@ -961,9 +321,9 @@ int main(int argc, char *argv[]) {
NSLog(@"vphoned: starting (pid=%d, path=%s)", getpid(), selfPath ?: "?");
if (!load_iokit()) return 1;
if (!load_xpc()) NSLog(@"vphoned: XPC unavailable, devmode disabled");
load_corelocation();
if (!vp_hid_load()) return 1;
if (!vp_devmode_load()) NSLog(@"vphoned: XPC unavailable, devmode disabled");
vp_location_load();
int sock = socket(AF_VSOCK, SOCK_STREAM, 0);
if (sock < 0) { perror("vphoned: socket(AF_VSOCK)"); return 1; }

View File

@@ -0,0 +1,22 @@
/*
* vphoned_devmode — Developer mode control via AMFI XPC.
*
* Talks to com.apple.amfi.xpc to query / arm developer mode.
* Reference: TrollStore RootHelper/devmode.m
* Requires entitlement: com.apple.private.amfi.developer-mode-control
*/
#pragma once
#import <Foundation/Foundation.h>
/// Load XPC/CoreFoundation symbols. Returns NO on failure (devmode disabled).
BOOL vp_devmode_load(void);
/// Whether devmode XPC is available (load succeeded).
BOOL vp_devmode_available(void);
/// Query current developer mode status.
BOOL vp_devmode_status(void);
/// Arm developer mode. Returns YES on success. Sets *alreadyEnabled if already on.
BOOL vp_devmode_arm(BOOL *alreadyEnabled);

View File

@@ -0,0 +1,107 @@
#import "vphoned_devmode.h"
#include <dlfcn.h>
// XPC functions resolved via dlsym to avoid iOS SDK availability
// guards (xpc_connection_create_mach_service is marked unavailable
// on iOS but works at runtime with the right entitlements).
typedef void *xpc_conn_t; // opaque, avoids typedef conflict with SDK
typedef void *xpc_obj_t;
static xpc_conn_t (*pXpcCreateMach)(const char *, dispatch_queue_t, uint64_t);
static void (*pXpcSetHandler)(xpc_conn_t, void (^)(xpc_obj_t));
static void (*pXpcResume)(xpc_conn_t);
static void (*pXpcCancel)(xpc_conn_t);
static xpc_obj_t (*pXpcSendSync)(xpc_conn_t, xpc_obj_t);
static xpc_obj_t (*pXpcDictGet)(xpc_obj_t, const char *);
static xpc_obj_t (*pCFToXPC)(CFTypeRef);
static CFTypeRef (*pXPCToCF)(xpc_obj_t);
static BOOL gXPCLoaded = NO;
BOOL vp_devmode_load(void) {
void *libxpc = dlopen("/usr/lib/system/libxpc.dylib", RTLD_NOW);
if (!libxpc) { NSLog(@"vphoned: dlopen libxpc failed"); return NO; }
void *libcf = dlopen("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", RTLD_NOW);
if (!libcf) { NSLog(@"vphoned: dlopen CoreFoundation failed"); return NO; }
pXpcCreateMach = dlsym(libxpc, "xpc_connection_create_mach_service");
pXpcSetHandler = dlsym(libxpc, "xpc_connection_set_event_handler");
pXpcResume = dlsym(libxpc, "xpc_connection_resume");
pXpcCancel = dlsym(libxpc, "xpc_connection_cancel");
pXpcSendSync = dlsym(libxpc, "xpc_connection_send_message_with_reply_sync");
pXpcDictGet = dlsym(libxpc, "xpc_dictionary_get_value");
pCFToXPC = dlsym(libcf, "_CFXPCCreateXPCMessageWithCFObject");
pXPCToCF = dlsym(libcf, "_CFXPCCreateCFObjectFromXPCMessage");
if (!pXpcCreateMach || !pXpcSetHandler || !pXpcResume || !pXpcCancel ||
!pXpcSendSync || !pXpcDictGet || !pCFToXPC || !pXPCToCF) {
NSLog(@"vphoned: missing XPC/CF symbols");
return NO;
}
NSLog(@"vphoned: XPC loaded");
gXPCLoaded = YES;
return YES;
}
BOOL vp_devmode_available(void) {
return gXPCLoaded;
}
// MARK: - AMFI XPC
typedef enum {
kAMFIActionArm = 0, // arm developer mode (prompts on next reboot)
kAMFIActionDisable = 1, // disable developer mode immediately
kAMFIActionStatus = 2, // query: {success, status, armed}
} AMFIXPCAction;
static NSDictionary *amfi_send(AMFIXPCAction action) {
xpc_conn_t conn = pXpcCreateMach("com.apple.amfi.xpc", NULL, 0);
if (!conn) {
NSLog(@"vphoned: amfi xpc connection failed");
return nil;
}
pXpcSetHandler(conn, ^(xpc_obj_t event) {});
pXpcResume(conn);
xpc_obj_t msg = pCFToXPC((__bridge CFDictionaryRef)@{@"action": @(action)});
xpc_obj_t reply = pXpcSendSync(conn, msg);
pXpcCancel(conn);
if (!reply) {
NSLog(@"vphoned: amfi xpc no reply");
return nil;
}
xpc_obj_t cfReply = pXpcDictGet(reply, "cfreply");
if (!cfReply) {
NSLog(@"vphoned: amfi xpc no cfreply");
return nil;
}
NSDictionary *dict = (__bridge_transfer NSDictionary *)pXPCToCF(cfReply);
NSLog(@"vphoned: amfi reply: %@", dict);
return dict;
}
BOOL vp_devmode_status(void) {
NSDictionary *reply = amfi_send(kAMFIActionStatus);
if (!reply) return NO;
NSNumber *success = reply[@"success"];
if (!success || ![success boolValue]) return NO;
NSNumber *status = reply[@"status"];
return [status boolValue];
}
BOOL vp_devmode_arm(BOOL *alreadyEnabled) {
BOOL enabled = vp_devmode_status();
if (alreadyEnabled) *alreadyEnabled = enabled;
if (enabled) return YES;
NSDictionary *reply = amfi_send(kAMFIActionArm);
if (!reply) return NO;
NSNumber *success = reply[@"success"];
return success && [success boolValue];
}

View File

@@ -0,0 +1,13 @@
/*
* vphoned_files — Remote file operations over vsock.
*
* Handles file_list, file_get, file_put, file_mkdir, file_delete, file_rename.
* file_get and file_put perform inline binary I/O on the socket.
*/
#pragma once
#import <Foundation/Foundation.h>
/// Handle a file command. Returns a response dict, or nil if the response
/// was already written inline (e.g. file_get with streaming data).
NSDictionary *vp_handle_file_command(int fd, NSDictionary *msg);

View File

@@ -0,0 +1,236 @@
#import "vphoned_files.h"
#import "vphoned_protocol.h"
#include <sys/stat.h>
#include <unistd.h>
NSDictionary *vp_handle_file_command(int fd, NSDictionary *msg) {
NSString *type = msg[@"t"];
id reqId = msg[@"id"];
// -- file_list: list directory contents --
if ([type isEqualToString:@"file_list"]) {
NSString *path = msg[@"path"];
if (!path) {
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = @"missing path";
return r;
}
NSFileManager *fm = [NSFileManager defaultManager];
NSError *err = nil;
NSArray *contents = [fm contentsOfDirectoryAtPath:path error:&err];
if (!contents) {
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = err.localizedDescription ?: @"list failed";
return r;
}
NSMutableArray *entries = [NSMutableArray arrayWithCapacity:contents.count];
for (NSString *name in contents) {
NSString *full = [path stringByAppendingPathComponent:name];
NSDictionary *attrs = [fm attributesOfItemAtPath:full error:nil];
if (!attrs) continue;
NSString *fileType = attrs[NSFileType];
NSString *typeStr = @"file";
if ([fileType isEqualToString:NSFileTypeDirectory]) typeStr = @"dir";
else if ([fileType isEqualToString:NSFileTypeSymbolicLink]) typeStr = @"link";
NSNumber *size = attrs[NSFileSize] ?: @0;
NSDate *mtime = attrs[NSFileModificationDate];
NSNumber *posixPerms = attrs[NSFilePosixPermissions];
[entries addObject:@{
@"name": name,
@"type": typeStr,
@"size": size,
@"perm": [NSString stringWithFormat:@"%lo", [posixPerms unsignedLongValue]],
@"mtime": @(mtime ? [mtime timeIntervalSince1970] : 0),
}];
}
NSMutableDictionary *r = vp_make_response(@"ok", reqId);
r[@"entries"] = entries;
return r;
}
// -- file_get: download file from guest to host --
if ([type isEqualToString:@"file_get"]) {
NSString *path = msg[@"path"];
if (!path) {
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = @"missing path";
return r;
}
int fileFd = open([path fileSystemRepresentation], O_RDONLY);
if (fileFd < 0) {
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = [NSString stringWithFormat:@"open failed: %s", strerror(errno)];
return r;
}
struct stat st;
if (fstat(fileFd, &st) != 0) {
close(fileFd);
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = [NSString stringWithFormat:@"stat failed: %s", strerror(errno)];
return r;
}
if (!S_ISREG(st.st_mode)) {
close(fileFd);
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = @"not a regular file";
return r;
}
// Send header with file size
NSMutableDictionary *header = vp_make_response(@"file_data", reqId);
header[@"size"] = @((unsigned long long)st.st_size);
if (!vp_write_message(fd, header)) {
close(fileFd);
return nil;
}
// Stream file data in chunks
uint8_t buf[32768];
ssize_t n;
while ((n = read(fileFd, buf, sizeof(buf))) > 0) {
if (!vp_write_fully(fd, buf, (size_t)n)) {
NSLog(@"vphoned: file_get write failed for %@", path);
close(fileFd);
return nil;
}
}
close(fileFd);
return nil; // Response already written inline
}
// -- file_put: upload file from host to guest --
if ([type isEqualToString:@"file_put"]) {
NSString *path = msg[@"path"];
NSUInteger size = [msg[@"size"] unsignedIntegerValue];
NSString *perm = msg[@"perm"];
if (!path) {
// Must still drain the raw bytes to keep protocol in sync
if (size > 0) vp_drain(fd, size);
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = @"missing path";
return r;
}
// Create parent directories if needed
NSString *parent = [path stringByDeletingLastPathComponent];
[[NSFileManager defaultManager] createDirectoryAtPath:parent
withIntermediateDirectories:YES
attributes:nil
error:nil];
// Write to temp file, then rename (atomic)
char tmp_path[PATH_MAX];
snprintf(tmp_path, sizeof(tmp_path), "%s.XXXXXX", [path fileSystemRepresentation]);
int tmp_fd = mkstemp(tmp_path);
if (tmp_fd < 0) {
vp_drain(fd, size);
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = [NSString stringWithFormat:@"mkstemp failed: %s", strerror(errno)];
return r;
}
uint8_t buf[32768];
NSUInteger remaining = size;
BOOL ok = YES;
while (remaining > 0) {
size_t chunk = remaining < sizeof(buf) ? remaining : sizeof(buf);
if (!vp_read_fully(fd, buf, chunk)) { ok = NO; break; }
if (write(tmp_fd, buf, chunk) != (ssize_t)chunk) { ok = NO; break; }
remaining -= chunk;
}
close(tmp_fd);
if (!ok) {
unlink(tmp_path);
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = @"file transfer failed";
return r;
}
// Set permissions
if (perm) {
unsigned long mode = strtoul([perm UTF8String], NULL, 8);
chmod(tmp_path, (mode_t)mode);
} else {
chmod(tmp_path, 0644);
}
if (rename(tmp_path, [path fileSystemRepresentation]) != 0) {
unlink(tmp_path);
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = [NSString stringWithFormat:@"rename failed: %s", strerror(errno)];
return r;
}
NSLog(@"vphoned: file_put %@ (%lu bytes)", path, (unsigned long)size);
return vp_make_response(@"ok", reqId);
}
// -- file_mkdir --
if ([type isEqualToString:@"file_mkdir"]) {
NSString *path = msg[@"path"];
if (!path) {
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = @"missing path";
return r;
}
NSError *err = nil;
if (![[NSFileManager defaultManager] createDirectoryAtPath:path
withIntermediateDirectories:YES
attributes:nil
error:&err]) {
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = err.localizedDescription ?: @"mkdir failed";
return r;
}
return vp_make_response(@"ok", reqId);
}
// -- file_delete --
if ([type isEqualToString:@"file_delete"]) {
NSString *path = msg[@"path"];
if (!path) {
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = @"missing path";
return r;
}
NSError *err = nil;
if (![[NSFileManager defaultManager] removeItemAtPath:path error:&err]) {
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = err.localizedDescription ?: @"delete failed";
return r;
}
return vp_make_response(@"ok", reqId);
}
// -- file_rename --
if ([type isEqualToString:@"file_rename"]) {
NSString *from = msg[@"from"];
NSString *to = msg[@"to"];
if (!from || !to) {
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = @"missing from/to";
return r;
}
NSError *err = nil;
if (![[NSFileManager defaultManager] moveItemAtPath:from toPath:to error:&err]) {
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = err.localizedDescription ?: @"rename failed";
return r;
}
return vp_make_response(@"ok", reqId);
}
NSMutableDictionary *r = vp_make_response(@"err", reqId);
r[@"msg"] = [NSString stringWithFormat:@"unknown file command: %@", type];
return r;
}

View File

@@ -0,0 +1,18 @@
/*
* vphoned_hid — HID event injection via IOKit private API.
*
* Matches TrollVNC's STHIDEventGenerator approach: create an
* IOHIDEventSystemClient, fabricate keyboard events, and dispatch.
*/
#pragma once
#import <Foundation/Foundation.h>
/// Load IOKit symbols and create HID event client. Returns NO on failure.
BOOL vp_hid_load(void);
/// Send a full key press (down + 100ms delay + up).
void vp_hid_press(uint32_t page, uint32_t usage);
/// Send a single key down or key up event.
void vp_hid_key(uint32_t page, uint32_t usage, BOOL down);

View File

@@ -0,0 +1,72 @@
#import "vphoned_hid.h"
#include <dlfcn.h>
#include <mach/mach_time.h>
#include <unistd.h>
typedef void *IOHIDEventSystemClientRef;
typedef void *IOHIDEventRef;
static IOHIDEventSystemClientRef (*pCreate)(CFAllocatorRef);
static IOHIDEventRef (*pKeyboard)(CFAllocatorRef, uint64_t,
uint32_t, uint32_t, int, int);
static void (*pSetSender)(IOHIDEventRef, uint64_t);
static void (*pDispatch)(IOHIDEventSystemClientRef, IOHIDEventRef);
static IOHIDEventSystemClientRef gClient;
static dispatch_queue_t gHIDQueue;
BOOL vp_hid_load(void) {
void *h = dlopen("/System/Library/Frameworks/IOKit.framework/IOKit", RTLD_NOW);
if (!h) { NSLog(@"vphoned: dlopen IOKit failed"); return NO; }
pCreate = dlsym(h, "IOHIDEventSystemClientCreate");
pKeyboard = dlsym(h, "IOHIDEventCreateKeyboardEvent");
pSetSender = dlsym(h, "IOHIDEventSetSenderID");
pDispatch = dlsym(h, "IOHIDEventSystemClientDispatchEvent");
if (!pCreate || !pKeyboard || !pSetSender || !pDispatch) {
NSLog(@"vphoned: missing IOKit symbols");
return NO;
}
gClient = pCreate(kCFAllocatorDefault);
if (!gClient) { NSLog(@"vphoned: IOHIDEventSystemClientCreate returned NULL"); return NO; }
dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(
DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0);
gHIDQueue = dispatch_queue_create("com.vphone.vphoned.hid", attr);
NSLog(@"vphoned: IOKit loaded");
return YES;
}
static void send_hid_event(IOHIDEventRef event) {
IOHIDEventRef strong = (IOHIDEventRef)CFRetain(event);
dispatch_async(gHIDQueue, ^{
pSetSender(strong, 0x8000000817319372);
pDispatch(gClient, strong);
CFRelease(strong);
});
}
void vp_hid_press(uint32_t page, uint32_t usage) {
IOHIDEventRef down = pKeyboard(kCFAllocatorDefault, mach_absolute_time(),
page, usage, 1, 0);
if (!down) return;
send_hid_event(down);
CFRelease(down);
usleep(100000);
IOHIDEventRef up = pKeyboard(kCFAllocatorDefault, mach_absolute_time(),
page, usage, 0, 0);
if (!up) return;
send_hid_event(up);
CFRelease(up);
}
void vp_hid_key(uint32_t page, uint32_t usage, BOOL down) {
IOHIDEventRef ev = pKeyboard(kCFAllocatorDefault, mach_absolute_time(),
page, usage, down ? 1 : 0, 0);
if (ev) { send_hid_event(ev); CFRelease(ev); }
}

View File

@@ -0,0 +1,24 @@
/*
* vphoned_location — CoreLocation simulation via CLSimulationManager.
*
* Uses private CLSimulationManager API to inject simulated GPS coordinates
* into the guest. Probes available selectors at runtime since the API
* varies across iOS versions.
*/
#pragma once
#import <Foundation/Foundation.h>
/// Load CoreLocation and probe CLSimulationManager selectors. Returns NO on failure.
BOOL vp_location_load(void);
/// Whether location simulation is available (load succeeded).
BOOL vp_location_available(void);
/// Simulate a GPS location update.
void vp_location_simulate(double lat, double lon, double alt,
double hacc, double vacc,
double speed, double course);
/// Clear simulated location.
void vp_location_clear(void);

View File

@@ -0,0 +1,164 @@
#import "vphoned_location.h"
#include <dlfcn.h>
#include <objc/runtime.h>
#include <objc/message.h>
static id gSimManager = nil;
static SEL gSetLocationSel = NULL;
static SEL gClearLocationsSel = NULL;
static SEL gFlushSel = NULL;
static SEL gStartSimSel = NULL;
static BOOL gLocationLoaded = NO;
BOOL vp_location_load(void) {
void *h = dlopen("/System/Library/Frameworks/CoreLocation.framework/CoreLocation", RTLD_NOW);
if (!h) { NSLog(@"vphoned: dlopen CoreLocation failed"); return NO; }
Class cls = NSClassFromString(@"CLSimulationManager");
if (!cls) { NSLog(@"vphoned: CLSimulationManager not found"); return NO; }
gSimManager = [[cls alloc] init];
if (!gSimManager) { NSLog(@"vphoned: CLSimulationManager alloc/init failed"); return NO; }
// Probe available selectors for setting location
SEL candidates[] = {
NSSelectorFromString(@"setSimulatedLocation:"),
NSSelectorFromString(@"appendSimulatedLocation:"),
NSSelectorFromString(@"setLocation:"),
};
for (int i = 0; i < 3; i++) {
if ([gSimManager respondsToSelector:candidates[i]]) {
gSetLocationSel = candidates[i];
break;
}
}
if (!gSetLocationSel) {
NSLog(@"vphoned: no set-location selector found, dumping methods:");
unsigned int count = 0;
Method *methods = class_copyMethodList([gSimManager class], &count);
for (unsigned int i = 0; i < count; i++) {
NSLog(@" %s", sel_getName(method_getName(methods[i])));
}
free(methods);
return NO;
}
// Probe clear selector
SEL clearCandidates[] = {
NSSelectorFromString(@"clearSimulatedLocations"),
NSSelectorFromString(@"stopLocationSimulation"),
};
for (int i = 0; i < 2; i++) {
if ([gSimManager respondsToSelector:clearCandidates[i]]) {
gClearLocationsSel = clearCandidates[i];
break;
}
}
// Probe flush selector
SEL flushCandidates[] = {
NSSelectorFromString(@"flush"),
NSSelectorFromString(@"flushSimulatedLocations"),
};
for (int i = 0; i < 2; i++) {
if ([gSimManager respondsToSelector:flushCandidates[i]]) {
gFlushSel = flushCandidates[i];
break;
}
}
// Probe startLocationSimulation selector
SEL startCandidates[] = {
NSSelectorFromString(@"startLocationSimulation"),
NSSelectorFromString(@"startSimulation"),
};
for (int i = 0; i < 2; i++) {
if ([gSimManager respondsToSelector:startCandidates[i]]) {
gStartSimSel = startCandidates[i];
break;
}
}
// Start simulation session if available
if (gStartSimSel) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[gSimManager performSelector:gStartSimSel];
#pragma clang diagnostic pop
}
NSLog(@"vphoned: CoreLocation simulation loaded (set=%s, clear=%s, flush=%s, start=%s)",
sel_getName(gSetLocationSel),
gClearLocationsSel ? sel_getName(gClearLocationsSel) : "(none)",
gFlushSel ? sel_getName(gFlushSel) : "(none)",
gStartSimSel ? sel_getName(gStartSimSel) : "(none)");
gLocationLoaded = YES;
return YES;
}
BOOL vp_location_available(void) {
return gLocationLoaded;
}
void vp_location_simulate(double lat, double lon, double alt,
double hacc, double vacc,
double speed, double course) {
if (!gLocationLoaded || !gSimManager || !gSetLocationSel) return;
@try {
typedef struct { double latitude; double longitude; } CLCoord2D;
CLCoord2D coord = {lat, lon};
Class locClass = NSClassFromString(@"CLLocation");
id locInst = [locClass alloc];
// Try full init including speed and course
SEL initSel = NSSelectorFromString(
@"initWithCoordinate:altitude:horizontalAccuracy:verticalAccuracy:course:speed:timestamp:");
if (![locInst respondsToSelector:initSel]) {
// Fallback to simpler init
initSel = NSSelectorFromString(
@"initWithCoordinate:altitude:horizontalAccuracy:verticalAccuracy:timestamp:");
typedef id (*InitFunc5)(id, SEL, CLCoord2D, double, double, double, id);
id location = ((InitFunc5)objc_msgSend)(locInst, initSel, coord, alt, hacc, vacc, [NSDate date]);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[gSimManager performSelector:gSetLocationSel withObject:location];
if (gFlushSel) [gSimManager performSelector:gFlushSel];
#pragma clang diagnostic pop
NSLog(@"vphoned: simulate_location lat=%.6f lon=%.6f (fallback init)", lat, lon);
return;
}
typedef id (*InitFunc7)(id, SEL, CLCoord2D, double, double, double, double, double, id);
id location = ((InitFunc7)objc_msgSend)(locInst, initSel, coord, alt, hacc, vacc, course, speed, [NSDate date]);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[gSimManager performSelector:gSetLocationSel withObject:location];
if (gFlushSel) [gSimManager performSelector:gFlushSel];
#pragma clang diagnostic pop
NSLog(@"vphoned: simulate_location lat=%.6f lon=%.6f alt=%.1f spd=%.1f crs=%.1f",
lat, lon, alt, speed, course);
} @catch (NSException *e) {
NSLog(@"vphoned: simulate_location exception: %@", e);
}
}
void vp_location_clear(void) {
if (!gLocationLoaded || !gSimManager) return;
@try {
if (gClearLocationsSel) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[gSimManager performSelector:gClearLocationsSel];
#pragma clang diagnostic pop
NSLog(@"vphoned: cleared simulated location");
}
} @catch (NSException *e) {
NSLog(@"vphoned: clear_simulated_location exception: %@", e);
}
}

View File

@@ -0,0 +1,22 @@
/*
* vphoned_protocol — Length-prefixed JSON framing over vsock.
*
* Each message: [uint32 big-endian length][UTF-8 JSON payload]
*/
#pragma once
#import <Foundation/Foundation.h>
#define PROTOCOL_VERSION 1
BOOL vp_read_fully(int fd, void *buf, size_t count);
BOOL vp_write_fully(int fd, const void *buf, size_t count);
/// Discard exactly `size` bytes from fd. Used to keep protocol in sync on error paths.
void vp_drain(int fd, size_t size);
NSDictionary *vp_read_message(int fd);
BOOL vp_write_message(int fd, NSDictionary *dict);
/// Build a response dict with protocol version, type, and optional request ID echo.
NSMutableDictionary *vp_make_response(NSString *type, id reqId);

View File

@@ -0,0 +1,64 @@
#import "vphoned_protocol.h"
#include <unistd.h>
BOOL vp_read_fully(int fd, void *buf, size_t count) {
size_t offset = 0;
while (offset < count) {
ssize_t n = read(fd, (uint8_t *)buf + offset, count - offset);
if (n <= 0) return NO;
offset += n;
}
return YES;
}
BOOL vp_write_fully(int fd, const void *buf, size_t count) {
size_t offset = 0;
while (offset < count) {
ssize_t n = write(fd, (const uint8_t *)buf + offset, count - offset);
if (n <= 0) return NO;
offset += n;
}
return YES;
}
void vp_drain(int fd, size_t size) {
uint8_t buf[32768];
size_t remaining = size;
while (remaining > 0) {
size_t chunk = remaining < sizeof(buf) ? remaining : sizeof(buf);
if (!vp_read_fully(fd, buf, chunk)) break;
remaining -= chunk;
}
}
NSDictionary *vp_read_message(int fd) {
uint32_t header = 0;
if (!vp_read_fully(fd, &header, 4)) return nil;
uint32_t length = ntohl(header);
if (length == 0 || length > 4 * 1024 * 1024) return nil;
NSMutableData *payload = [NSMutableData dataWithLength:length];
if (!vp_read_fully(fd, payload.mutableBytes, length)) return nil;
NSError *err = nil;
id obj = [NSJSONSerialization JSONObjectWithData:payload options:0 error:&err];
if (![obj isKindOfClass:[NSDictionary class]]) return nil;
return obj;
}
BOOL vp_write_message(int fd, NSDictionary *dict) {
NSError *err = nil;
NSData *json = [NSJSONSerialization dataWithJSONObject:dict options:0 error:&err];
if (!json) return NO;
uint32_t header = htonl((uint32_t)json.length);
if (!vp_write_fully(fd, &header, 4)) return NO;
if (!vp_write_fully(fd, json.bytes, json.length)) return NO;
return YES;
}
NSMutableDictionary *vp_make_response(NSString *type, id reqId) {
NSMutableDictionary *r = [@{@"v": @PROTOCOL_VERSION, @"t": type} mutableCopy];
if (reqId) r[@"id"] = reqId;
return r;
}

View File

@@ -4,7 +4,7 @@ import Virtualization
class VPhoneAppDelegate: NSObject, NSApplicationDelegate {
private let cli: VPhoneCLI
private var vm: VPhoneVM?
private var vm: VPhoneVirtualMachine?
private var control: VPhoneControl?
private var windowController: VPhoneWindowController?
private var menuController: VPhoneMenuController?
@@ -31,7 +31,7 @@ class VPhoneAppDelegate: NSObject, NSApplicationDelegate {
Task { @MainActor in
do {
try await self.startVM()
try await self.startVirtualMachine()
} catch {
print("[vphone] Fatal: \(error)")
NSApp.terminate(nil)
@@ -40,7 +40,7 @@ class VPhoneAppDelegate: NSObject, NSApplicationDelegate {
}
@MainActor
private func startVM() async throws {
private func startVirtualMachine() async throws {
let romURL = URL(fileURLWithPath: cli.rom)
guard FileManager.default.fileExists(atPath: romURL.path) else {
throw VPhoneError.romNotFound(cli.rom)
@@ -67,7 +67,7 @@ class VPhoneAppDelegate: NSObject, NSApplicationDelegate {
print(" rom : \(cli.sepRom)")
print("")
let options = VPhoneVM.Options(
let options = VPhoneVirtualMachine.Options(
romURL: romURL,
nvramURL: nvramURL,
machineIDURL: machineIDURL,
@@ -82,7 +82,7 @@ class VPhoneAppDelegate: NSObject, NSApplicationDelegate {
screenScale: cli.screenScale
)
let vm = try VPhoneVM(options: options)
let vm = try VPhoneVirtualMachine(options: options)
self.vm = vm
try await vm.start(forceDFU: cli.dfu)
@@ -96,17 +96,7 @@ class VPhoneAppDelegate: NSObject, NSApplicationDelegate {
}
let provider = VPhoneLocationProvider(control: control)
self.locationProvider = provider
control.onConnect = { [weak provider] caps in
if caps.contains("location") {
provider?.startForwarding()
} else {
print("[location] guest does not support location simulation")
}
}
control.onDisconnect = { [weak provider] in
provider?.stopForwarding()
}
locationProvider = provider
if let device = vm.virtualMachine.socketDevices.first as? VZVirtioSocketDevice {
control.connect(device: device)
@@ -134,7 +124,39 @@ class VPhoneAppDelegate: NSObject, NSApplicationDelegate {
guard let fileWC, let control else { return }
fileWC.showWindow(control: control)
}
if let provider = locationProvider {
mc.locationProvider = provider
}
menuController = mc
// Wire location toggle through onConnect/onDisconnect
control.onConnect = { [weak mc, weak provider = locationProvider] caps in
if caps.contains("location") {
mc?.updateLocationCapability(available: true)
// Auto-resume if user had toggle on
if mc?.locationMenuItem?.state == .on {
provider?.startForwarding()
}
} else {
print("[location] guest does not support location simulation")
}
}
control.onDisconnect = { [weak mc, weak provider = locationProvider] in
provider?.stopForwarding()
mc?.updateLocationCapability(available: false)
}
} else if !cli.dfu {
// Headless mode: auto-start location as before (no menu exists)
control.onConnect = { [weak provider = locationProvider] caps in
if caps.contains("location") {
provider?.startForwarding()
} else {
print("[location] guest does not support location simulation")
}
}
control.onDisconnect = { [weak provider = locationProvider] in
provider?.stopForwarding()
}
}
}

View File

@@ -250,42 +250,35 @@ class VPhoneControl {
// MARK: - Developer Mode
func sendDevModeStatus() {
nextRequestId += 1
let msg: [String: Any] = [
"v": Self.protocolVersion,
"t": "devmode",
"id": String(nextRequestId, radix: 16),
"action": "status",
]
guard let fd = connection?.fileDescriptor, writeMessage(fd: fd, dict: msg) else {
print("[control] send failed (not connected)")
return
}
print("[control] devmode status query sent")
struct DevModeStatus {
let enabled: Bool
}
func sendDevModeEnable() {
nextRequestId += 1
let msg: [String: Any] = [
"v": Self.protocolVersion,
"t": "devmode",
"id": String(nextRequestId, radix: 16),
"action": "enable",
]
guard let fd = connection?.fileDescriptor, writeMessage(fd: fd, dict: msg) else {
print("[control] send failed (not connected)")
return
}
print("[control] devmode enable sent")
struct DevModeEnableResult {
let alreadyEnabled: Bool
let message: String
}
func sendPing() {
nextRequestId += 1
let msg: [String: Any] = [
"v": Self.protocolVersion, "t": "ping", "id": String(nextRequestId, radix: 16),
]
guard let fd = connection?.fileDescriptor, writeMessage(fd: fd, dict: msg) else { return }
func sendDevModeStatus() async throws -> DevModeStatus {
let (resp, _) = try await sendRequest(["t": "devmode", "action": "status"])
let enabled = resp["enabled"] as? Bool ?? false
return DevModeStatus(enabled: enabled)
}
func sendDevModeEnable() async throws -> DevModeEnableResult {
let (resp, _) = try await sendRequest(["t": "devmode", "action": "enable"])
let alreadyEnabled = resp["already_enabled"] as? Bool ?? false
let message = resp["msg"] as? String ?? ""
return DevModeEnableResult(alreadyEnabled: alreadyEnabled, message: message)
}
func sendPing() async throws {
_ = try await sendRequest(["t": "ping"])
}
func sendVersion() async throws -> String {
let (resp, _) = try await sendRequest(["t": "version"])
return resp["hash"] as? String ?? "unknown"
}
// MARK: - Async Request-Response
@@ -389,9 +382,11 @@ class VPhoneControl {
// MARK: - Location
func sendLocation(latitude: Double, longitude: Double, altitude: Double,
func sendLocation(
latitude: Double, longitude: Double, altitude: Double,
horizontalAccuracy: Double, verticalAccuracy: Double,
speed: Double, course: Double) {
speed: Double, course: Double
) {
nextRequestId += 1
let msg: [String: Any] = [
"v": Self.protocolVersion,
@@ -410,12 +405,16 @@ class VPhoneControl {
print("[control] sendLocation failed (not connected)")
return
}
print("[control] sendLocation lat=\(latitude) lon=\(longitude)")
print("[control] location lat=\(latitude) lon=\(longitude)")
}
func sendLocationStop() {
nextRequestId += 1
let msg: [String: Any] = ["v": Self.protocolVersion, "t": "location_stop", "id": String(nextRequestId, radix: 16)]
let msg: [String: Any] = [
"v": Self.protocolVersion,
"t": "location_stop",
"id": String(nextRequestId, radix: 16),
]
guard let fd = connection?.fileDescriptor, writeMessage(fd: fd, dict: msg) else { return }
}
@@ -492,6 +491,9 @@ class VPhoneControl {
if !detail.isEmpty { print("[vphoned] ok: \(detail)") }
case "pong":
print("[vphoned] pong")
case "version":
let hash = msg["hash"] as? String ?? "unknown"
print("[vphoned] build: \(hash)")
case "err":
let detail = msg["msg"] as? String ?? "unknown"
print("[vphoned] error: \(detail)")

View File

@@ -31,6 +31,7 @@ class VPhoneFileWindowController {
window.contentMinSize = NSSize(width: 500, height: 300)
window.center()
window.toolbarStyle = .unified
window.isReleasedWhenClosed = false
// Add toolbar so the unified title bar shows
let toolbar = NSToolbar(identifier: "vphone-files-toolbar")

View File

@@ -17,7 +17,7 @@ class VPhoneKeyHelper {
return arr.object(at: 0) as AnyObject
}
init(vm: VPhoneVM, control: VPhoneControl) {
init(vm: VPhoneVirtualMachine, control: VPhoneControl) {
self.vm = vm.virtualMachine
self.control = control
}

View File

@@ -25,11 +25,11 @@ class VPhoneLocationProvider: NSObject {
self?.forward(location)
}
}
self.delegateProxy = proxy
delegateProxy = proxy
let mgr = CLLocationManager()
mgr.delegate = proxy
mgr.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager = mgr
locationManager = mgr
print("[location] host location forwarding ready")
}
@@ -86,9 +86,10 @@ private class LocationDelegateProxy: NSObject, CLLocationManagerDelegate {
func locationManager(_: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
NSLog("[location] got location: %.6f,%.6f (±%.0fm)",
location.coordinate.latitude, location.coordinate.longitude,
location.horizontalAccuracy)
let c = location.coordinate
print(
"[location] got location: \(String(format: "%.6f,%.6f", c.latitude, c.longitude))\(String(format: "%.0f", location.horizontalAccuracy))m)"
)
handler(location)
}
@@ -96,12 +97,12 @@ private class LocationDelegateProxy: NSObject, CLLocationManagerDelegate {
let clErr = (error as NSError).code
// kCLErrorLocationUnknown (0) = transient, just waiting for fix
if clErr == 0 { return }
NSLog("[location] CLLocationManager error: %@ (code %ld)", error.localizedDescription, clErr)
print("[location] CLLocationManager error: \(error.localizedDescription) (code \(clErr))")
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
let status = manager.authorizationStatus
NSLog("[location] authorization status: %d", status.rawValue)
print("[location] authorization status: \(status.rawValue)")
if status == .authorized || status == .authorizedAlways {
manager.startUpdatingLocation()
}

View File

@@ -0,0 +1,91 @@
import AppKit
// MARK: - Connect Menu
extension VPhoneMenuController {
func buildConnectMenu() -> NSMenuItem {
let item = NSMenuItem()
let menu = NSMenu(title: "Connect")
menu.addItem(makeItem("File Browser", action: #selector(openFiles)))
menu.addItem(NSMenuItem.separator())
menu.addItem(makeItem("Developer Mode Status", action: #selector(devModeStatus)))
menu.addItem(makeItem("Enable Developer Mode", action: #selector(devModeEnable)))
menu.addItem(NSMenuItem.separator())
menu.addItem(makeItem("Ping", action: #selector(sendPing)))
menu.addItem(makeItem("Guest Version", action: #selector(queryGuestVersion)))
item.submenu = menu
return item
}
@objc func openFiles() {
onFilesPressed?()
}
@objc func devModeStatus() {
Task {
do {
let status = try await control.sendDevModeStatus()
showAlert(
title: "Developer Mode",
message: status.enabled ? "Developer Mode is enabled." : "Developer Mode is disabled.",
style: .informational
)
} catch {
showAlert(title: "Developer Mode", message: "\(error)", style: .warning)
}
}
}
@objc func devModeEnable() {
Task {
do {
let result = try await control.sendDevModeEnable()
showAlert(
title: "Developer Mode",
message: result.message.isEmpty
? (result.alreadyEnabled ? "Developer Mode already enabled." : "Developer Mode enabled.")
: result.message,
style: .informational
)
} catch {
showAlert(title: "Developer Mode", message: "\(error)", style: .warning)
}
}
}
@objc func sendPing() {
Task {
do {
try await control.sendPing()
showAlert(title: "Ping", message: "pong", style: .informational)
} catch {
showAlert(title: "Ping", message: "\(error)", style: .warning)
}
}
}
@objc func queryGuestVersion() {
Task {
do {
let hash = try await control.sendVersion()
showAlert(title: "Guest Version", message: "build: \(hash)", style: .informational)
} catch {
showAlert(title: "Guest Version", message: "\(error)", style: .warning)
}
}
}
// MARK: - Alert
private func showAlert(title: String, message: String, style: NSAlert.Style) {
let alert = NSAlert()
alert.messageText = title
alert.informativeText = message
alert.alertStyle = style
if let window = NSApp.keyWindow {
alert.beginSheetModal(for: window)
} else {
alert.runModal()
}
}
}

View File

@@ -5,10 +5,12 @@ import Foundation
@MainActor
class VPhoneMenuController {
private let keyHelper: VPhoneKeyHelper
private let control: VPhoneControl
let keyHelper: VPhoneKeyHelper
let control: VPhoneControl
var onFilesPressed: (() -> Void)?
var locationProvider: VPhoneLocationProvider?
var locationMenuItem: NSMenuItem?
init(keyHelper: VPhoneKeyHelper, control: VPhoneControl) {
self.keyHelper = keyHelper
@@ -24,93 +26,27 @@ class VPhoneMenuController {
// App menu
let appMenuItem = NSMenuItem()
let appMenu = NSMenu(title: "vphone")
let buildItem = NSMenuItem(title: "Build: \(VPhoneBuildInfo.commitHash)", action: nil, keyEquivalent: "")
buildItem.isEnabled = false
appMenu.addItem(buildItem)
appMenu.addItem(NSMenuItem.separator())
appMenu.addItem(
withTitle: "Quit vphone", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"
)
appMenuItem.submenu = appMenu
mainMenu.addItem(appMenuItem)
// Keys menu hardware buttons that need vphoned HID injection
let keysMenuItem = NSMenuItem()
let keysMenu = NSMenu(title: "Keys")
keysMenu.addItem(makeItem("Home Screen", action: #selector(sendHome)))
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("Spotlight (Cmd+Space)", action: #selector(sendSpotlight)))
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)
// Connect menu guest agent commands
let agentMenuItem = NSMenuItem()
let agentMenu = NSMenu(title: "Connect")
agentMenu.addItem(makeItem("File Browser", action: #selector(openFiles)))
agentMenu.addItem(NSMenuItem.separator())
agentMenu.addItem(makeItem("Developer Mode Status", action: #selector(devModeStatus)))
agentMenu.addItem(makeItem("Enable Developer Mode", action: #selector(devModeEnable)))
agentMenu.addItem(NSMenuItem.separator())
agentMenu.addItem(makeItem("Ping", action: #selector(sendPing)))
agentMenuItem.submenu = agentMenu
mainMenu.addItem(agentMenuItem)
mainMenu.addItem(buildKeysMenu())
mainMenu.addItem(buildTypeMenu())
mainMenu.addItem(buildConnectMenu())
mainMenu.addItem(buildLocationMenu())
NSApp.mainMenu = mainMenu
}
private func makeItem(_ title: String, action: Selector) -> NSMenuItem {
func makeItem(_ title: String, action: Selector) -> NSMenuItem {
let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
item.target = self
return item
}
// MARK: - Keys (hardware buttons via vphoned HID)
@objc private func sendHome() {
keyHelper.sendHome()
}
@objc private func sendPower() {
keyHelper.sendPower()
}
@objc private func sendVolumeUp() {
keyHelper.sendVolumeUp()
}
@objc private func sendVolumeDown() {
keyHelper.sendVolumeDown()
}
@objc private func sendSpotlight() {
keyHelper.sendSpotlight()
}
@objc private func typeFromClipboard() {
keyHelper.typeFromClipboard()
}
// MARK: - vphoned Agent Commands
@objc private func openFiles() {
onFilesPressed?()
}
@objc private func devModeStatus() {
control.sendDevModeStatus()
}
@objc private func devModeEnable() {
control.sendDevModeEnable()
}
@objc private func sendPing() {
control.sendPing()
}
}

View File

@@ -0,0 +1,38 @@
import AppKit
// MARK: - Keys Menu
extension VPhoneMenuController {
func buildKeysMenu() -> NSMenuItem {
let item = NSMenuItem()
let menu = NSMenu(title: "Keys")
menu.addItem(makeItem("Home Screen", action: #selector(sendHome)))
menu.addItem(makeItem("Power", action: #selector(sendPower)))
menu.addItem(makeItem("Volume Up", action: #selector(sendVolumeUp)))
menu.addItem(makeItem("Volume Down", action: #selector(sendVolumeDown)))
menu.addItem(NSMenuItem.separator())
menu.addItem(makeItem("Spotlight (Cmd+Space)", action: #selector(sendSpotlight)))
item.submenu = menu
return item
}
@objc func sendHome() {
keyHelper.sendHome()
}
@objc func sendPower() {
keyHelper.sendPower()
}
@objc func sendVolumeUp() {
keyHelper.sendVolumeUp()
}
@objc func sendVolumeDown() {
keyHelper.sendVolumeDown()
}
@objc func sendSpotlight() {
keyHelper.sendSpotlight()
}
}

View File

@@ -0,0 +1,37 @@
import AppKit
// MARK: - Location Menu
extension VPhoneMenuController {
func buildLocationMenu() -> NSMenuItem {
let item = NSMenuItem()
let menu = NSMenu(title: "Location")
let toggle = makeItem("Sync Host Location", action: #selector(toggleLocationSync))
toggle.state = .off
toggle.isEnabled = false
locationMenuItem = toggle
menu.addItem(toggle)
item.submenu = menu
return item
}
/// Enable or disable the location toggle based on guest capability.
/// Preserves the user's checkmark state across connect/disconnect cycles.
func updateLocationCapability(available: Bool) {
locationMenuItem?.isEnabled = available
}
@objc func toggleLocationSync() {
guard let item = locationMenuItem else { return }
if item.state == .on {
locationProvider?.stopForwarding()
control.sendLocationStop()
item.state = .off
print("[location] sync toggled off by user")
} else {
locationProvider?.startForwarding()
item.state = .on
print("[location] sync toggled on by user")
}
}
}

View File

@@ -0,0 +1,17 @@
import AppKit
// MARK: - Type Menu
extension VPhoneMenuController {
func buildTypeMenu() -> NSMenuItem {
let item = NSMenuItem()
let menu = NSMenu(title: "Type")
menu.addItem(makeItem("Type ASCII from Clipboard", action: #selector(typeFromClipboard)))
item.submenu = menu
return item
}
@objc func typeFromClipboard() {
keyHelper.typeFromClipboard()
}
}

View File

@@ -4,7 +4,7 @@ import Virtualization
/// Minimal VM for booting a vphone (virtual iPhone) in DFU mode.
@MainActor
class VPhoneVM: NSObject, VZVirtualMachineDelegate {
class VPhoneVirtualMachine: NSObject, VZVirtualMachineDelegate {
let virtualMachine: VZVirtualMachine
/// Read handle for VM serial output.
private var serialOutputReadHandle: FileHandle?

View File

@@ -3,7 +3,7 @@ import Dynamic
import Foundation
import Virtualization
class VPhoneVMView: VZVirtualMachineView {
class VPhoneVirtualMachineView: VZVirtualMachineView {
var keyHelper: VPhoneKeyHelper?
private var currentTouchSwipeAim: Int = 0

View File

@@ -16,7 +16,7 @@ class VPhoneWindowController: NSObject, NSToolbarDelegate {
) {
self.control = control
let view = VPhoneVMView()
let view = VPhoneVirtualMachineView()
view.virtualMachine = vm
view.capturesSystemKeys = true
view.keyHelper = keyHelper
@@ -34,6 +34,7 @@ class VPhoneWindowController: NSObject, NSToolbarDelegate {
defer: false
)
window.isReleasedWhenClosed = false
window.contentAspectRatio = windowSize
window.title = "vphone"
window.subtitle = "daemon connecting..."