Child Processes in vCAC Stubs are killed upon Exit of the Parent Process

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;

Trackbacks

  1. […] And here is how to reject the waiting approval (this would be executed asynchronously as described in Child Processes in vCAC Stubs are killed upon Exit of the Parent Process): […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: