Using C# to run remote PowerShell code looses information through serialization? - c#

For various reason I need to query the mailbox auto-reply configuration for a user using a remote PowerShell command from C# code instead of using the EWS API.
I'm pretty much using this article as a template on how to do this and I'm running into an issue that I can't wrap my head around. Specifically it looks like there is some information being lost through the serialization/deserialization process for remote PowerShell commands. So I'm not able to cast it to another type and use it in the C# code. Would anyone have an idea how to either find a workaround or avoid this?
Down below you can see the code that runs the PowerShell code and returns the objects and tries to do stuff with it. The problem is that the BaseObject type is PSCustomObject and as such the cast/check doesn't work. I am not sure how I'd access the attributes that are exposed by the custom object either. With the debugging tools in VS I'm able to see that it actually kind of has all the data. If I run the code directly in PowerShell I can see that the data type for $configuration would be Deserialized.Microsoft.Exchange.Data.Storage.Management.MailboxAutoReplyConfiguration. So I guess it actually looses some of the information for that object during serialization?
An alternative problem I haven't checked yet (as I'd really like to avoid it) would be that the system I'm running this code on doesn't have the Exchange assemblies installed. That's also why I'm using the clunky BaseObject.GetType().ToString() method in order to check the type as I'm not able to reference the type and use is. But I'd actually kind of expect to get a data structure that is self sufficient from the PowerShell object. Am I wrong about how this would work?
using (PowerShell PowerShellInstance = PowerShell.Create())
{
// add a script that creates a new instance of an object from the caller's namespace
PowerShellInstance.AddScript(#"
$session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionURI <URI>
Import-PSSession $session
$configuration = Get-MailboxAutoReplyConifguration -identity <E-Mail>
# Put it on the output stream
$configuration
");
// invoke execution on the pipeline (collecting output)
Collection<PSObject> PSOutput = PowerShellInstance.Invoke();
// loop through each output object item
foreach (PSObject outputItem in PSOutput)
{
if (outputItem != null)
{
if(outputItem.BaseObject.GetType().ToString() == "Microsoft.Exchange.Data.Storage.Management.MailboxAutoReplyConfiguration"){
# We have a decrepancy here as the above is the Exchange API class and
# below would be the EWS API class. As they expose the same attributes I'd expect it to work.
OofSettings settings = outputItem.BaseObject as OofSettings
}
}
}
}

That's exactly the problem: The Deserialization destroys the original Powershell-Object your script generated and create a new one, with the data of the origin object, but not the methods afaik (Type PSObject).
The workaround is to do the task you have to do in a powershell-script either or directly in the first script (whatever fits better to your needs).
For your example, i mean this:
using (PowerShell PowerShellInstance = PowerShell.Create())
{
// add a script that creates a new instance of an object from the caller's namespace
PowerShellInstance.AddScript(#"
$session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionURI <URI>
Import-PSSession $session
$configuration = Get-MailboxAutoReplyConifguration -identity <E-Mail>
***INSERT HERE Powershell-Cmdlets to do the things you need***
# Put it on the output stream
$configuration
");
// invoke execution on the pipeline (collecting output)
Collection<PSObject> PSOutput = PowerShellInstance.Invoke();
// loop through each output object item
foreach (PSObject outputItem in PSOutput)
{
if (outputItem != null)
{
if(outputItem.BaseObject.GetType().ToString() == "Microsoft.Exchange.Data.Storage.Management.MailboxAutoReplyConfiguration"){
# We have a decrepancy here as the above is the Exchange API class and
# below would be the EWS API class. As they expose the same attributes I'd expect it to work.
OofSettings settings = outputItem.BaseObject as OofSettings
}
}
}
}

Related

In c#, add commands to PowerShell InitialSessionState using script and make them discoverable from C#

Is there a way to find functions that were imported into an InitialSessionState using ImportPSModule()?
I have this wrapper around PowerShell to run scripts self-hosted by my applicaton (comments and error checking removed for brevity)
public class Engine
{
private readonly InitialSessionState sessionState;
public Engine(string initializationModulePath)
{
this.sessionState = InitialSessionState.CreateDefault();
this.sessionState.LanguageMode = PSLanguageMode.FullLanguage;
this.sessionState.AuthorizationManager = null;
// New functionality - allow an initialization file.
this.sessionState.ImportPSModule(new[] { initializationModulePath });
}
public void AddCommand(String name, Type implementingType) => this.sessionState.Commands.Add(
new SessionStateCmdletEntry(
name,
implementingType,
null));
public void RunScript(string script, string scriptComment)
{
using (PowerShell powerShellCommand = PowerShell.Create())
{
powerShellCommand.AddScript(script);
using (powerShellCommand.Runspace = RunspaceFactory.CreateRunspace(this.sessionState))
{
powerShellCommand.Runspace.Open();
Collection<PSObject> results = powerShellCommand.Invoke();
// Results processing omitted.
}
}
}
// Testing - I see things added via AddCommand() but not anything imported via ImportPSModule
public void ListCommands()
{
foreach (SessionStateCommandEntry commandEntry in this.sessionState.Commands)
{
Console.WriteLine($"Command: {commandEntry.Name}, Type: {commandEntry.CommandType}");
}
}
}
When I add types using engine.AddCommand(), they show up in engine.ListCommands() as expected. I now want to allow users to have a custom set of functions they've predefine which I'll import via ImportPSModule(). Finally, I'd like to be able to list out those commands in the UI as commands that are available.
I created a really simple module (init.psm1)
function Add-Number {[CmdletBinding()] param ([int]$a, [int]$b) $a + $b }
function Subtract-Number {[CmdletBinding()] param ([int]$a, [int]$b) $a - $b }
and tested importing it, but the functions don't show up as available commands. (Though user scripts can use them just fine.)
I looked at System.Management.Automation.Language.Parser.ParseFile based on this question and could check for tokens after {function}, but it seems like a round-about way to do it. (Currently, this is the best option I have.)
I considered running a little script with Get-ChildItem function:, but Get-ChildItem is not available when using InitialSessionState.CreateDefault() (I do not want to use InitialSessionState.Create(). My scenario requires the engine to be more locked down than that.)
Is there another option I can use to get the list of user-defined functions? Importing them in another manner is acceptable so long as users can provide them as powershell code.
Thanks!
Edit
Re-reading the documentation, it seems I had missed this very important line:
Add a list of modules to import when the runspace is created.
So I'll update the question a little to be "Is there an way to create a SessionStateCommandEntry() from a script file that can be added to initialSessionState.Commands?"

How to catch full invoke text from powershell class in C#

I want to catch the output exactly as I get it when I run commands in PowerShell.
For instance when I type LS, I get:
Yet when I use this code:
Runspace runspace = RunspaceFactory.CreateRunspace();
runspace.Open();
PowerShell ps = PowerShell.Create(); // Create a new PowerShell instance
ps.Runspace = runspace; // Add the instance to the runspace
ps.Commands.AddScript("ls"); // Add a script
Collection<PSObject> results = ps.Invoke();
runspace.Close();
StringBuilder stringBuilder = new StringBuilder();
foreach (PSObject obj in results)
{
Console.WriteLine(obj.ToString());
}
I get the following output:
Microsoft.Management.Infrastructure.dll
System.Management.Automation.dll
System.Management.Automation.xml
WpfApp1.exe
WpfApp1.exe.config
WpfApp1.pdb
Although this output may come in handy, with an other application I dont get the right output in return, so I would prefer the exact output as I see it in PowerShell itself.
Is there a way to read the output as I get it in PowerShell, line for line?
If you want the exact text that powershell produces you then you can use Out-String in the powershell command:
ps.Commands.AddScript("ls | Out-String");
You can also read the values by accessing the Properties of the PSObject:
foreach (PSObject obj in results)
{
var name = obj.Properties["Name"]?.Value.ToString()
var mode = obj.Properties["Mode"]?.Value.ToString();
var length = obj.Properties["Length"]?.Value.ToString();
var lastMod = (DateTime?)obj.Properties["LastWriteTime"]?.Value;
Console.WriteLine(string.Format("{0} {1} {2} {3}", mode, lastMod, length, name));
}
Note, as mentioned in mklement0's answer, you don't need to use Runspace to execute this powershell. Consider using Get-ChildItem rather than ls.
Note: This answer also recommends what part of haldo's helpful answer shows, in a more focused manner and with supplementary information.
Modify your script to pipe your command to the Out-String
cmdlet, which uses PowerShell's formatting system to render to a string, the same way that output renders to the console.
ps.AddScript("ls | Out-String"); // Add a script
Note:
Windows PowerShell assumes a fixed line width of 120 characters and with (implied) tabular (Format-Table) or wide (Format-Wide) formatting, truncates lines that are longer (except if the output object is of type [string]), with the point of truncation indicated with ...
PowerShell [Core] 7+ exhibits the same behavior fundamentally, but only uses default width 120 as a fallback: when the hosting (console-subsystem) executable is running in a console (terminal), the console window's width is used instead, which is the same behavior you get in a regular PowerShell console window (see this answer).
To fix that, pass a large-enough line width to -Width; e.g.:
ps.AddScript("ls | Out-String -Width 200");
Note:
In Windows PowerShell, do not use -Width ([int]::MaxValue-1), because every line is then padded to that width, which will result in excessively large output.
PowerShell [Core] 7+, this padding is no longer performed, and you can safely use
-Width ([int]::MaxValue-1)
A few asides:
For robustness, I suggest avoiding the use of aliases (such as ls for Get-ChildItem) in scripts and compiled code.
In the case at hand, ls wouldn't work on Unix-like platforms, because the alias isn't defined there, so as not to conflict with the platform-native ls utility.
It's best to wrap PowerShell ps = PowerShell.Create(); in a using block to ensure that the PowerShell instance is disposed of: using (PowerShell ps = PowerShell.Create()) { ... }
There is generally no need to create a runspace explicitly - PowerShell.Create() will create one for you.
The System.Management.Automation.PowerShell instance returned by PowerShell.Create() directly exposes methods such as .AddScript() - no need to use the .Commands property.
You can get compressed json output from powershell with this command
ls | ConvertTo-Json -Compress
Then deserialize. Also this command provide extra info than see in powershell output.

Retrieving GetExecutingRequests from PowerShell into c# object

I am trying to retrieve data from PowerShell into a c# object. The data I am looking for is returned from a PowerShell Invoke() of GetExecutingRequests on a remote web server. The issue I'm having is that I am not getting an error code, but the results from the Invoke() that I'm looking for are nowhere in the return data, or on the PowerShell object.
using (Runspace runspace = RunspaceFactory.CreateRunspace(cxn))
{
runspace.Open();
using (PowerShell ps = PowerShell.Create())
{
ps.Runspace = runspace;
string script = String.Format("Get-WmiObject
WorkerProcess -Namespace root\\WebAdministration -ComputerName {0} |
Invoke-WmiMethod -Name GetExecutingRequests", server);
ps.AddScript(script);
ps.AddParameter("OutputElement", new HttpRequest[0]);
var result = ps.Invoke();
}
}
This code executes, and returns a Collection with 29 items. However, none of them show the GetExecutingRequests results, and there is nothing relevant on the PowerShell object either.
I would like to get the output of GetExecutingRequests into a c# object, so I can do further processing. the PSDataStreams on the ps object also have no results.
Any help would be appreciated.
MORE INFO:
I was able to solve this with a change to the PowerShell script I was sending:
string script = String.Format("Get-WmiObject WorkerProcess -Namespace root\\WebAdministration -ComputerName {0} | Invoke-WmiMethod -Name GetExecutingRequests | %{{ $_ | Select-Object -ExpandProperty OutputElement }}", server);
I'm not quite sure if I can resolve the results directly, but I'd advise running below command to get some some more information on the object being returned. From there, you could look into how you'd handle that returning in C#.
Get-WmiObject -Class $(<scriptblock>) | get-member
Define the object types in C# & see if you're able to capture it in that way first.
If you're not able to make any progress with capturing the object type being returned in powershell yourself, it may be worth posting here to see if anyone else might be able to offer any insight/experience with workarounds for interfacing those objects types into C#.
If above really isn't possible, forcing the powershell to return everything from standard out would help find if it's writing anything meaningful that you can scrape/format in C#. I think the best way to do that in your quoted powershell command would be like so:
return $(Get-WmiObject
WorkerProcess -Namespace root\WebAdministration -ComputerName {0} |
Invoke-WmiMethod -Name GetExecutingRequests | *>&1)
This returns all 5+ standard outs from powershell to the return object (for those reading, see these docs on ps streams). Youshould definitely be able to capture the return in your results variable, but it can't hurt to make sure you're able to throw/capture errors from overflow.
Hope this helps to continue the digging!

$global: is set in a PowerShell session but not in the System.Management.Automation.PowerShell instance

I'm moving the execution of a PowerShell script (StartBackup.ps1) that we would normally run in a standalone PowerShell session into a C# application. The script executes normally directly in PowerShell, imports modules/DLLs, calls into other scripts and sets a ton of variables.
In the C# application, I have:
using (PowerShell powerShell = PowerShell.Create())
{
powerShell.AddCommand("Set-ExecutionPolicy");
powerShell.AddParameter("Scope", "Process");
powerShell.AddParameter("ExecutionPolicy", "RemoteSigned");
powerShell.AddCommand("Set-Location");
powerShell.AddParameter("Path", "E:\\BackupTools");
powerShell.AddCommand("E:\\BackupTools\\StartBackup.ps1", false);
powerShell.AddParameter("Type", "Closed");
Collection<PSObject> results = powerShell.Invoke();
foreach (var resultItem in results)
{
...
}
}
The above runs just fine up until the point where $global: stuff gets set, and that's where it starts to throw errors. All of those values are null/empty.
I added a couple of powerShell.AddCommands to check whether or not those values are set after the script executes, and they are indeed all null in the PowerShell instance. In the standalone shell they're all set just fine.
What is the issue here? Why is the PowerShell instance different from an actual shell?
EDIT: The intention is not to just fire-and-forget the script. The intention is to have it do its job and then continue working with whatever artifacts it leaves behind in the PowerShell instance just as I normally would if this was powershell.exe.
If you want to just execute an existing PowerShell script, the simplest way would be to use the Process class. You can build the command line and run it.
The C# PowerShell Class is required if you want to build your script itself in your C# code.
Also, your AddCommand will chain the commands. Is that your requirement ?
MSDN post
Call AddCommand() methods to add this content to the execution pipeline.
using (PowerShell PowerShellInstance = PowerShell.Create())
{
// use "AddScript" to add the contents of a script file to the end of the execution pipeline.
// use "AddCommand" to add individual commands/cmdlets to the end of the execution pipeline.
PowerShellInstance.AddScript("param($param1) $d = get-date; $s = 'test string value'; " +
"$d; $s; $param1; get-service");
// use "AddParameter" to add a single parameter to the last command/script on the pipeline.
PowerShellInstance.AddParameter("param1", "parameter 1 value!");
}

How to access PowerShell host from C#

In a PowerShell profile, one can identify the PowerShell host in order to do appropriate setup for that host's environment. For example:
if ($host.Name -eq 'ConsoleHost')
{
Import-Module PSReadline
# differentiate verbose from warnings!
$privData = (Get-Host).PrivateData
$privData.VerboseForegroundColor = "cyan"
}
elseif ($host.Name -like '*ISE Host')
{
Start-Steroids
Import-Module PsIseProjectExplorer
}
I would like to be able to do the equivalent identification from a C# context primarily because PowerShell ISE does not support Console.ReadLine so I want to know if it is safe to use it in the current PS host's environment.
I first explored trying to get the output of the Get-Host cmdlet from within C# (per Invoking a cmdlet within a cmdlet). After I located the Microsoft.PowerShell.Commands.Utility assembly (under C:\Program Files (x86)\Reference Assemblies\Microsoft\WindowsPowerShell\3.0) I could compile this but it yielded null...
var cmd = new Microsoft.PowerShell.Commands.GetHostCommand();
var myHost = cmd.Invoke();
...while this would not compile due to the InternalHost class being (ironically!) internal:
var cmd = new Microsoft.PowerShell.Commands.GetHostCommand();
var myHost = cmd.Invoke<System.Management.Automation.Internal.Host.InternalHost>();
Next, I then modified my cmdlet to inherit from PSCmdlet rather than Cmdlet (to allow access to the SessionState), so I could then access the PS host object like this:
var psVarObject = SessionState.PSVariable.GetValue("Host");
Of course, that returns a pure Object, which I then needed to cast to... oh, wait... it's still internal!... so this would not compile:
string psHost = ((System.Management.Automation.Internal.Host.InternalHost)psVarObject).Name;
Leaving me no alternative but to use reflection on a foreign assembly (horrors!):
string psHost = (string)psVarObject.GetType().GetProperty("Name").GetValue(psVarObject, null);
That works, but is less than ideal, because reflecting upon any 3rd-party assembly is a fragile thing to do.
Any alternative ideas on either (a) identifying the host or, (b) backing up a bit, being able to use the host's own Read-Host cmdlet to get a typed input from a user?
You can just use Host property from PSCmdlet class. And if you want to do Read-Host:
Host.UI.ReadLine()
When getting
var psVarObject = SessionState.PSVariable.GetValue("Host");
You can cast it to System.Management.Automation.Host.PSHost instead of InternalHost

Categories

Resources