using System.Diagnostics; using System.IO; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Messaging; using Livia.Properties; using Livia.Utility; using Microsoft.Extensions.Logging; namespace Livia.Models; public interface IProcessManager { bool Start(ServerFileLocation[] serverFileLocations); } public readonly struct ServerFileLocation { public ServerFileLocation(string fileName, string workingDirectory) { FileName = fileName; FileNameNoExtension = Path.GetFileNameWithoutExtension(fileName); WorkingDirectory = workingDirectory; FullPath = Path.Combine(WorkingDirectory, FileName); } public readonly string FileName; public readonly string FileNameNoExtension; public readonly string WorkingDirectory; public readonly string FullPath; } public class ProcessManager : ObservableRecipient, IProcessManager, IRecipient { private readonly ILogger _logger; private ServerFileLocation[] _serverFileLocations = Array.Empty(); private Process[] _servers = Array.Empty(); private const int MaxRestartCount = 10; private int _restartCount; public ProcessManager(ILogger logger) { _logger = logger; WeakReferenceMessenger.Default.RegisterAll(this); } private Process StartProcess(ServerFileLocation fileLocation) { _logger.LogInformation("Starting service"); Process process = new(); process.StartInfo.FileName = fileLocation.FileName; process.StartInfo.WorkingDirectory = fileLocation.WorkingDirectory; process.StartInfo.UseShellExecute = true; process.StartInfo.CreateNoWindow = true; process.EnableRaisingEvents = true; process.StartInfo.WindowStyle = Settings.Default.DeveloperMode ? ProcessWindowStyle.Normal : ProcessWindowStyle.Hidden; process.Exited += ProcessOnExited; if (!process.Start()) { throw new Exception("Failed to start process"); } return process; } private void ProcessOnExited(object? sender, EventArgs e) { if (_restartCount >= MaxRestartCount) { _logger.LogCritical("ProcessManager MaxRestartCount reached"); return; } _logger.LogCritical("Server process has exited! Attempting to restart it"); for (int i = 0; i < _servers.Length; i++) { if (sender != _servers[i]) continue; _restartCount++; _servers[i] = StartProcess(_serverFileLocations[i]); return; } _logger.LogCritical("Cannot determine which process has exited!"); } private void CleanUp() { foreach (Process process in _servers) { //disable this so that we are not going to attempt to restart it process.EnableRaisingEvents = false; } KillExisting(); //remove everything from temp string[] folders = Directory.GetDirectories(Path.GetTempPath()); foreach (string folderPath in folders) { DirectoryInfo info = new(folderPath); string folderName = info.Name; if (!folderName.StartsWith("_MEI")) continue; //pretty new, wait and see if it will be auto deleted TimeSpan folderAge = DateTime.Now - info.LastWriteTime; if (folderAge < TimeSpan.FromDays(1)) continue; try { _logger.LogInformation("{folder} is {age} old, deleting it", folderPath, folderAge); Directory.Delete(folderPath, true); } catch (Exception e) { _logger.LogInformation(e, "Error Deleting {folder}", folderPath); } } } private void KillExisting() { //find all and kill them foreach (ServerFileLocation fileLocation in _serverFileLocations) { try { Process[] processes = Process.GetProcessesByName(fileLocation.FileNameNoExtension); Process? lastCreatedProcess = processes.Where(process => process.MainModule?.FileName == fileLocation.FullPath).MaxBy(process => process.StartTime); lastCreatedProcess?.Kill(); lastCreatedProcess?.WaitForExit(); lastCreatedProcess?.Dispose(); } catch (Exception e) { _logger.LogError(e, "Error killing process{process}", fileLocation.FileNameNoExtension); } } } public bool Start(ServerFileLocation[] serverFileLocations) { KillExisting(); _serverFileLocations = serverFileLocations; _servers = new Process[_serverFileLocations.Length]; try { for (int i = 0; i < _serverFileLocations.Length; i++) { _servers[i] = StartProcess(_serverFileLocations[i]); } } catch (Exception ex) { _logger.LogCritical(ex, "Cannot start server"); return false; } return true; } public void Receive(ShutdownMessage message) { CleanUp(); } }