I am having trouble getting the PID from a Start-Process within a Start-Job in Powershell (latest version). The Start-Process cmdlet runs and returns the PID as expected when run outside of the Start-Job Scriptblock. When the statement is added to the Start-Job Scriptblock, no PID is returned. Can someone point this newbie in the right direction?
$myJob = Start-Job -Name newJob -ScriptBlock {
$procID = (Start-Process myprogram.exe -PassThru -ArgumentList "myArgs" -WorkingDirectory "myWorkingDir").Id
}
Perhaps you think that assigning to a variable in a script block being executed in a background job makes that variable visible to the caller, which is not the case.
Background jobs run in an entirely separate session, and, in order to communicate data to the caller, the success output stream must be used, whose content the caller must collect via the Receive-Job cmdlet:
$myJob = Start-Job -Name newJob -ScriptBlock {
# Implicitly output the PID of the asynchronously launched process,
# by using -PassThru and returning the output object's .Id property value.
# Note: By default, Start-Process runs the process
# *asynchronously*, in a *new window*.
(
Start-Process myprogram.exe -PassThru -ArgumentList "myArgs" -WorkingDirectory "myWorkingDir"
).Id
}
# Use Receive-Job to collect the background job's output, once available,
# then remove the job automatically.
# Note: The process launched *asynchronously* from the background job
# with Start-Process lives on.
$procId = Receive-Job -Wait -AutoRemoveJob
Taking a step back: Start-Process is itself asynchronous, so there's no need to use a background job: just start myprogram.exe directly in the caller's context:
# Launch myprogram.exe asynchronously, in a new window.
# $proc receives a value once the program has *launched* and
# your script continues right after.
$proc = Start-Process myprogram.exe -PassThru -ArgumentList "myArgs" -WorkingDirectory "myWorkingDir"
# $proc.Id retrieves the PID (process ID)
# $proc.HasExited tells you whether the process has exited.
# $proc.WaitForExit() allows you to wait synchronously for the process to exit.
# * If you intend to wait *right away*, i.e run in a new window, but
# in a blocking fashion, you can omit -PassThru and pass -Wait instead.
However, if myprogram.exe is a console (terminal) application that you want to run asynchronously and whose output you want to capture, do use Start-Job, but don't use Start-Process to launch myprogram.exe: invoke it directly from the background job:
$myJob = Start-Job -Name newJob -ScriptBlock {
Set-Location "myWorkingDir"
# Launch console app myprogram.exe *synchronously*, *invisibly*,
# and relay its (stdout) output as the job's output.
myprogram.exe
}
# ... do other things
# Wait for myprogram.exe to finish, at which point the
# job exits, relaying its (stdout) output.
$myJob | Receive-Job -Wait -AutoRemoveJob
Related
I wrote a script, which opens 7 programs approximately 10 times (yes its a prankscript).
My question is, is there a way to observe, if the last process is closed and if so, restarting the whole script again?
while ($start -le 10){
Start-Process mspaint.exe
Start-Process notepad.exe
Start-Process write.exe
Start-Process cmd.exe
Start-Process explorer.exe
Start-Process control.exe
Start-Process calc.exe
$start =+ 1
}
My script now looks like following:
$start; $process
PowerShell.exe -windowstyle hidden { script.ps1 }
while ($start -le 10){
Start-Process mspaint.exe
Start-Process notepad.exe
Start-Process write.exe
Start-Process cmd.exe
Start-Process explorer.exe
Start-Process control.exe
Start-Process calc.exe
$start =+ 1
}
$process = Get-Process mspaint.exe
if ($process = $false){
Start-Process -FilePath c:/script.ps1
}
I did test this, but it starts all over again... I think that I use Get-Process wrong...
Is there another way to observe, if the process is closed or not?
If it's acceptable to handle the re-launching inside the same, indefinitely running script:
# Note: This runs indefinitely.
# If run in the foreground, you can use Ctrl-C to stop.
while ($true) {
1..10 | ForEach-Object {
# Launch all processes and pass information
# about them through (-PassThru)
'mspaint.exe',
'notepad.exe',
'write.exe',
'cmd.exe',
'explorer.exe',
'control.exe',
'calc.exe' | ForEach-Object {
Start-Process -PassThru $_
}
} | Wait-Process # Wait for all processes to terminate.
# Continue the loop, which launches the programs again.
}
You could then launch the script invisibly in the background, via Start-Process; e.g.:
Start-Process -WindowStyle Hidden powershell.exe '-File c:\script.ps1'
Caveat: To stop the operation, you'll have to locate the hidden PowerShell process and terminate it. If you add -PassThru, you'll get a process-information object representing the hidden process back.
More work is needed if you want to be able to call the script itself normally, and let it spawn a hidden background process that monitors the launched processes and then reinvokes the script (invisibly):
# Launch all processes 10 times and
# collect the new processes' IDs (PIDs)
$allPids = (
1..10 | ForEach-Object {
'mspaint.exe',
'notepad.exe',
'write.exe',
'cmd.exe',
'explorer.exe',
'control.exe',
'calc.exe' | ForEach-Object {
Start-Process -PassThru $_
}
}
).Id
# Launch a hidden PowerShell instance
# in the background that waits for all launched processes
# to terminate and then invisibly reinvokes this script:
Start-Process -WindowStyle Hidden powershell.exe #"
-Command Wait-Process -Id $($allPids -join ',');
Start-Process -WindowStyle Hidden powershell.exe '-File \"$PSCommandPath\"'
"#
Caveat: To stop the operation, you'll have to locate the hidden PowerShell process and terminate it.
Without seeing your actual script you can use something along the lines of
$validate = Get-Process -Name pwsh
if ($validate){
Start-Process -FilePath c:/script.ps1
}
So I am running a script to download from web and install a binary. I do that in a separate command prompt. When its done, prompt is closed. How can I know when that happened?
Example (from Windows 10) - powershell Start-Process cmd "/C tasklist"
Can I assign it to a variable and listen to some fallback function call, etc.? Like this pseudo code:
$process = powershell Start-Process cmd "/C tasklist"
while (!$process.done) {
// do stuff
}
p.s. I am very new to PowerShell. So sorry if that doesnt make sence
You could use WaitForExit()
$process = Start-Process cmd "/C tasklist" -PassThru
$process.WaitForExit()
Since you are using PowerShell, you can use background jobs for scenario:
$job = Start-Job -ScriptBlock {cmd.exe /c tasklist}
while ($job.State -eq 'Running') {
# do other stuff while waiting for job to complete
}
# To get the job results
Receive-Job $job
Based on your comments, adding -Wait makes Start-Process a synchronous command. At that point, you don't need Start-Process. You may as well just execute cmd.exe /c tasklist.
I'm currently struggling with an issue and would appreciated any help or input.
I have a powershell script that, during it's execution, spawns other powershell scripts that run various commands. All of these generated powershell scripts exit automatically after their commands have been executed.
What I'm trying to do is that after all of these spawned powershell instances exited, run another command within the initial powershell script. Basically wait for all generated powershell instances to exit and then run a command.
Any ideas on how to implement that?
Thanks
Edit: The code that spawns the powershell scripts looks like this:
foreach ($var in $filters){
start-process powershell.exe -Verb Runas -argument "-nologo -noprofile -command .\$var"}
You can use Wait-Process. Use the -PassThru switch to return the started processes. This should work:
$processes = foreach ($var in $filters) {
Start-Process powershell.exe -PassThru -Verb RunAs -Arg "-nologo -noprofile -command .\$var"
}
# wait for processes to finish
$processes | Wait-Process
# now, run your other commands
# ...
You could also use background jobs like this (using the $PWD automatic variable):
# run all scripts as background jobs
$jobs = foreach ($var in $filters) {
Start-Job -FilePath "$PWD\$var"
}
# wait for all jobs to finish
$jobs | Wait-Job
# now, run your other commands
# ...
or in a one-liner:
$filters | foreach { Start-Job -FilePath "$PWD\$_" } | Wait-Job
You can try this using powershell jobs as #jeorenmostert said
$j=$filters|%{
start-job -filepath "$($_)"
}
$j|wait-job
here first iterate over the $filters using % alias of ForEach
then start each job ($_) is the pipeline variable
get them inside $j variable
pipe $j jobs to wait-job to wait until the jobs has finished (you can pipe receive-job too to get passthru like results)
I have this batch file which runs the powershell script.
I want to run it the background but if I run with "windowstyle hidden" still visible.
powershell -ExecutionPolicy ByPass -windowstyle hidden -File "C:\script.ps1"
You can run, e.g. long running scripts, as a jobs.
To start it you run
$job = Start-Job -ScriptBlock {Get-Process}
this will start the Get-Process cmdlet in the background. The script can be also some custom made script or a longer script, it doesn't need to be a one-liner.
You can check its status by running
$job | Get-Job
and to receive the output you run
$job | Receive-Job
just note that once the data is received, it's lost. You can only receive it once, after that it's up to you to save it in a variable or later processing.
Finally to remove the job from the queue you run
$job | Remove-Job
I use the following function:
function bg() {
Start-Process `
-WorkingDirectory (Get-Location) `
-NoNewWindow `
-FilePath "powershell" `
-ArgumentList "-NoProfile -command `"$args`" "
}
It starts a new powershell instance which is executed in background and allows the usage of cmdlets.
You call it like:
bg "Start-Sleep 2; get-location; write 'done' "
When I create a automation script with PowerShell 5.1, I got an issue – in a script block of a job, the code after Start-Process will not get chance to execute. Here’s a simple repro:
Step 1 >> Prepare a .cmd file for Start-Process, the code in callee.cmd is:
#echo off
echo "Callee is executing ..."
exit /B 0
Step 2 >> Prepare the PowerShell code,
$scriptBlock = {
$res = Start-Process -FilePath "cmd.exe" -Wait -PassThru -NoNewWindow -ArgumentList "/c .\callee.cmd"
throw "ERROR!"
}
$job = Start-Job -ScriptBlock $scriptBlock
Wait-Job $job
Receive-Job $job
Write-Host($job.State)
Step 3 >> Run the PowerShell script, the output on screen is:
Id Name PSJobTypeName State HasMoreData Location Command
-- ---- ------------- ----- ----------- -------- -------
1 Job1 BackgroundJob Completed True localhost ...
Completed
The expected value should be “Failed”.
Does my code have problem or I’m using jobs in a wrong way?
Start-Job run job in separate PowerShell process in so-called server mode. In this mode PowerShell job process use standard input and output streams to exchange messages with the parent process.
-NoNewWindow parameter of Start-Process cmdlet instruct it to connect spawned console child process to the standard streams of its parent.
Thus, using Start-Process -NoNewWindow inside of PowerShell job, you connect spawned cmd.exe process to the same streams, which PowerShell job process use to exchange messages with its own parent process.
Now, when spawned cmd.exe write something into its standard output stream, it disturb normal message exchange between PowerShell job process and its parent, which leads to some unexpected behavior.
PetSerAl gave a great explanation of why it happens but it took me while to find a proper solution.
First thing - as some people mentioned don't use -NoNewWindow, use -WindowStyle Hidden instead.
Second, output results to file and handle them in your script block. There are two parameters, one for output and another for errors -RedirectStandardOutput and -RedirectStandardError.
For some reasons I was sure that first one will handle all output and my code worked well if there were no errors, but was failing with no output in case of exceptions in script block.
I created a function that also handles process status result:
function RunProcesSafe($pathToExe, $ArgumentList)
{
Write-Host "starting $pathToExe $ArgumentList"
$logFile = Join-Path $env:TEMP ([guid]::NewGuid())
$errorLogFile = Join-Path $env:TEMP ([guid]::NewGuid())
try
{
Write-Host "starting $pathToExe $ArgumentList"
$proc = Start-Process "$pathToExe" -Wait -PassThru -RedirectStandardError $errorLogFile -RedirectStandardOutput $logFile -WindowStyle Hidden -ArgumentList $ArgumentList
$handle = $proc.Handle
$proc.WaitForExit();
if ($proc.ExitCode -ne 0) {
Write-Host "FAILED with code:"+$proc.ExitCode
Throw "$_ exited with status code $($proc.ExitCode)"
}
}
Finally
{
Write-Host (Get-Content -Path $logFile)
Write-Host (Get-Content -Path $errorLogFile)
Remove-Item -Path $logFile -Force
Remove-Item -Path $errorLogFile -Force
}
}