diff --git a/src/DependencyManagement/DependencyManager.cs b/src/DependencyManagement/DependencyManager.cs index 414d8599..d55785cc 100644 --- a/src/DependencyManagement/DependencyManager.cs +++ b/src/DependencyManagement/DependencyManager.cs @@ -55,7 +55,8 @@ public DependencyManager( _installer = installer ?? new DependencySnapshotInstaller( moduleProvider ?? new PowerShellGalleryModuleProvider(), _storage, - new PowerShellModuleSnapshotComparer()); + new PowerShellModuleSnapshotComparer(), + new PowerShellModuleSnapshotLogger()); _newerSnapshotDetector = newerSnapshotDetector ?? new NewerDependencySnapshotDetector(_storage, new WorkerRestarter()); _backgroundSnapshotMaintainer = maintainer ?? new BackgroundDependencySnapshotMaintainer( diff --git a/src/DependencyManagement/DependencySnapshotInstaller.cs b/src/DependencyManagement/DependencySnapshotInstaller.cs index d88d9fda..ded9ebd6 100644 --- a/src/DependencyManagement/DependencySnapshotInstaller.cs +++ b/src/DependencyManagement/DependencySnapshotInstaller.cs @@ -22,15 +22,18 @@ internal class DependencySnapshotInstaller : IDependencySnapshotInstaller private readonly IModuleProvider _moduleProvider; private readonly IDependencyManagerStorage _storage; private readonly IDependencySnapshotComparer _snapshotComparer; + private readonly IDependencySnapshotContentLogger _snapshotContentLogger; public DependencySnapshotInstaller( IModuleProvider moduleProvider, IDependencyManagerStorage storage, - IDependencySnapshotComparer snapshotComparer) + IDependencySnapshotComparer snapshotComparer, + IDependencySnapshotContentLogger snapshotContentLogger) { _moduleProvider = moduleProvider ?? throw new ArgumentNullException(nameof(moduleProvider)); _storage = storage ?? throw new ArgumentNullException(nameof(storage)); _snapshotComparer = snapshotComparer ?? throw new ArgumentNullException(nameof(snapshotComparer)); + _snapshotContentLogger = snapshotContentLogger ?? throw new ArgumentNullException(nameof(snapshotContentLogger)); } public void InstallSnapshot( @@ -75,6 +78,8 @@ public void InstallSnapshot( isUserOnlyLog: false, LogLevel.Trace, string.Format(PowerShellWorkerStrings.PromotedDependencySnapshot, installingPath, targetPath)); + + _snapshotContentLogger.LogDependencySnapshotContent(targetPath, logger); } } catch (Exception e) diff --git a/src/DependencyManagement/IDependencySnapshotContentLogger.cs b/src/DependencyManagement/IDependencySnapshotContentLogger.cs new file mode 100644 index 00000000..ad69671e --- /dev/null +++ b/src/DependencyManagement/IDependencySnapshotContentLogger.cs @@ -0,0 +1,14 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.Azure.Functions.PowerShellWorker.DependencyManagement +{ + using Microsoft.Azure.Functions.PowerShellWorker.Utility; + + internal interface IDependencySnapshotContentLogger + { + void LogDependencySnapshotContent(string snapshotPath, ILogger logger); + } +} diff --git a/src/DependencyManagement/PowerShellModuleSnapshotComparer.cs b/src/DependencyManagement/PowerShellModuleSnapshotComparer.cs index 9f6f4599..440b9c3c 100644 --- a/src/DependencyManagement/PowerShellModuleSnapshotComparer.cs +++ b/src/DependencyManagement/PowerShellModuleSnapshotComparer.cs @@ -32,8 +32,8 @@ public bool AreEquivalent(string snapshotPathA, string snapshotPathB, ILogger lo { try { - var versionSubdirsA = GetModuleVersionSubdirectories(snapshotPathA); - var versionSubdirsB = GetModuleVersionSubdirectories(snapshotPathB); + var versionSubdirsA = PowerShellModuleSnapshotTools.GetModuleVersionSubdirectories(snapshotPathA, _getSubdirectories); + var versionSubdirsB = PowerShellModuleSnapshotTools.GetModuleVersionSubdirectories(snapshotPathB, _getSubdirectories); return versionSubdirsA.SequenceEqual(versionSubdirsB); } catch (IOException e) @@ -50,20 +50,5 @@ public bool AreEquivalent(string snapshotPathA, string snapshotPathB, ILogger lo return false; } } - - private IEnumerable GetModuleVersionSubdirectories(string snapshotPath) - { - var modulePaths = _getSubdirectories(snapshotPath).ToList(); - modulePaths.Sort(); - foreach (var modulePath in modulePaths) - { - var versionPaths = _getSubdirectories(modulePath).ToList(); - versionPaths.Sort(); - foreach (var versionPath in versionPaths) - { - yield return Path.Join(Path.GetFileName(modulePath), Path.GetFileName(versionPath)); - } - } - } } } diff --git a/src/DependencyManagement/PowerShellModuleSnapshotLogger.cs b/src/DependencyManagement/PowerShellModuleSnapshotLogger.cs new file mode 100644 index 00000000..50da9443 --- /dev/null +++ b/src/DependencyManagement/PowerShellModuleSnapshotLogger.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.Azure.Functions.PowerShellWorker.DependencyManagement +{ + using System; + using System.IO; + using Microsoft.Azure.Functions.PowerShellWorker.Utility; + + using LogLevel = Microsoft.Azure.WebJobs.Script.Grpc.Messages.RpcLog.Types.Level; + + internal class PowerShellModuleSnapshotLogger : IDependencySnapshotContentLogger + { + public void LogDependencySnapshotContent(string snapshotPath, ILogger logger) + { + try + { + var moduleVersionSubdirectories = PowerShellModuleSnapshotTools.GetModuleVersionSubdirectories(snapshotPath); + + foreach (var moduleVersionSubdirectory in moduleVersionSubdirectories) + { + // Module version subdirectories follow the following pattern: \\ + var moduleName = Path.GetFileName(Path.GetDirectoryName(moduleVersionSubdirectory)); + var moduleVersion = Path.GetFileName(moduleVersionSubdirectory); + var snapshotName = Path.GetFileName(snapshotPath); + + var message = string.Format(PowerShellWorkerStrings.DependencyShapshotContent, snapshotName, moduleName, moduleVersion); + logger.Log(isUserOnlyLog: false, LogLevel.Trace, message); + } + } + catch (Exception e) when (e is IOException || e is UnauthorizedAccessException) + { + var message = string.Format(PowerShellWorkerStrings.FailedToEnumerateDependencySnapshotContent, snapshotPath, e.Message); + logger.Log(isUserOnlyLog: false, LogLevel.Warning, message); + } + } + } +} diff --git a/src/DependencyManagement/PowerShellModuleSnapshotTools.cs b/src/DependencyManagement/PowerShellModuleSnapshotTools.cs new file mode 100644 index 00000000..5274b394 --- /dev/null +++ b/src/DependencyManagement/PowerShellModuleSnapshotTools.cs @@ -0,0 +1,61 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.Azure.Functions.PowerShellWorker.DependencyManagement +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + + internal class PowerShellModuleSnapshotTools + { + /// + /// Returns all module version subdirectories, assuming that the directory structure follows + /// regular PowerShell conventions. + /// + /// For example, if PowerShell modules are installed into the "C:\Modules" directory + /// and the content of this directory looks like this: + /// + /// C:\ + /// Modules\ + /// ModuleA\ + /// 1.0\ + /// ... + /// 2.1\ + /// ... + /// ModuleB\ + /// 1.3.2\ + /// ... + /// then GetModuleVersionSubdirectories("C:\Modules") will return the following strings: + /// + /// C:\Modules\ModuleA\1.0 + /// C:\Modules\ModuleA\2.1 + /// C:\Modules\ModuleB\1.3.2 + /// + /// + public static IEnumerable GetModuleVersionSubdirectories(string snapshotPath) + { + return GetModuleVersionSubdirectories(snapshotPath, Directory.EnumerateDirectories); + } + + public static IEnumerable GetModuleVersionSubdirectories( + string snapshotPath, + Func> getSubdirectories) + { + var modulePaths = getSubdirectories(snapshotPath).ToList(); + modulePaths.Sort(); + foreach (var modulePath in modulePaths) + { + var versionPaths = getSubdirectories(modulePath).ToList(); + versionPaths.Sort(); + foreach (var versionPath in versionPaths) + { + yield return Path.Join(Path.GetFileName(modulePath), Path.GetFileName(versionPath)); + } + } + } + } +} diff --git a/src/resources/PowerShellWorkerStrings.resx b/src/resources/PowerShellWorkerStrings.resx index ce60d7f6..29d82432 100644 --- a/src/resources/PowerShellWorkerStrings.resx +++ b/src/resources/PowerShellWorkerStrings.resx @@ -292,4 +292,10 @@ Invoking function '{0}' code {1} ms after receiving request. Invocation performance details: {2} + + Dependency snapshot '{0}' contains module '{1}' version '{2}'. + + + Failed to enumerate dependency snapshot '{0}' content: {1} + \ No newline at end of file diff --git a/test/Unit/DependencyManagement/DependencySnapshotInstallerTests.cs b/test/Unit/DependencyManagement/DependencySnapshotInstallerTests.cs index ec18e4e3..3de1dbc9 100644 --- a/test/Unit/DependencyManagement/DependencySnapshotInstallerTests.cs +++ b/test/Unit/DependencyManagement/DependencySnapshotInstallerTests.cs @@ -23,6 +23,7 @@ public class DependencySnapshotInstallerTests private readonly Mock _mockModuleProvider = new Mock(); private readonly Mock _mockStorage = new Mock(MockBehavior.Strict); private readonly Mock _mockSnapshotComparer = new Mock(MockBehavior.Strict); + private readonly Mock _mockSnapshotContentLogger = new Mock(); private readonly Mock _mockLogger = new Mock(); private readonly string _targetPathInstalled; @@ -94,6 +95,7 @@ public void PromotesInstallingSnapshotToInstalledIfSaveModuleDoesNotThrow() _mockStorage.Verify(_ => _.CreateInstallingSnapshot(_targetPathInstalled), Times.Once); _mockStorage.Verify(_ => _.PromoteInstallingSnapshotToInstalledAtomically(_targetPathInstalled), Times.Once); + _mockSnapshotContentLogger.Verify(_ => _.LogDependencySnapshotContent(_targetPathInstalled, _mockLogger.Object), Times.Once); } [Fact] @@ -110,6 +112,7 @@ public void PromotesInstallingSnapshotToInstalledIfNotEquivalentToLatest() _mockStorage.Verify(_ => _.CreateInstallingSnapshot(_targetPathInstalled), Times.Once); _mockStorage.Verify(_ => _.PromoteInstallingSnapshotToInstalledAtomically(_targetPathInstalled), Times.Once); + _mockSnapshotContentLogger.Verify(_ => _.LogDependencySnapshotContent(_targetPathInstalled, _mockLogger.Object), Times.Once); } [Fact] @@ -230,7 +233,11 @@ public void DoesNotPromoteSnapshotIfItIsEquivalentToLatest() private DependencySnapshotInstaller CreateDependenciesSnapshotInstallerWithMocks() { - return new DependencySnapshotInstaller(_mockModuleProvider.Object, _mockStorage.Object, _mockSnapshotComparer.Object); + return new DependencySnapshotInstaller( + _mockModuleProvider.Object, + _mockStorage.Object, + _mockSnapshotComparer.Object, + _mockSnapshotContentLogger.Object); } private void VerifyLoggedOnce(IEnumerable messageParts)