Update check without shutdown (#177)

* Add in update subcommand

* Setup skeleton for updating

* Reorganize server functions and constants

* Cleanup logic

* Add more help info for cli

* Add in parsing buildids

* Test cleanup

* Remove appinfo to force latest info to be pulled

* Change update script to use `odin update`

* Implement review comments

* Fix incorrect command exit status check

* Add in unit tests for `UpdateInfo`

* Small spelling and grammar fixes

* Update `AUTO_UPDATE` description

* Another grammatical fix

* Address review comments

* Update outdated message

* Fix check exit statuses

* Supress `pidof` output

* Use more appropriate variable name

Co-authored-by: Michael <12646562+mbround18@users.noreply.github.com>
This commit is contained in:
LovecraftianHorror
2021-03-06 23:41:35 -05:00
committed by GitHub
parent 78e6b3610a
commit 3e2c851a7f
25 changed files with 1089 additions and 194 deletions

5
Cargo.lock generated
View File

@@ -609,6 +609,7 @@ dependencies = [
"flate2",
"inflections",
"log",
"once_cell",
"rand",
"reqwest",
"serde",
@@ -621,9 +622,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.5.2"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0"
checksum = "10acf907b94fc1b1a152d08ef97e7759650268cf986bf127f387e602b02c7e5a"
[[package]]
name = "openssl"

View File

@@ -21,6 +21,7 @@ reqwest = { version = "0.11.1", features = ["blocking", "json"] }
chrono = "0.4"
[dev-dependencies]
once_cell = "1.7"
rand = "0.8.3"
serial_test = "0.5.1"

View File

@@ -37,7 +37,7 @@
| WORLD | `Dedicated` | TRUE | This is used to generate the name of your world. |
| PUBLIC | `1` | FALSE | Sets whether or not your server is public on the server list. |
| PASSWORD | `12345` | TRUE | Set this to something unique! |
| AUTO_UPDATE | `0` | FALSE | Set to `1` if you want your container to auto update! This means at 1 am it will update, stop, and then restart your server. |
| AUTO_UPDATE | `0` | FALSE | Set to `1` if you want your container to auto update! This means at the times indicated by `AUTO_UPDATE_SCHEDULE` it will check for server updates. If there is an update then the server will be shut down, updated, and brought back online if the server was running before. |
| AUTO_UPDATE_SCHEDULE | `0 1 * * *` | FALSE | This works in conjunction with `AUTO_UPDATE` and sets the schedule to which it will run an auto update. [If you need help figuring out a cron schedule click here]
| AUTO_BACKUP | `0` | FALSE | Set to `1` to enable auto backups. Backups are stored under `/home/steam/backups` which means you will have to add a volume mount for this directory. |
| AUTO_BACKUP_SCHEDULE | `*/15 * * * *` | FALSE | Change to set how frequently you would like the server to backup. [If you need help figuring out a cron schedule click here].

View File

@@ -82,6 +82,28 @@ subcommands:
about: Sets the output file to use
required: true
index: 2
- update:
about: >
Attempts to update an existing Valheim server installation. By
default this involves checking for an update, if an update is
available, the server will be shut down, updated, and brought back online
if it was running before. If no update is available then there should
be no effect from calling this.
args:
- check:
long: check
short: c
about: >
Check for a server update, exiting with 0 if one is available
and 10 if the server is up to date.
conflicts_with:
- force
- force:
long: force
short: f
about: Force an update attempt, even if no update is detected.
conflicts_with:
- check
- notify:
about: Sends a notification to the provided webhook.
version: "1.1"

View File

@@ -1,24 +1,6 @@
use crate::executable::execute_mut;
use crate::steamcmd::steamcmd_command;
use crate::utils::get_working_dir;
use log::{debug, info};
use std::process::{ExitStatus, Stdio};
use crate::server;
use std::process::ExitStatus;
pub fn invoke(app_id: i64) -> std::io::Result<ExitStatus> {
info!("Installing {} to {}", app_id, get_working_dir());
let login = "+login anonymous".to_string();
debug!("Argument set: {}", login);
let force_install_dir = format!("+force_install_dir {}", get_working_dir());
debug!("Argument set: {}", force_install_dir);
let app_update = format!("+app_update {}", app_id);
debug!("Argument set: {}", app_update);
let mut steamcmd = steamcmd_command();
debug!("Setting up install command...");
let install_command = steamcmd
.args(&[login, force_install_dir, app_update])
.arg("+quit")
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
debug!("Launching up install command...");
execute_mut(install_command)
server::install(app_id)
}

View File

@@ -4,3 +4,4 @@ pub mod install;
pub mod notify;
pub mod start;
pub mod stop;
pub mod update;

View File

@@ -1,107 +1,35 @@
mod bepinex;
use crate::commands::start::bepinex::{build_environment, is_bepinex_installed};
use crate::executable::create_execution;
use crate::files::config::{config_file, read_config};
use crate::files::{create_file, ValheimArguments};
use crate::messages::modding_disclaimer;
use crate::utils::{environment, get_working_dir};
use crate::files::config::load_config;
use crate::server;
use clap::ArgMatches;
use daemonize::Daemonize;
use log::{debug, error, info};
use std::process::{exit, Child};
const LD_LIBRARY_PATH_VAR: &str = "LD_LIBRARY_PATH";
const LD_PRELOAD_VAR: &str = "LD_PRELOAD";
fn exit_action() {
if is_bepinex_installed() {
info!("Server has been started with BepInEx! Keep in mind this may cause errors!!");
modding_disclaimer()
}
info!("Server has been started and Daemonized. It should be online shortly!");
info!("Keep an eye out for 'Game server connected' in the log!");
info!("(this indicates its online without any errors.)")
}
fn spawn_server(config: &ValheimArguments) -> std::io::Result<Child> {
let mut command = create_execution(&config.command);
info!("--------------------------------------------------------------------------------------------------------------");
let ld_library_path_value = environment::fetch_multiple_var(
LD_LIBRARY_PATH_VAR,
format!("{}/linux64", get_working_dir()).as_str(),
);
debug!("Setting up base command");
let base_command = command
.args(&[
"-nographics",
"-batchmode",
"-port",
&config.port.as_str(),
"-name",
&config.name.as_str(),
"-world",
&config.world.as_str(),
"-password",
&config.password.as_str(),
"-public",
&config.public.as_str(),
])
.env("SteamAppId", environment::fetch_var("APPID", "892970"))
.current_dir(get_working_dir());
info!("Executable: {}", &config.command);
info!("Launching Command...");
if is_bepinex_installed() {
info!("BepInEx detected! Switching to run with BepInEx...");
let bepinex_env = build_environment();
bepinex::invoke(base_command, &bepinex_env)
} else {
info!("Everything looks good! Running normally!");
base_command
.env(LD_LIBRARY_PATH_VAR, ld_library_path_value)
.spawn()
}
}
use std::process::exit;
pub fn invoke(args: &ArgMatches) {
info!("Setting up start scripts...");
debug!("Loading config file...");
let config = config_file();
let config_content: ValheimArguments = read_config(config);
debug!("Checking password compliance...");
if config_content.password.len() < 5 {
error!("The supplied password is too short! It much be 5 characters or greater!");
exit(1)
}
let config = load_config();
let dry_run: bool = args.is_present("dry_run");
debug!("Dry run condition: {}", dry_run);
info!("Looking for burial mounds...");
if !dry_run {
let stdout = create_file(format!("{}/logs/valheim_server.log", get_working_dir()).as_str());
let stderr = create_file(format!("{}/logs/valheim_server.err", get_working_dir()).as_str());
let daemonize = Daemonize::new()
.working_directory(get_working_dir())
.user("steam")
.group("steam")
.stdout(stdout)
.stderr(stderr)
.exit_action(exit_action)
.privileged_action(move || spawn_server(&config_content));
match daemonize.start() {
match server::start_daemonized(config) {
Ok(_) => info!("Success, daemonized"),
Err(e) => error!("Error, {}", e),
Err(e) => {
error!("Error: {}", e);
exit(1);
}
}
} else {
info!(
"This command would have launched\n{} -nographics -batchmode -port {} -name {} -world {} -password {} -public {}",
&config_content.command,
&config_content.port,
&config_content.name,
&config_content.world,
&config_content.password,
&config_content.public,
&config.command,
&config.port,
&config.name,
&config.world,
&config.password,
&config.public,
)
}
}

View File

@@ -1,60 +1,20 @@
use crate::utils::{get_working_dir, server_installed, VALHEIM_EXECUTABLE_NAME};
use clap::ArgMatches;
use log::{error, info};
use sysinfo::{ProcessExt, Signal, System, SystemExt};
use std::{thread, time::Duration};
use std::process::exit;
fn send_shutdown() {
info!("Scanning for Valheim process");
let mut system = System::new();
system.refresh_all();
let processes = system.get_process_by_name(VALHEIM_EXECUTABLE_NAME);
if processes.is_empty() {
info!("Process NOT found!")
} else {
for found_process in processes {
info!(
"Found Process with pid {}! Sending Interrupt!",
found_process.pid()
);
if found_process.kill(Signal::Interrupt) {
info!("Process signal interrupt sent successfully!")
} else {
error!("Failed to send signal interrupt!")
}
}
}
}
fn wait_for_server_exit() {
info!("Waiting for server to completely shutdown...");
let mut system = System::new();
loop {
system.refresh_all();
let processes = system.get_process_by_name(VALHEIM_EXECUTABLE_NAME);
if processes.is_empty() {
break;
} else {
// Delay to keep down CPU usage
thread::sleep(Duration::from_secs(1));
}
}
info!("Server has been shutdown successfully!")
}
use crate::{constants, server, utils::get_working_dir};
pub fn invoke(args: &ArgMatches) {
info!("Stopping server {}", get_working_dir());
if args.is_present("dry_run") {
info!("This command would have run: ");
info!("kill -2 {}", VALHEIM_EXECUTABLE_NAME)
info!("kill -2 {}", constants::VALHEIM_EXECUTABLE_NAME)
} else {
if !server_installed() {
if !server::is_installed() {
error!("Failed to find server executable!");
return;
exit(1);
}
send_shutdown();
wait_for_server_exit();
server::blocking_shutdown();
}
}

159
src/commands/update.rs Normal file
View File

@@ -0,0 +1,159 @@
use clap::ArgMatches;
use log::{debug, error, info};
use std::process::exit;
use crate::server;
const EXIT_NO_UPDATE_AVAILABLE: i32 = 10;
const EXIT_UPDATE_AVAILABLE: i32 = 0;
enum UpdateAction {
Check,
Force,
Regular,
}
impl UpdateAction {
fn new(check: bool, force: bool) -> Self {
match (check, force) {
(true, true) => panic!("`check` and `force` are mutually exlusive!"),
(true, false) => Self::Check,
(false, true) => Self::Force,
(false, false) => Self::Regular,
}
}
}
enum RunAction {
Real,
Dry,
}
#[derive(Clone, Copy)]
enum UpdateState {
Pending,
UpToDate,
}
impl UpdateState {
fn new() -> Self {
if server::update_is_available() {
Self::Pending
} else {
Self::UpToDate
}
}
fn as_exit_code(&self) -> i32 {
match self {
Self::UpToDate => EXIT_NO_UPDATE_AVAILABLE,
Self::Pending => EXIT_UPDATE_AVAILABLE,
}
}
}
enum ServerState {
Running,
Stopped,
}
impl ServerState {
fn new() -> Self {
if server::is_running() {
Self::Running
} else {
Self::Stopped
}
}
}
pub fn invoke(args: &ArgMatches) {
info!("Checking for updates");
if !server::is_installed() {
error!(
"Failed to find server executable. Can't update! If the server isn't installed yet then you \
likely need to run `odin install`."
);
exit(1);
}
let run_action = if args.is_present("dry_run") {
RunAction::Dry
} else {
RunAction::Real
};
let check = args.is_present("check");
let force = args.is_present("force");
let server_state = ServerState::new();
let update_state = UpdateState::new();
match update_state {
UpdateState::Pending => info!("A server update is available!"),
UpdateState::UpToDate => info!("No server updates found"),
}
match UpdateAction::new(check, force) {
UpdateAction::Check => update_check(run_action, update_state),
UpdateAction::Force => update_force(run_action, server_state),
UpdateAction::Regular => update_regular(run_action, server_state, update_state),
}
}
fn update_check(run_action: RunAction, update_state: UpdateState) {
match (run_action, update_state) {
(RunAction::Dry, UpdateState::Pending) => {
info!(
"Dry run: An update is available. This would exit with {} to indicate this.",
update_state.as_exit_code()
)
}
(RunAction::Dry, UpdateState::UpToDate) => {
info!(
"Dry run: No update is available. This would exit with {} to indicate this.",
update_state.as_exit_code()
)
}
(_, update_state) => exit(update_state.as_exit_code()),
}
}
fn update_force(run_action: RunAction, server_state: ServerState) {
match (run_action, server_state) {
(RunAction::Dry, ServerState::Running) => {
info!("Dry run: Server would be shutdown, updated, and brought back online")
}
(RunAction::Dry, ServerState::Stopped) => {
info!("Dry run: The server is offline and would be updated")
}
_ => {
debug!("Force updating!");
server::update_server();
}
}
}
fn update_regular(run_action: RunAction, server_state: ServerState, update_state: UpdateState) {
match (run_action, server_state, update_state) {
(RunAction::Dry, ServerState::Running, UpdateState::Pending) => {
info!(
"Dry run: An update is available and the server is ONLINE. The server would be shutdown \
updated, and brought back online."
)
}
(RunAction::Dry, ServerState::Stopped, UpdateState::Pending) => {
info!(
"Dry run: An update is available and the server is OFFLINE. The server would be updated."
)
}
(RunAction::Dry, _, UpdateState::UpToDate) => {
info!("Dry run: No update is available. Nothing to do.")
}
(_, _, UpdateState::Pending) => {
debug!("Updating the installation!");
server::update_server()
}
_ => debug!("No update available, nothing to do!"),
}
}

9
src/constants.rs Normal file
View File

@@ -0,0 +1,9 @@
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const GAME_ID: i64 = 896660;
pub const VALHEIM_EXECUTABLE_NAME: &str = "valheim_server.x86_64";
pub const LD_LIBRARY_PATH_VAR: &str = "LD_LIBRARY_PATH";
pub const LD_PRELOAD_VAR: &str = "LD_PRELOAD";
pub const ODIN_WORKING_DIR: &str = "ODIN_WORKING_DIR";

View File

@@ -1,7 +1,8 @@
use crate::constants;
use crate::files::ValheimArguments;
use crate::files::{FileManager, ManagedFile};
use crate::utils::environment::fetch_var;
use crate::utils::{get_variable, get_working_dir, VALHEIM_EXECUTABLE_NAME};
use crate::utils::{get_variable, get_working_dir};
use clap::ArgMatches;
use log::{debug, error};
use std::fs;
@@ -10,6 +11,19 @@ use std::process::exit;
const ODIN_CONFIG_FILE_VAR: &str = "ODIN_CONFIG_FILE";
pub fn load_config() -> ValheimArguments {
let file = config_file();
let config = read_config(file);
debug!("Checking password compliance...");
if config.password.len() < 5 {
error!("The supplied password is too short! It must be 5 characters or greater!");
exit(1);
}
config
}
pub fn config_file() -> ManagedFile {
let name = fetch_var(ODIN_CONFIG_FILE_VAR, "config.json");
debug!("Config file set to: {}", name);
@@ -25,7 +39,11 @@ pub fn read_config(config: ManagedFile) -> ValheimArguments {
}
pub fn write_config(config: ManagedFile, args: &ArgMatches) -> bool {
let server_executable: &str = &[get_working_dir(), VALHEIM_EXECUTABLE_NAME.to_string()].join("/");
let server_executable: &str = &[
get_working_dir(),
constants::VALHEIM_EXECUTABLE_NAME.to_string(),
]
.join("/");
let command = match fs::canonicalize(PathBuf::from(get_variable(
args,
"server_executable",

View File

@@ -5,21 +5,22 @@ use crate::executable::handle_exit_status;
use crate::logger::OdinLogger;
use crate::utils::environment;
mod commands;
mod constants;
mod errors;
mod executable;
mod files;
mod logger;
mod messages;
mod mods;
mod notifications;
mod server;
mod steamcmd;
mod utils;
use crate::notifications::enums::event_status::EventStatus;
use crate::notifications::enums::notification_event::NotificationEvent;
const VERSION: &str = env!("CARGO_PKG_VERSION");
static LOGGER: OdinLogger = OdinLogger;
static GAME_ID: i64 = 896660;
fn setup_logger(debug: bool) -> Result<(), SetLoggerError> {
let level = if debug {
@@ -35,7 +36,7 @@ fn setup_logger(debug: bool) -> Result<(), SetLoggerError> {
fn main() {
// The YAML file is found relative to the current file, similar to how modules are found
let yaml = load_yaml!("cli.yaml");
let app = App::from(yaml).version(VERSION);
let app = App::from(yaml).version(constants::VERSION);
let matches = app.get_matches();
let debug_mode = matches.is_present("debug") || environment::fetch_var("DEBUG_MODE", "0").eq("1");
setup_logger(debug_mode).unwrap();
@@ -49,7 +50,7 @@ fn main() {
};
if let Some(ref _match) = matches.subcommand_matches("install") {
debug!("Launching install command...");
let result = commands::install::invoke(GAME_ID);
let result = commands::install::invoke(constants::GAME_ID);
handle_exit_status(result, "Successfully installed Valheim!".to_string())
};
if let Some(ref start_matches) = matches.subcommand_matches("start") {
@@ -72,4 +73,9 @@ fn main() {
debug!("Launching notify command...");
commands::notify::invoke(notify_matches);
};
if let Some(ref update_matches) = matches.subcommand_matches("update") {
debug!("Launching update command...");
commands::update::invoke(update_matches);
}
}

View File

@@ -1,4 +1,4 @@
use crate::commands::start::{LD_LIBRARY_PATH_VAR, LD_PRELOAD_VAR};
use crate::constants;
use crate::utils::{environment, get_working_dir, path_exists};
use log::{debug, info};
use std::ops::Add;
@@ -46,9 +46,10 @@ pub struct BepInExEnvironment {
}
pub fn build_environment() -> BepInExEnvironment {
let ld_preload = environment::fetch_var(LD_PRELOAD_VAR, "").add(doorstop_lib().as_str());
let ld_preload =
environment::fetch_var(constants::LD_PRELOAD_VAR, "").add(doorstop_lib().as_str());
let ld_library_path = environment::fetch_var(
LD_LIBRARY_PATH_VAR,
constants::LD_LIBRARY_PATH_VAR,
format!("./linux64:{}", doorstop_libs()).as_str(),
);
let doorstop_invoke_dll_value = doorstop_invoke_dll();
@@ -122,9 +123,9 @@ pub fn invoke(command: &mut Command, environment: &BepInExEnvironment) -> std::i
&environment.doorstop_corlib_override_path,
)
// LD_LIBRARY_PATH must not have quotes around it.
.env(LD_LIBRARY_PATH_VAR, &environment.ld_library_path)
.env(constants::LD_LIBRARY_PATH_VAR, &environment.ld_library_path)
// LD_PRELOAD must not have quotes around it.
.env(LD_PRELOAD_VAR, &environment.ld_preload)
.env(constants::LD_PRELOAD_VAR, &environment.ld_preload)
// DYLD_LIBRARY_PATH is weird af and MUST have quotes around it.
.env(
DYLD_LIBRARY_PATH_VAR,

1
src/mods/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod bepinex;

View File

@@ -22,25 +22,38 @@ Password: (REDACTED)
"
line
cd /home/steam/valheim || exit 1
log "Stopping server..."
odin stop || exit 1
if [ "${AUTO_BACKUP_ON_UPDATE:=0}" -eq 1 ]; then
/bin/bash /home/steam/scripts/auto_backup.sh "pre-update-backup"
if odin update --check; then
log "An update is available. Starting the update process..."
# Store if the server is currently running
! pidof valheim_server.x86_64 > /dev/null
SERVER_RUNNING=$?
# Stop the server if it's running
if [ "${SERVER_RUNNING}" -eq 1 ]; then
odin stop || exit 1
fi
if [ "${AUTO_BACKUP_ON_UPDATE:=0}" -eq 1 ]; then
/bin/bash /home/steam/scripts/auto_backup.sh "pre-update-backup"
fi
odin update || exit 1
# Start the server if it was running before
if [ "${SERVER_RUNNING}" -eq 1 ]; then
odin start || exit 1
line
log "
Finished updating and everything looks happy <3
Check your output.log for 'Game server connected'
"
fi
else
log "No update available"
fi
log "Installing Updates..."
odin install || exit 1
log "Starting server..."
odin start || exit 1
line
log "
Everything looks happy <3
Check your output.log for 'Game server connected'
"
line

34
src/server/install.rs Normal file
View File

@@ -0,0 +1,34 @@
use log::{debug, info};
use std::{
io,
path::Path,
process::{ExitStatus, Stdio},
};
use crate::{
constants, executable::execute_mut, steamcmd::steamcmd_command, utils::get_working_dir,
};
pub fn is_installed() -> bool {
Path::new(&get_working_dir())
.join(constants::VALHEIM_EXECUTABLE_NAME)
.exists()
}
pub fn install(app_id: i64) -> io::Result<ExitStatus> {
info!("Installing {} to {}", app_id, get_working_dir());
let login = "+login anonymous".to_string();
let force_install_dir = format!("+force_install_dir {}", get_working_dir());
let app_update = format!("+app_update {}", app_id);
let mut steamcmd = steamcmd_command();
let install_command = steamcmd
.args(&[login, force_install_dir, app_update])
.arg("+quit")
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
debug!("Launching install command: {:#?}", install_command);
execute_mut(install_command)
}

8
src/server/mod.rs Normal file
View File

@@ -0,0 +1,8 @@
mod install;
mod shutdown;
mod startup;
mod update;
mod utils;
// Rexport all public functions
pub use crate::server::{install::*, shutdown::*, startup::*, update::*, utils::*};

49
src/server/shutdown.rs Normal file
View File

@@ -0,0 +1,49 @@
use log::{error, info};
use sysinfo::{ProcessExt, Signal, System, SystemExt};
use std::{thread, time::Duration};
use crate::constants;
pub fn blocking_shutdown() {
send_shutdown_signal();
wait_for_exit();
}
pub fn send_shutdown_signal() {
info!("Scanning for Valheim process");
let mut system = System::new();
system.refresh_all();
let processes = system.get_process_by_name(constants::VALHEIM_EXECUTABLE_NAME);
if processes.is_empty() {
info!("Process NOT found!")
} else {
for found_process in processes {
info!(
"Found Process with pid {}! Sending Interrupt!",
found_process.pid()
);
if found_process.kill(Signal::Interrupt) {
info!("Process signal interrupt sent successfully!")
} else {
error!("Failed to send signal interrupt!")
}
}
}
}
fn wait_for_exit() {
info!("Waiting for server to completely shutdown...");
let mut system = System::new();
loop {
system.refresh_all();
let processes = system.get_process_by_name(constants::VALHEIM_EXECUTABLE_NAME);
if processes.is_empty() {
break;
} else {
// Delay to keep down CPU usage
thread::sleep(Duration::from_secs(1));
}
}
info!("Server has been shutdown successfully!")
}

77
src/server/startup.rs Normal file
View File

@@ -0,0 +1,77 @@
use daemonize::{Daemonize, DaemonizeError};
use log::{debug, info};
use std::{io, process::Child};
use crate::{
constants,
executable::create_execution,
files::{create_file, ValheimArguments},
messages,
mods::bepinex,
utils::{environment, get_working_dir},
};
type CommandResult = io::Result<Child>;
pub fn start_daemonized(config: ValheimArguments) -> Result<CommandResult, DaemonizeError> {
let stdout = create_file(format!("{}/logs/valheim_server.log", get_working_dir()).as_str());
let stderr = create_file(format!("{}/logs/valheim_server.err", get_working_dir()).as_str());
Daemonize::new()
.working_directory(get_working_dir())
.user("steam")
.group("steam")
.stdout(stdout)
.stderr(stderr)
.exit_action(|| {
if bepinex::is_bepinex_installed() {
info!("Server has been started with BepInEx! Keep in mind this may cause errors!!");
messages::modding_disclaimer()
}
info!("Server has been started and Daemonized. It should be online shortly!");
info!("Keep an eye out for 'Game server connected' in the log!");
info!("(this indicates its online without any errors.)")
})
.privileged_action(move || start(&config))
.start()
}
pub fn start(config: &ValheimArguments) -> CommandResult {
let mut command = create_execution(&config.command);
info!("--------------------------------------------------------------------------------------------------------------");
let ld_library_path_value = environment::fetch_multiple_var(
constants::LD_LIBRARY_PATH_VAR,
format!("{}/linux64", get_working_dir()).as_str(),
);
debug!("Setting up base command");
let base_command = command
.args(&[
"-nographics",
"-batchmode",
"-port",
&config.port.as_str(),
"-name",
&config.name.as_str(),
"-world",
&config.world.as_str(),
"-password",
&config.password.as_str(),
"-public",
&config.public.as_str(),
])
.env("SteamAppId", environment::fetch_var("APPID", "892970"))
.current_dir(get_working_dir());
info!("Executable: {}", &config.command);
info!("Launching Command...");
if bepinex::is_bepinex_installed() {
info!("BepInEx detected! Switching to run with BepInEx...");
let bepinex_env = bepinex::build_environment();
bepinex::invoke(base_command, &bepinex_env)
} else {
info!("Everything looks good! Running normally!");
base_command
.env(constants::LD_LIBRARY_PATH_VAR, ld_library_path_value)
.spawn()
}
}

250
src/server/update.rs Normal file
View File

@@ -0,0 +1,250 @@
use log::{debug, error, info};
use std::{fs, io::ErrorKind, path::Path, process::exit};
use crate::{
constants, files::config::load_config, server, steamcmd::steamcmd_command, utils::get_working_dir,
};
#[derive(Clone, Debug, PartialEq)]
pub struct UpdateInfo {
current_build_id: String,
latest_build_id: String,
}
impl UpdateInfo {
pub fn new() -> Self {
let current_build_id = get_current_build_id();
let latest_build_id = get_latest_build_id();
Self::internal_new(current_build_id, latest_build_id)
}
#[cfg(test)]
pub fn new_testing(manifest_contents: &str, app_info_output: &str) -> Self {
let current_build_id = extract_build_id_from_manifest(manifest_contents).to_string();
let latest_build_id = extract_build_id_from_app_info(app_info_output).to_string();
Self::internal_new(current_build_id, latest_build_id)
}
fn internal_new(current_build_id: String, latest_build_id: String) -> Self {
Self {
current_build_id,
latest_build_id,
}
}
pub fn update_available(&self) -> bool {
self.current_build_id != self.latest_build_id
}
// pub fn current_build_id(&self) -> &str {
// &self.current_build_id
// }
// pub fn latest_build_id(&self) -> &str {
// &self.latest_build_id
// }
}
pub fn update_is_available() -> bool {
let info = UpdateInfo::new();
debug!("{:#?}", info);
info.update_available()
}
pub fn update_server() {
// Shutdown the server if it's running
let server_was_running = server::is_running();
if server_was_running {
server::blocking_shutdown();
}
// Update the installation
if let Err(e) = server::install(constants::GAME_ID) {
error!("Failed to install server: {}", e);
exit(1);
}
// Bring the server up if it was running before
if server_was_running {
let config = load_config();
match server::start_daemonized(config) {
Ok(_) => info!("Server daemon started"),
Err(e) => {
error!("Error daemonizing: {}", e);
exit(1);
}
}
}
}
fn get_current_build_id() -> String {
let manifest_path = Path::new(&get_working_dir())
.join("steamapps")
.join(&format!("appmanifest_{}.acf", constants::GAME_ID));
let manifest_data = fs::read_to_string(&manifest_path).unwrap_or_else(|_| {
panic!(
"Failed to read manifest file at '{}'",
manifest_path.display()
)
});
extract_build_id_from_manifest(&manifest_data).to_string()
}
fn get_latest_build_id() -> String {
// Remove the cached file to force an updated response. This is done because `steamcmd` seems to
// refuse to update information before querying the app_info even with `+app_info_update 1` or
// `+@bCSForceNoCache 1`
let appinfo_file = Path::new("/home/steam/Steam/appcache/appinfo.vdf");
fs::remove_file(&appinfo_file).unwrap_or_else(|e| match e.kind() {
// AOK if it doesn't exist
ErrorKind::NotFound => {}
err_kind => {
error!(
"Failed to remove appinfo file at '{}'! Error: {:?}",
appinfo_file.display(),
err_kind
);
exit(1);
}
});
// Now pull the latest app info
let args = &[
"+@ShutdownOnFailedCommand 1",
"+login anonymous",
&format!("+app_info_print {}", constants::GAME_ID),
"+quit",
];
let mut steamcmd = steamcmd_command();
let app_info_output = steamcmd
.args(args)
.output()
.expect("Failed to run steamcmd");
assert!(app_info_output.status.success());
let stdout = String::from_utf8(app_info_output.stdout).expect("steamcmd returned invalid UTF-8");
extract_build_id_from_app_info(&stdout).to_string()
}
fn extract_build_id_from_manifest(manifest: &str) -> &str {
for line in manifest.lines() {
if line.trim().starts_with("\"buildid\"") {
return split_vdf_key_val(line).1;
}
}
panic!("Unexpected manifest format:\n{}", manifest);
}
fn extract_build_id_from_app_info(app_info: &str) -> &str {
let mut lines = app_info.lines();
while let Some(line) = lines.next() {
if line.trim() == "\"public\"" {
break;
}
}
assert_eq!(
lines.next().map(|line| line.trim()),
Some("{"),
"Invalid app info"
);
let build_id_line = lines
.next()
.unwrap_or_else(|| panic!("Invalid app info format:\n{}", app_info))
.trim();
assert!(build_id_line.starts_with("\"buildid\""), "Invalid app info");
split_vdf_key_val(build_id_line).1
}
// Note: This is super brittle and will fail if there is whitespace within the key or value _or_ if
// there are escaped " at the end of the key or value
fn split_vdf_key_val(vdf_pair: &str) -> (&str, &str) {
let mut pieces = vdf_pair.trim().split_whitespace();
let key = pieces.next().expect("Missing vdf key").trim_matches('"');
let val = pieces.next().expect("Missing vdf val").trim_matches('"');
(key, val)
}
#[cfg(test)]
mod tests {
use super::*;
use once_cell::sync::Lazy;
use std::path::PathBuf;
static TEST_ASSET_DIR: Lazy<PathBuf> = Lazy::new(|| {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("assets")
});
const CURRENT_MANIFEST_FILENAME: &str = "example_current_app_manifest.txt";
const CURRENT_APP_INFO_FILENAME: &str = "example_current_steamcmd_app_info.txt";
const UPDATED_APP_INFO_FILENAME: &str = "example_updated_steamcmd_app_info.txt";
const CURRENT_BUILD_ID: &str = "6246034";
const UPDATED_BUILD_ID: &str = "6315977";
fn read_sample_file(filename: &str) -> String {
let filepath = TEST_ASSET_DIR.join(filename);
fs::read_to_string(&filepath)
.unwrap_or_else(|_| panic!("Sample file missing: '{}'", filepath.display()))
}
#[test]
fn extracting_build_id_from_manifest() {
let manifest_data = read_sample_file(CURRENT_MANIFEST_FILENAME);
assert_eq!(
extract_build_id_from_manifest(&manifest_data),
CURRENT_BUILD_ID
);
}
#[test]
fn extracting_build_id_from_app_info() {
let app_info_output = read_sample_file(CURRENT_APP_INFO_FILENAME);
assert_eq!(
extract_build_id_from_app_info(&app_info_output),
CURRENT_BUILD_ID
);
}
#[test]
fn update_info() {
let current_manifest_data = read_sample_file(CURRENT_MANIFEST_FILENAME);
let current_app_info_output = read_sample_file(CURRENT_APP_INFO_FILENAME);
let updated_app_info_output = read_sample_file(UPDATED_APP_INFO_FILENAME);
// Verify updated info looks right
let updated_update_info =
UpdateInfo::new_testing(&current_manifest_data, &current_app_info_output);
assert_eq!(
updated_update_info,
UpdateInfo {
current_build_id: CURRENT_BUILD_ID.to_string(),
latest_build_id: CURRENT_BUILD_ID.to_string()
}
);
assert!(!updated_update_info.update_available());
// Verify that info indicating an update looks right
let pending_update_info =
UpdateInfo::new_testing(&current_manifest_data, &updated_app_info_output);
assert_eq!(
pending_update_info,
UpdateInfo {
current_build_id: CURRENT_BUILD_ID.to_string(),
latest_build_id: UPDATED_BUILD_ID.to_string()
}
);
assert!(pending_update_info.update_available());
}
}

11
src/server/utils.rs Normal file
View File

@@ -0,0 +1,11 @@
use sysinfo::{System, SystemExt};
use crate::constants;
pub fn is_running() -> bool {
let mut system = System::new();
system.refresh_processes();
let valheim_processes = system.get_process_by_name(constants::VALHEIM_EXECUTABLE_NAME);
!valheim_processes.is_empty()
}

View File

@@ -5,12 +5,11 @@ use log::debug;
use std::env;
use std::path::Path;
const ODIN_WORKING_DIR: &str = "ODIN_WORKING_DIR";
pub const VALHEIM_EXECUTABLE_NAME: &str = "valheim_server.x86_64";
use crate::constants;
pub fn get_working_dir() -> String {
environment::fetch_var(
ODIN_WORKING_DIR,
constants::ODIN_WORKING_DIR,
env::current_dir().unwrap().to_str().unwrap(),
)
}
@@ -33,10 +32,6 @@ pub fn get_variable(args: &ArgMatches, name: &str, default: String) -> String {
.to_string()
}
pub fn server_installed() -> bool {
Path::new(&[get_working_dir(), VALHEIM_EXECUTABLE_NAME.to_string()].join("/")).exists()
}
pub(crate) fn path_exists(path: &str) -> bool {
let state = Path::new(path).exists();
debug!(

View File

@@ -0,0 +1,36 @@
"AppState"
{
"appid" "896660"
"Universe" "1"
"name" "Valheim Dedicated Server"
"StateFlags" "4"
"installdir" "Valheim dedicated server"
"LastUpdated" "1613644649"
"UpdateResult" "0"
"SizeOnDisk" "1045271601"
"buildid" "6246034"
"LastOwner" "76561201190449363"
"BytesToDownload" "564763584"
"BytesDownloaded" "564763584"
"BytesToStage" "1045271601"
"BytesStaged" "1045271601"
"AutoUpdateBehavior" "0"
"AllowOtherDownloadsWhileRunning" "0"
"ScheduledAutoUpdate" "0"
"InstalledDepots"
{
"1006"
{
"manifest" "6688153055340488873"
"size" "59862244"
}
"896661"
{
"manifest" "521795651741005384"
"size" "985409357"
}
}
"UserConfig"
{
}
}

View File

@@ -0,0 +1,167 @@
WARNING: setlocale('en_US.UTF-8') failed, using locale: 'C'. International characters may not work.
Redirecting stderr to '/home/steam/Steam/logs/stderr.txt'
/tmp/dumps is not owned by us - delete and recreate
Unable to delete /tmp/dumps. Continuing anyway.
[ 0%] Checking for available updates...
[----] Verifying installation...
Steam Console Client (c) Valve Corporation
-- type 'quit' to exit --
Loading Steam API...OK.
Connecting anonymously to Steam Public...Logged in OK
Waiting for user info...OK
AppID : 896660, change number : 10778299/4294967295, last change : Fri Feb 19 18:42:01 2021
"896660"
{
"common"
{
"name" "Valheim Dedicated Server"
"type" "Tool"
"parent" "892970"
"oslist" "windows,linux"
"osarch" ""
"icon" "1aab0586723c8578c7990ced7d443568649d0df2"
"logo" "233d73a1c963515ee4a9b59507bc093d85a4e2dc"
"logo_small" "233d73a1c963515ee4a9b59507bc093d85a4e2dc_thumb"
"clienticon" "c55a6b50b170ac6ed56cf90521273c30dccb5f12"
"clienttga" "35e067b9efc8d03a9f1cdfb087fac4b970a48daf"
"ReleaseState" "released"
"associations"
{
}
"gameid" "896660"
}
"config"
{
"installdir" "Valheim dedicated server"
"launch"
{
"0"
{
"executable" "start_server_xterm.sh"
"type" "server"
"config"
{
"oslist" "linux"
}
}
"1"
{
"executable" "start_headless_server.bat"
"type" "server"
"config"
{
"oslist" "windows"
}
}
}
}
"depots"
{
"1004"
{
"name" "Steamworks SDK Redist (WIN32)"
"config"
{
"oslist" "windows"
}
"manifests"
{
"public" "6473168357831043306"
}
"maxsize" "39546856"
"depotfromapp" "1007"
}
"1005"
{
"name" "Steamworks SDK Redist (OSX32)"
"config"
{
"oslist" "macos"
}
"manifests"
{
"public" "2135359612286175146"
}
"depotfromapp" "1007"
}
"1006"
{
"name" "Steamworks SDK Redist (LINUX32)"
"config"
{
"oslist" "linux"
}
"manifests"
{
"public" "6688153055340488873"
}
"maxsize" "59862244"
"depotfromapp" "1007"
}
"896661"
{
"name" "Valheim dedicated server Linux"
"config"
{
"oslist" "linux"
}
"manifests"
{
"public" "521795651741005384"
}
"maxsize" "985409357"
"encryptedmanifests"
{
"experimental"
{
"encrypted_gid_2" "BEDF872D73873D16C025EF87E27C2BDB"
"encrypted_size_2" "2559486959C6E5DCEA5C71ED32BA9080"
}
}
}
"896662"
{
"name" "Valheim dedicated server Windows"
"config"
{
"oslist" "windows"
}
"manifests"
{
"public" "5449924312569304795"
}
"maxsize" "963189471"
"encryptedmanifests"
{
"experimental"
{
"encrypted_gid_2" "9FD2B7B42FACB1D1FC439DD83ED2BED9"
"encrypted_size_2" "B2D602E667364DEDCB7C3D6EE9AA7374"
}
}
}
"branches"
{
"public"
{
"buildid" "6246034"
"timeupdated" "1613558776"
}
"experimental"
{
"buildid" "6263839"
"description" "Experimental version of Valheim"
"pwdrequired" "1"
"timeupdated" "1613728251"
}
"unstable"
{
"buildid" "6246034"
"description" "Unstable test version of valheim"
"pwdrequired" "1"
"timeupdated" "1613469743"
}
}
}
}

View File

@@ -0,0 +1,166 @@
WARNING: setlocale('en_US.UTF-8') failed, using locale: 'C'. International characters may not work.
Redirecting stderr to '/home/steam/Steam/logs/stderr.txt'
[ 0%] Checking for available updates...
[----] Verifying installation...
Steam Console Client (c) Valve Corporation
-- type 'quit' to exit --
Loading Steam API...OK.
"@ShutdownOnFailedCommand" = "1"
Connecting anonymously to Steam Public...Logged in OK
Waiting for user info...OK
AppID : 896660, change number : 10865381/10865381, last change : Wed Mar 3 00:41:58 2021
"896660"
{
"common"
{
"name" "Valheim Dedicated Server"
"type" "Tool"
"parent" "892970"
"oslist" "windows,linux"
"osarch" ""
"icon" "1aab0586723c8578c7990ced7d443568649d0df2"
"logo" "233d73a1c963515ee4a9b59507bc093d85a4e2dc"
"logo_small" "233d73a1c963515ee4a9b59507bc093d85a4e2dc_thumb"
"clienticon" "c55a6b50b170ac6ed56cf90521273c30dccb5f12"
"clienttga" "35e067b9efc8d03a9f1cdfb087fac4b970a48daf"
"ReleaseState" "released"
"associations"
{
}
"gameid" "896660"
}
"config"
{
"installdir" "Valheim dedicated server"
"launch"
{
"0"
{
"executable" "start_server_xterm.sh"
"type" "server"
"config"
{
"oslist" "linux"
}
}
"1"
{
"executable" "start_headless_server.bat"
"type" "server"
"config"
{
"oslist" "windows"
}
}
}
}
"depots"
{
"1004"
{
"name" "Steamworks SDK Redist (WIN32)"
"config"
{
"oslist" "windows"
}
"manifests"
{
"public" "6473168357831043306"
}
"maxsize" "39546856"
"depotfromapp" "1007"
}
"1005"
{
"name" "Steamworks SDK Redist (OSX32)"
"config"
{
"oslist" "macos"
}
"manifests"
{
"public" "2135359612286175146"
}
"depotfromapp" "1007"
}
"1006"
{
"name" "Steamworks SDK Redist (LINUX32)"
"config"
{
"oslist" "linux"
}
"manifests"
{
"public" "6688153055340488873"
}
"maxsize" "59862244"
"depotfromapp" "1007"
}
"896661"
{
"name" "Valheim dedicated server Linux"
"config"
{
"oslist" "linux"
}
"manifests"
{
"public" "6588021550109601388"
}
"maxsize" "991737299"
"encryptedmanifests"
{
"experimental"
{
"encrypted_gid_2" "91B9C2C233637DEDCA25AE056AAD25FA"
"encrypted_size_2" "652E81496E51312FC0F04B49B67718E8"
}
}
}
"896662"
{
"name" "Valheim dedicated server Windows"
"config"
{
"oslist" "windows"
}
"manifests"
{
"public" "415644664754619686"
}
"maxsize" "984116773"
"encryptedmanifests"
{
"experimental"
{
"encrypted_gid_2" "E7E59A011C08BFE6CA0FD3A704DA1C8D"
"encrypted_size_2" "1578D11B163520DC211016502F4A37F2"
}
}
}
"branches"
{
"public"
{
"buildid" "6315977"
"timeupdated" "1614679211"
}
"experimental"
{
"buildid" "6306893"
"description" "Experimental version of Valheim"
"pwdrequired" "1"
"timeupdated" "1614510566"
}
"unstable"
{
"buildid" "6315977"
"description" "Unstable test version of valheim"
"pwdrequired" "1"
"timeupdated" "1614676060"
}
}
}
}