Calling PowerShell function from within the script in C# Application - c#

I just started playing with PowerShell two days ago. I am running a PowerShell script form a C# console application. I pass in a list of objects (MyObj1) as a parameter to the script and do some data manipulation to the object. However, when I call a function within the script I get a function not recognized error while printing out the errors. Here is a small sample of what I am doing.
public class MyObj1
{
public string Property1 {get; set;}
public int Property2 {get; set;}
}
Here is part of code that I am using to run the script:
var rs = RunspaceFactory.CreateRunspace();
rs.Open();
rs.SessionStateProxy.SetVariable("list", myObjectList);
rs.SessionStateProxy.SetVariable("xml", xml);
var ps = PowerShell.Create();
ps.Runspace = rs;
var psScript = System.IO.File.ReadAllText(#"C:\temp\ps.ps1");
ps.AddScript(psScript);
ps.Invoke();
foreach (var item in myObjectList)
{
Console.WriteLine(item.Property1 + "; " + item.Property1);
}
Console.WriteLine("\n================================ ERRORS ============================ \n");
foreach (ErrorRecord err in ps.Streams.Error)
{
Console.WriteLine(err.ToString());
}
And here is the actual script:
$list | ForEach {
$_.Property1 = $_.GetType()
DoSomeThing ([ref] $_)
}
Function DoSomeThing ($MyObject)
{
$Asset.Property2 = $Asset.Property2 + $Asset.Property2
}
Two days ago while I was playing with same script using some dummy classes like above, I was able to modify the data but since this morning, I have modified the script to use real classes. And Ctrl+Z will only take me back to a certain point in my editor. In the ForEach loop everything works until I cal the function.
As for the project, the plan is to push this application with required data manipulation in C# and then have some optional changes done in the script. The point is to avoid push to the application to production server and handling all/most data manipulation in the script.

Move function declaration to the beginnig of the script file.
Function DoSomeThing ($MyObject)
{
$Asset.Property2 = $Asset.Property2 + $Asset.Property2
}
$list | ForEach {
$_.Property1 = $_.GetType()
DoSomeThing ([ref] $_)
}
PS doesn't compile script, it executes it command by command. So you have to create/declare function before you use it.

Related

Capturing the output of a PowerShell process started from a WPF app

I am trying to do some operations in the WPF application based on the PowerShell output that is captured and stored in a .log file.
I have a WPF application that takes some configurations and starts a PowerShell process. The output of the Powershell scripts is written as they are being executed in a .log file and keeps on updating until the scripts are fully executed. The Code for starting the PowerShell process looks like this:
private void OpenPowershell(string path, string script, string logFile, string type)
{
try
{
BtnExecute.IsEnabled = false;
bool is64 = IntPtr.Size == 8;
var ENV = "Get-ItemProperty HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\* "
+ (is64 ? ",HKLM:\\Software\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*" : "")
+ " | Select-Object DisplayName";
ProcessStartInfo startInstall = new ProcessStartInfo(path, ENV);
startInstall.UseShellExecute = false;
startInstall.Arguments = script;
startInstall.EnvironmentVariables.Add("RedirectStandardOutput", "true");
startInstall.EnvironmentVariables.Add("RedirectStandardError", "true");
startInstall.EnvironmentVariables.Add("UseShellExecute", "false");
startInstall.EnvironmentVariables.Add("CreateNoWindow", "true");
Process Install = Process.Start(startInstall);
Thread.Sleep(3000);
Install.Close();
txtInstallationStarted.Text = $"Installation Started... More details can be found at {logFile}";
}
catch (Exception error)
{
MessageBox.Show(error.Message);
}
}
private void ProcessSectionScript1(List<string> sectionData)
{
if (sectionData.Count == 0)
{
return;
}
if (sectionData.ToString().Contains("error"))
{
Script1Error = true;
}
if (Script1Error)
{
Script1StatusStack.Visibility = Visibility.Visible;
Script1Fail.Visibility = Visibility.Visible;
Script1Check.Visibility = Visibility.Collapsed;
Script1TextBlock.Text = "Script1 Installation Failed";
}
else
{
Script1StatusStack.Visibility = Visibility.Visible;
Script1Check.Visibility = Visibility.Visible;
Script1Fail.Visibility = Visibility.Collapsed;
Script1TextBlock.Text = "Script1 Installed Successfully";
}
}
This PS script also executes a bunch of other scripts and I need to do some UI changes based on the output. The execution of the PS scripts can be differentiated too as I have lines such as:
Script 1 started
//lines
Script 1 complete
Script 2 started
//lines
Script 2 complete
.
.
From Script 1 to Script 2, I need to check if a line containing "Error" is displayed. If it is, I need to enable a component and if not, I need to enable another indicating that everything was correct. This needs to be then checked for Script 2 to Script 3.
For example, if I see something like this:
Script 1
Error in executing xyz command
Script 2
I need to do something like this:
Script1Success.Visibility = Visibility.Collapsed;
Script1Fail.Visibility = Visibility.Visible;
I cannot seem to make it work. With this, I'm having the following problems:
The if (sectionData.ToString().Contains("error") does not get true even if there is an error in the line.
Also, I think the execution is wrong. Please note that from the time you press the button to start the Powershell scripts to the .log file being created. There is a small delay of maybe 2-3 seconds because the Powershell window needs to open and start which is why I have added Thread.Sleep(3000);.
I was thinking that perhaps I need to split it up in sections from Script 1 to Script 2 so that I can check them accordingly. However, I cannot seem to make it work and right now, it just shows the Completed icon visibility regardless of the status.
If further clarification is required about what I am trying to do, I'll update the question. Thank you for reading this.

How do I call Python scripts with packages from IronPython?

How would I come to implementing a Python script that requires external modules (such as BeautifulSoup4 and requests) in my C# project? I want to call a function in the Python script but it keeps throwing me a "IronPython.Runtime.Exceptions.ImportException: 'No module named requests'".
I do not have any IronPython script written because I'm using a Python script that was made from another user. I have a virtual environment set up with Python to import the necessary modules.
I have no idea where to approach this. I've looked on multiple sites but they all require me to write an IronPython script. Are there no other options to calling functions other than setting up the necessary script?
C# Code:
static dynamic pythonFile;
static void Main(string[] args)
{
// Loading Python file
var ipy = Python.CreateRuntime();
pythonFile = ipy.UseFile("test.py"); //throws error when this launches.
DisplayMain();
Console.ReadKey();
}
Python Code (small snippet for example):
import requests
def get_stock(stock_name, price_metric):
print(stock_name + " " + price_metric)
def get_industry_indicators(startTime, endTime):
print(startTime + " " + endTime)
def get_market_cap(industry, startTime, endTime):
print(industry + " " + startTime + " " + endTime)
def get_headers(link):
print(requests.get(link))
I want to be able to call my python function (get_headers) and pass in a link and then have it print on my C# Console. Please help!
I found the error. Search paths for the needed modules must be included. Here is an example.
var engine = Python.CreateEngine();
var searchPaths = new List<string>();
searchPaths.Add(AppDomain.CurrentDomain.BaseDirectory + #"\Lib\site-packages");
searchPaths.Add(#"C:\...\projectName\Lib\");
engine.SetSearchPaths(searchPaths);
engine.ExecuteFile("test.py");

C# & Powershell: Not returning all Powershell results

I've been making a C# application that runs some Powershell stuff, the issue is that it doesn't seem to be be collecting the correct/all results of the Powershell commands.
The below code:
private void Button_Click(object sender, RoutedEventArgs e)
{
List<string> results = RunScript("Get-ADUser testuser2 -Properties *");
this.ShowMessageAsync("OK",results[0]);
}
private List<string> RunScript(string comand)
{
var powerShell = PowerShell.Create();
powerShell.Runspace = runspace;
powerShell.AddScript(comand);
Collection<PSObject> results = powerShell.Invoke();
List<string> resultsList = new List<string>();
if (powerShell.Streams.Error.Count > 0)
{
this.ShowMessageAsync("Something went wrong!", "Check error log for details!");
runspaceBox.AppendText(powerShell.Streams.Error[0] + "\r\n");
}
else
{
foreach (PSObject obj in results)
{
runspaceBox.AppendText(obj.ToString() + "\r\n");
resultsList.Add(obj.ToString());
}
}
return resultsList;
}
Results in:
{CN=testuser2,DC=testdomain,DC=com}
Putting the command into Powershell directly results in the below:
PS C:\Users\Ultimate-V1> Get-ADUser testuser2 -Properties *
[A bunch of extra stuff above]
Description :
DisplayName : Test User2
DistinguishedName : CN=Test
One,CN=Users,DC=testdomain,DC=com
Division :
DoesNotRequirePreAuth : False
[A lot of extra stuff below]
I've snipped a lot of the results out of the above command as it is very long.
What I can't work out is why it is only returning the Distinguished Name and not all of the results, this happens with some commands but not others. The command to return all AD groups on a user works fine for example.
If possible i'd love it to have EVERYTHING that the command returns, I will be formatting the resultsList later in the code.
Any help or pointers would be greatly appreciated!
Because you casted the result as a string, so you got a string representation of the complex object (whatever the result of the object's .ToString() method).
In PowerShell itself, it doesn't cast everything to a string to display it; it has various rules about how to display objects in general (usually a table or list format), and some types have specific formatters.
PetSerAl already answered in the comments, but the way to get a string representation of what PowerShell displays on the console is to send your output through Out-String:
powerShell.AddScript(comand).AddCommand("Out-String");

Unsure how to handle powershell response in .NET

I have 2 powershell scripts that I execute from c# which I'm using to first list message using an IMAP cmdlet and the 2nd script performs a view on a specific message. Both execute successfully from powershell, the 2nd one I am trying to retrieve an attachment and I see a bunch of data output to the console like so...
Sent from my iPhone
------=_NextPart_000_0027_01CCDAA7.399EBE00
Content-Type: image/jpeg; name="photo.JPG"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="photo.JPG"
/9j/4QH6RXhpZgAATU0AKgAAAAgACgEPAAIAAAAGAAAAhgEQAAIAAAAHAAAAjAESAAMAAAABAAYA
AAEaAAUAAAABAAAAlAEbAAUAAAABAAAAnAEoAAMAAAABAAIAAAEyAAIAAAAUAAAApAITAAMAAAAB
AAEAAIdpAAQAAAABAAAAuIglAAQAAAABAAABZgAAAABBcHBsZQBpUGhvbmUAAAAAAEgAAAABAAAA
SAAAAAEyMDA5OjA5OjIwIDE1OjEwOjU1AAAKgp0ABQAAAAEAAAE2kAAABwAAAAQwMjIxkAMAAgAA
ABQAAAE+kAQAAgAAABQAAAFSkQEABwAAAAQBAgMAoAAABwAAAAQwMTAwoAEAAwAAAAEAAQAAoAIA
BAAAAAEAAAZAoAMABAAAAAEAAASwpAYAAwAAAAEAAAAAAAAAAAAAAA4AAAAFMjAwOTowOToyMCAx
so it appears everything is fine so far - except there is I believe a difference in the function result as possible a stream/pipe from powershell?
The first one returns a collection of PSObjects which is called like so
PowerShell ps = PowerShell.Create();
ps.Runspace = runspace;
ps.AddScript(script);
ps.Invoke();
foreach (PSObject result in ps.Invoke())
{
dynamic val = result.BaseObject;
}
The 2nd script executes without error using Invoke() however trying to retrieve data via for each or
dynamic xx = ps2.Invoke();
xx is empty;
I thought maybe I need to use BeginInvoke and call asynchronously so I tried
static dynamic GotMail(dynamic o)
{
return o;
}
delegate dynamic SomeDelegate(dynamic o);
and attempted to use like so..
SomeDelegate sd = GotMail;
IAsyncResult ar = ps2.BeginInvoke();
dynamic val2 = sd.EndInvoke(ar);
and I recieve "The async result object is null or of an unexpected type." I'm not even passing parameters into the powershell scripts..everything is hardcoded - the script I run from powershell successfully is exactly what I execute from .NET
Any suggestions for this much appreciated.
Thanks
You're invoking the script twice by calling Invoke() twice. Remove the first call to invoke e.g.:
PowerShell ps = PowerShell.Create();
ps.Runspace = runspace;
ps.AddScript(script);
//ps.Invoke();
foreach (PSObject result in ps.Invoke()) {
dynamic val = result.BaseObject;
}

How do I deal with Paths when writing a PowerShell Cmdlet?

What is the proper way to receive a file as a parameter when writing a C# cmdlet? So far I just have a property LiteralPath (aligning with their parameter naming convention) that is a string. This is a problem because you just get whatever is typed into the console; which could be the full path or could be a relative path.
Using Path.GetFullPath(string) doesn't work. It thinks I'm currently at ~, I'm not. Same problem occurs if I change the property from a string to a FileInfo.
EDIT: For anyone interested, this workaround is working for me:
SessionState ss = new SessionState();
Directory.SetCurrentDirectory(ss.Path.CurrentFileSystemLocation.Path);
LiteralPath = Path.GetFullPath(LiteralPath);
LiteralPath is the string parameter. I'm still interested in learning what is the recommended way to handle file paths that are passed as parameters.
EDIT2: This is better, so that you don't mess with the users current directory, you should set it back.
string current = Directory.GetCurrentDirectory();
Directory.SetCurrentDirectory(ss.Path.CurrentFileSystemLocation.Path);
LiteralPath = Path.GetFullPath(LiteralPath);
Directory.SetCurrentDirectory(current);
This is a surprisingly complex area, but I have a ton of experience here. In short, there are some cmdlets that accept win32 paths straight from the System.IO APIs, and these typically use a -FilePath parameter. If you want to write a well behaved "powershelly" cmdlet, you need -Path and -LiteralPath, to accept pipeline input and work with relative and absolute provider paths. Here's an excerpt from a blog post I wrote a while ago:
Paths in PowerShell are tough to understand [at first.] PowerShell Paths - or PSPaths, not to be confused with Win32 paths - in their absolute forms, they come in two distinct flavours:
Provider-qualified: FileSystem::c:\temp\foo.txt
PSDrive-qualified: c:\temp\foo.txt
It's very easy to get confused over provider-internal (The ProviderPath property of a resolved System.Management.Automation.PathInfo – the portion to the right of :: of the provider-qualified path above) and drive-qualified paths since they look the same if you look at the default FileSystem provider drives. That is to say, the PSDrive has the same name (C) as the native backing store, the windows filesystem (C). So, to make it easier for yourself to understand the differences, create yourself a new PSDrive:
ps c:\> new-psdrive temp filesystem c:\temp\
ps c:\> cd temp:
ps temp:\>
Now, let's look at this again:
Provider-qualified: FileSystem::c:\temp\foo.txt
Drive-qualified: temp:\foo.txt
A bit easier this time to see what’s different this time. The bold text to the right of the provider name is the ProviderPath.
So, your goals for writing a generalized provider-friendly Cmdlet (or advanced function) that accepts paths are:
Define a LiteralPath path parameter aliased to PSPath
Define a Path parameter (which will resolve wildcards / glob)
Always assume you are receiving PSPaths, NOT native provider-paths (e.g. Win32 paths)
Point number three is especially important. Also, obviously LiteralPath and Path should belong in mutually exclusive parameter sets.
Relative Paths
A good question is: how do we deal with relative paths being passed to a Cmdlet. As you should assume all paths being given to you are PSPaths, let’s look at what the Cmdlet below does:
ps temp:\> write-zip -literalpath foo.txt
The command should assume foo.txt is in the current drive, so this should be resolved immediately in the ProcessRecord or EndProcessing block like (using the scripting API here to demo):
$provider = $null;
$drive = $null
$pathHelper = $ExecutionContext.SessionState.Path
$providerPath = $pathHelper.GetUnresolvedProviderPathFromPSPath(
"foo.txt", [ref]$provider, [ref]$drive)
Now you everything you need to recreate the two absolute forms of PSPaths, and you also have the native absolute ProviderPath. To create a provider-qualified PSPath for foo.txt, use $provider.Name + “::” + $providerPath. If $drive is not $null (your current location might be provider-qualified in which case $drive will be $null) then you should use $drive.name + ":\" + $drive.CurrentLocation + "\" + "foo.txt" to get a drive-qualified PSPath.
Quickstart C# Skeleton
Here's a skeleton of a C# provider-aware cmdlet to get you going. It has built in checks to ensure it has been handed a FileSystem provider path. I am in the process of packaging this up for NuGet to help others get writing well-behaved provider-aware Cmdlets:
using System;
using System.Collections.Generic;
using System.IO;
using System.Management.Automation;
using Microsoft.PowerShell.Commands;
namespace PSQuickStart
{
[Cmdlet(VerbsCommon.Get, Noun,
DefaultParameterSetName = ParamSetPath,
SupportsShouldProcess = true)
]
public class GetFileMetadataCommand : PSCmdlet
{
private const string Noun = "FileMetadata";
private const string ParamSetLiteral = "Literal";
private const string ParamSetPath = "Path";
private string[] _paths;
private bool _shouldExpandWildcards;
[Parameter(
Mandatory = true,
ValueFromPipeline = false,
ValueFromPipelineByPropertyName = true,
ParameterSetName = ParamSetLiteral)
]
[Alias("PSPath")]
[ValidateNotNullOrEmpty]
public string[] LiteralPath
{
get { return _paths; }
set { _paths = value; }
}
[Parameter(
Position = 0,
Mandatory = true,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true,
ParameterSetName = ParamSetPath)
]
[ValidateNotNullOrEmpty]
public string[] Path
{
get { return _paths; }
set
{
_shouldExpandWildcards = true;
_paths = value;
}
}
protected override void ProcessRecord()
{
foreach (string path in _paths)
{
// This will hold information about the provider containing
// the items that this path string might resolve to.
ProviderInfo provider;
// This will be used by the method that processes literal paths
PSDriveInfo drive;
// this contains the paths to process for this iteration of the
// loop to resolve and optionally expand wildcards.
List<string> filePaths = new List<string>();
if (_shouldExpandWildcards)
{
// Turn *.txt into foo.txt,foo2.txt etc.
// if path is just "foo.txt," it will return unchanged.
filePaths.AddRange(this.GetResolvedProviderPathFromPSPath(path, out provider));
}
else
{
// no wildcards, so don't try to expand any * or ? symbols.
filePaths.Add(this.SessionState.Path.GetUnresolvedProviderPathFromPSPath(
path, out provider, out drive));
}
// ensure that this path (or set of paths after wildcard expansion)
// is on the filesystem. A wildcard can never expand to span multiple
// providers.
if (IsFileSystemPath(provider, path) == false)
{
// no, so skip to next path in _paths.
continue;
}
// at this point, we have a list of paths on the filesystem.
foreach (string filePath in filePaths)
{
PSObject custom;
// If -whatif was supplied, do not perform the actions
// inside this "if" statement; only show the message.
//
// This block also supports the -confirm switch, where
// you will be asked if you want to perform the action
// "get metadata" on target: foo.txt
if (ShouldProcess(filePath, "Get Metadata"))
{
if (Directory.Exists(filePath))
{
custom = GetDirectoryCustomObject(new DirectoryInfo(filePath));
}
else
{
custom = GetFileCustomObject(new FileInfo(filePath));
}
WriteObject(custom);
}
}
}
}
private PSObject GetFileCustomObject(FileInfo file)
{
// this message will be shown if the -verbose switch is given
WriteVerbose("GetFileCustomObject " + file);
// create a custom object with a few properties
PSObject custom = new PSObject();
custom.Properties.Add(new PSNoteProperty("Size", file.Length));
custom.Properties.Add(new PSNoteProperty("Name", file.Name));
custom.Properties.Add(new PSNoteProperty("Extension", file.Extension));
return custom;
}
private PSObject GetDirectoryCustomObject(DirectoryInfo dir)
{
// this message will be shown if the -verbose switch is given
WriteVerbose("GetDirectoryCustomObject " + dir);
// create a custom object with a few properties
PSObject custom = new PSObject();
int files = dir.GetFiles().Length;
int subdirs = dir.GetDirectories().Length;
custom.Properties.Add(new PSNoteProperty("Files", files));
custom.Properties.Add(new PSNoteProperty("Subdirectories", subdirs));
custom.Properties.Add(new PSNoteProperty("Name", dir.Name));
return custom;
}
private bool IsFileSystemPath(ProviderInfo provider, string path)
{
bool isFileSystem = true;
// check that this provider is the filesystem
if (provider.ImplementingType != typeof(FileSystemProvider))
{
// create a .NET exception wrapping our error text
ArgumentException ex = new ArgumentException(path +
" does not resolve to a path on the FileSystem provider.");
// wrap this in a powershell errorrecord
ErrorRecord error = new ErrorRecord(ex, "InvalidProvider",
ErrorCategory.InvalidArgument, path);
// write a non-terminating error to pipeline
this.WriteError(error);
// tell our caller that the item was not on the filesystem
isFileSystem = false;
}
return isFileSystem;
}
}
}
Cmdlet Development Guidelines (Microsoft)
Here is some more generalized advice that should help you out in the long run:
http://msdn.microsoft.com/en-us/library/ms714657%28VS.85%29.aspx

Categories

Resources