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