前篇文章介紹了如何在無開發工具的管制環境撰寫 Program.cs 並轉為 Program.exe,以便在執行環境修改與測試現有 DLL 程式庫重現問題。

前陣子從頭學習了 Powershell,知道 Powershell 可直接引用 .NET 類別,理論上也能做到同樣的事。

但實際做過一回才發現沒想像來得簡單,其中眉角不少,以下是我的實戰經驗分享。

直接看程式,解說部分我直接寫成註解。至於程式用途及 C# 範例請直接參考前文,在此不重複贅述。

try {
    # 引用 System.Configuration 以使用 OpenMappedExeConfiguration、AppSettings
    Add-Type -AssemblyName System.Configuration;
    $fileMap = New-Object System.Configuration.ExeConfigurationFileMap;
    $fileMap.ExeConfigFilename = 'C:\MyApp\MyApp.exe.config';
    # 用 OpenMappedExeConfiguration() 讀取其他程式的 .exe.config 
    $config = [System.Configuration.ConfigurationManager]::OpenMappedExeConfiguration($fileMap, [System.Configuration.ConfigurationUserLevel]::None);
    # 由 MyApp.exe.config 取出 appSetting
    $idKey = $config.AppSettings.Settings['idKey'].Value;
    # 直接指向 Dll 所在位置
    Add-Type -Path C:\MyApp\MyAppLibrary.dll;
    Add-Type -Path C:\MyApp\Oracle.DataAccess.dll;
    # 呼叫自訂程式庫取得 OracleConnection
    $cn = [MyAppLibrary.DataLayer]::GetOraConnection($idKey);
    # 以下為標準 ODP.NET 操作
    $cmd = $cn.CreateCommand();
    $cmd.CommandText = @"
SELECT A,B,C 
FROM T 
WHERE D = :p_date
"@;
    $cmd.BindByName = $true; 
    $p = $cmd.Parameters.Add("p_date", [Oracle.DataAccess.Client.OracleDbType]::Date);
    $p.Value = New-Object DateTime -ArgumentList 2019, 1, 1;
    $dr = $cmd.ExecuteReader();
    $cnt = 0;
    while ($dr.Read()) 
    {
        $cnt++;
    }
    Write-Host "Data Count=$($cnt)";
}
catch 
{
    # 用 catch 配合 $error[0] 可得到較詳細的訊息
    $error[0] | Format-List -Force;
    # 找不到相依組件出錯時,LoaderExceptions 才有缺少組件名稱
    $error[0].Exception.LoaderExceptions | Select -First 1 | Format-List -Force;
}
finally 
{
    # Powershell 沒有 using,用 finally 確保結束物件釋放資源
    if ($cn -ne $null) {
        $cn.Dispose();
    }
}

補充說明加入 catch 段的好處。未加入 try ... catch 前,若呼叫 .NET 方法出錯,程式預設會繼續往下執行,而錯誤訊息類似這樣。

以 "2" 引數呼叫 "OpenMappedExeConfiguration" 時發生例外狀況: "The string parameter 'fileMap.ExeConfigFilename' cannot be null or empty.
Parameter name: fileMap.ExeConfigFilename"
位於 D:\Temp\OraTest\TestOra.ps1:5 字元:86
+     $config = [System.Configuration.ConfigurationManager]::OpenMappedExeConfiguration <<<< ($fileMap, [System.Configuration.ConfigurationUserLevel]::None);
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException

加上 catch 顯示 $error[0] | Fromat-List -Force 後,程式遇錯會跳至 catch 段,中止後續程式執行,而錯誤訊息內容包含詳細的 .NET StackTrace,較易除錯。

Exception             : System.Management.Automation.MethodInvocationException:
                         以 "2" 引數呼叫 "OpenMappedExeConfiguration" 時發生例外狀況: "The string parameter 'fileMap.ExeConfigFilename' cannot be null or empty.
                        Parameter name: fileMap.ExeConfigFilename" ---> System.ArgumentException: The string parameter 'fileMap.ExeConfigFilename' cannot be null or empty.
                        Parameter name: fileMap.ExeConfigFilename
                           at System.Configuration.ClientConfigurationHost.OpenExeConfiguration(ConfigurationFileMap fileMap, Boolean isMachine, ConfigurationUserLevel userLevel, String exePath)
                           at System.Configuration.ConfigurationManager.OpenExeConfigurationImpl(ConfigurationFileMap fileMap, Boolean isMachine, ConfigurationUserLevel userLevel, String exePath, Boolean preLoad)
                           at OpenMappedExeConfiguration(Object , Object[] )
                           at System.Management.Automation.MethodInformation.Invoke(Object target, Object[] arguments)
                           at System.Management.Automation.DotNetAdapter.AuxiliaryMethodInvoke(Object target, Object[] arguments, MethodInformation methodInformation, Object[] originalArguments)
                           --- End of inner exception stack trace ---
                           at System.Management.Automation.StatementListNode.ExecuteStatement(ParseTreeNode statement, Array input, Pipe outputPipe, ArrayList& resultList, ExecutionContext context)
                           at System.Management.Automation.StatementListNode.Execute(Array input, Pipe outputPipe, ArrayList& resultList, ExecutionContext context)
                           at System.Management.Automation.TryStatementNode.Execute(Array input, Pipe outputPipe, ArrayList& resultList, ExecutionContext context)
