嗨,Scripting Guy!遠從墳墓裡執行桌面管理

Microsoft Scripting Guy

下載本文程式碼: HeyScriptingGuy2007_11.exe (151KB)

為了回應廣大讀者的 要求,本月我們打算來點不一樣的:文章一開始我們不討論任何系統管理指令碼,而是要講一個鬼故事 (恐怖的配樂響起)!

附註:好吧,嚴格來說,如果本月真的要做一點與眾不同的事,那麼開頭就應該先來談一談系統管理指令碼。不過配合我們演一下,好嗎?謝謝啦!

多年前,有一位 Scripting 曾曾曾祖母過世了。就在祖母躺進儉樸的木製棺材後,祖父開始作惡夢,在惡夢中,他的愛妻拼命想要爬出墳墓。經不起惡夢連連的祖父不斷哀求,相關單位終於同意挖出屍體。棺木打開時,眾人驚見祖母的指甲都已折彎,而且棺材內部佈滿抓痕!

好啦,這個故事不見得百分之百真實;事實上,這個故事越想越覺得不可能是真的。不過,它還是告訴我們一個很重要的教訓。是什麼教訓我們不曉得,不過一定有。

等一下,想起來了!棺材原本的用意是要保護往生者免受打擾,也是為了防止軀體分解。不幸的是,這些棺材可能造成無心之過:理論上,棺材可讓您活埋生人,而且他永遠逃不出來。Scripting 曾曾曾祖母的故事清楚地告訴我們,人算不如天算,再周詳的計畫也可能以災難收場,還可能生人活埋!(恐怖的配樂再度響起。)

請注意:當然,除非您選用 1860 年 Franz Vester 發明的「改良式棺材」,這又另當別論。這副棺材裡有一條線連接到地面上的鈴鐺;如果有人一息尚存卻被埋進土裡,那麼「死者」只要搖一下鈴就可以求救。「改良式棺材」還配備一個摺疊梯,只是我們不太清楚您要怎麼使用摺疊梯,從六呎深的地底逃出生天就是了。如果您碰巧埋在某人的車庫屋頂上,此時摺疊梯就很有用了。不然的話 ....

同樣的事情 (也就是人算不如天算引發災難的事情) 也印證在網際網路防火牆上(嗯,也算是啦)。防火牆的設計初衷是要把壞人擋在門外:封鎖連入網路流量,藉此讓駭客和入侵者遠離您的電腦。這是很棒的功能,但是 — 就像活埋的問題一樣 — 也會造成無心之過:防火牆也會把好人擋在門外。尤其當 Windows® Management Instrumentation (WMI) 依賴 DCOM 在遠端電腦上執行系統管理工作時,更是如此。防火牆傾向封鎖所有連入 DCOM 流量,因此要透過程式設計的方式在網際網路上管理電腦很難,甚至是根本不可能。事實上,若未開放額外的連接埠,因而讓您更容易遭受駭客與怪客 (Cracker) 的入侵,就完全不可能執行這項作業。當然,除非您選擇 WinRM:Windows 遠端管理 (不附摺疊梯)。

什麼是 Windows 遠端管理?

根據 WinRM SDK (msdn2.microsoft.com/aa384426),Windows 遠端管理是「Microsoft 的 WS-Management 通訊協定實作,它是以 SOAP 為主且與防火牆互動良好的標準通訊協定,可讓不同廠商的硬體與作業系統交互操作」。怎麼樣,厲害吧?本月專欄不會討論 WS-Management 通訊協定的細節,因此建議您閱讀 WinRM SDK 以瞭解詳細資訊。目前我們只在乎 WinRM 可以用於 Windows Server® 2003 R2、Windows Vista® 及 Windows Server 2008,還可讓您透過網際網路管理電腦。WinRM 使用連接埠 80 來執行這項工作,連接埠 80 是大部分防火牆都開放的標準網際網路服務連接埠 (不過,WinRM 使用的連接埠和預設的傳輸機制 HTTP 可以視需要變更)。

