Script-Block parsing issues - powershell

I am running some code that is read from an encrypted file and converted into a ScriptBlock. The code will be a full complex script, but for simplicity let's assume it is the following:
"$(date) Agent started." | Out-File -FilePath 'C:\TMP\test_agent.log' -Append
while($true) {
'$(date) Will check back in 30 seconds...' | Out-File -FilePath 'C:\TMP\test_agent.log' -Append
Start-Sleep -Seconds 30
}
Below is the simple code that launches it, and it works just fine (the $sStr variable contains the above script as a string):
$job = Start-Job -ScriptBlock {
$sb = $executioncontext.invokecommand.NewScriptBlock($args[0])
Invoke-Command -ScriptBlock $sb
} -ArgumentList $sStr | Wait-Job -Timeout 1 | Receive-Job
Again, this works fine. However, I need this to run as a new PowerShell process. But when I try the below the ScriptBlock is not parsed properly and I get errors. Here is the modified launcher:
$job = Start-Job -ScriptBlock {
$sb = $executioncontext.invokecommand.NewScriptBlock($args[0])
powershell "Invoke-Command -ScriptBlock $sb"
} -ArgumentList $sStr | Wait-Job -Timeout 1 | Receive-Job
How to start a new powershell process (and kill the parent) so that the ScriptBlock is correctly parsed and executed?
Thanks!

Maybe you could solve your problem with a PowerShell background job, which can be created via Start-Job. Start-Job also has a -ScriptBlock parameter.
Example 7 of Get-Job shows you how to check if the job is already completed, and how you can retrieve the results of the job.
Hope that helps.

Related

Powershell Execute program in parallel and wait end of execution

I need to execute a program (.exe) in a powershell script in a foreach loop, I need to wait the end of execution before doing some other tasks.
I tried this solution, the program is launched but it's closing immediately
$jobArray = New-Object -TypeName System.Collections.ArrayList
ForEach ($item in Get-Content C:\items.txt) {
$job = Start-Job -ScriptBlock {Start-Process "C:\Development\Console.exe" -ArgumentList /count, /Id:$item, /verbose }
$jobArray.Add($job)
}
Write-Verbose "started" -Verbose
#Wait for all jobs
ForEach ($job in $jobArray) {
$job | Wait-Job
}
A process already is like a job, it runs in parallel. In fact, PowerShell jobs are also just processes, so you currently start a process just to start another process.
Use Start-Process -PassThru without -Wait to capture a process object on which you can wait using Wait-Process.
$processes = ForEach ($item in Get-Content C:\items.txt) {
Start-Process -PassThru -FilePath 'C:\Development\Console.exe' -ArgumentList '/count', "/Id:$item", '/verbose'
}
Write-Verbose "started" -Verbose
# Wait for all processes
$processes | Wait-Process
As another optimization, most of the times you use a foreach or ForEach-Object loop you don't need to use ArrayList explicitly. Just assign the loop statement or pipeline to a variable and let PowerShell create an array for you in an efficient way.

PowerShell Script - Run multiple executables in parallel and wait for all launched executables to terminate before proceeding

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!
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

Run console EXE remotely with PowerShell and get its process ID and stdout

I use PowerShell to run a console EXE on a remote server and see its output like this:
Invoke-Command -ComputerName %SERVER_NAME% -ScriptBlock { Set-Location "%EXE_DIR%" ; .\MyExe.exe %EXE_ARGS% }
This works, but gives me no way to kill the process, except connecting to the server via RDP. If I could just save its process ID to a file I should be able to kill it using Stop-Process. I tried this:
Invoke-Command -ComputerName %SERVER_NAME% -ScriptBlock { Start-Process -NoNewWindow -PassThru -Wait -WorkingDirectory "%EXE_DIR%" "%EXE_DIR%\MyExe.exe" "%EXE_ARGS%" }
The process runs, but now I don't see its standard output! When I use Start-Process locally (without Invoke-Command) I see the output.
How can I get both the process ID and the standard output/error?
Use a background job. You can either start a local job to invoke a command on a remote host:
$job = Start-Job -ScriptBlock {
Invoke-Command -ComputerName $computer -ScriptBlock {
Set-Location 'C:\some\folder'
& 'C:\path\to\your.exe' $args
} -ArgumentList $args
} -ArgumentList $argument_list
or open a session to a remote host and start a local job there:
Enter-PSSession -Computer $computer
$job = Start-Job -ScriptBlock {
Set-Location 'C:\some\folder'
& 'C:\path\to\your.exe' $args
} -ArgumentList $argument_list
Exit-PSSession
Job output can be received via Receive-Job:
while ($job.HasMoreData) {
Receive-Job $job
Start-Sleep -Milliseconds 100
}
You can terminate it via its StopJob() method:
$job.StopJob()
Remove-Job removes the job from the job list:
Remove-Job $job

