WMI 的一个实现
作者:Paul Li
翻译:Abbey
原文出处:Code Project:Windows Management Instrumentation (WMI) Implementation
源代码下载:wmi.zip(45KB)

这是我在继上一篇文章"My Explorer"之后关于Windows Management Instrumentation(Windows管理规范)的又一新作。我将向你展示一些技巧,让你可以在远程地访问网络中其他计算机的操作系统、服务、当前运行着的进程等等信息,当然前提是你必须得拥有这些计算机的管理员权限。同时我也将向你展示如何利用WMI来启动或者停止服务、终止进程、创建进程。这是程序的主界面:


在这个WMI应用程序里,我创建了一个包含了四个用户控制的库WMIControlLibrary。这四个用户控制分别是Explorer,SystemInfo,Services与Processes。每个控制都有其特定的功用。以下是对每个控制作用的一个简单描述:
Explorer控制 我把我那个"My Explorer"转换成了一个用户控制,它还是用来显示你系统上的驱动器、目录、文件等信息。
SystemInfo 控制* 这个控制用来显示操作系统与硬件数据及清单等信息。
Services 控制* 这个控制用来显示系统当前运行着的服务。
Process 控制* 这个控制用来显示系统当前运行着的进程。
(*注意:这个控制可以用来监控本地或者网络上的远程系统。)
上述的每个控制都引用了System.Management命名空间,以保证它们能访问各自特定的系统信息。

这其中的一些控制需要点时间才能从系统获取相关的信息,因此我在每个控制中都实现了一个事件UpdateStatus(string e)。这样每个控制就可以更新主应用程序窗体的状态条,用户也能很清楚地知道控制正在干什么了。
//控制内部的代码
//声明一个Status的事件委托类型
public delegate void Status(string e);
//声明了一个更新状态的事件
public event Status UpdateStatus;
//更新状态条
UpdateStatus("Hello world.");
//主程序代码
//用参数中的字符串刷新状态条的显示文本
private void refreshStatusBar(string stringStatus)
{
//更新状态条
statusBarStatus.Text = stringStatus;
}


在Explorer控制内部,我使用了WMI的Win32_LogicalDisk类来访问所有本地的及网络映射的驱动器。要访问驱动器的相关信息,我得先使用一个ManagementObjectSearcher对象来获取一个包含了我所需驱动器信息的ManagementOjbectCollection对象(译注:原文用的是class,我认为不准确,因此改译为对象)。之后,我们就可以自由支配所有这些驱动器的信息。(比如驱动器名、类型、卷标、标识等等)。你也可以只查询剩余空间低于1MByte的驱动器的信息,对此只需要改变ManagementObjectSearcher参数而已:
//译注:这句就是查询剩余空间低于1MByte的SQL语句,用在ManagementObjectSearcher的构造时。
//是不是很象一般数据库编程里用的SQL语句啊?
Select * From Win32_LogicalDisk Where FreeSpace
new ManagementObjectSearcher ("SELECT * From Win32_LogicalDisk ");
ManagementObjectCollection queryCollection = query.Get();
//遍历每个对象,以获取每个驱动器的信息
foreach ( ManagementObject mo in queryCollection)
{
switch (int.Parse( mo["DriveType"].ToString()))
{
case Removable: //可移动驱动器
imageIndex = 5;
selectIndex = 5;
break;
case LocalDisk: //本地驱动器
imageIndex = 6;
selectIndex = 6;
break;
case CD: //CD-ROM驱动器
imageIndex = 7;
selectIndex = 7;
break;
case Network: //网络驱动器
imageIndex = 8;
selectIndex = 8;
break;
default: //缺省:文件夹
imageIndex = 2;
selectIndex = 3;
break;
}
//获取驱动器名
Console.WriteLine("Drive: " + mo["Name"].ToString());
}


