mirror of
https://gitee.com/xiarenalofs/squad-rain-ops-mini.git
synced 2026-08-05 21:15:52 +08:00
366 lines
12 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|