Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions OneWare.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
<Project Path="tests/OneWare.Essentials.UnitTests/OneWare.Essentials.UnitTests.csproj" />
<Project Path="tests/OneWare.Studio.Desktop.UnitTests/OneWare.Studio.Desktop.UnitTests.csproj" />
<Project Path="tests/OneWare.TestPlugin/OneWare.TestPlugin.csproj" />
<Project Path="tests/OneWare.ToolEngine.UnitTests/OneWare.ToolEngine.UnitTests.csproj" />
<Project Path="tests/OneWare.UniversalFpgaProjectSystem.Tests/OneWare.UniversalFpgaProjectSystem.Tests.csproj" />
<Project Path="tests/OneWare.Vcd.Parser.UnitTests/OneWare.Vcd.Parser.UnitTests.csproj" />
<Project Path="tests/OneWare.Vhdl.UnitTests/OneWare.Vhdl.UnitTests.csproj" />
Expand Down
153 changes: 153 additions & 0 deletions src/OneWare.Essentials/Services/IToolCommandBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
using OneWare.Essentials.Enums;
using OneWare.Essentials.ToolEngine;

namespace OneWare.Essentials.Services;

/// <summary>
/// A fluent builder for creating <see cref="ToolCommand"/> instances.
/// Supports cross-platform path handling, scripting placeholders, and container/networking configurations.
/// </summary>
public interface IToolCommandBuilder
{
/// <summary>
/// Sets the path to the executable file.
/// If not explicitly set, the ToolName provided during initialization will be used as the executable.
/// </summary>
IToolCommandBuilder WithExecutable(string path);

/// <summary>
/// Sets the working directory for the tool execution. Defaults to "."
/// </summary>
IToolCommandBuilder WithWorkingDirectory(string dir);

/// <summary>
/// Defines the status message and application state to be displayed in the UI during execution.
/// </summary>
IToolCommandBuilder WithStatus(string status, AppState state = AppState.Loading);

/// <summary>
/// Determines whether a timer should be displayed in the UI during the tool's execution.
/// </summary>
IToolCommandBuilder WithTimer(bool show);

/// <summary>
/// Registers a handler for the standard output stream (stdout).
/// </summary>
IToolCommandBuilder WithOutputHandler(Func<string, bool>? handler);

/// <summary>
/// Registers a handler for the error output stream (stderr).
/// </summary>
IToolCommandBuilder WithErrorHandler(Func<string, bool>? handler);

/// <summary>
/// Adds a simple string literal as a command-line argument.
/// </summary>
IToolCommandBuilder Add(string literal);

/// <summary>
/// Adds multiple string literals as command-line arguments.
/// </summary>
IToolCommandBuilder Add(params string[] literals);

/// <summary>
/// Adds a collection of string literals as command-line arguments.
/// </summary>
IToolCommandBuilder AddRange(IEnumerable<string> literals);

/// <summary>
/// Adds an argument only if the specified condition is met.
/// </summary>
IToolCommandBuilder AddIf(bool condition, string literal);

/// <summary>
/// Adds an argument only if the provided string is not null or whitespace.
/// </summary>
IToolCommandBuilder AddIfNotNull(string? literal);

/// <summary>
/// Adds a file or directory path as an argument.
/// The path will be normalized according to the target OS (Windows/Linux) during preparation.
/// </summary>
IToolCommandBuilder AddPath(string path);

/// <summary>
/// Adds a collection of paths as arguments, ensuring OS-specific normalization for each.
/// </summary>
IToolCommandBuilder AddPaths(IEnumerable<string> paths);

/// <summary>
/// Adds an option consisting of a flag and a value (e.g., "-o", "output.bin").
/// </summary>
IToolCommandBuilder AddOption(string flag, string value);

/// <summary>
/// Adds an option consisting of a flag and a value only if the value is not null or whitespace.
/// </summary>
IToolCommandBuilder AddOptionIfNotNull(string flag, string? value);

/// <summary>
/// Adds an option consisting of a flag and a path. The path will be normalized.
/// </summary>
IToolCommandBuilder AddPathOption(string flag, string path);

/// <summary>
/// Adds an option consisting of a flag and a path only if the path is not null or whitespace.
/// </summary>
IToolCommandBuilder AddPathOptionIfNotNull(string flag, string? path);

/// <summary>
/// Adds a complex command string using template placeholders.
/// Placeholders are treated as simple literals.
/// </summary>
IToolCommandBuilder AddScript(string template, params (string placeholder, string value)[] literals);

/// <summary>
/// Adds a complex command string using template placeholders.
/// Allows placeholders to be explicitly marked as paths for OS-specific normalization.
/// </summary>
IToolCommandBuilder AddScript(string template, params (string placeholder, string value, bool isPath)[] mappings);

/// <summary>
/// Parses a raw string (e.g., from user settings) into individual arguments, respecting quotes.
/// </summary>
IToolCommandBuilder AddRawArguments(string? rawArgs);

/// <summary>
/// Looks up a path in a dictionary by its key and adds it as a normalized path argument.
/// </summary>
IToolCommandBuilder AddPathFromMap<TKey>(TKey key, IDictionary<TKey, string> map) where TKey : notnull;

/// <summary>
/// Sets an environment variable for the tool process.
/// </summary>
IToolCommandBuilder WithEnvironmentVariable(string key, string value);

/// <summary>
/// Adds a collection of environment variables for the tool process.
/// </summary>
IToolCommandBuilder WithEnvironmentVariables(IDictionary<string, string> variables);

/// <summary>
/// Sets an environment variable only if the specified condition is met.
/// </summary>
IToolCommandBuilder WithEnvironmentVariableIf(bool condition, string key, string value);

/// <summary>
/// Declares a network port that the tool listens on internally.
/// Used for firewall configuration or container port exposure.
/// </summary>
IToolCommandBuilder WithExposedPort(int port, string protocol = "TCP");

/// <summary>
/// Defines a network port mapping.
/// Native runners typically use the guestPort directly, while container runners map the hostPort to the guestPort.
/// </summary>
IToolCommandBuilder AddPortMapping(int hostPort, int guestPort, string protocol = "TCP");

/// <summary>
/// Builds the final <see cref="ToolCommand"/> instance.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if neither ToolName nor Executable is set.</exception>
ToolCommand Build();
}
17 changes: 17 additions & 0 deletions src/OneWare.Essentials/Services/IToolExecutionDispatcherService.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics;
using OneWare.Essentials.ToolEngine;

