PowerShell kill multiple processes - arrays

I am trying to accomplish the following via PS and having an issue getting what I need. I have tried many different formats for writing this script and this is the closest I have gotten I think.
I run the following and no error, but no result either.
$softwarelist = 'chrome|firefox|iexplore|opera'
get-process |
Where-Object {$_.ProcessName -eq $softwarelist} |
stop-process -force
Here is another example that I was trying, but this one wasn't terminating all the process I was providing (IE in W8.1).
1..50 | % {notepad;calc}
$null = Get-WmiObject win32_process -Filter "name = 'notepad.exe' OR name = 'calc.exe'" |
% { $_.Terminate() }
Thanks for any help!

Your $softwarelist variable looks like a regular expression, but in your Where-Object condition, you're using the -eq operator. I think you want the -match operator:
$softwarelist = 'chrome|firefox|iexplore|opera'
get-process |
Where-Object {$_.ProcessName -match $softwarelist} |
stop-process -force
You can also pass multiple processes to Get-Process, e.g.
Get-Process -Name 'chrome','firefox','iexplore','opera' | Stop-Process -Force

# First, create an array of strings.
$array = #("chrome","firefox","iexplore","opera")
# Next, loop through each item in your array, and stop the process.
foreach ($process in $array)
{
Stop-Process -Name $process
}

Related

redirecting objects in powershell into other functions

