Command execution, CreateContainer command WIP

This commit is contained in:
2022-05-23 18:12:22 +02:00
parent 6b3a8f7127
commit d4bd9d3ba1
29 changed files with 499 additions and 26 deletions

View File

@@ -0,0 +1,8 @@
namespace FileTime.Core.Command;
public enum ExecutionState
{
Waiting,
Running,
Finished
}

View File

@@ -5,5 +5,5 @@ namespace FileTime.Core.Command;
public interface ICommand
{
Task<CanCommandRun> CanRun(PointInTime currentTime);
Task<PointInTime?> SimulateCommand(PointInTime? currentTime);
Task<PointInTime> SimulateCommand(PointInTime currentTime);
}

View File

@@ -0,0 +1,9 @@
using FileTime.Core.Timeline;
namespace FileTime.Core.Command;
public interface ICommandHandler
{
bool CanHandle(ICommand command);
Task ExecuteAsync(ICommand command);
}

View File

@@ -0,0 +1,8 @@
using FileTime.Core.Timeline;
namespace FileTime.Core.Command;
public interface ICommandRunner
{
Task RunCommandAsync(ICommand command);
}

View File

@@ -4,5 +4,5 @@ namespace FileTime.Core.Command;
public interface IExecutableCommand : ICommand
{
Task Execute(ICommandScheduler commandScheduler);
Task Execute();
}

View File

@@ -0,0 +1,73 @@
using System.Reactive.Linq;
using DynamicData;
using FileTime.Core.Models;
namespace FileTime.Core.Extensions;
public static class DynamicDataExtensions
{
private class DisposableContext<TParam, TTaskResult>
{
private readonly Func<TParam, TTaskResult> _transformResult;
private readonly TaskCompletionSource<TTaskResult?> _taskCompletionSource;
public IDisposable? Disposable { get; set; }
public DisposableContext(Func<TParam, TTaskResult> transformResult,
TaskCompletionSource<TTaskResult?> taskCompletionSource, IDisposable? disposable = null)
{
_transformResult = transformResult;
_taskCompletionSource = taskCompletionSource;
Disposable = disposable;
}
public void OnNext(TParam param)
{
Disposable?.Dispose();
var result = _transformResult(param);
_taskCompletionSource.SetResult(result);
}
public void OnError(Exception ex)
{
Disposable?.Dispose();
_taskCompletionSource.SetException(ex);
}
public void OnCompleted()
{
Disposable?.Dispose();
_taskCompletionSource.SetResult(default(TTaskResult));
}
}
public static async Task<IEnumerable<AbsolutePath>?> GetItemsAsync(
this IObservable<IObservable<IChangeSet<AbsolutePath>>?> stream)
=> await GetItemsAsync(stream
.Select(s =>
s is null
? new SourceList<AbsolutePath>().Connect().StartWithEmpty().ToCollection()
: s.ToCollection())
.Switch());
public static async Task<IEnumerable<AbsolutePath>?> GetItemsAsync(
this IObservable<IChangeSet<AbsolutePath>> stream)
=> await GetItemsAsync(stream.ToCollection());
private static Task<IEnumerable<AbsolutePath>?> GetItemsAsync(
this IObservable<IReadOnlyCollection<AbsolutePath>> stream)
{
var taskCompletionSource = new TaskCompletionSource<IEnumerable<AbsolutePath>?>();
var context = new DisposableContext<IReadOnlyCollection<AbsolutePath>, IEnumerable<AbsolutePath>?>(r => r,
taskCompletionSource);
var disposable = stream
.Subscribe(
context.OnNext,
context.OnError,
context.OnCompleted
);
context.Disposable = disposable;
return taskCompletionSource.Task;
}
}

View File

@@ -24,4 +24,8 @@
<ProjectReference Include="..\..\Library\InitableService\InitableService.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="ContentAccess" />
</ItemGroup>
</Project>

View File

