2

I have an executable file (.exe) which has to be run multiple times with different arguments in parallel (ideally on different cores) from a PowerShell script, and at the end wait for all launched executables to terminate. To implement that in my PowerShell script I have used the Start-Job command that runs multiple threads in parallel. And as the script needs to wait for all jobs to finish their execution I used Start-Job in the combination with Get-Job | Wait-Job. This makes the script wait for all of the jobs running in the session to finish:

$SCRIPT_PATH = "path/to/Program.exe"
$jobs = Get-ChildItem -Path $DIR | Foreach-Object {

    if ($_ -like "Folder") { 
        # Do nothing 
    }
    else {

        $ARG1_VAR = "Directory\$($_.BaseName)"
        $ARG2_VAR = "Directory\$($_.BaseName)\SubDirectory"
        $ARG3_VAR = "Directory\$($_.BaseName)\SubSubDirectory"

        if (Test-Path -Path $ARG1_VAR)
        {
            Start-Job -Name -ScriptBlock {
               & $using:SCRIPT_PATH -arg1 $using:ARG1_VAR -arg2 $using:ARG2_VAR
            }
        }
        else
        {
            Start-Job -Name -ScriptBlock {
                & $using:SCRIPT_PATH -arg1 $using:ARG1_VAR -arg3 $using:ARG3_VAR
             }
        }
    }
}

$jobs | Receive-Job -Wait -AutoRemoveJob

However, it seems that -FilePath argument of Start-Job does NOT accept .exe files, but only .ps1 files, and therefore I get an exception.

Thus, I decided to use Start-Process command instead which spawns seperate processes instead of seperate threads. But I was not able to find a command that can wait for the termination of all started processed from my script. Therefore, I tried to do it manually by storing all started processes in an array list. And then I tried to wait for each process (using process ID) to terminate. However, that does not seem to work either, because Start-Process -FilePath Program.exe -ArgumentList $ARG_LIST returns NULL, and therefore nothing is saved in the $Process_List.

$SCRIPT_PATH = "path/to/Program.exe"
$procs = Get-ChildItem -Path $DIR | Foreach-Object {

    if ($_ -like "Folder") { 
        # Do nothing 
    }
    else {

        $ARG1_VAR = "Directory\$($_.BaseName)"
        $ARG2_VAR = "Directory\$($_.BaseName)\SubDirectory"
        $ARG3_VAR = "Directory\$($_.BaseName)\SubSubDirectory"

        if (Test-Path -Path $ARG1_VAR)
        {
            $ARG_LIST = @( "-arg1 $ARG1_VAR", "-arg2 $ARG2_VAR")

            Start-Process -FilePath $SCRIPT_PATH -ArgumentList $ARG_LIST -PassThru -NoNewWindow
        }
        else
        {
            
            $ARG_LIST = @( "-arg1 $ARG1_VAR", "-arg3 $ARG3_VAR")

            Start-Process -FilePath $SCRIPT_PATH -ArgumentList $ARG_LIST -PassThru -NoNewWindow
        }
    }
}

$procs | Wait-Process

I would appreciate any help. Please note I am using Powershell 5.1, thus ForEach-Object -Parallelconstruct is not supported on my machine.

Thank you!

SimpleThings
  • 147
  • 2
  • 12
  • So, on the last code snippet, you're using `Start-Process` yet for waiting for them, you're using `Receive-Job` which is wrong. Process and Jobs are not the same, use `Wait-Process` instead for waiting for Processes – Santiago Squarzon Jul 08 '22 at 14:17
  • 1
    The first code snippet looks correct, I don't see anything wrong with it. You could ask a new question with that code snippet specifically to troubleshoot it – Santiago Squarzon Jul 08 '22 at 14:18
  • Regarding the 2nd code snippet. Sorry I posted the wrong code. That was a copy-paste mistake and I corrected it now. I was using `$procs | Wait-Process` in my code actually. – SimpleThings Jul 08 '22 at 14:22
  • Now after the update, both snippets looks correct to me. I don't see nothing wrong with them – Santiago Squarzon Jul 08 '22 at 14:25
  • I have tried to output both string `"$procs"` as well as non-string `$procs`. The string `"$procs"` outputs `System.Object[]` whereas `$procs` lists correct content (process name, ID, etc.) of all started processes. Therefore, the variable `$procs` seems to be filled correctly. However, is actually `System.Object[]` the correct data type to be passed to `$procs | Wait-Process` ? Shouldn't the data type look something like `System.Diagnostics.Process[]` instead of `System.Object[]`? Exception message says: _Wait-Process : The input object cannot be bound to any parameters of the command …_ – SimpleThings Jul 08 '22 at 14:47
  • 1
    `Object[]` is correct, `Wait-Process` has no problem with that, for example: `$procs = 0..2 | %{ Start-Process powershell.exe -PassThru }; $procs | Wait-Process` 3 pwsh processes will appear and you'll see the main thread waiting for them. After closing the 3 pwsh processes you will regain control of the main thread. – Santiago Squarzon Jul 08 '22 at 14:54
  • Okay, thank you for your help. Then I have no other idea why `$procs | Wait-Process` does not work in my case. – SimpleThings Jul 08 '22 at 15:05
  • ‍♂️ the issue is `-NoNewWindow` most likely. Remove it and try – Santiago Squarzon Jul 08 '22 at 15:08
  • The behaviour unforunately stays same after removing `-NoNewWindow`. Could it be due to PowerShell version I am using? I am using V5.1. – SimpleThings Jul 08 '22 at 15:22
  • 1
    No, it should work properly in 5.1. My advise would be to ask a new question to see if someone else can spot anything wrong but as you have it right both codes looks correct to me to be honest – Santiago Squarzon Jul 08 '22 at 15:31

