Browse Source

query: filter parameter

ipv6
Denis Drakhnia 11 months ago
parent
commit
1254e41adf
  1. 2
      master/src/master_server.rs
  2. 5
      protocol/src/color.rs
  3. 2
      protocol/src/cursor.rs
  4. 57
      protocol/src/filter.rs
  5. 31
      protocol/src/game.rs
  6. 8
      protocol/src/lib.rs
  7. 23
      query/src/cli.rs
  8. 33
      query/src/main.rs

2
master/src/master_server.rs

@ -229,7 +229,7 @@ impl MasterServer {
match p { match p {
game::Packet::QueryServers(p) => { game::Packet::QueryServers(p) => {
trace!("{}: recv {:?}", from, p); trace!("{}: recv {:?}", from, p);
if p.filter.clver < self.clver { if p.filter.clver.map_or(false, |v| v < self.clver) {
let iter = std::iter::once(self.update_addr); let iter = std::iter::once(self.update_addr);
self.send_server_list(from, iter)?; self.send_server_list(from, iter)?;
} else { } else {

5
protocol/src/color.rs

@ -59,7 +59,10 @@ impl<'a> Iterator for ColorIter<'a> {
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
if !self.inner.is_empty() { if !self.inner.is_empty() {
let i = self.inner[1..].find('^').map(|i| i + 1).unwrap_or(self.inner.len()); let i = self.inner[1..]
.find('^')
.map(|i| i + 1)
.unwrap_or(self.inner.len());
let (head, tail) = self.inner.split_at(i); let (head, tail) = self.inner.split_at(i);
let (color, text) = trim_start_color(head); let (color, text) = trim_start_color(head);
self.inner = tail; self.inner = tail;

2
protocol/src/cursor.rs

@ -8,7 +8,7 @@ use std::slice;
use std::str; use std::str;
use super::types::Str; use super::types::Str;
use super::{Error, color}; use super::{color, Error};
pub trait GetKeyValue<'a>: Sized { pub trait GetKeyValue<'a>: Sized {
fn get_key_value(cur: &mut Cursor<'a>) -> Result<Self, Error>; fn get_key_value(cur: &mut Cursor<'a>) -> Result<Self, Error>;

57
protocol/src/filter.rs

@ -90,7 +90,7 @@ pub struct Version {
} }
impl Version { impl Version {
pub fn new(major: u8, minor: u8) -> Self { pub const fn new(major: u8, minor: u8) -> Self {
Self { major, minor } Self { major, minor }
} }
} }
@ -136,11 +136,11 @@ impl PutKeyValue for Version {
#[derive(Clone, Debug, Default, PartialEq, Eq)] #[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Filter<'a> { pub struct Filter<'a> {
/// Servers running the specified modification (ex. cstrike) /// Servers running the specified modification (ex. cstrike)
pub gamedir: &'a [u8], pub gamedir: Option<&'a [u8]>,
/// Servers running the specified map (ex. cs_italy) /// Servers running the specified map (ex. cs_italy)
pub map: &'a [u8], pub map: Option<&'a [u8]>,
/// Client version. /// Client version.
pub clver: Version, pub clver: Option<Version>,
pub flags: FilterFlags, pub flags: FilterFlags,
pub flags_mask: FilterFlags, pub flags_mask: FilterFlags,
@ -154,13 +154,15 @@ impl Filter<'_> {
pub fn matches(&self, _addr: SocketAddrV4, info: &ServerInfo) -> bool { pub fn matches(&self, _addr: SocketAddrV4, info: &ServerInfo) -> bool {
!((info.flags & self.flags_mask) != self.flags !((info.flags & self.flags_mask) != self.flags
|| (!self.gamedir.is_empty() && self.gamedir != &*info.gamedir) || self.gamedir.map_or(false, |s| s != &*info.gamedir)
|| (!self.map.is_empty() && self.map != &*info.map)) || self.map.map_or(false, |s| s != &*info.map))
} }
} }
impl<'a> Filter<'a> { impl<'a> TryFrom<&'a [u8]> for Filter<'a> {
pub fn from_bytes(src: &'a [u8]) -> Result<Self, Error> { type Error = Error;
fn try_from(src: &'a [u8]) -> Result<Self, Self::Error> {
let mut cur = Cursor::new(src); let mut cur = Cursor::new(src);
let mut filter = Self::default(); let mut filter = Self::default();
@ -174,17 +176,18 @@ impl<'a> Filter<'a> {
match *key { match *key {
b"dedicated" => filter.insert_flag(FilterFlags::DEDICATED, cur.get_key_value()?), b"dedicated" => filter.insert_flag(FilterFlags::DEDICATED, cur.get_key_value()?),
b"secure" => filter.insert_flag(FilterFlags::SECURE, cur.get_key_value()?), b"secure" => filter.insert_flag(FilterFlags::SECURE, cur.get_key_value()?),
b"gamedir" => filter.gamedir = cur.get_key_value()?, b"gamedir" => filter.gamedir = Some(cur.get_key_value()?),
b"map" => filter.map = cur.get_key_value()?, b"map" => filter.map = Some(cur.get_key_value()?),
b"empty" => filter.insert_flag(FilterFlags::NOT_EMPTY, cur.get_key_value()?), b"empty" => filter.insert_flag(FilterFlags::NOT_EMPTY, cur.get_key_value()?),
b"full" => filter.insert_flag(FilterFlags::FULL, cur.get_key_value()?), b"full" => filter.insert_flag(FilterFlags::FULL, cur.get_key_value()?),
b"password" => filter.insert_flag(FilterFlags::PASSWORD, cur.get_key_value()?), b"password" => filter.insert_flag(FilterFlags::PASSWORD, cur.get_key_value()?),
b"noplayers" => filter.insert_flag(FilterFlags::NOPLAYERS, cur.get_key_value()?), b"noplayers" => filter.insert_flag(FilterFlags::NOPLAYERS, cur.get_key_value()?),
b"clver" => { b"clver" => {
filter.clver = cur filter.clver = Some(
.get_key_value::<&str>()? cur.get_key_value::<&str>()?
.parse() .parse()
.map_err(|_| Error::InvalidPacket)? .map_err(|_| Error::InvalidPacket)?,
);
} }
b"nat" => filter.insert_flag(FilterFlags::NAT, cur.get_key_value()?), b"nat" => filter.insert_flag(FilterFlags::NAT, cur.get_key_value()?),
b"lan" => filter.insert_flag(FilterFlags::LAN, cur.get_key_value()?), b"lan" => filter.insert_flag(FilterFlags::LAN, cur.get_key_value()?),
@ -214,18 +217,20 @@ impl fmt::Display for &Filter<'_> {
display_flag!("dedicated", FilterFlags::DEDICATED); display_flag!("dedicated", FilterFlags::DEDICATED);
display_flag!("secure", FilterFlags::SECURE); display_flag!("secure", FilterFlags::SECURE);
if !self.gamedir.is_empty() { if let Some(s) = self.gamedir {
write!(fmt, "\\gamedir\\{}", Str(self.gamedir))?; write!(fmt, "\\gamedir\\{}", Str(s))?;
} }
display_flag!("secure", FilterFlags::SECURE); display_flag!("secure", FilterFlags::SECURE);
if !self.map.is_empty() { if let Some(s) = self.map {
write!(fmt, "\\map\\{}", Str(self.map))?; write!(fmt, "\\map\\{}", Str(s))?;
} }
display_flag!("empty", FilterFlags::NOT_EMPTY); display_flag!("empty", FilterFlags::NOT_EMPTY);
display_flag!("full", FilterFlags::FULL); display_flag!("full", FilterFlags::FULL);
display_flag!("password", FilterFlags::PASSWORD); display_flag!("password", FilterFlags::PASSWORD);
display_flag!("noplayers", FilterFlags::NOPLAYERS); display_flag!("noplayers", FilterFlags::NOPLAYERS);
write!(fmt, "\\clver\\{}", self.clver)?; if let Some(v) = self.clver {
write!(fmt, "\\clver\\{}", v)?;
}
display_flag!("nat", FilterFlags::NAT); display_flag!("nat", FilterFlags::NAT);
display_flag!("lan", FilterFlags::LAN); display_flag!("lan", FilterFlags::LAN);
display_flag!("bots", FilterFlags::BOTS); display_flag!("bots", FilterFlags::BOTS);
@ -253,7 +258,7 @@ mod tests {
.. Filter::default() .. Filter::default()
}; };
$(assert_eq!( $(assert_eq!(
Filter::from_bytes($src), Filter::try_from($src as &[u8]),
Ok(Filter { Ok(Filter {
$($field: $value,)* $($field: $value,)*
..predefined ..predefined
@ -266,17 +271,17 @@ mod tests {
tests! { tests! {
parse_gamedir { parse_gamedir {
b"\\gamedir\\valve" => { b"\\gamedir\\valve" => {
gamedir: &b"valve"[..], gamedir: Some(&b"valve"[..]),
} }
} }
parse_map { parse_map {
b"\\map\\crossfire" => { b"\\map\\crossfire" => {
map: &b"crossfire"[..], map: Some(&b"crossfire"[..]),
} }
} }
parse_clver { parse_clver {
b"\\clver\\0.20" => { b"\\clver\\0.20" => {
clver: Version::new(0, 20), clver: Some(Version::new(0, 20)),
} }
} }
parse_dedicated(flags_mask: FilterFlags::DEDICATED) { parse_dedicated(flags_mask: FilterFlags::DEDICATED) {
@ -349,9 +354,9 @@ mod tests {
\\password\\1\ \\password\\1\
\\secure\\1\ \\secure\\1\
" => { " => {
gamedir: &b"valve"[..], gamedir: Some(&b"valve"[..]),
map: &b"crossfire"[..], map: Some(&b"crossfire"[..]),
clver: Version::new(0, 20), clver: Some(Version::new(0, 20)),
flags: FilterFlags::all(), flags: FilterFlags::all(),
flags_mask: FilterFlags::all(), flags_mask: FilterFlags::all(),
} }
@ -383,7 +388,7 @@ mod tests {
macro_rules! matches { macro_rules! matches {
($servers:expr, $filter:expr$(, $expected:expr)*) => ( ($servers:expr, $filter:expr$(, $expected:expr)*) => (
let servers = &$servers; let servers = &$servers;
let filter = Filter::from_bytes($filter).unwrap(); let filter = Filter::try_from($filter as &[u8]).unwrap();
let iter = servers let iter = servers
.iter() .iter()
.enumerate() .enumerate()

31
protocol/src/game.rs

@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com> // SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
use std::fmt;
use std::net::SocketAddrV4; use std::net::SocketAddrV4;
use crate::cursor::{Cursor, CursorMut}; use crate::cursor::{Cursor, CursorMut};
@ -9,18 +10,23 @@ use crate::server::Region;
use crate::Error; use crate::Error;
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct QueryServers<'a> { pub struct QueryServers<T> {
pub region: Region, pub region: Region,
pub last: SocketAddrV4, pub last: SocketAddrV4,
pub filter: Filter<'a>, pub filter: T,
} }
impl<'a> QueryServers<'a> { impl QueryServers<()> {
pub const HEADER: &'static [u8] = b"1"; pub const HEADER: &'static [u8] = b"1";
}
impl<'a, T: 'a> QueryServers<T>
where
T: TryFrom<&'a [u8], Error = Error>,
{
pub fn decode(src: &'a [u8]) -> Result<Self, Error> { pub fn decode(src: &'a [u8]) -> Result<Self, Error> {
let mut cur = Cursor::new(src); let mut cur = Cursor::new(src);
cur.expect(Self::HEADER)?; cur.expect(QueryServers::HEADER)?;
let region = cur.get_u8()?.try_into().map_err(|_| Error::InvalidPacket)?; let region = cur.get_u8()?.try_into().map_err(|_| Error::InvalidPacket)?;
let last = cur.get_cstr_as_str()?; let last = cur.get_cstr_as_str()?;
let filter = cur.get_cstr()?; let filter = cur.get_cstr()?;
@ -28,13 +34,18 @@ impl<'a> QueryServers<'a> {
Ok(Self { Ok(Self {
region, region,
last: last.parse().map_err(|_| Error::InvalidPacket)?, last: last.parse().map_err(|_| Error::InvalidPacket)?,
filter: Filter::from_bytes(&filter)?, filter: T::try_from(*filter)?,
}) })
} }
}
impl<'a, T: 'a> QueryServers<T>
where
for<'b> &'b T: fmt::Display,
{
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, Error> { pub fn encode(&self, buf: &mut [u8]) -> Result<usize, Error> {
Ok(CursorMut::new(buf) Ok(CursorMut::new(buf)
.put_bytes(Self::HEADER)? .put_bytes(QueryServers::HEADER)?
.put_u8(self.region as u8)? .put_u8(self.region as u8)?
.put_as_str(self.last)? .put_as_str(self.last)?
.put_u8(0)? .put_u8(0)?
@ -76,7 +87,7 @@ impl GetServerInfo {
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum Packet<'a> { pub enum Packet<'a> {
QueryServers(QueryServers<'a>), QueryServers(QueryServers<Filter<'a>>),
GetServerInfo(GetServerInfo), GetServerInfo(GetServerInfo),
} }
@ -106,9 +117,9 @@ mod tests {
region: Region::RestOfTheWorld, region: Region::RestOfTheWorld,
last: SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 0), last: SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 0),
filter: Filter { filter: Filter {
gamedir: &b"valve"[..], gamedir: Some(&b"valve"[..]),
map: &b"crossfire"[..], map: Some(&b"crossfire"[..]),
clver: Version::new(0, 20), clver: Some(Version::new(0, 20)),
flags: FilterFlags::all(), flags: FilterFlags::all(),
flags_mask: FilterFlags::all(), flags_mask: FilterFlags::all(),
}, },

8
protocol/src/lib.rs

@ -5,18 +5,22 @@ mod cursor;
mod server_info; mod server_info;
pub mod admin; pub mod admin;
pub mod color;
pub mod filter; pub mod filter;
pub mod game; pub mod game;
pub mod master; pub mod master;
pub mod server; pub mod server;
pub mod types; pub mod types;
pub mod color;
pub use server_info::ServerInfo; pub use server_info::ServerInfo;
use thiserror::Error; use thiserror::Error;
pub const VERSION: u8 = 49; use crate::filter::Version;
pub const PROTOCOL_VERSION: u8 = 49;
pub const CLIENT_VERSION: Version = Version::new(0, 20);
#[derive(Error, Debug, PartialEq, Eq)] #[derive(Error, Debug, PartialEq, Eq)]
pub enum Error { pub enum Error {

23
query/src/cli.rs

@ -5,6 +5,8 @@ use std::process;
use getopts::Options; use getopts::Options;
use xash3d_protocol as proto;
const BIN_NAME: &str = env!("CARGO_BIN_NAME"); const BIN_NAME: &str = env!("CARGO_BIN_NAME");
const PKG_NAME: &str = env!("CARGO_PKG_NAME"); const PKG_NAME: &str = env!("CARGO_PKG_NAME");
const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
@ -22,6 +24,7 @@ pub struct Cli {
pub json: bool, pub json: bool,
pub debug: bool, pub debug: bool,
pub force_color: bool, pub force_color: bool,
pub filter: String,
} }
impl Default for Cli { impl Default for Cli {
@ -34,10 +37,12 @@ impl Default for Cli {
args: Default::default(), args: Default::default(),
master_timeout: 2, master_timeout: 2,
server_timeout: 2, server_timeout: 2,
protocol: vec![xash3d_protocol::VERSION, xash3d_protocol::VERSION - 1], protocol: vec![proto::PROTOCOL_VERSION, proto::PROTOCOL_VERSION - 1],
json: false, json: false,
debug: false, debug: false,
force_color: false, force_color: false,
// if changed do not forget to update cli parsing
filter: format!("\\gamedir\\valve\\clver\\{}", proto::CLIENT_VERSION),
} }
} }
} }
@ -94,6 +99,8 @@ pub fn parse() -> Cli {
opts.optflag("j", "json", "output JSON"); opts.optflag("j", "json", "output JSON");
opts.optflag("d", "debug", "output debug"); opts.optflag("d", "debug", "output debug");
opts.optflag("F", "force-color", "force colored output"); opts.optflag("F", "force-color", "force colored output");
let help = format!("query filter [default: {:?}]", cli.filter);
opts.optopt("f", "filter", &help, "FILTER");
let matches = match opts.parse(&args[1..]) { let matches = match opts.parse(&args[1..]) {
Ok(m) => m, Ok(m) => m,
@ -124,7 +131,7 @@ pub fn parse() -> Cli {
} }
} }
match matches.opt_get("master") { match matches.opt_get("master-timeout") {
Ok(Some(t)) => cli.master_timeout = t, Ok(Some(t)) => cli.master_timeout = t,
Ok(None) => {} Ok(None) => {}
Err(_) => { Err(_) => {
@ -161,6 +168,18 @@ pub fn parse() -> Cli {
} }
} }
if let Some(s) = matches.opt_str("filter") {
let mut filter = String::with_capacity(cli.filter.len() + s.len());
if !s.contains("\\gamedir") {
filter.push_str("\\gamedir\\valve");
}
if !s.contains("\\clver") {
filter.push_str("\\clver\\0.20");
}
filter.push_str(&s);
cli.filter = filter;
}
cli.json = matches.opt_present("json"); cli.json = matches.opt_present("json");
cli.debug = matches.opt_present("debug"); cli.debug = matches.opt_present("debug");
cli.force_color = matches.opt_present("force-color"); cli.force_color = matches.opt_present("force-color");

33
query/src/main.rs

@ -9,14 +9,14 @@ use std::fmt;
use std::io; use std::io;
use std::net::{Ipv4Addr, SocketAddrV4, UdpSocket}; use std::net::{Ipv4Addr, SocketAddrV4, UdpSocket};
use std::process; use std::process;
use std::sync::mpsc; use std::sync::{mpsc, Arc};
use std::thread; use std::thread;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use serde::Serialize; use serde::Serialize;
use thiserror::Error; use thiserror::Error;
use xash3d_protocol::types::Str; use xash3d_protocol::types::Str;
use xash3d_protocol::{color, filter, game, master, server, Error as ProtocolError}; use xash3d_protocol::{color, game, master, server, Error as ProtocolError};
use crate::cli::Cli; use crate::cli::Cli;
@ -135,6 +135,7 @@ struct InfoResult<'a> {
master_timeout: u32, master_timeout: u32,
server_timeout: u32, server_timeout: u32,
masters: &'a [Box<str>], masters: &'a [Box<str>],
filter: &'a str,
servers: &'a [&'a ServerResult], servers: &'a [&'a ServerResult],
} }
@ -142,6 +143,7 @@ struct InfoResult<'a> {
struct ListResult<'a> { struct ListResult<'a> {
master_timeout: u32, master_timeout: u32,
masters: &'a [Box<str>], masters: &'a [Box<str>],
filter: &'a str,
servers: &'a [&'a str], servers: &'a [&'a str],
} }
@ -199,7 +201,12 @@ fn cmp_address(a: &str, b: &str) -> cmp::Ordering {
} }
} }
fn query_servers(host: &str, timeout: Duration, tx: &mpsc::Sender<Message>) -> Result<(), Error> { fn query_servers(
host: &str,
cli: &Cli,
timeout: Duration,
tx: &mpsc::Sender<Message>,
) -> Result<(), Error> {
let sock = UdpSocket::bind("0.0.0.0:0")?; let sock = UdpSocket::bind("0.0.0.0:0")?;
sock.connect(host)?; sock.connect(host)?;
@ -207,12 +214,7 @@ fn query_servers(host: &str, timeout: Duration, tx: &mpsc::Sender<Message>) -> R
let p = game::QueryServers { let p = game::QueryServers {
region: server::Region::RestOfTheWorld, region: server::Region::RestOfTheWorld,
last: SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 0), last: SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 0),
filter: filter::Filter { filter: cli.filter.as_str(),
gamedir: b"valve",
clver: filter::Version::new(0, 20),
// TODO: filter
..Default::default()
},
}; };
let n = p.encode(&mut buf)?; let n = p.encode(&mut buf)?;
sock.send(&buf[..n])?; sock.send(&buf[..n])?;
@ -282,7 +284,7 @@ fn get_server_info(
Ok(ServerResult::protocol(addr)) Ok(ServerResult::protocol(addr))
} }
fn query_server_info(cli: &Cli, servers: &[String]) -> Result<(), Error> { fn query_server_info(cli: &Arc<Cli>, servers: &[String]) -> Result<(), Error> {
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
let mut workers = 0; let mut workers = 0;
@ -291,8 +293,9 @@ fn query_server_info(cli: &Cli, servers: &[String]) -> Result<(), Error> {
let master = i.to_owned(); let master = i.to_owned();
let tx = tx.clone(); let tx = tx.clone();
let timeout = Duration::from_secs(cli.master_timeout as u64); let timeout = Duration::from_secs(cli.master_timeout as u64);
let cli = cli.clone();
thread::spawn(move || { thread::spawn(move || {
if let Err(e) = query_servers(&master, timeout, &tx) { if let Err(e) = query_servers(&master, &cli, timeout, &tx) {
eprintln!("master({}) error: {}", master, e); eprintln!("master({}) error: {}", master, e);
} }
tx.send(Message::End).unwrap(); tx.send(Message::End).unwrap();
@ -341,6 +344,7 @@ fn query_server_info(cli: &Cli, servers: &[String]) -> Result<(), Error> {
master_timeout: cli.master_timeout, master_timeout: cli.master_timeout,
server_timeout: cli.server_timeout, server_timeout: cli.server_timeout,
masters: &cli.masters, masters: &cli.masters,
filter: &cli.filter,
servers: &servers, servers: &servers,
}; };
@ -408,7 +412,7 @@ fn query_server_info(cli: &Cli, servers: &[String]) -> Result<(), Error> {
Ok(()) Ok(())
} }
fn list_servers(cli: &Cli) -> Result<(), Error> { fn list_servers(cli: &Arc<Cli>) -> Result<(), Error> {
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
let mut workers = 0; let mut workers = 0;
@ -416,8 +420,9 @@ fn list_servers(cli: &Cli) -> Result<(), Error> {
let master = i.to_owned(); let master = i.to_owned();
let tx = tx.clone(); let tx = tx.clone();
let timeout = Duration::from_secs(cli.master_timeout as u64); let timeout = Duration::from_secs(cli.master_timeout as u64);
let cli = cli.clone();
thread::spawn(move || { thread::spawn(move || {
if let Err(e) = query_servers(&master, timeout, &tx) { if let Err(e) = query_servers(&master, &cli, timeout, &tx) {
eprintln!("master({}) error: {}", master, e); eprintln!("master({}) error: {}", master, e);
} }
tx.send(Message::End).unwrap(); tx.send(Message::End).unwrap();
@ -448,6 +453,7 @@ fn list_servers(cli: &Cli) -> Result<(), Error> {
let result = ListResult { let result = ListResult {
master_timeout: cli.master_timeout, master_timeout: cli.master_timeout,
masters: &cli.masters, masters: &cli.masters,
filter: &cli.filter,
servers: &servers, servers: &servers,
}; };
@ -468,6 +474,7 @@ fn list_servers(cli: &Cli) -> Result<(), Error> {
} }
fn execute(cli: Cli) -> Result<(), Error> { fn execute(cli: Cli) -> Result<(), Error> {
let cli = Arc::new(cli);
match cli.args.get(0).map(|s| s.as_str()).unwrap_or_default() { match cli.args.get(0).map(|s| s.as_str()).unwrap_or_default() {
"all" | "" => query_server_info(&cli, &[])?, "all" | "" => query_server_info(&cli, &[])?,
"info" => query_server_info(&cli, &cli.args[1..])?, "info" => query_server_info(&cli, &cli.args[1..])?,

Loading…
Cancel
Save