414 lines
14 KiB
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;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|