mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 09:14:25 +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:
@@ -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