我們也不會在本月專欄中討論如何安裝及設定 WinRM。因為已經有很多資訊可以協助您 (msdn2.microsoft.com/aa384372)。但是,我們要強調一個重點:若您要使用 WinRM 從遠端電腦擷取資訊 (當然,這是之所以開始使用 WinRM 的主要原因),那麼本機電腦和遠端電腦都必須執行 WinRM。

這是什麼意思呢?意思是,如果您未升級用戶端電腦到 Windows Vista (您不會還沒升級吧!),或者您的伺服器尚未升級到 Windows Server 2003 R2 或 Windows Server 2008,您就不會覺得 WinRM 特別好用,至少目前是如此。而且不用說,未來也不見得會變得好用 (當然,只要防火牆允許的話,您始終可以使用 WMI 和 DCOM 管理遠端電腦)。

傳回類別的所有屬性和執行個體

雖然有這些林林總總的限制和規定,但是誰在乎呢?撇開這些胡言亂語不談,我們來試試看如何撰寫使用 WinRM 的指令碼。還真巧,我們剛好有一小段指令碼是使用 HTTP 通訊協定和連接埠 80 來連線到名為 atl-fs-01.fabrikam.com 的電腦,然後傳回有關該電腦所安裝的全部服務的完整資訊。請參閱 [圖 1] 中這段了不起的指令碼。

Figure 1 Silverlight 專案元件

檔案 描述
CreateSilverlight.js 這是指定起始 Silverlight 開機設定所用的 JScript 指令碼 (初版的 Silverlight 只支援 JScript 指令碼),其中包括設定使用者介面和圖形物件所用的 XAML 檔。
SampleProject.js 這是讓您放置 JScript 函數用的一個空檔。
Silverlight.js 這個檔案的目的,是將 Silverlight 控制項初始化。
SampleProject.html 這就是做出所有好玩效果的地方。SampleProject.html 只是一個 HTML 檔,其中包含讀取三個 .js 檔的程式碼。同時它也包含將 Silverlight 控制項具體化的程式碼。
SampleProject.xaml 這是做什麼用的?想知道答案,就回到本專欄的本文吧。
   

Figure 1 列出遠端電腦上的服務

strComputer = "atl-fs-01.fabrikam.com"

Set objWRM = CreateObject("WSMan.Automation")
Set objSession = objWRM.CreateSession("http://" & strComputer)

strResource = "https://schemas.microsoft.com/wbem/wsman/1/wmi/root/cimv2/Win32_Service"

Set objResponse = objSession.Enumerate(strResource)

Do Until objResponse.AtEndOfStream
    DisplayOutput(objResponse.ReadItem)
Loop

Sub DisplayOutput(strWinRMXml)
    Set xmlFile = CreateObject("MSXml2.DOMDocument.3.0")    
    Set xslFile = CreateObject("MSXml2.DOMDocument.3.0")
    xmlFile.LoadXml(strWinRMXml)
    xslFile.Load("WsmTxt.xsl")
    Wscript.Echo xmlFile.TransformNode(xslFile)
End Sub

如您所見,我們一開始先將電腦 (atl-fs-01.fabrikam.com) 的 DNS 名稱指派到稱為 strComputer 的變數。或者,我們可以使用電腦的 IP 位址 (甚或是 IPv6 位址) 來建立連線。例如:

strComputer = "192.168.1.1"

指派 strComputer 的值之後,我們接著建立 WSMan.Automation 物件的執行個體,然後呼叫 CreateSession 方法以連線至遠端電腦,在此是使用 HTTP 通訊協定 (就像我們剛才說過的):

Set objSession = objWRM.CreateSession _
    ("http://" & strComputer)

之前提到,我們要傳回遠端電腦上安裝的服務資訊。此外,至少就這個範例來說,我們還想取得關於每個服務的所有屬性資訊。這是什麼意思呢?這表示我們必須指定一個 URI 資源,以便將我們繫結到遠端電腦上的 Win32_Service 類別:

strResource = _
  "https://schemas.microsoft.com" & _
  "/wbem/wsman/1/wmi/root/cimv2" & _
  "/Win32_Service"

