Not all of my PowerShell v2 jobs get run - powershell

I'm trying to use jobs to have simultaneous instances of PowerShell run, so that my script can finish faster. The scripts I'm calling are all independent, no need for mutual exclusion etc. Problem is that it will call five out of 50 scripts and then it will stop. The rest of the scripts never seem to run. Something wrong with wait-job? Also for some reason the five scripts that actually run seem to be executed twice so I get double output...
$Invocation = (Get-Variable MyInvocation -Scope 1).Value
$ScriptDir = Split-Path $Invocation.MyCommand
$ScriptDir -match '(?<content>.*\d)' | out-null
$MainDir = $matches['content']
$ScriptName = $MyInvocation.MyCommand.Name
$Items = Get-ChildItem -Path $MainDir | Where-Object {$_.mode -match "d"}
$jobMax = 4
$jobs = #()
$jobWork = {
param ($MyInput,$dir)
$command = "$dir\" + $MyInput.name + "\" + $MyInput.name + ".ps1"
Start-Process powershell.exe -argumentlist $command -WindowStyle Hidden #-wait
}
foreach ($Item in $Items) {
if ($jobs.Count -le $jobMax) {
$jobs += Start-Job -ScriptBlock $jobWork -ArgumentList $Item,$MainDir
} else {
$jobs | Wait-Job -Any
}
}
$jobs | Wait-Job
Edit: Also, since I'm using start-process powershell all the scripts run simultaneously(unless I enable -wait), without the need for start-job. I wanted to use jobs so that I can throttle. Maybe it's wrong logic that I have a job start a new instance of powershell?
Edit2: As I thought it was wrong to use start-job to start a new instance of poweshell. That way the job was done after it opened the new powershell instance, the actual script contents had nothing to do with the job's start and finish. Here is my fixed script :)
$Invocation = (Get-Variable MyInvocation -Scope 1).Value
$ScriptDir = Split-Path $Invocation.MyCommand
$ScriptDir -match '(?<content>.*\d)' | out-null
$MainDir = $matches['content']
$ScriptName = $MyInvocation.MyCommand.Name
$Items = Get-ChildItem -Path $MainDir | Where-Object {$_.mode -match "d"}
$maxJobs = 4
$jobs = #()
foreach ($Item in $Items) {
$command = "$MainDir\" + $Item.name + "\" + $Item.name + ".ps1"
$jobs += Start-Job -filepath $command -ArgumentList $MainDir,$Item
$running = #($jobs | ? {$_.State -eq 'Running'})
while ($running.Count -ge $maxJobs) {
$finished = Wait-Job -Job $jobs -Any
$running = #($jobs | ? {$_.State -eq 'Running'})
}
}
Wait-Job -Job $jobs > $null
I had to edit all my script so that I could pass the arguments as parameters, but now everything works fine. This thread was helpful Running multiple scriptblocks at the same time with Start-Job (instead of looping)