1 Answers1

3

Regarding your first example with Start-Job, instead of using the -FilePath parameter you could use the -ScriptBlock parameter:

$path = 'path/to/my.exe'
$jobs = Get-ChildItem -Path $DIR | Foreach-Object {
    Start-Job -ScriptBlock {
        & $using:path -arg1 $using:_ -arg2 $using:ARG2_VAR
    }
}
$jobs | Receive-Job -Wait -AutoRemoveJob

Regarding your second example, using Start-Process you should note that, this cmdlet produces no output without the -PassThru switch, hence you're adding effectively nothing to your list.

$processes = Get-ChildItem -Path $DIR | Foreach-Object {
    Start-Process -FilePath Program.exe -ArgumentList $ARG_LIST -PassThru
}

With this minor addition of the -PassThru switch you can either use a while loop checking the .HasExited Property of the objects in case you need to do something else with your code while waiting for the processes:

# block the thread until all processes have finished
while($processes.HasExited -contains $false) {
    # do something else here if needed
    Start-Sleep -Milliseconds 200
}

Or even simpler, as mklement0 points out, if you only need to wait for the processes, you can use Wait-Process:

$processes | Wait-Process
Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
  • 1
    Thank you the good comment. I have tried to use both of the versions you have provided, one with `Start-Job` and other with `Start-Process`. Somehow, passing the same arguments to those 2 PS-Commands produces different outcome. Namely when I use `Start-Job` the executable is launched, and it finishes its execution, but somehow the executable does not create an output file that it is supposed to create. Whereas `Start-Process` creates the file successfuly. For my job, I used `Wait-Job -Job $job` to wait for job to end. But that should not influence the behaviour from my understanding? – SimpleThings Jul 07 '22 at 07:17
  • 1
    One more comment: `$processes | Wait-Process` throws an exception in my case. It looks like `$processes` is empty after executing `$processes = Get-ChildItem -Path $DIR | Foreach-Object`. I think that is because I am calling `Start-Process` in an IF-statement inside of `Foreach-Object`. How can I store my processes into `$processes` when the command is called inside of IF-Statement of the Foreach loop? – SimpleThings Jul 07 '22 at 09:05
  • @SimpleThings I'm sorry totally missed these comments. I guess you were able to sort the questions above ? – Santiago Squarzon Jul 08 '22 at 13:36
  • No problem. No I have not sorted it out yet. I would really appreciate your help :). – SimpleThings Jul 08 '22 at 13:43
  • So reg. the first question, why would the file not be written, if you are passing an outside variable containing the path where the file should be written to remember to use `$using:` so the Job sees that variable (aside from that I'm not sure what could be the possible reason) best advise would be to ask a new question with all details. `Wait-Job` will only influence your current thread, will block your current thread until the jobs are completed. Does not influence the logic inside your Jobs. – Santiago Squarzon Jul 08 '22 at 13:48
  • 1
    Reg. the second question, about the `If` statement, can you edit your question showing the code you're using? @SimpleThings – Santiago Squarzon Jul 08 '22 at 13:49
  • I have actually edited both the 1st and 2nd questions. Reg. the 1st question: I have used `$using:` since the beginning as you suggested. Please see my edited post. – SimpleThings Jul 08 '22 at 14:15