Skip to content

Commit 8256599

Browse files
committed
fix: stop harmony emulator keepalive
1 parent 7b99515 commit 8256599

14 files changed

Lines changed: 249 additions & 4 deletions

File tree

.agents/tritonkit-skills/public/tritonkit-dev-feedback/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ Repository: `NeptuneKit/TritonKit` (`https://github.com/NeptuneKit/TritonKit`)
9090
- `triton device list --platform harmony --json`
9191
- `triton device alias set harmony-a --platform harmony --target <hdc-target> --json` for repeated multi-emulator validation;
9292
- `triton device wait-ready --device harmony-a --json`
93+
- `triton device stop --platform harmony --hvd <hvd-name> --path <deployed-path> --confirm --json` when Triton's `triton-harmony-emulator` launchd keepalive job started the HVD;
9394
- `triton app inspect --platform harmony --bundle <bundle> --target <hdc-target> --json`
9495
- `triton app install --device harmony-a --hap <debug-signed.hap> --json`
9596
- `triton app launch --device harmony-a --bundle <bundle> --ability <ability> --json`

.agents/tritonkit-skills/public/tritonkit-emulator-cli-takeover/SKILL.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,13 @@ triton device wait-ready --device iphone15 --json
8080
triton device wait-ready --device harmony-a --json
8181
triton device screenshot --device iphone15 --output /tmp/<case>-sim.png --json
8282
triton device screenshot --device harmony-a --output /tmp/<case>.jpeg --json
83+
triton device stop --platform harmony --hvd "Codex Test Phone" --path ~/.Huawei/Emulator/deployed --confirm --json
8384
```
8485

8586
Use `--device <selector>` as the default agent-facing target selector for common host-side commands. Selectors can be aliases, full ids such as `sim:<udid>` / `harmony:<target>`, raw platform ids, `booted`, or `current`. `--platform`, `--name`, `--runtime`, `--state`, and `--ready` are filters; they may auto-select only when the filtered candidate set is unique. Keep `sim` for iOS-only advanced maintenance; `device runtime-url --device <selector>` is the Harmony embedded runtime port-forward setup path, with `--platform harmony --target <target>` retained for compatibility.
8687

88+
When a Harmony HVD was started through Triton's `triton-harmony-emulator` launchd keepalive job, stop it through `triton device stop --platform harmony ... --confirm --json`. The command checks and unloads `gui/<uid>/triton-harmony-emulator` before running DevEco `Emulator -stop`, which prevents launchd from immediately restarting the emulator.
89+
8790
iOS Simulator:
8891

8992
```bash

.agents/tritonkit-skills/public/tritonkit-real-project-regression/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ Real-project validation is not the same as demo smoke. Treat the business app as
9090
- list HDC targets: `triton device list --platform harmony --json`;
9191
- for repeated multi-emulator work, create a stable selector: `triton device alias set harmony-a --platform harmony --target <hdc-target> --json`;
9292
- wait for boot readiness: `triton device wait-ready --device harmony-a --json`;
93+
- close a Triton-supervised HVD with launchd cleanup: `triton device stop --platform harmony --hvd <hvd-name> --path <deployed-path> --confirm --json`;
9394
- inspect app metadata: `triton app inspect --platform harmony --bundle <bundle> --target <hdc-target> --json`;
9495
- install a debug HAP when needed: `triton app install --device harmony-a --hap <debug-signed.hap> --json`;
9596
- launch an Ability: `triton app launch --device harmony-a --bundle <bundle> --ability <ability> --json`;

