using System.Diagnostics; using System.Drawing; namespace Neta.Tray; public sealed class TrayApplicationContext : ApplicationContext { private readonly NotifyIcon _notifyIcon; private readonly BackendProcessManager _processManager; private readonly StatusClient _statusClient; private RuntimeBootstrapInfo? _runtime; private RuntimeStatusDto? _lastStatus; private bool _starting; public TrayApplicationContext() : this(new BackendProcessManager(), new StatusClient(new HttpClient())) { } internal TrayApplicationContext(BackendProcessManager processManager, StatusClient statusClient) { _processManager = processManager; _statusClient = statusClient; _notifyIcon = new NotifyIcon { Text = "Neta - 启动中…", Icon = SystemIcons.Application, Visible = true, ContextMenuStrip = new ContextMenuStrip() }; _notifyIcon.ContextMenuStrip.Items.Add("打开系统", null, WrapAsync(OpenSystemAsync)); _notifyIcon.ContextMenuStrip.Items.Add("重启服务", null, WrapAsync(RestartBackendAsync)); _notifyIcon.ContextMenuStrip.Items.Add("停止服务", null, WrapAsync(StopBackendAsync)); _notifyIcon.ContextMenuStrip.Items.Add("打开日志目录", null, (_, _) => OpenLogs()); _notifyIcon.ContextMenuStrip.Items.Add("打开配置目录", null, (_, _) => OpenConfigDir()); _notifyIcon.ContextMenuStrip.Items.Add("退出程序", null, WrapAsync(ExitAllAsync)); _ = StartBackendSafe(); } private EventHandler WrapAsync(Func action) => async (_, _) => { try { await action(); } catch (Exception ex) { ShowError(ex.Message); } }; private void ShowError(string message) { _notifyIcon.BalloonTipTitle = "Neta"; _notifyIcon.BalloonTipText = message; _notifyIcon.BalloonTipIcon = ToolTipIcon.Error; _notifyIcon.ShowBalloonTip(5000); } private void MarkRunning() { _notifyIcon.Text = "Neta - 运行中"; } private async Task StartBackendSafe() { try { await EnsureBackendAttachedAsync(); MarkRunning(); } catch (Exception ex) { _notifyIcon.Text = "Neta - 启动失败"; ShowError($"后端启动失败: {ex.Message}"); } } private async Task EnsureBackendAttachedAsync() { if (_starting) return; _starting = true; try { _runtime = RuntimeInfoStore.LoadFromInstalledConfig(AppContext.BaseDirectory); if (_runtime is not null) { var status = await _statusClient.GetStatusAsync(_runtime, CancellationToken.None); if (status is { Ready: true }) { _lastStatus = status; MarkRunning(); return; } if (_processManager.IsBackendProcessAlive(_runtime.Pid)) return; } var traySecret = Guid.NewGuid().ToString("N"); var backendExe = Path.Combine(AppContext.BaseDirectory, "backend.exe"); _processManager.Start(backendExe, traySecret); _runtime = RuntimeInfoStore.WaitUntilAvailable(AppContext.BaseDirectory, TimeSpan.FromSeconds(30)); _lastStatus = await _statusClient.GetStatusAsync(_runtime, CancellationToken.None); if (_lastStatus is not null) { MarkRunning(); } } finally { _starting = false; } } private async Task OpenSystemAsync() { if (_lastStatus is null) await EnsureBackendAttachedAsync(); if (_lastStatus is null) throw new InvalidOperationException("后端尚未就绪"); MarkRunning(); Process.Start(new ProcessStartInfo(_lastStatus.Url) { UseShellExecute = true }); } private async Task RestartBackendAsync() { await StopBackendAsync(); _lastStatus = null; _notifyIcon.Text = "Neta - 重启中…"; await EnsureBackendAttachedAsync(); _notifyIcon.Text = "Neta - 运行中"; } private async Task StopBackendAsync() { var backendExePath = Path.Combine(AppContext.BaseDirectory, "backend.exe"); _runtime ??= RuntimeInfoStore.LoadFromInstalledConfig(AppContext.BaseDirectory); if (_runtime is not null) { var pid = _runtime.Pid; var stoppedGracefully = false; try { await _statusClient.StopAsync(_runtime, CancellationToken.None); stoppedGracefully = true; } catch { } _processManager.WaitForExit(pid, TimeSpan.FromSeconds(10)); if (_processManager.IsBackendProcessAlive(pid)) { _processManager.KillProcess(pid); } _notifyIcon.Text = stoppedGracefully ? "Neta - 已停止" : "Neta - 已强制停止"; } else { _notifyIcon.Text = "Neta - 已强制停止"; } _processManager.KillInstalledBackendProcesses(backendExePath); _lastStatus = null; _runtime = null; } private async Task ExitAllAsync() { await StopBackendAsync(); _notifyIcon.Visible = false; _notifyIcon.Dispose(); ExitThread(); } private void OpenLogs() => RuntimeInfoStore.OpenLogs(AppContext.BaseDirectory); private void OpenConfigDir() => RuntimeInfoStore.OpenConfigDir(AppContext.BaseDirectory); }