Changes
5 changed files (+362/-95)
-
-
@@ -18,11 +18,13 @@ const std = @import("std");const clap = @import("clap"); const ExitCode = @import("../exit.zig").ExitCode; const find = @import("./server/find.zig"); const list = @import("./server/list.zig"); const Commands = enum { list, ls, find, }; const parser = .{
-
@@ -34,6 +36,7 @@ \\-h, --help Prints this message to stdout and exits.\\<command> \\Available commands: \\* list ... List available Roon Server on network (alias: ls) \\* find ... Find a Roon Server matches to given ID \\ );
-
@@ -61,5 +64,6 @@ };return switch (command) { .ls, .list => list.run(allocator, iter), .find => find.run(allocator, iter), }; }
-
-
-
@@ -0,0 +1,134 @@// 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 clap = @import("clap"); const core = @import("core"); const ExitCode = @import("../../exit.zig").ExitCode; const OutputFormat = enum { address, text, json, }; const parser = .{ .format = clap.parsers.enumeration(OutputFormat), .server_id = clap.parsers.string, }; const params = clap.parseParamsComptime( \\-h, --help Prints this message to stdout and exits. \\-f, --format <format> Output format. \\Available values are: \\* address ... Only display an IP address and port number. \\* text ... Plain text format for human consumption. \\* json ... A JSON object. \\<server_id> \\ ); pub fn run(allocator: std.mem.Allocator, iter: *std.process.ArgIterator) ExitCode { var diag = clap.Diagnostic{}; var res = clap.parseEx(clap.Help, ¶ms, parser, iter, .{ .diagnostic = &diag, .allocator = allocator, }) catch |err| { diag.report(std.io.getStdErr().writer(), err) catch {}; return ExitCode.incorrect_usage; }; defer res.deinit(); const stdout = std.io.getStdOut().writer(); const stderr = std.io.getStdErr().writer(); if (res.args.help > 0) { clap.help(stdout, clap.Help, ¶ms, .{}) catch {}; return ExitCode.ok; } const server_id = res.positionals[0] orelse { stderr.print("server_id is required.\n", .{}) catch {}; clap.help(stderr, clap.Help, ¶ms, .{}) catch {}; return ExitCode.incorrect_usage; }; const scanner = core.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. return ExitCode.not_ok; }; defer scanner.free(); const server_id_cstr = allocator.dupeZ(u8, server_id) catch { // TODO: Print as error log stderr.print("Unable to find server: out of memory\n", .{}) catch {}; // TODO: Create dedicated exit code. return ExitCode.not_ok; }; defer allocator.free(server_id_cstr); var result = scanner.find(server_id_cstr.ptr, server_id_cstr.len) orelse { // TODO: Print as error log stderr.print("Unable to find server: out of memory\n", .{}) catch {}; // TODO: Create dedicated exit code. return ExitCode.not_ok; }; defer result.free(); if (result.code != .ok) { // TODO: Print as error log stderr.print("Failed to find server: {s}\n", .{@tagName(result.code)}) catch {}; return ExitCode.not_ok; } const found = result.server orelse { // TODO: Print as error log stderr.print("Server not found\n", .{}) catch {}; // TODO: Create dedicated exit code. return ExitCode.not_ok; }; switch (res.args.format orelse .address) { .address => { stdout.print("{}\n", .{found.getAddr()}) catch { return ExitCode.not_ok; }; }, .text => { stdout.print("ID={s} IP={} VERSION=\"{s}\"\n", .{ found.getId(), found.getAddr(), found.getVersion(), }) catch { // TODO: Create and return appropriate error code return ExitCode.not_ok; }; }, .json => { stdout.print("{}\n", .{ std.json.fmt(found, .{ .whitespace = .minified }), }) catch { // TODO: Create and return appropriate error code return ExitCode.not_ok; }; }, } return ExitCode.ok; }
-
-
-
@@ -77,6 +77,15 @@ plac_server *plac_scan_result_next(plac_scan_result*);void plac_scan_result_reset(plac_scan_result*); void plac_scan_result_free(plac_scan_result*); /* FindResult */ typedef struct { plac_scan_result_code const code; plac_server * const server; } plac_find_result; void plac_find_result_free(plac_find_result*); /* ServerScanner */ typedef struct {
-
@@ -86,5 +95,6 @@plac_server_scanner *plac_server_scanner_make(); void plac_server_scanner_free(plac_server_scanner*); plac_scan_result *plac_server_scanner_scan(plac_server_scanner*, plac_server_scan_options*); plac_find_result *plac_server_scanner_find(plac_server_scanner*, char * const, unsigned int); #endif
-
-
-
@@ -70,6 +70,14 @@ [CCode (cname = "plac_scan_result_reset")]public void reset (); } [CCode (cname = "plac_find_result", free_function = "plac_find_result_free")] [Compact] public class FindResult { public ScanResultCode code; public Server? server; } [CCode (cname = "plac_server_scanner", free_function = "plac_server_scanner_free")] [Compact] public class ServerScanner {
-
@@ -78,5 +86,8 @@ public ServerScanner ();[CCode (cname = "plac_server_scanner_scan")] public ScanResult? scan (ScanOptions opts); [CCode (cname = "plac_server_scanner_find")] public FindResult? find (string id, uint len); } }
-
-
-
@@ -71,6 +71,36 @@ 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); }
-
@@ -138,6 +168,20 @@ 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, }; } }; comptime {
-
@@ -193,9 +237,29 @@ }}; 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" }); } pub const ServerScanner = extern struct {
-
@@ -231,6 +295,74 @@ 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,
-
@@ -250,17 +382,7 @@ };self.scan_internal(opts, result) catch |err| { result.* = .{ .code = switch (err) { ScanError.Unknown => .unknown_error, ScanError.OutOfMemory => .out_of_memory, ScanError.SocketSetupError => .socket_setup_error, ScanError.UDPSendError => .udp_send_error, ScanError.UDPRecvError => .udp_recv_error, ScanError.SocketPermissionDenied => .socket_permission_error, ScanError.TooManySocketError => .too_many_socket_error, ScanError.InvalidReceiveWindow => .invalid_receive_window, ScanError.NetworkUnavailable => .network_unavailable, }, .code = ScanResultCode.fromScanError(err), }; };
-
@@ -268,69 +390,15 @@ return result;} fn scan_internal(self: *ServerScanner, opts: *const ScanOptions, result: *ScanResult) ScanError!void { const sockfd = self.sockfd orelse 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; const sockfd = try self.setupSocket(); break :sockfd fd; }; const sec = std.math.divFloor(u32, opts.receive_window_ms, 1_000) catch { return ScanError.InvalidReceiveWindow; }; const usec = @min(std.math.maxInt(i32), 1_000 * (std.math.rem(u32, opts.receive_window_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, }; try setSocketReceiveTimeout(sockfd, opts.receive_window_ms); var servers = std.StringHashMap(*Server).init(allocator); defer servers.deinit(); for (0..opts.count) |_| { _ = 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, }; try sendDiscoveryQuery(sockfd); while (true) { // Discovery response from servers usually fits under 300 bytes.
-
@@ -361,33 +429,7 @@ defer if (stale) |server| {server.free(); }; var ip_addr = src; 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, }; try servers.put(response.unique_id, entry); try servers.put(response.unique_id, try Server.fromSoodResponse(src, &response)); } }
-
@@ -407,6 +449,72 @@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, }; }
-