zcbot/windows-node/Zcbot.WindowsNode/TrayApplicationContext.cs

291 lines
9.5 KiB
C#

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<NodeDataMigrationResult> 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 = 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 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];
private static string CompactMenuText(string value, int length)
{
var singleLine = value.Replace('\r', ' ').Replace('\n', ' ').Trim();
return singleLine.Length <= length
? singleLine
: singleLine[..(length - 1)] + "…";
}
}