野外求生系列 - 使用 Powershell 測試現有 DLL
0 |
前篇文章介紹了如何在無開發工具的管制環境撰寫 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