Simple PowerShell parallel command execution - powershell

I have two scripts I wish to run in parallel. I have a driver script I wish to use to execute the two parallel scripts.
The method I'm using of course does not run in parallel. I have looked all over the google universe, and at using a workflow, but I can't get that to work either.
Driver Script:
Clear-Host
$a = 4
$b = 29
$command1 = "C:\JUNK\Code\PowerScript\CS1.ps1 $a"
$command2 = "C:\JUNK\Code\PowerScript\CS2.ps1 $b"
#Invoke-Expression $command1
#Invoke-Expression $command2
The scripts to get called resemble this:
#CS1.ps1
Param(
[string]$build
)
$ADATE = Get-Date
$DESTLOG1="C:\JUNK\Code\PowerScript\LOGS\status1.log"
$RunDefintion1 = ($build, $ADATE -join",")
Add-Content $DESTLOG1 $RunDefintion1

From powershell 2 upwards you could use the Start-Job command, something like this ( untested, might need some adjustments )
Start-Job { C:\JUNK\Code\PowerScript\CS1.ps1 $a }
Start-Job { C:\JUNK\Code\PowerScript\CS2.ps1 $b }

Related

Increment a variable in a job

I want to increment a variable in a PowerShell job with a number defined before the job starts.
I tried with a global variable but it didn't work, so now I try to write in a file and load it in my job but that didn't work either.
I summarize my loop:
$increment = 1
$Job_Nb = 1..3
foreach ($nb in $Job_Nb) {
$increment > "C:\increment.txt"
Start-Job -Name $nb -ScriptBlock {
$increment_job = Get-Content -Path "C:\increment.txt"
$increment_job
}
$increment++
}
I want my 2 variables $increment_job and $increment to be equal.
I obtain the good result with the command Wait-Job, like that:
$increment = 1
$Job_Nb = 1..3
foreach ($nb in $Job_Nb) {
$increment > "C:\increment.txt"
Start-Job -Name $nb -ScriptBlock {
$increment_job = Get-Content -Path "C:\increment.txt"
$increment_job
} | Wait-Job | Receive-Job
$increment++
}
But I can't wait each job to finish before starting the next, it's too long... I need to execute a lot of jobs in the background.
For me, even $nb, $increment and $increment_job can be equal.
If it can help you to understand, a really simple way to put it:
$nb = 1
$Job_Nb = 1..3
foreach ($nb in $Job_Nb) {
Start-Job -Name $nb -ScriptBlock {$nb}
$nb++
}
If you want the two variables to be equal, you can just pass $increment into your script block as an argument.
# Configure the Jobs
$increment = 1
$Job_Nb = 1..3
Foreach ($nb in $Job_Nb) {
Start-Job -Name $nb -ScriptBlock {
$increment_job = $args[0]
$increment_job
} -ArgumentList $increment
$increment++
}
# Retrieve the Jobs After Waiting For All to Complete
Wait-Job -Name $job_nb | Receive-Job
The problem with your initial approach as you have discovered is that PowerShell processes the entire loop before a single job completes. Therefore, the job doesn't read the increment.txt file until after its contents are set to 3.
Passing values into the -ArgumentList parameter of a script block without a parameter block will automatically assign the arguments to the $args array. Space delimited arguments will each become an element of the array. A value not space-separated can simply be retrieved as $args or $args[0] with the difference being $args will return a type of Object[] and $args[0] will return the type of the data you passed into it.
Obviously, you do not need to wait for all jobs to complete. You can just use Get-Job to retrieve whichever jobs you want at any time.

How do you pass values to another script under a foreach statement?

I want to run 1 script multiple times for each port select via a range and pass through the port details in which it needs to use to connect, I was trying to use the following:
$availableports = 7000..7050
while ($availableports -notcontains $SPort) {
[string]$SPort= Read-Host -Prompt 'S Ports'
}
while ($availableports -notcontains $FPort) {
[string]$FPort= Read-Host -Prompt 'F Ports'
}
$massport = ($SPort)..($FPort)
foreach ($Port in $massport) {
C:\PShell-Projects\Firmware\SCP-FMUPv2.ps1 -Port "$Port"
}
This works but will not move on to the next port until the referenced script has finished.
I would like to run them all in parallel.
I tried
$arg = #("-Port", $portm)
Start-Job -FilePath C:\PShell-Projects\Firmware\SCP-FMUPv2.ps1 -ArgumentList $arg
but the job becomes blocked and when I used Receive-Job it asks for the port to open a connection to, which should have been sent as part of the loop.
I seem to be missing some key information, but don't know where to start and well when looking up the information nothing seems to be standing out.
This works for me:
$jobs = #()
foreach ($Port in $massport) {
$jobs += start-job -FilePath "job.ps1" -ArgumentList #($port)
}
Receive-Job $jobs -Wait
(no named arguments in ArgumentList).
You can also take a look at Invoke-Parallel function, which simplifies running parallel tasks.
You could probably use -asjob after your C:\PShell-Projects\Firmware\SCP-FMUPv2.ps1 -Port "$Port command to have it just do them all simultaneously.
If the ps1 won't take -asjob, you might be able to wrap it with invoke-command {}

