How to load StackExchange.Redis.dll into powershell? - powershell

I'm trying to create a powershell script to clear a redis cache, it's in Azure but I don't think that's relevant. I've seen 2 examples which I'm trying to copy where people have loaded StackExchange.Redis.dll into their script: https://www.powershellgallery.com/packages/Saritasa.Redis/1.2.0/Content/Saritasa.Redis.psm1 and Clearing Azure Redis Cache using PowerShell during deployment.
I've downloaded the current StackExchange.Redis.dll from nuget.org. I've tried to load it on 2 servers, one with .Net 4.61 installed, the other with .Net 4.8. I get the same problem on both.
If I try to use [System.Reflection.Assembly]::LoadFrom I get as below:
PS E:\redis\stackexchange.redis.2.2.88\lib\net461> dir
Directory: E:\redis\stackexchange.redis.2.2.88\lib\net461
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 05/11/2021 00:42 637440 StackExchange.Redis.dll
-a--- 05/11/2021 00:42 705989 StackExchange.Redis.xml
PS E:\redis\stackexchange.redis.2.2.88\lib\net461> [System.Reflection.Assembly]::LoadFrom("E:\redis\stackexchange.redis.
2.2.88\lib\net461\StackExchange.Redis.dll")
GAC Version Location
--- ------- --------
False v4.0.30319 E:\redis\stackexchange.redis.2.2.88\lib\net461\StackExchange.Redis.dll
PS E:\redis\stackexchange.redis.2.2.88\lib\net461> [StackExchange.Redis.ConnectionMultiplexer]::Connect($myConnectionStr
ing)
Unable to find type [StackExchange.Redis.ConnectionMultiplexer]: make sure that the assembly containing this type is
loaded.
At line:1 char:1
+ [StackExchange.Redis.ConnectionMultiplexer]::Connect($myConnectionString)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (StackExchange.R...tionMultiplexer:TypeName) [], RuntimeException
+ FullyQualifiedErrorId : TypeNotFound
PS E:\redis\stackexchange.redis.2.2.88\lib\net461>
If I try to use Add-Type I get:
PS E:\redis\stackexchange.redis.2.2.88\lib\net461> Add-Type -AssemblyName .\StackExchange.Redis.dll
Add-Type : Could not load file or assembly '.\\StackExchange.Redis.dll' or one of its dependencies. The given assembly
name or codebase was invalid. (Exception from HRESULT: 0x80131047)
At line:1 char:1
+ Add-Type -AssemblyName .\StackExchange.Redis.dll
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Add-Type], FileLoadException
+ FullyQualifiedErrorId : System.IO.FileLoadException,Microsoft.PowerShell.Commands.AddTypeCommand
PS E:\redis\stackexchange.redis.2.2.88\lib\net461>
I've looked through the dependencies in nuget.org, I saw one non-Microsoft one called Pipelines.Sockets.Unofficial which I also downloaded and got the same thing. There's a whole hierarchy of other dependencies which I think are all part of .Net, surely I haven't got to download them all if .Net is installed on the server? Thanks for any help, I've been trying all day!