namespace OneWare.Essentials.Services;
Expand All @@ -8,4 +9,20 @@ public interface IToolExecutionDispatcherService
/// Executes a tool command using the configured execution strategy.
/// </summary>
public Task<(bool success, string output)> ExecuteAsync(ToolCommand command);

/// <summary>
/// Starts the tool as a background process without waiting for its completion or capturing its output.
/// </summary>
/// <param name="command">The run configuration for the strategy.</param>
/// <returns>A <see cref="WeakReference{Process}"/> to the started process, allowing it to be garbage collected if no other references exist.</returns>
public WeakReference<Process> StartWeakProcess(ToolCommand command);

/// <summary>
/// Creates a new instance of <see cref="IToolCommandBuilder"/> for a specific tool.
/// This is the entry point for configuring a tool command with specific arguments, environment variables, and mappings.
/// </summary>
/// <param name="toolName">The name of the tool to be executed (e.g., "yosys", "gcc").
/// Used for logging and identifying the executable if no path is provided.</param>
/// <returns>A fluent builder instance to configure the command.</returns>
public IToolCommandBuilder CreateToolCommandBuilder(string toolName);
}
9 changes: 9 additions & 0 deletions src/OneWare.Essentials/Services/IToolExecutionStrategy.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics;
using OneWare.Essentials.ToolEngine;

namespace OneWare.Essentials.Services;
Expand All @@ -10,6 +11,14 @@ public interface IToolExecutionStrategy
/// <param name="command">The run configuration for the strategy</param>
/// <returns></returns>
Task<(bool success, string output)> ExecuteAsync(ToolCommand command);


/// <summary>
/// Starts the tool as a background process without waiting for its completion or capturing its output.
/// </summary>
/// <param name="command">The run configuration for the strategy.</param>
/// <returns>A <see cref="WeakReference{Process}"/> to the started process, allowing it to be garbage collected if no other references exist.</returns>
public WeakReference<Process> StartWeakProcess(ToolCommand command);

/// <summary>
/// Returns the display name for a strategy.
Expand Down
10 changes: 10 additions & 0 deletions src/OneWare.Essentials/ToolEngine/ICommandArgument.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Runtime.InteropServices;

namespace OneWare.Essentials.ToolEngine;

public interface ICommandArgument
{
string GetArgument();

void Prepare(OSPlatform osPlatform, Func<string, string>? pathMapper = null);
}
12 changes: 10 additions & 2 deletions src/OneWare.Essentials/ToolEngine/Strategies/NativeStrategy.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
using System.Diagnostics;
using OneWare.Essentials.Models;
using OneWare.Essentials.Services;

namespace OneWare.Essentials.ToolEngine.Strategies;

