#[allow(dead_code)]
mod build_publish;
mod config;
mod control;
mod machine;
mod peer;
mod plugin;
mod web;
use std::path::PathBuf;
use std::sync::OnceLock;
use anyhow::{Context, Result};
use config::Config;
static STATE_DIR: OnceLock<PathBuf> = OnceLock::new();
fn main() -> Result<()> {
if std::env::args().skip(1).any(|argument| argument == "--version") {
println!("wsb-daemon {}", wsb::get_version());
return Ok(());
}
let config = Config::from_args(std::env::args().skip(1)).map_err(|e| anyhow::anyhow!(e))?;
{
use tracing_subscriber::util::SubscriberInitExt;
subscriber(config.debug).init();
}
let machine_dir = wsb::daemon::machine_dir().map_err(|e| anyhow::anyhow!(e))?;
let _ = STATE_DIR.set(machine_dir.clone());
std::env::set_current_dir("/").context("cannot leave the starting directory")?;
refuse_if_already_running(&machine_dir)?;
tracing::info!(
machine_dir = %machine_dir.display(),
listen = %config.listen,
port = config.http_port,
peer_listen = %config.peer_listen,
peer_port = config.peer_port,
version = wsb::get_version(),
"wsb-daemon starting"
);
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.context("cannot start the daemon's runtime")?
.block_on(serve(config, machine_dir))
}
async fn serve(config: Config, machine_dir: PathBuf) -> Result<()> {
let machine = machine::MachineState::new(
machine_dir.clone(),
config.listen,
config.http_port,
config.peer_listen,
config.peer_port,
)?;
match wsb_prj::Prj::from_env() {
Ok(prj) => {
for line in prj.migrate_project_state().map_err(|e| anyhow::anyhow!(e))? {
tracing::info!("{line}");
}
}
Err(wsb_prj::PrjError::NotSet) => {}
Err(error) => return Err(anyhow::anyhow!(error)),
}
for root in machine.forget_contained_worktrees().map_err(|e| anyhow::anyhow!(e))? {
tracing::info!(
root = %root.display(),
"forgot a recorded worktree that is inside another one"
);
}
if let Some(root) = &config.workspace_root {
match machine.resolve(root).await {
Ok(runtime) => {
web::spawn_worktree_services_once(&runtime);
tracing::info!(
worktree = %runtime.registered.id,
"opened the worktree this daemon was started in"
)
}
Err(error) => tracing::warn!(
%error,
root = %root.display(),
"the worktree named at startup could not be opened"
),
}
}
let (bound, is_bound) = tokio::sync::oneshot::channel();
let server = tokio::spawn(web::start_server(machine.clone(), config.parent_pid, bound));
let http_bound = match is_bound.await {
Ok(address) => address,
Err(_) => {
let failure = server
.await
.context("the workbench server ended before it started")?;
failure.context("the workbench server could not start")?;
anyhow::bail!("the workbench server ended before it started, without an error");
}
};
let peer_listener = if config.peer_port == 0 {
tracing::info!("peer listening is off (--peer-port 0)");
None
} else {
match machine.peers.listen() {
Ok(listener) => Some(listener),
Err(error) => {
tracing::error!(%error, "peers cannot reach this machine");
None
}
}
};
let peer_bound = match &peer_listener {
Some(listener) => listener
.local_addr()
.context("the peer listener has no address")?
.port(),
None => 0,
};
if let Some(listener) = peer_listener {
peer::service::spawn_accept_loop(machine.clone(), listener);
peer::coordinator::spawn_reconcile_loop(machine.clone(), tokio::runtime::Handle::current());
}
wsb::daemon::MachineRendezvous {
version: wsb::get_version().to_string(),
pid: std::process::id(),
http_address: config.listen,
http_port: http_bound.port(),
machine_id: machine.identity.id().to_string(),
machine_name: machine.identity.name().to_string(),
peer_address: config.peer_listen,
peer_port: peer_bound,
started_at: machine.started_at,
control_token: machine.control_token.clone(),
}
.write(&machine_dir)
.map_err(|e| anyhow::anyhow!(e))?;
let stopping = tokio::spawn(async {
if tokio::signal::ctrl_c().await.is_ok() {
remove_rendezvous();
tracing::info!("interrupted; stopping");
std::process::exit(0);
}
});
let outcome = server.await.context("the workbench server ended")?;
stopping.abort();
remove_rendezvous();
outcome.context("the workbench server failed")
}
fn subscriber(debug: bool) -> impl tracing::Subscriber + Send + Sync {
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::Layer;
let default_level = if debug { "debug" } else { "info" };
let console = tracing_subscriber::fmt::layer().with_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| {
tracing_subscriber::EnvFilter::new(format!("{default_level},refinery=warn,refinery_core=warn"))
}),
);
tracing_subscriber::registry()
.with(console)
.with(wsb::logging::BroadcastLayer.with_filter(if debug {
tracing_subscriber::filter::LevelFilter::DEBUG
} else {
tracing_subscriber::filter::LevelFilter::INFO
}))
}
pub(crate) fn remove_rendezvous() {
if let Some(machine_dir) = STATE_DIR.get() {
if let Err(error) = wsb::daemon::MachineRendezvous::remove_if_published_by(machine_dir, std::process::id()) {
tracing::warn!(%error, "could not remove the rendezvous file");
}
}
}
fn refuse_if_already_running(machine_dir: &std::path::Path) -> Result<()> {
let Some(existing) =
wsb::daemon::MachineRendezvous::read(machine_dir).map_err(|e| anyhow::anyhow!(e))?
else {
if let Some(published) =
wsb::daemon::MachineRendezvous::published_by(machine_dir).map_err(|e| anyhow::anyhow!(e))?
{
if wsb::ops::process::is_alive(published.pid) {
anyhow::bail!(
"wsb-daemon {} is already serving this machine (pid {}), and this build \
cannot read the file it published.\n One daemon serves one machine. Stop it first:\n wsb daemon stop",
published.version,
published.pid
);
}
wsb::daemon::MachineRendezvous::remove_if_published_by(machine_dir, published.pid)
.map_err(|e| anyhow::anyhow!(e))?;
}
return Ok(());
};
let client = wsb::daemon::Client::from_rendezvous(&existing)
.map_err(|e| anyhow::anyhow!(e))?;
match client.info() {
Ok(info) => anyhow::bail!(
"wsb-daemon {} is already serving this machine on port {} (pid {}).\n \
One daemon serves one machine. Stop it first:\n \
wsb daemon stop",
info.version,
info.http_port,
info.pid
),
Err(_) => {
if wsb::daemon::launch::still_serving(&existing) {
anyhow::bail!(
"wsb-daemon (pid {}) is serving this machine on port {} but did not answer: it \
is busy, not gone.\n One daemon serves one machine. Wait for it, or stop it \
first:\n wsb daemon stop",
existing.pid,
existing.http_port
);
}
wsb::daemon::MachineRendezvous::remove_if_published_by(machine_dir, existing.pid)
.map_err(|e| anyhow::anyhow!(e))?;
Ok(())
}
}
}
#[cfg(test)]
#[path = "main_tests.rs"]
mod tests;