use std::path::PathBuf;
use std::time::Duration;
use anyhow::{Context, Result, bail};
use crate::client::{self, Client};
use crate::ipc::{self, Request, Response, ServerMessage, VersionedRequest};
use crate::server;
fn versioned(request: Request) -> VersionedRequest {
VersionedRequest {
version: ipc::PROTOCOL_VERSION.to_string(),
request,
}
}
fn format_duration(secs: u64) -> String {
if secs < 60 {
return format!("{secs}s");
}
let days = secs / 86400;
let hours = (secs % 86400) / 3600;
let mins = (secs % 3600) / 60;
let s = secs % 60;
let mut parts = Vec::new();
if days > 0 {
parts.push(format!("{days}d"));
}
if hours > 0 {
parts.push(format!("{hours}h"));
}
if mins > 0 {
parts.push(format!("{mins}m"));
}
if s > 0 {
parts.push(format!("{s}s"));
}
parts.join(" ")
}
pub async fn start(
host: &str,
auth_token_file: &std::path::Path,
timeout: u64,
json: bool,
) -> Result<()> {
if client::is_server_alive(host) {
let pid = client::read_server_pid(host).unwrap_or(0);
if json {
println!(
"{}",
serde_json::json!({"status": "already_running", "pid": pid})
);
} else {
println!("Server already running for {host} (PID {pid}).");
}
return Ok(());
}
let _ = std::fs::remove_file(server::pid_path(host));
let _ = std::fs::remove_file(server::socket_path(host));
let client = Client::new(host, auth_token_file, timeout);
let resp = client.send_request(Request::Status).await?;
match resp {
Response::Ok { data } => {
let pid = data.get("pid").and_then(|v| v.as_u64()).unwrap_or(0);
if json {
println!("{}", serde_json::json!({"status": "started", "pid": pid}));
} else {
println!("Server started for {host} (PID {pid}).");
}
}
Response::Error { message } => bail!("server start failed: {message}"),
Response::TvDisconnected { message } => bail!("TV not reachable: {message}"),
}
Ok(())
}
pub async fn stop(host: &str, json: bool) -> Result<()> {
if !client::is_server_alive(host) {
if json {
println!("{}", serde_json::json!({"status": "not_running"}));
} else {
println!("No server running for {host}.");
}
return Ok(());
}
let sock = server::socket_path(host);
let stream = tokio::net::UnixStream::connect(&sock)
.await
.context("failed to connect to server")?;
let (mut reader, mut writer) = stream.into_split();
ipc::write_message(&mut writer, &versioned(Request::Shutdown)).await?;
let _ = tokio::time::timeout(
Duration::from_secs(5),
ipc::read_message::<_, ServerMessage>(&mut reader),
)
.await;
if json {
println!("{}", serde_json::json!({"status": "stopped"}));
} else {
println!("Server for {host} stopped.");
}
Ok(())
}
pub async fn restart(
host: &str,
auth_token_file: &std::path::Path,
timeout: u64,
json: bool,
) -> Result<()> {
if client::is_server_alive(host) {
let sock = server::socket_path(host);
if let Ok(stream) = tokio::net::UnixStream::connect(&sock).await {
let (mut reader, mut writer) = stream.into_split();
let _ = ipc::write_message(&mut writer, &versioned(Request::Shutdown)).await;
let _ = tokio::time::timeout(
Duration::from_secs(5),
ipc::read_message::<_, ServerMessage>(&mut reader),
)
.await;
}
for _ in 0..20 {
if !client::is_server_alive(host) {
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
start(host, auth_token_file, timeout, json).await
}
pub async fn status(host: &str, json: bool) -> Result<()> {
if !client::is_server_alive(host) {
if json {
println!(
"{}",
serde_json::json!({"status": "not_running", "host": host})
);
} else {
println!("No server running for {host}.");
}
return Ok(());
}
let sock = server::socket_path(host);
let stream = tokio::net::UnixStream::connect(&sock)
.await
.context("failed to connect to server")?;
let (mut reader, mut writer) = stream.into_split();
ipc::write_message(&mut writer, &versioned(Request::Status)).await?;
match tokio::time::timeout(
Duration::from_secs(5),
ipc::read_message::<_, ServerMessage>(&mut reader),
)
.await
{
Ok(Ok(ServerMessage::Response(Response::Ok { data }))) => {
if json {
println!("{}", serde_json::to_string_pretty(&data)?);
} else {
let pid = data.get("pid").and_then(|v| v.as_u64()).unwrap_or(0);
let host_val = data
.get("host")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let log = data
.get("log_path")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let uptime = data
.get("server_uptime_secs")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let tv_connected = data
.get("tv_connected")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let conn_uptime = data.get("connection_uptime_secs").and_then(|v| v.as_u64());
let last_error = data.get("last_error").and_then(|v| v.as_str());
println!("PID: {pid}");
println!("Host: {host_val}");
println!("Log: {log}");
println!("Server uptime: {}", format_duration(uptime));
println!(
"TV connected: {}",
if tv_connected { "yes" } else { "no" }
);
if let Some(cu) = conn_uptime {
println!("Connection uptime: {}", format_duration(cu));
}
if let Some(err) = last_error {
println!("Last error: {err}");
}
}
}
Ok(Ok(ServerMessage::Response(Response::Error { message }))) => {
bail!("server error: {message}");
}
_ => bail!("unexpected response from server"),
}
Ok(())
}
pub async fn tail_logs(host: &str) -> Result<()> {
let log_path = get_log_path_from_server(host)
.await
.unwrap_or_else(|| server::log_path(host));
if !log_path.exists() {
bail!(
"Log file does not exist: {}\nIs a server running for {host}? Try: framesmith {host} server start",
log_path.display()
);
}
eprintln!("Tailing {}", log_path.display());
use std::os::unix::process::CommandExt;
let err = std::process::Command::new("tail")
.args(["-F", &log_path.to_string_lossy()])
.exec();
bail!("failed to exec tail: {err}");
}
async fn get_log_path_from_server(host: &str) -> Option<PathBuf> {
if !client::is_server_alive(host) {
return None;
}
let sock = server::socket_path(host);
let stream = tokio::net::UnixStream::connect(&sock).await.ok()?;
let (mut reader, mut writer) = stream.into_split();
ipc::write_message(&mut writer, &versioned(Request::Status))
.await
.ok()?;
let result = tokio::time::timeout(
Duration::from_secs(5),
ipc::read_message::<_, ServerMessage>(&mut reader),
)
.await;
if let Ok(Ok(ServerMessage::Response(Response::Ok { data }))) = result {
data.get("log_path")
.and_then(|v| v.as_str())
.map(PathBuf::from)
} else {
None
}
}