within powershell I'd like to learn the best way to call a variable to a start job so I don't have to edit the script for each server as it will be specific based on the client I've placed my script on.
$Servername = 'Server1'
$pingblock = {
pathping $servername | Out-File C:\client\PS\ServerPing.TXT
}
start-job $pingblock
when I run my code above I just get a file with the help as if I forgot the specify the $servername.
Use the -ArgumentList parameter on Start-Job e.g.:
Start-Job -Scriptblock {param($p) "`$p is $p"} -Arg 'Server1'
In your case:
$pingblock = {param($servername) pathping $servername | Out-File C:\...\ServerPing.txt}
Start-Job $pingblock -Arg Server1
To complement Keith Hill's helpful answer with a PSv3+ alternative:
The $using: scope modifier can be used to reference the values of variables in the caller's scope inside the script block passed to Start-Job, as an alternative to passing arguments (by default, a script block executed as a background job does not see any of the caller's variables or other definitions):
$Servername = 'Server1'
Start-Job { "Target: " + $using:ServerName } | Receive-Job -Wait -AutoRemoveJob
The above yields:
Target: Server1
Note:
The same technique can be used with:
Invoke-Command for remote execution - see this question.
Start-ThreadJob, available by default in PowerShell (Core) v6+, installable on demand in Windows PowerShell.
ForEach-Object -Parallel, available in PowerShell (Core) v7+ only.
Note that, as with -ArgumentList (-Args), it is only variable values that are being passed, not the variables themselves; that is, you cannot modify the caller's variables.[1]
[1] However, the thread-based concurrency features - Start-ThreadJob and ForEach-Object Parallel - permit indirect modification, namely if the variable value at hand happens to be an instance of a (mutable) .NET reference type, such as a hashtable, in which case the object that that the variable "points to" can be modified (if it is mutable). Note that taking advantage of that requires additional, nontrivial effort to make the concurrent modifications thread-safe, such as by use of concurrent (synchronized) collections - see this answer - and/or explicit locking of individual objects - see this answer.
Some other ways, $args and $input. This goes for invoke-command too, which I think uses the same mechanism. The $input method works in an unexpected way with arrays.
start-job { $args[0] } -args hi | receive-job -wait -auto
hi
echo hi | start-job { $input } | receive-job -wait -auto
hi
echo hi there | start-job { $input.gettype() } | receive-job -wait -auto
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
False False <GetReadEnumerator>d__20 System.Object
For arrays, it's probably better to use a foreach-object (%) loop instead, so it runs on each array item in parallel. See also start-threadjob or foreach-object -parallel in powershell 7. There's actually no -throttlelimit option to start-job, so use with care.
echo yahoo.com facebook.com |
% { $_ | start-job { test-netconnection $input } } |
receive-job -wait -auto | select * -exclude runspaceid,pssourcejob* | ft
ComputerName RemoteAddress ResolvedAddresses PingSucce
eded
------------ ------------- ----------------- ---------
yahoo.com 74.6.143.25 {74.6.143.25,...} True
facebook.com 31.13.71.36 {31.13.71.36} True
Related
I have a Powershell function KillChildren that works on Windows, but not in bash (on my Mac). The problem is that it uses the windows-only Get-CimInstance cmdlet.
Here is the windows-centric version (whose authorship is unknown to me):
function KillChildren {
Param (
[Parameter(Position = 0, Mandatory, ValueFromPipeline)]
[int[]]$ProcessId,
[switch]$PassThru
)
process {
foreach ($p in $ProcessId) {
Get-CimInstance -Class Win32_Process -Filter "ParentProcessId = '$p' AND NOT Name LIKE 'conhost%'" -Property ProcessId |
Select-Object -ExpandProperty ProcessId |
KillChildren -PassThru |
ForEach-Object {
Stop-Process -Id $_ -ErrorAction Ignore -Force
}
if ($PassThru -and $PassThru.IsPresent) {
$p
}
}
}
}
How would you write a version of this function that would work in Unix?
The Unix logic-path can't use Get-CimInstance, because that's a windows-specific cmdlet.
This is in an multi-platform repo, and I'm hoping to maintain only a single set of PS scripts, instead of parallel PS and batch scripts.
The following should work on Unix-like platforms to get a given process' child processes (with the given process identified via its PID (process ID), $p) :
Get-Process | Where-Object { $_.Parent.Id -eq $p }
It also works on Windows in principle, assuming you're running PowerShell (Core) 7+ rather than Windows PowerShell (where the Get-Process output objects have no .Parent property), but there's a pitfall (which your Get-CimInstance solution already (mostly) accounts for - see next section):
If the process whose child processes to kill is the current process and you're running in a regular console window (conhost.exe), you must exclude the automatically created conhost child process from the results, as killing it too would close the terminal window and therefore kill the target process as well.
This is not a concern in Windows Terminal.
The following is a cross-edition, cross-platform solution:
# Assumes that $p contains the PID (process ID) of interest.
$(if ($env:OS -eq 'Windows_NT') {
(Get-CimInstance -Class Win32_Process -Filter "ParentProcessId = $p AND Name != 'conhost.exe'" -Property ProcessId).ProcessId
} else {
(Get-Process | Where-Object { $_.Parent.Id -eq $p }).Id
}) # | ...
Note:
At least hypothetically, eliminating all conhost.exe child processes isn't fully robust, because it bears the risk of false positives (excluding child processes that shouldn't be excluded), given that it's possible to explicitly launch conhost.exe child processes with commands such as conhost cmd /k.
Can someone tell me why this invoke-command is not exporting all the variables?
icm -computername Server1 -scriptblock {
$A=$env:username;$B=hostname;$C=$env:userdomain;echo $A,$B,$C
} |
% {$_ | out-file "c:\temp\$($_.pscomputername).txt"}
The text file only contains "$C=$env:userdomain" variable. How can i export $A,$B,$C variables to the text file while keeping the $_.pscomputername as the name of the exported file?
thank you
The problem with your approach is that each individual object output by icm (Invoke-Command) causes the output file passed to Out-File in the % (ForEach-Object) script block to be rewritten, so that you end up with only the last one in the file.
Toni makes good points about the values you're returning from your remote script block, and about collecting the information in a single object, which solves your problem.
Applying the same idea to the multiple values you're trying to return, you can return them as a single array, as a whole, so that you only have one object to write, which contains all the information.
To that end, you not only must construct an array, but also prevent its enumeration on output, which is most easily done by using the unary form of , to create an aux. wrapper array (alternatively, use Write-Output -NoEnumerate; see this answer for more information).
icm -computername Server1 -scriptblock {
# Return the values as a single array, wrapped in
# in a aux. array to prevent enumeration.
, ($env:username, (hostname), $env:userdomain)
} |
% {
# Now each targeted computer returns only *one* object.
$_ | out-file "c:\temp\$($_.pscomputername).txt"
}
This is more typical usage. Instead of format-table, you can pipe to export-csv. It runs in parallel on both computers.
icm -computername com001,com002 -scriptblock {
[pscustomobject]#{Username=$env:username;
Hostname=hostname;
Userdomain=$env:userdomain}
} | format-table
Username Hostname Userdomain PSComputerName RunspaceId
-------- -------- ---------- -------------- ----------
js-adm COM001 AD com001 2d660bb5-761c-4e79-9e4f-c3b98f5f8c61
js-adm COM002 AD com002 7544a8ff-e419-49c2-9fc3-2a92f50c1424
This Invoke-Parallel is taken from here
Below is my script block
$scriptblock ={
get-service -ComputerName $parameter.computername | where {$_.name -eq $parameter.serviceName }
}
below is a custom object
$pscustomobject = #()
$pscustomobject += [pscustomobject]#{
Computername ='server1'
Servicename ="service1"
}
$pscustomobject += [pscustomobject]#{
Computername ='server2'
Servicename ="service2"
}
I tried using Invoke-Parallel using below method ,but this doesnt work
Invoke-Parallel -ScriptBlock $scriptblock -Parameter $pscustomobject
"server1","server2" | Invoke-Parallel -ScriptBlock $scriptblock -Parameter $pscustomobject
Few services exist in few servers and on few others,they dont,so created a custom object which tightly maps services to servers.
Any ideas would be greatly helpfull
The -Parameter switch makes the properties available to each item in the script block. I think if you make a small modification like this, then it should work:
$scriptblock ={
$thisServer = $_
$thisServerParams = $parameter.Where({$_.Computername -eq $thisServer})
get-service -ComputerName $thisServerParams.computername | where {$_.name -eq $thisServerParams.serviceName }
}
We can use this code above to find the right properties we should use for each server. With the code before, we were effectively using Invoke-Parallel to run the same command on all servers each time. And the logic wouldn't work for matching services in the Where block because it would compute to
$pscustomobject.ServiceName
service1
service2
PS C:\Users\Stephen> $pscustomobject.ServiceName.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
An individual service coming back from Get-Service would never equal to System.Array
Thanks to Foxdeploy,found one more way to achieve what i need,below method is more flexible for me,since..when i pass two services,one service may take a lot of time to start or stop and may hold data from another service as well
$pscustomobject | Invoke-Parallel -ScriptBlock {
$computername = $_.computername
$servicename = $_.servicename
get-service -ComputerName $($computername) | where {$_.name -eq $($serviceName) }
}
I might be wrong but I believe this would be faster, using builtin Powershell cmdlet and also looks cleaner imo:
$servers=#(
'server1'
'server2'
)
$scriptblock={
switch($env:COMPUTERNAME)
{
'server1'{Get-Service service1}
'server2'{Get-Service service2}
}
}
$result=Invoke-Command -ComputerName $servers -ScriptBlock $scriptblock
Edit:
For the sake of demonstration, here I'm waiting 10 seconds and then resolving a easy mathematical equation on 7 servers depending on their name 3 pairs of them start with the same name. If this was a linear invocation it should've taken 10+10+10+10 seconds yet it only took 15 seconds for all of them. This demonstrates that you can execute different commands on different remote computers at the same time.
This question already has answers here:
If using Test-Connection on multiple computers with -Quiet how do I know which result is for which computer?
(2 answers)
Closed 2 years ago.
It's my first post here, I'm tring to write scripts on PS on my own, now my target is to write script that checks if computer is online at network, for example: test-Connection 192.168.0.1, 2, 3 etc. Doing this one by one on loop for takes some time if some computers are offline, I've found some tutorials on this site to use -AsJob param, but I'm not really Sure how could it work. I mean I'd like to output every checked PC to excel, so i need if operator. eg:
if (Job1 completed successfull (computer pings)){
do smth}...
I need to get output from Job boolean (true/false), but one by one. I'm taking my first steps in PS, I've made program that checks it one by one in for loop, but as i said it take some time till my excel file fill...
I can see, that AsJob makes working more effective and I think it's important to understand it
Thanks and sorry for bad text formatting, by the time I'll go on with this!
In your example, in the Start-Job scriptblock you are trying to access $_ which is not available in the codeblock scope. If you replace $_ with $args[0] it should work since you are passing in the $ip value as an argument
Your Example
$ipki = Get-Content 'C:\Users\pchor\Desktop\ipki.txt'
foreach ($ip in $ipki) {
Start-Job -Name "$ip" -ScriptBlock {
Test-Connection $_ -Count 1 # <---- replace $_ with $args[0]
} -ArgumentList $_ # <----- change $_ to $ip
}
You'll probably also want to wait for all the jobs to finish. I recommend something like this
$computers = #(
'www.google.com'
'www.yahoo.com'
)
$jobs = $computers |
ForEach-Object {
Start-Job -ScriptBlock {
[pscustomobject]#{
Computer = $using:_
Alive = Test-Connection $using:_ -Count 1 -Quiet
}
}
}
# Loop until all jobs have stopped running
While ($jobs |
Where-Object { $_.state -eq 'Running' }) {
"# of jobs still running $( ($jobs | Where-Object {$_.state -eq 'Running'}).Count )";
Start-Sleep -Seconds 2
}
$results = $jobs | Receive-Job | Select-Object Computer, Alive
$results | Format-Table
Output
Computer Alive
-------- -----
www.google.com True
www.yahoo.com True
To modify the properties to what you want there are different ways of doing this. Easiest in this case is probably to use a calculated property
$newResults = $results |
Select-Object Computer,
#{Label = 'State'; Expression = { if ($_.Alive) { 'Online' } else { 'Offline' } } }
Objects will now look like this (I added another fake address to illustrate offline state)
Computer State
-------- -----
www.google.com Online
www.yahoo.com Online
xxx.NotAValidAddress.xxx Offline
You can then export the objects to csv using Export-csv
$newResults | Export-Csv -Path c:\temp\output.csv
I'm writing a Powershell script in which I have to create several temporary files. I stumbled upon a .net class that sounds very useful to manage this task:[System.CodeDom.Compiler.TempFileCollection].
However for some strange reason I'm not able to create an object.
Here's what I've tried to do:
$a = new-object CodeDom.Compiler.TempFileCollection -argumentlist "C:\folder1\"
$a is empty after this call.
However if I do this:
(new-object CodeDom.Compiler.TempFileCollection -argumentlist "C:\folder1\").tempdir
It returns:
C:\folder1\
This means an object has been created. I just seem to be unable to save it into a variable! Does anybody have any idea why that is?
Another strange thing:
new-object CodeDom.Compiler.TempFileCollection -argumentlist "C:\folder1\" | gm
Returns:
Name MemberType Definition
---- ---------- ----------
AddExtension Method string AddExtension(string fileExtension), string
...
However this returns an exception (Get-Member : No object has been specified to the get-member cmdlet.) :
(new-object CodeDom.Compiler.TempFileCollection -argumentlist "C:\folder1\") | gm
I'm sorry for articulating this problem so badly, I don't know how to explain it better.
Any help would be much appreciated.
TL;DR: it's not empty.
How are you verifying that $a is empty? I think if you'll actually try just using it, it will work fine:
PS> $a = New-Object CodeDom.Compiler.TempFileCollection -argumentlist "C:\folder1\"
PS> $a.TempDir
C:\folder1\
PS> $a.AddFile("test1.txt",$false)
PS> $a.AddFile("test2.txt",$false)
PS> $a
C:\Users\Jaykul\AppData\Local\Temp\test2.txt
C:\Users\Jaykul\AppData\Local\Temp\test1.txt
PS>
In a nutshell, the problem you're having is that PowerShell unrolls collections (that is, it outputs the CONTENTS of the collection) when you send them through the pipeline. That's because otherwise, when you type:
PS> $a = Get-ChildItem
PS> $a
You would just see that it's an array: System.Object[] ...
In that sort of situation, you almost always want to see the contents of the array. As an aside: this is a great example of how PowerShell is first a shell language, and then a scripting language: it prioritizes the right behavior for a shell.
Anyway, there are a few exceptions for that unrolling of enumerable collections (strings are IEnumerable, but are not unrolled), and in fact, I think New-Object ... | is special-cased which is why it worked to pipe New-Object to Get-Member at all. Of course, once you wrapped it in parenthesis, you created a sub-expression which is parsed separately, so when you write:
(new-object CodeDom.Compiler.TempFileCollection -argumentlist "C:\folder1\") | gm
You basically have two pipelines, and the special treatment of New-Object vanishes, your collection is unrolled, and it's contents (nothing, because it's empty) are passed down the pipeline.
You can avoid this by using the unary comma operator:
$a = new-object CodeDom.Compiler.TempFileCollection -argumentlist "C:\folder1\"
,$a | gm
or
,(new-object CodeDom.Compiler.TempFileCollection -argumentlist "C:\folder1\") | gm
or by passing it to the -InputObject parameter directly instead of through the pipeline.
$a = new-object CodeDom.Compiler.TempFileCollection -argumentlist "C:\folder1\"
gm -InputObject $a
However, when you're using a fancy collection, and you want to see the values of the properties of the collection object itself, you'll probably need to do both:
PS> ft -in (,$a)
Count TempDir BasePath KeepFiles SyncRoot IsSynchronized
----- ------- -------- --------- -------- --------------
3 C:\folder1\ C:\folder1\t22jmahj False False