From d8539c498ae8d4f0446334625b393f14ed9f9dad Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 4 Oct 2018 13:46:44 +0100 Subject: [PATCH] Enhance automount to wait for volume to be available Enhance the auto-mounting logic to watch for new volumes to be attached, and to wait for BitLocker drives to be unlocked before mounting. Add ability to list the state of all known repositories using the cmd: `gvfs service --list-all`. TODO: tests! --- .../FileSystem/IPlatformFileSystem.cs | 37 +++++ .../FileSystem/IVolumeStateWatcher.cs | 25 +++ .../FileSystem/VolumeStateChangedEventArgs.cs | 36 +++++ .../NamedPipes/NamedPipeMessages.cs | 2 + GVFS/GVFS.Common/NativeMethods.cs | 7 + GVFS/GVFS.Platform.Mac/MacFileSystem.cs | 29 +++- .../MacVolumeStateWatcher.cs | 27 ++++ .../GVFS.Platform.Windows/BitLockerHelpers.cs | 67 ++++++++ .../GVFS.Platform.Windows.csproj | 2 + GVFS/GVFS.Platform.Windows/ProjFSFilter.cs | 9 +- .../WindowsFileSystem.cs | 37 ++++- .../WindowsVolumeStateWatcher.cs | 144 +++++++++++++++++ GVFS/GVFS.Service/GVFS.Service.csproj | 5 +- ...tProcess.cs => GVFSMountProcessManager.cs} | 10 +- GVFS/GVFS.Service/GvfsService.cs | 26 ++- .../Handlers/GetActiveRepoListHandler.cs | 10 +- GVFS/GVFS.Service/RepoAutoMounter.cs | 150 ++++++++++++++++++ GVFS/GVFS.Service/RepoRegistry.cs | 94 ++++------- .../Mock/FileSystem/MockPlatformFileSystem.cs | 20 +++ GVFS/GVFS/CommandLine/ServiceVerb.cs | 40 ++++- 20 files changed, 680 insertions(+), 97 deletions(-) create mode 100644 GVFS/GVFS.Common/FileSystem/IVolumeStateWatcher.cs create mode 100644 GVFS/GVFS.Common/FileSystem/VolumeStateChangedEventArgs.cs create mode 100644 GVFS/GVFS.Platform.Mac/MacVolumeStateWatcher.cs create mode 100644 GVFS/GVFS.Platform.Windows/BitLockerHelpers.cs create mode 100644 GVFS/GVFS.Platform.Windows/WindowsVolumeStateWatcher.cs rename GVFS/GVFS.Service/{GVFSMountProcess.cs => GVFSMountProcessManager.cs} (75%) create mode 100644 GVFS/GVFS.Service/RepoAutoMounter.cs diff --git a/GVFS/GVFS.Common/FileSystem/IPlatformFileSystem.cs b/GVFS/GVFS.Common/FileSystem/IPlatformFileSystem.cs index 93c8cb2e9e..962a688e97 100644 --- a/GVFS/GVFS.Common/FileSystem/IPlatformFileSystem.cs +++ b/GVFS/GVFS.Common/FileSystem/IPlatformFileSystem.cs @@ -8,5 +8,42 @@ public interface IPlatformFileSystem void CreateHardLink(string newLinkFileName, string existingFileName); bool TryGetNormalizedPath(string path, out string normalizedPath, out string errorMessage); void ChangeMode(string path, int mode); + + /// + /// Check if a path exists as a subpath of the specified directory. + /// + /// Directory path. + /// Path to query. + /// True if exists as a subpath of , false otherwise. + bool IsPathUnderDirectory(string directoryPath, string path); + + /// + /// Get the path to the volume root that the given path. + /// + /// File path to find the volume for. + /// Path to the root of the volume. + string GetVolumeRoot(string path); + + /// + /// Check if the volume for the given path is available and ready for use. + /// + /// Path to any directory or file on a volume. + /// + /// A volume might be unavailable for multiple reasons, including: + /// + /// - the volume resides on a removable device which is not present + /// + /// - the volume is not mounted in the operating system + /// + /// - the volume is encrypted or locked + /// + /// True if the volume is available, false otherwise. + bool IsVolumeAvailable(string path); + + /// + /// Create an which monitors for changes to the state of volumes on the system. + /// + /// Volume watcher + IVolumeStateWatcher CreateVolumeStateWatcher(); } } diff --git a/GVFS/GVFS.Common/FileSystem/IVolumeStateWatcher.cs b/GVFS/GVFS.Common/FileSystem/IVolumeStateWatcher.cs new file mode 100644 index 0000000000..fff3a19ade --- /dev/null +++ b/GVFS/GVFS.Common/FileSystem/IVolumeStateWatcher.cs @@ -0,0 +1,25 @@ +using System; + +namespace GVFS.Common.FileSystem +{ + /// + /// Monitors the system for changes to the state of any disk volume, notifying subscribers when a change occurs. + /// + public interface IVolumeStateWatcher : IDisposable + { + /// + /// Raised when the state of a volume has changed, such as becoming available. + /// + event EventHandler VolumeStateChanged; + + /// + /// Start watching for changes to the states of volumes on the system. + /// + void Start(); + + /// + /// Stop watching for changes to the states of volumes on the system. + /// + void Stop(); + } +} diff --git a/GVFS/GVFS.Common/FileSystem/VolumeStateChangedEventArgs.cs b/GVFS/GVFS.Common/FileSystem/VolumeStateChangedEventArgs.cs new file mode 100644 index 0000000000..b05b438fad --- /dev/null +++ b/GVFS/GVFS.Common/FileSystem/VolumeStateChangedEventArgs.cs @@ -0,0 +1,36 @@ +using System; + +namespace GVFS.Common.FileSystem +{ + public enum VolumeStateChangeType + { + /// + /// The volume is now available and ready to use. + /// + VolumeAvailable, + + /// + /// The volume is no longer available. + /// + VolumeUnavailable, + } + + public class VolumeStateChangedEventArgs : EventArgs + { + public VolumeStateChangedEventArgs(string volumePath, VolumeStateChangeType changeType) + { + this.VolumePath = volumePath; + this.ChangeType = changeType; + } + + /// + /// Path to the root of the volume that has changed. + /// + public string VolumePath { get; } + + /// + /// Type of change. + /// + public VolumeStateChangeType ChangeType { get; } + } +} \ No newline at end of file diff --git a/GVFS/GVFS.Common/NamedPipes/NamedPipeMessages.cs b/GVFS/GVFS.Common/NamedPipes/NamedPipeMessages.cs index df6b707e13..86bdf0b846 100644 --- a/GVFS/GVFS.Common/NamedPipes/NamedPipeMessages.cs +++ b/GVFS/GVFS.Common/NamedPipes/NamedPipeMessages.cs @@ -300,6 +300,8 @@ public class Response : BaseResponse { public List RepoList { get; set; } + public List InvalidRepoList { get; set; } + public static Response FromMessage(Message message) { return JsonConvert.DeserializeObject(message.Body); diff --git a/GVFS/GVFS.Common/NativeMethods.cs b/GVFS/GVFS.Common/NativeMethods.cs index 65074fdb05..35b5ae4ab2 100644 --- a/GVFS/GVFS.Common/NativeMethods.cs +++ b/GVFS/GVFS.Common/NativeMethods.cs @@ -164,6 +164,13 @@ public static DateTime GetLastRebootTime() return DateTime.Now - uptime; } + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool GetVolumePathName( + string volumeName, + StringBuilder volumePathName, + uint bufferLength); + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern bool MoveFileEx( string existingFileName, diff --git a/GVFS/GVFS.Platform.Mac/MacFileSystem.cs b/GVFS/GVFS.Platform.Mac/MacFileSystem.cs index 96dec6a59b..3cff233a7c 100644 --- a/GVFS/GVFS.Platform.Mac/MacFileSystem.cs +++ b/GVFS/GVFS.Platform.Mac/MacFileSystem.cs @@ -1,5 +1,6 @@ -using GVFS.Common; +using GVFS.Common; using GVFS.Common.FileSystem; +using System; using System.IO; using System.Runtime.InteropServices; @@ -33,6 +34,32 @@ public void ChangeMode(string path, int mode) Chmod(path, mode); } + public bool IsPathUnderDirectory(string directoryPath, string path) + { + // TODO(Mac): Check if the user has set HFS+/APFS to case sensitive + // TODO(Mac): Normalize paths + // Note: this may be called with paths or volumes which do not exist/are not mounted + return path.StartsWith(directoryPath, StringComparison.OrdinalIgnoreCase); + } + + public string GetVolumeRoot(string path) + { + // TODO(Mac): Query the volume mount points and check if the path is under any of those + // For now just assume everything is under the system root. + return "/"; + } + + public bool IsVolumeAvailable(string path) + { + // TODO(Mac): Perform any additional checks for locked or encrypted volumes + return Directory.Exists(path) || File.Exists(path); + } + + public IVolumeStateWatcher CreateVolumeStateWatcher() + { + return new MacVolumeStateWatcher(); + } + public bool TryGetNormalizedPath(string path, out string normalizedPath, out string errorMessage) { return MacFileSystem.TryGetNormalizedPathImplementation(path, out normalizedPath, out errorMessage); diff --git a/GVFS/GVFS.Platform.Mac/MacVolumeStateWatcher.cs b/GVFS/GVFS.Platform.Mac/MacVolumeStateWatcher.cs new file mode 100644 index 0000000000..79b0012824 --- /dev/null +++ b/GVFS/GVFS.Platform.Mac/MacVolumeStateWatcher.cs @@ -0,0 +1,27 @@ +using System; +using GVFS.Common.FileSystem; + +namespace GVFS.Platform.Mac +{ + public class MacVolumeStateWatcher : IVolumeStateWatcher + { + public event EventHandler VolumeStateChanged; + + public void Start() + { + } + + public void Stop() + { + } + + public void Dispose() + { + } + + protected void RaiseVolumeStateChanged(string volumePath, VolumeStateChangeType changeType) + { + this.VolumeStateChanged?.Invoke(this, new VolumeStateChangedEventArgs(volumePath, changeType)); + } + } +} diff --git a/GVFS/GVFS.Platform.Windows/BitLockerHelpers.cs b/GVFS/GVFS.Platform.Windows/BitLockerHelpers.cs new file mode 100644 index 0000000000..31c33ff265 --- /dev/null +++ b/GVFS/GVFS.Platform.Windows/BitLockerHelpers.cs @@ -0,0 +1,67 @@ +using System; +using System.Linq; +using System.Management; + +namespace GVFS.Platform.Windows +{ + internal static class BitLockerHelpers + { + public const string VolumeEncryptionNamespace = @"root\cimv2\security\MicrosoftVolumeEncryption"; + + public static bool TryGetVolumeLockStatus(string driveName, out bool isLocked) + { + string queryString = $"SELECT * FROM Win32_EncryptableVolume WHERE DriveLetter = '{driveName}'"; + + using (var searcher = new ManagementObjectSearcher(VolumeEncryptionNamespace, queryString)) + { + ManagementObjectCollection results = searcher.Get(); + ManagementObject mo = results.OfType().FirstOrDefault(); + if (mo != null) + { + // Check the protection status + // ProtectionStatus == 0 means the drive is not protected by BitLocker and therefore cannot be locked + if (mo.Properties["ProtectionStatus"].Value is uint protectedInt && protectedInt == 0) + { + isLocked = false; + return true; + } + + // Check the lock status + // Lock status is not a property and must be retrieved by the GetLockStatus method + var args = new object[1]; + if (TryInvokeMethod(mo, "GetLockStatus", args) && args[0] is uint lockedInt) + { + // Unlocked + if (lockedInt == 0) + { + isLocked = false; + return true; + } + + // Locked + if (lockedInt == 1) + { + isLocked = true; + return true; + } + } + } + } + + isLocked = false; + return false; + } + + private static bool TryInvokeMethod(ManagementObject mo, string methodName, object[] args) + { + try + { + return mo.InvokeMethod(methodName, args) is uint result && result == 0; + } + catch (Exception) + { + return false; + } + } + } +} diff --git a/GVFS/GVFS.Platform.Windows/GVFS.Platform.Windows.csproj b/GVFS/GVFS.Platform.Windows/GVFS.Platform.Windows.csproj index c461a584b5..86e9e629f5 100644 --- a/GVFS/GVFS.Platform.Windows/GVFS.Platform.Windows.csproj +++ b/GVFS/GVFS.Platform.Windows/GVFS.Platform.Windows.csproj @@ -72,6 +72,7 @@ CommonAssemblyVersion.cs + @@ -101,6 +102,7 @@ + diff --git a/GVFS/GVFS.Platform.Windows/ProjFSFilter.cs b/GVFS/GVFS.Platform.Windows/ProjFSFilter.cs index d82693c424..9962042f5d 100644 --- a/GVFS/GVFS.Platform.Windows/ProjFSFilter.cs +++ b/GVFS/GVFS.Platform.Windows/ProjFSFilter.cs @@ -53,7 +53,7 @@ public static bool TryAttach(ITracer tracer, string enlistmentRoot, out string e try { StringBuilder volumePathName = new StringBuilder(GVFSConstants.MaxPath); - if (!NativeMethods.GetVolumePathName(enlistmentRoot, volumePathName, GVFSConstants.MaxPath)) + if (!Common.NativeMethods.GetVolumePathName(enlistmentRoot, volumePathName, GVFSConstants.MaxPath)) { errorMessage = "Could not get volume path name"; tracer.RelatedError($"{nameof(TryAttach)}:{errorMessage}"); @@ -626,13 +626,6 @@ public static extern uint FilterAttach( string instanceName, uint createdInstanceNameLength = 0, string createdInstanceName = null); - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool GetVolumePathName( - string volumeName, - StringBuilder volumePathName, - uint bufferLength); } } } diff --git a/GVFS/GVFS.Platform.Windows/WindowsFileSystem.cs b/GVFS/GVFS.Platform.Windows/WindowsFileSystem.cs index 89aa726b40..40790c640f 100644 --- a/GVFS/GVFS.Platform.Windows/WindowsFileSystem.cs +++ b/GVFS/GVFS.Platform.Windows/WindowsFileSystem.cs @@ -1,4 +1,6 @@ -using GVFS.Common; +using System; +using System.Text; +using GVFS.Common; using GVFS.Common.FileSystem; namespace GVFS.Platform.Windows @@ -29,6 +31,39 @@ public void ChangeMode(string path, int mode) { } + public bool IsPathUnderDirectory(string directoryPath, string path) + { + // TODO: Normalize paths + // We can't use the existing TryGetNormalizedPathImplementation method + // because it relies on actual calls to the disk to check if directories exist. + // This may be called with paths or volumes which do not actually exist. + return path.StartsWith(directoryPath, StringComparison.OrdinalIgnoreCase); + } + + public string GetVolumeRoot(string path) + { + var volumePathName = new StringBuilder(GVFSConstants.MaxPath); + if (NativeMethods.GetVolumePathName(path, volumePathName, GVFSConstants.MaxPath)) + { + return volumePathName.ToString(); + } + + return null; + } + + public bool IsVolumeAvailable(string path) + { + // No paths 'exist' on locked BitLocker volumes so it is sufficent to just + // check if the directory/file exists using the framework APIs. + return System.IO.Directory.Exists(path) || System.IO.File.Exists(path); + } + + public IVolumeStateWatcher CreateVolumeStateWatcher() + { + // TODO: Extract the polling interval to a configuration value? + return new WindowsVolumeStateWatcher(TimeSpan.FromSeconds(15)); + } + public bool TryGetNormalizedPath(string path, out string normalizedPath, out string errorMessage) { return WindowsFileSystem.TryGetNormalizedPathImplementation(path, out normalizedPath, out errorMessage); diff --git a/GVFS/GVFS.Platform.Windows/WindowsVolumeStateWatcher.cs b/GVFS/GVFS.Platform.Windows/WindowsVolumeStateWatcher.cs new file mode 100644 index 0000000000..1120ba8dac --- /dev/null +++ b/GVFS/GVFS.Platform.Windows/WindowsVolumeStateWatcher.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Management; +using GVFS.Common.FileSystem; + +namespace GVFS.Platform.Windows +{ + public class WindowsVolumeStateWatcher : IVolumeStateWatcher + { + private readonly ManagementEventWatcher volumeWatcher; + private readonly ManagementEventWatcher cryptoVolumeWatcher; + private readonly IDictionary cryptoVolumeLockStatuses = + new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + public WindowsVolumeStateWatcher(TimeSpan pollingInterval) + { + int intervalSeconds = (int)pollingInterval.TotalSeconds; + + // Watch for mount and unmount of volumes + string volumeQuery = $"SELECT * FROM Win32_VolumeChangeEvent WITHIN {intervalSeconds}"; + this.volumeWatcher = new ManagementEventWatcher(volumeQuery); + this.volumeWatcher.EventArrived += this.OnVolumeEvent; + + // Watch for changes to BitLocker-protected volume states (unlock, lock, etc) + string cryptoVolumeQuery = $"SELECT * FROM __InstanceModificationEvent WITHIN {intervalSeconds} WHERE TargetInstance ISA 'Win32_EncryptableVolume'"; + this.cryptoVolumeWatcher = new ManagementEventWatcher(BitLockerHelpers.VolumeEncryptionNamespace, cryptoVolumeQuery); + this.cryptoVolumeWatcher.EventArrived += this.OnCryptoVolumeEvent; + } + + public event EventHandler VolumeStateChanged; + + public void Start() + { + this.volumeWatcher.Start(); + this.cryptoVolumeWatcher.Start(); + } + + public void Stop() + { + this.cryptoVolumeWatcher.Stop(); + this.volumeWatcher.Stop(); + } + + public void Dispose() + { + this.Stop(); + + this.volumeWatcher.EventArrived -= this.OnVolumeEvent; + this.volumeWatcher.Dispose(); + + this.cryptoVolumeWatcher.EventArrived -= this.OnCryptoVolumeEvent; + this.cryptoVolumeWatcher.Dispose(); + } + + protected void RaiseVolumeStateChanged(string volumePath, VolumeStateChangeType changeType) + { + this.VolumeStateChanged?.Invoke(this, new VolumeStateChangedEventArgs(volumePath, changeType)); + } + + private void OnVolumeEvent(object sender, EventArrivedEventArgs e) + { + ManagementBaseObject mbo = e.NewEvent; + + var driveName = (string)mbo["DriveName"]; + var eventType = (ushort)mbo["EventType"]; + + string volumePath = $@"{driveName}\"; + + if (eventType == 2) + { + // Device Arrival + // Check if the volume is not left locked by BitLocker on mount + if (BitLockerHelpers.TryGetVolumeLockStatus(driveName, out bool isLocked) && !isLocked) + { + this.RaiseVolumeStateChanged(volumePath, VolumeStateChangeType.VolumeAvailable); + } + } + else if (eventType == 3) + { + // Device Removal + this.RaiseVolumeStateChanged(volumePath, VolumeStateChangeType.VolumeUnavailable); + } + } + + private void OnCryptoVolumeEvent(object sender, EventArrivedEventArgs e) + { + ManagementBaseObject mbo = e.NewEvent; + var targetMbo = (ManagementBaseObject)mbo["TargetInstance"]; + + var driveName = (string)targetMbo?["DriveLetter"]; + if (driveName == null) + { + return; + } + + string volumePath = $@"{driveName}\"; + + // Get the new lock status of the volume + // Note: we can only invoke the required "GetLockStatus" method on instances of type + // ManagementObject, but management events only return instances of ManagementBaseObject. + if (!BitLockerHelpers.TryGetVolumeLockStatus(driveName, out bool newLockStatus)) + { + return; + } + + // Get previous lock status + // We only want to raise the 'VolumeStateChanged' event if the lock status has changed, but this event could + // be raised for any modification to the BitLocker volume's configuration such as auto unlock on/off. + // We track the previously seen lock statuses for all volumes so that we can detect changes to the locked status + // and only raise the 'VolumeStateChanged' event in those cases. + if (this.cryptoVolumeLockStatuses.TryGetValue(driveName, out bool prevLockStatus)) + { + if (prevLockStatus && !newLockStatus) + { + // Locked -> Unlocked + this.RaiseVolumeStateChanged(volumePath, VolumeStateChangeType.VolumeAvailable); + } + else if (!prevLockStatus && newLockStatus) + { + // Unlocked -> Locked + this.RaiseVolumeStateChanged(volumePath, VolumeStateChangeType.VolumeUnavailable); + } + } + else + { + // We've never seen this volume before (must have just been mounted).. report the current locked state as an event + if (!newLockStatus) + { + // Unlocked + this.RaiseVolumeStateChanged(volumePath, VolumeStateChangeType.VolumeAvailable); + } + else + { + // Locked + this.RaiseVolumeStateChanged(volumePath, VolumeStateChangeType.VolumeUnavailable); + } + } + + // Update new lock status + this.cryptoVolumeLockStatuses[driveName] = newLockStatus; + } + } +} diff --git a/GVFS/GVFS.Service/GVFS.Service.csproj b/GVFS/GVFS.Service/GVFS.Service.csproj index 19975fabe5..7b9c3b6bab 100644 --- a/GVFS/GVFS.Service/GVFS.Service.csproj +++ b/GVFS/GVFS.Service/GVFS.Service.csproj @@ -1,4 +1,4 @@ - + @@ -74,9 +74,10 @@ + - + diff --git a/GVFS/GVFS.Service/GVFSMountProcess.cs b/GVFS/GVFS.Service/GVFSMountProcessManager.cs similarity index 75% rename from GVFS/GVFS.Service/GVFSMountProcess.cs rename to GVFS/GVFS.Service/GVFSMountProcessManager.cs index bc636b5c45..01e6e2daf7 100644 --- a/GVFS/GVFS.Service/GVFSMountProcess.cs +++ b/GVFS/GVFS.Service/GVFSMountProcessManager.cs @@ -6,13 +6,13 @@ namespace GVFS.Service { - public class GVFSMountProcess : IDisposable + public class GVFSMountProcessManager : IDisposable { private const string ParamPrefix = "--"; private readonly ITracer tracer; - public GVFSMountProcess(ITracer tracer, int sessionId) + public GVFSMountProcessManager(ITracer tracer, int sessionId) { this.tracer = tracer; this.CurrentUser = new CurrentUser(this.tracer, sessionId); @@ -20,20 +20,20 @@ public GVFSMountProcess(ITracer tracer, int sessionId) public CurrentUser CurrentUser { get; private set; } - public bool Mount(string repoRoot) + public bool StartMount(string repoRoot) { if (!ProjFSFilter.IsServiceRunning(this.tracer)) { string error; if (!EnableAndAttachProjFSHandler.TryEnablePrjFlt(this.tracer, out error)) { - this.tracer.RelatedError($"{nameof(this.Mount)}: Unable to start the GVFS.exe process: {error}"); + this.tracer.RelatedError($"{nameof(this.StartMount)}: Unable to start the GVFS.exe process: {error}"); } } if (!this.CallGVFSMount(repoRoot)) { - this.tracer.RelatedError($"{nameof(this.Mount)}: Unable to start the GVFS.exe process."); + this.tracer.RelatedError($"{nameof(this.StartMount)}: Unable to start the GVFS.exe process."); return false; } diff --git a/GVFS/GVFS.Service/GvfsService.cs b/GVFS/GVFS.Service/GvfsService.cs index 9f47a6f7e9..2e19e5f842 100644 --- a/GVFS/GVFS.Service/GvfsService.cs +++ b/GVFS/GVFS.Service/GvfsService.cs @@ -4,6 +4,8 @@ using GVFS.Common.Tracing; using GVFS.Service.Handlers; using System; +using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -26,6 +28,7 @@ public class GVFSService : ServiceBase private string serviceName; private string serviceDataLocation; private RepoRegistry repoRegistry; + private IDictionary repoMounters; private ProductUpgradeTimer productUpgradeTimer; public GVFSService(JsonTracer tracer) @@ -42,6 +45,7 @@ public void Run() { this.repoRegistry = new RepoRegistry(this.tracer, new PhysicalFileSystem(), this.serviceDataLocation); this.repoRegistry.Upgrade(); + this.repoMounters = new Dictionary(); this.productUpgradeTimer.Start(); string pipeName = this.serviceName + ".Pipe"; this.tracer.RelatedInfo("Starting pipe server with name: " + pipeName); @@ -115,18 +119,30 @@ protected override void OnSessionChange(SessionChangeDescription changeDescripti if (!GVFSEnlistment.IsUnattended(tracer: null)) { + int sessionId = changeDescription.SessionId; + if (changeDescription.Reason == SessionChangeReason.SessionLogon) { - this.tracer.RelatedInfo("SessionLogon detected, sessionId: {0}", changeDescription.SessionId); + this.tracer.RelatedInfo("SessionLogon detected, sessionId: {0}", sessionId); using (ITracer activity = this.tracer.StartActivity("LogonAutomount", EventLevel.Informational)) { - this.repoRegistry.AutoMountRepos(changeDescription.SessionId); - this.repoRegistry.TraceStatus(); + var mounter = new RepoAutoMounter(this.tracer, this.repoRegistry, sessionId); + this.repoMounters.Add(sessionId, mounter); + + mounter.Start(); + + // TODO: this.repoRegistry.TraceStatus(); } } else if (changeDescription.Reason == SessionChangeReason.SessionLogoff) { - this.tracer.RelatedInfo("SessionLogoff detected"); + this.tracer.RelatedInfo("SessionLogoff detected, sesssionId: {0}", sessionId); + RepoAutoMounter mounter; + if (this.repoMounters.TryGetValue(sessionId, out mounter)) + { + mounter.Stop(); + mounter.Dispose(); + } } } } @@ -205,7 +221,7 @@ private void Start() this.serviceThread = new Thread(this.Run); this.serviceThread.Start(); - } + } private void HandleRequest(ITracer tracer, string request, NamedPipeServer.Connection connection) { diff --git a/GVFS/GVFS.Service/Handlers/GetActiveRepoListHandler.cs b/GVFS/GVFS.Service/Handlers/GetActiveRepoListHandler.cs index dbdf6025a0..56f1c3669e 100644 --- a/GVFS/GVFS.Service/Handlers/GetActiveRepoListHandler.cs +++ b/GVFS/GVFS.Service/Handlers/GetActiveRepoListHandler.cs @@ -32,6 +32,7 @@ public void Run() NamedPipeMessages.GetActiveRepoListRequest.Response response = new NamedPipeMessages.GetActiveRepoListRequest.Response(); response.State = NamedPipeMessages.CompletionState.Success; response.RepoList = new List(); + response.InvalidRepoList = new List(); List repos; if (this.registry.TryGetActiveRepos(out repos, out errorMessage)) @@ -42,14 +43,7 @@ public void Run() { if (!this.IsValidRepo(repoRoot)) { - if (!this.registry.TryRemoveRepo(repoRoot, out errorMessage)) - { - this.tracer.RelatedInfo("Removing an invalid repo failed with error: " + response.ErrorMessage); - } - else - { - this.tracer.RelatedInfo("Removed invalid repo entry from registry: " + repoRoot); - } + response.InvalidRepoList.Add(repoRoot); } else { diff --git a/GVFS/GVFS.Service/RepoAutoMounter.cs b/GVFS/GVFS.Service/RepoAutoMounter.cs new file mode 100644 index 0000000000..da3509ea6d --- /dev/null +++ b/GVFS/GVFS.Service/RepoAutoMounter.cs @@ -0,0 +1,150 @@ +using System; +using GVFS.Common; +using GVFS.Common.FileSystem; +using GVFS.Common.NamedPipes; +using GVFS.Common.Tracing; +using GVFS.Service.Handlers; + +namespace GVFS.Service +{ + public class RepoAutoMounter : IDisposable + { + private readonly ITracer tracer; + private readonly RepoRegistry repoRegistry; + private readonly int sessionId; + private readonly GVFSMountProcessManager mountProcessManager; + private readonly string userSid; + + private IVolumeStateWatcher volumeWatcher; + + public RepoAutoMounter(ITracer tracer, RepoRegistry repoRegistry, int sessionId) + { + this.tracer = tracer ?? throw new ArgumentNullException(nameof(tracer)); + this.repoRegistry = repoRegistry ?? throw new ArgumentNullException(nameof(repoRegistry)); + this.sessionId = sessionId; + + // Create a mount process factory for this session/user + this.mountProcessManager = new GVFSMountProcessManager(this.tracer, sessionId); + this.userSid = this.mountProcessManager.CurrentUser.Identity.User?.Value; + } + + public void Start() + { + this.tracer.RelatedInfo("Starting auto mounter for session {0}", this.sessionId); + + // Try mounting all the user's active repo straight away + this.tracer.RelatedInfo("Attempting to mount all known repos for user {0}", this.userSid); + this.MountAll(); + + // Start watching for changes to volume availability + this.volumeWatcher = GVFSPlatform.Instance.FileSystem.CreateVolumeStateWatcher(); + this.volumeWatcher.VolumeStateChanged += this.OnVolumeStateChanged; + this.volumeWatcher.Start(); + } + + public void Stop() + { + this.tracer.RelatedInfo("Stopping auto mounter for session {0}", this.sessionId); + + // Stop watching for changes to volume availability + if (this.volumeWatcher != null) + { + this.volumeWatcher.Stop(); + this.volumeWatcher.VolumeStateChanged -= this.OnVolumeStateChanged; + this.volumeWatcher.Dispose(); + this.volumeWatcher = null; + } + } + + public void Dispose() + { + this.Stop(); + this.mountProcessManager?.Dispose(); + } + + private void MountAll(string rootPath = null) + { + if (this.repoRegistry.TryGetActiveReposForUser(this.userSid, out var activeRepos, out string errorMessage)) + { + foreach (RepoRegistration repo in activeRepos) + { + if (rootPath == null || GVFSPlatform.Instance.FileSystem.IsPathUnderDirectory(rootPath, repo.EnlistmentRoot)) + { + this.Mount(repo.EnlistmentRoot); + } + } + } + else + { + this.tracer.RelatedError("Could not get repos to auto mount for user. Error: " + errorMessage); + } + } + + private void Mount(string enlistmentRoot) + { + var metadata = new EventMetadata + { + ["EnlistmentRoot"] = enlistmentRoot + }; + + using (var activity = this.tracer.StartActivity("AutoMount", EventLevel.Informational, metadata)) + { + string volumeRoot = GVFSPlatform.Instance.FileSystem.GetVolumeRoot(enlistmentRoot); + if (GVFSPlatform.Instance.FileSystem.IsVolumeAvailable(volumeRoot)) + { + // TODO #1043088: We need to respect the elevation level of the original mount + if (this.mountProcessManager.StartMount(enlistmentRoot)) + { + this.SendNotification("GVFS AutoMount", "The following GVFS repo is now mounted:\n{0}", enlistmentRoot); + activity.RelatedInfo("Auto mount was successful for '{0}'", enlistmentRoot); + } + else + { + this.SendNotification("GVFS AutoMount", "The following GVFS repo failed to mount:\n{0}", enlistmentRoot); + activity.RelatedError("Failed to auto mount '{0}'", enlistmentRoot); + } + } + else + { + activity.RelatedInfo("Cannot auto mount '{0}' because the volume '{1}' not available.", enlistmentRoot, volumeRoot); + } + } + } + + private void OnVolumeStateChanged(object sender, VolumeStateChangedEventArgs e) + { + var metadata = new EventMetadata + { + ["State"] = e.ChangeType.ToString(), + ["Volume"] = e.VolumePath, + }; + + this.tracer.RelatedEvent(EventLevel.Informational, "VolumeStateChange", metadata); + + switch (e.ChangeType) + { + case VolumeStateChangeType.VolumeAvailable: + this.MountAll(rootPath: e.VolumePath); + break; + case VolumeStateChangeType.VolumeUnavailable: + // There is no need to do anything here to stop any potentially orphaned mount processes + // since they will self-terminate if the volume is removed. + break; + default: + this.tracer.RelatedWarning("Unknown volume state change type: {0}", e.ChangeType); + break; + } + } + + private void SendNotification(string title, string format, params object[] args) + { + var request = new NamedPipeMessages.Notification.Request + { + Title = title, + Message = string.Format(format, args) + }; + + NotificationHandler.Instance.SendNotification(this.tracer, this.sessionId, request); + } + } +} diff --git a/GVFS/GVFS.Service/RepoRegistry.cs b/GVFS/GVFS.Service/RepoRegistry.cs index 646691440a..abf011e0a8 100644 --- a/GVFS/GVFS.Service/RepoRegistry.cs +++ b/GVFS/GVFS.Service/RepoRegistry.cs @@ -162,36 +162,6 @@ public bool TryRemoveRepo(string repoRoot, out string errorMessage) return false; } - public void AutoMountRepos(int sessionId) - { - using (ITracer activity = this.tracer.StartActivity("AutoMount", EventLevel.Informational)) - { - using (GVFSMountProcess process = new GVFSMountProcess(activity, sessionId)) - { - List activeRepos = this.GetActiveReposForUser(process.CurrentUser.Identity.User.Value); - if (activeRepos.Count == 0) - { - return; - } - - this.SendNotification(sessionId, "GVFS AutoMount", "Attempting to mount {0} GVFS repo(s)", activeRepos.Count); - - foreach (RepoRegistration repo in activeRepos) - { - // TODO #1043088: We need to respect the elevation level of the original mount - if (process.Mount(repo.EnlistmentRoot)) - { - this.SendNotification(sessionId, "GVFS AutoMount", "The following GVFS repo is now mounted: \n{0}", repo.EnlistmentRoot); - } - else - { - this.SendNotification(sessionId, "GVFS AutoMount", "The following GVFS repo failed to mount: \n{0}", repo.EnlistmentRoot); - } - } - } - } - } - public Dictionary ReadRegistry() { Dictionary allRepos = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -232,31 +202,38 @@ public Dictionary ReadRegistry() RepoRegistration registration = RepoRegistration.FromJson(entry); string errorMessage; - string normalizedEnlistmentRootPath = registration.EnlistmentRoot; - if (GVFSPlatform.Instance.FileSystem.TryGetNormalizedPath(registration.EnlistmentRoot, out normalizedEnlistmentRootPath, out errorMessage)) + string enlistmentPath = registration.EnlistmentRoot; + + // Try and normalize the enlistment path if the volume is available, otherwise just take the + // path verbatim. + string volumePath = GVFSPlatform.Instance.FileSystem.GetVolumeRoot(registration.EnlistmentRoot); + if (GVFSPlatform.Instance.FileSystem.IsVolumeAvailable(volumePath)) { - if (!normalizedEnlistmentRootPath.Equals(registration.EnlistmentRoot, StringComparison.OrdinalIgnoreCase)) + string normalizedPath; + if (GVFSPlatform.Instance.FileSystem.TryGetNormalizedPath(registration.EnlistmentRoot, out normalizedPath, out errorMessage)) + { + if (!normalizedPath.Equals(registration.EnlistmentRoot, StringComparison.OrdinalIgnoreCase)) + { + enlistmentPath = normalizedPath; + + EventMetadata metadata = new EventMetadata(); + metadata.Add("registration.EnlistmentRoot", registration.EnlistmentRoot); + metadata.Add(nameof(normalizedPath), normalizedPath); + metadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(ReadRegistry)}: Mapping registered enlistment root to final path"); + this.tracer.RelatedEvent(EventLevel.Informational, $"{nameof(ReadRegistry)}_NormalizedPathMapping", metadata); + } + } + else { EventMetadata metadata = new EventMetadata(); metadata.Add("registration.EnlistmentRoot", registration.EnlistmentRoot); - metadata.Add(nameof(normalizedEnlistmentRootPath), normalizedEnlistmentRootPath); - metadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(ReadRegistry)}: Mapping registered enlistment root to final path"); - this.tracer.RelatedEvent(EventLevel.Informational, $"{nameof(ReadRegistry)}_NormalizedPathMapping", metadata); + metadata.Add("NormalizedEnlistmentRootPath", normalizedPath); + metadata.Add("ErrorMessage", errorMessage); + this.tracer.RelatedWarning(metadata, $"{nameof(ReadRegistry)}: Failed to get normalized path name for registed enlistment root"); } - } - else - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("registration.EnlistmentRoot", registration.EnlistmentRoot); - metadata.Add("NormalizedEnlistmentRootPath", normalizedEnlistmentRootPath); - metadata.Add("ErrorMessage", errorMessage); - this.tracer.RelatedWarning(metadata, $"{nameof(ReadRegistry)}: Failed to get normalized path name for registed enlistment root"); } - if (normalizedEnlistmentRootPath != null) - { - allRepos[normalizedEnlistmentRootPath] = registration; - } + allRepos[enlistmentPath] = registration; } catch (Exception e) { @@ -298,36 +275,31 @@ public bool TryGetActiveRepos(out List repoList, out string er } } - private List GetActiveReposForUser(string ownerSID) + public bool TryGetActiveReposForUser(string ownerSID, out List repoList, out string errorMessage) { + repoList = null; + errorMessage = null; + lock (this.repoLock) { try { Dictionary repos = this.ReadRegistry(); - return repos + repoList = repos .Values .Where(repo => repo.IsActive) .Where(repo => string.Equals(repo.OwnerSID, ownerSID, StringComparison.InvariantCultureIgnoreCase)) .ToList(); + return true; } catch (Exception e) { - this.tracer.RelatedError("Unable to get list of active repos for user {0}: {1}", ownerSID, e.ToString()); - return new List(); + errorMessage = string.Format("Unable to get list of active repos for user {0}: {1}", ownerSID, e.ToString()); + return false; } } } - private void SendNotification(int sessionId, string title, string format, params object[] args) - { - NamedPipeMessages.Notification.Request request = new NamedPipeMessages.Notification.Request(); - request.Title = title; - request.Message = string.Format(format, args); - - NotificationHandler.Instance.SendNotification(this.tracer, sessionId, request); - } - private void WriteRegistry(Dictionary registry) { string tempFilePath = Path.Combine(this.registryParentFolderPath, RegistryTempName); diff --git a/GVFS/GVFS.UnitTests/Mock/FileSystem/MockPlatformFileSystem.cs b/GVFS/GVFS.UnitTests/Mock/FileSystem/MockPlatformFileSystem.cs index 999f7efd90..a048884d8c 100644 --- a/GVFS/GVFS.UnitTests/Mock/FileSystem/MockPlatformFileSystem.cs +++ b/GVFS/GVFS.UnitTests/Mock/FileSystem/MockPlatformFileSystem.cs @@ -27,6 +27,26 @@ public void ChangeMode(string path, int mode) throw new NotSupportedException(); } + public bool IsPathUnderDirectory(string directoryPath, string path) + { + throw new NotImplementedException(); + } + + public string GetVolumeRoot(string path) + { + throw new NotImplementedException(); + } + + public bool IsVolumeAvailable(string path) + { + throw new NotImplementedException(); + } + + public IVolumeStateWatcher CreateVolumeStateWatcher() + { + throw new NotImplementedException(); + } + public bool TryGetNormalizedPath(string path, out string normalizedPath, out string errorMessage) { errorMessage = null; diff --git a/GVFS/GVFS/CommandLine/ServiceVerb.cs b/GVFS/GVFS/CommandLine/ServiceVerb.cs index e701a74c83..b5e608da17 100644 --- a/GVFS/GVFS/CommandLine/ServiceVerb.cs +++ b/GVFS/GVFS/CommandLine/ServiceVerb.cs @@ -1,6 +1,5 @@ using CommandLine; using GVFS.Common; -using GVFS.Common.FileSystem; using GVFS.Common.NamedPipes; using System; using System.Collections.Generic; @@ -33,7 +32,14 @@ public class ServiceVerb : GVFSVerb.ForNoEnlistment Default = false, Required = false, HelpText = "Prints a list of all mounted repos")] - public bool List { get; set; } + public bool ListMounted { get; set; } + + [Option( + "list-all", + Default = false, + Required = false, + HelpText = "Prints a list of all known repos and their statuses")] + public bool ListAll { get; set; } protected override string VerbName { @@ -42,7 +48,7 @@ protected override string VerbName public override void Execute() { - int optionCount = new[] { this.MountAll, this.UnmountAll, this.List }.Count(flag => flag); + int optionCount = new[] { this.MountAll, this.UnmountAll, this.ListMounted, this.ListAll }.Count(flag => flag); if (optionCount == 0) { this.ReportErrorAndExit($"Error: You must specify an argument. Run 'gvfs {ServiceVerbName} --help' for details."); @@ -54,12 +60,13 @@ public override void Execute() string errorMessage; List repoList; - if (!this.TryGetRepoList(out repoList, out errorMessage)) + List invalidRepoList; + if (!this.TryGetRepoList(out repoList, out invalidRepoList, out errorMessage)) { this.ReportErrorAndExit("Error getting repo list: " + errorMessage); } - if (this.List) + if (this.ListMounted) { foreach (string repoRoot in repoList) { @@ -69,6 +76,25 @@ public override void Execute() } } } + else if (this.ListAll) + { + foreach (string repoRoot in repoList) + { + if (this.IsRepoMounted(repoRoot)) + { + this.Output.WriteLine("{0} (mounted)", repoRoot); + } + else + { + this.Output.WriteLine("{0} (not mounted)", repoRoot); + } + } + + foreach (string repoRoot in invalidRepoList) + { + this.Output.WriteLine("{0} (not available)", repoRoot); + } + } else if (this.MountAll) { // Always ask the service to ensure that PrjFlt is enabled. This will ensure that the GVFS installer properly waits for @@ -135,9 +161,10 @@ public override void Execute() } } - private bool TryGetRepoList(out List repoList, out string errorMessage) + private bool TryGetRepoList(out List repoList, out List invalidRepoList, out string errorMessage) { repoList = null; + invalidRepoList = null; errorMessage = string.Empty; NamedPipeMessages.GetActiveRepoListRequest request = new NamedPipeMessages.GetActiveRepoListRequest(); @@ -171,6 +198,7 @@ private bool TryGetRepoList(out List repoList, out string errorMessage) else { repoList = message.RepoList; + invalidRepoList = message.InvalidRepoList; return true; } }