keep server alive if it has players

This commit is contained in:
Robin Appelman 2021-08-05 20:01:06 +02:00
commit da2b13feb1
4 changed files with 169 additions and 153 deletions

151
Cargo.lock generated
View file

@ -221,12 +221,13 @@ dependencies = [
[[package]]
name = "cron"
version = "0.8.0"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "628a3464535cee4e75af89e8c293bab926deaddfa166553b75029066c846be3f"
checksum = "e009ed0b762cf7a967a34dfdc67d5967d3f828f12901d37081432c3dd1668f8f"
dependencies = [
"chrono",
"nom",
"once_cell",
]
[[package]]
@ -300,16 +301,17 @@ dependencies = [
"async-trait",
"camino",
"chrono",
"cron",
"futures-util",
"petname",
"pretty_env_logger",
"rcon",
"reqwest",
"serde",
"thiserror",
"thrussh",
"thrussh-keys",
"tokio",
"tokio-cron-scheduler",
"toml",
]
@ -341,6 +343,20 @@ dependencies = [
"termcolor",
]
[[package]]
name = "err-derive"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcc7f65832b62ed38939f98966824eb6294911c3629b0e9a262bfb80836d9686"
dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"rustversion",
"syn",
"synstructure",
]
[[package]]
name = "flate2"
version = "1.0.20"
@ -631,15 +647,6 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "instant"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d"
dependencies = [
"cfg-if",
]
[[package]]
name = "ipnet"
version = "2.3.1"
@ -694,15 +701,6 @@ dependencies = [
"walkdir",
]
[[package]]
name = "lock_api"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.14"
@ -838,31 +836,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "parking_lot"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb"
dependencies = [
"instant",
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018"
dependencies = [
"cfg-if",
"instant",
"libc",
"redox_syscall",
"smallvec",
"winapi",
]
[[package]]
name = "password-hash"
version = "0.2.2"
@ -938,6 +911,30 @@ dependencies = [
"log",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro-hack"
version = "0.5.19"
@ -1014,6 +1011,16 @@ dependencies = [
"rand_core",
]
[[package]]
name = "rcon"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d982f524250f15117766c710229add75d47c54c9ef654b8c7b5a4da49058750"
dependencies = [
"err-derive",
"tokio",
]
[[package]]
name = "redox_syscall"
version = "0.2.9"
@ -1114,6 +1121,12 @@ dependencies = [
"webpki",
]
[[package]]
name = "rustversion"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088"
[[package]]
name = "ryu"
version = "1.0.5"
@ -1129,12 +1142,6 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "sct"
version = "0.6.1"
@ -1216,12 +1223,6 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527"
[[package]]
name = "smallvec"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
[[package]]
name = "socket2"
version = "0.4.1"
@ -1255,6 +1256,18 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "synstructure"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "474aaa926faa1603c40b7885a9eaea29b444d1cb2850cb7c0e37bb1a4182f4fa"
dependencies = [
"proc-macro2",
"quote",
"syn",
"unicode-xid",
]
[[package]]
name = "termcolor"
version = "1.1.2"
@ -1398,25 +1411,12 @@ dependencies = [
"mio",
"num_cpus",
"once_cell",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"tokio-macros",
"winapi",
]
[[package]]
name = "tokio-cron-scheduler"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6af42ec81010dbf80a8762206e4cf5273ef291b91f203b48e250130ca4289392"
dependencies = [
"chrono",
"cron",
"tokio",
"uuid",
]
[[package]]
name = "tokio-macros"
version = "1.3.0"
@ -1559,15 +1559,6 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "uuid"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [
"getrandom",
]
[[package]]
name = "vcpkg"
version = "0.2.15"

View file

@ -11,11 +11,12 @@ thiserror = "1"
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] }
serde = { version = "1.0", features = ["derive"] }
toml = "0.5"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] }
camino = "1"
petname = "1"
thrussh = "0.33"
thrussh-keys = "0.21"
futures-util = "0.3"
pretty_env_logger = "0.4"
tokio-cron-scheduler = "0.2"
cron = "0.9"
rcon = "0.4"

View file

