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.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using PSInfisicalAPI.Process;
|
||||
using Xunit;
|
||||
|
||||
namespace PSInfisicalAPI.Tests
|
||||
{
|
||||
public class InfisicalProcessRunnerHelpersTests
|
||||
{
|
||||
[Fact]
|
||||
public void FormatFriendly_Zero_Returns_NotAvailable()
|
||||
{
|
||||
Assert.Equal("N/A", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.Zero));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatFriendly_Single_Unit_Plural()
|
||||
{
|
||||
Assert.Equal("30 seconds", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.FromSeconds(30)));
|
||||
Assert.Equal("5 minutes", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.FromMinutes(5)));
|
||||
Assert.Equal("250 milliseconds", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.FromMilliseconds(250)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatFriendly_Single_Unit_Singular()
|
||||
{
|
||||
Assert.Equal("1 second", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.FromSeconds(1)));
|
||||
Assert.Equal("1 minute", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.FromMinutes(1)));
|
||||
Assert.Equal("1 hour", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.FromHours(1)));
|
||||
Assert.Equal("1 day", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.FromDays(1)));
|
||||
Assert.Equal("1 millisecond", InfisicalProcessRunnerHelpers.FormatFriendly(TimeSpan.FromMilliseconds(1)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatFriendly_Two_Units_Uses_And_Join()
|
||||
{
|
||||
TimeSpan value = TimeSpan.FromSeconds(7) + TimeSpan.FromMilliseconds(364);
|
||||
Assert.Equal("7 seconds, and 364 milliseconds", InfisicalProcessRunnerHelpers.FormatFriendly(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatFriendly_Multiple_Units_Uses_Comma_And_Trailing_And()
|
||||
{
|
||||
TimeSpan value = TimeSpan.FromHours(1) + TimeSpan.FromMinutes(2) + TimeSpan.FromSeconds(3) + TimeSpan.FromMilliseconds(45);
|
||||
Assert.Equal("1 hour, 2 minutes, 3 seconds, and 45 milliseconds", InfisicalProcessRunnerHelpers.FormatFriendly(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatFriendly_Skips_Zero_Components()
|
||||
{
|
||||
TimeSpan value = TimeSpan.FromHours(2) + TimeSpan.FromMilliseconds(500);
|
||||
Assert.Equal("2 hours, and 500 milliseconds", InfisicalProcessRunnerHelpers.FormatFriendly(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatFriendly_Mixed_Singular_And_Plural()
|
||||
{
|
||||
TimeSpan value = TimeSpan.FromMinutes(1) + TimeSpan.FromSeconds(30);
|
||||
Assert.Equal("1 minute, and 30 seconds", InfisicalProcessRunnerHelpers.FormatFriendly(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatFriendly_Days_Component()
|
||||
{
|
||||
TimeSpan value = TimeSpan.FromDays(2) + TimeSpan.FromHours(3);
|
||||
Assert.Equal("2 days, and 3 hours", InfisicalProcessRunnerHelpers.FormatFriendly(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatFriendly_SubMillisecond_Returns_NotAvailable()
|
||||
{
|
||||
TimeSpan value = TimeSpan.FromTicks(100);
|
||||
Assert.Equal("N/A", InfisicalProcessRunnerHelpers.FormatFriendly(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace PSInfisicalAPI.Models
|
||||
{
|
||||
public sealed class InfisicalProcessResult
|
||||
{
|
||||
public string FilePath { get; set; }
|
||||
public int? ProcessId { get; set; }
|
||||
public int? ExitCode { get; set; }
|
||||
public string ExitCodeAsHex { get; set; }
|
||||
public int? ExitCodeAsInteger { get; set; }
|
||||
public string ExitCodeAsDecimal { get; set; }
|
||||
public DateTime? StartTime { get; set; }
|
||||
public DateTime? ExitTime { get; set; }
|
||||
public TimeSpan? Duration { get; set; }
|
||||
public string DurationFriendly { get; set; }
|
||||
public string StandardOutput { get; set; }
|
||||
public string StandardError { get; set; }
|
||||
public Match[] StandardOutputObject { get; set; }
|
||||
public Match[] StandardErrorObject { get; set; }
|
||||
public bool TimedOut { get; set; }
|
||||
public bool Succeeded { get; set; }
|
||||
public int SecretCount { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using PSInfisicalAPI.Models;
|
||||
|
||||
namespace PSInfisicalAPI.Process
|
||||
{
|
||||
public sealed class InfisicalProcessOptions
|
||||
{
|
||||
public string FilePath { get; set; }
|
||||
public DirectoryInfo WorkingDirectory { get; set; }
|
||||
public string[] ArgumentList { get; set; }
|
||||
public string[] AcceptableExitCodeList { get; set; }
|
||||
public string WindowStyle { get; set; }
|
||||
public bool CreateNoWindow { get; set; }
|
||||
public bool NoWait { get; set; }
|
||||
public string Priority { get; set; }
|
||||
public TimeSpan? ExecutionTimeout { get; set; }
|
||||
public TimeSpan ExecutionTimeoutInterval { get; set; }
|
||||
public object[] StandardInputObjectList { get; set; }
|
||||
public IDictionary EnvironmentVariables { get; set; }
|
||||
public Regex ParsingExpression { get; set; }
|
||||
public bool SecureArgumentList { get; set; }
|
||||
public bool LogOutput { get; set; }
|
||||
public bool ContinueOnError { get; set; }
|
||||
public InfisicalSecret[] Secrets { get; set; }
|
||||
public string Prefix { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using PSInfisicalAPI.Logging;
|
||||
using PSInfisicalAPI.Models;
|
||||
using SystemProcess = System.Diagnostics.Process;
|
||||
using static PSInfisicalAPI.Process.InfisicalProcessRunnerHelpers;
|
||||
|
||||
namespace PSInfisicalAPI.Process
|
||||
{
|
||||
public static class InfisicalProcessRunner
|
||||
{
|
||||
private const string Component = "InfisicalProcessRunner";
|
||||
|
||||
public static InfisicalProcessResult Run(InfisicalProcessOptions options, IInfisicalLogger logger)
|
||||
{
|
||||
if (options == null) { throw new ArgumentNullException(nameof(options)); }
|
||||
if (string.IsNullOrWhiteSpace(options.FilePath)) { throw new ArgumentException("FilePath is required.", nameof(options)); }
|
||||
|
||||
string[] acceptable = (options.AcceptableExitCodeList != null && options.AcceptableExitCodeList.Length > 0)
|
||||
? options.AcceptableExitCodeList
|
||||
: new[] { "0", "3010" };
|
||||
|
||||
InfisicalProcessResult result = new InfisicalProcessResult
|
||||
{
|
||||
FilePath = options.FilePath,
|
||||
ExitCode = -1,
|
||||
SecretCount = options.Secrets != null ? options.Secrets.Length : 0
|
||||
};
|
||||
|
||||
SystemProcess process = new SystemProcess();
|
||||
process.StartInfo.FileName = options.FilePath;
|
||||
process.StartInfo.UseShellExecute = false;
|
||||
process.StartInfo.RedirectStandardOutput = true;
|
||||
process.StartInfo.RedirectStandardError = true;
|
||||
process.StartInfo.RedirectStandardInput = true;
|
||||
|
||||
if (options.CreateNoWindow) { process.StartInfo.CreateNoWindow = true; }
|
||||
else if (!string.IsNullOrWhiteSpace(options.WindowStyle))
|
||||
{
|
||||
process.StartInfo.WindowStyle = (ProcessWindowStyle)Enum.Parse(typeof(ProcessWindowStyle), options.WindowStyle, true);
|
||||
}
|
||||
|
||||
if (options.WorkingDirectory != null && !string.IsNullOrWhiteSpace(options.WorkingDirectory.FullName))
|
||||
{
|
||||
if (!Directory.Exists(options.WorkingDirectory.FullName))
|
||||
{
|
||||
Directory.CreateDirectory(options.WorkingDirectory.FullName);
|
||||
}
|
||||
process.StartInfo.WorkingDirectory = options.WorkingDirectory.FullName;
|
||||
}
|
||||
|
||||
ApplyEnvironment(process.StartInfo.Environment, options, logger);
|
||||
|
||||
if (options.ArgumentList != null && options.ArgumentList.Length > 0)
|
||||
{
|
||||
process.StartInfo.Arguments = string.Join(" ", options.ArgumentList);
|
||||
}
|
||||
|
||||
LogCommand(process.StartInfo, options, logger);
|
||||
LogVerbose(logger, string.Concat("Acceptable exit codes: ", string.Join("; ", acceptable)));
|
||||
|
||||
Stopwatch timer = Stopwatch.StartNew();
|
||||
process.Start();
|
||||
result.ProcessId = process.Id;
|
||||
try { result.StartTime = process.StartTime; } catch { }
|
||||
|
||||
ApplyPriority(process, options.Priority, logger);
|
||||
|
||||
StringBuilder stdoutBuffer = new StringBuilder();
|
||||
StringBuilder stderrBuffer = new StringBuilder();
|
||||
object stdoutGate = new object();
|
||||
object stderrGate = new object();
|
||||
process.OutputDataReceived += (sender, args) => { if (args.Data != null) { lock (stdoutGate) { stdoutBuffer.AppendLine(args.Data); } } };
|
||||
process.ErrorDataReceived += (sender, args) => { if (args.Data != null) { lock (stderrGate) { stderrBuffer.AppendLine(args.Data); } } };
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
WriteStandardInput(process, options.StandardInputObjectList, options.SecureArgumentList, logger);
|
||||
|
||||
if (options.NoWait)
|
||||
{
|
||||
LogVerbose(logger, string.Concat("Skipping wait for process ID ", process.Id, "."));
|
||||
return result;
|
||||
}
|
||||
|
||||
WaitForExit(process, options.ExecutionTimeout, options.ExecutionTimeoutInterval, timer, result, logger);
|
||||
|
||||
try { process.WaitForExit(); } catch { }
|
||||
lock (stdoutGate) { result.StandardOutput = stdoutBuffer.Length > 0 ? stdoutBuffer.ToString() : null; }
|
||||
lock (stderrGate) { result.StandardError = stderrBuffer.Length > 0 ? stderrBuffer.ToString() : null; }
|
||||
|
||||
try { result.ExitCode = process.ExitCode; } catch { result.ExitCode = null; }
|
||||
FormatExitCodes(result);
|
||||
try { result.ExitTime = process.ExitTime; } catch { }
|
||||
if (result.StartTime.HasValue && result.ExitTime.HasValue) { result.Duration = result.ExitTime.Value - result.StartTime.Value; }
|
||||
if (result.Duration.HasValue)
|
||||
{
|
||||
result.DurationFriendly = FormatFriendly(result.Duration.Value);
|
||||
LogVerbose(logger, string.Concat("The command execution took ", result.DurationFriendly, "."));
|
||||
}
|
||||
|
||||
ApplyRegex(result, options.ParsingExpression);
|
||||
result.Succeeded = IsAcceptable(result, acceptable);
|
||||
|
||||
try { process.Dispose(); } catch { }
|
||||
|
||||
if (options.LogOutput || !result.Succeeded)
|
||||
{
|
||||
LogVerbose(logger, string.Concat("StandardOutput: ", string.IsNullOrEmpty(result.StandardOutput) ? "N/A" : result.StandardOutput));
|
||||
LogVerbose(logger, string.Concat("StandardError: ", string.IsNullOrEmpty(result.StandardError) ? "N/A" : result.StandardError));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool IsAcceptable(InfisicalProcessResult result, string[] acceptable)
|
||||
{
|
||||
if (acceptable.Any(c => string.Equals(c, "*", StringComparison.Ordinal))) { return true; }
|
||||
HashSet<string> set = new HashSet<string>(acceptable, StringComparer.OrdinalIgnoreCase);
|
||||
if (result.ExitCode.HasValue && set.Contains(result.ExitCode.Value.ToString(CultureInfo.InvariantCulture))) { return true; }
|
||||
if (!string.IsNullOrEmpty(result.ExitCodeAsHex) && set.Contains(result.ExitCodeAsHex)) { return true; }
|
||||
if (result.ExitCodeAsInteger.HasValue && set.Contains(result.ExitCodeAsInteger.Value.ToString(CultureInfo.InvariantCulture))) { return true; }
|
||||
if (!string.IsNullOrEmpty(result.ExitCodeAsDecimal) && set.Contains(result.ExitCodeAsDecimal)) { return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void FormatExitCodes(InfisicalProcessResult result)
|
||||
{
|
||||
if (!result.ExitCode.HasValue) { return; }
|
||||
try
|
||||
{
|
||||
result.ExitCodeAsHex = "0x" + Convert.ToString(result.ExitCode.Value, 16).PadLeft(8, '0').ToUpperInvariant();
|
||||
int parsed;
|
||||
if (int.TryParse(result.ExitCodeAsHex.Substring(2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out parsed))
|
||||
{
|
||||
result.ExitCodeAsInteger = parsed;
|
||||
}
|
||||
result.ExitCodeAsDecimal = result.ExitCode.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private static void LogVerbose(IInfisicalLogger logger, string message)
|
||||
{
|
||||
if (logger != null) { logger.Verbose(Component, message); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using PSInfisicalAPI.Logging;
|
||||
using PSInfisicalAPI.Models;
|
||||
using PSInfisicalAPI.Security;
|
||||
using SystemProcess = System.Diagnostics.Process;
|
||||
|
||||
namespace PSInfisicalAPI.Process
|
||||
{
|
||||
internal static class InfisicalProcessRunnerHelpers
|
||||
{
|
||||
private const string Component = "InfisicalProcessRunner";
|
||||
|
||||
internal static void ApplyEnvironment(IDictionary<string, string> processEnv, InfisicalProcessOptions options, IInfisicalLogger logger)
|
||||
{
|
||||
if (processEnv == null) { return; }
|
||||
|
||||
if (options.EnvironmentVariables != null && options.EnvironmentVariables.Count > 0)
|
||||
{
|
||||
Log(logger, string.Concat("Injecting ", options.EnvironmentVariables.Count, " explicit environment variable(s) into the process."));
|
||||
foreach (DictionaryEntry entry in options.EnvironmentVariables)
|
||||
{
|
||||
if (entry.Key == null) { continue; }
|
||||
string key = entry.Key.ToString();
|
||||
string value = entry.Value != null ? entry.Value.ToString() : string.Empty;
|
||||
processEnv[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.Secrets == null || options.Secrets.Length == 0) { return; }
|
||||
|
||||
Log(logger, string.Concat("Injecting ", options.Secrets.Length, " Infisical secret(s) into the process environment."));
|
||||
foreach (InfisicalSecret secret in options.Secrets)
|
||||
{
|
||||
if (secret == null || string.IsNullOrEmpty(secret.SecretName) || secret.SecretValue == null) { continue; }
|
||||
string name = string.IsNullOrEmpty(options.Prefix) ? secret.SecretName : string.Concat(options.Prefix, secret.SecretName);
|
||||
SecureStringUtility.UsePlainText(secret.SecretValue, plain =>
|
||||
{
|
||||
processEnv[name] = plain;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
internal static void LogCommand(ProcessStartInfo startInfo, InfisicalProcessOptions options, IInfisicalLogger logger)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.Append("Attempting to execute: ");
|
||||
builder.Append(startInfo.FileName);
|
||||
|
||||
if (!string.IsNullOrEmpty(startInfo.Arguments))
|
||||
{
|
||||
builder.Append(' ');
|
||||
if (options.SecureArgumentList)
|
||||
{
|
||||
int len = Math.Min(20, Math.Max(5, startInfo.Arguments.Length));
|
||||
builder.Append(new string('*', len));
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(startInfo.Arguments);
|
||||
}
|
||||
}
|
||||
|
||||
Log(logger, builder.ToString());
|
||||
}
|
||||
|
||||
internal static void ApplyPriority(SystemProcess process, string priority, IInfisicalLogger logger)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(priority)) { return; }
|
||||
ProcessPriorityClass desired;
|
||||
if (!Enum.TryParse(priority, true, out desired)) { return; }
|
||||
try
|
||||
{
|
||||
if (process.PriorityClass != desired)
|
||||
{
|
||||
process.PriorityClass = desired;
|
||||
Log(logger, string.Concat("Set process priority class to '", desired, "' for process ID ", process.Id, "."));
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log(logger, string.Concat("Unable to set process priority class to '", desired, "': ", exception.Message));
|
||||
}
|
||||
}
|
||||
|
||||
internal static void WriteStandardInput(SystemProcess process, object[] inputs, bool secureArguments, IInfisicalLogger logger)
|
||||
{
|
||||
if (inputs == null || inputs.Length == 0) { return; }
|
||||
for (int i = 0; i < inputs.Length; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
object value = inputs[i];
|
||||
string preview = secureArguments ? new string('*', 8) : (value != null ? value.ToString() : string.Empty);
|
||||
Log(logger, string.Concat("Writing standard input object ", i + 1, " of ", inputs.Length, " to process ID ", process.Id, ": ", preview));
|
||||
process.StandardInput.WriteLine(value);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log(logger, string.Concat("Failed to write standard input object ", i + 1, ": ", exception.Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void WaitForExit(SystemProcess process, TimeSpan? timeout, TimeSpan interval, Stopwatch timer, InfisicalProcessResult result, IInfisicalLogger logger)
|
||||
{
|
||||
TimeSpan pollInterval = interval.TotalMilliseconds > 0 ? interval : TimeSpan.FromSeconds(15);
|
||||
int processId = process.Id;
|
||||
|
||||
if (!timeout.HasValue)
|
||||
{
|
||||
Log(logger, string.Concat("A timeout was not specified for process ID ", processId, "."));
|
||||
Log(logger, string.Concat("The wait for process ID ", processId, " termination will be indefinite."));
|
||||
}
|
||||
else
|
||||
{
|
||||
Log(logger, string.Concat("Process timeout duration: ", FormatFriendly(timeout.Value)));
|
||||
}
|
||||
|
||||
while (!GetProcessHasExited(processId))
|
||||
{
|
||||
Log(logger, string.Concat("Process ID ", processId, " has been running for ", FormatFriendly(timer.Elapsed), "."));
|
||||
|
||||
if (timeout.HasValue && timer.Elapsed >= timeout.Value)
|
||||
{
|
||||
Log(logger, string.Concat("Process ID ", processId, " exceeded the maximum timeout duration of ", FormatFriendly(timeout.Value), "; terminating."));
|
||||
try { process.Kill(); result.TimedOut = true; } catch { }
|
||||
break;
|
||||
}
|
||||
|
||||
Log(logger, string.Concat("Checking again in another ", FormatFriendly(pollInterval), ". Please wait..."));
|
||||
System.Threading.Thread.Sleep(pollInterval);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool GetProcessHasExited(int processId)
|
||||
{
|
||||
try { return SystemProcess.GetProcessById(processId).HasExited; }
|
||||
catch { return true; }
|
||||
}
|
||||
|
||||
internal static string FormatFriendly(TimeSpan value)
|
||||
{
|
||||
string[] names = new[] { "Days", "Hours", "Minutes", "Seconds", "Milliseconds" };
|
||||
int[] values = new[] { value.Days, value.Hours, value.Minutes, value.Seconds, value.Milliseconds };
|
||||
|
||||
List<int> nonZeroIndices = new List<int>();
|
||||
for (int i = 0; i < values.Length; i++) { if (values[i] > 0) { nonZeroIndices.Add(i); } }
|
||||
|
||||
if (nonZeroIndices.Count == 0) { return "N/A"; }
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
int last = nonZeroIndices.Count - 1;
|
||||
for (int i = 0; i < nonZeroIndices.Count; i++)
|
||||
{
|
||||
int index = nonZeroIndices[i];
|
||||
int amount = values[index];
|
||||
string name = names[index].ToLowerInvariant();
|
||||
if (amount == 1) { name = name.TrimEnd('s'); }
|
||||
|
||||
if (nonZeroIndices.Count > 1 && i == last) { builder.Append("and "); }
|
||||
builder.Append(amount);
|
||||
builder.Append(' ');
|
||||
builder.Append(name);
|
||||
if (nonZeroIndices.Count > 1 && i != last) { builder.Append(", "); }
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
internal static void ApplyRegex(InfisicalProcessResult result, Regex expression)
|
||||
{
|
||||
if (expression == null) { return; }
|
||||
if (!string.IsNullOrEmpty(result.StandardOutput))
|
||||
{
|
||||
result.StandardOutputObject = expression.Matches(result.StandardOutput).Cast<Match>().ToArray();
|
||||
}
|
||||
if (!string.IsNullOrEmpty(result.StandardError))
|
||||
{
|
||||
result.StandardErrorObject = expression.Matches(result.StandardError).Cast<Match>().ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static void Log(IInfisicalLogger logger, string message)
|
||||
{
|
||||
if (logger != null) { logger.Verbose(Component, message); }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user