I'll put some things I learned above my code in case someone as inexperienced as me reads this. These points aren't in any order:
To download a nuget package for use by powershell, the easiest way is to use the Install-Package cmdlet, for example:
Install-Package -Name System.IO.Pipelines -ProviderName NuGet -SkipDependencies -Destination C:\Stackexchange.Redis-packages -RequiredVersion 5.0.1
Note that -SkipDependencies is needed because without it Install-Package gave me an error message about a circular dependency, described in https://endjin.com/blog/2020/12/how-to-consume-a-nuget-package-in-powershell. You have to download the dependencies yourself!
The alternative is: in nuget.org click the download link to download the .nupkg file, rename it to .zip, extract the files, then in File Explorer right-click the dll file, click Properties, Unblock.
I thought this app was great for showing the DLL dependencies of a DLL, it shows the whole heirarchy of dependencies and if they're found or not https://github.com/lucasg/Dependencies
To get the assembly versions of dll files in powershell:
Get-ChildItem -Filter *.dll -Recurse | Select-Object Name,#{n='FileVersion';e={$_.VersionInfo.FileVersion}},#{n='AssemblyVersion';e={[Reflection.AssemblyName]::GetAssemblyName($_.FullName).Version}}
from Get file version and assembly version of DLL files in the current directory and all sub directories
When loading DLLs, Add-Type behaves differently from [System.Reflection.Assembly]::LoadFrom(). Add-Type seems to load dependencies which can be useful. If Add-Type -Literalpath <dllFileName> fails with an error message “Add-Type : Unable to load one or more of the requested types. Retrieve the LoaderExceptions property for more
information” you can get the DLL it was looking for with $error[0].Exception.GetBaseException().LoaderExceptions https://www.reddit.com/r/PowerShell/comments/7a4vw6/addtype_how_do_i_retrieve_the_loaderexceptions/
To get round the situation where DLLs in the heirarchy have a dependency on different versions of the same DLL (which seems to be very common) this guy's solution is simply fantastic. I couldn't find any alternative and it seems to work perfectly https://www.azurefromthetrenches.com/powershell-binding-redirects-and-visual-studio-team-services/
The DLLs I used
I ended up with these DLL files in a folder:
Name FileVersion AssemblyVersion
---- ----------- ---------------
Microsoft.Bcl.AsyncInterfaces.dll 6.0.21.52210 6.0.0.0
Pipelines.Sockets.Unofficial.dll 2.2.0.45337 1.0.0.0
StackExchange.Redis.dll 2.2.88.56325 2.0.0.0
System.Buffers.dll 4.6.28619.01 4.0.3.0
System.IO.Pipelines.dll 5.0.120.57516 5.0.0.1
System.Memory.dll 4.6.28619.01 4.0.1.1
System.Numerics.Vectors.dll 4.6.26515.06 4.1.4.0
System.Runtime.CompilerServices.Unsafe.dll 6.0.21.52210 6.0.0.0
System.Threading.Channels.dll 6.0.21.52210 6.0.0.0
System.Threading.Tasks.Extensions.dll 4.6.28619.01 4.2.0.1
The code It's not finished, but shows the Stackexhange.Redis DLL loaded and used.
# Code to load the DLLs needed for Stackexchange.Redis.dll and clear the cache
# Basically copied from https://www.azurefromthetrenches.com/powershell-binding-redirects-and-visual-studio-team-services/
$DllPath = 'C:\Stackexchange.Redis-packages\combined DLLS'
$redisHostName = '<my cache name here>.redis.cache.windows.net'
$redisConnectionString = '<my cache name here>.redis.cache.windows.net:6380,password=<my cache password here>,ssl=True,abortConnect=False'
# Load DLL assemblies into memory, required by the event handler below
$SystemBuffersDll = [System.Reflection.Assembly]::LoadFrom("$DllPath\System.Buffers.dll")
$SystemRuntimeCompilerServicesUnsafeDll = [System.Reflection.Assembly]::LoadFrom("$DllPath\System.Runtime.CompilerServices.Unsafe.dll")
$SystemMemoryDll = [System.Reflection.Assembly]::LoadFrom("$DllPath\System.Memory.dll")
$SystemSystemThreadingTasksExtensionsDll = [System.Reflection.Assembly]::LoadFrom("$DllPath\System.Threading.Tasks.Extensions.dll")
$SystemIoPipelinesDll = [System.Reflection.Assembly]::LoadFrom("$DllPath\System.IO.Pipelines.dll")
$MicrosoftBclAsyncInterfacesDll = [System.Reflection.Assembly]::LoadFrom("$DllPath\Microsoft.Bcl.AsyncInterfaces.dll")
$PipelinesSocketsUnofficialDll = [System.Reflection.Assembly]::LoadFrom("$DllPath\Pipelines.Sockets.Unofficial.dll")
$SystemThreadingChannelsDll = [System.Reflection.Assembly]::LoadFrom("$DllPath\System.Threading.Channels.dll")
# Event handler to be run when the AssemblyResolve event occurs
$onAssemblyResolveEventHandler = [System.ResolveEventHandler] {
param($sender, $e)
Write-Verbose "Assembly resolve event for $($e.Name)"
$dllName = $e.Name.Split(',')[0]
switch ($dllName) {
'System.Buffers' {return $SystemBuffersDll}
'System.Runtime.CompilerServices.Unsafe' {return $SystemRuntimeCompilerServicesUnsafeDll}
'System.Memory' {return $SystemMemoryDll}
'System.Threading.Tasks.Extensions' {return $SystemSystemThreadingTasksExtensionsDll}
'Microsoft.Bcl.AsyncInterfaces' {return $MicrosoftBclAsyncInterfacesDll}
'Pipelines.Sockets.Unofficial' {return $PipelinesSocketsUnofficialDll}
'System.Threading.Channels' {return $SystemThreadingChannelsDll}
'System.Numerics.Vectors' {return $SystemNumericsVectorsDll}
'System.IO.Pipelines' {return $SystemIoPipelinesDll}
}
foreach($assembly in [System.AppDomain]::CurrentDomain.GetAssemblies()) {
if ($assembly.FullName -eq $e.Name) {
return $assembly
}
}
return $null
}
# Set up the handler above to be triggered when the AssemblyResolve event occurs
[System.AppDomain]::CurrentDomain.add_AssemblyResolve($onAssemblyResolveEventHandler)
# Load StackExchange.Redis.dll, prefer Add-Type because it seems to include dependencies, LoadFrom doesn't so might get an error later
Add-Type -LiteralPath "$DllPath\StackExchange.Redis.dll"
$redis = [StackExchange.Redis.ConnectionMultiplexer]::Connect("$redisConnectionString, allowAdmin=true")
$redisServer = $redis.GetServer($redisHostName, 6380)
# $rs.FlushAllDatabases(async=true)
$redisServer.FlushAllDatabases()
# Detach the event handler (not detaching can lead to stack overflow issues when closing PS)
[System.AppDomain]::CurrentDomain.remove_AssemblyResolve($onAssemblyResolveEventHandler)

