using System.Diagnostics; using System.Runtime.InteropServices; using WinForms = System.Windows.Forms; namespace Zcbot.WindowsNode; internal sealed class TrayHost : IDisposable { private readonly NodeApplicationController controller; private readonly MainWindow window; private readonly Action shutdown; private readonly WinForms.NotifyIcon tray; private readonly WinForms.ToolStripMenuItem statusItem; private readonly WinForms.ToolStripMenuItem exitItem; private NodeStatus current = NodeStatus.Create(NodeState.NotRegistered, "尚未注册"); private bool exiting; private bool disposed; internal TrayHost( NodeApplicationController controller, MainWindow window, Action shutdown) { this.controller = controller; this.window = window; this.shutdown = shutdown; statusItem = new WinForms.ToolStripMenuItem("尚未注册") { Enabled = false }; exitItem = new WinForms.ToolStripMenuItem("退出节点"); exitItem.Click += async (_, _) => await ExitNodeAsync(); var menu = new WinForms.ContextMenuStrip(); menu.Items.Add(statusItem); menu.Items.Add(new WinForms.ToolStripSeparator()); menu.Items.Add("打开主窗口", null, (_, _) => ShowMainWindow()); menu.Items.Add("立即重连", null, (_, _) => RestartConnection()); menu.Items.Add(new WinForms.ToolStripSeparator()); menu.Items.Add(exitItem); tray = new WinForms.NotifyIcon { ContextMenuStrip = menu, Icon = TrayIconFactory.Create(NodeState.NotRegistered), Text = "zcbot Windows Node - 尚未注册", Visible = true, }; tray.DoubleClick += (_, _) => ShowMainWindow(); window.ViewModel.CopyDiagnosticsRequested += CopyDiagnostics; window.ViewModel.HideRequested += window.Hide; window.ViewModel.ExitRequested += ExitNodeAsync; window.ViewModel.RegisterRequested += RegisterFromWpfAsync; window.ViewModel.ReconnectRequested += ReconnectFromWpfAsync; window.ViewModel.ResetIdentityRequested += ResetIdentityFromWpfAsync; window.ViewModel.SoftwarePage.ChooseLocationRequested += ChooseSoftwareLocation; window.ViewModel.SoftwarePage.ConfirmInstallRequested += ConfirmRuntimeInstall; window.ViewModel.AcceptancePage.ChooseReportDirectoryRequested += ChooseAcceptanceReportDirectory; window.ViewModel.AcceptancePage.ConfirmRunRequested += ConfirmAnsysAcceptance; window.ViewModel.AcceptancePage.ConfirmEnableGateRequested += ConfirmEnableAnsysGate; window.ViewModel.DataRootPage.ChooseTargetRequested += ChooseDataRootTarget; window.ViewModel.DataRootPage.ConfirmMigrationRequested += ConfirmDataRootMigration; window.ViewModel.DataRootPage.MigrationRequested += MigrateDataRootFromWpfAsync; window.ViewModel.DataRootPage.MigrationCompleted += CompleteDataRootMigration; window.ViewModel.DataRootPage.OpenRequested += OpenDataRoot; controller.StatusChanged += PostStatus; } internal void Start() { controller.Start(); UpdateStatus(controller.CurrentStatus); if (current.State is NodeState.NotRegistered or NodeState.AuthenticationRequired) { ShowMainWindow(); } } public void Dispose() { if (disposed) return; disposed = true; controller.StatusChanged -= PostStatus; window.ViewModel.CopyDiagnosticsRequested -= CopyDiagnostics; window.ViewModel.HideRequested -= window.Hide; window.ViewModel.ExitRequested -= ExitNodeAsync; window.ViewModel.RegisterRequested -= RegisterFromWpfAsync; window.ViewModel.ReconnectRequested -= ReconnectFromWpfAsync; window.ViewModel.ResetIdentityRequested -= ResetIdentityFromWpfAsync; window.ViewModel.SoftwarePage.ChooseLocationRequested -= ChooseSoftwareLocation; window.ViewModel.SoftwarePage.ConfirmInstallRequested -= ConfirmRuntimeInstall; window.ViewModel.AcceptancePage.ChooseReportDirectoryRequested -= ChooseAcceptanceReportDirectory; window.ViewModel.AcceptancePage.ConfirmRunRequested -= ConfirmAnsysAcceptance; window.ViewModel.AcceptancePage.ConfirmEnableGateRequested -= ConfirmEnableAnsysGate; window.ViewModel.DataRootPage.ChooseTargetRequested -= ChooseDataRootTarget; window.ViewModel.DataRootPage.ConfirmMigrationRequested -= ConfirmDataRootMigration; window.ViewModel.DataRootPage.MigrationRequested -= MigrateDataRootFromWpfAsync; window.ViewModel.DataRootPage.MigrationCompleted -= CompleteDataRootMigration; window.ViewModel.DataRootPage.OpenRequested -= OpenDataRoot; window.ViewModel.Dispose(); controller.Dispose(); tray.Visible = false; tray.Icon?.Dispose(); tray.Dispose(); } private void PostStatus(NodeStatus status) { if (window.Dispatcher.CheckAccess()) { UpdateStatus(status); } else { _ = window.Dispatcher.InvokeAsync(() => UpdateStatus(status)); } } private void UpdateStatus(NodeStatus status) { current = status; window.ViewModel.ApplyStatus(status, controller.CurrentConfig); statusItem.Text = CompactMenuText(status.Message, 24); tray.Text = Limit($"zcbot Windows Node - {status.Message}", 63); var oldIcon = tray.Icon; tray.Icon = TrayIconFactory.Create(status.State); oldIcon?.Dispose(); } private void ShowMainWindow() => window.ShowAndActivate(); private async void RestartConnection() { if (!await controller.ReconnectAsync()) { ShowMainWindow(); } } private static bool CopyDiagnostics(string text) { try { WinForms.Clipboard.SetText(text); return true; } catch (ExternalException) { return false; } } private Task RegisterFromWpfAsync(EnrollOptions options) => controller.RegisterAsync(options); private Task ReconnectFromWpfAsync() => controller.ReconnectAsync(); private async Task ResetIdentityFromWpfAsync() { if (WinForms.MessageBox.Show( "这会删除本机加密身份,随后需要使用新注册码重新注册。请先在管理后台删除或禁用云端旧节点。是否继续?", "清除本机节点身份", WinForms.MessageBoxButtons.YesNo, WinForms.MessageBoxIcon.Warning, WinForms.MessageBoxDefaultButton.Button2) != WinForms.DialogResult.Yes) { return false; } await controller.ResetIdentityAsync(); return true; } private string? ChooseSoftwareLocation( SoftwarePathKind pathKind, string displayName, string requiredRelativePath) { if (pathKind == SoftwarePathKind.Executable) { using var dialog = new WinForms.OpenFileDialog { Title = $"选择 {displayName} 可执行文件", Filter = $"{requiredRelativePath}|{requiredRelativePath}|可执行文件 (*.exe)|*.exe", CheckFileExists = true, Multiselect = false, }; return dialog.ShowDialog() == WinForms.DialogResult.OK ? dialog.FileName : null; } if (pathKind == SoftwarePathKind.Directory) { using var dialog = new WinForms.FolderBrowserDialog { Description = $"选择 {displayName} 安装根目录", UseDescriptionForTitle = true, ShowNewFolderButton = false, }; return dialog.ShowDialog() == WinForms.DialogResult.OK ? dialog.SelectedPath : null; } return null; } private static string? ChooseAcceptanceReportDirectory() { using var dialog = new WinForms.FolderBrowserDialog { Description = "选择保存 ANSYS 验收报告的父目录", UseDescriptionForTitle = true, ShowNewFolderButton = true, }; return dialog.ShowDialog() == WinForms.DialogResult.OK ? dialog.SelectedPath : null; } private static bool ConfirmAnsysAcceptance() => WinForms.MessageBox.Show( "程序会使用内置标准试件执行一次进程树取消和连续 20 次真实静力求解,期间会获取并释放 ANSYS 许可证,可能耗时较长。确认开始?", "开始 ANSYS 真机验收", WinForms.MessageBoxButtons.YesNo, WinForms.MessageBoxIcon.Warning, WinForms.MessageBoxDefaultButton.Button2) == WinForms.DialogResult.Yes; private static bool ConfirmEnableAnsysGate() => WinForms.MessageBox.Show( "请先在许可证管理端确认正常求解和取消后的席位均已释放。开启后需要完全退出并重新启动 Node 才会进入实际调度。确认开启机器级执行门?", "开启 ANSYS 执行门", WinForms.MessageBoxButtons.YesNo, WinForms.MessageBoxIcon.Warning, WinForms.MessageBoxDefaultButton.Button2) == WinForms.DialogResult.Yes; private static string? ChooseDataRootTarget(string currentRoot) { using var dialog = new WinForms.FolderBrowserDialog { Description = "选择新的 zcbot Windows Node 数据目录(必须为空)", UseDescriptionForTitle = true, SelectedPath = currentRoot, ShowNewFolderButton = true, }; return dialog.ShowDialog() == WinForms.DialogResult.OK ? dialog.SelectedPath : null; } private Task MigrateDataRootFromWpfAsync(string target) => controller.MigrateDataRootAsync(target); private static bool ConfirmDataRootMigration(string target) => WinForms.MessageBox.Show( $"Node 将先停止接收任务,把现有数据复制并校验到:\n{target}\n\n" + "校验成功后切换目录并重启;旧目录不会自动删除。是否继续?", "迁移数据目录", WinForms.MessageBoxButtons.YesNo, WinForms.MessageBoxIcon.Warning, WinForms.MessageBoxDefaultButton.Button2) == WinForms.DialogResult.Yes; private static void CompleteDataRootMigration(NodeDataMigrationResult result) { WinForms.MessageBox.Show( $"数据目录迁移完成:{result.FileCount} 个文件。\n" + $"新目录:{result.TargetDirectory}\n\n旧目录仍保留:{result.SourceDirectory}\n" + "确认新节点运行正常后,可由管理员手工清理旧目录。Node 现在将重启。", "迁移完成", WinForms.MessageBoxButtons.OK, WinForms.MessageBoxIcon.Information); WinForms.Application.Restart(); } private static void OpenDataRoot(string path) { try { Directory.CreateDirectory(path); var startInfo = new ProcessStartInfo { FileName = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.Windows), "explorer.exe"), UseShellExecute = false, }; startInfo.ArgumentList.Add(path); Process.Start(startInfo); } catch (Exception exception) when ( exception is IOException or UnauthorizedAccessException or InvalidOperationException) { WinForms.MessageBox.Show( exception.Message, "无法打开数据目录", WinForms.MessageBoxButtons.OK, WinForms.MessageBoxIcon.Error); } } private static bool ConfirmRuntimeInstall(string displayName) => WinForms.MessageBox.Show( $"将为 {displayName} 创建独立 Python 3.12 运行环境并安装固定依赖。是否继续?", "安装专业软件运行环境", WinForms.MessageBoxButtons.YesNo, WinForms.MessageBoxIcon.Question, WinForms.MessageBoxDefaultButton.Button2) == WinForms.DialogResult.Yes; private async Task ExitNodeAsync() { if (exiting) return; var activeJobs = controller.ActiveJobCount; var cancelJobs = false; if (activeJobs > 0) { var choice = WinForms.MessageBox.Show( $"当前有 {activeJobs} 个本机任务尚未结束。\n\n" + "选择“是”:停止接收新任务,等待现有任务完成后退出。\n" + "选择“否”:取消现有任务并退出。\n" + "选择“取消”:返回,不退出。", "退出 zcbot Windows Node", WinForms.MessageBoxButtons.YesNoCancel, WinForms.MessageBoxIcon.Warning, WinForms.MessageBoxDefaultButton.Button1); if (choice == WinForms.DialogResult.Cancel) return; cancelJobs = choice == WinForms.DialogResult.No; if (cancelJobs && WinForms.MessageBox.Show( "取消任务会终止正在运行的专业软件进程,未完成结果不会上传。确认取消任务并退出?", "确认取消任务", WinForms.MessageBoxButtons.YesNo, WinForms.MessageBoxIcon.Warning, WinForms.MessageBoxDefaultButton.Button2) != WinForms.DialogResult.Yes) { return; } } exiting = true; window.ViewModel.BeginExit(); window.IsEnabled = false; exitItem.Enabled = false; if (tray.ContextMenuStrip is not null) tray.ContextMenuStrip.Enabled = false; await controller.StopAsync( cancelJobs ? NodeShutdownMode.CancelJobs : NodeShutdownMode.WaitForJobs); tray.Visible = false; window.AllowClose = true; window.Close(); shutdown(); } private static string Limit(string value, int length) => value.Length <= length ? value : value[..length]; private static string CompactMenuText(string value, int length) { var singleLine = value.Replace('\r', ' ').Replace('\n', ' ').Trim(); return singleLine.Length <= length ? singleLine : singleLine[..(length - 1)] + "…"; } }