Changes
5 changed files (+210/-17)
-
-
@@ -22,20 +22,24 @@ const exit = @import("../../exit.zig").exit;const OutputFormat = enum { text, tsv, }; const parser = .{ .format = clap.parsers.enumeration(OutputFormat), .sec = clap.parsers.int(u32, 10), .uint = clap.parsers.int(u32, 10), }; const params = clap.parseParamsComptime( \\-h, --help Prints this message to stdout and exits. \\-c, --count <uint> How many discovery requests to send. \\-w, --wait <sec> Waits <sec> after sending a discovery request. \\--header Whether emits header row (only on --format=tsv) \\-f, --format <format> Output format. \\-r, --resend <sec> Interval for sending request. \\-t, --timeout <sec> Exits after <sec>. \\Available values are: \\* text ... Plain text format for human consumption. \\* tsv ... TSV (tab-separated values). \\ );
-
@@ -55,12 +59,63 @@ clap.help(std.io.getStdOut().writer(), clap.Help, ¶ms, .{}) catch {};exit(.ok); } const resend = res.args.resend orelse 1; const timeout = res.args.timeout orelse 3; 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 exit(.not_ok); }; defer scanner.deinit(); _ = resend; _ = timeout; const stdout = std.io.getStdOut().writer(); const stderr = std.io.getStdErr().writer(); 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 exit(.not_ok); }; } var server_iter = scanner.servers.iterator(); switch (res.args.format orelse .text) { .text => { while (server_iter.next()) |server| { stdout.print("ID={s} IP={} PORT={d} VERSION=\"{s}\"\n", .{ server.value_ptr.unique_id, server.value_ptr.ip_addr, server.value_ptr.http_port, server.value_ptr.version, }) catch { // TODO: Create and return appropriate error code exit(.not_ok); }; } }, .tsv => { if (res.args.header > 0) { stdout.writeAll("ID\tName\tIP address\tHTTP port\tVersion\n") catch { exit(.not_ok); }; } while (server_iter.next()) |server| { // TODO: Escape tabs from name and version stdout.print("{s}\t{s}\t{}\t{d}\t{s}\n", .{ server.value_ptr.unique_id, server.value_ptr.name, server.value_ptr.ip_addr, server.value_ptr.http_port, server.value_ptr.version, }) catch { // TODO: Create and return appropriate error code exit(.not_ok); }; } }, } std.debug.print("Not implemented, but adder(1,2) = {}\n", .{core.adder(1, 2)}); exit(.not_ok); exit(.ok); }
-
-
-
@@ -26,11 +26,19 @@ pub fn build(b: *std.Build) void {const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const sood = sood: { const dep = b.dependency("sood", .{}); break :sood dep.module("sood"); }; // Zig module { _ = b.addModule("core", .{ const mod = b.addModule("core", .{ .root_source_file = b.path("src/mod.zig"), }); mod.addImport("sood", sood); } // Static library (C API)
-
-
-
@@ -18,7 +18,12 @@ .name = .plac_core,.version = "0.0.0", .fingerprint = 0xc2c55a63ff4f6fb3, .minimum_zig_version = "0.14.0", .dependencies = .{}, .dependencies = .{ .sood = .{ .url = "https://git.pocka.jp/libsood.git/archive/8080245c2696cc6404b0628e5c3eb363cb80014f.tar.gz", .hash = "sood-0.0.0-A_jj-7ITAQAPlaaR2AHFXwPvBWzNBCCPTT--OCHnRQ_i", }, }, .paths = .{ "build.zig", "build.zig.zon",
-
-
-
@@ -16,9 +16,7 @@ // SPDX-License-Identifier: Apache-2.0//! This is an entrypoint of the static library. const mod = @import("./mod.zig"); // TODO: Remove after implementing actual exports. export fn adder(a: c_int, b: c_int) c_int { return mod.adder(a, b); return a + b; }
-
-
-
@@ -16,7 +16,134 @@ // SPDX-License-Identifier: Apache-2.0//! This is an entrypoint of Zig module. // TODO: Remove after implementing actual exports. pub fn adder(a: i32, b: i32) i32 { return a + b; } const std = @import("std"); const sood = @import("sood"); /// Data required to connect to a Roon Server pub const Server = struct { /// Pointer for the buffer used in this struct's slice fields. message: []const u8, ip_addr: std.net.Address, unique_id: []const u8, name: []const u8, version: []const u8, http_port: u16, }; 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| { // `received` will be invalidated after a loop or function exits. // Cloning the buffer so strings (e.g. `name`) can continue working. const message = try self.allocator.dupe(u8, received[0..received_size]); errdefer self.allocator.free(message); const response = sood.discovery.Response.parse(message) catch { // Non-SOOD message. Unlikely but technically possible. self.allocator.free(message); continue; }; if (self.servers.get(response.unique_id)) |existing| { // Release copied buffers. self.allocator.free(existing.message); } try self.servers.put(response.unique_id, Server{ .message = message, .unique_id = response.unique_id, .version = response.display_version, .name = response.name, .ip_addr = src, .http_port = response.http_port, }); } 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| { self.allocator.free(server.value_ptr.message); } self.servers.deinit(); std.posix.close(self.sockfd); self.* = undefined; } };
-