Added BepInEx Support (#148)

* Memory Check

* Removed path on ci

* Valheim BepInEx support

* Separation of concern.

* Added switch for installation

* Yall dont need to run the dev compose ;P

* Refactored to make debugging easier

* Updated to use quotes and readme
This commit is contained in:
Michael
2021-02-22 17:49:21 -08:00
committed by GitHub
parent fc2a59226a
commit 474ed4d5cd
28 changed files with 757 additions and 422 deletions

View File

@@ -3,7 +3,7 @@ root = true
[*]
# charset = utf-8
end_of_line = lf
indent_size = 4
indent_size = 2
indent_style = space
insert_final_newline = true
# max_line_length = 120

View File

@@ -20,3 +20,5 @@ jobs:
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- name: Lint
run: cargo fmt -- --check

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@
target/
tmp/
*.env*
docker-compose.*.yml

1
.rustfmt.toml Normal file
View File

@@ -0,0 +1 @@
tab_spaces = 2

2
Cargo.lock generated
View File

@@ -234,7 +234,7 @@ dependencies = [
[[package]]
name = "odin"
version = "1.1.0"
version = "1.2.0"
dependencies = [
"cargo-husky",
"clap",

View File

@@ -1,6 +1,6 @@
[package]
name = "odin"
version = "1.1.0"
version = "1.2.0"
authors = ["mbround18 <12646562+mbround18@users.noreply.github.com>"]
edition = "2018"
@@ -17,7 +17,6 @@ daemonize = "0.4"
tar = "0.4"
flate2 = "1.0"
[dev-dependencies]
rand = "0.8.3"

View File

@@ -1,21 +1,22 @@
# ------------------ #
# -- Odin Builder -- #
# ------------------ #
FROM mbround18/valheim-odin:latest as RustBuilder
ARG ODIN_IMAGE_VERSION=latest
FROM mbround18/valheim-odin:${ODIN_IMAGE_VERSION} as RustBuilder
# --------------- #
# -- Steam CMD -- #
# --------------- #
FROM cm2network/steamcmd:root
RUN apt-get update \
&& apt-get install -y \
htop net-tools nano \
netcat curl wget \
cron sudo gosu dos2unix \
libsdl2-2.0-0 jq \
&& rm -rf /var/lib/apt/lists/* \
&& gosu nobody true \
RUN apt-get update \
&& apt-get install -y \
htop net-tools nano gcc g++ \
netcat curl wget zip unzip \
cron sudo gosu dos2unix \
libsdl2-2.0-0 jq libc6-dev \
&& rm -rf /var/lib/apt/lists/* \
&& gosu nobody true \
&& dos2unix
# Container informaiton
@@ -49,19 +50,22 @@ ENV AUTO_BACKUP_DAYS_TO_LIVE "3"
ENV AUTO_BACKUP_ON_UPDATE "0"
ENV AUTO_BACKUP_ON_SHUTDOWN "0"
COPY --chmod=755 ./src/scripts/*.sh /home/steam/scripts/
COPY --chmod=755 ./src/scripts/entrypoint.sh /entrypoint.sh
COPY --from=RustBuilder --chmod=755 /data/odin/target/release/odin /usr/local/bin/odin
COPY --chown=steam:steam ./src/scripts/steam_bashrc.sh /home/steam/.bashrc
COPY ./src/scripts/*.sh /home/steam/scripts/
COPY ./src/scripts/entrypoint.sh /entrypoint.sh
COPY --from=RustBuilder /data/odin/target/release/odin /usr/local/bin/odin
COPY ./src/scripts/steam_bashrc.sh /home/steam/.bashrc
RUN usermod -u ${PUID} steam \
&& groupmod -g ${PGID} steam \
&& chsh -s /bin/bash steam \
&& printf "${GITHUB_SHA}\n${GITHUB_REF}\n${GITHUB_REPOSITORY}\n" >/home/steam/.version
&& printf "${GITHUB_SHA}\n${GITHUB_REF}\n${GITHUB_REPOSITORY}\n" >/home/steam/.version \
&& chmod 755 -R /home/steam/scripts/ \
&& chmod 755 /entrypoint.sh \
&& chmod 755 /usr/local/bin/odin
HEALTHCHECK --interval=1m --timeout=3s \
CMD gosu steam pidof valheim_server.x86_64 || exit 1
CMD pidof valheim_server.x86_64 || exit 1
ENTRYPOINT ["/bin/bash","/entrypoint.sh"]
CMD ["/bin/bash", "/home/steam/scripts/start_valheim.sh"]

View File

@@ -6,7 +6,8 @@ FROM rust:latest as RustBuilder
WORKDIR /data/odin
COPY . .
RUN cargo build --release
RUN cargo build --release && find . ! -name 'odin' -type f -exec rm -f {} +
ENTRYPOINT ["/data/odin/target/release/odin"]
CMD ["--version"]

View File

@@ -13,14 +13,18 @@
<img src="https://img.shields.io/github/workflow/status/mbround18/valheim-docker/Rust?label=Docker&style=for-the-badge">
</a>
## Docker
> [If you are looking for a guide on how to get started click here](https://github.com/mbround18/valheim-docker/discussions/28)
>
> Mod Support! It is supported to launch the server with BepInEx but!!!!! as a disclaimer! You take responsibility for debugging why your server won't start.
> Modding is not supported by the Valheim officially currently; Which means you WILL run into errors. This repo has been tested with running ValheimPlus as a test mod and does not have any issues.
> See [Getting started with mods]
### Environment Variables
> See further on down for advanced environment variables.
| Variable | Default | Required | Description |
|--------------------------|------------------------|----------|-------------|
| TZ | `America/Los_Angeles` | FALSE | Sets what timezone your container is running on. This is used for timestamps and cron jobs. [Click Here for which timezones are valid.](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) |
@@ -39,7 +43,6 @@
| AUTO_BACKUP_DAYS_TO_LIVE | `3` | FALSE | This is the number of days you would like to keep backups for. While backups are compressed and generally small it is best to change this number as needed. |
| AUTO_BACKUP_ON_UPDATE | `0` | FALSE | Create a backup on right before updating and starting your server. |
| AUTO_BACKUP_ON_SHUTDOWN | `0` | FALSE | Create a backup on shutdown. |
| ODIN_CONFIG_FILE | `config.json` | FALSE | This file stores start parameters to restart the instance, change if you run multiple container instances on the same host |
### Docker Compose
@@ -100,6 +103,36 @@ services:
- ./valheim/backups:/home/steam/backups
```
### Advanced Environment Variables
> Editing or adding these can cause issues! They are intended to give you more access to the system.
#### Odin Specific
> These are set automatically by [Odin];
> you DO NOT need to set these and only mess with them if you Know what you are doing.
| Variable | Default | Required | Description |
|--------------------------|------------------------|----------|-------------|
| DEBUG_MOD | `0` | FALSE | Set to `1` if you want a noisy output and to see what Odin is doing.
| ODIN_CONFIG_FILE | `config.json` | FALSE | This file stores start parameters to restart the instance, change if you run multiple container instances on the same host |
| ODIN_WORKING_DIR | `$PWD` | FALSE | Sets the directory you wish to run `odin` commands in and can be used to set where valheim is managed from. |
#### BepInEx/Modded Variables
> These are set automatically by [Odin] for a basic BepInEx installation;
> you DO NOT need to set these and only mess with them if you Know what you are doing.
| Variable | Default | Required | Description |
|--------------------------|----------------------------------------------------------|----------|-------------|
| LD_PRELOAD | `libdoorstop_x64.so` | TRUE | Sets which library to preload on Valheim start. |
| LD_LIBRARY_PATH | `./linux64:/home/steam/valheim/doorstop_libs` | TRUE | Sets which library paths it should look in for preload libs. |
| DOORSTOP_ENABLE | `TRUE` | TRUE | Enables Doorstop or not. |
| DOORSTOP_LIB | `libdoorstop_x64.so` | TRUE | Which doorstop lib to load |
| DOORSTOP_LIBS | `/home/steam/valheim/doorstop_libs` | TRUE | Where to look for doorstop libs. |
| DOORSTOP_INVOKE_DLL_PATH | `/home/steam/valheim/BepInEx/core/BepInEx.Preloader.dll` | TRUE | BepInEx preload dll to load. |
| DYLD_LIBRARY_PATH | `"/home/steam/valheim/doorstop_libs"` | TRUE | Sets the library paths. NOTE: This variable is weird and MUST have quotes around it! |
| DYLD_INSERT_LIBRARIES | `/home/steam/valheim/doorstop_libs/libdoorstop_x64.so` | TRUE | Sets which library to load. |
### [Odin]
@@ -108,7 +141,8 @@ This repo has a CLI tool called [Odin] in it! It is used for managing the server
## Versions:
- latest (Stable):
- [#100] Added backup feature to run based on cronjob.
- [#100] Added backup feature to run based on cronjob.
- [#148] Added Mod support
- 1.2.0 (Stable):
- Readme update to include the versions section and environment variables section.
- [#18] Changed to `root` as the default user to allow updated steams User+Group IDs.
@@ -135,6 +169,7 @@ This repo has a CLI tool called [Odin] in it! It is used for managing the server
- Has a bug in which it does not read passed in variables appropriately to Odin. Env variables are not impacted see [#3].
[//]: <> (Github Issues below...........)
[#148]: https://github.com/mbround18/valheim-docker/pull/148
[#100]: https://github.com/mbround18/valheim-docker/pull/100
[#89]: https://github.com/mbround18/valheim-docker/pull/89
[#77]: https://github.com/mbround18/valheim-docker/pull/77
@@ -151,6 +186,7 @@ This repo has a CLI tool called [Odin] in it! It is used for managing the server
[//]: <> (Links below...................)
[Odin]: ./docs/odin.md
[Valheim]: https://www.valheimgame.com/
[Getting started with mods]: ./docs/getting_started_with_mods.md
[If you need help figuring out a cron schedule click here]: https://crontab.guru/#0_1_*_*_*
[//]: <> (Image Base Url: https://github.com/mbround18/valheim-docker/blob/main/docs/assets/name.png?raw=true)

View File

@@ -0,0 +1,23 @@
# Getting started with Mods
> For this example we will be going over installing ValheimPlus. There is a lot of mysteries when it comes to modding but this should help you get started.
## Steps
1. Download the mod file, for our example we want the `UnixServer.zip` from `https://github.com/nxPublic/ValheimPlus/releases`
2. Place the file in your server volume mount. `cp UnixServer.zip /home/youruser/valheim/server`
3. Unzip the archive `unzip UnixServer.zip -d .` hit A to replace all as needed.
4. Restart your server.
> Odin automatically detects if you are running with BepInEx and adds the environment variables appropriately.
>
> DISCLAIMER! Modding your server can cause a lot of errors.
> Please do NOT post an issue on the valheim-docker repo based on mod issues.
> By installing mods, you agree that you will do a root cause analysis to why your server is failing before you make a post.
> Modding is currently unsupported by the Valheim developers and limited support by the valheim-docker repo.
> If you have issues please contact the MOD developer FIRST based on the output logs.
## Valheim Updated Help!!!!
Mod development is slow, and the more mods you have the more complicated it will be to keep everything up to date.
It is a suggestion that you turn off the AUTO_UPDATE variable when you are using mods and refrain from updating your local client until all your mods have been updated.

View File

@@ -59,7 +59,7 @@ subcommands:
takes_value: true
- install:
about: Installs Valheim with steamcmd
version: "2.0"
version: "2.1"
author: mbround18
- start:
about: Starts Valheim

View File

@@ -6,24 +6,24 @@ use std::fs::File;
use std::process::exit;
pub fn invoke(args: &ArgMatches) {
let input = args.value_of("INPUT_DIR").unwrap();
let output = args.value_of("OUTPUT_FILE").unwrap();
debug!("Creating archive of {}", input);
debug!("Output set to {}", output);
let tar_gz = match File::create(output) {
Ok(file) => file,
Err(_) => {
error!("Failed to create backup file at {}", output);
exit(1)
}
};
let enc = GzEncoder::new(tar_gz, Compression::default());
let mut tar = tar::Builder::new(enc);
match tar.append_dir_all("saves", input) {
Ok(_) => debug!("Successfully created backup zip at {}", output),
Err(_) => {
error!("Failed to add {} to backup file", input);
exit(1)
}
};
let input = args.value_of("INPUT_DIR").unwrap();
let output = args.value_of("OUTPUT_FILE").unwrap();
debug!("Creating archive of {}", input);
debug!("Output set to {}", output);
let tar_gz = match File::create(output) {
Ok(file) => file,
Err(_) => {
error!("Failed to create backup file at {}", output);
exit(1)
}
};
let enc = GzEncoder::new(tar_gz, Compression::default());
let mut tar = tar::Builder::new(enc);
match tar.append_dir_all("saves", input) {
Ok(_) => debug!("Successfully created backup zip at {}", output),
Err(_) => {
error!("Failed to add {} to backup file", input);
exit(1)
}
};
}

View File

@@ -1,7 +1,10 @@
use crate::files::config::{config_file, write_config};
use clap::ArgMatches;
use log::debug;
pub fn invoke(args: &ArgMatches) {
let config = config_file();
write_config(config, args);
debug!("Pulling config file...");
let config = config_file();
debug!("Writing config file...");
write_config(config, args);
}

View File

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

View File

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

View File

@@ -0,0 +1,123 @@
use crate::commands::start::{LD_LIBRARY_PATH_VAR, LD_PRELOAD_VAR};
use crate::utils::{fetch_env, get_working_dir};
use log::{debug, info};
use std::ops::Add;
use std::path::Path;
use std::process::{Child, Command};
const DYLD_LIBRARY_PATH_VAR: &str = "DYLD_LIBRARY_PATH";
const DYLD_INSERT_LIBRARIES_VAR: &str = "DYLD_INSERT_LIBRARIES";
const DOORSTOP_ENABLE_VAR: &str = "DOORSTOP_ENABLE";
const DOORSTOP_LIB_VAR: &str = "DOORSTOP_LIB";
const DOORSTOP_LIBS_VAR: &str = "DOORSTOP_LIBS";
const DOORSTOP_INVOKE_DLL_PATH_VAR: &str = "DOORSTOP_INVOKE_DLL_PATH";
fn doorstop_lib() -> String {
fetch_env(DOORSTOP_LIB_VAR, "libdoorstop_x64.so", false)
}
fn doorstop_libs() -> String {
fetch_env(
DOORSTOP_LIBS_VAR,
format!("{}/doorstop_libs", get_working_dir()).as_str(),
false,
)
}
fn doorstop_insert_lib() -> String {
let default = format!("{}/{}", doorstop_libs(), doorstop_lib().replace(":", ""));
fetch_env(DYLD_INSERT_LIBRARIES_VAR, default.as_str(), false)
}
fn doorstop_invoke_dll() -> String {
fetch_env(
DOORSTOP_INVOKE_DLL_PATH_VAR,
format!("{}/BepInEx/core/BepInEx.Preloader.dll", get_working_dir()).as_str(),
false,
)
}
pub fn is_bepinex_installed() -> bool {
let doorstep_insert_lib_exists = Path::new(doorstop_insert_lib().as_str()).exists();
let doorstep_libs_dir_exists = Path::new(doorstop_libs().as_str()).exists();
debug!("doorstep insert lib exists: {}", doorstep_insert_lib_exists);
debug!(
"doorstep libs directory exists: {}",
doorstep_libs_dir_exists
);
doorstep_insert_lib_exists && doorstep_libs_dir_exists
}
pub struct BepInExEnvironment {
ld_preload: String,
ld_library_path: String,
doorstop_enable: String,
doorstop_invoke_dll: String,
dyld_library_path: String,
dyld_insert_libraries: String,
}
pub fn build_environment() -> BepInExEnvironment {
let ld_preload = fetch_env(LD_PRELOAD_VAR, "", false).add(doorstop_lib().as_str());
let ld_library_path = fetch_env(
LD_LIBRARY_PATH_VAR,
format!("./linux64:{}", doorstop_libs()).as_str(),
false,
);
let doorstop_invoke_dll_value = doorstop_invoke_dll();
let dyld_library_path = fetch_env(DYLD_LIBRARY_PATH_VAR, doorstop_libs().as_str(), false);
let dyld_insert_libraries = fetch_env(
DYLD_INSERT_LIBRARIES_VAR,
doorstop_insert_lib().as_str(),
false,
);
info!("Loading BepInEx Environment...");
let environment = BepInExEnvironment {
ld_preload,
ld_library_path,
doorstop_enable: true.to_string().to_uppercase(),
doorstop_invoke_dll: doorstop_invoke_dll_value,
dyld_library_path,
dyld_insert_libraries,
};
debug!("LD_PRELOAD: {}", &environment.ld_preload);
debug!("LD_LIBRARY_PATH: {}", &environment.ld_library_path);
debug!("DOORSTOP_ENABLE: {}", &environment.doorstop_enable);
debug!(
"DOORSTOP_INVOKE_DLL_PATH: {}",
&environment.doorstop_invoke_dll
);
debug!("DYLD_LIBRARY_PATH: {}", &environment.dyld_library_path);
debug!(
"DYLD_INSERT_LIBRARIES: {}",
&environment.dyld_insert_libraries
);
environment
}
pub fn invoke(command: &mut Command, environment: &BepInExEnvironment) -> std::io::Result<Child> {
info!("BepInEx found! Setting up Environment...");
command
// DOORSTOP_ENABLE must not have quotes around it.
.env(DOORSTOP_ENABLE_VAR, &environment.doorstop_enable)
// DOORSTOP_INVOKE_DLL_PATH must not have quotes around it.
.env(
DOORSTOP_INVOKE_DLL_PATH_VAR,
&environment.doorstop_invoke_dll,
)
// LD_LIBRARY_PATH must not have quotes around it.
.env(LD_LIBRARY_PATH_VAR, &environment.ld_library_path)
// LD_PRELOAD must not have quotes around it.
.env(LD_PRELOAD_VAR, &environment.ld_preload)
// DYLD_LIBRARY_PATH is weird af and MUST have quotes around it.
.env(
DYLD_LIBRARY_PATH_VAR,
format!("\"{}\"", &environment.dyld_library_path),
)
// DYLD_INSERT_LIBRARIES must not have quotes around it.
.env(
DYLD_INSERT_LIBRARIES_VAR,
&environment.dyld_insert_libraries,
)
.spawn()
}

View File

@@ -7,54 +7,54 @@ use sysinfo::{ProcessExt, Signal, System, SystemExt};
use std::{thread, time::Duration};
fn send_shutdown() {
info!("Scanning for Valheim process");
let mut system = System::new();
system.refresh_all();
let processes = system.get_process_by_name(VALHEIM_EXECUTABLE_NAME);
if processes.is_empty() {
info!("Process NOT found!")
} else {
for found_process in processes {
info!(
"Found Process with pid {}! Sending Interrupt!",
found_process.pid()
);
if found_process.kill(Signal::Interrupt) {
info!("Process signal interrupt sent successfully!")
} else {
error!("Failed to send signal interrupt!")
}
}
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!("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!")
}
info!("Server has been shutdown successfully!")
}
pub fn invoke(args: &ArgMatches) {
info!("Stopping server {}", get_working_dir());
if args.is_present("dry_run") {
info!("This command would have run: ");
info!("kill -2 {}", VALHEIM_EXECUTABLE_NAME)
} else {
if !server_installed() {
error!("Failed to find server executable!");
return;
}
send_shutdown();
wait_for_server_exit();
info!("Stopping server {}", get_working_dir());
if args.is_present("dry_run") {
info!("This command would have run: ");
info!("kill -2 {}", VALHEIM_EXECUTABLE_NAME)
} else {
if !server_installed() {
error!("Failed to find server executable!");
return;
}
send_shutdown();
wait_for_server_exit();
}
}

View File

@@ -3,56 +3,56 @@ use std::path::Path;
use std::process::{exit, Command, ExitStatus};
pub fn find_command(executable: &str) -> Option<Command> {
let script_file = Path::new(executable);
if script_file.exists() {
info!("Executing: {} .....", executable.to_string());
Option::from(Command::new(executable.to_string()))
} else {
match which::which(executable) {
Ok(executable_path) => Option::from(Command::new(executable_path)),
Err(_e) => {
error!("Failed to find {} in path", executable);
None
}
}
let script_file = Path::new(executable);
if script_file.exists() {
info!("Executing: {} .....", executable.to_string());
Option::from(Command::new(executable.to_string()))
} else {
match which::which(executable) {
Ok(executable_path) => Option::from(Command::new(executable_path)),
Err(_e) => {
error!("Failed to find {} in path", executable);
None
}
}
}
}
pub fn create_execution(executable: &str) -> Command {
match find_command(executable) {
Some(command) => command,
None => {
error!("Unable to launch command {}", executable);
exit(1)
}
match find_command(executable) {
Some(command) => command,
None => {
error!("Unable to launch command {}", executable);
exit(1)
}
}
}
pub fn execute_mut(command: &mut Command) -> std::io::Result<ExitStatus> {
match command.spawn() {
Ok(mut subprocess) => subprocess.wait(),
_ => {
error!("Failed to run process!");
exit(1)
}
match command.spawn() {
Ok(mut subprocess) => subprocess.wait(),
_ => {
error!("Failed to run process!");
exit(1)
}
}
}
pub fn handle_exit_status(result: std::io::Result<ExitStatus>, success_message: String) {
match result {
Ok(exit_status) => {
if exit_status.success() {
info!("{}", success_message);
} else {
match exit_status.code() {
Some(code) => info!("Exited with status code: {}", code),
None => info!("Process terminated by signal"),
}
}
}
_ => {
error!("An error has occurred and the command returned no exit code!");
exit(1)
match result {
Ok(exit_status) => {
if exit_status.success() {
info!("{}", success_message);
} else {
match exit_status.code() {
Some(code) => info!("Exited with status code: {}", code),
None => info!("Process terminated by signal"),
}
}
}
_ => {
error!("An error has occurred and the command returned no exit code!");
exit(1)
}
}
}

View File

@@ -2,7 +2,7 @@ use crate::files::ValheimArguments;
use crate::files::{FileManager, ManagedFile};
use crate::utils::{get_variable, get_working_dir, VALHEIM_EXECUTABLE_NAME};
use clap::ArgMatches;
use log::error;
use log::{debug, error};
use std::env;
use std::fs;
use std::path::PathBuf;
@@ -11,66 +11,70 @@ use std::process::exit;
const ODIN_CONFIG_FILE_VAR: &str = "ODIN_CONFIG_FILE";
pub fn config_file() -> ManagedFile {
let name = env::var(ODIN_CONFIG_FILE_VAR).unwrap_or_else(|_| "config.json".to_string());
ManagedFile { name }
let name = env::var(ODIN_CONFIG_FILE_VAR).unwrap_or_else(|_| "config.json".to_string());
debug!("Config file set to: {}", name);
ManagedFile { name }
}
pub fn read_config(config: ManagedFile) -> ValheimArguments {
let content = config.read();
if content.is_empty() {
panic!("Please initialize odin with `odin configure`. See `odin configure --help`")
}
serde_json::from_str(content.as_str()).unwrap()
let content = config.read();
if content.is_empty() {
panic!("Please initialize odin with `odin configure`. See `odin configure --help`")
}
serde_json::from_str(content.as_str()).unwrap()
}
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(), VALHEIM_EXECUTABLE_NAME.to_string()].join("/");
let command = match fs::canonicalize(PathBuf::from(get_variable(
args,
"server_executable",
server_executable.to_string(),
))) {
std::result::Result::Ok(command_path) => command_path.to_str().unwrap().to_string(),
std::result::Result::Err(_) => {
error!("Failed to find server executable! Please run `odin install`");
exit(1)
}
};
let command = match fs::canonicalize(PathBuf::from(get_variable(
args,
"server_executable",
server_executable.to_string(),
))) {
std::result::Result::Ok(command_path) => command_path.to_str().unwrap().to_string(),
std::result::Result::Err(_) => {
error!("Failed to find server executable! Please run `odin install`");
exit(1)
}
};
let content = &ValheimArguments {
port: get_variable(args, "port", "2456".to_string()),
name: get_variable(args, "name", "Valheim powered by Odin".to_string()),
world: get_variable(args, "world", "Dedicated".to_string()),
public: get_variable(args, "public", "1".to_string()),
password: get_variable(args, "password", "12345".to_string()),
command,
};
config.write(serde_json::to_string(content).unwrap())
let content = &ValheimArguments {
port: get_variable(args, "port", "2456".to_string()),
name: get_variable(args, "name", "Valheim powered by Odin".to_string()),
world: get_variable(args, "world", "Dedicated".to_string()),
public: get_variable(args, "public", "1".to_string()),
password: get_variable(args, "password", "12345".to_string()),
command,
};
let content_to_write = serde_json::to_string(content).unwrap();
debug!(
"Writing config content: \n{}",
serde_json::to_string(content).unwrap()
);
config.write(content_to_write)
}
#[cfg(test)]
mod tests {
use super::*;
use rand::Rng;
use std::env::current_dir;
use super::*;
use rand::Rng;
use std::env::current_dir;
#[test]
#[should_panic(
expected = "Please initialize odin with `odin configure`. See `odin configure --help`"
)]
fn can_read_config_panic() {
let mut rng = rand::thread_rng();
let n1: u8 = rng.gen();
env::set_var(
ODIN_CONFIG_FILE_VAR,
format!(
"{}/config.{}.json",
current_dir().unwrap().to_str().unwrap(),
n1
),
);
read_config(config_file());
}
#[test]
#[should_panic(
expected = "Please initialize odin with `odin configure`. See `odin configure --help`"
)]
fn can_read_config_panic() {
let mut rng = rand::thread_rng();
let n1: u8 = rng.gen();
env::set_var(
ODIN_CONFIG_FILE_VAR,
format!(
"{}/config.{}.json",
current_dir().unwrap().to_str().unwrap(),
n1
),
);
read_config(config_file());
}
}

View File

@@ -1,4 +1,5 @@
pub mod config;
use crate::executable::create_execution;
use crate::utils::get_working_dir;
use log::{error, info};
@@ -7,84 +8,91 @@ use std::fs;
use std::fs::{remove_file, File};
use std::io::Write;
use std::path::Path;
use std::process::exit;
#[derive(Deserialize, Serialize)]
pub struct ValheimArguments {
pub(crate) port: String,
pub(crate) name: String,
pub(crate) world: String,
pub(crate) public: String,
pub(crate) password: String,
pub(crate) command: String,
pub(crate) port: String,
pub(crate) name: String,
pub(crate) world: String,
pub(crate) public: String,
pub(crate) password: String,
pub(crate) command: String,
}
pub fn create_file(path: &str) -> File {
let output_path = Path::new(path);
match File::create(output_path) {
Ok(file) => file,
Err(_) => {
error!("Failed to create {}", path);
exit(1)
}
}
}
pub trait FileManager {
fn path(&self) -> String;
fn exists(&self) -> bool {
Path::new(self.path().as_str()).exists()
fn path(&self) -> String;
fn exists(&self) -> bool {
Path::new(self.path().as_str()).exists()
}
fn remove(&self) -> bool {
match remove_file(self.path()) {
Ok(_) => {
info!("Successfully deleted {}", self.path());
true
}
Err(_) => {
error!("Did not find or could not delete {}", self.path());
false
}
}
fn remove(&self) -> bool {
match remove_file(self.path()) {
Ok(_) => {
info!("Successfully deleted {}", self.path());
true
}
Err(_) => {
error!("Did not find or could not delete {}", self.path());
false
}
}
}
fn read(&self) -> String {
if self.exists() {
fs::read_to_string(self.path()).unwrap()
} else {
"".to_string()
}
fn read(&self) -> String {
if self.exists() {
fs::read_to_string(self.path()).unwrap()
} else {
"".to_string()
}
}
fn write(&self, content: String) -> bool {
let mut file = create_file(self.path().as_str());
match file.write_all(content.as_bytes()) {
Ok(_) => {
info!("Successfully written {}", self.path());
true
}
_ => {
error!("Failed to write {}", self.path());
false
}
}
fn write(&self, content: String) -> bool {
match File::create(self.path()) {
Ok(mut file) => match file.write_all(content.as_bytes()) {
Ok(_) => {
info!("Successfully written {}", self.path());
true
}
_ => {
error!("Failed to write {}", self.path());
false
}
},
_ => {
error!("Failed to write {}", self.path());
false
}
}
}
fn set_executable(&self) -> bool {
if let Ok(_output) = create_execution("chmod")
.args(&["+x", self.path().as_str()])
.output()
{
info!("Successfully set {} to executable", self.path());
true
} else {
error!("Unable to set {} to executable", self.path());
false
}
}
fn set_executable(&self) -> bool {
if let Ok(_output) = create_execution("chmod")
.args(&["+x", self.path().as_str()])
.output()
{
info!("Successfully set {} to executable", self.path());
true
} else {
error!("Unable to set {} to executable", self.path());
false
}
}
}
pub struct ManagedFile {
pub(crate) name: String,
pub(crate) name: String,
}
impl FileManager for ManagedFile {
fn path(&self) -> String {
let supplied_path = Path::new(self.name.as_str());
if supplied_path.exists() {
supplied_path.to_str().unwrap().to_string()
} else {
format!("{}/{}", get_working_dir(), self.name)
}
fn path(&self) -> String {
let supplied_path = Path::new(self.name.as_str());
if supplied_path.exists() {
supplied_path.to_str().unwrap().to_string()
} else {
format!("{}/{}", get_working_dir(), self.name)
}
}
}

View File

@@ -3,24 +3,24 @@ use log::{Level, Metadata, Record};
pub struct OdinLogger;
impl log::Log for OdinLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= Level::Debug
}
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= Level::Debug
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
let prefix = format!(
"{:width$}",
format!("[ODIN][{}]", record.level()),
width = 13
);
// This creates text blocks of logs if they include a new line.
// I think it looks good <3
let message = format!("{} - {}", prefix, record.args())
.replace("\n", format!("\n{} - ", prefix).as_str());
println!("{}", message);
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
let prefix = format!(
"{:width$}",
format!("[ODIN][{}]", record.level()),
width = 13
);
// This creates text blocks of logs if they include a new line.
// I think it looks good <3
let message = format!("{} - {}", prefix, record.args())
.replace("\n", format!("\n{} - ", prefix).as_str());
println!("{}", message);
}
}
fn flush(&self) {}
fn flush(&self) {}
}

View File

@@ -1,50 +1,64 @@
use clap::{load_yaml, App};
use log::{debug, info, LevelFilter, SetLoggerError};
use crate::executable::handle_exit_status;
use crate::logger::OdinLogger;
use crate::utils::fetch_env;
mod commands;
mod executable;
mod files;
mod logger;
mod messages;
mod steamcmd;
mod utils;
use crate::executable::handle_exit_status;
use crate::logger::OdinLogger;
use clap::{load_yaml, App};
use log::{debug, LevelFilter, SetLoggerError};
const VERSION: &str = env!("CARGO_PKG_VERSION");
static LOGGER: OdinLogger = OdinLogger;
static GAME_ID: i64 = 896660;
fn setup_logger(debug: bool) -> Result<(), SetLoggerError> {
let level = if debug {
LevelFilter::Debug
} else {
LevelFilter::Info
};
let result = log::set_logger(&LOGGER).map(|_| log::set_max_level(level));
debug!("Debugging set to {}", debug.to_string());
result
let level = if debug {
LevelFilter::Debug
} else {
LevelFilter::Info
};
let result = log::set_logger(&LOGGER).map(|_| log::set_max_level(level));
debug!("Debugging set to {}", debug.to_string());
result
}
fn main() {
// The YAML file is found relative to the current file, similar to how modules are found
let yaml = load_yaml!("cli.yaml");
let app = App::from(yaml).version(VERSION);
let matches = app.get_matches();
setup_logger(matches.is_present("debug")).unwrap();
if let Some(ref configure_matches) = matches.subcommand_matches("configure") {
commands::configure::invoke(configure_matches);
};
if let Some(ref _match) = matches.subcommand_matches("install") {
let result = commands::install::invoke(GAME_ID);
handle_exit_status(result, "Successfully installed Valheim!".to_string())
};
if let Some(ref start_matches) = matches.subcommand_matches("start") {
commands::start::invoke(start_matches);
};
if let Some(ref stop_matches) = matches.subcommand_matches("stop") {
commands::stop::invoke(stop_matches);
};
if let Some(ref backup_matches) = matches.subcommand_matches("backup") {
commands::backup::invoke(backup_matches);
};
// The YAML file is found relative to the current file, similar to how modules are found
let yaml = load_yaml!("cli.yaml");
let app = App::from(yaml).version(VERSION);
let matches = app.get_matches();
let debug_mode = matches.is_present("debug") || fetch_env("DEBUG_MODE", "0", false).eq("1");
setup_logger(debug_mode).unwrap();
if !debug_mode {
info!("Run with DEBUG_MODE as 1 if you think there is an issue with Odin");
}
debug!("Debug mode enabled!");
if let Some(ref configure_matches) = matches.subcommand_matches("configure") {
debug!("Launching configure command...");
commands::configure::invoke(configure_matches);
};
if matches.subcommand_matches("install").is_some() {
debug!("Launching install command...");
let result = commands::install::invoke(GAME_ID);
handle_exit_status(result, "Successfully installed Valheim!".to_string())
};
if let Some(ref start_matches) = matches.subcommand_matches("start") {
debug!("Launching start command...");
commands::start::invoke(start_matches);
};
if let Some(ref stop_matches) = matches.subcommand_matches("stop") {
debug!("Launching stop command...");
commands::stop::invoke(stop_matches);
};
if let Some(ref backup_matches) = matches.subcommand_matches("backup") {
debug!("Launching backup command...");
commands::backup::invoke(backup_matches);
};
}

13
src/messages/mod.rs Normal file
View File

@@ -0,0 +1,13 @@
use log::info;
pub fn modding_disclaimer() {
info!("##########################################################################################################################");
info!("DISCLAIMER! Modding your server can cause a lot of errors.");
info!("Please do NOT post issue on the valheim-docker repo based on mod issues.");
info!("By installing mods, you agree that you will do a root cause analysis to why your server is failing before you make a post.");
info!("Modding is currently unsupported by the Valheim developers and limited support by the valheim-docker repo.");
info!("If you have issues please contact the MOD developer FIRST based on the output logs.");
info!("----------------------------------------------------------------");
info!("Additional Note: BepInEx does not support SIGINT shutdown, which means you will have to manually save your world before shutting down.");
info!("##########################################################################################################################");
}

View File

@@ -14,7 +14,7 @@ log "Starting auto backup process..."
odin backup /home/steam/.config/unity3d/IronGate/Valheim "/home/steam/backups/${file_name}" || exit 1
if [ "${AUTO_BACKUP_REMOVE_OLD:=0}" -eq 1 ]; then
find /home/steam/backups/*.tar.gz -mtime +${AUTO_BACKUP_DAYS_TO_LIVE:-5} -exec rm {} \;
find /home/steam/backups -mtime +${AUTO_BACKUP_DAYS_TO_LIVE:-5} -exec rm {} \;
fi
log "Backup process complete! Created ${file_name}"

View File

@@ -54,7 +54,7 @@ setup_cron() {
CRON_SCHEDULE=$3
CRON_ENV="$4"
LOG_LOCATION="/home/steam/valheim/logs/$CRON_NAME.out"
rm $LOG_LOCATION
rm $LOG_LOCATION > /dev/null
printf "%s %s /usr/sbin/gosu steam /bin/bash %s >> %s 2>&1" \
"${CRON_SCHEDULE}" \
"${CRON_ENV:-""}" \

View File

@@ -31,44 +31,45 @@ cleanup() {
fi
}
initialize "Installing Valheim via Odin..."
initialize "Installing Valheim via $(odin --version)..."
log "Variables loaded....."
log "
Port: ${PORT}
Name: ${NAME}
World: ${WORLD}
Public: ${PUBLIC}
Password: (REDACTED)
"
log "Port: ${PORT}"
log "Name: ${NAME}"
log "World: ${WORLD}"
log "Public: ${PUBLIC}"
log "Password: (REDACTED)"
export SteamAppId=${APPID:-892970}
# Setting up server
log "Running Install..."
odin install || exit 1
if [ ! -f "./valheim_server.x86_64" ] || [ "${FORCE_INSTALL:-0}" -eq 1 ]; then
odin install || exit 1
else
log "Skipping install process, looks like valheim_server is already installed :)"
fi
cp /home/steam/steamcmd/linux64/steamclient.so /home/steam/valheim/linux64/
# Setting up server
log "Initializing Variables...."
odin configure || exit 1
# Setting up script traps
trap 'cleanup' INT TERM
log "Herding Cats..."
# Starting server
log "Starting server..."
odin start || exit 1
initialize "
Valheim Server Started...
Keep an eye out for 'Game server connected' in the log!
(this indicates its online without any errors.)
" >> /home/steam/valheim/logs/output.log
sleep 2
# Initializing all logs
log "Herding Graydwarfs..."
log_names=("valheim_server.log" "valheim_server.err" "output.log" "auto-update.out" "auto-backup.out")
log_files=("${log_names[@]/#/\/home\/steam\/valheim\/logs/}")
touch "${log_files[@]}"
touch "${log_files[@]}" # Destroy logs on start up, this can be changed later to roll logs or archive them.
tail -F ${log_files[*]} &
export TAIL_PID=$!
# Waiting for logs.
wait $TAIL_PID

View File

@@ -4,23 +4,23 @@ use std::process::{exit, Command};
const STEAMCMD_EXE: &str = "/home/steam/steamcmd/steamcmd.sh";
pub fn steamcmd_command() -> Command {
match find_command("steamcmd") {
match find_command("steamcmd") {
Some(steamcmd) => {
info!("steamcmd found in path");
steamcmd
}
None => {
error!("Checking for script under steam user.");
match find_command(STEAMCMD_EXE) {
Some(steamcmd) => {
info!("steamcmd found in path");
steamcmd
info!("Using steamcmd script at {}", STEAMCMD_EXE);
steamcmd
}
None => {
error!("Checking for script under steam user.");
match find_command(STEAMCMD_EXE) {
Some(steamcmd) => {
info!("Using steamcmd script at {}", STEAMCMD_EXE);
steamcmd
}
None => {
error!("\nSteamCMD Executable Not Found! \nPlease install steamcmd... \nhttps://developer.valvesoftware.com/wiki/SteamCMD\n");
exit(1);
}
}
error!("\nSteamCMD Executable Not Found! \nPlease install steamcmd... \nhttps://developer.valvesoftware.com/wiki/SteamCMD\n");
exit(1);
}
}
}
}
}

View File

@@ -1,45 +1,99 @@
use clap::ArgMatches;
use log::debug;
use log::error;
use std::env;
use std::fs::File;
use std::path::Path;
use std::process::exit;
const ODIN_WORKING_DIR: &str = "ODIN_WORKING_DIR";
pub const VALHEIM_EXECUTABLE_NAME: &str = "valheim_server.x86_64";
pub fn get_working_dir() -> String {
env::current_dir().unwrap().to_str().unwrap().to_string()
fetch_env(
ODIN_WORKING_DIR,
env::current_dir().unwrap().to_str().unwrap(),
false,
)
}
pub fn get_variable(args: &ArgMatches, name: &str, default: String) -> String {
debug!("Checking env for {}", name);
if let Ok(env_val) = env::var(name.to_uppercase()) {
if !env_val.is_empty() {
debug!("Env variable found {}={}", name, env_val);
return env_val;
}
debug!("Checking env for {}", name);
if let Ok(env_val) = env::var(name.to_uppercase()) {
if !env_val.is_empty() {
debug!("Env variable found {}={}", name, env_val);
return env_val;
}
if let Ok(env_val) = env::var(format!("SERVER_{}", name).to_uppercase()) {
debug!("Env variable found {}={}", name, env_val);
return env_val;
}
args.value_of(name)
.unwrap_or_else(|| default.as_str())
.to_string()
}
if let Ok(env_val) = env::var(format!("SERVER_{}", name).to_uppercase()) {
debug!("Env variable found {}={}", name, env_val);
return env_val;
}
args
.value_of(name)
.unwrap_or_else(|| default.as_str())
.to_string()
}
pub fn server_installed() -> bool {
Path::new(&[get_working_dir(), VALHEIM_EXECUTABLE_NAME.to_string()].join("/")).exists()
Path::new(&[get_working_dir(), VALHEIM_EXECUTABLE_NAME.to_string()].join("/")).exists()
}
pub fn create_file(path: &str) -> File {
let output_path = Path::new(path);
match File::create(output_path) {
Ok(file) => file,
Err(_) => {
error!("Failed to create {}", path);
exit(1)
}
pub(crate) fn fetch_env(name: &str, default: &str, is_multiple: bool) -> String {
let mut formatted_value = match env::var(name) {
Ok(val) => val.replace("\"", ""),
Err(_) => {
debug!("Using default env var '{}': '{}'", name, default);
default.to_string()
}
};
if is_multiple && !formatted_value.is_empty() {
formatted_value = format!("{}:", formatted_value)
}
debug!("Found env var '{}': '{}'", name, formatted_value);
formatted_value
}
#[cfg(test)]
mod fetch_env_tests {
use crate::utils::fetch_env;
use std::env;
#[test]
fn is_multiple_false() {
let expected_key = "is_multiple_false";
let expected_value = "123";
env::set_var(expected_key, expected_value);
let observed_value = fetch_env(expected_key, "", false);
assert_eq!(expected_value, observed_value);
}
#[test]
fn is_multiple_true() {
let expected_key = "is_multiple_true";
let expected_value = "456";
env::set_var(expected_key, expected_value);
let observed_value = fetch_env(expected_key, "", false);
assert_eq!(expected_value, observed_value);
}
#[test]
fn has_default() {
let expected_key = "has_default";
let expected_value = "789";
env::remove_var(expected_key);
let observed_value = fetch_env(expected_key, expected_value, false);
assert_eq!(expected_value, observed_value);
}
#[test]
fn is_empty() {
let expected_key = "is_empty";
let expected_value = "";
env::remove_var(expected_key);
let observed_value = fetch_env(expected_key, expected_value, false);
assert_eq!(expected_value, observed_value);
}
#[test]
fn is_empty_multiple() {
let expected_key = "is_empty_multiple";
let expected_value = "";
env::remove_var(expected_key);
let observed_value = fetch_env(expected_key, expected_value, true);
assert_eq!(expected_value, observed_value);
}
}