TargetObject          :
CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
FullyQualifiedErrorId : DotNetMethodException
ErrorDetails          :
InvocationInfo        : System.Management.Automation.InvocationInfo
PipelineIterationInfo : {}
PSMessageDetails      :

至於 LoaderExceptions 刖是用在找不到相依組件時,預設 $error[0] 只會看到如下訊息,無從得知缺少哪個組件:

Exception             : System.Reflection.ReflectionTypeLoadException: 無法載入一或多個要求類型。請擷取 LoaderExceptions 屬性以取得詳細資訊。
                           於 System.Reflection.RuntimeModule.GetTypes(RuntimeModule module)
                           於 System.Reflection.Assembly.GetTypes()
                           於 Microsoft.PowerShell.Commands.AddTypeCommand.LoadAssemblyFromPathOrName(List`1 generatedTypes)
                           於 Microsoft.PowerShell.Commands.AddTypeCommand.EndProcessing()
                           於 System.Management.Automation.CommandProcessorBase.Complete()
TargetObject          :
CategoryInfo          : NotSpecified: (:) [Add-Type], ReflectionTypeLoadException
FullyQualifiedErrorId : System.Reflection.ReflectionTypeLoadException,Microsoft.PowerShell
                        .Commands.AddTypeCommand
ErrorDetails          :
InvocationInfo        : System.Management.Automation.InvocationInfo
ScriptStackTrace      : 位於 <ScriptBlock>,E:\OraTest\RunOra.ps1: 第 15 行
                        位於 <ScriptBlock>,<無檔案>: 第 1 行
PipelineIterationInfo : {}
PSMessageDetails      :

透過 $error[0].Exception.LoaderExceptions | Select -First 1 | Format-List -Force 可得到詳細說明,明確指出缺少的 DLL 為何,補上 Add-Type -Path ... 即可解決:

Message        : Could not load file or assembly 'Some.DepLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies.         
                 系統找不到指定的檔案。
FileName       : Some.DepLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
FusionLog      : 警告: 組件繫結記錄切換為 OFF。
                 若要記錄組件繫結失敗,請將登錄值 [HKLM\Software\Microsoft\Fusion!EnableLog] (DWORD) 設為 1。
                 注意: 與組件繫結失敗記錄相關的效能會有部分負面影響。
                 若要關閉此功能,請移除登錄值 [HKLM\Software\Microsoft\Fusion!EnableLog]。
Data           : {}
InnerException :
TargetSite     :
StackTrace     :
HelpLink       :
Source         :
HResult        : -2147024894

最後是執行的一些小技巧,我發現 Windows 2008 R2 預載的 Powershell 版本是 2.0,其搭配 .NET CLR 版本為 2.0。

如試圖載入 .NET 4.0 寫的組件會噴出以下錯誤:

System.BadImageFormatException: 無法載入檔案或組件 'file:///C:\MyApp\SomeNet4.dll' 或其相依性的其中之一。 此組件是由比目前載入的執行階段還新的執行階段所建置,因此無法載入。
   於 System.Reflection.Assembly._nLoad(AssemblyName fileName, String codeBase, Evidence assemblySecurity, Assembly locationHint, StackCrawlMark& stackMark, Boolean throwOnFileNotFound, Boolean forIntrospection)
   於 System.Reflection.Assembly.InternalLoad(AssemblyName assemblyRef, Evidence assemblySecurity, StackCrawlMark& stackMark, Boolean forIntrospection)
   於 System.Reflection.Assembly.LoadFrom(String assemblyFile)
   於 Microsoft.PowerShell.Commands.AddTypeCommand.LoadAssemblyFromPathOrName(List`1 generatedTypes)
   於 Microsoft.PowerShell.Commands.AddTypeCommand.EndProcessing()
   於 System.Management.Automation.CommandProcessorBase.Complete()

除了升級 Powersehll 外,我找到一個不需動用管理者權限的解法,將 C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe 複製到自訂資料夾,於該資料夾新增一個 powershell.exe.config:

<?xml version="1.0"?> 
<configuration> 
    <startup useLegacyV2RuntimeActivationPolicy="true"> 
        <supportedRuntime version="v4.0.30319"/> 
        <supportedRuntime version="v2.0.50727"/> 
    </startup> 
</configuration>

改用這個加了額外 config 的 powershell.exe,.NET CLR 的版本就會升級到 4.0:

最後一個問題跟 ODP.NET 32/64 有關,Powershell 也有分 32/64 版,64 位元版在 C:\Windows\System32\WindowsPowerShell\v1.0\,32 位元在 C:\Windows\SysWOW64\WindowsPowerShell\v1.0\,版本匹配不對時會看到「System.BadImageFormatException: Could not load file or assembly 'file:///C:\MyApp\Oracle.DataAccess.dll' or one of its dependencies. 試圖載入格式錯誤的程式。」訊息,換用對應版本即可解決。

參考:Enable .NET 4 Runtime for PowerShell and Other Applications

This article provide some tips of calling existing custom .NET assembly with Powershell.


Comments

Be the first to post a comment

Post a comment


52 - 19 =