#![deny(warnings, clippy::all, clippy::pedantic,
// Do cfg(test) right
clippy::cfg_not_test,
clippy::tests_outside_test_module,
// Guard against left-over debugging output
clippy::dbg_macro,
clippy::unimplemented,
clippy::use_debug,
clippy::todo,
// Don't panic carelessly
clippy::get_unwrap,
clippy::unused_result_ok,
clippy::unwrap_in_result,
clippy::indexing_slicing,
// Do not carelessly ignore errors
clippy::let_underscore_must_use,
clippy::let_underscore_untyped,
// Code smells
clippy::float_cmp_const,
clippy::if_then_some_else_none,
clippy::large_include_file,
// Disable as casts
clippy::as_conversions,
)]
#![forbid(unsafe_code)]
use std::fs::File;
use std::io::{BufReader, Error, ErrorKind, Result, stdin};
use std::net::{SocketAddr, ToSocketAddrs};
use std::path::PathBuf;
use std::process::ExitCode;
use std::str::FromStr;
use std::thread::sleep;
use std::time::Duration;
use clap::{ArgAction, Parser, ValueHint, builder::ArgPredicate};
use wol::file::MagicPacketDestination;
use wol::{MacAddress, SecureOn};
#[derive(Debug)]
struct ResolvedWakeUpTarget {
hardware_address: MacAddress,
socket_addr: SocketAddr,
secure_on: Option<SecureOn>,
}
#[derive(Debug, Default, Clone, Copy)]
enum ResolveMode {
#[default]
Default,
PreferIpv6,
}
#[derive(Debug)]
struct WakeUpTarget {
hardware_address: MacAddress,
host: MagicPacketDestination,
port: u16,
secure_on: Option<SecureOn>,
}
impl WakeUpTarget {
fn resolve(&self, mode: ResolveMode) -> Result<ResolvedWakeUpTarget> {
match &self.host {
MagicPacketDestination::Dns(dns) => {
let mut socket_addrs = (dns.as_str(), self.port).to_socket_addrs()?;
let socket_addr = match mode {
ResolveMode::Default => socket_addrs.next(),
ResolveMode::PreferIpv6 => socket_addrs.find(SocketAddr::is_ipv6),
};
if let Some(socket_addr) = socket_addr {
Ok(ResolvedWakeUpTarget {
hardware_address: self.hardware_address,
socket_addr,
secure_on: self.secure_on,
})
} else {
Err(Error::new(
ErrorKind::HostUnreachable,
format!("Host {dns} not reachable"),
))
}
}
MagicPacketDestination::Ip(ip_addr) => Ok(ResolvedWakeUpTarget {
hardware_address: self.hardware_address,
socket_addr: SocketAddr::new(*ip_addr, self.port),
secure_on: self.secure_on,
}),
}
}
}
#[derive(Debug, Clone)]
enum PathOrStdin {
Stdin,
Path(PathBuf),
}
impl From<String> for PathOrStdin {
fn from(value: String) -> Self {
if value == "-" {
Self::Stdin
} else {
Self::Path(value.into())
}
}
}
const AFTER_HELP: &str = "Copyright (C) Sebastian Wiesner <sebastian@swsnr.de>
https://codeberg.org/swsnr/wol.rs
Licensed under the EUPL
See <https://interoperable-europe.ec.europa.eu/collection/eupl/eupl-text-eupl-12>";
#[derive(Parser, Debug, Clone)]
#[command(
version,
about,
disable_help_flag = true,
after_help = AFTER_HELP
)]
#[group()]
struct CliArgs {
#[arg(short = '?', long = "help", action = ArgAction::Help)]
help: (),
#[arg(
short = 'h',
long = "host",
visible_short_alias = 'i',
visible_alias = "ipaddr",
default_value = "255.255.255.255",
default_value_if("ipv6", ArgPredicate::IsPresent, Some("ff02::1")),
verbatim_doc_comment
)]
host: MagicPacketDestination,
#[arg(short = '6', long = "ipv6")]
ipv6: bool,
#[arg(
short = 'p',
long = "port",
default_value = "40000",
verbatim_doc_comment
)]
port: u16,
#[arg(short = 'f', long = "file", value_hint = ValueHint::FilePath)]
file: Option<PathOrStdin>,
#[arg(short = 'v', long = "verbose")]
verbose: bool,
#[arg(
short = 'w',
long = "wait",
value_name = "MSECS",
value_parser = |v: &str| u64::from_str(v).map(Duration::from_millis),
verbatim_doc_comment
)]
wait: Option<Duration>,
#[arg(long = "passwd")]
passwd: Option<SecureOn>,
#[arg(
value_name = "MAC-ADDRESS",
required_unless_present("file"),
verbatim_doc_comment
)]
hardware_addresses: Vec<wol::MacAddress>,
}
impl CliArgs {
fn iter_file(&self) -> Result<Box<dyn Iterator<Item = Result<wol::file::WakeUpTarget>>>> {
match &self.file {
Some(PathOrStdin::Stdin) => {
Ok(Box::new(wol::file::from_reader(BufReader::new(stdin()))))
}
Some(PathOrStdin::Path(path)) => Ok(Box::new(wol::file::from_reader(BufReader::new(
File::open(path)?,
)))),
None => Ok(Box::new(std::iter::empty())),
}
}
fn targets(&self) -> Result<impl Iterator<Item = Result<WakeUpTarget>>> {
let file_targets = self.iter_file()?.map(|target| {
target.map(|target| WakeUpTarget {
hardware_address: target.hardware_address(),
host: target
.packet_destination()
.cloned()
.unwrap_or(self.host.clone()),
port: target.port().unwrap_or(self.port),
secure_on: target.secure_on().or(self.passwd),
})
});
let cli_targets = self
.hardware_addresses
.iter()
.map(move |hardware_address| WakeUpTarget {
hardware_address: *hardware_address,
host: self.host.clone(),
port: self.port,
secure_on: self.passwd,
})
.map(Ok);
Ok(file_targets.chain(cli_targets))
}
fn resolve_mode(&self) -> ResolveMode {
if self.ipv6 {
ResolveMode::PreferIpv6
} else {
ResolveMode::Default
}
}
}
#[derive(Debug, Parser)]
#[command(
version,
about,
disable_help_flag = true,
after_help = AFTER_HELP
)]
struct Cli {
#[clap(flatten)]
args: CliArgs,
#[cfg(feature = "manpage")]
#[arg(long = "print-manpage", exclusive = true)]
manpage: bool,
#[cfg(feature = "completions")]
#[arg(long = "print-completions", exclusive = true)]
completions: Option<clap_complete::Shell>,
}
fn wakeup(target: &WakeUpTarget, mode: ResolveMode, verbose: bool) -> Result<()> {
if verbose {
if let MagicPacketDestination::Ip(ip_address) = target.host {
let target_socket_address = SocketAddr::new(ip_address, target.port);
println!(
"Waking up {} with {}...",
target.hardware_address, target_socket_address
);
} else {
println!(
"Waking up {} with {}:{}...",
target.hardware_address, target.host, target.port
);
}
} else {
println!("Waking up {}...", target.hardware_address);
}
let target = target.resolve(mode)?;
wol::send_magic_packet(
target.hardware_address,
target.secure_on,
target.socket_addr,
)
}
fn process_cli(cli: Cli) -> Result<ExitCode> {
#[cfg(feature = "manpage")]
if cli.manpage {
use clap::CommandFactory;
clap_mangen::Man::new(CliArgs::command()).render(&mut std::io::stdout())?;
return Ok(ExitCode::SUCCESS);
}
#[cfg(feature = "completions")]
if let Some(shell) = cli.completions {
use clap::CommandFactory;
clap_complete::generate(
shell,
&mut CliArgs::command(),
"wol",
&mut std::io::stdout(),
);
return Ok(ExitCode::SUCCESS);
}
let args = cli.args;
let resolve_mode = args.resolve_mode();
let mut exit_code = ExitCode::SUCCESS;
for (i, target) in args.targets()?.enumerate() {
let target = target?;
if 0 < i {
if let Some(wait) = args.wait.filter(|d| !d.is_zero()) {
sleep(wait);
}
}
if let Err(error) = wakeup(&target, resolve_mode, args.verbose) {
eprintln!("Failed to wake up {}: {error}", target.hardware_address);
exit_code = ExitCode::FAILURE;
}
}
Ok(exit_code)
}
fn main() -> ExitCode {
match process_cli(Cli::parse()) {
Err(error) => {
eprintln!("{error}");
ExitCode::FAILURE
}
Ok(exit_code) => exit_code,
}
}