即使這個 URI 不算是我們見過最漂亮的 URI (但是仔細想想,我們好像還沒看過什麼漂亮的 URI)。不過還好,大部分的 URI 都是現成的;您只需要注意結尾處的 WMI 路徑即可:

https://schemas.microsoft.com/wbem/wsman/1/wmi/root/cimv2/Win32_Service

這個部分應該蠻簡單明瞭的。假如您想要連接到 root/cimv2/Win32_Process 類別,該怎麼做?只要照此修改 URI 路徑就好了:

https://schemas.microsoft.com/wbem/wsman/1/wmi/root/cimv2/Win32_Process

對 root/default/SystemRestore 類別感興趣?還是一樣,只要修改 URI 類別,小心指定預設的命名空間 (而非 cimv2 命名空間):

https://schemas.microsoft.com/wbem/wsman/1/wmi/root/default/SystemRestore

然後依此類推......有點麻煩的是您也必須加入 URI 的 https://schemas.microsoft.com/wbem/wsman/1/wmi 部分,但也無可奈何......

我們現在已經準備好取回某些資料。為此,我們直接呼叫 Enumerate 方法,將變數 strResource 當作單一方法參數傳送:

Set objResponse = _
  objSession.Enumerate(strResource)

這一行程式碼真的會將電腦 atl-fs-01 上安裝的服務相關資訊填入 objResponse 中嗎?當然會。但是與標準 WMI 指令碼不同,您將不會取回一連串的物件,而且每個物件都包含自己的屬性及屬性方法。相反地,您取回的是一個大型的舊 XML Blob,看起來有點類似 [圖 2]

Figure 2 漂亮的顏色

<Canvas
 xmlns="https://schemas.microsoft.com/client/2007"
 xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
 Width="800"
 Height="300">
 
 <Canvas.Background>
 <LinearGradientBrush>
 <GradientStop Color="Blue" Offset="0.0" />
 <GradientStop Color="Black" Offset="1.0" />
 </LinearGradientBrush>
 </Canvas.Background>

</Canvas>

Figure 2 大型的舊 XML Blob

<p:Win32_Service xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="
https://schemas.microsoft.com/wbem/wsman/1/wmi/root/cimv2/Win32_Service" xmlns:ci
m="http://schemas.dmtf.org/wbem/wscim/1/common" xsi:type="p:Win32_Service_Type"
xml:lang="en-US"><p:AcceptPause>false</p:AcceptPause><p:AcceptStop>false</p:Acce
ptStop><p:Caption>Windows Media Center Service Launcher</p:Caption><p:CheckPoint
>0</p:CheckPoint><p:CreationClassName>Win32_Service</p:CreationClassName><p:Desc
ription>Starts Windows Media Center Scheduler and Windows Media Center Receiver
services at startup if TV is enabled within Windows Media Center.</p:Description
><p:DesktopInteract>false</p:DesktopInteract><p:DisplayName>Windows Media Center

如果您是 XML 老手,這就難不倒您;熟悉 XML 的人應該都能夠輕而易舉地剖析和輸出此資訊 (即便如此,根據 WinRM SDK 的描述,此資訊仍然不屬於「適合人類閱讀的格式」)。但是,萬一您不是 XML 老手呢?在這種情況下,您有兩個選擇。第一個選擇是,您可以等到下個月,到時我們會示範幾個處理 WinRM XML 的技巧。或者您可以套用我們在範例指令碼中的做法:使用與 WinRM 一起安裝的 XSL 轉換。

XSL 轉什麼換?

XSL 轉換只是一個描述如何顯示 XML 檔案的範本。XSL 檔案的詳盡說明不屬於本月專欄的討論範圍 — 而且就算是簡略介紹 XSL 檔案也超出本月專欄的篇幅。因此,我們不打算解釋 WsmTxt.xsl (內建轉換的名稱) 的實際運作方式。相反地,我們只要示範如何在您的指令碼中使用此轉換。

