From a9b7d8ea885374f66db67877f2fdbc65bbaf5663 Mon Sep 17 00:00:00 2001 From: Cezary Piatek Date: Sun, 1 Sep 2024 10:19:30 +0200 Subject: [PATCH] Add option to automatically generate parameter value using external script --- schema/v1/ScriptRunnerSchema.json | 3 + .../DependencyInjectionExtensions.cs | 2 - .../ScriptRunner.GUI/ParamsPanelFactory.cs | 31 +++++- .../ScriptConfigs/ScriptConfig.cs | 1 + .../ScriptReader/ScriptConfigReader.cs | 46 ++++----- .../ViewModels/MainWindowViewModel.cs | 96 ++++++++++++------- .../ViewModels/RunningJobViewModel.cs | 21 +++- .../Views/RunningJobsSection.axaml | 3 +- 8 files changed, 130 insertions(+), 73 deletions(-) diff --git a/schema/v1/ScriptRunnerSchema.json b/schema/v1/ScriptRunnerSchema.json index 3a9f662..98c7423 100644 --- a/schema/v1/ScriptRunnerSchema.json +++ b/schema/v1/ScriptRunnerSchema.json @@ -95,6 +95,9 @@ "autoParameterBuilderPattern":{ "type": "string" }, + "valueGeneratorCommand":{ + "type": "string" + }, "prompt": { "type":"string", "enum": [ diff --git a/src/ScriptRunner/ScriptRunner.GUI/Infrastructure/DependencyInjectionExtensions.cs b/src/ScriptRunner/ScriptRunner.GUI/Infrastructure/DependencyInjectionExtensions.cs index 05ea95d..ab00a44 100644 --- a/src/ScriptRunner/ScriptRunner.GUI/Infrastructure/DependencyInjectionExtensions.cs +++ b/src/ScriptRunner/ScriptRunner.GUI/Infrastructure/DependencyInjectionExtensions.cs @@ -1,7 +1,5 @@ using System; using Avalonia; -using Avalonia.Controls; -using Avalonia.Platform; using Microsoft.Extensions.DependencyInjection; using Splat.Microsoft.Extensions.DependencyInjection; diff --git a/src/ScriptRunner/ScriptRunner.GUI/ParamsPanelFactory.cs b/src/ScriptRunner/ScriptRunner.GUI/ParamsPanelFactory.cs index 702966e..04dd76a 100644 --- a/src/ScriptRunner/ScriptRunner.GUI/ParamsPanelFactory.cs +++ b/src/ScriptRunner/ScriptRunner.GUI/ParamsPanelFactory.cs @@ -4,12 +4,15 @@ using System.IO; using System.Linq; using System.Text; +using System.Threading.Tasks; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Data; using Avalonia.Data.Converters; using Avalonia.Layout; using Avalonia.Media; +using Avalonia.Threading; +using Projektanker.Icons.Avalonia; using ScriptRunner.GUI.Infrastructure; using ScriptRunner.GUI.ScriptConfigs; using ScriptRunner.GUI.Settings; @@ -27,7 +30,7 @@ public ParamsPanelFactory(VaultProvider vaultProvider) _vaultProvider = vaultProvider; } - public ParamsPanel Create(ScriptConfig action, Dictionary values) + public ParamsPanel Create(ScriptConfig action, Dictionary values, Func> commandExecutor) { var paramsPanel = new StackPanel { @@ -68,7 +71,31 @@ public ParamsPanel Create(ScriptConfig action, Dictionary values "paramRow" } }; - + if (string.IsNullOrWhiteSpace(param.ValueGeneratorCommand) == false) + { + var generateButton = new Button() + { + Margin = new(5,0,5,0), + Width = 50, + VerticalAlignment = VerticalAlignment.Stretch, + HorizontalContentAlignment = HorizontalAlignment.Center + }; + generateButton.Click += async(sender, args) => + { + var result = await commandExecutor($"Generate parameter for '{param.Name}'", param.ValueGeneratorCommand); + Dispatcher.UIThread.Post(() => + { + //TODO: Handle other controls + if (controlRecord is { Control: TextBox tb }) + { + tb.Text = result?.Trim() ?? string.Empty; + } + }); + }; + Attached.SetIcon(generateButton, "fas fa-wand-magic-sparkles"); + ToolTip.SetTip(generateButton, "Auto fill"); + actionPanel.Children.Add(generateButton); + } paramsPanel.Children.Add(actionPanel); controlRecords.Add(controlRecord); } diff --git a/src/ScriptRunner/ScriptRunner.GUI/ScriptConfigs/ScriptConfig.cs b/src/ScriptRunner/ScriptRunner.GUI/ScriptConfigs/ScriptConfig.cs index d964667..9c2316d 100644 --- a/src/ScriptRunner/ScriptRunner.GUI/ScriptConfigs/ScriptConfig.cs +++ b/src/ScriptRunner/ScriptRunner.GUI/ScriptConfigs/ScriptConfig.cs @@ -66,6 +66,7 @@ public class ScriptParam public string Default { get; set; } public Dictionary PromptSettings { get; set; } = new(); public string? AutoParameterBuilderPattern { get; set; } + public string? ValueGeneratorCommand { get; set; } public bool GetPromptSettings(string name, [NotNullWhen(true)] out string? value) { diff --git a/src/ScriptRunner/ScriptRunner.GUI/ScriptReader/ScriptConfigReader.cs b/src/ScriptRunner/ScriptRunner.GUI/ScriptReader/ScriptConfigReader.cs index ae83904..858802f 100644 --- a/src/ScriptRunner/ScriptRunner.GUI/ScriptReader/ScriptConfigReader.cs +++ b/src/ScriptRunner/ScriptRunner.GUI/ScriptReader/ScriptConfigReader.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Data.SqlTypes; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text; @@ -126,10 +127,11 @@ private static IEnumerable LoadFileSource(string fileName, foreach (var action in scriptConfig.Actions) { action.Source = fileName; + var parameterBuilder = CreateBuilder(action); var actionDir = Path.GetDirectoryName(fileName); - + string ResolveAbsolutePath(string path) { if (string.IsNullOrWhiteSpace(path) == false) @@ -143,17 +145,20 @@ string ResolveAbsolutePath(string path) return path; } - if (string.IsNullOrWhiteSpace(action.Command) == false && (action.Command.StartsWith("."))) + [return:NotNullIfNotNull("command")] + string? AdjustCommandPath(string? command) { - var (commandPath, args) = MainWindowViewModel.SplitCommandAndArgs(action.Command); - action.Command = (ResolveAbsolutePath(commandPath) + " " + args).Trim(); + if (string.IsNullOrWhiteSpace(command) == false && (command.StartsWith("."))) + { + var (commandPath, args) = MainWindowViewModel.SplitCommandAndArgs(command); + return (ResolveAbsolutePath(commandPath) + " " + args).Trim(); + } + + return command; } - if (string.IsNullOrWhiteSpace(action.InstallCommand) == false && (action.Command.StartsWith("."))) - { - var (commandPath, args) = MainWindowViewModel.SplitCommandAndArgs(action.InstallCommand); - action.InstallCommand = (ResolveAbsolutePath(commandPath) + " " + args).Trim(); - } + action.Command = AdjustCommandPath(action.Command); + action.InstallCommand = AdjustCommandPath(action.InstallCommand); var autoGeneratedParameters = action.Params.Select(param => parameterBuilder.Build(param)).Where(paramString => string.IsNullOrWhiteSpace(paramString) == false); action.Command += " "+string.Join(" ", autoGeneratedParameters); @@ -169,23 +174,8 @@ string ResolveAbsolutePath(string path) } } - if (string.IsNullOrWhiteSpace(action.WorkingDirectory)) - { - action.WorkingDirectory = actionDir; - } - else - { - action.WorkingDirectory = ResolveAbsolutePath(action.WorkingDirectory); - } - - if (string.IsNullOrWhiteSpace(action.InstallCommandWorkingDirectory)) - { - action.InstallCommandWorkingDirectory = actionDir; - } - else - { - action.InstallCommandWorkingDirectory = ResolveAbsolutePath(action.InstallCommandWorkingDirectory); - } + action.WorkingDirectory = string.IsNullOrWhiteSpace(action.WorkingDirectory) ? actionDir : ResolveAbsolutePath(action.WorkingDirectory); + action.InstallCommandWorkingDirectory = string.IsNullOrWhiteSpace(action.InstallCommandWorkingDirectory) ? actionDir : ResolveAbsolutePath(action.InstallCommandWorkingDirectory); var defaultSet = new ArgumentSet() { @@ -194,6 +184,7 @@ string ResolveAbsolutePath(string path) foreach (var param in action.Params) { + param.ValueGeneratorCommand = AdjustCommandPath(param.ValueGeneratorCommand); defaultSet.Arguments[param.Name] = param.Default; } @@ -248,9 +239,6 @@ string ResolveAbsolutePath(string path) { if (set.Arguments.TryGetValue(param.Name, out var defaultValue)) { - - - set.Arguments[param.Name] = ResolveAbsolutePath(defaultValue); } } diff --git a/src/ScriptRunner/ScriptRunner.GUI/ViewModels/MainWindowViewModel.cs b/src/ScriptRunner/ScriptRunner.GUI/ViewModels/MainWindowViewModel.cs index 959741c..f205c40 100644 --- a/src/ScriptRunner/ScriptRunner.GUI/ViewModels/MainWindowViewModel.cs +++ b/src/ScriptRunner/ScriptRunner.GUI/ViewModels/MainWindowViewModel.cs @@ -425,7 +425,27 @@ private void RenderParameterForm(ScriptConfig action, Dictionary //var actionPanel = new StackPanel(); // Create IPanel with controls for all parameters - var paramsPanel = _paramsPanelFactory.Create(action, parameterValues); + var paramsPanel = _paramsPanelFactory.Create(action, parameterValues, (title, command) => + { + if (SelectedAction != null) + { + var taskCompletionSource = new TaskCompletionSource(); + try + { + ExecuteCommand(command, this.SelectedAction, title, s => + { + taskCompletionSource.SetResult(s); + }); + } + catch (Exception e) + { + taskCompletionSource.SetException(e); + } + return taskCompletionSource.Task; + } + + return Task.FromResult(""); + }); // Add panel with param controls to action panel //actionPanel.Children.Add(paramsPanel.Panel); @@ -470,8 +490,7 @@ public void InstallScript() var (commandPath, args) = SplitCommandAndArgs(installCommand); var job = new RunningJobViewModel { - Tile = "#" + jobCounter++, - CommandName = $"Install {selectedAction.Name}", + Tile = $"#{jobCounter++} Install {selectedAction.Name}", ExecutedCommand = installCommand, EnvironmentVariables = new Dictionary() }; @@ -745,46 +764,55 @@ public void RunScript() } - RegisterExecution(selectedAction); + AddExecutionAudit(selectedAction); - var (commandPath, args) = SplitCommandAndArgs(selectedAction.Command); - var maskedArgs = args; + var selectedActionCommand = selectedAction.Command; + + ExecuteCommand(selectedActionCommand, selectedAction); + // Some audit staff + var usedParams = HarvestCurrentParameters(vaultPrefixForNewEntries: $"{selectedAction.Name}_{Guid.NewGuid():N}"); + var executionLogAction = new ExecutionLogAction(DateTime.Now, selectedAction.SourceName, selectedAction.Name, usedParams); + ExecutionLog.Insert(0, executionLogAction); + SelectedRecentExecution = executionLogAction; + AppSettingsService.UpdateExecutionLog(ExecutionLog.ToList()); + } + + } - var envVariables = new Dictionary(selectedAction.EnvironmentVariables); + private void ExecuteCommand(string command, ScriptConfig selectedAction, string? title = null, Action? onComplete = null) + { + var (commandPath, args) = SplitCommandAndArgs(command); + var envVariables = new Dictionary(selectedAction.EnvironmentVariables); + var maskedArgs = args; + foreach (var controlRecord in _controlRecords) + { + var controlValue = controlRecord.GetFormattedValue(); + args = args.Replace($"{{{controlRecord.Name}}}", controlValue); + maskedArgs = maskedArgs.Replace($"{{{controlRecord.Name}}}", controlRecord.MaskingRequired? "*****": controlValue); - foreach (var controlRecord in _controlRecords) + foreach (var (key, val) in envVariables) { - // This is definitely not pretty, should be using some ReactiveUI observables to read values? - var controlValue = controlRecord.GetFormattedValue(); - args = args.Replace($"{{{controlRecord.Name}}}", controlValue); - maskedArgs = maskedArgs.Replace($"{{{controlRecord.Name}}}", controlRecord.MaskingRequired? "*****": controlValue); - - foreach (var (key, val) in envVariables) - { - if(string.IsNullOrWhiteSpace(val) == false) - envVariables[key] = val.Replace($"{{{controlRecord.Name}}}", controlValue); - } + if(string.IsNullOrWhiteSpace(val) == false) + envVariables[key] = val.Replace($"{{{controlRecord.Name}}}", controlValue); } + } - var job = new RunningJobViewModel - { - Tile = "#"+jobCounter++, - CommandName = selectedAction.Name, - ExecutedCommand = $"{commandPath} {maskedArgs}", - EnvironmentVariables = envVariables - }; - this.RunningJobs.Add(job); - SelectedRunningJob = job; - job.RunJob(commandPath, args, selectedAction.WorkingDirectory, selectedAction.InteractiveInputs, selectedAction.Troubleshooting); + var job = new RunningJobViewModel + { + Tile = $"#{jobCounter++} {title ?? selectedAction.Name}", + ExecutedCommand = $"{commandPath} {maskedArgs}", + EnvironmentVariables = envVariables + }; + this.RunningJobs.Add(job); + SelectedRunningJob = job; - var usedParams = HarvestCurrentParameters(vaultPrefixForNewEntries: $"{selectedAction.Name}_{Guid.NewGuid():N}"); - var executionLogAction = new ExecutionLogAction(DateTime.Now, selectedAction.SourceName, selectedAction.Name, usedParams); - ExecutionLog.Insert(0, executionLogAction); - SelectedRecentExecution = executionLogAction; - AppSettingsService.UpdateExecutionLog(ExecutionLog.ToList()); + if (onComplete != null) + { + job.ExecutionCompleted += (sender, args) => onComplete(job.RawOutput); } + job.RunJob(commandPath, args, selectedAction.WorkingDirectory, selectedAction.InteractiveInputs, selectedAction.Troubleshooting); } public ObservableCollection ExecutionLog { get; set; } = new (); @@ -806,7 +834,7 @@ public ExecutionLogAction SelectedRecentExecution - private void RegisterExecution(ScriptConfig selectedAction) + private void AddExecutionAudit(ScriptConfig selectedAction) { AppSettingsService.UpdateRecent(recent => { diff --git a/src/ScriptRunner/ScriptRunner.GUI/ViewModels/RunningJobViewModel.cs b/src/ScriptRunner/ScriptRunner.GUI/ViewModels/RunningJobViewModel.cs index ada5cb2..d504961 100644 --- a/src/ScriptRunner/ScriptRunner.GUI/ViewModels/RunningJobViewModel.cs +++ b/src/ScriptRunner/ScriptRunner.GUI/ViewModels/RunningJobViewModel.cs @@ -53,7 +53,6 @@ public RunningJobStatus Status set => this.RaiseAndSetIfChanged(ref _status, value); } - public string CommandName { get; set; } public string ExecutedCommand { get; set; } public void CancelExecution() => ExecutionCancellation.Cancel(); public void DismissTroubleshootingMessage() @@ -92,10 +91,12 @@ public void RunJob(string commandPath, string args, string? workingDirectory, _troubleshooting = troubleshooting; CurrentRunOutput = ""; ExecutionPending = true; + Task.Factory.StartNew(async () => { var stopWatch = new Stopwatch(); stopWatch.Start(); + var rawOutput = new StringBuilder(); try { await using var inputStream = new MultiplexerStream(); @@ -107,13 +108,16 @@ await Cli.Wrap(commandPath) //TODO: Working dir should be read from the config with the fallback set to the config file dir .WithWorkingDirectory(workingDirectory ?? "Scripts/") .WithStandardInputPipe(PipeSource.FromStream(inputStream,autoFlush:true)) - .WithStandardOutputPipe(PipeTarget.ToDelegate(s => AppendToOutput(s, ConsoleOutputLevel.Normal))) + .WithStandardOutputPipe(PipeTarget.ToDelegate(s => + { + rawOutput.Append(s); + AppendToOutput(s, ConsoleOutputLevel.Normal); + })) .WithStandardErrorPipe(PipeTarget.ToDelegate(s => AppendToOutput(s, ConsoleOutputLevel.Error))) .WithValidation(CommandResultValidation.None) .WithEnvironmentVariables(EnvironmentVariables) .ExecuteAsync(ExecutionCancellation.Token); ChangeStatus(RunningJobStatus.Finished); - Dispatcher.UIThread.Post(RaiseExecutionCompleted); } catch (Exception e) { @@ -136,7 +140,14 @@ await Cli.Wrap(commandPath) stopWatch.Stop(); AppendToOutput("---------------------------------------------", ConsoleOutputLevel.Normal); AppendToOutput($"Execution finished after {stopWatch.Elapsed}", ConsoleOutputLevel.Normal); - Dispatcher.UIThread.Post(() => { ExecutionPending = false; }); + + + Dispatcher.UIThread.Post(() => + { + ExecutionPending = false; + RawOutput = rawOutput.ToString(); + RaiseExecutionCompleted(); + }); _logForwarder.Finish(); } }, TaskCreationOptions.LongRunning); @@ -686,6 +697,8 @@ public bool ExecutionPending set => this.RaiseAndSetIfChanged(ref _executionPending, value); } + public string RawOutput { get; set; } + private int _outputIndex; private bool _executionPending; private StreamWriter? inputWriter; diff --git a/src/ScriptRunner/ScriptRunner.GUI/Views/RunningJobsSection.axaml b/src/ScriptRunner/ScriptRunner.GUI/Views/RunningJobsSection.axaml index ed86bd6..4fbd22f 100644 --- a/src/ScriptRunner/ScriptRunner.GUI/Views/RunningJobsSection.axaml +++ b/src/ScriptRunner/ScriptRunner.GUI/Views/RunningJobsSection.axaml @@ -16,8 +16,7 @@ - - +