diff --git a/GVFS/GVFS.Platform.Windows/ProjFSFilter.cs b/GVFS/GVFS.Platform.Windows/ProjFSFilter.cs index 3d49968180..83ef1cd898 100644 --- a/GVFS/GVFS.Platform.Windows/ProjFSFilter.cs +++ b/GVFS/GVFS.Platform.Windows/ProjFSFilter.cs @@ -25,6 +25,12 @@ public class ProjFSFilter : IKernelDriver private const string PrjFltAutoLoggerKey = "SYSTEM\\CurrentControlSet\\Control\\WMI\\Autologger\\Microsoft-Windows-ProjFS-Filter-Log"; private const string PrjFltAutoLoggerStartValue = "Start"; + // ERROR_SERVICE_DISABLED - returned by StartService when the service Start type is Disabled. + private const int ServiceDisabledWin32Error = 1058; + + // ERROR_SERVICE_DOES_NOT_EXIST - returned when the service is not installed. + private const int ServiceDoesNotExistWin32Error = 1060; + private const string System32LogFilesRoot = @"%SystemRoot%\System32\LogFiles"; // From "Autologger" section of prjflt.inf @@ -46,6 +52,15 @@ private enum ProjFSInboxStatus Disabled = 4, } + // Stable values: recorded in telemetry, so do not rename them. + public enum ServiceStartFailureReason + { + None, + ServiceDisabled, + ServiceNotFound, + StartFailed, + } + public bool EnumerationExpandsDirectories { get; } = false; public bool EmptyPlaceholdersRequireFileSize { get; } = true; @@ -176,32 +191,91 @@ public static bool IsServiceRunningAndInstalled( return isRunning; } - public static bool TryStartService(ITracer tracer) + public static bool TryStartService(ITracer tracer, out ServiceStartFailureReason failureReason, out string error) { + failureReason = ServiceStartFailureReason.None; + error = null; try { ServiceController controller = new ServiceController(DriverName); + if (!controller.Status.Equals(ServiceControllerStatus.Running)) { + // A disabled service cannot be started. Return a specific, actionable message + // naming the driver and the re-enable command. + if (TryGetDisabledStartTypeError(controller.StartType, out error)) + { + failureReason = ServiceStartFailureReason.ServiceDisabled; + tracer.RelatedError($"{nameof(TryStartService)}: {error}"); + return false; + } + controller.Start(); } return true; } - catch (InvalidOperationException e) + catch (Exception e) when (e is InvalidOperationException || e is Win32Exception) { + failureReason = GetStartFailureReason(e); + error = GetStartFailureMessage(failureReason); EventMetadata metadata = CreateEventMetadata(e); - tracer.RelatedError(metadata, $"{nameof(TryStartService)}: InvalidOperationException: {ServiceName} Service was not found"); + metadata.Add(nameof(failureReason), failureReason.ToString()); + tracer.RelatedError(metadata, $"{nameof(TryStartService)}: {error}"); } - catch (Win32Exception e) + + return false; + } + + // Returns true with an actionable message when a service start type is Disabled; otherwise + // returns false and a null message. + internal static bool TryGetDisabledStartTypeError(ServiceStartMode startType, out string error) + { + if (startType == ServiceStartMode.Disabled) { - EventMetadata metadata = CreateEventMetadata(e); - tracer.RelatedError(metadata, $"{nameof(TryStartService)}: Win32Exception while trying to start prjflt"); + error = DisabledServiceErrorMessage; + return true; } + error = null; return false; } + // ServiceController reports a failed native service call as an InvalidOperationException whose + // InnerException is the Win32Exception, so unwrap before reading the native error code. + internal static ServiceStartFailureReason GetStartFailureReason(Exception exception) + { + Win32Exception win32Exception = exception as Win32Exception ?? exception.InnerException as Win32Exception; + + switch (win32Exception?.NativeErrorCode) + { + case ServiceDisabledWin32Error: + return ServiceStartFailureReason.ServiceDisabled; + case ServiceDoesNotExistWin32Error: + return ServiceStartFailureReason.ServiceNotFound; + default: + return ServiceStartFailureReason.StartFailed; + } + } + + internal static string GetStartFailureMessage(ServiceStartFailureReason failureReason) + { + switch (failureReason) + { + case ServiceStartFailureReason.ServiceDisabled: + return DisabledServiceErrorMessage; + case ServiceStartFailureReason.ServiceNotFound: + return $"The {ServiceName} service was not found."; + default: + return $"Failed to start the {ServiceName} service."; + } + } + + private static string DisabledServiceErrorMessage => + $"The {ServiceName} driver service is disabled and cannot be started. " + + "A security policy, anti-virus product, or another program may have disabled it. " + + $"To re-enable it, run this command from an elevated command prompt, then try again: sc.exe config {DriverName} start= auto"; + public static bool IsAutoLoggerEnabled(ITracer tracer) { object startValue; diff --git a/GVFS/GVFS.Service/Handlers/EnableAndAttachProjFSHandler.cs b/GVFS/GVFS.Service/Handlers/EnableAndAttachProjFSHandler.cs index 704f300063..a1c7c04aed 100644 --- a/GVFS/GVFS.Service/Handlers/EnableAndAttachProjFSHandler.cs +++ b/GVFS/GVFS.Service/Handlers/EnableAndAttachProjFSHandler.cs @@ -74,13 +74,16 @@ public static bool TryEnablePrjFlt(ITracer tracer, out string error) if (isPrjfltServiceInstalled) { - if (ProjFSFilter.TryStartService(tracer)) + ProjFSFilter.ServiceStartFailureReason startFailureReason; + string startServiceError; + if (ProjFSFilter.TryStartService(tracer, out startFailureReason, out startServiceError)) { isPrjfltServiceRunning = true; } else { - error = "Failed to start prjflt service"; + error = string.IsNullOrEmpty(startServiceError) ? "Failed to start prjflt service" : startServiceError; + prjFltHealthMetadata.Add(nameof(startFailureReason), startFailureReason.ToString()); tracer.RelatedError($"{nameof(TryEnablePrjFlt)}: {error}"); } } diff --git a/GVFS/GVFS.UnitTests/Windows/Platform/ProjFSFilterTests.cs b/GVFS/GVFS.UnitTests/Windows/Platform/ProjFSFilterTests.cs index 8eaced8137..acf3a4d2b8 100644 --- a/GVFS/GVFS.UnitTests/Windows/Platform/ProjFSFilterTests.cs +++ b/GVFS/GVFS.UnitTests/Windows/Platform/ProjFSFilterTests.cs @@ -6,7 +6,9 @@ using Moq; using NUnit.Framework; using System; +using System.ComponentModel; using System.IO; +using System.ServiceProcess; namespace GVFS.UnitTests.Windows.Platform { @@ -59,5 +61,67 @@ public void IsNativeLibInstalled_ReturnsFalseWhenOnlyAppLocalLibExists() this.mockFileSystem.Setup(fileSystem => fileSystem.FileExists(this.appLocalNativeLibPath)).Returns(true); ProjFSFilter.IsNativeLibInstalled(this.mockTracer, this.mockFileSystem.Object).ShouldBeFalse(); } + + [TestCase] + public void TryGetDisabledStartTypeError_ReturnsActionableMessageForDisabledService() + { + // A disabled driver service must produce the actionable enable-and-retry message, + // not a generic start failure. + bool isDisabled = ProjFSFilter.TryGetDisabledStartTypeError(ServiceStartMode.Disabled, out string error); + isDisabled.ShouldBeTrue(); + error.ShouldContain("sc.exe config prjflt start= auto"); + } + + [TestCase(ServiceStartMode.Automatic)] + [TestCase(ServiceStartMode.Manual)] + [TestCase(ServiceStartMode.Boot)] + [TestCase(ServiceStartMode.System)] + public void TryGetDisabledStartTypeError_ReturnsFalseForEnabledStartTypes(ServiceStartMode startType) + { + bool isDisabled = ProjFSFilter.TryGetDisabledStartTypeError(startType, out string error); + isDisabled.ShouldBeFalse(); + error.ShouldBeNull(); + } + + [TestCase(1058, ProjFSFilter.ServiceStartFailureReason.ServiceDisabled)] + [TestCase(1060, ProjFSFilter.ServiceStartFailureReason.ServiceNotFound)] + [TestCase(5, ProjFSFilter.ServiceStartFailureReason.StartFailed)] // ERROR_ACCESS_DENIED + [TestCase(1056, ProjFSFilter.ServiceStartFailureReason.StartFailed)] // ERROR_SERVICE_ALREADY_RUNNING + public void GetStartFailureReason_UnwrapsWin32ExceptionFromInvalidOperationException(int nativeErrorCode, ProjFSFilter.ServiceStartFailureReason expectedReason) + { + // ServiceController reports a failed native service call this way: an InvalidOperationException + // that wraps the Win32Exception. + InvalidOperationException wrapped = new InvalidOperationException("Cannot start service", new Win32Exception(nativeErrorCode)); + ProjFSFilter.GetStartFailureReason(wrapped).ShouldEqual(expectedReason); + } + + [TestCase] + public void GetStartFailureReason_HandlesBareWin32Exception() + { + ProjFSFilter.GetStartFailureReason(new Win32Exception(1058)).ShouldEqual(ProjFSFilter.ServiceStartFailureReason.ServiceDisabled); + } + + [TestCase] + public void GetStartFailureReason_ReturnsStartFailedWhenNoWin32ExceptionIsWrapped() + { + ProjFSFilter.GetStartFailureReason(new InvalidOperationException("no inner exception")).ShouldEqual(ProjFSFilter.ServiceStartFailureReason.StartFailed); + } + + [TestCase] + public void GetStartFailureMessage_ReturnsActionableMessageForDisabledService() + { + // A disabled driver service must produce the actionable enable-and-retry message, + // not a generic start failure. + ProjFSFilter.GetStartFailureMessage(ProjFSFilter.ServiceStartFailureReason.ServiceDisabled).ShouldContain("sc.exe config prjflt start= auto"); + } + + [TestCase(ProjFSFilter.ServiceStartFailureReason.ServiceNotFound, "was not found")] + [TestCase(ProjFSFilter.ServiceStartFailureReason.StartFailed, "Failed to start")] + public void GetStartFailureMessage_ReturnsDistinctMessageForOtherReasons(ProjFSFilter.ServiceStartFailureReason failureReason, string expectedText) + { + string error = ProjFSFilter.GetStartFailureMessage(failureReason); + error.ShouldContain(expectedText); + error.ShouldNotContain(false, "sc.exe config"); + } } }