1use 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}