@ -1,19 +1,24 @@
use crate::cloud::{Cloud, CloudError};
use crate::cloud::{Cloud, CloudError, Server};
use crate::config::{Config, ConfigError, ServerConfig};
use crate::dns::{DynDnsClient, DynDnsError};
use crate::rcon::Rcon;
use crate::ssh::SshError;
use chrono::Utc;
use cron::Schedule;
use ssh::SshSession;
use std::env::args;
use std::sync::{Arc, Mutex};
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use thiserror::Error;
use tokio::task::{spawn, JoinError};
use tokio::select;
use tokio::signal::ctrl_c;
use tokio::time::sleep;
use tokio_cron_scheduler::{Job, JobScheduler};
mod cloud;
mod config;
mod dns;
mod rcon;
mod ssh;
#[derive(Debug, Error)]
@ -31,25 +36,9 @@ pub enum Error {
#[error("Already running")]
AlreadyRunning,
#[error("{0}")]
Schedule(ScheduleError),
}
#[derive(Debug, Error)]
Schedule(#[from] cron::error::Error),
#[error("{0}")]
pub struct ScheduleError(ScheduleErrorImpl);
#[derive(Debug, Error)]
enum ScheduleErrorImpl {
#[error("Error setting up schedule")]
Schedule(String),
#[error("Error running schedule")]
Join(JoinError),
}
impl From<ScheduleErrorImpl> for Error {
fn from(e: ScheduleErrorImpl) -> Self {
Error::Schedule(ScheduleError(e))
}
Rcon(#[from] ::rcon::Error),
}
async fn setup(ssh: &mut SshSession, config: &ServerConfig) -> Result<(), Error> {
@ -124,65 +113,80 @@ async fn main() -> Result<(), Error> {
};
let cloud = config.cloud()?;
let mut sched = JobScheduler::new();
let start_schedule = Schedule::from_str(&config.schedule.start)?;
let stop_schedule = Schedule::from_str(&config.schedule.stop)?;
let server_id: Arc<Mutex<Option<String>>> = Arc::default();
sched
.add(stop_job(cloud.clone(), &config, server_id.clone()))
.map_err(|e| ScheduleErrorImpl::Schedule(format!("{:#}", e)))?;
sched
.add(start_job(cloud, config, server_id))
.map_err(|e| ScheduleErrorImpl::Schedule(format!("{:#}", e)))?;
sched.start().await.map_err(ScheduleErrorImpl::Join)?;
select! {
_ = run_loop(cloud, config, start_schedule, stop_schedule) => {},
_ = ctrl_c() => {},
}
Ok(())
}
fn stop_job(cloud: Arc<dyn Cloud>, config: &Config, server_id: Arc<Mutex<Option<String>>>) -> Job {
Job::new(&config.schedule.stop, move |_uuid, _l| {
let server_id = server_id.clone();
let cloud = cloud.clone();
spawn(async move {
let id = server_id.lock().unwrap().take();
if let Some(id) = id {
async fn run_loop(
cloud: Arc<dyn Cloud>,
config: Config,
start_schedule: Schedule,
stop_schedule: Schedule,
) {
let mut active_server: Option<Server> = None;
loop {
let next_start = start_schedule.upcoming(Utc).next().unwrap();
let next_stop = stop_schedule.upcoming(Utc).next().unwrap();
// we're between start time and stop time
if active_server.is_none() && next_start > next_stop {
println!("Starting server");
match start(cloud.as_ref(), &config).await {
Ok(server) => active_server = Some(server),
Err(e) => eprintln!("{:#}", e),
};
}
// we're between stop time and start time
if active_server.is_some() && next_stop > next_start {
let active_players_res = match Rcon::new(
(active_server.as_ref().unwrap().ip, 27015),
&config.server.rcon,
)
.await
{
Ok(mut rcon) => rcon.player_count().await,
Err(e) => Err(e),
};
let stop = match active_players_res {
Ok(0) => true,
Ok(count) => {
println!(
"Want to stop server, but there are still {} active players",
count
);
false
}
Err(e) => {
eprintln!("{}", e);
true
}
};
if stop {
let id = &active_server.as_ref().unwrap().id;
println!("Stopping server {}", id);
match cloud.kill(&id).await {
Ok(_) => {}
Ok(_) => {
active_server = None;
}
Err(e) => eprintln!("{:#}", e),
};
} else {
println!("No server to stop")
}
}
});
})
.unwrap()
}
fn start_job(cloud: Arc<dyn Cloud>, config: Config, server_id: Arc<Mutex<Option<String>>>) -> Job {
let schedule = config.schedule.start.clone();
let config = Arc::new(config);
Job::new(&schedule, move |_uuid, _l| {
let cloud = cloud.clone();
let config = config.clone();
let server_id = server_id.clone();
spawn(async move {
let cloud = cloud.as_ref();
let already_started = { server_id.lock().unwrap().is_some() };
if !already_started {
println!("Starting server");
match start(cloud, &config).await {
Ok(id) => *server_id.lock().unwrap() = Some(id),
Err(e) => eprintln!("{:#}", e),
};
sleep(Duration::from_secs(60)).await;
}
});
})
.unwrap()
}
async fn start(cloud: &dyn Cloud, config: &Config) -> Result<String, Error> {
async fn start(cloud: &dyn Cloud, config: &Config) -> Result<Server, Error> {
let list = cloud.list().await?;
if !list.is_empty() {
return Err(Error::AlreadyRunning);
@ -227,5 +231,5 @@ async fn start(cloud: &dyn Cloud, config: &Config) -> Result<String, Error> {
" connect {}; password {}",
connect_host, config.server.password
);
Ok(server.id)
Ok(server)
}

20
src/rcon.rs Normal file
View file

@ -0,0 +1,20 @@
use crate::Error;
use rcon::Connection;
use tokio::net::ToSocketAddrs;
pub struct Rcon(Connection);
impl Rcon {
pub async fn new<A: ToSocketAddrs>(host: A, password: &str) -> Result<Self, Error> {
Ok(Rcon(Connection::builder().connect(host, password).await?))
}
pub async fn player_count(&mut self) -> Result<usize, Error> {
let status = self.0.cmd("status").await?;
let player_lines = status
.lines()
.filter(|line| line.starts_with('#'))
.filter(|line| !line.contains(" BOT "));
Ok(player_lines.count())
}
}