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 DeadProcKillswitch = Completer(); void resetKillswitch() { DeadProcKillswitch = Completer(); } static Future monitorProcess() async { try { int code = await PROC!.exitCode; DeadProcKillswitch.complete(); } catch (E) { DeadProcKillswitch.complete(); } } var _currentState = States.Inactive; StreamController _stateController = StreamController.broadcast(); Stream get stateChanges => _stateController.stream; States get currentState => _currentState; void changeState(States n) { _currentState = n; _stateController.add(n); } Future 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 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 currentMods = await doScanMods(true); List 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; } } }); } }