using System.Diagnostics; using System.IO.Pipes; using LumaTunnel.Client.Interface; using LumaTunnel.Client.Profiles; using LumaTunnel.Shared.Windows; using XFEExtension.NetCore.WinUIHelper.Implements.Services; namespace LumaTunnel.Client.Implements; public sealed class SystemProxyService(ClientProfileStore profileStore) : GlobalServiceBase, ISystemProxyService, IDisposable { private readonly string _snapshotPath = Path.Combine(profileStore.StatePath, "system-proxy-snapshot.json"); private NamedPipeServerStream? _watchdogPipe; private Process? _watchdog; private SystemProxySnapshot? _snapshot; private string? _ownerId; private int _httpPort; public bool IsEnabled { get; private set; } public async Task EnableAsync(int httpPort, CancellationToken cancellationToken = default) { if (IsEnabled) return; _ownerId = Guid.NewGuid().ToString("N"); _httpPort = httpPort; _snapshot = SystemProxyRegistry.Capture(_ownerId); await SystemProxyRegistry.SaveSnapshotAsync(_snapshotPath, _snapshot, cancellationToken).ConfigureAwait(false); SystemProxyRegistry.ApplyLoopbackProxy(httpPort); IsEnabled = true; StartWatchdog(); } public async Task DisableAsync(CancellationToken cancellationToken = default) { if (!IsEnabled && _snapshot is null) return false; var snapshot = _snapshot ?? await SystemProxyRegistry.LoadSnapshotAsync(_snapshotPath, cancellationToken).ConfigureAwait(false); var restored = snapshot is not null && SystemProxyRegistry.RestoreIfOwned(snapshot, _httpPort); IsEnabled = false; await SignalNormalExitAsync(cancellationToken).ConfigureAwait(false); _snapshot = null; if (File.Exists(_snapshotPath)) File.Delete(_snapshotPath); return restored; } public bool EmergencyRestore() { try { var snapshot = _snapshot ?? SystemProxyRegistry.LoadSnapshotAsync(_snapshotPath).GetAwaiter().GetResult(); return snapshot is not null && SystemProxyRegistry.RestoreIfOwned(snapshot, _httpPort == 0 ? AppServices.Profile.HttpPort : _httpPort); } catch { return false; } } public async Task RecoverStaleSnapshotAsync(int expectedHttpPort, CancellationToken cancellationToken = default) { var snapshot = await SystemProxyRegistry.LoadSnapshotAsync(_snapshotPath, cancellationToken).ConfigureAwait(false); if (snapshot is null) return false; var restored = SystemProxyRegistry.RestoreIfOwned(snapshot, expectedHttpPort); if (restored && File.Exists(_snapshotPath)) File.Delete(_snapshotPath); return restored; } private void StartWatchdog() { var executable = Path.Combine(AppContext.BaseDirectory, "LumaTunnel.Watchdog.exe"); if (!File.Exists(executable) || _ownerId is null) return; var pipeName = "LumaTunnel.Watchdog." + _ownerId; _watchdogPipe = new NamedPipeServerStream(pipeName, PipeDirection.Out, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); _ = _watchdogPipe.WaitForConnectionAsync(); var startInfo = new ProcessStartInfo(executable) { UseShellExecute = false, CreateNoWindow = true }; foreach (var argument in new[] { "--parent", Environment.ProcessId.ToString(System.Globalization.CultureInfo.InvariantCulture), "--pipe", pipeName, "--snapshot", _snapshotPath, "--owner", _ownerId, "--port", _httpPort.ToString(System.Globalization.CultureInfo.InvariantCulture) }) startInfo.ArgumentList.Add(argument); _watchdog = Process.Start(startInfo); } private async Task SignalNormalExitAsync(CancellationToken cancellationToken) { try { if (_watchdogPipe is { IsConnected: true } pipe) { await pipe.WriteAsync("normal"u8.ToArray(), cancellationToken).ConfigureAwait(false); await pipe.FlushAsync(cancellationToken).ConfigureAwait(false); } } catch (IOException) { } finally { _watchdogPipe?.Dispose(); _watchdogPipe = null; _watchdog?.Dispose(); _watchdog = null; } } public void Dispose() { _watchdogPipe?.Dispose(); _watchdog?.Dispose(); GC.SuppressFinalize(this); } }