mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 00:54:26 +08:00
Add external public IPC host/client and plugin SDK
Introduce a new LanMountainDesktop.Shared.IPC project implementing a public IPC host and client (LanMountainDesktopIpcClient, PublicIpcHostService), IPC constants and routed notify IDs, DTOs and DI helpers for registering public services. Update Plugin SDK to allow plugins to contribute public IPC services and registrations, add related descriptors/records and extension helpers. Migrate Launcher/App to use the new public IPC for startup/loading notifications and wiring (including TryConnect helper), switch LoadingStateReporter to use the external notification publisher, and add host-side public services (app info, shell control, plugin catalog). Include integration tests and spec/checklist/docs for the external IPC public API.
This commit is contained in:
@@ -20,10 +20,13 @@ using LanMountainDesktop.DesktopHost;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.ExternalIpc;
|
||||
using LanMountainDesktop.Services.Launcher;
|
||||
using LanMountainDesktop.Services.Loading;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.Shared.Contracts.Launcher;
|
||||
using LanMountainDesktop.Shared.IPC;
|
||||
using LanMountainDesktop.Shared.IPC.Abstractions.Services;
|
||||
using LanMountainDesktop.Theme;
|
||||
using LanMountainDesktop.ViewModels;
|
||||
using LanMountainDesktop.Views;
|
||||
@@ -55,6 +58,7 @@ public partial class App : Application
|
||||
private readonly IHostApplicationLifecycle _hostApplicationLifecycle = new HostApplicationLifecycleService();
|
||||
private readonly IDetachedComponentLibraryWindowService _detachedComponentLibraryWindowService = new DetachedComponentLibraryWindowService();
|
||||
private readonly ILocationService _locationService = HostLocationServiceProvider.GetOrCreate();
|
||||
private readonly DateTimeOffset _startupAt = DateTimeOffset.UtcNow;
|
||||
private ISettingsPageRegistry? _settingsPageRegistry;
|
||||
private ISettingsWindowService? _settingsWindowService;
|
||||
private WeatherLocationRefreshService? _weatherLocationRefreshService;
|
||||
@@ -75,7 +79,7 @@ public partial class App : Application
|
||||
private bool _mainWindowClosed;
|
||||
private bool _uiUnhandledExceptionHooked;
|
||||
private DesktopShellHost? _desktopShellHost;
|
||||
private LauncherIpcClient? _launcherIpcClient;
|
||||
private PublicIpcHostService? _publicIpcHostService;
|
||||
private LoadingStateManager? _loadingStateManager;
|
||||
private LoadingStateReporter? _loadingStateReporter;
|
||||
private bool _singleInstanceReleased;
|
||||
@@ -160,6 +164,7 @@ public partial class App : Application
|
||||
|
||||
RegisterUiUnhandledExceptionGuard();
|
||||
LinuxDesktopEntryInstaller.EnsureInstalled();
|
||||
InitializePublicIpc();
|
||||
_ = InitializeLauncherIpcAsync();
|
||||
DesktopBootstrap.InitializeApplication(this, InitializeDesktopShell);
|
||||
|
||||
@@ -173,34 +178,24 @@ public partial class App : Application
|
||||
|
||||
private async Task InitializeLauncherIpcAsync()
|
||||
{
|
||||
if (!LauncherIpcClient.IsLaunchedByLauncher())
|
||||
if (_loadingStateManager is not null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_launcherIpcClient = new LauncherIpcClient();
|
||||
var connected = await _launcherIpcClient.ConnectAsync();
|
||||
if (!connected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AppLogger.Info("LauncherIpc", "Connected to Launcher IPC server.");
|
||||
|
||||
bool hadBufferedMessages;
|
||||
lock (_launcherProgressLock)
|
||||
{
|
||||
hadBufferedMessages = _pendingLauncherProgressMessages.Count > 0;
|
||||
}
|
||||
|
||||
await FlushPendingLauncherProgressAsync();
|
||||
|
||||
_loadingStateManager = new LoadingStateManager();
|
||||
_loadingStateReporter = new LoadingStateReporter(_loadingStateManager, _launcherIpcClient);
|
||||
_loadingStateReporter = new LoadingStateReporter(_loadingStateManager, _publicIpcHostService);
|
||||
_loadingStateReporter.Start();
|
||||
|
||||
_loadingStateManager.RegisterItem("system.init", LoadingItemType.System, "System Initialization", "Initialize core application services.");
|
||||
_loadingStateManager.StartItem("system.init", "Launcher IPC connected.");
|
||||
_loadingStateManager.StartItem("system.init", "Public IPC host ready.");
|
||||
await FlushPendingLauncherProgressAsync();
|
||||
|
||||
if (!hadBufferedMessages)
|
||||
{
|
||||
@@ -238,8 +233,8 @@ public partial class App : Application
|
||||
|
||||
private void QueueOrSendLauncherProgress(StartupProgressMessage message, bool logSuccess)
|
||||
{
|
||||
var ipcClient = _launcherIpcClient;
|
||||
if (ipcClient is null || !ipcClient.IsConnected)
|
||||
var publicIpcHostService = _publicIpcHostService;
|
||||
if (publicIpcHostService is null)
|
||||
{
|
||||
lock (_launcherProgressLock)
|
||||
{
|
||||
@@ -250,13 +245,13 @@ public partial class App : Application
|
||||
return;
|
||||
}
|
||||
|
||||
_ = SendLauncherProgressAsync(ipcClient, message, logSuccess);
|
||||
_ = SendLauncherProgressAsync(publicIpcHostService, message, logSuccess);
|
||||
}
|
||||
|
||||
private async Task FlushPendingLauncherProgressAsync()
|
||||
{
|
||||
var ipcClient = _launcherIpcClient;
|
||||
if (ipcClient is null || !ipcClient.IsConnected)
|
||||
var publicIpcHostService = _publicIpcHostService;
|
||||
if (publicIpcHostService is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -270,15 +265,15 @@ public partial class App : Application
|
||||
|
||||
foreach (var pendingMessage in pendingMessages)
|
||||
{
|
||||
await SendLauncherProgressAsync(ipcClient, pendingMessage, logSuccess: false);
|
||||
await SendLauncherProgressAsync(publicIpcHostService, pendingMessage, logSuccess: false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendLauncherProgressAsync(LauncherIpcClient ipcClient, StartupProgressMessage message, bool logSuccess)
|
||||
private async Task SendLauncherProgressAsync(PublicIpcHostService publicIpcHostService, StartupProgressMessage message, bool logSuccess)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ipcClient.ReportProgressAsync(message);
|
||||
await publicIpcHostService.PublishStartupProgressAsync(message);
|
||||
if (logSuccess)
|
||||
{
|
||||
AppLogger.Info("LauncherIpc", $"Successfully reported stage: {message.Stage}");
|
||||
@@ -463,7 +458,7 @@ public partial class App : Application
|
||||
try
|
||||
{
|
||||
_pluginRuntimeService?.Dispose();
|
||||
_pluginRuntimeService = new PluginRuntimeService(_settingsFacade);
|
||||
_pluginRuntimeService = new PluginRuntimeService(_settingsFacade, _publicIpcHostService);
|
||||
HostSettingsFacadeProvider.BindPluginRuntime(_pluginRuntimeService);
|
||||
_pluginRuntimeService.LoadInstalledPlugins();
|
||||
}
|
||||
@@ -1043,6 +1038,19 @@ public partial class App : Application
|
||||
_pluginRuntimeService = null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_publicIpcHostService?.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("PublicIpc", "Failed to dispose public IPC host during shutdown.", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_publicIpcHostService = null;
|
||||
}
|
||||
|
||||
_settingsWindowService?.Close();
|
||||
if (_settingsPageRegistry is IDisposable disposableRegistry)
|
||||
{
|
||||
@@ -1336,6 +1344,56 @@ public partial class App : Application
|
||||
var languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
|
||||
return _localizationService.GetString(languageCode, key, fallback);
|
||||
}
|
||||
|
||||
internal bool TryActivateMainWindowFromExternalIpc(string source)
|
||||
{
|
||||
return RestoreOrCreateMainWindowCore(showSingleInstanceNotice: false, source);
|
||||
}
|
||||
|
||||
private void InitializePublicIpc()
|
||||
{
|
||||
if (_publicIpcHostService is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var version = typeof(App).Assembly.GetName().Version?.ToString() ?? "1.0.0";
|
||||
_publicIpcHostService = new PublicIpcHostService();
|
||||
_publicIpcHostService.PluginDescriptorProvider = BuildPublicPluginDescriptors;
|
||||
_publicIpcHostService.RegisterPublicService<IPublicAppInfoService>(
|
||||
new PublicAppInfoService(version, "Administrate", _startupAt));
|
||||
_publicIpcHostService.RegisterPublicService<IPublicShellControlService>(
|
||||
new PublicShellControlService());
|
||||
_publicIpcHostService.RegisterPublicService<IPublicPluginCatalogService>(
|
||||
new PublicPluginCatalogService(_publicIpcHostService));
|
||||
_publicIpcHostService.Start();
|
||||
AppLogger.Info("PublicIpc", $"Public IPC host started. PipeName='{IpcConstants.DefaultPipeName}'.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("PublicIpc", "Failed to initialize public IPC host.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private IReadOnlyList<PublicPluginDescriptor> BuildPublicPluginDescriptors()
|
||||
{
|
||||
var runtime = _pluginRuntimeService;
|
||||
if (runtime is null)
|
||||
{
|
||||
return Array.Empty<PublicPluginDescriptor>();
|
||||
}
|
||||
|
||||
return runtime.Catalog
|
||||
.Select(entry => new PublicPluginDescriptor(
|
||||
entry.Manifest.Id,
|
||||
entry.Manifest.Name,
|
||||
entry.Manifest.Version,
|
||||
entry.IsLoaded,
|
||||
entry.IsEnabled))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LanMountainDesktop.Host.Abstractions\LanMountainDesktop.Host.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Shared.IPC\LanMountainDesktop.Shared.IPC.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Settings.Core\LanMountainDesktop.Settings.Core.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Appearance\LanMountainDesktop.Appearance.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.DesktopComponents.Runtime\LanMountainDesktop.DesktopComponents.Runtime.csproj" />
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using LanMountainDesktop.Shared.IPC;
|
||||
using LanMountainDesktop.Shared.IPC.Abstractions.Services;
|
||||
|
||||
namespace LanMountainDesktop.Services.ExternalIpc;
|
||||
|
||||
internal sealed class PublicAppInfoService : IPublicAppInfoService
|
||||
{
|
||||
private readonly string _version;
|
||||
private readonly string _codename;
|
||||
private readonly DateTimeOffset _startedAt;
|
||||
|
||||
public PublicAppInfoService(string version, string codename, DateTimeOffset startedAt)
|
||||
{
|
||||
_version = version;
|
||||
_codename = codename;
|
||||
_startedAt = startedAt;
|
||||
}
|
||||
|
||||
public PublicAppInfoSnapshot GetAppInfo()
|
||||
{
|
||||
return new PublicAppInfoSnapshot(
|
||||
"LanMountainDesktop",
|
||||
_version,
|
||||
_codename,
|
||||
IpcConstants.DefaultPipeName,
|
||||
Environment.ProcessId,
|
||||
_startedAt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using LanMountainDesktop.Shared.IPC;
|
||||
using LanMountainDesktop.Shared.IPC.Abstractions.Services;
|
||||
|
||||
namespace LanMountainDesktop.Services.ExternalIpc;
|
||||
|
||||
internal sealed class PublicPluginCatalogService : IPublicPluginCatalogService
|
||||
{
|
||||
private readonly PublicIpcHostService _publicIpcHostService;
|
||||
|
||||
public PublicPluginCatalogService(PublicIpcHostService publicIpcHostService)
|
||||
{
|
||||
_publicIpcHostService = publicIpcHostService;
|
||||
}
|
||||
|
||||
public PublicIpcCatalogSnapshot GetCatalog()
|
||||
{
|
||||
return _publicIpcHostService.GetCatalogSnapshot();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Shared.IPC.Abstractions.Services;
|
||||
|
||||
namespace LanMountainDesktop.Services.ExternalIpc;
|
||||
|
||||
internal sealed class PublicShellControlService : IPublicShellControlService
|
||||
{
|
||||
public Task<bool> ActivateMainWindowAsync()
|
||||
{
|
||||
return Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
return (Application.Current as App)?.TryActivateMainWindowFromExternalIpc("PublicIpc") == true;
|
||||
}).GetTask();
|
||||
}
|
||||
|
||||
public Task<bool> OpenSettingsAsync(string? pageTag = null)
|
||||
{
|
||||
return Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
if (Application.Current is not App app)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
app.OpenIndependentSettingsModule("PublicIpc", pageTag);
|
||||
return true;
|
||||
}).GetTask();
|
||||
}
|
||||
|
||||
public Task<bool> RestartAsync()
|
||||
{
|
||||
var lifecycle = App.CurrentHostApplicationLifecycle;
|
||||
return Task.FromResult(lifecycle?.TryRestart(new HostApplicationLifecycleRequest(
|
||||
Source: "PublicIpc",
|
||||
Reason: "External IPC requested restart.")) == true);
|
||||
}
|
||||
|
||||
public Task<bool> ExitAsync()
|
||||
{
|
||||
var lifecycle = App.CurrentHostApplicationLifecycle;
|
||||
return Task.FromResult(lifecycle?.TryExit(new HostApplicationLifecycleRequest(
|
||||
Source: "PublicIpc",
|
||||
Reason: "External IPC requested exit.")) == true);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Timers;
|
||||
using LanMountainDesktop.Services.Launcher;
|
||||
using LanMountainDesktop.Shared.Contracts.Launcher;
|
||||
using LanMountainDesktop.Shared.IPC;
|
||||
|
||||
namespace LanMountainDesktop.Services.Loading;
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace LanMountainDesktop.Services.Loading;
|
||||
public class LoadingStateReporter : IDisposable
|
||||
{
|
||||
private readonly LoadingStateManager _manager;
|
||||
private readonly LauncherIpcClient? _ipcClient;
|
||||
private readonly IExternalIpcNotificationPublisher? _notificationPublisher;
|
||||
private readonly System.Timers.Timer _reportTimer;
|
||||
private readonly object _lock = new();
|
||||
private bool _isDisposed;
|
||||
@@ -36,10 +36,10 @@ public class LoadingStateReporter : IDisposable
|
||||
|
||||
public LoadingStateReporter(
|
||||
LoadingStateManager manager,
|
||||
LauncherIpcClient? ipcClient = null)
|
||||
IExternalIpcNotificationPublisher? notificationPublisher = null)
|
||||
{
|
||||
_manager = manager ?? throw new ArgumentNullException(nameof(manager));
|
||||
_ipcClient = ipcClient;
|
||||
_notificationPublisher = notificationPublisher;
|
||||
|
||||
// 创建定时上报定时器
|
||||
_reportTimer = new System.Timers.Timer(ReportIntervalMs);
|
||||
@@ -80,7 +80,7 @@ public class LoadingStateReporter : IDisposable
|
||||
/// </summary>
|
||||
public async Task ReportImmediatelyAsync()
|
||||
{
|
||||
if (_isDisposed || _ipcClient == null) return;
|
||||
if (_isDisposed || _notificationPublisher == null) return;
|
||||
|
||||
var message = CreateDetailedProgressMessage();
|
||||
await SendMessageAsync(message);
|
||||
@@ -91,7 +91,7 @@ public class LoadingStateReporter : IDisposable
|
||||
/// </summary>
|
||||
public async Task ReportItemProgressAsync(string itemId, int percent, string? message = null)
|
||||
{
|
||||
if (_isDisposed || _ipcClient == null) return;
|
||||
if (_isDisposed || _notificationPublisher == null) return;
|
||||
|
||||
var item = _manager.GetAllItems().FirstOrDefault(i => i.Id == itemId);
|
||||
if (item == null) return;
|
||||
@@ -121,7 +121,7 @@ public class LoadingStateReporter : IDisposable
|
||||
/// </summary>
|
||||
public async Task ReportStageChangeAsync(StartupStage stage, string? message = null)
|
||||
{
|
||||
if (_isDisposed || _ipcClient == null) return;
|
||||
if (_isDisposed || _notificationPublisher == null) return;
|
||||
|
||||
var progressMessage = new DetailedProgressMessage
|
||||
{
|
||||
@@ -140,7 +140,7 @@ public class LoadingStateReporter : IDisposable
|
||||
/// </summary>
|
||||
public async Task ReportErrorAsync(string errorMessage, string? details = null)
|
||||
{
|
||||
if (_isDisposed || _ipcClient == null) return;
|
||||
if (_isDisposed || _notificationPublisher == null) return;
|
||||
|
||||
var fullMessage = string.IsNullOrEmpty(details)
|
||||
? errorMessage
|
||||
@@ -280,7 +280,7 @@ public class LoadingStateReporter : IDisposable
|
||||
/// </summary>
|
||||
private async Task SendMessageAsync(DetailedProgressMessage message)
|
||||
{
|
||||
if (_ipcClient == null) return;
|
||||
if (_notificationPublisher == null) return;
|
||||
|
||||
// 检查最小上报间隔
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
@@ -293,15 +293,15 @@ public class LoadingStateReporter : IDisposable
|
||||
try
|
||||
{
|
||||
// 转换为 StartupProgressMessage 以保持兼容性
|
||||
var baseMessage = new StartupProgressMessage
|
||||
var loadingStateMessage = _manager.GetLoadingStateMessage() with
|
||||
{
|
||||
Stage = message.Stage,
|
||||
ProgressPercent = message.ProgressPercent,
|
||||
OverallProgressPercent = message.ProgressPercent,
|
||||
Message = FormatMessage(message),
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
await _ipcClient.ReportProgressAsync(baseMessage);
|
||||
|
||||
await _notificationPublisher.NotifyAsync(IpcRoutedNotifyIds.LauncherLoadingState, loadingStateMessage);
|
||||
_lastReportTime = DateTimeOffset.UtcNow;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -25,6 +25,7 @@ public sealed class LoadedPlugin : IDisposable, IAsyncDisposable
|
||||
IReadOnlyList<PluginDesktopComponentRegistration> desktopComponents,
|
||||
IReadOnlyList<PluginDesktopComponentEditorRegistration> desktopComponentEditors,
|
||||
IReadOnlyList<PluginServiceExportDescriptor> exportedServices,
|
||||
IReadOnlyList<PluginPublicIpcServiceDescriptor> publicIpcServices,
|
||||
IReadOnlyList<IHostedService> hostedServices,
|
||||
PluginLoadContext loadContext)
|
||||
{
|
||||
@@ -39,6 +40,7 @@ public sealed class LoadedPlugin : IDisposable, IAsyncDisposable
|
||||
DesktopComponents = desktopComponents;
|
||||
DesktopComponentEditors = desktopComponentEditors;
|
||||
ExportedServices = exportedServices;
|
||||
PublicIpcServices = publicIpcServices;
|
||||
HostedServices = hostedServices;
|
||||
LoadContext = loadContext;
|
||||
}
|
||||
@@ -67,6 +69,8 @@ public sealed class LoadedPlugin : IDisposable, IAsyncDisposable
|
||||
|
||||
public IReadOnlyList<PluginServiceExportDescriptor> ExportedServices { get; }
|
||||
|
||||
public IReadOnlyList<PluginPublicIpcServiceDescriptor> PublicIpcServices { get; }
|
||||
|
||||
public PluginLoadContext LoadContext { get; }
|
||||
|
||||
private IReadOnlyList<IHostedService> HostedServices { get; }
|
||||
|
||||
@@ -14,8 +14,10 @@ using System.Threading.Tasks;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Shared.IPC;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using dotnetCampus.Ipc.CompilerServices.Attributes;
|
||||
|
||||
namespace LanMountainDesktop.Plugins;
|
||||
|
||||
@@ -187,9 +189,10 @@ public sealed class PluginLoader
|
||||
.OrderBy(editor => editor.ComponentId, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
var exportedServices = ResolveExports(manifest, pluginServices);
|
||||
var publicIpcServices = ResolvePublicIpcServices(manifest, pluginServices);
|
||||
AppLogger.Info(
|
||||
"PluginLoader",
|
||||
$"Plugin contributions resolved. PluginId='{manifest.Id}'; SettingsSections={settingsSections.Length}; Widgets={desktopComponents.Length}; Editors={desktopComponentEditors.Length}; Exports={exportedServices.Count}.");
|
||||
$"Plugin contributions resolved. PluginId='{manifest.Id}'; SettingsSections={settingsSections.Length}; Widgets={desktopComponents.Length}; Editors={desktopComponentEditors.Length}; Exports={exportedServices.Count}; PublicIpcServices={publicIpcServices.Count}.");
|
||||
hostedServices = pluginServices.GetServices<IHostedService>().ToArray();
|
||||
StartHostedServices(hostedServices);
|
||||
AppLogger.Info("PluginLoader", $"Hosted services started. PluginId='{manifest.Id}'; HostedServices={hostedServices.Count}.");
|
||||
@@ -206,6 +209,7 @@ public sealed class PluginLoader
|
||||
desktopComponents,
|
||||
desktopComponentEditors,
|
||||
exportedServices,
|
||||
publicIpcServices,
|
||||
hostedServices,
|
||||
loadContext);
|
||||
|
||||
@@ -332,6 +336,7 @@ public sealed class PluginLoader
|
||||
RegisterHostService<ISettingsService>(services, hostServices);
|
||||
RegisterHostService<ISettingsCatalog>(services, hostServices);
|
||||
RegisterHostService<IAppearanceThemeService>(services, hostServices);
|
||||
RegisterHostService<IExternalIpcNotificationPublisher>(services, hostServices);
|
||||
|
||||
return services;
|
||||
}
|
||||
@@ -413,6 +418,68 @@ public sealed class PluginLoader
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<PluginPublicIpcServiceDescriptor> ResolvePublicIpcServices(
|
||||
PluginManifest manifest,
|
||||
IServiceProvider services)
|
||||
{
|
||||
var descriptors = new List<PluginPublicIpcServiceDescriptor>();
|
||||
var seenKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var registration in services.GetServices<PluginPublicIpcServiceRegistration>())
|
||||
{
|
||||
var implementation = services.GetService(registration.ContractType)
|
||||
?? throw new InvalidOperationException(
|
||||
$"Plugin '{manifest.Id}' registered public IPC contract '{registration.ContractType.FullName}', but no singleton service instance was found.");
|
||||
|
||||
AddDescriptor(registration.ContractType, implementation, registration.ObjectId, registration.NotifyIds);
|
||||
}
|
||||
|
||||
var builder = new RuntimePluginPublicIpcBuilder(services, AddDescriptor);
|
||||
foreach (var contributor in services.GetServices<IPluginPublicIpcContributor>())
|
||||
{
|
||||
contributor.ConfigurePublicIpc(builder);
|
||||
}
|
||||
|
||||
return descriptors;
|
||||
|
||||
void AddDescriptor(Type contractType, object implementation, string? objectId, IEnumerable<string>? notifyIds)
|
||||
{
|
||||
EnsurePublicIpcContract(manifest, contractType);
|
||||
|
||||
var normalizedObjectId = objectId ?? string.Empty;
|
||||
var dedupeKey = $"{contractType.AssemblyQualifiedName}::{normalizedObjectId}";
|
||||
if (!seenKeys.Add(dedupeKey))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Plugin '{manifest.Id}' registered duplicate public IPC contract '{contractType.FullName}' with object id '{normalizedObjectId}'.");
|
||||
}
|
||||
|
||||
descriptors.Add(new PluginPublicIpcServiceDescriptor(
|
||||
contractType,
|
||||
implementation,
|
||||
string.IsNullOrEmpty(normalizedObjectId) ? null : normalizedObjectId,
|
||||
notifyIds?
|
||||
.Where(id => !string.IsNullOrWhiteSpace(id))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray() ?? []));
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsurePublicIpcContract(PluginManifest manifest, Type contractType)
|
||||
{
|
||||
if (!contractType.IsInterface)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Plugin '{manifest.Id}' public IPC contract '{contractType.FullName}' must be an interface.");
|
||||
}
|
||||
|
||||
if (!Attribute.IsDefined(contractType, typeof(IpcPublicAttribute), inherit: false))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Plugin '{manifest.Id}' public IPC contract '{contractType.FullName}' must be marked with '{nameof(IpcPublicAttribute)}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsSupportedExportContract(PluginManifest manifest, Type contractType)
|
||||
{
|
||||
if (contractType.Assembly == typeof(IPlugin).Assembly)
|
||||
@@ -1074,4 +1141,42 @@ public sealed class PluginLoader
|
||||
string SourcePath,
|
||||
PluginManifest Manifest,
|
||||
PluginSourceKind SourceKind);
|
||||
|
||||
private sealed class RuntimePluginPublicIpcBuilder : IPluginPublicIpcBuilder
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly Action<Type, object, string?, IEnumerable<string>?> _register;
|
||||
|
||||
public RuntimePluginPublicIpcBuilder(
|
||||
IServiceProvider services,
|
||||
Action<Type, object, string?, IEnumerable<string>?> register)
|
||||
{
|
||||
_services = services;
|
||||
_register = register;
|
||||
}
|
||||
|
||||
public IPluginPublicIpcBuilder AddService<TContract>(
|
||||
string? objectId = null,
|
||||
IEnumerable<string>? notifyIds = null)
|
||||
where TContract : class
|
||||
{
|
||||
var implementation = _services.GetService(typeof(TContract))
|
||||
?? throw new InvalidOperationException(
|
||||
$"Plugin public IPC contributor requested contract '{typeof(TContract).FullName}', but no singleton service was registered.");
|
||||
_register(typeof(TContract), implementation, objectId, notifyIds);
|
||||
return this;
|
||||
}
|
||||
|
||||
public IPluginPublicIpcBuilder AddService(
|
||||
Type contractType,
|
||||
object implementation,
|
||||
string? objectId = null,
|
||||
IEnumerable<string>? notifyIds = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(contractType);
|
||||
ArgumentNullException.ThrowIfNull(implementation);
|
||||
_register(contractType, implementation, objectId, notifyIds);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Plugins;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.Shared.IPC;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
@@ -31,6 +32,7 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
private readonly IPluginPackageManager _packageManager;
|
||||
private readonly ISettingsFacadeService _settingsFacade;
|
||||
private readonly SettingsCatalogService _settingsCatalogService;
|
||||
private readonly PublicIpcHostService? _publicIpcHostService;
|
||||
private readonly List<LoadedPlugin> _loadedPlugins = [];
|
||||
private readonly List<PluginLoadResult> _loadResults = [];
|
||||
private readonly List<PluginCatalogEntry> _catalog = [];
|
||||
@@ -39,13 +41,16 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
private readonly List<PluginDesktopComponentEditorContribution> _desktopComponentEditors = [];
|
||||
private readonly object _packageMutationGate = new();
|
||||
|
||||
public PluginRuntimeService(ISettingsFacadeService? settingsFacade = null)
|
||||
public PluginRuntimeService(
|
||||
ISettingsFacadeService? settingsFacade = null,
|
||||
PublicIpcHostService? publicIpcHostService = null)
|
||||
{
|
||||
PluginsDirectory = Path.Combine(GetUserDataRootDirectory(), "Extensions", "Plugins");
|
||||
_sharedContractManager = new PluginSharedContractManager(
|
||||
Path.Combine(GetUserDataRootDirectory(), "PluginMarket"));
|
||||
_packageManager = new PluginRuntimePackageManager(this);
|
||||
_settingsFacade = settingsFacade ?? new SettingsFacadeService();
|
||||
_publicIpcHostService = publicIpcHostService;
|
||||
_settingsCatalogService = _settingsFacade.Catalog as SettingsCatalogService
|
||||
?? new SettingsCatalogService();
|
||||
if (_settingsFacade is SettingsFacadeService concreteFacade)
|
||||
@@ -58,7 +63,8 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
_exportRegistry,
|
||||
_settingsFacade,
|
||||
_settingsFacade.Settings,
|
||||
_settingsFacade.Catalog);
|
||||
_settingsFacade.Catalog,
|
||||
_publicIpcHostService);
|
||||
_loaderOptions = CreateOptions();
|
||||
_loader = new PluginLoader(_loaderOptions);
|
||||
}
|
||||
@@ -675,6 +681,8 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
AddSharedAssembly(options, typeof(App).Assembly);
|
||||
AddSharedAssembly(options, typeof(IServiceCollection).Assembly);
|
||||
AddSharedAssembly(options, typeof(HostBuilderContext).Assembly);
|
||||
AddSharedAssembly(options, typeof(IExternalIpcNotificationPublisher).Assembly);
|
||||
AddSharedAssembly(options, typeof(dotnetCampus.Ipc.Pipes.IpcProvider).Assembly);
|
||||
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
@@ -761,6 +769,19 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
{
|
||||
_desktopComponentEditors.Add(new PluginDesktopComponentEditorContribution(loadedPlugin, desktopComponentEditor));
|
||||
}
|
||||
|
||||
if (_publicIpcHostService is not null)
|
||||
{
|
||||
foreach (var publicIpcService in loadedPlugin.PublicIpcServices)
|
||||
{
|
||||
_publicIpcHostService.RegisterPublicService(
|
||||
publicIpcService.ContractType,
|
||||
publicIpcService.Implementation,
|
||||
publicIpcService.ObjectId,
|
||||
loadedPlugin.Manifest.Id,
|
||||
publicIpcService.NotifyIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterSharedContractsForLoad(PluginManifest manifest)
|
||||
@@ -990,11 +1011,12 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
{
|
||||
private readonly IPluginPackageManager _packageManager;
|
||||
private readonly IHostApplicationLifecycle _applicationLifecycle;
|
||||
private readonly IPluginExportRegistry _exportRegistry;
|
||||
private readonly ISettingsFacadeService _settingsFacade;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ISettingsCatalog _settingsCatalog;
|
||||
private readonly IAppearanceThemeService _appearanceThemeService;
|
||||
private readonly IPluginExportRegistry _exportRegistry;
|
||||
private readonly ISettingsFacadeService _settingsFacade;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ISettingsCatalog _settingsCatalog;
|
||||
private readonly IAppearanceThemeService _appearanceThemeService;
|
||||
private readonly IExternalIpcNotificationPublisher? _externalIpcNotificationPublisher;
|
||||
|
||||
public PluginHostServiceProvider(
|
||||
IPluginPackageManager packageManager,
|
||||
@@ -1002,7 +1024,8 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
IPluginExportRegistry exportRegistry,
|
||||
ISettingsFacadeService settingsFacade,
|
||||
ISettingsService settingsService,
|
||||
ISettingsCatalog settingsCatalog)
|
||||
ISettingsCatalog settingsCatalog,
|
||||
IExternalIpcNotificationPublisher? externalIpcNotificationPublisher)
|
||||
{
|
||||
_packageManager = packageManager;
|
||||
_applicationLifecycle = applicationLifecycle;
|
||||
@@ -1011,6 +1034,7 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
_settingsService = settingsService;
|
||||
_settingsCatalog = settingsCatalog;
|
||||
_appearanceThemeService = HostAppearanceThemeProvider.GetOrCreate();
|
||||
_externalIpcNotificationPublisher = externalIpcNotificationPublisher;
|
||||
}
|
||||
|
||||
public object? GetService(Type serviceType)
|
||||
@@ -1050,6 +1074,11 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
return _appearanceThemeService;
|
||||
}
|
||||
|
||||
if (serviceType == typeof(IExternalIpcNotificationPublisher))
|
||||
{
|
||||
return _externalIpcNotificationPublisher;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user