Tuesday, January 1, 2008

Creating and managing processes in PowerShell

 

process

PowerShell's get-process cmdlet gets the processes that are running on the local computer, and is restricted to the local computer only, you can't interact with processes that runs on remote computers nor create ones locally or remotely.


You can use WMI (see below), but I want to show another way of doing it, less documented one - static methods.

Static methods give you the option to break loose of the cmdlet 'wrapper' and work directly on the underlying NET class. To find which static methods you can use on processes we'll use PowerShell Get-Process cmdlet, we'll pipe it to Get-Member to reveal the underlying NET type that get-process is using: 

 

PS:55 >Get-Process | Get-Member

   TypeName: System.Diagnostics.Process

Name                       MemberType     Definition
----                       ----------     ----------
Handles                    AliasProperty  Handles = Handlecount
Name                       AliasProperty  Name = ProcessName
(...)

 

 

Managing processes with static methods

As you can see, below the surface, Get-Process is using the [System.Diagnostics.Process] NET type to retrieve a process object for each process. From now on, you can work with the static methods of this type to interact with process objects. To find the static methods (surround the type name with square brackets), type:

PS > [System.Diagnostics.Process] | Get-Member -Static -MemberType method



TypeName: System.Diagnostics.Process
Name               MemberType Definition
----               ---------- ----------(...)
EnterDebugMode     Method     static System.Void EnterDebugMode()...
Equals             Method     static System.Boolean Equals(Object...
GetCurrentProcess  Method     static System.Diagnostics.Process G...
GetProcessById     Method     static System.Diagnostics.Process G...
GetProcesses       Method     static System.Diagnostics.Process[]...
GetProcessesByName Method     static System.Diagnostics.Process[]...
LeaveDebugMode     Method     static System.Void LeaveDebugMode()...
ReferenceEquals    Method     static System.Boolean ReferenceEqua...
Start              Method     static System.Diagnostics.Process S...
(...)

 

The Definition column shows all the available overloads for each method. The notation for calling a static method is:
PS > [System.Diagnostics.Process]::StaticMethodName(<args>)

Lets find the overloads for the GetProcesses method, (each overload is delimited with a comma):

PS > ([System.Diagnostics.Process] | gm -s GetProcesses).Definition

static System.Diagnostics.Process[] GetProcesses(),
static System.Diagnostics.Process[] GetProcesses(String machineName)

The output shows two ways to call the method. You can call it with no arguments and get all running processes on the local computer or use the later to get processes from a remote computer. We are interested in the second one since it accept a string argument for a computer name. To save typing space we'll assign the type name to a variable:

PS > $process = [System.Diagnostics.Process]


# examples
$server="server"
$process = [System.Diagnostics.Process]

# get all running processes on the remote server
PS > $process::GetProcesses($server)

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    165       7    15364      21684    56            2992 vmserverdWin32
    196       6     2764       5132    54            3740 LVComSX
     73       4     1100       3392    30            2452 ctfmon
(...)


As you can see the output is the same output produced by the get-process. To find which methods you can invoke on a process, pipe it to get-member:

PS > $process::GetProcessById(2992,$server) | gm -member method



   TypeName: System.Diagnostics.Process

Name                      MemberType Definition
----                      ---------- ----------
(...)
Close                     Method     System.Void Close()
CloseMainWindow           Method     System.Boolean CloseMainWindow()
CreateObjRef              Method     System.Runtime.Remoting.ObjRef Cr
Dispose                   Method     System.Void Dispose()
Kill                      Method     System.Void Kill()
Refresh                   Method     System.Void Refresh()
Start                     Method     System.Boolean Start()
ToString                  Method     System.String ToString()
WaitForExit               Method     System.Boolean WaitForExit(...
(...)

 

This are the same methods you'll get if you run get-process | gm -member method. To call a static method simply append it to the command:

PS > $process::GetProcessById(2992,$server).Kill()

In the same manner you can get a list all properties of a process.

PS > $process::GetProcessById(2992,$server) | gm -member property
 

# get remote process by its process id
PS > $process::GetProcessById(2992,$server)

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    165       7    15364      21684    56            2992 vmserverdWin32


# get remote process by its name
PS > $process::GetProcessesByName("vmserverdWin32",$server)

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    165       7    15364      21684    56            2992 vmserverdWin32


# get PowerShell's process, locally
PS > [System.Diagnostics.Process]::GetCurrentProcess()

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    512       9    39340      33148   163     4.06  11952 powershell

 


Creating local processes

To create processes, locally, you need to use the static Start() method. To find what overloads this method takes:

PS > $process = [System.Diagnostics.Process]
PS > (($process | gm -static start).definition).replace("static","`nstatic")
static System.Diagnostics.Process Start(String fileName, String userName, SecureString password, String domain),


static
System.Diagnostics.Process Start(String fileName, String arguments, String userName, SecureString password, String domain),



static
System.Diagnostics.Process Start(String fileName)

static System.Diagnostics.Process Start(String fileName, String arguments),

static System.Diagnostics.Process Start(ProcessStartInfo startInfo)

 

There are five different ways to call it, each overload accepts different set of arguments:

1. Run a file (executable) with alternate credentials and a secure password.
2. Adds an option to add arguments for the file name.
3. Gets only file name.
4. File name and arguments, no alternate credentials.
5. Gets a ProcessStartInfo object.

 

# Use the third overload to run notepad

PS > [System.Diagnostics.Process]::Start("notepad")

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
      0       1      136         84     1     0.00  13720 notepad
 
# Use the fourth to run notepad with arguments, in this case make notepad load the system.ini file.
PS > [System.Diagnostics.Process]::Start("notepad","$env:windir\system.ini")

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
      5       1      340        840     8     0.03  23728 notepad
 
sysini
 

BTW, if you want to avoid the default output, assign the command to a variable or cast it to [void], as in:
 
PS > [void][System.Diagnostics.Process]::Start("notepad","$env:windir\system.ini")
PS >
 


The fifth option is the one I want to use since it is using another type to specify what starting info to assign to the new process, including alternate credentials (though in plain text and not secure as the first and second overloads which uses the SecureString parameter). Here's a partial property list, you can find the full list here:

- Arguments
- FileName
- Password
- UserName
- WorkingDirectory


 

Using the New-Object cmdlet

In this example I'll use the New-Object cmdlet to instantiate a new System.Diagnostics.ProcessStartInfo object to specify a set of values to be used when the process start:
 

# Specifies a set of values that are used when you start a process.
$si = new-object System.Diagnostics.ProcessStartInfo
$si.fileName = "notepad.exe"
$si.Arguments= "$env:windir\system.ini"
$si.WorkingDirectory = $pwd
# this will launch the process in the background (hidden)
# $si.windowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
# run process under alternate credentials
# $si.UserName="username"
# $si.Password="Password"

# start the process
$process = New-Object System.Diagnostics.Process
$process.startInfo=$si

# this is also valid
# $process = [System.Diagnostics.Process]::Start($si)

# close notepad and watch the output
if($process.start()) { $process.waitForExit(); "existed"} 

 

Note: The WaitForExit() method is available only for processes that are running on the local computer. If you need to track a process on a remote system construct a loop to check if the remote process exists.
Want to know how to list all name/value pairs of the [System.Diagnostics.ProcessWindowStyle] type or any other [enum] type, see my post on Listing enumeration type values.

 

 

Creating remote processes

So far so good. But how about creating new processes on remote computers? Well, that's another story (Class). None of the above, Get-Process nor its underlying type, let you do that. The only way to do it is by using WMI.

You can access WMI with the Get-WmiObject cmdlet or with PowerShell's Type accelerators (shortcuts). Type accelerators were developed by the Windows PowerShell team to make WMI scripts more easy to write for system administrators:

[WMI] - type accelerator for ManagementObject.
[WMICLASS] - type accelerator for ManagementClass.
[WMISEARCHER] - type accelerator for a ManagementObjectSearcher.

See the second resource link below for more information on this. 

 

Here's a one-liner to create new process(es) on local/remote computers:

PS > ([WMICLASS]"\\$computer\ROOT\CIMV2:win32_process").Create("notepad")


__GENUS          : 2
__CLASS          : __PARAMETERS
__SUPERCLASS     :
__DYNASTY        : __PARAMETERS
__RELPATH        :
__PROPERTY_COUNT : 2
__DERIVATION     : {}
__SERVER         :
__NAMESPACE      :
__PATH           :
ProcessId        : 7380
ReturnValue      : 0

If the ReturnValue property is set to 0 then you've successfully created the process on the remote computer, the ProcessId property shows the process id for the new process on the remote computer.

Note: For security reasons the WMI Win32_Process.Create method cannot be used to start an interactive process remotely (it runs hidden, in the background).

 

Summary

The above technique on static methods, opens a whole new world of discovering PowerShell and NET classes. You can apply it to any cmdlet and work your way up or down, depending on your point of view :-). Soon enough you'll find your way to MSDN, where you can get more information and code examples on each class.

This post only scratches the surface and there's a lot more to write on processes... but now comes your part. Check out what Get-Service has to offer and what can you get by applying all of this to it.

Last but not least, I'd like to thank Hal Rottenberg from TechProsaic and the PowerScripting Podcast for his help with editing and prompting me to write this article.


Happy New Year!

 

Additional Resources:

Managing Processes in PowerShell
Improved Support for WMI
Getting Services associated with a Process via PowerShell
Process Class
ProcessStartInfo Class
Create Method of the Win32_Process Class
Casts and Get-WmiObject

30 comments:

Flea# said...

I found the remote script execution very useful, however, I was wondering how can I check to see if this process has stopped?

([WMICLASS]"\\$computer\ROOT\CIMV2:win32_process").Create("notepad")

In my PowerShell script I am going to use this to install various MSI's and EXE's on a server however, I need to be able to tell PowerShell to wait until the first process is finished before firing off the next install. Can you provide some assistance with doing this?

Thanks,
Flea#

Shay Levy said...

Hi Flea#

Put this in a script and run to see it in action:


$computer="."
$process = ([WMICLASS]"\\$computer\ROOT\CIMV2:win32_process").Create("notepad")
$procId=$process.ProcessId
#$procId


$exist=$true

while ($exist){

trap{
$script:exist=$false
continue
}

[void][System.Diagnostics.Process]::GetProcessById($procId,$computer)

$processid
start-sleep -s 1

}

"process id: $procId has exited"

Anonymous said...

Hey firstly thanks for the info, found it very useful, ive written a script to check service stats and to run new services also, but one of the services for some reason you must end it's corresponding process, the process is on a remote server, i followed this example,
heres a snippet:

$server = "GH-345632"

$process = [System.Diagnostics.Process]


$process::GetProcessById(1564,$server).Kill()

it's throwing me an error i can't resolve : Exception calling "Kill" with "0" argument(s): "Feature is not supported for remote machines."

Please Help!!!! as my head is in bits trying to figure out whats wrong. it can get any process information from the desired server but cannot kill any process!!



Sean

Amit Tank said...

I like this,
([WMICLASS]"\\$computer\ROOT\CIMV2:win32_process").Create("notepad")

Is there any way to execute this on DMZ servers with different credential?

Shay Levy said...

Sean,

Sorry for the delay, I've just came back from a long vacation :)
As your error message states it is not possible to kill process remotely, only local ones.
The docs on MSDN stses the same:

Check the Exceptions (NotSupportedException) section on the Kill method page:

http://msdn2.microsoft.com/en-us/library/system.diagnostics.process.kill.aspx

"You are attempting to call Kill for a process that is running on a remote computer.
The method is available only for processes running on the local computer."

Fear not, you can kill/Terminate it via WMI:

([WMI]"\\$server\root\cimv2:Win32_Process.Handle='1564'").Terminate()

If the ReturnValue is equel to 0 then you're OK.

Shay Levy said...

Amit,

I think it can be done with a few more lines of code. I can't test it against a DMZ server, you may have to twick it a bit.
Keep in mind that firewall rules must permit WMI remote management calls to the screened server(s).


$cmd="notepad.exe"
$server="serverName"
$user="domain\userName"
$pass="p@ssw0rd"

$process = [WMIClass]"\\$server\ROOT\cimv2:Win32_Process"
$process.psbase.Scope.Options.userName=$user
$process.psbase.Scope.Options.Password=$pass
#$process.psbase.Scope.Options.Impersonation = [System.Management.ImpersonationLevel]::Impersonate
$process.psbase.Scope.Options.Authentication = [System.Management.AuthenticationLevel]::PacketPrivacy
$process.Create($cmd)

# get bach the process id and returnValue
$process.ProcessId
$process.ReturnValue

Ruprict said...

So, can I check the Responding property of a remote process? I try this:

@process::GetProcessesByName("processname","servername") | select -Property -Responding


and I get "Feature is not supported for remote machines."

Thanks...hope this isn't a stoopid question....

Shay Levy said...

Sadly it is not supported for remote machines. It would be possible to get the value In PowerShell v2 remoting, I'll try to find a solution.


By the way, the blog has moved to a new location:
http://blogs.microsoft.co.il/blogs/ScriptFanatic/

-Shay

Mihail Stacanov said...
This comment has been removed by the author.
Mihail Stacanov said...

Hey guys how can I use asterisk here
([WMICLASS]"\\192.168.29.4\ROOT\CIMV2:win32_process").Create("cmd /c net send * test")
powershell recognize it like wildcard may be and can't run

This works
([WMICLASS]"\\192.168.29.4\ROOT\CIMV2:win32_process").Create("cmd /c net send 192.168.29.4 test")

Shay Levy said...

Try to escape it with a backtick (i.e `):

([WMICLASS]"\\192.168.29.4\ROOT\CIMV2:win32_process").Create("cmd /c net send `* test")

Mihail Stacanov said...

Sorry, doesnt work i've tried. I have no idea what to do next. Tried to found in regular expressions - can't

Shay Levy said...

What if you put the command inside single quetes?

([WMICLASS]"\\192.168.29.4\ROOT\CIMV2:win32_process").Create('cmd /c net send * test')

Mihail Stacanov said...

Doesn't work. Only with ips

Shay Levy said...

And this?

$asterisk = '*'

...("cmd /c net send $asterisk test")

Mihail Stacanov said...

And this one too

Shay Levy said...

Do you get any error message?

Mihail Stacanov said...

No error message.
Only this like everything is Ok
__GENUS : 2
__CLASS : __PARAMETERS
__SUPERCLASS :
__DYNASTY : __PARAMETERS
__RELPATH :
__PROPERTY_COUNT : 2
__DERIVATION : {}
__SERVER :
__NAMESPACE :
__PATH :
ProcessId : 2836
ReturnValue : 0

Shay Levy said...

ReturnValue of 0 means success. Can you replace * with your user/computer and see if it works?

Mihail Stacanov said...

If I put name of pc or ip message appears and returnvalue:0 but with asterisk message do not appears

Shay Levy said...

And if you do the same from a dos prompt?

Mihail Stacanov said...

From dos promt on that pc I've runned net send * test and net send 192.168.5.1 test both works :(

Shay Levy said...

Lets take this offline, email me to scriptolog AT gmail DOT com.

... said...

thanks very usefull
What to do if you what to kill process remotely and you dont know the handle only the process name?
Is there an easier way?

Shay Levy said...

Kate,

To kill a process remotely you need to use WMI and the Terminate method (unless you're using PowerShell 2.0 and can issue Stop-Process on the remote machine).

Get-WMIObject Win32_Process -ComputerName comp1 -Filter "Name='processName.exe'" | Foreach-Object {$_.Terminate()}

Unknown said...

Hy,

I have a problem when i try to run notepad PC using our code:
$cmd="notepad.exe"
$server="10.1.1.10"
$user="Administrator"
$pass="1qaz@WSX"

$process = [WMICLASS]"\\$server\ROOT\cimv2:Win32_Process"
$process.psbase.Scope.Options.userName=$user
$process.psbase.Scope.Options.Password=$pass
$process.psbase.Scope.Options.Impersonation = [System.Management.ImpersonationLevel]::Impersonate
$process.psbase.Scope.Options.Authentication = [System.Management.AuthenticationLevel]::PacketPrivacy
$process.Create($cmd)

# get bach the process id and returnValue
$process.ProcessId
$process.ReturnValue

i recive the following error:
Cannot convert value "\\(10.1.1.10)\ROOT\cimv2:Win32_Process" to type "System.Management.ManagementClass". Error: "The RPC server is unavailable. (Exception from HRESULT: 0x800706BA)"

do you know what could be the problem?

Unknown said...

also i've put $server in brackets $(server) : $process = [WMICLASS]"\\$($server)\ROOT\cimv2:Win32_Process"

Now i recive the following error:
Cannot convert value "\\10.1.1.10\ROOT\cimv2:Win32_Process" to type "System.Management.ManagementClass". Error: "Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))"

Shay Levy said...

any firewalls inbetween?

Soumyajit Das said...

Hi,
In case of Windows mobile standard emulators and devices there seems to be a problem with the GetProcessById API.Could you please explain the reason for this?

Thanks in advance.

Shay Levy said...

Sorry, I cannot explain it. I don't use mobile emulators :(

Does mobiles have .net framwork?