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.
Files changed (78) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/FEATURES.md +1 -1
  3. package/README.md +9 -1
  4. package/build.zig.zon +2 -2
  5. package/clients/kotlin/README.md +1 -1
  6. package/clients/kotlin/build.gradle.kts +1 -1
  7. package/clients/python/pyproject.toml +1 -1
  8. package/clients/rust/Cargo.lock +1 -1
  9. package/clients/rust/Cargo.toml +1 -1
  10. package/clients/typescript/package.json +1 -1
  11. package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
  12. package/docs/protocol.md +10 -10
  13. package/examples/ios-dev-client-open-link.json +24 -13
  14. package/examples/ios-dev-client-route-snapshot.json +33 -8
  15. package/npm/scenarios.mjs +15 -8
  16. package/npm/wizard.mjs +1 -1
  17. package/package.json +3 -1
  18. package/prebuilds/darwin-arm64/zmr +0 -0
  19. package/prebuilds/darwin-x64/zmr +0 -0
  20. package/prebuilds/linux-arm64/zmr +0 -0
  21. package/prebuilds/linux-x64/zmr +0 -0
  22. package/scripts/create-react-native-expo-demo-app.sh +11 -13
  23. package/shims/ios/ZMRShim.swift +40 -12
  24. package/shims/ios/ZMRShimUITestCase.swift +142 -16
  25. package/src/android.zig +10 -9
  26. package/src/android_emulator.zig +22 -11
  27. package/src/android_screen_recording.zig +11 -7
  28. package/src/bundle.zig +10 -9
  29. package/src/bundle_redaction.zig +29 -28
  30. package/src/bundle_tar.zig +15 -12
  31. package/src/cli_devices.zig +7 -3
  32. package/src/cli_discover.zig +7 -3
  33. package/src/cli_doctor.zig +7 -3
  34. package/src/cli_draft.zig +51 -47
  35. package/src/cli_explore.zig +7 -3
  36. package/src/cli_import.zig +8 -4
  37. package/src/cli_info.zig +13 -6
  38. package/src/cli_init.zig +9 -5
  39. package/src/cli_inspect.zig +8 -4
  40. package/src/cli_run.zig +22 -16
  41. package/src/cli_serve.zig +3 -3
  42. package/src/cli_trace.zig +25 -12
  43. package/src/cli_validate.zig +8 -4
  44. package/src/command.zig +81 -99
  45. package/src/config.zig +2 -1
  46. package/src/config_diagnostics.zig +2 -1
  47. package/src/config_paths.zig +2 -1
  48. package/src/doctor.zig +8 -7
  49. package/src/doctor_hints.zig +1 -1
  50. package/src/errors.zig +5 -5
  51. package/src/importer.zig +8 -7
  52. package/src/ios.zig +26 -29
  53. package/src/ios_devices.zig +6 -5
  54. package/src/ios_lifecycle.zig +4 -4
  55. package/src/json_rpc.zig +39 -40
  56. package/src/json_rpc_methods.zig +8 -8
  57. package/src/json_rpc_observation.zig +9 -8
  58. package/src/json_rpc_params.zig +1 -1
  59. package/src/json_rpc_trace.zig +22 -21
  60. package/src/main.zig +22 -10
  61. package/src/mcp.zig +28 -19
  62. package/src/mcp_trace.zig +30 -29
  63. package/src/report.zig +39 -36
  64. package/src/report_html.zig +5 -4
  65. package/src/runner.zig +2 -1
  66. package/src/runner_actions.zig +20 -17
  67. package/src/runner_diagnostics.zig +4 -4
  68. package/src/runner_events.zig +55 -51
  69. package/src/runner_native.zig +21 -19
  70. package/src/runner_waits.zig +46 -41
  71. package/src/scaffold.zig +25 -24
  72. package/src/scenario.zig +4 -3
  73. package/src/stdio.zig +129 -0
  74. package/src/trace.zig +34 -26
  75. package/src/trace_summary.zig +3 -2
  76. package/src/trace_summary_diagnostic.zig +15 -13
  77. package/src/validation.zig +5 -4
  78. 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
- if (urlMayNeedOpenConfirmation(url)) {
132
- self.acceptOpenURLConfirmationBestEffort();
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
- std.Thread.sleep(timeout_ms * std.time.ns_per_ms);
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}", .{std.time.milliTimestamp()});
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 = std.time.milliTimestamp(),
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", .{std.time.nanoTimestamp()});
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.fs.cwd().deleteFile(path) catch {};
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 std.fs.cwd().readFileAlloc(self.allocator, path, default_max_output);
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 != .Exited or result.term.Exited != 0) return null;
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 = std.ArrayList(u8).empty;
282
- defer payload.deinit(writer.allocator);
283
- const out = payload.writer(writer.allocator);
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", payload.items);
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 = std.ArrayList(u8).empty;
340
- defer input.deinit(self.allocator);
341
- try ios_shim.writeCommandJson(input.writer(self.allocator), shim_command);
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.items, 4 * 1024 * 1024, timeout_ms);
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
- std.Thread.sleep(shim_bootstrap_retry_delay_ms * std.time.ns_per_ms);
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
- .Exited => |code| if (code == 0) return false,
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(std.posix.getenv(shim_timeout_env));
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
  }
