ConanServerManager/lib/statemachine.dart

414 lines
14 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) {
PROC = await Process.start(executable, conanArgs,
workingDirectory: settings.getServerPath());
} 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();
exit(0);
} else {
resetKillswitch();
SessionData.timer = settings.inst!.timer.time.copy();
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);
List<Mod> currentMods = await doScanMods(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;
// 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${updatedMods.join(', ')}");
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;
}
}
});
}
}