呼叫 Enumerate 方法時,WinRM 傳回資料流。處理此資料最簡單的方法就是設定一個 Do Until 迴圈,這個迴圈會持續執行直到達到資料流尾端為止。在此我們就是這樣執行:

Do Until objResponse.AtEndOfStream
    DisplayOutput(objResponse.ReadItem)
Loop

如您所見,我們在迴圈中呼叫名為 DisplayOutput 的副程式。當我們呼叫此副程式時,我們也一起傳送資料流的 ReadItem 方法值做為副程式參數 (從這整個過程可以看出,XML 資料流將會分成不同部分傳回,而非一整個大型的資料 Blob。於是我們的指令碼會逐個部分或逐個項目讀取 XML 資料)。

另一方面,DisplayOutput 副程式如下所示:

Sub DisplayOutput(strWinRMXml)
  Set xmlFile = _
    CreateObject("MSXml2.DOMDocument.3.0")    
  Set xslFile = _
    CreateObject("MSXml2.DOMDocument.3.0")
  xmlFile.LoadXml(strWinRMXml)
  xslFile.Load("WsmTxt.xsl")
  Wscript.Echo xmlFile.TransformNode(xslFile)
End Sub

總而言之,我們一開始先建立 MSXml2.DOMDocument.3.0 物件的兩個執行個體;將 XML 資料流 (strWinRMXML) 載入其中一個物件中,再將 XSL 檔案 (WsmTxt.xsl) 載入另一個物件中。這時,我們呼叫 TransformNode 方法來使用 XSL 檔案中的這些資訊進行格式化並顯示從 XML 抓取的資料。

沒錯,的確有點複雜。不過至少輸出 (雖然離完美還很遠) 比較容易讀懂 (見 [圖 3])。

Figure 3 為文字上色

<TextBlock 
 Name="Test"
 FontSize="40"
 FontFamily="Georgia"
 FontWeight="Bold"
 Canvas.Top="20" 
 Canvas.Left="20"
 Text="The TechNet Script Center">

 <TextBlock.Foreground>
 <SolidColorBrush Name="test_brush" Color="red"/>
 </TextBlock.Foreground>

</TextBlock>

Figure 3 整齊的 XML

Win32_Service
    AcceptPause = false
    AcceptStop = true
    Caption = User Profile Service
    CheckPoint = 0
    CreationClassName = Win32_Service
    Description = This service is responsible for loading and unloading user profiles. If this service is stopped or disabled, users will no longer be able to successfully logon or logoff, applications may have problems getting to users' data, and components registered to receive profile event notifications will not receive them.

我們說過了,這個結果還可以,但不是最棒的,因此您更要密切注意下個月的專欄,我們會教您幾個方法來自行操控 XML 輸出。

傳回類別的選定執行個體和屬性

毫無疑問地,以上所說的的確很精彩,但是有個問題:這可能並不完全符合您平常工作的模式。沒錯,有時候您會想要傳回類別的所有執行個體的所有屬性;不過,有時候 (而且可能這種時候更常見) 您只想要傳回類別的選定屬性或執行個體。舉例來說,您可能只想要傳回執行中服務的相關資訊,這時您會使用如下的程式碼在一般 WMI 指令碼中這麼做:

Set colItems = objWMIService.ExecQuery _
  ("Select * From Win32_Service " & _
   "Where State = 'Running'")

還不錯,但是您要如何修改 Resource 字串才能達到相同的目的?

老實說,您不用將 Resource 字串修改的像 ExecQuery 陳述式一樣。您確實需要修改 Resource 字串,而且還需要採取其他一些動作。

知道這一點之後,讓我們來看一下 [圖 4]。其中的 WinRM 指令碼會傳回在電腦上執行的服務的相關資訊 (而非安裝在電腦上的所有服務)。

Figure 4 TextBlock 竅門

<TextBlock.Triggers>
 <EventTrigger RoutedEvent=
     "TextBlock.Loaded">
 <BeginStoryboard>
 <Storyboard>
 <DoubleAnimation
 Storyboard.TargetName="Test"
 Storyboard.TargetProperty="Opacity"
 From="0.0" To="1.0" 
 Duration="0:0:5" />
 </Storyboard>
 </BeginStoryboard>
 </EventTrigger>
