Changes
6 changed files (+557/-507)
-
-
@@ -67,7 +67,7 @@ clap.help(stderr, clap.Help, ¶ms, .{}) catch {};return ExitCode.incorrect_usage; }; const scanner = core.ServerScanner.make() orelse { const scanner = core.server.ServerScanner.make() orelse { // TODO: Print as error log stderr.print("Unable to create a scanner: out of memory\n", .{}) catch {}; // TODO: Create dedicated exit code.
-
-
-
@@ -67,12 +67,12 @@const stdout = std.io.getStdOut().writer(); const stderr = std.io.getStdErr().writer(); var opts: core.ScanOptions = undefined; var opts: core.server.ScanOptions = undefined; opts.init(); opts.count = count; opts.receive_window_ms = wait * 1_000; const maybe_scanner = core.ServerScanner.make(); const maybe_scanner = core.server.ServerScanner.make(); const scanner = maybe_scanner orelse { // TODO: Print as error log stderr.print("Unable to create a scanner: out of memory\n", .{}) catch {};
-
-
-
@@ -16,514 +16,22 @@ // SPDX-License-Identifier: Apache-2.0//! This is an entrypoint of the static library. const std = @import("std"); const sood = @import("sood"); const allocator = std.heap.c_allocator; comptime { @export(&Server.dupe, .{ .name = "plac_server_dupe" }); @export(&Server.free, .{ .name = "plac_server_free" }); } pub const Server = extern struct { /// Network address of the server. sockaddr: std.posix.sockaddr, addrlen: std.posix.socklen_t, /// Unique ID of the server. It won't change even on server reboot. id: [*:0]const u8, id_len: usize, /// Display name of the server. Users who can configure the server can change the name. name: [*:0]const u8, name_len: usize, /// Free-format version string. version: [*:0]const u8, version_len: usize, /// Returns `null` on OOM. pub fn dupe(ptr: ?*const Server) callconv(.C) ?*Server { if (ptr) |server| { var new = allocator.create(Server) catch return null; new.id = allocator.dupeZ(u8, server.id[0..server.id_len]) catch return null; new.id_len = server.id_len; new.name = allocator.dupeZ(u8, server.name[0..server.name_len]) catch return null; new.name_len = server.name_len; new.version = allocator.dupeZ(u8, server.version[0..server.version_len]) catch return null; new.version_len = server.version_len; new.sockaddr = server.sockaddr; new.addrlen = server.addrlen; return new; } else { return null; } } pub fn free(ptr: ?*Server) callconv(.C) void { if (ptr) |server| { allocator.free(server.id[0..server.id_len]); allocator.free(server.name[0..server.name_len]); allocator.free(server.version[0..server.version_len]); allocator.destroy(server); } } pub fn fromSoodResponse(addr: std.net.Address, response: *const sood.discovery.Response) std.mem.Allocator.Error!*Server { var ip_addr = addr; ip_addr.setPort(response.http_port); const unique_id = try allocator.dupeZ(u8, response.unique_id); errdefer allocator.free(unique_id); const name = try allocator.dupeZ(u8, response.name); errdefer allocator.free(name); const version = try allocator.dupeZ(u8, response.display_version); errdefer allocator.free(version); const entry = try allocator.create(Server); errdefer allocator.destroy(entry); entry.* = .{ .sockaddr = ip_addr.any, .addrlen = ip_addr.getOsSockLen(), .id = unique_id.ptr, .id_len = unique_id.len, .name = name.ptr, .name_len = name.len, .version = version.ptr, .version_len = version.len, }; return entry; } pub fn getAddr(self: *const Server) std.net.Address { return std.net.Address.initPosix(&self.sockaddr); } pub fn getId(self: *const Server) []const u8 { return self.id[0..self.id_len]; } pub fn getName(self: *const Server) []const u8 { return self.name[0..self.name_len]; } pub fn getVersion(self: *const Server) []const u8 { return self.version[0..self.version_len]; } pub fn jsonStringify(self: *const Server, jws: anytype) !void { try jws.beginObject(); try jws.objectField("unique_id"); try jws.write(self.getId()); try jws.objectField("name"); try jws.write(self.getName()); try jws.objectField("version"); try jws.write(self.getVersion()); try jws.objectField("address"); try jws.print("\"{}\"", .{self.getAddr()}); try jws.endObject(); } }; comptime { @export(&ScanOptions.init, .{ .name = "plac_server_scan_options_init" }); } pub const ScanOptions = extern struct { /// How many times a scanner sends request UDP message? count: c_uint, /// How long will a scanner wait for response UDP message? receive_window_ms: c_uint, /// Set default option values. pub fn init(ptr: ?*ScanOptions) callconv(.C) void { if (ptr) |opts| { opts.count = 1; opts.receive_window_ms = 1_000; } } }; pub const ScanResultCode = enum(c_int) { ok = 0, unknown_error = 1, out_of_memory = 2, socket_setup_error = 3, udp_send_error = 4, udp_recv_error = 5, null_pointer_args = 6, socket_permission_error = 7, too_many_socket_error = 8, invalid_receive_window = 9, network_unavailable = 10, pub fn fromScanError(err: ServerScanner.ScanError) ScanResultCode { return switch (err) { ServerScanner.ScanError.Unknown => .unknown_error, ServerScanner.ScanError.OutOfMemory => .out_of_memory, ServerScanner.ScanError.SocketSetupError => .socket_setup_error, ServerScanner.ScanError.UDPSendError => .udp_send_error, ServerScanner.ScanError.UDPRecvError => .udp_recv_error, ServerScanner.ScanError.SocketPermissionDenied => .socket_permission_error, ServerScanner.ScanError.TooManySocketError => .too_many_socket_error, ServerScanner.ScanError.InvalidReceiveWindow => .invalid_receive_window, ServerScanner.ScanError.NetworkUnavailable => .network_unavailable, }; } }; pub const server = @import("./server.zig"); comptime { @export(&ScanResult.next, .{ .name = "plac_scan_result_next" }); @export(&ScanResult.reset, .{ .name = "plac_scan_result_reset" }); @export(&ScanResult.free, .{ .name = "plac_scan_result_free" }); } @export(&server.Server.dupe, .{ .name = "plac_server_dupe" }); @export(&server.Server.free, .{ .name = "plac_server_free" }); pub const ScanResult = extern struct { code: ScanResultCode, servers: ?*const []*Server = null, i: usize = 0, @export(&server.ScanOptions.init, .{ .name = "plac_server_scan_options_init" }); /// Returns `null` when the first argument is `null`, `code` field is /// not "ok", or iterator reaches to the end. /// Caller owns the returned `Server`. /// Call `.free()` after use. pub fn next(self_ptr: ?*ScanResult) callconv(.C) ?*Server { const self = self_ptr orelse return null; const servers = self.servers orelse return null; @export(&server.ScanResult.next, .{ .name = "plac_scan_result_next" }); @export(&server.ScanResult.reset, .{ .name = "plac_scan_result_reset" }); @export(&server.ScanResult.free, .{ .name = "plac_scan_result_free" }); if (self.i >= servers.len) { return null; } @export(&server.FindResult.free, .{ .name = "plac_find_result_free" }); const found = servers.*[self.i]; self.i += 1; return found.dupe(); } /// Reset internal iterator. pub fn reset(self_ptr: ?*ScanResult) callconv(.C) void { if (self_ptr) |self| { self.i = 0; } } /// Call to this function does NOT invalidate "Servers" previously returned by `next()`. pub fn free(self_ptr: ?*ScanResult) callconv(.C) void { if (self_ptr) |self| { if (self.servers) |servers| { for (servers.*) |server| { server.free(); } allocator.free(servers.*); allocator.destroy(servers); } allocator.destroy(self); } } }; comptime { @export(&FindResult.free, .{ .name = "plac_find_result_free" }); } pub const FindResult = extern struct { code: ScanResultCode, server: ?*Server = null, pub fn free(self_ptr: ?*FindResult) callconv(.C) void { if (self_ptr) |self| { if (self.server) |server| { server.free(); } allocator.destroy(self); } } }; comptime { @export(&ServerScanner.make, .{ .name = "plac_server_scanner_make" }); @export(&ServerScanner.free, .{ .name = "plac_server_scanner_free" }); @export(&ServerScanner.scan, .{ .name = "plac_server_scanner_scan" }); @export(&ServerScanner.find, .{ .name = "plac_server_scanner_find" }); @export(&server.ServerScanner.make, .{ .name = "plac_server_scanner_make" }); @export(&server.ServerScanner.free, .{ .name = "plac_server_scanner_free" }); @export(&server.ServerScanner.scan, .{ .name = "plac_server_scanner_scan" }); @export(&server.ServerScanner.find, .{ .name = "plac_server_scanner_find" }); } pub const ServerScanner = extern struct { sockfd: ?*std.posix.socket_t = null, /// Returns `null` on OOM. pub fn make() callconv(.C) ?*ServerScanner { const scanner = allocator.create(ServerScanner) catch return null; scanner.* = .{}; return scanner; } pub fn free(self_ptr: ?*ServerScanner) callconv(.C) void { ServerScanner.close(self_ptr); if (self_ptr) |self| { allocator.destroy(self); } } const udp_dst = std.net.Address.initIp4( sood.discovery.multicast_ipv4_address, sood.discovery.udp_port, ); const ScanError = error{ Unknown, SocketSetupError, UDPSendError, UDPRecvError, SocketPermissionDenied, TooManySocketError, InvalidReceiveWindow, NetworkUnavailable, } || std.mem.Allocator.Error; fn setupSocket(self: *ServerScanner) ScanError!*std.posix.socket_t { if (self.sockfd) |sockfd| { return sockfd; } const fd = try allocator.create(std.posix.socket_t); errdefer allocator.destroy(fd); fd.* = 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.SystemFdQuotaExceeded, std.posix.SocketError.ProcessFdQuotaExceeded => ScanError.TooManySocketError, std.posix.SocketError.SystemResources => ScanError.OutOfMemory, else => ScanError.SocketSetupError, }; }; errdefer std.posix.close(fd.*); std.posix.setsockopt( fd.*, 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.SocketSetupError, }; self.sockfd = fd; return fd; } inline fn setSocketReceiveTimeout(sockfd: *std.posix.socket_t, timeout_ms: u32) ScanError!void { const sec = std.math.divFloor(u32, timeout_ms, 1_000) catch { return ScanError.InvalidReceiveWindow; }; const usec = @min(std.math.maxInt(i32), 1_000 * (std.math.rem(u32, timeout_ms, 1_000) catch { return ScanError.InvalidReceiveWindow; })); 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.SocketSetupError, }; } inline fn sendDiscoveryQuery(sockfd: *std.posix.socket_t) ScanError!void { _ = std.posix.sendto( sockfd.*, sood.discovery.Query.prebuilt, 0, &udp_dst.any, udp_dst.getOsSockLen(), ) catch |err| return switch (err) { std.posix.SendToError.NetworkSubsystemFailed, std.posix.SendToError.NetworkUnreachable => ScanError.NetworkUnavailable, else => ScanError.UDPSendError, }; } /// Returns `null` on OOM or when an argument is a null pointer. pub fn scan( self_ptr: ?*ServerScanner, opts_ptr: ?*const ScanOptions, ) callconv(.C) ?*ScanResult { const self = self_ptr orelse return null; const result = allocator.create(ScanResult) catch return null; const opts = opts_ptr orelse { result.* = .{ .code = .null_pointer_args, }; return result; }; self.scan_internal(opts, result) catch |err| { result.* = .{ .code = ScanResultCode.fromScanError(err), }; }; return result; } fn scan_internal(self: *ServerScanner, opts: *const ScanOptions, result: *ScanResult) ScanError!void { const sockfd = try self.setupSocket(); try setSocketReceiveTimeout(sockfd, opts.receive_window_ms); var servers = std.StringHashMap(*Server).init(allocator); defer servers.deinit(); for (0..opts.count) |_| { try sendDiscoveryQuery(sockfd); while (true) { // 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 => break, std.posix.RecvFromError.MessageTooBig => continue, else => return ScanError.UDPRecvError, }; const response = sood.discovery.Response.parse(received[0..size]) catch { // Non-SOOD message. Unlikely but technically possible. continue; }; const stale = servers.get(response.unique_id); defer if (stale) |server| { server.free(); }; try servers.put(response.unique_id, try Server.fromSoodResponse(src, &response)); } } const servers_slice = try allocator.create([]*Server); errdefer allocator.destroy(servers_slice); servers_slice.* = try allocator.alloc(*Server, servers.count()); errdefer allocator.free(servers_slice.*); var i: usize = 0; var iter = servers.valueIterator(); while (iter.next()) |server| { servers_slice.*[i] = server.*; i += 1; } result.* = .{ .code = .ok, .servers = servers_slice, }; } /// Returns `null` on OOM or when an argument is a null pointer. pub fn find(self_ptr: ?*ServerScanner, unique_id: [*:0]const u8, unique_id_len: usize) callconv(.C) ?*FindResult { const self = self_ptr orelse return null; const unique_id_slice = unique_id[0..unique_id_len]; const result = allocator.create(FindResult) catch return null; self.findInternal(unique_id_slice, result) catch |err| { result.* = .{ .code = ScanResultCode.fromScanError(err), }; }; return result; } fn findInternal(self: *ServerScanner, unique_id: []const u8, result: *FindResult) ScanError!void { const sockfd = try self.setupSocket(); try setSocketReceiveTimeout(sockfd, 1_000); // Roon Server throttles discovery response to 3~5 secs. for (0..10) |_| { try sendDiscoveryQuery(sockfd); while (true) { // 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 => break, std.posix.RecvFromError.MessageTooBig => continue, else => return ScanError.UDPRecvError, }; const response = sood.discovery.Response.parse(received[0..size]) catch { // Non-SOOD message. Unlikely but technically possible. continue; }; if (!std.mem.eql(u8, response.unique_id, unique_id)) { continue; } result.* = .{ .code = .ok, .server = try Server.fromSoodResponse(src, &response), }; return; } } result.* = .{ .code = .ok, }; } pub fn close(self_ptr: ?*ServerScanner) callconv(.C) void { if (self_ptr) |self| { if (self.sockfd) |sockfd| { std.posix.close(sockfd.*); allocator.destroy(sockfd); } } } };
-
-
core/src/server.zig (new)
-
@@ -0,0 +1,18 @@// 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 pub usingnamespace @import("./server/Server.zig"); pub usingnamespace @import("./server/scanner.zig");
-
-
-
@@ -0,0 +1,130 @@// 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 const std = @import("std"); const sood = @import("sood"); const allocator = std.heap.c_allocator; pub const Server = extern struct { /// Network address of the server. sockaddr: std.posix.sockaddr, addrlen: std.posix.socklen_t, /// Unique ID of the server. It won't change even on server reboot. id: [*:0]const u8, id_len: usize, /// Display name of the server. Users who can configure the server can change the name. name: [*:0]const u8, name_len: usize, /// Free-format version string. version: [*:0]const u8, version_len: usize, /// Returns `null` on OOM. pub fn dupe(ptr: ?*const Server) callconv(.C) ?*Server { if (ptr) |server| { var new = allocator.create(Server) catch return null; new.id = allocator.dupeZ(u8, server.id[0..server.id_len]) catch return null; new.id_len = server.id_len; new.name = allocator.dupeZ(u8, server.name[0..server.name_len]) catch return null; new.name_len = server.name_len; new.version = allocator.dupeZ(u8, server.version[0..server.version_len]) catch return null; new.version_len = server.version_len; new.sockaddr = server.sockaddr; new.addrlen = server.addrlen; return new; } else { return null; } } pub fn free(ptr: ?*Server) callconv(.C) void { if (ptr) |server| { allocator.free(server.id[0..server.id_len]); allocator.free(server.name[0..server.name_len]); allocator.free(server.version[0..server.version_len]); allocator.destroy(server); } } pub fn fromSoodResponse(addr: std.net.Address, response: *const sood.discovery.Response) std.mem.Allocator.Error!*Server { var ip_addr = addr; ip_addr.setPort(response.http_port); const unique_id = try allocator.dupeZ(u8, response.unique_id); errdefer allocator.free(unique_id); const name = try allocator.dupeZ(u8, response.name); errdefer allocator.free(name); const version = try allocator.dupeZ(u8, response.display_version); errdefer allocator.free(version); const entry = try allocator.create(Server); errdefer allocator.destroy(entry); entry.* = .{ .sockaddr = ip_addr.any, .addrlen = ip_addr.getOsSockLen(), .id = unique_id.ptr, .id_len = unique_id.len, .name = name.ptr, .name_len = name.len, .version = version.ptr, .version_len = version.len, }; return entry; } pub fn getAddr(self: *const Server) std.net.Address { return std.net.Address.initPosix(&self.sockaddr); } pub fn getId(self: *const Server) []const u8 { return self.id[0..self.id_len]; } pub fn getName(self: *const Server) []const u8 { return self.name[0..self.name_len]; } pub fn getVersion(self: *const Server) []const u8 { return self.version[0..self.version_len]; } pub fn jsonStringify(self: *const Server, jws: anytype) !void { try jws.beginObject(); try jws.objectField("unique_id"); try jws.write(self.getId()); try jws.objectField("name"); try jws.write(self.getName()); try jws.objectField("version"); try jws.write(self.getVersion()); try jws.objectField("address"); try jws.print("\"{}\"", .{self.getAddr()}); try jws.endObject(); } };
-
-
-
@@ -0,0 +1,394 @@// 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 const std = @import("std"); const sood = @import("sood"); const Server = @import("./Server.zig").Server; const allocator = std.heap.c_allocator; pub const ScanOptions = extern struct { /// How many times a scanner sends request UDP message? count: c_uint, /// How long will a scanner wait for response UDP message? receive_window_ms: c_uint, /// Set default option values. pub fn init(ptr: ?*ScanOptions) callconv(.C) void { if (ptr) |opts| { opts.count = 1; opts.receive_window_ms = 1_000; } } }; pub const ScanResultCode = enum(c_int) { ok = 0, unknown_error = 1, out_of_memory = 2, socket_setup_error = 3, udp_send_error = 4, udp_recv_error = 5, null_pointer_args = 6, socket_permission_error = 7, too_many_socket_error = 8, invalid_receive_window = 9, network_unavailable = 10, pub fn fromScanError(err: ServerScanner.ScanError) ScanResultCode { return switch (err) { ServerScanner.ScanError.Unknown => .unknown_error, ServerScanner.ScanError.OutOfMemory => .out_of_memory, ServerScanner.ScanError.SocketSetupError => .socket_setup_error, ServerScanner.ScanError.UDPSendError => .udp_send_error, ServerScanner.ScanError.UDPRecvError => .udp_recv_error, ServerScanner.ScanError.SocketPermissionDenied => .socket_permission_error, ServerScanner.ScanError.TooManySocketError => .too_many_socket_error, ServerScanner.ScanError.InvalidReceiveWindow => .invalid_receive_window, ServerScanner.ScanError.NetworkUnavailable => .network_unavailable, }; } }; pub const ScanResult = extern struct { code: ScanResultCode, servers: ?*const []*Server = null, i: usize = 0, /// Returns `null` when the first argument is `null`, `code` field is /// not "ok", or iterator reaches to the end. /// Caller owns the returned `Server`. /// Call `.free()` after use. pub fn next(self_ptr: ?*ScanResult) callconv(.C) ?*Server { const self = self_ptr orelse return null; const servers = self.servers orelse return null; if (self.i >= servers.len) { return null; } const found = servers.*[self.i]; self.i += 1; return found.dupe(); } /// Reset internal iterator. pub fn reset(self_ptr: ?*ScanResult) callconv(.C) void { if (self_ptr) |self| { self.i = 0; } } /// Call to this function does NOT invalidate "Servers" previously returned by `next()`. pub fn free(self_ptr: ?*ScanResult) callconv(.C) void { if (self_ptr) |self| { if (self.servers) |servers| { for (servers.*) |server| { server.free(); } allocator.free(servers.*); allocator.destroy(servers); } allocator.destroy(self); } } }; pub const FindResult = extern struct { code: ScanResultCode, server: ?*Server = null, pub fn free(self_ptr: ?*FindResult) callconv(.C) void { if (self_ptr) |self| { if (self.server) |server| { server.free(); } allocator.destroy(self); } } }; pub const ServerScanner = extern struct { sockfd: ?*std.posix.socket_t = null, /// Returns `null` on OOM. pub fn make() callconv(.C) ?*ServerScanner { const scanner = allocator.create(ServerScanner) catch return null; scanner.* = .{}; return scanner; } pub fn free(self_ptr: ?*ServerScanner) callconv(.C) void { ServerScanner.close(self_ptr); if (self_ptr) |self| { allocator.destroy(self); } } const udp_dst = std.net.Address.initIp4( sood.discovery.multicast_ipv4_address, sood.discovery.udp_port, ); const ScanError = error{ Unknown, SocketSetupError, UDPSendError, UDPRecvError, SocketPermissionDenied, TooManySocketError, InvalidReceiveWindow, NetworkUnavailable, } || std.mem.Allocator.Error; fn setupSocket(self: *ServerScanner) ScanError!*std.posix.socket_t { if (self.sockfd) |sockfd| { return sockfd; } const fd = try allocator.create(std.posix.socket_t); errdefer allocator.destroy(fd); fd.* = 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.SystemFdQuotaExceeded, std.posix.SocketError.ProcessFdQuotaExceeded => ScanError.TooManySocketError, std.posix.SocketError.SystemResources => ScanError.OutOfMemory, else => ScanError.SocketSetupError, }; }; errdefer std.posix.close(fd.*); std.posix.setsockopt( fd.*, 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.SocketSetupError, }; self.sockfd = fd; return fd; } inline fn setSocketReceiveTimeout(sockfd: *std.posix.socket_t, timeout_ms: u32) ScanError!void { const sec = std.math.divFloor(u32, timeout_ms, 1_000) catch { return ScanError.InvalidReceiveWindow; }; const usec = @min(std.math.maxInt(i32), 1_000 * (std.math.rem(u32, timeout_ms, 1_000) catch { return ScanError.InvalidReceiveWindow; })); 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.SocketSetupError, }; } inline fn sendDiscoveryQuery(sockfd: *std.posix.socket_t) ScanError!void { _ = std.posix.sendto( sockfd.*, sood.discovery.Query.prebuilt, 0, &udp_dst.any, udp_dst.getOsSockLen(), ) catch |err| return switch (err) { std.posix.SendToError.NetworkSubsystemFailed, std.posix.SendToError.NetworkUnreachable => ScanError.NetworkUnavailable, else => ScanError.UDPSendError, }; } /// Returns `null` on OOM or when an argument is a null pointer. pub fn scan( self_ptr: ?*ServerScanner, opts_ptr: ?*const ScanOptions, ) callconv(.C) ?*ScanResult { const self = self_ptr orelse return null; const result = allocator.create(ScanResult) catch return null; const opts = opts_ptr orelse { result.* = .{ .code = .null_pointer_args, }; return result; }; self.scan_internal(opts, result) catch |err| { result.* = .{ .code = ScanResultCode.fromScanError(err), }; }; return result; } fn scan_internal(self: *ServerScanner, opts: *const ScanOptions, result: *ScanResult) ScanError!void { const sockfd = try self.setupSocket(); try setSocketReceiveTimeout(sockfd, opts.receive_window_ms); var servers = std.StringHashMap(*Server).init(allocator); defer servers.deinit(); for (0..opts.count) |_| { try sendDiscoveryQuery(sockfd); while (true) { // 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 => break, std.posix.RecvFromError.MessageTooBig => continue, else => return ScanError.UDPRecvError, }; const response = sood.discovery.Response.parse(received[0..size]) catch { // Non-SOOD message. Unlikely but technically possible. continue; }; const stale = servers.get(response.unique_id); defer if (stale) |server| { server.free(); }; try servers.put(response.unique_id, try Server.fromSoodResponse(src, &response)); } } const servers_slice = try allocator.create([]*Server); errdefer allocator.destroy(servers_slice); servers_slice.* = try allocator.alloc(*Server, servers.count()); errdefer allocator.free(servers_slice.*); var i: usize = 0; var iter = servers.valueIterator(); while (iter.next()) |server| { servers_slice.*[i] = server.*; i += 1; } result.* = .{ .code = .ok, .servers = servers_slice, }; } /// Returns `null` on OOM or when an argument is a null pointer. pub fn find(self_ptr: ?*ServerScanner, unique_id: [*:0]const u8, unique_id_len: usize) callconv(.C) ?*FindResult { const self = self_ptr orelse return null; const unique_id_slice = unique_id[0..unique_id_len]; const result = allocator.create(FindResult) catch return null; self.findInternal(unique_id_slice, result) catch |err| { result.* = .{ .code = ScanResultCode.fromScanError(err), }; }; return result; } fn findInternal(self: *ServerScanner, unique_id: []const u8, result: *FindResult) ScanError!void { const sockfd = try self.setupSocket(); try setSocketReceiveTimeout(sockfd, 1_000); // Roon Server throttles discovery response to 3~5 secs. for (0..10) |_| { try sendDiscoveryQuery(sockfd); while (true) { // 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 => break, std.posix.RecvFromError.MessageTooBig => continue, else => return ScanError.UDPRecvError, }; const response = sood.discovery.Response.parse(received[0..size]) catch { // Non-SOOD message. Unlikely but technically possible. continue; }; if (!std.mem.eql(u8, response.unique_id, unique_id)) { continue; } result.* = .{ .code = .ok, .server = try Server.fromSoodResponse(src, &response), }; return; } } result.* = .{ .code = .ok, }; } pub fn close(self_ptr: ?*ServerScanner) callconv(.C) void { if (self_ptr) |self| { if (self.sockfd) |sockfd| { std.posix.close(sockfd.*); allocator.destroy(sockfd); } } } };
-