When you have a vCAC stub (i.e. ‘BuildingMachine’ or any other stub) and invoke a script or process (like a PowerShell script) and from that script you start a new process as direct descendant of the original process it will be killed by vCAC when the stub exits. vCAC seems to search all processes and compares their Parent ProcessID against its own ProcessID in order to kill them for whatever reason.
A workaround for this exists and is easy to implement. In Windows there is no real process hierarchy except that a process saves the ProcessID of its parent in its PEB. When the parent process terminates the ProcessID is still present but meaningless. So in order to ‘trick’ vCAC that there are no child processes you only have to create a child process (as a stub) that also creates a child process (that does the real work). Once the first child process terminates the chain is broken and the child-child process lives as long as it wishes (or generates some kind of terminating exception).
Here is a quick wrapper that executes a PowerShell script as a nested child process:
[CmdletBinding( SupportsShouldProcess = $true , ConfirmImpact = "Low" , HelpURI='http://dfch.biz/PS/Vcac/Utilities/Start-ScriptAsync.ps1/' )] Param ( [Parameter(Mandatory = $true, Position = 0)] [Alias('ScriptName')] [string] $Name , [Parameter(Mandatory = $true, Position = 1, ParameterSetName = 'FirstInvoke')] [Alias('ScriptParameters')] [hashtable] $Parameters , [Parameter(Mandatory = $true, Position = 1, ParameterSetName = 'SecondInvoke')] [string] $CommandLine ) # Param <# ######################################## # # ScriptMain # ######################################## #> Set-Variable gotoSuccess -Option 'Constant' -Value 'biz.dfch.System.Exception.gotoSuccess' -Confirm:$false -WhatIf:$false; Set-Variable gotoError -Option 'Constant' -Value 'biz.dfch.System.Exception.gotoError' -Confirm:$false -WhatIf:$false; Set-Variable gotoFailure -Option 'Constant' -Value 'biz.dfch.System.Exception.gotoFailure' -Confirm:$false -WhatIf:$false; Set-Variable gotoNotFound -Option 'Constant' -Value 'biz.dfch.System.Exception.gotoNotFound' -Confirm:$false -WhatIf:$false; $datBegin = [datetime]::Now; [string] $fn = $MyInvocation.MyCommand.Name; Log-Debug -fn $fn -msg ("CALL. Name '{0}'. Parameters '{1}'. CommandLine '{2}'. ParameterSetName '{3}'." -f $Name, ($Parameters -is [hashtable]), $CommandLine, $PSCmdlet.ParameterSetName) -fac 1; # Default test variable for checking function response codes. [Boolean] $fReturn = $false; # Return values are always and only returned via OutputParameter. $OutputParameter = $null; $PowerShellExe = 'powershell.exe'; try { # Parameter validation if($PSCmdlet.ParameterSetName -eq 'FirstInvoke') { $fCommandExists = Get-Command $Name -ErrorAction:SilentlyContinue if(!$fCommandExists) { $fScriptExists = Test-Path -Path $Name; } # if if( !$fScriptExists -And !$fCommandExists ) { Log-Error $fn ("ScriptName '{0}' does not exist." -f $Name); return $null; } # if $Cmd = Get-Command $Name; $ScriptParams = ''; foreach($k in $Parameters.Keys) { if(!$Cmd.Parameters.ContainsKey($k)) { $msg = ("'{0}' does not contain specified parameter '{1}'. Skipping ..." -f $Name, $k); Log-Critical $fn $msg; $e = New-CustomErrorRecord -m $msg -cat InvalidArgument -o $k; throw($gotoError); } # if $ScriptParam = $Parameters.$k; switch($Cmd.Parameters.$k.ParameterType.Name) { 'SwitchParameter' { if($ScriptParam) { $ScriptParam = "(1 -as [Boolean])"; } else { $ScriptParam = "(0 -as [Boolean])"; }; break; } 'String' { $ScriptParam = "'{0}'" -f $ScriptParam.Replace("'", "''"); break; } default { $ScriptParam = "{0}" -f $ScriptParam; break; } } # switch $ScriptParams = "{0} -{1}:{2}" -f $ScriptParams, $k, $ScriptParam; } # foreach $ScriptCommandline = "{0}{1}" -f $Name, $ScriptParams; Log-Debug $fn ("ScriptCommandline: '{0}'" -f $ScriptCommandline); $Command = "{0} -Name:'{1}' -CommandLine:'{2}'" -f $PSCommandPath, $Name, $ScriptParams.Replace("'","''"); Log-Debug $fn ("Command '{0}'" -f $Command); $pr = Start-Process $PowerShellExe -PassThru -ArgumentList @('-Command',$Command); Log-Debug $fn ("Start-Process '{0}' with ProcessID '{1}'." -f $Name, $pr.ID); $OutputParameter = $pr; } elseif($PSCmdlet.ParameterSetName -eq 'SecondInvoke') { $ScriptCommandline = "{0} `"{1}`"" -f $Name, $CommandLine; Log-Debug $fn ("ScriptCommandline: '{0}'" -f $ScriptCommandline); $pr = Start-Process $PowerShellExe -PassThru -ArgumentList @('-Command',('"{0}"' -f $ScriptCommandline)); Log-Debug $fn ("Start-Process '{0}' with ProcessID '{1}'." -f $Name, $pr.ID); $OutputParameter = $pr; } else { Log-Critical $fn ("Unexpected ParameterSetName '{0}'. Aborting." -f $PSCmdlet.ParameterSetName); throw($gotoFailure); } # if $fReturn = $true; } # try catch { if($gotoSuccess -eq $_.Exception.Message) { $fReturn = $true; $error.RemoveAt(0); } else { [string] $ErrorText = "catch [$($_.FullyQualifiedErrorId)]"; $ErrorText += (($_ | fl * -Force) | Out-String); $ErrorText += (($_.Exception | fl * -Force) | Out-String); $ErrorText += (Get-PSCallStack | Out-String); if($_.Exception -is [System.Net.WebException]) { Log-Critical $fn ("[WebException] Request FAILED with Status '{0}'. [{1}]." -f $_.Status, $_); Log-Debug $fn $ErrorText -fac 3; } # [System.Net.WebException] else { Log-Error $fn $ErrorText -fac 3; if($gotoError -eq $_.Exception.Message) { Log-Error $fn $e.Exception.Message; } elseif($gotoFailure -ne $_.Exception.Message) { Write-Verbose ("$fn`n$ErrorText"); } else { # N/A } # if } # other exceptions $fReturn = $false; $OutputParameter = $null; } # !$gotoSuccess } # catch finally { # Clean up } # finally $datEnd = [datetime]::Now; Log-Debug -fn $fn -msg ("RET. fReturn: [{0}]. Execution time: [{1}]ms. Started: [{2}]." -f $fReturn, ($datEnd - $datBegin).TotalMilliseconds, $datBegin.ToString('yyyy-MM-dd HH:mm:ss.fffzzz')) -fac 2; # Return values are always and only returned via OutputParameter. return $OutputParameter;
1 Comment »