use crate::daemon::ipc::socket_name_string;
use crate::daemon::server::DaemonState;
use crate::errors::CliError;
use std::path::PathBuf;
fn cache_dir() -> PathBuf {
if let Ok(dir) = std::env::var("XDG_CACHE_HOME") {
return PathBuf::from(dir).join("steamroom");
}
#[cfg(windows)]
if let Some(dir) = dirs_next::cache_dir() {
return dir.join("steamroom");
}
if let Some(home) = std::env::var_os("HOME") {
return PathBuf::from(home).join(".cache").join("steamroom");
}
PathBuf::from("/tmp").join(format!("steamroom-{}", unix_uid()))
}
pub fn pid_file_path() -> PathBuf {
cache_dir().join("daemon.pid")
}
#[cfg(unix)]
fn unix_uid() -> u32 {
unsafe { libc::getuid() }
}
#[cfg(not(unix))]
fn unix_uid() -> u32 {
0
}
pub fn write_pid_file(pid: u32) -> Result<(), CliError> {
let path = pid_file_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(CliError::Io)?;
}
std::fs::write(&path, format!("{pid}\n")).map_err(CliError::Io)
}
pub fn read_pid_file() -> Result<u32, CliError> {
let data = std::fs::read_to_string(pid_file_path()).map_err(CliError::Io)?;
data.trim()
.parse::<u32>()
.map_err(|e| CliError::MalformedFrame(format!("pid file: {e}")))
}
pub fn remove_pid_file() {
let _ = std::fs::remove_file(pid_file_path());
}
pub fn render_daemon_info() {
let path = pid_file_path();
println!("pid file: {}", path.display());
match read_pid_file() {
Ok(pid) => println!("pid : {pid}"),
Err(_) => println!("pid : (none; no daemon recorded)"),
}
println!("socket : {}", socket_name_string());
println!("stop : steamroom daemon stop");
}
pub fn log_path() -> PathBuf {
cache_dir().join("daemon.log")
}
pub fn recent_history_path() -> PathBuf {
cache_dir().join("recent.json")
}
pub async fn load_recent_history(state: &DaemonState) {
let path = recent_history_path();
let Ok(data) = std::fs::read_to_string(&path) else {
return;
};
let Ok(records) = serde_json::from_str::<Vec<crate::daemon::proto::JobRecord>>(&data) else {
tracing::warn!("recent history at {} is corrupt; ignoring", path.display());
return;
};
let mut recent = state.recent.lock().await;
for r in records {
recent.push(r);
}
}
pub async fn save_recent_history(state: &DaemonState) {
let path = recent_history_path();
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let records: Vec<_> = state.recent.lock().await.iter().cloned().collect();
match serde_json::to_string(&records) {
Ok(json) => {
if let Err(e) = std::fs::write(&path, json) {
tracing::warn!("failed to write recent history to {}: {e}", path.display());
}
}
Err(e) => {
tracing::warn!("failed to serialize recent history: {e}");
}
}
}
pub fn detach_and_exec_resume(username: &str, log_path: &std::path::Path) -> Result<(), CliError> {
use std::process::Command;
use std::process::Stdio;
if let Some(parent) = log_path.parent() {
std::fs::create_dir_all(parent).map_err(CliError::Io)?;
}
let log_out = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(log_path)
.map_err(CliError::Io)?;
let log_err = log_out.try_clone().map_err(CliError::Io)?;
let exe = std::env::current_exe().map_err(CliError::Io)?;
let mut cmd = Command::new(exe);
cmd.arg("--daemon-resume")
.arg(username)
.arg("daemon")
.arg("start");
cmd.stdin(Stdio::null());
cmd.stdout(Stdio::from(log_out));
cmd.stderr(Stdio::from(log_err));
#[cfg(unix)]
unsafe {
use std::os::unix::process::CommandExt;
cmd.pre_exec(|| {
if libc::setsid() == -1 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
const DETACHED_PROCESS: u32 = 0x0000_0008;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
cmd.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP);
}
let child = cmd.spawn().map_err(CliError::Io)?;
let pid = child.id();
std::mem::forget(child);
if !wait_for_socket(std::time::Duration::from_secs(5)) {
eprintln!("steamroom daemon (pid {pid}) failed to bind socket within 5s");
eprintln!("check the log for the failure:");
eprintln!(" {}", log_path.display());
std::process::exit(1);
}
#[cfg(windows)]
let manual_kill = format!("taskkill /PID {pid} /F");
#[cfg(not(windows))]
let manual_kill = format!("kill {pid}");
println!("steamroom daemon started");
println!(" pid : {pid}");
println!(" socket : {}", socket_name_string());
println!(" stop : steamroom daemon stop (or: {manual_kill})");
println!(" logs : {}", log_path.display());
std::process::exit(0);
}
fn wait_for_socket(timeout: std::time::Duration) -> bool {
let rt = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(rt) => rt,
Err(_) => return false,
};
rt.block_on(async move {
let deadline = std::time::Instant::now() + timeout;
let mut delay = std::time::Duration::from_millis(50);
while std::time::Instant::now() < deadline {
if crate::daemon::ipc::probe_peer().await.is_ok() {
return true;
}
tokio::time::sleep(delay).await;
delay = (delay * 2).min(std::time::Duration::from_millis(200));
}
false
})
}
use crate::cli::Cli;
use crate::commands::shared;
use crate::daemon::ipc;
use crate::daemon::server::handle_connection;
use crate::daemon::server::worker_loop;
use crate::daemon::tracing_layer::JobIdAttachmentInstaller;
use crate::daemon::tracing_layer::JobScopedLogLayer;
pub async fn launch_daemon_authenticate(cli: &Cli) -> Result<Option<String>, CliError> {
if ipc::probe_peer().await.is_ok() {
return Err(CliError::DaemonAlreadyRunning);
}
if let Ok(stale_pid) = read_pid_file()
&& !pid_is_alive(stale_pid)
{
remove_pid_file();
}
let auth = &cli.auth;
let has_explicit_auth = auth.username.is_some()
|| auth.password.is_some()
|| auth.qr
|| auth.use_steam_token
|| auth.device_name.is_some();
if !has_explicit_auth {
return Ok(None);
}
let client = shared::connect_and_login(auth, None).await?;
let username = auth
.username
.clone()
.or_else(|| shared::detect_steam_user().map(|(u, _)| u))
.ok_or(CliError::InteractiveAuthRequired)?;
drop(client);
Ok(Some(username))
}
#[cfg(unix)]
fn pid_is_alive(pid: u32) -> bool {
let rc = unsafe { libc::kill(pid as libc::pid_t, 0) };
if rc == 0 {
return true;
}
std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
}
#[cfg(not(unix))]
fn pid_is_alive(_pid: u32) -> bool {
true
}
pub async fn serve_resumed(username: String, _cli: Cli) -> Result<(), CliError> {
let (initial_client, preferred_user) = if username.is_empty() {
(None, None)
} else {
let token = shared::load_saved_token(&username).ok_or(CliError::InteractiveAuthRequired)?;
let client = steamroom_client::login::LoginBuilder::new()
.device_name("steamroom")
.with_refresh_token(&username, &token)
.login()
.await?;
(Some(client), Some(username.clone()))
};
let listener = ipc::bind_listener().await?;
let pid = std::process::id();
write_pid_file(pid)?;
let account_label = if username.is_empty() {
None
} else {
Some(username.clone())
};
let state = DaemonState::new(account_label, pid, unix_now_lifecycle());
load_recent_history(&state).await;
use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
let _ = tracing_subscriber::registry()
.with(crate::commands::shared::log_filter(LevelFilter::INFO))
.with(tracing_subscriber::fmt::layer())
.with(JobIdAttachmentInstaller)
.with(JobScopedLogLayer::new(state.events.clone()))
.try_init();
let worker_state = state.clone();
let worker_client = initial_client;
let worker_user = preferred_user;
let mut worker_task = Some(tokio::spawn(async move {
worker_loop(worker_state, worker_client, worker_user).await;
}));
crate::daemon::server::spawn_replay_collector(state.clone());
loop {
let join_arm = match worker_task {
Some(ref mut h) => h,
None => break,
};
tokio::select! {
_ = state.shutdown.cancelled() => break,
res = join_arm => {
match res {
Ok(()) => tracing::info!("worker_loop exited"),
Err(ref e) if e.is_panic() => tracing::error!("worker_loop panicked: {e}"),
Err(ref e) => tracing::warn!("worker_loop join error: {e}"),
}
worker_task = None;
state.shutdown.cancel();
break;
}
res = ipc::accept(&listener) => match res {
Ok(stream) => {
let st = state.clone();
tokio::spawn(handle_connection(st, stream));
}
Err(e) => {
tracing::warn!("accept failed: {e}");
}
}
}
}
let _ = state.events.send(crate::daemon::proto::Event::Log {
job_id: None,
level: crate::daemon::proto::LogLevel::Info,
target: "daemon".into(),
message: "shutting down".into(),
});
if let Some(h) = worker_task {
h.abort();
let _ = h.await;
}
save_recent_history(&state).await;
remove_pid_file();
Ok(())
}
fn unix_now_lifecycle() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}