Files
PSInfisicalAPI/src/PSInfisicalAPI/Cmdlets/StartInfisicalProcessCmdlet.cs
T
GraceSolutions 207e7429e4 feat(process): add Start-InfisicalProcess with event-based capture and friendly TimeSpan logging
- New cmdlet Start-InfisicalProcess: launches a child process with InfisicalSecret
  objects decrypted directly into ProcessStartInfo.Environment (optional -Prefix),
  additional -EnvironmentVariables, stdout/stderr capture, -AcceptableExitCodeList,
  -ParsingExpression regex parsing, -ExecutionTimeout / -ExecutionTimeoutInterval,
  -NoWait, -WindowStyle / -CreateNoWindow parameter sets, -Priority,
  -StandardInputObjectList, -SecureArgumentList, -LogOutput, -ContinueOnError, and
  ShouldProcess support. Secret plaintext is never written to user or machine scope.
- Stream capture uses event-based OutputDataReceived/ErrorDataReceived with
  BeginOutputReadLine/BeginErrorReadLine (no Task / ReadToEndAsync /
  GetAwaiter().GetResult()) to avoid PowerShell SynchronizationContext deadlocks.
- Restored the do { log; sleep } while (!HasExited) polling pattern using
  Thread.Sleep(pollInterval) so verbose "has been running for X" / "Checking again
  in Y" messages fire at the configured cadence even when no -ExecutionTimeout is
  supplied.
- TimeSpan values in verbose logs and on the result now use a friendly format
  ("7 seconds, and 364 milliseconds", "1 minute, and 30 seconds", "N/A" when zero)
  matching the legacy Start-ProcessWithOutput GetTimeSpanMessage scriptblock.
- Added DurationFriendly property to InfisicalProcessResult and a "The command
  execution took X" verbose line at completion.
- build.ps1 CmdletsToExport and Test-ModuleImports expected list contain 42 cmdlets.
- Added 9 xUnit tests covering FormatFriendly singular/plural, multi-unit joining,
  zero, sub-millisecond, and skip-zero-components behavior.
2026-06-06 18:29:30 -04:00

160 lines
5.9 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Management.Automation;
using System.Text.RegularExpressions;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Process;
namespace PSInfisicalAPI.Cmdlets
{
[Cmdlet(VerbsLifecycle.Start, "InfisicalProcess", DefaultParameterSetName = WindowStyleSet, SupportsShouldProcess = true)]
[OutputType(typeof(InfisicalProcessResult))]
public sealed class StartInfisicalProcessCmdlet : InfisicalCmdletBase
{
private const string Component = "StartInfisicalProcessCmdlet";
private const string WindowStyleSet = "WindowStyle";
private const string CreateNoWindowSet = "CreateNoWindow";
[Parameter(Mandatory = true, Position = 0)]
[ValidateNotNullOrEmpty]
[Alias("FP")]
public string FilePath { get; set; }
[Parameter]
[Alias("WD")]
public DirectoryInfo WorkingDirectory { get; set; }
[Parameter]
[AllowEmptyCollection]
[AllowNull]
[Alias("AL")]
public string[] ArgumentList { get; set; }
[Parameter]
[AllowEmptyCollection]
[AllowNull]
[Alias("AECL")]
public string[] AcceptableExitCodeList { get; set; }
[Parameter(ParameterSetName = WindowStyleSet)]
[ValidateSet("Normal", "Hidden", "Minimized", "Maximized")]
[Alias("WS")]
public string WindowStyle { get; set; } = "Hidden";
[Parameter(ParameterSetName = CreateNoWindowSet)]
[Alias("CNW")]
public SwitchParameter CreateNoWindow { get; set; }
[Parameter]
[Alias("NW")]
public SwitchParameter NoWait { get; set; }
[Parameter]
[ValidateSet("AboveNormal", "BelowNormal", "High", "Idle", "Normal", "RealTime")]
[Alias("P")]
public string Priority { get; set; } = "Normal";
[Parameter]
[Alias("ET")]
public TimeSpan ExecutionTimeout { get; set; }
[Parameter]
[Alias("ETI")]
public TimeSpan ExecutionTimeoutInterval { get; set; } = TimeSpan.FromSeconds(15);
[Parameter]
[Alias("SIO")]
public object[] StandardInputObjectList { get; set; }
[Parameter]
[Alias("ENV")]
public IDictionary EnvironmentVariables { get; set; }
[Parameter]
[Alias("StandardOutputParsingExpression", "SOPE", "PE")]
public Regex ParsingExpression { get; set; }
[Parameter]
[Alias("SAL")]
public SwitchParameter SecureArgumentList { get; set; }
[Parameter]
[Alias("LO")]
public SwitchParameter LogOutput { get; set; }
[Parameter]
[Alias("COE")]
public SwitchParameter ContinueOnError { get; set; }
[Parameter(ValueFromPipeline = true)]
[Alias("Secrets", "InputObject")]
public InfisicalSecret[] Secret { get; set; }
[Parameter]
public string Prefix { get; set; }
private readonly List<InfisicalSecret> _secretBuffer = new List<InfisicalSecret>();
protected override void ProcessRecord()
{
if (Secret == null) { return; }
foreach (InfisicalSecret secret in Secret)
{
if (secret != null) { _secretBuffer.Add(secret); }
}
}
protected override void EndProcessing()
{
try
{
string target = string.IsNullOrEmpty(WorkingDirectory != null ? WorkingDirectory.FullName : null)
? FilePath
: string.Concat(FilePath, " (in ", WorkingDirectory.FullName, ")");
if (!ShouldProcess(target, "Start process with Infisical secrets")) { return; }
InfisicalProcessOptions options = new InfisicalProcessOptions
{
FilePath = FilePath,
WorkingDirectory = WorkingDirectory,
ArgumentList = ArgumentList,
AcceptableExitCodeList = AcceptableExitCodeList,
WindowStyle = WindowStyle,
CreateNoWindow = CreateNoWindow.IsPresent,
NoWait = NoWait.IsPresent,
Priority = Priority,
ExecutionTimeout = MyInvocation.BoundParameters.ContainsKey("ExecutionTimeout") ? (TimeSpan?)ExecutionTimeout : null,
ExecutionTimeoutInterval = ExecutionTimeoutInterval,
StandardInputObjectList = StandardInputObjectList,
EnvironmentVariables = EnvironmentVariables,
ParsingExpression = ParsingExpression,
SecureArgumentList = SecureArgumentList.IsPresent,
LogOutput = LogOutput.IsPresent,
ContinueOnError = ContinueOnError.IsPresent,
Secrets = _secretBuffer.ToArray(),
Prefix = Prefix
};
InfisicalProcessResult result = InfisicalProcessRunner.Run(options, Logger);
WriteObject(result);
if (!result.Succeeded && !NoWait.IsPresent && !ContinueOnError.IsPresent)
{
string message = string.Concat("Process '", FilePath, "' exited with code ", result.ExitCode.HasValue ? result.ExitCode.Value.ToString() : "<null>", " which is not in the acceptable exit code list.");
InvalidOperationException exception = new InvalidOperationException(message);
ErrorRecord error = new ErrorRecord(exception, "StartInfisicalProcess.UnacceptableExitCode", ErrorCategory.InvalidResult, result);
ThrowTerminatingError(error);
}
}
catch (PipelineStoppedException) { throw; }
catch (Exception exception)
{
ThrowTerminatingForException(Component, "StartProcess", exception);
}
}
}
}