start-job to run script parallelly

Please help , really very much worried
How to transform the below script using start-job , I have 6 Modules to compare , but sequentially it's taking too much time I am trying to adopt start-job option so that I can run this compare parallelly or in background
Tried this -
Start-Job -Name "Comparecontrol" -filepath $ExecuteSbtWithDcmDm -ArgumentList $CompareControl,"",$false,$false | Out-Null
echolog $THISSCRIPT $DCM_UPDATE_LOG_FILE $LLINFO "Finished Control Master Comparison
Main Script
The general flow would be something like this:
$jobs = #()
$jobs += Start-Job -scriptblock {...}
...
$jobs += Start-Job -scriptblock {...}
Wait-Job $jobs
$results = Receive-Job $jobs
You can use a job name as an alternative to storing the job instance returned by Start-Job e.g.:
$jobName = 'CompareControl'
foreach ($script in $scripts)
{
Start-Job -Name $jobName-scriptblock {&$script} -ArgumentList ...
}
Wait-Job -Name $jobName
$results = Receive-Job -Name $jobName

Variables in Start-Job do not get evaluated

My Powershell code doesn't evaluate the $agent variable:
foreach ($agent in $agentcomputers) {
Write-Output 'Starting agent on '$agent
# psexc to start the agent
Start-Job -ScriptBlock {& psexec $agent c:\grinder\examples\startAgent.cmd}
}
This link is similar to my problem, except I'm not calling an external Powershell script.
I tried adding that in, using $args[0] for $agent, and adding the -ArgumentList parameters, but that didn't work.
Edits/Replies
$agentcomputers is just a list of computer names - each on its own line:
$agentcomputers = Get-Content c:\grinder-dist\agent-computers.txt
I have also tried this - and $args[0] doesn't evaluate:
Start-Job -ScriptBlock {& psexec $args[0] c:\grinder\examples\startAgent.cmd} -ArgumentList #($agent)
Here are 3 different ways I would do it.
First, all aligned and pretty.
$agents = Get-Content c:\grinder-dist\agent-computers.txt
$jobs = {
Param($agent)
write-host "Starting agent on" $agent
& psexec \\$agent c:\grinder\examples\startAgent.cmd
}
foreach($agent in $agents) {
Start-Job -ScriptBlock $jobs -argumentlist $agent | Out-Null
}
Get-Job | Wait-Job | Receive-Job
Or you could just put it all on one line without creating any variables.
(Get-Content c:\grinder-dist\agent-computers.txt) | %{ Start-Job -ScriptBlock { param($_) write-host "Starting agent on" $_; & psexec \\$_ c:\grinder\examples\startAgent.cmd } -argumentlist $_ | Out-Null }
Get-Job | Wait-Job | Receive-Job
And in this final example, you could manage how many threads are run concurrently by doing it this way.
$MaxThreads = 5
$agents = Get-Content c:\grinder-dist\agent-computers.txt
$jobs = {
Param($agent)
write-host "Starting agent on" $agent
& psexec \\$agent c:\grinder\examples\startAgent.cmd
}
foreach($agent in $agents) {
Start-Job -ScriptBlock $jobs -argumentlist $agent | Out-Null
While($(Get-Job -State 'Running').Count -ge $MaxThreads) {
sleep 10
}
Get-Job | Wait-Job | Receive-Job
}
Here is the solution. As Andy said, I needed to use $args array with the -ArgumentList parameter. This other thread was helpful: Powershell: passing parameters to a job
foreach($agent in $agentcomputers){
$agentslash = "\\"+$agent
$args = ($agentslash,"c:\grinder\examples\startAgent.cmd")
Write-Output 'Starting agent on '$agent
#psexc to start the agent
$ScriptBlock = {& 'psexec' #args }
Start-Job -ScriptBlock $ScriptBlock -ArgumentList $args
}