NOTE: Moves the jail folder where the mods folder used to be. The Jail folder is then recreated after the next restart.
452 lines
16 KiB
Dart
452 lines
16 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:libac_dart/packets/packets.dart';
|
|
import 'package:libac_dart/utils/IOTools.dart';
|
|
import 'package:libac_dart/utils/TimeUtils.dart';
|
|
import 'package:servermanager/game.dart';
|
|
import 'package:servermanager/structs/SessionData.dart';
|
|
import 'package:servermanager/structs/discordHookHelper.dart';
|
|
import 'package:servermanager/structs/mod.dart';
|
|
import 'package:servermanager/structs/settings.dart';
|
|
import 'package:servermanager/wine.dart';
|
|
|
|
enum States {
|
|
Idle, // For when the state machine is waiting for a state change
|
|
PreStart, // Backup task
|
|
Starting, // For when the server is starting up
|
|
ModUpdateCheck, // For when the mods are being checked in the jail folder against the live mods
|
|
WarnPlayersNonIntrusive, // Gives a non-instrusive warning about the upcoming restart
|
|
WarnPlayersIntrusive, // Sends a message intrusively about the upcoming restart
|
|
FullStop, // Intends to set the inactive state, and immediately stops the server
|
|
|
|
Inactive // The inactive state will be used for when the state machine is not supposed to be doing anything.
|
|
}
|
|
|
|
enum WarnType { Intrusive, NonIntrusive }
|
|
|
|
enum WarnIntervals {
|
|
FIVE_SECONDS(seconds: 5, type: WarnType.Intrusive, warning: "Tiiiiiimber!"),
|
|
TEN_SECONDS(
|
|
seconds: 10,
|
|
type: WarnType.Intrusive,
|
|
warning: "The server has 10 seconds until restart"),
|
|
TWENTY_SECONDS(
|
|
seconds: 20,
|
|
type: WarnType.NonIntrusive,
|
|
warning: "The server is about to go down in 20 seconds"),
|
|
THIRTY_SECONDS(
|
|
seconds: 30,
|
|
type: WarnType.NonIntrusive,
|
|
warning: "The server will restart in 30 seconds"),
|
|
ONE_MINUTE(
|
|
seconds: 60,
|
|
type: WarnType.Intrusive,
|
|
warning: "The server will restart in 1 minute"),
|
|
FIVE_MIN(
|
|
seconds: (5 * 60),
|
|
type: WarnType.NonIntrusive,
|
|
warning: "The server will restart in 5 minutes"),
|
|
TEN_MIN(
|
|
seconds: (10 * 60),
|
|
type: WarnType.Intrusive,
|
|
warning: "The server will restart in 10 minutes"),
|
|
ONE_HOUR(
|
|
seconds: (1 * 60 * 60),
|
|
type: WarnType.Intrusive,
|
|
warning: "The server will restart in 1 hour"),
|
|
TWO_HOURS(
|
|
seconds: (2 * 60 * 60),
|
|
type: WarnType.NonIntrusive,
|
|
warning: "The server will restart in 2 hours"),
|
|
ELEVEN_HOURS(
|
|
seconds: (11 * 60 * 60),
|
|
type: WarnType.NonIntrusive,
|
|
warning: "The server will restart in 11 hours"),
|
|
NONE(
|
|
seconds: 2147483647,
|
|
type: WarnType.NonIntrusive,
|
|
warning:
|
|
""); // int32 max value should never be possible, is more seconds than in a single day.
|
|
|
|
final int seconds;
|
|
final WarnType type;
|
|
final String warning;
|
|
|
|
const WarnIntervals(
|
|
{required this.seconds, required this.type, required this.warning});
|
|
}
|
|
|
|
class StateMachine {
|
|
static Process? PROC;
|
|
static Completer<void> DeadProcKillswitch = Completer();
|
|
|
|
void resetKillswitch() {
|
|
DeadProcKillswitch = Completer();
|
|
}
|
|
|
|
static Future<void> monitorProcess() async {
|
|
Settings settings = Settings();
|
|
// Ping RCON. If we can connect, the server is alive.
|
|
// Only start pinging once a minute, after the first 10 minutes.
|
|
if (SessionData.operating_time.getTotalSeconds() >
|
|
Time(minutes: 10, hours: 0, seconds: 0).getTotalSeconds() &&
|
|
!SessionData.canPingServer) {
|
|
SessionData.canPingServer = true;
|
|
}
|
|
|
|
if (SessionData.canPingServer) {
|
|
SessionData.timeSinceLastPing.tickUp();
|
|
if (SessionData.timeSinceLastPing.getTotalSeconds() > 60) {
|
|
SessionData.timeSinceLastPing.apply(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
var _currentState = States.Inactive;
|
|
StreamController<States> _stateController = StreamController.broadcast();
|
|
Stream<States> get stateChanges => _stateController.stream;
|
|
|
|
States get currentState => _currentState;
|
|
|
|
void changeState(States n) {
|
|
_currentState = n;
|
|
_stateController.add(n);
|
|
}
|
|
|
|
Future<void> runTask() async {
|
|
if (currentState == States.Idle) {
|
|
return; // Nothing to do here
|
|
} else if (currentState == States.FullStop) {
|
|
Settings settings = Settings();
|
|
print("Sending shutdown command to server");
|
|
await settings.sendRconCommand("shutdown");
|
|
|
|
Timer.periodic(Duration(seconds: 30), (timer) {
|
|
timer.cancel();
|
|
|
|
print("Sending killswitch to server");
|
|
|
|
PROC!.kill(ProcessSignal.sigkill);
|
|
PacketServer.socket!.close();
|
|
});
|
|
|
|
DiscordHookHelper.sendWebHook(
|
|
settings.inst!.discord,
|
|
DiscordHookProps.OFFLINE_ALERT,
|
|
"Server is now offline",
|
|
"The server is shutting down");
|
|
|
|
changeState(States.Inactive);
|
|
} else if (currentState == States.Starting) {
|
|
// Server startup in progress
|
|
Settings settings = Settings();
|
|
//await settings.RunUpdate(valid: false);
|
|
//await doDownloadMods(false);
|
|
|
|
//settings.inst!.mods = await doScanMods(false);
|
|
settings.Write();
|
|
|
|
await settings.writeOutModListFile();
|
|
|
|
var conanArgs = [
|
|
"-RconEnabled=1",
|
|
"-RconPassword=${settings.inst!.serverSettings.RconPassword}",
|
|
"-RconPort=${settings.inst!.serverSettings.RconPort}",
|
|
"-Port=${settings.inst!.serverSettings.GamePort}",
|
|
"-QueryPort=${settings.inst!.serverSettings.QueryPort}",
|
|
"-log",
|
|
"-console"
|
|
];
|
|
// Start the server now
|
|
String executable = PathHelper.builder(settings.getServerPath())
|
|
.resolve("ConanSandbox")
|
|
.resolve("Binaries")
|
|
.resolve("Win64")
|
|
.resolve("ConanSandboxServer-Win64-Shipping.exe")
|
|
.build();
|
|
|
|
if (Platform.isWindows) {
|
|
runWindows(executable, conanArgs);
|
|
} else {
|
|
runDetachedWine(executable, conanArgs, settings.getServerPath());
|
|
}
|
|
|
|
Timer.periodic(Duration(seconds: 20), (timer) async {
|
|
File logFile = File(PathHelper.builder(settings.getServerPath())
|
|
.resolve("ConanSandbox")
|
|
.resolve("Saved")
|
|
.resolve("Logs")
|
|
.resolve("ConanSandbox.log")
|
|
.build());
|
|
await logFile.create(recursive: true);
|
|
|
|
tailAndPrint(logFile);
|
|
|
|
timer.cancel();
|
|
});
|
|
|
|
changeState(States.Idle);
|
|
} else if (currentState == States.PreStart) {
|
|
// Perform Backup Task
|
|
|
|
// Finally, allow the server to start up
|
|
changeState(States.Starting);
|
|
}
|
|
}
|
|
|
|
StateMachine() {
|
|
stateChanges.listen((event) async {
|
|
await runTask();
|
|
});
|
|
}
|
|
|
|
Timer? task;
|
|
|
|
Future<void> pollModUpdates() async {
|
|
await doDownloadMods(false);
|
|
await doScanMods(false);
|
|
}
|
|
|
|
/// You should only start this task once the server is ready to be started.
|
|
/// This task will start the server on first-start
|
|
///
|
|
/// It will monitor the states for shutdown, it will also monitor the process handle
|
|
///
|
|
/// This is what runs the automatic restart timers as well. All state change logic that should happen automatically is contained in here.
|
|
Future<void> startScheduler() async {
|
|
Settings settings = Settings();
|
|
SessionData.timer = settings.inst!.timer.time.copy();
|
|
changeState(States.PreStart);
|
|
|
|
DiscordHookHelper.sendWebHook(
|
|
settings.inst!.discord,
|
|
DiscordHookProps.ONLINE_ALERT,
|
|
"Server is now starting up",
|
|
"The server is starting up now, it should appear on the server list in a few minutes");
|
|
|
|
resetKillswitch();
|
|
SessionData.enableRestartTimer = settings.inst!.timer.enabled;
|
|
|
|
// Schedule the server task
|
|
task = Timer.periodic(Duration(seconds: 1), (timer) async {
|
|
switch (currentState) {
|
|
case States.Inactive:
|
|
{
|
|
timer.cancel();
|
|
|
|
// Check if we should perform a world restore
|
|
if (SessionData.isWorldRestore) {
|
|
await DiscordHookHelper.sendWebHook(
|
|
settings.inst!.discord,
|
|
DiscordHookProps.OFFLINE_ALERT,
|
|
"RESTORE",
|
|
"Restoring backup file: ${SessionData.snapshotToRestore}");
|
|
|
|
// Now restore the backup file
|
|
File backup = File(SessionData.snapshotToRestore);
|
|
await backup.copy(settings.getWorldGameDB());
|
|
}
|
|
|
|
//Settings settings = Settings();
|
|
if (settings.inst!.pterodactylMode) {
|
|
// Shut down the server processes now
|
|
PacketServer.socket!.close();
|
|
|
|
// Check if the shutdown reason was for a Mod Update
|
|
if (SessionData.shutDownReason == ShutDownReason.MODS) {
|
|
// Delete the old Mods Folder
|
|
Directory mods = Directory(settings.getModPath());
|
|
await mods.delete(recursive: true);
|
|
Directory jail = Directory(settings.getModJailPath());
|
|
await jail.rename(settings.getModPath());
|
|
|
|
File modsTxt = settings.getModListFile();
|
|
await modsTxt.delete();
|
|
}
|
|
|
|
exit(0);
|
|
} else {
|
|
resetKillswitch();
|
|
SessionData.timer = settings.inst!.timer.time.copy();
|
|
|
|
// Check if the shutdown reason was for a Mod Update
|
|
if (SessionData.shutDownReason == ShutDownReason.MODS) {
|
|
// Delete the old Mods Folder
|
|
Directory mods = Directory(settings.getModPath());
|
|
await mods.delete(recursive: true);
|
|
Directory jail = Directory(settings.getModJailPath());
|
|
await jail.rename(settings.getModPath());
|
|
|
|
File modsTxt = settings.getModListFile();
|
|
await modsTxt.delete();
|
|
}
|
|
|
|
await pollModUpdates();
|
|
changeState(States.PreStart);
|
|
SessionData.enableRestartTimer = settings.inst!.timer.enabled;
|
|
}
|
|
break;
|
|
}
|
|
case States.Idle:
|
|
{
|
|
// Restart timers and such
|
|
SessionData.timer.tickDown();
|
|
SessionData.operating_time.tickUp();
|
|
SessionData.timeSinceLastPing.tickUp();
|
|
SessionData.bumpModUpdateChecker();
|
|
|
|
// Check if we should send an alert
|
|
int sec = SessionData.timer.getTotalSeconds();
|
|
WarnIntervals current = SessionData.CURRENT_INTERVAL;
|
|
bool send = false;
|
|
for (WarnIntervals WI in WarnIntervals.values) {
|
|
if (WI == WarnIntervals.NONE) continue;
|
|
|
|
if (WI.seconds >= sec &&
|
|
(current == WarnIntervals.NONE ||
|
|
WI.seconds <= current.seconds) &&
|
|
WI != current) {
|
|
current = WI;
|
|
send = true;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Ping the server to check if it is alive
|
|
/*
|
|
if (SessionData.timeSinceLastPing.getTotalSeconds() > 60 &&
|
|
SessionData.operating_time.getTotalSeconds() > (10 * 60)) {
|
|
SessionData.timeSinceLastPing.apply(0);
|
|
int pid = PROC!.pid;
|
|
|
|
try {
|
|
Socket sock = await Socket.connect(
|
|
"127.0.0.1", settings.inst!.serverSettings.QueryPort);
|
|
await sock.close();
|
|
} catch (E, stack) {
|
|
print("Dead process checker caught: ${E}\n\n${stack}\n\n");
|
|
|
|
DeadProcKillswitch.complete();
|
|
}
|
|
}*/
|
|
|
|
if (send && SessionData.enableRestartTimer) {
|
|
// Send the alert message
|
|
SessionData.CURRENT_INTERVAL = current;
|
|
int alertColor = 0;
|
|
if (current.type == WarnType.Intrusive) {
|
|
print("Sending alert '${current.warning}'");
|
|
settings.sendRconCommand("broadcast ${current.warning}");
|
|
|
|
// Set discord alert color
|
|
alertColor = DiscordHookProps.ALERT_INTRUSIVE;
|
|
} else if (current.type == WarnType.NonIntrusive) {
|
|
print("Sending chat message '${current.warning}'");
|
|
//settings.sendRconCommand(
|
|
// "ast chat \"global\" \"${current.warning}\"");
|
|
|
|
// Set discord alert color
|
|
alertColor = DiscordHookProps.ALERT;
|
|
}
|
|
|
|
DiscordHookHelper.sendWebHook(settings.inst!.discord, alertColor,
|
|
"Server Restart Alert", current.warning);
|
|
}
|
|
|
|
// Check Shutdown Pending
|
|
if (SessionData.shutdownMessage.isNotEmpty) {
|
|
settings
|
|
.sendRconCommand("broadcast ${SessionData.shutdownMessage}");
|
|
SessionData.shutdownMessage = "";
|
|
}
|
|
|
|
if (SessionData.shutdownPending) {
|
|
// Shut down the server
|
|
changeState(States.FullStop);
|
|
SessionData.shutdownPending = false;
|
|
}
|
|
|
|
// Check mod updates
|
|
if (SessionData.shouldCheckModUpdates()) {
|
|
print("Scheduling mod update checker...");
|
|
SessionData.resetModUpdateChecker();
|
|
Timer.periodic(Duration(seconds: 10), (timer) async {
|
|
timer.cancel();
|
|
|
|
await doDownloadMods(true);
|
|
if (SessionData.IS_FIRST_MOD_CHECK) {
|
|
List<Mod> actualMods =
|
|
await doScanMods(false, computeHashes: true);
|
|
settings.inst!.mods = actualMods;
|
|
|
|
settings
|
|
.Write(); // Write the settings file to disk after this scan has completed.
|
|
|
|
SessionData.IS_FIRST_MOD_CHECK = false;
|
|
}
|
|
|
|
List<Mod> currentMods =
|
|
await doScanMods(true, computeHashes: true);
|
|
List<String> updatedMods = [];
|
|
for (int i = 0; i < currentMods.length; i++) {
|
|
Mod currentMod = settings.inst!.mods[i];
|
|
Mod scannedMod = currentMods[i];
|
|
|
|
if (currentMod.mod_hash == scannedMod.mod_hash) {
|
|
// Mod is ok
|
|
} else {
|
|
// Mod is not ok
|
|
updatedMods.add(scannedMod.mod_name);
|
|
}
|
|
}
|
|
|
|
if (updatedMods.isNotEmpty) {
|
|
settings.sendRconCommand(
|
|
"broadcast The server will be going down for a restart in 5 minutes. The following mods have been updated: ${updatedMods.join(', ')}");
|
|
SessionData.timer.apply((5 * 60));
|
|
print(
|
|
"Scheduling restart for mod updates: ${updatedMods.join(', ')}");
|
|
SessionData.enableRestartTimer = true;
|
|
SessionData.shutDownReason = ShutDownReason.MODS;
|
|
|
|
// Send discord alert!
|
|
DiscordHookHelper.sendWebHook(
|
|
settings.inst!.discord,
|
|
DiscordHookProps.ALERT_INTRUSIVE,
|
|
"Mods have been updated",
|
|
"The server is going to restart because the following mods have been updated: \n\n${updatedMods.join('\n')}");
|
|
DiscordHookHelper.sendWebHook(
|
|
settings.inst!.discord,
|
|
DiscordHookProps.ALERT,
|
|
"Server Restart Alert",
|
|
"The server will restart in 5 minutes");
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check Total Seconds
|
|
if (SessionData.timer.getTotalSeconds() == 0 &&
|
|
SessionData.enableRestartTimer) {
|
|
print("Shutdown is pending, restart timer has hit zero");
|
|
SessionData.shutdownPending = true;
|
|
}
|
|
|
|
// Check Dead Process
|
|
if (DeadProcKillswitch.isCompleted) {
|
|
// Switch state
|
|
print("Dead process detected - Entering restart loop");
|
|
changeState(States.FullStop); // This has the stop logic
|
|
}
|
|
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|