CLI/Tests/TritonKitCLITests/DeviceCrossPlatformTests.swift

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,60 @@ struct DeviceCrossPlatformTests {
1919
#expect(optionNames.contains("wait-ready --device <selector>"))
2020
#expect(optionNames.contains("screenshot --device <selector> --output <path>"))
2121
#expect(optionNames.contains("runtime-url --device <selector>"))
22+
#expect(optionNames.contains("stop --platform harmony --hvd <name> --path <deployed-path> --confirm"))
2223
#expect(optionNames.contains("--device"))
2324
#expect(optionNames.contains("--name"))
2425
#expect(optionNames.contains("--runtime"))
2526
#expect(optionNames.contains("runtime-url --platform harmony --target <target>"))
2627
#expect(device.examples.contains("triton device runtime-url --device harmony-a --probe-manifest --json"))
28+
#expect(device.examples.contains("triton device stop --platform harmony --hvd 'Codex Test Phone' --path ~/.Huawei/Emulator/deployed --confirm --json"))
2729
#expect(device.providedCapabilities.contains("host-device"))
2830
#expect(device.providedCapabilities.contains("device-alias"))
2931
#expect(device.providedCapabilities.contains("host-device-selector"))
3032
#expect(device.providedCapabilities.contains("device-list"))
3133
#expect(device.providedCapabilities.contains("device-use"))
3234
#expect(device.providedCapabilities.contains("device-wait-ready"))
3335
#expect(device.providedCapabilities.contains("device-screenshot"))
36+
#expect(device.providedCapabilities.contains("harmony-device-stop"))
37+
}
38+
39+
@Test("Harmony emulator stop plans launchd bootout before DevEco stop")
40+
func harmonyEmulatorStopPlansLaunchdBootoutBeforeDevEcoStop() throws {
41+
let plan = try harmonyEmulatorStopPlan(
42+
hvd: "Codex Test Phone",
43+
deployedPath: "/Users/linhey/.Huawei/Emulator/deployed",
44+
emulator: "/Applications/DevEco-Studio.app/Contents/tools/emulator/Emulator",
45+
launchdLabel: "triton-harmony-emulator",
46+
launchdDomain: "gui/501",
47+
includeLaunchd: true,
48+
confirmed: true
49+
)
50+
51+
#expect(plan.action == "device.stop")
52+
#expect(plan.platform == "harmony")
53+
#expect(plan.hvd == "Codex Test Phone")
54+
#expect(plan.launchdLabel == "triton-harmony-emulator")
55+
#expect(plan.launchdDomain == "gui/501")
56+
#expect(plan.commands.map(hostSourceCommand) == [
57+
"launchctl print gui/501/triton-harmony-emulator",
58+
"launchctl bootout gui/501/triton-harmony-emulator",
59+
"/Applications/DevEco-Studio.app/Contents/tools/emulator/Emulator -stop 'Codex Test Phone' -path /Users/linhey/.Huawei/Emulator/deployed",
60+
])
61+
}
62+
63+
@Test("Harmony emulator stop requires explicit confirmation")
64+
func harmonyEmulatorStopRequiresConfirmation() {
65+
#expect(throws: HostDeviceSelectionError.self) {
66+
_ = try harmonyEmulatorStopPlan(
67+
hvd: "Codex Test Phone",
68+
deployedPath: "/Users/linhey/.Huawei/Emulator/deployed",
69+
emulator: "/Applications/DevEco-Studio.app/Contents/tools/emulator/Emulator",
70+
launchdLabel: "triton-harmony-emulator",
71+
launchdDomain: "gui/501",
72+
includeLaunchd: true,
73+
confirmed: false
74+
)
75+
}
3476
}
3577

3678
@Test("app and smoke schemas expose unified device selector with compatibility paths")

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,7 @@ triton device list --platform harmony --json
420420
triton device use --platform harmony --target 127.0.0.1:10100 --json
421421
triton device wait-ready --device 127.0.0.1:10100 --json
422422
triton device screenshot --device 127.0.0.1:10100 --output /tmp/smoke.jpeg --json
423+
triton device stop --platform harmony --hvd "Codex Test Phone" --path ~/.Huawei/Emulator/deployed --confirm --json
423424
triton app inspect --platform harmony --bundle com.example.app --target 127.0.0.1:10100 --json
424425
triton app install --device 127.0.0.1:10100 --hap /tmp/Demo.hap --json
425426
triton app launch --device 127.0.0.1:10100 --bundle com.example.app --ability EntryAbility --json
@@ -432,6 +433,8 @@ triton screenshot --platform harmony --target 127.0.0.1:10100 --output /tmp/smok
432433

433434
When multiple HDC targets are `Connected`, Triton returns `error.code=ambiguous_target` and requires an explicit `--target`. The adapter records `sourceCommand`; risk/policy metadata is for audit and configuration validation, not an interactive confirmation gate.
434435

436+
When Triton starts a Harmony HVD through its `triton-harmony-emulator` launchd keepalive job, close it with `triton device stop --platform harmony ... --confirm --json` instead of raw `Emulator -stop`. The Triton command unloads `gui/<uid>/triton-harmony-emulator` before calling DevEco `Emulator -stop`, so launchd does not restart the emulator after a successful stop.
437+
435438
Harmony host-side `ax/wait/tap/screenshot` wrap `uitest dumpLayout`, `uitest uiInput click`, and `snapshot_display` with JSON envelopes. Layout and screenshot outputs can contain private UI data; inspect or redact artifacts before attaching them to public issues.
436439

437440
For repeatable regression flows, wait for asynchronous UI state before the next action or assertion:

Sources/TritonKitCLI/CLIHostCommands.swift

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1694,7 +1694,7 @@ struct Device: AsyncParsableCommand {
16941694
static let configuration = CommandConfiguration(
16951695
commandName: "device",
16961696
abstract: "Discover and inspect host-side devices and emulators",
1697-
subcommands: [DeviceDoctor.self, DeviceList.self, DeviceAlias.self, DeviceUse.self, DeviceCurrent.self, DeviceResolve.self, DeviceWaitReady.self, DeviceScreenshot.self, DeviceRuntimeURL.self]
1697+
subcommands: [DeviceDoctor.self, DeviceList.self, DeviceAlias.self, DeviceUse.self, DeviceCurrent.self, DeviceResolve.self, DeviceWaitReady.self, DeviceScreenshot.self, DeviceRuntimeURL.self, DeviceStop.self]
16981698
)
16991699
}
17001700

