307 lines
15 KiB
C#
307 lines
15 KiB
C#
using System.Diagnostics;
|
|
|
|
namespace Dada.Supervisor;
|
|
|
|
internal sealed class SupervisorForm : Form
|
|
{
|
|
private readonly NotifyIcon trayIcon;
|
|
private readonly Label statusTitle;
|
|
private readonly Label statusDetail;
|
|
private readonly Label apiStatus;
|
|
private readonly Label workerStatus;
|
|
private readonly Label storageStatus;
|
|
private readonly FlowLayoutPanel actions;
|
|
private readonly ToolStripMenuItem openMenuItem;
|
|
private readonly ToolStripMenuItem diagnosticsMenuItem;
|
|
private readonly ToolStripMenuItem restartMenuItem;
|
|
private readonly string correlationId = $"SUP-{Guid.NewGuid():N}"[..12].ToUpperInvariant();
|
|
private SupervisorState state;
|
|
|
|
internal SupervisorForm(SupervisorState initialState = SupervisorState.Starting)
|
|
{
|
|
state = initialState;
|
|
AutoScaleMode = AutoScaleMode.Dpi;
|
|
BackColor = Color.FromArgb(246, 247, 249);
|
|
ClientSize = new Size(480, 480);
|
|
Font = new Font("Segoe UI", 9F);
|
|
FormBorderStyle = FormBorderStyle.FixedDialog;
|
|
MaximizeBox = false;
|
|
StartPosition = FormStartPosition.CenterScreen;
|
|
Text = "Dada";
|
|
|
|
var header = new Panel { BackColor = Color.FromArgb(24, 28, 34), Dock = DockStyle.Top, Height = 126, Padding = new Padding(28, 20, 28, 16) };
|
|
var brand = new Label { AutoSize = true, Font = new Font("Segoe UI Semibold", 12F), ForeColor = Color.White, Location = new Point(28, 18), Text = "DADA" };
|
|
statusTitle = new Label { AutoEllipsis = true, Font = new Font("Microsoft YaHei UI", 15F, FontStyle.Bold), ForeColor = Color.White, Location = new Point(28, 49), Size = new Size(420, 32) };
|
|
statusDetail = new Label { Font = new Font("Microsoft YaHei UI", 9F), ForeColor = Color.FromArgb(181, 188, 198), Location = new Point(28, 82), Size = new Size(420, 38) };
|
|
header.Controls.AddRange([brand, statusTitle, statusDetail]);
|
|
|
|
var componentPanel = new TableLayoutPanel
|
|
{
|
|
BackColor = Color.White,
|
|
ColumnCount = 2,
|
|
Dock = DockStyle.Top,
|
|
Height = 172,
|
|
Padding = new Padding(28, 20, 28, 12),
|
|
RowCount = 3,
|
|
};
|
|
componentPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 62));
|
|
componentPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 38));
|
|
componentPanel.RowStyles.Add(new RowStyle(SizeType.Percent, 33));
|
|
componentPanel.RowStyles.Add(new RowStyle(SizeType.Percent, 33));
|
|
componentPanel.RowStyles.Add(new RowStyle(SizeType.Percent, 34));
|
|
apiStatus = AddStatusRow(componentPanel, 0, "本机 API", ApiStatus(initialState));
|
|
workerStatus = AddStatusRow(componentPanel, 1, "后台 Worker", WorkerStatus(initialState));
|
|
storageStatus = AddStatusRow(componentPanel, 2, "本地数据", StorageStatus(initialState));
|
|
|
|
actions = new FlowLayoutPanel
|
|
{
|
|
BackColor = Color.FromArgb(246, 247, 249),
|
|
Dock = DockStyle.Fill,
|
|
FlowDirection = FlowDirection.LeftToRight,
|
|
Padding = new Padding(24, 24, 20, 16),
|
|
WrapContents = true,
|
|
};
|
|
|
|
Controls.Add(actions);
|
|
Controls.Add(componentPanel);
|
|
Controls.Add(header);
|
|
|
|
var menu = new ContextMenuStrip();
|
|
openMenuItem = new ToolStripMenuItem("打开 Dada", null, (_, _) => OpenProduct());
|
|
menu.Items.Add(openMenuItem);
|
|
menu.Items.Add("运行状态", null, (_, _) => RestoreWindow());
|
|
diagnosticsMenuItem = new ToolStripMenuItem("打开诊断", null, (_, _) => ShowDiagnostics());
|
|
menu.Items.Add(diagnosticsMenuItem);
|
|
restartMenuItem = new ToolStripMenuItem("重新启动服务", null, (_, _) =>
|
|
{
|
|
SetState(SupervisorState.Starting);
|
|
RestartRequested?.Invoke();
|
|
});
|
|
menu.Items.Add(restartMenuItem);
|
|
menu.Items.Add(new ToolStripSeparator());
|
|
menu.Items.Add("退出 Dada", null, (_, _) => Close());
|
|
trayIcon = new NotifyIcon
|
|
{
|
|
ContextMenuStrip = menu,
|
|
Icon = SystemIcons.Application,
|
|
Text = TrayText(initialState),
|
|
Visible = !SystemInformation.UserInteractive ? false : true,
|
|
};
|
|
trayIcon.DoubleClick += (_, _) => RestoreWindow();
|
|
|
|
FormClosing += (_, _) => trayIcon.Visible = false;
|
|
Resize += (_, _) =>
|
|
{
|
|
if (WindowState == FormWindowState.Minimized) Hide();
|
|
};
|
|
SetState(initialState);
|
|
}
|
|
|
|
internal event Action? RestartRequested;
|
|
|
|
internal IReadOnlyList<string> TrayMenuLabels => trayIcon.ContextMenuStrip?.Items
|
|
.OfType<ToolStripMenuItem>()
|
|
.Select(item => item.Text ?? string.Empty)
|
|
.ToArray() ?? [];
|
|
|
|
internal void SetState(SupervisorState nextState)
|
|
{
|
|
state = nextState;
|
|
(statusTitle.Text, statusDetail.Text) = nextState switch
|
|
{
|
|
SupervisorState.Starting => ("正在启动本机服务", "请稍候,Dada 正在检查 API、Worker 和本地数据。"),
|
|
SupervisorState.Ready => ("Dada 已就绪", "本机服务运行正常。"),
|
|
SupervisorState.StartupFailed => ("Dada 启动失败", $"服务未能启动 · {DateTimeOffset.Now:HH:mm:ss} · 关联 ID {correlationId}\n诊断信息已移除敏感内容。"),
|
|
SupervisorState.PortInUse => ("Dada 无法使用固定端口启动", "固定端口 43121 已被占用,Dada 不会改用其他端口。"),
|
|
SupervisorState.ApiDegraded => ("本机 API 不可用", "网页暂时无法使用,可打开诊断或重新启动服务。"),
|
|
SupervisorState.WorkerDegraded => ("后台服务暂时不可用", "可继续查看、下载、编辑和删除;暂不能创建新任务。"),
|
|
SupervisorState.StorageFull => ("本地存储空间已满", "可继续查看和导出;清理空间后再创建新内容。"),
|
|
SupervisorState.StorageUnavailable => ("本地数据暂时不可用", "请恢复已配置的数据目录,然后重试。"),
|
|
SupervisorState.DiagnosticsRunning => ("正在运行诊断", "正在检查本机服务状态。"),
|
|
SupervisorState.DiagnosticsReady => ("诊断已完成", "可复制已脱敏的诊断结果。"),
|
|
_ => throw new ArgumentOutOfRangeException(nameof(nextState)),
|
|
};
|
|
trayIcon.Text = TrayText(nextState);
|
|
SetStatusLabel(apiStatus, ApiStatus(nextState));
|
|
SetStatusLabel(workerStatus, WorkerStatus(nextState));
|
|
SetStatusLabel(storageStatus, StorageStatus(nextState));
|
|
var allowed = SupervisorActions.For(nextState);
|
|
openMenuItem.Enabled = allowed.Contains(SupervisorAction.OpenProduct);
|
|
diagnosticsMenuItem.Enabled = allowed.Contains(SupervisorAction.OpenDiagnostics);
|
|
restartMenuItem.Visible = allowed.Contains(SupervisorAction.RestartServices);
|
|
RenderActions();
|
|
}
|
|
|
|
internal void RestoreWindow()
|
|
{
|
|
Show();
|
|
WindowState = FormWindowState.Normal;
|
|
Activate();
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (disposing) trayIcon.Dispose();
|
|
base.Dispose(disposing);
|
|
}
|
|
|
|
private void RenderActions()
|
|
{
|
|
actions.SuspendLayout();
|
|
actions.Controls.Clear();
|
|
foreach (var action in SupervisorActions.For(state))
|
|
{
|
|
var button = new Button
|
|
{
|
|
AutoSize = false,
|
|
BackColor = action is SupervisorAction.Retry or SupervisorAction.OpenProduct ? Color.FromArgb(28, 32, 38) : Color.White,
|
|
FlatStyle = FlatStyle.Flat,
|
|
ForeColor = action is SupervisorAction.Retry or SupervisorAction.OpenProduct ? Color.White : Color.FromArgb(33, 37, 43),
|
|
Height = 38,
|
|
Margin = new Padding(4),
|
|
Text = ActionLabel(action),
|
|
Width = action switch
|
|
{
|
|
SupervisorAction.OpenChrome => 132,
|
|
SupervisorAction.OpenEdge => 122,
|
|
SupervisorAction.OpenDiagnostics => 122,
|
|
SupervisorAction.CheckExistingInstance => 132,
|
|
SupervisorAction.RestartServices => 122,
|
|
_ => 112,
|
|
},
|
|
};
|
|
button.FlatAppearance.BorderColor = Color.FromArgb(215, 219, 225);
|
|
button.Click += (_, _) => InvokeAction(action);
|
|
actions.Controls.Add(button);
|
|
}
|
|
actions.ResumeLayout();
|
|
}
|
|
|
|
private void InvokeAction(SupervisorAction action)
|
|
{
|
|
switch (action)
|
|
{
|
|
case SupervisorAction.OpenProduct:
|
|
OpenProduct();
|
|
break;
|
|
case SupervisorAction.OpenChrome:
|
|
SupportedBrowserLauncher.OpenChrome(LoopbackEndpoint.ProductUri);
|
|
break;
|
|
case SupervisorAction.OpenEdge:
|
|
SupportedBrowserLauncher.OpenEdge(LoopbackEndpoint.ProductUri);
|
|
break;
|
|
case SupervisorAction.OpenDiagnostics:
|
|
case SupervisorAction.CheckExistingInstance:
|
|
ShowDiagnostics();
|
|
break;
|
|
case SupervisorAction.Retry:
|
|
case SupervisorAction.RestartServices:
|
|
SetState(SupervisorState.Starting);
|
|
RestartRequested?.Invoke();
|
|
break;
|
|
case SupervisorAction.Exit:
|
|
Close();
|
|
break;
|
|
}
|
|
}
|
|
|
|
internal static bool OpenProductInSupportedBrowser() => SupportedBrowserLauncher.OpenDefaultSupported(LoopbackEndpoint.ProductUri);
|
|
|
|
private static void OpenProduct() => OpenProductInSupportedBrowser();
|
|
|
|
private void ShowDiagnostics()
|
|
{
|
|
using var diagnostics = new DiagnosticsForm(state);
|
|
diagnostics.ShowDialog(this);
|
|
}
|
|
|
|
private static Label AddStatusRow(TableLayoutPanel panel, int row, string label, string status)
|
|
{
|
|
panel.Controls.Add(new Label { Anchor = AnchorStyles.Left, AutoSize = true, Font = new Font("Microsoft YaHei UI", 10F), Text = label }, 0, row);
|
|
var statusLabel = new Label { Anchor = AnchorStyles.Right, AutoSize = true, Font = new Font("Microsoft YaHei UI", 9F), Text = status };
|
|
SetStatusLabel(statusLabel, status);
|
|
panel.Controls.Add(statusLabel, 1, row);
|
|
return statusLabel;
|
|
}
|
|
|
|
private static void SetStatusLabel(Label label, string status)
|
|
{
|
|
label.Text = status;
|
|
label.ForeColor = status is "可用" or "正常" ? Color.FromArgb(28, 122, 76) : Color.FromArgb(180, 86, 32);
|
|
}
|
|
|
|
private static string ApiStatus(SupervisorState value) => value is SupervisorState.ApiDegraded or SupervisorState.StartupFailed or SupervisorState.PortInUse ? "不可用" : value == SupervisorState.Starting ? "启动中" : "正常";
|
|
private static string WorkerStatus(SupervisorState value) => value is SupervisorState.WorkerDegraded or SupervisorState.StartupFailed or SupervisorState.PortInUse ? "不可用" : value == SupervisorState.Starting ? "启动中" : "正常";
|
|
private static string TrayText(SupervisorState value) => value switch
|
|
{
|
|
SupervisorState.Starting => "Dada - 正在启动",
|
|
SupervisorState.Ready => "Dada - 运行正常",
|
|
SupervisorState.ApiDegraded => "Dada - API 不可用",
|
|
SupervisorState.WorkerDegraded => "Dada - Worker 不可用",
|
|
SupervisorState.StorageFull => "Dada - 存储空间已满",
|
|
SupervisorState.StorageUnavailable => "Dada - 本地数据不可用",
|
|
SupervisorState.PortInUse => "Dada - 固定端口被占用",
|
|
SupervisorState.StartupFailed => "Dada - 启动失败",
|
|
_ => "Dada - 运行诊断",
|
|
};
|
|
|
|
private static string StorageStatus(SupervisorState value) => value switch
|
|
{
|
|
SupervisorState.StorageFull => "已满",
|
|
SupervisorState.StorageUnavailable => "不可用",
|
|
_ => "可用",
|
|
};
|
|
|
|
private static string ActionLabel(SupervisorAction action) => action switch
|
|
{
|
|
SupervisorAction.OpenProduct => "打开 Dada",
|
|
SupervisorAction.OpenChrome => "使用 Chrome 打开",
|
|
SupervisorAction.OpenEdge => "使用 Edge 打开",
|
|
SupervisorAction.OpenDiagnostics => "打开诊断",
|
|
SupervisorAction.CheckExistingInstance => "检查已有 Dada",
|
|
SupervisorAction.Retry => "重试",
|
|
SupervisorAction.RestartServices => "重新启动服务",
|
|
SupervisorAction.Exit => "退出 Dada",
|
|
_ => throw new ArgumentOutOfRangeException(nameof(action)),
|
|
};
|
|
}
|
|
|
|
internal sealed class DiagnosticsForm : Form
|
|
{
|
|
internal DiagnosticsForm(SupervisorState state)
|
|
{
|
|
BackColor = Color.White;
|
|
ClientSize = new Size(760, 440);
|
|
Font = new Font("Microsoft YaHei UI", 9F);
|
|
StartPosition = FormStartPosition.CenterParent;
|
|
Text = "Dada 运行诊断";
|
|
var title = new Label { AutoSize = true, Font = new Font("Microsoft YaHei UI", 15F, FontStyle.Bold), Location = new Point(28, 24), Text = "运行诊断" };
|
|
var detail = new Label { ForeColor = Color.FromArgb(91, 97, 105), Location = new Point(30, 62), Size = new Size(700, 42), Text = "诊断仅包含服务状态、固定端口、目录可用性和凭据是否已配置,不包含凭据值或用户内容。" };
|
|
var checks = new ListView { Location = new Point(30, 116), Size = new Size(700, 250), View = View.Details, FullRowSelect = true };
|
|
checks.Columns.Add("组件", 170);
|
|
checks.Columns.Add("状态", 110);
|
|
checks.Columns.Add("检查结果", 390);
|
|
checks.Items.Add(new ListViewItem(["Supervisor", "正常", StateLabel(state)]));
|
|
checks.Items.Add(new ListViewItem(["固定端口", "43121", "仅绑定 127.0.0.1"]));
|
|
checks.Items.Add(new ListViewItem(["凭据", "已脱敏", "仅显示是否已配置"]));
|
|
var copy = new Button { Location = new Point(592, 382), Size = new Size(138, 36), Text = "复制脱敏结果" };
|
|
copy.Click += (_, _) => Clipboard.SetText("Dada diagnostics: redacted");
|
|
Controls.AddRange([title, detail, checks, copy]);
|
|
}
|
|
|
|
private static string StateLabel(SupervisorState state) => state switch
|
|
{
|
|
SupervisorState.Starting => "正在启动",
|
|
SupervisorState.Ready => "运行正常",
|
|
SupervisorState.StartupFailed => "启动失败",
|
|
SupervisorState.PortInUse => "固定端口被占用",
|
|
SupervisorState.ApiDegraded => "API 不可用",
|
|
SupervisorState.WorkerDegraded => "Worker 不可用",
|
|
SupervisorState.StorageFull => "存储空间已满",
|
|
SupervisorState.StorageUnavailable => "本地数据不可用",
|
|
SupervisorState.DiagnosticsRunning => "诊断中",
|
|
SupervisorState.DiagnosticsReady => "诊断完成",
|
|
_ => "未知",
|
|
};
|
|
}
|