namespace Zcbot.WindowsNode; internal sealed class TrayApplicationContext : ApplicationContext { private readonly NodeConfigStore store; private readonly ConfigurationForm form; private readonly NotifyIcon tray; private readonly ToolStripMenuItem statusItem; private readonly ToolStripMenuItem exitItem; private CancellationTokenSource? connectionStop; private Task? connectionTask; private NodeConnectionLoop? connectionLoop; private NodeConfig? config; private bool exiting; private NodeStatus current = NodeStatus.Create(NodeState.NotRegistered, "尚未注册"); internal TrayApplicationContext(NodeConfigStore store) { this.store = store; form = new ConfigurationForm(); // Create the hidden form handle before the connection loop starts. Fast local // connections can otherwise report Connecting/Online before a handle exists, // and PostStatus would discard both updates until the user reconnects manually. _ = form.Handle; form.RegisterRequested += RegisterAsync; form.ReconnectRequested += RestartConnection; form.ResetIdentityRequested += ResetIdentity; form.DataRootMigrationRequested += MigrateDataRootAsync; statusItem = new ToolStripMenuItem("尚未注册") { Enabled = false }; var menu = new ContextMenuStrip(); menu.Items.Add(statusItem); menu.Items.Add(new ToolStripSeparator()); menu.Items.Add("打开配置", null, (_, _) => ShowConfiguration()); menu.Items.Add("立即重连", null, (_, _) => RestartConnection()); menu.Items.Add(new ToolStripSeparator()); exitItem = new ToolStripMenuItem("退出"); exitItem.Click += async (_, _) => await ExitNodeAsync(); menu.Items.Add(exitItem); tray = new NotifyIcon { ContextMenuStrip = menu, Icon = TrayIconFactory.Create(NodeState.NotRegistered), Text = "zcbot Windows Node - 尚未注册", Visible = true, }; tray.DoubleClick += (_, _) => ShowConfiguration(); if (store.Exists) { try { config = store.Load(); UpdateStatus(NodeStatus.Create(NodeState.Connecting, "正在连接 zcbot…")); StartConnection(); } catch (NodeConfigurationException exception) { UpdateStatus(NodeStatus.Create(NodeState.AuthenticationRequired, exception.Message)); ShowConfiguration(); } } else { UpdateStatus(current); ShowConfiguration(); } } protected override void Dispose(bool disposing) { if (disposing) { connectionStop?.Cancel(); tray.Visible = false; tray.Dispose(); form.Dispose(); connectionStop?.Dispose(); } base.Dispose(disposing); } private async Task RegisterAsync(EnrollOptions options) { UpdateStatus(NodeStatus.Create(NodeState.Connecting, "正在注册节点…")); try { await EnrollmentClient.EnrollAsync(options, store, CancellationToken.None); config = store.Load(); UpdateStatus(NodeStatus.Create(NodeState.Connecting, "注册成功,正在连接…")); StartConnection(); } catch { UpdateStatus(NodeStatus.Create(NodeState.NotRegistered, "注册失败,请检查配置")); throw; } } private void StartConnection() { if (exiting || config is null || connectionTask is { IsCompleted: false }) { return; } connectionStop?.Dispose(); connectionStop = new CancellationTokenSource(); connectionTask = RunConnectionAsync(config, connectionStop.Token); } private async Task RunConnectionAsync(NodeConfig nodeConfig, CancellationToken cancellationToken) { var loop = new NodeConnectionLoop(nodeConfig, status => PostStatus(status)); connectionLoop = loop; try { await loop.RunAsync(cancellationToken); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { PostStatus(NodeStatus.Create(NodeState.Stopped, "连接已停止")); } catch (NodeConfigurationException exception) { PostStatus(NodeStatus.Create(NodeState.AuthenticationRequired, exception.Message)); } catch (Exception exception) { PostStatus(NodeStatus.Create(NodeState.Offline, $"节点异常:{exception.Message}")); } finally { if (ReferenceEquals(connectionLoop, loop)) { connectionLoop = null; } } } private void RestartConnection() { if (config is null) { ShowConfiguration(); return; } UpdateStatus(NodeStatus.Create(NodeState.Connecting, "等待本机任务收尾后重连…")); connectionStop?.Cancel(); _ = RestartAfterStopAsync(); } private void ResetIdentity() { connectionStop?.Cancel(); store.DeleteLocalIdentity(); config = null; UpdateStatus(NodeStatus.Create(NodeState.NotRegistered, "本机身份已清除,请使用新注册码注册")); } private async Task RestartAfterStopAsync() { if (connectionTask is not null) { await connectionTask; } StartConnection(); } private async Task MigrateDataRootAsync(string targetRoot) { var sourceRoot = NodePaths.ForCurrentMachine().RootDirectory; connectionStop?.Cancel(); try { if (connectionTask is not null) { await connectionTask; } var result = await NodeDataMigrator.MigrateAsync( sourceRoot, targetRoot, CancellationToken.None); NodeDataRootSettings.SaveUserRoot(result.TargetDirectory); return result; } catch { StartConnection(); throw; } } private void PostStatus(NodeStatus status) { if (form.IsHandleCreated) { form.BeginInvoke(() => UpdateStatus(status)); } } private void UpdateStatus(NodeStatus status) { current = status; form.ApplyStatus(status, config); statusItem.Text = status.Message; tray.Text = Limit($"zcbot Windows Node - {status.Message}", 63); var oldIcon = tray.Icon; tray.Icon = TrayIconFactory.Create(status.State); oldIcon?.Dispose(); } private void ShowConfiguration() { form.ApplyStatus(current, config); form.Show(); form.WindowState = FormWindowState.Normal; form.Activate(); } private async Task ExitNodeAsync() { if (exiting) { return; } var activeJobs = new JobInboxStore(NodePaths.ForCurrentMachine().JobsDirectory) .ReadJobSnapshots() .Count(item => item.IsActive); var cancelJobs = false; if (activeJobs > 0) { var choice = MessageBox.Show( $"当前有 {activeJobs} 个本机任务尚未结束。\n\n" + "选择“是”:停止接收新任务,等待现有任务完成后退出。\n" + "选择“否”:取消现有任务并退出。\n" + "选择“取消”:返回,不退出。", "退出 zcbot Windows Node", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button1); if (choice == DialogResult.Cancel) { return; } cancelJobs = choice == DialogResult.No; if (cancelJobs && MessageBox.Show( "取消任务会终止正在运行的专业软件进程,未完成结果不会上传。确认取消任务并退出?", "确认取消任务", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) != DialogResult.Yes) { return; } } exiting = true; exitItem.Enabled = false; if (tray.ContextMenuStrip is not null) { tray.ContextMenuStrip.Enabled = false; } form.Enabled = false; UpdateStatus(NodeStatus.Create( NodeState.Stopped, cancelJobs ? "正在取消本机任务并退出…" : activeJobs > 0 ? "等待本机任务完成后退出…" : "正在退出…")); if (cancelJobs) { connectionLoop?.CancelActiveJobsForExit(); } connectionStop?.Cancel(); if (connectionTask is not null) { await connectionTask; } tray.Visible = false; ExitThread(); } private static string Limit(string value, int length) => value.Length <= length ? value : value[..length]; }