@@ -2137,3 +2137,61 @@ struct DeviceRuntimeURL: AsyncParsableCommand {
21372137
}
21382138
}
21392139
}
2140+
2141+
struct DeviceStop: AsyncParsableCommand {
2142+
static let configuration = CommandConfiguration(commandName: "stop", abstract: "Stop a host-side device or emulator")
2143+
2144+
@Option(help: "Platform adapter: harmony") var platform: HostDevicePlatform = .harmony
2145+
@Option(help: "Harmony HVD name, for example Codex Test Phone") var hvd: String
2146+
@Option(help: "DevEco deployed emulator path, for example ~/.Huawei/Emulator/deployed") var path: String
2147+
@Option(help: "Path to DevEco Emulator executable") var emulator: String = "/Applications/DevEco-Studio.app/Contents/tools/emulator/Emulator"
2148+
@Option(help: "Triton launchd job label to unload before stopping") var launchdLabel: String = "triton-harmony-emulator"
2149+
@Option(help: "launchd domain, defaults to gui/<uid>") var launchdDomain: String = defaultLaunchdDomain()
2150+
@Flag(help: "Skip launchd print/bootout and only run Emulator -stop") var skipLaunchd = false
2151+
@Flag(help: "Confirm stopping the emulator and unloading Triton launchd supervision") var confirm = false
2152+
@Flag(help: "Alias for --format json") var json = false
2153+
@Option(help: "Output format: text or json") var format: ClientOutputFormat = .json
2154+
2155+
func run() async throws {
2156+
let outputFormat = effectiveFormat(format, json: json)
2157+
do {
2158+
guard platform == .harmony else {
2159+
throw RuntimeError("device stop currently supports Harmony Emulator only")
2160+
}
2161+
let plan = try harmonyEmulatorStopPlan(
2162+
hvd: hvd,
2163+
deployedPath: path,
2164+
emulator: emulator,
2165+
launchdLabel: launchdLabel,
2166+
launchdDomain: launchdDomain,
2167+
includeLaunchd: !skipLaunchd,
2168+
confirmed: confirm
2169+
)
2170+
var sourceCommands: [String] = []
2171+
for command in plan.commands {
2172+
let result = try runHostCommand(command)
2173+
sourceCommands.append(result.sourceCommand)
2174+
}
2175+
let output = HostDeviceStopOutput(
2176+
ok: true,
2177+
action: plan.action,
2178+
platform: plan.platform,
2179+
hvd: plan.hvd,
2180+
deployedPath: plan.deployedPath,
2181+
emulator: plan.emulator,
2182+
launchdLabel: plan.launchdLabel,
2183+
launchdDomain: plan.launchdDomain,
2184+
sourceCommands: sourceCommands,
2185+
note: "Harmony emulator stop completed. Verify with `triton device list --platform harmony --json`; the target should remain disconnected or absent."
2186+
)
2187+
switch outputFormat {
2188+
case .json:
2189+
print(try encodeJSON(output))
2190+
case .text:
2191+
print(output.note)
2192+
}
2193+
} catch {
2194+
try failHostCommand(error, outputFormat: outputFormat)
2195+
}
2196+
}
2197+
}

Sources/TritonKitCLI/CLIHostModels.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,30 @@ struct HostDeviceArtifactOutput: Encodable {
195195
let note: String
196196
}
197197

198+
struct HarmonyEmulatorStopPlan {
199+
let action: String
200+
let platform: String
201+
let hvd: String
202+
let deployedPath: String
203+
let emulator: String
204+
let launchdLabel: String?
205+
let launchdDomain: String?
206+
let commands: [TKHostCommand]
207+
}
208+
209+
struct HostDeviceStopOutput: Encodable {
210+
let ok: Bool
211+
let action: String
212+
let platform: String
213+
let hvd: String
214+
let deployedPath: String
215+
let emulator: String
216+
let launchdLabel: String?
217+
let launchdDomain: String?
218+
let sourceCommands: [String]
219+
let note: String
220+
}
221+
198222
enum HostDeviceSelectionError: Error, CustomStringConvertible {
199223
case ambiguousTargets([HostDeviceTarget])
200224
case targetNotFound(String)

Sources/TritonKitCLI/CLIHostRuntime.swift

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,89 @@ func harmonyTarget(from target: HostDeviceTarget) -> TKHarmonyTarget {
547547
)
548548
}
549549

