共用方式為


應用程式復原:解除鎖定 Windows Installer 的隱藏功能

 

Michael S
701 軟體

2005 年 3 月

總結: Windows Installer 有數個功能,大部分都不受開發社群注意到。 這些功能可讓應用程式在執行時間自行修復,或根據使用者與應用程式的互動來安裝選擇性元件。 (10 個列印頁面)

下載 MSI 整合範例 Code.msi

目錄

簡介
透過殼層整合的復原能力
Windows Installer API 簡介
重要應用程式 API
挑戰 #1:Self-Invoked 復原
挑戰 #2:隨選安裝
結論

簡介

身為開發人員,我們真的會考慮在理想環境中、在理想系統上執行的應用程式,以及在成功安裝之後使用應用程式的理想使用者。 事實上,在我們成功安裝應用程式之後,該使用者的存留期就只是剛開始。 我們的應用程式在剩餘穩定且功能上面臨的挑戰有很多,但大部分的應用程式都未準備好處理作業環境中可能無法運作的應用程式變更。

Windows Installer 提供復原功能,讓應用程式保持穩定,但這項功能是以使用者與殼層互動時所採取的特定動作為基礎,以提供 Windows Installer 偵測應用程式設定問題並採取步驟進行修復的「進入點」。

以下是 Windows Installer「進入點」的簡短清單:

  • 快捷方式。 Windows Installer 引進了特殊的快捷方式類型,雖然對使用者而言是透明的,但包含 Windows Installer 透過殼層整合使用的額外中繼資料,可在啟動應用程式之前驗證指定應用程式安裝的狀態。
  • 檔案關聯。 Windows Installer 提供一種機制來攔截檔或檔案相關應用程式的呼叫,以便在使用者使用殼層開啟檔或檔案時,Windows Installer 可以在啟動相關聯的應用程式之前驗證應用程式。
  • COM Advertising。 Windows Installer 提供連結至 COM 子系統的機制,讓任何建立 Windows Installer 所安裝 COM 元件的實例的應用程式 (並設定為使用此功能,) Windows Installer 確認該元件的安裝狀態之後,就會收到該元件的實例。

在某些情況下,Windows Installer 的內建復原功能可能無法偵測應用程式設定的所有問題,或者應用程式可能會以未啟用所需的進入點的方式來運作。 幸運的是,Windows Installer 小組中的智慧人瞭解這項挑戰,並透過豐富的 Windows Installer API 提供額外的復原功能。

透過殼層整合的復原能力

在繼續進行 Windows Installer API 提供的進階復原功能之前,讓我們先看看一般復原案例,當我們使用 Windows Installer 部署應用程式時,通常會免費取得。

