WIP CommandScheduler UI

This commit is contained in:
2023-01-31 23:26:36 +01:00
parent 06a9fc27d7
commit 30ecc6e515
37 changed files with 553 additions and 181 deletions

View File

@@ -2,6 +2,7 @@ namespace FileTime.Core.Command;
public enum ExecutionState
{
Initializing,
Waiting,
Running,
Finished

View File

@@ -4,6 +4,10 @@ namespace FileTime.Core.Command;
public interface ICommand
{
IObservable<string> DisplayLabel { get; }
IObservable<int> TotalProgress { get; }
IObservable<int> CurrentProgress { get; }
Task<CanCommandRun> CanRun(PointInTime currentTime);
Task<PointInTime> SimulateCommand(PointInTime currentTime);
}

View File

@@ -0,0 +1,27 @@
using DynamicData;
using FileTime.Core.Models;
namespace FileTime.Core.Extensions;
public static class BindedCollectionExtensions
{
public static BindedCollection<T> ToBindedCollection<T>(this IObservable<IChangeSet<T>> source)
{
return new BindedCollection<T>(source);
}
public static BindedCollection<T> ToBindedCollection<T>(this IObservable<IObservable<IChangeSet<T>>?> source)
{
return new BindedCollection<T>(source);
}
public static BindedCollection<T, TKey> ToBindedCollection<T, TKey>(this IObservable<IChangeSet<T, TKey>> source) where TKey : notnull
{
return new BindedCollection<T, TKey>(source);
}
public static BindedCollection<T, TKey> ToBindedCollection<T, TKey>(this IObservable<IObservable<IChangeSet<T, TKey>>?> source) where TKey : notnull
{
return new BindedCollection<T, TKey>(source);
}
}

View File

@@ -53,7 +53,7 @@ public static class DynamicDataExtensions
this IObservable<IChangeSet<AbsolutePath, string>> stream)
=> await GetItemsAsync(stream.ToCollection());
private static Task<IEnumerable<AbsolutePath>?> GetItemsAsync(
public static Task<IEnumerable<AbsolutePath>?> GetItemsAsync(
this IObservable<IReadOnlyCollection<AbsolutePath>> stream)
{
var taskCompletionSource = new TaskCompletionSource<IEnumerable<AbsolutePath>?>();

View File

@@ -0,0 +1,106 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using DynamicData;
using PropertyChanged.SourceGenerator;
namespace FileTime.Core.Models;
public partial class BindedCollection<T> : IDisposable, INotifyPropertyChanged
{
private readonly IDisposable? _disposable;
private IDisposable? _innerDisposable;
[Notify] private ReadOnlyObservableCollection<T>? _collection;
public BindedCollection()
{
}
public BindedCollection(IObservable<IChangeSet<T>> dynamicList)
{
_disposable = dynamicList
.Bind(out var collection)
.DisposeMany()
.Subscribe();
_collection = collection;
}
public BindedCollection(IObservable<IObservable<IChangeSet<T>>?> dynamicListSource)
{
_disposable = dynamicListSource.Subscribe(dynamicList =>
{
_innerDisposable?.Dispose();
if (dynamicList is not null)
{
_innerDisposable = dynamicList
.Bind(out var collection)
.DisposeMany()
.Subscribe();
Collection = collection;
}
else
{
Collection = null;
}
});
}
public void Dispose()
{
_disposable?.Dispose();
_innerDisposable?.Dispose();
GC.SuppressFinalize(this);
}
}
public partial class BindedCollection<T, TKey> : IDisposable, INotifyPropertyChanged where TKey : notnull
{
private readonly IDisposable? _disposable;
private IDisposable? _innerDisposable;
[Notify] private ReadOnlyObservableCollection<T>? _collection;
public BindedCollection()
{
}
public BindedCollection(IObservable<IChangeSet<T, TKey>> dynamicList)
{
_disposable = dynamicList
.Bind(out var collection)
.DisposeMany()
.Subscribe();
_collection = collection;
}
public BindedCollection(IObservable<IObservable<IChangeSet<T, TKey>>?> dynamicListSource)
{
_disposable = dynamicListSource.Subscribe(dynamicList =>
{
_innerDisposable?.Dispose();
if (dynamicList is not null)
{
_innerDisposable = dynamicList
.Bind(out var collection)
.DisposeMany()
.Subscribe();
Collection = collection;
}
else
{
Collection = null;
}
});
}
public void Dispose()
{
_disposable?.Dispose();
_innerDisposable?.Dispose();
GC.SuppressFinalize(this);
}
}

View File

@@ -4,18 +4,50 @@ namespace FileTime.Core.Timeline;
public class CommandTimeState
{
private object _executionStateLock = new object();
private ExecutionState _executionState;
public ICommand Command { get; }
public CanCommandRun CanRun { get; private set; } = CanCommandRun.False;
public bool ForceRun { get; set; }
public ExecutionState ExecutionState { get; set; }
public ExecutionState ExecutionState
{
get
{
lock (_executionStateLock)
{
return _executionState;
}
}
set
{
lock (_executionStateLock)
{
_executionState = value;
}
}
}
public CommandTimeState(ICommand command)
{
Command = command;
SetToWait();
}
public async Task UpdateStateAsync(PointInTime? startPoint)
{
CanRun = startPoint == null ? CanCommandRun.False : await Command.CanRun(startPoint);
}
private async void SetToWait()
{
// This command won't be shown in the scheduler until this timeout is over
// Short living commands while not "blink on" the list
await Task.Delay(100);
lock (_executionStateLock)
{
if (_executionState == ExecutionState.Initializing)
_executionState = ExecutionState.Waiting;
}
}
}

View File

@@ -1,3 +1,4 @@
using DynamicData;
using FileTime.Core.Command;
using FileTime.Core.Models;
@@ -7,5 +8,8 @@ public interface ICommandScheduler
{
Task AddCommand(ICommand command, int? batchId = null, bool toNewBatch = false);
IObservable<FullName> ContainerToRefresh { get; }
IObservable<IChangeSet<ParallelCommands>> CommandsToRun { get; }
bool IsRunningEnabled { get; }
void RefreshContainer(FullName container);
Task SetRunningEnabledAsync(bool value);
}

View File

@@ -1,15 +1,32 @@
using DynamicData;
using FileTime.Core.Command;
using FileTime.Core.Extensions;
using FileTime.Core.Models;
namespace FileTime.Core.Timeline;
public class ParallelCommands
public class ParallelCommands : IDisposable
{
private static ushort _idCounter;
private List<CommandTimeState> _commands;
private readonly SourceList<CommandTimeState> _commands;
private PointInTime? _startTime;
public ushort Id { get; }
public IReadOnlyList<CommandTimeState> Commands { get; }
public IObservable<IChangeSet<CommandTimeState>> Commands { get; }
public BindedCollection<CommandTimeState> CommandsCollection { get; }
public PointInTime? Result { get; private set; }
public PointInTime? StartTime => _startTime;
public async Task<PointInTime?> SetStartTimeAsync(PointInTime? startTime)
{
_startTime = startTime;
return await RefreshResult();
}
public ParallelCommands(PointInTime? result)
: this(new List<CommandTimeState>(), result)
{
@@ -19,8 +36,10 @@ public class ParallelCommands
{
Id = _idCounter++;
_commands = commands;
Commands = _commands.AsReadOnly();
_commands = new SourceList<CommandTimeState>();
_commands.Edit((innerList) => innerList.AddRange(commands));
Commands = _commands.Connect();
CommandsCollection = Commands.ToBindedCollection();
Result = result;
}
@@ -63,10 +82,17 @@ public class ParallelCommands
}
}
public async Task<PointInTime?> RefreshResult(PointInTime? startPoint)
public async Task RemoveCommand(ICommand command)
{
var result = startPoint;
foreach (var commandTimeState in _commands)
var commandTimeState = _commands.Items.First(c => c.Command == command);
_commands.Remove(commandTimeState);
await RefreshResult();
}
public async Task<PointInTime?> RefreshResult()
{
var result = StartTime;
foreach (var commandTimeState in _commands.Items)
{
await commandTimeState.UpdateStateAsync(result);
if (result != null)
@@ -87,7 +113,8 @@ public class ParallelCommands
return Result;
}
public void RemoveAt(int number) => _commands.RemoveAt(number);
internal void Remove(CommandTimeState command) => _commands.Remove(command);
public void Dispose()
{
CommandsCollection.Dispose();
}
}

View File

@@ -0,0 +1,36 @@
using System.Reactive.Linq;
using System.Reactive.Subjects;
using FileTime.Core.Timeline;
namespace FileTime.Core.Command;
public abstract class CommandBase : ICommand
{
private readonly BehaviorSubject<string> _displayLabel;
private readonly BehaviorSubject<int> _totalProgress;
private readonly BehaviorSubject<int> _currentProgress;
public IObservable<string> DisplayLabel { get; }
public IObservable<int> TotalProgress { get; }
public IObservable<int> CurrentProgress { get; }
protected CommandBase(string displayLabel = "", int totalProgress = 0, int currentProgress = 0)
{
_displayLabel = new(displayLabel);
_totalProgress = new(totalProgress);
_currentProgress = new(currentProgress);
DisplayLabel = _displayLabel.AsObservable();
TotalProgress = _totalProgress.AsObservable();
CurrentProgress = _currentProgress.AsObservable();
}
public abstract Task<CanCommandRun> CanRun(PointInTime currentTime);
public abstract Task<PointInTime> SimulateCommand(PointInTime currentTime);
protected void SetDisplayLabel(string displayLabel) => _displayLabel.OnNext(displayLabel);
protected void SetTotalProgress(int totalProgress) => _totalProgress.OnNext(totalProgress);
protected void SetCurrentProgress(int currentProgress) => _currentProgress.OnNext(currentProgress);
}

View File

@@ -5,7 +5,7 @@ using FileTime.Core.Timeline;
namespace FileTime.Core.Command.Copy;
public class CopyCommand : ITransportationCommand
public class CopyCommand : CommandBase, ITransportationCommand
{
private readonly ITimelessContentProvider _timelessContentProvider;
private readonly ICommandSchedulerNotifier _commandSchedulerNotifier;
@@ -22,18 +22,19 @@ public class CopyCommand : ITransportationCommand
public CopyCommand(
ITimelessContentProvider timelessContentProvider,
ICommandSchedulerNotifier commandSchedulerNotifier)
: base("Copy")
{
_timelessContentProvider = timelessContentProvider;
_commandSchedulerNotifier = commandSchedulerNotifier;
}
public Task<CanCommandRun> CanRun(PointInTime currentTime)
public override Task<CanCommandRun> CanRun(PointInTime currentTime)
{
//TODO:
return Task.FromResult(CanCommandRun.True);
}
public async Task<PointInTime> SimulateCommand(PointInTime currentTime)
public override async Task<PointInTime> SimulateCommand(PointInTime currentTime)
{
if (Sources == null) throw new ArgumentException(nameof(Sources) + " can not be null");
if (Target == null) throw new ArgumentException(nameof(Target) + " can not be null");

View File

@@ -7,7 +7,7 @@ using InitableService;
namespace FileTime.Core.Command.Create;
public abstract class CreateItemBase : IExecutableCommand, IInitable<FullName, string>
public abstract class CreateItemBase : CommandBase, IExecutableCommand, IInitable<FullName, string>
{
private readonly ITimelessContentProvider _timelessContentProvider;
private readonly IContentAccessorFactory _contentAccessorFactory;
@@ -17,12 +17,13 @@ public abstract class CreateItemBase : IExecutableCommand, IInitable<FullName, s
protected CreateItemBase(
ITimelessContentProvider timelessContentProvider,
IContentAccessorFactory contentAccessorFactory)
: base("Create")
{
_timelessContentProvider = timelessContentProvider;
_contentAccessorFactory = contentAccessorFactory;
}
public async Task<CanCommandRun> CanRun(PointInTime currentTime)
public override async Task<CanCommandRun> CanRun(PointInTime currentTime)
{
if (Parent is null)
{
@@ -49,7 +50,7 @@ public abstract class CreateItemBase : IExecutableCommand, IInitable<FullName, s
return existingItem switch
{
null => CanCommandRun.True,
{ Type: AbsolutePathType.Container } => CanCommandRun.Forcable,
{Type: AbsolutePathType.Container} => CanCommandRun.Forcable,
_ => CanCommandRun.False
};
}
@@ -60,7 +61,7 @@ public abstract class CreateItemBase : IExecutableCommand, IInitable<FullName, s
return CanCommandRun.False;
}
public Task<PointInTime> SimulateCommand(PointInTime currentTime)
public override Task<PointInTime> SimulateCommand(PointInTime currentTime)
{
if (Parent is null)
{

View File

@@ -5,7 +5,7 @@ using FileTime.Core.Timeline;
namespace FileTime.Core.Command.Delete;
public class DeleteCommand : IExecutableCommand
public class DeleteCommand : CommandBase, IExecutableCommand
{
private readonly IContentAccessorFactory _contentAccessorFactory;
private readonly ITimelessContentProvider _timelessContentProvider;
@@ -15,18 +15,19 @@ public class DeleteCommand : IExecutableCommand
public DeleteCommand(
IContentAccessorFactory contentAccessorFactory,
ITimelessContentProvider timelessContentProvider)
: base("Delete")
{
_contentAccessorFactory = contentAccessorFactory;
_timelessContentProvider = timelessContentProvider;
}
public Task<CanCommandRun> CanRun(PointInTime currentTime)
public override Task<CanCommandRun> CanRun(PointInTime currentTime)
{
//TODO
return Task.FromResult(CanCommandRun.True);
}
public Task<PointInTime> SimulateCommand(PointInTime currentTime)
public override Task<PointInTime> SimulateCommand(PointInTime currentTime)
{
//TODO
return Task.FromResult(currentTime);
@@ -38,8 +39,8 @@ public class DeleteCommand : IExecutableCommand
//Delete
await TraverseTree(
PointInTime.Present,
ItemsToDelete,
PointInTime.Present,
ItemsToDelete,
new Dictionary<string, IItemDeleter>(),
new DeleteStrategy()
);
@@ -69,9 +70,9 @@ public class DeleteCommand : IExecutableCommand
if (itemToDelete is IContainer container)
{
await TraverseTree(
currentTime,
(await container.Items.GetItemsAsync())?.Select(i => i.Path) ?? Enumerable.Empty<FullName>(),
itemDeleters,
currentTime,
(await container.Items.GetItemsAsync())?.Select(i => i.Path) ?? Enumerable.Empty<FullName>(),
itemDeleters,
deleteStrategy
);
}

View File

@@ -13,28 +13,31 @@ public class CommandScheduler : ICommandScheduler
private readonly Subject<FullName> _containerToRefresh = new();
private readonly object _guard = new();
private bool _enableRunning = true;
private bool _isRunningEnabled = true;
private bool _resourceIsInUse;
public IObservable<FullName> ContainerToRefresh { get; }
public bool EnableRunning
{
get
{
var result = true;
RunWithLock(() => result = _enableRunning);
return result;
}
public bool IsRunningEnabled => _isRunningEnabled;
set { RunWithLock(() => _enableRunning = value); }
public async Task SetRunningEnabledAsync(bool value)
{
_isRunningEnabled = value;
if (value)
{
await RunWithLockAsync(ExecuteCommands);
}
}
public IObservable<IChangeSet<ParallelCommands>> CommandsToRun { get; }
public CommandScheduler(ILocalCommandExecutor localExecutor)
{
CommandsToRun = _commandsToRun.Connect();
ContainerToRefresh = _containerToRefresh.AsObservable();
localExecutor.CommandFinished += LocalExecutorOnCommandFinished;
localExecutor.CommandFinished += ExecutorOnCommandFinished;
_commandExecutors.Add(localExecutor);
}
@@ -46,6 +49,7 @@ public class CommandScheduler : ICommandScheduler
if (_commandsToRun.Count == 0)
{
//TODO: Add event handler to update
batchToAdd = new ParallelCommands(PointInTime.CreateEmpty());
_commandsToRun.Add(batchToAdd);
}
@@ -82,13 +86,11 @@ public class CommandScheduler : ICommandScheduler
private void ExecuteCommands()
{
if (!_enableRunning) return;
if (!_isRunningEnabled) return;
var parallelCommandsToExecute = _commandsToRun.Items.FirstOrDefault();
if (parallelCommandsToExecute is null ||
parallelCommandsToExecute.Commands.All(c => c.ExecutionState != ExecutionState.Waiting)) return;
var commandsToExecute = _commandsToRun.Items.FirstOrDefault()?.CommandsCollection.Collection;
if (commandsToExecute is null || commandsToExecute.All(c => c.ExecutionState != ExecutionState.Initializing && c.ExecutionState != ExecutionState.Waiting)) return;
var commandsToExecute = parallelCommandsToExecute.Commands;
foreach (var commandToExecute in commandsToExecute)
{
@@ -108,15 +110,20 @@ public class CommandScheduler : ICommandScheduler
return _commandExecutors[0];
}
private void LocalExecutorOnCommandFinished(object? sender, ICommand command)
private async void ExecutorOnCommandFinished(object? sender, ICommand command)
{
var parallelCommandsToExecute = _commandsToRun.Items.FirstOrDefault();
if (parallelCommandsToExecute is null) return;
var firstCommandBlock = _commandsToRun
.Items
.FirstOrDefault();
var state = firstCommandBlock
?.CommandsCollection
.Collection
?.FirstOrDefault(c => c.Command == command);
var state = parallelCommandsToExecute.Commands.FirstOrDefault(c => c.Command == command);
if (state is null) return;
state.ExecutionState = ExecutionState.Finished;
if (firstCommandBlock is not null) await firstCommandBlock.RemoveCommand(command);
}
private async Task RefreshCommands()
@@ -125,7 +132,7 @@ public class CommandScheduler : ICommandScheduler
foreach (var batch in _commandsToRun.Items)
{
currentTime = await batch.RefreshResult(currentTime);
currentTime = await batch.SetStartTimeAsync(currentTime);
}
}

View File

@@ -5,7 +5,7 @@ namespace FileTime.Core.Timeline;
public class LocalCommandExecutor : ILocalCommandExecutor
{
private readonly ICommandRunner _commandRunner;
public event EventHandler<ICommand> CommandFinished;
public event EventHandler<ICommand>? CommandFinished;
public LocalCommandExecutor(ICommandRunner commandRunner)
{
@@ -15,7 +15,7 @@ public class LocalCommandExecutor : ILocalCommandExecutor
public void ExecuteCommand(ICommand command)
{
var context = new CommandRunnerContext(command);
var thread = new Thread(new ParameterizedThreadStart(RunCommand));
var thread = new Thread(RunCommand);
thread.Start(context);
}
@@ -29,6 +29,6 @@ public class LocalCommandExecutor : ILocalCommandExecutor
}
catch (Exception ex) { }
CommandFinished.Invoke(this, context.Command);
CommandFinished?.Invoke(this, context.Command);
}
}