diff --git a/3rd/Directory.Packages.props b/3rd/Directory.Packages.props new file mode 100644 index 000000000..ef3a8262f --- /dev/null +++ b/3rd/Directory.Packages.props @@ -0,0 +1,8 @@ + + + + + + true + + \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index 3877ca214..e61b0ee8c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,6 +1,5 @@ - true 10.0.9 10.5.2 10.0.9 diff --git a/Everywhere.slnx b/Everywhere.slnx index 2e9d7c400..99af81eec 100644 --- a/Everywhere.slnx +++ b/Everywhere.slnx @@ -16,6 +16,7 @@ + @@ -57,6 +58,8 @@ + + @@ -71,6 +74,7 @@ + diff --git a/patches/Directory.Packages.props b/patches/Directory.Packages.props new file mode 100644 index 000000000..70bc5acdf --- /dev/null +++ b/patches/Directory.Packages.props @@ -0,0 +1,6 @@ + + + + true + + \ No newline at end of file diff --git a/src/Build.Pure.DI.MS.targets b/src/Build.Pure.DI.MS.targets new file mode 100644 index 000000000..268b77596 --- /dev/null +++ b/src/Build.Pure.DI.MS.targets @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props new file mode 100644 index 000000000..d4f3c6e95 --- /dev/null +++ b/src/Directory.Packages.props @@ -0,0 +1,10 @@ + + + + true + + + + + + \ No newline at end of file diff --git a/src/Everywhere.Cloud/DependencyInjection/CloudComposition.cs b/src/Everywhere.Cloud/DependencyInjection/CloudComposition.cs new file mode 100644 index 000000000..a2bef42cd --- /dev/null +++ b/src/Everywhere.Cloud/DependencyInjection/CloudComposition.cs @@ -0,0 +1,41 @@ +using Everywhere.Database; +using Microsoft.Extensions.Logging; +using Pure.DI; +using Pure.DI.MS; +using static Pure.DI.Lifetime; + +namespace Everywhere.Cloud.DependencyInjection; + +public partial class CloudComposition : ServiceProviderFactory +{ + // ReSharper disable once UnusedMember.Local + private static void SetupCloudServices() => + DI.Setup() + // Pure.DI.MS hooks and framework fallbacks. + .Hint(Hint.OnCannotResolve, "On") + .Hint(Hint.OnCannotResolvePartial, "Off") + .Hint(Hint.OnNewRoot, "On") + .Hint(Hint.OnNewRootPartial, "Off") + .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Microsoft.Extensions.*") + .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Microsoft.AspNetCore.*") + .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Microsoft.Maui.*") + .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Microsoft.EntityFrameworkCore.*") + .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "System.Net.Http.*") + .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Everywhere.*") + + // Logging facade instances are created by Pure.DI; ILoggerFactory stays in MS DI. + .Bind>().As(Singleton).To>() + + // Cloud service implementations. + .Bind().Bind().As(Singleton).To() + .Bind().Bind().As(Singleton).To() + .Bind().Bind().As(Singleton).To() + + // Cloud roots exported to the final MS provider. + .Root(kind: RootKinds.Exported) + .Root(kind: RootKinds.Exported) + .Root(kind: RootKinds.Exported) + .Root(kind: RootKinds.Exported) + .Root(kind: RootKinds.Exported) + .Root(kind: RootKinds.Exported); +} diff --git a/src/Everywhere.Cloud/DependencyInjection/CloudServiceCollection.cs b/src/Everywhere.Cloud/DependencyInjection/CloudServiceCollection.cs new file mode 100644 index 000000000..9d9c097a4 --- /dev/null +++ b/src/Everywhere.Cloud/DependencyInjection/CloudServiceCollection.cs @@ -0,0 +1,53 @@ +using System.Diagnostics.CodeAnalysis; +using System.Net; +using Everywhere.Common; +using Microsoft.Extensions.DependencyInjection; + +namespace Everywhere.Cloud.DependencyInjection; + +public static class CloudServiceCollection +{ + public static IServiceCollection Configure(IServiceCollection services) + { + // Cloud HTTP user-agent handler. + services.AddTransient(); + + // Cloud API named HTTP client. + services + .AddHttpClient( + nameof(ICloudClient), + client => client.Timeout = TimeSpan.FromSeconds(30)) + .ConfigurePrimaryHttpMessageHandler(sp => CreateHttpClientHandler(sp.GetRequiredService())) + .AddHttpMessageHandler(sp => sp.GetRequiredService().CreateAuthenticationHandler()) + .AddHttpMessageHandler(); + + return services; + } + + public static IServiceCollection ConfigureAliases(IServiceCollection services) + { + // Cloud startup initializer aliases. + services.AddSingleton(sp => sp.GetRequiredService()); + services.AddSingleton(sp => sp.GetRequiredService()); + return services; + } + + private static HttpClientHandler CreateHttpClientHandler(IWebProxy proxy) => + new() + { + Proxy = proxy, + UseProxy = true, + AllowAutoRedirect = true, + }; + + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] + private sealed class CloudUserAgentHandler : DelegatingHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + request.Headers.Remove("User-Agent"); + request.Headers.Add("User-Agent", $"Everywhere/{App.Version}"); + return base.SendAsync(request, cancellationToken); + } + } +} diff --git a/src/Everywhere.Cloud/Everywhere.Cloud.csproj b/src/Everywhere.Cloud/Everywhere.Cloud.csproj index 004f70425..b5ac60855 100644 --- a/src/Everywhere.Cloud/Everywhere.Cloud.csproj +++ b/src/Everywhere.Cloud/Everywhere.Cloud.csproj @@ -3,15 +3,16 @@ true Everywhere.Cloud.I18N + $(NoWarn);CS0436 - - - - + + + + @@ -19,6 +20,14 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + - + diff --git a/src/Everywhere.Core/App.axaml.cs b/src/Everywhere.Core/App.axaml.cs index 81fde6ee3..c1082f4c0 100644 --- a/src/Everywhere.Core/App.axaml.cs +++ b/src/Everywhere.Core/App.axaml.cs @@ -63,7 +63,9 @@ public override void Initialize() #if DEBUG if (Design.IsDesignMode) { - ServiceLocator.Build(x => x.AddAvaloniaBasicServices()); + var services = new ServiceCollection(); + services.AddAvaloniaBasicServices(); + ServiceLocator.SetProvider(services.BuildServiceProvider()); return; } diff --git a/src/Everywhere.Core/Chat/Plugins/Mcp/McpServiceExtension.cs b/src/Everywhere.Core/Chat/Plugins/Mcp/McpServiceExtension.cs index 1b1a9f70f..81df7fbb3 100644 --- a/src/Everywhere.Core/Chat/Plugins/Mcp/McpServiceExtension.cs +++ b/src/Everywhere.Core/Chat/Plugins/Mcp/McpServiceExtension.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Http.Headers; +using System.Runtime.CompilerServices; using Microsoft.Extensions.DependencyInjection; namespace Everywhere.Chat.Plugins.Mcp; @@ -36,94 +37,94 @@ public static IServiceCollection AddManagedMcp(this IServiceCollection services) return services; } - - /// - /// A delegating handler that buffers the request content to compute and set the - /// Content-Length header. This is useful for servers that do not support - /// chunked transfer encoding. - /// - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] - private sealed class ContentLengthBufferingHandler : DelegatingHandler +} + +/// +/// A delegating handler that buffers the request content to compute and set the +/// Content-Length header. This is useful for servers that do not support +/// chunked transfer encoding. +/// +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +public sealed class ContentLengthBufferingHandler : DelegatingHandler +{ + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + if (request.Content is not null) { - if (request.Content is not null) - { - // By calling LoadIntoBufferAsync, we force the content to be buffered in memory. - // This allows the HttpContent instance to calculate its length, which then gets - // automatically set as the Content-Length header when the request is sent. - // This effectively disables chunked transfer encoding. - await request.Content.LoadIntoBufferAsync(cancellationToken).ConfigureAwait(false); - request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - } - - return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + // By calling LoadIntoBufferAsync, we force the content to be buffered in memory. + // This allows the HttpContent instance to calculate its length, which then gets + // automatically set as the Content-Length header when the request is sent. + // This effectively disables chunked transfer encoding. + await request.Content.LoadIntoBufferAsync(cancellationToken).ConfigureAwait(false); + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); } - } - /// - /// A delegating handler that intercepts non-404 4xx responses from MCP servers - /// and converts them to 404 if the response body indicates a session expired error. - /// This allows the SDK's standard SetSessionExpired path to handle it. - /// - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] - private sealed class McpSessionExpiryHandler : DelegatingHandler + return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + } +} + +/// +/// A delegating handler that intercepts non-404 4xx responses from MCP servers +/// and converts them to 404 if the response body indicates a session expired error. +/// This allows the SDK's standard SetSessionExpired path to handle it. +/// +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +public sealed class McpSessionExpiryHandler : DelegatingHandler +{ + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); - - // Only intercept non-404 4xx responses. - if (response.StatusCode is not HttpStatusCode.NotFound && (int)response.StatusCode is >= 400 and < 500) - { - // Buffer the response content so we can read it and still return it if no match. - var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); - if (!ContainsSessionExpiredKeyword(body)) return response; + var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); - var newContent = new StringContent(body); + // Only intercept non-404 4xx responses. + if (response.StatusCode is not HttpStatusCode.NotFound && (int)response.StatusCode is >= 400 and < 500) + { + // Buffer the response content so we can read it and still return it if no match. + var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + if (!ContainsSessionExpiredKeyword(body)) return response; - // Preserve original content headers (such as Content-Type/charset). - // Skip Content-Length because it is computed from the replacement content. - foreach (var header in response.Content.Headers) - { - if (!string.Equals(header.Key, "Content-Length", StringComparison.OrdinalIgnoreCase)) - { - newContent.Headers.TryAddWithoutValidation(header.Key, header.Value); - } - } + var newContent = new StringContent(body); - var newResponse = new HttpResponseMessage(HttpStatusCode.NotFound) - { - RequestMessage = response.RequestMessage, - ReasonPhrase = "Session Expired (rewritten by McpSessionExpiryHandler)", - Version = response.Version, - Content = newContent, - }; - - // Copy response headers. - foreach (var header in response.Headers) + // Preserve original content headers (such as Content-Type/charset). + // Skip Content-Length because it is computed from the replacement content. + foreach (var header in response.Content.Headers) + { + if (!string.Equals(header.Key, "Content-Length", StringComparison.OrdinalIgnoreCase)) { - newResponse.Headers.TryAddWithoutValidation(header.Key, header.Value); + newContent.Headers.TryAddWithoutValidation(header.Key, header.Value); } + } - // Preserve trailing headers as well. - foreach (var header in response.TrailingHeaders) - { - newResponse.TrailingHeaders.TryAddWithoutValidation(header.Key, header.Value); - } + var newResponse = new HttpResponseMessage(HttpStatusCode.NotFound) + { + RequestMessage = response.RequestMessage, + ReasonPhrase = "Session Expired (rewritten by McpSessionExpiryHandler)", + Version = response.Version, + Content = newContent, + }; + + // Copy response headers. + foreach (var header in response.Headers) + { + newResponse.Headers.TryAddWithoutValidation(header.Key, header.Value); + } - response.Dispose(); - return newResponse; + // Preserve trailing headers as well. + foreach (var header in response.TrailingHeaders) + { + newResponse.TrailingHeaders.TryAddWithoutValidation(header.Key, header.Value); } - return response; + response.Dispose(); + return newResponse; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool ContainsSessionExpiredKeyword(string body) => - body.Contains("session", StringComparison.OrdinalIgnoreCase) && - (body.Contains("expired", StringComparison.OrdinalIgnoreCase) || - body.Contains("expires", StringComparison.OrdinalIgnoreCase) || - body.Contains("not found", StringComparison.OrdinalIgnoreCase)); + return response; } -} \ No newline at end of file + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool ContainsSessionExpiredKeyword(string body) => + body.Contains("session", StringComparison.OrdinalIgnoreCase) && + (body.Contains("expired", StringComparison.OrdinalIgnoreCase) || + body.Contains("expires", StringComparison.OrdinalIgnoreCase) || + body.Contains("not found", StringComparison.OrdinalIgnoreCase)); +} diff --git a/src/Everywhere.Core/Common/ServiceLocator.cs b/src/Everywhere.Core/Common/ServiceLocator.cs index f748e432a..9b24ee222 100644 --- a/src/Everywhere.Core/Common/ServiceLocator.cs +++ b/src/Everywhere.Core/Common/ServiceLocator.cs @@ -6,23 +6,22 @@ public static class ServiceLocator { private static IServiceProvider? _serviceProvider; - public static void Build(Action configureServices) + public static void SetProvider(IServiceProvider serviceProvider) { if (_serviceProvider != null) throw new InvalidOperationException($"{nameof(ServiceLocator)} is already built."); - var serviceCollection = new ServiceCollection(); - configureServices(serviceCollection); - _serviceProvider = serviceCollection.BuildServiceProvider(); + _serviceProvider = serviceProvider; } public static object Resolve(Type type, object? key = null) { if (_serviceProvider == null) throw new InvalidOperationException($"{nameof(ServiceLocator)} is not built."); + if (key != null) throw new NotSupportedException("Keyed service resolution is not supported by the source-generated provider."); if (key == null) return _serviceProvider.GetRequiredService(type); - return _serviceProvider.GetRequiredKeyedService(type, key); + throw new InvalidOperationException("Unreachable service resolution branch."); } public static T Resolve(object? key = null) where T : class { return (T)Resolve(typeof(T), key); } -} \ No newline at end of file +} diff --git a/src/Everywhere.Core/DependencyInjection/ApplicationServiceCollection.cs b/src/Everywhere.Core/DependencyInjection/ApplicationServiceCollection.cs new file mode 100644 index 000000000..8f5ca7363 --- /dev/null +++ b/src/Everywhere.Core/DependencyInjection/ApplicationServiceCollection.cs @@ -0,0 +1,164 @@ +using System.Diagnostics.CodeAnalysis; +using System.Net; +using Everywhere.AI.Prompts.Database; +using Everywhere.Chat; +using Everywhere.Chat.Plugins; +using Everywhere.Chat.Plugins.BuiltIn; +using Everywhere.Chat.Plugins.Mcp; +using Everywhere.Common; +using Everywhere.Configuration; +using Everywhere.Configuration.Engine; +using Everywhere.Database; +using Everywhere.Initialization; +using Everywhere.Interop; +using Everywhere.Skills; +using Everywhere.Statistics; +using Everywhere.Statistics.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Serilog; +using Serilog.Extensions.Logging; + +namespace Everywhere.DependencyInjection; + +public static class ApplicationServiceCollection +{ + public static IServiceCollection Configure(IServiceCollection services) => + services + .ConfigureLogging() + .ConfigureHttpClients() + .ConfigureEntityFramework(); + + // Pure.DI.MS exports roots by their own contract. These aliases preserve + // MS DI enumerable behavior for services also consumed through aggregate + // contracts such as IAsyncInitializer and BuiltInChatPlugin. + public static IServiceCollection ConfigureCoreAliases(IServiceCollection services) + { + // Startup initializer aliases. + services.AddTransient(x => x.GetRequiredService()); + services.AddTransient(x => x.GetRequiredService()); + services.AddTransient(x => x.GetRequiredService()); + services.AddTransient(x => x.GetRequiredService()); + services.AddTransient(x => x.GetRequiredService()); + services.AddTransient(x => x.GetRequiredService()); + services.AddTransient(x => x.GetRequiredService()); + services.AddTransient(x => x.GetRequiredService()); + services.AddTransient(x => x.GetRequiredService()); + services.AddTransient(x => x.GetRequiredService()); + services.AddTransient(x => x.GetRequiredService()); + services.AddTransient(x => x.GetRequiredService()); + + // Built-in chat plugin aliases. + services.AddTransient(x => x.GetRequiredService()); + services.AddTransient(x => x.GetRequiredService()); + services.AddTransient(x => x.GetRequiredService()); + services.AddTransient(x => x.GetRequiredService()); + services.AddTransient(x => x.GetRequiredService()); + + return services; + } + + // Platform initializers and plugins are registered after the platform + // composition exports its concrete roots. + public static IServiceCollection ConfigurePlatformAliases(IServiceCollection services) where TPlatformPlugin : BuiltInChatPlugin + { + // Platform startup initializer aliases. + services.AddTransient(x => x.GetRequiredService()); + services.AddTransient(x => x.GetRequiredService()); + + // Platform chat plugin alias. + services.AddTransient(x => x.GetRequiredService()); + return services; + } + + private static IServiceCollection ConfigureLogging(this IServiceCollection services) => + services.AddLogging(builder => builder +#if DEBUG + .SetMinimumLevel(LogLevel.Debug) +#endif + .AddSerilog(dispose: true) + .AddFilter("Microsoft.EntityFrameworkCore", LogLevel.Warning)); + + private static IServiceCollection ConfigureHttpClients(this IServiceCollection services) + { + // Delegating handlers used by named clients. + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + // Default app HTTP client. + services + .AddHttpClient( + Options.DefaultName, + client => client.Timeout = TimeSpan.FromSeconds(10)) + .ConfigurePrimaryHttpMessageHandler(x => CreateHttpClientHandler(x.GetRequiredService())) + .AddHttpMessageHandler(); + + // MCP transport HTTP client. + services + .AddHttpClient( + McpServiceExtension.McpClientName, + client => client.Timeout = TimeSpan.FromSeconds(30)) + .ConfigurePrimaryHttpMessageHandler(x => CreateHttpClientHandler(x.GetRequiredService())) + .AddHttpMessageHandler() + .AddHttpMessageHandler(); + + return services; + } + + private static IServiceCollection ConfigureEntityFramework(this IServiceCollection services) => + services + // Chat database context factory. + .AddDbContextFactory((_, options) => + { + var dbPath = RuntimeConstants.GetDatabasePath("chat.db"); + options.UseSqlite($"Data Source={dbPath}"); + }) + + // Prompt database context factory. + .AddDbContextFactory((_, options) => + { + var dbPath = RuntimeConstants.GetDatabasePath("prompt.db"); + options.UseSqlite($"Data Source={dbPath}"); + }) + + // Statistics database context factory. + .AddDbContextFactory((_, options) => + { + var dbPath = RuntimeConstants.GetDatabasePath("statistics.db"); + options.UseSqlite($"Data Source={dbPath}"); + }); + + private static HttpClientHandler CreateHttpClientHandler(IWebProxy proxy) => + new() + { + Proxy = proxy, + UseProxy = true, + AllowAutoRedirect = true, + }; + + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] + private sealed class DefaultUserAgentHandler : DelegatingHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + request.Headers.Remove("User-Agent"); + request.Headers.Add("User-Agent", $"Chrome/142.0.0.0 Safari/537.36 Everywhere/{App.Version}"); + return base.SendAsync(request, cancellationToken); + } + } + + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] + private sealed class CloudUserAgentHandler : DelegatingHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + request.Headers.Remove("User-Agent"); + request.Headers.Add("User-Agent", $"Everywhere/{App.Version}"); + return base.SendAsync(request, cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Everywhere.Core/DependencyInjection/ApplicationServiceProviderFactories.cs b/src/Everywhere.Core/DependencyInjection/ApplicationServiceProviderFactories.cs new file mode 100644 index 000000000..64f888329 --- /dev/null +++ b/src/Everywhere.Core/DependencyInjection/ApplicationServiceProviderFactories.cs @@ -0,0 +1,21 @@ +using Avalonia.Controls.ApplicationLifetimes; +using Everywhere.Views; +using ShadUI; +using ZLinq; + +namespace Everywhere.DependencyInjection; + +public static class ApplicationServiceProviderFactories +{ + public static DialogManager CreateDialogManager() => TryGetReactiveHost()?.DialogHost.Manager ?? new DialogManager(); + + public static ToastHost CreateToastHost() => TryGetReactiveHost()?.ToastHost ?? new ToastHost(); + + private static IReactiveHost? TryGetReactiveHost() + { + if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime lifetime) return null; + return lifetime.Windows.AsValueEnumerable().FirstOrDefault(w => w.IsActive) as IReactiveHost ?? + lifetime.MainWindow as IReactiveHost ?? + lifetime.Windows.AsValueEnumerable().OfType().FirstOrDefault(); + } +} \ No newline at end of file diff --git a/src/Everywhere.Core/DependencyInjection/AvaloniaServices.cs b/src/Everywhere.Core/DependencyInjection/AvaloniaServices.cs new file mode 100644 index 000000000..119190b3f --- /dev/null +++ b/src/Everywhere.Core/DependencyInjection/AvaloniaServices.cs @@ -0,0 +1,50 @@ +using Everywhere.Views; +using Everywhere.Views.Pages; +using Pure.DI; +using ShadUI; +using static Pure.DI.Lifetime; + +namespace Everywhere.DependencyInjection; + +public partial class CoreComposition +{ + // ReSharper disable once UnusedMember.Local + private static void SetupAvaloniaServices() => + DI.Setup() + // UI host services that require Avalonia runtime state. + .Bind().To(_ => ApplicationServiceProviderFactories.CreateDialogManager()) + .Bind().To(_ => ApplicationServiceProviderFactories.CreateToastHost()) + .Bind().As(Singleton).To() + + // Chat window shell and animation target. + .Bind().As(Singleton).To() + .Bind().Bind().As(Singleton).To() + + // Main navigation pages. + .Bind().As(Singleton).To() + .Bind().Bind(Tag.Unique).As(Singleton).To() + .Bind().As(Singleton).To() + .Bind().Bind(Tag.Unique).As(Singleton).To() + .Bind().As(Singleton).To() + .Bind().Bind(Tag.Unique).As(Singleton).To() + .Bind().To() + .Bind().To() + .Bind().As(Singleton).To() + .Bind().Bind(Tag.Unique).As(Singleton).To() + .Bind().As(Singleton).To() + .Bind().Bind(Tag.Unique).As(Singleton).To() + .Bind().As(Singleton).To() + .Bind().Bind(Tag.Unique).As(Singleton).To() + .Bind().Bind(Tag.Unique).To() + + // Secondary views and app shell. + .Bind().To() + .Bind().To() + .Bind().To() + .Bind().To() + .Bind().As(Singleton).To() + .Bind().As(Singleton).To() + + // Visual effects. + .Bind().As(Singleton).To(); +} \ No newline at end of file diff --git a/src/Everywhere.Core/DependencyInjection/ChatPluginServices.cs b/src/Everywhere.Core/DependencyInjection/ChatPluginServices.cs new file mode 100644 index 000000000..27fb4790a --- /dev/null +++ b/src/Everywhere.Core/DependencyInjection/ChatPluginServices.cs @@ -0,0 +1,19 @@ +using Everywhere.Chat.Plugins; +using Everywhere.Chat.Plugins.BuiltIn; +using Pure.DI; +using static Pure.DI.Lifetime; + +namespace Everywhere.DependencyInjection; + +public partial class CoreComposition +{ + // ReSharper disable once UnusedMember.Local + private static void SetupChatPluginServices() => + DI.Setup() + // Core built-in chat plugins. + .Bind().Bind(Tag.Unique).As(Singleton).To() + .Bind().Bind(Tag.Unique).As(Singleton).To() + .Bind().Bind(Tag.Unique).As(Singleton).To() + .Bind().Bind(Tag.Unique).As(Singleton).To() + .Bind().Bind(Tag.Unique).As(Singleton).To(); +} \ No newline at end of file diff --git a/src/Everywhere.Core/DependencyInjection/ChatServices.cs b/src/Everywhere.Core/DependencyInjection/ChatServices.cs new file mode 100644 index 000000000..9de8936eb --- /dev/null +++ b/src/Everywhere.Core/DependencyInjection/ChatServices.cs @@ -0,0 +1,31 @@ +using Everywhere.AI; +using Everywhere.Chat; +using Everywhere.Common; +using Everywhere.Skills; +using Everywhere.Web; +using Pure.DI; +using static Pure.DI.Lifetime; + +namespace Everywhere.DependencyInjection; + +public partial class CoreComposition +{ + // ReSharper disable once UnusedMember.Local + private static void SetupChatServices() => + DI.Setup() + // Kernel mixins and skill services. + .Bind().As(Singleton).To() + .Bind().As(Singleton).To() + .Bind().Bind().Bind().As(Singleton).To() + + // Chat runtime services. + .Bind().As(Singleton).To() + .Bind().As(Singleton).To() + + // Chat-adjacent host helpers. + .Bind().As(Singleton).To() + .Bind().As(Singleton).To() + + // Chat context state. + .Bind().Bind().As(Singleton).To(); +} \ No newline at end of file diff --git a/src/Everywhere.Core/DependencyInjection/CoreComposition.cs b/src/Everywhere.Core/DependencyInjection/CoreComposition.cs new file mode 100644 index 000000000..52fee21b6 --- /dev/null +++ b/src/Everywhere.Core/DependencyInjection/CoreComposition.cs @@ -0,0 +1,159 @@ +using System.Net; +using Everywhere.AI; +using Everywhere.AI.Prompts; +using Everywhere.AI.Prompts.Database; +using Everywhere.Chat; +using Everywhere.Chat.Plugins.BuiltIn; +using Everywhere.Common; +using Everywhere.Common.Notification; +using Everywhere.Configuration; +using Everywhere.Configuration.Engine; +using Everywhere.Database; +using Everywhere.Initialization; +using Everywhere.Interop; +using Everywhere.Skills; +using Everywhere.Statistics; +using Everywhere.Storage; +using Everywhere.StrategyEngine; +using Everywhere.Views; +using Everywhere.Views.Pages; +using Everywhere.Web; +using Microsoft.Extensions.Logging; +using Pure.DI; +using Pure.DI.MS; +using ShadUI; +using static Pure.DI.Lifetime; + +namespace Everywhere.DependencyInjection; + +#pragma warning disable CA1416 + +// Project DI convention: +// - Bind application services in the business-focused partial setup files. +// - Root exports a service to the final Microsoft IServiceProvider; Pure.DI.MS +// registers by contract type, tag, and lifetime, so roots stay anonymous unless +// code directly calls a generated composition member. +// - OnNewRoot is required by Pure.DI.MS to add roots to IServiceCollection. +// - OnCannotResolve is reserved for framework services owned by MS DI. +public partial class CoreComposition : ServiceProviderFactory +{ + private const RootKinds ExportedRoot = RootKinds.Default | RootKinds.Exported; + + // ReSharper disable once UnusedMember.Local + private static void SetupCoreServices() => + DI.Setup() + // Pure.DI.MS hooks and framework fallbacks. + .Hint(Hint.OnCannotResolve, "On") + .Hint(Hint.OnCannotResolvePartial, "Off") + .Hint(Hint.OnNewRoot, "On") + .Hint(Hint.OnNewRootPartial, "Off") + .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Microsoft.Extensions.*") + .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Microsoft.EntityFrameworkCore.*") + .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "System.Net.Http.*") + + // Logging facade instances are created by Pure.DI; ILoggerFactory stays in MS DI. + .Bind>().As(Singleton).To>() + + // Root service provider and settings roots. + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) +#if WINDOWS + .Root(kind: ExportedRoot) +#endif + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + + // Network and runtime roots. + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + + // Storage, prompt, notification, and statistics roots. + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + + // Avalonia host, shell, and effect roots. + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + + // Navigation page roots. + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + + // Secondary view roots. + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + + // Chat, skill, and browser interaction roots. + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + + // Interop and strategy engine roots. + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + + // Built-in chat plugin concrete roots. + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + + // Pure.DI aggregate consumed through the final MS provider. + .Root>(kind: ExportedRoot); +} \ No newline at end of file diff --git a/src/Everywhere.Core/DependencyInjection/ExternalBoundaryServices.cs b/src/Everywhere.Core/DependencyInjection/ExternalBoundaryServices.cs new file mode 100644 index 000000000..dd09b6955 --- /dev/null +++ b/src/Everywhere.Core/DependencyInjection/ExternalBoundaryServices.cs @@ -0,0 +1,31 @@ +using Everywhere.Chat.Plugins; +using Everywhere.Cloud; +using Everywhere.Common; +using Everywhere.Interop; +using Microsoft.Extensions.DependencyInjection; +using Pure.DI; +using static Pure.DI.Lifetime; + +namespace Everywhere.DependencyInjection; + +public partial class CoreComposition +{ + // Core services sometimes depend on platform or cloud services that are + // registered by later compositions. This file is the intentional boundary + // where Core asks the final MS provider for those external roots. + // ReSharper disable once UnusedMember.Local + private void SetupExternalBoundaryServices() => + DI.Setup() + // Final MS provider bridge. + .Bind().To(_ => ServiceProvider) + + // Cloud and platform services consumed by Core. + .Bind().As(Singleton).To((IServiceProvider x) => x.GetRequiredService()) + .Bind().As(Singleton).To((IServiceProvider x) => x.GetRequiredService()) + .Bind().As(Singleton).To((IServiceProvider x) => x.GetRequiredService()) + .Bind().As(Singleton).To((IServiceProvider x) => x.GetRequiredService()) + .Bind().As(Singleton).To((IServiceProvider x) => x.GetRequiredService()) + .Bind().As(Singleton).To((IServiceProvider x) => x.GetRequiredService()) + .Bind().As(Singleton).To((IServiceProvider x) => x.GetRequiredService()) + .Bind().As(Singleton).To((IServiceProvider x) => x.GetRequiredService()); +} \ No newline at end of file diff --git a/src/Everywhere.Core/DependencyInjection/InteropServices.cs b/src/Everywhere.Core/DependencyInjection/InteropServices.cs new file mode 100644 index 000000000..e56e6d1be --- /dev/null +++ b/src/Everywhere.Core/DependencyInjection/InteropServices.cs @@ -0,0 +1,14 @@ +using Everywhere.Interop; +using Pure.DI; +using static Pure.DI.Lifetime; + +namespace Everywhere.DependencyInjection; + +public partial class CoreComposition +{ + // ReSharper disable once UnusedMember.Local + private static void SetupInteropServices() => + DI.Setup() + // Core watchdog bridge. + .Bind().Bind().As(Singleton).To(); +} \ No newline at end of file diff --git a/src/Everywhere.Core/DependencyInjection/NetworkServices.cs b/src/Everywhere.Core/DependencyInjection/NetworkServices.cs new file mode 100644 index 000000000..c82e1ea2f --- /dev/null +++ b/src/Everywhere.Core/DependencyInjection/NetworkServices.cs @@ -0,0 +1,21 @@ +using System.Net; +using Everywhere.Common; +using Everywhere.Initialization; +using Pure.DI; +using static Pure.DI.Lifetime; + +namespace Everywhere.DependencyInjection; + +public partial class CoreComposition +{ + // ReSharper disable once UnusedMember.Local + private static void SetupNetworkServices() => + DI.Setup() + // Network facade services. + .Bind().Bind().As(Singleton).To() + .Bind().Bind().As(Singleton).To() + .Bind().Bind().As(Singleton).To() + + // Startup network initialization. + .Bind().To(); +} \ No newline at end of file diff --git a/src/Everywhere.Core/DependencyInjection/SettingsServices.cs b/src/Everywhere.Core/DependencyInjection/SettingsServices.cs new file mode 100644 index 000000000..a86b0d76a --- /dev/null +++ b/src/Everywhere.Core/DependencyInjection/SettingsServices.cs @@ -0,0 +1,36 @@ +using Everywhere.Configuration; +using Everywhere.Configuration.Engine; +using Everywhere.Initialization; +using Everywhere.Views; +using Pure.DI; +using static Pure.DI.Lifetime; + +namespace Everywhere.DependencyInjection; + +// Pure.DI reports platform-specific bindings at the composition setup site. +#pragma warning disable CA1416 + +public partial class CoreComposition +{ + // ReSharper disable once UnusedMember.Local + private static void SetupSettingsServices() => + DI.Setup() + // Settings model and engine. + .Bind().As(Singleton).To() + .Bind().To() + + // Settings page controls. + .Bind().To() +#if WINDOWS + .Bind().To() +#endif + .Bind().To() + .Bind().To() + + // Persistent settings storage. + .Bind().Bind().As(Singleton).To() + .Bind().As(Singleton).To() + + // Settings-backed assistant initialization. + .Bind().To(); +} \ No newline at end of file diff --git a/src/Everywhere.Core/DependencyInjection/StorageServices.cs b/src/Everywhere.Core/DependencyInjection/StorageServices.cs new file mode 100644 index 000000000..36bac7692 --- /dev/null +++ b/src/Everywhere.Core/DependencyInjection/StorageServices.cs @@ -0,0 +1,38 @@ +using Everywhere.AI.Prompts; +using Everywhere.AI.Prompts.Database; +using Everywhere.Common.Notification; +using Everywhere.Database; +using Everywhere.Statistics; +using Everywhere.Storage; +using Pure.DI; +using static Pure.DI.Lifetime; + +namespace Everywhere.DependencyInjection; + +public partial class CoreComposition +{ + // ReSharper disable once UnusedMember.Local + private static void SetupStorageServices() => + DI.Setup() + // Prompt storage and resolution. + .Bind().As(Singleton).To() + .Bind().As(Singleton).To() + .Bind().As(Singleton).To() + .Bind().As(Singleton).To() + + // Blob and chat context storage. + .Bind().As(Singleton).To() + .Bind().As(Singleton).To() + + // Notifications and statistics. + .Bind().Bind().As(Singleton).To() + .Bind>().As(Singleton).To>() + .Bind().As(Singleton).To() + .Bind().As(Singleton).To() + + // Database initialization. + .Bind().To() + .Bind().To() + .Bind().To() + .Bind().To(); +} \ No newline at end of file diff --git a/src/Everywhere.Core/DependencyInjection/StrategyEngineServices.cs b/src/Everywhere.Core/DependencyInjection/StrategyEngineServices.cs new file mode 100644 index 000000000..b0e0cdc0e --- /dev/null +++ b/src/Everywhere.Core/DependencyInjection/StrategyEngineServices.cs @@ -0,0 +1,23 @@ +using Everywhere.StrategyEngine; +using Everywhere.StrategyEngine.BuiltIn; +using Pure.DI; +using static Pure.DI.Lifetime; + +namespace Everywhere.DependencyInjection; + +public partial class CoreComposition +{ + // ReSharper disable once UnusedMember.Local + private static void SetupStrategyEngineServices() => + DI.Setup() + // Strategy engine core services. + .Bind().As(Singleton).To() + .Bind().As(Singleton).To() + + // Built-in strategy providers. + .Bind(Tag.Unique).As(Singleton).To() + .Bind(Tag.Unique).As(Singleton).To() + .Bind(Tag.Unique).As(Singleton).To() + .Bind(Tag.Unique).As(Singleton).To() + .Bind(Tag.Unique).As(Singleton).To(); +} diff --git a/src/Everywhere.Core/Everywhere.Core.csproj b/src/Everywhere.Core/Everywhere.Core.csproj index d3df93492..58ca725f6 100644 --- a/src/Everywhere.Core/Everywhere.Core.csproj +++ b/src/Everywhere.Core/Everywhere.Core.csproj @@ -8,7 +8,7 @@ - + @@ -20,9 +20,9 @@ - + - + @@ -41,8 +41,8 @@ - - + + @@ -50,7 +50,12 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + @@ -58,25 +63,24 @@ - + - + - + - + - - - - + + + @@ -106,7 +110,7 @@ - + diff --git a/src/Everywhere.Linux/DependencyInjection/LinuxComposition.cs b/src/Everywhere.Linux/DependencyInjection/LinuxComposition.cs new file mode 100644 index 000000000..680741604 --- /dev/null +++ b/src/Everywhere.Linux/DependencyInjection/LinuxComposition.cs @@ -0,0 +1,87 @@ +using Everywhere.Chat.Plugins; +using Everywhere.Common; +using Everywhere.Common.Notification; +using Everywhere.Configuration; +using Everywhere.Initialization; +using Everywhere.Interop; +using Everywhere.Linux.Chat.Plugins; +using Everywhere.Linux.Common; +using Everywhere.Linux.Interop; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Pure.DI; +using Pure.DI.MS; +using static Pure.DI.Lifetime; + +namespace Everywhere.Linux.DependencyInjection; + +public partial class LinuxComposition : ServiceProviderFactory +{ + private const RootKinds ExportedRoot = RootKinds.Default | RootKinds.Exported; + + // Platform compositions only bind native services, the platform plugin, and + // startup initializers. Shared app services live in CoreComposition. + // ReSharper disable once UnusedMember.Local + private void SetupLinuxServices() => + DI.Setup() + // Pure.DI.MS hooks and framework fallbacks. + .Hint(Hint.OnCannotResolve, "On") + .Hint(Hint.OnCannotResolvePartial, "Off") + .Hint(Hint.OnNewRoot, "On") + .Hint(Hint.OnNewRootPartial, "Off") + .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Microsoft.Extensions.*") + .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Microsoft.EntityFrameworkCore.*") + .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "System.Net.Http.*") + .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Everywhere.*") + + // Final MS provider and logging bridge. + .Bind().To(_ => ServiceProvider) + .Bind>().As(Singleton).To>() + + // Platform-wide notification publishers. + .Bind>().As(Singleton).To>() + + // Linux X11 interop services. + .Bind().Bind().Bind().Bind().As(Singleton).To() + .Bind().As(Singleton).To() + .Bind().As(Singleton).To() + .Bind().As(Singleton).To() + + // Linux update services. + .Bind().As(Singleton).To() + .Bind().As(Singleton).To() + + // Linux chat plugin and plugin manager. + .Bind().Bind(Tag.Unique).As(Singleton).To() + .Bind().Bind().As(Singleton).To(( + IServiceProvider serviceProvider, + Settings settings, + IRuntimeManager runtimeManager, + ILogger logger) => + new ChatPluginManager( + serviceProvider, + serviceProvider.GetRequiredService>(), + settings, + runtimeManager, + logger)) + + // Platform startup initializers. + .Bind().To() + .Bind().To() + + // Linux roots exported to the final MS provider. + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot); +} \ No newline at end of file diff --git a/src/Everywhere.Linux/Everywhere.Linux.csproj b/src/Everywhere.Linux/Everywhere.Linux.csproj index 9ac274ae4..338f0a42f 100644 --- a/src/Everywhere.Linux/Everywhere.Linux.csproj +++ b/src/Everywhere.Linux/Everywhere.Linux.csproj @@ -10,21 +10,28 @@ ..\..\img\Everywhere.ico true true + $(NoWarn);CS0436 - - - - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + - - - + + + + @@ -43,7 +50,7 @@ - + + - diff --git a/src/Everywhere.Linux/Interop/X11WindowBackend.cs b/src/Everywhere.Linux/Interop/X11WindowBackend.cs index 654993c28..53fb57f7a 100644 --- a/src/Everywhere.Linux/Interop/X11WindowBackend.cs +++ b/src/Everywhere.Linux/Interop/X11WindowBackend.cs @@ -28,6 +28,8 @@ public sealed class X11WindowBackend : IWindowBackend, IEventHelper public X11WindowBackend(ILogger logger) { + ValidateEnvironment(logger); + // 1. Initialize Context (Thread & Display) Context = new X11Context(logger); @@ -45,6 +47,19 @@ public X11WindowBackend(ILogger logger) SelectionHandler = new X11SelectionHandler(logger, Context, CoreServices); } + private static void ValidateEnvironment(ILogger logger) + { + if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DISPLAY"))) + { + throw new InvalidOperationException("Fatal Error: DISPLAY environment variable is not set. You should start in GUI env."); + } + + if (Environment.GetEnvironmentVariable("XDG_SESSION_TYPE") != "x11") + { + logger.LogWarning("Not X11 Session, Maybe not supported well"); + } + } + ~X11WindowBackend() { Context.Dispose(); @@ -196,4 +211,4 @@ public IDisposable Subscribe(IObserver observer) .Select(text => new TextSelectionData(text, GetFocusedWindowElement())) .Subscribe(observer); } -} \ No newline at end of file +} diff --git a/src/Everywhere.Linux/Program.cs b/src/Everywhere.Linux/Program.cs index 391556021..490bcf720 100644 --- a/src/Everywhere.Linux/Program.cs +++ b/src/Everywhere.Linux/Program.cs @@ -1,104 +1,49 @@ -using System.Runtime.InteropServices; using Avalonia; using Avalonia.Controls; -using Everywhere.AI; -using Everywhere.Chat; -using Everywhere.Chat.Plugins; +using Everywhere.Cloud.DependencyInjection; using Everywhere.Common; -using Everywhere.Extensions; -using Everywhere.Initialization; -using Everywhere.Interop; +using Everywhere.DependencyInjection; using Everywhere.Linux.Chat.Plugins; -using Everywhere.Linux.Common; -using Everywhere.Linux.Interop; -using Everywhere.StrategyEngine; +using Everywhere.Linux.DependencyInjection; using Microsoft.Extensions.DependencyInjection; -using Serilog; namespace Everywhere.Linux; public static class Program { - - public static IServiceCollection AddWindowEventHelper(this IServiceCollection services) - { - // CheckEnv - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - throw new PlatformNotSupportedException("Fatal Error: Not Linux OS platform."); - if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DISPLAY"))) - throw new InvalidOperationException("Fatal Error: DISPLAY environment variable is not set. You should start in GUI env."); - var session = Environment.GetEnvironmentVariable("XDG_SESSION_TYPE"); - // for future use - // var desktop = Environment.GetEnvironmentVariable("XDG_SESSION_DESKTOP"); - services.AddSingleton(); - if (session != "x11") - { - // not x11, may be not fully supported - Log.Logger.Warning("Not X11 Session, Maybe not supported well"); - } - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(sp => sp.GetRequiredService()); - services.AddSingleton(sp => sp.GetRequiredService()); - return services; - } - [STAThread] public static async Task Main(string[] args) { await Entrance.InitializeAsync(args); - ServiceLocator.Build(x => x - - #region Basic - - .AddApplicationLogging() - .AddWindowEventHelper() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSettings() - .AddWatchdogManager() - .ConfigureNetwork() - .AddAvaloniaBasicServices() - .AddViewsAndViewModels() - .AddDatabaseAndStorage() - .AddChatEssentials() - - #endregion - - #region Chat Plugins - - .AddTransient() - - #endregion - - #region Chat - - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddChatContextManager() + var serviceProvider = CreateServiceProvider(); + ServiceLocator.SetProvider(serviceProvider); - #endregion - - #region Strategy Engine - - .AddStrategyEngine() - - #endregion - - #region Initialize - - .AddTransient() - .AddTransient() - - #endregion - - ); + BuildAvaloniaApp(serviceProvider).StartWithClassicDesktopLifetime(args, ShutdownMode.OnExplicitShutdown); + } - BuildAvaloniaApp(ServiceLocator.Resolve()).StartWithClassicDesktopLifetime(args, ShutdownMode.OnExplicitShutdown); + private static ServiceProvider CreateServiceProvider() + { + var services = new ServiceCollection(); + ApplicationServiceCollection.Configure(services); + CloudServiceCollection.Configure(services); + + var coreComposition = new CoreComposition(); + var cloudComposition = new CloudComposition(); + var linuxComposition = new LinuxComposition(); + + coreComposition.CreateBuilder(services); + cloudComposition.CreateBuilder(services); + linuxComposition.CreateBuilder(services); + ApplicationServiceCollection.ConfigureCoreAliases(services); + CloudServiceCollection.ConfigureAliases(services); + ApplicationServiceCollection.ConfigurePlatformAliases(services); + + var serviceProvider = services.BuildServiceProvider(); + coreComposition.ServiceProvider = serviceProvider; + cloudComposition.ServiceProvider = serviceProvider; + linuxComposition.ServiceProvider = serviceProvider; + return serviceProvider; } private static AppBuilder BuildAvaloniaApp(IServiceProvider serviceProvider) => diff --git a/src/Everywhere.Mac/DependencyInjection/MacComposition.cs b/src/Everywhere.Mac/DependencyInjection/MacComposition.cs new file mode 100644 index 000000000..0a53c75b5 --- /dev/null +++ b/src/Everywhere.Mac/DependencyInjection/MacComposition.cs @@ -0,0 +1,84 @@ +using Everywhere.Chat.Plugins; +using Everywhere.Common; +using Everywhere.Common.Notification; +using Everywhere.Configuration; +using Everywhere.Initialization; +using Everywhere.Interop; +using Everywhere.Mac.Chat.Plugin; +using Everywhere.Mac.Common; +using Everywhere.Mac.Interop; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Pure.DI; +using Pure.DI.MS; +using static Pure.DI.Lifetime; + +namespace Everywhere.Mac.DependencyInjection; + +public partial class MacComposition : ServiceProviderFactory +{ + private const RootKinds ExportedRoot = RootKinds.Default | RootKinds.Exported; + + // Platform compositions only bind native services, the platform plugin, and + // startup initializers. Shared app services live in CoreComposition. + // ReSharper disable once UnusedMember.Local + private void SetupMacServices() => + DI.Setup() + // Pure.DI.MS hooks and framework fallbacks. + .Hint(Hint.OnCannotResolve, "On") + .Hint(Hint.OnCannotResolvePartial, "Off") + .Hint(Hint.OnNewRoot, "On") + .Hint(Hint.OnNewRootPartial, "Off") + .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Microsoft.Extensions.*") + .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Microsoft.EntityFrameworkCore.*") + .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "System.Net.Http.*") + .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Everywhere.*") + + // Final MS provider and logging bridge. + .Bind().To(_ => ServiceProvider) + .Bind>().As(Singleton).To>() + + // Platform-wide notification publishers. + .Bind>().As(Singleton).To>() + + // macOS native interop services. + .Bind().As(Singleton).To() + .Bind().As(Singleton).To() + .Bind().As(Singleton).To() + .Bind().As(Singleton).To() + + // macOS update services. + .Bind().As(Singleton).To() + .Bind().As(Singleton).To() + + // macOS chat plugin and plugin manager. + .Bind().Bind(Tag.Unique).As(Singleton).To() + .Bind().Bind().As(Singleton).To(( + IServiceProvider serviceProvider, + Settings settings, + IRuntimeManager runtimeManager, + ILogger logger) => + new ChatPluginManager( + serviceProvider, + serviceProvider.GetRequiredService>(), + settings, + runtimeManager, + logger)) + + // Platform startup initializers. + .Bind().To() + .Bind().To() + + // macOS roots exported to the final MS provider. + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot); +} \ No newline at end of file diff --git a/src/Everywhere.Mac/Everywhere.Mac.csproj b/src/Everywhere.Mac/Everywhere.Mac.csproj index 186a5df0e..c0e7fa08e 100644 --- a/src/Everywhere.Mac/Everywhere.Mac.csproj +++ b/src/Everywhere.Mac/Everywhere.Mac.csproj @@ -12,6 +12,7 @@ true true true + $(NoWarn);CS0436 false @@ -20,13 +21,13 @@ Entitlements.plist - + - + - <_DynamicLibraryToReidentify Remove="@(_DynamicLibraryToReidentify)" Condition="'%(Filename)' == 'libonigwrap'" /> + <_DynamicLibraryToReidentify Remove="@(_DynamicLibraryToReidentify)" Condition="'%(Filename)' == 'libonigwrap'"/> @@ -47,26 +48,26 @@ - + - <_ExtractedLibOnigWrap Include="@(_FileNativeReference)" Condition="'%(Filename)' == 'libonigwrap'" /> - <_FileNativeReference Remove="@(_ExtractedLibOnigWrap)" /> - <_DynamicLibraryToReidentify Remove="@(_DynamicLibraryToReidentify)" Condition="'%(Filename)' == 'libonigwrap'" /> + <_ExtractedLibOnigWrap Include="@(_FileNativeReference)" Condition="'%(Filename)' == 'libonigwrap'"/> + <_FileNativeReference Remove="@(_ExtractedLibOnigWrap)"/> + <_DynamicLibraryToReidentify Remove="@(_DynamicLibraryToReidentify)" Condition="'%(Filename)' == 'libonigwrap'"/> - - + + - + - + @@ -144,13 +145,18 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - + - - + + diff --git a/src/Everywhere.Mac/Program.cs b/src/Everywhere.Mac/Program.cs index a66882b1e..82992b99c 100644 --- a/src/Everywhere.Mac/Program.cs +++ b/src/Everywhere.Mac/Program.cs @@ -1,15 +1,11 @@ using Avalonia; using Avalonia.Controls; -using Everywhere.Chat.Plugins; -using Everywhere.Cloud; +using Everywhere.Cloud.DependencyInjection; using Everywhere.Common; -using Everywhere.Extensions; -using Everywhere.Initialization; +using Everywhere.DependencyInjection; using Everywhere.Interop; using Everywhere.Mac.Chat.Plugin; -using Everywhere.Mac.Common; -using Everywhere.Mac.Interop; -using Everywhere.StrategyEngine; +using Everywhere.Mac.DependencyInjection; using Microsoft.Extensions.DependencyInjection; namespace Everywhere.Mac; @@ -25,54 +21,38 @@ public static async Task Main(string[] args) await Entrance.InitializeAsync(args); - ServiceLocator.Build(x => x - - #region Basic - - .AddApplicationLogging() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSettings() - .AddWatchdogManager() - .ConfigureNetwork() - .AddAvaloniaBasicServices() - .AddViewsAndViewModels() - .AddDatabaseAndStorage() - .AddCloudClient() - .AddChatEssentials() - - #endregion - - #region Chat Plugins - - .AddTransient() - - #endregion - - #region Strategy Engine - - .AddStrategyEngine() - - #endregion - - #region Initialize - - .AddTransient() - .AddTransient() - - #endregion - - ); + var serviceProvider = CreateServiceProvider(); + ServiceLocator.SetProvider(serviceProvider); NSApplication.CheckForIllegalCrossThreadCalls = false; NSApplication.Init(); NSApplication.SharedApplication.Delegate = new AppDelegate(); - BuildAvaloniaApp(ServiceLocator.Resolve()).StartWithClassicDesktopLifetime(args, ShutdownMode.OnExplicitShutdown); + BuildAvaloniaApp(serviceProvider).StartWithClassicDesktopLifetime(args, ShutdownMode.OnExplicitShutdown); + } + + private static ServiceProvider CreateServiceProvider() + { + var services = new ServiceCollection(); + ApplicationServiceCollection.Configure(services); + CloudServiceCollection.Configure(services); + + var coreComposition = new CoreComposition(); + var cloudComposition = new CloudComposition(); + var macComposition = new MacComposition(); + + coreComposition.CreateBuilder(services); + cloudComposition.CreateBuilder(services); + macComposition.CreateBuilder(services); + ApplicationServiceCollection.ConfigureCoreAliases(services); + CloudServiceCollection.ConfigureAliases(services); + ApplicationServiceCollection.ConfigurePlatformAliases(services); + + var serviceProvider = services.BuildServiceProvider(); + coreComposition.ServiceProvider = serviceProvider; + cloudComposition.ServiceProvider = serviceProvider; + macComposition.ServiceProvider = serviceProvider; + return serviceProvider; } private static NativeMessageBoxResult MessageBoxHandler(string title, string message, NativeMessageBoxButtons buttons, NativeMessageBoxIcon icon) diff --git a/src/Everywhere.Windows/DependencyInjection/WindowsComposition.cs b/src/Everywhere.Windows/DependencyInjection/WindowsComposition.cs new file mode 100644 index 000000000..c697e3d59 --- /dev/null +++ b/src/Everywhere.Windows/DependencyInjection/WindowsComposition.cs @@ -0,0 +1,84 @@ +using Everywhere.Chat.Plugins; +using Everywhere.Common; +using Everywhere.Common.Notification; +using Everywhere.Configuration; +using Everywhere.Initialization; +using Everywhere.Interop; +using Everywhere.Windows.Chat.Plugins; +using Everywhere.Windows.Common; +using Everywhere.Windows.Interop; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Pure.DI; +using Pure.DI.MS; +using static Pure.DI.Lifetime; + +namespace Everywhere.Windows.DependencyInjection; + +public partial class WindowsComposition : ServiceProviderFactory +{ + private const RootKinds ExportedRoot = RootKinds.Default | RootKinds.Exported; + + // Platform compositions only bind native services, the platform plugin, and + // startup initializers. Shared app services live in CoreComposition. + // ReSharper disable once UnusedMember.Local + private void SetupWindowsServices() => + DI.Setup() + // Pure.DI.MS hooks and framework fallbacks. + .Hint(Hint.OnCannotResolve, "On") + .Hint(Hint.OnCannotResolvePartial, "Off") + .Hint(Hint.OnNewRoot, "On") + .Hint(Hint.OnNewRootPartial, "Off") + .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Microsoft.Extensions.*") + .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Microsoft.EntityFrameworkCore.*") + .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "System.Net.Http.*") + .Hint(Hint.OnCannotResolveContractTypeNameWildcard, "Everywhere.*") + + // Final MS provider and logging bridge. + .Bind().To(_ => ServiceProvider) + .Bind>().As(Singleton).To>() + + // Platform-wide notification publishers. + .Bind>().As(Singleton).To>() + + // Windows native interop services. + .Bind().As(Singleton).To() + .Bind().As(Singleton).To() + .Bind().As(Singleton).To() + .Bind().As(Singleton).To() + + // Windows update services. + .Bind().As(Singleton).To() + .Bind().As(Singleton).To() + + // Windows chat plugin and plugin manager. + .Bind().Bind(Tag.Unique).As(Singleton).To() + .Bind().Bind().As(Singleton).To(( + IServiceProvider serviceProvider, + Settings settings, + IRuntimeManager runtimeManager, + ILogger logger) => + new ChatPluginManager( + serviceProvider, + serviceProvider.GetRequiredService>(), + settings, + runtimeManager, + logger)) + + // Platform startup initializers. + .Bind().To() + .Bind().To() + + // Windows roots exported to the final MS provider. + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot) + .Root(kind: ExportedRoot); +} \ No newline at end of file diff --git a/src/Everywhere.Windows/Everywhere.Windows.csproj b/src/Everywhere.Windows/Everywhere.Windows.csproj index 54cb47ec1..4787b7b05 100644 --- a/src/Everywhere.Windows/Everywhere.Windows.csproj +++ b/src/Everywhere.Windows/Everywhere.Windows.csproj @@ -11,7 +11,7 @@ true true true - $(NoWarn),IL2057 + $(NoWarn);IL2057;CS0436 @@ -25,6 +25,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + @@ -34,8 +39,8 @@ - - + + @@ -62,7 +67,7 @@ - + @@ -106,13 +111,13 @@ - - - - - - - + + + + + + + diff --git a/src/Everywhere.Windows/Program.cs b/src/Everywhere.Windows/Program.cs index f2355f03a..7dfd2fb20 100644 --- a/src/Everywhere.Windows/Program.cs +++ b/src/Everywhere.Windows/Program.cs @@ -4,17 +4,12 @@ using Windows.Win32.UI.Shell; using Avalonia; using Avalonia.Controls; -using Everywhere.Chat.Plugins; -using Everywhere.Cloud; +using Everywhere.Cloud.DependencyInjection; using Everywhere.Common; -using Everywhere.Extensions; -using Everywhere.Initialization; -using Everywhere.Interop; +using Everywhere.DependencyInjection; using Everywhere.Messages; -using Everywhere.StrategyEngine; using Everywhere.Windows.Chat.Plugins; -using Everywhere.Windows.Common; -using Everywhere.Windows.Interop; +using Everywhere.Windows.DependencyInjection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Win32; using Serilog; @@ -44,50 +39,34 @@ private static async Task MainAsync(string[] args) RegisterUrlProtocol(); - ServiceLocator.Build(x => x + var serviceProvider = CreateServiceProvider(); + ServiceLocator.SetProvider(serviceProvider); - #region Basic - - .AddApplicationLogging() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSettings() - .AddWatchdogManager() - .ConfigureNetwork() - .AddAvaloniaBasicServices() - .AddViewsAndViewModels() - .AddDatabaseAndStorage() - .AddCloudClient() - .AddChatEssentials() - - #endregion - - #region Chat Plugins - - .AddTransient() - - #endregion - - #region Strategy Engine - - .AddStrategyEngine() - - #endregion - - #region Initialize - - .AddTransient() - .AddTransient() - - #endregion - - ); + BuildAvaloniaApp(serviceProvider).StartWithClassicDesktopLifetime(args, ShutdownMode.OnExplicitShutdown); + } - BuildAvaloniaApp(ServiceLocator.Resolve()).StartWithClassicDesktopLifetime(args, ShutdownMode.OnExplicitShutdown); + private static ServiceProvider CreateServiceProvider() + { + var services = new ServiceCollection(); + ApplicationServiceCollection.Configure(services); + CloudServiceCollection.Configure(services); + + var coreComposition = new CoreComposition(); + var cloudComposition = new CloudComposition(); + var windowsComposition = new WindowsComposition(); + + coreComposition.CreateBuilder(services); + cloudComposition.CreateBuilder(services); + windowsComposition.CreateBuilder(services); + ApplicationServiceCollection.ConfigureCoreAliases(services); + CloudServiceCollection.ConfigureAliases(services); + ApplicationServiceCollection.ConfigurePlatformAliases(services); + + var serviceProvider = services.BuildServiceProvider(); + coreComposition.ServiceProvider = serviceProvider; + cloudComposition.ServiceProvider = serviceProvider; + windowsComposition.ServiceProvider = serviceProvider; + return serviceProvider; } private static AppBuilder BuildAvaloniaApp(IServiceProvider serviceProvider) => diff --git a/tests/Directory.Packages.props b/tests/Directory.Packages.props new file mode 100644 index 000000000..70bc5acdf --- /dev/null +++ b/tests/Directory.Packages.props @@ -0,0 +1,6 @@ + + + + true + + \ No newline at end of file