360 lines
14 KiB
C#
360 lines
14 KiB
C#
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)] + "…";
|
||
}
|
||
}
|