-1

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!

  • Maybe it's simpler if you use the [ServiceController](https://learn.microsoft.com/en-us/dotnet/api/system.serviceprocess.servicecontroller) class to get status information (and a plethora of other details) about the services (or devices) of specific machines – Jimi Oct 29 '22 at 02:53

1 Answers1

0

C# WinForms Calling PowerShell 5.1

Problems to Overcome:

  1. 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.
  2. 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.
  3. 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.
  4. 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...
  5. 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
Darin
  • 1,423
  • 1
  • 10
  • 12
  • Would you also happen to know how I could send this data to a TextBox after obtaining it? Instead of sending this data to Console.WriteLine. – Stefan Meeuwessen Nov 02 '22 at 08:45
  • @StefanMeeuwessen, I can probably help you figure it out. What is the name of the textbox? What exactly do you want in the textbox? Is this a multi line textbox? – Darin Nov 02 '22 at 16:20
  • Yes, this is a Multiline text box named Textbox1. I'd love to get the same or a similar output to the example you have already given with the Console.WriteLine. – Stefan Meeuwessen Nov 07 '22 at 06:52
  • @StefanMeeuwessen, give the info a try. The process is more complex than I expected, but hope you are able to get it to work. – Darin Nov 10 '22 at 00:01