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