SystemInfo控制用于显示你的本地计算机或者远程计算机上一些不同类型的信息。它首先定义一个ConnectionOptions对象,并设置好该对象的UserName与Password属性,准备用此来建立一个WMI的连接。之后再以该ConnectionOptions对象为参数,使用本地或远程计算机的主机名创建一个ManagementScope对象。
//建立远程计算机连接
ConnectionOptions co = new ConnectionOptions();
co.Username = textUserID.Text;
co.Password = textPassword.Text;
//将管理范围确定为该远程计算机
System.Management.ManagementScope ms = new System.Management.ManagementScope
("\\\\" + stringHostName + "\\root\\cimv2", co);
现在我们就要准备通过创建一个ObjectQuery 成员对象来访问这个系统上的信息了。我们需要利用这个ObjectQuery对象和之前的那个ManagementScope对象来创建一个ManagementObjectSearcher对象。然后再调用该ManagementObjectSearcher对象的Get()方法来执行ObjectQuery对象定义的那个查询命令,并将查询结果返回到一个ManagementObject对象集中。
//查询操作系统信息
oq = new System.Management.ObjectQuery("SELECT * FROM Win32_OperatingSystem");
query = new ManagementObjectSearcher(ms,oq);
queryCollection = query.Get();
foreach ( ManagementObject mo in queryCollection)
{
//在树中创建一个操作系统的子结点
createChildNode(nodeCollection, "Operating System: " + mo["Caption"]);
createChildNode(nodeCollection, "Version: " + mo["Version"]);
createChildNode(nodeCollection, "Manufacturer : " + mo["Manufacturer"]);
createChildNode(nodeCollection, "Computer Name : " + mo["csname"]);
createChildNode(nodeCollection, "Windows Directory : " + mo["WindowsDirectory"]);
}
如果你只关心本地主机的信息,那你可以不用创建ConnectionOption,ManagementScope,与ObjectQuery这些对象。你只需要用SQL查询语句串创建一个ManagementObjectSearcher对象,然后直接调用该对象的Get()方法,就能以一个ManagementObjectCollection对象的形式返回本地主机的信息了。
ManagementObjectSearcher query = new ManagementObjectSearcher("SELECT * From Win32_OperatingSystem");
ManagementObjectCollection queryCollection = query.Get();
SystemInfo控制也用于显示计算机相关的其他信息:系统制造商,处理器,BIOS,时区,内存、网络连接、显卡等等。用于查询这些信息的代码只是在SQL查询语句和返回属性上不同而已,所以为了减少篇幅我就不把代码写出来了。具体的代码你可以看下载包里的内容。


