PowerShell using an existing session state to fill in InitialSessionState - c#

I am trying to call\create runspace within a class that derived from PSCmdlet. Since PSCmdlet includes a default session state that contains shared data I want to reuse in the runspace, I am wondering if there is a programmatically way to convert the current sessionState into the runspace's InitialSessionState ?
If there is no such way, I am not really understand why such session state info cannot be shared within different runspace. This looks like running a remote runspace to me. Can anyone explain?
For example,
namespace CustomCmdlet.Test
{
[Cmdlet("Get", "Test1")]
public class GetTest1 : PSCmdlet
{
protected override void ProcessRecord()
{ WriteObject("1"); }
}
[Cmdlet("Get", "Test2")]
public class GetTest2 : PSCmdlet
{
protected override void ProcessRecord()
{
// instead of import the module dll using Runspace
// InitialSessionState.ImportModule(new string[] {"CustomCmdlet.Test.dll"});
// Runspace runspace = RunspaceFactory.CreateRunspace(InitialSessionState)
// is it any way to import the PSCmdlet.SessionState into the InitialSessionState?
}
}
We are using PowerShell 4.0, if this is relevant.

Session state definitely can't be shared across runspaces, it holds session specific data like variables.
The InitialSessionState that was used to create a runspace is a property of the runspace. You can access the current runspace via thread local storage using the Runspace.DefaultRunspace property: http://msdn.microsoft.com/en-us/library/system.management.automation.runspaces.runspace.defaultrunspace(v=vs.85).aspx
That said, you may want to look at RunspacePool - the pool of runspaces will all be created from the same InitialSessionState. This way you avoid creating more runspaces than necessary, instead just reusing a runspace from the pool.

I know this is years late, but
$maxThreads = 17;
$iss = [system.management.automation.runspaces.initialsessionstate]::CreateDefault();
[void]$iss.ImportPSModule($exchangeModule.Path)
$runspacePool = [runspacefactory]::CreateRunspacePool(1, $maxThreads, $iss, $host)
$runspacePool.Open() > $null;
#EndRegion Initial sessionstate
#Region Run threads and collect data
$threads = New-Object System.Collections.ArrayList;
foreach ($server in $servers)
{
if (!($server.ToString().Contains("unreachable")))
{
$powerShell = [powershell]::Create($iss);
$powerShell.RunspacePool = $runspacePool;
[void]$powerShell.AddScript({
Param ($server)
[pscustomobject]#{
server = $server
} | Out-Null
$global:ProgressPreference = 'SilentlyContinue';
$returnVal = Get-Queue -Server $server; # | where {$_.MessageCount -gt 1};
$customObject = New-Object -TypeName psobject;
$customObject | Add-Member -MemberType NoteProperty -Name ServerName -Value $server;
$customObject | Add-Member -MemberType NoteProperty -Name MessageCountObject -Value $returnVal;
#Write-Host "Object return value is is: " $messageCountObject;
return $customObject;
}) # end of powershell.Add script
$powerShell.AddParameter('server', $server) | Out-Null;
$returnVal = $PowerShell.BeginInvoke();
$temp = "" | Select PowerShell,returnVal;
$temp.PowerShell = $PowerShell;
$temp.returnVal = $returnVal;
$threads.Add($Temp) | Out-Null;
}
else
{
$threads.Add($server) | Out-Null;
}
} #foreach server
#EndRegion Run threads and collect data

Related

How to avoid sending a Connect/Disconnect-ExchangeOnline for every pwsh command in a c# Blazor app

