Skip to main content

node_headless/
host.rs

1// #t(file: rust_hardcoded_url) user-facing product URLs in log messages
2
3//! `NodeCore` construction, event logging, and foreground driving.
4
5use std::sync::{Arc, atomic::AtomicBool};
6
7use wowlab_node::{
8    ConnectionStatus, NodeApplication, NodeConfig, NodeCore, NodeCoreEvent, NodeState,
9};
10
11use crate::{VERSION, shutdown};
12
13#[derive(Debug, thiserror::Error)]
14pub(crate) enum HostError {
15    #[error(transparent)]
16    Config(#[from] wowlab_node::NodeConfigError),
17    #[error(transparent)]
18    Core(#[from] wowlab_node::SentinelError),
19    #[error(transparent)]
20    EngineComposition(#[from] wowlab_engine_ports::ContentCatalogError),
21    #[error("failed to register shutdown handlers: {0}")]
22    Shutdown(#[from] std::io::Error),
23}
24
25pub(crate) fn run() -> Result<(), HostError> {
26    tracing::info!(
27        app_version = VERSION,
28        node_version = wowlab_node::VERSION,
29        engine_content_version = wowlab_engine_content::VERSION,
30        "Starting WoW Lab Node"
31    );
32
33    let config = NodeConfig::load()?;
34    let application = NodeApplication::new(env!("CARGO_PKG_NAME"), VERSION);
35    let catalog = wowlab_engine_ports::content_catalog()?;
36    let (mut core, mut events) = NodeCore::with_supabase(config, application, catalog)?;
37    let running = Arc::new(AtomicBool::new(true));
38
39    shutdown::install(&core, Arc::clone(&running))?;
40
41    print_status(&core);
42    core.drive_until_stopped(&mut events, &running, handle_event);
43
44    tracing::info!("Shutting down");
45
46    Ok(())
47}
48
49fn handle_event(event: &NodeCoreEvent, core: &NodeCore) {
50    match event {
51        NodeCoreEvent::StateChanged(state) => log_state(state),
52        NodeCoreEvent::ConnectionChanged(status) => {
53            let message = match status {
54                ConnectionStatus::Connecting => "Connecting",
55                ConnectionStatus::Connected => "Connected",
56                ConnectionStatus::Disconnected => "Disconnected",
57            };
58
59            tracing::info!(status = message, "Connection changed");
60        }
61        NodeCoreEvent::ChunkAssigned {
62            job_id,
63            chunk_index,
64            iterations,
65        } => tracing::info!(%job_id, chunk_index, iterations, "Chunk assigned"),
66        NodeCoreEvent::ChunkCompleted {
67            job_id,
68            chunk_index,
69            mean_dps,
70        } => tracing::info!(%job_id, chunk_index, mean_dps, "Chunk completed"),
71        NodeCoreEvent::ChunkFailed {
72            job_id,
73            chunk_index,
74            error,
75        } => tracing::error!(%job_id, chunk_index, %error, "Chunk failed"),
76        NodeCoreEvent::Error(error) => tracing::error!(%error, "Node error"),
77        NodeCoreEvent::UnlinkCompleted(result) => log_unlink_completion(result),
78    }
79
80    let stats = core.stats();
81
82    if stats.active_jobs > 0 || stats.completed_chunks > 0 {
83        tracing::debug!(
84            active_jobs = stats.active_jobs,
85            completed_chunks = stats.completed_chunks,
86            sims_per_second = stats.sims_per_second,
87            "Node statistics"
88        );
89    }
90}
91
92fn log_state(state: &NodeState) {
93    match state {
94        NodeState::Setup => {
95            tracing::error!("No claim token configured");
96            tracing::error!("Set NODE_CLAIM_TOKEN environment variable");
97            tracing::error!("Get your token at https://wowlab.gg/account/nodes");
98        }
99        NodeState::Verifying => tracing::info!("Verifying node..."),
100        NodeState::Registering => tracing::info!("Registering with server..."),
101        NodeState::Running => tracing::info!("Node running"),
102        NodeState::NotFound => {
103            tracing::error!("Node not found in database");
104            tracing::error!("Re-register with your claim token");
105            tracing::error!("Set NODE_CLAIM_TOKEN environment variable");
106            tracing::error!("Get your token at https://wowlab.gg/account/nodes");
107        }
108        NodeState::Unavailable => {
109            tracing::error!("Server unavailable");
110            tracing::error!("Check https://wowlab.gg/status for updates");
111        }
112        NodeState::Unlinking => tracing::info!("Unlinking node..."),
113        NodeState::Unlinked => tracing::info!("Node unlinked"),
114    }
115}
116
117fn log_unlink_completion(result: &Result<wowlab_node::UnlinkOutcome, wowlab_node::UnlinkError>) {
118    match result {
119        Ok(outcome) => tracing::info!(
120            remote = ?outcome.remote(),
121            local_identity = ?outcome.local_identity(),
122            "Node unlink completed"
123        ),
124        Err(error) => tracing::error!(%error, "Node unlink failed"),
125    }
126}
127
128fn print_status(core: &NodeCore) {
129    let stats = core.stats();
130
131    tracing::info!(name = core.node_name(), "Node identity");
132    tracing::info!(
133        total = stats.total_cores,
134        max_parallel = stats.max_workers,
135        "Node cores"
136    );
137
138    match core.state() {
139        NodeState::Setup => {
140            tracing::error!("Status: Setup required");
141            tracing::error!("Set NODE_CLAIM_TOKEN=wlab_claim_xxx environment variable");
142        }
143        NodeState::Verifying => tracing::info!("Status: Verifying..."),
144        NodeState::Registering => tracing::info!("Status: Registering..."),
145        NodeState::Running => tracing::info!("Status: Ready"),
146        NodeState::NotFound => {
147            tracing::error!("Status: Node not found");
148            tracing::error!("Re-register with NODE_CLAIM_TOKEN=wlab_claim_xxx");
149        }
150        NodeState::Unavailable => {
151            tracing::error!("Status: Server unavailable");
152            tracing::error!("Check https://wowlab.gg/status for updates");
153        }
154        NodeState::Unlinking => tracing::info!("Status: Unlinking..."),
155        NodeState::Unlinked => tracing::info!("Status: Unlinked"),
156    }
157}