</TextBlock.Triggers>

Figure 4 尋找執行中的服務

strComputer = "atl-fs-01.fabrikam.com"

Set objWRM = CreateObject("WSMan.Automation")
Set objSession = objWRM.CreateSession("http://" & strComputer)

strResource = "https://schemas.microsoft.com/wbem/wsman/1/wmi/root/cimv2/*"
strFilter = "Select * From Win32_Service Where State = 'Running'"
strDialect = "https://schemas.microsoft.com/wbem/wsman/1/WQL"

Set objResponse = objSession.Enumerate(strResource, strFilter, strDialect)

Do Until objResponse.AtEndOfStream
    DisplayOutput(objResponse.ReadItem)
Loop

Sub DisplayOutput(strWinRMXml)
    Set xmlFile = CreateObject("MSXml2.DOMDocument.3.0")    
    Set xslFile = CreateObject("MSXml2.DOMDocument.3.0")
    xmlFile.LoadXml(strWinRMXml)
    xslFile.Load("WsmTxt.xsl")
    Wscript.Echo xmlFile.TransformNode(xslFile)
End Sub

乍看之下,這個指令碼可能與之前顯示的第一個 WinRM 指令碼完全相同;但其實有一些重大差異。首先來看一下我們指派給 Resource 字串的值:

strResource = _
  "https://schemas.microsoft.com" & _
  "/wbem/wsman/1/wmi/root/cimv2/*"

請注意,在撰寫篩選查詢時,我們並未指定要處理的實際類別名稱 (Win32_Service);而是直接連接到該類別所在的命名空間 (root/cimv2)。只要記得在結尾加上星號 (*)。若未加上星號,指令碼將會失敗並出現「......類別名稱必須是 '*' (星號)」,也就是說必須以 * 表示類別名稱。

此外,我們還需要定義 Filter 和 Dialect:

strFilter = _
  "Select * From Win32_Service " & _
  "Where State = 'Running'"
strDialect = _
  "https://schemas.microsoft.com" & _
  "/wbem/wsman/1/WQL"

Filter 應該很容易理解;我們會在此放入 Windows Management Instrumentation 查詢語言 (WQL) 查詢 ("Select * From Win32_Service Where State = 'Running'")。另一方面,Dialect 則是用於建立 Filter 的查詢語言。這時候只允許一種查詢語言:WQL。但是必須指定 Dialect,否則指令碼就會失敗並附註「不支援篩選器方言......」。

請注意:有趣的是,此錯誤訊息建議您在呼叫 Enumerate 方法時移除 Dialect。您最好不要聽取這個建議。執行篩選查詢時,一定要指定 Dialect,而且必須是 WQL。就這麼簡單。

我們只需要在呼叫 Enumerate 方法時再進行另一項變動。屆時,我們需要傳送代表 Filter (strFilter) 和 Dialect (strDialect) 的變數,以及代表 Resource (strResource) 的變數:

Set objResponse = _
  objSession.Enumerate _
  (strResource, strFilter, strDialect)

試試結果如何吧。

那麼,要如何只傳回選定的類別屬性呢?舉例來說,假設您只想要傳回在電腦上執行的所有服務的 Name 及 DisplayName。那該怎麼做?

在這種情況下,您可以嘗試操控 XML,以便只顯示 Name 和 DisplayName。這是可行的,但絕對比較困難。比較簡單的方法是在指派 Filter 時,只指定那些屬性:

strFilter = _
  "Select Name, DisplayName " & _
  "From Win32_Service " & _
  "Where State = 'Running'"

這麼做之後,您就只會取得每個服務的 Name 及 DisplayName,如下所示:

XmlFragment
    DisplayName = Windows Event Log
    Name = EventLog

XmlFragment
    DisplayName = COM+ Event System
    Name = EventSystem

不過,這個格式好像有點蠢。(XmlFragment 到底怎麼了?)這也是下期專欄非看不可的好理由。

