diff --git a/src/Common/src/Net/WindowsNetworkFileShare.cs b/src/Common/src/Net/WindowsNetworkFileShare.cs index 353b61abcf..db6b491897 100644 --- a/src/Common/src/Net/WindowsNetworkFileShare.cs +++ b/src/Common/src/Net/WindowsNetworkFileShare.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. +using System.ComponentModel; using System.Net; namespace Steeltoe.Common.Net; @@ -12,57 +13,6 @@ namespace Steeltoe.Common.Net; public sealed class WindowsNetworkFileShare : IDisposable { private const int NoError = 0; - private const int ErrorAccessDenied = 5; - private const int ErrorAlreadyAssigned = 85; - private const int ErrorPathNotFound = 53; - private const int ErrorBadDevice = 1200; - private const int ErrorBadNetName = 67; - private const int ErrorBadProvider = 1204; - private const int ErrorCancelled = 1223; - private const int ErrorExtendedError = 1208; - private const int ErrorInvalidAddress = 487; - private const int ErrorInvalidParameter = 87; - private const int ErrorInvalidPassword = 86; - private const int ErrorInvalidPasswordName = 1216; - private const int ErrorMoreData = 234; - private const int ErrorNoMoreItems = 259; - private const int ErrorNoNetOrBadPath = 1203; - private const int ErrorNoNetwork = 1222; - private const int ErrorBadProfile = 1206; - private const int ErrorCannotOpenProfile = 1205; - private const int ErrorDeviceInUse = 2404; - private const int ErrorNotConnected = 2250; - private const int ErrorOpenFiles = 2401; - private const int ErrorLogonFailure = 1326; - - // Created with excel formula: - // ="new ErrorClass("&A1&", """&PROPER(SUBSTITUTE(MID(A1,7,LEN(A1)-6), "_", " "))&"""), " - private static readonly Dictionary ErrorMessageLookupTable = new() - { - [ErrorAccessDenied] = "Error: Access Denied", - [ErrorAlreadyAssigned] = "Error: Already Assigned", - [ErrorBadDevice] = "Error: Bad Device", - [ErrorBadNetName] = "Error: Bad Net Name", - [ErrorBadProvider] = "Error: Bad Provider", - [ErrorCancelled] = "Error: Cancelled", - [ErrorExtendedError] = "Error: Extended Error", - [ErrorInvalidAddress] = "Error: Invalid Address", - [ErrorInvalidParameter] = "Error: Invalid Parameter", - [ErrorInvalidPassword] = "Error: Invalid Password", - [ErrorInvalidPasswordName] = "Error: Invalid Password Format", - [ErrorMoreData] = "Error: More Data", - [ErrorNoMoreItems] = "Error: No More Items", - [ErrorNoNetOrBadPath] = "Error: No Net Or Bad Path", - [ErrorNoNetwork] = "Error: No Network", - [ErrorBadProfile] = "Error: Bad Profile", - [ErrorCannotOpenProfile] = "Error: Cannot Open Profile", - [ErrorDeviceInUse] = "Error: Device In Use", - [ErrorNotConnected] = "Error: Not Connected", - [ErrorOpenFiles] = "Error: Open Files", - [ErrorLogonFailure] = "The user name or password is incorrect", - [ErrorPathNotFound] = "The network path not found" - }; - private readonly string _networkName; private readonly IMultipleProviderRouter _multipleProviderRouter; @@ -118,12 +68,9 @@ internal static void ThrowForNonZeroResult(int errorNumber, string operation) { if (errorNumber != NoError) { - if (ErrorMessageLookupTable.TryGetValue(errorNumber, out string? errorMessage)) - { - throw new IOException($"Failed to {operation} with error {errorNumber}: {errorMessage}."); - } + var innerException = new Win32Exception(errorNumber); - throw new IOException($"Failed to {operation} with error {errorNumber}."); + throw new IOException($"Failed to {operation}.", innerException); } } } diff --git a/src/Common/test/Net.Test/WindowsNetworkFileShareTest.cs b/src/Common/test/Net.Test/WindowsNetworkFileShareTest.cs index b9eb26c147..84e63126ab 100644 --- a/src/Common/test/Net.Test/WindowsNetworkFileShareTest.cs +++ b/src/Common/test/Net.Test/WindowsNetworkFileShareTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. +using System.ComponentModel; using System.Net; namespace Steeltoe.Common.Net.Test; @@ -12,17 +13,23 @@ public sealed class WindowsNetworkFileShareTest public void GetErrorForKnownNumber_ReturnsKnownError() { Action action = () => WindowsNetworkFileShare.ThrowForNonZeroResult(5, "execute"); - action.Should().ThrowExactly().WithMessage("*Error: Access Denied*"); + + action.Should().ThrowExactly().WithInnerException() + .WithMessage(Platform.IsWindows ? "Access is Denied*" : "Input/output error"); action = () => WindowsNetworkFileShare.ThrowForNonZeroResult(1222, "execute"); - action.Should().ThrowExactly().WithMessage("*Error: No Network*"); + + action.Should().ThrowExactly().WithInnerException() + .WithMessage(Platform.IsWindows ? "The network is not present or not started." : "Unknown error 1222"); } [Fact] public void GetErrorForUnknownNumber_ReturnsUnKnownError() { Action action = () => WindowsNetworkFileShare.ThrowForNonZeroResult(9999, "execute"); - action.Should().ThrowExactly().WithMessage("Failed to execute with error 9999."); + + action.Should().ThrowExactly().WithInnerException() + .WithMessage(Platform.IsWindows ? "Unknown error (0x270f)" : "Unknown error 9999"); } [Fact] @@ -55,7 +62,8 @@ public void WindowsNetworkFileShare_Constructor_ThrowsOn_ConnectFail() var router = new FakeMultipleProviderRouter(false); var exception = Assert.Throws(() => new WindowsNetworkFileShare("doesn't-matter", new NetworkCredential("user", "password"), router)); + Assert.NotNull(exception.InnerException); - Assert.Equal("Failed to connect to network share with error 1200: Error: Bad Device.", exception.Message); + Assert.Equal(Platform.IsWindows ? "The specified device name is invalid." : "Unknown error 1200", exception.InnerException.Message); } } diff --git a/src/Management/src/Endpoint/Actuators/Health/Contributors/DiskSpaceHealthContributor.cs b/src/Management/src/Endpoint/Actuators/Health/Contributors/DiskSpaceHealthContributor.cs index 49ad093c58..4c0c724510 100644 --- a/src/Management/src/Endpoint/Actuators/Health/Contributors/DiskSpaceHealthContributor.cs +++ b/src/Management/src/Endpoint/Actuators/Health/Contributors/DiskSpaceHealthContributor.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using Microsoft.Extensions.Options; +using Steeltoe.Common; using Steeltoe.Common.HealthChecks; namespace Steeltoe.Management.Endpoint.Actuators.Health.Contributors; @@ -37,33 +38,22 @@ public DiskSpaceHealthContributor(IOptionsMonitor o if (!string.IsNullOrEmpty(options.Path)) { - string absolutePath = Path.GetFullPath(options.Path); + HealthCheckResult? networkDiskHealth = GetNetworkDiskSpaceHealth(options); - if (Directory.Exists(absolutePath)) + if (networkDiskHealth != null) { - DriveInfo[] systemDrives = DriveInfo.GetDrives(); - DriveInfo? driveInfo = FindVolume(absolutePath, systemDrives); + return networkDiskHealth; + } - if (driveInfo != null) - { - long freeSpaceInBytes = driveInfo.TotalFreeSpace; + HealthCheckResult? localDiskHealth = GetLocalDiskSpaceHealth(options); - var result = new HealthCheckResult - { - Status = freeSpaceInBytes >= options.Threshold ? HealthStatus.Up : HealthStatus.Down - }; - - result.Details.Add("total", driveInfo.TotalSize); - result.Details.Add("free", freeSpaceInBytes); - result.Details.Add("threshold", options.Threshold); - result.Details.Add("path", absolutePath); - result.Details.Add("exists", driveInfo.RootDirectory.Exists); - return result; - } + if (localDiskHealth != null) + { + return localDiskHealth; } } - return new HealthCheckResult + var unknownDiskHealth = new HealthCheckResult { Status = HealthStatus.Unknown, Description = "Failed to determine free disk space.", @@ -72,6 +62,70 @@ public DiskSpaceHealthContributor(IOptionsMonitor o ["error"] = "The configured path is invalid or does not exist." } }; + + if (!string.IsNullOrEmpty(options.Path)) + { + unknownDiskHealth.Details["path"] = options.Path; + } + + return unknownDiskHealth; + } + + private static HealthCheckResult? GetNetworkDiskSpaceHealth(DiskSpaceContributorOptions options) + { + if (Platform.IsWindows && options.Path?.StartsWith(@"\\", StringComparison.Ordinal) == true) + { + bool directoryExists = Directory.Exists(options.Path); + + if (directoryExists && NativeMethods.GetDiskFreeSpaceEx(options.Path, out ulong freeBytesAvailable, out ulong totalNumberOfBytes, out _)) + { + return new HealthCheckResult + { + Status = freeBytesAvailable >= (ulong)options.Threshold ? HealthStatus.Up : HealthStatus.Down, + Details = + { + ["total"] = totalNumberOfBytes, + ["free"] = freeBytesAvailable, + ["threshold"] = options.Threshold, + ["path"] = options.Path, + ["exists"] = true + } + }; + } + } + + return null; + } + + private static HealthCheckResult? GetLocalDiskSpaceHealth(DiskSpaceContributorOptions options) + { + string absolutePath = Path.GetFullPath(options.Path!); + + if (Directory.Exists(absolutePath)) + { + DriveInfo[] systemDrives = DriveInfo.GetDrives(); + DriveInfo? driveInfo = FindVolume(absolutePath, systemDrives); + + if (driveInfo != null) + { + long freeSpaceInBytes = driveInfo.TotalFreeSpace; + + return new HealthCheckResult + { + Status = freeSpaceInBytes >= options.Threshold ? HealthStatus.Up : HealthStatus.Down, + Details = + { + ["total"] = driveInfo.TotalSize, + ["free"] = freeSpaceInBytes, + ["threshold"] = options.Threshold, + ["path"] = absolutePath, + ["exists"] = driveInfo.RootDirectory.Exists + } + }; + } + } + + return null; } internal static DriveInfo? FindVolume(string absolutePath, IEnumerable systemDrives) diff --git a/src/Management/src/Endpoint/Actuators/Health/Contributors/NativeMethods.cs b/src/Management/src/Endpoint/Actuators/Health/Contributors/NativeMethods.cs new file mode 100644 index 0000000000..2479b1c88c --- /dev/null +++ b/src/Management/src/Endpoint/Actuators/Health/Contributors/NativeMethods.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; + +namespace Steeltoe.Management.Endpoint.Actuators.Health.Contributors; + +internal static partial class NativeMethods +{ + [LibraryImport("kernel32.dll", EntryPoint = "GetDiskFreeSpaceExW", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool GetDiskFreeSpaceEx(string lpDirectoryName, out ulong lpFreeBytesAvailableToCaller, out ulong lpTotalNumberOfBytes, + out ulong lpTotalNumberOfFreeBytes); +} diff --git a/src/Management/test/Endpoint.Test/Actuators/Health/Contributors/DiskSpaceHealthContributorTest.cs b/src/Management/test/Endpoint.Test/Actuators/Health/Contributors/DiskSpaceHealthContributorTest.cs index bedad8517d..99deee61a8 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Health/Contributors/DiskSpaceHealthContributorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Health/Contributors/DiskSpaceHealthContributorTest.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using Microsoft.Extensions.Options; +using Steeltoe.Common; using Steeltoe.Common.HealthChecks; using Steeltoe.Common.TestResources; using Steeltoe.Management.Endpoint.Actuators.Health.Contributors; @@ -110,4 +111,29 @@ public void Selects_correct_volume(string path, string? expected, params Platfor drive.RootDirectory.FullName.Should().Be(expected); } } + + [Fact(Skip = "Integration test - Requires Windows file share")] + public async Task SupportsWindowsFileShare() + { + if (Platform.IsWindows) + { + Dictionary settings = new() + { + ["Management:Endpoints:Health:DiskSpace:Path"] = @"\\localhost\steeltoe_network_share" + }; + + IOptionsMonitor optionsMonitor = GetOptionsMonitorFromSettings(settings); + var contributor = new DiskSpaceHealthContributor(optionsMonitor); + Assert.Equal("diskSpace", contributor.Id); + HealthCheckResult? result = await contributor.CheckHealthAsync(TestContext.Current.CancellationToken); + Assert.NotNull(result); + Assert.Equal(HealthStatus.Up, result.Status); + Assert.NotNull(result.Details); + Assert.True(result.Details.ContainsKey("total")); + Assert.True(result.Details.ContainsKey("free")); + Assert.True(result.Details.ContainsKey("threshold")); + Assert.True(result.Details.ContainsKey("path")); + Assert.True(result.Details.ContainsKey("exists")); + } + } }