Related

How to fix DLL LoaderExceptions in PowerShell

I'm trying to load a .dll of itext7, but if I use this
Add-Type -Path "D:\Eigene\Packages\itext7.7.1.5\lib\net40\itext.kernel.dll"
I get the following exception (translated from german):
Add-Type : Add-Type : Unable to load one or more of the requested types. Retrieve the LoaderExceptions property for more information.
In Zeile:2 Zeichen:1
+ Add-Type -Path "D:\Eigene\Packages\itext7.7.1.5\lib\net40\itext.kerne ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Add-Type], ReflectionTypeLoadException
+ FullyQualifiedErrorId : System.Reflection.ReflectionTypeLoadException,Microsoft.PowerShell.Commands.AddTypeCommand
When I use:
try { Add-Type -Path "D:\Eigene\Packages\itext7.7.1.5\lib\net40\itext.kernel.dll" }
catch { $_.Exception.LoaderExceptions }
It says (also translated from german):
The File or Assembly "BouncyCastle.Crypto, Version=1.8.1.0,
Culture=neutral, PublicKeyToken=0e99375e54769942" or a dependency of
it was not found. The system can not find the specified file.
How can I fix this?
EDIT:
I found a BouncyCastle dll on my system that it also downloaded when I installed the itext7 package but it also doesn't work, if I load "D:\Eigene\Packages\Portable.BouncyCastle.1.8.5\lib\net40\BouncyCastle.Crypto.dll" before i load the itext.kernel.dll.
Your assembly is looking for a different version than you have.
I'm not sure if you can do an assembly binding in powershell like you could with an application where you could bind an older assembly to a new one example:
https://learn.microsoft.com/en-us/dotnet/framework/deployment/configuring-assembly-binding-redirection
Your error is asking you to load the dll of version 1.8.1.0 with the public token of: PublicKeyToken=0e99375e54769942
Generally the assembly publisher isn't changing their public token although it is technically possible bouncy castle could so you likely don't need to specify that.
Anyway you need to find the older version (seems like you have 1.8.5). It is likely these are compatible, but best / safest bet is to use the same assembly the one you are loading needs.
You may find some help here if you need to do binding redirection to the newer assembly: Powershell - Assembly binding redirect NOT found in application configuration file

