Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 63 additions & 14 deletions sdk/remote/Client.zig
Original file line number Diff line number Diff line change
Expand Up @@ -135,17 +135,28 @@ pub fn deinit(self: *Client) void {
if (self.socket_path) |p| self.gpa.free(p);
}

/// Where fizzy would be installed on `env.os`, likeliest first; not `$PATH` (`onPath`). Touches nothing.
/// Where fizzy would be installed on `env.os`, likeliest first; not `$PATH` (`onPath`). Touches
/// nothing.
pub fn candidates(arena: std.mem.Allocator, identity: Identity, env: Env) error{OutOfMemory}![]const Install {
var list: std.ArrayList(Install) = .empty;
const sep: []const u8 = if (env.os == .windows) "\\" else "/";
switch (env.os) {
.macos => {
const roots = [_]?[]const u8{ "/Applications", if (env.home) |h| try join(arena, "/", &.{ h, "Applications" }) else null };
const roots = [_]?[]const u8{
"/Applications",
if (env.home) |h| try join(
arena,
"/",
&.{ h, "Applications" },
) else null,
};
for (roots) |root_opt| {
const root = root_opt orelse continue;
const app = try std.fmt.allocPrint(arena, "{s}/{s}.app", .{ root, identity.display_name });
try list.append(arena, .{ .exe = try join(arena, "/", &.{ app, "Contents/MacOS", identity.name }), .bundle = app });
try list.append(
arena,
.{ .exe = try join(arena, "/", &.{ app, "Contents/MacOS", identity.name }), .bundle = app },
);
}
},
.windows => if (env.local_app_data) |base| {
Expand Down Expand Up @@ -209,15 +220,23 @@ fn lookupSystem(io: Io, arena: std.mem.Allocator, identity: Identity, env: Env)
switch (env.os) {
.macos => {
if (std.mem.findAny(u8, identity.bundle_id, "\"\\") != null) return null;
const query = std.fmt.allocPrint(arena, "kMDItemCFBundleIdentifier == \"{s}\"", .{identity.bundle_id}) catch return null;
const query = std.fmt.allocPrint(
arena,
"kMDItemCFBundleIdentifier == \"{s}\"",
.{identity.bundle_id},
) catch return null;
const out = runCapture(io, arena, &.{ "/usr/bin/mdfind", query }) orelse return null;
const bundle = std.mem.trim(u8, firstLine(out), " \t\r");
if (bundle.len == 0) return null;
const exe = join(arena, "/", &.{ bundle, "Contents/MacOS", identity.name }) catch return null;
return if (exists(io, exe)) .{ .exe = exe, .bundle = bundle } else null;
},
.windows => for ([_][]const u8{ "HKCU", "HKLM" }) |hive| {
const key = std.fmt.allocPrint(arena, "{s}\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\{s}.exe", .{ hive, identity.name }) catch return null;
const key = std.fmt.allocPrint(
arena,
"{s}\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\{s}.exe",
.{ hive, identity.name },
) catch return null;
const out = runCapture(io, arena, &.{ "reg", "query", key, "/ve" }) orelse continue;
const exe = parseRegValue(out) orelse continue;
if (exists(io, exe)) return .{ .exe = exe };
Expand All @@ -241,7 +260,11 @@ fn firstLine(text: []const u8) []const u8 {

/// Stdout of a run that exited 0, or null.
fn runCapture(io: Io, arena: std.mem.Allocator, argv: []const []const u8) ?[]const u8 {
const result = std.process.run(arena, io, .{ .argv = argv, .timeout = .{ .duration = .{ .raw = .fromSeconds(5), .clock = .awake } } }) catch return null;
const result = std.process.run(
arena,
io,
.{ .argv = argv, .timeout = .{ .duration = .{ .raw = .fromSeconds(5), .clock = .awake } } },
) catch return null;
return switch (result.term) {
.exited => |code| if (code == 0) result.stdout else null,
else => null,
Expand All @@ -254,7 +277,11 @@ pub fn controlPath(io: Io, arena: std.mem.Allocator, install: Install, profile:
&.{ install.exe, "--control-path", "--profile", p }
else
&.{ install.exe, "--control-path" };
const result = std.process.run(arena, io, .{ .argv = argv, .timeout = .{ .duration = .{ .raw = .fromSeconds(10), .clock = .awake } } }) catch |err| return spawnError(err);
const result = std.process.run(
arena,
io,
.{ .argv = argv, .timeout = .{ .duration = .{ .raw = .fromSeconds(10), .clock = .awake } } },
) catch |err| return spawnError(err);
// A fizzy that predates `--control-path` starts as an editor instead: no path to read.
const ok = switch (result.term) {
.exited => |code| code == 0,
Expand Down Expand Up @@ -358,7 +385,10 @@ fn timeoutMs(ms: u32) Io.Timeout {
/// `timeout` passed first.
pub fn readSome(io: Io, stream: Io.net.Stream, buf: []u8, timeout: Io.Timeout) error{Closed}!?usize {
var data: [1][]u8 = .{buf};
const result = io.operateTimeout(.{ .net_read = .{ .socket_handle = stream.socket.handle, .data = &data } }, timeout) catch |err| switch (err) {
const result = io.operateTimeout(
.{ .net_read = .{ .socket_handle = stream.socket.handle, .data = &data } },
timeout,
) catch |err| switch (err) {
error.Timeout => return null,
else => return error.Closed,
};
Expand Down Expand Up @@ -428,14 +458,23 @@ pub fn connect(self: *Client) Error!void {
/// `GET /remote` on `stream`, then the hellos. Takes ownership of `stream`.
fn upgrade(self: *Client, stream: Io.net.Stream, deadline: Deadline) Error!void {
errdefer stream.close(self.io);
try writeAll(self.io, stream, "GET /remote HTTP/1.1\r\nHost: localhost\r\nConnection: Upgrade\r\nUpgrade: " ++ upgrade_token ++ "\r\n\r\n");
try writeAll(
self.io,
stream,
"GET /remote HTTP/1.1\r\nHost: localhost\r\nConnection: Upgrade\r\nUpgrade: " ++ upgrade_token ++ "\r\n\r\n",
);

var head: [2048]u8 = undefined;
var len: usize = 0;
const body_at = while (true) {
if (std.mem.find(u8, head[0..len], "\r\n\r\n")) |at| break at + 4;
if (len == head.len) return error.Protocol;
const n = try readSome(self.io, stream, head[len..], timeoutMs(deadline.remainingMs())) orelse return error.TimedOut;
const n = try readSome(
self.io,
stream,
head[len..],
timeoutMs(deadline.remainingMs()),
) orelse return error.TimedOut;
if (n == 0) return error.Closed;
len += n;
};
Expand Down Expand Up @@ -509,7 +548,8 @@ pub fn connected(self: *const Client) bool {
}

/// Check `msg` against the conversation's rules and send it. A message the rules refuse is the
/// caller's mistake: it is not sent and the stream stays open. A write that fails closes the client.
/// caller's mistake: it is not sent and the stream stays open. A write that fails closes the
/// client.
pub fn send(self: *Client, msg: message.Message) Error!void {
const ep = &(self.endpoint orelse return error.Closed);
if (self.stream == null) return self.closeStale();
Expand Down Expand Up @@ -598,7 +638,11 @@ test candidates {
const mac_path = try onPath(arena, id, .{ .os = .macos, .path = "/usr/bin:/opt/bin" });
try std.testing.expectEqualStrings("/opt/bin/fizzy", mac_path[1].exe);

const win = try candidates(arena, id, .{ .os = .windows, .local_app_data = "C:\\Users\\a\\AppData\\Local", .path = "C:\\bin;D:\\tools" });
const win = try candidates(
arena,
id,
.{ .os = .windows, .local_app_data = "C:\\Users\\a\\AppData\\Local", .path = "C:\\bin;D:\\tools" },
);
try std.testing.expectEqualStrings("C:\\Users\\a\\AppData\\Local\\fizzy\\current\\fizzy.exe", win[0].exe);
const win_path = try onPath(arena, id, .{ .os = .windows, .path = "C:\\bin;D:\\tools" });
try std.testing.expectEqualStrings("D:\\tools\\fizzy.exe", win_path[1].exe);
Expand All @@ -607,11 +651,16 @@ test candidates {
try std.testing.expectEqualStrings("/home/a/.local/share/paint/current/paint", linux[0].exe);
try std.testing.expectEqualStrings("/home/a/Applications/paint.AppImage", linux[2].exe);
try std.testing.expectEqual(@as(usize, 3), linux.len);
try std.testing.expectEqualStrings("/usr/bin/paint", (try onPath(arena, .{ .name = "paint" }, .{ .os = .linux, .path = "/usr/bin" }))[0].exe);
try std.testing.expectEqualStrings(
"/usr/bin/paint",
(try onPath(arena, .{ .name = "paint" }, .{ .os = .linux, .path = "/usr/bin" }))[0].exe,
);
}

test parseRegValue {
const out = "\r\nHKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\fizzy.exe\r\n (Default) REG_SZ C:\\Users\\a\\AppData\\Local\\fizzy\\current\\fizzy.exe\r\n";
const out = "\r\nHKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion" ++
"\\App Paths\\fizzy.exe\r\n (Default) REG_SZ " ++
"C:\\Users\\a\\AppData\\Local\\fizzy\\current\\fizzy.exe\r\n";
try std.testing.expectEqualStrings("C:\\Users\\a\\AppData\\Local\\fizzy\\current\\fizzy.exe", parseRegValue(out).?);
try std.testing.expect(parseRegValue("ERROR: The system was unable to find the specified registry key") == null);
}
36 changes: 29 additions & 7 deletions sdk/remote/Connection.zig
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,11 @@ fn apply(self: *Connection, msg: Message, from: Role) Error!void {
.create_view => |m| {
if (m.view == 0) return error.UnknownView;
if (self.views.contains(m.view)) return error.DuplicateView;
try self.views.put(self.gpa, m.view, .{ .tier = m.tier, .width = m.width, .height = m.height, .scale = m.scale });
try self.views.put(
self.gpa,
m.view,
.{ .tier = m.tier, .width = m.width, .height = m.height, .scale = m.scale },
);
},
.view_created => |m| {
const v = self.views.getPtr(m.view) orelse return error.UnknownView;
Expand Down Expand Up @@ -254,14 +258,23 @@ test "a view's life, and the frames that answer requests" {
try say(&p.editor, &p.host, .{ .view_created = .{ .view = 7, .tier = .draw_list, .width = 100, .height = 50 } });
try t.expect(p.host.view(7).?.focused);

try t.expectError(error.UnexpectedFrame, p.editor.sending(.{ .frame = .{ .view = 7, .seq = 1, .tier = .draw_list } }));
try t.expectError(
error.UnexpectedFrame,
p.editor.sending(.{ .frame = .{ .view = 7, .seq = 1, .tier = .draw_list } }),
);
try say(&p.host, &p.editor, .{ .frame_request = .{ .view = 7, .seq = 1 } });
try t.expectError(error.SeqNotIncreasing, p.host.sending(.{ .frame_request = .{ .view = 7, .seq = 1 } }));
try say(&p.host, &p.editor, .{ .frame_request = .{ .view = 7, .seq = 3 } });
// Fizzy may skip seq 1 and answer 3 only; 2 was never asked, and 3 cannot be answered twice.
try say(&p.editor, &p.host, .{ .frame = .{ .view = 7, .seq = 3, .tier = .draw_list } });
try t.expectError(error.UnexpectedFrame, p.editor.sending(.{ .frame = .{ .view = 7, .seq = 3, .tier = .draw_list } }));
try t.expectError(error.UnexpectedFrame, p.editor.sending(.{ .frame = .{ .view = 7, .seq = 4, .tier = .draw_list } }));
try t.expectError(
error.UnexpectedFrame,
p.editor.sending(.{ .frame = .{ .view = 7, .seq = 3, .tier = .draw_list } }),
);
try t.expectError(
error.UnexpectedFrame,
p.editor.sending(.{ .frame = .{ .view = 7, .seq = 4, .tier = .draw_list } }),
);

try say(&p.host, &p.editor, .{ .close_view = .{ .view = 7 } });
try t.expectError(error.ViewClosing, p.host.sending(.{ .resize = .{ .view = 7, .width = 1, .height = 1 } }));
Expand All @@ -287,7 +300,10 @@ test "each side may send only its own messages" {
defer p.editor.deinit();
try open(&p);
try t.expectError(error.WrongDirection, p.host.sending(.{ .cursor = .{ .view = 1, .shape = .arrow } }));
try t.expectError(error.WrongDirection, p.host.received(.{ .create_view = .{ .view = 1, .width = 1, .height = 1 } }));
try t.expectError(
error.WrongDirection,
p.host.received(.{ .create_view = .{ .view = 1, .width = 1, .height = 1 } }),
);
}

test "no common version closes, naming who updates" {
Expand Down Expand Up @@ -332,7 +348,13 @@ test "view 0 is no view, and a frame must match its view's tier" {
try t.expectError(error.UnknownView, p.host.sending(.{ .create_view = .{ .view = 0, .width = 1, .height = 1 } }));
try say(&p.host, &p.editor, .{ .create_view = .{ .view = 1, .width = 1, .height = 1 } });
try say(&p.host, &p.editor, .{ .frame_request = .{ .view = 1, .seq = 1 } });
try t.expectError(error.UnexpectedFrame, p.editor.sending(.{ .frame = .{ .view = 1, .seq = 1, .tier = .draw_list } }));
try t.expectError(
error.UnexpectedFrame,
p.editor.sending(.{ .frame = .{ .view = 1, .seq = 1, .tier = .draw_list } }),
);
try say(&p.editor, &p.host, .{ .view_created = .{ .view = 1, .tier = .draw_list, .width = 1, .height = 1 } });
try t.expectError(error.UnexpectedFrame, p.editor.sending(.{ .frame = .{ .view = 1, .seq = 1, .tier = .shared_texture } }));
try t.expectError(
error.UnexpectedFrame,
p.editor.sending(.{ .frame = .{ .view = 1, .seq = 1, .tier = .shared_texture } }),
);
}
24 changes: 19 additions & 5 deletions sdk/remote/capi.zig
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ export fn fizzy_remote_open(options: ?*const Options, out: ?*?*Handle) callconv(
const o = options orelse return @backingInt(Status.invalid_argument);
const result = out orelse return @backingInt(Status.invalid_argument);
result.* = null;
if (o.size < @offsetOf(Options, "socket_path") + @sizeOf(?[*:0]const u8)) return @backingInt(Status.invalid_argument);
if (o.size < @offsetOf(Options, "socket_path") + @sizeOf(?[*:0]const u8))
return @backingInt(Status.invalid_argument);

const handle = gpa.create(Handle) catch return @backingInt(Status.out_of_memory);
handle.* = .{ .arena = .init(gpa), .threaded = .init(gpa, .{ .environ = processEnviron() }), .client = undefined };
Expand Down Expand Up @@ -156,16 +157,24 @@ export fn fizzy_remote_send(handle: ?*Handle, frame: ?[*]const u8, len: usize) c
reader.push(bytes) catch return @backingInt(Status.out_of_memory);
var arena: std.heap.ArenaAllocator = .init(gpa);
defer arena.deinit();
const frame_in = (reader.next() catch return @backingInt(Status.protocol)) orelse return @backingInt(Status.protocol);
const msg = (codec.decode(arena.allocator(), frame_in) catch return @backingInt(Status.protocol)) orelse return @backingInt(Status.protocol);
const frame_in = (reader.next() catch return @backingInt(Status.protocol)) orelse
return @backingInt(Status.protocol);
const msg = (codec.decode(arena.allocator(), frame_in) catch return @backingInt(Status.protocol)) orelse
return @backingInt(Status.protocol);
h.client.send(msg) catch |err| return status(err);
return 0;
}

/// The next frame from fizzy into `buf`, waiting up to `timeout_ms` (negative: forever).
/// `*len` is its length; 0 with `ok` means the wait passed. A frame larger than `cap` is kept:
/// `buffer_too_small` with `*len` the size needed, then call again with room.
export fn fizzy_remote_recv(handle: ?*Handle, buf: ?[*]u8, cap: usize, len: ?*usize, timeout_ms: i32) callconv(.c) c_int {
export fn fizzy_remote_recv(
handle: ?*Handle,
buf: ?[*]u8,
cap: usize,
len: ?*usize,
timeout_ms: i32,
) callconv(.c) c_int {
const h = handle orelse return @backingInt(Status.invalid_argument);
const out_len = len orelse return @backingInt(Status.invalid_argument);
out_len.* = 0;
Expand Down Expand Up @@ -248,7 +257,12 @@ test "the C ABI opens, round trips a frame and closes" {

var out: std.ArrayList(u8) = .empty;
defer out.deinit(t.allocator);
try codec.encode(t.allocator, &out, .{ .create_view = .{ .view = 3, .width = 64, .height = 48 } }, codec.default_max_frame);
try codec.encode(
t.allocator,
&out,
.{ .create_view = .{ .view = 3, .width = 64, .height = 48 } },
codec.default_max_frame,
);
try t.expectEqual(@as(c_int, 0), fizzy_remote_send(handle, out.items.ptr, out.items.len));

var small: [4]u8 = undefined;
Expand Down
35 changes: 31 additions & 4 deletions sdk/remote/fake_fizzy.zig
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@ pub fn main(init: std.process.Init) !void {
}
if (quiet) try appendLaunch(gpa, io, dir);

const text = try Io.Dir.cwd().readFileAlloc(io, try std.fmt.allocPrint(gpa, "{s}/mode", .{dir}), gpa, .limited(256));
const text = try Io.Dir.cwd().readFileAlloc(
io,
try std.fmt.allocPrint(gpa, "{s}/mode", .{dir}),
gpa,
.limited(256),
);
var words = std.mem.tokenizeAny(u8, text, " \r\n");
const mode = std.meta.stringToEnum(Mode, words.next() orelse "serve") orelse .serve;
switch (mode) {
Expand Down Expand Up @@ -91,9 +96,23 @@ fn talk(gpa: std.mem.Allocator, io: Io, stream: Io.net.Stream, mode: Mode, n: us
len += got;
};
if (mode == .noremote) return Client.writeAll(io, stream, "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n");
try Client.writeAll(io, stream, "HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: " ++ Client.upgrade_token ++ "\r\n\r\n");
try Client.writeAll(
io,
stream,
"HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: " ++ Client.upgrade_token ++ "\r\n\r\n",
);

var ep: Endpoint = .init(gpa, .editor, .{ .app_id = "fizzy-fake", .mode = .shared, .tiers_offered = .{ .draw_list = true }, .version_min = if (mode == .old) 9 else message.min_version, .version_max = if (mode == .old) 9 else message.max_version });
var ep: Endpoint = .init(
gpa,
.editor,
.{
.app_id = "fizzy-fake",
.mode = .shared,
.tiers_offered = .{ .draw_list = true },
.version_min = if (mode == .old) 9 else message.min_version,
.version_max = if (mode == .old) 9 else message.max_version,
},
);
defer ep.deinit();
try ep.start();
try ep.feed(head[body_at..len]);
Expand All @@ -107,7 +126,15 @@ fn talk(gpa: std.mem.Allocator, io: Io, stream: Io.net.Stream, mode: Mode, n: us
try Client.writeAll(io, stream, ep.pending());
return;
},
.create_view => |cv| if (mode == .bye) try ep.send(.{ .bye = .{ .reason = .shutdown, .text = "going away" } }) else try ep.send(.{ .view_created = .{ .view = cv.view, .tier = .draw_list, .width = cv.width, .height = cv.height } }),
.create_view => |cv| if (mode == .bye)
try ep.send(.{ .bye = .{ .reason = .shutdown, .text = "going away" } })
else
try ep.send(.{ .view_created = .{
.view = cv.view,
.tier = .draw_list,
.width = cv.width,
.height = cv.height,
} }),
.bye => return,
else => {},
};
Expand Down
24 changes: 22 additions & 2 deletions sdk/remote/message.zig
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,16 @@ pub const Type = enum(u16) {
/// know an optional type skips it by its length. Sent in the frame header.
pub fn required(t: Type) bool {
return switch (t) {
.hello, .bye, .create_view, .view_created, .close_view, .view_closed, .resize, .frame_request, .frame => true,
.hello,
.bye,
.create_view,
.view_created,
.close_view,
.view_closed,
.resize,
.frame_request,
.frame,
=> true,
else => false,
};
}
Expand All @@ -56,7 +65,18 @@ pub const Type = enum(u16) {
pub fn sender(t: Type) Sender {
return switch (t) {
.hello, .bye => .either,
.view_created, .view_closed, .cursor, .ime_rect, .text_input_active, .clipboard_request, .wants_keys, .open_url, .drag_begin, .frame, .frame_ready => .editor,
.view_created,
.view_closed,
.cursor,
.ime_rect,
.text_input_active,
.clipboard_request,
.wants_keys,
.open_url,
.drag_begin,
.frame,
.frame_ready,
=> .editor,
else => .host,
};
}
Expand Down
Loading
Loading