Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 80 additions & 6 deletions GVFS/GVFS.Platform.Windows/ProjFSFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;

Expand Down Expand Up @@ -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;
Expand Down
7 changes: 5 additions & 2 deletions GVFS/GVFS.Service/Handlers/EnableAndAttachProjFSHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
}
}
Expand Down
64 changes: 64 additions & 0 deletions GVFS/GVFS.UnitTests/Windows/Platform/ProjFSFilterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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");
}
}
}
Loading