I am developing a server side Blazor app to facilitate Exchange online management via a GUI. I have a helper class that setups the runspace and communicates the results back to the the Blazor component. I added the class via an scoped Dependency Injection.
The helper class is setup as:
InitialSessionState runspaceConfiguration = InitialSessionState.CreateDefault();
runspaceConfiguration.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.RemoteSigned;
runspaceConfiguration.ImportPSModule("ExchangeOnlineManagement");
runspaceConfiguration.ThrowOnRunspaceOpenError = true;
_runspace = RunspaceFactory.CreateRunspace(runspaceConfiguration);
// Open local runspace
_runspace.Open();
_shell = PowerShell.Create();
_shell.Runspace = _runspace;
SetUpEventHandlers();
My function to execute commands is a follow:
public async Task ExecuteScript(string scriptContents)
{
_shell.AddScript(scriptContents);
OutputEventArgs rtc = new() {
Data = await _shell.InvokeAsync().ConfigureAwait(true)
};
_shell.Streams.ClearStreams();
_shell.Commands.Clear();
ResultsAvailable?.Invoke(this, rtc);
}
To connect to Exchnage Online I use a certificate, it looks like:
string? certificateThumbPrint = config.GetValue<string>("certificateThumbPrint");
string? appId = config.GetValue<string>("appId");
string? tenant = config.GetValue<string>("tenant");
logger.Info($"Running : Connect-ExchangeOnline -Certificate {certificateThumbPrint} -AppID {appId} -Organization {tenant} -ShowProgress $false -ShowBanner $false");
string scriptContents = $"Connect-ExchangeOnline -CertificateThumbPrint {certificateThumbPrint} -AppID {appId} -Organization {tenant}";
await objPowerShell.ExecuteScript(scriptContents);
I have setup EventHandler's that handle all the streams and results async, and everything works fine for one user. The problem is when I have multiple users. It still all works fine, except when one user disconnects the session, all active connections to the Exchange Online environment are disconnected. As I am using EXO v3, I cannot save the PSSession and import it.
So my question is how can I disconnect one user without having an impact on the other.
I did tried to do the same thing directly in PowerShell:
$Initialstate = [InitialSessionState]::CreateDefault()
$RunspacePool = [runspacefactory]::CreateRunspacePool(1,5)
$RunspacePool.Open()
$Runspace1 = [runspacefactory]::CreateRunspace()
$PowerShell1 = [powershell]::Create()
$PowerShell1.RunspacePool = $RunspacePool
$Runspace1.Open()
[void]$PowerShell1.AddScript({
$cert = Get-ChildItem Cert:\LocalMachine\My\0CF..4C17.
Connect-ExchangeOnline -Certificate $cert -AppID 70539...5889 -Organization myorg.onmicrosoft.com
})
$PowerShell1.Invoke()
$PowerShell1.Commands.Clear()
[void]$PowerShell1.AddScript({
Get-ConnectionInformation
})
$conn1 = $PowerShell1.Invoke()
$PowerShell1.Commands.Clear()
[void]$PowerShell1.AddScript({
Get-MailBox "ue*" -resultsize 2 | Select-Object PrimarySmtpAddress
})
$mbx1 = $PowerShell1.Invoke()
$mbx1 # Gets the info ok
$PowerShell2 = [powershell]::Create()
$PowerShell2.RunspacePool = $RunspacePool
[void]$PowerShell2.AddScript({
$cert = Get-ChildItem Cert:\LocalMachine\My\0CF.4C17D
Connect-ExchangeOnline -Certificate $cert -AppID 70539...5889 -Organization myorg.onmicrosoft.com
})
$PowerShell2.Invoke()
$PowerShell2.Commands.Clear()
[void]$PowerShell2.AddScript({
Get-ConnectionInformation
})
$conn2 = $PowerShell2.Invoke()
$PowerShell2.Commands.Clear()
[void]$PowerShell2.AddScript({
Get-MailBox "uz*" -resultsize 2 | Select-Object PrimarySmtpAddress
})
$mbx2 = $PowerShell2.Invoke()
$mbx2 # Gets the info ok
But when I execute now "Get-ConnectionInformation" I see 2 connections and "Disconnect-ExchangeOnline" disconnects them both. So, how can I only disconnect one ?

C# Pipeline.Invoke() not returning powershell collection