靜心等待

還好,本月專欄的內容應該可以讓您開始探索 WinRM 奇幻世界。當然,在 WinRM 專欄結尾之前,我們不得不提一下在德國一度盛行的「等待期太平間」(Waiting Mortuaries)。設立等待期太平間的城市不會立刻埋葬屍體。相反地,屍體會放置在溫暖的房間裡,並在手腳指頭上纏上細線。當然,這麼做的用意為了讓任何輕微的動作都可觸發警鈴以求助。屍體會停放在這些溫暖的太平間,直到這些人顯然回天乏術,再也動不了為止。

這麼一說,等待期太平間跟被派到 Scripting Guys 小組頗有異曲同工之妙,不是嗎?當然,等待期太平間從來無人復生,反倒是被派到 Scripting Guys 小組的人則......

Dr. Scripto 的指令碼謎題 (Scripting Perplexer)

2007 年 6 月,Scripting Guy 參加佛羅里達州奧蘭多市舉辦的 2007 年 TechEd 會議。光是參加會議不能滿足我們,因此我們決定找點樂子。不只如此,我們覺得大家也都需要調劑一下。因此我們發明了 Dr. Scripto 的遊戲書 (Fun Book),這本小冊子裡充滿與指令碼有關的猜謎以及其他各種小知識。我們夥同《TechNet Magazine》 — 也就是說,我們說服他們讓出他們在展覽會場攤位的一個小角落 — 然後分發這本遊戲書給經過的人。

結果證明,遊戲書大受好評 (雖然可能沒有 Dr. Scripto 搖頭玩偶那麼受歡迎,不過也很接近了)。這些親切又投機的《TechNet Magazine》同事,馬上看到又多了個機會可以利用 Scripting Guy 的成功謀利 (因為 Scripting Guys 總是沒辦法從自己的成功上獲利),因此拜託我們替他們設計一些猜謎。當 Scripting Guy Jean Ross 才一轉身沒多久的時間,Scripting Guy Greg Stemp 馬上就說:「沒問題,交給我們!」。隆重介紹:Dr. Scripto 的指令碼謎題 (Scripting Perplexer)。好好享受吧。

掉落指令碼

在這個猜謎中,上半部的所有字母恢復原狀後可建立一個指令碼 (VBScript)。不過別擔心,您不需要將整個指令碼恢復原狀;而是只要一次恢復一欄。在上半部的每一欄中的字母填入下半部相同欄的空格中。請看以下範例:

如您所見,在第 1 欄中有字母 S、C 和 T。這三個字母應該以某種順序置入底下的格子中。當所有字母都以適當的順序落下,底端的格子 — 閱讀方向從左到右 — 就會產生邏輯。請看一下解法:

您可以看到,第 1 欄中的字母 S、C 和 T 依照 T、S 和 C 的順序落下。結果就成了「The Script Center」這幾個字的首字母。實際的猜謎比較困難一點,因為長度較長,而且最後的解答會是完整的指令碼。

提示:最後的指令碼答案一開始是檔案完整路徑,接著剖析並顯示檔案名稱。

祝您好運!

ANSWER:

Dr. Scripto 的指令碼謎題 (Scripting Perplexer)

答案是:掉落指令碼,2007 年 11 月

在這個猜謎中,您需要讓欄中的字元掉入底下正確的方塊中,好讓底部形成一列指令碼。獨立指令碼如下:

name = "C:\Scripts\Test.txt"
arr = Split(name, "\")
index = Ubound(arr)
Wscript.Echo "Filename: " _
& arr(index)
        

在猜謎格子中看起來像是:

Microsoft Scripting Guy 為 Microsoft 做事,也就是受雇於 Microsoft。他們在不玩、不教或不看棒球 (或者其他各種活動) 的時候,就負責管理 TechNet 指令碼中心。請到 www.scriptingguys.com 一探究竟。

© 2008 Microsoft Corporation and CMP Media, LLC. 保留所有權利;未經允許,嚴禁部分或全部複製.