Merge pull request #51 from SongXiaoXi/main

feat: add host location passthrough to guest VM
This commit is contained in:
Lakr
2026-03-02 18:36:38 +08:00
committed by Lakr
parent e5fdad341f
commit c0f0efa492
10 changed files with 403 additions and 5 deletions

View File

@@ -12,6 +12,9 @@ CFW_INPUT ?= cfw_input
# ─── Paths ────────────────────────────────────────────────────────
SCRIPTS := scripts
BINARY := .build/release/vphone-cli
BUNDLE := .build/vphone-cli.app
BUNDLE_BIN := $(BUNDLE)/Contents/MacOS/vphone-cli
INFO_PLIST := sources/Info.plist
ENTITLEMENTS := sources/vphone.entitlements
VENV := .venv
LIMD_PREFIX := .limd
@@ -83,7 +86,7 @@ setup_libimobiledevice:
# Build
# ═══════════════════════════════════════════════════════════════════
.PHONY: build install clean
.PHONY: build install clean bundle
build: $(BINARY)
@@ -95,6 +98,13 @@ $(BINARY): $(SWIFT_SOURCES) Package.swift $(ENTITLEMENTS)
codesign --force --sign - --entitlements $(ENTITLEMENTS) $@
@echo " signed OK"
bundle: build $(INFO_PLIST)
@mkdir -p $(BUNDLE)/Contents/MacOS
@cp -f $(BINARY) $(BUNDLE_BIN)
@cp -f $(INFO_PLIST) $(BUNDLE)/Contents/Info.plist
@codesign --force --sign - --entitlements $(ENTITLEMENTS) $(BUNDLE_BIN)
@echo " bundled → $(BUNDLE)"
install: build
mkdir -p ./bin
cp -f $(BINARY) ./bin/vphone-cli
@@ -137,8 +147,8 @@ vphoned_sign: $(SCRIPTS)/vphoned/vphoned
vm_new:
zsh $(SCRIPTS)/vm_create.sh --dir $(VM_DIR) --disk-size $(DISK_SIZE)
boot: build vphoned_sign
cd $(VM_DIR) && "$(CURDIR)/$(BINARY)" \
boot: bundle vphoned_sign
cd $(VM_DIR) && "$(CURDIR)/$(BUNDLE_BIN)" \
--rom ./AVPBooter.vresearch1.bin \
--disk ./Disk.img \
--nvram ./nvram.bin \

View File

@@ -23,6 +23,7 @@ let package = Package(
.linkedFramework("Virtualization"),
.linkedFramework("AppKit"),
.linkedFramework("SwiftUI"),
.linkedFramework("CoreLocation"),
]
),
]

View File

