176 lines
5.6 KiB
C#
Raw Normal View History

2026-05-20 21:39:12 +08:00
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<Task> 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);
}