Update-Module not finding PSGallery repo in script as a scheduled task

I have a scheduled task which runs an exported function from my PowerShell module which is hosted at powershellgallery.com. The function performs a check against the built-in PSGallery repository to see if a newer version is available and if so, update it.
I've noticed my module is not being updated as it should and to troubleshoot the issue I've redirected the output from two separate commands. First, to make sure the repository is 'visible' to the SYSTEM account running the task I run:
Get-PSRepository *>> c:\repo.log
This yields the following output:
Name InstallationPolicy SourceLocation
---- ------------------ --------------
PSGallery Untrusted https://www.powershellgallery.com/api/v2
So the SYSTEM account running the function as a scheduled task can 'see' the repo; no problem. Next, the function runs the Update-Module command as such:
Update-Module -Name $ProductName -Confirm:$false -Force -Verbose *>> c:\update.log
This yields the following output:
Checking for updates for module '[removed by me]'.
PackageManagement\Install-Package : Unable to find repository 'https://www.powershellgallery.com/api/v2/'. Use
Get-PSRepository to see all available repositories.
At C:\Program Files\WindowsPowerShell\Modules\PowerShellGet\2.0.4\PSModule.psm1:12546 char:20
+ ... $sid = PackageManagement\Install-Package #PSBoundParameters
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (Microsoft.Power....InstallPackage:InstallPackage) [Install-Package], Ex
ception
+ FullyQualifiedErrorId : SourceNotFound,Microsoft.PowerShell.PackageManagement.Cmdlets.InstallPackage
Lastly, I checked the module to make sure it is indeed associated with PSGallery by running:
Get-InstalledModule -Name $ProductName | fl
The output shows:
RepositorySourceLocation : https://www.powershellgallery.com/api/v2/
Repository : PSGallery
UPDATE: I decided to use the Install-Module with the -Force switch to 'update' the module instead as I couldn't get the other command to work. Oddly though, when I do a Get-InstalledModule -AllVersions I can clearly see a difference between a module installed interactively and one installed under the SYSTEM account (running as a scheduled task). Pay attention to the Repository column:
If I run the function interactively it works without issue.
If I run Find-Module -Name $ProductName from within the function it finds the module without issue.
Tried both solutions from other question to no avail...
Any idea why the Update-Module command can't find the repo??
I believe you are seeing the bug described in https://github.com/PowerShell/PowerShellGet/issues/349 . It's not really related to SYSTEM account. Simplest workaround until fixed version is released is to uninstall the module then reinstall it again. You should only have to do that once, and update-module should work thereafter.
UPDATE: This is resolved in newer builds of PowerShellGet.

Import-Module works only when piped from Get-Module

