First working version with vite-express integration.

Refactor the main page to show the Steam login when none is detected. We can't do anything without it at first. Later, we can detect if the server is installed and allow operations. Or not.
Remove server container. This will be created and maintained by the web container going forward.
Use node 18, as vite requires it.
Use a single package.json for everything. This way it can be installed at the root of the container and not show up in the bind mount.
Refactor store to include actions. We can just define them and call them, instead of using fetch directly everywhere. WIP.
Begin to implement some of the backend methods, like the steam login. It works!

Remove the old code.
This commit is contained in:
Daniel Ceregatti 2024-07-28 10:35:43 -07:00
parent 28fba94599
commit 85e59ae8c6
30 changed files with 2495 additions and 2624 deletions

277
README.md
View file

@ -1,27 +1,19 @@
# DayZDockerServer
A Linux [DayZ](https://dayz.com) server in a [Docker](https://docs.docker.com/) container. The main script's functionality is derived from [this project](https://github.com/thelastnoc/dayz-sa_linuxserver). That functionality is described [here](https://steamcommunity.com/sharedfiles/filedetails/?id=1517338673). The goal is to reproduce some of that functionality but also add more features.
A Linux [DayZ](https://dayz.com) server orchestration tool in a [Docker](https://docs.docker.com/) container.
The main goal is to provide a turnkey DayZ server with mod support that can be spun up with as little as a machine running Linux with Docker and Docker Compose installed.
## Features
This project started when the Linux DayZ server was released for DayZ experimental version 1.14 on 09/02/2021. As of 02/20/2024, there is now an official Linux DayZ server, but...
* [SteamCMD](https://developer.valvesoftware.com/wiki/SteamCMD) integration for installing the DayZ server and mods.
* A web UI for logging into Steam, managing server files and mods, and provisioning and running servers.
* Seamless integrations for many popular mods. Many require XML/JSON modifications or additions to the game files to work properly. See [files/mods/README.md](./files/mods/README.md) for a list of supported mods.
* Base server files are restored to their upstream state before every server start and all mod integrations are re-applied at that time. This allows for seamless upgrades when the server files are updated (as long as XML schema doesn't change, which is rare).
* Everything runs in Docker containers. Docker volumes are used to persist data. The web tool is the main container, which orchestrates server containers, however many your machine can handle.
* Shared docker volumes. All servers share the server files and mod volumes. Avoids duplication of many gigs of files. Updates are faster as there is only one copy.
## Caveats
## Build and Run
* Some mods are known to crash the server on startup:
* [DayZ Expansion AI](https://steamcommunity.com/sharedfiles/filedetails/?id=2792982069)
* [Red Falcon Flight System Heliz](https://steamcommunity.com/workshop/filedetails/?id=2692979668) - Bug report [here](https://feedback.bistudio.com/T176564)
* Some mods work, but have bugs:
* [DayZ Expansion Groups](https://steamcommunity.com/sharedfiles/filedetails/?id=2792983364)
* The save file becomes corrupted and when the server restarts so the changes do not persist.
* There are other bugs:
* [Server doesn't stop with SIGTERM](https://feedback.bistudio.com/T170721)
This project is a work in progress: See the [roadmap](ROADMAP.md).
## Configure and Build
Ensure [Docker](https://docs.docker.com/engine/install/) and [Docker compose](https://docs.docker.com/compose/install/) are properly installed. This means setting it up so it runs as your user. Make sure you're running these commands as your user, in your home directory.
Ensure [Docker](https://docs.docker.com/engine/install/) and [Docker compose](https://docs.docker.com/compose/install/) and [git](https://git-scm.com/) are properly installed. Make sure you're running these commands as your user, in your home directory, not as root.
Clone the repo, and change into the newly created directory:
@ -30,245 +22,48 @@ git clone https://ceregatti.org/git/daniel/dayzdockerserver.git
cd dayzdockerserver
```
Create a `.env` file for the web container that contains your user id. Usually the `${UID}` shell variable has this:
```shell
Build the Docker image and bring the stack up in the background:
```shell
echo "export USER_ID=${UID}" | tee .env
docker compose up --build -d
```
Repeat the above for server1, which uses .env1 (and so on):
```shell
echo "export USER_ID=${UID}" | tee .env1
```
But each server must also set its own unique ports and id:
```shell
echo "export SERVER_ID=1" | tee -a .env1
echo "export SERVER_PORT=2302" | tee -a .env1
echo "export RCON_PORT=2303" | tee -a .env1
echo "export STEAM_PORT=27016" | tee -a .env1
```
Repeat the above for each server you want to run, making sure the ports are unique across all servers:
```shell
echo "export USER_ID=${UID}" | tee .env2
echo "export SERVER_ID=2" | tee -a .env2
echo "export SERVER_PORT=2312" | tee -a .env2
echo "export RCON_PORT=2313" | tee -a .env2
echo "export STEAM_PORT=27116" | tee -a .env2
Tail the log to see when the web UI is ready, and get the URL and password:
```shell
Build the Docker images:
```shell
docker compose build
docker compose logs -f web
```
### Steam Integration
Hit control c to exit the log. Go to the URL shown in the log.
[SteamCMD](https://developer.valvesoftware.com/wiki/SteamCMD) is used to manage Steam downloads. A vanilla DayZ server can be installed with the `anonymous` Steam user, but most mods cannot. If the goal is to add mods, a real Steam login must be used. Login:
## Web UI
```shell
docker compose run --rm web dz login
```
Once at the web UI, you will be prompted to set a username and password for the web tool. Once done, a form will be presented to log into Steam. Downloading DayZ server files and mods using `steamcmd` requires a Steam account with DayZ in the library. The values will be first used by the `steamcmd` command line to perform a one-time login to Steam, and when successful, create a session that requires only that the username be saved locally. This supports Steam Guard codes. Neither the password nor the code will be stored. Subsequent calls to `steamcmd` by the web container will not require the credentials again until the session expires, which may be many months.
Follow the prompts. Hit enter to accept the default, which is to use the `anonymous` user, otherwise use your real username and keep following the prompts to add your password and Steam Guard code. This process will wait indefinitely until the code is entered.
After a successful login to Steam, the server files will automatically start downloading. The web UI will show the main page, which will list the progress of the download. The server files are about 3GB.
The credentials will be managed by [SteamCMD](https://developer.valvesoftware.com/wiki/SteamCMD). This will store a session token in the `homedir` docker volume. All subsequent SteamCMD commands will use this. so this process does not need to be repeated unless the session expires or the docker volume is deleted.
### Main Page
To manage the login credentials, simply run the above command again. See [Manage](#manage).
### Installing workshop mods
## Install
### Provisioning servers
The base server files must be installed before the server can be run:
```shell
docker compose run --rm web dz install
```
## Linux Server Caveats
This will download about 2.9G of files.
## Run
Edit `files/serverDZ.cfg` and set the values of any variables there. See the [documentation](https://forums.dayz.com/topic/239635-dayz-server-files-documentation/) if you want, but most of the default values are fine. At the very least, change the server name:
```
hostname = "Something other than Server Name"; // Server name
```
Install the server config file:
```shell
docker compose run --rm server1 dz c
```
The maintenance of the config file is a work in progress. The goal is to create a facility for merging changes into the config file and maintain a paper trail of changes.
Launch the stack into the background:
```shell
docker compose up -d
```
There will be nothing in mpmissions when a server container starts for the first time. A pristine copy of `dayzOffline.chernarusplus` will be copied from the `mpmission` volume to the server container. This will be the default map. To install other maps, see [Maps](#maps).
To see the server log:
```shell
docker compose logs -f server1
```
## Stopping the server
To stop the DayZ server:
```shell
docker compose exec server1 dz stop
```
If it exits cleanly, the container will also stop. Otherwise a crash is presumed and the server will restart. Ideally, the server would always exit cleanly, but... it's DayZ.
To stop the containers:
```shell
docker compose stop
```
To bring the entire stack down:
```shell
docker compose down
```
## Manage
### Maps
Installing another map requires installing its mod and mpmissions files. Some maps maintain github repositories or public web sites for their mpmissions, while others do not. This project aims to support DayZ maps whose mpmissions are easily accessible "Out of the box" by maintaining configuration files for them.
The following management commands presume the server has been brought [up](#run).
### RCON
A terminal-based RCON client is included: https://github.com/indepth666/py3rcon.
The dz script manages what's necessary to configure and run it:
```shell
docker compose exec server1 dz rcon
```
To reset the RCON password in the Battle Eye configuration file, simply delete it, and a random one will be generated on the next server startup:
```shell
docker compose run --rm server1 rm serverfiles/battleye/baserver_x64_active*
```
Don't expect much from this RCON at this time.
### Update the DayZ server files
It's probably not a good idea to update the server files while a server is running. Bring everything down first:
```shell
docker compose down
```
Then run the command:
```shell
docker compose run --rm web dz update
```
This will update the server base files as well as all installed mods.
Don't forget to [bring it back up](#run).
### Stop the DayZ server
To stop the server:
```shell
docker compose exec server1 dz stop
```
The above sends the SIGINT signal to the server process. The server sometimes fails to stop with this signal. It may be necessary to force stop it with the SIGKILL:
```shell
docker compose exec server1 dz force
```
When the server exits cleanly, i.e. exit code 0, the container also stops. Otherwise, a crash is presumed, and the server will be automatically restarted.
To bring the entire stack down:
```shell
docker compose down
```
### Workshop - Add / List / Remove / Update mods
Interactive interface for managing mods.
```
docker compose exec server1 dz activate id | add id1 | deactivate id | list | modupdate | remove id
docker compose exec server1 dz a id | add id1 | d id | l | m | r id
```
Look for mods in the [DayZ Workshop](https://steamcommunity.com/app/221100/workshop/). Browse to one. In its URL will be
an `id` parameter. Here is the URL to SimpleAutoRun: https://steamcommunity.com/sharedfiles/filedetails/?id=2264162971. To
add it:
```
docker compose exec web dz add 2264162971
```
Adding and removing mods will add and remove their names from the `-mod=` parameter.
Optionally, to avoid re-downloading large mods, the `activate` and `deactivate` workshop commands will
simply disable the mod but keep its files. Keep in mind that mod updates will also update deactivated
mods.
The above is a bad example, as SimpleAutorun depends on Community Framework, which must also be installed, as well as made to load first.
### Looking under the hood
All the server files persist in a docker volume that represents the container's unprivileged user's home directory. Open a bash shell in
the running container:
```
docker compose exec web bash
```
Or open a shell into a new container if the docker stack is not up:
```
docker compose run --rm web bash
```
All the files used by the server are in a docker volume. Any change made will be reflected upon the next container startup.
Use this shell cautiously.
# Development mode
Add the following to the `.env` file:
```shell
export DEVELOPMENT=1
```
Bring the stack down then back up. Now, instead of the server starting when the server container comes up it will simply block, keeping the container up and accessible.
This allows access to the server container using exec. You can then start and stop the server manually, using `dz`:
```shell
# Go into the server container
docker compose exec server1 bash
# See what the server status is
dz s
# Start it
dz start
```
To stop the server, hit control c.
Caveat: Some times the server doesn't stop with control c. If that's the case, control z, exit, then `dz f`. YMMV.
* Some mods are known to crash the server on startup or when a player connects:
* [DayZ Expansion AI](https://steamcommunity.com/sharedfiles/filedetails/?id=2792982069)
* [Survivor Animations](https://steamcommunity.com/sharedfiles/filedetails/?id=2918418331)
* [Red Falcon Flight System Heliz](https://steamcommunity.com/workshop/filedetails/?id=2692979668) - Bug report [here](https://feedback.bistudio.com/T176564)
* All these mods are like due to the same underlying bug.
* Some mods work, but have bugs:
* [DayZ Expansion Groups](https://steamcommunity.com/sharedfiles/filedetails/?id=2792983364)
* The save file becomes corrupted and when the server restarts so the changes do not persist.
* There are other bugs:
* [Server doesn't stop with SIGTERM](https://feedback.bistudio.com/T170721)
## TODO
* Create web management tool:
* It shells out to `dz` (for now) for all the heavy lifting.
* Create some way to send messages to players on the server using RCON.
* Implement multiple ids for mod commands. (In progress)
* Add CF tools
* Support for experimental servers.
* Rootless Docker

View file

@ -2,149 +2,34 @@ volumes:
# Only in the web container.
# For steamcmd files and resource files used by the scripts.
homedir_main:
# Shared by all containers.
# Mods.
mods:
# Where the server files will be installed
# Server files
serverfiles:
# Upstream mission files
servermpmissions:
# Server-specific volumes
# For Steam, for now
homedir_server1:
homedir_server2:
homedir_server3:
# Server mission files
mpmissions1:
mpmissions2:
mpmissions3:
# Server profile files
profiles1:
profiles2:
profiles3:
mpmissions:
services:
web:
profiles:
- main
build:
context: web
args:
- USER_ID
user: ${USER_ID}
hostname: web
volumes:
- homedir_main:/home/user
- serverfiles:/serverfiles
- servermpmissions:/serverfiles/mpmissions
- mpmissions:/serverfiles/mpmissions
- mods:/serverfiles/steamapps/workshop/content
- mods:/mods
- ./files:/files
- ./web:/web
ports:
- "8001:8001/tcp"
- "8000:8000/tcp"
restart: always
env_file:
- .env
server-image:
&server-image
profiles:
- build
build:
context: server
args:
- USER_ID
image: server-image
pull_policy: never
env_file:
- .env
server1:
<<: *server-image
profiles:
- main
user: ${USER_ID}
volumes:
# Common volumes
- ./files:/files
- mods:/mods
- ./server:/server
- serverfiles:/serverfiles
- servermpmissions:/mpmissions:ro
# Server-specific volumes
- homedir_server1:/home/user
- mpmissions1:/serverfiles/mpmissions
- profiles1:/profiles
# To have the server show up in the LAN tab of the DayZ launcher,
# it must run under host mode.
network_mode: host
# The above is mutually exclusive with the below. If you don't need
# the server to show up on the LAN, comment out the network_mode above
# and uncomment the port mappings below.
# ports:
# # Game port
# - 2302:2302/udp
# # RCON port
# - 2303:2303/udp
# # Steam port
# - 27016:27016/udp
# The server script execs itself when the server exits, unless told not to by `dz stop`
restart: no
# Allows attaching a debugger from the host
# cap_add:
# - SYS_PTRACE
# Allows core files to be created within the container. These are VERY LARGE! Enable only for debugging!
# One must also set the sysctl kernel.core_pattern parameter ON THE HOST to a path that is writable within the container. YMMV
# ulimits:
# core:
# soft: -1
# hard: -1
env_file:
- .env1
# # Copy and paste this for every other server you want to run, replacing 2 with 3, and so on.
# server2: # <-- here
# build:
# context: server
# args:
# - USER_ID
# user: ${USER_ID}
# volumes:
# # Common volumes
# - ./files:/files
# - mods:/mods
# - ./server:/server
# - serverfiles:/serverfiles
# - servermpmissions:/mpmissions:ro
# # Server-specific volumes
# - homedir_server2:/home/user # <-- here
# - mpmissions2:/serverfiles/mpmissions # <-- here
# - profiles2:/profiles # <-- here
# network_mode: host
# restart: no
# env_file:
# - .env2 # <-- here
#
# server3: # <-- here
# build:
# context: server
# args:
# - USER_ID
# user: ${USER_ID}
# volumes:
# # Common volumes
# - ./files:/files
# - mods:/mods
# - ./server:/server
# - serverfiles:/serverfiles
# - servermpmissions:/mpmissions:ro
# # Server-specific volumes
# - homedir_server3:/home/user # <-- here
# - mpmissions3:/serverfiles/mpmissions # <-- here
# - profiles3:/profiles # <-- here
# network_mode: host
# restart: no
# env_file:
# - .env3 # <-- here
- .env

View file

@ -33,7 +33,7 @@ RUN wget -q https://github.com/WoozyMasta/bercon/releases/download/1.0.0/bercon
# Install nodejs
RUN mkdir /usr/local/nvm
ENV NVM_DIR /usr/local/nvm
ENV NODE_VERSION 20.15.1
ENV NODE_VERSION 18.20.4
# Install nvm with node and npm
RUN wget -O - https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash \
@ -109,7 +109,7 @@ ENV LC_ALL=en_US.UTF-8
# This was installed in the download stage
ENV NVM_DIR /usr/local/nvm
ENV NODE_VERSION=20.15.1
ENV NODE_VERSION=18.20.4
ENV NODE_PATH=$NVM_DIR/versions/node/v$NODE_VERSION/lib/node_modules
ENV PATH=$NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH
@ -121,6 +121,11 @@ ENV PATH=/usr/games:/files/bin:/web/bin:${PATH}
# Shut steamcmd up
RUN cd /usr/lib/i386-linux-gnu && ln -s /web/bin/steamservice.so
# Install node modules
COPY package* /
RUN cd / && npm i
# Setup a non-privileged user
ARG USER_ID
@ -136,5 +141,4 @@ USER user
WORKDIR /home/user
# Run the web server
ENTRYPOINT ["entrypoint.sh"]
CMD ["start.sh"]

View file

@ -1,10 +1,16 @@
#!/usr/bin/env bash
set -eE
trap '
echo "Shutting down..."
' SIGINT SIGTERM
# Set PS1 so we know we're in the container
if ! echo .bashrc | grep -q "dz-web"
if grep -q "dz-web" .bashrc
then
echo "Adding PS1 to .bashrc..."
cat > .bashrc <<EOF
cat >> .bashrc <<EOF
alias ls='ls --color'
export PS1="${debian_chroot:+($debian_chroot)}\u@dz-web:\w\$ "
unset DEVELOPMENT
@ -19,10 +25,6 @@ then
fi
cd /web
npm i
export DEBUG='express:*'
npx nodemon web.js &
cd docroot
npm i
exec npm run dev
#export DEBUG=express:*
npm run dev &
wait $!

File diff suppressed because it is too large Load diff

View file

@ -1,22 +0,0 @@
{
"name": "docroot",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite --host",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@popperjs/core": "^2.11.8",
"@vueuse/core": "^10.1.2",
"bootstrap": "^5.3.0",
"bootstrap-icons": "^1.10.5",
"pinia": "^2.1.3",
"vue": "^3.3.2"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.3",
"vite": "^4.3.5"
}
}

View file

@ -1,17 +1,24 @@
<script setup>
import Body from '@/components/Body.vue'
import Error from '@/components/Error.vue'
import Header from '@/components/Header.vue'
import Login from '@/components/Login.vue'
import Main from '@/components/Main.vue'
import { useFetch } from '@vueuse/core'
import { useAppStore } from '@/store.js'
const store = useAppStore()
useFetch('/status', {
afterFetch(response) {
store.steamStatus = response.data
return response
}
}).get().json()
</script>
<template>
<Suspense>
<main>
<Error />
<div class="container-fluid min-vh-100 d-flex flex-column bg-light">
<Header />
<Body />
</div>
<Login v-if="! store.steamStatus.loggedIn" />
<Main v-else />
</main>
</Suspense>
</template>

View file

@ -1,12 +1,14 @@
<script setup>
import { onMounted } from 'vue'
import { watch } from 'vue'
import { Modal } from 'bootstrap'
import { useAppStore } from '@/stores/app.js'
import { useAppStore } from '@/store.js'
const store = useAppStore()
let modal = {}
onMounted(() => {
watch(() => store.errorText, () => {
modal = new Modal('#errorModal', {})
// modal.show()
if (store.errorText) {
modal.show()
}
})
</script>

View file

@ -3,38 +3,43 @@ import Search from '@/components/Search.vue'
import Status from '@/components/Status.vue'
import Servers from '@/components/Servers.vue'
import { useFetch } from '@vueuse/core'
import { useAppStore } from '@/stores/app.js'
import { useAppStore } from '@/store.js'
const store = useAppStore()
import { config } from '@/config'
const { error, data } = await useFetch(config.baseUrl + '/status').get().json()
const set = (w, e) => {
store.section = w
const active = Array.from(document.getElementsByClassName('active'))
active.forEach((a) => a.classList.remove('active'))
e.target.classList.add('active')
}
async function base() {
let which = '/installbase'
if (store.steamStatus.installed) {
which = '/updatebase'
}
const { data } = await useFetch(which).get().json()
store.errorText = data.value.message
}
</script>
<template>
<div v-if="data" class="row">
<div v-if="store.steamStatus" class="row">
<div class="col-3 text-center">
<h1>DayZ Docker Server</h1>
</div>
<div class="col-5">
<button
@click="installbase"
:class="'btn btn-sm ' + (data.installed ? 'btn-danger' : 'btn-success')"
@click="base"
class="btn btn-sm btn-success"
>
Install Server Files
{{ store.steamStatus.installed ? "Update" : "Install" }} Server Files
</button>
<button @click="updatebase" class="btn btn-sm btn-outline-success">Update Server Files</button>
<button @click="updatemods" class="btn btn-sm btn-outline-success">Update Mods</button>
<button type="button" @click="set('servers', $event)" class="btn btn-sm btn-outline-primary">Servers</button>
<button type="button" @click="set('mods', $event)" class="btn btn-sm btn-outline-primary active" data-bs-toggle="button">Mods</button>
<button type="button" @click="set('search', $event)" class="btn btn-sm btn-outline-primary">Search</button>
</div>
<Search />
<Status :status="data" />
<Status />
<Servers />
</div>
</template>

View file

@ -0,0 +1,34 @@
<script setup>
</script>
<template>
<div class="middle">
<h1>Login to Steam</h1>
<form name="form" method="POST" action="/login" enctype="application/x-www-form-urlencoded">
<div>
<input type="text" name="username" placeholder="Username" />
</div>
<div>
<input type="password" name="password" placeholder="Password" />
</div>
<div>
<input type="text" name="guard" placeholder="Steam Guard" />
</div>
<div>
<input type="checkbox" name="remember" id="remember" />
<label for="remember">Remember me</label>
</div>
<div>
<button type="submit" name="submit">Login</button>
</div>
</form>
</div>
</template>
<style scoped>
.middle {
margin: 0 auto;
width: 50%;
text-align: center;
}
</style>

View file

@ -0,0 +1,11 @@
<script setup>
import Body from './Body.vue'
import Header from './Header.vue'
</script>
<template>
<div class="container-fluid min-vh-100 d-flex flex-column bg-light">
<Header />
<Body />
</div>
</template>

View file

@ -1,7 +1,7 @@
<script setup>
import { useFetch } from "@vueuse/core"
import XmlFile from '@/components/XmlFile.vue'
import { useAppStore } from '@/stores/app.js'
import { useAppStore } from '@/store.js'
const store = useAppStore()
import { config } from '@/config'
const { data, error } = useFetch(() => config.baseUrl + `/mod/${store.modId}`, {

View file

@ -1,7 +1,7 @@
<script setup>
import { config } from '@/config'
import { useFetch } from '@vueuse/core'
import { useAppStore } from '@/stores/app.js'
import { useAppStore } from '@/store.js'
import ModInfo from '@/components/Modinfo.vue'
const store = useAppStore()
const { data, error } = useFetch(config.baseUrl + '/mods', {
@ -17,7 +17,8 @@ const { data, error } = useFetch(config.baseUrl + '/mods', {
<div v-if="error" class="row text-danger">
{{ error }}
</div>
<div class="col-md-3 border" v-if="data">
<div v-if="store.mods.length === 0">No mods are installed</div>
<div v-else class="col-md-3 border" v-if="data">
<div>
<h4 class="text-center">Installed Mods</h4>
<table>

View file

@ -1,5 +1,5 @@
<script setup>
import { useAppStore } from '@/stores/app.js'
import { useAppStore } from '@/store.js'
const store = useAppStore()
</script>

View file

@ -2,9 +2,9 @@
import { config } from '@/config'
import { BKMG } from '@/util'
import { useFetch} from '@vueuse/core'
import { useAppStore } from '@/stores/app.js'
import { useAppStore } from '@/store.js'
const store = useAppStore()
const { data: searchResults, error, isFetching } = useFetch(() => config.baseUrl + `/search/${store.searchText}`, {
const { data: searchResults, error, isFetching } = useFetch(() => `/search/${store.searchText}`, {
immediate: false,
refetch: true,
afterFetch(response) {

View file

@ -1,5 +1,5 @@
<script setup>
import { useAppStore } from '@/stores/app.js'
import { useAppStore } from '@/store.js'
const store = useAppStore()
</script>

View file

@ -1,17 +1,23 @@
<script setup>
const { status } = defineProps(['status'])
import { useAppStore } from '@/store.js'
const store = useAppStore()
</script>
<template>
<div class="col">
<div>
Logged into Steam:
<span v-if="store.steamStatus.loggedIn" class="bi bi-check h2 text-success"></span>
<span v-else class="bi bi-x h2 danger text-danger"></span>
</div>
<div>
Server files installed:
<span v-if="status.installed" class="bi bi-check h2 text-success"></span>
<span v-if="store.steamStatus.installed" class="bi bi-check h2 text-success"></span>
<span v-else class="bi bi-x h2 danger text-danger"></span>
</div>
<div v-if="status.version">
Version: <span class="text-success fw-bold">{{ status.version }}</span>
<span class="text-success fw-bold">({{ status.appid }})</span>
<div v-if="store.steamStatus.version">
Version: <span class="text-success fw-bold">{{ store.steamStatus.version }}</span>
<span class="text-success fw-bold">({{ store.steamStatus.appid }})</span>
</div>
</div>
</template>

View file

@ -1,7 +1,7 @@
<script setup>
import { useFetch } from '@vueuse/core'
import { config } from '@/config'
import { useAppStore } from '@/stores/app.js'
import { useAppStore } from '@/store.js'
import XmlTree from '@/components/XmlTree.vue'
const store = useAppStore()
const { data, error } = await useFetch(() => config.baseUrl + `/mod/${store.modId}/${store.modFile}`, {

View file

@ -6,7 +6,7 @@ import './css/index.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import {useAppStore} from "@/stores/app";
import {useAppStore} from '@/store'
// Create an instance of our Vue app
const app = createApp(App)

View file

@ -5,8 +5,10 @@ export const useAppStore = defineStore('app', {
errorText: '',
modId: 0,
modFile: '',
messageText: '',
mods: [],
searchText: '',
section: 'mods',
steamStatus: {appid: 0, installed: false, loggedIn: false, version: ''},
})
})

View file

@ -8,6 +8,6 @@
</head>
<body>
<div id="app"></div>
<script type="module" src="src/main.js"></script>
<script type="module" src="docroot/src/main.js"></script>
</body>
</html>

View file

@ -7,20 +7,12 @@
but to also make them available for the creation of server containers.
*/
import express from 'express'
import ViteExpress from 'vite-express'
import path from 'path'
import fs from 'fs'
import https from 'https'
import { spawn } from 'child_process'
const app = express()
app.use((req, res, next) => {
res.append('Access-Control-Allow-Origin', ['*'])
res.append('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE')
res.append('Access-Control-Allow-Headers', 'Content-Type')
next()
})
/*
The DayZ server Steam app ID. USE ONE OR THE OTHER!!
@ -28,12 +20,12 @@ app.use((req, res, next) => {
Meanwhile, if we have a release-compatible binary, the base files must be installed from this id,
even if the server binary and required shared objects don't come from it. (They'd come from...elsewhere...)
*/
//const server_appid = "223350"
const server_appid = "223350"
/*
Without a release binary, we must use the experimental server app ID.
*/
const server_appid = "1042420"
// const server_appid = "1042420"
/*
DayZ release client Steam app ID. This is for mods, as only the release client has them.
@ -54,6 +46,7 @@ const appid_version = versions[server_appid]
*/
const modDir = "/mods"
const serverFiles = "/serverfiles"
const homeDir = "/home/user"
/*
File path delimiter
@ -115,16 +108,14 @@ const allConfigFiles = {
const config = {
installFile: serverFiles + "/DayZServer",
loginFile: homeDir + "/steamlogin",
modDir: modDir + "/" + client_appid,
port: 8000,
steamAPIKey: process.env["STEAMAPIKEY"]
}
const getVersion = (installed) => {
if(installed) {
return "1.22.bogus"
}
return ""
const getVersion = () => {
return "1.25.bogus"
}
const getDirSize = (dirPath) => {
@ -175,29 +166,77 @@ const getMods = () => {
return mods
}
const login = () => {
const args = "+force_install_dir " + serverFiles + " +login '" + config.steamLogin + "' +quit"
steamcmd(args)
}
const steamcmd = (args) => {
const proc = spawn('steamcmd ' + args)
const steamcmd = async (args) => {
let out = ''
let err = ''
const command = 'steamcmd +force_install_dir ' + serverFiles + ' ' + args + ' +quit'
// console.log(command)
const proc = spawn(command, {shell: true})
proc.stdout.on('data', (data) => {
res.write(data)
// console.log("[OUT] " + data)
out += data + "\n"
})
proc.stderr.on('data', (data) => {
res.write(data)
// console.log("[ERROR] " + data)
err += data + "\n"
})
proc.on('error', (error) => {
res.write(error)
// console.log("[ERROR] " + error)
err += error + "\n"
})
proc.on('close', (error) => {
if(error) res.write(error)
res.end()
if(error) err += error
// console.log("Return")
return { out: out, err: err }
})
}
app.use(express.static('root'))
const app = express()
app.use(express.json())
app.use(express.urlencoded({extended: true}))
app.disable('etag')
app.use((req, res, next) => {
res.append('Access-Control-Allow-Origin', ['*'])
res.append('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE')
res.append('Access-Control-Allow-Headers', 'Content-Type')
next()
})
// Install a mod
app.get(('/install/:modId'), (req, res) => {
const modId = req.params["modId"]
// Shell out to steamcmd, monitor the process, and display the output as it runs
res.send(modId + " was installed")
})
// Install base files
app.get('/installbase', (req, res) => {
const ret = {
"message": "Base files were installed"
}
res.send(ret)
})
// Login to Steam
app.post(('/login'), async (req, res) => {
const username = req.body?.username;
const password = req.body?.password;
const guard = req.body?.guard;
const remember = req.body?.remember;
let args = `+login "${username}" "${password}"`
if (guard) args += ` "${guard}"`
const result = await steamcmd(args)
console.log(result)
if (result) {
fs.writeFileSync(config.loginFile, username)
res.send(1)
} else {
res.send(0)
}
})
// Get mod metadata by ID
app.get('/mod/:modId', (req, res) => {
@ -224,6 +263,24 @@ app.get('/mod/:modId/:file', (req, res) => {
}
})
/*
Get all mod metadata
*/
app.get('/mods', (req, res) => {
const mods = getMods()
const ret = {
"mods": mods
}
res.send(ret)
})
// Remove a mod
app.get(('/remove/:modId'), (req, res) => {
const modId = req.params["modId"]
// Shell out to steamcmd, monitor the process, and display the output as it runs
res.send(modId + " was removed")
})
// Search for a mod
app.get(('/search/:searchString'), (req, res) => {
const searchString = req.params["searchString"]
@ -241,59 +298,40 @@ app.get(('/search/:searchString'), (req, res) => {
})
})
// Install a mod
app.get(('/install/:modId'), (req, res) => {
const modId = req.params["modId"]
// Shell out to steamcmd, monitor the process, and display the output as it runs
res.send(modId + " was installed")
})
// Remove a mod
app.get(('/remove/:modId'), (req, res) => {
const modId = req.params["modId"]
// Shell out to steamcmd, monitor the process, and display the output as it runs
res.send(modId + " was removed")
})
// Update base files
app.get('/updatebase', (req, res) => {
login()
res.send("Base files were updates")
})
// Update mods
app.get('/updatemods', (req, res) => {
res.send("Mod files were updates")
})
/*
Get the status of things:
If the base files are installed, the version of the server, the appid (If release or experimental)
*/
app.get('/status', (req, res) => {
// FIXME Async/await this stuff...
app.get('/status', (_, res) => {
const installed = fs.existsSync(config.installFile)
const version = getVersion(installed)
const loggedIn = fs.existsSync(config.loginFile)
const ret = {
"appid": appid_version,
"installed": installed,
"version": version
"loggedIn": loggedIn,
}
ret.error = "This is a test error from the back end"
res.send(ret)
})
/*
Get all mod metadata
*/
app.get('/mods', (req, res) => {
const mods = getMods()
const ret = {
"mods": mods
if (installed) {
ret.version = getVersion()
}
res.send(ret)
})
app.listen(config.port, () => {
console.log(`Listening on port ${config.port}`)
// Update base files
app.get('/updatebase', (req, res) => {
res.send("Base files were updated")
})
// Update mods
app.get('/updatemods', (req, res) => {
res.send("Mod files were updated")
})
ViteExpress.listen(app, config.port, () =>
console.log(`Server is listening on port ${config.port}`)
)
// const server = app.listen(config.port, "0.0.0.0", () =>
// console.log(`Server is listening on port ${config.port}`)
// )
//
// ViteExpress.bind(app, server)

2486
web/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,16 +4,26 @@
"description": "",
"main": "index.js",
"scripts": {
"dev": "nodemon index.js -w index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.2"
},
"type": "module",
"devDependencies": {
"nodemon": "^2.0.22"
"@vitejs/plugin-vue": "^5.1.1",
"express": "^4.19.2",
"nodemon": "^3.1.4",
"vite": "^5.3.5",
"vite-express": "^0.17.0"
},
"dependencies": {
"@popperjs/core": "^2.11.8",
"@vueuse/core": "^10.11.0",
"bootstrap": "^5.3.3",
"bootstrap-icons": "^1.11.3",
"pinia": "^2.2.0",
"vue": "^3.4.34"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

View file

@ -1,22 +0,0 @@
button {
padding: 5px;
margin: 10px;
}
th, td {
padding-right: 10px
}
.selected {
background-color: cyan;
}
.simulink {
cursor: pointer;
text-underline: blue;
}
.simulink:hover {
background-color: cyan;
}

View file

@ -1,23 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>DayZ Docker Server</title>
<link rel="icon" type="image/x-icon" href="/favicon.png">
<!-- Bootstrap -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.3.0/font/bootstrap-icons.css">
<!-- Our CSS -->
<link rel="stylesheet" href="/index.css">
</head>
<body>
<div id="app"></div>
<script type="module">
import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
import app from '/index.js'
createApp(app).mount('#app')
</script>
</body>
</html>

View file

@ -1,313 +0,0 @@
const template = `
<div
class="modal"
id="errorModal"
data-bs-backdrop="static"
data-bs-keyboard="false"
tabindex="-1"
aria-labelledby="errorModalLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="errorModalLabel">Error</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{{ fetchError }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="container-fluid min-vh-100 d-flex flex-column bg-light">
<div class="row">
<div class="col-3 text-center">
<h1>DayZ Docker Server</h1>
</div>
<div class="col-5">
<button
@click="installbase"
:class="'btn ' + (installed ? 'btn-danger' : 'btn-success')"
>
Install Server Files
</button>
<button @click="updatebase" class="btn btn-success">Update Server Files</button>
<button @click="updatemods" class="btn btn-success">Update Mods</button>
<button @click="servers" class="btn btn-primary">Servers</button>
<button @click="listmods" class="btn btn-primary">Mods</button>
</div>
<div class="col form-control-lg text-center">
<form @submit="handleSubmit">
<input name="search" placeholder="Search mods..." autofocus>
</form>
</div>
<div class="col">
<div>
Server files installed:
<span class="bi bi-check h2 text-success" v-if="installed"></span>
<span class="bi bi-x h2 danger text-danger" v-else></span>
</div>
<div v-if="version != ''">
Version: <span class="text-success font-weight-bold">{{ version }}</span>
</div>
</div>
</div>
<div class="row flex-grow-1">
<div class="col-md-3 border">
<div>
<h4 class="text-center">Installed Mods</h4>
<table>
<tr>
<th>Steam Link</th>
<th>Mod Info</th>
</tr>
<template
v-for="mod in mods"
>
<tr>
<td>
<a
target="_blank"
:href="steamURL + mod.id"
>
{{ mod.id }}
</a>
</td>
<td>
<a class="simulink" @click="getModInfo(mod.id)">{{ mod.name }}</a>
</td>
</tr>
</template>
</table>
</div>
</div>
<div class="col-md-9 border">
<div class="d-flex" v-if="modInfo != ''">
<div>
<div>
<strong>{{ modInfo.name }}</strong>
</div>
<div>
ID: {{ modInfo.id }}
</div>
<div>
Size: {{ modInfo.size.toLocaleString("en-US") }}
</div>
<div v-if="modInfo.customXML.length > 0">
Custom XML files:
<ul>
<li v-for="info in modInfo.customXML">
<a
:class="'simulink xmlfile ' + info.name"
@click="getXMLInfo(modInfo.id,info.name)"
>
{{ info.name }}
</a>
</li>
</ul>
</div>
</div>
<div class="col-1"></div>
<div>
<xmltree v-if="XMLInfo != ''" :xmlData="XMLInfo" />
</div>
</div>
<div v-if="searchResults != ''" class="d-flex">
<table>
<tr>
<th>Steam Link</th>
<th>Title</th>
<th>Size</th>
<th>Last Updated</th>
<th>Subscriptions</th>
<th></th>
</tr>
<tr v-for="result in searchResults">
<td>
<a
target="_blank"
:href="steamURL + result.publishedfileid"
>
<img :alt="result.short_description" data-bs-toggle="tooltip" data-bs-placement="left" :title="result.short_description" width="160" height="90" :src="result.preview_url">
</a>
</td>
<td>{{ result.title }}</td>
<td>{{ BKMG(result.file_size) }}</td>
<td>{{ new Date(result.time_updated * 1000).toLocaleDateString("en-us") }}</td>
<td>{{ result.lifetime_subscriptions }}</td>
<td>
<button v-if="mods.find(o => o.id == result.publishedfileid)" @click="removeMod(result.publishedfileid)" type="button" class="btn btn-danger">Remove</button>
<button v-else @click="installMod(result.publishedfileid)" type="button" class="btn btn-success">Install</button>
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
`
import xmltree from "/xmltree.js"
const fetcher = (args) => {
fetch(args.url)
.then(response => (
args.type === "json" ? response.json() : response.text()
))
.then(response => args.callback(response))
.catch((error) => {
console.error(error)
this.fetchError = error.message
})
}
export default {
name: 'DazDockerServer',
template: template,
components: {
xmltree
},
data() {
return {
fetchError: "",
installed: false,
mods: [],
modInfo: "",
searchResults: [],
steamURL: 'https://steamcommunity.com/sharedfiles/filedetails/?id=',
version: "Unknown",
XMLFile: "",
XMLInfo: "",
}
},
methods: {
getModInfo(modId) {
fetcher ({
url: '/mod/' + modId,
type: "json",
callback: (response) => {
this.modInfo = response
this.XMLInfo = ""
this.searchResults = ""
}
})
},
getXMLInfo(modId, file) {
for (const e of document.getElementsByClassName("selected")) e.classList.remove("selected")
fetch('/mod/' + modId + '/' + file)
.then(response => response.text())
.then(response => {
this.XMLFile = file
this.XMLInfo = response
for (const e of document.getElementsByClassName(file)) e.classList.add("selected")
})
.catch((error) => {
console.error(error)
this.fetchError = error.message
})
},
handleSubmit(e) {
e.preventDefault()
fetch('/search/' + e.target.search.value)
.then(response => response.json())
.then(response => {
this.modInfo = ""
this.XMLInfo = ""
// const sortField = "time_updated"
const sortField = "lifetime_subscriptions"
response.response.publishedfiledetails.sort((a, b) =>
a[sortField] < b[sortField] ? 1 : -1
)
this.searchResults = response.response.publishedfiledetails
})
.then(() => {
// Enable all tooltips
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
})
// Enable all alerts
$('.alert').alert()
})
.catch((error) => {
console.error(error)
this.fetchError = error.message
})
},
installMod(modId) {
fetch('/install/' + modId)
.then(response => response.text())
.then(response => {
console.log(response)
})
.catch((error) => {
console.error(error)
this.fetchError = error.message
})
},
removeMod(modId) {
fetch('/remove/' + modId)
.then(response => response.text())
.then(response => {
console.log(response)
})
.catch((error) => {
console.error(error)
this.fetchError = error.message
})
},
BKMG(val) {
const units = ['bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
let l = 0, n = parseInt(val, 10) || 0
while(n >= 1024 && ++l){
n = n/1024
}
return(n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l])
},
installbase() {
console.log("Install base files")
},
servers() {
console.log("List servers")
},
listmods() {
console.log("List mods")
},
updatebase() {
console.log("Update base files")
},
updatemods() {
console.log("Update mod files")
}
},
mounted() {
// Get the data
fetch('/status')
.then(response => response.json())
.then(response => {
this.installed = response.installed
this.version = response.version
this.mods = response.mods
if(response.error) {
this.fetchError = response.error
// Since it's a modal, we have to manually show it...?
const modal = new bootstrap.Modal(document.getElementById('errorModal'))
modal.show()
}
})
.catch((error) => {
console.error(error)
this.fetchError = error.message
})
}
}
/*
{ "result": 1, "publishedfileid": "2489240546", "creator": "76561199068873691", "creator_appid": 221100, "consumer_appid": 221100, "consumer_shortcutid": 0, "filename": "", "file_size": "276817803", "preview_file_size": "27678", "preview_url": "https://steamuserimages-a.akamaihd.net/ugc/2011465736408144669/A7137390FBB9F4F94E0BFE5389932F6DE7AB7B87/", "url": "", "hcontent_file": "4050838808220661564", "hcontent_preview": "2011465736408144669", "title": "LastDayZ_Helis", "short_description": "The author of the helicopter mod https://sibnic.info on the site you can download the latest version of free helicopters, If you need help with installation, go to discord https://sibnic.info/discord", "time_created": 1621186063, "time_updated": 1684985831, "visibility": 0, "flags": 5632, "workshop_file": false, "workshop_accepted": false, "show_subscribe_all": false, "num_comments_public": 0, "banned": false, "ban_reason": "", "banner": "76561197960265728", "can_be_deleted": true, "app_name": "DayZ", "file_type": 0, "can_subscribe": true, "subscriptions": 7935, "favorited": 3, "followers": 0, "lifetime_subscriptions": 22759, "lifetime_favorited": 5, "lifetime_followers": 0, "lifetime_playtime": "0", "lifetime_playtime_sessions": "0", "views": 535, "num_children": 0, "num_reports": 0, "tags": [ { "tag": "Animation", "display_name": "Animation" }, { "tag": "Environment", "display_name": "Environment" }, { "tag": "Sound", "display_name": "Sound" }, { "tag": "Vehicle", "display_name": "Vehicle" }, { "tag": "Mod", "display_name": "Mod" } ], "language": 0, "maybe_inappropriate_sex": false, "maybe_inappropriate_violence": false, "revision_change_number": "14", "revision": 1, "ban_text_check_result": 5 }
*/

View file

@ -1,103 +0,0 @@
const template = `
<div
v-if="elem.nodeType === 1 && isText"
:style="'padding-left: ' + (depth * 10) + 'px'"
@click="collapse"
>
<span class="xml-tree-tag">&lt;{{elem.nodeName}}</span>
<span v-if="elem.hasAttributes()" v-for="attribute in elem.attributes">
<span class="xml-tree-attr">&nbsp;{{attribute.name}}</span>
<span>=</span>
<span class="xml-tree-attr">"{{attribute.value}}"</span>
</span>
<span class="xml-tree-tag">></span>
<span>{{this.children[0].data.trim()}}</span>
<span class="xml-tree-tag">&lt;/{{elem.nodeName}}></span>
</div>
<div v-else :style="'padding-left: ' + (depth * 10) + 'px'">
<span v-if="elem.nodeType === 1" class="d-flex">
<span
v-if="elem.children.length > 0"
class="bi-dash simulink text-center"
@click="collapse"
/>
<span v-else></span>
<span class="xml-tree-tag">&lt;{{elem.nodeName}}</span>
<span v-if="elem.hasAttributes()" v-for="attribute in elem.attributes">
<span class="xml-tree-attr">&nbsp;{{attribute.name}}</span>
<span>=</span>
<span class="xml-tree-attr">"{{attribute.value}}"</span>
</span>
<span v-if="elem.children.length === 0" class="xml-tree-tag">&nbsp;/></span>
<span v-else class="xml-tree-tag">></span>
</span>
<span v-if="elem.nodeType === 3">{{elem.data.trim()}}</span>
<div v-for="child in children">
<xmltree v-if="child.nodeType !== 8" :element="child" :d="depth" />
</div>
<span
v-if="elem.nodeType === 1 && elem.children.length > 0"
style="padding-left: -10px"
>
<span style="padding-left: 20px" class="xml-tree-tag">&lt;/{{elem.nodeName}}></span>
</span>
</div>
`
export default {
name: "xmltree",
props: {
d: {
type: Number,
default: 0
},
element: {
type: [Element, Text],
default: undefined
},
xmlData: String
},
template: template,
data() {
return {
depth: 1
}
},
methods: {
collapse() {
this.children.forEach(x => x.classList?.add("d-none"))
},
log(message) {
console.log(message)
}
},
computed: {
elem() {
this.depth = parseInt(this.d) + 1
if (this.element) {
return this.element
} else {
const parser = new DOMParser()
const xmlDoc = parser.parseFromString(this.xmlData, "text/xml")
return xmlDoc.documentElement
}
},
children() {
let children = []
let node = this.elem.firstChild
while (node) {
children.push(node)
node = node.nextSibling
}
return children
},
isText() {
if (this.children.length === 1) {
if (this.children[0].nodeType === 3) {
return true
}
}
return false
}
}
}

View file

@ -1,17 +1,17 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
cacheDir: '/tmp/vite',
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
'@': '/web/docroot/src'
}
},
server: {
port: 8001
fs: {
allow: ['/node_modules', '/web']
}
}
})