use anyhow::{anyhow, Context, Result};
#[cfg(target_os = "windows")]
use std::collections::VecDeque;
#[cfg(target_os = "windows")]
use std::io::{Read, Seek, SeekFrom, Write};
use std::net::TcpStream;
#[cfg(target_os = "windows")]
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;
#[cfg(target_os = "windows")]
use std::time::Instant;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Proxy,
Client,
}
impl Mode {
pub fn from_str_opt(s: &str) -> Option<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"proxy" | "proxy-server" => Some(Mode::Proxy),
"client" | "client-server" => Some(Mode::Client),
_ => None,
}
}
pub fn subcommand(self) -> &'static str {
match self {
Mode::Proxy => "proxy-server",
Mode::Client => "client-server",
}
}
#[allow(dead_code)]
pub fn service_name(self) -> &'static str {
match self {
Mode::Proxy => "wakezilla-proxy",
Mode::Client => "wakezilla-client",
}
}
#[allow(dead_code)]
pub fn service_display_name(self) -> &'static str {
match self {
Mode::Proxy => "Wakezilla Proxy",
Mode::Client => "Wakezilla Client",
}
}
#[allow(dead_code)]
pub fn service_arg(self) -> &'static str {
match self {
Mode::Proxy => "proxy",
Mode::Client => "client",
}
}
#[allow(dead_code)]
pub fn launchd_label(self) -> &'static str {
match self {
Mode::Proxy => "dev.wakezilla.proxy",
Mode::Client => "dev.wakezilla.client",
}
}
pub fn default_port(self) -> u16 {
match self {
Mode::Proxy => 3000,
Mode::Client => 3001,
}
}
}
pub fn service_program_args(mode: Mode) -> [&'static str; 2] {
["--no-update-check", mode.subcommand()]
}
#[allow(dead_code)]
pub fn windows_service_program_args(mode: Mode) -> [&'static str; 3] {
["--no-update-check", "windows-service", mode.service_arg()]
}
#[allow(dead_code)]
pub fn firewall_rule_name(mode: Mode) -> &'static str {
match mode {
Mode::Proxy => "Wakezilla Proxy Server",
Mode::Client => "Wakezilla Client Server",
}
}
#[allow(dead_code)]
pub fn service_log_file_name(mode: Mode) -> String {
format!("{}.log", mode.service_name())
}
#[allow(dead_code)]
pub fn service_log_path(mode: Mode) -> PathBuf {
crate::config::data_path(&service_log_file_name(mode))
}
pub fn configure_firewall(mode: Mode, exe: &str, port: u16) -> Result<()> {
#[cfg(target_os = "windows")]
{
let rule_name = firewall_rule_name(mode);
let name_arg = format!("name={rule_name}");
let program_arg = format!("program={exe}");
let port_arg = format!("localport={port}");
run_ignore_err(
"netsh",
&["advfirewall", "firewall", "delete", "rule", &name_arg],
);
run(
"netsh",
&[
"advfirewall",
"firewall",
"add",
"rule",
&name_arg,
"dir=in",
"action=allow",
&program_arg,
"enable=yes",
"profile=any",
"protocol=TCP",
&port_arg,
],
)
}
#[cfg(not(target_os = "windows"))]
{
let _ = (mode, exe, port);
Ok(())
}
}
pub fn remove_firewall(mode: Mode) -> Result<()> {
#[cfg(target_os = "windows")]
{
let rule_name = firewall_rule_name(mode);
let name_arg = format!("name={rule_name}");
let output = run_output(
"netsh",
&["advfirewall", "firewall", "delete", "rule", &name_arg],
)?;
if output.status.success() || netsh_rule_not_found(&output) {
return Ok(());
}
anyhow::bail!(
"netsh failed to remove firewall rule '{}': {}",
rule_name,
command_output_text(&output)
);
}
#[cfg(not(target_os = "windows"))]
{
let _ = mode;
Ok(())
}
}
#[allow(dead_code)]
pub fn generate_systemd_unit(mode: Mode, exe: &str) -> String {
let [no_update_check, sub] = service_program_args(mode);
format!(
"[Unit]\n\
Description=Wakezilla {desc}\n\
After=network-online.target\n\
Wants=network-online.target\n\
\n\
[Service]\n\
Type=simple\n\
ExecStart={exe} {no_update_check} {sub}\n\
Restart=on-failure\n\
RestartSec=5\n\
\n\
[Install]\n\
WantedBy=multi-user.target\n",
desc = mode.subcommand(),
exe = exe,
no_update_check = no_update_check,
sub = sub,
)
}
pub const MACOS_LOG_DIR: &str = "/Library/Logs/wakezilla";
fn macos_stdout_log(mode: Mode) -> String {
format!("{MACOS_LOG_DIR}/{}.out.log", mode.launchd_label())
}
fn macos_stderr_log(mode: Mode) -> String {
format!("{MACOS_LOG_DIR}/{}.err.log", mode.launchd_label())
}
#[allow(dead_code)]
pub fn generate_launchd_plist(mode: Mode, exe: &str) -> String {
let [no_update_check, sub] = service_program_args(mode);
format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n\
<plist version=\"1.0\">\n\
<dict>\n\
\t<key>Label</key>\n\
\t<string>{label}</string>\n\
\t<key>ProgramArguments</key>\n\
\t<array>\n\
\t\t<string>{exe}</string>\n\
\t\t<string>{no_update_check}</string>\n\
\t\t<string>{sub}</string>\n\
\t</array>\n\
\t<key>RunAtLoad</key>\n\
\t<true/>\n\
\t<key>KeepAlive</key>\n\
\t<true/>\n\
\t<key>StandardOutPath</key>\n\
\t<string>{out}</string>\n\
\t<key>StandardErrorPath</key>\n\
\t<string>{err}</string>\n\
</dict>\n\
</plist>\n",
label = mode.launchd_label(),
exe = exe,
no_update_check = no_update_check,
sub = sub,
out = macos_stdout_log(mode),
err = macos_stderr_log(mode),
)
}
pub fn validate(port: u16, attempts: u32) -> Result<()> {
let addr = format!("127.0.0.1:{port}");
let socket = addr
.parse()
.with_context(|| format!("invalid validation address {addr}"))?;
for attempt in 1..=attempts {
match TcpStream::connect_timeout(&socket, Duration::from_secs(1)) {
Ok(_) => return Ok(()),
Err(_) if attempt < attempts => {
std::thread::sleep(Duration::from_millis(500));
}
Err(e) => {
return Err(anyhow!(
"service did not accept connections on {addr} after {attempts} attempts: {e}"
));
}
}
}
Err(anyhow!("service not reachable on {addr}"))
}
pub fn is_elevated() -> bool {
#[cfg(unix)]
{
Command::new("id")
.arg("-u")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim() == "0")
.unwrap_or(false)
}
#[cfg(windows)]
{
Command::new("net")
.arg("session")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
}
pub fn install(mode: Mode, exe: &str) -> Result<()> {
#[cfg(target_os = "linux")]
{
let unit = generate_systemd_unit(mode, exe);
let path = format!("/etc/systemd/system/{}.service", mode.service_name());
std::fs::write(&path, unit).with_context(|| format!("writing {path}"))?;
run("systemctl", &["daemon-reload"])?;
run("systemctl", &["enable", mode.service_name()])?;
run("systemctl", &["restart", mode.service_name()])?;
Ok(())
}
#[cfg(target_os = "macos")]
{
std::fs::create_dir_all(MACOS_LOG_DIR)
.with_context(|| format!("creating log dir {MACOS_LOG_DIR}"))?;
let plist = generate_launchd_plist(mode, exe);
let path = format!("/Library/LaunchDaemons/{}.plist", mode.launchd_label());
std::fs::write(&path, plist).with_context(|| format!("writing {path}"))?;
run_ignore_err("launchctl", &["unload", &path]);
run("launchctl", &["load", "-w", &path])?;
Ok(())
}
#[cfg(target_os = "windows")]
{
install_windows_service(mode, exe)?;
start(mode)?;
Ok(())
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
let _ = (mode, exe);
Err(anyhow!("service install not supported on this OS"))
}
}
pub fn managed_modes() -> [Mode; 2] {
[Mode::Proxy, Mode::Client]
}
pub fn uninstall(mode: Mode) -> Result<()> {
#[cfg(target_os = "linux")]
let service_result: Result<()> = {
run_ignore_err("systemctl", &["stop", mode.service_name()]);
run_ignore_err("systemctl", &["disable", mode.service_name()]);
remove_file_if_exists(&descriptor_path(mode))?;
run("systemctl", &["daemon-reload"])?;
run_ignore_err("systemctl", &["reset-failed", mode.service_name()]);
Ok(())
};
#[cfg(target_os = "macos")]
let service_result = {
let path = descriptor_path(mode);
run_ignore_err("launchctl", &["unload", &path]);
remove_file_if_exists(&path)
};
#[cfg(target_os = "windows")]
let service_result = uninstall_windows_service(mode);
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
let service_result = Err(anyhow!("service uninstall not supported on this OS"));
let firewall_result = remove_firewall(mode);
service_result?;
firewall_result?;
Ok(())
}
pub fn uninstall_all() -> Result<Vec<Mode>> {
let mut removed = Vec::new();
for mode in managed_modes() {
let was_installed = is_installed(mode);
uninstall(mode).with_context(|| format!("failed to uninstall {}", mode.subcommand()))?;
if was_installed {
removed.push(mode);
}
}
Ok(removed)
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn remove_file_if_exists(path: &str) -> Result<()> {
match std::fs::remove_file(path) {
Ok(()) => Ok(()),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(err) => Err(err).with_context(|| format!("removing {path}")),
}
}
#[allow(dead_code)]
fn run(cmd: &str, args: &[&str]) -> Result<()> {
let status = Command::new(cmd)
.args(args)
.status()
.with_context(|| format!("failed to run {cmd}"))?;
if !status.success() {
return Err(anyhow!("{cmd} {args:?} exited with {status}"));
}
Ok(())
}
#[allow(dead_code)]
fn run_ignore_err(cmd: &str, args: &[&str]) {
let _ = Command::new(cmd).args(args).status();
}
#[cfg(target_os = "windows")]
fn run_output(cmd: &str, args: &[&str]) -> Result<std::process::Output> {
Command::new(cmd)
.args(args)
.output()
.with_context(|| format!("failed to run {cmd}"))
}
#[cfg(target_os = "windows")]
fn command_output_text(output: &std::process::Output) -> String {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let text = format!("{stdout}{stderr}");
let text = text.trim();
if text.is_empty() {
output.status.to_string()
} else {
text.to_string()
}
}
#[cfg(target_os = "windows")]
fn netsh_rule_not_found(output: &std::process::Output) -> bool {
let text = command_output_text(output).to_ascii_lowercase();
text.contains("no rules match") || text.contains("nenhuma regra")
}
#[allow(dead_code)]
fn run_inherit(cmd: &str, args: &[&str]) -> Result<()> {
Command::new(cmd)
.args(args)
.status()
.with_context(|| format!("failed to run {cmd}"))?;
Ok(())
}
#[cfg(target_os = "macos")]
fn descriptor_path(mode: Mode) -> String {
format!("/Library/LaunchDaemons/{}.plist", mode.launchd_label())
}
#[cfg(target_os = "linux")]
fn descriptor_path(mode: Mode) -> String {
format!("/etc/systemd/system/{}.service", mode.service_name())
}
pub fn is_installed(mode: Mode) -> bool {
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
std::path::Path::new(&descriptor_path(mode)).exists()
}
#[cfg(target_os = "windows")]
{
open_windows_service(mode, windows_service::service::ServiceAccess::QUERY_STATUS).is_ok()
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
let _ = mode;
false
}
}
pub fn installed_modes() -> Vec<Mode> {
managed_modes()
.into_iter()
.filter(|m| is_installed(*m))
.collect()
}
pub fn start(mode: Mode) -> Result<()> {
#[cfg(target_os = "linux")]
{
run("systemctl", &["start", mode.service_name()])
}
#[cfg(target_os = "macos")]
{
run("launchctl", &["load", "-w", &descriptor_path(mode)])
}
#[cfg(target_os = "windows")]
{
let service = open_windows_service(mode, windows_service::service::ServiceAccess::START)?;
service.start::<std::ffi::OsString>(&[]).map_err(Into::into)
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
let _ = mode;
Err(anyhow!("service control not supported on this OS"))
}
}
pub fn stop(mode: Mode) -> Result<()> {
#[cfg(target_os = "linux")]
{
run("systemctl", &["stop", mode.service_name()])
}
#[cfg(target_os = "macos")]
{
run("launchctl", &["unload", &descriptor_path(mode)])
}
#[cfg(target_os = "windows")]
{
let service = open_windows_service(mode, windows_service::service::ServiceAccess::STOP)?;
service.stop().map(|_| ()).map_err(Into::into)
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
let _ = mode;
Err(anyhow!("service control not supported on this OS"))
}
}
pub fn restart(mode: Mode) -> Result<()> {
#[cfg(target_os = "linux")]
{
run("systemctl", &["restart", mode.service_name()])
}
#[cfg(target_os = "macos")]
{
run_ignore_err("launchctl", &["unload", &descriptor_path(mode)]);
run("launchctl", &["load", "-w", &descriptor_path(mode)])
}
#[cfg(target_os = "windows")]
{
stop_windows_service_for_restart(mode, Duration::from_secs(15))?;
start(mode)
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
let _ = mode;
Err(anyhow!("service control not supported on this OS"))
}
}
pub fn is_running(mode: Mode) -> bool {
#[cfg(target_os = "linux")]
{
Command::new("systemctl")
.args(["is-active", "--quiet", mode.service_name()])
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[cfg(target_os = "macos")]
{
let label = format!("system/{}", mode.launchd_label());
Command::new("launchctl")
.args(["print", &label])
.output()
.map(|o| {
o.status.success() && String::from_utf8_lossy(&o.stdout).contains("state = running")
})
.unwrap_or(false)
}
#[cfg(target_os = "windows")]
{
open_windows_service(mode, windows_service::service::ServiceAccess::QUERY_STATUS)
.and_then(|service| service.query_status().map_err(Into::into))
.map(|status| status.current_state == windows_service::service::ServiceState::Running)
.unwrap_or(false)
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
let _ = mode;
false
}
}
#[cfg(target_os = "windows")]
fn open_windows_service(
mode: Mode,
access: windows_service::service::ServiceAccess,
) -> Result<windows_service::service::Service> {
use windows_service::service_manager::{ServiceManager, ServiceManagerAccess};
let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)?;
manager
.open_service(mode.service_name(), access)
.with_context(|| format!("failed to open Windows service {}", mode.service_name()))
}
#[cfg(target_os = "windows")]
fn install_windows_service(mode: Mode, exe: &str) -> Result<()> {
use std::ffi::OsString;
use windows_service::service::{
ServiceAccess, ServiceErrorControl, ServiceInfo, ServiceStartType, ServiceType,
};
use windows_service::service_manager::{ServiceManager, ServiceManagerAccess};
let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE;
let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?;
delete_windows_service_if_exists(&service_manager, mode)?;
let service_info = ServiceInfo {
name: OsString::from(mode.service_name()),
display_name: OsString::from(mode.service_display_name()),
service_type: ServiceType::OWN_PROCESS,
start_type: ServiceStartType::AutoStart,
error_control: ServiceErrorControl::Normal,
executable_path: PathBuf::from(exe),
launch_arguments: windows_service_program_args(mode)
.into_iter()
.map(OsString::from)
.collect(),
dependencies: vec![],
account_name: None,
account_password: None,
};
let service = service_manager.create_service(
&service_info,
ServiceAccess::CHANGE_CONFIG | ServiceAccess::START,
)?;
service
.set_description(format!(
"Runs Wakezilla {} as a Windows service.",
mode.subcommand()
))
.ok();
Ok(())
}
#[cfg(target_os = "windows")]
fn stop_windows_service_for_restart(mode: Mode, timeout: Duration) -> Result<()> {
use windows_service::service::{ServiceAccess, ServiceState};
let service = open_windows_service(mode, ServiceAccess::QUERY_STATUS | ServiceAccess::STOP)?;
let status = service.query_status()?;
if status.current_state == ServiceState::Stopped {
return Ok(());
}
if status.current_state != ServiceState::StopPending {
service.stop()?;
}
let deadline = Instant::now() + timeout;
loop {
let status = service.query_status()?;
if status.current_state == ServiceState::Stopped {
return Ok(());
}
if Instant::now() >= deadline {
anyhow::bail!(
"{} service did not stop within {:?}; current state: {:?}",
mode.subcommand(),
timeout,
status.current_state
);
}
std::thread::sleep(Duration::from_millis(250));
}
}
#[cfg(target_os = "windows")]
fn uninstall_windows_service(mode: Mode) -> Result<()> {
use windows_service::service_manager::{ServiceManager, ServiceManagerAccess};
let service_manager =
ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)?;
delete_windows_service_if_exists(&service_manager, mode)
}
#[cfg(target_os = "windows")]
fn delete_windows_service_if_exists(
service_manager: &windows_service::service_manager::ServiceManager,
mode: Mode,
) -> Result<()> {
use windows_service::service::{ServiceAccess, ServiceState};
let access = ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE;
let service = match service_manager.open_service(mode.service_name(), access) {
Ok(service) => service,
Err(_) => return Ok(()),
};
if service
.query_status()
.map(|status| status.current_state != ServiceState::Stopped)
.unwrap_or(false)
{
let _ = service.stop();
}
service.delete()?;
drop(service);
for _ in 0..10 {
if service_manager
.open_service(mode.service_name(), ServiceAccess::QUERY_STATUS)
.is_err()
{
return Ok(());
}
std::thread::sleep(Duration::from_millis(500));
}
Ok(())
}
#[cfg(target_os = "windows")]
windows_service::define_windows_service!(ffi_service_main, windows_service_main);
#[cfg(target_os = "windows")]
static WINDOWS_SERVICE_MODE: std::sync::OnceLock<Mode> = std::sync::OnceLock::new();
#[cfg(target_os = "windows")]
pub fn run_windows_service(mode: Mode) -> Result<()> {
let _ = WINDOWS_SERVICE_MODE.set(mode);
windows_service::service_dispatcher::start(mode.service_name(), ffi_service_main)
.with_context(|| format!("failed to start Windows service {}", mode.service_name()))?;
Ok(())
}
#[cfg(not(target_os = "windows"))]
pub fn run_windows_service(mode: Mode) -> Result<()> {
let _ = mode;
Err(anyhow!(
"Windows service entrypoint is only supported on Windows"
))
}
#[cfg(target_os = "windows")]
fn windows_service_main(_arguments: Vec<std::ffi::OsString>) {
if let Err(err) = run_windows_service_inner() {
tracing::error!("Windows service failed: {err:#}");
}
}
#[cfg(target_os = "windows")]
fn run_windows_service_inner() -> Result<()> {
use std::sync::{Arc, Mutex};
use tokio::sync::oneshot;
use windows_service::service::{ServiceControl, ServiceControlAccept, ServiceState};
use windows_service::service_control_handler::{
self, ServiceControlHandlerResult, ServiceStatusHandle,
};
let mode = WINDOWS_SERVICE_MODE
.get()
.copied()
.ok_or_else(|| anyhow!("missing Windows service mode"))?;
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
let shutdown_tx = Arc::new(Mutex::new(Some(shutdown_tx)));
let status_handle_slot: Arc<Mutex<Option<ServiceStatusHandle>>> = Arc::new(Mutex::new(None));
let handler_shutdown_tx = Arc::clone(&shutdown_tx);
let handler_status = Arc::clone(&status_handle_slot);
let event_handler = move |control_event| -> ServiceControlHandlerResult {
match control_event {
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
ServiceControl::Stop => {
if let Some(status_handle) =
*handler_status.lock().unwrap_or_else(|e| e.into_inner())
{
let _ = status_handle.set_service_status(windows_status(
ServiceState::StopPending,
ServiceControlAccept::empty(),
));
}
if let Some(tx) = handler_shutdown_tx
.lock()
.unwrap_or_else(|e| e.into_inner())
.take()
{
let _ = tx.send(());
}
ServiceControlHandlerResult::NoError
}
_ => ServiceControlHandlerResult::NotImplemented,
}
};
let status_handle = service_control_handler::register(mode.service_name(), event_handler)?;
*status_handle_slot.lock().unwrap_or_else(|e| e.into_inner()) = Some(status_handle);
status_handle.set_service_status(windows_status(
ServiceState::Running,
ServiceControlAccept::STOP,
))?;
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.context("failed to start Tokio runtime for Windows service")?;
let config = crate::config::Config::load();
let result = runtime.block_on(async move {
let shutdown = async {
let _ = shutdown_rx.await;
};
match mode {
Mode::Proxy => crate::proxy_server::start_with_shutdown(config, shutdown).await,
Mode::Client => {
crate::client_server::start_with_shutdown(config.server.client_port, shutdown).await
}
}
});
status_handle.set_service_status(windows_status(
ServiceState::Stopped,
ServiceControlAccept::empty(),
))?;
result
}
#[cfg(target_os = "windows")]
fn windows_status(
current_state: windows_service::service::ServiceState,
controls_accepted: windows_service::service::ServiceControlAccept,
) -> windows_service::service::ServiceStatus {
windows_service::service::ServiceStatus {
service_type: windows_service::service::ServiceType::OWN_PROCESS,
current_state,
controls_accepted,
exit_code: windows_service::service::ServiceExitCode::Win32(0),
checkpoint: 0,
wait_hint: Duration::default(),
process_id: None,
}
}
pub fn logs(mode: Mode, follow: bool, lines: u32) -> Result<()> {
#[cfg(target_os = "linux")]
{
let n = lines.to_string();
let mut args = vec!["-u", mode.service_name(), "-n", &n];
if follow {
args.push("-f");
}
run_inherit("journalctl", &args)
}
#[cfg(target_os = "macos")]
{
let path = macos_stderr_log(mode);
if !std::path::Path::new(&path).exists() {
anyhow::bail!(
"no log file at {path} yet. The service may not have started or \
produced any output."
);
}
let n = lines.to_string();
let mut args = vec!["-n", &n];
if follow {
args.push("-f");
}
args.push(&path);
run_inherit("tail", &args)
}
#[cfg(target_os = "windows")]
{
let path = service_log_path(mode);
if !path.exists() {
anyhow::bail!(
"no log file at {} yet. The service may not have started or \
produced any output.",
path.display()
);
}
print_last_log_lines(&path, lines)?;
if follow {
follow_log_file(&path)?;
}
Ok(())
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
let _ = (mode, follow, lines);
Err(anyhow!("log viewing not supported on this OS"))
}
}
#[cfg(target_os = "windows")]
fn print_last_log_lines(path: &Path, line_count: u32) -> Result<()> {
if line_count == 0 {
return Ok(());
}
let tail = read_tail_for_lines(path, line_count)?;
let contents = String::from_utf8_lossy(&tail);
let lines: Vec<&str> = contents.lines().collect();
let start = lines.len().saturating_sub(line_count as usize);
for line in &lines[start..] {
println!("{line}");
}
Ok(())
}
#[cfg(target_os = "windows")]
fn read_tail_for_lines(path: &Path, line_count: u32) -> Result<Vec<u8>> {
const CHUNK_SIZE: u64 = 8192;
let mut file =
std::fs::File::open(path).with_context(|| format!("failed to open {}", path.display()))?;
let mut remaining = file
.seek(SeekFrom::End(0))
.with_context(|| format!("failed to seek {}", path.display()))?;
let mut newline_count = 0usize;
let mut chunks = VecDeque::new();
while remaining > 0 && newline_count <= line_count as usize {
let read_len = remaining.min(CHUNK_SIZE);
remaining -= read_len;
file.seek(SeekFrom::Start(remaining))
.with_context(|| format!("failed to seek {}", path.display()))?;
let mut chunk = vec![0; read_len as usize];
file.read_exact(&mut chunk)
.with_context(|| format!("failed to read {}", path.display()))?;
newline_count += chunk.iter().filter(|byte| **byte == b'\n').count();
chunks.push_front(chunk);
}
let total_len = chunks.iter().map(Vec::len).sum();
let mut tail = Vec::with_capacity(total_len);
for chunk in chunks {
tail.extend(chunk);
}
Ok(tail)
}
#[cfg(target_os = "windows")]
fn follow_log_file(path: &Path) -> Result<()> {
let mut file =
std::fs::File::open(path).with_context(|| format!("failed to open {}", path.display()))?;
file.seek(SeekFrom::End(0))
.with_context(|| format!("failed to seek {}", path.display()))?;
loop {
let mut chunk = String::new();
let bytes = file
.read_to_string(&mut chunk)
.with_context(|| format!("failed to read {}", path.display()))?;
if bytes > 0 {
print!("{chunk}");
std::io::stdout().flush().ok();
}
std::thread::sleep(Duration::from_millis(500));
}
}