zeno-mobile-runner 0.2.0 → 0.2.2
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 +52 -0
- package/FEATURES.md +1 -1
- package/README.md +9 -1
- package/build.zig.zon +2 -2
- package/clients/kotlin/README.md +1 -1
- package/clients/kotlin/build.gradle.kts +1 -1
- package/clients/python/pyproject.toml +1 -1
- package/clients/rust/Cargo.lock +1 -1
- package/clients/rust/Cargo.toml +1 -1
- package/clients/typescript/package.json +1 -1
- package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
- package/docs/protocol.md +10 -10
- package/examples/ios-dev-client-open-link.json +24 -13
- package/examples/ios-dev-client-route-snapshot.json +33 -8
- package/npm/scenarios.mjs +15 -8
- package/npm/wizard.mjs +1 -1
- package/package.json +3 -1
- 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/scripts/create-react-native-expo-demo-app.sh +11 -13
- package/shims/ios/ZMRShim.swift +40 -12
- package/shims/ios/ZMRShimUITestCase.swift +142 -16
- package/src/android.zig +10 -9
- package/src/android_emulator.zig +22 -11
- package/src/android_screen_recording.zig +11 -7
- package/src/bundle.zig +10 -9
- package/src/bundle_redaction.zig +29 -28
- package/src/bundle_tar.zig +15 -12
- package/src/cli_devices.zig +7 -3
- package/src/cli_discover.zig +7 -3
- package/src/cli_doctor.zig +7 -3
- package/src/cli_draft.zig +51 -47
- package/src/cli_explore.zig +7 -3
- package/src/cli_import.zig +8 -4
- package/src/cli_info.zig +13 -6
- package/src/cli_init.zig +9 -5
- package/src/cli_inspect.zig +8 -4
- package/src/cli_run.zig +22 -16
- package/src/cli_serve.zig +3 -3
- package/src/cli_trace.zig +25 -12
- package/src/cli_validate.zig +8 -4
- package/src/command.zig +81 -99
- package/src/config.zig +2 -1
- package/src/config_diagnostics.zig +2 -1
- package/src/config_paths.zig +2 -1
- package/src/doctor.zig +8 -7
- package/src/doctor_hints.zig +1 -1
- package/src/errors.zig +5 -5
- package/src/importer.zig +8 -7
- package/src/ios.zig +26 -29
- package/src/ios_devices.zig +6 -5
- package/src/ios_lifecycle.zig +4 -4
- package/src/json_rpc.zig +39 -40
- package/src/json_rpc_methods.zig +8 -8
- package/src/json_rpc_observation.zig +9 -8
- package/src/json_rpc_params.zig +1 -1
- package/src/json_rpc_trace.zig +22 -21
- package/src/main.zig +22 -10
- package/src/mcp.zig +28 -19
- package/src/mcp_trace.zig +30 -29
- package/src/report.zig +39 -36
- package/src/report_html.zig +5 -4
- package/src/runner.zig +2 -1
- package/src/runner_actions.zig +20 -17
- package/src/runner_diagnostics.zig +4 -4
- package/src/runner_events.zig +55 -51
- package/src/runner_native.zig +21 -19
- package/src/runner_waits.zig +46 -41
- package/src/scaffold.zig +25 -24
- package/src/scenario.zig +4 -3
- package/src/stdio.zig +129 -0
- package/src/trace.zig +34 -26
- package/src/trace_summary.zig +3 -2
- package/src/trace_summary_diagnostic.zig +15 -13
- package/src/validation.zig +5 -4
- package/src/version.zig +1 -1
package/src/ios.zig
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const std = @import("std");
|
|
2
|
+
const stdio = @import("stdio.zig");
|
|
2
3
|
const command = @import("command.zig");
|
|
3
4
|
const ios_devices = @import("ios_devices.zig");
|
|
4
5
|
const ios_lifecycle = @import("ios_lifecycle.zig");
|
|
@@ -128,9 +129,13 @@ pub const IosDevice = struct {
|
|
|
128
129
|
const result = try self.runSimctl(&.{ "openurl", self.target(), url }, default_max_output);
|
|
129
130
|
defer result.deinit(self.allocator);
|
|
130
131
|
try result.ensureSuccess();
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
// Opening a URL on the simulator can raise a SpringBoard "Open in <App>?"
|
|
133
|
+
// confirmation for universal links (http/https) and, just as often, for
|
|
134
|
+
// custom schemes — the common Expo dev-client case
|
|
135
|
+
// (exp+scheme://expo-development-client/...). Attempt a best-effort accept
|
|
136
|
+
// whenever a shim is configured; the shim probes briefly and returns fast
|
|
137
|
+
// when no dialog is present, so this stays cheap on the no-prompt path.
|
|
138
|
+
self.acceptOpenURLConfirmationBestEffort();
|
|
134
139
|
}
|
|
135
140
|
|
|
136
141
|
pub fn tap(self: *IosDevice, x: i32, y: i32) !void {
|
|
@@ -186,11 +191,11 @@ pub const IosDevice = struct {
|
|
|
186
191
|
.duration_ms = @as(u32, @intCast(@min(timeout_ms, std.math.maxInt(u32)))),
|
|
187
192
|
});
|
|
188
193
|
}
|
|
189
|
-
|
|
194
|
+
stdio.sleepNs(timeout_ms * std.time.ns_per_ms);
|
|
190
195
|
}
|
|
191
196
|
|
|
192
197
|
pub fn snapshot(self: *IosDevice, writer: ?*trace.TraceWriter) !types.ObservationSnapshot {
|
|
193
|
-
const id = if (writer) |tw| try tw.nextSnapshotId() else try std.fmt.allocPrint(self.allocator, "snapshot-{d}", .{
|
|
198
|
+
const id = if (writer) |tw| try tw.nextSnapshotId() else try std.fmt.allocPrint(self.allocator, "snapshot-{d}", .{stdio.nowMs()});
|
|
194
199
|
errdefer self.allocator.free(id);
|
|
195
200
|
|
|
196
201
|
var screenshot_artifact: ?[]const u8 = null;
|
|
@@ -235,7 +240,7 @@ pub const IosDevice = struct {
|
|
|
235
240
|
|
|
236
241
|
return .{
|
|
237
242
|
.id = id,
|
|
238
|
-
.timestamp_ms =
|
|
243
|
+
.timestamp_ms = stdio.nowMs(),
|
|
239
244
|
.viewport = viewport,
|
|
240
245
|
.active_package = active_package,
|
|
241
246
|
.active_activity = null,
|
|
@@ -253,21 +258,21 @@ pub const IosDevice = struct {
|
|
|
253
258
|
defer self.allocator.free(response);
|
|
254
259
|
return try ios_shim.parseScreenshotPng(self.allocator, response);
|
|
255
260
|
}
|
|
256
|
-
const path = try std.fmt.allocPrint(self.allocator, "/tmp/zmr-ios-screenshot-{d}.png", .{
|
|
261
|
+
const path = try std.fmt.allocPrint(self.allocator, "/tmp/zmr-ios-screenshot-{d}.png", .{stdio.nowNs()});
|
|
257
262
|
defer self.allocator.free(path);
|
|
258
|
-
defer std.
|
|
263
|
+
defer std.Io.Dir.cwd().deleteFile(stdio.io(), path) catch {};
|
|
259
264
|
|
|
260
265
|
const result = try self.runSimctl(&.{ "io", self.target(), "screenshot", path }, default_max_output);
|
|
261
266
|
defer result.deinit(self.allocator);
|
|
262
267
|
try result.ensureSuccess();
|
|
263
|
-
return try
|
|
268
|
+
return try stdio.readFileAlloc(self.allocator, path, default_max_output);
|
|
264
269
|
}
|
|
265
270
|
|
|
266
271
|
fn logDelta(self: *IosDevice) !?[]const u8 {
|
|
267
272
|
if (self.target_kind == .physical) return null;
|
|
268
273
|
const result = try self.runSimctl(&.{ "spawn", self.target(), "log", "show", "--style", "compact", "--last", "30s" }, 1024 * 1024);
|
|
269
274
|
defer result.deinit(self.allocator);
|
|
270
|
-
if (result.term != .
|
|
275
|
+
if (result.term != .exited or result.term.exited != 0) return null;
|
|
271
276
|
return try self.allocator.dupe(u8, result.stdout);
|
|
272
277
|
}
|
|
273
278
|
|
|
@@ -278,15 +283,15 @@ pub const IosDevice = struct {
|
|
|
278
283
|
}
|
|
279
284
|
|
|
280
285
|
fn recordSnapshotSemanticFailure(self: *IosDevice, writer: *trace.TraceWriter, screenshot_artifact: []const u8, err: anyerror) !void {
|
|
281
|
-
var payload =
|
|
282
|
-
defer payload.deinit(
|
|
283
|
-
const out = payload.writer
|
|
286
|
+
var payload: std.Io.Writer.Allocating = .init(writer.allocator);
|
|
287
|
+
defer payload.deinit();
|
|
288
|
+
const out = &payload.writer;
|
|
284
289
|
try out.writeAll("{\"status\":\"failed\",\"artifactStatus\":\"captured\",\"semanticStatus\":\"failed\",\"error\":");
|
|
285
290
|
try trace.writeJsonString(out, @errorName(err));
|
|
286
291
|
try out.writeAll(",\"screenshotArtifact\":");
|
|
287
292
|
try trace.writeJsonString(out, screenshot_artifact);
|
|
288
293
|
try out.writeAll(",\"source\":\"ios-xctest-shim\"}");
|
|
289
|
-
try writer.recordEvent("observe.snapshot.semanticExtraction",
|
|
294
|
+
try writer.recordEvent("observe.snapshot.semanticExtraction", out.buffered());
|
|
290
295
|
_ = self;
|
|
291
296
|
}
|
|
292
297
|
|
|
@@ -336,19 +341,19 @@ pub const IosDevice = struct {
|
|
|
336
341
|
fn runShimWithTimeout(self: *IosDevice, shim_command: ios_shim.Command, timeout_ms: u64) ![]u8 {
|
|
337
342
|
const path = self.shim_path orelse return error.IosXCTestShimRequired;
|
|
338
343
|
|
|
339
|
-
var input =
|
|
340
|
-
defer input.deinit(
|
|
341
|
-
try ios_shim.writeCommandJson(input.writer
|
|
344
|
+
var input: std.Io.Writer.Allocating = .init(self.allocator);
|
|
345
|
+
defer input.deinit();
|
|
346
|
+
try ios_shim.writeCommandJson(&input.writer, shim_command);
|
|
342
347
|
|
|
343
348
|
var attempt: usize = 0;
|
|
344
349
|
while (attempt < shim_command_attempts) {
|
|
345
350
|
attempt += 1;
|
|
346
|
-
const result = try command.runWithInputTimeout(self.allocator, &.{path}, input.
|
|
351
|
+
const result = try command.runWithInputTimeout(self.allocator, &.{path}, input.writer.buffered(), 4 * 1024 * 1024, timeout_ms);
|
|
347
352
|
defer result.deinit(self.allocator);
|
|
348
353
|
|
|
349
354
|
result.ensureSuccess() catch |err| {
|
|
350
355
|
if (attempt < shim_command_attempts and err == error.CommandFailed and isTransientShimBootstrapFailure(result)) {
|
|
351
|
-
|
|
356
|
+
stdio.sleepNs(shim_bootstrap_retry_delay_ms * std.time.ns_per_ms);
|
|
352
357
|
continue;
|
|
353
358
|
}
|
|
354
359
|
return err;
|
|
@@ -387,7 +392,7 @@ pub const IosDevice = struct {
|
|
|
387
392
|
fn isTransientShimBootstrapFailure(result: command.ExecResult) bool {
|
|
388
393
|
if (result.timed_out) return false;
|
|
389
394
|
switch (result.term) {
|
|
390
|
-
.
|
|
395
|
+
.exited => |code| if (code == 0) return false,
|
|
391
396
|
else => return false,
|
|
392
397
|
}
|
|
393
398
|
return std.mem.indexOf(u8, result.stderr, "iOS shim server exited before it became ready") != null or
|
|
@@ -396,7 +401,7 @@ fn isTransientShimBootstrapFailure(result: command.ExecResult) bool {
|
|
|
396
401
|
}
|
|
397
402
|
|
|
398
403
|
fn shimTimeoutMs() u64 {
|
|
399
|
-
return parseShimTimeoutMs(
|
|
404
|
+
return parseShimTimeoutMs(stdio.getenv(shim_timeout_env));
|
|
400
405
|
}
|
|
401
406
|
|
|
402
407
|
fn parseShimTimeoutMs(raw: ?[]const u8) u64 {
|
|
@@ -406,14 +411,6 @@ fn parseShimTimeoutMs(raw: ?[]const u8) u64 {
|
|
|
406
411
|
return parsed;
|
|
407
412
|
}
|
|
408
413
|
|
|
409
|
-
fn urlMayNeedOpenConfirmation(url: []const u8) bool {
|
|
410
|
-
return startsWithIgnoreCase(url, "http://") or startsWithIgnoreCase(url, "https://");
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
fn startsWithIgnoreCase(value: []const u8, prefix: []const u8) bool {
|
|
414
|
-
return value.len >= prefix.len and std.ascii.eqlIgnoreCase(value[0..prefix.len], prefix);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
414
|
pub fn listDevices(allocator: std.mem.Allocator, xcrun_path: []const u8) ![]types.DeviceInfo {
|
|
418
415
|
return try ios_devices.listSimulators(allocator, xcrun_path);
|
|
419
416
|
}
|
package/src/ios_devices.zig
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const std = @import("std");
|
|
2
|
+
const stdio = @import("stdio.zig");
|
|
2
3
|
const command = @import("command.zig");
|
|
3
4
|
const types = @import("types.zig");
|
|
4
5
|
|
|
@@ -39,7 +40,7 @@ pub fn runSimctlCommand(
|
|
|
39
40
|
}
|
|
40
41
|
result.deinit(allocator);
|
|
41
42
|
attempt += 1;
|
|
42
|
-
|
|
43
|
+
stdio.sleepNs(simctl_retry_delay_ms * std.time.ns_per_ms);
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -62,9 +63,9 @@ pub fn runDevicectlJsonCommand(
|
|
|
62
63
|
xcrun_path: []const u8,
|
|
63
64
|
extra: []const []const u8,
|
|
64
65
|
) ![]u8 {
|
|
65
|
-
const path = try std.fmt.allocPrint(allocator, "/tmp/zmr-devicectl-{d}.json", .{
|
|
66
|
+
const path = try std.fmt.allocPrint(allocator, "/tmp/zmr-devicectl-{d}.json", .{stdio.nowNs()});
|
|
66
67
|
defer allocator.free(path);
|
|
67
|
-
defer std.
|
|
68
|
+
defer std.Io.Dir.deleteFileAbsolute(stdio.io(), path) catch {};
|
|
68
69
|
|
|
69
70
|
var argv = std.ArrayList([]const u8).empty;
|
|
70
71
|
defer argv.deinit(allocator);
|
|
@@ -76,7 +77,7 @@ pub fn runDevicectlJsonCommand(
|
|
|
76
77
|
const result = try command.run(allocator, argv.items, default_max_output);
|
|
77
78
|
defer result.deinit(allocator);
|
|
78
79
|
try result.ensureSuccess();
|
|
79
|
-
return try std.
|
|
80
|
+
return try std.Io.Dir.cwd().readFileAlloc(stdio.io(), path, allocator, .limited(default_max_output));
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
pub fn parseSimulatorsJson(allocator: std.mem.Allocator, content: []const u8) ![]types.DeviceInfo {
|
|
@@ -158,7 +159,7 @@ pub fn findPidForBundleId(allocator: std.mem.Allocator, content: []const u8, app
|
|
|
158
159
|
fn isRetriableSimctlFailure(result: command.ExecResult) bool {
|
|
159
160
|
if (result.timed_out) return false;
|
|
160
161
|
switch (result.term) {
|
|
161
|
-
.
|
|
162
|
+
.exited => |code| if (code == 0) return false,
|
|
162
163
|
else => return false,
|
|
163
164
|
}
|
|
164
165
|
return std.mem.indexOf(u8, result.stderr, "CoreSimulatorService connection became invalid") != null or
|
package/src/ios_lifecycle.zig
CHANGED
|
@@ -65,7 +65,7 @@ pub fn uninstallPhysicalBestEffort(
|
|
|
65
65
|
|
|
66
66
|
pub fn isMissingInstalledApp(result: command.ExecResult) bool {
|
|
67
67
|
switch (result.term) {
|
|
68
|
-
.
|
|
68
|
+
.exited => |code| if (code == 0) return false,
|
|
69
69
|
else => return false,
|
|
70
70
|
}
|
|
71
71
|
return std.mem.indexOf(u8, result.stderr, "No installed application with bundle identifier") != null;
|
|
@@ -73,7 +73,7 @@ pub fn isMissingInstalledApp(result: command.ExecResult) bool {
|
|
|
73
73
|
|
|
74
74
|
pub fn isAppNotRunning(result: command.ExecResult) bool {
|
|
75
75
|
switch (result.term) {
|
|
76
|
-
.
|
|
76
|
+
.exited => |code| if (code == 0) return false,
|
|
77
77
|
else => return false,
|
|
78
78
|
}
|
|
79
79
|
return std.mem.indexOf(u8, result.stderr, "found nothing to terminate") != null;
|
|
@@ -89,7 +89,7 @@ test "simctl terminate missing running app is best-effort" {
|
|
|
89
89
|
try std.testing.expect(isAppNotRunning(.{
|
|
90
90
|
.stdout = stdout,
|
|
91
91
|
.stderr = stderr,
|
|
92
|
-
.term = .{ .
|
|
92
|
+
.term = .{ .exited = 3 },
|
|
93
93
|
}));
|
|
94
94
|
}
|
|
95
95
|
|
|
@@ -103,6 +103,6 @@ test "simctl terminate success is not classified as already stopped" {
|
|
|
103
103
|
try std.testing.expect(!isAppNotRunning(.{
|
|
104
104
|
.stdout = stdout,
|
|
105
105
|
.stderr = stderr,
|
|
106
|
-
.term = .{ .
|
|
106
|
+
.term = .{ .exited = 0 },
|
|
107
107
|
}));
|
|
108
108
|
}
|
package/src/json_rpc.zig
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
const std = @import("std");
|
|
2
|
+
const stdio = @import("stdio.zig");
|
|
2
3
|
const errors = @import("errors.zig");
|
|
3
4
|
const methods = @import("json_rpc_methods.zig");
|
|
4
5
|
const protocol = @import("json_rpc_protocol.zig");
|
|
5
6
|
const trace = @import("trace.zig");
|
|
6
7
|
|
|
8
|
+
const net = std.Io.net;
|
|
9
|
+
|
|
7
10
|
pub const ServeOptions = struct {
|
|
8
11
|
transport: []const u8 = "stdio",
|
|
9
12
|
};
|
|
@@ -13,12 +16,19 @@ pub fn serveStdio(allocator: std.mem.Allocator, device: anytype) !void {
|
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
pub fn serveStdioWithTrace(allocator: std.mem.Allocator, device: anytype, live_trace: ?*trace.TraceWriter) !void {
|
|
16
|
-
var
|
|
17
|
-
|
|
19
|
+
var stdin_io: stdio.Input = .{};
|
|
20
|
+
stdin_io.init(.stdin());
|
|
21
|
+
const stdin = stdin_io.reader();
|
|
22
|
+
|
|
23
|
+
var stdout_io: stdio.Output = .{};
|
|
24
|
+
stdout_io.init(.stdout());
|
|
25
|
+
defer stdout_io.deinit();
|
|
26
|
+
const stdout = stdout_io.writer();
|
|
18
27
|
|
|
19
28
|
while (true) {
|
|
20
|
-
const line =
|
|
29
|
+
const line = stdio.readLineAlloc(stdin, allocator, 16 * 1024 * 1024) catch |err| {
|
|
21
30
|
try protocol.writeError(stdout, null, -32700, @errorName(err));
|
|
31
|
+
try stdout_io.flush();
|
|
22
32
|
continue;
|
|
23
33
|
};
|
|
24
34
|
const owned_line = line orelse break;
|
|
@@ -26,6 +36,7 @@ pub fn serveStdioWithTrace(allocator: std.mem.Allocator, device: anytype, live_t
|
|
|
26
36
|
const trimmed = std.mem.trim(u8, owned_line, " \t\r\n");
|
|
27
37
|
if (trimmed.len == 0) continue;
|
|
28
38
|
try dispatchLineWithTrace(allocator, device, trimmed, stdout, live_trace);
|
|
39
|
+
try stdout_io.flush();
|
|
29
40
|
}
|
|
30
41
|
}
|
|
31
42
|
|
|
@@ -34,45 +45,33 @@ pub fn serveTcp(allocator: std.mem.Allocator, device: anytype, port: u16) !void
|
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
pub fn serveTcpWithTrace(allocator: std.mem.Allocator, device: anytype, port: u16, live_trace: ?*trace.TraceWriter) !void {
|
|
37
|
-
const address = try
|
|
38
|
-
var server = try address.listen(.{ .reuse_address = true });
|
|
39
|
-
defer server.deinit();
|
|
48
|
+
const address = try net.IpAddress.parse("127.0.0.1", port);
|
|
49
|
+
var server = try address.listen(stdio.io(), .{ .reuse_address = true });
|
|
50
|
+
defer server.deinit(stdio.io());
|
|
40
51
|
|
|
41
52
|
while (true) {
|
|
42
|
-
|
|
43
|
-
defer connection.
|
|
44
|
-
try serveTcpConnection(allocator, device, connection
|
|
53
|
+
const connection = try server.accept(stdio.io());
|
|
54
|
+
defer connection.close(stdio.io());
|
|
55
|
+
try serveTcpConnection(allocator, device, connection, live_trace);
|
|
45
56
|
}
|
|
46
57
|
}
|
|
47
58
|
|
|
48
|
-
fn serveTcpConnection(allocator: std.mem.Allocator, device: anytype, stream:
|
|
59
|
+
fn serveTcpConnection(allocator: std.mem.Allocator, device: anytype, stream: net.Stream, live_trace: ?*trace.TraceWriter) !void {
|
|
49
60
|
var write_buffer: [8192]u8 = undefined;
|
|
50
|
-
var stream_writer = stream.writer(&write_buffer);
|
|
61
|
+
var stream_writer = stream.writer(stdio.io(), &write_buffer);
|
|
51
62
|
const writer = &stream_writer.interface;
|
|
52
63
|
|
|
53
|
-
var line = std.ArrayList(u8).empty;
|
|
54
|
-
defer line.deinit(allocator);
|
|
55
|
-
|
|
56
64
|
var read_buffer: [4096]u8 = undefined;
|
|
65
|
+
var stream_reader = stream.reader(stdio.io(), &read_buffer);
|
|
57
66
|
while (true) {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
line.clearRetainingCapacity();
|
|
68
|
-
} else {
|
|
69
|
-
try line.append(allocator, ch);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (line.items.len != 0) {
|
|
75
|
-
const trimmed = std.mem.trim(u8, line.items, " \t\r\n");
|
|
67
|
+
const line = stdio.readLineAlloc(&stream_reader.interface, allocator, 16 * 1024 * 1024) catch |err| {
|
|
68
|
+
try protocol.writeError(writer, null, -32700, @errorName(err));
|
|
69
|
+
try writer.flush();
|
|
70
|
+
continue;
|
|
71
|
+
};
|
|
72
|
+
const owned_line = line orelse break;
|
|
73
|
+
defer allocator.free(owned_line);
|
|
74
|
+
const trimmed = std.mem.trim(u8, owned_line, " \t\r\n");
|
|
76
75
|
if (trimmed.len != 0) {
|
|
77
76
|
try dispatchLineWithTrace(allocator, device, trimmed, writer, live_trace);
|
|
78
77
|
try writer.flush();
|
|
@@ -124,21 +123,21 @@ pub fn dispatchLineWithTrace(
|
|
|
124
123
|
}
|
|
125
124
|
|
|
126
125
|
fn recordRpcEvent(tw: *trace.TraceWriter, kind: []const u8, method: []const u8, id: ?std.json.Value) !void {
|
|
127
|
-
var payload =
|
|
128
|
-
defer payload.deinit(
|
|
129
|
-
const writer = payload.writer
|
|
126
|
+
var payload: std.Io.Writer.Allocating = .init(tw.allocator);
|
|
127
|
+
defer payload.deinit();
|
|
128
|
+
const writer = &payload.writer;
|
|
130
129
|
try writer.writeAll("{\"method\":");
|
|
131
130
|
try trace.writeJsonString(writer, method);
|
|
132
131
|
try writer.writeAll(",\"id\":");
|
|
133
132
|
try protocol.writeId(writer, id);
|
|
134
133
|
try writer.writeAll("}");
|
|
135
|
-
try tw.recordEvent(kind,
|
|
134
|
+
try tw.recordEvent(kind, writer.buffered());
|
|
136
135
|
}
|
|
137
136
|
|
|
138
137
|
fn recordRpcErrorEvent(tw: *trace.TraceWriter, method: []const u8, id: ?std.json.Value, err: anyerror) !void {
|
|
139
|
-
var payload =
|
|
140
|
-
defer payload.deinit(
|
|
141
|
-
const writer = payload.writer
|
|
138
|
+
var payload: std.Io.Writer.Allocating = .init(tw.allocator);
|
|
139
|
+
defer payload.deinit();
|
|
140
|
+
const writer = &payload.writer;
|
|
142
141
|
try writer.writeAll("{\"method\":");
|
|
143
142
|
try trace.writeJsonString(writer, method);
|
|
144
143
|
try writer.writeAll(",\"id\":");
|
|
@@ -146,5 +145,5 @@ fn recordRpcErrorEvent(tw: *trace.TraceWriter, method: []const u8, id: ?std.json
|
|
|
146
145
|
try writer.writeAll(",\"error\":");
|
|
147
146
|
try trace.writeJsonString(writer, @errorName(err));
|
|
148
147
|
try writer.writeAll("}");
|
|
149
|
-
try tw.recordEvent("rpc.error",
|
|
148
|
+
try tw.recordEvent("rpc.error", writer.buffered());
|
|
150
149
|
}
|
package/src/json_rpc_methods.zig
CHANGED
|
@@ -107,13 +107,13 @@ fn dispatchAppMethod(
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
fn recordAppOpenLink(tw: *trace.TraceWriter, url: []const u8) !void {
|
|
110
|
-
var payload =
|
|
111
|
-
defer payload.deinit(
|
|
112
|
-
const writer = payload.writer
|
|
110
|
+
var payload: std.Io.Writer.Allocating = .init(tw.allocator);
|
|
111
|
+
defer payload.deinit();
|
|
112
|
+
const writer = &payload.writer;
|
|
113
113
|
try writer.writeAll("{\"status\":\"ok\",\"url\":");
|
|
114
114
|
try trace.writeJsonString(writer, url);
|
|
115
115
|
try writer.writeAll("}");
|
|
116
|
-
try tw.recordEvent("app.openLink",
|
|
116
|
+
try tw.recordEvent("app.openLink", writer.buffered());
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
fn dispatchObserveMethod(
|
|
@@ -312,10 +312,10 @@ fn dispatchScenarioMethod(
|
|
|
312
312
|
const path = try params_parser.requiredString(params, "path");
|
|
313
313
|
var result = try validation.validateFile(allocator, path);
|
|
314
314
|
defer result.deinit(allocator);
|
|
315
|
-
var payload =
|
|
316
|
-
defer payload.deinit(
|
|
317
|
-
try cli_output.writeValidationJson(payload.writer
|
|
318
|
-
try protocol.writeResultRaw(writer, id, std.mem.
|
|
315
|
+
var payload: std.Io.Writer.Allocating = .init(allocator);
|
|
316
|
+
defer payload.deinit();
|
|
317
|
+
try cli_output.writeValidationJson(&payload.writer, path, result);
|
|
318
|
+
try protocol.writeResultRaw(writer, id, std.mem.trimEnd(u8, payload.writer.buffered(), " \t\r\n"));
|
|
319
319
|
return true;
|
|
320
320
|
}
|
|
321
321
|
return false;
|
|
@@ -20,12 +20,13 @@ pub fn writeResult(writer: anytype, id: ?std.json.Value, snap: types.Observation
|
|
|
20
20
|
pub fn recordArtifact(tw: *trace.TraceWriter, kind: []const u8, snap: types.ObservationSnapshot) !void {
|
|
21
21
|
const path = try tw.writeSnapshot(snap);
|
|
22
22
|
defer tw.allocator.free(path);
|
|
23
|
-
var payload =
|
|
24
|
-
defer payload.deinit(
|
|
25
|
-
|
|
26
|
-
try
|
|
27
|
-
try
|
|
28
|
-
try
|
|
29
|
-
try
|
|
30
|
-
try
|
|
23
|
+
var payload: std.Io.Writer.Allocating = .init(tw.allocator);
|
|
24
|
+
defer payload.deinit();
|
|
25
|
+
const out = &payload.writer;
|
|
26
|
+
try out.writeAll("{\"path\":");
|
|
27
|
+
try trace.writeJsonString(out, path);
|
|
28
|
+
try out.writeAll(",\"snapshotId\":");
|
|
29
|
+
try trace.writeJsonString(out, snap.id);
|
|
30
|
+
try out.writeAll("}");
|
|
31
|
+
try tw.recordEvent(kind, out.buffered());
|
|
31
32
|
}
|
package/src/json_rpc_params.zig
CHANGED
|
@@ -56,5 +56,5 @@ pub fn optionalDirection(params: ?std.json.Value, key: []const u8, default_value
|
|
|
56
56
|
if (value != .string) return error.ParamMustBeString;
|
|
57
57
|
if (std.mem.eql(u8, value.string, "down")) return .down;
|
|
58
58
|
if (std.mem.eql(u8, value.string, "up")) return .up;
|
|
59
|
-
return error.
|
|
59
|
+
return error.unknownScrollDirection;
|
|
60
60
|
}
|
package/src/json_rpc_trace.zig
CHANGED
|
@@ -4,6 +4,7 @@ const cli_explore = @import("cli_explore.zig");
|
|
|
4
4
|
const protocol = @import("json_rpc_protocol.zig");
|
|
5
5
|
const report = @import("report.zig");
|
|
6
6
|
const runner_events = @import("runner_events.zig");
|
|
7
|
+
const stdio = @import("stdio.zig");
|
|
7
8
|
const trace = @import("trace.zig");
|
|
8
9
|
|
|
9
10
|
pub fn writeEventsResult(
|
|
@@ -23,7 +24,7 @@ pub fn writeEventsResult(
|
|
|
23
24
|
|
|
24
25
|
const events_path = try std.fs.path.join(allocator, &.{ tw.root_dir, "events.jsonl" });
|
|
25
26
|
defer allocator.free(events_path);
|
|
26
|
-
const content =
|
|
27
|
+
const content = stdio.readFileAlloc(allocator, events_path, 64 * 1024 * 1024) catch |err| switch (err) {
|
|
27
28
|
error.FileNotFound => try allocator.dupe(u8, ""),
|
|
28
29
|
else => return err,
|
|
29
30
|
};
|
|
@@ -35,9 +36,9 @@ pub fn writeEventsResult(
|
|
|
35
36
|
try trace.writeJsonString(writer, tw.root_dir);
|
|
36
37
|
try writer.print(",\"afterSeq\":{d},\"nextSeq\":", .{after_seq});
|
|
37
38
|
|
|
38
|
-
var events_json =
|
|
39
|
-
defer events_json.deinit(
|
|
40
|
-
|
|
39
|
+
var events_json: std.Io.Writer.Allocating = .init(allocator);
|
|
40
|
+
defer events_json.deinit();
|
|
41
|
+
const events_writer = &events_json.writer;
|
|
41
42
|
var next_seq = after_seq;
|
|
42
43
|
var emitted: u64 = 0;
|
|
43
44
|
|
|
@@ -60,20 +61,20 @@ pub fn writeEventsResult(
|
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
try writer.print("{d},\"latestSeq\":{d},\"events\":[", .{ next_seq, tw.event_count });
|
|
63
|
-
try writer.writeAll(
|
|
64
|
+
try writer.writeAll(events_writer.buffered());
|
|
64
65
|
try writer.writeAll("]}}\n");
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
pub fn recordSimplePayload(tw: *trace.TraceWriter, kind: []const u8, key: []const u8, value: []const u8) !void {
|
|
68
|
-
var payload =
|
|
69
|
-
defer payload.deinit(
|
|
70
|
-
const writer = payload.writer
|
|
69
|
+
var payload: std.Io.Writer.Allocating = .init(tw.allocator);
|
|
70
|
+
defer payload.deinit();
|
|
71
|
+
const writer = &payload.writer;
|
|
71
72
|
try writer.writeAll("{");
|
|
72
73
|
try trace.writeJsonString(writer, key);
|
|
73
74
|
try writer.writeAll(":");
|
|
74
75
|
try trace.writeJsonString(writer, value);
|
|
75
76
|
try writer.writeAll("}");
|
|
76
|
-
try tw.recordEvent(kind,
|
|
77
|
+
try tw.recordEvent(kind, writer.buffered());
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
pub fn writeDiscoverResult(
|
|
@@ -113,10 +114,10 @@ pub fn writeDiscoverResult(
|
|
|
113
114
|
discovered.summary.validated,
|
|
114
115
|
);
|
|
115
116
|
|
|
116
|
-
var payload =
|
|
117
|
-
defer payload.deinit(
|
|
118
|
-
try cli_discover.writeJson(payload.writer
|
|
119
|
-
try protocol.writeResultRaw(writer, id, std.mem.
|
|
117
|
+
var payload: std.Io.Writer.Allocating = .init(allocator);
|
|
118
|
+
defer payload.deinit();
|
|
119
|
+
try cli_discover.writeJson(&payload.writer, discovered.summary, discovered.validation);
|
|
120
|
+
try protocol.writeResultRaw(writer, id, std.mem.trimEnd(u8, payload.writer.buffered(), " \t\r\n"));
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
pub fn writeExploreResult(
|
|
@@ -159,10 +160,10 @@ pub fn writeExploreResult(
|
|
|
159
160
|
explored.discovered.summary.validated,
|
|
160
161
|
);
|
|
161
162
|
|
|
162
|
-
var payload =
|
|
163
|
-
defer payload.deinit(
|
|
164
|
-
try cli_explore.writeJson(payload.writer
|
|
165
|
-
try protocol.writeResultRaw(writer, id, std.mem.
|
|
163
|
+
var payload: std.Io.Writer.Allocating = .init(allocator);
|
|
164
|
+
defer payload.deinit();
|
|
165
|
+
try cli_explore.writeJson(&payload.writer, explored.summary, explored.discovered.summary, explored.discovered.validation);
|
|
166
|
+
try protocol.writeResultRaw(writer, id, std.mem.trimEnd(u8, payload.writer.buffered(), " \t\r\n"));
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
pub fn writeExplainResult(
|
|
@@ -177,9 +178,9 @@ pub fn writeExplainResult(
|
|
|
177
178
|
};
|
|
178
179
|
|
|
179
180
|
try tw.flushManifest();
|
|
180
|
-
var payload =
|
|
181
|
-
defer payload.deinit(
|
|
182
|
-
try report.writeTraceExplanationJson(allocator, tw.root_dir, payload.writer
|
|
183
|
-
try protocol.writeResultRaw(writer, id, std.mem.
|
|
181
|
+
var payload: std.Io.Writer.Allocating = .init(allocator);
|
|
182
|
+
defer payload.deinit();
|
|
183
|
+
try report.writeTraceExplanationJson(allocator, tw.root_dir, &payload.writer);
|
|
184
|
+
try protocol.writeResultRaw(writer, id, std.mem.trimEnd(u8, payload.writer.buffered(), " \t\r\n"));
|
|
184
185
|
try tw.recordEvent("trace.explain", "{\"status\":\"ok\"}");
|
|
185
186
|
}
|
package/src/main.zig
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const std = @import("std");
|
|
2
|
+
const stdio = @import("stdio.zig");
|
|
2
3
|
const cli_devices = @import("cli_devices.zig");
|
|
3
4
|
const cli_discover = @import("cli_discover.zig");
|
|
4
5
|
const cli_doctor = @import("cli_doctor.zig");
|
|
@@ -14,14 +15,17 @@ const cli_trace = @import("cli_trace.zig");
|
|
|
14
15
|
const cli_validate = @import("cli_validate.zig");
|
|
15
16
|
const errors = @import("errors.zig");
|
|
16
17
|
|
|
17
|
-
pub fn main() void {
|
|
18
|
-
mainInner() catch |err| {
|
|
18
|
+
pub fn main(init: std.process.Init.Minimal) void {
|
|
19
|
+
mainInner(init) catch |err| {
|
|
20
|
+
// stdout's consumer went away (e.g. `zmr ... | head`); exit quietly
|
|
21
|
+
// with the conventional SIGPIPE status instead of reporting an error.
|
|
22
|
+
if (err == error.BrokenPipe) std.process.exit(141);
|
|
19
23
|
writeTopLevelError(err);
|
|
20
24
|
std.process.exit(exitCodeForError(err));
|
|
21
25
|
};
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
fn mainInner() !void {
|
|
28
|
+
fn mainInner(init: std.process.Init.Minimal) !void {
|
|
25
29
|
const GeneralAllocator = if (@hasDecl(std.heap, "GeneralPurposeAllocator"))
|
|
26
30
|
std.heap.GeneralPurposeAllocator
|
|
27
31
|
else
|
|
@@ -29,8 +33,10 @@ fn mainInner() !void {
|
|
|
29
33
|
var gpa = GeneralAllocator(.{}){};
|
|
30
34
|
defer _ = gpa.deinit();
|
|
31
35
|
const allocator = gpa.allocator();
|
|
36
|
+
stdio.initProcess(init, allocator);
|
|
37
|
+
defer stdio.deinitProcess();
|
|
32
38
|
|
|
33
|
-
var args = try std.process.
|
|
39
|
+
var args = try std.process.Args.Iterator.initAllocator(init.args, allocator);
|
|
34
40
|
defer args.deinit();
|
|
35
41
|
_ = args.next();
|
|
36
42
|
const command_name = args.next() orelse {
|
|
@@ -77,26 +83,29 @@ fn mainInner() !void {
|
|
|
77
83
|
} else {
|
|
78
84
|
std.debug.print("unknown command: {s}\n\n", .{command_name});
|
|
79
85
|
try usage();
|
|
80
|
-
return error.
|
|
86
|
+
return error.unknownCommand;
|
|
81
87
|
}
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
fn writeTopLevelError(err: anyerror) void {
|
|
85
91
|
const public = errors.classify(err);
|
|
86
|
-
|
|
92
|
+
var stderr_io: stdio.Output = .{};
|
|
93
|
+
stderr_io.init(.stderr());
|
|
94
|
+
defer stderr_io.deinit();
|
|
95
|
+
const stderr = stderr_io.writer();
|
|
87
96
|
stderr.print("error[{s}]: {s}\n", .{ public.code, public.message }) catch {};
|
|
88
97
|
if (err == error.CommandFailed) {
|
|
89
98
|
stderr.writeAll("hint: run `zmr doctor --json` for setup diagnostics.\n") catch {};
|
|
90
99
|
}
|
|
91
|
-
if (err == error.
|
|
100
|
+
if (err == error.unknownFlag) {
|
|
92
101
|
stderr.writeAll("hint: run `zmr help` for each command's flags and arguments.\n") catch {};
|
|
93
102
|
}
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
fn exitCodeForError(err: anyerror) u8 {
|
|
97
106
|
return switch (err) {
|
|
98
|
-
error.
|
|
99
|
-
error.
|
|
107
|
+
error.unknownCommand,
|
|
108
|
+
error.unknownFlag,
|
|
100
109
|
error.MissingScenarioPath,
|
|
101
110
|
error.MissingDeviceSerial,
|
|
102
111
|
error.MissingTraceDir,
|
|
@@ -122,7 +131,10 @@ fn exitCodeForError(err: anyerror) u8 {
|
|
|
122
131
|
}
|
|
123
132
|
|
|
124
133
|
fn usage() !void {
|
|
125
|
-
|
|
134
|
+
var stdout_io: stdio.Output = .{};
|
|
135
|
+
stdout_io.init(.stdout());
|
|
136
|
+
defer stdout_io.deinit();
|
|
137
|
+
const stdout = stdout_io.writer();
|
|
126
138
|
try stdout.writeAll(
|
|
127
139
|
\\zmr - Zeno Mobile Runner
|
|
128
140
|
\\
|