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 ;
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 ,
IPluginMessageBus messageBus )
{
_manifest = manifest ;
_pluginDirectory = pluginDirectory ;
_dataDirectory = dataDirectory ;
_hostApplicationName = hostApplicationName ;
_hostVersion = hostVersion ;
_sdkApiVersion = sdkApiVersion ;
_messageBus = messageBus ;
_frontend = CreateEntry (
"frontend" ,
"Frontend" ,
SamplePluginHealthState . Pending ,
"Pending" ,
"Waiting for a plugin UI surface to connect." ) ;
_component = CreateEntry (
"component" ,
"Component" ,
SamplePluginHealthState . Pending ,
"Pending" ,
"No component instance has been created yet." ) ;
2026-03-09 12:27:33 +08:00
2026-03-09 14:14:50 +08:00
_backend = CreateEntry (
"backend" ,
"Backend" ,
SamplePluginHealthState . Pending ,
"Pending" ,
"Plugin initialization is in progress." ) ;
_service = CreateEntry (
"service" ,
"Clock Service" ,
SamplePluginHealthState . Pending ,
"Pending" ,
"Clock service is not attached yet." ) ;
}
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-09 14:14:50 +08:00
"Clock Service" ,
2026-03-09 12:27:33 +08:00
SamplePluginHealthState . Pending ,
2026-03-09 14:14:50 +08:00
"Attached" ,
"Clock service was attached and is waiting for the first tick." ) ;
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" ,
"Frontend" ,
SamplePluginHealthState . Healthy ,
"Healthy" ,
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" ,
"Backend" ,
SamplePluginHealthState . Healthy ,
"Healthy" ,
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" ,
"Backend" ,
SamplePluginHealthState . Faulted ,
"Faulted" ,
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-09 14:14:50 +08:00
"Clock Service" ,
2026-03-09 12:27:33 +08:00
SamplePluginHealthState . Healthy ,
"Healthy" ,
2026-03-09 14:14:50 +08:00
$"Clock service is running. Current service time: {currentTime.LocalDateTime: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-09 14:14:50 +08:00
"Clock Service" ,
2026-03-09 12:27:33 +08:00
SamplePluginHealthState . Faulted ,
"Faulted" ,
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
? "(none)"
: string . Join ( ", " , context . Properties . Keys . OrderBy ( key = > key , StringComparer . OrdinalIgnoreCase ) ) ;
return
[
new SamplePluginCapabilityItem (
"IPluginContext.Manifest" ,
$"Readable. Current plugin id: {context.Manifest.Id}; version: {context.Manifest.Version ?? " dev "}." ) ,
new SamplePluginCapabilityItem (
"IPluginContext.PluginDirectory / DataDirectory" ,
$"Readable. Plugin directory: {context.PluginDirectory}; data directory: {context.DataDirectory}." ) ,
new SamplePluginCapabilityItem (
"IPluginContext.Properties" ,
$"Readable. Host properties currently exposed: {propertyNames}." ) ,
new SamplePluginCapabilityItem (
"IPluginContext.GetService<T>()" ,
$"Callable. State service resolved: {hasStateService}; clock service resolved: {hasClockService}; message bus resolved: {hasMessageBus}." ) ,
new SamplePluginCapabilityItem (
"IPluginContext.RegisterService<TService>()" ,
"Callable during plugin initialization. This plugin registers SamplePluginRuntimeStateService and SamplePluginClockService into the plugin service container." ) ,
new SamplePluginCapabilityItem (
"Plugin communication bus" ,
"This plugin uses IPluginMessageBus to push clock ticks and state change notifications into plugin UI surfaces." ) ,
new SamplePluginCapabilityItem (
"PluginDesktopComponentContext" ,
"Widgets can read ComponentId, PlacementId, CellSize, and call GetService<T>() against the same plugin service container." )
] ;
}
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" ,
"Component" ,
SamplePluginHealthState . Healthy ,
"Placed" ,
$"Placed count: {placementIds.Length}; preview count: {previewCount}; placements: {string.Join(" , ", placementIds)}" ) ;
return ;
}
if ( previewCount > 0 )
{
_component = CreateEntry (
"component" ,
"Component" ,
SamplePluginHealthState . Healthy ,
"Preview" ,
$"Preview instances: {previewCount}; no placed desktop instance is active yet." ) ;
return ;
}
_component = CreateEntry (
"component" ,
"Component" ,
SamplePluginHealthState . Pending ,
"Pending" ,
"No component instance is active." ) ;
}
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-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-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 ,
IPluginMessageBus messageBus )
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-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-09 14:14:50 +08:00
_stateService . MarkClockServiceFaulted ( $"Clock state write failed: {ex.Message}" ) ;
2026-03-09 12:27:33 +08:00
}
}
}