From 9824184d9073a38d8dd4fcba610814fb177bcae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Tue, 1 Feb 2022 13:03:00 +0100 Subject: [PATCH] SMB, GUI imput handler, ThreadSafe SMB --- src/Core/FileTime.Core/Components/Tab.cs | 7 +- .../Interactions/BasicInputHandler.cs | 9 +++ src/GuiApp/FileTime.Avalonia/Startup.cs | 4 +- .../ViewModels/ContainerViewModel.cs | 42 +++++++++-- .../ViewModels/MainPageViewModel.cs | 44 ++++++++++- .../LocalContentProvider.cs | 4 +- .../SmbClientContext.cs | 62 ++++++++++++++++ .../SmbContentProvider.cs | 16 +++- .../FileTime.Providers.Smb/SmbServer.cs | 73 ++++++++++++++++--- .../FileTime.Providers.Smb/SmbShare.cs | 52 ++++++------- 10 files changed, 265 insertions(+), 48 deletions(-) create mode 100644 src/Core/FileTime.Core/Interactions/BasicInputHandler.cs create mode 100644 src/Providers/FileTime.Providers.Smb/SmbClientContext.cs diff --git a/src/Core/FileTime.Core/Components/Tab.cs b/src/Core/FileTime.Core/Components/Tab.cs index b2b8c6b..62151b9 100644 --- a/src/Core/FileTime.Core/Components/Tab.cs +++ b/src/Core/FileTime.Core/Components/Tab.cs @@ -54,7 +54,10 @@ namespace FileTime.Core.Components IItem? itemToSelect = null; if (value != null) { - itemToSelect = (await _currentLocation.GetItems())?.FirstOrDefault(i => i.FullName == value?.FullName); + itemToSelect = (await _currentLocation.GetItems())?.FirstOrDefault(i => + i.FullName == null && value?.FullName == null + ? i.Name == value?.Name + : i.FullName == value?.FullName); if (itemToSelect == null) throw new IndexOutOfRangeException("Provided item does not exists in the current container."); } @@ -123,7 +126,7 @@ namespace FileTime.Core.Components private async Task HandleCurrentLocationRefresh(object? sender, AsyncEventArgs e) { - var currentSelectedName = (await GetCurrentSelectedItem())?.FullName ?? (await GetItemByLastPath()).FullName; + var currentSelectedName = (await GetCurrentSelectedItem())?.FullName ?? (await GetItemByLastPath())?.FullName; var currentLocationItems = (await (await GetCurrentLocation()).GetItems())!; if (currentSelectedName != null) { diff --git a/src/Core/FileTime.Core/Interactions/BasicInputHandler.cs b/src/Core/FileTime.Core/Interactions/BasicInputHandler.cs new file mode 100644 index 0000000..b1e287d --- /dev/null +++ b/src/Core/FileTime.Core/Interactions/BasicInputHandler.cs @@ -0,0 +1,9 @@ +namespace FileTime.Core.Interactions +{ + public class BasicInputHandler : IInputInterface + { + public Func, Task>? InputHandler { get; set; } + public async Task ReadInputs(IEnumerable fields) => + InputHandler != null ? await InputHandler.Invoke(fields) : throw new NotImplementedException(nameof(InputHandler) + " is not set"); + } +} \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/Startup.cs b/src/GuiApp/FileTime.Avalonia/Startup.cs index d44fc52..13e7e81 100644 --- a/src/GuiApp/FileTime.Avalonia/Startup.cs +++ b/src/GuiApp/FileTime.Avalonia/Startup.cs @@ -3,6 +3,7 @@ using FileTime.Avalonia.Application; using FileTime.Avalonia.Services; using FileTime.Avalonia.ViewModels; using FileTime.Core.Command; +using FileTime.Core.Interactions; using Microsoft.Extensions.DependencyInjection; namespace FileTime.Avalonia @@ -13,7 +14,8 @@ namespace FileTime.Avalonia { return serviceCollection .AddSingleton() - .AddTransient(); + .AddTransient() + .AddSingleton(); } internal static IServiceCollection AddServices(this IServiceCollection serviceCollection) { diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs index a0c554f..10987e1 100644 --- a/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/ContainerViewModel.cs @@ -15,8 +15,9 @@ namespace FileTime.Avalonia.ViewModels { [ViewModel] [Inject(typeof(ItemNameConverterService))] - public partial class ContainerViewModel : IItemViewModel + public partial class ContainerViewModel : IItemViewModel, IDisposable { + private bool _disposed; private bool _isRefreshing; private bool _isInitialized; private INewItemProcessor _newItemProcessor; @@ -128,13 +129,20 @@ namespace FileTime.Avalonia.ViewModels { _isRefreshing = true; - var containers = (await _container.GetContainers()).Select(c => AdoptOrCreateItem(c, (c2) => new ContainerViewModel(_newItemProcessor, this, c2, ItemNameConverterService))).ToList(); - var elements = (await _container.GetElements()).Select(e => AdoptOrCreateItem(e, (e2) => new ElementViewModel(e2, this, ItemNameConverterService))).ToList(); + var containers = (await _container.GetContainers())!.Select(c => AdoptOrReuseOrCreateItem(c, (c2) => new ContainerViewModel(_newItemProcessor, this, c2, ItemNameConverterService))).ToList(); + var elements = (await _container.GetElements())!.Select(e => AdoptOrReuseOrCreateItem(e, (e2) => new ElementViewModel(e2, this, ItemNameConverterService))).ToList(); + + var containersToRemove = _containers.Except(containers); _containers.Clear(); _elements.Clear(); _items.Clear(); + foreach (var containerToRemove in containersToRemove) + { + containerToRemove?.Dispose(); + } + foreach (var container in containers) { if (initializeChildren) await container.Init(false); @@ -161,11 +169,14 @@ namespace FileTime.Avalonia.ViewModels _isRefreshing = false; } - private TResult AdoptOrCreateItem(T item, Func generator) where T : IItem + private TResult AdoptOrReuseOrCreateItem(T item, Func generator) where T : class, IItem { - var itemToAdopt = ChildrenToAdopt.Find(i => i.Item.Name == item.Name); + var itemToAdopt = ChildrenToAdopt.Find(i => i.Item == item); if (itemToAdopt is TResult itemViewModel) return itemViewModel; + var existingViewModel = _items?.FirstOrDefault(i => i.Item == item); + if (existingViewModel is TResult itemViewModelToReuse) return itemViewModelToReuse; + return generator(item); } @@ -177,6 +188,7 @@ namespace FileTime.Avalonia.ViewModels foreach (var container in _containers) { container.Unload(true); + container.Dispose(); container.ChildrenToAdopt.Clear(); } } @@ -203,5 +215,25 @@ namespace FileTime.Avalonia.ViewModels if (!_isInitialized) await Task.Run(Refresh); return _items; } + + ~ContainerViewModel() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!_disposed && disposing) + { + Container.Refreshed.Remove(Container_Refreshed); + } + _disposed = true; + } } } diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs index d270b26..181ee14 100644 --- a/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs @@ -22,6 +22,7 @@ using FileTime.App.Core.Clipboard; using Microsoft.Extensions.DependencyInjection; using FileTime.Core.Command; using FileTime.Core.Timeline; +using FileTime.Core.Providers; namespace FileTime.Avalonia.ViewModels { @@ -40,7 +41,7 @@ namespace FileTime.Avalonia.ViewModels private IClipboard _clipboard; private TimeRunner _timeRunner; - + private IEnumerable? _contentProviders; private Action? _inputHandler; [Property] @@ -67,6 +68,11 @@ namespace FileTime.Avalonia.ViewModels { _clipboard = App.ServiceProvider.GetService()!; _timeRunner = App.ServiceProvider.GetService()!; + _contentProviders = App.ServiceProvider.GetService>(); + var inputInterface = (BasicInputHandler)App.ServiceProvider.GetService()!; + inputInterface.InputHandler = ReadInputs2; + App.ServiceProvider.GetService(); + _timeRunner.CommandsChanged += (o, e) => OnPropertyChanged(nameof(TimelineCommands)); InitCommandBindings(); @@ -496,6 +502,19 @@ namespace FileTime.Avalonia.ViewModels await _timeRunner.Refresh(); } + private async Task GoToContainer() + { + var handler = () => + { + if (Inputs != null) + { + + } + }; + + ReadInputs(new List() { new Core.Interactions.InputElement("Path", InputType.Text) }, handler); + } + [Command] public void ProcessInputs() { @@ -669,6 +688,24 @@ namespace FileTime.Avalonia.ViewModels _inputHandler = inputHandler; } + public async Task ReadInputs2(IEnumerable fields) + { + var waiting = true; + var result = new string[0]; + ReadInputs(fields.ToList(), () => + { + if(Inputs != null) + { + result = Inputs.Select(i => i.Value).ToArray(); + } + waiting = false; + }); + + while (waiting) await Task.Delay(100); + + return result; + } + private void ShowMessageBox(string text, Action inputHandler) { MessageBoxText = text; @@ -847,6 +884,11 @@ namespace FileTime.Avalonia.ViewModels FileTime.App.Core.Command.Commands.Refresh, new KeyWithModifiers[]{new KeyWithModifiers(Key.R)}, RefreshCurrentLocation), + new CommandBinding( + "go to", + FileTime.App.Core.Command.Commands.Refresh, + new KeyWithModifiers[]{new KeyWithModifiers(Key.L, ctrl: true)}, + GoToContainer), }; var universalCommandBindings = new List() { diff --git a/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs b/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs index 7c26679..6965e2f 100644 --- a/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs +++ b/src/Providers/FileTime.Providers.Local/LocalContentProvider.cs @@ -49,8 +49,8 @@ namespace FileTime.Providers.Local public async Task GetByPath(string path) { var pathParts = (IsCaseInsensitive ? path.ToLower() : path).TrimStart(Constants.SeparatorChar).Split(Constants.SeparatorChar); - - if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && pathParts.Length == 1 && pathParts[0] == "") return this; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && pathParts.Length == 1 && pathParts[0] == "") return this; var rootContainer = _rootContainers.FirstOrDefault(c => NormalizePath(c.Name) == NormalizePath(pathParts[0])); diff --git a/src/Providers/FileTime.Providers.Smb/SmbClientContext.cs b/src/Providers/FileTime.Providers.Smb/SmbClientContext.cs new file mode 100644 index 0000000..d808f85 --- /dev/null +++ b/src/Providers/FileTime.Providers.Smb/SmbClientContext.cs @@ -0,0 +1,62 @@ +using SMBLibrary.Client; + +namespace FileTime.Providers.Smb +{ + public class SmbClientContext + { + private readonly Func> _getSmbClient; + private readonly Action _disposeClient; + private bool _isRunning; + private readonly object _lock = new object(); + + public SmbClientContext(Func> getSmbClient, Action disposeClient) + { + _getSmbClient = getSmbClient; + _disposeClient = disposeClient; + } + + public async Task RunWithSmbClientAsync(Func func) + { + while (true) + { + lock (_lock) + { + if(!_isRunning) + { + _isRunning = true; + break; + } + } + + await Task.Delay(1); + } + try + { + ISMBClient client; + while (true) + { + try + { + client = await _getSmbClient(); + return func(client); + } + catch (Exception e) when (e.Source == "SMBLibrary") + { + _disposeClient(); + } + catch (Exception e) + { + throw new Exception("Exception was thrown while executing method with SmbClient.", e); + } + } + } + finally + { + lock (_lock) + { + _isRunning = false; + } + } + } + } +} \ No newline at end of file diff --git a/src/Providers/FileTime.Providers.Smb/SmbContentProvider.cs b/src/Providers/FileTime.Providers.Smb/SmbContentProvider.cs index eea2a48..a99ebdc 100644 --- a/src/Providers/FileTime.Providers.Smb/SmbContentProvider.cs +++ b/src/Providers/FileTime.Providers.Smb/SmbContentProvider.cs @@ -63,9 +63,21 @@ namespace FileTime.Providers.Smb throw new NotSupportedException(); } - public Task GetByPath(string path) + public async Task GetByPath(string path) { - throw new NotImplementedException(); + if (path == null) return this; + + var pathParts = path.TrimStart(Constants.SeparatorChar).Split(Constants.SeparatorChar); + + var rootContainer = _rootContainers.Find(c => c.Name == pathParts[0]); + + if (rootContainer == null) + { + return null; + } + + var remainingPath = string.Join(Constants.SeparatorChar, pathParts.Skip(1)); + return remainingPath.Length == 0 ? rootContainer : await rootContainer.GetByPath(remainingPath); } public IContainer? GetParent() => _parent; diff --git a/src/Providers/FileTime.Providers.Smb/SmbServer.cs b/src/Providers/FileTime.Providers.Smb/SmbServer.cs index e8c4cf3..5f32073 100644 --- a/src/Providers/FileTime.Providers.Smb/SmbServer.cs +++ b/src/Providers/FileTime.Providers.Smb/SmbServer.cs @@ -17,7 +17,10 @@ namespace FileTime.Providers.Smb private IReadOnlyList? _items; private readonly IReadOnlyList? _elements = new List().AsReadOnly(); private ISMBClient? _client; + private readonly object _clientGuard = new object(); + private bool _refreshingClient; private readonly IInputInterface _inputInterface; + private readonly SmbClientContext _smbClientContext; public string Name { get; } @@ -37,6 +40,7 @@ namespace FileTime.Providers.Smb public SmbServer(string path, SmbContentProvider contentProvider, IInputInterface inputInterface) { _inputInterface = inputInterface; + _smbClientContext = new SmbClientContext(GetSmbClient, DisposeSmbClient); Provider = contentProvider; FullName = Name = path; @@ -86,26 +90,60 @@ namespace FileTime.Providers.Smb public async Task Refresh() { - ISMBClient client = await GetSmbClient(); + List shares = await _smbClientContext.RunWithSmbClientAsync((client) => client.ListShares(out var status)); - List shares = client.ListShares(out var status); - - _shares = shares.ConvertAll(s => new SmbShare(s, Provider, this, GetSmbClient)).AsReadOnly(); + _shares = shares.ConvertAll(s => new SmbShare(s, Provider, this, _smbClientContext)).AsReadOnly(); _items = _shares.Cast().ToList().AsReadOnly(); await Refreshed.InvokeAsync(this, AsyncEventArgs.Empty); } public Task Clone() => Task.FromResult((IContainer)this); + private void DisposeSmbClient() + { + lock (_clientGuard) + { + _client = null; + } + } + private async Task GetSmbClient() { - if (_client == null) + bool isClientNull; + lock (_clientGuard) + { + isClientNull = _client == null; + } + + while (isClientNull) + { + if (!await RefreshSmbClient()) + { + await Task.Delay(1); + } + + lock (_clientGuard) + { + isClientNull = _client == null; + } + } + return _client!; + } + + private async Task RefreshSmbClient() + { + lock (_clientGuard) + { + if (_refreshingClient) return false; + _refreshingClient = true; + } + try { var couldParse = IPAddress.TryParse(Name[2..], out var ipAddress); - _client = new SMB2Client(); + var client = new SMB2Client(); var connected = couldParse - ? _client.Connect(ipAddress, SMBTransportType.DirectTCPTransport) - : _client.Connect(Name[2..], SMBTransportType.DirectTCPTransport); + ? client.Connect(ipAddress, SMBTransportType.DirectTCPTransport) + : client.Connect(Name[2..], SMBTransportType.DirectTCPTransport); if (connected) { @@ -122,14 +160,29 @@ namespace FileTime.Providers.Smb _password = inputs[1]; } - if (_client.Login(string.Empty, _username, _password) != NTStatus.STATUS_SUCCESS) + if (client.Login(string.Empty, _username, _password) != NTStatus.STATUS_SUCCESS) { _username = null; _password = null; } + else + { + lock (_clientGuard) + { + _client = client; + } + } } } - return _client; + finally + { + lock (_clientGuard) + { + _refreshingClient = false; + } + } + + return true; } public Task Rename(string newName) => throw new NotSupportedException(); diff --git a/src/Providers/FileTime.Providers.Smb/SmbShare.cs b/src/Providers/FileTime.Providers.Smb/SmbShare.cs index 73acb7b..e59820d 100644 --- a/src/Providers/FileTime.Providers.Smb/SmbShare.cs +++ b/src/Providers/FileTime.Providers.Smb/SmbShare.cs @@ -11,7 +11,7 @@ namespace FileTime.Providers.Smb private IReadOnlyList? _items; private IReadOnlyList? _containers; private IReadOnlyList? _elements; - private Func> _getSmbClient; + private SmbClientContext _smbClientContext; private readonly IContainer? _parent; public string Name { get; } @@ -28,10 +28,10 @@ namespace FileTime.Providers.Smb public AsyncEventHandler Refreshed { get; } = new(); - public SmbShare(string name, SmbContentProvider contentProvider, IContainer parent, Func> getSmbClient) + public SmbShare(string name, SmbContentProvider contentProvider, IContainer parent, SmbClientContext smbClientContext) { _parent = parent; - _getSmbClient = getSmbClient; + _smbClientContext = smbClientContext; Name = name; FullName = parent?.FullName == null ? Name : parent.FullName + Constants.SeparatorChar + Name; @@ -117,40 +117,42 @@ namespace FileTime.Providers.Smb public async Task<(List containers, List elements)> ListFolder(IContainer parent, string shareName, string folderName) { - var containers = new List(); - var elements = new List(); - - var client = await _getSmbClient(); - ISMBFileStore fileStore = client.TreeConnect(shareName, out var status); - if (status == NTStatus.STATUS_SUCCESS) + return await _smbClientContext.RunWithSmbClientAsync(client => { - status = fileStore.CreateFile(out object directoryHandle, out FileStatus fileStatus, folderName, AccessMask.GENERIC_READ, SMBLibrary.FileAttributes.Directory, ShareAccess.Read | ShareAccess.Write, CreateDisposition.FILE_OPEN, CreateOptions.FILE_DIRECTORY_FILE, null); + var containers = new List(); + var elements = new List(); + NTStatus status = NTStatus.STATUS_DATA_ERROR; + ISMBFileStore fileStore = client.TreeConnect(shareName, out status); if (status == NTStatus.STATUS_SUCCESS) { - status = fileStore.QueryDirectory(out List fileList, directoryHandle, "*", FileInformationClass.FileDirectoryInformation); - status = fileStore.CloseFile(directoryHandle); - - foreach (var item in fileList) + status = fileStore.CreateFile(out object directoryHandle, out FileStatus fileStatus, folderName, AccessMask.GENERIC_READ, SMBLibrary.FileAttributes.Directory, ShareAccess.Read | ShareAccess.Write, CreateDisposition.FILE_OPEN, CreateOptions.FILE_DIRECTORY_FILE, null); + if (status == NTStatus.STATUS_SUCCESS) { - if (item is FileDirectoryInformation fileDirectoryInformation && fileDirectoryInformation.FileName != "." && fileDirectoryInformation.FileName != "..") + status = fileStore.QueryDirectory(out List fileList, directoryHandle, "*", FileInformationClass.FileDirectoryInformation); + status = fileStore.CloseFile(directoryHandle); + + foreach (var item in fileList) { - if ((fileDirectoryInformation.FileAttributes & SMBLibrary.FileAttributes.Directory) == SMBLibrary.FileAttributes.Directory) + if (item is FileDirectoryInformation fileDirectoryInformation && fileDirectoryInformation.FileName != "." && fileDirectoryInformation.FileName != "..") { - containers.Add(new SmbFolder(fileDirectoryInformation.FileName, Provider, this, parent)); - } - else - { - elements.Add(new SmbFile(fileDirectoryInformation.FileName, Provider, parent)); + if ((fileDirectoryInformation.FileAttributes & SMBLibrary.FileAttributes.Directory) == SMBLibrary.FileAttributes.Directory) + { + containers.Add(new SmbFolder(fileDirectoryInformation.FileName, Provider, this, parent)); + } + else + { + elements.Add(new SmbFile(fileDirectoryInformation.FileName, Provider, parent)); + } } } } } - } - containers = containers.OrderBy(c => c.Name).ToList(); - elements = elements.OrderBy(e => e.Name).ToList(); + containers = containers.OrderBy(c => c.Name).ToList(); + elements = elements.OrderBy(e => e.Name).ToList(); - return (containers, elements); + return (containers, elements); + }); } public Task Rename(string newName) => throw new NotSupportedException();