SMB, GUI imput handler, ThreadSafe SMB

This commit is contained in:
2022-02-01 13:03:00 +01:00
parent c2dcb49016
commit 9824184d90
10 changed files with 265 additions and 48 deletions

View File

@@ -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)
{

View File

@@ -0,0 +1,9 @@
namespace FileTime.Core.Interactions
{
public class BasicInputHandler : IInputInterface
{
public Func<IEnumerable<InputElement>, Task<string?[]>>? InputHandler { get; set; }
public async Task<string?[]> ReadInputs(IEnumerable<InputElement> fields) =>
InputHandler != null ? await InputHandler.Invoke(fields) : throw new NotImplementedException(nameof(InputHandler) + " is not set");
}
}

View File

@@ -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<AppState>()
.AddTransient<MainPageViewModel>();
.AddTransient<MainPageViewModel>()
.AddSingleton<IInputInterface, BasicInputHandler>();
}
internal static IServiceCollection AddServices(this IServiceCollection serviceCollection)
{

View File

@@ -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, TResult>(T item, Func<T, TResult> generator) where T : IItem
private TResult AdoptOrReuseOrCreateItem<T, TResult>(T item, Func<T, TResult> 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;
}
}
}

View File

@@ -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<IContentProvider>? _contentProviders;
private Action? _inputHandler;
[Property]
@@ -67,6 +68,11 @@ namespace FileTime.Avalonia.ViewModels
{
_clipboard = App.ServiceProvider.GetService<IClipboard>()!;
_timeRunner = App.ServiceProvider.GetService<TimeRunner>()!;
_contentProviders = App.ServiceProvider.GetService<IEnumerable<IContentProvider>>();
var inputInterface = (BasicInputHandler)App.ServiceProvider.GetService<IInputInterface>()!;
inputInterface.InputHandler = ReadInputs2;
App.ServiceProvider.GetService<TopContainer>();
_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<Core.Interactions.InputElement>() { 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<string?[]> ReadInputs2(IEnumerable<Core.Interactions.InputElement> 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<CommandBinding>()
{

View File

@@ -0,0 +1,62 @@
using SMBLibrary.Client;
namespace FileTime.Providers.Smb
{
public class SmbClientContext
{
private readonly Func<Task<ISMBClient>> _getSmbClient;
private readonly Action _disposeClient;
private bool _isRunning;
private readonly object _lock = new object();
public SmbClientContext(Func<Task<ISMBClient>> getSmbClient, Action disposeClient)
{
_getSmbClient = getSmbClient;
_disposeClient = disposeClient;
}
public async Task<T> RunWithSmbClientAsync<T>(Func<ISMBClient, T> 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;
}
}
}
}
}

View File

@@ -63,9 +63,21 @@ namespace FileTime.Providers.Smb
throw new NotSupportedException();
}
public Task<IItem?> GetByPath(string path)
public async Task<IItem?> 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;

View File

@@ -17,7 +17,10 @@ namespace FileTime.Providers.Smb
private IReadOnlyList<IItem>? _items;
private readonly IReadOnlyList<IElement>? _elements = new List<IElement>().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<string> shares = await _smbClientContext.RunWithSmbClientAsync((client) => client.ListShares(out var status));
List<string> 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<IItem>().ToList().AsReadOnly();
await Refreshed.InvokeAsync(this, AsyncEventArgs.Empty);
}
public Task<IContainer> Clone() => Task.FromResult((IContainer)this);
private void DisposeSmbClient()
{
lock (_clientGuard)
{
_client = null;
}
}
private async Task<ISMBClient> 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<bool> 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();

View File

@@ -11,7 +11,7 @@ namespace FileTime.Providers.Smb
private IReadOnlyList<IItem>? _items;
private IReadOnlyList<IContainer>? _containers;
private IReadOnlyList<IElement>? _elements;
private Func<Task<ISMBClient>> _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<Task<ISMBClient>> 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;
@@ -116,12 +116,13 @@ namespace FileTime.Providers.Smb
}
public async Task<(List<IContainer> containers, List<IElement> elements)> ListFolder(IContainer parent, string shareName, string folderName)
{
return await _smbClientContext.RunWithSmbClientAsync(client =>
{
var containers = new List<IContainer>();
var elements = new List<IElement>();
var client = await _getSmbClient();
ISMBFileStore fileStore = client.TreeConnect(shareName, out var status);
NTStatus status = NTStatus.STATUS_DATA_ERROR;
ISMBFileStore fileStore = client.TreeConnect(shareName, out status);
if (status == NTStatus.STATUS_SUCCESS)
{
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);
@@ -151,6 +152,7 @@ namespace FileTime.Providers.Smb
elements = elements.OrderBy(e => e.Name).ToList();
return (containers, elements);
});
}
public Task Rename(string newName) => throw new NotSupportedException();