在此案例中,我們會部署簡單的文字編輯應用程式,我們將呼叫 SimplePad。 若要建立安裝,我們將使用 Microsoft 的開放原始碼 WiX 工具組 (以取得詳細資訊,請參閱 https://sourceforge.net/projects/wix/.) ,但您可以使用您選擇的任何工具完成相同的作業。

<Directory Id="TARGETDIR" Name="SourceDir">
<Component Id="SimplePad" Guid="BDDFA5DC-BD69-4232-998E-5167814C21B9" 
  KeyPath="no">
  <File Id="SimplePadConfig" Name="SP.cfg"
    src="$(var.SrcFilesPath)SimplePad.exe.config"
    LongName="SimplePad.exe.config" Vital="yes" KeyPath="no" DiskId="1" />
  <File Id="SimplePad" Name="Simple~1.exe"
    src="$(var.SrcFilesPath)SimplePad.EXE" LongName="SimplePad.exe" Vital="yes"
    KeyPath="yes" DiskId="1" >
  <Shortcut Id="SC1" Advertise="yes"  Directory="ProgramMenuFolder"
    Name="SimpPad" LongName="Run SimplePad"  />
  </File>
</Component>
<Directory Id="ProgramMenuFolder" Name="ProgMenu"></Directory>
</Directory>

如您在上述 XML 片段中所見,我們建立了非常簡單的安裝,其中包含一個檔案 (SimplePad.exe) ,以及位於使用者 [開始] 功能表中的單一快捷方式。 請務必注意,在此範例中,我們要建立的快捷方式是 Windows Installer 將用來偵測應用程式狀態並視需要加以修復的進入點。

此時,我們可以建置安裝程式、安裝應用程式,並使用新建立的 [開始] 功能表快捷方式來執行它。 如預期般運作,應用程式會如預期般運作。 若要測試 Windows Installer 的內建復原功能,我們可以刪除 SimplePad.exe 檔案,然後嘗試從 [開始] 功能表快捷方式執行應用程式。 同樣地,Windows Installer 會如預期般偵測到遺漏 SimplePad.exe,並啟動修復。 在修復作業期間,Windows Installer 會從安裝套件的內部快取複本讀取必要的組態資訊,最後取代遺失的檔案,如果來源安裝媒體不存在於原始位置,則會提示使用者輸入來源安裝媒體。 修復作業完成後,應用程式會正常啟動。

圖 1. 進行中的修復作業

Windows Installer 也會透過一些其他值得在這裡提及的機制來提供應用程式復原功能。 確保應用程式保持高可用性的第二個最常見方法是透過 Windows Installer 檔案關聯。 此機制的運作方式與 Windows Installer 快捷方式非常相同,但不會直接連結至應用程式的可執行檔,而是由已註冊的檔案類型建立關聯。 如圖 2 所示,Windows Installer 檔案關聯是使用標準檔案關聯所使用的相同機制來定義,但有一個例外狀況。 請注意,圖 2 中會列出一般 「shell\Open\command」 登錄機碼底下的額外值。 另一個值 (名為 「command」) 是 Windows Installer 在 Windows 殼層內按兩下檔案時所看起來的位置。 這個隱含的字串有時稱為「文文描述元」,實際上是特定產品、元件和功能的編碼標記法。 如果存在這個額外值,Windows Installer 會解碼資料,並用它來對該產品和元件執行檢查。 如果發現元件遺失或損毀,Windows Installer 會啟動修復以還原遺漏的檔案或資料,最後依正常方式啟動參考的應用程式,並將適當的命令列選項傳遞給它。

圖 2. 檢視檔案關聯的「Darwin 描述元」

我們今天將討論的最終復原機制通常稱為 COM Advertising。 在查看 COM Advertising 的機制之前,請務必瞭解其背後的使用案例。 假設您是提供 COM 型共用程式庫的元件廠商,可提供即時郵件費率。 由於此元件可由許多不同的產品使用,因此會安裝到使用者系統上的單一共用位置。 為了確保元件一律會安裝到相同的位置,並確保元件維持高可用性,請將它寄送給已正確設定為利用 COM Advertising 的合併模組中的客戶。 當然,由於您的解決方案隨附為沒有使用者介面的單一 .dll 檔案,因此其他復原機制就不夠。 在此情況下,我們可以依賴 COM Advertising 來確保元件在使用者的系統上保持正確安裝及註冊。 當應用程式透過一般 COM 機制建立此元件的實例時,Windows Installer 會以與檔案關聯一樣的方式「攔截」程式。 請注意,在圖 3 中,這次會將 「Darwin 描述元」 儲存在元件 COM 註冊的 InprocServer32 登錄值中。 同樣地,Windows Installer 會解碼此資訊並使用,以確保我們的元件已正確安裝及設定,並視需要執行任何修復,最後再將元件的實例傳回呼叫應用程式。

值得一提的是,這個獨特的功能完全獨立于使用 元件的應用程式運作。 換句話說,即使未使用 Windows Installer 安裝使用元件的應用程式,元件的 COM Advertising 仍會繼續正常運作,即使呼叫的應用程式只是 VBScript。

圖 3. 檢視 COM 伺服器的「Darwin 描述元」

到目前為止,我們所討論和示範的所有專案都已利用 Windows Installer 的功能,而不需要撰寫單行程式碼,但現在是時候繼續進行更豐富且更強固的實作。

Windows Installer API 簡介

Windows Installer 的預設行為在先前的案例中很適合我們,但通常,在真實世界中,我們有稍微複雜的應用程式。 讓我們擴充我們的範例案例,以處理更具挑戰性的案例。

應用程式通常是由一個以上的可執行檔所組成。 其中一個範例可能是使用啟動載入器可執行檔的應用程式,檢查並安裝應用程式的更新,如 Updater 應用程式區塊中所見。 在此情況下,第一個可執行檔是在使用者按一下 [開始] 功能表上的快捷方式時所叫用的可執行檔。 接著會啟動包含應用程式主要使用者介面的第二個可執行檔。 視安裝設定方式而定,主要應用程式可執行檔的問題可能不受 Windows Installer 引擎偵測到。 雖然其中一個選項可能是撰寫一堆在啟動時執行的程式碼,以檢查執行時間環境,但如果可執行檔本身遺失或損毀,而且無法輕易修復問題,這只會運作。 更有效率的解決方案是利用 Windows Installer 對於已定義于部署套件中的應用程式設定知識。

Windows Installer API 會公開相同的機制,以驗證使用者在與殼層互動時所使用的應用程式完整性。 藉由從應用程式內使用這些 API 呼叫,我們可以確定仍然達到相同的優點,而不需要依賴稍早討論的殼層「進入點」。

以下是 Windows Installer 殼層整合復原功能未涵蓋的案例清單:

  • 以作業系統開頭的應用程式 (執行或執行一次登錄機碼)
  • 系統服務
  • 排定的工作
  • 由其他應用程式執行的應用程式
  • 命令列應用程式

我確定有更多案例可以新增至上述清單,但我認為您有想法。 在下列範例中,我將示範如何獲得 Windows Installer 復原的優點,而不需要依賴我們稍早討論的殼層整合功能。 完成時,您將能夠採用這些概念,並輕鬆地將它們套用至任何涉及執行可執行程式碼的案例。

重要應用程式 API

在深入探討一些範例案例之前,讓我們先看看一些可從應用程式內使用的重要 Windows Installer API。 如需這些 API 使用方式的特定資訊,請參閱平臺 SDK 中的 Windows Installer Finction 參考

主要 Windows Installer 函式 Description
MsiProvideComponent 擷取元件的已安裝位置,並視需要安裝或修復,以確保元件可供使用。
MsiQueryFeatureState 傳回指定功能的安裝狀態。 例如,此函式會告訴您此功能是否已安裝、未安裝或公告。
MsiQueryProductState 傳回產品的安裝狀態。 例如,此函式會告訴您產品是否已安裝、公告、安裝給其他使用者,或完全未安裝。
MsiConfigureProduct
MsiConfigureProductEx
這兩個函式可讓您以程式設計方式安裝或卸載應用程式。 MsiConfigureProductEx 可讓您指定與命令列上通常執行的類似選項,以提供更大的控制權。
MsiConfigureFeature 此函式可讓您安裝、卸載或公告應用程式的特定功能。
MsiGetUserInfo 此函式會傳回在產品安裝順序期間收集的使用者名稱、組織和產品序號。
MsiGetComponentPath
MsiLocateComponent
這兩個函式可協助您判斷目標系統上元件檔案或登錄機碼的實體位置。 MsiGetComponentPath 會傳回特定產品所安裝元件實例的路徑,而 MsiLocateComponent 會傳回 ANY 產品所安裝之元件的第一個實例。

挑戰 #1:Self-Invoked 復原

我們先前曾討論過非常基本的案例,我們可以實際從系統刪除應用程式的可執行檔,並使用快捷方式來偵測並修復問題,方法是重新安裝遺失的檔案。 雖然該案例很適合用來示範 Windows Installer 所運用的殼層整合,但為了更深入地瞭解這些概念,我們將探討稍微複雜的案例。

在此案例中,我們的應用程式是由單一 .exe 檔案和數個文字檔所組成,可為應用程式提供重要的組態資訊。

我們假設軟體公司的技術支援人員收到許多支援要求,顯示 Windows Installer 不會解決應用程式設定問題,因為使用者直接在 Windows 檔案總管中按兩下可執行檔來執行應用程式,而不是使用安裝所建立的 [開始] 功能表快捷方式。

向小組的常駐部署專家諮詢之後,我們的工程師小組會決定應用程式在啟動時執行自己的復原檢查,以確保其已正確設定,來大幅獲益。 為了達成此目的,小組只會新增 MsiProvideComponent API 的呼叫,以確保應用程式安裝套件中定義的重要元件已正確安裝和設定。

<DllImport("msi.dll")> _
Private Shared Function MsiProvideComponent(ByVal szProduct As String, ByVal _
 szFeature As String, ByVal szComponent As String, ByVal dwInstallMode As _
 MSI_REINSTALLMODE, ByVal lpPathBuf As System.Text.StringBuilder, ByRef _
 pcchPathBuf As IntPtr) As Integer
End Function

Public Shared Function ProvideComponent(ByVal productCode As String, ByVal _
 featureName As String, ByVal compID As String) As String
  Dim iRet As Integer
  Dim cbBuffer As Integer = 255
  Dim buffer1 As New System.text.StringBuilder(cbBuffer)
  Dim pSize As New IntPtr(cbBuffer)
  iRet = MsiProvideComponent(productCode, featureName, compID, _
   MSI_INSTALLMODE.INSTALLMODE_DEFAULT, buffer1, pSize)
  Return buffer1.ToString
End Function

為了更妥善地封裝此程式碼,系統會將名為 WIHelper 的新類別新增至專案,以存放 Windows Installer API 方法宣告和包裝函式方法。 呼叫此程式碼是將幾行新增至主要 表單 Load 事件處理常式 的簡單事項。

Private CONST PRODUCTID As String = "PRODUCT_GUID_HERE"
Private CONST MAIN_FEATUREID As String = "DefaultFeatureKey"
Private CONST COMPID_1 As String = "COMP1_GUID_HERE"
Private CONST COMPID_2 As String = "COMP2_GUID_HERE"
Private Sub MainForm_Load() Handles MyBase.Load
  If WIHelper.IsProductInstalled(PRODUCTID) Then
    WIHelper.ProvideComponent(PRODUCTID, MAIN_FEATUREID, COMPID_1)
    WIHelper.ProvideComponent(PRODUCTID, MAIN_FEATUREID, COMPID_2)
  End If
End Sub
 

在上述範例程式碼中,我們會先測試應用程式是否已透過其安裝套件來實際安裝。 這是一個重要概念,因為我們想要確定即使我們正在開發環境中偵錯,應用程式仍能正常運作。 為了達成此目的,我們會在協助程式類別中呼叫名為 IsProductInstalled的方法。 接著,這個方法只會呼叫 MsiQueryProductState ,以判斷產品是否已安裝在系統上。 如果我們對 IsProductInstalled 的呼叫顯示已安裝產品,我們會在協助程式類別中對 ProvideComponent 方法進行一系列呼叫。 這個方法同樣是 MsiProvideComponent API 的簡單包裝函式,它會傳回指定元件的完整路徑,並確保元件已正確安裝並可供使用。 視您特定產品的需求而定,您可以視您想要呼叫 ProvideComponent 方法多次,以確保應用程式完全可供使用者使用。

挑戰 #2:隨選安裝

我們的假設公司銷售主管聽到許多客戶的意見反應,他們想要看到一組使用 SimplePad 傳遞的標準範本。 雖然有些客戶已表達這項功能的強大需求,但有些客戶也擔心安裝大部分使用者可能不需要的額外資料。

在過度瞭解工程師討論如何處理這項新需求之後,我們的安裝工程師會跳入,並指出 Windows Installer 可以輕鬆地以少量的額外程式碼來處理這一點。

在快速規劃之後,小組會決定他們會在應用程式的 [檔案] 功能表中實作新的 [範本] 功能表項目。 如果範本功能安裝在本機使用者系統上,使用者會看到一個飛出功能表,列出每個可用的範本,以及卸載範本功能的選項。 如果尚未安裝範本功能,範本飛出功能表將會有單一專案,讓使用者能夠安裝其他範本。

Private Sub mnuFile_Popup(ByVal sender As Object, ByVal e As System.EventArgs) _ Handles mnuFile.Popup
  Dim newItem As MenuItem
  With mnuTemplates.MenuItems
    .Clear()
    If WIHelper.IsFeatureInstalled(PRODUCTID, TEMPLATES_FEATUREID) Then
      Dim dirInfo As New DirectoryInfo(Application.ExecutablePath)
      For Each dirFile As FileInfo In dirInfo.Parent.GetFiles("*.tpl")
        Dim mi As New MenuItem(Path.GetFileNameWithoutExtension(dirFile.Name))
        AddHandler mi.Click, AddressOf OpenTemplate
        .Add(mi)
      Next
      .Add("-")
      newItem = New MenuItem("Uninstall Templates")
      AddHandler newItem.Click, AddressOf UninstallTemplates
      .Add(newItem)
    Else
      newItem = New MenuItem("Install Templates")
      AddHandler newItem.Click, AddressOf InstallTemplates
      .Add(newItem)
    End If
  End With
End Sub

如您所見,我們先檢查範本功能是否已安裝。 如果是,我們會列舉應用程式資料夾中具有 「tpl」 副檔名的檔案,並將每個範本的名稱新增至功能表。 如果不是,我們只要新增選項讓使用者安裝範本即可。 在查看這一點之前,讓我們先看看如何判斷是否已安裝範本功能。

<DllImport("msi.dll")> _
Private Shared Function MsiQueryFeatureState(ByVal szProduct As String, 
ByVal szFeature As String) As MSI_INSTALLSTATE
End Function    
Public Shared Function IsFeatureInstalled(ByVal pid As String, ByVal fid As String) As Boolean
  Return MsiQueryFeatureState(pid, fid) = MSI_INSTALLSTATE.INSTALLSTATE_LOCAL
End Function

在此簡單函式中,我們直接呼叫 Windows Installer MsiQueryFeatureState 函式,傳入應用程式的 ProductCode 和我們詢問的功能名稱。 如果 Windows Installer 傳回INSTALLSTATE_LOCAL,則我們會傳回 true,因為這表示此功能已安裝在本機。

安裝及卸載我們的範本功能,就像一樣容易完成。

<DllImport("msi.dll")> _
Private Shared Function MsiConfigureFeature(ByVal szProduct As String, ByVal szFeature As String, ByVal eInstallState As MSI_INSTALLSTATE) As Integer
End Function

Public Shared Function InstallFeature(ByVal pid As String, ByVal fid As String)
  As Boolean
  Return MsiConfigureFeature(pid, fid, MSI_INSTALLSTATE.INSTALLSTATE_LOCAL) = ERROR_SUCCESS
End Function

Public Shared Function UninstallFeature(ByVal pid As String, ByVal fid As String) As Boolean
  Return MsiConfigureFeature(pid, fid, 
MSI_INSTALLSTATE.INSTALLSTATE_ABSENT) = ERROR_SUCCESS
End Function

當使用者按一下 [安裝範本] 功能表項目時,會呼叫具有ProductCodeMsiConfigureFeature、我們想要設定的功能名稱,以及指出我們想要在本機安裝此功能的列舉值。 使用者會看到 Windows Installer 進度對話方塊在安裝範本功能時短暫出現。 當對話方塊消失時,範本將會安裝並可供使用。 當使用者回到 [檔案] 功能表時,範本子功能表會填入範本的名稱,如上所述。

結論

利用 Windows Installer 公開的「免費」功能和 API,提供一些非經常性的功能,可大幅降低支援成本、增加應用程式穩定性,以及增強使用者體驗。 這裡示範的範例本質上有點簡單,但希望形成實作您自己的獨特解決方案的絕佳起點。 我們已探討一些可用的 API,但當然尚未涵蓋這些 API。 花一些時間探索 Windows Installer API 的所有功能,我知道您會感到意外地注意到,您可以利用這些相對未完成的 Windows Installer 功能。

 

關於作者

Michael Sн 是 701 Software () http://www.701software.com 的首席軟體架構設計人員。 在形成 701 之前,Michael 是 ActiveInstall Corporation 的首席和 CEO,由 Zero G Software 取得。 ActiveInstall 針對其 Windows Installer 撰寫解決方案達到不必要。 Michael 是 Microsoft 認證解決方案開發人員 (MCSD) 、Microsoft Certified Systems Engineer (MCSE) ,以及 Windows Installer MVP。 您可以在 閱讀 Michael 的部落格 http://msmvps.com/michael