Creating a persistent Powershell session in C# - c#

I have a project which calls a bunch of Powershell scripts, a few of which take several minutes to run through and cause the page to timeout. I thought about using Start-Job to run several processes in the background to free up the page from timing out, but each time I run a Powershell command it opens a new instance of Powershell and cannot find any jobs.
protected void RunShell()
{
var Shell = PowerShell.Create();
Shell.Commands.AddScript(Textbox.Text);
var results = Shell.Invoke();
if (results.Count > 0)
{
var builder = new StringBuilder();
foreach (var psObject in results)
{
builder.AppendLine(psObject.BaseObject.ToString();
}
Textbox2.Text = builder.ToString();
}
}
protected void button1_Click(object sender, EventAgrs e)
{
Textbox1.Text = "Start-Job -ScriptBlock{.\\script.ps1} | Out-String";
RunShell();
}
protected void button2_Click(object sender, EventArgs e)
{
Textbox1.Text = "Get-Job * | Out-String";
RunShell();
}
I am now hoping to create a persistent session of powershell that I am able to send commands to and retrieve the jobs and statuses from. I cannot figure out how to set up this snippet of code as a global variable.
Any pointers in the right direction would be appreciated.

Did you try this?
PowerShell.Create(CurrentRunspace)
http://msdn.microsoft.com/en-us/library/system.management.automation.runspacemode%28v=vs.85%29.aspx
Edit: If you want to share a PowerShell, you can do this (to share it across the entire application, replace Session with Application):
private PowerShell GetPowerShell()
{
PowerShell Shell = Session["PowerShell"] as PowerShell;
if (Shell == null)
{
Shell = PowerShell.Create();
Session["PowerShell"] = Shell;
}
return Shell;
}

Related

How get session id on remote computers

I need to get the session id of remote users programmatically. I wrote a C# code to submit powershell scripts/commands and get the answer from there using "query session /server:remoteComputer". The script runs well from PowerShell and I got all the session IDs. But when I try to execute the script from my C# code I got "'query' is not recognized as the name of a cmdlet..." . I even tried to submit the same command "query session /server:remoteComputer" from C# and I got the same result. Any clue or advice will be very appreciated. Thanks!
string errorMesg = string.Empty;
//psinstance.AddScript(#"\\fs\PC_Support\PC_Support_SW\getloggedusers.ps1");
psinstance.AddScript("query session /server:remoteComputer");
psinstance.AddCommand("out-string");
PSDataCollection<PSObject> outputCollection = new PSDataCollection<PSObject>();
psinstance.Streams.Error.DataAdded += (object sender1, DataAddedEventArgs e1) =>
{
errorMesg = ((PSDataCollection<ErrorRecord>)sender1)[e1.Index].ToString();
};
IAsyncResult result = psinstance.BeginInvoke<PSObject, PSObject>(null, outputCollection);
psinstance.EndInvoke(result);
//Collection<PSObject> psOutput = psinstance.Invoke();
StringBuilder sb = new StringBuilder();
foreach (var outputItem in outputCollection)
{
sb.AppendLine(outputItem.BaseObject.ToString());
}
if(!string.IsNullOrEmpty(errorMesg))
{
MessageBox.Show(errorMesg);
}
else
{
MessageBox.Show(sb.ToString());
}
return;

How can I run a short powershell script in my C# WinForms Application?

I've built a monitoring application and one of the many things it will monitor are a few services on a few different servers in our network and I'd like to display the status of these services and whether they are still working or not.
So, lets say I'd want to run this short Powershell script;
Get-Service -ComputerName "DESKTOP" -Name "WinDefend"
And let's say I'd like to run this every 1 minute using the Timer event. Which would look something like this;
private void InitializeTimer()
{
// Call this procedure when the application starts
timer1.Interval = 60000; // 60 seconds
timer1.Tick += new EventHandler(timer1_Tick);
// Enable timer
timer1.Enabled = true;
}
// Timer tick
private void timer1_Tick(object sender, EventArgs e)
{
// Powershell script here
}
How would I actually implement this short Powershell script into this example?
My second question would also be, how could I correctly display the data after retreiving it? Could I somehow Write the data to maybe a text box or Label?
Thank you very much for any feedback and/ or help!
C# WinForms Calling PowerShell 5.1
Problems to Overcome:
To run PowerShell version 5.1 in C#, I believe you need your C# program to be based on .NET Framework, probably 4.x. This may not be true, but I've already spent far more time on this than I ever expected, so not going to investigate alternatives.
C# used to offer through Nuget an easy method for adding PowerShell 5.1 to your project. This is no longer true and you now have to take extra steps to obtain a version of System.Management.Automation.dll that is designed for PowerShell 5.1.
While you can, with extra steps, obtains System.Management.Automation.dll from NuGet, it is not as new as the version used by PowerShell 5.1 on a fully updated Windows 10 system.
The version of Get-Service provided by versions of PowerShell 7.x, and I believe 6.x as well, do NOT provide the ComputerName parameter, nor any built in method for accessing remote computers. There are options for executing Get-Service on a remote system and returning the results, but this too can be problematic - as in firewall issues, certain service(s) not running, I'm thinking certain registry key setting can cause issues, etc...
The Forms.Timer class can update controls on a form due to the fact that it uses the same thread, BUT, this means that waiting 20 seconds for Get-Service to return info from a remote computer will lock that thread and freeze the form.
Building VS C# project with correct version of System.Management.Automation.dll:
Some generalized steps:
Start Visual Studio 2022:
"Create a new project"
C# -> Windows -> Desktop
Windows Forms App (.NET Framework) -> Next
Set "Project name" to project's name
Set "Location" to project's path
Set "Framework" to ".NET Framework 4.8"
"Create"
Working in the Visual Studio 2022 Project:
Tools -> NuGet Package Manager -> Package Manager Console
Enter command to add System.Management.Automation.dll
Get command from here: https://www.nuget.org/packages/System.Management.Automation.dll/
NuGet command: NuGet\Install-Package System.Management.Automation.dll -Version 10.0.10586
Optional:
In PowerShell 5.1
Navigate to the project's \packages\System.Management.Automation.dll.10.0.10586.0\lib\net40
Execute: Copy ([PSObject].Assembly.Location)
The trick to copy System.Management.Automation.dll from Windows' PowerShell 5.1 came from here (Other useful info on the same page):
https://stackoverflow.com/a/13485939/4190564
When designing the form and controls, I believe this is all of the unique settings that I used:
Form:
(Name) = runPowerShellForm
Size = 700, 200
Text = "Test Form for Running PowerShell"
{Set Load to run RunPS}
Label:
(Name) = outputLabel
AutoSize = false
Anchor = Top, Bottom, Left, Right
BackColor = White
Font = Lucida Console, 12pt
ForeColor = Navy
Size = 660, 114
Text = ""
TextBox:
(Name) = computerNameTextBox
Anchor = Bottom, Left, Right
Size = 532, 20
Button:
(Name) = updateComputerButton
Anchor = Bottom, Right
Size = 122, 23
Text = "Update Computer"
{Set button click}
And this is the actual code:
public partial class runPowerShellForm : Form {
private static string _computerName = ".";
private static int _tickCount = 0;
private static System.Timers.Timer _timer = new System.Timers.Timer();
private static Label _outputLabel = null;
private static PowerShell ps = PowerShell.Create();
private static void NewGetService(string computerName) {
_computerName = computerName;
ps = PowerShell.Create();
ps.AddCommand("Get-Service").AddParameter("ComputerName", computerName).AddParameter("Name", "WinDefend");
}
private static void RunPS() {
string LabelText = "Computer: " + _computerName + "\r\nTick Count: " + (++_tickCount).ToString() + "\r\n\r\n";
LabelText += "Status Name DisplayName\r\n";
LabelText += "------ ---- -----------\r\n";
foreach(PSObject result in ps.Invoke()) {
LabelText += String.Format(
"{0,-9}{1,-19}{2}",
result.Members["Status"].Value,
result.Members["Name"].Value,
result.Members["DisplayName"].Value);
}
_outputLabel.BeginInvoke(new UpdateLabel(UpdateMethod), _outputLabel, LabelText);
}
public delegate void UpdateLabel(Label arg1, string arg2);
public static void UpdateMethod(Label labelCtrl, string textStr) {
labelCtrl.Text = textStr;
}
private static void OnTickEvent(Object source, System.Timers.ElapsedEventArgs e) {
RunPS();
}
public runPowerShellForm() {
InitializeComponent();
}
private void updateComputerButton_Click(object sender, EventArgs e) {
NewGetService(this.computerNameTextBox.Text);
this.computerNameTextBox.Text = "";
RunPS();
}
private void runPowerShellForm_Load(object sender, EventArgs e) {
_outputLabel = this.outputLabel;
_timer.Elapsed += OnTickEvent;
_timer.Interval = 30000;
_timer.Enabled = true;
NewGetService(_computerName);
RunPS();
}
private void runPowerShellForm_SizeChanged(object sender, EventArgs e) {
this.computerNameTextBox.Text = this.Size.ToString();
}
}
The Original Answer:
This falls far short of answering the question, but it does illustrate how to call PowerShell from the C# that is called from PowerShell.
If you are trying to execute a single PowerShell command, then maybe the answers to Run PSCmdLets in C# code (Citrix XenDesktop) and How to execute a powershell script using c# and setting execution policy? would do what you want.
I'm doing everything in PowerShell right now, so just easier for me to create a PowerShell script that calls C# that Calls PowerShell. This PowerShell script calls the Run method of the C# class DoPS that invokes Get-Service -ComputerName "." -Name "WinDefend" and then uses WriteLine statements to mimic the expected output of Get-Service:
Add-Type -Language 'CSharp' -TypeDefinition #'
using System;
using System.Management.Automation;
public class DoPS {
public void Run(){
//Get-Service -ComputerName "." -Name "WinDefend"
PowerShell ps = PowerShell.Create();
ps.AddCommand("Get-Service").AddParameter("ComputerName", ".").AddParameter("Name", "WinDefend");
Console.WriteLine("Status Name DisplayName");
Console.WriteLine("------ ---- -----------");
foreach (PSObject result in ps.Invoke()) {
Console.WriteLine(
"{0,-9}{1,-19}{2}",
result.Members["Status"].Value,
result.Members["Name"].Value,
result.Members["DisplayName"].Value);
}
}
}
'#
$DoPS = [DoPS]::new()
$DoPS.Run()
Which outputs this text:
Status Name DisplayName
------ ---- -----------
Running WinDefend Microsoft Defender Antivirus Service

Mimic this very simple powershell command in C#

Trying to mimic the command Get-CimInstance CIM_ManagedSystemElement in C#
string NamespacePath = "\\\\.\\Root\\CIMv2";
string ClassName = "CIM_ManagedSystemElement";
//Create ManagementClass
ManagementClass oClass = new ManagementClass(NamespacePath + ":" + ClassName);
//Get all instances of the class and enumerate them
foreach (ManagementObject oObject in oClass.GetInstances())
{
//access a property of the Management object
Console.WriteLine("Caption : {0}", oObject["Caption"]);
}
Sadly, that didnt work as expected, would like to get some help
Thanks
You do this like this (you have to add System.Management namespace)
Because CIM_ManagedSystemElement is at the default WMI namespace( which is Root\CIMV2) you don't have to specify it at ManagementObjectSearcher.
Also, be sure that you have the minimum supported client- Windows Vista
string query = #"SELECT * FROM CIM_ManagedSystemElement";
var moSearch = new ManagementObjectSearcher(query);
var moCollection = moSearch.Get();
foreach (ManagementObject mo in moCollection)
{
Console.WriteLine("Caption = " + mo["Caption"]);
}
Furthermore i suggest you use an ORM to remove boilerplate code like ORMi or Kexla
I also couldn't get your code to work, but in the meantime if you need a workaround you can use the PowerShell API from within C# using this simple program I wrote based on some online documentation. It will give you an output you're looking for. You should have access to all the properties in OutputCollection_DataAdded so if you need more than Caption you can grab it here. Also, at the end of the execution there is a foreach() loop that will contain the entire output collection if you need to do something with that. The execution is extremely slow so I had to make it async to work.
static void Main(string[] args)
{
using (PowerShell ps = PowerShell.Create())
{
ps.AddCommand("Get-CimInstance");
ps.AddParameter("-ClassName", "CIM_ManagedSystemElement");
var outputCollection = new PSDataCollection<PSObject>();
outputCollection.DataAdded += OutputCollection_DataAdded;
// invoke execution on the pipeline (collecting output)
var async = ps.BeginInvoke<PSObject, PSObject>(null, outputCollection);
// do something else until execution has completed.
// this could be sleep/wait, or perhaps some other work
while (async.IsCompleted == false)
{
Console.WriteLine("Waiting for pipeline to finish...");
Thread.Sleep(1000);
// might want to place a timeout here...
}
Console.WriteLine("Execution has stopped. The pipeline state: " + ps.InvocationStateInfo.State);
// loop through each output object item
foreach (PSObject outputItem in ps.EndInvoke(async))
{
// if null object was dumped to the pipeline during the script then a null
// object may be present here. check for null to prevent potential NRE.
if (outputItem != null)
{
//TODO: do something with the output item
// outputItem.BaseOBject
}
}
Console.Read();
}
}
private static void OutputCollection_DataAdded(object sender, DataAddedEventArgs e)
{
if (sender is PSDataCollection<PSObject>)
{
var output = (PSDataCollection<PSObject>)sender;
// Handle the output item here
var caption = output.Last().Properties["Caption"];
if (caption != null)
{
Console.WriteLine($"Caption: {caption.Value}");
}
}
}

Hyper-V Get-VM in powershell via asp.net c#

I am trying to write a web interface that will show the current VMs on a remote Hyper-V host.
So far, I have this
protected void getVMS(object sender, GridViewCommandEventArgs e)
{
//command to run
string cmdToRun = "get-vm -computername fsyovs02";
var shell = PowerShell.Create();
shell.Commands.AddScript(cmdToRun);
var results = shell.Invoke();
if (results.Count > 0)
{
var builder = new StringBuilder();
foreach (PSObject vm in results)
{
builder.Append(vm.BaseObject.ToString() + "\r\n");
}
ResultBox.Text = Server.HtmlEncode(builder.ToString());
}
}
This is returning something, but not what I want. For each VM, it is returning the line
Microsoft.HyperV.PowerShell.VirtualMachine
What I want is it to display exactly how it does via powershell.
Can anyone help me please - as I am going out of my mind!
Many Thanks
Mark
Sorry had to edit my answer as you cannot access those types from C#. You should then use the Members collection on PSObject to access particular properties of the VirtualMachine. Please check this approach in your case:
foreach (PSObject vm in results)
{
builder.Append(vm.Members["Name"].Value + "\r\n");
}

How to close a file in Autocad using C# keeping acad.exe running?

I am using visual studio 2010 and I am having a .DWG file which I want to open in autocad. Till now I have used this.
Process p = new Process();
ProcessStartInfo s = new ProcessStartInfo("D:/Test File/" + fileName);
p.StartInfo = s;
p.Start();
But what I want is to close the file inside the Autocad but not the autocad itself. (Means atocad.exe should be kept running).
Till now I hve used this but its closing the acad.exe not the file.
foreach (Process Proc in Process.GetProcesses())
{
if (Proc.ProcessName.Equals("acad"))
{
Proc.CloseMainWindow();
Proc.Kill();
}
}
Take the Autocad .NET libraries from Autodesk Sites (http://usa.autodesk.com/adsk/servlet/index?id=773204&siteID=123112)
Then you will be able to use Application and Document classes.
They will give you full control over opening and closing documents within the application.
You can find many articles on that, and can ask further questions.
AutoCAD does have an api. there are 4 assemblys. Two for in-process and two for COM.
inprocess :
acdbmgd.dll
acmgd.dll
COMInterop :
Autodesk.Autocad.Interop.dll
Autodesk.Autocad.Interop.Common.dll
this is a method that will open a new instance of AutoCAD or it will connect to an existing running instance of AutoCAD.
you will need to load these .dlls into your project references.
using Autodesk.AutoCAD.Interop;
using Autodesk.AutoCAD.Interop.Common;
namespace YourNameSpace {
public class YourClass {
AcadApplication AcApp;
private const string progID = "AutoCAD.Application.18.2";// this is AutoCAD 2012 program id
private string profileName = "<<Unnamed Profile>>";
private const string acadPath = #"C:\Program Files\Autodesk\AutoCAD 2012 - English\acad.exe";
public void GetAcApp()
{
try
{
AcApp = (AcadApplication)Marshal.GetActiveObject(progID);
} catch {
try {
var acadProcess = new Process();
acadProcess.StartInfo.Arguments = string.Format("/nologo /p \"{0}\"", profileName);
acadProcess.StartInfo.FileName = (#acadPath);
acadProcess.Start();
while(AcApp == null)
{
try { AcApp = (AcadApplication)Marshal.GetActiveObject(progID); }
catch { }
}
} catch(COMException) {
MessageBox.Show(String.Format("Cannot create object of type \"{0}\"",progID));
}
}
try {
int i = 0;
var appState = AcApp.GetAcadState();
while (!appState.IsQuiescent)
{
if(i == 120)
{
Application.Exit();
}
// Wait .25s
Thread.Sleep(250);
i++;
}
if(AcApp != null){
// set visibility
AcApp.Visible = true;
}
} catch (COMException err) {
if(err.ErrorCode.ToString() == "-2147417846"){
Thread.Sleep(5000);
}
}
}
}
}
closeing it is as simple as
Application.Exit();
and forgive the code. its atrocious, this was one of my first methods when i just started developing...
I doubt you will be able to do this unless AutoCAD has an API that you can hook into and ask it to close the file for you.
Your c# app can only do things to the process (acad.exe) , it doesn't have access to the internal operations of that process.
Also, you shouldn't use Kill unless the process has become unresponsive and certainly not immediately after CloseMainWindow.
CloseMainWindow is the polite way to ask an application to close itself. Kill is like pulling the power lead from the socket. You aren't giving it the chance to clean up after itself and exit cleanly.
There is one other possibility - this will only work if your C# code is running on the same machine as the AutoCAD process and it is not really recommended, but, if you are really stuck and are prepared to put up with the hassle of window switching you can send key strokes to an application using the SendKeys command.
MSDN articles here:
http://msdn.microsoft.com/EN-US/library/ms171548(v=VS.110,d=hv.2).aspx
http://msdn.microsoft.com/en-us/library/system.windows.forms.sendkeys.send.aspx
Using this you could send the key strokes to simulate the user using the menu commands to close the file.
To perform the closing of file, best way out is to follow the steps at this ObjectARX SDK for c# and change the following code with the below code.
[CommandMethod("CD", CommandFlags.Session)]
static public void CloseDocuments()
{
DocumentCollection docs = Application.DocumentManager;
foreach (Document doc in docs)
{
// First cancel any running command
if (doc.CommandInProgress != "" &&
doc.CommandInProgress != "CD")
{
AcadDocument oDoc =
(AcadDocument)doc.AcadDocument;
oDoc.SendCommand("\x03\x03");
}
if (doc.IsReadOnly)
{
doc.CloseAndDiscard();
}
else
{
// Activate the document, so we can check DBMOD
if (docs.MdiActiveDocument != doc)
{
docs.MdiActiveDocument = doc;
}
int isModified =
System.Convert.ToInt32(
Application.GetSystemVariable("DBMOD")
);
// No need to save if not modified
if (isModified == 0)
{
doc.CloseAndDiscard();
}
else
{
// This may create documents in strange places
doc.CloseAndSave(doc.Name);
}
}
}

Categories

Resources