Files
squad-rain-ops-mini/Services/FrpManager.cs

366 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
namespace RainOpsMini.Services
{
public class FrpSettings
{
public bool AutoRestartEnabled { get; set; }
public int RestartIntervalMinutes { get; set; }
}
public class FrpManager : IDisposable
{
private Process? _process;
private readonly string _frpDir;
private readonly string _exePath;
private readonly string _configPath;
private readonly string _settingsPath;
private readonly string _pidPath;
private Timer? _restartTimer;
private readonly List<string> _logs = new();
private const int MaxLogLines = 2000;
private readonly object _logLock = new();
public bool IsRunning => _process != null && !_process.HasExited;
public int? ProcessId => IsRunning ? _process?.Id : null;
public DateTime? StartTime { get; private set; }
public bool AutoRestartEnabled { get; private set; } = false;
public int RestartIntervalMinutes { get; private set; } = 0;
public DateTime? NextRestartTime { get; private set; }
public FrpManager()
{
// Ensure the child process is killed when the application exits
AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
// Determine the directory where the executable is located (for persistent storage)
// In single-file publish, Environment.CurrentDirectory is a temp folder.
// We want to store configuration next to the executable.
string installDir = Environment.CurrentDirectory;
try
{
using (var process = Process.GetCurrentProcess())
{
if (process.MainModule != null)
{
installDir = Path.GetDirectoryName(process.MainModule.FileName) ?? Environment.CurrentDirectory;
}
}
}
catch
{
// Fallback if permission denied or other error
}
_frpDir = Path.Combine(installDir, "FRP");
_exePath = Path.Combine(_frpDir, "frpc.exe");
_configPath = Path.Combine(_frpDir, "frpc.ini");
_settingsPath = Path.Combine(_frpDir, "frp_settings.json");
_pidPath = Path.Combine(_frpDir, "frpc.pid");
// Ensure the directory exists
if (!Directory.Exists(_frpDir)) Directory.CreateDirectory(_frpDir);
// Kill previous process if it exists
KillPreviousProcess();
// Copy files from bundled location (Environment.CurrentDirectory/FRP) to install location if missing
ExtractBundledFilesIfNeeded();
LoadSettings();
}
private void ExtractBundledFilesIfNeeded()
{
try
{
string bundledFrpDir = Path.Combine(Environment.CurrentDirectory, "FRP");
// If bundled directory exists (it should in single-file mode) and is different from target
if (Directory.Exists(bundledFrpDir) &&
!string.Equals(Path.GetFullPath(bundledFrpDir), Path.GetFullPath(_frpDir), StringComparison.OrdinalIgnoreCase))
{
// Copy frpc.exe if missing
string bundledExe = Path.Combine(bundledFrpDir, "frpc.exe");
if (!File.Exists(_exePath) && File.Exists(bundledExe))
{
File.Copy(bundledExe, _exePath);
}
// Copy frpc.ini if missing (preserve user config)
string bundledConfig = Path.Combine(bundledFrpDir, "frpc.ini");
if (!File.Exists(_configPath) && File.Exists(bundledConfig))
{
File.Copy(bundledConfig, _configPath);
}
// Copy frpc_full.ini if missing
string targetFullConfig = Path.Combine(_frpDir, "frpc_full.ini");
string bundledFullConfig = Path.Combine(bundledFrpDir, "frpc_full.ini");
if (!File.Exists(targetFullConfig) && File.Exists(bundledFullConfig))
{
File.Copy(bundledFullConfig, targetFullConfig);
}
}
}
catch (Exception ex)
{
AddLog($"Warning: Failed to extract bundled files: {ex.Message}");
}
}
private void KillPreviousProcess()
{
try
{
if (File.Exists(_pidPath))
{
string pidStr = File.ReadAllText(_pidPath);
if (int.TryParse(pidStr, out int pid))
{
try
{
var process = Process.GetProcessById(pid);
// Verify it's an frpc process to avoid killing random processes
if (process.ProcessName.ToLower().Contains("frpc"))
{
AddLog($"Found previous FRPC process (PID: {pid}). Killing it...");
process.Kill();
process.WaitForExit(3000);
AddLog($"Previous FRPC process killed.");
}
}
catch (ArgumentException)
{
// Process not running
}
catch (Exception ex)
{
AddLog($"Error killing previous process: {ex.Message}");
}
}
try { File.Delete(_pidPath); } catch { }
}
}
catch (Exception ex)
{
AddLog($"Error cleaning up previous process: {ex.Message}");
}
}
public IEnumerable<string> GetLogs()
{
lock (_logLock)
{
return _logs.ToList();
}
}
public void ClearLogs()
{
lock (_logLock)
{
_logs.Clear();
}
}
private void AddLog(string? message)
{
if (string.IsNullOrWhiteSpace(message)) return;
// Output to console as well
Console.WriteLine($"[FRP] {message}");
lock (_logLock)
{
// Format: [HH:mm:ss] message
var logEntry = $"[{DateTime.Now:HH:mm:ss}] {message}";
_logs.Add(logEntry);
if (_logs.Count > MaxLogLines)
{
_logs.RemoveAt(0);
}
}
}
public void Start()
{
if (IsRunning) return;
if (!File.Exists(_exePath)) throw new FileNotFoundException("frpc.exe not found");
if (!File.Exists(_configPath)) throw new FileNotFoundException("frpc.ini not found");
var psi = new ProcessStartInfo
{
FileName = _exePath,
Arguments = $"-c \"{_configPath}\"",
WorkingDirectory = _frpDir,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
_process = new Process { StartInfo = psi };
_process.OutputDataReceived += (s, e) => AddLog(e.Data);
_process.ErrorDataReceived += (s, e) => AddLog(e.Data);
_process.Start();
// Record PID
try
{
File.WriteAllText(_pidPath, _process.Id.ToString());
}
catch (Exception ex)
{
AddLog($"Warning: Failed to write PID file: {ex.Message}");
}
_process.BeginOutputReadLine();
_process.BeginErrorReadLine();
StartTime = DateTime.Now;
AddLog("FRP Process Started");
UpdateRestartTimer();
}
public void Stop()
{
try
{
_restartTimer?.Dispose();
_restartTimer = null;
NextRestartTime = null;
// Delete PID file
if (File.Exists(_pidPath))
{
try { File.Delete(_pidPath); } catch { }
}
if (_process != null && !_process.HasExited)
{
_process.Kill();
_process.WaitForExit(1000);
}
}
catch { }
finally
{
_process = null;
StartTime = null;
}
}
public void Restart()
{
Stop();
Thread.Sleep(1000);
Start();
}
public string GetConfig()
{
if (File.Exists(_configPath))
return File.ReadAllText(_configPath);
return "";
}
public void SaveConfig(string content)
{
File.WriteAllText(_configPath, content);
// If running, user might want to restart to apply changes.
// We won't auto-restart here, let user decide.
}
public void SetAutoRestart(bool enabled, int intervalMinutes)
{
AutoRestartEnabled = enabled;
RestartIntervalMinutes = intervalMinutes;
SaveSettings();
if (IsRunning)
{
UpdateRestartTimer();
}
}
private void UpdateRestartTimer()
{
_restartTimer?.Dispose();
_restartTimer = null;
NextRestartTime = null;
if (AutoRestartEnabled && RestartIntervalMinutes > 0 && IsRunning)
{
NextRestartTime = DateTime.Now.AddMinutes(RestartIntervalMinutes);
long dueTime = (long)(NextRestartTime.Value - DateTime.Now).TotalMilliseconds;
if (dueTime < 0) dueTime = 0;
_restartTimer = new Timer(_ =>
{
// Execute restart on thread pool
try { Restart(); } catch { }
}, null, dueTime, Timeout.Infinite);
}
}
private void LoadSettings()
{
try
{
if (File.Exists(_settingsPath))
{
var json = File.ReadAllText(_settingsPath);
var settings = JsonSerializer.Deserialize<FrpSettings>(json);
if (settings != null)
{
AutoRestartEnabled = settings.AutoRestartEnabled;
RestartIntervalMinutes = settings.RestartIntervalMinutes;
}
}
}
catch { }
}
private void SaveSettings()
{
try
{
var settings = new FrpSettings
{
AutoRestartEnabled = AutoRestartEnabled,
RestartIntervalMinutes = RestartIntervalMinutes
};
var json = JsonSerializer.Serialize(settings);
File.WriteAllText(_settingsPath, json);
}
catch { }
}
public void Dispose()
{
Stop();
_restartTimer?.Dispose();
AppDomain.CurrentDomain.ProcessExit -= OnProcessExit;
}
private void OnProcessExit(object? sender, EventArgs e)
{
Stop();
}
}
}