valheim-oz-dsm 1.5.1 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +77 -1
- package/README.md +81 -27
- package/dist/main.js +756 -56
- package/dist/main.js.map +1 -1
- package/package.json +4 -3
package/dist/main.js
CHANGED
|
@@ -1196,6 +1196,11 @@ function getPlatform() {
|
|
|
1196
1196
|
if (os === "darwin") return "darwin";
|
|
1197
1197
|
return "linux";
|
|
1198
1198
|
}
|
|
1199
|
+
function getSteamPlatform() {
|
|
1200
|
+
const platform = getPlatform();
|
|
1201
|
+
if (platform === "darwin") return "macos";
|
|
1202
|
+
return platform;
|
|
1203
|
+
}
|
|
1199
1204
|
function getHomeDir() {
|
|
1200
1205
|
const platform = getPlatform();
|
|
1201
1206
|
if (platform === "windows") {
|
|
@@ -1690,9 +1695,11 @@ function parseValue(value) {
|
|
|
1690
1695
|
}
|
|
1691
1696
|
|
|
1692
1697
|
// src/cli/commands/doctor.ts
|
|
1698
|
+
import { exec as exec2 } from "child_process";
|
|
1693
1699
|
import fs3 from "fs/promises";
|
|
1694
1700
|
import net from "net";
|
|
1695
1701
|
import path4 from "path";
|
|
1702
|
+
import { promisify as promisify2 } from "util";
|
|
1696
1703
|
import steamcmd4 from "@caleb-collar/steamcmd";
|
|
1697
1704
|
|
|
1698
1705
|
// src/steamcmd/mod.ts
|
|
@@ -1925,8 +1932,11 @@ async function installValheim(onProgress) {
|
|
|
1925
1932
|
message: "Starting Valheim installation..."
|
|
1926
1933
|
});
|
|
1927
1934
|
try {
|
|
1935
|
+
const platform = getSteamPlatform();
|
|
1928
1936
|
await steamcmd3.install({
|
|
1929
1937
|
applicationId: VALHEIM_APP_ID,
|
|
1938
|
+
platform,
|
|
1939
|
+
// Explicitly set platform to ensure correct server binaries are downloaded
|
|
1930
1940
|
onProgress: (p) => {
|
|
1931
1941
|
const stage = mapPhaseToStage2(p.phase);
|
|
1932
1942
|
const progress = p.percent ?? 0;
|
|
@@ -2005,6 +2015,7 @@ async function getInstalledVersion() {
|
|
|
2005
2015
|
}
|
|
2006
2016
|
|
|
2007
2017
|
// src/cli/commands/doctor.ts
|
|
2018
|
+
var execAsync2 = promisify2(exec2);
|
|
2008
2019
|
async function exists(filePath) {
|
|
2009
2020
|
try {
|
|
2010
2021
|
await fs3.access(filePath);
|
|
@@ -2171,6 +2182,69 @@ async function checkPermissions() {
|
|
|
2171
2182
|
};
|
|
2172
2183
|
}
|
|
2173
2184
|
}
|
|
2185
|
+
async function checkLinux32BitLibs() {
|
|
2186
|
+
const platform = getPlatform();
|
|
2187
|
+
if (platform !== "linux") {
|
|
2188
|
+
return {
|
|
2189
|
+
name: "32-bit Libraries (Linux)",
|
|
2190
|
+
status: "pass",
|
|
2191
|
+
message: "Not applicable on this platform"
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2194
|
+
try {
|
|
2195
|
+
const { stdout: dpkgCheck } = await execAsync2("which dpkg 2>/dev/null");
|
|
2196
|
+
if (!dpkgCheck.trim()) {
|
|
2197
|
+
return {
|
|
2198
|
+
name: "32-bit Libraries (Linux)",
|
|
2199
|
+
status: "pass",
|
|
2200
|
+
message: "Non-Debian system detected, assuming dependencies are met"
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
const { stdout: archCheck } = await execAsync2(
|
|
2204
|
+
"dpkg --print-foreign-architectures 2>/dev/null"
|
|
2205
|
+
);
|
|
2206
|
+
const hasI386 = archCheck.includes("i386");
|
|
2207
|
+
if (!hasI386) {
|
|
2208
|
+
return {
|
|
2209
|
+
name: "32-bit Libraries (Linux)",
|
|
2210
|
+
status: "fail",
|
|
2211
|
+
message: "i386 architecture not enabled. Run: sudo dpkg --add-architecture i386 && sudo apt update",
|
|
2212
|
+
fixable: false
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
const requiredPackages = ["lib32gcc-s1", "lib32stdc++6", "libc6:i386"];
|
|
2216
|
+
const missingPackages = [];
|
|
2217
|
+
for (const pkg of requiredPackages) {
|
|
2218
|
+
try {
|
|
2219
|
+
const { stdout } = await execAsync2(`dpkg -s ${pkg} 2>/dev/null`);
|
|
2220
|
+
if (!stdout.includes("Status: install ok installed")) {
|
|
2221
|
+
missingPackages.push(pkg);
|
|
2222
|
+
}
|
|
2223
|
+
} catch {
|
|
2224
|
+
missingPackages.push(pkg);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
if (missingPackages.length > 0) {
|
|
2228
|
+
return {
|
|
2229
|
+
name: "32-bit Libraries (Linux)",
|
|
2230
|
+
status: "fail",
|
|
2231
|
+
message: `Missing packages: ${missingPackages.join(", ")}. Install with: sudo apt install ${missingPackages.join(" ")}`,
|
|
2232
|
+
fixable: false
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
return {
|
|
2236
|
+
name: "32-bit Libraries (Linux)",
|
|
2237
|
+
status: "pass",
|
|
2238
|
+
message: "All required 32-bit libraries are installed"
|
|
2239
|
+
};
|
|
2240
|
+
} catch (error2) {
|
|
2241
|
+
return {
|
|
2242
|
+
name: "32-bit Libraries (Linux)",
|
|
2243
|
+
status: "warn",
|
|
2244
|
+
message: `Could not verify 32-bit libraries: ${error2}`
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2174
2248
|
async function runAllChecks() {
|
|
2175
2249
|
const checks = [];
|
|
2176
2250
|
checks.push(await checkSteamCmd());
|
|
@@ -2179,6 +2253,7 @@ async function runAllChecks() {
|
|
|
2179
2253
|
checks.push(await checkSaveDirectory());
|
|
2180
2254
|
checks.push(await checkPorts());
|
|
2181
2255
|
checks.push(await checkPermissions());
|
|
2256
|
+
checks.push(await checkLinux32BitLibs());
|
|
2182
2257
|
const summary = {
|
|
2183
2258
|
passed: checks.filter((c) => c.status === "pass").length,
|
|
2184
2259
|
warnings: checks.filter((c) => c.status === "warn").length,
|
|
@@ -2569,6 +2644,25 @@ import * as fs5 from "fs/promises";
|
|
|
2569
2644
|
import { dirname, join } from "path";
|
|
2570
2645
|
|
|
2571
2646
|
// src/server/logs.ts
|
|
2647
|
+
function parseLogLine(line) {
|
|
2648
|
+
const timestamp = /* @__PURE__ */ new Date();
|
|
2649
|
+
let level = "info";
|
|
2650
|
+
let message = line.trim();
|
|
2651
|
+
if (line.includes("Error") || line.includes("Exception")) {
|
|
2652
|
+
level = "error";
|
|
2653
|
+
} else if (line.includes("Warning") || line.includes("WARN")) {
|
|
2654
|
+
level = "warn";
|
|
2655
|
+
} else if (line.includes("DEBUG") || line.includes("[Debug]")) {
|
|
2656
|
+
level = "debug";
|
|
2657
|
+
}
|
|
2658
|
+
const timestampMatch = line.match(
|
|
2659
|
+
/^(\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}:\d{2}): (.+)$/
|
|
2660
|
+
);
|
|
2661
|
+
if (timestampMatch) {
|
|
2662
|
+
message = timestampMatch[2];
|
|
2663
|
+
}
|
|
2664
|
+
return { timestamp, level, message, raw: line };
|
|
2665
|
+
}
|
|
2572
2666
|
function parseEvent(line) {
|
|
2573
2667
|
if (line.includes("Got character ZDOID from")) {
|
|
2574
2668
|
const match = line.match(/Got character ZDOID from (\S+)/);
|
|
@@ -2615,9 +2709,179 @@ function parseEvent(line) {
|
|
|
2615
2709
|
return null;
|
|
2616
2710
|
}
|
|
2617
2711
|
|
|
2712
|
+
// src/server/logTail.ts
|
|
2713
|
+
import { open } from "fs/promises";
|
|
2714
|
+
var LogTailer = class {
|
|
2715
|
+
filePath;
|
|
2716
|
+
handle = null;
|
|
2717
|
+
position = 0;
|
|
2718
|
+
running = false;
|
|
2719
|
+
pollInterval = null;
|
|
2720
|
+
buffer = "";
|
|
2721
|
+
onLine;
|
|
2722
|
+
onEvent;
|
|
2723
|
+
pollMs;
|
|
2724
|
+
/**
|
|
2725
|
+
* Creates a new log tailer
|
|
2726
|
+
* @param filePath Path to the log file to tail
|
|
2727
|
+
* @param onLine Callback for each new log line
|
|
2728
|
+
* @param options Optional settings
|
|
2729
|
+
*/
|
|
2730
|
+
constructor(filePath, onLine, options) {
|
|
2731
|
+
this.filePath = filePath;
|
|
2732
|
+
this.onLine = onLine;
|
|
2733
|
+
this.onEvent = options?.onEvent;
|
|
2734
|
+
this.pollMs = options?.pollMs ?? 500;
|
|
2735
|
+
}
|
|
2736
|
+
/**
|
|
2737
|
+
* Starts tailing the log file
|
|
2738
|
+
* @param fromEnd If true, start from end of file (skip existing content)
|
|
2739
|
+
*/
|
|
2740
|
+
async start(fromEnd = true) {
|
|
2741
|
+
if (this.running) return;
|
|
2742
|
+
try {
|
|
2743
|
+
this.handle = await open(this.filePath, "r");
|
|
2744
|
+
if (fromEnd) {
|
|
2745
|
+
const stats = await this.handle.stat();
|
|
2746
|
+
this.position = stats.size;
|
|
2747
|
+
} else {
|
|
2748
|
+
this.position = 0;
|
|
2749
|
+
}
|
|
2750
|
+
this.running = true;
|
|
2751
|
+
this.pollInterval = setInterval(() => this.poll(), this.pollMs);
|
|
2752
|
+
await this.poll();
|
|
2753
|
+
} catch (_error) {
|
|
2754
|
+
this.running = true;
|
|
2755
|
+
this.pollInterval = setInterval(() => this.poll(), this.pollMs);
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
/**
|
|
2759
|
+
* Stops tailing the log file
|
|
2760
|
+
*/
|
|
2761
|
+
async stop() {
|
|
2762
|
+
this.running = false;
|
|
2763
|
+
if (this.pollInterval) {
|
|
2764
|
+
clearInterval(this.pollInterval);
|
|
2765
|
+
this.pollInterval = null;
|
|
2766
|
+
}
|
|
2767
|
+
if (this.handle) {
|
|
2768
|
+
await this.handle.close();
|
|
2769
|
+
this.handle = null;
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
/**
|
|
2773
|
+
* Polls the log file for new content
|
|
2774
|
+
*/
|
|
2775
|
+
async poll() {
|
|
2776
|
+
if (!this.running) return;
|
|
2777
|
+
try {
|
|
2778
|
+
if (!this.handle) {
|
|
2779
|
+
try {
|
|
2780
|
+
this.handle = await open(this.filePath, "r");
|
|
2781
|
+
const stats2 = await this.handle.stat();
|
|
2782
|
+
if (this.position > stats2.size) {
|
|
2783
|
+
this.position = 0;
|
|
2784
|
+
}
|
|
2785
|
+
} catch {
|
|
2786
|
+
return;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
const stats = await this.handle.stat();
|
|
2790
|
+
if (stats.size <= this.position) {
|
|
2791
|
+
if (stats.size < this.position) {
|
|
2792
|
+
this.position = 0;
|
|
2793
|
+
}
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2796
|
+
const bytesToRead = stats.size - this.position;
|
|
2797
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
2798
|
+
const { bytesRead } = await this.handle.read(
|
|
2799
|
+
buffer,
|
|
2800
|
+
0,
|
|
2801
|
+
bytesToRead,
|
|
2802
|
+
this.position
|
|
2803
|
+
);
|
|
2804
|
+
if (bytesRead > 0) {
|
|
2805
|
+
this.position += bytesRead;
|
|
2806
|
+
this.processChunk(buffer.toString("utf-8", 0, bytesRead));
|
|
2807
|
+
}
|
|
2808
|
+
} catch (_error) {
|
|
2809
|
+
if (this.handle) {
|
|
2810
|
+
try {
|
|
2811
|
+
await this.handle.close();
|
|
2812
|
+
} catch {
|
|
2813
|
+
}
|
|
2814
|
+
this.handle = null;
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
/**
|
|
2819
|
+
* Processes a chunk of log data, splitting into lines
|
|
2820
|
+
*/
|
|
2821
|
+
processChunk(chunk) {
|
|
2822
|
+
this.buffer += chunk;
|
|
2823
|
+
const lines = this.buffer.split("\n");
|
|
2824
|
+
this.buffer = lines.pop() ?? "";
|
|
2825
|
+
for (const line of lines) {
|
|
2826
|
+
const trimmed = line.trim();
|
|
2827
|
+
if (!trimmed) continue;
|
|
2828
|
+
const entry = parseLogLine(trimmed);
|
|
2829
|
+
this.onLine(trimmed, entry);
|
|
2830
|
+
const event = parseEvent(trimmed);
|
|
2831
|
+
if (event && this.onEvent) {
|
|
2832
|
+
this.onEvent(event);
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
/**
|
|
2837
|
+
* Reads the last N lines from the log file (for initial display)
|
|
2838
|
+
* @param lineCount Number of lines to read
|
|
2839
|
+
* @returns Array of log entries
|
|
2840
|
+
*/
|
|
2841
|
+
async readLastLines(lineCount = 100) {
|
|
2842
|
+
const entries = [];
|
|
2843
|
+
try {
|
|
2844
|
+
const handle = await open(this.filePath, "r");
|
|
2845
|
+
const stats = await handle.stat();
|
|
2846
|
+
const fileSize = stats.size;
|
|
2847
|
+
const chunkSize = Math.min(16384, fileSize);
|
|
2848
|
+
let position = Math.max(0, fileSize - chunkSize);
|
|
2849
|
+
let content = "";
|
|
2850
|
+
let lines = [];
|
|
2851
|
+
while (lines.length < lineCount && position >= 0) {
|
|
2852
|
+
const buffer = Buffer.alloc(Math.min(chunkSize, fileSize - position));
|
|
2853
|
+
await handle.read(buffer, 0, buffer.length, position);
|
|
2854
|
+
content = buffer.toString("utf-8") + content;
|
|
2855
|
+
lines = content.split("\n").filter((l) => l.trim());
|
|
2856
|
+
if (position === 0) break;
|
|
2857
|
+
position = Math.max(0, position - chunkSize);
|
|
2858
|
+
}
|
|
2859
|
+
await handle.close();
|
|
2860
|
+
const lastLines = lines.slice(-lineCount);
|
|
2861
|
+
for (const line of lastLines) {
|
|
2862
|
+
entries.push(parseLogLine(line.trim()));
|
|
2863
|
+
}
|
|
2864
|
+
} catch {
|
|
2865
|
+
}
|
|
2866
|
+
return entries;
|
|
2867
|
+
}
|
|
2868
|
+
/** Whether the tailer is currently running */
|
|
2869
|
+
get isRunning() {
|
|
2870
|
+
return this.running;
|
|
2871
|
+
}
|
|
2872
|
+
};
|
|
2873
|
+
|
|
2618
2874
|
// src/server/pidfile.ts
|
|
2619
2875
|
import fs6 from "fs/promises";
|
|
2620
2876
|
import path5 from "path";
|
|
2877
|
+
function getServerLogsDir() {
|
|
2878
|
+
return path5.join(getAppConfigDir(), "logs");
|
|
2879
|
+
}
|
|
2880
|
+
function getServerLogFile(timestamp) {
|
|
2881
|
+
const ts = timestamp ?? /* @__PURE__ */ new Date();
|
|
2882
|
+
const dateStr = ts.toISOString().split("T")[0];
|
|
2883
|
+
return path5.join(getServerLogsDir(), `valheim-server-${dateStr}.log`);
|
|
2884
|
+
}
|
|
2621
2885
|
function getPidFilePath() {
|
|
2622
2886
|
return path5.join(getConfigDir(), "oz-valheim", "server.pid");
|
|
2623
2887
|
}
|
|
@@ -2671,9 +2935,28 @@ async function getRunningServer() {
|
|
|
2671
2935
|
}
|
|
2672
2936
|
return data;
|
|
2673
2937
|
}
|
|
2938
|
+
async function ensureLogsDir() {
|
|
2939
|
+
const logsDir = getServerLogsDir();
|
|
2940
|
+
await fs6.mkdir(logsDir, { recursive: true });
|
|
2941
|
+
}
|
|
2942
|
+
async function cleanupOldLogs(keepCount = 7) {
|
|
2943
|
+
const logsDir = getServerLogsDir();
|
|
2944
|
+
try {
|
|
2945
|
+
const files = await fs6.readdir(logsDir);
|
|
2946
|
+
const logFiles = files.filter((f) => f.startsWith("valheim-server-") && f.endsWith(".log")).sort().reverse();
|
|
2947
|
+
for (const file of logFiles.slice(keepCount)) {
|
|
2948
|
+
try {
|
|
2949
|
+
await fs6.unlink(path5.join(logsDir, file));
|
|
2950
|
+
} catch {
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
} catch {
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2674
2956
|
|
|
2675
2957
|
// src/server/process.ts
|
|
2676
2958
|
import { spawn } from "child_process";
|
|
2959
|
+
import { createWriteStream } from "fs";
|
|
2677
2960
|
var defaultEvents = {
|
|
2678
2961
|
onStateChange: () => {
|
|
2679
2962
|
},
|
|
@@ -2692,6 +2975,12 @@ var ValheimProcess = class {
|
|
|
2692
2975
|
events;
|
|
2693
2976
|
config;
|
|
2694
2977
|
startTime = null;
|
|
2978
|
+
logFileStream = null;
|
|
2979
|
+
logTailer = null;
|
|
2980
|
+
_logFilePath = null;
|
|
2981
|
+
_isDetached = false;
|
|
2982
|
+
/** PID for detached processes (when we don't have a direct handle) */
|
|
2983
|
+
_detachedPid = null;
|
|
2695
2984
|
/**
|
|
2696
2985
|
* Creates a new Valheim process wrapper
|
|
2697
2986
|
* @param config Server launch configuration
|
|
@@ -2703,7 +2992,7 @@ var ValheimProcess = class {
|
|
|
2703
2992
|
}
|
|
2704
2993
|
/** Gets the process ID if running, null otherwise */
|
|
2705
2994
|
get pid() {
|
|
2706
|
-
return this.process?.pid ?? null;
|
|
2995
|
+
return this.process?.pid ?? this._detachedPid ?? null;
|
|
2707
2996
|
}
|
|
2708
2997
|
/** Gets the current process state */
|
|
2709
2998
|
get currentState() {
|
|
@@ -2713,6 +3002,14 @@ var ValheimProcess = class {
|
|
|
2713
3002
|
get uptime() {
|
|
2714
3003
|
return this.startTime;
|
|
2715
3004
|
}
|
|
3005
|
+
/** Gets the log file path (for detached mode) */
|
|
3006
|
+
get logFilePath() {
|
|
3007
|
+
return this._logFilePath;
|
|
3008
|
+
}
|
|
3009
|
+
/** Whether the server is running in detached mode */
|
|
3010
|
+
get isDetached() {
|
|
3011
|
+
return this._isDetached;
|
|
3012
|
+
}
|
|
2716
3013
|
/**
|
|
2717
3014
|
* Updates the process state and notifies listeners
|
|
2718
3015
|
* @param newState New process state
|
|
@@ -2731,21 +3028,16 @@ var ValheimProcess = class {
|
|
|
2731
3028
|
}
|
|
2732
3029
|
this.setState("starting");
|
|
2733
3030
|
this.startTime = /* @__PURE__ */ new Date();
|
|
3031
|
+
this._isDetached = this.config.detached ?? false;
|
|
2734
3032
|
const execPath = getValheimExecutablePath();
|
|
2735
3033
|
const args = this.buildArgs();
|
|
2736
3034
|
const env = this.getEnvironment();
|
|
2737
3035
|
try {
|
|
2738
|
-
this.
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
this.process.on("error", (error2) => {
|
|
2744
|
-
this.setState("crashed");
|
|
2745
|
-
this.startTime = null;
|
|
2746
|
-
this.events.onError(error2);
|
|
2747
|
-
});
|
|
2748
|
-
await Promise.resolve();
|
|
3036
|
+
if (this._isDetached) {
|
|
3037
|
+
await this.startDetached(execPath, args, env);
|
|
3038
|
+
} else {
|
|
3039
|
+
await this.startAttached(execPath, args, env);
|
|
3040
|
+
}
|
|
2749
3041
|
} catch (error2) {
|
|
2750
3042
|
this.setState("crashed");
|
|
2751
3043
|
this.startTime = null;
|
|
@@ -2753,6 +3045,169 @@ var ValheimProcess = class {
|
|
|
2753
3045
|
throw error2;
|
|
2754
3046
|
}
|
|
2755
3047
|
}
|
|
3048
|
+
/**
|
|
3049
|
+
* Starts the server in attached mode (piped stdout/stderr)
|
|
3050
|
+
*/
|
|
3051
|
+
async startAttached(execPath, args, env) {
|
|
3052
|
+
this.process = spawn(execPath, args, {
|
|
3053
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3054
|
+
env
|
|
3055
|
+
});
|
|
3056
|
+
this.streamOutput();
|
|
3057
|
+
this.process.on("error", (error2) => {
|
|
3058
|
+
this.setState("crashed");
|
|
3059
|
+
this.startTime = null;
|
|
3060
|
+
this.events.onError(error2);
|
|
3061
|
+
});
|
|
3062
|
+
await Promise.resolve();
|
|
3063
|
+
}
|
|
3064
|
+
/**
|
|
3065
|
+
* Starts the server in detached mode (log file output, independent process)
|
|
3066
|
+
*/
|
|
3067
|
+
async startDetached(execPath, args, env) {
|
|
3068
|
+
await ensureLogsDir();
|
|
3069
|
+
this._logFilePath = getServerLogFile(this.startTime);
|
|
3070
|
+
this.logFileStream = createWriteStream(this._logFilePath, { flags: "a" });
|
|
3071
|
+
const header = `
|
|
3072
|
+
${"=".repeat(60)}
|
|
3073
|
+
Server starting at ${this.startTime.toISOString()}
|
|
3074
|
+
World: ${this.config.world} | Port: ${this.config.port}
|
|
3075
|
+
${"=".repeat(60)}
|
|
3076
|
+
`;
|
|
3077
|
+
this.logFileStream.write(header);
|
|
3078
|
+
const platform = getPlatform();
|
|
3079
|
+
this.process = spawn(execPath, args, {
|
|
3080
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3081
|
+
env,
|
|
3082
|
+
detached: true
|
|
3083
|
+
});
|
|
3084
|
+
if (this.process.stdout) {
|
|
3085
|
+
this.process.stdout.pipe(this.logFileStream, { end: false });
|
|
3086
|
+
}
|
|
3087
|
+
if (this.process.stderr) {
|
|
3088
|
+
this.process.stderr.pipe(this.logFileStream, { end: false });
|
|
3089
|
+
}
|
|
3090
|
+
this.logTailer = new LogTailer(
|
|
3091
|
+
this._logFilePath,
|
|
3092
|
+
(line, _entry) => {
|
|
3093
|
+
this.events.onLog(line);
|
|
3094
|
+
},
|
|
3095
|
+
{
|
|
3096
|
+
onEvent: (event) => this.handleEvent(event),
|
|
3097
|
+
fromEnd: false
|
|
3098
|
+
// Read from where we started
|
|
3099
|
+
}
|
|
3100
|
+
);
|
|
3101
|
+
await this.logTailer.start(false);
|
|
3102
|
+
this.process.on("error", (error2) => {
|
|
3103
|
+
this.setState("crashed");
|
|
3104
|
+
this.startTime = null;
|
|
3105
|
+
this.events.onError(error2);
|
|
3106
|
+
});
|
|
3107
|
+
const pid = this.process.pid;
|
|
3108
|
+
if (pid) {
|
|
3109
|
+
await writePidFile({
|
|
3110
|
+
pid,
|
|
3111
|
+
startedAt: this.startTime.toISOString(),
|
|
3112
|
+
world: this.config.world,
|
|
3113
|
+
port: this.config.port,
|
|
3114
|
+
logFile: this._logFilePath,
|
|
3115
|
+
detached: true,
|
|
3116
|
+
serverName: this.config.name
|
|
3117
|
+
});
|
|
3118
|
+
}
|
|
3119
|
+
this.process.unref();
|
|
3120
|
+
if (platform === "windows") {
|
|
3121
|
+
if (this.process.stdout && typeof this.process.stdout.unref === "function") {
|
|
3122
|
+
this.process.stdout.unref();
|
|
3123
|
+
}
|
|
3124
|
+
if (this.process.stderr && typeof this.process.stderr.unref === "function") {
|
|
3125
|
+
this.process.stderr.unref();
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
this.process.on("exit", (code, signal) => {
|
|
3129
|
+
const exitCode = code ?? (signal ? 1 : 0);
|
|
3130
|
+
if (exitCode !== 0 && (this.state === "online" || this.state === "starting")) {
|
|
3131
|
+
this.startTime = null;
|
|
3132
|
+
this.setState("crashed");
|
|
3133
|
+
this.events.onError(new Error(`Server exited with code ${exitCode}`));
|
|
3134
|
+
}
|
|
3135
|
+
});
|
|
3136
|
+
await Promise.resolve();
|
|
3137
|
+
}
|
|
3138
|
+
/**
|
|
3139
|
+
* Handles a parsed event from log output
|
|
3140
|
+
*/
|
|
3141
|
+
handleEvent(event) {
|
|
3142
|
+
this.events.onEvent?.(event);
|
|
3143
|
+
switch (event.type) {
|
|
3144
|
+
case "player_join":
|
|
3145
|
+
this.events.onPlayerJoin(event.name);
|
|
3146
|
+
break;
|
|
3147
|
+
case "player_leave":
|
|
3148
|
+
this.events.onPlayerLeave(event.name);
|
|
3149
|
+
break;
|
|
3150
|
+
case "server_ready":
|
|
3151
|
+
if (this.state === "starting") {
|
|
3152
|
+
this.setState("online");
|
|
3153
|
+
}
|
|
3154
|
+
break;
|
|
3155
|
+
case "error":
|
|
3156
|
+
this.events.onError(new Error(event.message));
|
|
3157
|
+
break;
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
/**
|
|
3161
|
+
* Attaches to an already-running detached server
|
|
3162
|
+
* @param pidData PID file data for the running server
|
|
3163
|
+
*/
|
|
3164
|
+
async attach(pidData) {
|
|
3165
|
+
if (this.state !== "offline" && this.state !== "crashed") {
|
|
3166
|
+
throw new Error(`Cannot attach in state: ${this.state}`);
|
|
3167
|
+
}
|
|
3168
|
+
if (!isProcessRunning(pidData.pid)) {
|
|
3169
|
+
throw new Error(`Server process ${pidData.pid} is not running`);
|
|
3170
|
+
}
|
|
3171
|
+
this._isDetached = true;
|
|
3172
|
+
this._detachedPid = pidData.pid;
|
|
3173
|
+
this._logFilePath = pidData.logFile ?? null;
|
|
3174
|
+
this.startTime = new Date(pidData.startedAt);
|
|
3175
|
+
if (this._logFilePath) {
|
|
3176
|
+
this.logTailer = new LogTailer(
|
|
3177
|
+
this._logFilePath,
|
|
3178
|
+
(line, _entry) => {
|
|
3179
|
+
this.events.onLog(line);
|
|
3180
|
+
},
|
|
3181
|
+
{
|
|
3182
|
+
onEvent: (event) => this.handleEvent(event),
|
|
3183
|
+
pollMs: 500
|
|
3184
|
+
}
|
|
3185
|
+
);
|
|
3186
|
+
const history = await this.logTailer.readLastLines(50);
|
|
3187
|
+
for (const entry of history) {
|
|
3188
|
+
this.events.onLog(entry.raw);
|
|
3189
|
+
}
|
|
3190
|
+
await this.logTailer.start(true);
|
|
3191
|
+
}
|
|
3192
|
+
this.startProcessMonitor();
|
|
3193
|
+
this.setState("online");
|
|
3194
|
+
}
|
|
3195
|
+
/**
|
|
3196
|
+
* Monitors a detached process for exit
|
|
3197
|
+
*/
|
|
3198
|
+
startProcessMonitor() {
|
|
3199
|
+
const checkInterval = setInterval(() => {
|
|
3200
|
+
const pid = this._detachedPid;
|
|
3201
|
+
if (pid && !isProcessRunning(pid)) {
|
|
3202
|
+
clearInterval(checkInterval);
|
|
3203
|
+
this._detachedPid = null;
|
|
3204
|
+
this.startTime = null;
|
|
3205
|
+
this.setState("crashed");
|
|
3206
|
+
this.events.onError(new Error("Server process exited unexpectedly"));
|
|
3207
|
+
}
|
|
3208
|
+
}, 2e3);
|
|
3209
|
+
this._monitorInterval = checkInterval;
|
|
3210
|
+
}
|
|
2756
3211
|
/**
|
|
2757
3212
|
* Gracefully stops the server with optional timeout
|
|
2758
3213
|
* @param timeout Maximum time to wait for graceful shutdown (ms)
|
|
@@ -2762,6 +3217,31 @@ var ValheimProcess = class {
|
|
|
2762
3217
|
return;
|
|
2763
3218
|
}
|
|
2764
3219
|
this.setState("stopping");
|
|
3220
|
+
await this.cleanup();
|
|
3221
|
+
if (this._isDetached && this._detachedPid) {
|
|
3222
|
+
const platform = getPlatform();
|
|
3223
|
+
try {
|
|
3224
|
+
process.kill(this._detachedPid, "SIGTERM");
|
|
3225
|
+
} catch {
|
|
3226
|
+
}
|
|
3227
|
+
const startTime = Date.now();
|
|
3228
|
+
while (isProcessRunning(this._detachedPid) && Date.now() - startTime < timeout) {
|
|
3229
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3230
|
+
}
|
|
3231
|
+
if (isProcessRunning(this._detachedPid)) {
|
|
3232
|
+
try {
|
|
3233
|
+
process.kill(
|
|
3234
|
+
this._detachedPid,
|
|
3235
|
+
platform === "windows" ? "SIGTERM" : "SIGKILL"
|
|
3236
|
+
);
|
|
3237
|
+
} catch {
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
this._detachedPid = null;
|
|
3241
|
+
this.startTime = null;
|
|
3242
|
+
this.setState("offline");
|
|
3243
|
+
return;
|
|
3244
|
+
}
|
|
2765
3245
|
if (this.process) {
|
|
2766
3246
|
try {
|
|
2767
3247
|
this.process.kill("SIGTERM");
|
|
@@ -2789,6 +3269,18 @@ var ValheimProcess = class {
|
|
|
2789
3269
|
* Immediately kills the server process
|
|
2790
3270
|
*/
|
|
2791
3271
|
async kill() {
|
|
3272
|
+
await this.cleanup();
|
|
3273
|
+
if (this._isDetached && this._detachedPid) {
|
|
3274
|
+
const platform = getPlatform();
|
|
3275
|
+
try {
|
|
3276
|
+
process.kill(
|
|
3277
|
+
this._detachedPid,
|
|
3278
|
+
platform === "windows" ? "SIGTERM" : "SIGKILL"
|
|
3279
|
+
);
|
|
3280
|
+
} catch {
|
|
3281
|
+
}
|
|
3282
|
+
this._detachedPid = null;
|
|
3283
|
+
}
|
|
2792
3284
|
if (this.process) {
|
|
2793
3285
|
try {
|
|
2794
3286
|
this.process.kill("SIGKILL");
|
|
@@ -2800,6 +3292,36 @@ var ValheimProcess = class {
|
|
|
2800
3292
|
this.setState("offline");
|
|
2801
3293
|
await Promise.resolve();
|
|
2802
3294
|
}
|
|
3295
|
+
/**
|
|
3296
|
+
* Detaches from a running server without stopping it
|
|
3297
|
+
* Only valid for servers started in detached mode
|
|
3298
|
+
*/
|
|
3299
|
+
async detach() {
|
|
3300
|
+
if (!this._isDetached) {
|
|
3301
|
+
throw new Error("Cannot detach from non-detached server");
|
|
3302
|
+
}
|
|
3303
|
+
await this.cleanup();
|
|
3304
|
+
this.process = null;
|
|
3305
|
+
this._detachedPid = null;
|
|
3306
|
+
}
|
|
3307
|
+
/**
|
|
3308
|
+
* Cleans up resources (log tailer, monitor interval, log file stream)
|
|
3309
|
+
*/
|
|
3310
|
+
async cleanup() {
|
|
3311
|
+
const self = this;
|
|
3312
|
+
if (self._monitorInterval) {
|
|
3313
|
+
clearInterval(self._monitorInterval);
|
|
3314
|
+
self._monitorInterval = void 0;
|
|
3315
|
+
}
|
|
3316
|
+
if (this.logTailer) {
|
|
3317
|
+
await this.logTailer.stop();
|
|
3318
|
+
this.logTailer = null;
|
|
3319
|
+
}
|
|
3320
|
+
if (this.logFileStream) {
|
|
3321
|
+
this.logFileStream.end();
|
|
3322
|
+
this.logFileStream = null;
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
2803
3325
|
/**
|
|
2804
3326
|
* Builds command line arguments for Valheim server
|
|
2805
3327
|
* @returns Array of command line arguments
|
|
@@ -3115,6 +3637,18 @@ async function startCommand(args, config) {
|
|
|
3115
3637
|
console.log("Run 'valheim-dsm install' first to install the server.");
|
|
3116
3638
|
process.exit(1);
|
|
3117
3639
|
}
|
|
3640
|
+
const running = await getRunningServer();
|
|
3641
|
+
if (running) {
|
|
3642
|
+
console.error(`
|
|
3643
|
+
Error: A server is already running.`);
|
|
3644
|
+
console.log(` PID: ${running.pid}`);
|
|
3645
|
+
console.log(` World: ${running.world}`);
|
|
3646
|
+
console.log(` Port: ${running.port}`);
|
|
3647
|
+
console.log(` Started: ${new Date(running.startedAt).toLocaleString()}`);
|
|
3648
|
+
console.log("\nRun 'valheim-dsm stop' to stop it first.");
|
|
3649
|
+
process.exit(1);
|
|
3650
|
+
}
|
|
3651
|
+
await cleanupOldLogs();
|
|
3118
3652
|
const serverConfig = {
|
|
3119
3653
|
name: args.name ?? config.server.name,
|
|
3120
3654
|
port: args.port ?? config.server.port,
|
|
@@ -3124,7 +3658,9 @@ async function startCommand(args, config) {
|
|
|
3124
3658
|
crossplay: args.crossplay ?? config.server.crossplay,
|
|
3125
3659
|
savedir: args.savedir ?? config.server.savedir,
|
|
3126
3660
|
saveinterval: config.server.saveinterval,
|
|
3127
|
-
backups: config.server.backups
|
|
3661
|
+
backups: config.server.backups,
|
|
3662
|
+
// Always use detached mode for stability
|
|
3663
|
+
detached: true
|
|
3128
3664
|
};
|
|
3129
3665
|
console.log(`
|
|
3130
3666
|
Starting ${serverConfig.name}...`);
|
|
@@ -3132,6 +3668,7 @@ Starting ${serverConfig.name}...`);
|
|
|
3132
3668
|
console.log(` Port: ${serverConfig.port}`);
|
|
3133
3669
|
console.log(` Public: ${serverConfig.public}`);
|
|
3134
3670
|
console.log(` Crossplay: ${serverConfig.crossplay}`);
|
|
3671
|
+
console.log(` Mode: Detached (server continues after terminal exits)`);
|
|
3135
3672
|
console.log("");
|
|
3136
3673
|
activeWatchdog = new Watchdog(
|
|
3137
3674
|
serverConfig,
|
|
@@ -3145,6 +3682,12 @@ Starting ${serverConfig.name}...`);
|
|
|
3145
3682
|
{
|
|
3146
3683
|
onStateChange: (state) => {
|
|
3147
3684
|
console.log(`[Server] State: ${state}`);
|
|
3685
|
+
if (state === "online") {
|
|
3686
|
+
console.log("\n\u2713 Server is now online!");
|
|
3687
|
+
console.log(" The server will continue running in the background.");
|
|
3688
|
+
console.log(" Use 'valheim-dsm stop' to stop it.");
|
|
3689
|
+
console.log(" Use 'valheim-dsm' (TUI) to manage it.\n");
|
|
3690
|
+
}
|
|
3148
3691
|
},
|
|
3149
3692
|
onLog: (line) => {
|
|
3150
3693
|
console.log(`[Server] ${line}`);
|
|
@@ -3173,18 +3716,34 @@ Starting ${serverConfig.name}...`);
|
|
|
3173
3716
|
setupShutdownHandlers();
|
|
3174
3717
|
try {
|
|
3175
3718
|
await activeWatchdog.start();
|
|
3176
|
-
const
|
|
3177
|
-
if (
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3181
|
-
world: serverConfig.world,
|
|
3182
|
-
port: serverConfig.port
|
|
3183
|
-
});
|
|
3719
|
+
const logPath = activeWatchdog.serverProcess.logFilePath;
|
|
3720
|
+
if (logPath) {
|
|
3721
|
+
console.log(`
|
|
3722
|
+
Server log: ${logPath}`);
|
|
3184
3723
|
}
|
|
3185
|
-
console.log("\nServer
|
|
3186
|
-
|
|
3187
|
-
|
|
3724
|
+
console.log("\nServer is starting in detached mode.");
|
|
3725
|
+
console.log(
|
|
3726
|
+
"Press Ctrl+C to stop monitoring (server will keep running).\n"
|
|
3727
|
+
);
|
|
3728
|
+
const timeout = 12e4;
|
|
3729
|
+
const startTime = Date.now();
|
|
3730
|
+
while (Date.now() - startTime < timeout) {
|
|
3731
|
+
const state = activeWatchdog.serverProcess.currentState;
|
|
3732
|
+
if (state === "online") {
|
|
3733
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
3734
|
+
break;
|
|
3735
|
+
}
|
|
3736
|
+
if (state === "crashed" || state === "offline") {
|
|
3737
|
+
console.error("\nServer failed to start.");
|
|
3738
|
+
await cleanupAndExit(1);
|
|
3739
|
+
return;
|
|
3740
|
+
}
|
|
3741
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
3742
|
+
}
|
|
3743
|
+
console.log("\nDetaching from server...");
|
|
3744
|
+
await activeWatchdog.serverProcess.detach();
|
|
3745
|
+
activeWatchdog = null;
|
|
3746
|
+
process.exit(0);
|
|
3188
3747
|
} catch (error2) {
|
|
3189
3748
|
console.error(`
|
|
3190
3749
|
Failed to start server: ${error2.message}`);
|
|
@@ -3197,14 +3756,26 @@ function getActiveWatchdog() {
|
|
|
3197
3756
|
function clearActiveWatchdog() {
|
|
3198
3757
|
activeWatchdog = null;
|
|
3199
3758
|
}
|
|
3759
|
+
async function cleanupAndExit(code) {
|
|
3760
|
+
if (activeWatchdog) {
|
|
3761
|
+
try {
|
|
3762
|
+
await activeWatchdog.serverProcess.detach();
|
|
3763
|
+
} catch {
|
|
3764
|
+
}
|
|
3765
|
+
activeWatchdog = null;
|
|
3766
|
+
}
|
|
3767
|
+
process.exit(code);
|
|
3768
|
+
}
|
|
3200
3769
|
function setupShutdownHandlers() {
|
|
3201
3770
|
const shutdown = async () => {
|
|
3202
|
-
console.log("\n\
|
|
3771
|
+
console.log("\n\nDetaching from server (it will keep running)...");
|
|
3203
3772
|
if (activeWatchdog) {
|
|
3204
|
-
|
|
3773
|
+
try {
|
|
3774
|
+
await activeWatchdog.serverProcess.detach();
|
|
3775
|
+
} catch {
|
|
3776
|
+
}
|
|
3205
3777
|
activeWatchdog = null;
|
|
3206
3778
|
}
|
|
3207
|
-
await removePidFile();
|
|
3208
3779
|
process.exit(0);
|
|
3209
3780
|
};
|
|
3210
3781
|
process.on("SIGINT", shutdown);
|
|
@@ -3215,9 +3786,9 @@ function setupShutdownHandlers() {
|
|
|
3215
3786
|
|
|
3216
3787
|
// src/cli/commands/stop.ts
|
|
3217
3788
|
async function stopCommand(args) {
|
|
3789
|
+
const timeout = args.timeout ?? 3e4;
|
|
3218
3790
|
const watchdog2 = getActiveWatchdog();
|
|
3219
3791
|
if (watchdog2) {
|
|
3220
|
-
const timeout = args.timeout ?? 3e4;
|
|
3221
3792
|
if (args.force) {
|
|
3222
3793
|
console.log("\nForce stopping server...");
|
|
3223
3794
|
await watchdog2.kill();
|
|
@@ -3237,34 +3808,53 @@ Stopping server (timeout: ${timeout}ms)...`);
|
|
|
3237
3808
|
console.log("\nNote: Run 'valheim-dsm start' to start a server.");
|
|
3238
3809
|
return;
|
|
3239
3810
|
}
|
|
3240
|
-
const { pid, world, port, startedAt } = runningServer;
|
|
3811
|
+
const { pid, world, port, startedAt, detached, logFile } = runningServer;
|
|
3241
3812
|
console.log(`
|
|
3242
3813
|
Found running server:`);
|
|
3243
3814
|
console.log(` PID: ${pid}`);
|
|
3244
3815
|
console.log(` World: ${world}`);
|
|
3245
3816
|
console.log(` Port: ${port}`);
|
|
3246
3817
|
console.log(` Started: ${new Date(startedAt).toLocaleString()}`);
|
|
3818
|
+
console.log(` Mode: ${detached ? "Detached" : "Attached"}`);
|
|
3819
|
+
if (logFile) {
|
|
3820
|
+
console.log(` Log: ${logFile}`);
|
|
3821
|
+
}
|
|
3247
3822
|
if (!isProcessRunning(pid)) {
|
|
3248
3823
|
console.log("\nServer process is no longer running. Cleaning up...");
|
|
3249
3824
|
await removePidFile();
|
|
3250
3825
|
return;
|
|
3251
3826
|
}
|
|
3827
|
+
const platform = getPlatform();
|
|
3252
3828
|
if (args.force) {
|
|
3253
3829
|
console.log("\nForce killing server...");
|
|
3254
|
-
killProcess(pid,
|
|
3830
|
+
killProcess(pid, platform !== "windows");
|
|
3255
3831
|
} else {
|
|
3256
3832
|
console.log("\nSending stop signal...");
|
|
3257
3833
|
killProcess(pid, false);
|
|
3258
|
-
const timeout = args.timeout ?? 3e4;
|
|
3259
3834
|
const startTime = Date.now();
|
|
3835
|
+
let dots = 0;
|
|
3260
3836
|
while (isProcessRunning(pid) && Date.now() - startTime < timeout) {
|
|
3261
3837
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3262
3838
|
process.stdout.write(".");
|
|
3839
|
+
dots++;
|
|
3263
3840
|
}
|
|
3264
|
-
console.log();
|
|
3841
|
+
if (dots > 0) console.log();
|
|
3265
3842
|
if (isProcessRunning(pid)) {
|
|
3266
3843
|
console.log("Server did not stop gracefully, force killing...");
|
|
3267
|
-
killProcess(pid,
|
|
3844
|
+
killProcess(pid, platform !== "windows");
|
|
3845
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
3846
|
+
if (isProcessRunning(pid)) {
|
|
3847
|
+
console.error(
|
|
3848
|
+
"Failed to stop server. Process may require manual termination."
|
|
3849
|
+
);
|
|
3850
|
+
console.log(` PID: ${pid}`);
|
|
3851
|
+
if (platform === "windows") {
|
|
3852
|
+
console.log(` Try: taskkill /F /PID ${pid}`);
|
|
3853
|
+
} else {
|
|
3854
|
+
console.log(` Try: kill -9 ${pid}`);
|
|
3855
|
+
}
|
|
3856
|
+
return;
|
|
3857
|
+
}
|
|
3268
3858
|
}
|
|
3269
3859
|
}
|
|
3270
3860
|
await removePidFile();
|
|
@@ -3793,7 +4383,7 @@ import { useEffect as useEffect3, useMemo as useMemo2, useRef as useRef2, useSta
|
|
|
3793
4383
|
// package.json
|
|
3794
4384
|
var package_default = {
|
|
3795
4385
|
name: "valheim-oz-dsm",
|
|
3796
|
-
version: "1.
|
|
4386
|
+
version: "1.6.1",
|
|
3797
4387
|
description: "Land of OZ - Valheim Dedicated Server Manager",
|
|
3798
4388
|
type: "module",
|
|
3799
4389
|
bin: {
|
|
@@ -3812,7 +4402,8 @@ var package_default = {
|
|
|
3812
4402
|
},
|
|
3813
4403
|
scripts: {
|
|
3814
4404
|
dev: "tsx watch main.ts",
|
|
3815
|
-
start: "
|
|
4405
|
+
start: "npm run build && node dist/main.js",
|
|
4406
|
+
"start:dev": "tsx main.ts",
|
|
3816
4407
|
build: "tsup",
|
|
3817
4408
|
test: "vitest run",
|
|
3818
4409
|
"test:watch": "vitest",
|
|
@@ -3825,7 +4416,7 @@ var package_default = {
|
|
|
3825
4416
|
prepublishOnly: "npm run typecheck && npm run lint && npm test && npm run build"
|
|
3826
4417
|
},
|
|
3827
4418
|
dependencies: {
|
|
3828
|
-
"@caleb-collar/steamcmd": "^1.1.
|
|
4419
|
+
"@caleb-collar/steamcmd": "^1.1.1",
|
|
3829
4420
|
conf: "^13.0.1",
|
|
3830
4421
|
"fullscreen-ink": "^0.1.0",
|
|
3831
4422
|
ink: "^6.6.0",
|
|
@@ -43037,46 +43628,81 @@ var Spinner = (props) => {
|
|
|
43037
43628
|
};
|
|
43038
43629
|
|
|
43039
43630
|
// src/tui/hooks/useServer.ts
|
|
43040
|
-
import { useCallback as useCallback3, useEffect as useEffect6 } from "react";
|
|
43631
|
+
import { useCallback as useCallback3, useEffect as useEffect6, useRef as useRef3 } from "react";
|
|
43041
43632
|
|
|
43042
43633
|
// src/tui/serverManager.ts
|
|
43043
43634
|
var watchdog = null;
|
|
43044
43635
|
var updating = false;
|
|
43636
|
+
var isAttached = false;
|
|
43045
43637
|
function getWatchdog() {
|
|
43046
43638
|
return watchdog;
|
|
43047
43639
|
}
|
|
43048
43640
|
function hasActiveServer() {
|
|
43049
43641
|
return watchdog !== null;
|
|
43050
43642
|
}
|
|
43643
|
+
function isAttachedToServer() {
|
|
43644
|
+
return isAttached;
|
|
43645
|
+
}
|
|
43051
43646
|
function isUpdating() {
|
|
43052
43647
|
return updating;
|
|
43053
43648
|
}
|
|
43054
43649
|
function setUpdating(value) {
|
|
43055
43650
|
updating = value;
|
|
43056
43651
|
}
|
|
43652
|
+
async function checkRunningServer() {
|
|
43653
|
+
return getRunningServer();
|
|
43654
|
+
}
|
|
43655
|
+
async function attachToServer(pidData, events) {
|
|
43656
|
+
if (watchdog) {
|
|
43657
|
+
throw new Error("Already managing a server - stop it first");
|
|
43658
|
+
}
|
|
43659
|
+
const config = {
|
|
43660
|
+
name: pidData.serverName ?? "Valheim Server",
|
|
43661
|
+
port: pidData.port,
|
|
43662
|
+
world: pidData.world,
|
|
43663
|
+
password: "",
|
|
43664
|
+
// Not needed for attach
|
|
43665
|
+
public: false,
|
|
43666
|
+
crossplay: false,
|
|
43667
|
+
detached: true
|
|
43668
|
+
};
|
|
43669
|
+
watchdog = new Watchdog(config, { enabled: false }, events);
|
|
43670
|
+
await watchdog.serverProcess.attach(pidData);
|
|
43671
|
+
isAttached = true;
|
|
43672
|
+
return watchdog;
|
|
43673
|
+
}
|
|
43057
43674
|
async function startServer(config, watchdogConfig, events) {
|
|
43058
43675
|
if (watchdog) {
|
|
43059
43676
|
throw new Error("Server is already running");
|
|
43060
43677
|
}
|
|
43061
|
-
|
|
43062
|
-
|
|
43063
|
-
|
|
43064
|
-
|
|
43065
|
-
|
|
43066
|
-
pid,
|
|
43067
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
43068
|
-
world: config.world,
|
|
43069
|
-
port: config.port
|
|
43070
|
-
});
|
|
43678
|
+
const running = await getRunningServer();
|
|
43679
|
+
if (running) {
|
|
43680
|
+
throw new Error(
|
|
43681
|
+
`A server is already running (PID: ${running.pid}, World: ${running.world}). Stop it first or attach to it.`
|
|
43682
|
+
);
|
|
43071
43683
|
}
|
|
43684
|
+
const detachedConfig = {
|
|
43685
|
+
...config,
|
|
43686
|
+
detached: true
|
|
43687
|
+
};
|
|
43688
|
+
watchdog = new Watchdog(detachedConfig, watchdogConfig, events);
|
|
43689
|
+
await watchdog.start();
|
|
43690
|
+
isAttached = false;
|
|
43072
43691
|
return watchdog;
|
|
43073
43692
|
}
|
|
43074
|
-
async function stopServer() {
|
|
43693
|
+
async function stopServer(keepRunning = false) {
|
|
43075
43694
|
if (!watchdog) {
|
|
43076
43695
|
return;
|
|
43077
43696
|
}
|
|
43697
|
+
if (keepRunning && isAttached) {
|
|
43698
|
+
await watchdog.serverProcess.detach();
|
|
43699
|
+
watchdog = null;
|
|
43700
|
+
isAttached = false;
|
|
43701
|
+
return;
|
|
43702
|
+
}
|
|
43078
43703
|
await watchdog.stop();
|
|
43079
43704
|
watchdog = null;
|
|
43705
|
+
isAttached = false;
|
|
43080
43706
|
await removePidFile();
|
|
43081
43707
|
}
|
|
43082
43708
|
async function killServer() {
|
|
@@ -43085,21 +43711,46 @@ async function killServer() {
|
|
|
43085
43711
|
}
|
|
43086
43712
|
await watchdog.kill();
|
|
43087
43713
|
watchdog = null;
|
|
43714
|
+
isAttached = false;
|
|
43088
43715
|
await removePidFile();
|
|
43089
43716
|
}
|
|
43717
|
+
async function detachFromServer() {
|
|
43718
|
+
if (!watchdog) {
|
|
43719
|
+
return;
|
|
43720
|
+
}
|
|
43721
|
+
if (!isAttached && !watchdog.serverProcess.isDetached) {
|
|
43722
|
+
throw new Error("Cannot detach from a non-detached server");
|
|
43723
|
+
}
|
|
43724
|
+
await watchdog.serverProcess.detach();
|
|
43725
|
+
watchdog = null;
|
|
43726
|
+
isAttached = false;
|
|
43727
|
+
}
|
|
43728
|
+
function getLogFilePath() {
|
|
43729
|
+
return watchdog?.serverProcess.logFilePath ?? null;
|
|
43730
|
+
}
|
|
43090
43731
|
async function cleanupOnExit() {
|
|
43091
|
-
if (watchdog) {
|
|
43732
|
+
if (!watchdog) {
|
|
43733
|
+
return;
|
|
43734
|
+
}
|
|
43735
|
+
if (isAttached || watchdog.serverProcess.isDetached) {
|
|
43092
43736
|
try {
|
|
43093
|
-
await watchdog.
|
|
43737
|
+
await watchdog.serverProcess.detach();
|
|
43094
43738
|
} catch {
|
|
43095
|
-
try {
|
|
43096
|
-
await watchdog.kill();
|
|
43097
|
-
} catch {
|
|
43098
|
-
}
|
|
43099
43739
|
}
|
|
43100
43740
|
watchdog = null;
|
|
43101
|
-
|
|
43741
|
+
isAttached = false;
|
|
43742
|
+
return;
|
|
43102
43743
|
}
|
|
43744
|
+
try {
|
|
43745
|
+
await watchdog.stop();
|
|
43746
|
+
} catch {
|
|
43747
|
+
try {
|
|
43748
|
+
await watchdog.kill();
|
|
43749
|
+
} catch {
|
|
43750
|
+
}
|
|
43751
|
+
}
|
|
43752
|
+
watchdog = null;
|
|
43753
|
+
await removePidFile();
|
|
43103
43754
|
}
|
|
43104
43755
|
|
|
43105
43756
|
// src/tui/hooks/useServer.ts
|
|
@@ -43135,6 +43786,7 @@ function useServer() {
|
|
|
43135
43786
|
const config = useStore((s) => s.config);
|
|
43136
43787
|
const rcon = useStore((s) => s.rcon);
|
|
43137
43788
|
const actions = useStore((s) => s.actions);
|
|
43789
|
+
const hasCheckedForRunning = useRef3(false);
|
|
43138
43790
|
const createWatchdogEvents = useCallback3(
|
|
43139
43791
|
() => ({
|
|
43140
43792
|
onStateChange: (state) => {
|
|
@@ -43206,6 +43858,38 @@ function useServer() {
|
|
|
43206
43858
|
}),
|
|
43207
43859
|
[actions]
|
|
43208
43860
|
);
|
|
43861
|
+
const checkAndAttach = useCallback3(async () => {
|
|
43862
|
+
if (hasActiveServer()) {
|
|
43863
|
+
return;
|
|
43864
|
+
}
|
|
43865
|
+
const running = await checkRunningServer();
|
|
43866
|
+
if (!running) {
|
|
43867
|
+
return;
|
|
43868
|
+
}
|
|
43869
|
+
actions.addLog(
|
|
43870
|
+
"info",
|
|
43871
|
+
`Found running server (PID: ${running.pid}, World: ${running.world})`
|
|
43872
|
+
);
|
|
43873
|
+
actions.addLog("info", "Attaching to running server...");
|
|
43874
|
+
try {
|
|
43875
|
+
const events = createWatchdogEvents();
|
|
43876
|
+
await attachToServer(running, events);
|
|
43877
|
+
actions.setServerPid(running.pid);
|
|
43878
|
+
actions.setWorld(running.world);
|
|
43879
|
+
actions.setServerStatus("online");
|
|
43880
|
+
actions.setStartupPhase("ready");
|
|
43881
|
+
actions.addLog("info", "Successfully attached to running server");
|
|
43882
|
+
} catch (error2) {
|
|
43883
|
+
actions.addLog("error", `Failed to attach to running server: ${error2}`);
|
|
43884
|
+
}
|
|
43885
|
+
}, [actions, createWatchdogEvents]);
|
|
43886
|
+
useEffect6(() => {
|
|
43887
|
+
if (hasCheckedForRunning.current) return;
|
|
43888
|
+
hasCheckedForRunning.current = true;
|
|
43889
|
+
checkAndAttach().catch((error2) => {
|
|
43890
|
+
actions.addLog("error", `Error checking for running server: ${error2}`);
|
|
43891
|
+
});
|
|
43892
|
+
}, [checkAndAttach, actions]);
|
|
43209
43893
|
const start = useCallback3(async () => {
|
|
43210
43894
|
if (status !== "offline") {
|
|
43211
43895
|
actions.addLog("warn", "Server is not offline, cannot start");
|
|
@@ -43326,6 +44010,18 @@ function useServer() {
|
|
|
43326
44010
|
actions.addLog("error", `Failed to send save command: ${error2}`);
|
|
43327
44011
|
}
|
|
43328
44012
|
}, [rcon, actions]);
|
|
44013
|
+
const detach = useCallback3(async () => {
|
|
44014
|
+
if (!hasActiveServer()) {
|
|
44015
|
+
actions.addLog("warn", "No server to detach from");
|
|
44016
|
+
return;
|
|
44017
|
+
}
|
|
44018
|
+
try {
|
|
44019
|
+
await detachFromServer();
|
|
44020
|
+
actions.addLog("info", "Detached from server - it will continue running");
|
|
44021
|
+
} catch (error2) {
|
|
44022
|
+
actions.addLog("error", `Failed to detach: ${error2}`);
|
|
44023
|
+
}
|
|
44024
|
+
}, [actions]);
|
|
43329
44025
|
useEffect6(() => {
|
|
43330
44026
|
if (status !== "online") return;
|
|
43331
44027
|
const interval = setInterval(() => {
|
|
@@ -43341,10 +44037,14 @@ function useServer() {
|
|
|
43341
44037
|
restart,
|
|
43342
44038
|
update,
|
|
43343
44039
|
forceSave,
|
|
44040
|
+
detach,
|
|
44041
|
+
checkAndAttach,
|
|
43344
44042
|
isOnline: status === "online",
|
|
43345
44043
|
isOffline: status === "offline",
|
|
43346
44044
|
isTransitioning: status === "starting" || status === "stopping",
|
|
43347
44045
|
isUpdating: isUpdating(),
|
|
44046
|
+
isAttached: isAttachedToServer(),
|
|
44047
|
+
logFilePath: getLogFilePath(),
|
|
43348
44048
|
watchdog: getWatchdog()
|
|
43349
44049
|
};
|
|
43350
44050
|
}
|
|
@@ -46006,7 +46706,7 @@ function launchTui() {
|
|
|
46006
46706
|
}
|
|
46007
46707
|
|
|
46008
46708
|
// src/mod.ts
|
|
46009
|
-
var VERSION2 = "1.
|
|
46709
|
+
var VERSION2 = "1.6.1";
|
|
46010
46710
|
var APP_NAME = "Land of OZ - Valheim DSM";
|
|
46011
46711
|
|
|
46012
46712
|
// main.ts
|