On Windows 10, in C# I am trying to call a powershell script via Pipeline.Invoke() to retrieve all the disks on a network server, for each server on a network. But can't get it to return the data to C# when there are multiple disks on a server.
I have the following C# code:
foreach (var server in _collServers)
{
Pipeline pipeline2 = runspace.CreatePipeline();
pipeline2 = runspace.CreatePipeline();
scriptCommand = new Command(#"C:\DEV\Monitor\Monitor\bin\scripts\GetDisks.ps1");
CommandParameter commandParm = new CommandParameter("$server", server);
scriptCommand.Parameters.Add(commandParm);
pipeline2.Commands.Add(scriptCommand);
var collDisks = pipeline2.Invoke();
foreach (var item in collDisks)
{
var s = item.ToString();
}
}
which executes the following powershell script (GetDisks.ps1), with ComputerName hardcoded to a specific server for now:
$disks = Get-CimInstance -ClassName CIM_LogicalDisk -ComputerName SERVER01 | Where-Object { ($_.DeviceID -ge 'C') -and ($_.DriveType -eq 3)} | Select-Object DeviceID, VolumeName, Size, Freespace
$disksout = [System.Collections.Generic.List[PSObject]]::New()
ForEach($d in $disks)
{
$disk = [PSObject] #{
'DeviceID' = $d.DeviceID
'VolumeName' = $d.VolumeName
'Size' = $d.Size
'Freespace' = $d.Freespace
}
$disksout.Add($disk)
}
$disksout
but it doesnt return a PSObjects collection into collDisks.
It works in the ISE (and every re-arrangement I try seems to work there).
It works if I run it for my local machine by removing -ComputerName SERVER01
$disks = Get-CimInstance -ClassName CIM_LogicalDisk | Where-Object { ($_.DeviceID -ge 'C') -and ($_.DriveType -eq 3)} | Select-Object DeviceID, VolumeName, Size, Freespace
It doesnt make any difference if I try using PSCustomObject instead, or creating the object collection different ways.

Opening Runspace Pool using custom classes (C#) throws error

I have to add multithreading capabilities to a PowerShell script. Unfortunately I get an error when I try to open an runspace pool using custom C# classes in the InitialSessionState object. The following example is a DEMO code only, not the original script. You can copy&paste it and it will run without any modifications needed. What is really strange is, that all seems to work correctly despite the error message coming up when opening the runspace pool.
This is the error message (translated):
Error while loading the extended type data file: Error in type data "FooBar.BarClass": The TypeData must have: "Members", "TypeConverters", "TypeAdapters" or "StandardMembers".
I have already worked and tested for hours and have no idea what is the reason for that message. And yes, I know that there are very good libraries and cmdlets available for multithreading, but I cannot use them for different reasons.
# Simple c-sharp classes
Add-Type -TypeDef #"
namespace FooBar {
public class FooClass {
public string Foo() {
return "Foo";
}
}
public class BarClass {
public string Bar() {
return "Bar";
}
}
}
"#
function callFooBar {
[FooBar.FooClass]$foo = New-Object FooBar.FooClass
[FooBar.BarClass]$bar = New-Object FooBar.BarClass
$foo.Foo() + $bar.Bar()
}
$scriptBlock = {
Write-Output ( callFooBar )
}
# Setting up an initial session state object
$initialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
# Getting the function definition for the functions to add
$functionDefinition = Get-Content function:\callFooBar
$functionEntry = New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry -ArgumentList 'callFooBar', $functionDefinition
# And add it to the iss object
[void]$initialSessionState.Commands.Add($functionEntry)
# Get the type data for the custom types to add
$typeData = New-Object System.Management.Automation.Runspaces.TypeData -ArgumentList 'FooBar.FooClass'
$typeEntry = New-Object System.Management.Automation.Runspaces.SessionStateTypeEntry -ArgumentList $typeData, $false
# And add it to the iss object
[void]$initialSessionState.Types.Add($typeEntry)
# Get the type data for the custom types to add
$typeData = New-Object System.Management.Automation.Runspaces.TypeData -ArgumentList 'FooBar.BarClass'
$typeEntry = New-Object System.Management.Automation.Runspaces.SessionStateTypeEntry -ArgumentList $typeData, $false
# And add it to the iss object
[void]$initialSessionState.Types.Add($typeEntry)
# Create Runspace pool
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1, ([int]$env:NUMBER_OF_PROCESSORS + 1), $initialSessionState, $Host)
$RunspacePool.ApartmentState = 'MTA'
[void]$RunspacePool.Open() # <<<< Error occurs here!
[System.Collections.Generic.List[object]]$Jobs = #()
1..2 | % {
$job = [System.Management.Automation.PowerShell]::Create($initialSessionState)
$job.RunspacePool = $RunspacePool
[void]$job.AddScript($scriptBlock)
$jobs += New-Object PSObject -Property #{
RunNum = $_
Pipe = $job
Result = $job.BeginInvoke()
}
}
do {
} while ($jobs.Result.IsCompleted -contains $false)
Write-Host "All jobs completed!"
$Results = #()
foreach ($job in $jobs) {
$Results += $job.Pipe.EndInvoke($job.Result)
}
$Results
$RunspacePool.Close()
$RunspacePool.Dispose()
#mklement0 Your hint leads me to the right direction! Custom C# classes with type-definition must be added in a different way to the runspacepool. This was working now (after a few hours testing with all possible runspace methods...):
# Get the script data for the custom c# class to add
$scriptDefinition = New-Object System.Management.Automation.Runspaces.ScriptConfigurationEntry -ArgumentList 'FooBar.FooClass', $false
$scriptEntry = New-Object System.Management.Automation.Runspaces.SessionStateScriptEntry -ArgumentList $scriptDefinition
# And add it to the iss object
[void]$initialSessionState.Commands.Add($scriptEntry)
# Get the script data for the custom c# class to add
$scriptDefinition = New-Object System.Management.Automation.Runspaces.ScriptConfigurationEntry -ArgumentList 'FooBar.BarClass', $false
$scriptEntry = New-Object System.Management.Automation.Runspaces.SessionStateScriptEntry -ArgumentList $scriptDefinition
# And add it to the iss object
[void]$initialSessionState.Commands.Add($scriptEntry)
Not by type, but with script and command. Thank you for your hint.

