How to access PowerShell host from C# - 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

Related

What is the best way to check PowerShell Execution Policy in C#?

When you run Get-ExecutionPolicy in PowerShell, it gets the effective execution policy. I need to know the best way to get that information in C#. I don't need to know how to change it like many other questions about PowerShell Execution Policy, I just need to know how to get it in C#.
Note:
PowerShell execution policies apply only on Windows.
With respect to Windows, the answer below covers both PowerShell editions.
It can be inferred from the docs that boxdog pointed to in a comment, but to spell it out:
using System;
using System.Management.Automation;
namespace demo
{
class ConsoleApp {
static void Main() {
using (var ps = PowerShell.Create()) {
var effectivePolicy = ps.AddCommand("Get-ExecutionPolicy").Invoke()[0].ToString();
ps.Commands.Clear();
Console.WriteLine("Effective execution policy: " + effectivePolicy);
}
}
}
}
Note:
The above assumes that you're using the PowerShell SDK - see this answer for the appropriate NuGet package to add to your project.
If you're using a PowerShell (Core) 7+ SDK, additional considerations apply:
On Unix-like platforms, execution policies fundamentally do not apply (Unrestricted is reported, though in effect it is Bypass), so the following applies to Windows only:
The LocalMachine scope of any - by definition install-on-demand - PowerShell (Core) 7+ version does not apply; only - if defined - the CurrentUser and GPO-based policies (which preempt the former) do.
On Windows:
In the absence of a relevant execution policy being defined, Restricted is the default, which categorically prevents execution of script files (.ps1).
If your application needs to execute .ps1 files when hosting the PowerShell SDK, for predictable behavior it is best to set the execution policy, for the current process only, as shown in this answer.
The most elegant solution would probably be to get the ExecutionPolicy registry key in HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell. For this solution to work, your program needs to be running on the same architecture (x64 or x86) as the operating system it's running on or it won't be able to see the registry key. Code to do this would look something like this:
using Microsoft.Win32
...
string executionPolicy = Registry.GetValue(#"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell", "ExecutionPolicy", null)?.ToString();
If for any reason you can't do the first solution, the second way I would recommend is by using the System.Management.Automation.PowerShell NuGet package. This method would look something like this:
using(var ps = PowerShell.Create()){
ps.AddScript("Get-ExecutionPolicy");
Collection<PSObject> output = ps.Invoke();
Console.WriteLine($"Execution policy is {output[0]}")
}
If you really don't want to add an extra NuGet package to your project, there is another, but quite a bit messier way of doing this using System.Diagnostics.Process and it's output stream. It would look something like this:
var procInfo = new ProcessStartInfo("powershell.exe", "-Command \"Get-ExecutionPolicy\"")
{
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardOutput = true
};
var proc = new Process
{
StartInfo = procInfo
};
proc.OutputDataReceived += Proc_OutputDataReceived;
proc.Start();
proc.BeginOutputReadLine();
Console.ReadLine();
...
private static void Proc_OutputDataReceived(object sender, DataReceivedEventArgs e)
{
if (!string.IsNullOrWhiteSpace(e.Data))
Console.WriteLine($"Execution policy is {e.Data}");
}

need an optimal way to get a process by means of its executable path

I'm trying to get a process ID by means of the process's execution-path. For that I'm executing the below Powershell command which runs perfectly in Powershell's console:
(Get-Process | Where-Object {$_.Path -eq 'C:\WINDOWS\system32\winlogon.exe'}).Id
But executing the same through C# is giving no results. below is the code snippet I'm following:
string cmd = "(Get-Process | Where-Object {{$_.Path -eq '{0}'}}).Id";
string path = #"C:\WINDOWS\system32\winlogon.exe";
string finalCmd = string.Format(cmd, System.IO.Path.GetFullPath(path));
powershell.Runspace = runspace;
powershell.AddScript(finalCmd);
var result = powershell.Invoke();
I'm using double-culry-braces for escape sequence. But still powershell.Invoke() returns nothing but null. Is there any other way to get the Process Id with its executable path?
My ultimate goal is that I should be able to push an application (MSI installer) to all the PCs in network through Active Directory(irrespective of x86/x64) and I should get the Process Ids for the given executable path. Thanks for the suggestions but in my case I need a generic solution which should work seamlessly for both x86 and x64.
Doesn't seem like you need to use Powershell here. .NET code can query processes directly.
Something like:
Process.GetProcesses().Where(p=>p.MainModule.FileName==path)
should return you an enumerable of all matching processes, from which you can easily retrieve their IDs. And decide what to do if you find more than one!

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

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
}
}
}
}

Escaping single quotes in C# powershell runspace

I am trying to run Exchange cmdlets using System.Automation dll in C#.
In http://technet.microsoft.com/en-us/library/dd315325.aspx, they have said that for escaping single quotes, we basically need to append it with another single quote
For example,
[PS] C:\Windows\system32>Write-Host 'Live and let le''arn'
Live and let le'arn
However, when I try to do the same thing with my cmdlet,
New-Mailbox -Name 'user1.''.cn'
The new mailbox is actually created with name as user.''.cn. We would like it to be user.'.cn
Code to execute this cmdlet is as follows:
AutomatedRunspace.Command command = new AutomatedRunspace.Command(cmdlet.Command);
foreach (CmdletParameter param in cmdlet.GetParameters())
{
command.Parameters.Add(param.Name, param.Value);
}
pipeline.Commands.Add(command);
Is there anything we can do to correctly escape it?
When you're invoking cmdlets in C#, you need to also worry about the C# string quoting behavior. It's not clear in your question what exactly you're setting the Name parameter to in C# code. I would try this:
string nameArg = "user1.'.cn";
That is, the PowerShell API should be bypassing the parameter parsing phase since you're supplying the argument directly via the API.

Call Set-Location from within a cmdlet

I'm creating a cmdlet for PowerShell and I need to be able to call Set-Location (aka cd) from within a the cmdlet. I would do it like this
var setLocation = new Microsoft.PowerShell.Commands.SetLocationCommand();
setLocation.Path = path;
setLocation.Invoke();
except that it gives me an error that says You cannot invoke a cmdlet that derrives from PSCmdlet. I'd like to use Set-Location, but I'd be happy with just simply changing the directory of the shell.
The best answer I could find was to use InvokeScript to change directory:
InvokeCommand.InvokeScript(string.Format("Set-Location {0}", fullPath));
It's possible that there's a "more C#" way to do this, but I couldn't find it.
The question is a bit older but here is a slightly nicer solution:
The class PathIntrinsics contains the method SetLocation.
To manipulate the path of the current session, you can access the PathIntrinsics via the SessionState using the Path property.
long story short:
SessionState.Path.SetLocation("C:/some/path");
You could try to derive from CMDLET not PSCMDLET.
Simply using Set-Location inside the script should do the work.
So for example:
# Script.ps1
CD ~
Get-Location
Set-Location c:\Windows
Get-location
Once the script finishes, try Get-Location again!
If I use Set-Location in a script, I usually like to first use Push-Location to store the current location and then at the end of my script I can use Pop-Location to return the user to where they were.
Push-Location $PWD
Set-Location $somewhere
#script body
Pop-Location
Unless of course, the intention of the script is to change directories for the user.

Categories

Resources