I have a function that works to compare processes against users but when I try to pipe the output into stop-process I get the following errors:
"Stop-Process : Cannot evaluate parameter 'InputObject' because its argument is specified as a script block
and there is no input. A script block cannot be evaluated without input. "
Function Proc {
$TargetUsers = get-content oldusers.txt
$WmiArguments = #{
'Class' = 'Win32_process'
}
$processes = Get-WMIobject #WmiArguments | ForEach-Object {
$Owner = $_.getowner();
$Process = New-Object PSObject
$Process | Add-Member Noteproperty 'ComputerName' $Computer
$Process | Add-Member Noteproperty 'ProcessName' $_.ProcessName
$Process | Add-Member Noteproperty 'ProcessID' $_.ProcessID
$Process | Add-Member Noteproperty 'Domain' $Owner.Domain
$Process | Add-Member Noteproperty 'User' $Owner.User
$Process
}
ForEach ($Process in $Processes) {
if ($TargetUsers -Contains $Process.User) {
stop-process -id {$_.processid}
}
}
}
I have read serveral articles on piping commands in powershell and that it is object based but I am lost on how to use the returned object of one function and piping into another even though I am sure it is simple when you know how
I like Theo's answer. I just want to add some additional stuff that certainly wouldn't fit in a comment...
Initially I think we're all debugging your code, but keeping to the pattern you originally laid out. Strictly speaking there's nothing wrong with that.
I think what's somewhat lost here is your actual question; why can't you simply pipe the output of one function to another?. The answer is in how the receiving cmdlet or function is expecting the data.
If you look at Get-Help Stop-Process -Parameter Id (OR Parameter Name) you'll see it will take the property via pipeline if the property is named correctly:
-Id <Int32[]>
Specifies the process IDs of the processes to stop. To specify multiple IDs, use commas to separate the IDs. To find the PID of a process, type `Get-Process`.
Required? true
Position? 0
Default value None
Accept pipeline input? True (ByPropertyName)
Accept wildcard characters? false
So you would've been able to pipe if you're custom object had a property named "Id".
Stop-Process will accept the process id, but it's looking for the property name to be "Id" and Win32_Process returns "ProcessID".
But there is a second issue. The value of the property passed in must be acceptable to the receiving function/cmdlet. Unfortunately, Win32_Process usually returns the Name with a ".exe" suffix, and Get-Process won't accept that.
Theo's answer is very good and works with Stop-Process because his new object has a property named "ID" which is accepted by the pipeline and part of the default parameter set, meaning it's preferred over the name property.
However, if you were to pipe those objects to Get-Process it wouldn't work. Get-Process prefers name over ID and is expecting a value like "notepad" not "Notepad.exe, which Win32_Process returns. In that case Get-Process wouldn't be able to find the process and would error.
Note: The above was corrected based on collaboration with Theo, you can look at the previous revision & comments for reference.
To make the objects also work with Get-Process simply modify the value going into the "Name" property to remove the trailing '.exe' . I edited Theo's answer just to add that bit. You should see that if he approves it.
I realize that's not part of your original question, but it's to illustrate the additional caveat of piping between different tools/cmdlets/functions etc...
Note: There may still be a couple of exceptions. For example: Win32_Process returns "System Idle Process" But Get-Process returns "Idle". For your purposes that's probably not an issue. Of course you'd never stop that process!
Note: The likely reason Get-Process prefers the name while Stop-Process prefers the ID is that name is not unique but ID is. Stop-Process Notepad will kill all instances of Notepad, which is usually (and in your case) not what's intended.
Regarding the approach in general. I'd point out there are several ways to both extend objects and create PS Custom objects. Add-Member is a good approach if you need or want the instance type to remain the same; I'd consider that extending an object. However, in your case you are creating a custom object then adding members to it. In such a case I usually use Select-Object which already converts to PSCustomObjects.
Your code with corrected "Name" property:
$Processes = Get-WmiObject win32_process
$Processes |
ForEach-Object{
$Owner = $_.getowner()
$Process = New-Object PSObject
$Process | Add-Member NoteProperty 'ComputerName' $_.CSName
$Process | Add-Member NoteProperty 'ProcessName' $_.ProcessName
$Process | Add-Member NoteProperty 'ProcessID' $_.ProcessID
$Process | Add-Member NoteProperty 'Domain' $Owner.Domain
$Process | Add-Member NoteProperty 'User' $Owner.User
$Process | Add-Member NoteProperty 'Name' -Value ( $_.ProcessName -Replace '\.exe$' )
$Process
}
Note: For brevity I removed some surrounding code.
Using select would look something like:
$Processes = Get-WmiObject win32_process |
Select-Object ProcessID,
#{Name = 'ComputerName'; Expression = { $_.CSName }},
#{Name = 'Name '; Expression = { $_.ProcessName -Replace '\.exe$' } },
#{Name = 'Id'; Expression = { $_.ProcessID } },
#{Name = 'Domain'; Expression = { $_.GetOwner().Domain} },
#{Name = 'User'; Expression = { $_.GetOwner().User} }
This can then be piped directly to a where clause to filter the processes you are looking for, then piped again to the Stop-Process cmdlet:
Get-WmiObject win32_process |
Select-Object ProcessID,
#{Name = 'ComputerName'; Expression = { $_.CSName }},
#{Name = 'Name '; Expression = { $_.ProcessName -Replace '\.exe$' } },
#{Name = 'Id'; Expression = { $_.ProcessID } },
#{Name = 'Domain'; Expression = { $_.GetOwner().Domain} },
#{Name = 'User'; Expression = { $_.GetOwner().User} } |
Where-Object{ $TargetUsers -contains $_.User } |
Stop-Process
Note: This drops even the assignment to $Processes. You'd still needed to populate the $TargetUsers variable.
Also: An earlier comment pointed out that given what you are doing you don't need all the props so something like:
Get-WmiObject win32_process |
Select-Object #{Name = 'Name '; Expression = { $_.ProcessName -Replace '\.exe$' } },
#{Name = 'User'; Expression = { $_.GetOwner().User} } |
Where-Object{ $TargetUsers -contains $_.User } |
Stop-Process
However, if you are doing other things in your code like logging the terminated processes it's relatively harmless to establish & maintain more properties.
And just for illustration, piping could be facilitated through ForEach-Object with relative ease as well and no need to stray from the original objects:
Get-WmiObject win32_process |
Where{$TargetUsers -contains $_.GetOwner().User } |
ForEach-Object{ Stop-Process -Id $_.ProcessID }
One of the best things about PowerShell is there are a lot of ways to do stuff. That last example is very concise, but it would be sub-optimal (albeit doable) to add something like logging or console output...
Also Theo is right about Get-CimInstance. If I'm not mistaken Get-WmiObject is deprecated. Old habits are hard to break so all my examples used Get-WmiObject However, these concepts should applicable throughout PowerShell including Get-CimInstance...
At any rate, I hope I've added something here. There are a few articles out there discussing the different object creation and manipulation capabilities pros & cons etc... If I have time I'll try to track them down.
Inside your function, there is no need to do a ForEach-Object loop twice. If I read your question properly, all you want it to do is to stop processes where the owner username matches any of those read from an 'oldusers.txt' file.
Simplified, your function could look like:
function Stop-OldUserProcess {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
[ValidateScript({Test-Path -Path $_ -PathType Leaf})]
[Alias('FullName', 'FilePath')]
[string]$SourceFile
)
$TargetUsers = Get-Content $SourceFile
Get-WMIobject -Class Win32_Process | ForEach-Object {
$Owner = $_.GetOwner()
if ($TargetUsers -contains $Owner.User) {
Write-Verbose "Stopping process $($_.ProcessName)"
Stop-Process -Id $_.ProcessID -Force
}
}
}
and you call it like this:
Stop-OldUserProcess -SourceFile 'oldusers.txt' -Verbose
Another approach could be that you create a function to just gather the processes owned by old users and return that info as objects to the calling script:
function Get-OldUserProcess {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
[ValidateScript({Test-Path -Path $_ -PathType Leaf})]
[Alias('FullName', 'FilePath')]
[string]$SourceFile
)
$TargetUsers = Get-Content $SourceFile
Get-WMIobject -Class Win32_Process | ForEach-Object {
$Owner = $_.GetOwner()
if ($TargetUsers -contains $Owner.User) {
# output a PSObject
[PsCustomObject]#{
'Name' = $_.ProcessName
'Id' = $_.ProcessID
'Domain' = $Owner.Domain
'User' = $Owner.User
}
}
}
}
I opt for using 'Name' and 'Id' as property names, because the Stop-Process cmdlet can take objects through the pipeline and both the 'Name' and 'Id' property are accepted as pipeline input ByPropertyName.
Then call the function to receive (an array of) objects and do what you need to with that:
Get-OldUserProcess -SourceFile 'oldusers.txt' | Stop-Process -Force
I have changed the function names to comply with PowerShells Verb-Noun naming convention.
P.S. If you have PowerShell version 3.0 or better, you can change the lines
Get-WMIobject -Class Win32_Process | ForEach-Object {
$Owner = $_.GetOwner()
into
Get-CimInstance -ClassName Win32_Process | ForEach-Object {
$Owner = Invoke-CimMethod -InputObject $_ -MethodName GetOwner
for better performance. See Get-CIMInstance Vs Get-WMIObject
Change {$_.processid} to $Process.ProcessID
Function Proc {
$TargetUsers = get-content oldusers.txt
$WmiArguments = #{
'Class' = 'Win32_process'
}
$processes = Get-WMIobject #WmiArguments | ForEach-Object {
$Owner = $_.getowner();
$Process = New-Object PSObject
$Process | Add-Member Noteproperty 'ComputerName' $Computer
$Process | Add-Member Noteproperty 'ProcessName' $_.ProcessName
$Process | Add-Member Noteproperty 'ProcessID' $_.ProcessID
$Process | Add-Member Noteproperty 'Domain' $Owner.Domain
$Process | Add-Member Noteproperty 'User' $Owner.User
$Process
}
ForEach ($Process in $Processes) {
if ($TargetUsers -Contains $Process.User) {
stop-process -id $Process.ProcessID
}
}
}
There's great information in the existing answers; let me complement it by explaining the immediate issue:
I get the following errors:
"Stop-Process : Cannot evaluate parameter 'InputObject' because its argument is specified as a script block and there is no input. A script block cannot be evaluated without input. "
{$_.processid} is a script block, which is an apparent attempt to use that script block as a delay-bind parameter with pipeline input.
In your code you are not providing pipeline input to your Stop-Process call, which is what the error message is trying to tell you.
Instead, you're using a foreach loop to loop over input using $Process as the iteration variable, and you therefore need to specify the target ID as regular, direct argument based on that variable, using $Process.ProcessID, as shown in Desinternauta's answer.

