Files
LanMountainDesktop/LanAirApp/samples/LanMountainDesktop.SamplePlugin/SamplePluginRuntimeStatus.cs

525 lines
17 KiB
C#
Raw Normal View History

2026-03-09 12:27:33 +08:00
using System.Globalization;
using System.IO;
2026-03-09 14:14:50 +08:00
using System.Linq;
2026-03-09 12:27:33 +08:00
using System.Threading;
2026-03-09 14:14:50 +08:00
using LanMountainDesktop.PluginSdk;
2026-03-09 12:27:33 +08:00
namespace LanMountainDesktop.SamplePlugin;
internal enum SamplePluginHealthState
{
Healthy,
Pending,
Faulted
}
internal sealed record SamplePluginStatusEntry(
string Key,
string Title,
SamplePluginHealthState State,
string Summary,
string Detail,
DateTimeOffset UpdatedAt);
2026-03-09 14:14:50 +08:00
internal sealed record SamplePluginCapabilityItem(
string Title,
string Detail);
internal sealed record SamplePluginRuntimeSnapshot(
PluginManifest Manifest,
string PluginDirectory,
string DataDirectory,
string HostApplicationName,
string HostVersion,
string SdkApiVersion,
IReadOnlyList<SamplePluginStatusEntry> StatusEntries,
bool HasPlacedComponent,
int PlacedCount,
int PreviewCount,
IReadOnlyList<string> PlacementIds,
string? LastComponentId,
double LastCellSize,
DateTimeOffset? ServiceClockTime);
internal sealed record SamplePluginClockTickMessage(DateTimeOffset CurrentTime);
internal sealed record SamplePluginStateChangedMessage(string Reason);
internal sealed record SamplePluginComponentInstance(
string ComponentId,
string? PlacementId,
double CellSize)
2026-03-09 12:27:33 +08:00
{
2026-03-09 14:14:50 +08:00
public bool IsPlaced => !string.IsNullOrWhiteSpace(PlacementId);
}
2026-03-09 12:27:33 +08:00
2026-03-09 14:14:50 +08:00
internal sealed class SamplePluginRuntimeStateService
{
private readonly object _gate = new();
private readonly IPluginMessageBus _messageBus;
private readonly Dictionary<string, SamplePluginComponentInstance> _componentInstances =
new(StringComparer.OrdinalIgnoreCase);
2026-03-09 12:27:33 +08:00
2026-03-09 14:14:50 +08:00
private readonly PluginManifest _manifest;
private readonly string _pluginDirectory;
private readonly string _dataDirectory;
private readonly string _hostApplicationName;
private readonly string _hostVersion;
private readonly string _sdkApiVersion;
2026-03-10 00:40:26 +08:00
private readonly PluginLocalizer _localizer;
2026-03-09 14:14:50 +08:00
private SamplePluginStatusEntry _frontend;
private SamplePluginStatusEntry _component;
private SamplePluginStatusEntry _backend;
private SamplePluginStatusEntry _service;
private string? _lastComponentId;
private double _lastCellSize;
private DateTimeOffset? _serviceClockTime;
public SamplePluginRuntimeStateService(
PluginManifest manifest,
string pluginDirectory,
string dataDirectory,
string hostApplicationName,
string hostVersion,
string sdkApiVersion,
2026-03-10 00:40:26 +08:00
IPluginMessageBus messageBus,
PluginLocalizer localizer)
2026-03-09 14:14:50 +08:00
{
_manifest = manifest;
_pluginDirectory = pluginDirectory;
_dataDirectory = dataDirectory;
_hostApplicationName = hostApplicationName;
_hostVersion = hostVersion;
_sdkApiVersion = sdkApiVersion;
_messageBus = messageBus;
2026-03-10 00:40:26 +08:00
_localizer = localizer;
2026-03-09 14:14:50 +08:00
_frontend = CreateEntry(
"frontend",
2026-03-10 00:40:26 +08:00
T("status.frontend.title", "前端状态"),
2026-03-09 14:14:50 +08:00
SamplePluginHealthState.Pending,
2026-03-10 00:40:26 +08:00
T("status.summary.pending", "等待中"),
T("status.frontend.detail.pending", "等待插件界面接入。"));
2026-03-09 14:14:50 +08:00
_component = CreateEntry(
"component",
2026-03-10 00:40:26 +08:00
T("status.component.title", "组件状态"),
2026-03-09 14:14:50 +08:00
SamplePluginHealthState.Pending,
2026-03-10 00:40:26 +08:00
T("status.summary.pending", "等待中"),
T("status.component.detail.pending", "当前还没有创建组件实例。"));
2026-03-09 12:27:33 +08:00
2026-03-09 14:14:50 +08:00
_backend = CreateEntry(
"backend",
2026-03-10 00:40:26 +08:00
T("status.backend.title", "后端状态"),
2026-03-09 14:14:50 +08:00
SamplePluginHealthState.Pending,
2026-03-10 00:40:26 +08:00
T("status.summary.pending", "等待中"),
T("status.backend.detail.pending", "插件初始化进行中。"));
2026-03-09 14:14:50 +08:00
_service = CreateEntry(
"service",
2026-03-10 00:40:26 +08:00
T("status.service.title", "时钟服务"),
2026-03-09 14:14:50 +08:00
SamplePluginHealthState.Pending,
2026-03-10 00:40:26 +08:00
T("status.summary.pending", "等待中"),
T("status.service.detail.pending", "时钟服务尚未挂接。"));
2026-03-09 14:14:50 +08:00
}
public void AttachClockService(SamplePluginClockService clockService)
{
ArgumentNullException.ThrowIfNull(clockService);
lock (_gate)
{
_serviceClockTime = clockService.CurrentTime;
2026-03-09 12:27:33 +08:00
_service = CreateEntry(
"service",
2026-03-10 00:40:26 +08:00
T("status.service.title", "时钟服务"),
2026-03-09 12:27:33 +08:00
SamplePluginHealthState.Pending,
2026-03-10 00:40:26 +08:00
T("status.summary.attached", "已挂接"),
T("status.service.detail.attached", "时钟服务已挂接,正在等待第一次心跳。"));
2026-03-09 12:27:33 +08:00
}
2026-03-09 14:14:50 +08:00
PublishStateChanged("Clock service attached");
2026-03-09 12:27:33 +08:00
}
2026-03-09 14:14:50 +08:00
public void MarkFrontendReady(string detail)
2026-03-09 12:27:33 +08:00
{
2026-03-09 14:14:50 +08:00
lock (_gate)
2026-03-09 12:27:33 +08:00
{
_frontend = CreateEntry(
"frontend",
2026-03-10 00:40:26 +08:00
T("status.frontend.title", "前端状态"),
2026-03-09 12:27:33 +08:00
SamplePluginHealthState.Healthy,
2026-03-10 00:40:26 +08:00
T("status.summary.healthy", "正常"),
2026-03-09 12:27:33 +08:00
detail);
}
2026-03-09 14:14:50 +08:00
PublishStateChanged("Frontend updated");
2026-03-09 12:27:33 +08:00
}
2026-03-09 14:14:50 +08:00
public void MarkBackendReady(string detail)
2026-03-09 12:27:33 +08:00
{
2026-03-09 14:14:50 +08:00
lock (_gate)
2026-03-09 12:27:33 +08:00
{
_backend = CreateEntry(
"backend",
2026-03-10 00:40:26 +08:00
T("status.backend.title", "后端状态"),
2026-03-09 12:27:33 +08:00
SamplePluginHealthState.Healthy,
2026-03-10 00:40:26 +08:00
T("status.summary.healthy", "正常"),
2026-03-09 12:27:33 +08:00
detail);
}
2026-03-09 14:14:50 +08:00
PublishStateChanged("Backend updated");
2026-03-09 12:27:33 +08:00
}
2026-03-09 14:14:50 +08:00
public void MarkBackendFaulted(string detail)
2026-03-09 12:27:33 +08:00
{
2026-03-09 14:14:50 +08:00
lock (_gate)
2026-03-09 12:27:33 +08:00
{
_backend = CreateEntry(
"backend",
2026-03-10 00:40:26 +08:00
T("status.backend.title", "后端状态"),
2026-03-09 12:27:33 +08:00
SamplePluginHealthState.Faulted,
2026-03-10 00:40:26 +08:00
T("status.summary.faulted", "异常"),
2026-03-09 12:27:33 +08:00
detail);
}
2026-03-09 14:14:50 +08:00
PublishStateChanged("Backend faulted");
2026-03-09 12:27:33 +08:00
}
2026-03-09 14:14:50 +08:00
public void MarkClockServiceTick(DateTimeOffset currentTime)
2026-03-09 12:27:33 +08:00
{
2026-03-09 14:14:50 +08:00
lock (_gate)
2026-03-09 12:27:33 +08:00
{
2026-03-09 14:14:50 +08:00
_serviceClockTime = currentTime;
2026-03-09 12:27:33 +08:00
_service = CreateEntry(
"service",
2026-03-10 00:40:26 +08:00
T("status.service.title", "时钟服务"),
2026-03-09 12:27:33 +08:00
SamplePluginHealthState.Healthy,
2026-03-10 00:40:26 +08:00
T("status.summary.healthy", "正常"),
Tf(
"status.service.detail.running",
"时钟服务运行中,当前服务时间:{0}",
currentTime.LocalDateTime.ToString("HH:mm:ss")));
2026-03-09 12:27:33 +08:00
}
2026-03-09 14:14:50 +08:00
PublishStateChanged("Clock service tick");
2026-03-09 12:27:33 +08:00
}
2026-03-09 14:14:50 +08:00
public void MarkClockServiceFaulted(string detail)
2026-03-09 12:27:33 +08:00
{
2026-03-09 14:14:50 +08:00
lock (_gate)
2026-03-09 12:27:33 +08:00
{
_service = CreateEntry(
"service",
2026-03-10 00:40:26 +08:00
T("status.service.title", "时钟服务"),
2026-03-09 12:27:33 +08:00
SamplePluginHealthState.Faulted,
2026-03-10 00:40:26 +08:00
T("status.summary.faulted", "异常"),
2026-03-09 12:27:33 +08:00
detail);
}
2026-03-09 14:14:50 +08:00
PublishStateChanged("Clock service faulted");
2026-03-09 12:27:33 +08:00
}
2026-03-09 14:14:50 +08:00
public string RegisterComponentInstance(string componentId, string? placementId, double cellSize)
2026-03-09 12:27:33 +08:00
{
2026-03-09 14:14:50 +08:00
var instanceId = Guid.NewGuid().ToString("N");
lock (_gate)
2026-03-09 12:27:33 +08:00
{
2026-03-09 14:14:50 +08:00
_componentInstances[instanceId] = new SamplePluginComponentInstance(componentId, placementId, cellSize);
_lastComponentId = componentId;
_lastCellSize = cellSize;
UpdateComponentStatusNoLock();
2026-03-09 12:27:33 +08:00
}
2026-03-09 14:14:50 +08:00
PublishStateChanged("Component attached");
return instanceId;
}
public void UnregisterComponentInstance(string instanceId)
{
ArgumentException.ThrowIfNullOrWhiteSpace(instanceId);
var removed = false;
lock (_gate)
{
removed = _componentInstances.Remove(instanceId);
if (removed)
{
UpdateComponentStatusNoLock();
}
}
if (removed)
{
PublishStateChanged("Component detached");
}
}
public SamplePluginRuntimeSnapshot GetSnapshot()
{
lock (_gate)
{
var placementIds = _componentInstances.Values
.Where(instance => instance.IsPlaced)
.Select(instance => instance.PlacementId!)
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(id => id, StringComparer.OrdinalIgnoreCase)
.ToArray();
var previewCount = _componentInstances.Values.Count(instance => !instance.IsPlaced);
return new SamplePluginRuntimeSnapshot(
_manifest,
_pluginDirectory,
_dataDirectory,
_hostApplicationName,
_hostVersion,
_sdkApiVersion,
[_frontend, _component, _backend, _service],
placementIds.Length > 0,
placementIds.Length,
previewCount,
placementIds,
_lastComponentId,
_lastCellSize,
_serviceClockTime);
}
}
public IReadOnlyList<SamplePluginCapabilityItem> GetCapabilities(
IPluginContext context,
bool hasStateService,
bool hasClockService,
bool hasMessageBus)
{
ArgumentNullException.ThrowIfNull(context);
var propertyNames = context.Properties.Count == 0
2026-03-10 00:40:26 +08:00
? T("common.none", "(无)")
2026-03-09 14:14:50 +08:00
: string.Join(", ", context.Properties.Keys.OrderBy(key => key, StringComparer.OrdinalIgnoreCase));
return
[
new SamplePluginCapabilityItem(
2026-03-10 00:40:26 +08:00
T("capability.manifest.title", "IPluginContext.Manifest"),
Tf(
"capability.manifest.detail",
"可读取。当前插件 id{0};版本:{1}。",
context.Manifest.Id,
context.Manifest.Version ?? T("common.dev", "开发版"))),
2026-03-09 14:14:50 +08:00
new SamplePluginCapabilityItem(
2026-03-10 00:40:26 +08:00
T("capability.directories.title", "IPluginContext.PluginDirectory / DataDirectory"),
Tf(
"capability.directories.detail",
"可读取。插件目录:{0};数据目录:{1}。",
context.PluginDirectory,
context.DataDirectory)),
2026-03-09 14:14:50 +08:00
new SamplePluginCapabilityItem(
2026-03-10 00:40:26 +08:00
T("capability.properties.title", "IPluginContext.Properties"),
Tf(
"capability.properties.detail",
"可读取。宿主当前暴露的属性:{0}。",
propertyNames)),
2026-03-09 14:14:50 +08:00
new SamplePluginCapabilityItem(
2026-03-10 00:40:26 +08:00
T("capability.get_service.title", "IPluginContext.GetService<T>()"),
Tf(
"capability.get_service.detail",
"可调用。状态服务已解析:{0};时钟服务已解析:{1};消息总线已解析:{2}。",
FormatBoolean(hasStateService),
FormatBoolean(hasClockService),
FormatBoolean(hasMessageBus))),
2026-03-09 14:14:50 +08:00
new SamplePluginCapabilityItem(
2026-03-10 00:40:26 +08:00
T("capability.register_service.title", "IPluginContext.RegisterService<TService>()"),
T(
"capability.register_service.detail",
"可在插件初始化阶段调用。这个示例插件会把 SamplePluginRuntimeStateService 和 SamplePluginClockService 注册进插件服务容器。")),
2026-03-09 14:14:50 +08:00
new SamplePluginCapabilityItem(
2026-03-10 00:40:26 +08:00
T("capability.message_bus.title", "插件通信总线"),
T(
"capability.message_bus.detail",
"这个示例插件通过 IPluginMessageBus 向插件 UI 推送时钟心跳和状态变化通知。")),
2026-03-09 14:14:50 +08:00
new SamplePluginCapabilityItem(
2026-03-10 00:40:26 +08:00
T("capability.widget_context.title", "PluginDesktopComponentContext"),
T(
"capability.widget_context.detail",
"组件可以读取 ComponentId、PlacementId、CellSize并能在同一个插件服务容器上调用 GetService<T>()。"))
2026-03-09 14:14:50 +08:00
];
}
private void UpdateComponentStatusNoLock()
{
var placementIds = _componentInstances.Values
.Where(instance => instance.IsPlaced)
.Select(instance => instance.PlacementId!)
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(id => id, StringComparer.OrdinalIgnoreCase)
.ToArray();
var previewCount = _componentInstances.Values.Count(instance => !instance.IsPlaced);
if (placementIds.Length > 0)
{
_component = CreateEntry(
"component",
2026-03-10 00:40:26 +08:00
T("status.component.title", "组件状态"),
2026-03-09 14:14:50 +08:00
SamplePluginHealthState.Healthy,
2026-03-10 00:40:26 +08:00
T("status.summary.placed", "已放置"),
Tf(
"status.component.detail.placed",
"已放置数量:{0};预览数量:{1};放置位置:{2}",
placementIds.Length,
previewCount,
string.Join(", ", placementIds)));
2026-03-09 14:14:50 +08:00
return;
}
if (previewCount > 0)
{
_component = CreateEntry(
"component",
2026-03-10 00:40:26 +08:00
T("status.component.title", "组件状态"),
2026-03-09 14:14:50 +08:00
SamplePluginHealthState.Healthy,
2026-03-10 00:40:26 +08:00
T("status.summary.preview", "预览中"),
Tf(
"status.component.detail.preview",
"当前预览实例数量:{0};尚未有已放置的桌面实例。",
previewCount));
2026-03-09 14:14:50 +08:00
return;
}
_component = CreateEntry(
"component",
2026-03-10 00:40:26 +08:00
T("status.component.title", "组件状态"),
2026-03-09 14:14:50 +08:00
SamplePluginHealthState.Pending,
2026-03-10 00:40:26 +08:00
T("status.summary.pending", "等待中"),
T("status.component.detail.none", "当前没有活动中的组件实例。"));
2026-03-09 14:14:50 +08:00
}
private void PublishStateChanged(string reason)
{
_messageBus.Publish(new SamplePluginStateChangedMessage(reason));
2026-03-09 12:27:33 +08:00
}
private static SamplePluginStatusEntry CreateEntry(
string key,
string title,
SamplePluginHealthState state,
string summary,
string detail)
{
return new SamplePluginStatusEntry(
key,
title,
state,
summary,
detail,
DateTimeOffset.Now);
}
2026-03-10 00:40:26 +08:00
private string T(string key, string fallback)
{
return _localizer.GetString(key, fallback);
}
private string Tf(string key, string fallback, params object[] args)
{
return _localizer.Format(key, fallback, args);
}
private string FormatBoolean(bool value)
{
return value
? T("common.true", "是")
: T("common.false", "否");
}
2026-03-09 12:27:33 +08:00
}
2026-03-09 14:14:50 +08:00
internal sealed class SamplePluginClockService : IDisposable
2026-03-09 12:27:33 +08:00
{
2026-03-09 14:14:50 +08:00
private readonly object _gate = new();
private readonly string _clockStateFilePath;
private readonly SamplePluginRuntimeStateService _stateService;
private readonly IPluginMessageBus _messageBus;
2026-03-10 00:40:26 +08:00
private readonly PluginLocalizer _localizer;
2026-03-09 12:27:33 +08:00
private readonly Timer _timer;
2026-03-09 14:14:50 +08:00
private DateTimeOffset _currentTime = DateTimeOffset.Now;
2026-03-09 12:27:33 +08:00
private int _disposed;
2026-03-09 14:14:50 +08:00
public SamplePluginClockService(
string dataDirectory,
SamplePluginRuntimeStateService stateService,
2026-03-10 00:40:26 +08:00
IPluginMessageBus messageBus,
PluginLocalizer localizer)
2026-03-09 12:27:33 +08:00
{
2026-03-09 14:14:50 +08:00
_clockStateFilePath = Path.Combine(dataDirectory, "clock-service.txt");
_stateService = stateService;
_messageBus = messageBus;
2026-03-10 00:40:26 +08:00
_localizer = localizer;
2026-03-09 12:27:33 +08:00
_timer = new Timer(OnTimerTick);
}
2026-03-09 14:14:50 +08:00
public DateTimeOffset CurrentTime
{
get
{
lock (_gate)
{
return _currentTime;
}
}
}
2026-03-09 12:27:33 +08:00
public void Start()
{
2026-03-09 14:14:50 +08:00
PublishTick();
_timer.Change(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
2026-03-09 12:27:33 +08:00
}
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0)
{
return;
}
_timer.Dispose();
}
private void OnTimerTick(object? state)
{
2026-03-09 14:14:50 +08:00
PublishTick();
2026-03-09 12:27:33 +08:00
}
2026-03-09 14:14:50 +08:00
private void PublishTick()
2026-03-09 12:27:33 +08:00
{
if (Volatile.Read(ref _disposed) != 0)
{
return;
}
var now = DateTimeOffset.Now;
2026-03-09 14:14:50 +08:00
lock (_gate)
{
_currentTime = now;
}
2026-03-09 12:27:33 +08:00
try
{
File.WriteAllText(
2026-03-09 14:14:50 +08:00
_clockStateFilePath,
2026-03-09 12:27:33 +08:00
now.ToString("O", CultureInfo.InvariantCulture));
2026-03-09 14:14:50 +08:00
_stateService.MarkClockServiceTick(now);
_messageBus.Publish(new SamplePluginClockTickMessage(now));
2026-03-09 12:27:33 +08:00
}
catch (Exception ex)
{
2026-03-10 00:40:26 +08:00
_stateService.MarkClockServiceFaulted(_localizer.Format(
"status.service.detail.write_failed",
"时钟状态写入失败:{0}",
ex.Message));
2026-03-09 12:27:33 +08:00
}
}
}