Sending each line from text file to remote computer?

This is my script to whitelist IP in a remote system. I want my script to read data from a text file on my local system and then foreach line I want to execute the scriptblock on the remote server.
Text file looks like this:
url1
url2
url3
Here is my code:
Invoke-Command -ComputerName $($server.text) -Credential ciqdev\riteshthakur {
param($a, $b, $c, $url)
Set-Location "C:\Windows\System32\inetsrv"
$url | foreach {
.\appcmd.exe set config "$_" -section:system.webServer/security/ipSecurity /+"[ipAddress='$($a)',allowed='$($c)',subnetMask='$($b)']" /commit:apphost
}
} -ArgumentList $ip.text, $mask.text, $allowed, (get-content "File location")
This adds provided ip to all the pages in all the websites in IIS. Please help.
EDIT: Improved efficiency by generating the command dynamically, and invoking it once.
I'd suggest using a technique similar to the following, where you read in the text file as an array of lines, and then iterate over each line, generating the commands that you want to run on the remote system.
Once you've generated the command as a string, you simply call the static [ScriptBlock]::Create() method to create a ScriptBlock object, based on the command string, and pass that into Invoke-Command.
I'd suggest you get familiar with the concept of PowerShell Splatting, which I talk about in this YouTube video: https://www.youtube.com/watch?v=CkbSFXjTLOA. It's a really powerful concept, and helps make your code easier to read. The example code below uses PowerShell Splatting (available in PowerShell 3.0 and later).
### Read the text file on the local system
$Whitelist = Get-Content -Path IPwhitelist.txt;
### Generate the stub for the remote command
$RemoteCommand = #'
param($a, $b, $c)
Set-Location -Path C:\Windows\System32\inetsrv
'#
### Add more commands to the remote command
foreach ($Line in $Whitelist) {
$RemoteCommand += '{1}.\appcmd.exe set config "{0}" -section:system.webServer/security/ipSecurity /+"[ipAddress=''$($a)'',allowed=''$($c)'',subnetMask=''$($b)'']" /commit:apphost' -f $Line, "`n";
}
### Invoke the entire remote command (once)
$Command = #{
ComputerName = $Server.Text
Credential = Get-Credential -Credential ciqdev\riteshthakur
ScriptBlock = [ScriptBlock]::Create($RemoteCommand);
ArgumentList = #($ip.text, $mask.text, $allowed)
}
Invoke-Command #Command;
Just read the file using the Get-Content cmdlet and iterate over each item using the Foreach-Object cmdlet:
Invoke-Command -ComputerName $($server.text) -Credential ciqdev\riteshthakur {
param($a, $b, $c, $urls)
Set-Location "C:\Windows\System32\inetsrv"
$urls | Foreach {
.\appcmd.exe set config $_ -section:system.webServer/security/ipSecurity /+"[ipAddress='$($a)',allowed='$($c)',subnetMask='$($b)']" /commit:apphost
}
} -ArgumentList $ip.text, $mask.text, $allowed, (Get-Content 'Path_to_your_file')

remote Multiple invocations using Powershell

