zig-mobile-runner 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +484 -0
- package/CONTRIBUTING.md +42 -0
- package/FEATURES.md +112 -0
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/SECURITY.md +34 -0
- package/build.zig +38 -0
- package/build.zig.zon +7 -0
- package/clients/README.md +144 -0
- package/clients/go/README.md +24 -0
- package/clients/go/examples/fake-session/main.go +93 -0
- package/clients/go/go.mod +3 -0
- package/clients/go/zmr/client.go +432 -0
- package/clients/kotlin/README.md +35 -0
- package/clients/kotlin/build.gradle.kts +35 -0
- package/clients/kotlin/settings.gradle.kts +15 -0
- package/clients/kotlin/src/main/kotlin/dev/zmr/FakeSession.kt +86 -0
- package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +67 -0
- package/clients/python/README.md +29 -0
- package/clients/python/examples/fake_session.py +48 -0
- package/clients/python/pyproject.toml +13 -0
- package/clients/python/zmr_client.py +202 -0
- package/clients/rust/Cargo.lock +107 -0
- package/clients/rust/Cargo.toml +10 -0
- package/clients/rust/README.md +19 -0
- package/clients/rust/examples/fake_session.rs +70 -0
- package/clients/rust/src/lib.rs +461 -0
- package/clients/swift/Package.swift +16 -0
- package/clients/swift/README.md +36 -0
- package/clients/swift/Sources/ZMRClient/ZMRClient.swift +114 -0
- package/clients/swift/Sources/ZMRFakeSession/main.swift +86 -0
- package/clients/typescript/README.md +34 -0
- package/clients/typescript/examples/fake-session.mjs +36 -0
- package/clients/typescript/index.d.ts +144 -0
- package/clients/typescript/index.mjs +192 -0
- package/clients/typescript/package.json +8 -0
- package/docs/adr/0001-agent-native-runner-boundary.md +31 -0
- package/docs/adr/0002-app-local-zmr-contract.md +39 -0
- package/docs/adr/0003-ios-simulator-xctest-shim.md +41 -0
- package/docs/adr/0004-benchmark-claims-and-baseline-collection.md +37 -0
- package/docs/adr/README.md +12 -0
- package/docs/ai-agents.md +156 -0
- package/docs/app-integration.md +316 -0
- package/docs/benchmarking.md +275 -0
- package/docs/client-installation.md +141 -0
- package/docs/clients.md +98 -0
- package/docs/config.md +175 -0
- package/docs/demo.md +259 -0
- package/docs/dsl.md +57 -0
- package/docs/install.md +233 -0
- package/docs/market-positioning.md +70 -0
- package/docs/npm.md +359 -0
- package/docs/protocol-fixtures/README.md +8 -0
- package/docs/protocol-fixtures/core-session.requests.jsonl +8 -0
- package/docs/protocol-fixtures/core-session.responses.jsonl +8 -0
- package/docs/protocol-versioning.md +65 -0
- package/docs/protocol.md +560 -0
- package/docs/publication.md +77 -0
- package/docs/release-audit.md +99 -0
- package/docs/release-candidate.md +111 -0
- package/docs/release-evidence.md +188 -0
- package/docs/release-notes-template.md +58 -0
- package/docs/roadmap.md +334 -0
- package/docs/scenario-authoring.md +88 -0
- package/docs/shipping.md +170 -0
- package/docs/trace-privacy.md +88 -0
- package/docs/troubleshooting.md +256 -0
- package/examples/android-app-auth-probe.json +89 -0
- package/examples/android-app-error-state.json +13 -0
- package/examples/android-app-login-smoke.json +192 -0
- package/examples/android-app-onboarding.json +12 -0
- package/examples/android-app-referral-deep-link.json +12 -0
- package/examples/android-shim-smoke.json +19 -0
- package/examples/demo-failure.json +12 -0
- package/examples/demo-fake.json +14 -0
- package/examples/ios-dev-client-open-link.json +26 -0
- package/examples/ios-dev-client-route-snapshot.json +24 -0
- package/examples/ios-shim-smoke.json +23 -0
- package/examples/ios-smoke.json +9 -0
- package/go.work +3 -0
- package/npm/agents.mjs +183 -0
- package/npm/app-config.mjs +95 -0
- package/npm/build-zmr.mjs +21 -0
- package/npm/commands.mjs +104 -0
- package/npm/generated-files.mjs +50 -0
- package/npm/index.mjs +75 -0
- package/npm/init-app.mjs +80 -0
- package/npm/package-scripts.mjs +72 -0
- package/npm/postinstall.mjs +21 -0
- package/npm/scaffold.mjs +179 -0
- package/npm/scenarios.mjs +93 -0
- package/npm/setup.mjs +69 -0
- package/npm/wizard.mjs +117 -0
- package/npm/zmr.mjs +23 -0
- package/package.json +114 -0
- package/prebuilds/darwin-arm64/zmr +0 -0
- package/prebuilds/darwin-x64/zmr +0 -0
- package/prebuilds/linux-arm64/zmr +0 -0
- package/prebuilds/linux-x64/zmr +0 -0
- package/schemas/README.md +26 -0
- package/schemas/action-result.schema.json +27 -0
- package/schemas/capabilities-output.schema.json +98 -0
- package/schemas/devices-output.schema.json +25 -0
- package/schemas/doctor-output.schema.json +51 -0
- package/schemas/explain-output.schema.json +51 -0
- package/schemas/import-output.schema.json +23 -0
- package/schemas/init-output.schema.json +71 -0
- package/schemas/json-rpc.schema.json +55 -0
- package/schemas/release-manifest.schema.json +43 -0
- package/schemas/release-readiness-output.schema.json +127 -0
- package/schemas/run-output.schema.json +43 -0
- package/schemas/scenario.schema.json +128 -0
- package/schemas/schemas-output.schema.json +26 -0
- package/schemas/semantic-snapshot.schema.json +116 -0
- package/schemas/snapshot.schema.json +60 -0
- package/schemas/trace-event.schema.json +14 -0
- package/schemas/trace-manifest.schema.json +59 -0
- package/schemas/validate-output.schema.json +42 -0
- package/schemas/version-output.schema.json +23 -0
- package/schemas/zmr-config.schema.json +75 -0
- package/scripts/android-emulator.sh +126 -0
- package/scripts/assert-ios-physical-ready.sh +213 -0
- package/scripts/benchmark-command.sh +307 -0
- package/scripts/benchmark.sh +359 -0
- package/scripts/benchmark_gate.py +117 -0
- package/scripts/benchmark_result_row.py +88 -0
- package/scripts/compare-benchmarks.py +288 -0
- package/scripts/create-android-demo-app.sh +342 -0
- package/scripts/create-ios-demo-app.sh +261 -0
- package/scripts/demo-android-real.sh +232 -0
- package/scripts/demo-ios-real.sh +270 -0
- package/scripts/demo.sh +464 -0
- package/scripts/device-matrix.sh +338 -0
- package/scripts/ensure-ios-shim-target.rb +237 -0
- package/scripts/install-android-shim.sh +281 -0
- package/scripts/install-ios-shim.sh +589 -0
- package/scripts/pilot-gate.sh +560 -0
- package/scripts/release-readiness.py +838 -0
- package/scripts/release-readiness.sh +91 -0
- package/scripts/run-android-pilot.sh +561 -0
- package/scripts/run-ios-pilot.sh +509 -0
- package/shims/android/README.md +21 -0
- package/shims/android/ZMRShimInstrumentedTest.java +152 -0
- package/shims/android/protocol.md +18 -0
- package/shims/ios/README.md +50 -0
- package/shims/ios/ZMRShim.swift +110 -0
- package/shims/ios/ZMRShimUITestCase.swift +475 -0
- package/shims/ios/protocol.md +74 -0
- package/skills/zmr-mobile-testing/SKILL.md +127 -0
- package/src/android.zig +344 -0
- package/src/android_device_info.zig +99 -0
- package/src/android_emulator.zig +154 -0
- package/src/android_screen_recording.zig +112 -0
- package/src/android_shell.zig +112 -0
- package/src/bundle.zig +124 -0
- package/src/bundle_redaction.zig +272 -0
- package/src/bundle_tar.zig +123 -0
- package/src/cli_devices.zig +97 -0
- package/src/cli_doctor.zig +114 -0
- package/src/cli_import.zig +70 -0
- package/src/cli_info.zig +39 -0
- package/src/cli_init.zig +72 -0
- package/src/cli_output.zig +467 -0
- package/src/cli_run.zig +259 -0
- package/src/cli_serve.zig +287 -0
- package/src/cli_trace.zig +111 -0
- package/src/cli_validate.zig +41 -0
- package/src/command.zig +211 -0
- package/src/config.zig +305 -0
- package/src/config_diagnostics.zig +212 -0
- package/src/config_paths.zig +49 -0
- package/src/device_registry.zig +37 -0
- package/src/doctor.zig +412 -0
- package/src/doctor_hints.zig +52 -0
- package/src/errors.zig +55 -0
- package/src/fake_device.zig +163 -0
- package/src/health.zig +28 -0
- package/src/importer.zig +343 -0
- package/src/importer_json.zig +100 -0
- package/src/importer_model.zig +103 -0
- package/src/ios.zig +399 -0
- package/src/ios_devices.zig +219 -0
- package/src/ios_lifecycle.zig +72 -0
- package/src/ios_shim.zig +242 -0
- package/src/ios_snapshot.zig +20 -0
- package/src/json_fields.zig +80 -0
- package/src/json_rpc.zig +150 -0
- package/src/json_rpc_methods.zig +318 -0
- package/src/json_rpc_observation.zig +31 -0
- package/src/json_rpc_params.zig +52 -0
- package/src/json_rpc_protocol.zig +110 -0
- package/src/json_rpc_trace.zig +73 -0
- package/src/main.zig +135 -0
- package/src/mcp.zig +234 -0
- package/src/mcp_protocol.zig +64 -0
- package/src/mcp_trace.zig +83 -0
- package/src/report.zig +346 -0
- package/src/report_html.zig +63 -0
- package/src/report_values.zig +27 -0
- package/src/run_options.zig +152 -0
- package/src/runner.zig +280 -0
- package/src/runner_actions.zig +109 -0
- package/src/runner_config.zig +6 -0
- package/src/runner_diagnostics.zig +268 -0
- package/src/runner_events.zig +170 -0
- package/src/runner_native.zig +88 -0
- package/src/runner_waits.zig +300 -0
- package/src/scaffold.zig +472 -0
- package/src/scenario.zig +346 -0
- package/src/scenario_fields.zig +50 -0
- package/src/schema_registry.zig +53 -0
- package/src/selector.zig +84 -0
- package/src/semantic.zig +171 -0
- package/src/trace.zig +315 -0
- package/src/trace_json.zig +340 -0
- package/src/trace_summary.zig +218 -0
- package/src/trace_summary_diagnostic.zig +202 -0
- package/src/types.zig +120 -0
- package/src/uiautomator.zig +164 -0
- package/src/validation.zig +187 -0
- package/src/version.zig +22 -0
- package/viewer/app.js +373 -0
- package/viewer/index.html +126 -0
- package/viewer/parser.js +233 -0
- package/viewer/styles.css +585 -0
package/src/ios.zig
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const command = @import("command.zig");
|
|
3
|
+
const ios_devices = @import("ios_devices.zig");
|
|
4
|
+
const ios_lifecycle = @import("ios_lifecycle.zig");
|
|
5
|
+
const ios_snapshot = @import("ios_snapshot.zig");
|
|
6
|
+
const ios_shim = @import("ios_shim.zig");
|
|
7
|
+
const selector = @import("selector.zig");
|
|
8
|
+
const trace = @import("trace.zig");
|
|
9
|
+
const types = @import("types.zig");
|
|
10
|
+
|
|
11
|
+
const default_max_output = 32 * 1024 * 1024;
|
|
12
|
+
const shim_timeout_ms = 600_000;
|
|
13
|
+
const shim_best_effort_timeout_ms = 10_000;
|
|
14
|
+
const shim_command_attempts = 2;
|
|
15
|
+
const shim_bootstrap_retry_delay_ms = 500;
|
|
16
|
+
|
|
17
|
+
pub const TargetKind = enum {
|
|
18
|
+
simulator,
|
|
19
|
+
physical,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
pub const IosDevice = struct {
|
|
23
|
+
allocator: std.mem.Allocator,
|
|
24
|
+
xcrun_path: []const u8 = "xcrun",
|
|
25
|
+
udid: ?[]const u8 = null,
|
|
26
|
+
app_id: []const u8,
|
|
27
|
+
shim_path: ?[]const u8 = null,
|
|
28
|
+
target_kind: TargetKind = .simulator,
|
|
29
|
+
|
|
30
|
+
pub fn init(
|
|
31
|
+
allocator: std.mem.Allocator,
|
|
32
|
+
xcrun_path: []const u8,
|
|
33
|
+
udid: ?[]const u8,
|
|
34
|
+
app_id: []const u8,
|
|
35
|
+
) !IosDevice {
|
|
36
|
+
return try initWithShim(allocator, xcrun_path, udid, app_id, null);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
pub fn initWithShim(
|
|
40
|
+
allocator: std.mem.Allocator,
|
|
41
|
+
xcrun_path: []const u8,
|
|
42
|
+
udid: ?[]const u8,
|
|
43
|
+
app_id: []const u8,
|
|
44
|
+
shim_path: ?[]const u8,
|
|
45
|
+
) !IosDevice {
|
|
46
|
+
return try initWithKindAndShim(allocator, xcrun_path, udid, app_id, .simulator, shim_path);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
pub fn initWithKindAndShim(
|
|
50
|
+
allocator: std.mem.Allocator,
|
|
51
|
+
xcrun_path: []const u8,
|
|
52
|
+
udid: ?[]const u8,
|
|
53
|
+
app_id: []const u8,
|
|
54
|
+
target_kind: TargetKind,
|
|
55
|
+
shim_path: ?[]const u8,
|
|
56
|
+
) !IosDevice {
|
|
57
|
+
const owned_xcrun = try allocator.dupe(u8, xcrun_path);
|
|
58
|
+
errdefer allocator.free(owned_xcrun);
|
|
59
|
+
const owned_udid = try types.dupeOptional(allocator, udid);
|
|
60
|
+
errdefer if (owned_udid) |value| allocator.free(value);
|
|
61
|
+
const owned_app_id = try allocator.dupe(u8, app_id);
|
|
62
|
+
errdefer allocator.free(owned_app_id);
|
|
63
|
+
const owned_shim_path = try types.dupeOptional(allocator, shim_path);
|
|
64
|
+
|
|
65
|
+
return .{
|
|
66
|
+
.allocator = allocator,
|
|
67
|
+
.xcrun_path = owned_xcrun,
|
|
68
|
+
.udid = owned_udid,
|
|
69
|
+
.app_id = owned_app_id,
|
|
70
|
+
.shim_path = owned_shim_path,
|
|
71
|
+
.target_kind = target_kind,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
pub fn deinit(self: *IosDevice) void {
|
|
76
|
+
self.allocator.free(self.xcrun_path);
|
|
77
|
+
if (self.udid) |value| self.allocator.free(value);
|
|
78
|
+
self.allocator.free(self.app_id);
|
|
79
|
+
if (self.shim_path) |value| self.allocator.free(value);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
pub fn listDevices(self: *IosDevice) ![]types.DeviceInfo {
|
|
83
|
+
return switch (self.target_kind) {
|
|
84
|
+
.simulator => try ios_devices.listSimulators(self.allocator, self.xcrun_path),
|
|
85
|
+
.physical => try ios_devices.listPhysical(self.allocator, self.xcrun_path),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
pub fn install(self: *IosDevice, app_path: []const u8) !void {
|
|
90
|
+
if (self.target_kind == .physical) return try self.installPhysical(app_path);
|
|
91
|
+
const result = try self.runSimctl(&.{ "install", self.target(), app_path }, default_max_output);
|
|
92
|
+
defer result.deinit(self.allocator);
|
|
93
|
+
try result.ensureSuccess();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
pub fn launch(self: *IosDevice) !void {
|
|
97
|
+
if (self.target_kind == .physical) return try self.launchPhysical(null);
|
|
98
|
+
const result = try self.runSimctl(&.{ "launch", self.target(), self.app_id }, default_max_output);
|
|
99
|
+
defer result.deinit(self.allocator);
|
|
100
|
+
result.ensureSuccess() catch |err| {
|
|
101
|
+
if (self.appIsRunningFromShimBestEffort()) return;
|
|
102
|
+
return err;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
pub fn stop(self: *IosDevice) !void {
|
|
107
|
+
if (self.target_kind == .physical) return try self.stopPhysicalBestEffort();
|
|
108
|
+
const result = try self.runSimctl(&.{ "terminate", self.target(), self.app_id }, default_max_output);
|
|
109
|
+
defer result.deinit(self.allocator);
|
|
110
|
+
try result.ensureSuccess();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
pub fn clearState(self: *IosDevice) !void {
|
|
114
|
+
if (self.target_kind == .physical) return try self.uninstallPhysicalBestEffort();
|
|
115
|
+
const result = try self.runSimctl(&.{ "uninstall", self.target(), self.app_id }, default_max_output);
|
|
116
|
+
defer result.deinit(self.allocator);
|
|
117
|
+
if (ios_lifecycle.isMissingInstalledApp(result)) return;
|
|
118
|
+
try result.ensureSuccess();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
pub fn openLink(self: *IosDevice, url: []const u8) !void {
|
|
122
|
+
if (self.target_kind == .physical) return try self.launchPhysical(url);
|
|
123
|
+
const result = try self.runSimctl(&.{ "openurl", self.target(), url }, default_max_output);
|
|
124
|
+
defer result.deinit(self.allocator);
|
|
125
|
+
try result.ensureSuccess();
|
|
126
|
+
self.acceptOpenURLConfirmationBestEffort();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
pub fn tap(self: *IosDevice, x: i32, y: i32) !void {
|
|
130
|
+
try self.runShimAction(.{ .kind = .tap, .x = x, .y = y });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
pub fn tapBySelector(self: *IosDevice, wanted: selector.Selector) !bool {
|
|
134
|
+
return try self.runShimSelectorAction(.{ .kind = .tap }, wanted);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
pub fn visibleBySelector(self: *IosDevice, wanted: selector.Selector) !?bool {
|
|
138
|
+
if (self.shim_path == null) return null;
|
|
139
|
+
const shim_selector = try ios_shim.selectorString(self.allocator, wanted) orelse return null;
|
|
140
|
+
defer self.allocator.free(shim_selector);
|
|
141
|
+
|
|
142
|
+
const response = try self.runShim(.{ .kind = .query, .selector = shim_selector });
|
|
143
|
+
defer self.allocator.free(response);
|
|
144
|
+
return try ios_shim.parseQueryResponse(response);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
pub fn typeText(self: *IosDevice, text: []const u8) !void {
|
|
148
|
+
try self.runShimAction(.{ .kind = .type_text, .text = text });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
pub fn typeTextBySelector(self: *IosDevice, wanted: selector.Selector, text: []const u8) !bool {
|
|
152
|
+
return try self.runShimSelectorAction(.{ .kind = .type_text, .text = text }, wanted);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
pub fn eraseText(self: *IosDevice, max_chars: u32) !void {
|
|
156
|
+
try self.runShimAction(.{ .kind = .erase_text, .max_chars = max_chars });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
pub fn eraseTextBySelector(self: *IosDevice, wanted: selector.Selector, max_chars: u32) !bool {
|
|
160
|
+
return try self.runShimSelectorAction(.{ .kind = .erase_text, .max_chars = max_chars }, wanted);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
pub fn hideKeyboard(self: *IosDevice) !void {
|
|
164
|
+
try self.runShimAction(.{ .kind = .hide_keyboard });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
pub fn swipe(self: *IosDevice, x1: i32, y1: i32, x2: i32, y2: i32, duration_ms: u32) !void {
|
|
168
|
+
try self.runShimAction(.{ .kind = .swipe, .x1 = x1, .y1 = y1, .x2 = x2, .y2 = y2, .duration_ms = duration_ms });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
pub fn pressBack(self: *IosDevice) !void {
|
|
172
|
+
try self.runShimAction(.{ .kind = .press_back });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
pub fn settle(self: *IosDevice, timeout_ms: u64) !void {
|
|
176
|
+
if (self.shim_path != null) {
|
|
177
|
+
return try self.runShimAction(.{
|
|
178
|
+
.kind = .settle,
|
|
179
|
+
.duration_ms = @as(u32, @intCast(@min(timeout_ms, std.math.maxInt(u32)))),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
std.Thread.sleep(timeout_ms * std.time.ns_per_ms);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
pub fn snapshot(self: *IosDevice, writer: ?*trace.TraceWriter) !types.ObservationSnapshot {
|
|
186
|
+
const id = if (writer) |tw| try tw.nextSnapshotId() else try std.fmt.allocPrint(self.allocator, "snapshot-{d}", .{std.time.milliTimestamp()});
|
|
187
|
+
errdefer self.allocator.free(id);
|
|
188
|
+
|
|
189
|
+
var screenshot_artifact: ?[]const u8 = null;
|
|
190
|
+
errdefer if (screenshot_artifact) |path| self.allocator.free(path);
|
|
191
|
+
var viewport: types.Viewport = .{};
|
|
192
|
+
|
|
193
|
+
if (writer) |tw| {
|
|
194
|
+
if (tw.capture.capture_screenshots) {
|
|
195
|
+
const screenshot = self.captureScreenshot() catch null;
|
|
196
|
+
if (screenshot) |bytes| {
|
|
197
|
+
defer self.allocator.free(bytes);
|
|
198
|
+
viewport = ios_snapshot.parsePngViewport(bytes) orelse .{};
|
|
199
|
+
const file_name = try std.fmt.allocPrint(self.allocator, "{s}.png", .{id});
|
|
200
|
+
defer self.allocator.free(file_name);
|
|
201
|
+
screenshot_artifact = try tw.writeArtifact(file_name, bytes);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const capture_logs = if (writer) |tw| tw.capture.capture_logs else true;
|
|
207
|
+
const logs = if (capture_logs) self.logDelta() catch null else null;
|
|
208
|
+
errdefer if (logs) |value| self.allocator.free(value);
|
|
209
|
+
|
|
210
|
+
const active_package = try self.allocator.dupe(u8, self.app_id);
|
|
211
|
+
errdefer self.allocator.free(active_package);
|
|
212
|
+
const nodes = if (self.shim_path != null)
|
|
213
|
+
self.snapshotNodesFromShim() catch |err| blk: {
|
|
214
|
+
if (screenshot_artifact == null) return err;
|
|
215
|
+
if (writer) |tw| try self.recordSnapshotSemanticFailure(tw, screenshot_artifact.?, err);
|
|
216
|
+
break :blk try self.allocator.alloc(types.UiNode, 0);
|
|
217
|
+
}
|
|
218
|
+
else
|
|
219
|
+
try self.allocator.alloc(types.UiNode, 0);
|
|
220
|
+
errdefer self.allocator.free(nodes);
|
|
221
|
+
|
|
222
|
+
return .{
|
|
223
|
+
.id = id,
|
|
224
|
+
.timestamp_ms = std.time.milliTimestamp(),
|
|
225
|
+
.viewport = viewport,
|
|
226
|
+
.active_package = active_package,
|
|
227
|
+
.active_activity = null,
|
|
228
|
+
.screenshot_artifact = screenshot_artifact,
|
|
229
|
+
.tree_artifact = null,
|
|
230
|
+
.focused_node_id = null,
|
|
231
|
+
.log_delta = logs,
|
|
232
|
+
.nodes = nodes,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
fn captureScreenshot(self: *IosDevice) ![]u8 {
|
|
237
|
+
if (self.target_kind == .physical) {
|
|
238
|
+
const response = try self.runShim(.{ .kind = .screenshot });
|
|
239
|
+
defer self.allocator.free(response);
|
|
240
|
+
return try ios_shim.parseScreenshotPng(self.allocator, response);
|
|
241
|
+
}
|
|
242
|
+
const path = try std.fmt.allocPrint(self.allocator, "/tmp/zmr-ios-screenshot-{d}.png", .{std.time.nanoTimestamp()});
|
|
243
|
+
defer self.allocator.free(path);
|
|
244
|
+
defer std.fs.cwd().deleteFile(path) catch {};
|
|
245
|
+
|
|
246
|
+
const result = try self.runSimctl(&.{ "io", self.target(), "screenshot", path }, default_max_output);
|
|
247
|
+
defer result.deinit(self.allocator);
|
|
248
|
+
try result.ensureSuccess();
|
|
249
|
+
return try std.fs.cwd().readFileAlloc(self.allocator, path, default_max_output);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fn logDelta(self: *IosDevice) !?[]const u8 {
|
|
253
|
+
if (self.target_kind == .physical) return null;
|
|
254
|
+
const result = try self.runSimctl(&.{ "spawn", self.target(), "log", "show", "--style", "compact", "--last", "30s" }, 1024 * 1024);
|
|
255
|
+
defer result.deinit(self.allocator);
|
|
256
|
+
if (result.term != .Exited or result.term.Exited != 0) return null;
|
|
257
|
+
return try self.allocator.dupe(u8, result.stdout);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
fn snapshotNodesFromShim(self: *IosDevice) ![]types.UiNode {
|
|
261
|
+
const response = try self.runShim(.{ .kind = .snapshot });
|
|
262
|
+
defer self.allocator.free(response);
|
|
263
|
+
return try ios_shim.parseSnapshotNodes(self.allocator, response);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
fn recordSnapshotSemanticFailure(self: *IosDevice, writer: *trace.TraceWriter, screenshot_artifact: []const u8, err: anyerror) !void {
|
|
267
|
+
var payload = std.ArrayList(u8).empty;
|
|
268
|
+
defer payload.deinit(writer.allocator);
|
|
269
|
+
const out = payload.writer(writer.allocator);
|
|
270
|
+
try out.writeAll("{\"status\":\"failed\",\"artifactStatus\":\"captured\",\"semanticStatus\":\"failed\",\"error\":");
|
|
271
|
+
try trace.writeJsonString(out, @errorName(err));
|
|
272
|
+
try out.writeAll(",\"screenshotArtifact\":");
|
|
273
|
+
try trace.writeJsonString(out, screenshot_artifact);
|
|
274
|
+
try out.writeAll(",\"source\":\"ios-xctest-shim\"}");
|
|
275
|
+
try writer.recordEvent("observe.snapshot.semanticExtraction", payload.items);
|
|
276
|
+
_ = self;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
fn runShimAction(self: *IosDevice, shim_command: ios_shim.Command) !void {
|
|
280
|
+
const response = try self.runShim(shim_command);
|
|
281
|
+
defer self.allocator.free(response);
|
|
282
|
+
try ios_shim.parseOkResponse(response);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
fn runShimActionWithTimeout(self: *IosDevice, shim_command: ios_shim.Command, timeout_ms: u64) !void {
|
|
286
|
+
const response = try self.runShimWithTimeout(shim_command, timeout_ms);
|
|
287
|
+
defer self.allocator.free(response);
|
|
288
|
+
try ios_shim.parseOkResponse(response);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
fn acceptOpenURLConfirmationBestEffort(self: *IosDevice) void {
|
|
292
|
+
if (self.shim_path == null) return;
|
|
293
|
+
self.runShimActionWithTimeout(.{ .kind = .accept_system_alert, .text = "Open" }, shim_best_effort_timeout_ms) catch {};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
fn appIsRunningFromShimBestEffort(self: *IosDevice) bool {
|
|
297
|
+
if (self.shim_path == null) return false;
|
|
298
|
+
const response = self.runShimWithTimeout(.{ .kind = .app_state }, shim_best_effort_timeout_ms) catch return false;
|
|
299
|
+
defer self.allocator.free(response);
|
|
300
|
+
return ios_shim.parseAppStateRunning(response) catch false;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
fn runShimSelectorAction(self: *IosDevice, shim_command: ios_shim.Command, wanted: selector.Selector) !bool {
|
|
304
|
+
if (self.shim_path == null) return false;
|
|
305
|
+
const shim_selector = try ios_shim.selectorString(self.allocator, wanted) orelse return false;
|
|
306
|
+
defer self.allocator.free(shim_selector);
|
|
307
|
+
|
|
308
|
+
var command_with_selector = shim_command;
|
|
309
|
+
command_with_selector.selector = shim_selector;
|
|
310
|
+
try self.runShimAction(command_with_selector);
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
fn runShim(self: *IosDevice, shim_command: ios_shim.Command) ![]u8 {
|
|
315
|
+
return self.runShimWithTimeout(shim_command, shim_timeout_ms);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
fn runShimWithTimeout(self: *IosDevice, shim_command: ios_shim.Command, timeout_ms: u64) ![]u8 {
|
|
319
|
+
const path = self.shim_path orelse return error.IosXCTestShimRequired;
|
|
320
|
+
|
|
321
|
+
var input = std.ArrayList(u8).empty;
|
|
322
|
+
defer input.deinit(self.allocator);
|
|
323
|
+
try ios_shim.writeCommandJson(input.writer(self.allocator), shim_command);
|
|
324
|
+
|
|
325
|
+
var attempt: usize = 0;
|
|
326
|
+
while (attempt < shim_command_attempts) {
|
|
327
|
+
attempt += 1;
|
|
328
|
+
const result = try command.runWithInputTimeout(self.allocator, &.{path}, input.items, 4 * 1024 * 1024, timeout_ms);
|
|
329
|
+
defer result.deinit(self.allocator);
|
|
330
|
+
|
|
331
|
+
result.ensureSuccess() catch |err| {
|
|
332
|
+
if (attempt < shim_command_attempts and err == error.CommandFailed and isTransientShimBootstrapFailure(result)) {
|
|
333
|
+
std.Thread.sleep(shim_bootstrap_retry_delay_ms * std.time.ns_per_ms);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
return err;
|
|
337
|
+
};
|
|
338
|
+
return try self.allocator.dupe(u8, result.stdout);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return error.CommandFailed;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
fn target(self: *IosDevice) []const u8 {
|
|
345
|
+
return self.udid orelse "booted";
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
fn runSimctl(self: *IosDevice, extra: []const []const u8, max_output_bytes: usize) !command.ExecResult {
|
|
349
|
+
return try ios_devices.runSimctlCommand(self.allocator, self.xcrun_path, extra, max_output_bytes);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
fn installPhysical(self: *IosDevice, app_path: []const u8) !void {
|
|
353
|
+
try ios_lifecycle.installPhysical(self.allocator, self.xcrun_path, self.target(), app_path);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
fn launchPhysical(self: *IosDevice, url: ?[]const u8) !void {
|
|
357
|
+
try ios_lifecycle.launchPhysical(self.allocator, self.xcrun_path, self.target(), self.app_id, url);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
fn stopPhysicalBestEffort(self: *IosDevice) !void {
|
|
361
|
+
try ios_lifecycle.stopPhysicalBestEffort(self.allocator, self.xcrun_path, self.target(), self.app_id);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
fn uninstallPhysicalBestEffort(self: *IosDevice) !void {
|
|
365
|
+
try ios_lifecycle.uninstallPhysicalBestEffort(self.allocator, self.xcrun_path, self.target(), self.app_id);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
fn isTransientShimBootstrapFailure(result: command.ExecResult) bool {
|
|
370
|
+
if (result.timed_out) return false;
|
|
371
|
+
switch (result.term) {
|
|
372
|
+
.Exited => |code| if (code == 0) return false,
|
|
373
|
+
else => return false,
|
|
374
|
+
}
|
|
375
|
+
return std.mem.indexOf(u8, result.stderr, "iOS shim server exited before it became ready") != null or
|
|
376
|
+
std.mem.indexOf(u8, result.stderr, "Early unexpected exit") != null or
|
|
377
|
+
std.mem.indexOf(u8, result.stderr, "operation never finished bootstrapping") != null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
pub fn listDevices(allocator: std.mem.Allocator, xcrun_path: []const u8) ![]types.DeviceInfo {
|
|
381
|
+
return try ios_devices.listSimulators(allocator, xcrun_path);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
pub fn listPhysicalDevices(allocator: std.mem.Allocator, xcrun_path: []const u8) ![]types.DeviceInfo {
|
|
385
|
+
return try ios_devices.listPhysical(allocator, xcrun_path);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
pub fn parseDevicesJson(allocator: std.mem.Allocator, content: []const u8) ![]types.DeviceInfo {
|
|
389
|
+
return try ios_devices.parseSimulatorsJson(allocator, content);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
pub fn parsePhysicalDevicesJson(allocator: std.mem.Allocator, content: []const u8) ![]types.DeviceInfo {
|
|
393
|
+
return try ios_devices.parsePhysicalDevicesJson(allocator, content);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
test "ios xctest shim timeout allows cold xcodebuild startup" {
|
|
397
|
+
try std.testing.expect(shim_timeout_ms >= 300_000);
|
|
398
|
+
try std.testing.expect(shim_best_effort_timeout_ms <= 15_000);
|
|
399
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const command = @import("command.zig");
|
|
3
|
+
const types = @import("types.zig");
|
|
4
|
+
|
|
5
|
+
const default_max_output = 32 * 1024 * 1024;
|
|
6
|
+
const simctl_retry_attempts = 6;
|
|
7
|
+
const simctl_retry_delay_ms = 500;
|
|
8
|
+
|
|
9
|
+
pub fn listSimulators(allocator: std.mem.Allocator, xcrun_path: []const u8) ![]types.DeviceInfo {
|
|
10
|
+
const result = try runSimctlCommand(allocator, xcrun_path, &.{ "list", "devices", "--json" }, 4 * 1024 * 1024);
|
|
11
|
+
defer result.deinit(allocator);
|
|
12
|
+
try result.ensureSuccess();
|
|
13
|
+
return try parseSimulatorsJson(allocator, result.stdout);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
pub fn listPhysical(allocator: std.mem.Allocator, xcrun_path: []const u8) ![]types.DeviceInfo {
|
|
17
|
+
const json = try runDevicectlJsonCommand(allocator, xcrun_path, &.{ "list", "devices" });
|
|
18
|
+
defer allocator.free(json);
|
|
19
|
+
return try parsePhysicalDevicesJson(allocator, json);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
pub fn runSimctlCommand(
|
|
23
|
+
allocator: std.mem.Allocator,
|
|
24
|
+
xcrun_path: []const u8,
|
|
25
|
+
extra: []const []const u8,
|
|
26
|
+
max_output_bytes: usize,
|
|
27
|
+
) !command.ExecResult {
|
|
28
|
+
var argv = std.ArrayList([]const u8).empty;
|
|
29
|
+
defer argv.deinit(allocator);
|
|
30
|
+
try argv.append(allocator, xcrun_path);
|
|
31
|
+
try argv.append(allocator, "simctl");
|
|
32
|
+
try argv.appendSlice(allocator, extra);
|
|
33
|
+
|
|
34
|
+
var attempt: usize = 0;
|
|
35
|
+
while (true) {
|
|
36
|
+
const result = try command.run(allocator, argv.items, max_output_bytes);
|
|
37
|
+
if (attempt + 1 >= simctl_retry_attempts or !isRetriableSimctlFailure(result)) {
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
result.deinit(allocator);
|
|
41
|
+
attempt += 1;
|
|
42
|
+
std.Thread.sleep(simctl_retry_delay_ms * std.time.ns_per_ms);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
pub fn runDevicectlCommand(
|
|
47
|
+
allocator: std.mem.Allocator,
|
|
48
|
+
xcrun_path: []const u8,
|
|
49
|
+
extra: []const []const u8,
|
|
50
|
+
max_output_bytes: usize,
|
|
51
|
+
) !command.ExecResult {
|
|
52
|
+
var argv = std.ArrayList([]const u8).empty;
|
|
53
|
+
defer argv.deinit(allocator);
|
|
54
|
+
try argv.append(allocator, xcrun_path);
|
|
55
|
+
try argv.append(allocator, "devicectl");
|
|
56
|
+
try argv.appendSlice(allocator, extra);
|
|
57
|
+
return try command.run(allocator, argv.items, max_output_bytes);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
pub fn runDevicectlJsonCommand(
|
|
61
|
+
allocator: std.mem.Allocator,
|
|
62
|
+
xcrun_path: []const u8,
|
|
63
|
+
extra: []const []const u8,
|
|
64
|
+
) ![]u8 {
|
|
65
|
+
const path = try std.fmt.allocPrint(allocator, "/tmp/zmr-devicectl-{d}.json", .{std.time.nanoTimestamp()});
|
|
66
|
+
defer allocator.free(path);
|
|
67
|
+
defer std.fs.cwd().deleteFile(path) catch {};
|
|
68
|
+
|
|
69
|
+
var argv = std.ArrayList([]const u8).empty;
|
|
70
|
+
defer argv.deinit(allocator);
|
|
71
|
+
try argv.append(allocator, xcrun_path);
|
|
72
|
+
try argv.append(allocator, "devicectl");
|
|
73
|
+
try argv.appendSlice(allocator, extra);
|
|
74
|
+
try argv.appendSlice(allocator, &.{ "--json-output", path, "--quiet" });
|
|
75
|
+
|
|
76
|
+
const result = try command.run(allocator, argv.items, default_max_output);
|
|
77
|
+
defer result.deinit(allocator);
|
|
78
|
+
try result.ensureSuccess();
|
|
79
|
+
return try std.fs.cwd().readFileAlloc(allocator, path, default_max_output);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
pub fn parseSimulatorsJson(allocator: std.mem.Allocator, content: []const u8) ![]types.DeviceInfo {
|
|
83
|
+
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, content, .{});
|
|
84
|
+
defer parsed.deinit();
|
|
85
|
+
if (parsed.value != .object) return error.SimctlDevicesMustBeObject;
|
|
86
|
+
const devices_value = parsed.value.object.get("devices") orelse return error.SimctlDevicesMissingDevices;
|
|
87
|
+
if (devices_value != .object) return error.SimctlDevicesMustBeObject;
|
|
88
|
+
|
|
89
|
+
var devices = std.ArrayList(types.DeviceInfo).empty;
|
|
90
|
+
errdefer {
|
|
91
|
+
for (devices.items) |device| device.deinit(allocator);
|
|
92
|
+
devices.deinit(allocator);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
var runtime_iterator = devices_value.object.iterator();
|
|
96
|
+
while (runtime_iterator.next()) |runtime_entry| {
|
|
97
|
+
const runtime_devices = runtime_entry.value_ptr.*;
|
|
98
|
+
if (runtime_devices != .array) continue;
|
|
99
|
+
for (runtime_devices.array.items) |device_value| {
|
|
100
|
+
if (device_value != .object) continue;
|
|
101
|
+
const object = device_value.object;
|
|
102
|
+
if (fieldBool(object, "isAvailable") == false) continue;
|
|
103
|
+
const udid = fieldString(object, "udid") orelse continue;
|
|
104
|
+
const state = fieldString(object, "state") orelse continue;
|
|
105
|
+
if (!std.mem.eql(u8, state, "Booted")) continue;
|
|
106
|
+
try devices.append(allocator, .{
|
|
107
|
+
.serial = try allocator.dupe(u8, udid),
|
|
108
|
+
.state = try allocator.dupe(u8, state),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return try devices.toOwnedSlice(allocator);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
pub fn parsePhysicalDevicesJson(allocator: std.mem.Allocator, content: []const u8) ![]types.DeviceInfo {
|
|
117
|
+
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, content, .{});
|
|
118
|
+
defer parsed.deinit();
|
|
119
|
+
if (parsed.value != .object) return error.DevicectlDevicesMustBeObject;
|
|
120
|
+
const result_value = parsed.value.object.get("result") orelse return error.DevicectlDevicesMissingResult;
|
|
121
|
+
if (result_value != .object) return error.DevicectlDevicesMustBeObject;
|
|
122
|
+
const devices_value = result_value.object.get("devices") orelse return error.DevicectlDevicesMissingDevices;
|
|
123
|
+
if (devices_value != .array) return error.DevicectlDevicesMustBeArray;
|
|
124
|
+
|
|
125
|
+
var devices = std.ArrayList(types.DeviceInfo).empty;
|
|
126
|
+
errdefer {
|
|
127
|
+
for (devices.items) |device| device.deinit(allocator);
|
|
128
|
+
devices.deinit(allocator);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (devices_value.array.items) |device_value| {
|
|
132
|
+
if (device_value != .object) continue;
|
|
133
|
+
const object = device_value.object;
|
|
134
|
+
const hardware = fieldObject(object, "hardwareProperties") orelse continue;
|
|
135
|
+
if (!fieldStringEquals(hardware, "platform", "iOS")) continue;
|
|
136
|
+
if (!fieldStringEquals(hardware, "reality", "physical")) continue;
|
|
137
|
+
const serial = fieldString(object, "identifier") orelse fieldString(hardware, "udid") orelse continue;
|
|
138
|
+
const connection = fieldObject(object, "connectionProperties");
|
|
139
|
+
const state = if (connection) |value|
|
|
140
|
+
fieldString(value, "tunnelState") orelse fieldString(value, "pairingState") orelse "available"
|
|
141
|
+
else
|
|
142
|
+
"available";
|
|
143
|
+
try devices.append(allocator, .{
|
|
144
|
+
.serial = try allocator.dupe(u8, serial),
|
|
145
|
+
.state = try allocator.dupe(u8, state),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return try devices.toOwnedSlice(allocator);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
pub fn findPidForBundleId(allocator: std.mem.Allocator, content: []const u8, app_id: []const u8) !?i64 {
|
|
153
|
+
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, content, .{});
|
|
154
|
+
defer parsed.deinit();
|
|
155
|
+
return findPidInValue(parsed.value, app_id);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fn isRetriableSimctlFailure(result: command.ExecResult) bool {
|
|
159
|
+
if (result.timed_out) return false;
|
|
160
|
+
switch (result.term) {
|
|
161
|
+
.Exited => |code| if (code == 0) return false,
|
|
162
|
+
else => return false,
|
|
163
|
+
}
|
|
164
|
+
return std.mem.indexOf(u8, result.stderr, "CoreSimulatorService connection became invalid") != null or
|
|
165
|
+
std.mem.indexOf(u8, result.stderr, "Failed to initialize simulator device set") != null or
|
|
166
|
+
std.mem.indexOf(u8, result.stderr, "simdiskimaged") != null or
|
|
167
|
+
std.mem.indexOf(u8, result.stderr, "Connection refused") != null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
fn findPidInValue(value: std.json.Value, app_id: []const u8) ?i64 {
|
|
171
|
+
switch (value) {
|
|
172
|
+
.object => |object| {
|
|
173
|
+
var has_bundle = false;
|
|
174
|
+
var pid: ?i64 = null;
|
|
175
|
+
var iterator = object.iterator();
|
|
176
|
+
while (iterator.next()) |entry| {
|
|
177
|
+
if (entry.value_ptr.* == .string and std.mem.eql(u8, entry.value_ptr.string, app_id)) {
|
|
178
|
+
has_bundle = true;
|
|
179
|
+
}
|
|
180
|
+
if (std.mem.indexOf(u8, entry.key_ptr.*, "pid") != null or std.mem.indexOf(u8, entry.key_ptr.*, "processIdentifier") != null) {
|
|
181
|
+
if (entry.value_ptr.* == .integer) pid = entry.value_ptr.integer;
|
|
182
|
+
}
|
|
183
|
+
if (findPidInValue(entry.value_ptr.*, app_id)) |nested| return nested;
|
|
184
|
+
}
|
|
185
|
+
if (has_bundle) return pid;
|
|
186
|
+
return null;
|
|
187
|
+
},
|
|
188
|
+
.array => |array| {
|
|
189
|
+
for (array.items) |item| {
|
|
190
|
+
if (findPidInValue(item, app_id)) |pid| return pid;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
},
|
|
194
|
+
else => return null,
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
fn fieldObject(object: std.json.ObjectMap, key: []const u8) ?std.json.ObjectMap {
|
|
199
|
+
const value = object.get(key) orelse return null;
|
|
200
|
+
if (value != .object) return null;
|
|
201
|
+
return value.object;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
fn fieldStringEquals(object: std.json.ObjectMap, key: []const u8, expected: []const u8) bool {
|
|
205
|
+
const value = fieldString(object, key) orelse return false;
|
|
206
|
+
return std.mem.eql(u8, value, expected);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
fn fieldString(object: std.json.ObjectMap, key: []const u8) ?[]const u8 {
|
|
210
|
+
const value = object.get(key) orelse return null;
|
|
211
|
+
if (value != .string) return null;
|
|
212
|
+
return value.string;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
fn fieldBool(object: std.json.ObjectMap, key: []const u8) bool {
|
|
216
|
+
const value = object.get(key) orelse return true;
|
|
217
|
+
if (value != .bool) return true;
|
|
218
|
+
return value.bool;
|
|
219
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const command = @import("command.zig");
|
|
3
|
+
const ios_devices = @import("ios_devices.zig");
|
|
4
|
+
|
|
5
|
+
const default_max_output = 32 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
pub fn installPhysical(
|
|
8
|
+
allocator: std.mem.Allocator,
|
|
9
|
+
xcrun_path: []const u8,
|
|
10
|
+
target: []const u8,
|
|
11
|
+
app_path: []const u8,
|
|
12
|
+
) !void {
|
|
13
|
+
const result = try ios_devices.runDevicectlCommand(allocator, xcrun_path, &.{ "device", "install", "app", "--device", target, app_path }, default_max_output);
|
|
14
|
+
defer result.deinit(allocator);
|
|
15
|
+
try result.ensureSuccess();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pub fn launchPhysical(
|
|
19
|
+
allocator: std.mem.Allocator,
|
|
20
|
+
xcrun_path: []const u8,
|
|
21
|
+
target: []const u8,
|
|
22
|
+
app_id: []const u8,
|
|
23
|
+
url: ?[]const u8,
|
|
24
|
+
) !void {
|
|
25
|
+
var argv = std.ArrayList([]const u8).empty;
|
|
26
|
+
defer argv.deinit(allocator);
|
|
27
|
+
try argv.appendSlice(allocator, &.{ "device", "process", "launch", "--device", target, "--terminate-existing" });
|
|
28
|
+
if (url) |value| try argv.appendSlice(allocator, &.{ "--payload-url", value });
|
|
29
|
+
try argv.append(allocator, app_id);
|
|
30
|
+
|
|
31
|
+
const result = try ios_devices.runDevicectlCommand(allocator, xcrun_path, argv.items, default_max_output);
|
|
32
|
+
defer result.deinit(allocator);
|
|
33
|
+
try result.ensureSuccess();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pub fn stopPhysicalBestEffort(
|
|
37
|
+
allocator: std.mem.Allocator,
|
|
38
|
+
xcrun_path: []const u8,
|
|
39
|
+
target: []const u8,
|
|
40
|
+
app_id: []const u8,
|
|
41
|
+
) !void {
|
|
42
|
+
const process_json = ios_devices.runDevicectlJsonCommand(allocator, xcrun_path, &.{ "device", "info", "processes", "--device", target }) catch return;
|
|
43
|
+
defer allocator.free(process_json);
|
|
44
|
+
const pid = ios_devices.findPidForBundleId(allocator, process_json, app_id) catch null;
|
|
45
|
+
if (pid) |value| {
|
|
46
|
+
const pid_text = try std.fmt.allocPrint(allocator, "{d}", .{value});
|
|
47
|
+
defer allocator.free(pid_text);
|
|
48
|
+
const result = try ios_devices.runDevicectlCommand(allocator, xcrun_path, &.{ "device", "process", "terminate", "--device", target, "--pid", pid_text }, default_max_output);
|
|
49
|
+
defer result.deinit(allocator);
|
|
50
|
+
try result.ensureSuccess();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
pub fn uninstallPhysicalBestEffort(
|
|
55
|
+
allocator: std.mem.Allocator,
|
|
56
|
+
xcrun_path: []const u8,
|
|
57
|
+
target: []const u8,
|
|
58
|
+
app_id: []const u8,
|
|
59
|
+
) !void {
|
|
60
|
+
const result = try ios_devices.runDevicectlCommand(allocator, xcrun_path, &.{ "device", "uninstall", "app", "--device", target, app_id }, default_max_output);
|
|
61
|
+
defer result.deinit(allocator);
|
|
62
|
+
if (isMissingInstalledApp(result)) return;
|
|
63
|
+
try result.ensureSuccess();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
pub fn isMissingInstalledApp(result: command.ExecResult) bool {
|
|
67
|
+
switch (result.term) {
|
|
68
|
+
.Exited => |code| if (code == 0) return false,
|
|
69
|
+
else => return false,
|
|
70
|
+
}
|
|
71
|
+
return std.mem.indexOf(u8, result.stderr, "No installed application with bundle identifier") != null;
|
|
72
|
+
}
|