mirror of
				https://github.com/mbround18/valheim-docker.git
				synced 2021-10-22 21:53:54 +03:00 
			
		
		
		
	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:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							78e6b3610a
						
					
				
				
					commit
					3e2c851a7f
				
			
							
								
								
									
										5
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -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" | ||||
|   | ||||
| @@ -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" | ||||
|  | ||||
|   | ||||
| @@ -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]. | ||||
|   | ||||
							
								
								
									
										22
									
								
								src/cli.yaml
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								src/cli.yaml
									
									
									
									
									
								
							| @@ -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" | ||||
|   | ||||
| @@ -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) | ||||
| } | ||||
|   | ||||
| @@ -4,3 +4,4 @@ pub mod install; | ||||
| pub mod notify; | ||||
| pub mod start; | ||||
| pub mod stop; | ||||
| pub mod update; | ||||
|   | ||||
| @@ -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, | ||||
|     ) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										159
									
								
								src/commands/update.rs
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										9
									
								
								src/constants.rs
									
									
									
									
									
										Normal 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"; | ||||
| @@ -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", | ||||
|   | ||||
							
								
								
									
										14
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								src/main.rs
									
									
									
									
									
								
							| @@ -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); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										1
									
								
								src/mods/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| pub mod bepinex; | ||||
| @@ -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
									
								
							
							
						
						
									
										34
									
								
								src/server/install.rs
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										8
									
								
								src/server/mod.rs
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										49
									
								
								src/server/shutdown.rs
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										77
									
								
								src/server/startup.rs
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										250
									
								
								src/server/update.rs
									
									
									
									
									
										Normal 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(¤t_manifest_data, ¤t_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(¤t_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
									
								
							
							
						
						
									
										11
									
								
								src/server/utils.rs
									
									
									
									
									
										Normal 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() | ||||
| } | ||||
| @@ -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!( | ||||
|   | ||||
							
								
								
									
										36
									
								
								tests/assets/example_current_app_manifest.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								tests/assets/example_current_app_manifest.txt
									
									
									
									
									
										Normal 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" | ||||
| 	{ | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										167
									
								
								tests/assets/example_current_steamcmd_app_info.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								tests/assets/example_current_steamcmd_app_info.txt
									
									
									
									
									
										Normal 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" | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										166
									
								
								tests/assets/example_updated_steamcmd_app_info.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								tests/assets/example_updated_steamcmd_app_info.txt
									
									
									
									
									
										Normal 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" | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user