mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-10-01 23:54:35 +08:00
Let Launchpad update a stopped machine's guest environment
A machine's restore tree is removed after its first boot, so cfw install cannot run again and a machine made by an older bundle never gets newer hook dylibs. The helper gains updateGuestEnvironment, which runs the bundle's `vphone-cli cfw update-environment` as root through the same checks, output channel, lock and cancel as the install. Launchpad offers Update Guest Environment in a stopped, installed machine's menu, and vphone-launchpad-cli gains `cfw update-environment <name>`. The helper protocol changed, so CURRENT_PROJECT_VERSION goes to 9 and Launchpad reinstalls the helper. The CLI verb itself lands with the installer's resource-only mode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,7 @@ expires asks for an administrator password on the Mac, as the window does.
|
||||
| `vm wait <name>` / `vm log <name> [--kind create\|dfu\|patch]` | Wait for vphoned; read a console log |
|
||||
| `vm create <name> [...] [--from <step>]` | The New Machine pipeline; `--from` retries from a step |
|
||||
| `cfw install <name>` | Install CFW into a stopped machine through the helper |
|
||||
| `cfw update-environment <name>` | Redeploy the active bundle's guest resources (vphoned, hook dylibs) into a stopped machine through the helper; nothing else changes |
|
||||
| `guest send <name> <json>` | One raw `vphone.sock` request (tap, swipe, key, screenshot) |
|
||||
| `guest rpc <name> <method> [params]` | Any vphoned method, see `Research/vphoned_http_api.md` |
|
||||
| `exec <vphone-cli arguments>` | Run the active bundle's `vphone-cli`, streaming its output |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
MARKETING_VERSION = 2.2.0
|
||||
// Bump whenever the helper changes. The app blesses the helper again when the
|
||||
// installed helper's version or bytes differ from the copy it embeds.
|
||||
CURRENT_PROJECT_VERSION = 8
|
||||
CURRENT_PROJECT_VERSION = 9
|
||||
|
||||
@@ -57,6 +57,7 @@ struct VPhoneLaunchpadControlCommands {
|
||||
case "vm.log": return try await log(request)
|
||||
case "vm.create": return try await create(request, emit: emit)
|
||||
case "cfw.install": return try await installCustomFirmware(request, emit: emit)
|
||||
case "cfw.update-environment": return try await updateGuestEnvironment(request, emit: emit)
|
||||
case "guest.send": return try await sendToGuest(request)
|
||||
case "guest.rpc": return try await callGuest(request)
|
||||
case "exec": return try await exec(request, emit: emit)
|
||||
@@ -584,6 +585,26 @@ struct VPhoneLaunchpadControlCommands {
|
||||
return ["name": machine.name, "bundle": version, "status": status]
|
||||
}
|
||||
|
||||
private func updateGuestEnvironment(_ request: VPhoneLaunchpadControlRequest, emit: @escaping Emit) async throws -> Any {
|
||||
let machine = try await machine(request)
|
||||
guard let version = bundles.activeVersion else {
|
||||
throw VPhoneLaunchpadError("No Core Bundle version is in use.")
|
||||
}
|
||||
guard library.state(of: machine) == .stopped else {
|
||||
throw VPhoneLaunchpadError("Stop \(machine.name) before updating its guest environment.")
|
||||
}
|
||||
let status = try await model.helper.updateGuestEnvironment(
|
||||
bundleVersion: version,
|
||||
machineName: machine.name,
|
||||
libraryRoot: machine.libraryRoot,
|
||||
onLine: emit,
|
||||
)
|
||||
guard status == 0 else {
|
||||
throw VPhoneLaunchpadError("Unable to update the guest environment. Check the log for details.")
|
||||
}
|
||||
return ["name": machine.name, "bundle": version, "status": status]
|
||||
}
|
||||
|
||||
// MARK: - Guest
|
||||
|
||||
private func sendToGuest(_ request: VPhoneLaunchpadControlRequest) async throws -> Any {
|
||||
|
||||
@@ -277,6 +277,37 @@ final class VPhoneLaunchpadHelperClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs `cfw update-environment` as root: redeploys the active bundle's
|
||||
/// guest resources into a stopped machine. Output lines go to `onLine`.
|
||||
func updateGuestEnvironment(
|
||||
bundleVersion: String,
|
||||
machineName: String,
|
||||
libraryRoot: String,
|
||||
onLine: @escaping @Sendable (String) -> Void,
|
||||
) async throws -> Int32 {
|
||||
let authorization = try await authorizationSession.externalForm()
|
||||
receiver.setHandler(onLine)
|
||||
defer { receiver.setHandler(nil) }
|
||||
return try await withTaskCancellationHandler {
|
||||
try await request { proxy, done in
|
||||
proxy.updateGuestEnvironment(
|
||||
authorization: authorization,
|
||||
bundleVersion: bundleVersion,
|
||||
machineName: machineName,
|
||||
libraryRoot: libraryRoot,
|
||||
) { status, message in
|
||||
if let message {
|
||||
done(.failure(VPhoneLaunchpadError(message)))
|
||||
} else {
|
||||
done(.success(status))
|
||||
}
|
||||
}
|
||||
}
|
||||
} onCancel: {
|
||||
Task { @MainActor in self.cancelCustomFirmware() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Never prompts. The helper stops only an install this user started.
|
||||
func cancelCustomFirmware() {
|
||||
let proxy = currentConnection().remoteObjectProxy as? VPhoneLaunchpadHelperProtocol
|
||||
|
||||
@@ -6399,6 +6399,33 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Update Guest Environment" : {
|
||||
"localizations" : {
|
||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Update Guest Environment" } },
|
||||
"ja" : { "stringUnit" : { "state" : "translated", "value" : "ゲスト環境を更新" } },
|
||||
"ko" : { "stringUnit" : { "state" : "translated", "value" : "게스트 환경 업데이트" } },
|
||||
"vi" : { "stringUnit" : { "state" : "translated", "value" : "Cập nhật môi trường máy khách" } },
|
||||
"zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更新客户机环境" } }
|
||||
}
|
||||
},
|
||||
"Updating guest environment…" : {
|
||||
"localizations" : {
|
||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Updating guest environment…" } },
|
||||
"ja" : { "stringUnit" : { "state" : "translated", "value" : "ゲスト環境を更新中…" } },
|
||||
"ko" : { "stringUnit" : { "state" : "translated", "value" : "게스트 환경 업데이트 중…" } },
|
||||
"vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang cập nhật môi trường máy khách…" } },
|
||||
"zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "正在更新客户机环境…" } }
|
||||
}
|
||||
},
|
||||
"Unable to update the guest environment." : {
|
||||
"localizations" : {
|
||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Unable to update the guest environment." } },
|
||||
"ja" : { "stringUnit" : { "state" : "translated", "value" : "ゲスト環境を更新できません。" } },
|
||||
"ko" : { "stringUnit" : { "state" : "translated", "value" : "게스트 환경을 업데이트할 수 없습니다." } },
|
||||
"vi" : { "stringUnit" : { "state" : "translated", "value" : "Không thể cập nhật môi trường máy khách." } },
|
||||
"zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "无法更新客户机环境。" } }
|
||||
}
|
||||
},
|
||||
"Install Custom Firmware" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
|
||||
@@ -364,6 +364,41 @@ final class VPhoneLaunchpadMachineLibrary {
|
||||
await refresh()
|
||||
}
|
||||
|
||||
/// Redeploys the active bundle's guest resources (vphoned and the hook
|
||||
/// dylibs) into a stopped machine through the helper, and nothing else.
|
||||
/// This is how a machine created by an older bundle gets newer hooks,
|
||||
/// since its restore tree is gone after the first boot.
|
||||
func updateGuestEnvironment(_ machine: Path) async {
|
||||
guard let version = bundles.activeVersion else {
|
||||
actionError = VPhoneLaunchpadError(String(localized: "No Core Bundle version is in use. Choose a version in Core Bundle."))
|
||||
return
|
||||
}
|
||||
activities[machine] = String(localized: "Updating guest environment…")
|
||||
defer { activities[machine] = nil }
|
||||
appendConsoleLog(machine, "$ vphone-cli cfw update-environment \(machine.name)")
|
||||
let log = Self.consoleLog(machine)
|
||||
do {
|
||||
let status = try await helper.updateGuestEnvironment(
|
||||
bundleVersion: version,
|
||||
machineName: machine.name,
|
||||
libraryRoot: machine.libraryRoot,
|
||||
onLine: { line in Self.append(line, to: log) },
|
||||
)
|
||||
if status != 0 {
|
||||
actionError = VPhoneLaunchpadError(
|
||||
String(localized: "Unable to update the guest environment."),
|
||||
detail: String(localized: "Choose Show Console Log for the full output."),
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
if !(error is CancellationError) {
|
||||
actionError = error as? VPhoneLaunchpadError
|
||||
?? VPhoneLaunchpadError(String(localized: "Unable to update the guest environment."), detail: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
await refresh()
|
||||
}
|
||||
|
||||
func stop(_ machine: Path) async {
|
||||
await perform(String(localized: "Stopping…"), on: machine, ["vm", "stop", machine.name] + machine.libraryArguments)
|
||||
launched[machine]?.interrupt()
|
||||
|
||||
@@ -295,6 +295,12 @@ struct VPhoneLaunchpadMachinesView: View {
|
||||
// Only for an unfinished install: that is when the restore tree it
|
||||
// reads is still there. A finished one removes it.
|
||||
.disabled(!isStopped || machine.customFirmwareInstalled != false)
|
||||
// The finished-install counterpart: redeploys the active bundle's
|
||||
// guest resources without the restore tree.
|
||||
Button("Update Guest Environment") {
|
||||
Task { await library.updateGuestEnvironment(machine.path) }
|
||||
}
|
||||
.disabled(!isStopped || machine.restoreInfo == nil || machine.customFirmwareInstalled == false)
|
||||
Divider()
|
||||
Button("Show in Finder") {
|
||||
NSWorkspace.shared.activateFileViewerSelecting([machine.path.url])
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import Darwin
|
||||
import Foundation
|
||||
|
||||
/// A validated `vphone-cli cfw install` invocation. Built only from a store
|
||||
/// bundle whose cdhash still matches its receipt, and only for a VM directory
|
||||
/// the calling user owns.
|
||||
/// A validated `vphone-cli cfw install` or `cfw update-environment`
|
||||
/// invocation. Built only from a store bundle whose cdhash still matches its
|
||||
/// receipt, and only for a VM directory the calling user owns.
|
||||
struct VPhoneLaunchpadHelperFirmwareRequest {
|
||||
enum Operation {
|
||||
/// The full install, which needs the prepared restore tree.
|
||||
case install(keepArtifacts: Bool)
|
||||
/// Redeploys the bundle's guest resources into a stopped machine and
|
||||
/// nothing else.
|
||||
case updateEnvironment
|
||||
}
|
||||
|
||||
let executable: URL
|
||||
let arguments: [String]
|
||||
let environment: [String: String]
|
||||
let workingDirectory: URL
|
||||
|
||||
init(
|
||||
operation: Operation,
|
||||
bundleVersion: String,
|
||||
machineName: String,
|
||||
libraryRoot: String,
|
||||
keepArtifacts: Bool,
|
||||
callerUID: uid_t,
|
||||
callerGID: gid_t,
|
||||
) throws {
|
||||
@@ -45,9 +53,15 @@ struct VPhoneLaunchpadHelperFirmwareRequest {
|
||||
let userName = String(cString: account.pointee.pw_name)
|
||||
let home = String(cString: account.pointee.pw_dir)
|
||||
|
||||
var arguments = ["cfw", "install", machineName, "--library-root", libraryRoot]
|
||||
if keepArtifacts {
|
||||
arguments.append("--keep-artifacts")
|
||||
var arguments: [String]
|
||||
switch operation {
|
||||
case let .install(keepArtifacts):
|
||||
arguments = ["cfw", "install", machineName, "--library-root", libraryRoot]
|
||||
if keepArtifacts {
|
||||
arguments.append("--keep-artifacts")
|
||||
}
|
||||
case .updateEnvironment:
|
||||
arguments = ["cfw", "update-environment", machineName, "--library-root", libraryRoot]
|
||||
}
|
||||
|
||||
self.executable = executable
|
||||
|
||||
@@ -80,6 +80,44 @@ final class VPhoneLaunchpadHelperService: NSObject, VPhoneLaunchpadHelperProtoco
|
||||
libraryRoot: String,
|
||||
keepArtifacts: Bool,
|
||||
reply: @escaping @Sendable (Int32, String?) -> Void,
|
||||
) {
|
||||
runFirmware(
|
||||
.install(keepArtifacts: keepArtifacts),
|
||||
authorization: authorization,
|
||||
bundleVersion: bundleVersion,
|
||||
machineName: machineName,
|
||||
libraryRoot: libraryRoot,
|
||||
reply: reply,
|
||||
)
|
||||
}
|
||||
|
||||
func updateGuestEnvironment(
|
||||
authorization: Data,
|
||||
bundleVersion: String,
|
||||
machineName: String,
|
||||
libraryRoot: String,
|
||||
reply: @escaping @Sendable (Int32, String?) -> Void,
|
||||
) {
|
||||
runFirmware(
|
||||
.updateEnvironment,
|
||||
authorization: authorization,
|
||||
bundleVersion: bundleVersion,
|
||||
machineName: machineName,
|
||||
libraryRoot: libraryRoot,
|
||||
reply: reply,
|
||||
)
|
||||
}
|
||||
|
||||
/// Both operations share one slot, so an install and an environment
|
||||
/// update never write the same machine at once, and one cancel stops
|
||||
/// either.
|
||||
private func runFirmware(
|
||||
_ operation: VPhoneLaunchpadHelperFirmwareRequest.Operation,
|
||||
authorization: Data,
|
||||
bundleVersion: String,
|
||||
machineName: String,
|
||||
libraryRoot: String,
|
||||
reply: @escaping @Sendable (Int32, String?) -> Void,
|
||||
) {
|
||||
let callerUID = callerUID
|
||||
let callerGID = callerGID
|
||||
@@ -88,10 +126,10 @@ final class VPhoneLaunchpadHelperService: NSObject, VPhoneLaunchpadHelperProtoco
|
||||
do {
|
||||
try VPhoneLaunchpadHelperAuthorization.require(authorization)
|
||||
request = try VPhoneLaunchpadHelperFirmwareRequest(
|
||||
operation: operation,
|
||||
bundleVersion: bundleVersion,
|
||||
machineName: machineName,
|
||||
libraryRoot: libraryRoot,
|
||||
keepArtifacts: keepArtifacts,
|
||||
callerUID: callerUID,
|
||||
callerGID: callerGID,
|
||||
)
|
||||
@@ -113,7 +151,7 @@ final class VPhoneLaunchpadHelperService: NSObject, VPhoneLaunchpadHelperProtoco
|
||||
Self.firmwareLock.lock()
|
||||
guard Self.firmwareProcess == nil else {
|
||||
Self.firmwareLock.unlock()
|
||||
reply(-1, "Another CFW install is in progress. Wait for it to finish, then try again.")
|
||||
reply(-1, "Another CFW install or environment update is in progress. Wait for it to finish, then try again.")
|
||||
return
|
||||
}
|
||||
Self.firmwareProcess = process
|
||||
|
||||
@@ -171,6 +171,8 @@ nonisolated struct VPhoneLaunchpadControlCommand: Sendable {
|
||||
|
||||
Self(name: "cfw.install", arguments: ["name"], options: ["root"], flags: ["keep-artifacts"],
|
||||
summary: "Install CFW into a stopped machine through the root helper."),
|
||||
Self(name: "cfw.update-environment", arguments: ["name"], options: ["root"], flags: [],
|
||||
summary: "Redeploy the active bundle's guest resources (vphoned and hook dylibs) into a stopped machine, and nothing else."),
|
||||
|
||||
Self(name: "guest.send", arguments: ["name", "json"], options: ["root"], flags: [],
|
||||
summary: "Send one raw vphone.sock request, such as {\"t\":\"tap\",\"x\":645,\"y\":1398}."),
|
||||
|
||||
@@ -58,6 +58,18 @@ nonisolated protocol VPhoneLaunchpadHelperProtocol {
|
||||
reply: @escaping @Sendable (Int32, String?) -> Void,
|
||||
)
|
||||
|
||||
/// Runs `vphone-cli cfw update-environment` from a store bundle as root:
|
||||
/// redeploys the bundle's guest resources into a stopped machine and
|
||||
/// nothing else. Same output channel and reply as `installCustomFirmware`,
|
||||
/// and `cancelCustomFirmware` stops it.
|
||||
func updateGuestEnvironment(
|
||||
authorization: Data,
|
||||
bundleVersion: String,
|
||||
machineName: String,
|
||||
libraryRoot: String,
|
||||
reply: @escaping @Sendable (Int32, String?) -> Void,
|
||||
)
|
||||
|
||||
/// Sends SIGINT to a running CFW install started by the same user. It
|
||||
/// needs no authorization: it can only stop the caller's own install.
|
||||
func cancelCustomFirmware(reply: @escaping @Sendable () -> Void)
|
||||
|
||||
Reference in New Issue
Block a user