Service控制使用了这样的一个查询来返回系统中所有服务的信息:
SELECT * FROM Win32_Service
为了能启动或者停止一个服务,我为ListView动态地创建了一个弹出式菜单(上下文菜单)。你在列表的某个项上单击鼠标右键时,一个启动或停止服务(依赖于服务的当前运行状态)的菜单就会弹出。当菜单项被点击后,我需要利用这样的查询语句获得该服务的ManagementObject对象:
SELECT * FROM Win32_Service WHERE Name = ''ServiceName''
接着我就可以通过调用ManagementObject.InvokeMethod()方法来启动或者停止该服务了。InvokeMethod()方法的第一个参数是一个Observer。我传递一个ManagementOperationObserver对象给这个方法,来管理这些异步操作,以及相应的异步事件与信息。通过检查返回的completionHandlerObj.ReturnObject的returnValue属性,我们就可以确定操作是否成功了。
///
/// List view的鼠标右击事件导致动态上下文菜单的生成
///
private void listViewServices_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
{
System.Windows.Forms.ListView listViewObject = (System.Windows.Forms.ListView) sender;
ContextMenu mnuContextMenu = new ContextMenu();
MenuItem menuItem = new MenuItem();
ManagementObjectCollection queryCollection;
//是否是鼠标右键单击
if (e.Button == System.Windows.Forms.MouseButtons.Right)
{
//取得服务的名称
ServiceName = listViewObject.GetItemAt(e.X, e.Y).Text;
//取得列表项
ServiceItem = listViewObject.GetItemAt(e.X,e.Y);
//创建弹出式菜单
listViewObject.ContextMenu = mnuContextMenu;
try
{
//取得特定的服务对象
queryCollection = getServiceCollection("SELECT * FROM Win32_Service Where Name =
''" + ServiceName + "''");
foreach ( ManagementObject mo in queryCollection)
{
//据服务的当前状态创建相应的菜单
if (mo["Started"].Equals(true))
{
menuItem.Text = "Stop";
//设置动作Action属性
ServiceAction = "StopService";
}
else
{
menuItem.Text = "Start";
ServiceAction = "StartService";
}
mnuContextMenu.MenuItems.Add(menuItem);
// 给菜单项挂上事件处理函数
menuItem.Click += new System.EventHandler(this.menuItem_Click);
}
}
catch (Exception e1)
{
MessageBox.Show("Error: " + e1);
}
}
}
///
/// List view上下文菜单的事件响应函数
///
///
///
private void menuItem_Click(object sender, System.EventArgs e)
{
ManagementObjectCollection queryCollection;
ListViewItem lvItem;
//设置一个异步回调函数的句柄
ManagementOperationObserver observer = new ManagementOperationObserver();
completionHandler.MyHandler completionHandlerObj = new completionHandler.MyHandler();
observer.ObjectReady += new ObjectReadyEventHandler(completionHandlerObj.Done);
//获得特定的服务对象
queryCollection = getServiceCollection("Select * from Win32_Service Where Name =''" +
ServiceName + "''");
//更新状态条
updateStatus("Starting/Stopping service...");
foreach ( ManagementObject mo in queryCollection)
{
//启动或者停止服务
mo.InvokeMethod(observer, ServiceAction, null);
}
//等待,直到invoke调用完成或者超时5秒后
int intCount = 0;
while(!completionHandlerObj.IsComplete)
{
if
(intCount 10)
{
MessageBox.Show("Terminate process timed out.", "Terminate Process Status");
break;
}
//等待0.5秒
System.Threading.Thread.Sleep(500);
//增加计数器
intCount++;
}
//检查是否成功执行了
if (completionHandlerObj.ReturnObject.Properties["returnValue"].Value.ToString() == "0")
{
//成功
lvItem = ServiceItem;
if (ServiceAction == "StartService")
lvItem.SubItems[2].Text = "Started";
else
lvItem.SubItems[2].Text = "Stop";
}
else
{
//错误信息
string stringAction;
if (ServiceAction == "StartService")
stringAction = "start";
else
stringAction = "stop";
MessageBox.Show("Failed to " + stringAction + " service " + ServiceName + ".",
"Start/Stop Service Failure");
}
//清除对象
ServiceName = "";
ServiceAction = "";
ServiceItem = null;
//更新状态条
updateStatus("Ready");
this.Update();
}
//----------------------------------
// 完整的处理器
//----------------------------------
using System;
using System.Management;
namespace completionHandler
{
///
/// MyHandler类在InvokeMethod()调用完成后处理通知
///
public class MyHandler
{
private bool isComplete = false;
private ManagementBaseObject returnObject;
///
/// 当InvokeMethod完成后触发Done事件
///
public void Done(object sender, ObjectReadyEventArgs e)
{
isComplete = true;
returnObject = e.NewObject;
}
///
/// 取IsComplete属性
///
public bool IsComplete
{
get
{
return isComplete;
}
}
///
/// 属性允许访问主函数里的返回结果
///
public ManagementBaseObject ReturnObject
{
get
{
return returnObject;
}
}
}
}


Process控制显示系统中运行着的进程,启动进程的用户,CPU使用率,内存的使用情况。要获得进程的用户信息,需要调用GetOwner(User, Domain)方法,其中的User 与Domain是传出参数。我们如何才能从InvokeMethod()调用中取回这些传出型参数呢?这实际取决于我们是如何实现这个InvokeMethod()方法的。如果我们不需要管理异步操作,那么我们只需要传递一个string数组给InvokeMethod()以获取传出的参数值。否则,我们就无需给InvokeMethod()传递任何的参数了,而是从completionHandlerObj.ReturnObject属性中取回传出的参数值。
//-------------------------------------------------
//在不使用observer对象的情况下获取进程用户信息
//--------------------------------------------------
//为InvokeMethod()方法准备参数表
string[] methodArgs = {"", ""};
//获取进程用户信息
mo.InvokeMethod("GetOwner", methodArgs);
//methodArgs[0] 进程用户
//methodArgs[1] 进程的域
//-----------------------------------------------
//在使用observer对象的情况下获取进程用户信息
//-----------------------------------------------
mo.InvokeMethod(observer,"GetOwner", null);
while (!completionHandlerObj.IsComplete)
{
System.Threading.Thread.Sleep(500);
}
if (completionHandlerObj.ReturnObject["returnValue"].
ToString() == "0")
structProcess.stringUserName = completionHandlerObj.
ReturnObject.Properties["User"].Value.ToString();
else
structProcess.stringUserName = "";

终止一个特定的进程与启动或停止一个服务类似。首先还是要取得选中进程对应的 ManagementObject 对象,然后通过调用InvokeMethod(observer, "Terminate", null) 来杀死一个进程。
//设置一个异步回调的句柄
ManagementOperationObserver observer = new ManagementOperationObserver();
completionHandler.MyHandler completionHandlerObj = new completionHandler.MyHandler();
observer.ObjectReady += new ObjectReadyEventHandler(completionHandlerObj.Done);
//获取进程的ManagementObject
queryCollection = getProcessCollection("Select * from Win32_Process Where ProcessID =
''" + ProcessID + "''");
//更新状态条
updateStatus("Invoking terminate process");
foreach ( ManagementObject mo in queryCollection)
{
//启动或者停止服务(译注:作者真懒?)
mo.InvokeMethod(observer, "Terminate", null);
}
//等待,直到invoke调用完成或者超时5秒后
int intCount = 0;
while (!completionHandlerObj.IsComplete)
{
if (intCount == 10)
{
MessageBox.Show("Terminate process timed out.", "Terminate Process Status");
break;
}
//等待0.5秒
System.Threading.Thread.Sleep(500);
//增加计数器
intCount++;
}
if (intCount != 10)
{
//InvokeMethod尚未超时
if (completionHandlerObj.ReturnObject.Properties["returnValue"].Value.ToString() == "0")
{
lvItem = ProcessItem;
lvItem.Remove();
}
else
{
MessageBox.Show("Error terminating process.",
"Terminate Process");
}
}

要创建一个进程,我们需要调用ManagementClass 的InvokeMethod ()方法。我们可以这么创建一个ManagementClass对象:
ManagementClass processClass = New ManagementClass(ms,path,null);
其中的ms是一个ManagementScope对象,path是一个ManagementPath对象。ManagementScope对应了一个管理操作对应的范围。ManagementPath则提供了一个对Win32_Process进行解析与创建的封装。在调用ManagementClass.InvokeMethod(observer, methodName, inParameters)之前,我们还需要做点其他的准备。我们得把四个传入参数封装到一个object数组里。
uint32 Create(string CommandLine,
string CurrentDirectory,
Win32_ProcessStartup ProcessStartupInformation,
uint32* ProcessId);
参数说明
CommandLine - [传入] 要执行的命令行。如果有必要,系统会自动在末尾追加一个null字符来截断该串,表示真正要执行的文件。
CurrentDirectory - [传入] 子进程的当前驱动器与当前目录。这个串必须保证当前目录能解析到一个已知的路径。用户可以定义一个绝对的或相对的路径作为当前的工作目录。如果该参数为null,新创建的进程就会使用父进程的同一路径。这样做是主要是为了保证操作系统外壳能确定应用程序启动的初始驱动器和工作目录。
ProcessStartupInformation - [传入] 这是一个Windows进程的启动配置,请参见Win32_ProcessStartup.
ProcessId - [传出] 一个全局的用于标识进程的标识符。这个值的生存期自进程创建时起,至进程终结时止。
//为InvokeMethod()准备参数
object[] methodArgs = {stringCommandLine, null, null, 0};
//执行这个方法
processClass.InvokeMethod (observer, "Create", methodArgs);
下面是创建进程的实现代码。我编写了一个CreateProcess()函数接受一个传入的命令行字符串stringCommandLine作为参数。当你调用CreateProcess("Calc.exe")时,就意味着创建了一个新的计算器的进程。就这么简单。
///
/// 在一个本地或者远程机器上调用Create方法
///
///
private void CreateProcess(string stringCommandLine)
{
//设置一个异步回调的句柄
ManagementOperationObserver observer = new ManagementOperationObserver();
completionHandler.MyHandler completionHandlerObj = new completionHandler.MyHandler();
observer.ObjectReady += new ObjectReadyEventHandler(completionHandlerObj.Done);
string stringMachineName = "";
//连接到远程计算机
ConnectionOptions co = new ConnectionOptions();
if (radioMachine.Checked == true)
{
stringMachineName = "localhost";
}
else
{
stringMachineName = textIP.Text;
}
if (stringMachineName.Trim().Length == 0)
{
MessageBox.Show("Must enter machine IP address or name.");
return;
}
//获取用户名与密码
if (textUserID.Text.Trim().Length 0)
{
co.Username = textUserID.Text;
co.Password = textPassword.Text;
}
//获取指向机器的接入点
System.Management.ManagementScope ms = new System.Management.ManagementScope("\\\\" +
stringMachineName + "\\root\\cimv2", co);
//获取进程的路径
ManagementPath path = new ManagementPath( "Win32_Process");
//获取将要被调用的进程的对象
ManagementClass processClass = new ManagementClass(ms,path,null);
//更新状态条
updateStatus("Create process " + stringCommandLine + ".");
//为方法准备参数
object[] methodArgs = {stringCommandLine, null, null, 0};
//执行方法
processClass.InvokeMethod (observer, "Create", methodArgs);
//等待,直到invoke调用完成或者超时5秒后
int intCount = 0;
while (!completionHandlerObj.IsComplete)
{
if (intCount 10)
{
MessageBox.Show("Create process timed out.", "Terminate Process Status");
break;
}
//等待0.5秒
System.Threading.Thread.Sleep(500);
//增加计数器
intCount++;
}
if (intCount != 10)
{
//InvokeMethod尚未超时
//检查是否出现错误
if (completionHandlerObj.ReturnObject.Properties["returnValue"].Value.ToString() == "0")
{
//刷新进程列表
this.Refresh();
}
else
{
MessageBox.Show("Error creating new process.",
"Create New Process");
}
}
//更新状态条
updateStatus("Ready");
this.Update();
}

编写这个演示用的WMI应用程序,增加了我不少的经验。这只展示了WMI很小一部分的功能。我想有了我给出的注释,代码还容易理解吧。
你可以使用WMI完成下列工作:
控制硬件与软件
监控事件
就某个事件运行一个脚本
就某个事件发出一封Email
MSDN 中关于WMI的内容:Windows Management Instrumentation
Paul Li 是在纽约的 Dell 专业服务的首席软件安全顾问