289 lines
9.2 KiB
Dart
289 lines
9.2 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:libac_dart/packets/packets.dart';
|
|
import 'package:libac_dart/utils/IOTools.dart';
|
|
import 'package:servermanager/game.dart';
|
|
import 'package:servermanager/proton.dart';
|
|
import 'package:servermanager/structs/SessionData.dart';
|
|
import 'package:servermanager/structs/mod.dart';
|
|
import 'package:servermanager/structs/settings.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.NonIntrusive, warning: "Tiiiiiimber!"),
|
|
TEN_SECONDS(
|
|
seconds: 10,
|
|
type: WarnType.Intrusive,
|
|
warning: "The server has 10 seconds until restart"),
|
|
TWENTY_SECONDS(
|
|
seconds: 20,
|
|
type: WarnType.Intrusive,
|
|
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.NonIntrusive,
|
|
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.NonIntrusive,
|
|
warning: "The server will restart in 10 minutes"),
|
|
ONE_HOUR(
|
|
seconds: (1 * 60 * 60),
|
|
type: WarnType.NonIntrusive,
|
|
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"),
|
|
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 {
|
|
try {
|
|
int code = await PROC!.exitCode;
|
|
DeadProcKillswitch.complete();
|
|
} catch (E) {
|
|
DeadProcKillswitch.complete();
|
|
}
|
|
}
|
|
|
|
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);
|
|
});
|
|
|
|
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
|
|
if (Platform.isWindows) {
|
|
PROC = await Process.start(
|
|
PathHelper.combine(
|
|
settings.getServerPath(), "ConanSandboxServer.exe"),
|
|
conanArgs,
|
|
workingDirectory: settings.getServerPath());
|
|
} else {
|
|
runDetachedProton(
|
|
PathHelper.combine(
|
|
settings.getServerPath(), "ConanSandboxServer.exe"),
|
|
conanArgs,
|
|
settings.getServerPath());
|
|
}
|
|
|
|
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;
|
|
|
|
/// 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);
|
|
|
|
resetKillswitch();
|
|
SessionData.enableRestartTimer = settings.inst!.timer.enabled;
|
|
|
|
// Schedule the server task
|
|
task = Timer.periodic(Duration(seconds: 1), (timer) {
|
|
switch (currentState) {
|
|
case States.Inactive:
|
|
{
|
|
timer.cancel();
|
|
|
|
//Settings settings = Settings();
|
|
if (settings.inst!.pterodactylMode) {
|
|
// Shut down the server processes now
|
|
PacketServer.socket!.close();
|
|
} else {
|
|
resetKillswitch();
|
|
SessionData.timer = settings.inst!.timer.time.copy();
|
|
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.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.seconds <= sec && WI.seconds < current.seconds) {
|
|
current = WI;
|
|
send = true;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (send && SessionData.enableRestartTimer) {
|
|
// Send the alert message
|
|
SessionData.CURRENT_INTERVAL = current;
|
|
if (current.type == WarnType.Intrusive) {
|
|
settings.sendRconCommand("broadcast ${current.warning}");
|
|
}
|
|
}
|
|
|
|
// Check Shutdown Pending
|
|
if (SessionData.shutdownPending) {
|
|
// Shut down the server
|
|
changeState(States.FullStop);
|
|
SessionData.shutdownPending = false;
|
|
}
|
|
|
|
// Check mod updates
|
|
if (SessionData.shouldCheckModUpdates()) {
|
|
Timer.periodic(Duration(seconds: 10), (timer) async {
|
|
timer.cancel();
|
|
SessionData.resetModUpdateChecker();
|
|
|
|
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}");
|
|
}
|
|
}
|
|
|
|
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));
|
|
SessionData.enableRestartTimer = true;
|
|
});
|
|
}
|
|
|
|
// Check Total Seconds
|
|
if (SessionData.timer.getTotalSeconds() == 0 &&
|
|
SessionData.enableRestartTimer) {
|
|
SessionData.shutdownPending = true;
|
|
}
|
|
|
|
// Check Dead Process
|
|
if (DeadProcKillswitch.isCompleted) {
|
|
// Switch state
|
|
changeState(States.FullStop); // This has the stop logic
|
|
}
|
|
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|