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
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