I appreciate you taking the time to read this.
My issue is as follows: I'm trying to create a program that uses powershell to do the following:
Take a table generated outside of powershell
Loop calls to a powershell script with the parameters from the table
The powershell script calls a special type of .cmd file and then runs commands on it that are located in a different shared location.
Now my problem is with the 3rd point.
I'm currently using the following to call my script (and the arguements are just hard coded to get it working, they'll be generated by the calls from step 2 later on):
powershell.exe -ExecutionPolicy Bypass -Command {invoke-command -file \\sharedlocation\test5.ps1 -computername server1121 -argumentlist 7058,Jason}
The inside of test5.ps1 is currently:
param(
[Parameter(Mandatory=$true)]
[string] $Var1,
[Parameter(Mandatory=$true)]
[string] $Var2
)
$CommandsPath = "\\sharedlocation\testcommands.cmd"
$path = "C:\"+$Var1+"\TOOLS\"+$Var2+"launchtool.cmd"
$scriptPath = [scriptblock]::Create($path)
$out | invoke-command {PARAM($MyArg) $scriptPath } -ArgumentList $CommandsPath
I've also tried using
$CommandsPath = "\\sharedlocation\testcommands.cmd"
$path = "C:\"+$Var1+"\TOOLS\"+$Var2+"\launchtool.cmd & " + $CommandsPath
$scriptPath = [scriptblock]::Create($path)
$out | invoke-command {$scriptPath }
I've also tried to call with hardcoded testcommands instead of them being in a file.
Now my problem is in both cases, it DOES run launchtool.cmd, but it doesn't pass the testcommands.cmd file.
However when on the machine i run
C:\7058\TOOLS\Jason\launchtool.cmd & \\sharedlocation\testcommands.cmd
It works fine.
Any ideas what I'm doing wrong?
Try, invoke-expression "cmd.exe /c C:\7058\TOOLS\Jason\launchtool.cmd & \sharedlocation\testcommands.cmd"
cmd.exe /c is my best way to ensure consistency between cmd and powershell
Is the UNC Path accessible from powershell? Copy the testcommands.cmd to a local path and try if it works!
$CommandsPath = "\\sharedlocation\testcommands.cmd"
if(Test-Path $CommandsPath)
{
$path = "C:\"+$Var1+"\TOOLS\"+$Var2+"\launchtool.cmd & " + $CommandsPath
$scriptPath = [scriptblock]::Create($path)
$out | invoke-command {$scriptPath }
}

Possible to use -WhatIf and the invocation operator (&)?

Is it possible to use the -WhatIf argument when executing external commands? I want to be able to run a script with -WhatIf and have it print out a full list of all the external commands and arguments it's going to run without actually running them.
I've tried doing stuff like the following:
Function Invoke-Checked
{
param([ScriptBlock]$s)
if ($PSCmdlet.ShouldProcess($s.ToString(), "Execute"))
{
Invoke-Command $s
}
}
But that won't expand any variables that are present in the scriptblock - doing something like:
$s = { & dir $test }
Invoke-Checked $s
just prints
Performing the operation "Execute" on target " & dir $test ".
not particularly helpful.
Is there any way to do what I want?
First of all - you need to make sure that your 'wrapper' function supports WhatIf.
Another thing: you can expand the scriptBlock, but I'm not really convinced that is smart thing to do: e.g. if $test = 'Some path with spaces', it would stop working after expansion.
That being said: here are two options that work for me: using GetNewClosure() method on scriptBlock, and expanding whole thing:
function Invoke-ExpandedChecked {
[CmdletBinding(
SupportsShouldProcess = $true,
ConfirmImpact = 'Medium'
)]
param([ScriptBlock]$ScriptBlock)
$expanded = $ExecutionContext.InvokeCommand.ExpandString($ScriptBlock)
$script = [scriptblock]::Create($expanded)
if ($PSCmdlet.ShouldProcess($script.ToString(), "Execute"))
{
& $script
}
}
function Invoke-Checked {
[CmdletBinding(
SupportsShouldProcess = $true,
ConfirmImpact = 'Medium'
)]
param([ScriptBlock]$ScriptBlock)
$newClosure = $ScriptBlock.GetNewClosure()
if ($PSCmdlet.ShouldProcess($newClosure.ToString(), "Execute"))
{
& $newClosure
}
}
$test = '.\DSCDemo.ps_'
$s = { cmd /c dir $test}
Invoke-Checked $s -WhatIf
Invoke-Checked $s
Invoke-ExpandedChecked $s -WhatIf
Invoke-ExpandedChecked $s
And an example of results for path with spaces:
$test = 'C:\Program Files'
Invoke-Checked $s
Invoke-ExpandedChecked $s
Works fine for one with new enclosure. With expanded:
cmd : File Not Found
At line:1 char:2
+ cmd /c dir C:\Program Files
I'm going to interpret the question to mean, "how do I use -whatif with running external commands?", since that's how I found this question.
# myscript.ps1
[cmdletbinding(SupportsShouldProcess=$True)]
Param($path) # put Param() if no parameters
if ($pscmdlet.ShouldProcess($Path, 'creating folder')) { # not -whatif
cmd /c mkdir $path
}
.\myscript foo -whatif
What if: Performing the operation "creating folder" on target "foo".