zeno-mobile-runner 0.2.1 → 0.2.3
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 +39 -0
- package/FEATURES.md +1 -1
- package/README.md +1 -1
- package/build.zig +10 -0
- 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/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 +135 -15
- 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 +98 -19
- package/src/ios_devices.zig +6 -5
- package/src/ios_lifecycle.zig +4 -4
- package/src/ios_shim.zig +12 -0
- 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 +19 -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");
|
|
@@ -15,6 +16,8 @@ const default_max_output = 32 * 1024 * 1024;
|
|
|
15
16
|
const default_shim_timeout_ms = 5_400_000;
|
|
16
17
|
const shim_timeout_env = "ZMR_IOS_SHIM_TIMEOUT_MS";
|
|
17
18
|
const shim_best_effort_timeout_ms = 10_000;
|
|
19
|
+
const open_link_interruption_attempts = 3;
|
|
20
|
+
const open_link_interruption_retry_delay_ms = 1_000;
|
|
18
21
|
const shim_command_attempts = 2;
|
|
19
22
|
const shim_bootstrap_retry_delay_ms = 500;
|
|
20
23
|
|
|
@@ -190,11 +193,11 @@ pub const IosDevice = struct {
|
|
|
190
193
|
.duration_ms = @as(u32, @intCast(@min(timeout_ms, std.math.maxInt(u32)))),
|
|
191
194
|
});
|
|
192
195
|
}
|
|
193
|
-
|
|
196
|
+
stdio.sleepNs(timeout_ms * std.time.ns_per_ms);
|
|
194
197
|
}
|
|
195
198
|
|
|
196
199
|
pub fn snapshot(self: *IosDevice, writer: ?*trace.TraceWriter) !types.ObservationSnapshot {
|
|
197
|
-
const id = if (writer) |tw| try tw.nextSnapshotId() else try std.fmt.allocPrint(self.allocator, "snapshot-{d}", .{
|
|
200
|
+
const id = if (writer) |tw| try tw.nextSnapshotId() else try std.fmt.allocPrint(self.allocator, "snapshot-{d}", .{stdio.nowMs()});
|
|
198
201
|
errdefer self.allocator.free(id);
|
|
199
202
|
|
|
200
203
|
var screenshot_artifact: ?[]const u8 = null;
|
|
@@ -239,7 +242,7 @@ pub const IosDevice = struct {
|
|
|
239
242
|
|
|
240
243
|
return .{
|
|
241
244
|
.id = id,
|
|
242
|
-
.timestamp_ms =
|
|
245
|
+
.timestamp_ms = stdio.nowMs(),
|
|
243
246
|
.viewport = viewport,
|
|
244
247
|
.active_package = active_package,
|
|
245
248
|
.active_activity = null,
|
|
@@ -257,21 +260,21 @@ pub const IosDevice = struct {
|
|
|
257
260
|
defer self.allocator.free(response);
|
|
258
261
|
return try ios_shim.parseScreenshotPng(self.allocator, response);
|
|
259
262
|
}
|
|
260
|
-
const path = try std.fmt.allocPrint(self.allocator, "/tmp/zmr-ios-screenshot-{d}.png", .{
|
|
263
|
+
const path = try std.fmt.allocPrint(self.allocator, "/tmp/zmr-ios-screenshot-{d}.png", .{stdio.nowNs()});
|
|
261
264
|
defer self.allocator.free(path);
|
|
262
|
-
defer std.
|
|
265
|
+
defer std.Io.Dir.cwd().deleteFile(stdio.io(), path) catch {};
|
|
263
266
|
|
|
264
267
|
const result = try self.runSimctl(&.{ "io", self.target(), "screenshot", path }, default_max_output);
|
|
265
268
|
defer result.deinit(self.allocator);
|
|
266
269
|
try result.ensureSuccess();
|
|
267
|
-
return try
|
|
270
|
+
return try stdio.readFileAlloc(self.allocator, path, default_max_output);
|
|
268
271
|
}
|
|
269
272
|
|
|
270
273
|
fn logDelta(self: *IosDevice) !?[]const u8 {
|
|
271
274
|
if (self.target_kind == .physical) return null;
|
|
272
275
|
const result = try self.runSimctl(&.{ "spawn", self.target(), "log", "show", "--style", "compact", "--last", "30s" }, 1024 * 1024);
|
|
273
276
|
defer result.deinit(self.allocator);
|
|
274
|
-
if (result.term != .
|
|
277
|
+
if (result.term != .exited or result.term.exited != 0) return null;
|
|
275
278
|
return try self.allocator.dupe(u8, result.stdout);
|
|
276
279
|
}
|
|
277
280
|
|
|
@@ -282,15 +285,15 @@ pub const IosDevice = struct {
|
|
|
282
285
|
}
|
|
283
286
|
|
|
284
287
|
fn recordSnapshotSemanticFailure(self: *IosDevice, writer: *trace.TraceWriter, screenshot_artifact: []const u8, err: anyerror) !void {
|
|
285
|
-
var payload =
|
|
286
|
-
defer payload.deinit(
|
|
287
|
-
const out = payload.writer
|
|
288
|
+
var payload: std.Io.Writer.Allocating = .init(writer.allocator);
|
|
289
|
+
defer payload.deinit();
|
|
290
|
+
const out = &payload.writer;
|
|
288
291
|
try out.writeAll("{\"status\":\"failed\",\"artifactStatus\":\"captured\",\"semanticStatus\":\"failed\",\"error\":");
|
|
289
292
|
try trace.writeJsonString(out, @errorName(err));
|
|
290
293
|
try out.writeAll(",\"screenshotArtifact\":");
|
|
291
294
|
try trace.writeJsonString(out, screenshot_artifact);
|
|
292
295
|
try out.writeAll(",\"source\":\"ios-xctest-shim\"}");
|
|
293
|
-
try writer.recordEvent("observe.snapshot.semanticExtraction",
|
|
296
|
+
try writer.recordEvent("observe.snapshot.semanticExtraction", out.buffered());
|
|
294
297
|
_ = self;
|
|
295
298
|
}
|
|
296
299
|
|
|
@@ -308,7 +311,20 @@ pub const IosDevice = struct {
|
|
|
308
311
|
|
|
309
312
|
fn acceptOpenURLConfirmationBestEffort(self: *IosDevice) void {
|
|
310
313
|
if (self.shim_path == null) return;
|
|
311
|
-
|
|
314
|
+
var attempt: usize = 0;
|
|
315
|
+
while (attempt < open_link_interruption_attempts) {
|
|
316
|
+
attempt += 1;
|
|
317
|
+
if (self.acceptOpenURLConfirmationOnce() catch return) return;
|
|
318
|
+
if (attempt < open_link_interruption_attempts) {
|
|
319
|
+
stdio.sleepNs(open_link_interruption_retry_delay_ms * std.time.ns_per_ms);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
fn acceptOpenURLConfirmationOnce(self: *IosDevice) !bool {
|
|
325
|
+
const response = try self.runShimWithTimeout(.{ .kind = .accept_system_alert, .text = "Open" }, shim_best_effort_timeout_ms);
|
|
326
|
+
defer self.allocator.free(response);
|
|
327
|
+
return try ios_shim.parseAcceptSystemAlertResponse(response);
|
|
312
328
|
}
|
|
313
329
|
|
|
314
330
|
fn appIsRunningFromShimBestEffort(self: *IosDevice) bool {
|
|
@@ -340,19 +356,19 @@ pub const IosDevice = struct {
|
|
|
340
356
|
fn runShimWithTimeout(self: *IosDevice, shim_command: ios_shim.Command, timeout_ms: u64) ![]u8 {
|
|
341
357
|
const path = self.shim_path orelse return error.IosXCTestShimRequired;
|
|
342
358
|
|
|
343
|
-
var input =
|
|
344
|
-
defer input.deinit(
|
|
345
|
-
try ios_shim.writeCommandJson(input.writer
|
|
359
|
+
var input: std.Io.Writer.Allocating = .init(self.allocator);
|
|
360
|
+
defer input.deinit();
|
|
361
|
+
try ios_shim.writeCommandJson(&input.writer, shim_command);
|
|
346
362
|
|
|
347
363
|
var attempt: usize = 0;
|
|
348
364
|
while (attempt < shim_command_attempts) {
|
|
349
365
|
attempt += 1;
|
|
350
|
-
const result = try command.runWithInputTimeout(self.allocator, &.{path}, input.
|
|
366
|
+
const result = try command.runWithInputTimeout(self.allocator, &.{path}, input.writer.buffered(), 4 * 1024 * 1024, timeout_ms);
|
|
351
367
|
defer result.deinit(self.allocator);
|
|
352
368
|
|
|
353
369
|
result.ensureSuccess() catch |err| {
|
|
354
370
|
if (attempt < shim_command_attempts and err == error.CommandFailed and isTransientShimBootstrapFailure(result)) {
|
|
355
|
-
|
|
371
|
+
stdio.sleepNs(shim_bootstrap_retry_delay_ms * std.time.ns_per_ms);
|
|
356
372
|
continue;
|
|
357
373
|
}
|
|
358
374
|
return err;
|
|
@@ -391,7 +407,7 @@ pub const IosDevice = struct {
|
|
|
391
407
|
fn isTransientShimBootstrapFailure(result: command.ExecResult) bool {
|
|
392
408
|
if (result.timed_out) return false;
|
|
393
409
|
switch (result.term) {
|
|
394
|
-
.
|
|
410
|
+
.exited => |code| if (code == 0) return false,
|
|
395
411
|
else => return false,
|
|
396
412
|
}
|
|
397
413
|
return std.mem.indexOf(u8, result.stderr, "iOS shim server exited before it became ready") != null or
|
|
@@ -400,7 +416,7 @@ fn isTransientShimBootstrapFailure(result: command.ExecResult) bool {
|
|
|
400
416
|
}
|
|
401
417
|
|
|
402
418
|
fn shimTimeoutMs() u64 {
|
|
403
|
-
return parseShimTimeoutMs(
|
|
419
|
+
return parseShimTimeoutMs(stdio.getenv(shim_timeout_env));
|
|
404
420
|
}
|
|
405
421
|
|
|
406
422
|
fn parseShimTimeoutMs(raw: ?[]const u8) u64 {
|
|
@@ -410,6 +426,69 @@ fn parseShimTimeoutMs(raw: ?[]const u8) u64 {
|
|
|
410
426
|
return parsed;
|
|
411
427
|
}
|
|
412
428
|
|
|
429
|
+
test "ios simulator openLink keeps sweeping delayed XCTest interruptions until accepted" {
|
|
430
|
+
const allocator = std.heap.page_allocator;
|
|
431
|
+
const argv = [_][*:0]const u8{"zmr-ios-test"};
|
|
432
|
+
stdio.initProcess(.{
|
|
433
|
+
.args = .{ .vector = argv[0..] },
|
|
434
|
+
.environ = .empty,
|
|
435
|
+
}, allocator);
|
|
436
|
+
defer stdio.deinitProcess();
|
|
437
|
+
|
|
438
|
+
var tmp = std.testing.tmpDir(.{});
|
|
439
|
+
defer tmp.cleanup();
|
|
440
|
+
|
|
441
|
+
var shim = try tmp.dir.createFile(stdio.io(), "fake-ios-shim-delayed.sh", .{ .truncate = true });
|
|
442
|
+
{
|
|
443
|
+
var buffer: [4096]u8 = undefined;
|
|
444
|
+
var writer = shim.writerStreaming(stdio.io(), &buffer);
|
|
445
|
+
try writer.interface.writeAll(
|
|
446
|
+
\\#!/usr/bin/env bash
|
|
447
|
+
\\set -euo pipefail
|
|
448
|
+
\\request="$(cat)"
|
|
449
|
+
);
|
|
450
|
+
const shim_tail = try std.fmt.allocPrint(allocator,
|
|
451
|
+
\\
|
|
452
|
+
\\printf '%s\n' "$request" >> ".zig-cache/tmp/{s}/shim.log"
|
|
453
|
+
\\count_file=".zig-cache/tmp/{s}/count.txt"
|
|
454
|
+
\\count=0
|
|
455
|
+
\\if [[ -f "$count_file" ]]; then
|
|
456
|
+
\\ count="$(cat "$count_file")"
|
|
457
|
+
\\fi
|
|
458
|
+
\\count=$((count + 1))
|
|
459
|
+
\\printf '%s' "$count" > "$count_file"
|
|
460
|
+
\\if [[ "$count" -lt 3 ]]; then
|
|
461
|
+
\\ printf '{{"status":"ok","accepted":false,"count":0}}\n'
|
|
462
|
+
\\else
|
|
463
|
+
\\ printf '{{"status":"ok","accepted":true,"label":"Brick Rewards Test","count":1}}\n'
|
|
464
|
+
\\fi
|
|
465
|
+
\\
|
|
466
|
+
, .{ tmp.sub_path, tmp.sub_path });
|
|
467
|
+
defer allocator.free(shim_tail);
|
|
468
|
+
try writer.interface.writeAll(shim_tail);
|
|
469
|
+
try writer.interface.flush();
|
|
470
|
+
}
|
|
471
|
+
shim.close(stdio.io());
|
|
472
|
+
|
|
473
|
+
const shim_path = try std.fmt.allocPrint(allocator, ".zig-cache/tmp/{s}/fake-ios-shim-delayed.sh", .{tmp.sub_path});
|
|
474
|
+
defer allocator.free(shim_path);
|
|
475
|
+
const shim_path_z = try allocator.dupeZ(u8, shim_path);
|
|
476
|
+
defer allocator.free(shim_path_z);
|
|
477
|
+
if (std.c.chmod(shim_path_z, 0o755) != 0) return error.ChmodFailed;
|
|
478
|
+
|
|
479
|
+
var device = try IosDevice.initWithShim(allocator, "./tests/fake-xcrun.sh", "fake-ios-1", "com.example.mobiletest", shim_path);
|
|
480
|
+
defer device.deinit();
|
|
481
|
+
|
|
482
|
+
try device.openLink("exampleapp:///e2e-auth?probe=1");
|
|
483
|
+
|
|
484
|
+
const count_path = try std.fmt.allocPrint(allocator, ".zig-cache/tmp/{s}/count.txt", .{tmp.sub_path});
|
|
485
|
+
defer allocator.free(count_path);
|
|
486
|
+
const count_raw = try stdio.readFileAlloc(allocator, count_path, 1024);
|
|
487
|
+
defer allocator.free(count_raw);
|
|
488
|
+
const count = try std.fmt.parseInt(u8, count_raw, 10);
|
|
489
|
+
try std.testing.expectEqual(@as(u8, 3), count);
|
|
490
|
+
}
|
|
491
|
+
|
|
413
492
|
pub fn listDevices(allocator: std.mem.Allocator, xcrun_path: []const u8) ![]types.DeviceInfo {
|
|
414
493
|
return try ios_devices.listSimulators(allocator, xcrun_path);
|
|
415
494
|
}
|
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/ios_shim.zig
CHANGED
|
@@ -128,6 +128,18 @@ pub fn parseOkResponse(content: []const u8) !void {
|
|
|
128
128
|
if (!std.mem.eql(u8, status, "ok")) return error.IosShimResponseNotOk;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
pub fn parseAcceptSystemAlertResponse(content: []const u8) !bool {
|
|
132
|
+
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
|
133
|
+
defer arena.deinit();
|
|
134
|
+
const parsed = try std.json.parseFromSlice(std.json.Value, arena.allocator(), content, .{});
|
|
135
|
+
if (parsed.value != .object) return error.IosShimResponseMustBeObject;
|
|
136
|
+
const status = fieldString(parsed.value.object, "status") orelse return error.IosShimMissingStatus;
|
|
137
|
+
if (!std.mem.eql(u8, status, "ok")) return error.IosShimResponseNotOk;
|
|
138
|
+
const accepted = parsed.value.object.get("accepted") orelse return false;
|
|
139
|
+
if (accepted != .bool) return false;
|
|
140
|
+
return accepted.bool;
|
|
141
|
+
}
|
|
142
|
+
|
|
131
143
|
pub const SelectorActionResponse = enum {
|
|
132
144
|
ok,
|
|
133
145
|
selector_unavailable,
|
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
|
}
|