From c2488025d5d2c0351c45a02159eae481e3ed327e Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Thu, 24 Sep 2026 15:48:08 -0700 Subject: [PATCH] Report an actionable error when the PrjFlt driver service is disabled VFS for Git requires the PrjFlt ProjFS minifilter driver on every volume. Its load behavior is governed by its service Start type. When a security baseline, an anti-virus product, Group Policy, or manual troubleshooting disables the service, the driver cannot start. Today ProjFSFilter.TryStartService calls ServiceController.Start() on the disabled service. That fails with ERROR_SERVICE_DISABLED (1058), which is caught by a generic Win32Exception handler and surfaces to the user as the dead-end message "Failed to start prjflt service" with no indication that the driver is disabled and no way to fix it. Nothing else in the product detects this state (the existing AutoLogger Start handling is an unrelated ETW logging key). Detect the disabled state and report it, rather than silently re-enabling a driver that an administrator or security tool may have disabled on purpose. Changes: - TryStartService checks the service start type only when the service is not already running, so an already-running service succeeds regardless of its start type. When the service is not running and its start type is Disabled, it returns a specific error naming the driver and the exact elevated command to re-enable it. - ServiceController reports a failed native service call as an InvalidOperationException that wraps the Win32Exception. TryStartService unwraps it and maps the native error code to a ServiceStartFailureReason: ERROR_SERVICE_DISABLED is ServiceDisabled, ERROR_SERVICE_DOES_NOT_EXIST is ServiceNotFound, and any other code is StartFailed. Only a missing service is reported as "not found". - TryStartService reports its result through out parameters, so the caller surfaces the specific message instead of the generic "Failed to start prjflt service". - EnableAndAttachProjFSHandler.TryEnablePrjFlt records the ServiceStartFailureReason in the TryEnablePrjFlt_Summary health telemetry. The reason is a stable code, so rewording the user-facing message does not change the metric. - Added unit tests that cover the disabled and enabled start types, and the error-code mapping through the wrapped InvalidOperationException shape. Assisted-by: Claude Opus 4.8 Signed-off-by: Tyrie Vella --- GVFS/GVFS.Platform.Windows/ProjFSFilter.cs | 86 +++++++++++++++++-- .../Handlers/EnableAndAttachProjFSHandler.cs | 7 +- .../Windows/Platform/ProjFSFilterTests.cs | 64 ++++++++++++++ 3 files changed, 149 insertions(+), 8 deletions(-) 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"); + } } }