valheim-oz-dsm 1.5.4 → 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 +38 -0
- package/dist/main.js +679 -58
- package/dist/main.js.map +1 -1
- package/package.json +2 -5
- package/patches/@caleb-collar+steamcmd+1.1.0.patch +0 -13
package/dist/main.js
CHANGED
|
@@ -2644,6 +2644,25 @@ import * as fs5 from "fs/promises";
|
|
|
2644
2644
|
import { dirname, join } from "path";
|
|
2645
2645
|
|
|
2646
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
|
+
}
|
|
2647
2666
|
function parseEvent(line) {
|
|
2648
2667
|
if (line.includes("Got character ZDOID from")) {
|
|
2649
2668
|
const match = line.match(/Got character ZDOID from (\S+)/);
|
|
@@ -2690,9 +2709,179 @@ function parseEvent(line) {
|
|
|
2690
2709
|
return null;
|
|
2691
2710
|
}
|
|
2692
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
|
+
|
|
2693
2874
|
// src/server/pidfile.ts
|
|
2694
2875
|
import fs6 from "fs/promises";
|
|
2695
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
|
+
}
|
|
2696
2885
|
function getPidFilePath() {
|
|
2697
2886
|
return path5.join(getConfigDir(), "oz-valheim", "server.pid");
|
|
2698
2887
|
}
|
|
@@ -2746,9 +2935,28 @@ async function getRunningServer() {
|
|
|
2746
2935
|
}
|
|
2747
2936
|
return data;
|
|
2748
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
|
+
}
|
|
2749
2956
|
|
|
2750
2957
|
// src/server/process.ts
|
|
2751
2958
|
import { spawn } from "child_process";
|
|
2959
|
+
import { createWriteStream } from "fs";
|
|
2752
2960
|
var defaultEvents = {
|
|
2753
2961
|
onStateChange: () => {
|
|
2754
2962
|
},
|
|
@@ -2767,6 +2975,12 @@ var ValheimProcess = class {
|
|
|
2767
2975
|
events;
|
|
2768
2976
|
config;
|
|
2769
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;
|
|
2770
2984
|
/**
|
|
2771
2985
|
* Creates a new Valheim process wrapper
|
|
2772
2986
|
* @param config Server launch configuration
|
|
@@ -2778,7 +2992,7 @@ var ValheimProcess = class {
|
|
|
2778
2992
|
}
|
|
2779
2993
|
/** Gets the process ID if running, null otherwise */
|
|
2780
2994
|
get pid() {
|
|
2781
|
-
return this.process?.pid ?? null;
|
|
2995
|
+
return this.process?.pid ?? this._detachedPid ?? null;
|
|
2782
2996
|
}
|
|
2783
2997
|
/** Gets the current process state */
|
|
2784
2998
|
get currentState() {
|
|
@@ -2788,6 +3002,14 @@ var ValheimProcess = class {
|
|
|
2788
3002
|
get uptime() {
|
|
2789
3003
|
return this.startTime;
|
|
2790
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
|
+
}
|
|
2791
3013
|
/**
|
|
2792
3014
|
* Updates the process state and notifies listeners
|
|
2793
3015
|
* @param newState New process state
|
|
@@ -2806,21 +3028,16 @@ var ValheimProcess = class {
|
|
|
2806
3028
|
}
|
|
2807
3029
|
this.setState("starting");
|
|
2808
3030
|
this.startTime = /* @__PURE__ */ new Date();
|
|
3031
|
+
this._isDetached = this.config.detached ?? false;
|
|
2809
3032
|
const execPath = getValheimExecutablePath();
|
|
2810
3033
|
const args = this.buildArgs();
|
|
2811
3034
|
const env = this.getEnvironment();
|
|
2812
3035
|
try {
|
|
2813
|
-
this.
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
this.process.on("error", (error2) => {
|
|
2819
|
-
this.setState("crashed");
|
|
2820
|
-
this.startTime = null;
|
|
2821
|
-
this.events.onError(error2);
|
|
2822
|
-
});
|
|
2823
|
-
await Promise.resolve();
|
|
3036
|
+
if (this._isDetached) {
|
|
3037
|
+
await this.startDetached(execPath, args, env);
|
|
3038
|
+
} else {
|
|
3039
|
+
await this.startAttached(execPath, args, env);
|
|
3040
|
+
}
|
|
2824
3041
|
} catch (error2) {
|
|
2825
3042
|
this.setState("crashed");
|
|
2826
3043
|
this.startTime = null;
|
|
@@ -2828,6 +3045,169 @@ var ValheimProcess = class {
|
|
|
2828
3045
|
throw error2;
|
|
2829
3046
|
}
|
|
2830
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
|
+
}
|
|
2831
3211
|
/**
|
|
2832
3212
|
* Gracefully stops the server with optional timeout
|
|
2833
3213
|
* @param timeout Maximum time to wait for graceful shutdown (ms)
|
|
@@ -2837,6 +3217,31 @@ var ValheimProcess = class {
|
|
|
2837
3217
|
return;
|
|
2838
3218
|
}
|
|
2839
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
|
+
}
|
|
2840
3245
|
if (this.process) {
|
|
2841
3246
|
try {
|
|
2842
3247
|
this.process.kill("SIGTERM");
|
|
@@ -2864,6 +3269,18 @@ var ValheimProcess = class {
|
|
|
2864
3269
|
* Immediately kills the server process
|
|
2865
3270
|
*/
|
|
2866
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
|
+
}
|
|
2867
3284
|
if (this.process) {
|
|
2868
3285
|
try {
|
|
2869
3286
|
this.process.kill("SIGKILL");
|
|
@@ -2875,6 +3292,36 @@ var ValheimProcess = class {
|
|
|
2875
3292
|
this.setState("offline");
|
|
2876
3293
|
await Promise.resolve();
|
|
2877
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
|
+
}
|
|
2878
3325
|
/**
|
|
2879
3326
|
* Builds command line arguments for Valheim server
|
|
2880
3327
|
* @returns Array of command line arguments
|
|
@@ -3190,6 +3637,18 @@ async function startCommand(args, config) {
|
|
|
3190
3637
|
console.log("Run 'valheim-dsm install' first to install the server.");
|
|
3191
3638
|
process.exit(1);
|
|
3192
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();
|
|
3193
3652
|
const serverConfig = {
|
|
3194
3653
|
name: args.name ?? config.server.name,
|
|
3195
3654
|
port: args.port ?? config.server.port,
|
|
@@ -3199,7 +3658,9 @@ async function startCommand(args, config) {
|
|
|
3199
3658
|
crossplay: args.crossplay ?? config.server.crossplay,
|
|
3200
3659
|
savedir: args.savedir ?? config.server.savedir,
|
|
3201
3660
|
saveinterval: config.server.saveinterval,
|
|
3202
|
-
backups: config.server.backups
|
|
3661
|
+
backups: config.server.backups,
|
|
3662
|
+
// Always use detached mode for stability
|
|
3663
|
+
detached: true
|
|
3203
3664
|
};
|
|
3204
3665
|
console.log(`
|
|
3205
3666
|
Starting ${serverConfig.name}...`);
|
|
@@ -3207,6 +3668,7 @@ Starting ${serverConfig.name}...`);
|
|
|
3207
3668
|
console.log(` Port: ${serverConfig.port}`);
|
|
3208
3669
|
console.log(` Public: ${serverConfig.public}`);
|
|
3209
3670
|
console.log(` Crossplay: ${serverConfig.crossplay}`);
|
|
3671
|
+
console.log(` Mode: Detached (server continues after terminal exits)`);
|
|
3210
3672
|
console.log("");
|
|
3211
3673
|
activeWatchdog = new Watchdog(
|
|
3212
3674
|
serverConfig,
|
|
@@ -3220,6 +3682,12 @@ Starting ${serverConfig.name}...`);
|
|
|
3220
3682
|
{
|
|
3221
3683
|
onStateChange: (state) => {
|
|
3222
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
|
+
}
|
|
3223
3691
|
},
|
|
3224
3692
|
onLog: (line) => {
|
|
3225
3693
|
console.log(`[Server] ${line}`);
|
|
@@ -3248,18 +3716,34 @@ Starting ${serverConfig.name}...`);
|
|
|
3248
3716
|
setupShutdownHandlers();
|
|
3249
3717
|
try {
|
|
3250
3718
|
await activeWatchdog.start();
|
|
3251
|
-
const
|
|
3252
|
-
if (
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3256
|
-
world: serverConfig.world,
|
|
3257
|
-
port: serverConfig.port
|
|
3258
|
-
});
|
|
3719
|
+
const logPath = activeWatchdog.serverProcess.logFilePath;
|
|
3720
|
+
if (logPath) {
|
|
3721
|
+
console.log(`
|
|
3722
|
+
Server log: ${logPath}`);
|
|
3259
3723
|
}
|
|
3260
|
-
console.log("\nServer
|
|
3261
|
-
|
|
3262
|
-
|
|
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);
|
|
3263
3747
|
} catch (error2) {
|
|
3264
3748
|
console.error(`
|
|
3265
3749
|
Failed to start server: ${error2.message}`);
|
|
@@ -3272,14 +3756,26 @@ function getActiveWatchdog() {
|
|
|
3272
3756
|
function clearActiveWatchdog() {
|
|
3273
3757
|
activeWatchdog = null;
|
|
3274
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
|
+
}
|
|
3275
3769
|
function setupShutdownHandlers() {
|
|
3276
3770
|
const shutdown = async () => {
|
|
3277
|
-
console.log("\n\
|
|
3771
|
+
console.log("\n\nDetaching from server (it will keep running)...");
|
|
3278
3772
|
if (activeWatchdog) {
|
|
3279
|
-
|
|
3773
|
+
try {
|
|
3774
|
+
await activeWatchdog.serverProcess.detach();
|
|
3775
|
+
} catch {
|
|
3776
|
+
}
|
|
3280
3777
|
activeWatchdog = null;
|
|
3281
3778
|
}
|
|
3282
|
-
await removePidFile();
|
|
3283
3779
|
process.exit(0);
|
|
3284
3780
|
};
|
|
3285
3781
|
process.on("SIGINT", shutdown);
|
|
@@ -3290,9 +3786,9 @@ function setupShutdownHandlers() {
|
|
|
3290
3786
|
|
|
3291
3787
|
// src/cli/commands/stop.ts
|
|
3292
3788
|
async function stopCommand(args) {
|
|
3789
|
+
const timeout = args.timeout ?? 3e4;
|
|
3293
3790
|
const watchdog2 = getActiveWatchdog();
|
|
3294
3791
|
if (watchdog2) {
|
|
3295
|
-
const timeout = args.timeout ?? 3e4;
|
|
3296
3792
|
if (args.force) {
|
|
3297
3793
|
console.log("\nForce stopping server...");
|
|
3298
3794
|
await watchdog2.kill();
|
|
@@ -3312,34 +3808,53 @@ Stopping server (timeout: ${timeout}ms)...`);
|
|
|
3312
3808
|
console.log("\nNote: Run 'valheim-dsm start' to start a server.");
|
|
3313
3809
|
return;
|
|
3314
3810
|
}
|
|
3315
|
-
const { pid, world, port, startedAt } = runningServer;
|
|
3811
|
+
const { pid, world, port, startedAt, detached, logFile } = runningServer;
|
|
3316
3812
|
console.log(`
|
|
3317
3813
|
Found running server:`);
|
|
3318
3814
|
console.log(` PID: ${pid}`);
|
|
3319
3815
|
console.log(` World: ${world}`);
|
|
3320
3816
|
console.log(` Port: ${port}`);
|
|
3321
3817
|
console.log(` Started: ${new Date(startedAt).toLocaleString()}`);
|
|
3818
|
+
console.log(` Mode: ${detached ? "Detached" : "Attached"}`);
|
|
3819
|
+
if (logFile) {
|
|
3820
|
+
console.log(` Log: ${logFile}`);
|
|
3821
|
+
}
|
|
3322
3822
|
if (!isProcessRunning(pid)) {
|
|
3323
3823
|
console.log("\nServer process is no longer running. Cleaning up...");
|
|
3324
3824
|
await removePidFile();
|
|
3325
3825
|
return;
|
|
3326
3826
|
}
|
|
3827
|
+
const platform = getPlatform();
|
|
3327
3828
|
if (args.force) {
|
|
3328
3829
|
console.log("\nForce killing server...");
|
|
3329
|
-
killProcess(pid,
|
|
3830
|
+
killProcess(pid, platform !== "windows");
|
|
3330
3831
|
} else {
|
|
3331
3832
|
console.log("\nSending stop signal...");
|
|
3332
3833
|
killProcess(pid, false);
|
|
3333
|
-
const timeout = args.timeout ?? 3e4;
|
|
3334
3834
|
const startTime = Date.now();
|
|
3835
|
+
let dots = 0;
|
|
3335
3836
|
while (isProcessRunning(pid) && Date.now() - startTime < timeout) {
|
|
3336
3837
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3337
3838
|
process.stdout.write(".");
|
|
3839
|
+
dots++;
|
|
3338
3840
|
}
|
|
3339
|
-
console.log();
|
|
3841
|
+
if (dots > 0) console.log();
|
|
3340
3842
|
if (isProcessRunning(pid)) {
|
|
3341
3843
|
console.log("Server did not stop gracefully, force killing...");
|
|
3342
|
-
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
|
+
}
|
|
3343
3858
|
}
|
|
3344
3859
|
}
|
|
3345
3860
|
await removePidFile();
|
|
@@ -3868,7 +4383,7 @@ import { useEffect as useEffect3, useMemo as useMemo2, useRef as useRef2, useSta
|
|
|
3868
4383
|
// package.json
|
|
3869
4384
|
var package_default = {
|
|
3870
4385
|
name: "valheim-oz-dsm",
|
|
3871
|
-
version: "1.
|
|
4386
|
+
version: "1.6.1",
|
|
3872
4387
|
description: "Land of OZ - Valheim Dedicated Server Manager",
|
|
3873
4388
|
type: "module",
|
|
3874
4389
|
bin: {
|
|
@@ -3877,7 +4392,6 @@ var package_default = {
|
|
|
3877
4392
|
main: "./dist/main.js",
|
|
3878
4393
|
files: [
|
|
3879
4394
|
"dist",
|
|
3880
|
-
"patches",
|
|
3881
4395
|
"README.md",
|
|
3882
4396
|
"LICENSE",
|
|
3883
4397
|
"CHANGELOG.md"
|
|
@@ -3898,12 +4412,11 @@ var package_default = {
|
|
|
3898
4412
|
"lint:fix": "biome check --write .",
|
|
3899
4413
|
format: "biome format --write .",
|
|
3900
4414
|
typecheck: "tsc --noEmit",
|
|
3901
|
-
postinstall: "patch-package",
|
|
3902
4415
|
prepare: "tsx scripts/install-hooks.ts",
|
|
3903
4416
|
prepublishOnly: "npm run typecheck && npm run lint && npm test && npm run build"
|
|
3904
4417
|
},
|
|
3905
4418
|
dependencies: {
|
|
3906
|
-
"@caleb-collar/steamcmd": "^1.1.
|
|
4419
|
+
"@caleb-collar/steamcmd": "^1.1.1",
|
|
3907
4420
|
conf: "^13.0.1",
|
|
3908
4421
|
"fullscreen-ink": "^0.1.0",
|
|
3909
4422
|
ink: "^6.6.0",
|
|
@@ -3916,7 +4429,6 @@ var package_default = {
|
|
|
3916
4429
|
"@types/node": "^22.13.1",
|
|
3917
4430
|
"@types/react": "^19.2.10",
|
|
3918
4431
|
"@vitest/coverage-v8": "^3.2.4",
|
|
3919
|
-
"patch-package": "^8.0.1",
|
|
3920
4432
|
tsup: "^8.3.6",
|
|
3921
4433
|
tsx: "^4.19.2",
|
|
3922
4434
|
typescript: "^5.7.3",
|
|
@@ -43116,46 +43628,81 @@ var Spinner = (props) => {
|
|
|
43116
43628
|
};
|
|
43117
43629
|
|
|
43118
43630
|
// src/tui/hooks/useServer.ts
|
|
43119
|
-
import { useCallback as useCallback3, useEffect as useEffect6 } from "react";
|
|
43631
|
+
import { useCallback as useCallback3, useEffect as useEffect6, useRef as useRef3 } from "react";
|
|
43120
43632
|
|
|
43121
43633
|
// src/tui/serverManager.ts
|
|
43122
43634
|
var watchdog = null;
|
|
43123
43635
|
var updating = false;
|
|
43636
|
+
var isAttached = false;
|
|
43124
43637
|
function getWatchdog() {
|
|
43125
43638
|
return watchdog;
|
|
43126
43639
|
}
|
|
43127
43640
|
function hasActiveServer() {
|
|
43128
43641
|
return watchdog !== null;
|
|
43129
43642
|
}
|
|
43643
|
+
function isAttachedToServer() {
|
|
43644
|
+
return isAttached;
|
|
43645
|
+
}
|
|
43130
43646
|
function isUpdating() {
|
|
43131
43647
|
return updating;
|
|
43132
43648
|
}
|
|
43133
43649
|
function setUpdating(value) {
|
|
43134
43650
|
updating = value;
|
|
43135
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
|
+
}
|
|
43136
43674
|
async function startServer(config, watchdogConfig, events) {
|
|
43137
43675
|
if (watchdog) {
|
|
43138
43676
|
throw new Error("Server is already running");
|
|
43139
43677
|
}
|
|
43140
|
-
|
|
43141
|
-
|
|
43142
|
-
|
|
43143
|
-
|
|
43144
|
-
|
|
43145
|
-
pid,
|
|
43146
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
43147
|
-
world: config.world,
|
|
43148
|
-
port: config.port
|
|
43149
|
-
});
|
|
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
|
+
);
|
|
43150
43683
|
}
|
|
43684
|
+
const detachedConfig = {
|
|
43685
|
+
...config,
|
|
43686
|
+
detached: true
|
|
43687
|
+
};
|
|
43688
|
+
watchdog = new Watchdog(detachedConfig, watchdogConfig, events);
|
|
43689
|
+
await watchdog.start();
|
|
43690
|
+
isAttached = false;
|
|
43151
43691
|
return watchdog;
|
|
43152
43692
|
}
|
|
43153
|
-
async function stopServer() {
|
|
43693
|
+
async function stopServer(keepRunning = false) {
|
|
43154
43694
|
if (!watchdog) {
|
|
43155
43695
|
return;
|
|
43156
43696
|
}
|
|
43697
|
+
if (keepRunning && isAttached) {
|
|
43698
|
+
await watchdog.serverProcess.detach();
|
|
43699
|
+
watchdog = null;
|
|
43700
|
+
isAttached = false;
|
|
43701
|
+
return;
|
|
43702
|
+
}
|
|
43157
43703
|
await watchdog.stop();
|
|
43158
43704
|
watchdog = null;
|
|
43705
|
+
isAttached = false;
|
|
43159
43706
|
await removePidFile();
|
|
43160
43707
|
}
|
|
43161
43708
|
async function killServer() {
|
|
@@ -43164,21 +43711,46 @@ async function killServer() {
|
|
|
43164
43711
|
}
|
|
43165
43712
|
await watchdog.kill();
|
|
43166
43713
|
watchdog = null;
|
|
43714
|
+
isAttached = false;
|
|
43167
43715
|
await removePidFile();
|
|
43168
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
|
+
}
|
|
43169
43731
|
async function cleanupOnExit() {
|
|
43170
|
-
if (watchdog) {
|
|
43732
|
+
if (!watchdog) {
|
|
43733
|
+
return;
|
|
43734
|
+
}
|
|
43735
|
+
if (isAttached || watchdog.serverProcess.isDetached) {
|
|
43171
43736
|
try {
|
|
43172
|
-
await watchdog.
|
|
43737
|
+
await watchdog.serverProcess.detach();
|
|
43173
43738
|
} catch {
|
|
43174
|
-
try {
|
|
43175
|
-
await watchdog.kill();
|
|
43176
|
-
} catch {
|
|
43177
|
-
}
|
|
43178
43739
|
}
|
|
43179
43740
|
watchdog = null;
|
|
43180
|
-
|
|
43741
|
+
isAttached = false;
|
|
43742
|
+
return;
|
|
43181
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();
|
|
43182
43754
|
}
|
|
43183
43755
|
|
|
43184
43756
|
// src/tui/hooks/useServer.ts
|
|
@@ -43214,6 +43786,7 @@ function useServer() {
|
|
|
43214
43786
|
const config = useStore((s) => s.config);
|
|
43215
43787
|
const rcon = useStore((s) => s.rcon);
|
|
43216
43788
|
const actions = useStore((s) => s.actions);
|
|
43789
|
+
const hasCheckedForRunning = useRef3(false);
|
|
43217
43790
|
const createWatchdogEvents = useCallback3(
|
|
43218
43791
|
() => ({
|
|
43219
43792
|
onStateChange: (state) => {
|
|
@@ -43285,6 +43858,38 @@ function useServer() {
|
|
|
43285
43858
|
}),
|
|
43286
43859
|
[actions]
|
|
43287
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]);
|
|
43288
43893
|
const start = useCallback3(async () => {
|
|
43289
43894
|
if (status !== "offline") {
|
|
43290
43895
|
actions.addLog("warn", "Server is not offline, cannot start");
|
|
@@ -43405,6 +44010,18 @@ function useServer() {
|
|
|
43405
44010
|
actions.addLog("error", `Failed to send save command: ${error2}`);
|
|
43406
44011
|
}
|
|
43407
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]);
|
|
43408
44025
|
useEffect6(() => {
|
|
43409
44026
|
if (status !== "online") return;
|
|
43410
44027
|
const interval = setInterval(() => {
|
|
@@ -43420,10 +44037,14 @@ function useServer() {
|
|
|
43420
44037
|
restart,
|
|
43421
44038
|
update,
|
|
43422
44039
|
forceSave,
|
|
44040
|
+
detach,
|
|
44041
|
+
checkAndAttach,
|
|
43423
44042
|
isOnline: status === "online",
|
|
43424
44043
|
isOffline: status === "offline",
|
|
43425
44044
|
isTransitioning: status === "starting" || status === "stopping",
|
|
43426
44045
|
isUpdating: isUpdating(),
|
|
44046
|
+
isAttached: isAttachedToServer(),
|
|
44047
|
+
logFilePath: getLogFilePath(),
|
|
43427
44048
|
watchdog: getWatchdog()
|
|
43428
44049
|
};
|
|
43429
44050
|
}
|
|
@@ -46085,7 +46706,7 @@ function launchTui() {
|
|
|
46085
46706
|
}
|
|
46086
46707
|
|
|
46087
46708
|
// src/mod.ts
|
|
46088
|
-
var VERSION2 = "1.
|
|
46709
|
+
var VERSION2 = "1.6.1";
|
|
46089
46710
|
var APP_NAME = "Land of OZ - Valheim DSM";
|
|
46090
46711
|
|
|
46091
46712
|
// main.ts
|