diff --git a/src/Beutl.Api/Services/InstalledPackageRepository.cs b/src/Beutl.Api/Services/InstalledPackageRepository.cs index c778c5974..9d16e2030 100644 --- a/src/Beutl.Api/Services/InstalledPackageRepository.cs +++ b/src/Beutl.Api/Services/InstalledPackageRepository.cs @@ -16,6 +16,7 @@ public class InstalledPackageRepository : IBeutlApiResource { private readonly ILogger _logger = Log.CreateLogger(); private readonly HashSet _packages = []; + private readonly Dictionary _resolvedBeutlVersions = new(StringComparer.OrdinalIgnoreCase); private readonly Subject<(PackageIdentity Package, bool Exists)> _subject = new(); private const string FileName = "installedPackages.json"; @@ -44,6 +45,7 @@ public void UpgradePackages(PackageIdentity package) } _packages.RemoveWhere(x => StringComparer.OrdinalIgnoreCase.Equals(x.Id, package.Id)); _packages.Add(package); + _resolvedBeutlVersions[package.Id] = BeutlApplication.Version; Save(); foreach (PackageIdentity removed in removedItems) @@ -68,6 +70,7 @@ public void AddPackage(string name, string version) if (_packages.Add(package)) { + _resolvedBeutlVersions[package.Id] = BeutlApplication.Version; Save(); _subject.OnNext((package, true)); } @@ -87,6 +90,7 @@ public void AddPackage(PackageIdentity package) if (_packages.Add(package)) { + _resolvedBeutlVersions[package.Id] = BeutlApplication.Version; Save(); _subject.OnNext((package, true)); } @@ -101,6 +105,7 @@ public void RemovePackage(string name, string version) x => StringComparer.OrdinalIgnoreCase.Equals(x.Id, name) && x.Version == nugetVersion); if (package != null && _packages.Remove(package)) { + _resolvedBeutlVersions.Remove(name); Save(); _subject.OnNext((package, false)); } @@ -112,6 +117,7 @@ public void RemovePackage(PackageIdentity package) _logger.LogInformation("Removing package: {PackageId} with version: {PackageVersion}", package.Id, package.Version); if (_packages.Remove(package)) { + _resolvedBeutlVersions.Remove(package.Id); Save(); _subject.OnNext((package, false)); } @@ -127,6 +133,7 @@ public void RemovePackages(string name) removed = GetLocalPackages(name).ToArray(); } _packages.RemoveWhere(x => StringComparer.OrdinalIgnoreCase.Equals(x.Id, name)); + _resolvedBeutlVersions.Remove(name); Save(); foreach (PackageIdentity package in removed) { @@ -157,6 +164,22 @@ public IObservable GetObservable(string name, string? version = null) return new _Observable(this, name, version); } + public PackageIdentity[] GetPackagesNeedingDependencyReResolution() + { + string currentVersion = BeutlApplication.Version; + return [.. _packages.Where(p => + { + _resolvedBeutlVersions.TryGetValue(p.Id, out string? ver); + return ver != currentVersion; + })]; + } + + public void SetResolvedBeutlVersion(string packageId, string beutlVersion) + { + _resolvedBeutlVersions[packageId] = beutlVersion; + Save(); + } + private void Save() { _logger.LogInformation("Saving installed packages to file."); @@ -164,7 +187,10 @@ private void Save() using (FileStream stream = File.Create(fileName)) { JsonSerializer.Serialize(stream, _packages - .Select(x => new S_Package(x.Id, x.Version.ToString())) + .Select(x => new S_Package( + x.Id, + x.Version.ToString(), + _resolvedBeutlVersions.GetValueOrDefault(x.Id))) .ToArray()); } _logger.LogInformation("Saved {Count} packages to file.", _packages.Count); @@ -183,8 +209,17 @@ private void Restore() if (JsonSerializer.Deserialize(stream) is S_Package[] packages) { _packages.Clear(); + _resolvedBeutlVersions.Clear(); _packages.AddRange(packages.Select(x => new PackageIdentity(x.Name, new NuGetVersion(x.Version)))); + + foreach (S_Package pkg in packages) + { + if (pkg.BeutlVersion is { } beutlVersion) + { + _resolvedBeutlVersions[pkg.Name] = beutlVersion; + } + } } } catch (Exception ex) @@ -202,7 +237,7 @@ private void Restore() } // Serializable - private record S_Package(string Name, string Version); + private record S_Package(string Name, string Version, string? BeutlVersion = null); private sealed class _Observable : LightweightObservableBase { diff --git a/src/Beutl.Api/Services/LoggerAdapter.cs b/src/Beutl.Api/Services/LoggerAdapter.cs new file mode 100644 index 000000000..33c51944d --- /dev/null +++ b/src/Beutl.Api/Services/LoggerAdapter.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.Logging; +using NuGet.Common; +using LogLevel = NuGet.Common.LogLevel; + +namespace Beutl.Api.Services; + +public class LoggerAdapter(Microsoft.Extensions.Logging.ILogger logger) : LoggerBase +{ + private readonly Microsoft.Extensions.Logging.ILogger _logger = logger; + + public override void Log(ILogMessage message) + { + switch (message.Level) + { + case LogLevel.Debug: + _logger.LogDebug(message.ToString()); + break; + case LogLevel.Information: + _logger.LogInformation(message.ToString()); + break; + case LogLevel.Warning: + _logger.LogWarning(message.ToString()); + break; + case LogLevel.Error: + _logger.LogError(message.ToString()); + break; + case LogLevel.Verbose: + _logger.LogTrace(message.ToString()); + break; + case LogLevel.Minimal: + _logger.LogTrace(message.ToString()); + break; + default: + _logger.LogInformation(message.ToString()); + break; + } + } + + public override Task LogAsync(ILogMessage message) + { + Log(message); + return Task.CompletedTask; + } +} diff --git a/src/Beutl.Api/Services/PackageInstaller.cs b/src/Beutl.Api/Services/PackageInstaller.cs index fa69c55dd..cb7f246bd 100644 --- a/src/Beutl.Api/Services/PackageInstaller.cs +++ b/src/Beutl.Api/Services/PackageInstaller.cs @@ -286,6 +286,16 @@ static string ByteArrayToString(byte[] bytes) } } + public async Task ReResolveDependencies( + PackageIdentity package, + ILogger? logger, + CancellationToken cancellationToken = default) + { + var context = PrepareForInstall( + package.Id, package.Version.ToString(), force: true, cancellationToken); + await ResolveDependencies(context, logger, cancellationToken); + } + public async Task ResolveDependencies( PackageInstallContext context, ILogger? logger, @@ -304,11 +314,7 @@ public async Task ResolveDependencies( NuGetFramework nuGetFramework = Helper.GetFrameworkName(); package = new PackageIdentity(packageId, NuGetVersion.Parse(version)); -#if DEBUG - logger ??= ConsoleLogger.Instance; -#else - logger ??= NullLogger.Instance; -#endif + logger ??= new LoggerAdapter(_logger); IEnumerable repositories = _sourceRepositoryProvider.GetRepositories(); var availablePackages = new HashSet(PackageIdentityComparer.Default); diff --git a/src/Beutl.Api/Services/PluginDependencyResolver.cs b/src/Beutl.Api/Services/PluginDependencyResolver.cs index f5a571286..633d1a5b6 100644 --- a/src/Beutl.Api/Services/PluginDependencyResolver.cs +++ b/src/Beutl.Api/Services/PluginDependencyResolver.cs @@ -1,6 +1,6 @@ using System.Reflection; using System.Runtime.Loader; - +using Beutl.Logging; using NuGet.Common; using NuGet.Frameworks; using NuGet.Packaging; @@ -11,6 +11,7 @@ namespace Beutl.Api.Services; // https://github.com/dotnet/runtime/blob/9ec7fc21862f3446c6c6f7dcfff275942e3884d3/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyDependencyResolver.cs internal sealed class PluginDependencyResolver { + private readonly ILogger _logger = new LoggerAdapter(Log.CreateLogger()); private const string NeutralCultureName = "neutral"; private const string ResourceAssemblyExtension = ".dll"; @@ -32,7 +33,7 @@ public PluginDependencyResolver(string mainDirectory, PackageFolderReader? reade reader, reader.GetIdentity(), framework, - NullLogger.Instance, + _logger, availablePackages); } else diff --git a/src/Beutl/Services/StartupTasks/LoadInstalledExtensionTask.cs b/src/Beutl/Services/StartupTasks/LoadInstalledExtensionTask.cs index fe69e3dff..cc4f24382 100644 --- a/src/Beutl/Services/StartupTasks/LoadInstalledExtensionTask.cs +++ b/src/Beutl/Services/StartupTasks/LoadInstalledExtensionTask.cs @@ -16,7 +16,7 @@ public sealed class LoadInstalledExtensionTask : StartupTask private readonly ILogger _logger = Log.CreateLogger(); private readonly PackageManager _manager; - public LoadInstalledExtensionTask(PackageManager manager) + public LoadInstalledExtensionTask(PackageManager manager, Startup startup) { _manager = manager; @@ -24,6 +24,16 @@ public LoadInstalledExtensionTask(PackageManager manager) { using (Activity? activity = Telemetry.StartActivity("LoadInstalledExtensionTask")) { + // 依存関係の再復元完了を待機 + ResolvePackageDependenciesTask resolveTask = + startup.GetTask(); + await resolveTask.Task; + + // 依存関係の再復元に失敗したパッケージIDを収集 + HashSet failedPackageIds = new( + resolveTask.Failures.Select(f => f.Package.Id), + StringComparer.OrdinalIgnoreCase); + // .beutl/packages/ 内のパッケージを読み込む if (!await AsksRunInRestrictedMode()) { @@ -33,6 +43,16 @@ public LoadInstalledExtensionTask(PackageManager manager) Parallel.ForEach(packages, item => { + if (failedPackageIds.Contains(item.Name)) + { + _logger.LogWarning( + "Skipping package {PackageName} due to dependency re-resolution failure.", + item.Name); + Failures.Add((item, new InvalidOperationException( + $"Dependency re-resolution failed for package '{item.Name}'."))); + return; + } + try { _manager.Load(item); diff --git a/src/Beutl/Services/StartupTasks/ResolvePackageDependenciesTask.cs b/src/Beutl/Services/StartupTasks/ResolvePackageDependenciesTask.cs new file mode 100644 index 000000000..662740045 --- /dev/null +++ b/src/Beutl/Services/StartupTasks/ResolvePackageDependenciesTask.cs @@ -0,0 +1,72 @@ +using System.Collections.Concurrent; + +using Beutl.Api.Services; +using Beutl.Logging; + +using Microsoft.Extensions.Logging; + +using NuGet.Common; +using NuGet.Packaging.Core; + +namespace Beutl.Services.StartupTasks; + +public sealed class ResolvePackageDependenciesTask : StartupTask +{ + private readonly ILogger _logger = + Log.CreateLogger(); + + public ResolvePackageDependenciesTask( + InstalledPackageRepository repository, + PackageInstaller installer) + { + Task = Task.Run(async () => + { + using (Activity? activity = Telemetry.StartActivity("ResolvePackageDependenciesTask")) + { + PackageIdentity[] packages = repository.GetPackagesNeedingDependencyReResolution(); + + if (packages.Length == 0) + { + _logger.LogInformation( + "All installed packages were resolved under the current Beutl version. No re-resolution needed."); + return; + } + + _logger.LogInformation( + "Beutl version changed. Re-resolving dependencies for {Count} package(s).", + packages.Length); + + foreach (PackageIdentity package in packages) + { + try + { + activity?.AddEvent(new ActivityEvent( + $"Re-resolving {package.Id} {package.Version}")); + + await installer.ReResolveDependencies( + package, null, CancellationToken.None); + + repository.SetResolvedBeutlVersion( + package.Id, BeutlApplication.Version); + + _logger.LogInformation( + "Successfully re-resolved dependencies for {PackageId} {PackageVersion}.", + package.Id, package.Version); + } + catch (Exception ex) + { + activity?.SetStatus(ActivityStatusCode.Error); + _logger.LogError(ex, + "Failed to re-resolve dependencies for {PackageId} {PackageVersion}.", + package.Id, package.Version); + Failures.Add((package, ex)); + } + } + } + }); + } + + public override Task Task { get; } + + public ConcurrentBag<(PackageIdentity Package, Exception Error)> Failures { get; } = []; +} diff --git a/src/Beutl/Services/StartupTasks/Startup.cs b/src/Beutl/Services/StartupTasks/Startup.cs index 19971e454..ac911cc77 100644 --- a/src/Beutl/Services/StartupTasks/Startup.cs +++ b/src/Beutl/Services/StartupTasks/Startup.cs @@ -58,7 +58,11 @@ public T GetTask() private void RegisterAll() { Register(() => new AuthenticationTask(_apiApp)); - Register(() => new LoadInstalledExtensionTask(_apiApp.GetResource())); + Register(() => new ResolvePackageDependenciesTask( + _apiApp.GetResource(), + _apiApp.GetResource())); + Register(() => new LoadInstalledExtensionTask( + _apiApp.GetResource(), this)); Register(() => new LoadPrimitiveExtensionTask(_apiApp.GetResource())); Register(() => new LoadSideloadExtensionTask(_apiApp.GetResource())); Register(() => new AfterLoadingExtensionsTask(this));