Changes
4 changed files (+55/-212)
-
-
@@ -64,32 +64,41 @@const count = res.args.count orelse 3; const wait = res.args.wait orelse 2; var scanner = core.ServerScanner.init(allocator) catch { // TODO: Create and return appropriate error code const stdout = std.io.getStdOut().writer(); const stderr = std.io.getStdErr().writer(); var opts: core.ScanOptions = undefined; opts.init(); opts.count = count; opts.receive_window_ms = wait * 1_000; const maybe_scanner = core.ServerScanner.make(); const scanner = maybe_scanner orelse { // TODO: Print as error log stderr.print("Unable to create a scanner: out of memory\n", .{}) catch {}; return ExitCode.not_ok; }; defer scanner.deinit(); defer scanner.free(); const stdout = std.io.getStdOut().writer(); const stderr = std.io.getStdErr().writer(); var result = scanner.scan(&opts) orelse { // TODO: Print as error log stderr.print("Unable to scan: out of memory\n", .{}) catch {}; return ExitCode.not_ok; }; defer result.free(); for (0..count) |i| { // TODO: Hide this under verbose flag stderr.print("Sending discovery request ({d})...\n", .{i + 1}) catch {}; scanner.scan(.{ .receive_window_ms = wait * 1_000 }) catch { // TODO: Create and return appropriate error code return ExitCode.not_ok; }; if (result.code != .ok) { stderr.print("Failed to scan: {s}\n", .{@tagName(result.code)}) catch {}; return ExitCode.not_ok; } var server_iter = scanner.servers.iterator(); switch (res.args.format orelse .text) { .text => { while (server_iter.next()) |server| { while (result.next()) |server| { stdout.print("ID={s} IP={} VERSION=\"{s}\"\n", .{ server.value_ptr.unique_id, server.value_ptr.ip_addr, server.value_ptr.version, server.getId(), server.getAddr(), server.getVersion(), }) catch { // TODO: Create and return appropriate error code return ExitCode.not_ok;
-
@@ -102,13 +111,13 @@ stdout.writeAll("ID\tName\tIP address\tVersion\n") catch {return ExitCode.not_ok; }; } while (server_iter.next()) |server| { while (result.next()) |server| { // TODO: Escape tabs from name and version stdout.print("{s}\t{s}\t{}\t{s}\n", .{ server.value_ptr.unique_id, server.value_ptr.name, server.value_ptr.ip_addr, server.value_ptr.version, server.getId(), server.getName(), server.getAddr(), server.getVersion(), }) catch { // TODO: Create and return appropriate error code return ExitCode.not_ok;
-
@@ -116,9 +125,9 @@ };} }, .jsonl => { while (server_iter.next()) |server| { while (result.next()) |server| { stdout.print("{}\n", .{ std.json.fmt(server.value_ptr, .{ .whitespace = .minified }), std.json.fmt(server, .{ .whitespace = .minified }), }) catch { // TODO: Create and return appropriate error code return ExitCode.not_ok;
-
-
-
@@ -35,7 +35,7 @@// Zig module { const mod = b.addModule("core", .{ .root_source_file = b.path("src/mod.zig"), .root_source_file = b.path("src/lib.zig"), }); mod.addImport("sood", sood);
-
-
-
@@ -19,7 +19,6 @@const std = @import("std"); const sood = @import("sood"); const core = @import("./mod.zig"); const allocator = std.heap.c_allocator;
-
@@ -72,20 +71,36 @@ allocator.destroy(server);} } 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.id[0..self.id_len]); try jws.write(self.getId()); try jws.objectField("name"); try jws.write(self.name[0..self.name_len]); try jws.write(self.getName()); try jws.objectField("version"); try jws.write(self.version[0..self.version_len]); try jws.write(self.getVersion()); try jws.objectField("address"); try jws.print("\"{}\"", .{self.address}); try jws.print("\"{}\"", .{self.getAddr()}); try jws.endObject(); }
-
@@ -181,7 +196,7 @@ @export(&ServerScanner.free, .{ .name = "plac_server_scanner_free" });@export(&ServerScanner.scan, .{ .name = "plac_server_scanner_scan" }); } const ServerScanner = extern struct { pub const ServerScanner = extern struct { sockfd: ?*std.posix.socket_t = null, /// Returns `null` on OOM.
-
-
core/src/mod.zig (deleted)
-
@@ -1,181 +0,0 @@// 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 //! [Deprecated] Migrate to lib.h and let CLI test C API. //! This is an entrypoint of Zig module. const std = @import("std"); const sood = @import("sood"); /// Data required to connect to a Roon Server pub const Server = struct { ip_addr: std.net.Address, unique_id: []const u8, name: []const u8, version: []const u8, pub fn clone(self: Server, allocator: std.mem.Allocator) std.mem.Allocator.Error!Server { return Server{ .ip_addr = self.ip_addr, .unique_id = try allocator.dupe(u8, self.unique_id), .name = try allocator.dupe(u8, self.name), .version = try allocator.dupe(u8, self.version), }; } pub fn deinit(self: Server, allocator: std.mem.Allocator) void { allocator.free(self.unique_id); allocator.free(self.name); allocator.free(self.version); } pub fn jsonStringify(self: *const Server, jws: anytype) !void { try jws.beginObject(); try jws.objectField("unique_id"); try jws.write(self.unique_id); try jws.objectField("name"); try jws.write(self.name); try jws.objectField("version"); try jws.write(self.version); try jws.objectField("address"); try jws.print("\"{}\"", .{self.ip_addr}); try jws.endObject(); } }; pub const ServerScanner = struct { allocator: std.mem.Allocator, sockfd: std.posix.socket_t, servers: std.StringHashMap(Server), const dst = std.net.Address.initIp4( sood.discovery.multicast_ipv4_address, sood.discovery.udp_port, ); pub const InitError = std.mem.Allocator.Error || std.posix.SocketError || std.posix.SetSockOptError; /// Caller must call `deinit()` after use. pub fn init(allocator: std.mem.Allocator) InitError!ServerScanner { const sockfd = try std.posix.socket(std.posix.AF.INET, std.posix.SOCK.DGRAM, 0); errdefer std.posix.close(sockfd); try std.posix.setsockopt( sockfd, std.posix.SOL.SOCKET, std.posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)), ); const servers = std.StringHashMap(Server).init(allocator); return ServerScanner{ .allocator = allocator, .sockfd = sockfd, .servers = servers, }; } pub const ScanError = error{ IllegalReceiveWindow, } || std.mem.Allocator.Error || std.posix.SetSockOptError || std.posix.SendToError || std.posix.RecvFromError; pub const ScanOptions = struct { receive_window_ms: u32 = 1_000, }; pub fn scan(self: *ServerScanner, opts: ScanOptions) ScanError!void { const sec = std.math.divFloor(u32, opts.receive_window_ms, 1_000) catch { return ScanError.IllegalReceiveWindow; }; const usec = 1_000 * (std.math.rem(u32, opts.receive_window_ms, 1_000) catch { return ScanError.IllegalReceiveWindow; }); const timeout = std.posix.timeval{ .sec = sec, .usec = usec }; try std.posix.setsockopt( self.sockfd, std.posix.SOL.SOCKET, std.posix.SO.RCVTIMEO, &std.mem.toBytes(timeout), ); _ = try std.posix.sendto( self.sockfd, sood.discovery.Query.prebuilt, 0, &dst.any, dst.getOsSockLen(), ); // 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 = dst.getOsSockLen(); while (true) { if (std.posix.recvfrom(self.sockfd, &received, 0, &src.any, &src_len)) |received_size| { const response = sood.discovery.Response.parse(received[0..received_size]) catch { // Non-SOOD message. Unlikely but technically possible. continue; }; const stale = self.servers.get(response.unique_id); defer if (stale) |server| { server.deinit(self.allocator); }; var ip_addr = src; ip_addr.setPort(response.http_port); const new = Server{ .unique_id = response.unique_id, .version = response.display_version, .name = response.name, .ip_addr = ip_addr, }; // Server's fields point to `received` buffer, which invalidates every UDP receive. // We have to copy the slices. const entry = try new.clone(self.allocator); errdefer entry.deinit(self.allocator); try self.servers.put(response.unique_id, entry); } else |err| switch (err) { std.posix.RecvFromError.WouldBlock => return, std.posix.RecvFromError.MessageTooBig => continue, else => return err, } } } pub fn deinit(self: *ServerScanner) void { var iter = self.servers.iterator(); while (iter.next()) |server| { server.value_ptr.deinit(self.allocator); } self.servers.deinit(); std.posix.close(self.sockfd); self.* = undefined; } };
-