My guess is (after looking at your code) is that you will get only first 4 jobs started.
Jobs (even completed) do not disappear automagically. You need to clean them once they are completed (maybe Receive-Job's first). If you won't do that - PowerShell will spin $jobsMax jobs and once it's done - I would be really surprised to see any other "newcomers".
Add some Receive/Remove job logic and it should work better.
Also: as far as I can tell you are running scripts, so I would suggest using -File parameter on powershell.exe rather than -command.
HTH
Bartek

Related

Add a timeout within a foreach loop and write to host - Powershell

I have a small script that runs through all share paths within a csv file and invokes a quick Get-ChildItem to count the number of files within the folder/subfolders etc. This is working but it is taking an age, especially if a folder have more than 500k files.
I would like to add a timeout within the loop, so that it will just write-host and then output into another column 'TIMED OUT' but i can't seem to get it to work.
I have tried a few different ways but somehow my loop breaks and runs the top line in my csv over and over.
This is the code i have so far:
...
#Import middle to filter unique
$sharesMiddle = Import-Csv -Path './middle.csv' | Sort-Object NFTSPath -Unique
$result = foreach ($share in $sharesMiddle) {
#Replace the colon for $ within the path
$nftsPath = join-path \\ $share.AssetName $share.Path.Replace(':', '$')
$path = $share.Path
$count = Invoke-Command -computername $share.AssetName -ScriptBlock { param($path) (Get-ChildItem $path -File -Recurse).count } -Credential $Cred -Verbose -ArgumentList $path
Write-Host $nftsPath : $count
$share | Select-Object *, #{n = "Files"; e = { $count } }
}
$result | Export-CSV '.\newcsvfile.csv' -NoTypeInformation
Any help on this would be super!
Thanks.
I tested the following which should be suitable for you:
# define the time limit in seconds
$TimeoutLimitSeconds = 2
# Define the job, start job
$job = Start-Job -ScriptBlock {420+69 ; start-sleep -Seconds 3}
# Await job
$job | Wait-Job -Timeout ( $TimeoutLimitSeconds ) | Out-Null
# If job doesn't finish before the time limit is reached
if ($job.state -eq 'Running') {
# Job timed out
$job | Stop-Job | Remove-Job
$job = $null
$output = "TIMED OUT"
# If job managed to finalize in time
}Else{
# Finished on time
$job | Stop-Job
$output = Receive-Job $job
$job | Remove-Job
}
# Write the output to host
Write-Host "output is: $output"
output is: TIMED OUT
# define the time limit in seconds
$TimeoutLimitSeconds = 4
# Define the job, start job
$job = Start-Job -ScriptBlock {420+69 ; start-sleep -Seconds 3}
# Await job
$job | Wait-Job -Timeout ( $TimeoutLimitSeconds ) | Out-Null
# If job doesn't finish before the time limit is reached
if ($job.state -eq 'Running') {
# Job timed out
$job | Stop-Job | Remove-Job
$job = $null
$output = "TIMED OUT"
# If job managed to finalize in time
}Else{
# Finished on time
$job | Stop-Job
$output = Receive-Job $job
$job | Remove-Job
}
# Write the output to host
Write-Host "output is: $output"
output is: 489

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

Adobe flash player powershell remote install problem

I'm attempting to develop a script with PowerShell to remotely install/update flash player for multiple machines. No matter what I do, I can't get the install to work properly at all. I'm very limited with my tools so I have to use PowerShell, and the MSI install of Flashplayer. I'll post my script below, any help at all would be greatly appreciated.
$Computers = Get-Content C:\Users\name\Desktop\flash.txt
(tried these 3 methods to install none work)
$install = #("/a","/i", "\\$Computer\c$\temp\flash\install_flash_player_32_plugin.msi", "/qn","/norestart")
Invoke-Command -ComputerName $Computer -ScriptBlock {Start-Process "Msiexec" -arg "$using:install" -Wait -PassThru} -Filepath msiexec.exe
#This returns with "invoke-command: parameter set cannot be resolved using the specified named parameters"
Invoke-Command -ComputerName $computer -ScriptBlock {Start-Process -Filepath msiexec.exe "$using:install" -Wait -PassThru} -Filepath msiexec.exe
#this returns the same error.
Invoke-Command -ComputerName $Computer -ScriptBlock {start-process msiexec -argumentlist #('/a','/i','"\\$Computer\c$\temp\flash\install_flash_player_32_plugin.msi"','/qn')}
#this seemingly skips the install entirely.
I've used similar scripts for other programs and had no problems installing them, but none of the methods I use or have researched are working properly.
This should do the trick, I'll explain why it wasn't working bellow:
$Computers = Get-Content C:\Users\name\Desktop\flash.txt
$params = '/i <path to AcroPro.msi> LANG_LIST=en_US TRANSFORMS="1033.mst" /qb'
$Computers | % {
Invoke-Command -ScriptBlock {
Param(
[Parameter(Mandatory=$true,Position=0)]
[String]$arguments
)
return Start-Process msiexec.exe -ArgumentList $arguments -Wait -PassThru
} -ComputerName $_ -ArgumentList $params
}
So, it wasn't working because the ScriptBlock on Invoke-Command cant see variables that you've declared on your powershell session, think of it like you are walking to that remote computer and inputting that code by hand, you wont have the value (metaphor).
I did a few more changes:
I moved all params into 1 single string, no need to have them in array.
Added $Computers | to iterate through computer names.
Removed FilePath as this is meant to be used differently, documentation(Example #1).
Set $MinutesToWait to whatever amount of minutes you want.
No need to try to pass msiexec, as it comes with windows the default path is "C:\WINDOWS\system32\msiexec.exe"
Added a return even though its never necessary, but to make it more readable and to show you intent to return the output of the msiexec process.
Replaced \\$Computer\c$ with C:\ as there's no need to use a network connection if you are pointing to the host you are running the command in/
Hope it helps, good luck.
EDIT:
So, as you mentioned the pipeline execution gets stuck, I had this issue in the past when creating the computer preparation script for my department, what I did was use jobs to create parallel executions of the installation so if there's a computer that for some reason is slower or is just flat out stuck and never ends you can identify it, try the following as is to see how it works and then do the replaces:
#region ######## SetUp #######
$bannerInProgress = #"
#######################
#Jobs are still running
#######################
"#
$bannerDone = #"
##################################################
#DONE see results of finished installations bellow
##################################################
"#
#VARS TO SET
$MinutesToWait = 1
$computers = 1..10 | % {"qwerty"*$_} #REPLACE THIS WITH YOUR COMPUTER VALUES (Get-Content C:\Users\name\Desktop\flash.txt)
#endregion
#region ######## Main #######
#Start Jobs (REPLACE SCRIPTBLOCK OF JOB WITH YOUR INVOKE-COMMAND)
$jobs = [System.Collections.ArrayList]::new()
foreach($computer in $computers){
$jobs.Add(
(Start-Job -Name $computer -ScriptBlock {
Param(
[Parameter(Mandatory=$true, Position=0)]
[String]$computer
)
Sleep -s (Get-Random -Minimum 5 -Maximum 200)
$computer
} -ArgumentList $computer)
) | Out-Null
}
$timer = [System.Diagnostics.Stopwatch]::new()
$timer.Start()
$acceptedWait = $MinutesToWait * 60 * 1000 # mins -> sec -> millis
$running = $true
do {
cls
$jobsRunning = $jobs | Where-Object State -EQ 'Running'
if ($jobsRunning) {
Write-Host $bannerInProgress
foreach ($job in $jobsRunning) {
Write-Host "The job `"$($job.Name)`" is still running. It started at $($job.PSBeginTime)"
}
Sleep -s 3
} else {
$running = $false
}
if($timer.ElapsedMilliseconds -ge $acceptedWait){
$timer.Stop()
Write-Host "Accepted time was reached, stopping all jobs still pending." -BackgroundColor Red
$failed = #()
foreach($job in $jobsRunning){
$output = $job | Receive-Job
$failed += [PsCustomObject]#{
"ComputerName" = $job.Name;
"Output" = $output;
}
$job | Remove-Job -Force
$jobs.Remove($job)
}
$failed | Export-Csv .\pendingInstallations.csv -NoTypeInformation -Force
$running = $false
}
}while($running)
Write-host $bannerDone
$results = #()
foreach($job in $jobs){
$output = $job | Receive-Job
$results += [PsCustomObject]#{
"ComputerName" = $job.Name;
"Output" = $output;
}
}
$results | Export-Csv .\install.csv -NoTypeInformation -Force
#endregion
This script will trigger 10 jobs that only wait and return its names, then the jobs that got completed in the time that you set are consider correct and the ones that didn't are consider as pending, both groups get exported to a CSVfor review. You will need to replace the following to work as you intended:
Add $params = '/i <path to AcroPro.msi> LANG_LIST=en_US TRANSFORMS="1033.mst" /qb' in the SetUp region
Replace the declaration of $computers with $computers = Get-Content C:\Users\name\Desktop\flash.txt
Replace the body of Start-Job scriptblock with Invoke-command from thew first snippet of code in this answer.
you should end-up with something like:
.
.code
.
$params = '/i <path to AcroPro.msi> LANG_LIST=en_US TRANSFORMS="1033.mst" /qb'
#VARS TO SET
$MinutesToWait = 1
$computers = Get-Content C:\Users\name\Desktop\flash.txt
#endregion
#region ######## Main #######
#Start Jobs
$jobs = [System.Collections.ArrayList]::new()
foreach($computer in $computers){
$jobs.Add(
(Start-Job -Name $computer -ScriptBlock {
Param(
[Parameter(Mandatory=$true, Position=0)]
[String]$computer
)
Invoke-Command -ScriptBlock {
Param(
[Parameter(Mandatory=$true,Position=0)]
[String]$arguments
)
return Start-Process msiexec.exe -ArgumentList $arguments -Wait -PassThru
} -ComputerName $computer -ArgumentList $params
} -ArgumentList $computer)
) | Out-Null
}
.
. code
.
I know it looks like a complete mess, but it works.
Hope it helps.

Using Start-Job on script

Using a script in PowerShell to recursivly pass through all folders on multiple NAS boxes to display every folder with its full path in an Out-File.
Using the Get-FolderEntry script I found here.
Since I have multiple NAS boxes with more then 260 chars in the filename/pathlength I figured I'd use multithreading to speed the process up.
Code:
. C:\Users\mdevogea\Downloads\Get-FolderEntry.ps1
# list with the servers
$Computers = Get-Content C:\Users\mdevogea\Desktop\servers.txt
# scriptblock calling on get-FolderEntry
$sb = {
param ($Computer, $fname)
C:\Users\mdevogea\Downloads\Get-FolderEntry.ps1 -Path $Computer |
fl | Out-File -Append -Width 1000 -FilePath $fname
}
foreach($Computer in $Computers)
{
$name = $Computer.Replace("\", "")
$fname = $("C:\Users\mdevogea\Desktop\" + $name + ".txt")
#Get-FolderEntry -Path $Computer | fl | Out-File -Append -Width 1000 $fname
$res = Start-Job $sb -ArgumentList $Computer, $fname
}
# Wait for all jobs
Get-Job
while(Get-Job -State "Running")
{
Write-Host "Running..."
Start-Sleep 2
}
# Get all job results
Get-Job | Receive-Job | Out-GridView
So far:
I either get empty files with the correct naming of the file.
I get the correct named file with the code of Get-FolderEntry in it.
I get errors depend on what I pass along to the scriptblock.
In short, it's probably stupid but don't see it.
Found it eventually myself after some trial and error:
. C:\Users\mdevogea\Downloads\Get-FolderEntry.ps1
# list with the servers
$Computers = Get-Content C:\Users\mdevogea\Desktop\servers.txt
# scriptblock calling on get-FolderEntry
$sb = {
Param ($Computer, $fname)
. C:\Users\mdevogea\Downloads\Get-FolderEntry.ps1
(Get-FolderEntry -Path $Computer | fl | Out-File -Append -Width 1000 -FilePath $fname)
}
foreach ($Computer in $Computers)
{
$name = $Computer.Replace("\", "")
$fname = $("C:\Users\mdevogea\Desktop\" + $name + ".txt")
$res = Start-Job $sb -ArgumentList $Computer, $fname
}
# Wait for all jobs
Get-Job
while (Get-Job -State "Running")
{
Write-Host "Running..."
Start-Sleep 2
}
# Get all job results
Get-Job | Receive-Job | Out-GridView
Thanks a lot Ansgar for pointing my in the right direction!

How to put a job on wait in powershell

HI every one I Have the following scripts which i am working on but not sure how to put a wait for a zip to finish and than move on to the next block of code. following are the two scripts which i am using. The first script is a backup script which is calling another script for zipping
$Date = Get-Date
$folder_date = $Date.ToString("yyyy-MM-dd_HHmm")
$backup_folder_name = 'c:\Russia\' + $folder_date
$V3_folder_name = 'C:\121RussiaScaled\products'
$Admin_folder_name = 'C:\inetpub\wwwroot\admin'
$Tablet_folder_name = 'C:\inetpub\wwwroot\tabl'
$Service_folder_name = 'C:\Russia\dll'
if (!(Test-Path -path $backup_folder_name)) {
New-Item $backup_folder_name -type directory
} # if (!(Test-Path -path D:Data))
if ((Test-Path -path $V3_folder_name)) {
Start-job -scriptblock {gi $V3_folder_name | .\Library\out-zip.ps1
$backup_folder_name\V3.zip $_}
Wait-job -Id $Job.Id
}
if ((Test-Path -path $Service_folder_name)) {
$Job = Start-job -scriptblock {gi $Service_folder_name | .\Library\out-zip.ps1
$backup_folder_name\Services.zip $_}
Wait-job -Id $Job.Id
}
if ((Test-Path -path $Admin_folder_name)) {
$Job = Start-job -scriptblock {gi $Admin_folder_name | .\Library\out-zip.ps1
$backup_folder_name\admin.zip $_}
Wait-job -Id $Job.Id
}
if ((Test-Path -path $Tablet_folder_name)) {
$Job = Start-job -scriptblock {gi $Tablet_folder_name | .\Library\out-zip.ps1
$backup_folder_name\tablet.zip $_}
Wait-job -Id $Job.Id
}
This is my out.zip script
$path = $args[0]
$files = $input
write-output $path
if (-not $path.EndsWith('.zip')) {$path += '.zip'}
if (-not (test-path $path)) {
set-content $path ("PK" + [char]5 + [char]6 + ("$([char]0)" * 18))
}
$ZipFile = (new-object -com shell.application).NameSpace($path)
$files | foreach {$zipfile.CopyHere($_.fullname)}
while using the above script i am getting an error "Start-Job missing an argument for the paratmeter script block."
or is there another way so that i can put a wait for these zips to finish one by one
Try
$Job = Start-Job -ScriptBlock .....
Wait-Job -Id $Job.Id
For the ScriptBlock error, try specifying the starting brace at the same line like:
$Job = Start-Job -ScriptBlock {
# Job code here
}