1

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

1over1
  • 13
  • 2
  • 2
    why do you have `{}` surrounding the `$_.processid`? that makes it into a scriptblock ... and there is no apparent reason for that. – Lee_Dailey Feb 22 '20 at 01:29
  • 1
    If processID is all you need to stop the process, what's the reason for all other properties? – Jawad Feb 22 '20 at 03:11
  • As an aside: The CIM cmdlets (e.g., `Get-CimInstance`) superseded the WMI cmdlets (e.g., `Get-WmiObject`) in PowerShell v3 (released in September 2012). Therefore, the WMI cmdlets should be avoided, not least because PowerShell _Core_, where all future effort will go, doesn't even _have_ them anymore. For more information, see [this answer](https://stackoverflow.com/a/54508009/45375). – mklement0 Feb 23 '20 at 20:50

4 Answers4

3

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.

Steven
  • 6,817
  • 1
  • 14
  • 14
  • Nice answer, @Steven. Indeed, the WMI cmdlets have been deprecated since PowerShell 3.0 - see [this answer](https://stackoverflow.com/a/54508009/45375). Regarding the inconsistency of `Stop-Process` defaulting to `-Id` vs. `Get-Process`'s `-Name` default, see [this GitHub issue](https://github.com/PowerShell/PowerShell/issues/6551). – mklement0 Feb 23 '20 at 21:08
2

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

Theo
  • 57,719
  • 8
  • 24
  • 41
1

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
        }
    }
}
mklement0
  • 382,024
  • 64
  • 607
  • 775
Desinternauta
  • 74
  • 1
  • 8
1

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.

mklement0
  • 382,024
  • 64
  • 607
  • 775