veloren_server_cli/
main.rs

1#![deny(unsafe_code)]
2#![deny(clippy::clone_on_ref_ptr)]
3
4#[cfg(all(
5    target_os = "windows",
6    not(feature = "hot-agent"),
7    not(feature = "hot-site"),
8))]
9#[global_allocator]
10static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
11
12/// `server-cli` interface commands not to be confused with the commands sent
13/// from the client to the server
14mod cli;
15mod settings;
16mod shutdown_coordinator;
17mod tui_runner;
18mod tuilog;
19mod web;
20use crate::{
21    cli::{
22        Admin, ArgvApp, ArgvCommand, BenchParams, Message, MessageReturn, SharedCommand, Shutdown,
23    },
24    settings::Settings,
25    shutdown_coordinator::ShutdownCoordinator,
26    tui_runner::Tui,
27    tuilog::TuiLog,
28};
29use common::{
30    clock::Clock,
31    comp::{ChatType, Player},
32    consts::MIN_RECOMMENDED_TOKIO_THREADS,
33};
34use common_base::span;
35use core::sync::atomic::{AtomicUsize, Ordering};
36use rand::distr::SampleString;
37use server::{Event, Input, Server, persistence::DatabaseSettings, settings::Protocol};
38use std::{
39    io,
40    sync::{Arc, atomic::AtomicBool},
41    time::{Duration, Instant},
42};
43use tokio::sync::Notify;
44use tracing::{info, trace};
45
46lazy_static::lazy_static! {
47    pub static ref LOG: TuiLog<'static> = TuiLog::default();
48}
49const TPS: u64 = 30;
50
51fn main() -> io::Result<()> {
52    #[cfg(feature = "tracy")]
53    common_base::tracy_client::Client::start();
54
55    use clap::Parser;
56    let app = ArgvApp::parse();
57
58    let basic = !app.tui || app.command.is_some();
59    let noninteractive = app.non_interactive;
60    let no_auth = app.no_auth;
61    let sql_log_mode = app.sql_log_mode;
62
63    // noninteractive implies basic
64    let basic = basic || noninteractive;
65
66    let shutdown_signal = Arc::new(AtomicBool::new(false));
67
68    let (_guards, _guards2) = if basic {
69        (Vec::new(), common_frontend::init_stdout(None))
70    } else {
71        (common_frontend::init(None, &|| LOG.clone()), Vec::new())
72    };
73
74    // Load settings
75    let settings = settings::Settings::load();
76
77    #[cfg(any(target_os = "linux", target_os = "macos"))]
78    {
79        for signal in &settings.shutdown_signals {
80            let _ = signal_hook::flag::register(signal.to_signal(), Arc::clone(&shutdown_signal));
81        }
82    }
83
84    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
85    if !settings.shutdown_signals.is_empty() {
86        tracing::warn!(
87            "Server configuration contains shutdown signals, but your platform does not support \
88             them"
89        );
90    }
91
92    // Determine folder to save server data in
93    let server_data_dir = {
94        let mut path = common_base::userdata_dir_workspace!();
95        info!("Using userdata folder at {}", path.display());
96        path.push(server::DEFAULT_DATA_DIR_NAME);
97        path
98    };
99
100    // We don't need that many threads in the async pool, at least 2 but generally
101    // 25% of all available will do
102    // TODO: evaluate std::thread::available_concurrency as a num_cpus replacement
103    let runtime = Arc::new(
104        tokio::runtime::Builder::new_multi_thread()
105            .enable_all()
106            .worker_threads((num_cpus::get() / 4).max(MIN_RECOMMENDED_TOKIO_THREADS))
107            .thread_name_fn(|| {
108                static ATOMIC_ID: AtomicUsize = AtomicUsize::new(0);
109                let id = ATOMIC_ID.fetch_add(1, Ordering::SeqCst);
110                format!("tokio-server-{}", id)
111            })
112            .build()
113            .unwrap(),
114    );
115
116    #[cfg(feature = "hot-agent")]
117    {
118        agent::init();
119    }
120    #[cfg(feature = "hot-site")]
121    {
122        world::init();
123    }
124
125    // Load server settings
126    let mut server_settings = server::Settings::load(&server_data_dir);
127    let mut editable_settings = server::EditableSettings::load(&server_data_dir);
128
129    // Apply no_auth modifier to the settings
130    if no_auth {
131        server_settings.auth_server_address = None;
132    }
133
134    // Relative to data_dir
135    const PERSISTENCE_DB_DIR: &str = "saves";
136
137    let database_settings = DatabaseSettings {
138        db_dir: server_data_dir.join(PERSISTENCE_DB_DIR),
139        sql_log_mode,
140    };
141
142    let mut bench = None;
143    if let Some(command) = app.command {
144        match command {
145            ArgvCommand::Shared(SharedCommand::Admin { command }) => {
146                let login_provider = server::login_provider::LoginProvider::new(
147                    server_settings.auth_server_address,
148                    runtime,
149                );
150
151                return match command {
152                    Admin::Add { username, role } => {
153                        // FIXME: Currently the UUID can get returned even if the file didn't
154                        // change, so this can't be relied on as an error
155                        // code; moreover, we do nothing with the UUID
156                        // returned in the success case.  Fix the underlying function to return
157                        // enough information that we can reliably return an error code.
158                        let _ = server::add_admin(
159                            &username,
160                            role,
161                            &login_provider,
162                            &mut editable_settings,
163                            &server_data_dir,
164                        );
165                        Ok(())
166                    },
167                    Admin::Remove { username } => {
168                        // FIXME: Currently the UUID can get returned even if the file didn't
169                        // change, so this can't be relied on as an error
170                        // code; moreover, we do nothing with the UUID
171                        // returned in the success case.  Fix the underlying function to return
172                        // enough information that we can reliably return an error code.
173                        let _ = server::remove_admin(
174                            &username,
175                            &login_provider,
176                            &mut editable_settings,
177                            &server_data_dir,
178                        );
179                        Ok(())
180                    },
181                };
182            },
183            ArgvCommand::Bench(params) => {
184                bench = Some(params);
185                // If we are trying to benchmark, don't limit the server view distance.
186                server_settings.max_view_distance = None;
187                // TODO: add setting to adjust wildlife spawn density, note I
188                // tried but Index setup makes it a bit
189                // annoying, might require a more involved refactor to get
190                // working nicely
191            },
192        };
193    }
194
195    // Panic hook to ensure that console mode is set back correctly if in non-basic
196    // mode
197    if !basic {
198        let hook = std::panic::take_hook();
199        std::panic::set_hook(Box::new(move |info| {
200            Tui::shutdown(basic);
201            hook(info);
202        }));
203    }
204
205    let tui = (!noninteractive).then(|| Tui::run(basic));
206
207    info!("Starting server...");
208
209    let protocols_and_addresses = server_settings.gameserver_protocols.clone();
210    let web_port = &settings.web_address.port();
211    // Create server
212    #[cfg_attr(not(feature = "worldgen"), expect(unused_mut))]
213    let mut server = Server::new(
214        server_settings,
215        editable_settings,
216        database_settings,
217        &server_data_dir,
218        &|_| {},
219        Arc::clone(&runtime),
220    )
221    .expect("Failed to create server instance!");
222
223    let registry = Arc::clone(server.metrics_registry());
224    let chat = server.chat_cache().clone();
225    let metrics_shutdown = Arc::new(Notify::new());
226    let metrics_shutdown_clone = Arc::clone(&metrics_shutdown);
227    let web_chat_secret = settings.web_chat_secret.clone();
228    let ui_api_secret = settings.ui_api_secret.clone().unwrap_or_else(|| {
229        // when no secret is provided we generate one that we distribute via the /ui
230        // endpoint
231        use rand::distr::Alphanumeric;
232        Alphanumeric.sample_string(&mut rand::rng(), 32)
233    });
234
235    let (web_ui_request_s, web_ui_request_r) = tokio::sync::mpsc::channel(1000);
236
237    runtime.spawn(async move {
238        web::run(
239            registry,
240            chat,
241            web_chat_secret,
242            ui_api_secret,
243            web_ui_request_s,
244            settings.web_address,
245            metrics_shutdown_clone.notified(),
246        )
247        .await
248    });
249
250    // Collect addresses that the server is listening to log.
251    let gameserver_addresses = protocols_and_addresses
252        .into_iter()
253        .map(|protocol| match protocol {
254            Protocol::Tcp { address } => ("TCP", address),
255            Protocol::Quic {
256                address,
257                cert_file_path: _,
258                key_file_path: _,
259            } => ("QUIC", address),
260        });
261
262    info!(
263        ?web_port,
264        ?gameserver_addresses,
265        "Server is ready to accept connections."
266    );
267
268    #[cfg(feature = "worldgen")]
269    if let Some(bench) = bench {
270        server.create_centered_persister(bench.view_distance);
271    }
272
273    server_loop(
274        server,
275        bench,
276        settings,
277        tui,
278        web_ui_request_r,
279        shutdown_signal,
280    )?;
281
282    metrics_shutdown.notify_one();
283
284    Ok(())
285}
286
287fn server_loop(
288    mut server: Server,
289    bench: Option<BenchParams>,
290    settings: Settings,
291    tui: Option<Tui>,
292    mut web_ui_request_r: tokio::sync::mpsc::Receiver<(
293        Message,
294        tokio::sync::oneshot::Sender<MessageReturn>,
295    )>,
296    shutdown_signal: Arc<AtomicBool>,
297) -> io::Result<()> {
298    // Set up an fps clock
299    let mut clock = Clock::new(Duration::from_secs_f64(1.0 / TPS as f64));
300    let mut shutdown_coordinator = ShutdownCoordinator::new(Arc::clone(&shutdown_signal));
301    let mut bench_exit_time = None;
302
303    let mut tick_no = 0u64;
304    'outer: loop {
305        span!(guard, "work");
306        if let Some(bench) = bench {
307            if let Some(t) = bench_exit_time {
308                if Instant::now() > t {
309                    break;
310                }
311            } else if tick_no != 0 && !server.chunks_pending() {
312                println!("Chunk loading complete");
313                bench_exit_time = Some(Instant::now() + Duration::from_secs(bench.duration.into()));
314            }
315        };
316
317        tick_no += 1;
318        // Terminate the server if instructed to do so by the shutdown coordinator
319        if shutdown_coordinator.check(&mut server, &settings) {
320            break;
321        }
322
323        let events = server
324            .tick(Input::default(), clock.dt())
325            .expect("Failed to tick server");
326
327        for event in events {
328            match event {
329                Event::ClientConnected { entity: _ } => info!("Client connected!"),
330                Event::ClientDisconnected { entity: _ } => info!("Client disconnected!"),
331                Event::Chat { entity: _, msg } => info!("[Client] {}", msg),
332            }
333        }
334
335        // Clean up the server after a tick.
336        server.cleanup();
337
338        if tick_no.rem_euclid(1000) == 0 {
339            trace!(?tick_no, "keepalive")
340        }
341
342        let mut handle_msg = |msg, response: tokio::sync::oneshot::Sender<MessageReturn>| {
343            use specs::{Join, WorldExt};
344            match msg {
345                Message::Shutdown {
346                    command: Shutdown::Cancel,
347                } => shutdown_coordinator.abort_shutdown(&mut server),
348                Message::Shutdown {
349                    command: Shutdown::Graceful { seconds, reason },
350                } => {
351                    shutdown_coordinator.initiate_shutdown(
352                        &mut server,
353                        Duration::from_secs(seconds),
354                        reason,
355                    );
356                },
357                Message::Shutdown {
358                    command: Shutdown::Immediate,
359                } => {
360                    return true;
361                },
362                Message::Shared(SharedCommand::Admin {
363                    command: Admin::Add { username, role },
364                }) => {
365                    server.add_admin(&username, role);
366                },
367                Message::Shared(SharedCommand::Admin {
368                    command: Admin::Remove { username },
369                }) => {
370                    server.remove_admin(&username);
371                },
372                #[cfg(feature = "worldgen")]
373                Message::LoadArea { view_distance } => {
374                    server.create_centered_persister(view_distance);
375                },
376                Message::SqlLogMode { mode } => {
377                    server.set_sql_log_mode(mode);
378                },
379                Message::DisconnectAllClients => {
380                    server.disconnect_all_clients();
381                },
382                Message::ListPlayers => {
383                    let players: Vec<String> = server
384                        .state()
385                        .ecs()
386                        .read_storage::<Player>()
387                        .join()
388                        .map(|p| p.alias.clone())
389                        .collect();
390                    let _ = response.send(MessageReturn::Players(players));
391                },
392                Message::ListLogs => {
393                    let log = LOG.inner.lock().unwrap();
394                    let lines: Vec<_> = log
395                        .lines
396                        .iter()
397                        .rev()
398                        .take(30)
399                        .map(|l| l.to_string())
400                        .collect();
401                    let _ = response.send(MessageReturn::Logs(lines));
402                },
403                Message::SendGlobalMsg { msg } => {
404                    use server::state_ext::StateExt;
405                    let msg = ChatType::Meta.into_plain_msg(msg);
406                    server.state().send_chat(msg, false);
407                },
408            }
409            false
410        };
411
412        if let Some(tui) = tui.as_ref() {
413            while let Ok(msg) = tui.msg_r.try_recv() {
414                let (sender, mut recv) = tokio::sync::oneshot::channel();
415                if handle_msg(msg, sender) {
416                    info!("Closing the server");
417                    break 'outer;
418                }
419                if let Ok(msg_answ) = recv.try_recv() {
420                    match msg_answ {
421                        MessageReturn::Players(players) => info!("Players: {:?}", players),
422                        MessageReturn::Logs(_) => info!("skipp sending logs to tui"),
423                    };
424                }
425            }
426        }
427
428        while let Ok((msg, sender)) = web_ui_request_r.try_recv() {
429            if handle_msg(msg, sender) {
430                info!("Closing the server");
431                break 'outer;
432            }
433        }
434
435        drop(guard);
436        // Wait for the next tick.
437        clock.tick();
438        #[cfg(feature = "tracy")]
439        common_base::tracy_client::frame_mark();
440    }
441    Ok(())
442}