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
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.
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
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
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
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
}