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 _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 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(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(); } } }