I wrote a simple PowerShell module. I need to keep more versions of the module. All paths to versions are added to $env:PSModulePath. I'm facing strange problem when importing the module to my session.
This fails:
Import-Module Contoso.PowerShell -RequiredVersion "0.0.2"
Import-Module : The specified module 'Contoso.PowerShell' with version '0.0.2'
was not loaded because no valid module file was found in any module directory.
At line:1 char:1
+ Import-Module Contoso.PowerShell -RequiredVersion "0.0.2"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ResourceUnavailable: (Contoso.PowerShell:String) [Import-Module], FileNotFoundException
+ FullyQualifiedErrorId : Modules_ModuleWithVersionNotFound,Microsoft.PowerShell.Commands.ImportModuleCommand
And now the strange thing - the module with the "0.0.2" version exists. I can successfully list it (with Get-Module -ListAvailable). I can even import it and work with it, but the only way how to do it is this:
Get-Module Contoso.PowerShell -ListAvailable |
? { $_.Version -eq "0.0.2" } |
Import-Module
Everything works like a charm then. The question is: WHY? I'd like to be able to import the module with the first simple command.
EDIT:
Here is how I store the versions of the module:
Get-Module Contoso.PowerShell -ListAvailable
Directory: C:\Program Files\WindowsPowerShell\Modules\Contoso.PowerShell\Contoso.PowerShell.0.0.1
ModuleType Version Name ExportedCommands
---------- ------- ---- ----------------
Script 0.0.1 Contoso.PowerShell
Directory: C:\Program Files\WindowsPowerShell\Modules\Contoso.PowerShell\Contoso.PowerShell.0.0.2
ModuleType Version Name ExportedCommands
---------- ------- ---- ----------------
Script 0.0.2 Contoso.PowerShell
And sorry for confusion - I do NOT have paths to each version in the PSModulePath environment variable.
The reason Import-Module works is because it uses a different parameter set; one where it accepts one or more [PSModuleInfo] objects, which are what Get-Module returns.
Likely, it uses the work already done by Get-Module to determine which file to load.
The next question then is "why doesn't Import-Module find the version the same way Get-Module does?" and the answer to that is "I don't know."
While they should be consistent in any case, a possible cause for trouble is your directly structure. How are you storing multiple versions?
It looks to me like your module paths are incorrect.
Your structure should be:
Contoso.PowerShell\0.0.2
Contoso.PowerShell\0.0.3
etc.
The module files go directly in the version number folder, and it shouldn't additionally have the name inside it.
You can see this structure by using Install-Module to install one from a repository and taking a look at how it handles it.

Cannot load dll assembly in windows 10 (Exception from HRESULT: 0x80131515)

