In our automation framework, lot of proxy functions are being created.
Normally proxy function will be created by wrapping a function into another function. But our team is creating proxy function by wrapping functions into ScriptMethod. They are doing this to use member variables inside function.
See the below example:
function New-StorageArray {
Param
(
$Name,
$Credential,
[version] $Version
)
$array = [pscustomobject] #{
arrayName = $Name
arrayCredential = $Credential
arrayVersion = $version
}
$array | Add-Member -MemberType ScriptMethod ProxyForCmdlet1 {
Param
(
$property1,
$property2
)
try
{
Connect-StorageArray -ArrayName $this.ArrayName -Credential $this.ArrayCredential
$resultOfCmdlet1 = Cmdlet1 -property1 $property1 -property2 $property2
$status = $true
}
catch
{
$status = $true
Write-Error "Some problem occured"
}
finally
{
Disconnect-StroageArray
}
$retObject = [PSCustomObject] #{
Status = $status
Result = $resultOfCmdlet1
}
return $retObject
}
$array | Add-Member -MemberType ScriptMethod ProxyForCmdlet2 {
# same as ProxyForCmdlet1 except it is wrapper for another cmdlet
}
$array | Add-Member -MemberType ScriptMethod ProxyForCmdlet3 {
# same as ProxyForCmdlet1 except it is wrapper for another cmdlet
}
return $array }
To Instantiate array of particular version, they follow like below.
$arrayVerion5 = New-StorageArray -Name "MAchineIP" -Credential "Credential" -Version "5.0"
$arrayVersion5.ProxyForCmdlet1("Parameters")
$arrayVerion4 = New-StorageArray -Name "MAchineIP" -Credential "Credential" -Version "4.0"
It works good. But when proxy functions are wrapped inside Scriptmethod ,we lose pipelining, Help documentation. When the number of proxy function increases ,we are unable to write it into different files, because it is one function which has all the member function.
In C# they will write partial class and functions can be written across multiple files. But powershell don't have any thing like that.
We are losing powershell advantage of pipeline ,help documentation and also unable to handle complexity when proxy function increases. What is the best alternative approach to this?
Related
This is in continuation to this Question here, I have a PowerShell command which I have created and am able to call the command in a PowerShell window, but when trying to call from C# method, I am getting error as the cmdlet is not recognized, I tried with other existing commands and get same error, so I suspect issue in Importing the Module, though I don't get that error in streams. Error. The only error I get is "Get-RowAndPartitionKey is not a recognized cmndlt, please check the spelling.....".
Would like to know if there is any other way, I should try it or if I can debug more here to see if my Module fetches all command or not. right now I am clueless how to fix this.
public string RunScript( string contentScript, Dictionary<string, EntityProperty> parameters )
{
List<string> parameterList = new List<string>();
foreach( var item in parameters )
{
parameterList.Add( item.Value.ToString() );
}
using( PowerShell ps = PowerShell.Create() )
{
IAsyncResult async =
ps.AddCommand( "Import-Module" ).AddArgument( #"C:\Users\...\.D.PowerShell.dll" )
.AddStatement()
.AddCommand( "Get-RowAndPartitionKey" ).AddParameter( "Properties", "test" )
.BeginInvoke();
StringBuilder stringBuilder = new StringBuilder();
foreach( PSObject result in ps.EndInvoke( async ) )
{
stringBuilder.AppendLine( result.ToString() );
}
return stringBuilder.ToString();
}
}
}
Below method do not return any error in Streams.Error or Verbose but no output also:
public async Task<IEnumerable<object>> RunScript( string scriptContents, List<string> scriptParameters )
{
// create a new hosted PowerShell instance using the default runspace.
// wrap in a using statement to ensure resources are cleaned up.
using( PowerShell ps = PowerShell.Create() )
{
// specify the script code to run.
ps.AddScript( scriptContents );
// specify the parameters to pass into the script.
ps.AddParameter( "Properties" ,"test") ;
// execute the script and await the result.
var pipelineObjects = await ps.InvokeAsync().ConfigureAwait( false );
return pipelineObjects;
}
}
scriptContent
"\"$path = 'C:\\Users...\\.TabularData.PowerShell.dll'\\r\\nImport-Module $path\\r\\nGet-RowAndPartitionKeys\""
The following is self-contained PowerShell sample code that uses on-demand compilation of C# code:
It shows that the approach works in principle, as described in this answer to your original question.
Prerequisites: The PowerShell SDK package and .NET runtime used in the C# project that calls your custom Get-RowAndPartitionKey" cmdlet must be compatible with the PowerShell SDK and .NET runtime that you used to compile the assembly DLL that houses that cmdlet, to be imported via Import-Module.
The sample code below ensures that implicitly, by running directly from PowerShell, using the Add-Type cmdlet to compile C# code on demand - it works in Windows PowerShell as well as in PowerShell (Core) 7+.
In practice I've found that a .NET Framework-compiled DLL (from Windows PowerShell) also works in PowerShell (Core) (.NET (Core) 5.0), but not vice versa.
It shows troubleshooting techniques, namely:
Adding the -Verbose switch to the Import-Module call to produce verbose output that lists the commands being imported from the given module (DLL).
Printing these verbose messages (look for // --- TROUBLESHOOTING CODE)
Printing any non-terminating PowerShell errors that occurred (as opposed to exceptions that you'd have to handle in C# code).
# Create a (temporary) assembly containing cmdlet "Get-RowAndPartitionKey".
# This assembly can directly be imported as a module from PowerShell.
# The cmdlet simply outputs "Hi from Get-RowAndPartitionKey" and
# echoes the elements of the list passed to -Properties, one by one.
$tempModuleDll = Join-Path ([IO.Path]::GetTempPath()) "TempModule_$PID.dll"
Remove-Item -ErrorAction Ignore $tempModuleDll
Add-Type #'
using System.Management.Automation;
using System.Collections.Generic;
[Cmdlet("Get", "RowAndPartitionKey")]
public class GetRowAndPartitionKeyCmdlet : PSCmdlet {
[Parameter] public List<string> Properties { get; set; }
protected override void ProcessRecord() {
WriteObject("Hi from Get-RowAndPartitionKey: ");
WriteObject(Properties, true);
}
}
'# -ErrorAction Stop -OutputAssembly $tempModuleDll
# Compile a C# class ad hoc to simulate your project, and call its static
# method, which imports the module and effectively calls
# Get-RowAndPartitionKey -Properties "foo", "bar"
(Add-Type #"
using System;
using System.Management.Automation;
using System.Collections.Generic;
using System.Text;
public static class Foo {
public static string RunScript(List<string> parameterList)
{
using (System.Management.Automation.PowerShell ps = PowerShell.Create())
{
IAsyncResult async =
// Add -Verbose to the Import-Module call, so that the list of
// commands being imported is written to the verbose output stream.
ps.AddCommand("Import-Module").AddArgument(#"$tempModuleDll").AddParameter("Verbose", true)
.AddStatement()
.AddCommand("Get-RowAndPartitionKey").AddParameter("Properties", parameterList)
.BeginInvoke();
StringBuilder stringBuilder = new StringBuilder();
foreach (PSObject result in ps.EndInvoke(async))
{
stringBuilder.AppendLine(result.ToString());
}
// --- TROUBLESHOOTING CODE
// Print verbose output from the Import-Module call
foreach (var v in ps.Streams.Verbose) { Console.WriteLine("VERBOSE: " + v.ToString()); }
// Print any errors.
foreach (var e in ps.Streams.Error) { Console.WriteLine("ERROR: " + e.ToString()); }
// ---
return stringBuilder.ToString();
}
}
}
"# -ErrorAction Stop -PassThru)::RunScript(("foo", "bar"))
# Clean-up instructions:
if ($env:OS -eq 'Windows_NT') {
Write-Verbose -vb "NOTE: Re-running this code requires you to start a NEW SESSION."
Write-Verbose -vb "After exiting this session, you can delete the temporary module DLL(s) with:`n`n Remove-Item $($tempModuleDll -replace '_.+', '_*.dll')`n "
} else {
Write-Verbose -vb "NOTE: Re-running this code after modifying the embedded C# code requires you to start a NEW SESSION."
Remove-Item $tempModuleDll
}
On my Windows 10 machine, both from PowerShell (Core) 7.0.5 and Windows PowerShell 5.1, the above yields (clean-up instructions omitted) the following, showing that everything worked as intended:
VERBOSE: Loading module from path 'C:\Users\jdoe\AppData\Local\Temp\TempModule_11876.dll'.
VERBOSE: Importing cmdlet 'Get-RowAndPartitionKey'.
Hi from Get-RowAndPartitionKey:
foo
bar
Specifically, line VERBOSE: Importing cmdlet 'Get-RowAndPartitionKey'. indicates that the custom cmdlet was successfully imported into the session.
I would like to run a powershell build script where I have some config files (xml/json) that are not app.config, appsettings.json nor web.config files that I would like to transform based on the build configuration. The perfect tool for this appears to be VisualStudio.SlowCheetah since it supports both xml and json and it uses the same underlying technology as web.config transforms (which are also in my project). Is there any way to run this tool from powershell, it would be nice to have the same tool that does the transforms within the solution also do transforms on my auxiliary files?
So here is my proof of concept:
My folder contains 4 files:
PerformTransform.ps1 - Stand-in for my build script that will initiate the transform
Transform-Config.ps1 - Scripts which use SlowCheetah to perform transforms
Sample.config - A sample config file
Sample.Prod.config - A sample xml transform file
PerformTransform.ps1 looks like:
cls
$scriptPath = split-path -parent $MyInvocation.MyCommand.Definition
# Temporarily adds the script folder to the path
# so that the Transform-Config command is available
if(($env:Path -split ';') -notcontains $scriptPath) {
$env:Path += ';' + $scriptPath
}
Transform-Config "$scriptPath\Sample.config" "$scriptPath\Sample.Prod.config" "$scriptPath\Sample.Transformed.config"
Here is my Transform-Config.ps1:
#!/usr/bin/env powershell
<#
.SYNOPSIS
You can use this script to easly transform any XML file using XDT or JSON file using JDT.
To use this script you can just save it locally and execute it. The script
will download its dependencies automatically.
#>
[cmdletbinding()]
param(
[Parameter(
Mandatory=$true,
Position=0)]
$sourceFile,
[Parameter(
Mandatory=$true,
Position=1)]
$transformFile,
[Parameter(
Mandatory=$true,
Position=2)]
$destFile
)
$loggingStubSource = #"
using System;
namespace Microsoft.VisualStudio.SlowCheetah
{
public class LoggingStub : ITransformationLogger
{
public void LogError(string message, params object[] messageArgs) { }
public void LogError(string file, int lineNumber, int linePosition, string message, params object[] messageArgs) { }
public void LogErrorFromException(Exception ex) { }
public void LogErrorFromException(Exception ex, string file, int lineNumber, int linePosition) { }
public void LogMessage(LogMessageImportance importance, string message, params object[] messageArgs) { }
public void LogWarning(string message, params object[] messageArgs) { }
public void LogWarning(string file, int lineNumber, int linePosition, string message, params object[] messageArgs) { }
}
}
"# # this here-string terminator needs to be at column zero
<#
.SYNOPSIS
If nuget is not in the tools
folder then it will be downloaded there.
#>
function Get-Nuget(){
[cmdletbinding()]
param(
$toolsDir = "$env:LOCALAPPDATA\NuGet\BuildTools\",
$nugetDownloadUrl = 'https://dist.nuget.org/win-x86-commandline/latest/nuget.exe'
)
process{
$nugetDestPath = Join-Path -Path $toolsDir -ChildPath nuget.exe
if(!(Test-Path $nugetDestPath)){
'Downloading nuget.exe' | Write-Verbose
# download nuget
$webclient = New-Object System.Net.WebClient
$webclient.DownloadFile($nugetDownloadUrl, $nugetDestPath)
# double check that is was written to disk
if(!(Test-Path $nugetDestPath)){
throw 'unable to download nuget'
}
}
# return the path of the file
$nugetDestPath
}
}
function Get-Nuget-Package(){
[cmdletbinding()]
param(
[Parameter(
Mandatory=$true,
Position=0)]
$packageName,
[Parameter(
Mandatory=$true,
Position=1)]
$toolFileName,
$toolsDir = "$env:LOCALAPPDATA\NuGet\BuildTools\",
$nugetDownloadUrl = 'https://dist.nuget.org/win-x86-commandline/latest/nuget.exe'
)
process{
if(!(Test-Path $toolsDir)){
New-Item -Path $toolsDir -ItemType Directory | Out-Null
}
$toolPath = (Get-ChildItem -Path $toolsDir -Include $toolFileName -Recurse) | Select-Object -First 1
if($toolPath){
return $toolPath
}
"Downloading package [$packageName] since it was not found in the tools folder [$toolsDir]" | Write-Verbose
$cmdArgs = #('install',$packageName,'-OutputDirectory',(Resolve-Path $toolsDir).ToString())
"Calling nuget.exe to download [$packageName] with the following args: [{0} {1}]" -f (Get-Nuget -toolsDir $toolsDir -nugetDownloadUrl $nugetDownloadUrl), ($cmdArgs -join ' ') | Write-Verbose
&(Get-Nuget -toolsDir $toolsDir -nugetDownloadUrl $nugetDownloadUrl) $cmdArgs | Out-Null
$toolPath = (Get-ChildItem -Path $toolsDir -Include $toolFileName -Recurse) | Select-Object -First 1
return $toolPath
}
}
function Transform-Config{
[cmdletbinding()]
param(
[Parameter(
Mandatory=$true,
Position=0)]
$sourceFile,
[Parameter(
Mandatory=$true,
Position=1)]
$transformFile,
[Parameter(
Mandatory=$true,
Position=2)]
$destFile,
$toolsDir = "$env:LOCALAPPDATA\NuGet\BuildTools\"
)
process{
$sourcePath = (Resolve-Path $sourceFile).ToString()
$transformPath = (Resolve-Path $transformFile).ToString()
$cheetahPath = Get-Nuget-Package -packageName 'Microsoft.VisualStudio.SlowCheetah' -toolFileName 'Microsoft.VisualStudio.SlowCheetah.dll' -toolsDir $toolsDir
if(!$cheetahPath){
throw ('Failed to download Slow Cheetah package')
}
if (-not ([System.Management.Automation.PSTypeName]'Microsoft.VisualStudio.SlowCheetah.LoggingStub').Type)
{
[Reflection.Assembly]::LoadFrom($cheetahPath.FullName) | Out-Null
Add-Type -TypeDefinition $loggingStubSource -Language CSharp -ReferencedAssemblies $cheetahPath.FullName
}
$logStub = New-Object Microsoft.VisualStudio.SlowCheetah.LoggingStub
$transformer = [Microsoft.VisualStudio.SlowCheetah.TransformerFactory]::GetTransformer($sourcePath, $logStub);
$success = $transformer.Transform($sourcePath, $transformPath, $destFile);
if(!$success){
throw ("Transform of file [] failed!!!!")
}
Write-Host "Transform successful."
}
}
Transform-Config -sourceFile $sourceFile -transformFile $transformFile -destFile $destFile
The config files are not important, you should be able to use an existing app.config and app.ENV.config transform file to play with this.
If there is an easier way to do this, please let me know!
I'm looking to get all of the namespaces in a WinMD file programmatically. I would prefer a PowerShell or C#-based solution since I need it to be in a script, but any language will do as long as it gets the job done.
Here is the code I have right now, using Assembly.ReflectionOnlyLoadFrom:
var domain = AppDomain.CurrentDomain;
ResolveEventHandler assemblyHandler = (o, e) => Assembly.ReflectionOnlyLoad(e.Name);
EventHandler<NamespaceResolveEventArgs> namespaceHandler = (o, e) =>
{
string file = WindowsRuntimeMetadata
.ResolveNamespace(e.NamespaceName, Array.Empty<string>())
.FirstOrDefault();
if (file == null)
return;
var assembly = Assembly.ReflectionOnlyLoadFrom(file);
e.ResolvedAssemblies.Add(assembly);
};
try
{
// Load it! (plain .NET assemblies)
return Assembly.LoadFrom(path);
}
catch
{
try
{
// Hook up the handlers
domain.ReflectionOnlyAssemblyResolve += assemblyHandler;
WindowsRuntimeMetadata.ReflectionOnlyNamespaceResolve += namespaceHandler;
// Load it again! (WinMD components)
return Assembly.ReflectionOnlyLoadFrom(path);
}
finally
{
// Detach the handlers
domain.ReflectionOnlyAssemblyResolve -= assemblyHandler;
WindowsRuntimeMetadata.ReflectionOnlyNamespaceResolve -= namespaceHandler;
}
}
For some reason, it doesn't seem to be working. When I run it, I'm getting a ReflectionTypeLoadException when I try to load WinMD files. (You can see this question for the full details.)
So my question is, what's the best way to go about doing this, if the Reflection APIs aren't working? How do tools like Visual Studio or ILSpy do this when you hit F12 on a WinRT type? Is there any way to do this from PowerShell?
TL;DR: How do I extract all of the namespaces from a WinMD file? Any language solution accepted.
Thanks.
Ended up taking up #PetSerAl's suggestion for using Mono.Cecil, which is actually pretty solid. Here's the approach I ended up taking (written in PowerShell):
# Where the real work happens
function Get-Namespaces($assembly)
{
Add-CecilReference
$moduleDefinition = [Mono.Cecil.ModuleDefinition]
$module = $moduleDefinition::ReadModule($assembly)
return $module.Types | ? IsPublic | % Namespace | select -Unique
}
function Extract-Nupkg($nupkg, $out)
{
Add-Type -AssemblyName 'System.IO.Compression.FileSystem' # PowerShell lacks native support for zip
$zipFile = [IO.Compression.ZipFile]
$zipFile::ExtractToDirectory($nupkg, $out)
}
function Add-CecilReference
{
$url = 'https://www.nuget.org/api/v2/package/Mono.Cecil'
$directory = $PSScriptRoot, 'bin', 'Mono.Cecil' -Join '\'
$nupkg = Join-Path $directory 'Mono.Cecil.nupkg'
$assemblyPath = $directory, 'lib', 'net45', 'Mono.Cecil.dll' -Join '\'
if (Test-Path $assemblyPath)
{
# Already downloaded it from a previous script run/function call
Add-Type -Path $assemblyPath
return
}
ri -Recurse -Force $directory 2>&1 | Out-Null
mkdir -f $directory | Out-Null # prevent this from being interpreted as a return value
iwr $url -OutFile $nupkg
Extract-Nupkg $nupkg -Out $directory
Add-Type -Path $assemblyPath
}
You can find the full contents of the script here.
I am attempting to spawn a new thread in PowerShell's command line using:
$t = New-Object System.Threading.Thread ([System.Threading.ThreadStart]{
Write-Host "Hello World"
});
$t.Start();
What happens is that a dialog appears saying "Powershell has stopped working".
I want to use my own Job class, written in C#, with start, pause, continue and stop methods. It uses a couple of WaitHandles to achieve this together with a new Thead instance.
I am aware of Start-Job etc, but would like to use real threads.
Any way?
EDIT: There seems to be a way https://davewyatt.wordpress.com/2014/04/06/thread-synchronization-in-powershell/
UPDATE I have packaged the below into a module called PSRunspacedDelegate, which you can install using Install-Package PSRunspacedDelegate. You can find documentation on GitHub.
Adam Driscoll's PowerShell Parallel Foreach explains that a thread running PowerShell code must have a Runspace.
In other words [Runspace]::DefaultRunspace cannot be null.
I ended up writing a RunspacedDelegateModule.psm1 module, with a function New-RunspacedDelegate that does the work.
Add-Type -Path "$PSScriptRoot\RunspacedDelegateFactory.cs"
Function New-RunspacedDelegate(
[Parameter(Mandatory=$true)][System.Delegate]$Delegate,
[Runspace]$Runspace=[Runspace]::DefaultRunspace) {
[PowerShell.RunspacedDelegateFactory]::NewRunspacedDelegate($Delegate, $Runspace)
}
RunspacedDelegateFactory.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Management.Automation.Runspaces;
namespace PowerShell
{
public class RunspacedDelegateFactory
{
public static Delegate NewRunspacedDelegate(Delegate _delegate, Runspace runspace)
{
Action setRunspace = () => Runspace.DefaultRunspace = runspace;
return ConcatActionToDelegate(setRunspace, _delegate);
}
private static Expression ExpressionInvoke(Delegate _delegate, params Expression[] arguments)
{
var invokeMethod = _delegate.GetType().GetMethod("Invoke");
return Expression.Call(Expression.Constant(_delegate), invokeMethod, arguments);
}
public static Delegate ConcatActionToDelegate(Action a, Delegate d)
{
var parameters =
d.GetType().GetMethod("Invoke").GetParameters()
.Select(p => Expression.Parameter(p.ParameterType, p.Name))
.ToArray();
Expression body = Expression.Block(ExpressionInvoke(a), ExpressionInvoke(d, parameters));
var lambda = Expression.Lambda(d.GetType(), body, parameters);
var compiled = lambda.Compile();
return compiled;
}
}
}
What I noticed is that it would still crash if I used Write-Host, but Out-File seems to be ok.
Here is how to use it:
Import-Module RunspacedDelegateModule;
$writeHello = New-RunspacedDelegate ([System.Threading.ThreadStart]{
"$([DateTime]::Now) hello world" | Out-File "C:\Temp\log.txt" -Append -Encoding utf8
});
$t = New-Object System.Threading.Thread $writeHello;
$t.Start();
I'm using the following code to retrieve a specific address book from outlook:
$outlook = $(New-Object -ComObject Outlook.Application)
$Session = $outlook.Session
$Session.Logon()
$ab = $Session.AddressLists | ? {$_.Name -eq 'test'}
then attempting to use C# to convert the $ab object to .net using the following:
$source = #"
using Microsoft.Office.Interop.Outlook;
public unsafe static class OAB
{
public static void List(object Stream)
{
var i = Stream as Microsoft.Office.Interop.Outlook.AddressList;
return i;
}
}
"#
$cp = new-object System.CodeDom.Compiler.CompilerParameters
$cp.CompilerOptions = "/unsafe"
$cp.ReferencedAssemblies.Add('C:\wkdir\refassem\Microsoft.Office.Interop.Outlook.dll')
Add-Type -TypeDefinition $source -CompilerParameters $cp
The error I receive is:
Since 'OAB.List(object)' returns void, a return keyword must not be followed by an object expression
Disclaimer, I'm a complete newbie to C# so go easy on me!
You have your List function declared as a return type of void but you are returning a value. To fix, just change your function to this:
public static Microsoft.Office.Interop.Outlook.AddressList List(object Stream)
void functions mean that no value is going to be returns. If you do plan on returning a value, then you need to remove void and replace it with the type you want to return.