zcbot/windows-node/Zcbot.WindowsNode/Presentation/Tray/TrayHost.cs

360 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<bool> ReconnectFromWpfAsync() => controller.ReconnectAsync();
private async Task<bool> 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<NodeDataMigrationResult> 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)] + "…";
}
}