mirror of
https://github.com/mbround18/valheim-docker.git
synced 2021-10-22 21:53:54 +03:00
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:
@@ -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
|
||||
|
||||
2
.github/workflows/rust.yml
vendored
2
.github/workflows/rust.yml
vendored
@@ -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
1
.gitignore
vendored
@@ -2,3 +2,4 @@
|
||||
target/
|
||||
tmp/
|
||||
*.env*
|
||||
docker-compose.*.yml
|
||||
|
||||
1
.rustfmt.toml
Normal file
1
.rustfmt.toml
Normal file
@@ -0,0 +1 @@
|
||||
tab_spaces = 2
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -234,7 +234,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "odin"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"cargo-husky",
|
||||
"clap",
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
34
Dockerfile
34
Dockerfile
@@ -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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
44
README.md
44
README.md
@@ -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)
|
||||
|
||||
23
docs/getting_started_with_mods.md
Normal file
23
docs/getting_started_with_mods.md
Normal 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
123
src/commands/start/bepinex.rs
Normal file
123
src/commands/start/bepinex.rs
Normal 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()
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
134
src/files/mod.rs
134
src/files/mod.rs
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
82
src/main.rs
82
src/main.rs
@@ -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
13
src/messages/mod.rs
Normal 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!("##########################################################################################################################");
|
||||
}
|
||||
@@ -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}"
|
||||
|
||||
@@ -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:-""}" \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
106
src/utils/mod.rs
106
src/utils/mod.rs
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user