Merge pull request 'feat: add Start-InfisicalProcess cmdlet and -Prefix support on Export-InfisicalSecrets' (#12) from dev into main

Reviewed-on: #12
This commit was merged in pull request #12.
This commit is contained in:
2026-06-06 22:36:22 +00:00
15 changed files with 892 additions and 16 deletions
+53 -6
View File
@@ -6,41 +6,88 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos
## Unreleased
## 2026.06.06.2229
- Build produced from commit 207e7429e448.
## Unreleased (carried forward)
- `Start-InfisicalProcess`: switched stdout/stderr capture to event-based `OutputDataReceived`/`ErrorDataReceived` with `BeginOutputReadLine`/`BeginErrorReadLine` (removed `Task`/`ReadToEndAsync`/`GetAwaiter().GetResult()` to eliminate PowerShell `SynchronizationContext` deadlock risk). Restored the original `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.
- `Start-InfisicalProcess`: 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.
## 2026.06.06.2227
- Build produced from commit d3c7b83da717.
## Unreleased (carried forward)
## 2026.06.06.2221
- Build produced from commit d3c7b83da717.
## Unreleased (carried forward)
## 2026.06.06.2207
- Build produced from commit d3c7b83da717.
## Unreleased (carried forward)
## 2026.06.06.2206
- Build produced from commit d3c7b83da717.
## Unreleased (carried forward)
## 2026.06.06.2155
- Build produced from commit d3c7b83da717.
## Unreleased (carried forward)
- Added `Start-InfisicalProcess` cmdlet: launches a child process with `InfisicalSecret` objects (pipeline or `-Secret`) decrypted directly into `ProcessStartInfo.Environment`, with optional `-Prefix`, additional `-EnvironmentVariables`, stdout/stderr capture, `-AcceptableExitCodeList` validation, `-ParsingExpression` regex parsing, `-ExecutionTimeout`/`-ExecutionTimeoutInterval` polling, `-NoWait`, `-WindowStyle`/`-CreateNoWindow` parameter sets, `-Priority`, `-StandardInputObjectList`, `-SecureArgumentList`, `-LogOutput`, `-ContinueOnError`, and `ShouldProcess` support. Secret plaintext is never written to user or machine scope. `build.ps1` `CmdletsToExport` and `Test-ModuleImports` expected list now contain 42 cmdlets.
## 2026.06.06.2138
- Build produced from commit 318db7048017.
## Unreleased (carried forward)
## 2026.06.05.2040
- Build produced from commit 1270c9099cae.
## Unreleased (carried forward)
## Unreleased (carried forward)
## 2026.06.05.0240
- Build produced from commit b438abf18f18.
## Unreleased (carried forward)
## Unreleased (carried forward)
## 2026.06.05.0215
- Build produced from commit 82f99ea7d4a4.
## Unreleased (carried forward)
## Unreleased (carried forward)
## 2026.06.05.0205
- Build produced from commit 86968c18cb15.
## Unreleased (carried forward)
## Unreleased (carried forward)
## 2026.06.05.0117
- Build produced from commit cffda99591c9.
## Unreleased (carried forward)
## Unreleased (carried forward)
## 2026.06.05.0015
- Build produced from commit fb27ab8a8503.
## Unreleased (carried forward)
## Unreleased (carried forward)
- Fixed `ParameterNameConflictsWithAlias` registration error on `Get-InfisicalCertificateApplication`, `Get-InfisicalCertificateApplicationEnrollment`, and `New-InfisicalScepDynamicChallenge`. The cmdlets each declared an `[Alias]` entry that matched the parameter's own name, which PowerShell rejects at bind time and made the cmdlets unusable.
+4 -3
View File
@@ -1,6 +1,6 @@
@{
RootModule = 'PSInfisicalAPI.psm1'
ModuleVersion = '2026.06.05.2040'
ModuleVersion = '2026.06.06.2229'
GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51'
Author = 'Grace Solutions'
CompanyName = 'Grace Solutions'
@@ -50,7 +50,8 @@
'New-InfisicalScepDynamicChallenge',
'Get-InfisicalScepMdmProfile',
'Export-InfisicalScepMdmProfile',
'Write-InfisicalScepMdmProfileToWmi'
'Write-InfisicalScepMdmProfileToWmi',
'Start-InfisicalProcess'
)
AliasesToExport = @()
VariablesToExport = @()
@@ -62,7 +63,7 @@
LicenseUri = 'https://www.gnu.org/licenses/agpl-3.0.html'
ProjectUri = 'https://prod.git.gracesolution.info/gsadmin/PSInfisicalAPI'
ReleaseNotes = 'See CHANGELOG.md in the project repository for release history.'
CommitHash = '1270c9099cae'
CommitHash = '207e7429e448'
}
}
}
Binary file not shown.
@@ -333,7 +333,7 @@ $ConvertToInfisicalSecretDictionaryResult = ConvertTo-InfisicalSecretDictionary
<command:noun>InfisicalSecrets</command:noun>
</command:details>
<maml:description>
<maml:para>Buffers an incoming pipeline of InfisicalSecret objects and writes them to a file in the requested format (DotEnv, Json, Yaml, EnvironmentVariables, etc.) or sets them as environment variables on the chosen scope (Process, User, Machine). -Encoding controls text encoding for file outputs.</maml:para>
<maml:para>Buffers an incoming pipeline of InfisicalSecret objects and writes them to a file in the requested format (DotEnv, Json, Yaml, EnvironmentVariables, etc.) or sets them as environment variables on the chosen scope (Process, User, Machine). -Encoding controls text encoding for file outputs. -Prefix prepends a string to every emitted variable name regardless of format.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
@@ -361,6 +361,11 @@ $ExportInfisicalSecretsParameters.Verbose = $True
$ExportInfisicalSecretsResult = Export-InfisicalSecrets @ExportInfisicalSecretsParameters</dev:code>
<dev:remarks><maml:para>Projects the recursive secret result into Process-scope environment variables for the current PowerShell session.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 3</maml:title>
<dev:code>Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' | Export-InfisicalSecrets -Format EnvironmentVariables -Scope Process -Prefix 'MYAPP_'</dev:code>
<dev:remarks><maml:para>Imports secrets into the process environment with every variable name prefixed by 'MYAPP_' (e.g. API_KEY becomes MYAPP_API_KEY).</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
@@ -1649,4 +1654,51 @@ $WriteInfisicalScepMdmProfileToWmiResult = Write-InfisicalScepMdmProfileToWmi @W
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>Start-InfisicalProcess</command:name>
<maml:description><maml:para>Starts a child process with Infisical secrets injected directly into its environment block.</maml:para></maml:description>
<command:verb>Start</command:verb>
<command:noun>InfisicalProcess</command:noun>
</command:details>
<maml:description>
<maml:para>Launches the executable specified by -FilePath, captures stdout/stderr, validates the exit code against -AcceptableExitCodeList, and optionally parses output with -ParsingExpression. InfisicalSecret objects supplied via -Secret (pipeline or by name) are decrypted into the ProcessStartInfo.Environment dictionary only, never written to the user or machine scope; -Prefix prepends a string to each injected variable name. -EnvironmentVariables adds additional non-secret values. -ExecutionTimeout, -NoWait, -CreateNoWindow, -WindowStyle, -Priority, -StandardInputObjectList, -SecureArgumentList, -LogOutput, and -ContinueOnError mirror the semantics of the upstream Start-ProcessWithOutput helper. Honors -WhatIf and -Confirm.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>Secret values exist as plain strings only within the child process environment block; they are never persisted to the calling shell, the user scope, or the machine scope. Use -SecureArgumentList to mask sensitive command-line arguments in verbose output.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>Get-InfisicalSecret -SecretPath '/build' | Start-InfisicalProcess -FilePath 'dotnet.exe' -ArgumentList @('publish','-c','Release') -AcceptableExitCodeList @('0') -CreateNoWindow</dev:code>
<dev:remarks><maml:para>Decrypts every secret at /build, exposes each one as a process environment variable, and runs dotnet publish with no visible window.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>$Secrets = Get-InfisicalSecret -SecretPath '/runtime'
Start-InfisicalProcess -FilePath 'node.exe' -ArgumentList @('app.js') -Secret $Secrets -Prefix 'APP_' -ExecutionTimeout ([TimeSpan]::FromMinutes(5)) -LogOutput</dev:code>
<dev:remarks><maml:para>Injects the /runtime secrets as APP_-prefixed environment variables, runs node app.js, and forcibly terminates the process after five minutes if it has not exited.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 3</maml:title>
<dev:code>$StartInfisicalProcessParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$StartInfisicalProcessParameters.FilePath = 'pwsh.exe'
$StartInfisicalProcessParameters.ArgumentList = @('-NoProfile','-Command','Write-Host $env:DEPLOY_TOKEN.Length')
$StartInfisicalProcessParameters.Secret = Get-InfisicalSecret -SecretPath '/deploy'
$StartInfisicalProcessParameters.Prefix = 'DEPLOY_'
$StartInfisicalProcessParameters.AcceptableExitCodeList = @('0')
$StartInfisicalProcessParameters.CreateNoWindow = $True
$StartInfisicalProcessParameters.SecureArgumentList = $True
$StartInfisicalProcessParameters.LogOutput = $True
$StartInfisicalProcessParameters.Verbose = $True
$StartInfisicalProcessResult = Start-InfisicalProcess @StartInfisicalProcessParameters</dev:code>
<dev:remarks><maml:para>Splatted invocation that runs pwsh with DEPLOY_-prefixed secrets in scope, masks the command line in verbose output, and echoes both stdout and stderr to the verbose stream after exit.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
</helpItems>
@@ -333,7 +333,7 @@ $ConvertToInfisicalSecretDictionaryResult = ConvertTo-InfisicalSecretDictionary
<command:noun>InfisicalSecrets</command:noun>
</command:details>
<maml:description>
<maml:para>Buffers an incoming pipeline of InfisicalSecret objects and writes them to a file in the requested format (DotEnv, Json, Yaml, EnvironmentVariables, etc.) or sets them as environment variables on the chosen scope (Process, User, Machine). -Encoding controls text encoding for file outputs.</maml:para>
<maml:para>Buffers an incoming pipeline of InfisicalSecret objects and writes them to a file in the requested format (DotEnv, Json, Yaml, EnvironmentVariables, etc.) or sets them as environment variables on the chosen scope (Process, User, Machine). -Encoding controls text encoding for file outputs. -Prefix prepends a string to every emitted variable name regardless of format.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
@@ -361,6 +361,11 @@ $ExportInfisicalSecretsParameters.Verbose = $True
$ExportInfisicalSecretsResult = Export-InfisicalSecrets @ExportInfisicalSecretsParameters</dev:code>
<dev:remarks><maml:para>Projects the recursive secret result into Process-scope environment variables for the current PowerShell session.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 3</maml:title>
<dev:code>Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' | Export-InfisicalSecrets -Format EnvironmentVariables -Scope Process -Prefix 'MYAPP_'</dev:code>
<dev:remarks><maml:para>Imports secrets into the process environment with every variable name prefixed by 'MYAPP_' (e.g. API_KEY becomes MYAPP_API_KEY).</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
@@ -1649,4 +1654,51 @@ $WriteInfisicalScepMdmProfileToWmiResult = Write-InfisicalScepMdmProfileToWmi @W
</command:examples>
</command:command>
<command:command xmlns:maml="http://schemas.microsoft.com/maml/2004/10" xmlns:command="http://schemas.microsoft.com/maml/dev/command/2004/10" xmlns:dev="http://schemas.microsoft.com/maml/dev/2004/10">
<command:details>
<command:name>Start-InfisicalProcess</command:name>
<maml:description><maml:para>Starts a child process with Infisical secrets injected directly into its environment block.</maml:para></maml:description>
<command:verb>Start</command:verb>
<command:noun>InfisicalProcess</command:noun>
</command:details>
<maml:description>
<maml:para>Launches the executable specified by -FilePath, captures stdout/stderr, validates the exit code against -AcceptableExitCodeList, and optionally parses output with -ParsingExpression. InfisicalSecret objects supplied via -Secret (pipeline or by name) are decrypted into the ProcessStartInfo.Environment dictionary only, never written to the user or machine scope; -Prefix prepends a string to each injected variable name. -EnvironmentVariables adds additional non-secret values. -ExecutionTimeout, -NoWait, -CreateNoWindow, -WindowStyle, -Priority, -StandardInputObjectList, -SecureArgumentList, -LogOutput, and -ContinueOnError mirror the semantics of the upstream Start-ProcessWithOutput helper. Honors -WhatIf and -Confirm.</maml:para>
</maml:description>
<maml:alertSet>
<maml:title>Notes</maml:title>
<maml:alert>
<maml:para>Secret values exist as plain strings only within the child process environment block; they are never persisted to the calling shell, the user scope, or the machine scope. Use -SecureArgumentList to mask sensitive command-line arguments in verbose output.</maml:para>
</maml:alert>
</maml:alertSet>
<command:examples>
<command:example>
<maml:title>EXAMPLE 1</maml:title>
<dev:code>Get-InfisicalSecret -SecretPath '/build' | Start-InfisicalProcess -FilePath 'dotnet.exe' -ArgumentList @('publish','-c','Release') -AcceptableExitCodeList @('0') -CreateNoWindow</dev:code>
<dev:remarks><maml:para>Decrypts every secret at /build, exposes each one as a process environment variable, and runs dotnet publish with no visible window.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 2</maml:title>
<dev:code>$Secrets = Get-InfisicalSecret -SecretPath '/runtime'
Start-InfisicalProcess -FilePath 'node.exe' -ArgumentList @('app.js') -Secret $Secrets -Prefix 'APP_' -ExecutionTimeout ([TimeSpan]::FromMinutes(5)) -LogOutput</dev:code>
<dev:remarks><maml:para>Injects the /runtime secrets as APP_-prefixed environment variables, runs node app.js, and forcibly terminates the process after five minutes if it has not exited.</maml:para></dev:remarks>
</command:example>
<command:example>
<maml:title>EXAMPLE 3</maml:title>
<dev:code>$StartInfisicalProcessParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$StartInfisicalProcessParameters.FilePath = 'pwsh.exe'
$StartInfisicalProcessParameters.ArgumentList = @('-NoProfile','-Command','Write-Host $env:DEPLOY_TOKEN.Length')
$StartInfisicalProcessParameters.Secret = Get-InfisicalSecret -SecretPath '/deploy'
$StartInfisicalProcessParameters.Prefix = 'DEPLOY_'
$StartInfisicalProcessParameters.AcceptableExitCodeList = @('0')
$StartInfisicalProcessParameters.CreateNoWindow = $True
$StartInfisicalProcessParameters.SecureArgumentList = $True
$StartInfisicalProcessParameters.LogOutput = $True
$StartInfisicalProcessParameters.Verbose = $True
$StartInfisicalProcessResult = Start-InfisicalProcess @StartInfisicalProcessParameters</dev:code>
<dev:remarks><maml:para>Splatted invocation that runs pwsh with DEPLOY_-prefixed secrets in scope, masks the command line in verbose output, and echoes both stdout and stderr to the verbose stream after exit.</maml:para></dev:remarks>
</command:example>
</command:examples>
</command:command>
</helpItems>
+7 -1
View File
@@ -26,7 +26,7 @@ Import-Module -Name .\Module\PSInfisicalAPI
## Cmdlets
The module exports 37 cmdlets. Discovery cmdlets (`Get-Infisical*`) use a `List` (default) / single-record parameter-set pair: invoking without the identity parameter returns the collection, supplying the identity parameter returns one record.
The module exports 42 cmdlets. Discovery cmdlets (`Get-Infisical*`) use a `List` (default) / single-record parameter-set pair: invoking without the identity parameter returns the collection, supplying the identity parameter returns one record.
### Session
@@ -99,6 +99,12 @@ The module exports 37 cmdlets. Discovery cmdlets (`Get-Infisical*`) use a `List`
| `Export-InfisicalScepMdmProfile` | Writes a SCEP MDM profile to disk as a SyncML payload suitable for MDM delivery. |
| `Write-InfisicalScepMdmProfileToWmi`| Submits a SCEP MDM profile to the local MDM Bridge WMI provider to trigger enrollment. |
### Process
| Cmdlet | Purpose |
| ------------------------ | -------------------------------------------------------------------------------------------------- |
| `Start-InfisicalProcess` | Launches a child process with Infisical secrets injected directly into its environment block, capturing stdout/stderr and validating the exit code. |
Use `Get-Help <Cmdlet> -Full` for parameter details and `Get-Help about_PSInfisicalAPI` for the module overview.
## Quick start
+3 -2
View File
@@ -144,7 +144,8 @@ function Write-Manifest {
'New-InfisicalScepDynamicChallenge',
'Get-InfisicalScepMdmProfile',
'Export-InfisicalScepMdmProfile',
'Write-InfisicalScepMdmProfileToWmi'
'Write-InfisicalScepMdmProfileToWmi',
'Start-InfisicalProcess'
)
AliasesToExport = @()
VariablesToExport = @()
@@ -209,7 +210,7 @@ if (`$cmds.Count -eq 0) {
throw "No cmdlets were exported by the PSInfisicalAPI module."
}
`$expectedCmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','Copy-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder','Get-InfisicalTag','New-InfisicalTag','Update-InfisicalTag','Remove-InfisicalTag','Get-InfisicalCertificateAuthority','Get-InfisicalPkiSubscriber','Get-InfisicalCertificateProfile','Get-InfisicalCertificatePolicy','Get-InfisicalCertificate','Request-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate','Get-InfisicalCertificateApplication','Get-InfisicalCertificateApplicationEnrollment','New-InfisicalScepDynamicChallenge','Get-InfisicalScepMdmProfile','Export-InfisicalScepMdmProfile','Write-InfisicalScepMdmProfileToWmi')
`$expectedCmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','Copy-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder','Get-InfisicalTag','New-InfisicalTag','Update-InfisicalTag','Remove-InfisicalTag','Get-InfisicalCertificateAuthority','Get-InfisicalPkiSubscriber','Get-InfisicalCertificateProfile','Get-InfisicalCertificatePolicy','Get-InfisicalCertificate','Request-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate','Get-InfisicalCertificateApplication','Get-InfisicalCertificateApplicationEnrollment','New-InfisicalScepDynamicChallenge','Get-InfisicalScepMdmProfile','Export-InfisicalScepMdmProfile','Write-InfisicalScepMdmProfileToWmi','Start-InfisicalProcess')
foreach (`$expected in `$expectedCmds) {
if (-not (Get-Command -Name `$expected -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) {
throw "Cmdlet not found: `$expected"
+44 -1
View File
@@ -1213,7 +1213,8 @@ Export-InfisicalSecrets `
[-Path <FileInfo>] `
[-Scope <Process|User|Machine>] `
[-Force] `
[-Encoding <UTF8|UTF8Bom|Unicode>]
[-Encoding <UTF8|UTF8Bom|Unicode>] `
[-Prefix <string>]
```
## Parameter Rules
@@ -1490,6 +1491,48 @@ No warnings should be emitted.
---
# 16.6 Start-InfisicalProcess
Signature:
```text
Start-InfisicalProcess
-FilePath <string>
[-WorkingDirectory <DirectoryInfo>]
[-ArgumentList <string[]>]
[-AcceptableExitCodeList <string[]>]
[-WindowStyle <Normal|Hidden|Minimized|Maximized>]
[-CreateNoWindow]
[-NoWait]
[-Priority <AboveNormal|BelowNormal|High|Idle|Normal|RealTime>]
[-ExecutionTimeout <TimeSpan>]
[-ExecutionTimeoutInterval <TimeSpan>]
[-StandardInputObjectList <object[]>]
[-EnvironmentVariables <IDictionary>]
[-ParsingExpression <Regex>]
[-SecureArgumentList]
[-LogOutput]
[-ContinueOnError]
[-Secret <InfisicalSecret[]>]
[-Prefix <string>]
```
Behavior:
```text
Buffer pipeline InfisicalSecret objects in ProcessRecord.
Decrypt secrets only into ProcessStartInfo.Environment.
Apply -Prefix to each secret name before injection.
Never write secret plaintext to user or machine environment scope.
Honor -WhatIf / -Confirm.
Default -AcceptableExitCodeList = @('0','3010').
Throw a terminating error on unacceptable exit code unless -ContinueOnError is set.
```
Output: `InfisicalProcessResult` with `ExitCode`, `ExitCodeAsHex`, `ExitCodeAsInteger`, `ExitCodeAsDecimal`, `StandardOutput`, `StandardError`, `StandardOutputObject`, `StandardErrorObject`, `StartTime`, `ExitTime`, `Duration`, `DurationFriendly`, `ProcessId`, `TimedOut`, `Succeeded`, `SecretCount`.
---
# 17. SecureString Utility
Required utility:
@@ -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));
}
}
}
@@ -37,6 +37,9 @@ namespace PSInfisicalAPI.Cmdlets
[Parameter]
public InfisicalExportEncoding Encoding { get; set; } = InfisicalExportEncoding.UTF8;
[Parameter]
public string Prefix { get; set; }
private readonly List<InfisicalSecret> _buffer = new List<InfisicalSecret>();
protected override void ProcessRecord()
@@ -68,7 +71,7 @@ namespace PSInfisicalAPI.Cmdlets
InfisicalExportRequest request = new InfisicalExportRequest
{
Secrets = _buffer.ToArray(),
Secrets = ApplyPrefix(_buffer, Prefix),
Format = Format,
Path = Path,
Scope = Scope,
@@ -85,6 +88,38 @@ namespace PSInfisicalAPI.Cmdlets
}
}
private static InfisicalSecret[] ApplyPrefix(List<InfisicalSecret> source, string prefix)
{
if (string.IsNullOrEmpty(prefix)) { return source.ToArray(); }
InfisicalSecret[] result = new InfisicalSecret[source.Count];
for (int i = 0; i < source.Count; i++)
{
InfisicalSecret original = source[i];
result[i] = new InfisicalSecret
{
Id = original.Id,
InternalId = original.InternalId,
Workspace = original.Workspace,
Environment = original.Environment,
Version = original.Version,
Type = original.Type,
SecretName = string.Concat(prefix, original.SecretName),
SecretValue = original.SecretValue,
SecretValueHidden = original.SecretValueHidden,
SecretPath = original.SecretPath,
SecretComment = original.SecretComment,
CreatedAtUtc = original.CreatedAtUtc,
UpdatedAtUtc = original.UpdatedAtUtc,
IsRotatedSecret = original.IsRotatedSecret,
RotationId = original.RotationId,
Tags = original.Tags,
SecretMetadata = original.SecretMetadata
};
}
return result;
}
private static Encoding ResolveEncoding(InfisicalExportEncoding encoding)
{
switch (encoding)
@@ -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); }
}
}
}