mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-10-01 23:54:35 +08:00
Read the host location through an app so macOS asks for it
locationd never answers a client inside a generic bundle. From VPhone.bundle/Contents/MacOS, vphone-vm's requestWhenInUseAuthorization neither prompted nor changed the status, so Sync Host Location stayed at Not Determined and sent nothing. Every .bundle layout behaved the same; only an executable inside an .app was prompted. vphone-vm now starts Contents/Helpers/VPhoneLocation.app (vphone-location, an LSUIElement app) when sync is on. It asks for permission with its own InfoPlist.strings, so the prompt text is localized, and writes one JSON line per authorization change or fix. It exits when vphone-vm closes its stdin. The bundle's own location keys and vphone-vm's location entitlement are gone, ValidateBundle admits the helper and checks its prompt translations, and the "Host Location Unavailable" alert is localized. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -66,6 +66,18 @@ has supplied a coordinate or that the guest accepted one. This does not
|
||||
explain preset failure, which also reproduces through the HTTP API and
|
||||
`devicectl`.
|
||||
|
||||
**Cause and fix (2026-09-30).** locationd never answers a client whose
|
||||
executable sits in a generic bundle. From `VPhone.bundle/Contents/MacOS`,
|
||||
`requestWhenInUseAuthorization` neither prompts nor changes the status, and the
|
||||
usage descriptions in the bundle's Info.plist are never read. A probe showed the
|
||||
same for every `.bundle` layout (BNDL, APPL, with or without
|
||||
`CFBundleExecutable`, with an embedded `__info_plist`); only an executable
|
||||
inside an `.app`, or a bare one outside any bundle, was prompted. vphone-vm now
|
||||
starts `Contents/Helpers/VPhoneLocation.app` (`vphone-location`, an
|
||||
`LSUIElement` app) when Sync Host Location is on. The helper asks for
|
||||
permission with its own localized InfoPlist.strings and writes one JSON line
|
||||
per authorization change or fix; it exits when vphone-vm closes its stdin.
|
||||
|
||||
## Application-level workaround (2026-09-25)
|
||||
|
||||
`libvlocation.dylib` is loaded by SystemHook into newly launched apps. It
|
||||
|
||||
@@ -13,6 +13,7 @@ frameworks="$bundle/Contents/Frameworks"
|
||||
# Host programs run from Contents/MacOS. Everything installed into the guest
|
||||
# lives in guest-resources and never runs on the Mac.
|
||||
guest="$resources/guest-resources"
|
||||
location_app="$bundle/Contents/Helpers/VPhoneLocation.app"
|
||||
|
||||
# Xcode exports the bundle target's SDK and package paths to build phases. Nested
|
||||
# xcodebuild must resolve each project's own graph, especially the iOS daemon.
|
||||
@@ -82,11 +83,21 @@ fi
|
||||
"$resources/patches_presets/"
|
||||
|
||||
"${0:a:h}/SyncStrings.sh"
|
||||
for catalog in Localizable InfoPlist; do
|
||||
/usr/bin/xcrun xcstringstool compile \
|
||||
"$root/VPhoneExecutable/VPhoneVirtualization/Resources/$catalog.xcstrings" \
|
||||
--output-directory "$resources"
|
||||
done
|
||||
/usr/bin/xcrun xcstringstool compile \
|
||||
"$root/VPhoneExecutable/VPhoneVirtualization/Resources/Localizable.xcstrings" \
|
||||
--output-directory "$resources"
|
||||
|
||||
# locationd ignores a client inside a generic bundle, so vphone-vm reads the
|
||||
# host location through this app. Its InfoPlist.strings translate the
|
||||
# location prompt.
|
||||
/bin/rm -rf "$location_app"
|
||||
/bin/mkdir -p "$location_app/Contents/MacOS" "$location_app/Contents/Resources"
|
||||
/bin/cp "$root/VPhoneExecutable/VPhoneVirtualization/Resources/VPhoneLocation.Info.plist" \
|
||||
"$location_app/Contents/Info.plist"
|
||||
/bin/cp "$TARGET_BUILD_DIR/vphone-location" "$location_app/Contents/MacOS/vphone-location"
|
||||
/usr/bin/xcrun xcstringstool compile \
|
||||
"$root/VPhoneExecutable/VPhoneVirtualization/Resources/InfoPlist.xcstrings" \
|
||||
--output-directory "$location_app/Contents/Resources"
|
||||
|
||||
# vphone-vm needs Swift's span back-deployment library on macOS 15. Xcode's
|
||||
# generic bundle has no main executable. Use a private load name for the VM child.
|
||||
@@ -104,6 +115,7 @@ compatibility_library="$(/usr/bin/xcrun swift-stdlib-tool --print \
|
||||
/usr/bin/codesign --force --sign - "$macos/vphone-escalator"
|
||||
/usr/bin/codesign --force --sign - "$macos/libswiftCompatibilitySpan.vphone.dylib"
|
||||
/usr/bin/codesign --force --sign - --entitlements "$root/VPhoneExecutable/VPhoneVirtualization/Resources/VPhoneVirtualization.entitlements" "$macos/vphone-vm"
|
||||
/usr/bin/codesign --force --sign - "$location_app"
|
||||
/usr/bin/codesign --force --sign - "$bundle"
|
||||
|
||||
"${0:a:h}/ValidateBundle.sh" "$bundle"
|
||||
|
||||
@@ -8,6 +8,8 @@ macos="$bundle/Contents/MacOS"
|
||||
resources="$bundle/Contents/Resources"
|
||||
frameworks="$bundle/Contents/Frameworks"
|
||||
guest="$resources/guest-resources"
|
||||
location_app="$bundle/Contents/Helpers/VPhoneLocation.app"
|
||||
location_helper="$location_app/Contents/MacOS/vphone-location"
|
||||
|
||||
file_copy_spawns="$(/usr/bin/find "$root/VPhoneExecutable" "$root/VPhoneKit" \
|
||||
"$root/VPhoneDaemon" "$root/VPhoneGuestComponents" \
|
||||
@@ -38,6 +40,21 @@ for name in vphone-vm vphone-cli vphone-escalator libswiftCompatibilitySpan.vpho
|
||||
require_signed_macho "$macos/$name"
|
||||
done
|
||||
require_signed_macho "$frameworks/VPhonePatchKit.framework/Versions/A/VPhonePatchKit"
|
||||
require_signed_macho "$location_helper"
|
||||
/usr/bin/codesign --verify --strict "$location_app"
|
||||
# locationd answers only a client inside an .app, and prompts with its usage
|
||||
# description in the user's language.
|
||||
[[ "$(/usr/libexec/PlistBuddy -c 'Print :CFBundlePackageType' "$location_app/Contents/Info.plist")" == "APPL" ]] || {
|
||||
print -u2 "VPhoneLocation.app is not an application bundle"
|
||||
exit 1
|
||||
}
|
||||
for language in en zh-Hans ja ko vi; do
|
||||
strings="$location_app/Contents/Resources/$language.lproj/InfoPlist.strings"
|
||||
/usr/libexec/PlistBuddy -c 'Print :NSLocationWhenInUseUsageDescription' "$strings" >/dev/null 2>&1 || {
|
||||
print -u2 "Missing location prompt text in VPhoneLocation.app: $language"
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
# The patch API ships its interface so an out-of-tree patch set can build against
|
||||
# the same framework the bundle loads.
|
||||
[[ -d "$frameworks/VPhonePatchKit.framework/Versions/A/Modules/VPhonePatchKit.swiftmodule" ]] || {
|
||||
@@ -92,6 +109,9 @@ while IFS= read -r file; do
|
||||
"$macos/"*)
|
||||
[[ "$platform" != IOS ]] || { print -u2 "iOS binary in Contents/MacOS: ${file#$bundle/}"; exit 1; }
|
||||
;;
|
||||
"$location_helper")
|
||||
[[ "$platform" != IOS ]] || { print -u2 "iOS binary in VPhoneLocation.app"; exit 1; }
|
||||
;;
|
||||
"$frameworks/"*)
|
||||
[[ "$platform" != IOS ]] || { print -u2 "iOS binary in Contents/Frameworks: ${file#$bundle/}"; exit 1; }
|
||||
;;
|
||||
@@ -99,7 +119,7 @@ while IFS= read -r file; do
|
||||
[[ "$platform" == IOS ]] || { print -u2 "Non-iOS binary in guest-resources: ${file#$bundle/}"; exit 1; }
|
||||
;;
|
||||
*)
|
||||
print -u2 "Mach-O outside Contents/MacOS, Contents/Frameworks and guest-resources: ${file#$bundle/}"
|
||||
print -u2 "Mach-O outside Contents/MacOS, Contents/Frameworks, VPhoneLocation.app and guest-resources: ${file#$bundle/}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -124,7 +144,7 @@ daemon_entitlements="$(/usr/bin/codesign -d --entitlements - --xml "$guest/vphon
|
||||
print -u2 "vphoned has the wrong entitlements"
|
||||
exit 1
|
||||
}
|
||||
for name in vphone-cli vphone-escalator; do
|
||||
for name in vphone-cli vphone-escalator ../Helpers/VPhoneLocation.app/Contents/MacOS/vphone-location; do
|
||||
process_entitlements="$(/usr/bin/codesign -d --entitlements - --xml "$macos/$name" 2>/dev/null || true)"
|
||||
[[ "$process_entitlements" != *'com.apple.private.virtualization'* &&
|
||||
"$process_entitlements" != *'com.apple.CommCenter.fine-grained'* ]] || {
|
||||
@@ -133,7 +153,7 @@ for name in vphone-cli vphone-escalator; do
|
||||
}
|
||||
done
|
||||
|
||||
for name in vphone-vm vphone-cli vphone-escalator; do
|
||||
for name in vphone-vm vphone-cli vphone-escalator ../Helpers/VPhoneLocation.app/Contents/MacOS/vphone-location; do
|
||||
/usr/bin/otool -L "$macos/$name" | /usr/bin/awk 'NR > 1 {print $1}' |
|
||||
while IFS= read -r dependency; do
|
||||
case "$dependency" in
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// vphone-location — reads the Mac's location for vphone-vm.
|
||||
//
|
||||
// locationd never answers a client whose executable sits in a generic bundle:
|
||||
// from VPhone.bundle/Contents/MacOS, requestWhenInUseAuthorization neither
|
||||
// prompts nor changes the status. A process inside an .app is prompted, so
|
||||
// StageBundle.sh wraps this program in Contents/Helpers/VPhoneLocation.app with
|
||||
// the usage descriptions and their translations.
|
||||
//
|
||||
// vphone-vm starts it when Sync Host Location is turned on. It writes one JSON
|
||||
// object per line to stdout: the authorization status whenever it changes, then
|
||||
// each fix. It exits when vphone-vm closes its stdin.
|
||||
|
||||
import AppKit
|
||||
import CoreLocation
|
||||
|
||||
struct VPhoneLocationHelperMessage: Encodable {
|
||||
var authorization: Int32?
|
||||
var latitude: Double?
|
||||
var longitude: Double?
|
||||
var altitude: Double?
|
||||
var horizontalAccuracy: Double?
|
||||
var verticalAccuracy: Double?
|
||||
var speed: Double?
|
||||
var course: Double?
|
||||
var timestamp: Double?
|
||||
}
|
||||
|
||||
func emit(_ message: VPhoneLocationHelperMessage) {
|
||||
guard var line = try? JSONEncoder().encode(message) else { return }
|
||||
line.append(0x0A)
|
||||
FileHandle.standardOutput.write(line)
|
||||
}
|
||||
|
||||
final class VPhoneLocationHelperDelegate: NSObject, CLLocationManagerDelegate {
|
||||
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||
let status = manager.authorizationStatus
|
||||
emit(VPhoneLocationHelperMessage(authorization: status.rawValue))
|
||||
if status == .authorized || status == .authorizedAlways {
|
||||
manager.startUpdatingLocation()
|
||||
}
|
||||
}
|
||||
|
||||
func locationManager(_: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||
guard let location = locations.last else { return }
|
||||
emit(VPhoneLocationHelperMessage(
|
||||
latitude: location.coordinate.latitude,
|
||||
longitude: location.coordinate.longitude,
|
||||
altitude: location.altitude,
|
||||
horizontalAccuracy: location.horizontalAccuracy,
|
||||
verticalAccuracy: location.verticalAccuracy,
|
||||
speed: location.speed,
|
||||
course: location.course,
|
||||
timestamp: location.timestamp.timeIntervalSince1970,
|
||||
))
|
||||
}
|
||||
|
||||
func locationManager(_: CLLocationManager, didFailWithError error: any Error) {
|
||||
// kCLErrorLocationUnknown is transient: there is no fix yet.
|
||||
guard (error as NSError).code != CLError.locationUnknown.rawValue else { return }
|
||||
FileHandle.standardError.write(Data("[location] \(error.localizedDescription)\n".utf8))
|
||||
}
|
||||
}
|
||||
|
||||
Thread.detachNewThread {
|
||||
while !FileHandle.standardInput.availableData.isEmpty {}
|
||||
exit(0)
|
||||
}
|
||||
|
||||
let app = NSApplication.shared
|
||||
app.setActivationPolicy(.accessory)
|
||||
let delegate = VPhoneLocationHelperDelegate()
|
||||
let manager = CLLocationManager()
|
||||
manager.delegate = delegate
|
||||
manager.desiredAccuracy = kCLLocationAccuracyBest
|
||||
manager.requestWhenInUseAuthorization()
|
||||
app.run()
|
||||
@@ -12,11 +12,5 @@
|
||||
<string>19</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.2.1</string>
|
||||
<key>NSLocationUsageDescription</key>
|
||||
<string>Shares your location with the virtual iPhone so apps running inside it see the same location.</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>Shares your location with the virtual iPhone so apps running inside it see the same location.</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>Shares your location with the virtual iPhone so apps running inside it see the same location.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1949,6 +1949,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Allow VPhone to use your location in System Settings > Privacy & Security > Location Services to sync the Mac's location." : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Allow VPhone to use your location in System Settings > Privacy & Security > Location Services to sync the Mac's location."
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Mac の位置情報を同期するには、システム設定 > プライバシーとセキュリティ > 位置情報サービスで VPhone を許可してください。"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Mac 위치를 동기화하려면 시스템 설정 > 개인정보 보호 및 보안 > 위치 서비스에서 VPhone을 허용하십시오."
|
||||
}
|
||||
},
|
||||
"vi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Để đồng bộ vị trí của máy Mac, hãy cho phép VPhone trong Cài đặt hệ thống > Quyền riêng tư & Bảo mật > Dịch vụ định vị."
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "要同步 Mac 的位置,请在“系统设置”>“隐私与安全性”>“定位服务”中允许 VPhone。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Allow Network" : {
|
||||
|
||||
},
|
||||
@@ -10779,6 +10813,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Host Location Unavailable" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Host Location Unavailable"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ホストの位置情報を取得できません"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "호스트 위치를 사용할 수 없음"
|
||||
}
|
||||
},
|
||||
"vi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Không lấy được vị trí máy chủ"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "无法获取主机位置"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Host Name" : {
|
||||
"localizations" : {
|
||||
"ja" : {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?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.bundle.location</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>VPhone</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>vphone-location</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>15.0</string>
|
||||
<!-- Localized by InfoPlist.xcstrings, compiled into this app by StageBundle.sh. -->
|
||||
<key>NSLocationUsageDescription</key>
|
||||
<string>Shares your location with the virtual iPhone so apps running inside it see the same location.</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>Shares your location with the virtual iPhone so apps running inside it see the same location.</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>Shares your location with the virtual iPhone so apps running inside it see the same location.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -10,8 +10,6 @@
|
||||
<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/>
|
||||
<key>com.apple.private.bmk.allow</key>
|
||||
|
||||
+131
-68
@@ -3,10 +3,12 @@ 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.
|
||||
/// locationd never answers a client inside VPhone.bundle, so the Mac's
|
||||
/// location comes from `Contents/Helpers/VPhoneLocation.app`: it asks for
|
||||
/// permission and writes one JSON line per update, which this class forwards
|
||||
/// 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 {
|
||||
struct ReplayPoint {
|
||||
@@ -40,9 +42,10 @@ class VPhoneLocationProvider: NSObject {
|
||||
private let control: VPhoneGuestControl
|
||||
private var hostModeStarted = false
|
||||
|
||||
private var locationManager: CLLocationManager?
|
||||
private var delegateProxy: LocationDelegateProxy?
|
||||
private var lastHostLocation: CLLocation?
|
||||
private var helper: Process?
|
||||
private var helperInput: Pipe?
|
||||
private var helperReader: Task<Void, Never>?
|
||||
private var lastHostLocation: HostLocation?
|
||||
private var replayTask: Task<Void, Never>?
|
||||
private var replayName: String?
|
||||
var onAuthorizationFailure: (() -> Void)?
|
||||
@@ -54,50 +57,109 @@ class VPhoneLocationProvider: NSObject {
|
||||
init(control: VPhoneGuestControl) {
|
||||
self.control = control
|
||||
super.init()
|
||||
|
||||
let proxy = LocationDelegateProxy(
|
||||
locationHandler: { [weak self] location in
|
||||
Task { @MainActor in self?.forward(location) }
|
||||
},
|
||||
authorizationHandler: { [weak self] status in
|
||||
Task { @MainActor in self?.handleAuthorization(status) }
|
||||
},
|
||||
)
|
||||
delegateProxy = proxy
|
||||
let mgr = CLLocationManager()
|
||||
mgr.delegate = proxy
|
||||
mgr.desiredAccuracy = kCLLocationAccuracyBest
|
||||
locationManager = mgr
|
||||
print("[location] host location forwarding ready")
|
||||
}
|
||||
|
||||
/// Begin sending location to the guest. Safe to call on every (re)connect.
|
||||
func startForwarding() {
|
||||
stopReplay()
|
||||
guard let mgr = locationManager else { return }
|
||||
hostModeStarted = true
|
||||
mgr.requestWhenInUseAuthorization()
|
||||
handleAuthorization(mgr.authorizationStatus)
|
||||
print("[location] started host location tracking")
|
||||
if let last = lastHostLocation, abs(last.date.timeIntervalSinceNow) < 60 {
|
||||
forward(last)
|
||||
}
|
||||
guard helper == nil else { return }
|
||||
do {
|
||||
try launchHelper()
|
||||
print("[location] started host location tracking")
|
||||
} catch {
|
||||
print("[location] cannot start VPhoneLocation.app: \(error.localizedDescription)")
|
||||
hostModeStarted = false
|
||||
onAuthorizationFailure?()
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop forwarding host location updates.
|
||||
func stopForwarding() {
|
||||
if hostModeStarted {
|
||||
locationManager?.stopUpdatingLocation()
|
||||
hostModeStarted = false
|
||||
stopHelper()
|
||||
print("[location] stopped host location tracking")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Location Helper
|
||||
|
||||
private static var helperURL: URL {
|
||||
let executable = (Bundle.main.executableURL ?? URL(fileURLWithPath: CommandLine.arguments[0]))
|
||||
.resolvingSymlinksInPath()
|
||||
return executable
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent("Helpers/VPhoneLocation.app/Contents/MacOS/vphone-location")
|
||||
}
|
||||
|
||||
private func launchHelper() throws {
|
||||
let process = Process()
|
||||
process.executableURL = Self.helperURL
|
||||
// The helper exits when this pipe closes, including when vphone-vm dies.
|
||||
let input = Pipe()
|
||||
let output = Pipe()
|
||||
process.standardInput = input
|
||||
process.standardOutput = output
|
||||
process.terminationHandler = { [weak self] process in
|
||||
let status = process.terminationStatus
|
||||
Task { @MainActor in self?.helperDidExit(process, status: status) }
|
||||
}
|
||||
try process.run()
|
||||
helper = process
|
||||
helperInput = input
|
||||
helperReader = Task { @MainActor [weak self] in
|
||||
do {
|
||||
for try await line in output.fileHandleForReading.bytes.lines {
|
||||
self?.handleHelperLine(line)
|
||||
}
|
||||
} catch {
|
||||
print("[location] VPhoneLocation.app output failed: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func stopHelper() {
|
||||
helperReader?.cancel()
|
||||
helperReader = nil
|
||||
try? helperInput?.fileHandleForWriting.close()
|
||||
helperInput = nil
|
||||
helper?.terminate()
|
||||
helper = nil
|
||||
}
|
||||
|
||||
private func helperDidExit(_ process: Process, status: Int32) {
|
||||
guard helper === process else { return }
|
||||
print("[location] VPhoneLocation.app exited with status \(status)")
|
||||
helper = nil
|
||||
helperInput = nil
|
||||
helperReader = nil
|
||||
stopForwarding()
|
||||
}
|
||||
|
||||
private func handleHelperLine(_ line: String) {
|
||||
guard let message = try? JSONDecoder().decode(HelperMessage.self, from: Data(line.utf8)) else {
|
||||
print("[location] unreadable VPhoneLocation.app output: \(line)")
|
||||
return
|
||||
}
|
||||
if let rawStatus = message.authorization {
|
||||
handleAuthorization(CLAuthorizationStatus(rawValue: rawStatus) ?? .notDetermined)
|
||||
}
|
||||
if let location = message.location {
|
||||
let c = String(format: "%.6f,%.6f", location.latitude, location.longitude)
|
||||
print("[location] got location: \(c) (+/-\(String(format: "%.0f", location.horizontalAccuracy))m)")
|
||||
forward(location)
|
||||
}
|
||||
}
|
||||
|
||||
private func handleAuthorization(_ status: CLAuthorizationStatus) {
|
||||
print("[location] authorization status: \(status.rawValue)")
|
||||
guard hostModeStarted else { return }
|
||||
switch status {
|
||||
case .authorized, .authorizedAlways:
|
||||
locationManager?.startUpdatingLocation()
|
||||
if let last = lastHostLocation, abs(last.timestamp.timeIntervalSinceNow) < 60 {
|
||||
forward(last)
|
||||
}
|
||||
case .denied, .restricted:
|
||||
stopForwarding()
|
||||
onAuthorizationFailure?()
|
||||
@@ -193,7 +255,7 @@ class VPhoneLocationProvider: NSObject {
|
||||
replayName = nil
|
||||
}
|
||||
|
||||
private func forward(_ location: CLLocation) {
|
||||
private func forward(_ location: HostLocation) {
|
||||
lastHostLocation = location
|
||||
guard hostModeStarted else { return }
|
||||
guard control.isConnected else {
|
||||
@@ -201,8 +263,8 @@ class VPhoneLocationProvider: NSObject {
|
||||
return
|
||||
}
|
||||
control.sendLocation(
|
||||
latitude: location.coordinate.latitude,
|
||||
longitude: location.coordinate.longitude,
|
||||
latitude: location.latitude,
|
||||
longitude: location.longitude,
|
||||
altitude: location.altitude,
|
||||
horizontalAccuracy: location.horizontalAccuracy,
|
||||
verticalAccuracy: location.verticalAccuracy,
|
||||
@@ -237,42 +299,43 @@ class VPhoneLocationProvider: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CLLocationManagerDelegate Proxy
|
||||
// MARK: - Location Helper Messages
|
||||
|
||||
/// Separate object to avoid @MainActor vs nonisolated delegate conflicts.
|
||||
private class LocationDelegateProxy: NSObject, CLLocationManagerDelegate {
|
||||
let locationHandler: (CLLocation) -> Void
|
||||
let authorizationHandler: (CLAuthorizationStatus) -> Void
|
||||
/// One fix from VPhoneLocation.app.
|
||||
private struct HostLocation {
|
||||
let latitude: Double
|
||||
let longitude: Double
|
||||
let altitude: Double
|
||||
let horizontalAccuracy: Double
|
||||
let verticalAccuracy: Double
|
||||
let speed: Double
|
||||
let course: Double
|
||||
let date: Date
|
||||
}
|
||||
|
||||
init(
|
||||
locationHandler: @escaping (CLLocation) -> Void,
|
||||
authorizationHandler: @escaping (CLAuthorizationStatus) -> Void,
|
||||
) {
|
||||
self.locationHandler = locationHandler
|
||||
self.authorizationHandler = authorizationHandler
|
||||
}
|
||||
/// One line of VPhoneLocation.app output: an authorization status or a fix.
|
||||
private struct HelperMessage: Decodable {
|
||||
let authorization: Int32?
|
||||
let latitude: Double?
|
||||
let longitude: Double?
|
||||
let altitude: Double?
|
||||
let horizontalAccuracy: Double?
|
||||
let verticalAccuracy: Double?
|
||||
let speed: Double?
|
||||
let course: Double?
|
||||
let timestamp: Double?
|
||||
|
||||
func locationManager(_: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||
guard let location = locations.last else { return }
|
||||
let c = location.coordinate
|
||||
print(
|
||||
"[location] got location: \(String(format: "%.6f,%.6f", c.latitude, c.longitude)) (+/-\(String(format: "%.0f", location.horizontalAccuracy))m)",
|
||||
var location: HostLocation? {
|
||||
guard let latitude, let longitude else { return nil }
|
||||
return HostLocation(
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
altitude: altitude ?? 0,
|
||||
horizontalAccuracy: horizontalAccuracy ?? -1,
|
||||
verticalAccuracy: verticalAccuracy ?? -1,
|
||||
speed: speed ?? -1,
|
||||
course: course ?? -1,
|
||||
date: timestamp.map(Date.init(timeIntervalSince1970:)) ?? Date(),
|
||||
)
|
||||
locationHandler(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
|
||||
}
|
||||
print("[location] CLLocationManager error: \(error.localizedDescription) (code \(clErr))")
|
||||
}
|
||||
|
||||
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||
let status = manager.authorizationStatus
|
||||
print("[location] authorization status: \(status.rawValue)")
|
||||
authorizationHandler(status)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,8 +174,10 @@ class VPhoneVirtualMachineAppDelegate: NSObject, NSApplicationDelegate {
|
||||
provider.onAuthorizationFailure = { [weak mc] in
|
||||
mc?.locationMenuItem?.state = .off
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Host Location Unavailable"
|
||||
alert.informativeText = "Allow location access for vphone in System Settings to sync the Mac's location."
|
||||
alert.messageText = VPhoneLocalization.text("Host Location Unavailable")
|
||||
alert.informativeText = VPhoneLocalization.text(
|
||||
"Allow VPhone to use your location in System Settings > Privacy & Security > Location Services to sync the Mac's location.",
|
||||
)
|
||||
alert.runModal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,12 +30,20 @@
|
||||
remoteGlobalIDString = DD0000000000000000000013;
|
||||
remoteInfo = "vphone-vm";
|
||||
};
|
||||
DD0000000000000000000126 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = DD0000000000000000000001 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = DD0000000000000000000014;
|
||||
remoteInfo = "vphone-location";
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
DD0000000000000000000031 /* VPhone.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VPhone.bundle; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
DD0000000000000000000032 /* libVPhoneVirtualMachineKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libVPhoneVirtualMachineKit.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
DD0000000000000000000033 /* vphone-vm */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "vphone-vm"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
DD0000000000000000000034 /* vphone-location */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "vphone-location"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
@@ -43,6 +51,7 @@
|
||||
DD0000000000000000000023 /* Resources */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Resources; sourceTree = "<group>"; };
|
||||
DD0000000000000000000024 /* Build */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Build; sourceTree = "<group>"; };
|
||||
DD0000000000000000000025 /* VirtualMachineProcess */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VirtualMachineProcess; sourceTree = "<group>"; };
|
||||
DD0000000000000000000026 /* LocationHelperProcess */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = LocationHelperProcess; sourceTree = "<group>"; };
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -76,6 +85,7 @@
|
||||
DD0000000000000000000023 /* Resources */,
|
||||
DD0000000000000000000024 /* Build */,
|
||||
DD0000000000000000000025 /* VirtualMachineProcess */,
|
||||
DD0000000000000000000026 /* LocationHelperProcess */,
|
||||
DD0000000000000000000003 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
@@ -86,6 +96,7 @@
|
||||
DD0000000000000000000031 /* VPhone.bundle */,
|
||||
DD0000000000000000000032 /* libVPhoneVirtualMachineKit.a */,
|
||||
DD0000000000000000000033 /* vphone-vm */,
|
||||
DD0000000000000000000034 /* vphone-location */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -103,6 +114,7 @@
|
||||
);
|
||||
dependencies = (
|
||||
DD0000000000000000000123 /* PBXTargetDependency */,
|
||||
DD0000000000000000000125 /* PBXTargetDependency */,
|
||||
);
|
||||
name = VPhone;
|
||||
packageProductDependencies = (
|
||||
@@ -160,6 +172,26 @@
|
||||
productReference = DD0000000000000000000033 /* vphone-vm */;
|
||||
productType = "com.apple.product-type.tool";
|
||||
};
|
||||
DD0000000000000000000014 /* vphone-location */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = DD0000000000000000000084 /* Build configuration list for PBXNativeTarget "vphone-location" */;
|
||||
buildPhases = (
|
||||
DD0000000000000000000054 /* Sources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
DD0000000000000000000026 /* LocationHelperProcess */,
|
||||
);
|
||||
name = "vphone-location";
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = "vphone-location";
|
||||
productReference = DD0000000000000000000034 /* vphone-location */;
|
||||
productType = "com.apple.product-type.tool";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
@@ -188,6 +220,7 @@
|
||||
DD0000000000000000000011 /* VPhone */,
|
||||
DD0000000000000000000012 /* VPhoneVirtualMachineKit */,
|
||||
DD0000000000000000000013 /* vphone-vm */,
|
||||
DD0000000000000000000014 /* vphone-location */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -240,6 +273,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
DD0000000000000000000054 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
@@ -253,6 +293,11 @@
|
||||
target = DD0000000000000000000013 /* vphone-vm */;
|
||||
targetProxy = DD0000000000000000000124 /* PBXContainerItemProxy */;
|
||||
};
|
||||
DD0000000000000000000125 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = DD0000000000000000000014 /* vphone-location */;
|
||||
targetProxy = DD0000000000000000000126 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
@@ -404,6 +449,34 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
DD0000000000000000000079 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ARCHS = arm64;
|
||||
CODE_SIGNING_ALLOWED = NO;
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||
PRODUCT_NAME = "vphone-location";
|
||||
SDKROOT = macosx;
|
||||
SWIFT_STRICT_CONCURRENCY = complete;
|
||||
SWIFT_VERSION = 6.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
DD000000000000000000007A /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ARCHS = arm64;
|
||||
CODE_SIGNING_ALLOWED = NO;
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||
PRODUCT_NAME = "vphone-location";
|
||||
SDKROOT = macosx;
|
||||
SWIFT_STRICT_CONCURRENCY = complete;
|
||||
SWIFT_VERSION = 6.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
@@ -443,6 +516,15 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
DD0000000000000000000084 /* Build configuration list for PBXNativeTarget "vphone-location" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
DD0000000000000000000079 /* Debug */,
|
||||
DD000000000000000000007A /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
|
||||
Reference in New Issue
Block a user