Changes
12 changed files (+424/-484)
-
-
@@ -49,6 +49,19 @@ };const compile_gschema = b.option(bool, "compile-gschema", "Compile gschema XML file for local run") orelse false; const libsood = libsood: { const sood = b.dependency("sood", .{ .target = target, .optimize = optimize, .linkage = .static, }); const lib = sood.artifact("sood"); lib.installHeader(b.path("src/libsood.vapi"), "libsood.vapi"); break :libsood lib; }; const zig_lib = zig_lib: { const mod = b.createModule(.{ .root_source_file = b.path("src/core/main.zig"),
-
@@ -134,6 +147,10 @@valac.addArg("--vapidir"); valac.addDirectoryArg(b.path("src/core")); valac.addArg("--vapidir"); valac.addDirectoryArg(b.path("src")); valac.addFileInput(b.path("src/libsood.vapi")); // Tell Vala what system libraries to use. Perhaps type checking things? for (system_libraries) |lib| { valac.addArgs(&.{ "--pkg", lib });
-
@@ -141,6 +158,7 @@ }valac.addArgs(&.{ "--pkg", "posix" }); valac.addArgs(&.{ "--pkg", "plac" }); valac.addArgs(&.{ "--pkg", "libsood" }); // Tell Vala to emit C source files under the output directory. // The directory usually is Zig cache directory (like ".zig-cache/o/xxx").
-
@@ -222,6 +240,7 @@ exe.linkSystemLibrary(lib);} exe.root_module.addObject(zig_lib); exe.root_module.linkLibrary(libsood); exe.addCSourceFile(.{ .file = gresouce_c });
-
-
-
@@ -26,7 +26,6 @@ <file preprocess="xml-stripblanks">ui/playback-toolbar.ui</file><file preprocess="xml-stripblanks">ui/server-list.ui</file> <file preprocess="xml-stripblanks">ui/generic-error-dialog.ui</file> <file preprocess="xml-stripblanks">ui/server-connecting.ui</file> <file preprocess="xml-stripblanks">ui/server-list-unexpected-error-dialog.ui</file> <file preprocess="xml-stripblanks">ui/server-list-network-error-dialog.ui</file> <file preprocess="xml-stripblanks">ui/zone-output-row.ui</file> <file preprocess="xml-stripblanks">icons/scalable/actions/audio-volume-high-symbolic.svg</file>
-
-
-
@@ -1,24 +0,0 @@<?xml version="1.0" encoding="utf-8"?> <!-- Copyright 2025 Shota FUJI Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. SPDX-License-Identifier: Apache-2.0 --> <interface> <template class="PlacGtkAdwaitaServerListUnexpectedErrorDialog" parent="PlacGtkAdwaitaGenericErrorDialog"> <property name="title">Failed to scan Roon Servers</property> <property name="description">Encountered an unexpected error during scan operation. Scan again to see if the problem resolves.</property> </template> </interface>
-
-
-
@@ -61,14 +61,6 @@ </object></child> </object> </child> <child> <object class="AdwStatusPage" id="empty"> <property name="visible">false</property> <property name="title">No Servers Found</property> <property name="description">Scan again after setting up Roon Server.</property> <property name="icon-name">item-missing-symbolic</property> </object> </child> </object> </property> </object>
-
-
-
@@ -120,13 +120,6 @@ this.add_action_entries(app_entries, this);} } [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/server-list-unexpected-error-dialog.ui")] class ServerListUnexpectedErrorDialog : GenericErrorDialog { public ServerListUnexpectedErrorDialog(string details) { Object(details: details); } } [GtkTemplate(ui = "/jp/pocka/plac/gtk-adwaita/ui/server-list-network-error-dialog.ui")] class ServerListNetworkErrorDialog : GenericErrorDialog { public ServerListNetworkErrorDialog(string details) {
-
-
-
@@ -15,33 +15,170 @@ //// SPDX-License-Identifier: Apache-2.0 namespace Plac { namespace Discovery { public async Plac.Discovery.ScanResult? scan_async() { GLib.SourceFunc callback = scan_async.callback; Plac.Discovery.ScanResult? result = null; namespace V2 { public class Server : Object { /** * IP address of the server. */ public GLib.InetSocketAddress address { get; construct; } new GLib.Thread<void>("server-scanner", () => { result = Plac.Discovery.scan(); GLib.Idle.add((owned) callback); }); /** * TCP port for WebSocket and HTTP connection. */ public uint16 http_port { get; construct; } yield; return (owned) result; /** * String uniquely identifies a server among servers in a network. */ public string id { get; construct; } public string version { get; construct; } /** * User-facing display name. */ public string name { get; construct; } public Server(GLib.InetSocketAddress address, string id, string version, string name, uint16 http_port) { Object(address: address, id: id, version: version, name: name, http_port: http_port); } } public async Plac.Discovery.ScanResult? find_async(string server_id) { GLib.SourceFunc callback = find_async.callback; Plac.Discovery.ScanResult? result = null; public class ServerScanner : Object { /** * Found a server. * * ServerScanner emits this signal everytime server returned discovery response: * if a server A returned a response at t1 then returned again at t2, ServerScanner * emits this signal twice (t1 and t2.) */ public signal void found(Server server); /** * Scan has been aborted due to a socket error. */ public signal void scan_failed(GLib.Error error); private GLib.Cancellable? cancellable = null; public int64 read_timeout_us = 1000 * 1000 * 2; public ServerScanner() { Object(); } public void start() { GLib.log("Plac", LEVEL_DEBUG, "Starting server scanner..."); cancellable = new GLib.Cancellable(); new GLib.Thread<void>("plac-server-scanner", () => { try { var socket = new GLib.Socket(IPV4, DATAGRAM, UDP); socket.set_timeout(3); var inet_addr = new GLib.InetAddress.from_bytes(Sood.DISCOVERY_MULTICAST_IPV4_ADDRESS, IPV4); var sock_addr = new GLib.InetSocketAddress(inet_addr, Sood.DISCOVERY_SERVER_UDP_PORT); while (true) { try { socket.send_to(sock_addr, (uint8[]) Sood.DISCOVERY_QUERY_PREBUILT, cancellable); GLib.log("Plac", LEVEL_DEBUG, "Sent discovery query"); } catch (GLib.Error error) { if (error is GLib.IOError.CANCELLED) { return; } GLib.log("Plac", LEVEL_CRITICAL, "Failed to send discovery query: %s", error.message); var e = error.copy(); GLib.Idle.add(() => { scan_failed(e); return false; }); cancellable = null; return; } while (true) { try { GLib.SocketAddress addr; var bytes = socket.receive_bytes_from(out addr, 512, read_timeout_us, cancellable); if (!(addr is GLib.InetSocketAddress)) { GLib.log("Plac", LEVEL_WARNING, "Received discovery response from non-IP socket"); continue; } Sood.DiscoveryResponse resp; var result = Sood.DiscoveryResponse.parse(out resp, (char[]) bytes.get_data()); if (result != Sood.Result.OK) { GLib.log("Plac", LEVEL_WARNING, "Received malformed discovery response: %s", result.to_string()); continue; } // String in Vala is null-terminated, without exception. // Using non null-terminated string or casting uint8[] to string // will result in out of bound reads. I found no function, class, // or language builtins for allocating null-terminated string from // non-null one. Slicing is the closest I got. var server = new Server( (GLib.InetSocketAddress) addr, resp.unique_id.slice(0, (long) resp.unique_id_len), resp.display_version.slice(0, (long) resp.display_version_len), resp.name.slice(0, (long) resp.name_len), resp.http_port ); GLib.Idle.add(() => { found(server); return false; }); } catch (GLib.Error error) { if (error is GLib.IOError.TIMED_OUT) { break; } if (error is GLib.IOError.CANCELLED) { return; } GLib.log("Plac", LEVEL_CRITICAL, "Failed to read discovery response: %s", error.message); cancellable = null; var e = error.copy(); GLib.Idle.add(() => { scan_failed(e); return false; }); return; } } } } catch (GLib.Error error) { GLib.log("Plac", LEVEL_CRITICAL, "Discovery aborted due to an error: %s", error.message); var e = error.copy(); GLib.Idle.add(() => { scan_failed(e); return false; }); } new GLib.Thread<void>("server-find", () => { result = Plac.Discovery.find(server_id); GLib.Idle.add((owned) callback); }); cancellable = null; }); } yield; return (owned) result; public void stop() { GLib.log("Plac", LEVEL_DEBUG, "Stopping server scanner..."); cancellable.cancel(); } } } namespace Discovery { public async Plac.Discovery.ScanResult? resolve_async(string server_id, string ip_addr, uint16 http_port) { GLib.SourceFunc callback = resolve_async.callback; Plac.Discovery.ScanResult? result = null;
-
-
-
@@ -133,7 +133,7 @@ } else {resolve_server.begin((obj, res) => { try { resolve_server.end(res); } catch (ResolveError e) { } catch (GLib.Error e) { error_banner.title = e.message; error_banner.revealed = true; return;
-
@@ -200,33 +200,34 @@conn.activate(); } private async void resolve_server() throws ResolveError { private async void resolve_server() throws GLib.Error { GLib.SourceFunc callback = resolve_server.callback; ResolveError? error = null; GLib.Error? error = null; Plac.Discovery.find_async.begin(server_id, (obj, res) => { var result = Plac.Discovery.find_async.end(res); var scanner = new Plac.V2.ServerScanner(); scanner.start(); if (result.code != OK) { GLib.log("Plac", LEVEL_CRITICAL, "Failed to find server: %s", result.code.to_string()); scanner.found.connect((found) => { if (server_id == found.id) { var s = new Plac.Discovery.Server( found.id, found.name, found.version, found.address.to_string(), found.http_port ); server = s; conn = new Plac.AsyncConnection.with_token(s, settings.connected_server_token); Idle.add((owned) callback); error = new ResolveError.CONNECTION_ERROR("Failed to connect: %s".printf(result.code.to_string())); return; } if (result.servers.length < 1) { GLib.log("Plac", LEVEL_CRITICAL, "Server not found: ID=%s", server_id); Idle.add((owned) callback); error = new ResolveError.SERVER_NOT_FOUND("Server not found"); return; } }); server = result.servers[0]; conn = new Plac.AsyncConnection.with_token(server, settings.connected_server_token); Idle.add((owned) callback); scanner.scan_failed.connect((e) => { error = e; }); yield; scanner.stop(); if (error != null) { throw error;
-
-
-
@@ -35,15 +35,52 @@[GtkChild] private unowned Gtk.Button scan_button; [GtkChild] private unowned Adw.StatusPage empty; private ulong error_detail_hid; private Plac.V2.ServerScanner scanner = new Plac.V2.ServerScanner(); private Gee.HashMap<string, ServerRow>rows = new Gee.HashMap<string, ServerRow>(); public Window(Gtk.Application app) { Object(application: app); } construct { scanner.found.connect((server) => { stack.visible_child_name = "idle"; var existing = rows[server.id]; if (existing == null) { var row = new ServerRow(server); row.open.connect(() => { scanner.stop(); var window = new MainWindow( application, new Plac.Discovery.Server( row.server.id, row.server.name, row.server.version, row.server.address.to_string(), row.server.http_port ) ); window.start(); this.close(); }); servers_list.append(row); rows[server.id] = row; } else { existing.server = server; } }); scanner.scan_failed.connect((error) => { show_error(error.message); }); } public void start() { var scan_action = new SimpleAction("scan_servers", null); scan_action.activate.connect(this.scan);
-
@@ -63,87 +100,50 @@ }stack.visible_child_name = "loading"; scan_button.sensitive = false; Plac.Discovery.scan_async.begin((obj, res) => { var result = Plac.Discovery.scan_async.end(res); servers_list.remove_all(); switch (result.code) { case OK: break; case UNKNOWN: show_error(UNEXPECTED_ERROR, "Unexpected error"); return; case NETWORK_UNAVAILABLE: show_error(NETWORK_ERROR, "Network unavailable"); return; case SOCKET_PERMISSION_DENIED: show_error(NETWORK_ERROR, "No permission to create UDP socket"); return; case SOCKET_ERROR: show_error(NETWORK_ERROR, "Failed to operate on UDP socket"); return; case OUT_OF_MEMORY: show_error(UNEXPECTED_ERROR, "Out of memory"); return; } if (result.servers.length == 0) { servers_list.visible = false; empty.visible = true; scan_button.add_css_class("suggested-action"); stack.visible_child_name = "idle"; scan_button.sensitive = true; return; } servers_list.visible = true; empty.visible = false; servers_list.remove_all(); rows.clear(); foreach (Plac.Discovery.Server server in result.servers) { var row = new Adw.ActionRow(); row.title = server.name; row.subtitle = server.version; // AdwActionRow needs static widget to be activatable. However, if we link // an instance of `MainWindow` here, the application won't close due to there // is a still unclosed window. This workarounds that design flaw. var box = new Gtk.Box(HORIZONTAL, 0); row.activatable_widget = box; row.activated.connect(() => { var window = new MainWindow(application, server); window.start(); this.close(); }); servers_list.append(row); } stack.visible_child_name = "idle"; scan_button.sensitive = true; }); scanner.start(); } private void show_error(ScanErrorKind kind, string message) { private void show_error(string message) { scan_button.add_css_class("suggested-action"); error_detail_hid = failure_banner.button_clicked.connect(() => { switch (kind) { case NETWORK_ERROR: var dialog = new ServerListNetworkErrorDialog(message); dialog.present(this); break; case UNEXPECTED_ERROR: var dialog = new ServerListUnexpectedErrorDialog(message); dialog.present(this); break; } var dialog = new ServerListNetworkErrorDialog(message); dialog.present(this); }); failure_banner.revealed = true; stack.visible_child_name = "idle"; scan_button.sensitive = true; } } private class ServerRow : Adw.ActionRow { public signal void open(); private Plac.V2.Server _server; public Plac.V2.Server server { get { return _server; } construct set { _server = value; this.title = value.name; this.subtitle = value.version; } } public ServerRow(Plac.V2.Server server) { Object(server: server); } construct { // AdwActionRow needs static widget to be activatable. var box = new Gtk.Box(HORIZONTAL, 0); this.activatable_widget = box; this.activated.connect(() => { open(); }); } } }
-
-
-
@@ -24,14 +24,6 @@ const Arc = @import("./Arc.zig");const freelog = @import("./log.zig").freelog; const RegistryService = @import("./services/registry.zig").RegistryService; const udp_dst = std.net.Address.initIp4( sood.discovery.multicast_ipv4_address, sood.discovery.udp_port, ); const udp_send_tries = 4; const udp_receive_window_ms = 1300; pub const Server = extern struct { const cname = "plac_discovery_server"; const allocator = std.heap.c_allocator;
-
@@ -94,6 +86,68 @@return self; } fn init( id: [*:0]const u8, name: [*:0]const u8, version: [*:0]const u8, ip_addr: [*:0]const u8, http_port: u16, ) !*Server { const self = try allocator.create(Server); errdefer allocator.destroy(self); const internal = try allocator.create(Internal); errdefer allocator.destroy(internal); const id_copy = try allocator.dupeZ(u8, std.mem.span(id)); errdefer allocator.free(id_copy); const name_copy = try allocator.dupeZ(u8, std.mem.span(name)); errdefer allocator.free(name_copy); const version_copy = try allocator.dupeZ(u8, std.mem.span(version)); errdefer allocator.free(version_copy); const ip_addr_slice = std.mem.span(ip_addr); const ip_addr_copy = try allocator.dupeZ(u8, ip_addr_slice); errdefer allocator.free(ip_addr_copy); const colon = std.mem.indexOfScalar(u8, ip_addr_slice, ':') orelse { return error.UnexpectedIpFormat; }; const internal_addr = try std.net.Address.parseIp4(ip_addr_slice[0..colon], http_port); internal.* = .{ .address = internal_addr, }; self.* = .{ .internal = internal, .id = id_copy.ptr, .name = name_copy.ptr, .version = version_copy.ptr, .ip_addr = ip_addr_copy.ptr, .http_port = http_port, }; return self; } pub fn new( id: [*:0]const u8, name: [*:0]const u8, version: [*:0]const u8, ip_addr: [*:0]const u8, http_port: u16, ) callconv(.C) ?*Server { return init(id, name, version, ip_addr, http_port) catch |err| { std.log.err("Unable to construct Discovery.Server: {s}", .{@errorName(err)}); return null; }; } pub fn retain(ptr: ?*Server) callconv(.C) *Server { var self = ptr orelse @panic(std.fmt.comptimePrint( "Received null pointer on {s}_{s}",
-
@@ -124,6 +178,7 @@pub fn export_capi() void { @export(&retain, .{ .name = std.fmt.comptimePrint("{s}_retain", .{cname}) }); @export(&release, .{ .name = std.fmt.comptimePrint("{s}_release", .{cname}) }); @export(&new, .{ .name = std.fmt.comptimePrint("{s}_new", .{cname}) }); } };
-
@@ -170,21 +225,6 @@return self; } pub fn setServers(self: *ScanResult, input: *const std.StringHashMap(*Server)) !void { const servers = try allocator.alloc(*Server, input.count()); var i: usize = 0; var iter = input.valueIterator(); while (iter.next()) |server| { std.log.debug("Found server ({s})", .{server.*.name}); servers[i] = server.*.retain(); i += 1; } self.servers_ptr = servers.ptr; self.servers_len = servers.len; } pub fn initFindResult(self: *ScanResult, found: *Server) std.mem.Allocator.Error!void { const servers = try allocator.alloc(*Server, 1); errdefer allocator.free(servers);
-
@@ -230,45 +270,6 @@ @export(&release, .{ .name = std.fmt.comptimePrint("{s}_release", .{cname}) });} }; pub fn scan() callconv(.C) ?*ScanResult { const result = ScanResult.make() catch { return null; }; var arena = std.heap.ArenaAllocator.init(std.heap.c_allocator); defer arena.deinit(); scanInternal(arena.allocator(), result) catch |err| { result.code = switch (err) { error.OutOfMemory => .out_of_memory, ScanError.NetworkUnavailable => .network_unavailable, ScanError.SocketPermissionDenied => .socket_permission_denied, ScanError.SocketError => .socket_error, }; }; return result.retain(); } pub fn find(server_id: [*:0]const u8) callconv(.C) ?*ScanResult { const result = ScanResult.make() catch { return null; }; const id = std.mem.span(server_id); findInternal(id, result) catch |err| { result.code = switch (err) { error.OutOfMemory => .out_of_memory, ScanError.NetworkUnavailable => .network_unavailable, ScanError.SocketPermissionDenied => .socket_permission_denied, ScanError.SocketError => .socket_error, }; }; return result.retain(); } pub fn resolve(server_id: [*:0]const u8, ip_addr: [*:0]const u8, http_port: u16) callconv(.C) ?*ScanResult { var arena = std.heap.ArenaAllocator.init(std.heap.c_allocator); defer arena.deinit();
-
@@ -290,138 +291,6 @@return result.retain(); } const ScanError = error{ NetworkUnavailable, SocketPermissionDenied, SocketError, } || std.mem.Allocator.Error; fn scanInternal(allocator: std.mem.Allocator, result: *ScanResult) !void { const sockfd = try createSocket(); defer std.posix.close(sockfd); var servers = std.StringHashMap(*Server).init(allocator); errdefer servers.deinit(); for (0..udp_send_tries) |_| { try sendDiscoveryQuery(sockfd); try broadcastDiscoveryQuery(sockfd); while (true) { std.log.debug("Waiting for UDP message...", .{}); // Discovery response from servers usually fits under 300 bytes. // Extra bytes for safety. var received: [512]u8 = undefined; var src: std.net.Address = undefined; var src_len: std.posix.socklen_t = udp_dst.getOsSockLen(); const size = std.posix.recvfrom( sockfd, &received, 0, &src.any, &src_len, ) catch |err| switch (err) { std.posix.RecvFromError.WouldBlock => { std.log.debug("UDP read timeout.", .{}); break; }, std.posix.RecvFromError.MessageTooBig => { std.log.warn("Unable to read UDP message (message too big)", .{}); continue; }, else => return ScanError.SocketError, }; std.log.debug("Got UDP message.", .{}); const response = sood.discovery.Response.parse(received[0..size]) catch |err| { std.log.warn( "Unable to parse received UDP message as SOOD message: {s}", .{@errorName(err)}, ); // Non-SOOD message. Unlikely but technically possible. continue; }; const stale = servers.getPtr(response.unique_id); defer if (stale) |server| { server.*.release(); }; var server = try Server.make(&response, &src); errdefer server.release(); try servers.put(std.mem.span(server.id), server); } } try result.setServers(&servers); result.code = .ok; } fn findInternal(server_id: []const u8, result: *ScanResult) !void { const sockfd = try createSocket(); defer std.posix.close(sockfd); for (0..udp_send_tries) |_| { try sendDiscoveryQuery(sockfd); try broadcastDiscoveryQuery(sockfd); while (true) { std.log.debug("Waiting for UDP message...", .{}); // Discovery response from servers usually fits under 300 bytes. // Extra bytes for safety. var received: [512]u8 = undefined; var src: std.net.Address = undefined; var src_len: std.posix.socklen_t = udp_dst.getOsSockLen(); const size = std.posix.recvfrom( sockfd, &received, 0, &src.any, &src_len, ) catch |err| switch (err) { std.posix.RecvFromError.WouldBlock => { std.log.debug("UDP read timeout.", .{}); break; }, std.posix.RecvFromError.MessageTooBig => { std.log.warn("Unable to read UDP message (message too big)", .{}); continue; }, else => return ScanError.SocketError, }; std.log.debug("Got UDP message.", .{}); const response = sood.discovery.Response.parse(received[0..size]) catch |err| { std.log.warn( "Unable to parse received UDP message as SOOD message: {s}", .{@errorName(err)}, ); // Non-SOOD message. Unlikely but technically possible. continue; }; if (!std.mem.eql(u8, response.unique_id, server_id)) { continue; } var server = try Server.make(&response, &src); errdefer server.release(); try result.initFindResult(server); return; } } result.code = .ok; return; } fn resolveInternal( allocator: std.mem.Allocator, server_id: []const u8,
-
@@ -492,141 +361,9 @@ try result.initFindResult(server);return; } fn createSocket() !std.posix.socket_t { std.log.debug("Opening UDP socket...", .{}); const sockfd = std.posix.socket(std.posix.AF.INET, std.posix.SOCK.DGRAM, 0) catch |err| { return switch (err) { std.posix.SocketError.PermissionDenied => ScanError.SocketPermissionDenied, std.posix.SocketError.SystemResources => ScanError.OutOfMemory, else => ScanError.SocketError, }; }; errdefer std.posix.close(sockfd); std.posix.setsockopt( sockfd, std.posix.SOL.SOCKET, std.posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)), ) catch |err| return switch (err) { std.posix.SetSockOptError.PermissionDenied => ScanError.SocketPermissionDenied, std.posix.SetSockOptError.SystemResources => ScanError.OutOfMemory, else => ScanError.SocketError, }; const sec = comptime std.math.divFloor(u32, udp_receive_window_ms, 1_000) catch |err| { @compileError( std.fmt.comptimePrint( "Cannot divide udp_receive_window_ms ({d}) by 1,000: {s}", .{ udp_receive_window_ms, @errorName(err) }, ), ); }; const usec = comptime usec: { break :usec @min( std.math.maxInt(i32), 1_000 * (std.math.rem(u32, udp_receive_window_ms, 1_000) catch |err| { @compileError( std.fmt.comptimePrint( "Cannot get reminder of udp_receive_window_ms ({d}): {s}", .{ udp_receive_window_ms, @errorName(err) }, ), ); }), ); }; std.log.debug("Setting UDP read timeout to {d}ms ({d}sec, {d}usec)", .{ udp_receive_window_ms, sec, usec, }); const timeout = std.posix.timeval{ .sec = sec, .usec = usec }; std.posix.setsockopt( sockfd, std.posix.SOL.SOCKET, std.posix.SO.RCVTIMEO, &std.mem.toBytes(timeout), ) catch |err| return switch (err) { std.posix.SetSockOptError.PermissionDenied => ScanError.SocketPermissionDenied, std.posix.SetSockOptError.SystemResources => ScanError.OutOfMemory, else => ScanError.SocketError, }; return sockfd; } fn sendDiscoveryQuery(sockfd: std.posix.socket_t) !void { std.log.debug("Sending server discovery message to {}", .{udp_dst}); std.posix.setsockopt( sockfd, std.posix.SOL.SOCKET, std.posix.SO.BROADCAST, &std.mem.toBytes(@as(c_int, 0)), ) catch |err| return switch (err) { std.posix.SetSockOptError.PermissionDenied => ScanError.SocketPermissionDenied, std.posix.SetSockOptError.SystemResources => ScanError.OutOfMemory, else => ScanError.SocketError, }; _ = std.posix.sendto( sockfd, sood.discovery.Query.prebuilt, 0, &udp_dst.any, udp_dst.getOsSockLen(), ) catch |err| { std.log.err("Failed to send discovery message: {s}", .{@errorName(err)}); return switch (err) { std.posix.SendToError.NetworkSubsystemFailed, std.posix.SendToError.NetworkUnreachable, => ScanError.NetworkUnavailable, else => ScanError.SocketError, }; }; } fn broadcastDiscoveryQuery(sockfd: std.posix.socket_t) !void { std.log.debug("Broadcasting server discovery message", .{}); std.posix.setsockopt( sockfd, std.posix.SOL.SOCKET, std.posix.SO.BROADCAST, &std.mem.toBytes(@as(c_int, 1)), ) catch |err| return switch (err) { std.posix.SetSockOptError.PermissionDenied => { std.log.warn("No permission to broadcast UDP message, skipping", .{}); return; }, std.posix.SetSockOptError.SystemResources => ScanError.OutOfMemory, else => ScanError.SocketError, }; _ = std.posix.sendto( sockfd, sood.discovery.Query.prebuilt, 0, &udp_dst.any, udp_dst.getOsSockLen(), ) catch |err| { std.log.err("Failed to broadcast discovery message: {s}", .{@errorName(err)}); return switch (err) { std.posix.SendToError.NetworkSubsystemFailed, std.posix.SendToError.NetworkUnreachable, => ScanError.NetworkUnavailable, else => ScanError.SocketError, }; }; } pub fn export_capi() void { Server.export_capi(); ScanResult.export_capi(); @export(&scan, .{ .name = "plac_discovery_scan" }); @export(&find, .{ .name = "plac_discovery_find" }); @export(&resolve, .{ .name = "plac_discovery_resolve" }); }
-
-
-
@@ -40,6 +40,13 @@ uint16_t http_port;} plac_discovery_server; plac_discovery_server *plac_discovery_server_retain(plac_discovery_server*); void plac_discovery_server_release(plac_discovery_server*); plac_discovery_server *plac_discovery_server_new( const char *id, const char *name, const char *version, const char *ip_addr, uint16_t http_port ); // discovery.ScanResult.Code typedef enum {
-
@@ -62,8 +69,6 @@ plac_discovery_scan_result *plac_discovery_scan_result_retain(plac_discovery_scan_result*);void plac_discovery_scan_result_release(plac_discovery_scan_result*); // discovery plac_discovery_scan_result *plac_discovery_scan(); plac_discovery_scan_result *plac_discovery_find(const char *server_id); plac_discovery_scan_result *plac_discovery_resolve( const char *server_id, const char *ip_addr,
-
-
-
@@ -30,6 +30,8 @@[CCode (cname = "plac_discovery_server_release")] public void unref (); public Server(string id, string name, string version, string ip_addr, uint16 http_port); public string id; public string name; public string version;
-
@@ -73,11 +75,6 @@ public Server[] servers;public ScanResultCode code; } [CCode (cname = "plac_discovery_scan")] public ScanResult? scan(); [CCode (cname = "plac_discovery_find")] public ScanResult? find(string server_id); [CCode (cname = "plac_discovery_resolve")] public ScanResult? resolve(string server_id, string ip_addr, uint16 http_port);
-
-
src/libsood.vapi (new)
-
@@ -0,0 +1,84 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 [CCode (cheader_filename = "sood.h")] namespace Sood { [CCode ( cname = "sood_result", cprefix = "SOOD_", has_type_id = false )] public enum Result { OK = 0, ITERATOR_DONE = 1, ERR_PARSE_HEADER_SIZE_MISMATCH = 2, ERR_PARSE_INVALID_SIGNATURE = 3, ERR_PARSE_EMPTY_KEY = 4, ERR_PARSE_KEY_SIZE_MISMATCH = 5, ERR_PARSE_NON_UTF8_KEY = 6, ERR_PARSE_VALUE_SIZE_CORRUPTED = 7, ERR_PARSE_VALUE_SIZE_MISMATCH = 8, ERR_PARSE_NON_UTF8_VALUE = 9, ERR_VALIDATE_MISSING_REQUIRED_PROPERTY = 10, ERR_VALIDATE_UNEXPECTED_VALUE_TYPE = 11, ERR_VALIDATE_UNEXPECTED_MESSAGE_KIND = 12, } [CCode (cname = "sood_discovery_response", has_type_id = false)] public struct DiscoveryResponse { public uint16 http_port; [CCode ( cname = "name_ptr", array_length_cname = "name_len", array_length_type = "size_t", array_null_terminated = false )] public unowned string name; public size_t name_len; [CCode ( cname = "display_version_ptr", array_length_cname = "display_version_len", array_length_type = "size_t", array_null_terminated = false )] public unowned string display_version; public size_t display_version_len; [CCode ( cname = "unique_id_ptr", array_length_cname = "unique_id_len", array_length_type = "size_t", array_null_terminated = false )] public unowned string unique_id; public size_t unique_id_len; [CCode (array_length_type = "size_t")] public static Result parse(out DiscoveryResponse dst, char[] message); } [CCode (cname = "SOOD_DISCOVERY_QUERY_PREBUILT")] public const char[] DISCOVERY_QUERY_PREBUILT; [CCode (cname = "SOOD_DISCOVERY_MULTICAST_IPV4_ADDRESS")] public const uint8[] DISCOVERY_MULTICAST_IPV4_ADDRESS; [CCode (cname = "SOOD_DISCOVERY_SERVER_UDP_PORT")] public const uint16 DISCOVERY_SERVER_UDP_PORT; }
-