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:
		 LovecraftianHorror
					LovecraftianHorror
				
			
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			 GitHub
						GitHub
					
				
			
						parent
						
							78e6b3610a
						
					
				
				
					commit
					3e2c851a7f
				
			
							
								
								
									
										5
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -609,6 +609,7 @@ dependencies = [ | |||||||
|  "flate2", |  "flate2", | ||||||
|  "inflections", |  "inflections", | ||||||
|  "log", |  "log", | ||||||
|  |  "once_cell", | ||||||
|  "rand", |  "rand", | ||||||
|  "reqwest", |  "reqwest", | ||||||
|  "serde", |  "serde", | ||||||
| @@ -621,9 +622,9 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "once_cell" | name = "once_cell" | ||||||
| version = "1.5.2" | version = "1.7.0" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" | checksum = "10acf907b94fc1b1a152d08ef97e7759650268cf986bf127f387e602b02c7e5a" | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "openssl" | name = "openssl" | ||||||
|   | |||||||
| @@ -21,6 +21,7 @@ reqwest = { version = "0.11.1", features = ["blocking", "json"] } | |||||||
| chrono = "0.4" | chrono = "0.4" | ||||||
|  |  | ||||||
| [dev-dependencies] | [dev-dependencies] | ||||||
|  | once_cell = "1.7" | ||||||
| rand = "0.8.3" | rand = "0.8.3" | ||||||
| serial_test = "0.5.1" | serial_test = "0.5.1" | ||||||
|  |  | ||||||
|   | |||||||
| @@ -37,7 +37,7 @@ | |||||||
| | WORLD                    | `Dedicated`            | TRUE     | This is used to generate the name of your world. | | | 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. | | | PUBLIC                   | `1`                    | FALSE    | Sets whether or not your server is public on the server list. | | ||||||
| | PASSWORD                 | `12345`                | TRUE     | Set this to something unique! | | | 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_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              | `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]. | | 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 |             about: Sets the output file to use | ||||||
|             required: true |             required: true | ||||||
|             index: 2 |             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: |   - notify: | ||||||
|       about: Sends a notification to the provided webhook. |       about: Sends a notification to the provided webhook. | ||||||
|       version: "1.1" |       version: "1.1" | ||||||
|   | |||||||
| @@ -1,24 +1,6 @@ | |||||||
| use crate::executable::execute_mut; | use crate::server; | ||||||
| use crate::steamcmd::steamcmd_command; | use std::process::ExitStatus; | ||||||
| use crate::utils::get_working_dir; |  | ||||||
| use log::{debug, info}; |  | ||||||
| use std::process::{ExitStatus, Stdio}; |  | ||||||
|  |  | ||||||
| pub fn invoke(app_id: i64) -> std::io::Result<ExitStatus> { | pub fn invoke(app_id: i64) -> std::io::Result<ExitStatus> { | ||||||
|   info!("Installing {} to {}", app_id, get_working_dir()); |   server::install(app_id) | ||||||
|   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) |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,3 +4,4 @@ pub mod install; | |||||||
| pub mod notify; | pub mod notify; | ||||||
| pub mod start; | pub mod start; | ||||||
| pub mod stop; | pub mod stop; | ||||||
|  | pub mod update; | ||||||
|   | |||||||
| @@ -1,107 +1,35 @@ | |||||||
| mod bepinex; | use crate::files::config::load_config; | ||||||
|  | use crate::server; | ||||||
| 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 clap::ArgMatches; | use clap::ArgMatches; | ||||||
| use daemonize::Daemonize; |  | ||||||
| use log::{debug, error, info}; | use log::{debug, error, info}; | ||||||
| use std::process::{exit, Child}; | use std::process::exit; | ||||||
|  |  | ||||||
| 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() |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| pub fn invoke(args: &ArgMatches) { | pub fn invoke(args: &ArgMatches) { | ||||||
|   info!("Setting up start scripts..."); |   info!("Setting up start scripts..."); | ||||||
|   debug!("Loading config file..."); |   debug!("Loading config file..."); | ||||||
|   let config = config_file(); |   let config = load_config(); | ||||||
|   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 dry_run: bool = args.is_present("dry_run"); |   let dry_run: bool = args.is_present("dry_run"); | ||||||
|   debug!("Dry run condition: {}", dry_run); |   debug!("Dry run condition: {}", dry_run); | ||||||
|  |  | ||||||
|   info!("Looking for burial mounds..."); |   info!("Looking for burial mounds..."); | ||||||
|   if !dry_run { |   if !dry_run { | ||||||
|     let stdout = create_file(format!("{}/logs/valheim_server.log", get_working_dir()).as_str()); |     match server::start_daemonized(config) { | ||||||
|     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() { |  | ||||||
|       Ok(_) => info!("Success, daemonized"), |       Ok(_) => info!("Success, daemonized"), | ||||||
|       Err(e) => error!("Error, {}", e), |       Err(e) => { | ||||||
|  |         error!("Error: {}", e); | ||||||
|  |         exit(1); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } else { |   } else { | ||||||
|     info!( |     info!( | ||||||
|       "This command would have launched\n{} -nographics -batchmode -port {} -name {} -world {} -password {} -public {}", |       "This command would have launched\n{} -nographics -batchmode -port {} -name {} -world {} -password {} -public {}", | ||||||
|       &config_content.command, |       &config.command, | ||||||
|       &config_content.port, |       &config.port, | ||||||
|       &config_content.name, |       &config.name, | ||||||
|       &config_content.world, |       &config.world, | ||||||
|       &config_content.password, |       &config.password, | ||||||
|       &config_content.public, |       &config.public, | ||||||
|     ) |     ) | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,60 +1,20 @@ | |||||||
| use crate::utils::{get_working_dir, server_installed, VALHEIM_EXECUTABLE_NAME}; |  | ||||||
|  |  | ||||||
| use clap::ArgMatches; | use clap::ArgMatches; | ||||||
| use log::{error, info}; | use log::{error, info}; | ||||||
| use sysinfo::{ProcessExt, Signal, System, SystemExt}; |  | ||||||
|  |  | ||||||
| use std::{thread, time::Duration}; | use std::process::exit; | ||||||
|  |  | ||||||
| fn send_shutdown() { | use crate::{constants, server, utils::get_working_dir}; | ||||||
|   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!") |  | ||||||
| } |  | ||||||
|  |  | ||||||
| pub fn invoke(args: &ArgMatches) { | pub fn invoke(args: &ArgMatches) { | ||||||
|   info!("Stopping server {}", get_working_dir()); |   info!("Stopping server {}", get_working_dir()); | ||||||
|   if args.is_present("dry_run") { |   if args.is_present("dry_run") { | ||||||
|     info!("This command would have run: "); |     info!("This command would have run: "); | ||||||
|     info!("kill -2 {}", VALHEIM_EXECUTABLE_NAME) |     info!("kill -2 {}", constants::VALHEIM_EXECUTABLE_NAME) | ||||||
|   } else { |   } else { | ||||||
|     if !server_installed() { |     if !server::is_installed() { | ||||||
|       error!("Failed to find server executable!"); |       error!("Failed to find server executable!"); | ||||||
|       return; |       exit(1); | ||||||
|     } |     } | ||||||
|     send_shutdown(); |     server::blocking_shutdown(); | ||||||
|     wait_for_server_exit(); |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										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::ValheimArguments; | ||||||
| use crate::files::{FileManager, ManagedFile}; | use crate::files::{FileManager, ManagedFile}; | ||||||
| use crate::utils::environment::fetch_var; | 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 clap::ArgMatches; | ||||||
| use log::{debug, error}; | use log::{debug, error}; | ||||||
| use std::fs; | use std::fs; | ||||||
| @@ -10,6 +11,19 @@ use std::process::exit; | |||||||
|  |  | ||||||
| const ODIN_CONFIG_FILE_VAR: &str = "ODIN_CONFIG_FILE"; | 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 { | pub fn config_file() -> ManagedFile { | ||||||
|   let name = fetch_var(ODIN_CONFIG_FILE_VAR, "config.json"); |   let name = fetch_var(ODIN_CONFIG_FILE_VAR, "config.json"); | ||||||
|   debug!("Config file set to: {}", name); |   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 { | 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( |   let command = match fs::canonicalize(PathBuf::from(get_variable( | ||||||
|     args, |     args, | ||||||
|     "server_executable", |     "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::logger::OdinLogger; | ||||||
| use crate::utils::environment; | use crate::utils::environment; | ||||||
| mod commands; | mod commands; | ||||||
|  | mod constants; | ||||||
| mod errors; | mod errors; | ||||||
| mod executable; | mod executable; | ||||||
| mod files; | mod files; | ||||||
| mod logger; | mod logger; | ||||||
| mod messages; | mod messages; | ||||||
|  | mod mods; | ||||||
| mod notifications; | mod notifications; | ||||||
|  | mod server; | ||||||
| mod steamcmd; | mod steamcmd; | ||||||
| mod utils; | mod utils; | ||||||
|  |  | ||||||
| use crate::notifications::enums::event_status::EventStatus; | use crate::notifications::enums::event_status::EventStatus; | ||||||
| use crate::notifications::enums::notification_event::NotificationEvent; | use crate::notifications::enums::notification_event::NotificationEvent; | ||||||
|  |  | ||||||
| const VERSION: &str = env!("CARGO_PKG_VERSION"); |  | ||||||
| static LOGGER: OdinLogger = OdinLogger; | static LOGGER: OdinLogger = OdinLogger; | ||||||
| static GAME_ID: i64 = 896660; |  | ||||||
|  |  | ||||||
| fn setup_logger(debug: bool) -> Result<(), SetLoggerError> { | fn setup_logger(debug: bool) -> Result<(), SetLoggerError> { | ||||||
|   let level = if debug { |   let level = if debug { | ||||||
| @@ -35,7 +36,7 @@ fn setup_logger(debug: bool) -> Result<(), SetLoggerError> { | |||||||
| fn main() { | fn main() { | ||||||
|   // The YAML file is found relative to the current file, similar to how modules are found |   // The YAML file is found relative to the current file, similar to how modules are found | ||||||
|   let yaml = load_yaml!("cli.yaml"); |   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 matches = app.get_matches(); | ||||||
|   let debug_mode = matches.is_present("debug") || environment::fetch_var("DEBUG_MODE", "0").eq("1"); |   let debug_mode = matches.is_present("debug") || environment::fetch_var("DEBUG_MODE", "0").eq("1"); | ||||||
|   setup_logger(debug_mode).unwrap(); |   setup_logger(debug_mode).unwrap(); | ||||||
| @@ -49,7 +50,7 @@ fn main() { | |||||||
|   }; |   }; | ||||||
|   if let Some(ref _match) = matches.subcommand_matches("install") { |   if let Some(ref _match) = matches.subcommand_matches("install") { | ||||||
|     debug!("Launching install command..."); |     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()) |     handle_exit_status(result, "Successfully installed Valheim!".to_string()) | ||||||
|   }; |   }; | ||||||
|   if let Some(ref start_matches) = matches.subcommand_matches("start") { |   if let Some(ref start_matches) = matches.subcommand_matches("start") { | ||||||
| @@ -72,4 +73,9 @@ fn main() { | |||||||
|     debug!("Launching notify command..."); |     debug!("Launching notify command..."); | ||||||
|     commands::notify::invoke(notify_matches); |     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 crate::utils::{environment, get_working_dir, path_exists}; | ||||||
| use log::{debug, info}; | use log::{debug, info}; | ||||||
| use std::ops::Add; | use std::ops::Add; | ||||||
| @@ -46,9 +46,10 @@ pub struct BepInExEnvironment { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub fn build_environment() -> 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( |   let ld_library_path = environment::fetch_var( | ||||||
|     LD_LIBRARY_PATH_VAR, |     constants::LD_LIBRARY_PATH_VAR, | ||||||
|     format!("./linux64:{}", doorstop_libs()).as_str(), |     format!("./linux64:{}", doorstop_libs()).as_str(), | ||||||
|   ); |   ); | ||||||
|   let doorstop_invoke_dll_value = doorstop_invoke_dll(); |   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, |       &environment.doorstop_corlib_override_path, | ||||||
|     ) |     ) | ||||||
|     // LD_LIBRARY_PATH must not have quotes around it.
 |     // 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.
 |     // 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.
 |     // DYLD_LIBRARY_PATH is weird af and MUST have quotes around it.
 | ||||||
|     .env( |     .env( | ||||||
|       DYLD_LIBRARY_PATH_VAR, |       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 | line | ||||||
|  |  | ||||||
|  |  | ||||||
| cd /home/steam/valheim || exit 1 | cd /home/steam/valheim || exit 1 | ||||||
| log "Stopping server..." |  | ||||||
| odin stop || exit 1 |  | ||||||
|  |  | ||||||
| if [ "${AUTO_BACKUP_ON_UPDATE:=0}" -eq 1 ]; then | if odin update --check; then | ||||||
|     /bin/bash /home/steam/scripts/auto_backup.sh "pre-update-backup" |     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 | fi | ||||||
|  |  | ||||||
| log "Installing Updates..." |  | ||||||
| odin install || exit 1 |  | ||||||
| log "Starting server..." |  | ||||||
| odin start || exit 1 |  | ||||||
| line | 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::env; | ||||||
| use std::path::Path; | use std::path::Path; | ||||||
|  |  | ||||||
| const ODIN_WORKING_DIR: &str = "ODIN_WORKING_DIR"; | use crate::constants; | ||||||
| pub const VALHEIM_EXECUTABLE_NAME: &str = "valheim_server.x86_64"; |  | ||||||
|  |  | ||||||
| pub fn get_working_dir() -> String { | pub fn get_working_dir() -> String { | ||||||
|   environment::fetch_var( |   environment::fetch_var( | ||||||
|     ODIN_WORKING_DIR, |     constants::ODIN_WORKING_DIR, | ||||||
|     env::current_dir().unwrap().to_str().unwrap(), |     env::current_dir().unwrap().to_str().unwrap(), | ||||||
|   ) |   ) | ||||||
| } | } | ||||||
| @@ -33,10 +32,6 @@ pub fn get_variable(args: &ArgMatches, name: &str, default: String) -> String { | |||||||
|     .to_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 { | pub(crate) fn path_exists(path: &str) -> bool { | ||||||
|   let state = Path::new(path).exists(); |   let state = Path::new(path).exists(); | ||||||
|   debug!( |   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