@@ -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
- std.Thread.sleep(simctl_retry_delay_ms * std.time.ns_per_ms);
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", .{std.time.nanoTimestamp()});
66
+ const path = try std.fmt.allocPrint(allocator, "/tmp/zmr-devicectl-{d}.json", .{stdio.nowNs()});
66
67
  defer allocator.free(path);
67
- defer std.fs.cwd().deleteFile(path) catch {};
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.fs.cwd().readFileAlloc(allocator, path, default_max_output);
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
- .Exited => |code| if (code == 0) return false,
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
@@ -65,7 +65,7 @@ pub fn uninstallPhysicalBestEffort(
65
65
 
66
66
  pub fn isMissingInstalledApp(result: command.ExecResult) bool {
67
67
  switch (result.term) {
68
- .Exited => |code| if (code == 0) return false,
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
- .Exited => |code| if (code == 0) return false,
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 = .{ .Exited = 3 },
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 = .{ .Exited = 0 },
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 stdin = std.fs.File.stdin().deprecatedReader();
17
- const stdout = std.fs.File.stdout().deprecatedWriter();
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 = stdin.readUntilDelimiterOrEofAlloc(allocator, '\n', 16 * 1024 * 1024) catch |err| {
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 std.net.Address.parseIp("127.0.0.1", port);
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
- var connection = try server.accept();
43
- defer connection.stream.close();
44
- try serveTcpConnection(allocator, device, connection.stream, live_trace);
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: std.net.Stream, live_trace: ?*trace.TraceWriter) !void {
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 n = try stream.read(&read_buffer);
59
- if (n == 0) break;
60
- for (read_buffer[0..n]) |ch| {
61
- if (ch == '\n') {
62
- const trimmed = std.mem.trim(u8, line.items, " \t\r\n");
63
- if (trimmed.len != 0) {
64
- try dispatchLineWithTrace(allocator, device, trimmed, writer, live_trace);
65
- try writer.flush();
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 = std.ArrayList(u8).empty;
128
- defer payload.deinit(tw.allocator);
129
- const writer = payload.writer(tw.allocator);
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, payload.items);
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 = std.ArrayList(u8).empty;
140
- defer payload.deinit(tw.allocator);
141
- const writer = payload.writer(tw.allocator);
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", payload.items);
148
+ try tw.recordEvent("rpc.error", writer.buffered());
150
149
  }
@@ -107,13 +107,13 @@ fn dispatchAppMethod(
107
107
  }
108
108
 
109
109
  fn recordAppOpenLink(tw: *trace.TraceWriter, url: []const u8) !void {
110
- var payload = std.ArrayList(u8).empty;
111
- defer payload.deinit(tw.allocator);
112
- const writer = payload.writer(tw.allocator);
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", payload.items);
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 = std.ArrayList(u8).empty;
316
- defer payload.deinit(allocator);
317
- try cli_output.writeValidationJson(payload.writer(allocator), path, result);
318
- try protocol.writeResultRaw(writer, id, std.mem.trimRight(u8, payload.items, " \t\r\n"));
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 = std.ArrayList(u8).empty;
24
- defer payload.deinit(tw.allocator);
25
- try payload.writer(tw.allocator).writeAll("{\"path\":");
26
- try trace.writeJsonString(payload.writer(tw.allocator), path);
27
- try payload.writer(tw.allocator).writeAll(",\"snapshotId\":");
28
- try trace.writeJsonString(payload.writer(tw.allocator), snap.id);
29
- try payload.writer(tw.allocator).writeAll("}");
30
- try tw.recordEvent(kind, payload.items);
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
  }
@@ -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.UnknownScrollDirection;
59
+ return error.unknownScrollDirection;
60
60
  }
@@ -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 = std.fs.cwd().readFileAlloc(allocator, events_path, 64 * 1024 * 1024) catch |err| switch (err) {
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 = std.ArrayList(u8).empty;
39
- defer events_json.deinit(allocator);
40
- var events_writer = events_json.writer(allocator);
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(events_json.items);
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 = std.ArrayList(u8).empty;
69
- defer payload.deinit(tw.allocator);
70
- const writer = payload.writer(tw.allocator);
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, payload.items);
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 = std.ArrayList(u8).empty;
117
- defer payload.deinit(allocator);
118
- try cli_discover.writeJson(payload.writer(allocator), discovered.summary, discovered.validation);
119
- try protocol.writeResultRaw(writer, id, std.mem.trimRight(u8, payload.items, " \t\r\n"));
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 = std.ArrayList(u8).empty;
163
- defer payload.deinit(allocator);
164
- try cli_explore.writeJson(payload.writer(allocator), explored.summary, explored.discovered.summary, explored.discovered.validation);
165
- try protocol.writeResultRaw(writer, id, std.mem.trimRight(u8, payload.items, " \t\r\n"));
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 = std.ArrayList(u8).empty;
181
- defer payload.deinit(allocator);
182
- try report.writeTraceExplanationJson(allocator, tw.root_dir, payload.writer(allocator));
183
- try protocol.writeResultRaw(writer, id, std.mem.trimRight(u8, payload.items, " \t\r\n"));
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.argsWithAllocator(allocator);
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.UnknownCommand;
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
- const stderr = std.fs.File.stderr().deprecatedWriter();
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.UnknownFlag) {
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.UnknownCommand,
99
- error.UnknownFlag,
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
- const stdout = std.fs.File.stdout().deprecatedWriter();
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
  \\