mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-04-05 13:09:06 +08:00
Clean up location passthrough PR: consistent formatting and logging
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -317,3 +317,5 @@ __marimo__/
|
||||
TODO.md
|
||||
/references/
|
||||
scripts/vphoned/vphoned
|
||||
sources/vphone-cli/VPhoneBuildInfo.swift
|
||||
setup_logs/
|
||||
52
AGENTS.md
52
AGENTS.md
@@ -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
|
||||
|
||||
|
||||
18
Makefile
18
Makefile
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
|
||||
22
scripts/vphoned/vphoned_devmode.h
Normal file
22
scripts/vphoned/vphoned_devmode.h
Normal 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);
|
||||
107
scripts/vphoned/vphoned_devmode.m
Normal file
107
scripts/vphoned/vphoned_devmode.m
Normal 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];
|
||||
}
|
||||
13
scripts/vphoned/vphoned_files.h
Normal file
13
scripts/vphoned/vphoned_files.h
Normal 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);
|
||||
236
scripts/vphoned/vphoned_files.m
Normal file
236
scripts/vphoned/vphoned_files.m
Normal 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;
|
||||
}
|
||||
18
scripts/vphoned/vphoned_hid.h
Normal file
18
scripts/vphoned/vphoned_hid.h
Normal 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);
|
||||
72
scripts/vphoned/vphoned_hid.m
Normal file
72
scripts/vphoned/vphoned_hid.m
Normal 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); }
|
||||
}
|
||||
24
scripts/vphoned/vphoned_location.h
Normal file
24
scripts/vphoned/vphoned_location.h
Normal 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);
|
||||
164
scripts/vphoned/vphoned_location.m
Normal file
164
scripts/vphoned/vphoned_location.m
Normal 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);
|
||||
}
|
||||
}
|
||||
22
scripts/vphoned/vphoned_protocol.h
Normal file
22
scripts/vphoned/vphoned_protocol.h
Normal 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);
|
||||
64
scripts/vphoned/vphoned_protocol.m
Normal file
64
scripts/vphoned/vphoned_protocol.m
Normal 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;
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
91
sources/vphone-cli/VPhoneMenuConnect.swift
Normal file
91
sources/vphone-cli/VPhoneMenuConnect.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
38
sources/vphone-cli/VPhoneMenuKeys.swift
Normal file
38
sources/vphone-cli/VPhoneMenuKeys.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
37
sources/vphone-cli/VPhoneMenuLocation.swift
Normal file
37
sources/vphone-cli/VPhoneMenuLocation.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
17
sources/vphone-cli/VPhoneMenuType.swift
Normal file
17
sources/vphone-cli/VPhoneMenuType.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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?
|
||||
@@ -3,7 +3,7 @@ import Dynamic
|
||||
import Foundation
|
||||
import Virtualization
|
||||
|
||||
class VPhoneVMView: VZVirtualMachineView {
|
||||
class VPhoneVirtualMachineView: VZVirtualMachineView {
|
||||
var keyHelper: VPhoneKeyHelper?
|
||||
|
||||
private var currentTouchSwipeAim: Int = 0
|
||||
@@ -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..."
|
||||
|
||||
Reference in New Issue
Block a user