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.
Related
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
I have the following script to list the running vms on hyper-V servers:
$VirtualHosts = Get-content "C:\scripts\Hosts.txt"
ForEach ($Guest in $VirtualHosts)
{Get-VM -ComputerName $Guest | Where State -eq Running | FT Name}
I want to add the functionality of rebooting the computers using the Restart-Computer cmdlet. To do this, I plan on using a nested foreach statement. Can you help me with the nested statement?
Nesting two loop constructs is pretty straight-forward in PowerShell - just make sure the inner/nested loop is entirely contained within the body of the outer loop:
$VirtualHosts = Get-content "C:\scripts\Hosts.txt"
foreach($vHost in $VirtualHosts)
{
foreach($runningVM in Get-VM -ComputerName $vHost | Where State -eq Running)
{
$runningVM |Restart-VM
}
}
You can also skip the inner loop completely and just pipe the output from Get-VM directly to Restart-VM:
foreach($vHost in $VirtualHosts)
{
Get-VM -ComputerName $vHost | Where State -eq Running | Restart-VM
}
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 have the following script to find the process "dotnet.exe". In my system, I have many dotnet.exe processes running. But I want to kill the "dotnet.exe" which has command line argument "MyService\Web\argument". I'm trying to do it by the following script. But it doesn't find anything, although I see the process in the Task Manager.
$process = Get-WmiObject Win32_Process | select name, commandline
foreach ($p in $process)
{
if ($p.name -contains "dotnet.exe" -and $p.commandline -contains "web")
{
$kp = Get-Process $p;
$kp.CloseMainWindow();
if (!$kp.HasExited)
{
$kp | Stop-Process -Force
}
}
else
{
Write-Host name: $p.name and param: $p.commandline;
}
}
All you need to do is filter the process list directly via Get-WmiObject and then terminate the matching process(es):
$fltr = "name like '%dotnet.exe%' and commandline like '%web%'"
Get-WmiObject Win32_Process -Filter $fltr | ForEach-Object {
$_.Terminate()
}
You could also call Terminate() directly on the output of Get-WmiObject like this:
(Get-WmiObject Win32_Process -Filter $fltr).Terminate()
However, there are situations where this could fail, e.g. if Get-WmiObject doesn't return any results, or if you're using PowerShell v2 or earlier and Get-WmiObject returns more than one result (passing a method call to the members of an array requires member enumeration, which was introduced with PowerShell v3). Using a ForEach-Object loop is both more robust and backwards-compatible.
The Get-WmiObject cmdlet returns quite useful objects, but you have stripped off everything by selecting only the Name and CommandLine parameters:
$process = Get-WmiObject Win32_Process | select name, commandline
If you remove the | select name, commandline part, you can still loop through each process but also make use of methods like Terminate() that will still be available.
You could do it in one shot, as per #ansgar-wiechers comment, or still make use of the loop and add in more logging, etc. if you wanted:
$process = Get-WmiObject Win32_Process
foreach($p in $process){
if($p.Name -eq "*dotnet.exe" -and $p.CommandLine -like "*web*"){
$p.Terminate()
# and so on...
}
}
Note also the comment from #TheIncorrigible1 about the use of comparison operators. I have used -eq for the process name and -like for the command line.
I've problems to get the ParentProcessID from a Process where I have the ProcessID. I tried it like this, this is how it works with the ProcessID:
$p = Get-Process firefox
$p.Id
But if I try it with the ParentProcessID, it doesn't work:
$p.ParentProcessId
Is there a way to get the ParentProcessID by the ProcessID?
As mentioned in the comments, the objects returned from Get-Process (System.Diagnostics.Process) doesn't contain the parent process ID.
To get that, you'll need to retrieve an instance of the Win32_Process class:
PS C:\> $ParentProcessIds = Get-CimInstance -Class Win32_Process -Filter "Name = 'firefox.exe'"
PS C:\> $ParentProcessIds[0].ParentProcessId
3816
This worked for me:
$p = Get-Process firefox
$parent = (gwmi win32_process | ? processid -eq $p.Id).parentprocessid
$parent
The output is the following:
1596
And 1596 is the matching ParentProcessID I've checked it with the ProcessExplorer.
In PowerShell Core, the Process object returned by Get-Process cmdlet contains a Parent property which gives you the corresponding Process object for the parent process.
Example:
> $p = Get-Process firefox
> $p.Parent.Id
I wanted to get the PPID of the current running PS process, rather than for another process looked up by name. The following worked for me going back to PS v2. (I didn't test v1...)
$PPID = (gwmi win32_process -Filter "processid='$PID'").ParentProcessId
Write-Host "PID: $PID"
Write-Host "PPID: $PPID"