From 9c6461f695ccbc0f54c14f9f205d8db357f7f12c Mon Sep 17 00:00:00 2001 From: Marco Gavelli Date: Fri, 1 Apr 2022 21:52:04 +0200 Subject: [PATCH 1/8] Listen for directory changes of network shares in FTP --- .../MessageHandlers/Win32MessageHandler.cs | 94 ++++++++++++++++++- src/Files.Uwp/ViewModels/ItemViewModel.cs | 57 ++++++++++- 2 files changed, 145 insertions(+), 6 deletions(-) diff --git a/src/Files.Launcher/MessageHandlers/Win32MessageHandler.cs b/src/Files.Launcher/MessageHandlers/Win32MessageHandler.cs index 5932326a2f94..06c08acd6f06 100644 --- a/src/Files.Launcher/MessageHandlers/Win32MessageHandler.cs +++ b/src/Files.Launcher/MessageHandlers/Win32MessageHandler.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.IO; using System.IO.Pipes; +using System.Linq; using System.Runtime.Versioning; using System.Threading.Tasks; using Vanara.Windows.Shell; @@ -20,11 +21,18 @@ namespace FilesFullTrust.MessageHandlers [SupportedOSPlatform("Windows10.0.10240")] public class Win32MessageHandler : Disposable, IMessageHandler { + private IList dirWatchers; + private PipeStream connection; + public void Initialize(PipeStream connection) { + this.connection = connection; + DetectIsSetAsDefaultFileManager(); DetectIsSetAsOpenFileDialog(); ApplicationData.Current.LocalSettings.Values["TEMP"] = Environment.GetEnvironmentVariable("TEMP"); + + dirWatchers = new List(); } private static void DetectIsSetAsDefaultFileManager() @@ -159,7 +167,7 @@ public async Task ParseArgumentsAsync(PipeStream connection, Dictionary message, string action) + { + switch (action) + { + case "start": + { + var res = new ValueSet(); + var folderPath = (string)message["folderPath"]; + if (Directory.Exists(folderPath)) + { + var watcher = new FileSystemWatcher + { + Path = folderPath, + Filter = "*.*", + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName + }; + watcher.Created += DirectoryWatcher_Changed; + watcher.Deleted += DirectoryWatcher_Changed; + watcher.Renamed += DirectoryWatcher_Changed; + watcher.EnableRaisingEvents = true; + res.Add("watcherID", watcher.GetHashCode()); + dirWatchers.Add(watcher); + } + await Win32API.SendMessageAsync(connection, res, message.Get("RequestID", (string)null)); + } + break; + + case "cancel": + { + var watcherID = (long)message["watcherID"]; + var watcher = dirWatchers.SingleOrDefault(x => x.GetHashCode() == watcherID); + if (watcher != null) + { + dirWatchers.Remove(watcher); + watcher.Dispose(); + } + } + break; + } + } + + private async void DirectoryWatcher_Changed(object sender, FileSystemEventArgs e) + { + System.Diagnostics.Debug.WriteLine($"Directory watcher event: {e.ChangeType}, {e.FullPath}"); + if (connection?.IsConnected ?? false) + { + var response = new ValueSet() + { + { "FileSystem", Path.GetDirectoryName(e.FullPath) }, + { "Name", e.Name }, + { "Path", e.FullPath }, + { "Type", e.ChangeType.ToString() } + }; + if (e.ChangeType == WatcherChangeTypes.Created) + { + using var folderItem = new ShellItem(e.FullPath); + var shellFileItem = ShellFolderExtensions.GetShellFileItem(folderItem); + response["Item"] = JsonConvert.SerializeObject(shellFileItem); + } + else if (e.ChangeType == WatcherChangeTypes.Renamed) + { + response["OldPath"] = (e as RenamedEventArgs).OldFullPath; + } + // Send message to UWP app to refresh items + await Win32API.SendMessageAsync(connection, response); + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + foreach (var watcher in dirWatchers) + { + watcher.Dispose(); + } } } } diff --git a/src/Files.Uwp/ViewModels/ItemViewModel.cs b/src/Files.Uwp/ViewModels/ItemViewModel.cs index 46d93803d458..6eb336e9e3af 100644 --- a/src/Files.Uwp/ViewModels/ItemViewModel.cs +++ b/src/Files.Uwp/ViewModels/ItemViewModel.cs @@ -431,6 +431,20 @@ private async void Connection_RequestReceived(object sender, Dictionary x.ItemPath.Equals((string)message["OldPath"], StringComparison.OrdinalIgnoreCase)); + if (matchingItem != null) + { + await CoreApplication.MainView.DispatcherQueue.EnqueueAsync(() => + { + matchingItem.ItemPath = itemPath; + matchingItem.ItemNameRaw = (string)message["Name"]; + }); + await OrderFilesAndFoldersAsync(); + await ApplySingleFileChangeAsync(matchingItem); + } + break; + case "Deleted": // get the item that immediately follows matching item to be removed // if the matching item is the last item, try to get the previous item; otherwise, null @@ -1287,7 +1301,6 @@ private async Task RapidAddItemsToCollection(string path, LibraryItem library = currentStorageFolder ??= await FilesystemTasks.Wrap(() => StorageFileExtensions.DangerousGetFolderWithPathFromPathAsync(path)); var syncStatus = await CheckCloudDriveSyncStatusAsync(currentStorageFolder?.Item); PageTypeUpdated?.Invoke(this, new PageTypeUpdatedEventArgs() { IsTypeCloudDrive = syncStatus != CloudDriveSyncStatus.NotSynced && syncStatus != CloudDriveSyncStatus.Unknown }); - WatchForDirectoryChanges(path, syncStatus); break; @@ -1297,7 +1310,11 @@ private async Task RapidAddItemsToCollection(string path, LibraryItem library = WatchForStorageFolderChanges(currentStorageFolder?.Folder); break; - case 2: // Do no watch for changes in Box Drive folder to avoid constant refresh (#7428) + case 2: // Watch for changes using FTP in Box Drive folder (#7428) and network drives (#5869) + PageTypeUpdated?.Invoke(this, new PageTypeUpdatedEventArgs() { IsTypeCloudDrive = false }); + WatchForWin32FolderChanges(path); + break; + case -1: // Enumeration failed default: break; @@ -1490,8 +1507,9 @@ public async Task EnumerateItemsFromStandardFolderAsync(string path, Type s { // Flag to use FindFirstFileExFromApp or StorageFolder enumeration var isBoxFolder = App.CloudDrivesManager.Drives.FirstOrDefault(x => x.Text == "Box")?.Path?.TrimEnd('\\') is string boxFolder ? - path.StartsWith(boxFolder) : false; - bool enumFromStorageFolder = isBoxFolder; // Use storage folder for Box Drive (#4629) + path.StartsWith(boxFolder) : false; // Use storage folder for Box Drive (#4629) + var isNetworkFolder = System.Text.RegularExpressions.Regex.IsMatch(path, @"^\\(?!\?)"); // Use storage folder for network drives (*FromApp methods return access denied) + bool enumFromStorageFolder = isBoxFolder || isNetworkFolder; BaseStorageFolder rootFolder = null; @@ -1582,7 +1600,7 @@ await DialogDisplayHelper.ShowDialogAsync( } CurrentFolder = currentFolder; await EnumFromStorageFolderAsync(path, currentFolder, rootFolder, currentStorageFolder, sourcePageType, cancellationToken); - return isBoxFolder ? 2 : 1; // Workaround for #7428 + return isBoxFolder || isNetworkFolder ? 2 : 1; // Workaround for #7428 } else { @@ -1758,6 +1776,35 @@ await Task.Run(() => }); } + private async void WatchForWin32FolderChanges(string folderPath) + { + if (Connection != null) + { + var (status, response) = await Connection.SendMessageForResponseAsync(new ValueSet() + { + { "Arguments", "WatchDirectory" }, + { "action", "start" }, + { "folderPath", folderPath } + }); + if (status == AppServiceResponseStatus.Success + && response.ContainsKey("watcherID")) + { + watcherCTS.Token.Register(async () => + { + if (Connection != null) + { + await Connection.SendMessageAsync(new ValueSet() + { + { "Arguments", "WatchDirectory" }, + { "action", "cancel" }, + { "watcherID", (long)response["watcherID"] } + }); + } + }); + } + } + } + private async void ItemQueryResult_ContentsChanged(IStorageQueryResultBase sender, object args) { //query options have to be reapplied otherwise old results are returned From c7dd336bea6d78549c0c472b6965a6daacd33729 Mon Sep 17 00:00:00 2001 From: Marco Gavelli Date: Fri, 1 Apr 2022 22:04:17 +0200 Subject: [PATCH 2/8] Handle exceptions thrown when getting created item --- src/Files.Launcher/MessageHandlers/RecycleBinHandler.cs | 3 ++- src/Files.Launcher/MessageHandlers/Win32MessageHandler.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Files.Launcher/MessageHandlers/RecycleBinHandler.cs b/src/Files.Launcher/MessageHandlers/RecycleBinHandler.cs index c8c45b24856f..126116be11be 100644 --- a/src/Files.Launcher/MessageHandlers/RecycleBinHandler.cs +++ b/src/Files.Launcher/MessageHandlers/RecycleBinHandler.cs @@ -126,7 +126,8 @@ private async void RecycleBinWatcher_Changed(object sender, FileSystemEventArgs }; if (e.ChangeType == WatcherChangeTypes.Created) { - using var folderItem = new ShellItem(e.FullPath); + using var folderItem = SafetyExtensions.IgnoreExceptions(() => new ShellItem(e.FullPath)); + if (folderItem == null) return; var shellFileItem = ShellFolderExtensions.GetShellFileItem(folderItem); response["Item"] = JsonConvert.SerializeObject(shellFileItem); } diff --git a/src/Files.Launcher/MessageHandlers/Win32MessageHandler.cs b/src/Files.Launcher/MessageHandlers/Win32MessageHandler.cs index 06c08acd6f06..3b0455ca4d0e 100644 --- a/src/Files.Launcher/MessageHandlers/Win32MessageHandler.cs +++ b/src/Files.Launcher/MessageHandlers/Win32MessageHandler.cs @@ -305,7 +305,8 @@ private async void DirectoryWatcher_Changed(object sender, FileSystemEventArgs e }; if (e.ChangeType == WatcherChangeTypes.Created) { - using var folderItem = new ShellItem(e.FullPath); + using var folderItem = SafetyExtensions.IgnoreExceptions(() => new ShellItem(e.FullPath)); + if (folderItem == null) return; var shellFileItem = ShellFolderExtensions.GetShellFileItem(folderItem); response["Item"] = JsonConvert.SerializeObject(shellFileItem); } From 8d1969a8a2978f29a8044eb52aa98e3a9157e71a Mon Sep 17 00:00:00 2001 From: Marco Gavelli Date: Fri, 1 Apr 2022 22:22:32 +0200 Subject: [PATCH 3/8] Fix duplicate items when the same folder is open on multiple tabs --- src/Files.Launcher/MessageHandlers/Win32MessageHandler.cs | 3 ++- src/Files.Uwp/ViewModels/ItemViewModel.cs | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Files.Launcher/MessageHandlers/Win32MessageHandler.cs b/src/Files.Launcher/MessageHandlers/Win32MessageHandler.cs index 3b0455ca4d0e..3f9736c24b9d 100644 --- a/src/Files.Launcher/MessageHandlers/Win32MessageHandler.cs +++ b/src/Files.Launcher/MessageHandlers/Win32MessageHandler.cs @@ -301,7 +301,8 @@ private async void DirectoryWatcher_Changed(object sender, FileSystemEventArgs e { "FileSystem", Path.GetDirectoryName(e.FullPath) }, { "Name", e.Name }, { "Path", e.FullPath }, - { "Type", e.ChangeType.ToString() } + { "Type", e.ChangeType.ToString() }, + { "WatcherID", sender.GetHashCode() }, }; if (e.ChangeType == WatcherChangeTypes.Created) { diff --git a/src/Files.Uwp/ViewModels/ItemViewModel.cs b/src/Files.Uwp/ViewModels/ItemViewModel.cs index 6eb336e9e3af..a211e90f9793 100644 --- a/src/Files.Uwp/ViewModels/ItemViewModel.cs +++ b/src/Files.Uwp/ViewModels/ItemViewModel.cs @@ -2169,7 +2169,11 @@ private async Task AddFileOrFolderAsync(ListedItem item) return; } - filesAndFolders.Add(item); + if (!filesAndFolders.Any(x => x.ItemPath.Equals(item.ItemPath, StringComparison.OrdinalIgnoreCase))) // Avoid adding duplicate items + { + filesAndFolders.Add(item); + } + enumFolderSemaphore.Release(); } From 143c109546e9ce68e3f16baaf1324f12aee056ae Mon Sep 17 00:00:00 2001 From: Marco Gavelli Date: Sun, 3 Apr 2022 13:17:14 +0200 Subject: [PATCH 4/8] Fixed Regex --- src/Files.Uwp/ViewModels/ItemViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Files.Uwp/ViewModels/ItemViewModel.cs b/src/Files.Uwp/ViewModels/ItemViewModel.cs index a211e90f9793..e71de9cbe1e5 100644 --- a/src/Files.Uwp/ViewModels/ItemViewModel.cs +++ b/src/Files.Uwp/ViewModels/ItemViewModel.cs @@ -1508,7 +1508,7 @@ public async Task EnumerateItemsFromStandardFolderAsync(string path, Type s // Flag to use FindFirstFileExFromApp or StorageFolder enumeration var isBoxFolder = App.CloudDrivesManager.Drives.FirstOrDefault(x => x.Text == "Box")?.Path?.TrimEnd('\\') is string boxFolder ? path.StartsWith(boxFolder) : false; // Use storage folder for Box Drive (#4629) - var isNetworkFolder = System.Text.RegularExpressions.Regex.IsMatch(path, @"^\\(?!\?)"); // Use storage folder for network drives (*FromApp methods return access denied) + var isNetworkFolder = System.Text.RegularExpressions.Regex.IsMatch(path, @"^\\\\(?!\?)"); // Use storage folder for network drives (*FromApp methods return access denied) bool enumFromStorageFolder = isBoxFolder || isNetworkFolder; BaseStorageFolder rootFolder = null; From 6dce3b8bb6629503ec4c1c391a3c4fe9363b36ef Mon Sep 17 00:00:00 2001 From: Marco Gavelli Date: Sun, 3 Apr 2022 18:19:28 +0200 Subject: [PATCH 5/8] Elegant(?) way to restart directory watcher on app resume --- src/Files.Uwp/ViewModels/ItemViewModel.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Files.Uwp/ViewModels/ItemViewModel.cs b/src/Files.Uwp/ViewModels/ItemViewModel.cs index e71de9cbe1e5..03811a89f13d 100644 --- a/src/Files.Uwp/ViewModels/ItemViewModel.cs +++ b/src/Files.Uwp/ViewModels/ItemViewModel.cs @@ -80,6 +80,8 @@ public class ItemViewModel : ObservableObject, IDisposable private IFileListCache fileListCache = FileListCacheController.GetInstance(); + public event EventHandler ConnectionChanged; + private NamedPipeAsAppServiceConnection connection; private NamedPipeAsAppServiceConnection Connection @@ -96,6 +98,7 @@ private NamedPipeAsAppServiceConnection Connection { connection.RequestReceived += Connection_RequestReceived; } + ConnectionChanged?.Invoke(this, EventArgs.Empty); } } @@ -1506,7 +1509,7 @@ await CoreApplication.MainView.DispatcherQueue.EnqueueAsync(async () => public async Task EnumerateItemsFromStandardFolderAsync(string path, Type sourcePageType, CancellationToken cancellationToken, LibraryItem library = null) { // Flag to use FindFirstFileExFromApp or StorageFolder enumeration - var isBoxFolder = App.CloudDrivesManager.Drives.FirstOrDefault(x => x.Text == "Box")?.Path?.TrimEnd('\\') is string boxFolder ? + var isBoxFolder = App.CloudDrivesManager.Drives.FirstOrDefault(x => x.Text == "Box")?.Path?.TrimEnd('\\') is string boxFolder ? path.StartsWith(boxFolder) : false; // Use storage folder for Box Drive (#4629) var isNetworkFolder = System.Text.RegularExpressions.Regex.IsMatch(path, @"^\\\\(?!\?)"); // Use storage folder for network drives (*FromApp methods return access denied) bool enumFromStorageFolder = isBoxFolder || isNetworkFolder; @@ -1778,6 +1781,7 @@ await Task.Run(() => private async void WatchForWin32FolderChanges(string folderPath) { + void RestartWatcher(object sernder, EventArgs e) => WatchForWin32FolderChanges(folderPath); if (Connection != null) { var (status, response) = await Connection.SendMessageForResponseAsync(new ValueSet() @@ -1789,8 +1793,10 @@ private async void WatchForWin32FolderChanges(string folderPath) if (status == AppServiceResponseStatus.Success && response.ContainsKey("watcherID")) { + ConnectionChanged += RestartWatcher; watcherCTS.Token.Register(async () => { + ConnectionChanged -= RestartWatcher; if (Connection != null) { await Connection.SendMessageAsync(new ValueSet() From 8d849aa62907037132819fd025593379a5b6a105 Mon Sep 17 00:00:00 2001 From: Marco Gavelli Date: Sun, 3 Apr 2022 18:36:36 +0200 Subject: [PATCH 6/8] Unsubscribe restart watcher event --- src/Files.Uwp/ViewModels/ItemViewModel.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Files.Uwp/ViewModels/ItemViewModel.cs b/src/Files.Uwp/ViewModels/ItemViewModel.cs index 03811a89f13d..870590cc1c15 100644 --- a/src/Files.Uwp/ViewModels/ItemViewModel.cs +++ b/src/Files.Uwp/ViewModels/ItemViewModel.cs @@ -1781,7 +1781,14 @@ await Task.Run(() => private async void WatchForWin32FolderChanges(string folderPath) { - void RestartWatcher(object sernder, EventArgs e) => WatchForWin32FolderChanges(folderPath); + void RestartWatcher(object sender, EventArgs e) + { + if (Connection != null) + { + ConnectionChanged -= RestartWatcher; + WatchForWin32FolderChanges(folderPath); + } + } if (Connection != null) { var (status, response) = await Connection.SendMessageForResponseAsync(new ValueSet() From 7b24ce7d4b5df397e331bf1b6e70f81f88874ddd Mon Sep 17 00:00:00 2001 From: Marco Gavelli Date: Fri, 8 Apr 2022 01:56:39 +0200 Subject: [PATCH 7/8] Wait till transfer complete --- .../MessageHandlers/Win32MessageHandler.cs | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/Files.Launcher/MessageHandlers/Win32MessageHandler.cs b/src/Files.Launcher/MessageHandlers/Win32MessageHandler.cs index 3f9736c24b9d..1a700ea0a486 100644 --- a/src/Files.Launcher/MessageHandlers/Win32MessageHandler.cs +++ b/src/Files.Launcher/MessageHandlers/Win32MessageHandler.cs @@ -10,7 +10,9 @@ using System.IO.Pipes; using System.Linq; using System.Runtime.Versioning; +using System.Threading; using System.Threading.Tasks; +using Vanara.PInvoke; using Vanara.Windows.Shell; using Windows.ApplicationModel; using Windows.Foundation.Collections; @@ -306,9 +308,8 @@ private async void DirectoryWatcher_Changed(object sender, FileSystemEventArgs e }; if (e.ChangeType == WatcherChangeTypes.Created) { - using var folderItem = SafetyExtensions.IgnoreExceptions(() => new ShellItem(e.FullPath)); - if (folderItem == null) return; - var shellFileItem = ShellFolderExtensions.GetShellFileItem(folderItem); + var shellFileItem = await GetShellFileItemOnComplete(e.FullPath); + if (shellFileItem == null) return; response["Item"] = JsonConvert.SerializeObject(shellFileItem); } else if (e.ChangeType == WatcherChangeTypes.Renamed) @@ -320,6 +321,26 @@ private async void DirectoryWatcher_Changed(object sender, FileSystemEventArgs e } } + private async Task GetShellFileItemOnComplete(string fullPath) + { + while (true) + { + using var hFile = Kernel32.CreateFile(fullPath, Kernel32.FileAccess.GENERIC_READ, FileShare.Read, null, FileMode.Open, FileFlagsAndAttributes.FILE_FLAG_BACKUP_SEMANTICS); + if (!hFile.IsInvalid) + { + using var folderItem = SafetyExtensions.IgnoreExceptions(() => new ShellItem(fullPath)); + if (folderItem == null) return null; + return ShellFolderExtensions.GetShellFileItem(folderItem); + } + var lastError = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); + if (lastError != Win32Error.ERROR_SHARING_VIOLATION && lastError != Win32Error.ERROR_LOCK_VIOLATION) + { + return null; + } + await Task.Delay(200); + } + } + protected override void Dispose(bool disposing) { if (disposing) From cdf1777e20cc0b8f78bc18e8af5e6bf6c6f254cc Mon Sep 17 00:00:00 2001 From: Marco Gavelli Date: Fri, 8 Apr 2022 02:05:35 +0200 Subject: [PATCH 8/8] Rename method --- src/Files.Launcher/MessageHandlers/Win32MessageHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Files.Launcher/MessageHandlers/Win32MessageHandler.cs b/src/Files.Launcher/MessageHandlers/Win32MessageHandler.cs index 1a700ea0a486..d8548d8636a4 100644 --- a/src/Files.Launcher/MessageHandlers/Win32MessageHandler.cs +++ b/src/Files.Launcher/MessageHandlers/Win32MessageHandler.cs @@ -308,7 +308,7 @@ private async void DirectoryWatcher_Changed(object sender, FileSystemEventArgs e }; if (e.ChangeType == WatcherChangeTypes.Created) { - var shellFileItem = await GetShellFileItemOnComplete(e.FullPath); + var shellFileItem = await GetShellFileItemAsync(e.FullPath); if (shellFileItem == null) return; response["Item"] = JsonConvert.SerializeObject(shellFileItem); } @@ -321,7 +321,7 @@ private async void DirectoryWatcher_Changed(object sender, FileSystemEventArgs e } } - private async Task GetShellFileItemOnComplete(string fullPath) + private async Task GetShellFileItemAsync(string fullPath) { while (true) {