Inserting a PowerShell command into c#

I want to use this PowerShell command in a C# project:
Get-VM -Name Win8-Henry | Get-VMNetworkAdapter | Select MacAddress
This is what I normally do in c#:
public static void MacAdd(string machineName,Runspace run) {
// Get-VM -Name Win8-Henry | Get-VMNetworkAdapter | Select MacAddress
Command command = new Command("Get-VM");
command.Parameters.Add("Name", machineName);
using (Pipeline hostPipeline = run.CreatePipeline())
{
hostPipeline.Commands.Add(command);
Collection<PSObject> echos = hostPipeline.Invoke();
hostPipeline.Stop();
}
}
What I need help with is adding the second command, and then using the pipeline.
Use the AddScript() method on the PowerShell class.
var Command = String.Format("Get-VM -Name {0} | Get-VMNetworkAdapter | Select MacAddress", computername);
var PowerShell = PowerShell.Create();
PowerShell.AddScript(Command);
PowerShell.Invoke();
Just add the other commands to the pipeline too :)
hostPipeline.Commands.Add(new Command("Get-VMNetworkAdapter"));
Just to share what I do when I have a script that I need to execute in an application. I can then replace the parameters (i.e., "$!$machineName$!$" easily and get the result cleanly.
Dim lxScript As XElement = <Script>
.{
Get-VM -Name $!$machineName$!$ | Get-VMNetworkAdapter | Select MacAddress
}
</Script>

What is the command to access Exchange Management Tools from C# code in Exchange 2010

In Exchange 2007 this line of code is used to load the Exchange Poweshell commands snapin:
PSSnapInInfo info = rsConfig.AddPSSnapIn(
"Microsoft.Exchange.Management.PowerShell.Admin",
out snapInException);
However, this does not exist in Exchange 2010 and I am pulling my hair out trying to find out how to access the Exchange Powershell commands from C# code. Microsoft.Exchange.Management.PowerShell.Admin does not exist anywhere on the Exchange Server and I can find nothing on Google that talks about an equivalent line of code.
How do I access Exchange Management Tools from C# code in Exchange 2010?
Below is my complete code for reference, it all works until I add the line of code:
//Creating and Opening a Runspace
RunspaceConfiguration rsConfig = RunspaceConfiguration.Create();
PSSnapInException snapInException = null;
PSSnapInInfo info = rsConfig.AddPSSnapIn(
"Microsoft.Exchange.Management.PowerShell.Admin",
out snapInException);
Runspace myRunSpace = RunspaceFactory.CreateRunspace(rsConfig);
myRunSpace.Open();
//How Do I Run a Cmdlet?
//create a new instance of the Pipeline class
Pipeline pipeLine = myRunSpace.CreatePipeline();
//create an instance of the Command class
// by using the name of the cmdlet that you want to run
Command myCommand = new Command(txtCommand.Text);
//add the command to the Commands collection of the pipeline
pipeLine.Commands.Add(myCommand);
Collection<PSObject> commandResults = pipeLine.Invoke();
// iterate through the commandResults collection
// and get the name of each cmdlet
txtResult.Text = "start ....";
foreach (PSObject cmdlet in commandResults)
{
string cmdletName = cmdlet.Properties["Name"].Value.ToString();
System.Diagnostics.Debug.Print(cmdletName);
txtResult.Text += "cmdletName: " + cmdletName;
}
txtResult.Text += ".... end";
I don't know for sure, but Exchange 2010 powershell might be implemented as a powershell 2.0 module, which is loaded in a different manner. To find out, go to a system with the exchange management shell on it, and fire it up. Next, run:
ps> get-module
This will list the loaded v2 modules. I would expect the exchange one to appear if you have started the dedicated exchange management shell. If you loaded the regular powershell console, try:
ps> get-module -list
This will list all modules available to load. If you spot the right one, then you'll need to build your code against the v2 system.management.automation dll. For reasons beyond the scope of this reply, v2 powershell's assembly has the same strong name as v1's, so you cannot easily have both versions of powershell on the same machine. Build this from a machine with v2 powershell installed:
InitialSessionState initial = InitialSessionState.CreateDefault();
initialSession.ImportPSModule(new[] { *modulePathOrModuleName* });
Runspace runspace = RunspaceFactory.CreateRunspace(initial);
runspace.Open();
RunspaceInvoke invoker = new RunspaceInvoke(runspace);
Collection<PSObject> results = invoker.Invoke(*myScript*);
Hope this helps,
-Oisin
After a lot of trial and error, I finally figured this out. The problem with the code above is it works great when run against Exchange 2007 but things have changed in Exchange 2010. Instead of the snapin called "Microsoft.Exchange.Management.PowerShell.Admin", use this snapin, "Microsoft.Exchange.Management.PowerShell.E2010".
The complete code to run a Powershell command from C# looks like this. Hope this helps someone else trying to do this.
You will need references to System.Management.Automation.Runspaces, System.Collections.ObjectModel and System.Management.Automation also.
I found that the reference to System.Management.Automation had to be manually added to the csproj file itself in the ItemGroup section using notepad like this:
<Reference Include="System.Management.Automation" />
code below:
private class z_test
{
//set up
private RunspaceConfiguration rsConfig = RunspaceConfiguration.Create();
private PSSnapInException snapInException = null;
private Runspace runSpace;
private void RunPowerShell()
{
//create the runspace
runSpace = RunspaceFactory.CreateRunspace(rsConfig);
runSpace.Open();
rsConfig.AddPSSnapIn("Microsoft.Exchange.Management.PowerShell.E2010", out snapInException);
//set up the pipeline to run the powershell command
Pipeline pipeLine = runSpace.CreatePipeline();
//create the script to run
String sScript = "get-mailbox -identity 'rj'";
//invoke the command
pipeLine.Commands.AddScript(sScript);
Collection<PSObject> commandResults = pipeLine.Invoke();
//loop through the results of the command and load the SamAccountName into the list
foreach (PSObject results in commandResults)
{
Console.WriteLine(results.Properties["SamAccountName"].Value.ToString());
}
pipeLine.Dispose();
runSpace.Close();
}
}
This is what I am doing:
$sessionOptionsTimeout=180000
$sessionOptionsTimeout=180000
$so = New-PSSessionOption -OperationTimeout $sessionOptionsTimeout -IdleTimeout $sessionOptionsTimeout -OpenTimeout $sessionOptionsTimeout
$connectionUri="http://$fqdn/powershell?serializationLevel=Full;ExchClientVer=14.3.91.1"
$s = New-PSSession -ConnectionURI "$connectionUri" -ConfigurationName Microsoft.Exchange -SessionOption $so
$s | Enter-PSSession
PS>get-mailboxserver
EncryptionRequired AutoDatabaseMountDial DatabaseCopyAutoActivationPo
licy
------------------ --------------------- ----------------------------
e GoodAvailability Unrestricted
e GoodAvailability Unrestricted
Now, converting above to .net (c#) should be easy...
Essentially an exerpt from: "C:\Program Files\Microsoft\Exchange Server\V14\Bin\ConnectFunctions.ps1"
Please refer to the following function:
function _NewExchangeRunspace([String]$fqdn,
[System.Management.Automation.PSCredential]
$credential=$null,
[bool]$UseWIA=$true,
[bool]$SuppressError=$false,
$ClientApplication=$null,
$AllowRedirection=$false)
{
$hostFQDN = _GetHostFqdn
if (($fqdn -ne $null) -and ($hostFQDN -ne $null) -and ($hostFQDN.ToLower() -eq $fqdn.ToLower()))
{
$ServicesRunning = _CheckServicesStarted
if ($ServicesRunning -eq $false)
{
return
}
}
Write-Verbose ($ConnectFunctions_LocalizedStrings.res_0005 -f $fqdn)
$so = New-PSSessionOption -OperationTimeout $sessionOptionsTimeout -IdleTimeout $sessionOptionsTimeout -OpenTimeout $sessionOptionsTimeout;
$setupRegistryEntry = get-itemproperty HKLM:\SOFTWARE\Microsoft\ExchangeServer\v14\Setup -erroraction:silentlycontinue
if ( $setupRegistryEntry -ne $null)
{
$clientVersion = "{0}.{1}.{2}.{3}" -f $setupRegistryEntry.MsiProductMajor, $setupRegistryEntry.MsiProductMinor, $setupRegistryEntry.MsiBuildMajor, $setupRegistryEntry.MsiBuildMinor
$connectionUri = "http://$fqdn/powershell?serializationLevel=Full;ExchClientVer=$clientVersion"
}
else
{
$connectionUri = "http://$fqdn/powershell?serializationLevel=Full"
}
if ($ClientApplication -ne $null)
{
$connectionUri = $connectionUri + ";clientApplication=$ClientApplication"
}
write-host -fore Yellow ("connectionUri: " + $connectionUri)
$contents = 'New-PSSession -ConnectionURI "$connectionUri" -ConfigurationName Microsoft.Exchange -SessionOption $so'
if (-not $UseWIA)
{
$contents = $contents + ' -Authentication Kerberos -Credential $credential'
}
if ($SuppressError)
{
$contents = $contents + ' -erroraction silentlycontinue'
}
if ($AllowRedirection)
{
$contents = $contents + ' -AllowRedirection'
}
write-host -fore Yellow ("contents: " + $contents)
write-host -fore Yellow ("join n contents: " + [string]::join("`n", $contents))
[ScriptBlock] $command = $executioncontext.InvokeCommand.NewScriptBlock([string]::join("`n", $contents))
$session=invoke-command -Scriptblock $command
if (!$?)
{
# ERROR_ACCESS_DENIED = 5
# ERROR_LOGON_FAILURE = 1326
if (!(5 -eq $error[0].exception.errorcode) -and
!(1326 -eq $error[0].exception.errorcode))
{
#Write-Verbose ($ConnectFunctions_LocalizedStrings.res_0006 -f $fqdn)
return
}
else
{
# no retries if we get 5 (access denied) or 1326 (logon failure)
#$REVIEW$ connectedFqdn is not set. Is it okay?
break connectScope
}
}
$session
}

Categories

Resources