DEV Community

Skyler
Skyler

Posted on • Edited on

Powershell-inception: how to create a background process that spawns another process to run a script

Hello! Today, we're back for a technical article.

If you survived reading the title, then you might be wondering why the heck you would want to do that. And you know what? You're right. This is one of these situations where I really think you should ask yourself "do I really need to do this".

It is possible to do "powershell inception", however, I have been made aware it might not be the best tool to do it.

Let me explain why this happened, then I will show the code (I know that's what you want).

I was requested to do this at work. We wanted to use the agentless feature in TeamCity. The great thing is that, once your build is doing a call to external service, TC will give your build agent to another build configuration. that's cool. What is less cool is that you need the "external service" to send back a PUT saying "yes, I'm finished". We wanted to use it for Azure services, and well, Azure doesn't send this confirmation back.

So here we are spawning another process that won't get killed when the current process does.

Let's just get started.

We will cut this in 3 scripts:
1) Detached.ps1: Spawning a separate process that is not the child of the process running the script,
2) RunDetached.ps1: that script spawns another process that will itself run a script,
3) Script.ps1: The script that actually does the work you wanted.

Detached.ps1:

param (
    [string]
    $file,

    [parameter(Mandatory=$true, ValueFromRemainingArguments =$true)]
    $arguments
)

$process = Invoke-CimMethod -Class Win32_Process -MethodName Create -Arguments @{ CurrentDirectory = $location.Path ; CommandLine = "powershell -NonInteractive -ExecutionPolicy Bypass -File $file $arguments "}

if ($null -eq $process.ProcessId)
{
    Write-Warning "Background process did not work"
    Exit 1
}
else
{
    Write-Host "Started background process"
}
Enter fullscreen mode Exit fullscreen mode

Okay, a lot of things happen here. Let's look a bit closer.

ValueFromRemainingArguments is my latest discovery in Powershell and it is one of the best feature in my opinion. Basically, it will pass the remaining arguments to the next script. Let's say you call ./Detached.ps1 -script something.ps1 -whatever stuff -more value, it will understand that -more value is related to the script you're calling! I encourage you to have a look online, it is a bit tricky to explain.

You don't have to write CurrentDirectory = $location.Path ;, it just means you will spawn up the process in the current directory you are at, not in C:\Windows\System32 which is the default.

We then checked if the creation was successful or not.

RunDetached.ps1

param (
    [string]
    $anotherScript,

    [parameter(Mandatory=$true, ValueFromRemainingArguments = $true)]
)
)

function GetMessages {
    ## this will be used to get the messages from the script you will call, otherwise, you won't see anything.

    Get-Event -SourceIdentifier ProcOutput -ErrorAction SilentlyContinue | %{
        if ($_.SourceEventArgs.Data) {
            ##do whatever you want, write-Host or send to file, ...
        }
        Remove-Event -EventIdentifier $_.EventIdentifier
    }
    Get-Event -SourceIdentifier ProcError -ErrorAction SilentlyContinue | %{
        if ($_.SourceEventArgs.Data) {
            ##do whatever you want, write-Host or send to file, ...
        }
        Remove-Event -EventIdentifier $_.EventIdentifier
    }
}

try {
    #process information
    $pinfo = New-Object System.Diagnostics.ProcessStartInfo
    $pinfo.FileName = "powershell.exe"
    $pinfo.RedirectStandardError = $true
    $pinfo.RedirectStandardOutput = $true
    $pinfo.UseShellExecute = $false 
    $pinfo.Arguments = "-ExecutionPolicy Bypass -NonInteractive -File `"$anotherScript`" $arguments" #using `" `" to escape the double quotes

    #Create process object
    $proc = New-Object System.Diagnostics.Process
    $proc.StartInfo = $pinfo

    #Register event handler, allows us to read the streams asynchronously. We need to do that or we might deadlock the child script
    Register-ObjectEvent -InputObject $proc -EventName OutputDataReceived -SourceIdentifier ProcOutput
    Register-ObjectEvent -InputObject $proc -EventName ErrorDataReceived -SourceIdentifier ProcError

    #starting process
    $proc.Start() | Out-Null
    $proc.BeginErrorReadLine()
    $proc.BeginOutputReadLine()

    while (!$proc.WaitForExit(100)) {
        GetMessages
    }
    GetMessages #final one

    if ($proc.ExitCode -ne 0) {
        Exit $proc.ExitCode
    }

}
catch {
    #do the catch however you want
}
Enter fullscreen mode Exit fullscreen mode

Let me explain a bit more what happens here:
In GetMessages, you streamline the output and the errors to come up so you can see them (by default, you don't, as it is another process).
In the try, we spawn up a new process.

Then in $anotherScript you just put the Script.ps1 that does the work you actually wanted.

I hope this helps anyone at some point, this definitely took some time to figure out for me :).

See you next time!

Top comments (0)