public class NativeStrategy : IToolExecutionStrategy
{
private const string ToolKey = "NativeExecutionStrategy";
public const string ToolKey = "NativeExecutionStrategy";

public Task<(bool success, string output)> ExecuteAsync(ToolCommand command)
{
IChildProcessService childProcessService = ContainerLocator.Container.Resolve<IChildProcessService>();
var childProcessService = ContainerLocator.Container.Resolve<IChildProcessService>();
return childProcessService.ExecuteShellAsync(command.Executable ?? command.ToolName, command.Arguments, command.WorkingDirectory, command.StatusMessage,
command.State,
command.ShowTimer, command.OutputHandler, command.ErrorHandler);
}

public WeakReference<Process> StartWeakProcess(ToolCommand command)
{
var childProcessService = ContainerLocator.Container.Resolve<IChildProcessService>();
return childProcessService.StartWeakProcess(command.Executable ?? command.ToolName, command.Arguments,
command.WorkingDirectory);
}

public string GetStrategyName()
{
return "Native Execution Strategy";
Expand Down
67 changes: 34 additions & 33 deletions src/OneWare.Essentials/ToolEngine/ToolCommand.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,41 @@
using System.Runtime.InteropServices;
using OneWare.Essentials.Enums;
using OneWare.Essentials.Services;
using OneWare.Essentials.ToolEngine;

namespace OneWare.Essentials.ToolEngine;

public class ToolCommand
{
public required string ToolName { get; init; }
public string? Executable { get; init; }
public IReadOnlyCollection<string> Arguments { get; init; } = [];

public IReadOnlyCollection<ToolPort> ExposedPorts { get; init; } = Array.Empty<ToolPort>();

public IReadOnlyCollection<ToolPortMapping> PortMappings { get; init; } = Array.Empty<ToolPortMapping>();

public IReadOnlyCollection<string> Arguments =>
CommandArguments.Select(x => x.GetArgument()).ToList().AsReadOnly();

public required IReadOnlyCollection<ICommandArgument> CommandArguments { get; init; }
public string WorkingDirectory { get; init; } = ".";
public string StatusMessage { get; init; } = "Running tool...";
public AppState State { get; init; } = AppState.Loading;
public bool ShowTimer { get; init; }

public IReadOnlyDictionary<string, string> EnvironmentVariables { get; init; } = new Dictionary<string, string>();

public Func<string, bool>? OutputHandler { get; init; }
public Func<string, bool>? ErrorHandler { get; init; }

public void PrepareCommand(OSPlatform osPlatform, Func<string, string>? pathMapper = null)
{
foreach (var argument in CommandArguments)
{
argument.Prepare(osPlatform, pathMapper);
}
}
[Obsolete("Use IToolExecutionDispatcherService.CreateToolCommandBuilder instead.")]
public static ToolCommand FromShellParams(
string path,
IReadOnlyCollection<string> arguments,
Expand All @@ -25,39 +46,19 @@ public static ToolCommand FromShellParams(
Func<string, bool>? outputAction = null,
Func<string, bool>? errorAction = null)
{
return new ToolCommand
{
ToolName = Path.GetFileNameWithoutExtension(path),
Executable = path,
Arguments = arguments,
WorkingDirectory = workingDirectory,
StatusMessage = status,
State = state,
ShowTimer = showTimer,
OutputHandler = outputAction,
ErrorHandler = errorAction
};
return ContainerLocator.Current.Resolve<IToolExecutionDispatcherService>().
CreateToolCommandBuilder(Path.GetFileNameWithoutExtension(path))
.WithExecutable(path)
.WithWorkingDirectory(workingDirectory)
.WithStatus(status, state)
.WithOutputHandler(outputAction)
.WithErrorHandler(errorAction)
.WithTimer(showTimer)
.AddRange(arguments)
.Build();
}
}

public class ToolContext
{
public ToolContext(string name, string description, string key, List<string>? toolNames = null)
{
Name = name;
Description = description;
Key = key;
ToolNames = toolNames ?? [];
}

public string Name { get; init; }
public string Description { get; init; }
public string Key { get; init; }
public record ToolPort(int Number, string Protocol = "TCP");

public List<string> ToolNames { get; init; }
}

public class ToolConfiguration
{
public readonly Dictionary<string, string> StrategyMapping = new();
}
public record ToolPortMapping(ToolPort Host, ToolPort Guest);
6 changes: 6 additions & 0 deletions src/OneWare.Essentials/ToolEngine/ToolConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace OneWare.Essentials.ToolEngine;

public class ToolConfiguration
{
public readonly Dictionary<string, string> StrategyMapping = new();
}
10 changes: 10 additions & 0 deletions src/OneWare.Essentials/ToolEngine/ToolContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace OneWare.Essentials.ToolEngine;

public class ToolContext(string name, string description, string key, List<string>? toolNames = null)
{
public string Name { get; init; } = name;
public string Description { get; init; } = description;
public string Key { get; init; } = key;

public List<string> ToolNames { get; init; } = toolNames ?? [];
}
Loading
Loading