diff --git a/kuri-mobile/src/ios/cli.zig b/kuri-mobile/src/ios/cli.zig index 80ba31d..42d01ea 100644 --- a/kuri-mobile/src/ios/cli.zig +++ b/kuri-mobile/src/ios/cli.zig @@ -95,7 +95,13 @@ pub fn run(gpa: std.mem.Allocator, args: []const []const u8) !u8 { } const udid = udid_opt orelse try resolveBootedSim(gpa); defer if (udid_opt == null) gpa.free(udid); - try simctl.Sim.init(udid).screenshot(gpa, path); + simctl.Sim.init(udid).screenshot(gpa, path) catch |err| { + if (err == error.SimctlCommandFailed) { + io.writeStderr("screenshot failed: the target must be a booted iOS Simulator. Physical-device screenshots require XCUITest and are not supported in v1.\n"); + return 3; + } + return err; + }; return 0; } if (std.mem.eql(u8, sub, "list-apps")) { @@ -323,7 +329,7 @@ fn printUsage() !void { \\ navigate [--udid U] alias for openurl \\ launch --udid U [--simulator|--device] \\ terminate --udid U [--simulator|--device] - \\ screenshot [--udid U] [path.png] defaults to the booted sim if --udid omitted + \\ screenshot [--simulator] [--udid U] [path.png] Simulator only; defaults to booted sim \\ list-apps --udid U --simulator \\ \\Simulator-only input (macOS, device-pixel coords matching screenshot): diff --git a/kuri-mobile/src/ios/simctl.zig b/kuri-mobile/src/ios/simctl.zig index 1dd23b1..a877c7b 100644 --- a/kuri-mobile/src/ios/simctl.zig +++ b/kuri-mobile/src/ios/simctl.zig @@ -12,21 +12,26 @@ pub const Sim = struct { pub fn screenshot(self: Sim, gpa: std.mem.Allocator, path: []const u8) !void { const r = try io.runCommand(gpa, &.{ "xcrun", "simctl", "io", self.udid, "screenshot", path }, 64 * 1024 * 1024); - gpa.free(r.stdout); + defer gpa.free(r.stdout); + try ensureSuccess(r.term, r.stdout); } pub fn launch(self: Sim, gpa: std.mem.Allocator, bundle_id: []const u8) !void { const r = try io.runCommand(gpa, &.{ "xcrun", "simctl", "launch", self.udid, bundle_id }, 1024 * 1024); - gpa.free(r.stdout); + defer gpa.free(r.stdout); + try ensureSuccess(r.term, r.stdout); } pub fn terminate(self: Sim, gpa: std.mem.Allocator, bundle_id: []const u8) !void { const r = try io.runCommand(gpa, &.{ "xcrun", "simctl", "terminate", self.udid, bundle_id }, 1024 * 1024); - gpa.free(r.stdout); + defer gpa.free(r.stdout); + try ensureSuccess(r.term, r.stdout); } pub fn listApps(self: Sim, gpa: std.mem.Allocator) ![]u8 { const r = try io.runCommand(gpa, &.{ "xcrun", "simctl", "listapps", self.udid }, 16 * 1024 * 1024); + errdefer gpa.free(r.stdout); + try ensureSuccess(r.term, r.stdout); return r.stdout; } @@ -35,20 +40,32 @@ pub const Sim = struct { /// you tell Safari to load a page without typing in the address bar. pub fn openUrl(self: Sim, gpa: std.mem.Allocator, url: []const u8) !void { const r = try io.runCommand(gpa, &.{ "xcrun", "simctl", "openurl", self.udid, url }, 1024 * 1024); - gpa.free(r.stdout); + defer gpa.free(r.stdout); + try ensureSuccess(r.term, r.stdout); } pub fn boot(self: Sim, gpa: std.mem.Allocator) !void { const r = try io.runCommand(gpa, &.{ "xcrun", "simctl", "boot", self.udid }, 1024 * 1024); - gpa.free(r.stdout); + defer gpa.free(r.stdout); + try ensureSuccess(r.term, r.stdout); } pub fn shutdown(self: Sim, gpa: std.mem.Allocator) !void { const r = try io.runCommand(gpa, &.{ "xcrun", "simctl", "shutdown", self.udid }, 1024 * 1024); - gpa.free(r.stdout); + defer gpa.free(r.stdout); + try ensureSuccess(r.term, r.stdout); } }; +fn ensureSuccess(term: i32, output: []const u8) !void { + if (term == 0) return; + if (output.len != 0) { + io.writeStderr(output); + if (output[output.len - 1] != '\n') io.writeStderr("\n"); + } + return error.SimctlCommandFailed; +} + pub const SimDevice = struct { udid: []const u8, name: []const u8, @@ -58,6 +75,7 @@ pub const SimDevice = struct { pub fn listDevices(gpa: std.mem.Allocator) ![]SimDevice { const r = try io.runCommand(gpa, &.{ "xcrun", "simctl", "list", "devices", "--json" }, 16 * 1024 * 1024); defer gpa.free(r.stdout); + try ensureSuccess(r.term, r.stdout); var parsed = try std.json.parseFromSlice(std.json.Value, gpa, r.stdout, .{}); defer parsed.deinit(); @@ -90,6 +108,12 @@ pub fn listDevices(gpa: std.mem.Allocator) ![]SimDevice { return try list.toOwnedSlice(gpa); } +test "simctl command status is not silently ignored" { + try ensureSuccess(0, ""); + try std.testing.expectError(error.SimctlCommandFailed, ensureSuccess(256, "simctl failed")); + try std.testing.expectError(error.SimctlCommandFailed, ensureSuccess(9, "terminated")); +} + pub fn freeSimDevices(gpa: std.mem.Allocator, devs: []const SimDevice) void { for (devs) |d| { gpa.free(d.udid);