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
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|