Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 119bb443d9 |
@@ -1,248 +1,400 @@
|
|||||||
# Windrose Dedicated Server — TrueNAS SCALE Deployment Guide
|
Windrose Dedicated Server — TrueNAS SCALE Deployment Guide
|
||||||
|
|
||||||
A fully working configuration for running the Windrose Dedicated Server (Windows build) inside a TrueNAS SCALE container using Wine + DXVK, with persistent saves, explicit world selection, and fixed hostname resolution for ICE/P2P networking.
|
A fully working configuration for running the Windrose Dedicated Server (Windows build) inside a TrueNAS SCALE container using Wine + DXVK, with persistent saves, explicit world selection, and fixed hostname resolution for ICE/P2P networking.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
This README documents the exact setup currently running in production.
|
This README documents the exact setup currently running in production.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
- [Overview](#overview)
|
Table of Contents
|
||||||
- [Directory Layout](#directory-layout)
|
|
||||||
- [Environment Variables](#environment-variables)
|
|
||||||
- [Hostname Resolution (ICE/P2P Fix)](#hostname-resolution-icep2p-fix)
|
|
||||||
- [DXVK Behavior](#dxvk-behavior)
|
|
||||||
- [Server Launch Command](#server-launch-command)
|
|
||||||
- [Entrypoint Behavior Summary](#entrypoint-behavior-summary)
|
|
||||||
- [Networking Requirements](#networking-requirements)
|
|
||||||
- [Troubleshooting](#troubleshooting)
|
|
||||||
- [Future Enhancements](#future-enhancements)
|
|
||||||
|
|
||||||
---
|
Overview
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
|
Directory Layout
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Environment Variables
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Hostname Resolution (ICE/P2P Fix)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
DXVK Behavior
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Server Launch Command
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Entrypoint Behavior Summary
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Networking Requirements
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Troubleshooting
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Future Enhancements
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Overview
|
||||||
|
|
||||||
This deployment runs:
|
This deployment runs:
|
||||||
|
|
||||||
- Windrose Dedicated Server (Windows build)
|
|
||||||
- Inside a TrueNAS SCALE App container
|
|
||||||
- Using Wine + DXVK
|
Windrose Dedicated Server (Windows build)
|
||||||
- With persistent world saves
|
|
||||||
- With explicit WORLD_ID selection
|
|
||||||
- With hostname resolution fixes for ICE/P2P
|
|
||||||
- With non‑fatal DXVK installation fallback
|
Inside a TrueNAS SCALE App container
|
||||||
- With stable LAN/WAN connectivity
|
|
||||||
|
|
||||||
|
|
||||||
|
Using Wine + DXVK
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
With persistent world saves
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
With explicit WORLD\_ID selection
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
With hostname resolution fixes for ICE/P2P
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
With non‑fatal DXVK installation fallback
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
With stable LAN/WAN connectivity
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
This configuration is confirmed working as of:
|
This configuration is confirmed working as of:
|
||||||
|
|
||||||
**May 10, 2026**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Directory Layout
|
May 10, 2026
|
||||||
|
|
||||||
### Persistent Storage
|
|
||||||
|
|
||||||
|
Directory Layout
|
||||||
|
|
||||||
|
Persistent Storage
|
||||||
|
|
||||||
Mounted into the container:
|
Mounted into the container:
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
Code
|
||||||
|
|
||||||
/data/Saved
|
/data/Saved
|
||||||
```
|
|
||||||
|
|
||||||
Windrose stores all world data here:
|
Windrose stores all world data here:
|
||||||
|
|
||||||
```
|
|
||||||
/data/Saved/SaveProfiles/Default/RocksDB/0.10.0/Worlds/<WORLD_ID>/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Game Installation Directory
|
|
||||||
|
|
||||||
```
|
Code
|
||||||
|
|
||||||
|
/data/Saved/SaveProfiles/Default/RocksDB/0.10.0/Worlds/<WORLD\_ID>/
|
||||||
|
|
||||||
|
Game Installation Directory
|
||||||
|
|
||||||
|
Code
|
||||||
|
|
||||||
/server
|
/server
|
||||||
|
|
||||||
/server/R5
|
/server/R5
|
||||||
```
|
|
||||||
|
|
||||||
This is where SteamCMD installs the Windrose server files.
|
This is where SteamCMD installs the Windrose server files.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
Environment Variables
|
||||||
|
|
||||||
Set these in the TrueNAS SCALE App → Environment Variables.
|
Set these in the TrueNAS SCALE App → Environment Variables.
|
||||||
|
|
||||||
### Required
|
|
||||||
|
|
||||||
| Variable | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| WORLD_ID | Folder name of the world to load. Example: `352359B474804C2A4D83D84AADC7579D` |
|
|
||||||
|
|
||||||
### Optional
|
Required
|
||||||
|
|
||||||
| Variable | Default | Description |
|
Variable Description
|
||||||
|--------------|---------------------------|-------------|
|
|
||||||
| WINEPREFIX | /data/wine/wineprefix | Wine prefix location |
|
|
||||||
| SAVED_DIR | /data/Saved | Persistent save directory |
|
|
||||||
|
|
||||||
---
|
WORLD\_ID Folder name of the world to load. Example: 352359B474804C2A4D83D84AADC7579D
|
||||||
|
|
||||||
## Hostname Resolution (ICE/P2P Fix)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Optional
|
||||||
|
|
||||||
|
Variable Default Description
|
||||||
|
|
||||||
|
WINEPREFIX /data/wine/wineprefix Wine prefix location
|
||||||
|
|
||||||
|
SAVED\_DIR /data/Saved Persistent save directory
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Hostname Resolution (ICE/P2P Fix)
|
||||||
|
|
||||||
Windrose internally tries to resolve the hostname:
|
Windrose internally tries to resolve the hostname:
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
Code
|
||||||
|
|
||||||
windrose
|
windrose
|
||||||
```
|
|
||||||
|
|
||||||
Your deployment requires:
|
Your deployment requires:
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
Code
|
||||||
|
|
||||||
windrose.theminisip.ca
|
windrose.theminisip.ca
|
||||||
```
|
|
||||||
|
|
||||||
Both must resolve to the LAN IP of the TrueNAS host:
|
Both must resolve to the LAN IP of the TrueNAS host:
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
Code
|
||||||
|
|
||||||
192.168.3.41
|
192.168.3.41
|
||||||
```
|
|
||||||
|
|
||||||
The entrypoint injects this automatically:
|
The entrypoint injects this automatically:
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
bash
|
||||||
|
|
||||||
192.168.3.41 windrose windrose.theminisip.ca
|
192.168.3.41 windrose windrose.theminisip.ca
|
||||||
```
|
|
||||||
|
|
||||||
This prevents ICE errors such as:
|
This prevents ICE errors such as:
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
Code
|
||||||
|
|
||||||
Cannot resolve addresses for host windrose.
|
Cannot resolve addresses for host windrose.
|
||||||
|
|
||||||
Error on getting local ICE candidates
|
Error on getting local ICE candidates
|
||||||
|
|
||||||
P2pClient is broken
|
P2pClient is broken
|
||||||
```
|
|
||||||
|
|
||||||
---
|
DXVK Behavior
|
||||||
|
|
||||||
## DXVK Behavior
|
|
||||||
|
|
||||||
DXVK is required even in headless mode.
|
DXVK is required even in headless mode.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Your container includes:
|
Your container includes:
|
||||||
|
|
||||||
```
|
|
||||||
/opt/dxvk/x32/*.dll
|
|
||||||
/opt/dxvk/x64/*.dll
|
Code
|
||||||
```
|
|
||||||
|
/opt/dxvk/x32/\*.dll
|
||||||
|
|
||||||
|
/opt/dxvk/x64/\*.dll
|
||||||
|
|
||||||
The entrypoint:
|
The entrypoint:
|
||||||
|
|
||||||
- Runs setup-dxvk.sh if present
|
|
||||||
- Falls back to manual DLL copy
|
|
||||||
- Logs exit codes
|
Runs setup-dxvk.sh if present
|
||||||
- Never crashes if DXVK is missing or incomplete
|
|
||||||
|
|
||||||
|
|
||||||
|
Falls back to manual DLL copy
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Logs exit codes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Never crashes if DXVK is missing or incomplete
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
This ensures Wine initializes correctly.
|
This ensures Wine initializes correctly.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Server Launch Command
|
|
||||||
|
Server Launch Command
|
||||||
|
|
||||||
The server is launched with:
|
The server is launched with:
|
||||||
|
|
||||||
```
|
|
||||||
wine WindroseServer-Win64-Shipping.exe \
|
|
||||||
-log \
|
|
||||||
-unattended \
|
|
||||||
-nosteam \
|
|
||||||
-savedir "Saved" \
|
|
||||||
-saveprofile Default \
|
|
||||||
-world "$WORLD_ID"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
|
|
||||||
- `"Saved"` resolves to `/server/R5/Saved`
|
bash
|
||||||
- World data still loads from `/data/Saved` via RocksDB
|
|
||||||
- `-nosteam` avoids Steam API issues
|
|
||||||
- `-unattended` prevents UI prompts
|
|
||||||
|
|
||||||
---
|
wine WindroseServer-Win64-Shipping.exe \\
|
||||||
|
|
||||||
## Entrypoint Behavior Summary
|
  -log \\
|
||||||
|
|
||||||
|
  -unattended \\
|
||||||
|
|
||||||
|
  -nosteam \\
|
||||||
|
|
||||||
|
  -savedir "Saved" \\
|
||||||
|
|
||||||
|
  -saveprofile Default \\
|
||||||
|
|
||||||
|
  -world "$WORLD\_ID"
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"Saved" resolves to /server/R5/Saved
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
World data still loads from /data/Saved via RocksDB
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
\-nosteam avoids Steam API issues
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
\-unattended prevents UI prompts
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Entrypoint Behavior Summary
|
||||||
|
|
||||||
The entrypoint:
|
The entrypoint:
|
||||||
|
|
||||||
1. Initializes Wine prefix
|
|
||||||
2. Installs DXVK (non‑fatal)
|
|
||||||
3. Injects hostname resolution
|
Initializes Wine prefix
|
||||||
4. Validates WORLD_ID
|
|
||||||
5. Launches the server
|
|
||||||
|
|
||||||
|
Installs DXVK (non‑fatal)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Injects hostname resolution
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Validates WORLD\_ID
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Launches the server
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
This is the exact working version currently deployed.
|
This is the exact working version currently deployed.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Networking Requirements
|
|
||||||
|
|
||||||
### LAN
|
Networking Requirements
|
||||||
|
|
||||||
|
LAN
|
||||||
|
|
||||||
Works out of the box.
|
Works out of the box.
|
||||||
|
|
||||||
### WAN
|
|
||||||
|
|
||||||
|
WAN
|
||||||
|
|
||||||
Forward these ports:
|
Forward these ports:
|
||||||
|
|
||||||
| Port | Protocol | Purpose |
|
|
||||||
|------|----------|----------|
|
|
||||||
| 7777 | UDP | Game traffic |
|
Port Protocol Purpose
|
||||||
| 15000 | UDP | ICE / P2P |
|
|
||||||
| 27015 | UDP | Query port |
|
7777 UDP Game traffic
|
||||||
|
|
||||||
|
15000 UDP ICE / P2P
|
||||||
|
|
||||||
|
27015 UDP Query port
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Ensure Cloudflare DNS for:
|
Ensure Cloudflare DNS for:
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
Code
|
||||||
|
|
||||||
windrose.theminisip.ca
|
windrose.theminisip.ca
|
||||||
```
|
|
||||||
|
|
||||||
points to your public IP.
|
points to your public IP.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### ICE Errors
|
Troubleshooting
|
||||||
|
|
||||||
|
ICE Errors
|
||||||
|
|
||||||
If you see:
|
If you see:
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
Code
|
||||||
|
|
||||||
Cannot resolve addresses for host windrose
|
Cannot resolve addresses for host windrose
|
||||||
```
|
|
||||||
|
|
||||||
Check:
|
Check:
|
||||||
|
|
||||||
- /etc/hosts mapping
|
|
||||||
- DNS for windrose.theminisip.ca
|
|
||||||
- Correct WORLD_ID
|
|
||||||
|
|
||||||
### World Not Loading
|
|
||||||
|
/etc/hosts mapping
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
DNS for windrose.theminisip.ca
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Correct WORLD\_ID
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
World Not Loading
|
||||||
|
|
||||||
Ensure:
|
Ensure:
|
||||||
|
|
||||||
```
|
|
||||||
/data/Saved/SaveProfiles/Default/RocksDB/0.10.0/Worlds/<WORLD_ID>/
|
|
||||||
```
|
Code
|
||||||
|
|
||||||
|
/data/Saved/SaveProfiles/Default/RocksDB/0.10.0/Worlds/<WORLD\_ID>/
|
||||||
|
|
||||||
exists.
|
exists.
|
||||||
|
|
||||||
### DXVK Errors
|
|
||||||
|
|
||||||
|
DXVK Errors
|
||||||
|
|
||||||
Non‑fatal — server will still run.
|
Non‑fatal — server will still run.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
- Node.js web manager (deployment, config editing, restarts)
|
|
||||||
- Multi‑instance orchestration
|
|
||||||
- World backups & restores
|
|
||||||
- Metrics dashboard
|
|
||||||
- Log streaming
|
|
||||||
|
|||||||
@@ -37,3 +37,19 @@ services:
|
|||||||
|
|
||||||
tty: true
|
tty: true
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
|
|
||||||
|
manager:
|
||||||
|
build: ./manager
|
||||||
|
container_name: windrose-manager
|
||||||
|
environment:
|
||||||
|
- ADMIN_PASSWORD=changeme
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
# Manager needs access to real config files
|
||||||
|
- ./data:/data:rw
|
||||||
|
|
||||||
|
# Manager needs access to Docker to restart windrose container
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
ENV PORT=3000
|
||||||
|
|
||||||
|
CMD ["npm", "start"]
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "windrose-manager",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "dist/server.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "ts-node src/server.ts",
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dockerode": "^4.0.0",
|
||||||
|
"express": "^4.19.0",
|
||||||
|
"yaml": "^2.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.6.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import { getInstance } from "../core/config";
|
||||||
|
import { readJson, writeJson, listWorldDescriptions } from "../core/files";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get("/:id/server", (req, res) => {
|
||||||
|
const inst = getInstance(req.params.id);
|
||||||
|
if (!inst) return res.status(404).json({ error: "not found" });
|
||||||
|
|
||||||
|
res.json(readJson(inst.configPaths.serverDescription));
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put("/:id/server", (req, res) => {
|
||||||
|
const inst = getInstance(req.params.id);
|
||||||
|
if (!inst) return res.status(404).json({ error: "not found" });
|
||||||
|
|
||||||
|
writeJson(inst.configPaths.serverDescription, req.body);
|
||||||
|
res.json({ status: "updated" });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/:id/worlds/update-all", (req, res) => {
|
||||||
|
const inst = getInstance(req.params.id);
|
||||||
|
if (!inst) return res.status(404).json({ error: "not found" });
|
||||||
|
|
||||||
|
const files = listWorldDescriptions(inst.configPaths.worldsRoot);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const data = readJson(file);
|
||||||
|
const updated = { ...data, ...req.body };
|
||||||
|
writeJson(file, updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ status: "worlds-updated", count: files.length });
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
import { getInstance } from "../core/config";
|
||||||
|
import { restartInstance, getInstanceStatus } from "../core/docker";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.post("/:id/restart", async (req, res) => {
|
||||||
|
const inst = getInstance(req.params.id);
|
||||||
|
if (!inst) return res.status(404).json({ error: "not found" });
|
||||||
|
|
||||||
|
await restartInstance(inst);
|
||||||
|
res.json({ status: "restarted" });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/:id/status", async (req, res) => {
|
||||||
|
const inst = getInstance(req.params.id);
|
||||||
|
if (!inst) return res.status(404).json({ error: "not found" });
|
||||||
|
|
||||||
|
const status = await getInstanceStatus(inst);
|
||||||
|
res.json(status);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
|
||||||
|
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD;
|
||||||
|
|
||||||
|
export function requireAuth(req: Request, res: Response, next: NextFunction) {
|
||||||
|
const header = req.headers.authorization;
|
||||||
|
|
||||||
|
if (!header || !header.startsWith("Basic ")) {
|
||||||
|
res.setHeader("WWW-Authenticate", "Basic");
|
||||||
|
return res.status(401).send("Authentication required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoded = Buffer.from(header.replace("Basic ", ""), "base64").toString();
|
||||||
|
const [user, pass] = decoded.split(":");
|
||||||
|
|
||||||
|
if (pass !== ADMIN_PASSWORD) {
|
||||||
|
return res.status(403).send("Forbidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import YAML from "yaml";
|
||||||
|
|
||||||
|
export interface InstanceConfig {
|
||||||
|
id: string;
|
||||||
|
containerName: string;
|
||||||
|
image: string;
|
||||||
|
savedDir: string;
|
||||||
|
configPaths: {
|
||||||
|
serverDescription: string;
|
||||||
|
worldsRoot: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const CONFIG_PATH = path.join(__dirname, "..", "..", "config", "instances.yml");
|
||||||
|
|
||||||
|
let cache: { instances: InstanceConfig[] };
|
||||||
|
|
||||||
|
export function loadConfig() {
|
||||||
|
if (!cache) {
|
||||||
|
const raw = fs.readFileSync(CONFIG_PATH, "utf8");
|
||||||
|
cache = YAML.parse(raw);
|
||||||
|
}
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getInstance(id: string) {
|
||||||
|
return loadConfig().instances.find(i => i.id === id);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import Docker from "dockerode";
|
||||||
|
import { InstanceConfig } from "./config";
|
||||||
|
|
||||||
|
const docker = new Docker({ socketPath: "/var/run/docker.sock" });
|
||||||
|
|
||||||
|
export async function restartInstance(inst: InstanceConfig) {
|
||||||
|
const container = docker.getContainer(inst.containerName);
|
||||||
|
await container.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getInstanceStatus(inst: InstanceConfig) {
|
||||||
|
try {
|
||||||
|
const container = docker.getContainer(inst.containerName);
|
||||||
|
const info = await container.inspect();
|
||||||
|
return {
|
||||||
|
running: info.State.Running,
|
||||||
|
status: info.State.Status
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { running: false, status: "not found" };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export function readJson(file: string) {
|
||||||
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeJson(file: string, data: any) {
|
||||||
|
fs.writeFileSync(file, JSON.stringify(data, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listWorldDescriptions(worldsRoot: string) {
|
||||||
|
const versions = fs.readdirSync(worldsRoot);
|
||||||
|
const files: string[] = [];
|
||||||
|
|
||||||
|
for (const v of versions) {
|
||||||
|
const worldsDir = path.join(worldsRoot, v, "Worlds");
|
||||||
|
if (!fs.existsSync(worldsDir)) continue;
|
||||||
|
|
||||||
|
for (const worldId of fs.readdirSync(worldsDir)) {
|
||||||
|
const file = path.join(worldsDir, worldId, "WorldDescription.json");
|
||||||
|
if (fs.existsSync(file)) files.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import express from "express";
|
||||||
|
import { requireAuth } from "./auth";
|
||||||
|
import instanceApi from "./api/instance";
|
||||||
|
import configApi from "./api/config";
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Protect everything
|
||||||
|
app.use(requireAuth);
|
||||||
|
|
||||||
|
app.use("/instance", instanceApi);
|
||||||
|
app.use("/config", configApi);
|
||||||
|
|
||||||
|
const port = process.env.PORT || 3000;
|
||||||
|
app.listen(port, () => console.log(`Windrose Manager running on port ${port}`));
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"strict": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user