I was trying to load the compiled dll assembly of JSON.NET. However I get the following error message:
PS C:\Users\tamas\Desktop\garbage> Add-Type -path .\Newtonsoft.Json.dll
Add-Type : Could not load file or assembly 'file:///C:\Users\tamas\Desktop\garbage\Newtonsoft.Json.dll' or one of its d
ependencies. Operation is not supported. (Exception from HRESULT: 0x80131515)
At line:1 char:1
+ Add-Type -path .\Newtonsoft.Json.dll
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Add-Type], FileLoadException
+ FullyQualifiedErrorId : System.IO.FileLoadException,Microsoft.PowerShell.Commands.AddTypeCommand
On a different computer, under windows 7 I was able to do the same thing without any problem.
What could be the cause and the solution?
My .NET version:
PS C:\Users\tamas\Desktop\garbage> [System.Environment]::Version
Major Minor Build Revision
----- ----- ----- --------
4 0 30319 42000
PowerShell version:
PS C:\Users\tamas\Desktop\garbage> $Host.Version
Major Minor Build Revision
----- ----- ----- --------
5 1 14393 206
Thanks to #martin-brandl 's suggeston I was able to figure out the problem (I actually had to modify his code, becuase it did not output anything ($global:error[0].Exception has no LoaderExceptions field.))
PS C:\Users\tamas\Desktop\garbage>
>> try
>> {
>> Add-Type -Path .\Newtonsoft.Json.dll
>> }
>> catch
>> {
>> write-host $global:error[0].Exception.InnerException
>> }
System.NotSupportedException: An attempt was made to load an assembly from a network location which would have caused th
e assembly to be sandboxed in previous versions of the .NET Framework. This release of the .NET Framework does not enabl
e CAS policy by default, so this load may be dangerous. If this load is not intended to sandbox the assembly, please ena
ble the loadFromRemoteSources switch. See http://go.microsoft.com/fwlink/?LinkId=155569 for more information.
It turned out that since the dll file was from an external source, for safety reasons powershell threw an exception. Using th UnsafeLoadFrom(...) method I was able to load the dll assembly:
PS C:\Users\tamas\Desktop\garbage> [System.Reflection.Assembly]::UnsafeLoadFrom("c:\users\tamas\Desktop\garbage\Newtonso
ft.Json.dll") # absolute path required here!!
GAC Version Location
--- ------- --------
False v4.0.30319 C:\Users\tamas\Desktop\garbage\Newtonsoft.Json.dll
More info on this issue can be found on the msdn site here .
I can't tell you which of the dependencies you are missing but I can tell you how to find it out. Just surround your Add-Type within a try-catch cand retrieve the LoaderException:
try
{
Add-Type -Path .\Newtonsoft.Json.dll
}
catch
{
$global:error[0].Exception.LoaderExceptions | % { Write-Host $_ }
}

Unzip with Powershell doesn't work on XP

I got the Request to unzip a .zip file with Powershell. On the Internet i found the following code multiple times:
param( [String]$newlocation, [String]$filepath)
if(($newlocation -and $filepath) -and ((test-path $newlocation) -and (test-path $filepath)))
{
Copy-Item $filepath $newlocation
$shell_app=new-object -com shell.application
$filename = $filepath.split("\")[-1]
if(Test-Path "$newlocation\$filename")
{
$zip_file = $shell_app.namespace("$newlocation\$filename")
$destination = $shell_app.namespace($newlocation)
$destination.Copyhere($zip_file.items())
}
}
As I implemented it into my script it changed a little bit. This above is the changed version. Now I've got an error:
Exception calling "NameSpace" with "1" argument(s): "The system cannot find the file specified. (Exception from HRESULT
: 0x80070002)"
At Z:\MyScripts\deploy.ps1:34 char:34
+ $zip_file = $shell_app.namespace <<<< ("$newlocation\$filename")
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ComMethodTargetInvocation
And then another one, that is quite clear (caused by the first error
You cannot call a method on a null-valued expression.
At Z:\MyScripts\deploy.ps1:36 char:39
+ $destination.Copyhere($zip_file.items <<<< ())
+ CategoryInfo : InvalidOperation: (items:String) [], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
The File and the destination Path both exist and I've got rights to acces them (I created both). I'm running on Windows XP with PowerShell 2.0
Major Minor Build Revision
----- ----- ----- --------
2 0 -1 -1
Here is my entire dump from Powershell when I run it directly on the console.
I hope you guys can help me or at least tell me where I can go to find an answer.
I tried already to unzip the zip file manually and it worked, I have got access to both, the file and the file path (as I created both).
I've found this one on the web:
Also the code looks to me like it's dependent on the Windows Explorer support for zipFolders, which you may have turned off
Unregister (disable) XP Zip folders
REGSVR32 /u C:\Windows\System32\zipfldr.dll
Register (enable) XP Zip folders
REGSVR32 zipfldr.dll
It's from here.
I came to it during testing my script on several machines, e.g. on a Windows Server 2008 and on a Windows 7 client. Both worked, so I came to the conclusion that it's not my script,but it's my PC. After the registration of XP Zip folders, it worked.
Many thanks to the guy who wrote this one, I dumped too much time into this problem.
You might be running into a problem with accessing the COM objects. If you are using 64-bit windows, make sure you execute your script from a 64-bit powershell.exe. This means the powershell.exe in c:\windows\system32...\v1.0.... This was counter-intuitive for me, having the '32' in system32. I was executing powershell from Console2, which was a 32bit process, and was therefore launching 32-bit powershell (from c:\windows\syswow64...). Also make sure your powershell.exe is being run with administrator privileges.
Instead of trying to automate the Windows Shell, now in .NET Framework 4.5 there is a ZipFile class that you can use like this:
[System.Reflection.Assembly]::LoadWithPartialName('System.IO.Compression.FileSystem')
[System.IO.Compression.ZipFile]::ExtractToDirectory($sourceFile, $targetFolder)
Edit: Oops, .NET Framework 4.5 is not supported on Windows XP.
Anyhow, this answer might still prove useful for anyone else with ZIP problems in Powershell…