From 8bb73478ae6f460947047563df3bbb65db4d1477 Mon Sep 17 00:00:00 2001 From: Basil Kotov Date: Mon, 1 Jul 2024 15:20:10 +0100 Subject: [PATCH] VCST-1438: Fix 32/64 bit conflict when copying DLL files (#2811) Co-authored-by: Oleg Zhuk Co-authored-by: artem-dudarev --- VirtoCommerce.Platform.sln.DotSettings | 2 + .../Modularity/FileCompareResult.cs | 16 ++ .../Modularity/IFileCopyPolicy.cs | 8 + .../Modularity/IFileMetadataProvider.cs | 13 + .../Local/FileCopyPolicy.cs | 64 +++++ .../Local/FileMetadataProvider.cs | 100 ++++++++ .../Local/LocalStorageModuleCatalog.cs | 225 +++++++----------- src/VirtoCommerce.Platform.Web/Startup.cs | 4 + .../Modularity/ExternalModuleCatalogTests.cs | 151 +++++++++++- .../ModulePlatformCompatibilityTests.cs | 8 +- 10 files changed, 442 insertions(+), 149 deletions(-) create mode 100644 src/VirtoCommerce.Platform.Core/Modularity/FileCompareResult.cs create mode 100644 src/VirtoCommerce.Platform.Core/Modularity/IFileCopyPolicy.cs create mode 100644 src/VirtoCommerce.Platform.Core/Modularity/IFileMetadataProvider.cs create mode 100644 src/VirtoCommerce.Platform.Modules/Local/FileCopyPolicy.cs create mode 100644 src/VirtoCommerce.Platform.Modules/Local/FileMetadataProvider.cs diff --git a/VirtoCommerce.Platform.sln.DotSettings b/VirtoCommerce.Platform.sln.DotSettings index 70c1fa2ca44..f3054d0eea9 100644 --- a/VirtoCommerce.Platform.sln.DotSettings +++ b/VirtoCommerce.Platform.sln.DotSettings @@ -1,6 +1,8 @@  UI <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> + True True True True diff --git a/src/VirtoCommerce.Platform.Core/Modularity/FileCompareResult.cs b/src/VirtoCommerce.Platform.Core/Modularity/FileCompareResult.cs new file mode 100644 index 00000000000..51f175d7840 --- /dev/null +++ b/src/VirtoCommerce.Platform.Core/Modularity/FileCompareResult.cs @@ -0,0 +1,16 @@ +namespace VirtoCommerce.Platform.Core.Modularity; + +public class FileCompareResult +{ + public bool NewFile { get; set; } + public bool NewDate { get; set; } + + public bool SameVersion { get; set; } + public bool NewVersion { get; set; } + public bool SameOrNewVersion => SameVersion || NewVersion; + + public bool CompatibleArchitecture { get; set; } + public bool SameArchitecture { get; set; } + public bool NewArchitecture { get; set; } + public bool SameOrNewArchitecture => SameArchitecture || NewArchitecture; +} diff --git a/src/VirtoCommerce.Platform.Core/Modularity/IFileCopyPolicy.cs b/src/VirtoCommerce.Platform.Core/Modularity/IFileCopyPolicy.cs new file mode 100644 index 00000000000..e17fe8dbaac --- /dev/null +++ b/src/VirtoCommerce.Platform.Core/Modularity/IFileCopyPolicy.cs @@ -0,0 +1,8 @@ +using System.Runtime.InteropServices; + +namespace VirtoCommerce.Platform.Core.Modularity; + +public interface IFileCopyPolicy +{ + bool IsCopyRequired(Architecture environment, string sourceFilePath, string targetFilePath, out FileCompareResult result); +} diff --git a/src/VirtoCommerce.Platform.Core/Modularity/IFileMetadataProvider.cs b/src/VirtoCommerce.Platform.Core/Modularity/IFileMetadataProvider.cs new file mode 100644 index 00000000000..9a568ffc787 --- /dev/null +++ b/src/VirtoCommerce.Platform.Core/Modularity/IFileMetadataProvider.cs @@ -0,0 +1,13 @@ +using System; +using System.Runtime.InteropServices; + +namespace VirtoCommerce.Platform.Core.Modularity +{ + public interface IFileMetadataProvider + { + bool Exists(string filePath); + DateTime? GetDate(string filePath); + Version GetVersion(string filePath); + Architecture? GetArchitecture(string filePath); + } +} diff --git a/src/VirtoCommerce.Platform.Modules/Local/FileCopyPolicy.cs b/src/VirtoCommerce.Platform.Modules/Local/FileCopyPolicy.cs new file mode 100644 index 00000000000..ef5cacf1155 --- /dev/null +++ b/src/VirtoCommerce.Platform.Modules/Local/FileCopyPolicy.cs @@ -0,0 +1,64 @@ +using System.Runtime.InteropServices; +using VirtoCommerce.Platform.Core.Modularity; + +namespace VirtoCommerce.Platform.Modules.Local; + +public class FileCopyPolicy : IFileCopyPolicy +{ + private readonly IFileMetadataProvider _metadataProvider; + + public FileCopyPolicy(IFileMetadataProvider metadataProvider) + { + _metadataProvider = metadataProvider; + } + + public bool IsCopyRequired(Architecture environment, string sourceFilePath, string targetFilePath, out FileCompareResult result) + { + result = new FileCompareResult + { + NewFile = !_metadataProvider.Exists(targetFilePath), + }; + + CompareDates(sourceFilePath, targetFilePath, result); + CompareVersions(sourceFilePath, targetFilePath, result); + CompareArchitecture(sourceFilePath, targetFilePath, environment, result); + + return result.NewFile && result.CompatibleArchitecture || + result.NewVersion && result.SameOrNewArchitecture || + result.NewArchitecture && result.SameOrNewVersion || + result.NewDate && result.SameOrNewArchitecture && result.SameOrNewVersion; + } + + private void CompareDates(string sourceFilePath, string targetFilePath, FileCompareResult result) + { + var sourceDate = _metadataProvider.GetDate(sourceFilePath); + var targetDate = _metadataProvider.GetDate(targetFilePath); + + result.NewDate = sourceDate > targetDate; + } + + private void CompareVersions(string sourceFilePath, string targetFilePath, FileCompareResult result) + { + var sourceVersion = _metadataProvider.GetVersion(sourceFilePath); + var targetVersion = _metadataProvider.GetVersion(targetFilePath); + + result.SameVersion = sourceVersion == targetVersion; + result.NewVersion = targetVersion is not null && sourceVersion > targetVersion; + } + + private void CompareArchitecture(string sourceFilePath, string targetFilePath, Architecture environment, FileCompareResult result) + { + var sourceArchitecture = _metadataProvider.GetArchitecture(sourceFilePath); + var targetArchitecture = _metadataProvider.GetArchitecture(targetFilePath); + + result.CompatibleArchitecture = sourceArchitecture == targetArchitecture || + sourceArchitecture == environment || + sourceArchitecture == Architecture.X86 && environment == Architecture.X64; + + if (result.CompatibleArchitecture) + { + result.SameArchitecture = sourceArchitecture == targetArchitecture; + result.NewArchitecture = sourceArchitecture == environment && targetArchitecture != environment; + } + } +} diff --git a/src/VirtoCommerce.Platform.Modules/Local/FileMetadataProvider.cs b/src/VirtoCommerce.Platform.Modules/Local/FileMetadataProvider.cs new file mode 100644 index 00000000000..8c286121855 --- /dev/null +++ b/src/VirtoCommerce.Platform.Modules/Local/FileMetadataProvider.cs @@ -0,0 +1,100 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.Extensions.Options; +using VirtoCommerce.Platform.Core.Modularity; + +namespace VirtoCommerce.Platform.Modules.Local; + +public class FileMetadataProvider : IFileMetadataProvider +{ + private readonly LocalStorageModuleCatalogOptions _options; + + public FileMetadataProvider(IOptions options) + { + _options = options.Value; + } + + public bool Exists(string filePath) + { + return File.Exists(filePath); + } + + public DateTime? GetDate(string filePath) + { + if (!File.Exists(filePath)) + { + return null; + } + + var fileInfo = new FileInfo(filePath); + + return fileInfo.LastWriteTimeUtc; + } + + public Version GetVersion(string filePath) + { + if (!File.Exists(filePath)) + { + return null; + } + + var fileVersionInfo = FileVersionInfo.GetVersionInfo(filePath); + + return new Version( + fileVersionInfo.FileMajorPart, + fileVersionInfo.FileMinorPart, + fileVersionInfo.FileBuildPart, + fileVersionInfo.FilePrivatePart); + } + + public Architecture? GetArchitecture(string filePath) + { + if (!_options.AssemblyFileExtensions.Any(x => filePath.EndsWith(x, StringComparison.OrdinalIgnoreCase))) + { + return null; + } + + const int startPosition = 0x3C; + const int peSignature = 0x00004550; + + var fileInfo = new FileInfo(filePath); + if (!fileInfo.Exists || fileInfo.Length < startPosition + sizeof(uint)) + { + return null; + } + + using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read); + using var reader = new BinaryReader(stream); + + stream.Seek(startPosition, SeekOrigin.Begin); + var peOffset = reader.ReadUInt32(); + + if (fileInfo.Length < peOffset + sizeof(uint) + sizeof(ushort)) + { + return null; + } + + stream.Seek(peOffset, SeekOrigin.Begin); + var peHead = reader.ReadUInt32(); + + if (peHead != peSignature) + { + return null; + } + + var machineType = reader.ReadUInt16(); + + // https://stackoverflow.com/questions/480696/how-to-find-if-a-native-dll-file-is-compiled-as-x64-or-x86 + return machineType switch + { + 0x8664 => Architecture.X64, + 0xAA64 => Architecture.Arm64, + 0x1C0 => Architecture.Arm, + 0x14C => Architecture.X86, + _ => null + }; + } +} diff --git a/src/VirtoCommerce.Platform.Modules/Local/LocalStorageModuleCatalog.cs b/src/VirtoCommerce.Platform.Modules/Local/LocalStorageModuleCatalog.cs index d618a949497..40d45f21af9 100644 --- a/src/VirtoCommerce.Platform.Modules/Local/LocalStorageModuleCatalog.cs +++ b/src/VirtoCommerce.Platform.Modules/Local/LocalStorageModuleCatalog.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; -using System.Reflection; using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -21,38 +19,51 @@ public class LocalStorageModuleCatalog : ModuleCatalog, ILocalModuleCatalog private readonly LocalStorageModuleCatalogOptions _options; private readonly ILogger _logger; private readonly IInternalDistributedLockService _distributedLockProvider; + private readonly IFileCopyPolicy _fileCopyPolicy; + private readonly string _probingPath; private readonly string _discoveryPath; - public LocalStorageModuleCatalog(IOptions options, IInternalDistributedLockService distributedLockProvider, ILogger logger) + public LocalStorageModuleCatalog( + IOptions options, + IInternalDistributedLockService distributedLockProvider, + IFileCopyPolicy fileCopyPolicy, + ILogger logger) { _options = options.Value; + _probingPath = _options.ProbingPath is null ? null : Path.GetFullPath(_options.ProbingPath); + _discoveryPath = _options.DiscoveryPath; if (!_discoveryPath.EndsWith(PlatformInformation.DirectorySeparator)) { _discoveryPath += PlatformInformation.DirectorySeparator; } + // Resolve IConnectionMultiplexer as multiple services to avoid crash if the platform ran without Redis // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-3.1#service-registration-methods _distributedLockProvider = distributedLockProvider; + _fileCopyPolicy = fileCopyPolicy; _logger = logger; } protected override void InnerLoad() { if (string.IsNullOrEmpty(_options.ProbingPath)) + { throw new InvalidOperationException("The ProbingPath cannot contain a null value or be empty"); + } + if (string.IsNullOrEmpty(_options.DiscoveryPath)) + { throw new InvalidOperationException("The DiscoveryPath cannot contain a null value or be empty"); - + } var manifests = GetModuleManifests(); - var needToCopyAssemblies = _options.RefreshProbingFolderOnStart; - if (!Directory.Exists(_options.ProbingPath)) + if (!Directory.Exists(_probingPath)) { needToCopyAssemblies = true; // Force to refresh assemblies anyway, even if RefreshProbeFolderOnStart set to false, because the probing path is absent - Directory.CreateDirectory(_options.ProbingPath); + Directory.CreateDirectory(_probingPath); } if (needToCopyAssemblies) @@ -76,7 +87,7 @@ protected override void InnerLoad() else { //Set module assembly physical path for future loading by IModuleTypeLoader instance - moduleInfo.Ref = GetFileAbsoluteUri(_options.ProbingPath, manifest.AssemblyFile); + moduleInfo.Ref = GetFileAbsoluteUri(_probingPath, manifest.AssemblyFile); } moduleInfo.IsInstalled = true; @@ -97,7 +108,7 @@ public override IEnumerable CompleteListWithDependencies(IEnumerable { // Do not throw if module was missing // Use ValidateDependencyGraph to validate & write and error of module missing - result = Enumerable.Empty(); + result = []; } return result; @@ -107,6 +118,7 @@ protected override void ValidateDependencyGraph() { var modules = Modules.OfType(); var manifestModules = modules as ManifestModuleInfo[] ?? modules.ToArray(); + try { base.ValidateDependencyGraph(); @@ -145,7 +157,7 @@ public override void Validate() module.Errors.Add($"Module platform version {module.PlatformVersion} is incompatible with current {PlatformVersion.CurrentVersion}"); } - //Check that incompatible modules does not installed + //Check that incompatible modules are not installed if (!module.Incompatibilities.IsNullOrEmpty()) { var installedIncompatibilities = manifestModules.Select(x => x.Identity).Join(module.Incompatibilities, x => x.Id, y => y.Id, (x, y) => new { x, y }) @@ -173,20 +185,21 @@ private IDictionary GetModuleManifests() { var result = new Dictionary(); - if (Directory.Exists(_options.DiscoveryPath)) + if (Directory.Exists(_discoveryPath)) { - foreach (var manifestFile in Directory.EnumerateFiles(_options.DiscoveryPath, "module.manifest", SearchOption.AllDirectories)) + foreach (var manifestFile in Directory.EnumerateFiles(_discoveryPath, "module.manifest", SearchOption.AllDirectories)) { - // Exclude manifests from the builded modules + // Exclude manifests from the built modules // starting from the relative module directory, excluding the discovery modules path // particularly "artifacts" folder - if (!manifestFile.Substring(_options.DiscoveryPath.Length).Contains("artifacts")) + if (!manifestFile.Substring(_discoveryPath.Length).Contains("artifacts")) { var manifest = ManifestReader.Read(manifestFile); result.Add(manifestFile, manifest); } } } + return result; } @@ -196,11 +209,11 @@ private void CopyAssembliesSynchronized(IDictionary mani { if (x != DistributedLockCondition.Delayed) { - CopyAssemblies(_discoveryPath, _options.ProbingPath); // Copy platform files if needed + CopyAssemblies(_discoveryPath, _probingPath); // Copy platform files if needed foreach (var pair in manifests) { var modulePath = Path.GetDirectoryName(pair.Key); - CopyAssemblies(modulePath, _options.ProbingPath); // Copy module files if needed + CopyAssemblies(modulePath, _probingPath); // Copy module files if needed } } else // Delayed lock acquire, do nothing here with a notice logging @@ -217,172 +230,97 @@ private void CopyAssembliesSynchronized(IDictionary mani /// private string GetSourceMark() { - var markerFilePath = Path.Combine(_options.ProbingPath, "storage.mark"); + var markerFilePath = Path.Combine(_probingPath, "storage.mark"); var marker = Guid.NewGuid().ToString(); + try { if (File.Exists(markerFilePath)) { - using (var stream = File.OpenText(markerFilePath)) - { - marker = stream.ReadToEnd(); - } + using var stream = File.OpenText(markerFilePath); + marker = stream.ReadToEnd(); } else { // Non-marked storage, mark by placing a file with resource id. - using (var stream = File.CreateText(markerFilePath)) - { - stream.Write(marker); - } + using var stream = File.CreateText(markerFilePath); + stream.Write(marker); } } catch (IOException exc) { - throw new PlatformException($"An IO error occurred while marking local modules storage.", exc); + throw new PlatformException("An IO error occurred while marking local modules storage.", exc); } - return $@"{nameof(LocalStorageModuleCatalog)}-{marker}"; + + return $"{nameof(LocalStorageModuleCatalog)}-{marker}"; } - private void CopyAssemblies(string sourceParentPath, string targetDirectoryPath) + private void CopyAssemblies(string sourceDirectoryPath, string targetDirectoryPath) { - if (sourceParentPath != null) + if (sourceDirectoryPath is null) { - var sourceDirectoryPath = Path.Combine(sourceParentPath, "bin"); - - if (Directory.Exists(sourceDirectoryPath)) - { - foreach (var sourceFilePath in Directory.EnumerateFiles(sourceDirectoryPath, "*.*", SearchOption.AllDirectories)) - { - // Copy all assembly related files except assemblies that are included in TPA list & reference assemblies - if (IsAssemblyRelatedFile(sourceFilePath) && !(IsAssemblyFile(sourceFilePath) && - (IsReferenceAssemblyFile(sourceFilePath) || TPA.ContainsAssembly(Path.GetFileName(sourceFilePath))))) - { - // Copy localization resource files to related subfolders - var targetFilePath = Path.Combine( - IsLocalizationFile(sourceFilePath) ? Path.Combine(targetDirectoryPath, Path.GetFileName(Path.GetDirectoryName(sourceFilePath))) - : targetDirectoryPath, - Path.GetFileName(sourceFilePath)); - CopyFile(sourceFilePath, targetFilePath); - } - } - } + return; } - } - - private void CopyFile(string sourceFilePath, string targetFilePath) - { - var sourceFileInfo = new FileInfo(sourceFilePath); - var targetFileInfo = new FileInfo(targetFilePath); - - var sourceFileVersionInfo = FileVersionInfo.GetVersionInfo(sourceFilePath); - var sourceVersion = new Version(sourceFileVersionInfo.FileMajorPart, sourceFileVersionInfo.FileMinorPart, sourceFileVersionInfo.FileBuildPart, sourceFileVersionInfo.FilePrivatePart); - var targetVersion = sourceVersion; - if (targetFileInfo.Exists) + var sourceBinPath = Path.Combine(sourceDirectoryPath, "bin"); + if (!Directory.Exists(sourceBinPath)) { - var targetFileVersionInfo = FileVersionInfo.GetVersionInfo(targetFilePath); - targetVersion = new Version(targetFileVersionInfo.FileMajorPart, targetFileVersionInfo.FileMinorPart, targetFileVersionInfo.FileBuildPart, targetFileVersionInfo.FilePrivatePart); + return; } - var versionsAreSameButLaterDate = (sourceVersion == targetVersion && targetFileInfo.Exists && sourceFileInfo.Exists && targetFileInfo.LastWriteTimeUtc < sourceFileInfo.LastWriteTimeUtc); - - var replaceBitwiseReason = targetFileInfo.Exists - && sourceVersion.Equals(targetVersion) - && ReplaceBitwiseReason(sourceFilePath, targetFilePath); - - if (!targetFileInfo.Exists || sourceVersion > targetVersion || versionsAreSameButLaterDate || replaceBitwiseReason) + foreach (var sourceFilePath in Directory.EnumerateFiles(sourceBinPath, "*.*", SearchOption.AllDirectories)) { - var targetDirectoryPath = Path.GetDirectoryName(targetFilePath); - Directory.CreateDirectory(targetDirectoryPath); + var fileName = Path.GetFileName(sourceFilePath); - try - { - File.Copy(sourceFilePath, targetFilePath, true); - } - catch (IOException) + // Copy assembly related files except assemblies that are included in TPA list & reference assemblies + if (IsAssemblyRelatedFile(sourceFilePath) && + !(IsAssemblyFile(sourceFilePath) && (IsReferenceAssemblyFile(sourceFilePath) || TPA.ContainsAssembly(fileName)))) { - // VP-3719: Need to catch to avoid possible problem when different instances are trying to update the same file with the same version but different dates in the probing folder. - // We should not fail platform start in that case - just add warning into the log. In case of inability to place newer version - should fail platform start. - if (versionsAreSameButLaterDate) - { - _logger.LogWarning($"File '{targetFilePath}' was not updated by '{sourceFilePath}' of the same version but later modified date, because probably it was used by another process"); - } - else - { - throw; - } + // Copy localization resource files to related subfolders + var targetParentPath = IsLocalizationFile(sourceFilePath) + ? GetTargetLocalizationDirectoryPath(targetDirectoryPath, sourceFilePath) + : targetDirectoryPath; + + var targetFilePath = Path.Combine(targetParentPath, fileName); + + CopyFile(sourceFilePath, targetFilePath, targetParentPath); } } } - private bool ReplaceBitwiseReason(string sourceFilePath, string targetFilePath) + private void CopyFile(string sourceFilePath, string targetFilePath, string targetDirectoryPath) { - if (IsManagedLibrary(targetFilePath) || !sourceFilePath.EndsWith(".dll", StringComparison.InvariantCultureIgnoreCase)) - { - return false; - } - - var environment = RuntimeInformation.OSArchitecture; - var targetDllArchitecture = GetDllArchitecture(targetFilePath); - - if (environment == targetDllArchitecture) - { - return false; - } - - var sourceDllArchitecture = GetDllArchitecture(sourceFilePath); - if (environment == sourceDllArchitecture) + var environment = Environment.Is64BitProcess ? Architecture.X64 : Architecture.X86; + if (!_fileCopyPolicy.IsCopyRequired(environment, sourceFilePath, targetFilePath, out var result)) { - return true; + return; } - return false; - } + Directory.CreateDirectory(targetDirectoryPath); - private bool IsManagedLibrary(string pathToDll) - { try { - AssemblyName.GetAssemblyName(pathToDll); - return true; + File.Copy(sourceFilePath, targetFilePath, true); } - catch + catch (IOException) { - // file is unmanaged + // VP-3719: Need to catch to avoid possible problem when different instances are trying to update the same file with the same version but different dates in the probing folder. + // We should not fail platform start in that case - just add warning into the log. In case of inability to place newer version - should fail platform start. + if (result.NewDate) + { + _logger.LogWarning("File '{targetFilePath}' was not updated by '{sourceFilePath}' of the same version but later modified date, because probably it was used by another process", targetFilePath, sourceFilePath); + } + else + { + throw; + } } - - return false; } - private static Architecture? GetDllArchitecture(string dllPath) + private static string GetTargetLocalizationDirectoryPath(string targetDirectoryPath, string sourceFilePath) { - using var fs = new FileStream(dllPath, FileMode.Open, FileAccess.Read); - using var br = new BinaryReader(fs); - - fs.Seek(0x3C, SeekOrigin.Begin); - var peOffset = br.ReadInt32(); - fs.Seek(peOffset, SeekOrigin.Begin); - var peHead = br.ReadUInt32(); - const int peSignature = 0x00004550; - if (peHead != peSignature) - { - return null; - } - - var machineType = br.ReadUInt16(); - - // https://stackoverflow.com/questions/480696/how-to-find-if-a-native-dll-file-is-compiled-as-x64-or-x86 - Architecture? archType = machineType switch - { - 0x8664 => Architecture.X64, - 0xAA64 => Architecture.Arm64, - 0x1C0 => Architecture.Arm, - 0x14C => Architecture.X86, - _ => null - }; - - return archType; + var directoryName = GetLastDirectoryName(sourceFilePath); + return Path.Combine(targetDirectoryPath, directoryName); } private bool IsAssemblyRelatedFile(string path) @@ -401,8 +339,13 @@ private bool IsReferenceAssemblyFile(string path) // We need to rewrite platform initialization code // to use correct solution with MetadataLoadContext // TODO: PT-6241 - var lastFolderName = Path.GetFileName(Path.GetDirectoryName(path)); - return _options.ReferenceAssemblyFolders.Any(x => lastFolderName.Equals(x, StringComparison.OrdinalIgnoreCase)); + var directoryName = GetLastDirectoryName(path); + return _options.ReferenceAssemblyFolders.Any(directoryName.EqualsIgnoreCase); + } + + private static string GetLastDirectoryName(string filePath) + { + return Path.GetFileName(Path.GetDirectoryName(filePath)); } private bool IsLocalizationFile(string path) diff --git a/src/VirtoCommerce.Platform.Web/Startup.cs b/src/VirtoCommerce.Platform.Web/Startup.cs index 626d1588091..85f1cdd0737 100644 --- a/src/VirtoCommerce.Platform.Web/Startup.cs +++ b/src/VirtoCommerce.Platform.Web/Startup.cs @@ -55,6 +55,7 @@ using VirtoCommerce.Platform.DistributedLock; using VirtoCommerce.Platform.Hangfire.Extensions; using VirtoCommerce.Platform.Modules; +using VirtoCommerce.Platform.Modules.Local; using VirtoCommerce.Platform.Security; using VirtoCommerce.Platform.Security.Authorization; using VirtoCommerce.Platform.Security.ExternalSignIn; @@ -129,6 +130,9 @@ public void ConfigureServices(IServiceCollection services) //Get platform version from GetExecutingAssembly PlatformVersion.CurrentVersion = SemanticVersion.Parse(FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion); + services.AddSingleton(); + services.AddSingleton(); + services.AddDbContext((provider, options) => { var databaseProvider = Configuration.GetValue("DatabaseProvider", "SqlServer"); diff --git a/tests/VirtoCommerce.Platform.Tests/Modularity/ExternalModuleCatalogTests.cs b/tests/VirtoCommerce.Platform.Tests/Modularity/ExternalModuleCatalogTests.cs index 707f78c71ff..c92d60bea4d 100644 --- a/tests/VirtoCommerce.Platform.Tests/Modularity/ExternalModuleCatalogTests.cs +++ b/tests/VirtoCommerce.Platform.Tests/Modularity/ExternalModuleCatalogTests.cs @@ -1,15 +1,18 @@ using System; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Text; using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Moq; using Newtonsoft.Json; using VirtoCommerce.Platform.Core.Common; using VirtoCommerce.Platform.Core.Modularity; using VirtoCommerce.Platform.Modules; using VirtoCommerce.Platform.Modules.External; +using VirtoCommerce.Platform.Modules.Local; using Xunit; using Xunit.Extensions.Ordering; @@ -140,7 +143,7 @@ public void CreateDirectory_CreateTestDirectory(string platformVersion, string[] }, } }; - + //Act var extCatalog = CreateExternalModuleCatalog(new[] { moduleA }, includePrerelease); extCatalog.Load(); @@ -149,7 +152,7 @@ public void CreateDirectory_CreateTestDirectory(string platformVersion, string[] //Assert var actualVersions = extCatalog.Modules.OfType().Where(x => x.Id == moduleA.Id).Select(x => x.Version); var expectedVersions = expectedModuleVersions.Select(x => SemanticVersion.Parse(x)); - + Assert.Equal(expectedVersions, actualVersions); _mutex.ReleaseMutex(); @@ -194,14 +197,150 @@ public void CreateDirectory_NoDowngrades(string externalModuleVersion, string ef _mutex.ReleaseMutex(); } + [Theory] + [InlineData(false, null, true)] + [InlineData(true, false, false)] + [InlineData(true, true, true)] + public void NonExecutableFilesCopyTest( + bool targetExists, + bool? isSourceNewByDate, + bool expectedCopyRequired) + { + //Arrange + var metadataProvider = new Mock(); + var sourcePath = @"c:\source\non_executable.xml"; + var targetPath = @"c:\target\non_executable.xml"; + var targetDate = DateTime.UtcNow; + var sourceDate = isSourceNewByDate == true ? targetDate.AddDays(1) : targetDate; + + AddFile(metadataProvider, sourcePath, sourceDate, version: null, architecture: null); + + if (targetExists) + { + AddFile(metadataProvider, targetPath, targetDate, version: null, architecture: null); + } + + //Act + var copyFilePolicy = new FileCopyPolicy(metadataProvider.Object); + var actualCopyRequired = copyFilePolicy.IsCopyRequired(Architecture.X64, sourcePath, targetPath, out _); + + //Assert + Assert.Equal(expectedCopyRequired, actualCopyRequired); + } + + [Theory] + [InlineData(null, false, null, null, true)] + [InlineData(null, true, "1.0.0.0", true, false)] + [InlineData(null, true, "1.0.0.0", false, false)] + [InlineData("1.0.0.0", false, null, null, true)] + [InlineData("1.0.0.0", true, "1.0.0.0", false, false)] + [InlineData("1.0.0.0", true, "1.0.0.0", true, true)] + [InlineData("1.0.0.0", true, "1.0.0.1", false, false)] + [InlineData("1.0.0.0", true, "1.0.0.1", true, false)] + [InlineData("1.0.0.2", true, "1.0.0.1", false, true)] + [InlineData("1.0.0.2", true, "1.0.0.1", true, true)] + public void ExecutableFilesWithDifferentVersionsCopyTest( + string sourceVersion, + bool targetExists, + string targetVersion, + bool? isSourceNewByDate, + bool expectedCopyRequired) + { + //Arrange + var metadataProvider = new Mock(); + var sourcePath = @"c:\source\assembly.dll"; + var targetPath = @"c:\target\assembly.dll"; + var targetDate = DateTime.UtcNow; + var sourceDate = isSourceNewByDate == true ? targetDate.AddDays(1) : targetDate; + + AddFile(metadataProvider, sourcePath, sourceDate, sourceVersion, Architecture.X64); + + if (targetExists) + { + AddFile(metadataProvider, targetPath, targetDate, targetVersion, Architecture.X64); + } + + //Act + var copyFilePolicy = new FileCopyPolicy(metadataProvider.Object); + var actualCopyRequired = copyFilePolicy.IsCopyRequired(Architecture.X64, sourcePath, targetPath, out _); + + //Assert + Assert.Equal(expectedCopyRequired, actualCopyRequired); + } + + [Theory] + [InlineData(Architecture.X64, null, false, null, null, true)] + [InlineData(Architecture.X64, null, true, null, true, true)] + [InlineData(Architecture.X64, null, true, null, false, false)] + [InlineData(Architecture.X64, null, true, Architecture.X64, true, false)] + [InlineData(Architecture.X64, null, true, Architecture.X86, true, false)] + [InlineData(Architecture.X64, Architecture.X64, false, null, null, true)] + [InlineData(Architecture.X64, Architecture.X86, false, null, null, true)] + [InlineData(Architecture.X86, Architecture.X86, false, null, null, true)] + [InlineData(Architecture.X86, Architecture.X64, false, null, null, false)] + [InlineData(Architecture.X64, Architecture.X64, true, null, true, true)] + [InlineData(Architecture.X64, Architecture.X64, true, Architecture.X64, false, false)] + [InlineData(Architecture.X64, Architecture.X64, true, Architecture.X86, false, true)] + [InlineData(Architecture.X64, Architecture.X86, true, Architecture.X64, false, false)] + [InlineData(Architecture.X64, Architecture.X86, true, Architecture.X86, false, false)] + [InlineData(Architecture.X64, Architecture.X64, true, Architecture.X64, true, true)] + [InlineData(Architecture.X64, Architecture.X64, true, Architecture.X86, true, true)] + [InlineData(Architecture.X64, Architecture.X86, true, Architecture.X64, true, false)] + [InlineData(Architecture.X64, Architecture.X86, true, Architecture.X86, true, true)] + [InlineData(Architecture.X86, Architecture.X64, true, Architecture.X64, false, false)] + [InlineData(Architecture.X86, Architecture.X64, true, Architecture.X86, false, false)] + [InlineData(Architecture.X86, Architecture.X86, true, Architecture.X64, false, true)] + [InlineData(Architecture.X86, Architecture.X86, true, Architecture.X86, false, false)] + [InlineData(Architecture.X86, Architecture.X64, true, Architecture.X64, true, true)] + [InlineData(Architecture.X86, Architecture.X64, true, Architecture.X86, true, false)] + [InlineData(Architecture.X86, Architecture.X86, true, Architecture.X64, true, true)] + [InlineData(Architecture.X86, Architecture.X86, true, Architecture.X86, true, true)] + public void ExecutableFilesWithDifferentArchitectureCopyTest( + Architecture environment, + Architecture? sourceArchitecture, + bool targetExists, + Architecture? targetArchitecture, + bool? isSourceNewByDate, + bool expectedCopyRequired) + { + //Arrange + var metadataProvider = new Mock(); + var sourcePath = @"c:\source\assembly.dll"; + var targetPath = @"c:\target\assembly.dll"; + var targetDate = DateTime.UtcNow; + var sourceDate = isSourceNewByDate == true ? targetDate.AddDays(1) : targetDate; + + AddFile(metadataProvider, sourcePath, sourceDate, "1.0.0.0", sourceArchitecture); + + if (targetExists) + { + AddFile(metadataProvider, targetPath, targetDate, "1.0.0.0", targetArchitecture); + } + + //Act + var copyFilePolicy = new FileCopyPolicy(metadataProvider.Object); + var actualCopyRequired = copyFilePolicy.IsCopyRequired(environment, sourcePath, targetPath, out _); + + //Assert + Assert.Equal(expectedCopyRequired, actualCopyRequired); + } + + private static void AddFile(Mock metadataProvider, string path, DateTime date, string version, Architecture? architecture) + { + metadataProvider.Setup(x => x.Exists(path)).Returns(true); + metadataProvider.Setup(x => x.GetDate(path)).Returns(date); + metadataProvider.Setup(x => x.GetVersion(path)).Returns(version is null ? null : new Version(version)); + metadataProvider.Setup(x => x.GetArchitecture(path)).Returns(architecture); + } + private static ExternalModuleCatalog CreateExternalModuleCatalog(ExternalModuleManifest[] manifests, bool includePrerelease = false) { - var localModulesCatalog = new Moq.Mock(); + var localModulesCatalog = new Mock(); localModulesCatalog.Setup(x => x.Modules).Returns(GetManifestModuleInfos(new[] { new ModuleManifest { Id = "B", Version = "1.3.0", PlatformVersion = "3.0.0" } })); var json = JsonConvert.SerializeObject(manifests); - var client = new Moq.Mock(); - client.Setup(x => x.OpenRead(Moq.It.IsAny())).Returns(new MemoryStream(Encoding.UTF8.GetBytes(json ?? ""))); - var logger = new Moq.Mock>(); + var client = new Mock(); + client.Setup(x => x.OpenRead(It.IsAny())).Returns(new MemoryStream(Encoding.UTF8.GetBytes(json ?? ""))); + var logger = new Mock>(); var options = Options.Create(new ExternalModuleCatalogOptions() { ModulesManifestUrl = new Uri("http://nowhere.mock"), IncludePrerelease = includePrerelease }); var result = new ExternalModuleCatalog(localModulesCatalog.Object, client.Object, options, logger.Object); diff --git a/tests/VirtoCommerce.Platform.Tests/Modularity/ModulePlatformCompatibilityTests.cs b/tests/VirtoCommerce.Platform.Tests/Modularity/ModulePlatformCompatibilityTests.cs index cc532e859e4..c3528ab6d5f 100644 --- a/tests/VirtoCommerce.Platform.Tests/Modularity/ModulePlatformCompatibilityTests.cs +++ b/tests/VirtoCommerce.Platform.Tests/Modularity/ModulePlatformCompatibilityTests.cs @@ -24,9 +24,13 @@ public void Module(string targetPlatformVersion, string runningPlatformVersion, { var catalogOptionsMock = new Mock>(); catalogOptionsMock.Setup(x => x.Value).Returns(new LocalStorageModuleCatalogOptions() { DiscoveryPath = string.Empty }); - var catalog = new LocalStorageModuleCatalog(catalogOptionsMock.Object, new Mock().Object, new Mock>().Object); + var catalog = new LocalStorageModuleCatalog( + catalogOptionsMock.Object, + new Mock().Object, + new Mock().Object, + new Mock>().Object); PlatformVersion.CurrentVersion = SemanticVersion.Parse(runningPlatformVersion); - var module = new ManifestModuleInfo().LoadFromManifest(new ModuleManifest() { PlatformVersion = targetPlatformVersion, Id="Fake", Version ="0.0.0" /*Does not matter (not used in test)*/ }); + var module = new ManifestModuleInfo().LoadFromManifest(new ModuleManifest() { PlatformVersion = targetPlatformVersion, Id = "Fake", Version = "0.0.0" /*Does not matter (not used in test)*/ }); catalog.AddModule(module); catalog.Validate(); Assert.True(module.Errors.Count > 0 == violation);