Adding objects to an array in a hashtable

I want to create a Hashtable which groups files with the same name in arrays so I can later on work with those to list some properties of those files, like the folders where they're stored.
$ht = #{}
gci -recurse -file | % {
try{
$ht.Add($_.Name,#())
$ht[$_.Name] += $_
}
catch{
$ht[$_.Name] += $_
}
}
All I'm getting is:
Index operation failed; the array index evaluated to null.
At line:8 char:13
+ $ht[$_.Name] += $_
+ ~~~~~~~~~~~~~~~~~~
I'm not sure why this isn't working, I'd appreciate any help.
Don't reinvent the wheel. You want to group files with the same name, use the Group-Object cmdlet:
$groupedFiles = Get-ChildItem -recurse -file | Group-Object Name
Now you can easy retrieve all file names that are present at least twice using the Where-Object cmdlet:
$groupedFiles | Where-Object Count -gt 1
You are getting this error because if your code hits the catch block, the current pipeline variable ($_) represents the last error and not the current item. You can fix that by either storing the current item an a variable, or you use the -PipelineVariable advanced cmdlet parameter:
$ht = #{}
gci -recurse -file -PipelineVariable item | % {
try{
$ht.Add($item.Name,#())
$ht[$item.Name] += $item
}
catch{
$ht[$item.Name] += $item
}
}

Comparing AD users with File Names

I'm trying to find files that are named after members of a specific AD group with a foreach loop using the code below. The script seems to have a problem which causes the loop to stop after the first exception. I think I need to throw the exception because there seems to be no default return value or error if no file for one of the group members is found.
$ErrorActionPreference = "Stop"
$BVAU = Get-ADGroupMember ADGroupName | Select-Object -Property Name
foreach($entry in $BVAU) {
trap [System.IO.DirectoryNotFoundException]{
Write-Host $_.Exception.Message
continue
}
}
if (-not (Get-ChildItem "\\samplepath" -Recurse | Where-Object FullName -like "*$entry*")) {
throw [System.IO.DirectoryNotFoundException] "$entry not found"
}
}
I only want to display the group members that don't have an equally named file. (A PDF form that legally qualifies the AD group membership)
The if statement belongs inside the loop, and the trap should be defined before the loop. However, you don't need the trap in the first place, because it's pointless to throw exceptions just for echoing a username. Simply echo the name right away and continue. Also, avoid running code multiple times if it doesn't need to.
$files = Get-ChildItem "\\samplepath" -Recurse |
Select-Object -Expand Basename -Unique
Get-ADGroupMember ADGroupName |
Select-Object -Expand Name |
Where-Object { $files -notcontains $_ } |
ForEach-Object { "$_ not found" }
The above is assuming that the usernames aren't just partial matches of the file basenames. Otherwise the Where-Object filter becomes slightly more complicated:
...
Where-Object { $n = $_; -not ($files | Where-Object {$_ -like '*$n*'}) } |
...

local users and groups output to a file

I have a script that shows all the local users and their associated groups. However, I'm trying to output the results into a text file and that's where the script goes wrong, because it's not giving me the same results I'm receiving from the output window. For example, the code I have reads:
$LogFile = Test-Path C:\Users\FredAslami\Downloads\Test.txt
$LocalUsers = [ADSI]"WinNT://$env:COMPUTERNAME"
if ($LogFile) {
$LocalUsers.Children | where {$_.SchemaClassName -eq 'user'} | Foreach-Object {
$groups = $_.Groups() | Foreach-Object {
$_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null)
}
$_ | Select-Object #{n='UserName';e={$_.Name}},
#{n='Groups';e={$groups -join ';'}}
}
Write-Host "Got User Groups Info"
Out-File -FilePath C:\Users\FredAslami\Downloads\Test.txt `
-InputObject $LocalUsers -Append
Write-Host "Added info to text"
}
$LocalUsers.Dispose()
When I run that the text in the file will read
distinguishedName :
Path : WinNT://R68-CUSTOM-01
I have also tried using Add-Content, but that doesn't work either. It will add something like:
System.DirectoryServices.DirectoryEntry
I also, tried to debug using Write-Host after it retrieves the local users and group info and another Write-Host after it writes the results into the text file and noticed that it's writing the results before it gathered all the info. So I tried using the Start-Sleep, and that didnt seem to work.
On the second line you have $LocalUsers = [ADSI]"WinNT://$env:COMPUTERNAME". You never assigned it a different value, so that's what you're seeing as your output.
I would recommend piping your Select-Object statement to Export-Csv. Much easier and cleaner.
You get different results in screen and file output, because you're doing different things with your data. The pipeline starting with $localUsers.Children builds a list of the user objects and their group memberships and echoes that to the screen, but you don't do anything else with that data. Instead you're writing the unmodified variable $localUsers to the output file.
If you want tabular data to go both to the console and a file, I'd suggest using Write-Host for the console output, and Export-Csv for the file output:
$LocalUsers.Children | where {$_.SchemaClassName -eq 'user'} | Foreach-Object {
$groups = $_.Groups() | Foreach-Object {
$_.GetType().InvokeMember('Name', 'GetProperty', $null, $_, $null)
}
$o = New-Object -Type PSObject -Property #{
'UserName' = $_.Name
'Groups' = $groups -join ';'
}
Write-Host $o
$o
} | Export-Csv 'C:\Users\FredAslami\Downloads\Test.txt' -NoType
If you want the output to go to the success output stream instead of the console, you could capture the result in a variable and output that in two different ways:
$users = $LocalUsers.Children | where {
$_.SchemaClassName -eq 'user'
} | Foreach-Object {
$groups = $_.Groups() | Foreach-Object {
$_.GetType().InvokeMember('Name', 'GetProperty', $null, $_, $null)
}
New-Object -Type PSObject -Property #{
'UserName' = $_.Name
'Groups' = $groups -join ';'
}
}
$users
$users | Export-Csv 'C:\Users\FredAslami\Downloads\Test.txt' -NoType

Adding row of data to an array via iterator

I am iterating through a directory full of sub directories, looking for the newest file at each level.
The code below does this, but I need to be able to add each line/loop of the iterator to an array so that at the end I can output all the data in tabular format for use in Excel.
Any advice on how I can do this?
$arr = get-childItem -Path "\\network location\directory" | select FullName
$res = #()
foreach($fp in $arr)
{
get-childItem -Path $fp.FullName | sort LastWriteTime | select -last 1 Directory, FullName, Name, LastWriteTime
}
Here's a one-liner for you, split onto multiple lines for readability with the backtick escape character. You can copy paste this and it will run as is. The csv file will be created in the folder where you run this from.
dir -rec -directory | `
foreach {
dir $_.fullname -file | `
sort -Descending lastwritetime | `
select -first 1
} | `
export-csv newestfiles.csv
dir is an alias for get-childitem. foreach is an alias for foreach-object. %, gci and ls are even shorter aliases for get-childitem. Note that I am avoiding storing things in arrays, as this is doubling the work required. There is no need to enumerate the folders, and then enumerate the array afterwards as two separate operations.
Hope this helps.
If I understand you correctly, you just need to pipe the results into $res. So adding | %{$res += $_} should do the trick
$arr = get-childItem -Path "\\network location\directory" | select FullName
$res = #()
foreach($fp in $arr)
{
get-childItem -Path $fp.FullName | sort LastWriteTime | select -last 1 Directory, FullName, Name, LastWriteTime | % {$res += $_}
}
$res | % {write-host $_}

Resources