@@ -4,8 +4,6 @@ public record FullName(string Path)
{
public FullName? GetParent()
{
if (Path is null) return null;
var pathParts = Path.TrimEnd(Constants.SeparatorChar).Split(Constants.SeparatorChar);
return pathParts.Length switch
{
@@ -13,4 +11,10 @@ public record FullName(string Path)
_ => null
};
}
public string GetName()
=> Path.Split(Constants.SeparatorChar).Last();
public FullName GetChild(string childName)
=> new FullName(Path + Constants.SeparatorChar + childName);
}

View File

@@ -4,7 +4,7 @@ using InitableService;
namespace FileTime.Core.Services;
public interface ITab : IInitable<IContainer>
public interface ITab : IInitable<IContainer>, IDisposable
{
IObservable<IContainer?> CurrentLocation { get; }
IObservable<AbsolutePath?> CurrentSelectedItem { get; }

View File

@@ -7,6 +7,7 @@ public class CommandTimeState
public ICommand Command { get; }
public CanCommandRun CanRun { get; private set; } = CanCommandRun.False;
public bool ForceRun { get; set; }
public ExecutionState ExecutionState { get; set; }
public CommandTimeState(ICommand command, PointInTime? startTime)
{

View File

@@ -0,0 +1,9 @@
using FileTime.Core.Command;
namespace FileTime.Core.Timeline;
public interface ICommandExecutor
{
void ExecuteCommand(ICommand command);
event EventHandler<ICommand> CommandFinished;
}

View File

@@ -1,6 +1,8 @@
using FileTime.Core.Command;
namespace FileTime.Core.Timeline;
public interface ICommandScheduler
{
Task AddCommand(ICommand command, int? batchId = null, bool toNewBatch = false);
}

View File

@@ -0,0 +1,6 @@
namespace FileTime.Core.Timeline;
public interface ILocalCommandExecutor : ICommandExecutor
{
}

View File

@@ -5,7 +5,7 @@ namespace FileTime.Core.Timeline;
public class ParallelCommands
{
private static ushort _idCounter;
public List<CommandTimeState> _commands;
private List<CommandTimeState> _commands;
public ushort Id { get; }
public IReadOnlyList<CommandTimeState> Commands { get; }
public PointInTime? Result { get; private set; }

View File

@@ -2,10 +2,11 @@ namespace FileTime.Core.Timeline;
public class PointInTime
{
private readonly List<Difference> _differences;
public static readonly PointInTime Eternal = new PointInTime();
public static readonly PointInTime Present = new PointInTime();
private readonly List<Difference> _differences;
public IReadOnlyList<Difference> Differences { get; }
private PointInTime() : this(new List<Difference>())
@@ -26,6 +27,14 @@ public class PointInTime
public PointInTime WithDifferences(IEnumerable<Difference> differences) =>
new(this, differences);
public PointInTime WithDifferences(Func<PointInTime, IEnumerable<Difference>> differenceGenerator)
{
var newPointInTime = new PointInTime();
newPointInTime._differences.AddRange(differenceGenerator(newPointInTime));
return newPointInTime;
}
private static List<Difference> MergeDifferences(IEnumerable<Difference> previouses,
IEnumerable<Difference> differences)
{

View File

@@ -0,0 +1,37 @@
using Microsoft.Extensions.Logging;
namespace FileTime.Core.Command;
public class CommandRunner : ICommandRunner
{
private readonly List<ICommandHandler> _commandHandlers;
private readonly ILogger<CommandRunner> _logger;
public CommandRunner(
IEnumerable<ICommandHandler> commandHandlers,
ILogger<CommandRunner> logger)
{
_commandHandlers = commandHandlers.ToList();
_logger = logger;
}
public async Task RunCommandAsync(ICommand command)
{
if (command is IExecutableCommand executableCommand)
{
await executableCommand.Execute();
}
else
{
var commandHandler = _commandHandlers.Find(c => c.CanHandle(command));
if (commandHandler != null)
{
await commandHandler.ExecuteAsync(command);
}
else
{
_logger.LogError("No command handler for command {Command}", command.GetType().Name);
}
}
}
}

View File

@@ -9,7 +9,7 @@ public class CopyCommand : ITransportationCommand
throw new NotImplementedException();
}
public Task<PointInTime?> SimulateCommand(PointInTime? currentTime)
public Task<PointInTime> SimulateCommand(PointInTime currentTime)
{
throw new NotImplementedException();
}

View File

@@ -1,21 +1,76 @@
using FileTime.Core.Enums;
using FileTime.Core.Extensions;
using FileTime.Core.Models;
using FileTime.Core.Timeline;
namespace FileTime.Core.Command.CreateContainer;
public class CreateContainerCommand : IExecutableCommand
{
public Task<CanCommandRun> CanRun(PointInTime currentTime)
private readonly ITimelessContentProvider _timelessContentProvider;
public FullName Parent { get; }
public string NewContainerName { get; }
public CreateContainerCommand(
FullName parent,
string newContainerName,
ITimelessContentProvider timelessContentProvider)
{
throw new NotImplementedException();
_timelessContentProvider = timelessContentProvider;
Parent = parent;
NewContainerName = newContainerName;
}
public Task<PointInTime?> SimulateCommand(PointInTime? currentTime)
public async Task<CanCommandRun> CanRun(PointInTime currentTime)
{
throw new NotImplementedException();
try
{
var parent = await ResolveParentAsync();
if (parent is not IContainer parentContainer) return CanCommandRun.False;
var items = await parentContainer.Items.GetItemsAsync();
if (items is null) return CanCommandRun.Forcable;
var existingItem = items.FirstOrDefault(i => i.Path.GetName() == NewContainerName);
return existingItem switch
{
null => CanCommandRun.True,
{ Type: AbsolutePathType.Container } => CanCommandRun.Forcable,
_ => CanCommandRun.False
};
}
catch
{
}
return CanCommandRun.False;
}
public Task Execute(ICommandScheduler commandScheduler)
public Task<PointInTime> SimulateCommand(PointInTime currentTime)
{
throw new NotImplementedException();
return Task.FromResult(
currentTime.WithDifferences(newPointInTime =>
new List<Difference>()
{
new(
DifferenceActionType.Create,
new AbsolutePath(_timelessContentProvider,
newPointInTime,
Parent.GetChild(NewContainerName),
AbsolutePathType.Container
)
)
}
)
);
}
public Task Execute()
{
return Task.CompletedTask;
}
private async Task<IItem> ResolveParentAsync()
=> await _timelessContentProvider.GetItemByFullNameAsync(Parent, PointInTime.Present);
}

View File

@@ -4,6 +4,10 @@
<ProjectReference Include="..\FileTime.Core.Abstraction\FileTime.Core.Abstraction.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
</ItemGroup>
<PropertyGroup>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>

View File

@@ -109,4 +109,11 @@ public class Tab : ITab
if (resolvedSelectedItem is not IContainer resolvedContainer) return;
SetCurrentLocation(resolvedContainer);
}
public void Dispose()
{
_currentLocation.Dispose();
_currentSelectedItem.Dispose();
_itemFilters.Dispose();
}
}

View File

@@ -0,0 +1,13 @@
using FileTime.Core.Command;
namespace FileTime.Core.Timeline;
public class CommandRunnerContext
{
public ICommand Command { get; }
public CommandRunnerContext(ICommand command)
{
Command = command;
}
}

View File

@@ -1,6 +1,165 @@
using DynamicData;
using FileTime.Core.Command;
using Microsoft.Extensions.DependencyInjection;
namespace FileTime.Core.Timeline;
public class CommandScheduler : ICommandScheduler
{
private readonly IServiceProvider _serviceProvider;
private readonly SourceList<ParallelCommands> _commandsToRun = new();
private readonly List<ICommandExecutor> _commandExecutors = new();
private readonly object _guard = new();
private bool _enableRunning = true;
private bool _resourceIsInUse;
public bool EnableRunning
{
get
{
bool result = true;
RunWithLock(() => result = _enableRunning);
return result;
}
set { RunWithLock(() => _enableRunning = value); }
}
public CommandScheduler(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
var localExecutor = serviceProvider.GetRequiredService<ILocalCommandExecutor>();
localExecutor.CommandFinished += LocalExecutorOnCommandFinished;
_commandExecutors.Add(localExecutor);
}
public async Task AddCommand(ICommand command, int? batchId = null, bool toNewBatch = false)
{
await RunWithLockAsync(async () =>
{
ParallelCommands batchToAdd;
if (_commandsToRun.Count == 0)
{
batchToAdd = new ParallelCommands(PointInTime.CreateEmpty());
_commandsToRun.Add(batchToAdd);
}
else if (toNewBatch)
{
batchToAdd = new ParallelCommands(_commandsToRun.Items.Last().Result);
_commandsToRun.Add(batchToAdd);
}
else if (batchId != null && _commandsToRun.Items.First(b => b.Id == batchId) is { } parallelCommands)
{
batchToAdd = parallelCommands;
}
else
{
batchToAdd = _commandsToRun.Items.First();
}
await batchToAdd.AddCommand(command);
await RefreshCommands();
ExecuteCommands();
/*if (_commandRunners.Count == 0)
{
StartCommandRunner();
}
await UpdateReadOnlyCommands();*/
});
}
private void ExecuteCommands()
{
if (!_enableRunning) return;
var parallelCommandsToExecute = _commandsToRun.Items.FirstOrDefault();
if (parallelCommandsToExecute is null ||
parallelCommandsToExecute.Commands.All(c => c.ExecutionState != ExecutionState.Waiting)) return;
var commandsToExecute = parallelCommandsToExecute.Commands;
foreach (var commandToExecute in commandsToExecute)
{
if (commandToExecute.ExecutionState != ExecutionState.Waiting) continue;
var commandExecutor = GetCommandExecutor();
commandExecutor.ExecuteCommand(commandToExecute.Command);
commandToExecute.ExecutionState = ExecutionState.Running;
}
}
private ICommandExecutor GetCommandExecutor()
{
//TODO
return _commandExecutors[0];
}
private void LocalExecutorOnCommandFinished(object? sender, ICommand command)
{
var parallelCommandsToExecute = _commandsToRun.Items.FirstOrDefault();
if (parallelCommandsToExecute is null) return;
var state = parallelCommandsToExecute.Commands.FirstOrDefault(c => c.Command == command);
if (state is null) return;
state.ExecutionState = ExecutionState.Finished;
}
private async Task RefreshCommands()
{
var currentTime = PointInTime.CreateEmpty();
foreach (var batch in _commandsToRun.Items)
{
currentTime = await batch.RefreshResult(currentTime);
}
}
private void RunWithLock(Action action) => Task.Run(async () => await RunWithLockAsync(action)).Wait();
private async Task RunWithLockAsync(Action action)
{
await RunWithLockAsync(() =>
{
action();
return Task.CompletedTask;
});
}
private async Task RunWithLockAsync(Func<Task> func)
{
while (true)
{
lock (_guard)
{
if (!_resourceIsInUse)
{
_resourceIsInUse = true;
break;
}
}
await Task.Delay(1);
}
try
{
await func();
}
finally
{
lock (_guard)
{
_resourceIsInUse = false;
}
}
}
}

View File

@@ -0,0 +1,34 @@
using FileTime.Core.Command;
namespace FileTime.Core.Timeline;
public class LocalCommandExecutor : ILocalCommandExecutor
{
private readonly ICommandRunner _commandRunner;
public event EventHandler<ICommand> CommandFinished;
public LocalCommandExecutor(ICommandRunner commandRunner)
{
_commandRunner = commandRunner;
}
public void ExecuteCommand(ICommand command)
{
var context = new CommandRunnerContext(command);
var thread = new Thread(new ParameterizedThreadStart(RunCommand));
thread.Start(context);
}
private async void RunCommand(object? contextObj)
{
if (contextObj is not CommandRunnerContext context)
throw new ArgumentException($"Parameter must be of type {typeof(CommandRunnerContext)}");
try
{
await _commandRunner.RunCommandAsync(context.Command);
}
catch(Exception ex){}
CommandFinished.Invoke(this, context.Command);
}
}