550+
func defaultLaunchdDomain() -> String {
551+
"gui/\(getuid())"
552+
}
553+
554+
func harmonyLaunchctlPrintCommand(domain: String, label: String) -> TKHostCommand {
555+
TKHostCommand(
556+
executable: "launchctl",
557+
arguments: ["print", "\(domain)/\(label)"],
558+
riskLevel: .readonly,
559+
requiredConfig: [.timeout]
560+
)
561+
}
562+
563+
func harmonyLaunchctlBootoutCommand(domain: String, label: String) -> TKHostCommand {
564+
TKHostCommand(
565+
executable: "launchctl",
566+
arguments: ["bootout", "\(domain)/\(label)"],
567+
riskLevel: .automation,
568+
requiredConfig: [.timeout, .auditRecord]
569+
)
570+
}
571+
572+
func harmonyEmulatorStopCommand(hvd: String, deployedPath: String, emulator: String) -> TKHostCommand {
573+
TKHostCommand(
574+
executable: emulator,
575+
arguments: ["-stop", hvd, "-path", deployedPath],
576+
riskLevel: .automation,
577+
requiredConfig: [.target, .timeout, .auditRecord]
578+
)
579+
}
580+
581+
func harmonyEmulatorStopPlan(
582+
hvd: String,
583+
deployedPath: String,
584+
emulator: String,
585+
launchdLabel: String,
586+
launchdDomain: String,
587+
includeLaunchd: Bool,
588+
confirmed: Bool
589+
) throws -> HarmonyEmulatorStopPlan {
590+
guard confirmed else {
591+
throw HostDeviceSelectionError.parameterConflict("device stop requires --confirm.")
592+
}
593+
let trimmedHVD = hvd.trimmingCharacters(in: .whitespacesAndNewlines)
594+
let trimmedPath = deployedPath.trimmingCharacters(in: .whitespacesAndNewlines)
595+
let trimmedEmulator = emulator.trimmingCharacters(in: .whitespacesAndNewlines)
596+
guard !trimmedHVD.isEmpty else {
597+
throw HostDeviceSelectionError.parameterConflict("Harmony device stop requires --hvd.")
598+
}
599+
guard !trimmedPath.isEmpty else {
600+
throw HostDeviceSelectionError.parameterConflict("Harmony device stop requires --path.")
601+
}
602+
guard !trimmedEmulator.isEmpty else {
603+
throw HostDeviceSelectionError.parameterConflict("Harmony device stop requires --emulator.")
604+
}
605+
606+
var commands: [TKHostCommand] = []
607+
let label = launchdLabel.trimmingCharacters(in: .whitespacesAndNewlines)
608+
let domain = launchdDomain.trimmingCharacters(in: .whitespacesAndNewlines)
609+
if includeLaunchd {
610+
guard !label.isEmpty else {
611+
throw HostDeviceSelectionError.parameterConflict("Harmony device stop requires --launchd-label unless --skip-launchd is set.")
612+
}
613+
guard !domain.isEmpty else {
614+
throw HostDeviceSelectionError.parameterConflict("Harmony device stop requires --launchd-domain unless --skip-launchd is set.")
615+
}
616+
commands.append(harmonyLaunchctlPrintCommand(domain: domain, label: label))
617+
commands.append(harmonyLaunchctlBootoutCommand(domain: domain, label: label))
618+
}
619+
commands.append(harmonyEmulatorStopCommand(hvd: trimmedHVD, deployedPath: trimmedPath, emulator: trimmedEmulator))
620+
621+
return HarmonyEmulatorStopPlan(
622+
action: "device.stop",
623+
platform: "harmony",
624+
hvd: trimmedHVD,
625+
deployedPath: trimmedPath,
626+
emulator: trimmedEmulator,
627+
launchdLabel: includeLaunchd ? label : nil,
628+
launchdDomain: includeLaunchd ? domain : nil,
629+
commands: commands
630+
)
631+
}
632+
550633
func ensureParentDirectory(for path: String) throws {
551634
let directory = URL(fileURLWithPath: path).deletingLastPathComponent()
552635
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)

Sources/TritonKitCLI/CLIRuntimeTransport.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ func runtimeCapabilities(connected: Bool) -> [TKRuntimeCapability] {
270270
TKRuntimeCapability(name: "harmony-device-use", supported: true),
271271
TKRuntimeCapability(name: "harmony-device-wait-ready", supported: true),
272272
TKRuntimeCapability(name: "harmony-device-screenshot", supported: true),
273+
TKRuntimeCapability(name: "harmony-device-stop", supported: true),
273274
TKRuntimeCapability(name: "harmony-runtime-url", supported: true),
274275
TKRuntimeCapability(name: "harmony-app-install", supported: true),
275276
TKRuntimeCapability(name: "harmony-app-open-url", supported: true),

0 commit comments

Comments
 (0)