Add graceful SCUM stop, restart, and version update flow
SCUM 停止/重启/更新以前只有“结束进程”这一条路,插件没有声明任何优雅关闭方式,
平台也没有把停止后重新启动串起来。现在插件声明自己的关闭脚本,run 先执行它,
平台在停止或更新成功后再自动拉起服务。
run:
- lifecycle stop 支持插件声明的 gracefulStop(可执行文件、参数、环境、超时、
fallback=report|terminate);关闭命令超时且声明 report 时任务失败,不再默默杀进程。
- 新增 steam.update 依赖探针:调用 steamcmd +app_info_print 获取公开分支 buildid,
与本地 steamapps/appmanifest_<appid>.acf 的 buildid 比较,输出
installed/latest/update=yes|no|unknown。
platform:
- 新增 POST /api/v1/server-instances/{id}/restart 与 /update。
- restart 派发插件 stop 动作(走优雅关闭),终态成功后入队 start 作业。
- update 派发插件 install 动作;插件在更新前必须先优雅关闭 SCUM,关闭失败直接拒绝
SteamCMD 更新,成功后平台再拉起服务。
- 依赖检查输入带上插件声明的服务器安装根目录,供 steam.update 读取 appmanifest。
plugin (SCUM server plugin 0.1.16):
- bin/scum-stop.cmd:解析已声明的可执行文件路径,定位同路径正在运行的 SCUMServer.exe,
通过本地 RCON 公告并发送关闭命令,等待进程自行退出;不再使用 taskkill。
- bin/scum-rcon.ps1:插件自有的 Source RCON 客户端,从 UE4SS mod config.ini 读取
密码/端口,密钥不离开本机。
- actions/stop.json 声明 gracefulStop;actions/install.json 更新前先执行同一关闭脚本。
platform_web:
- 服务器详情新增“重启”按钮和“SCUM 版本更新”面板;点“检查更新”查询公开分支版本,
只有检测到更新时“更新版本”按钮才会置为可用并高亮,点击后先确认再派发更新任务。
This commit is contained in:
@@ -161,18 +161,40 @@ describe("plugin manifest validation", () => {
|
||||
const assetPaths = manifest.assetFiles.map((file: { path: string }) => file.path);
|
||||
const installAction = JSON.parse(fs.readFileSync(path.join(pluginDir, manifest.actions.install), "utf8"));
|
||||
const startAction = JSON.parse(fs.readFileSync(path.join(pluginDir, manifest.actions.start), "utf8"));
|
||||
const stopAction = JSON.parse(fs.readFileSync(path.join(pluginDir, manifest.actions.stop), "utf8"));
|
||||
const installScript = fs.readFileSync(path.join(pluginDir, installAction.executableKey), "utf8");
|
||||
const startScript = fs.readFileSync(path.join(pluginDir, startAction.executableKey), "utf8");
|
||||
const stopScript = fs.readFileSync(path.join(pluginDir, stopAction.gracefulStop.executableKey), "utf8");
|
||||
const rconScript = fs.readFileSync(path.join(pluginDir, "bin/scum-rcon.ps1"), "utf8");
|
||||
expect(manifest.runtimeProfiles.serverDeployments).toBeUndefined();
|
||||
expect(JSON.stringify(manifest.runtimeProfiles.installPlans ?? [])).not.toContain("steamcmd-app");
|
||||
expect(assetPaths).toEqual(expect.arrayContaining(["actions/install.json", "actions/start.json", "bin/scum-install-update.cmd", "bin/scum-start.cmd", "assets/map/scum-map-overview.jpg"]));
|
||||
expect(installAction).toMatchObject({ executableKey: "bin/scum-install-update.cmd", environment: { SERVER_STEAM_APP_ID: "3792580", SERVER_STEAMCMD_UPDATE_ARGS: "+login anonymous +app_update 3792580 +quit" } });
|
||||
expect(assetPaths).toEqual(expect.arrayContaining(["actions/install.json", "actions/start.json", "actions/stop.json", "bin/scum-install-update.cmd", "bin/scum-start.cmd", "bin/scum-stop.cmd", "bin/scum-rcon.ps1", "assets/map/scum-map-overview.jpg"]));
|
||||
expect(installAction).toMatchObject({ executableKey: "bin/scum-install-update.cmd", environment: { SERVER_STEAM_APP_ID: "3792580", SERVER_STEAMCMD_UPDATE_ARGS: "+login anonymous +app_update 3792580 validate +quit" } });
|
||||
expect(installAction.timeoutMs).toBe(7200000);
|
||||
expect(startAction).toMatchObject({ executableKey: "bin/scum-start.cmd", targetExecutableKey: "SCUM/Binaries/Win64/SCUMServer.exe", outputMode: "pipes", environment: { SERVER_LOG_FLAG: "-log -stdout -FullStdOutLogOutput" } });
|
||||
expect(installScript).not.toContain("taskkill /IM SCUMServer.exe /F");
|
||||
expect(installScript).toContain("call :stop_matching_scum");
|
||||
expect(installScript).toContain("Get-CimInstance Win32_Process");
|
||||
expect(installScript).toContain("[IO.Path]::GetFullPath($_.ExecutablePath) -ieq $target");
|
||||
expect(installScript).not.toContain("taskkill");
|
||||
expect(installScript).toContain("call :stop_running_scum");
|
||||
expect(installScript).toContain("scum-stop.cmd");
|
||||
expect(installScript).toContain("SCUM server did not shut down gracefully; refusing to update server files.");
|
||||
expect(stopAction).toMatchObject({
|
||||
mode: "control",
|
||||
environment: { SERVER_STOP_SHUTDOWN_COMMAND: "Quit", SERVER_STOP_NOTICE_SECONDS: "15", SERVER_STOP_TIMEOUT_SECONDS: "120" },
|
||||
gracefulStop: { executableKey: "bin/scum-stop.cmd", fallback: "terminate", timeoutMs: 300000 }
|
||||
});
|
||||
expect(stopScript).not.toContain("taskkill");
|
||||
expect(stopScript).toContain("Get-CimInstance Win32_Process");
|
||||
expect(stopScript).toContain("[IO.Path]::GetFullPath($_.ExecutablePath) -ieq $full");
|
||||
expect(stopScript).toContain("scum-rcon.ps1");
|
||||
expect(stopScript).toContain("ue4ss\\Mods\\scum_simple_rcon\\config.ini");
|
||||
expect(stopScript).toContain('call :send_rcon "Announce %SERVER_STOP_ANNOUNCE%"');
|
||||
expect(stopScript).toContain('call :send_rcon "%SERVER_STOP_SHUTDOWN_COMMAND%"');
|
||||
expect(stopScript).toContain("exit /b 2");
|
||||
expect(rconScript).toContain("Read-RconConfig");
|
||||
expect(rconScript).toContain('$config["rcon.password"]');
|
||||
expect(rconScript).toContain('$config["rcon.port"]');
|
||||
expect(rconScript).toContain("Send-RconPacket");
|
||||
expect(rconScript).toContain("Receive-RconPacket");
|
||||
expect(stopScript).toContain('set "SCUM_RCON_PORT_ARG=-Port %SERVER_STOP_RCON_PORT%"');
|
||||
expect(installScript).toContain("SERVER_ROOT_WINDOWS=%SERVER_ROOT:/=\\%");
|
||||
expect(installScript).toContain("SERVER_INSTALL_DIR=%SERVER_ROOT_WINDOWS%");
|
||||
expect(installScript).toContain("SERVER_STEAMCMD_DIR=%SERVER_ROOT_DRIVE%\\steamcmd");
|
||||
@@ -195,6 +217,16 @@ describe("plugin manifest validation", () => {
|
||||
expect(startScript).toContain("-port=%SERVER_CREATE_GAMEPORT% -QueryPort=%SERVER_CREATE_QUERYPORT% -MaxPlayers=%SERVER_CREATE_MAXPLAYERS% %SERVER_LOG_FLAG%");
|
||||
});
|
||||
|
||||
it("declares a plugin-owned SCUM build probe that the update button can check", () => {
|
||||
const pluginDir = path.join(pluginsRoot, "examples/scum-server-plugin");
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(pluginDir, "manifest.json"), "utf8")) as {
|
||||
runtimeProfiles: { dependencyProbes: Array<{ key: string; kind: string; targetKey: string; required?: boolean; steamAppId?: string; platforms?: string[] }> };
|
||||
};
|
||||
const probe = manifest.runtimeProfiles.dependencyProbes.find((candidate) => candidate.key === "scum-server-build");
|
||||
expect(probe).toMatchObject({ kind: "steam.update", targetKey: "steamcmd", steamAppId: "3792580", required: false, platforms: ["windows"] });
|
||||
expect(manifest.runtimeProfiles.dependencyProbes.filter((candidate) => candidate.kind === "steam.update")).toEqual([probe]);
|
||||
});
|
||||
|
||||
it("rejects lifecycle executable assets missing from the manifest seed declaration", () => {
|
||||
const errors = validateTemporaryScumManifest((manifest) => {
|
||||
manifest.assetFiles = manifest.assetFiles.filter((file: { path: string }) => file.path !== "bin/scum-start.cmd");
|
||||
@@ -202,6 +234,18 @@ describe("plugin manifest validation", () => {
|
||||
expect(errors.some((error) => error.includes("lifecycleAction.start.executableKey") && error.includes("manifest.assetFiles"))).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects graceful stop scripts missing from the manifest seed declaration", () => {
|
||||
const missingAsset = validateTemporaryScumManifest((manifest) => {
|
||||
manifest.assetFiles = manifest.assetFiles.filter((file: { path: string }) => file.path !== "bin/scum-stop.cmd");
|
||||
});
|
||||
expect(missingAsset.some((error) => error.includes("lifecycleAction.stop.gracefulStop.executableKey") && error.includes("manifest.assetFiles"))).toBe(true);
|
||||
|
||||
const wrongAction = validateTemporaryScumManifest((manifest) => {
|
||||
manifest.assetFiles = manifest.assetFiles.filter((file: { path: string }) => file.path !== "actions/install.json");
|
||||
});
|
||||
expect(wrongAction.some((error) => error.includes("lifecycleAction.install: action file must be declared"))).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects unsupported or unsafe inline create-field declarations", () => {
|
||||
const malformed = validateTemporaryScumManifest((manifest) => {
|
||||
manifest.server.createFields[0].type = "path";
|
||||
@@ -269,7 +313,7 @@ describe("plugin manifest validation", () => {
|
||||
const serialized = JSON.stringify(manifest).toLowerCase();
|
||||
|
||||
expect(serialized).not.toContain("local-proof");
|
||||
expect(manifest.version).toBe("0.1.15");
|
||||
expect(manifest.version).toBe("0.1.16");
|
||||
expect(installAction.environment?.SERVER_TEMPLATE).toBe("scum-server");
|
||||
expect(manifest.permissions).toEqual(expect.arrayContaining(["server.game-client.read", "server.game-client.command", "server.game-client.maintenance"]));
|
||||
expect(manifest.gameClientBridge.commands.map((command) => command.type)).toEqual(expect.arrayContaining([
|
||||
|
||||
Reference in New Issue
Block a user