@@ -232,7 +232,7 @@ wait_for_recovery() {
start_iproxy_2222() {
local iproxy_bin
iproxy_bin="${PROJECT_ROOT}/.limd/bin/iproxy"
[[ -n "$iproxy_bin" ]] || die "iproxy not found in PATH"
[[ -x "$iproxy_bin" ]] || die "iproxy not found at $iproxy_bin (run: make setup_libimobiledevice)"
mkdir -p "$LOG_DIR"
: > "$IPROXY_LOG"
@@ -262,6 +262,10 @@ stop_iproxy_2222() {
}
main() {
check_platform
install_brew_deps
ensure_python_linked
run_make "Project setup" setup_libimobiledevice
run_make "Project setup" setup_venv
run_make "Project setup" build

View File

@@ -11,6 +11,10 @@
<string>data-allowed</string>
<string>data-allowed-write</string>
</array>
<key>com.apple.locationd.preauthorized</key>
<true/>
<key>com.apple.locationd.simulation</key>
<true/>
<key>com.apple.Pasteboard.background-access</key>
<true/>
<key>com.apple.Pasteboard.originating-bundle-id-query</key>

View File

@@ -29,6 +29,8 @@
#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>
@@ -250,6 +252,169 @@ static BOOL devmode_arm(BOOL *alreadyEnabled) {
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) {
@@ -618,6 +783,23 @@ static NSDictionary *handle_command(NSDictionary *msg) {
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;
@@ -714,7 +896,7 @@ static BOOL handle_client(int fd) {
@"v": @PROTOCOL_VERSION,
@"t": @"hello",
@"name": @"vphoned",
@"caps": @[@"hid", @"devmode", @"file"],
@"caps": gLocationLoaded ? @[@"hid", @"devmode", @"file", @"location"] : @[@"hid", @"devmode", @"file"],
} mutableCopy];
if (needUpdate) helloResp[@"need_update"] = @YES;
@@ -726,6 +908,7 @@ static BOOL handle_client(int fd) {
while ((msg = read_message(fd)) != nil) {
@autoreleasepool {
NSString *t = msg[@"t"];
NSLog(@"vphoned: recv cmd: %@", t);
if ([t isEqualToString:@"update"]) {
NSUInteger size = [msg[@"size"] unsignedIntegerValue];
@@ -780,6 +963,7 @@ int main(int argc, char *argv[]) {
if (!load_iokit()) return 1;
if (!load_xpc()) NSLog(@"vphoned: XPC unavailable, devmode disabled");
load_corelocation();
int sock = socket(AF_VSOCK, SOCK_STREAM, 0);
if (sock < 0) { perror("vphoned: socket(AF_VSOCK)"); return 1; }

26
sources/Info.plist Normal file
View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>com.vphone.cli</string>
<key>CFBundleExecutable</key>
<string>vphone-cli</string>
<key>CFBundleName</key>
<string>vphone-cli</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>LSUIElement</key>
<true/>
<key>NSLocationUsageDescription</key>
<string>vphone-cli forwards your location to the guest VM for location simulation.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>vphone-cli forwards your location to the guest VM for location simulation.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>vphone-cli forwards your location to the guest VM for location simulation.</string>
</dict>
</plist>

View File

@@ -9,6 +9,7 @@ class VPhoneAppDelegate: NSObject, NSApplicationDelegate {
private var windowController: VPhoneWindowController?
private var menuController: VPhoneMenuController?
private var fileWindowController: VPhoneFileWindowController?
private var locationProvider: VPhoneLocationProvider?
private var sigintSource: DispatchSourceSignal?
init(cli: VPhoneCLI) {
@@ -93,6 +94,20 @@ class VPhoneAppDelegate: NSObject, NSApplicationDelegate {
if FileManager.default.fileExists(atPath: vphonedURL.path) {
control.guestBinaryURL = vphonedURL
}
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()
}
if let device = vm.virtualMachine.socketDevices.first as? VZVirtioSocketDevice {
control.connect(device: device)
}

View File

@@ -26,6 +26,12 @@ class VPhoneControl {
/// Path to the signed vphoned binary. When set, enables auto-update.
var guestBinaryURL: URL?
/// Called when guest is ready (not updating). Receives guest capabilities.
var onConnect: (([String]) -> Void)?
/// Called when the guest disconnects (before reconnect attempt).
var onDisconnect: (() -> Void)?
private var guestBinaryData: Data?
private var guestBinaryHash: String?
private var nextRequestId: UInt64 = 0
@@ -168,6 +174,7 @@ class VPhoneControl {
self.pushUpdate(fd: fd)
} else {
self.startReadLoop(fd: fd)
self.onConnect?(caps)
}
}
}
@@ -380,6 +387,38 @@ class VPhoneControl {
_ = try await sendRequest(["t": "file_rename", "from": from, "to": to])
}
// MARK: - Location
func sendLocation(latitude: Double, longitude: Double, altitude: Double,
horizontalAccuracy: Double, verticalAccuracy: Double,
speed: Double, course: Double) {
nextRequestId += 1
let msg: [String: Any] = [
"v": Self.protocolVersion,
"t": "location",
"id": String(nextRequestId, radix: 16),
"lat": latitude,
"lon": longitude,
"alt": altitude,
"hacc": horizontalAccuracy,
"vacc": verticalAccuracy,
"speed": speed,
"course": course,
"ts": Date().timeIntervalSince1970,
]
guard let fd = connection?.fileDescriptor, writeMessage(fd: fd, dict: msg) else {
print("[control] sendLocation failed (not connected)")
return
}
print("[control] sendLocation lat=\(latitude) lon=\(longitude)")
}
func sendLocationStop() {
nextRequestId += 1
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 }
}
// MARK: - Disconnect & Reconnect
private func disconnect() {
@@ -392,6 +431,10 @@ class VPhoneControl {
// Fail all pending requests
failAllPending()
if wasConnected {
onDisconnect?()
}
if wasConnected, device != nil {
print("[control] reconnecting in 3s...")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in

View File

@@ -0,0 +1,109 @@
import CoreLocation
import Foundation
/// Forwards the host Mac's location to the guest VM via vsock.
///
/// Uses macOS CoreLocation to track the Mac's real location and forwards
/// every update to the guest. Call `startForwarding()` when the guest
/// reports "location" capability. Safe to call multiple times (e.g.
/// after vphoned reconnects) re-sends the last known position.
@MainActor
class VPhoneLocationProvider: NSObject {
private let control: VPhoneControl
private var hostModeStarted = false
private var locationManager: CLLocationManager?
private var delegateProxy: LocationDelegateProxy?
private var lastLocation: CLLocation?
init(control: VPhoneControl) {
self.control = control
super.init()
let proxy = LocationDelegateProxy { [weak self] location in
Task { @MainActor in
self?.forward(location)
}
}
self.delegateProxy = proxy
let mgr = CLLocationManager()
mgr.delegate = proxy
mgr.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager = mgr
print("[location] host location forwarding ready")
}
/// Begin sending location to the guest. Safe to call on every (re)connect.
func startForwarding() {
guard let mgr = locationManager else { return }
mgr.requestAlwaysAuthorization()
mgr.startUpdatingLocation()
hostModeStarted = true
print("[location] started host location tracking")
// Re-send last known location immediately on reconnect
if let last = lastLocation {
forward(last)
print("[location] re-sent last known host location")
}
}
/// Stop forwarding and clear the simulated location in the guest.
func stopForwarding() {
if hostModeStarted {
locationManager?.stopUpdatingLocation()
hostModeStarted = false
print("[location] stopped host location tracking")
}
}
private func forward(_ location: CLLocation) {
lastLocation = location
guard control.isConnected else {
print("[location] forward: not connected, cached for later")
return
}
control.sendLocation(
latitude: location.coordinate.latitude,
longitude: location.coordinate.longitude,
altitude: location.altitude,
horizontalAccuracy: location.horizontalAccuracy,
verticalAccuracy: location.verticalAccuracy,
speed: location.speed,
course: location.course
)
}
}
// MARK: - CLLocationManagerDelegate Proxy
/// Separate object to avoid @MainActor vs nonisolated delegate conflicts.
private class LocationDelegateProxy: NSObject, CLLocationManagerDelegate {
let handler: (CLLocation) -> Void
init(handler: @escaping (CLLocation) -> Void) {
self.handler = handler
}
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)
handler(location)
}
func locationManager(_: CLLocationManager, didFailWithError error: any Error) {
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)
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
let status = manager.authorizationStatus
NSLog("[location] authorization status: %d", status.rawValue)
if status == .authorized || status == .authorizedAlways {
manager.startUpdatingLocation()
}
}
}

View File

@@ -10,6 +10,8 @@
<true/>
<key>com.apple.vm.networking</key>
<true/>
<key>com.apple.security.personal-information.location</key>
<true/>
<key>com.apple.security.get-task-allow</key>
<true/>
</dict>