locked
How to execute a Powershell script asynchronously with SignalR? RRS feed

  • Question

  • User260076833 posted

    Hello,

    I want to execute a PowerShell script, which is contained in a TextBox. The output of the script should be written into another TextBox. Te execution should be started, when the user clicks on a button "Execute".

    The script should be executed asynchroneously: When a long script produces output from time to time, the new portions of output should be written to the output TextBoox as they are produced.

    My approach is like this: When the button "Execute" is clicked, the button handler method creates a PowerShell instance and registers a callback method "output_DataAdded", which is called whenever new portions of output arrive. This method then sends the output to the client using SignalR (IHubContext.Clients.All.write).

    There is a problem which I cannot solve:

    • When the user first clicks on "Execute", the script is invoked and the callback method is called, which in turn calls a client-side method.
      The output is written into the output TextBox. Everything is ok so far.
    • When the user then clicks on "Execute" again, nothing happens. I can verify by setting a breakpoint that the callback method "output_DataAdded" is called.
      Even the call to "IHubContext.Clients.All.write" is reached, but it never makes it to the client. I can verify this by adding an alert trace at the client side method.
      This alert is only executed, when the user clicks the button for the first time. The procedure is not repeatable for some reason.

    In order to further analyze what's going on here, I removed all the PowerShell related code. The event handler method for the button click then only calls HubContext.Clients.All.write. This version works perfectly: Whenever the user clicks the button, an output text is written into the output TextBox.
    So the procedure is repeatable.

    I conclude that the problem must result from the way I process the PowerShell script. There must be something that prevents the procedure from being repeatable. Maybe I need to clean up something after the invokation is done? I don't know.

    Below is a minimal version of my code.
    I hope that someone can see what's the problem here.

    (Note that I started different threads on similar topics, because I don't want to mix problems with SignalR and problems with PowerShell code.)

    Thanks
    Magnus

    OutputHub.cs:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using Microsoft.AspNet.SignalR;
    
    namespace Playground.lab.PowerShellExecution.Asynchroneous
    {
        public class OutputHub : Hub
        {
            public void write(String txt)
            {
                Clients.All.write(txt);
            }
        }
    }

    Test.aspx:

    <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Test.aspx.cs" Inherits="Playground.lab.PowerShellExecution.Asynchroneous.Test" %>
    
    <!DOCTYPE html>
    
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head runat="server">
        <title></title>
        <script src="../../../Scripts/jquery-3.2.1.min.js"></script>
        <script src="../../../Scripts/jquery.signalR-2.2.2.min.js"></script>
        <script src="../../../signalr/hubs"></script>
    
        <script type="text/javascript">
    
            function write( txt)
            {
                alert(txt);
                $('#' + '<% = txt_Output.ClientID%>').append(txt + "<br />");
            }
    
            $(function ()
            {
                var outputHub = $.connection.outputHub;
                outputHub.client.write = write;
                $.connection.hub.start();
            });
        </script>
    
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            <asp:TextBox ID="txt_Input" runat="server" Height="250" Width="400" TextMode="MultiLine" Text="get-date"></asp:TextBox>
            <br />
            <asp:Button ID="btn_Execute" runat="server" Text="Execute" OnClick="btn_Execute_Click"/>
            <br />
            <asp:TextBox ID="txt_Output" runat="server" Height="250" Width="400" TextMode="MultiLine"></asp:TextBox>
        </div>
        </form>
    </body>
    </html>
    

    Test.aspx.cs:

    using System;
    using System.Management.Automation;
    using System.Management.Automation.Runspaces;
    using System.Security;
    using System.Collections.ObjectModel;
    using Microsoft.AspNet.SignalR;
    
    namespace Playground.lab.PowerShellExecution.Asynchroneous
    {
        public partial class Test : System.Web.UI.Page
        {
            private Runspace runSpace;
            
            protected void Page_Load(object sender, EventArgs e)
            {
                if (Session["output"] == null)
                    Session["output"] = "";
    
                txt_Output.Text = (String) Session["output"];
            }
    
            protected void btn_Execute_Click(object sender, EventArgs e)
            {
                //execute_1(); // works only for the first click
                execute_2(); // works always, without Powershell
            }
    
            private void execute_1()
            {
                Session["output"] = "";
                PowerShell psh = PowerShell.Create();
                PSDataCollection<PSObject> output = new PSDataCollection<PSObject>();
                output.DataAdded += output_DataAdded;
                psh.AddScript(txt_Input.Text);
                psh.Streams.Error.DataAdded += Error_DataAdded;
                var result = psh.BeginInvoke<PSObject, PSObject>(null, output);
            }
    
            private void execute_2()
            {
                IHubContext c = GlobalHost.ConnectionManager.GetHubContext<OutputHub>();
                write("CLICK");
            }
    
            void output_DataAdded(object snd, DataAddedEventArgs evt)
            {
                PSDataCollection<PSObject> col = (PSDataCollection<PSObject>)snd;
    
                Collection<PSObject> rsl = col.ReadAll();
    
                foreach (PSObject r in rsl)
                {
                    write(r.ToString());
                }
            }
    
            void Error_DataAdded(object sender, DataAddedEventArgs e)
            {
                //display error message in UI
            }
    
            private void write(String txt)
            {
                IHubContext c = GlobalHost.ConnectionManager.GetHubContext<OutputHub>();
    
                String t;
    
                if (Session["output"] != null)
                    t = (String)Session["output"];
                else
                    t = "";
    
                t = t + txt + "\n";
                Session["output"] = t;
                c.Clients.All.write( t);
            }
        }
    }

    Tuesday, November 28, 2017 1:50 PM

Answers

  • User61956409 posted

    Hi Magnus,

    I put the “Execute” button inside a UpdatePanel Control for prevent from refreshing the whole page, I can get alert message every time when I click the button.

    <asp:UpdatePanel ID="UpdatePanel1" runat="server">
        <ContentTemplate>
            <asp:Button ID="btn_Execute" runat="server" Text="Execute" OnClick="btn_Execute_Click" />
        </ContentTemplate>
    </asp:UpdatePanel>
    

    My test result:

    With Regards,

    Fei Han

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Friday, December 1, 2017 8:59 AM

All replies

  • User61956409 posted

    Hi Magnus,

    I do a test with the following sample code to execute PowerShell script from web application, and push notification message to clients after script executed via ASP.NET SignalR, which works for me, please refer to it.

    protected void btn_Execute_Click(object sender, EventArgs e)
    {
    
        execute_1();
    }
    
    private void execute_1()
    {
        PowerShell psh = PowerShell.Create();
        PSDataCollection<PSObject> output = new PSDataCollection<PSObject>();
    
        psh.AddScript(@"get-process|Select-Object -first 3");
    
        output.DataAdded += Output_DataAdded;
        psh.Streams.Error.DataAdded += Error_DataAdded;
        var result = psh.BeginInvoke<PSObject, PSObject>(null, output);
    
    }
    
    private void Error_DataAdded(object sender, DataAddedEventArgs e)
    {
        throw new NotImplementedException();
    }
    
    private void Output_DataAdded(object snd, DataAddedEventArgs e)
    {
        PSDataCollection<PSObject> col = (PSDataCollection<PSObject>)snd;
    
        Collection<PSObject> rsl = col.ReadAll();
    
        foreach (PSObject r in rsl)
        {
            write(r.ToString());
        }
    
    }
    
    private void write(String txt)
    {
        IHubContext c = GlobalHost.ConnectionManager.GetHubContext<ChatHub>();
             
        c.Clients.All.write(txt);
    }
    

    Hub method:

    public void write(String txt)
    {
        Clients.All.write(txt);
    }
    

    Test result:

    Besides,if you still can not find issue with your code, you can share a sample that can reproduce the issue, I will do my best to help you find and fix the issue.

    With Regards,

    Fei Han

    Wednesday, November 29, 2017 9:33 AM
  • User260076833 posted

    Dear Fei Han,

    I'm sorry, but I don't see a real difference between my code and yours. I believe that there is also a problem with the postback caused by the button click.

    In my example the input box contains the command "get-date" as default. It's written to the output box after the first click, but it's not updated (no increasing seconds) after the following clicks. Also the alert trace is fired only after the first click, which is the clearest hint that the push doesn't work after the first time.

    I post my complete example below. 

    Thank you very much
    Magnus

    Test.aspx:

    <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Test.aspx.cs" Inherits="Playground.lab.PowerShellExecution.Asynchroneous.Test" %>
    
    <!DOCTYPE html>
    
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head runat="server">
        <title></title>
        <script src="../../../Scripts/jquery-3.2.1.min.js"></script>
        <script src="../../../Scripts/jquery.signalR-2.2.2.min.js"></script>
        <script src="../../../signalr/hubs"></script>
    
        <script type="text/javascript">
    
            function write(txt)
            {
                alert(txt);
                $('#' + '<% = txt_Output.ClientID%>').append(txt + "<br />");
            }
    
            function init()
            {
                var outputHub = $.connection.outputHub;
                outputHub.client.write = write;
                $.connection.hub.start();
            }
    
            $(function ()
            {
                init();
            });
        </script>
    
    
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            <asp:TextBox ID="txt_Input" runat="server" Height="250" Width="400" TextMode="MultiLine" Text="get-date"></asp:TextBox>
            <br />
            <asp:Button ID="btn_Execute" runat="server" Text="Execute" OnClick="btn_Execute_Click"/>
            <br />
            <asp:TextBox ID="txt_Output" runat="server" Height="250" Width="400" TextMode="MultiLine"></asp:TextBox>
        </div>
        </form>
    </body>
    </html>
    

    Test.aspx.cs:

    using System;
    using System.Management.Automation;
    using System.Management.Automation.Runspaces;
    using System.Security;
    using System.Collections.ObjectModel;
    using Microsoft.AspNet.SignalR;
    
    namespace Playground.lab.PowerShellExecution.Asynchroneous
    {
        public partial class Test : System.Web.UI.Page
        {
            private Runspace runSpace;
            
            protected void Page_Load(object sender, EventArgs e)
            {
            }
    
            protected void btn_Execute_Click(object sender, EventArgs e)
            {
                execute_1(); // works only for the first click
            }
    
            private void execute_1()
            {
                PowerShell psh = PowerShell.Create();
                psh.AddScript(txt_Input.Text);
                PSDataCollection<PSObject> output = new PSDataCollection<PSObject>();
                output.DataAdded += output_DataAdded;
    
                psh.Streams.Error.DataAdded += Error_DataAdded;
                var result = psh.BeginInvoke<PSObject, PSObject>(null, output);
            }
    
            void output_DataAdded(object snd, DataAddedEventArgs evt)
            {
                PSDataCollection<PSObject> col = (PSDataCollection<PSObject>)snd;
    
                Collection<PSObject> rsl = col.ReadAll();
    
                foreach (PSObject r in rsl)
                {
                    write(r.ToString());
                }
    
            }
    
            void Error_DataAdded(object sender, DataAddedEventArgs e)
            {
                //display error message in UI
            }
    
            private void write(String txt)
            {
                IHubContext c = GlobalHost.ConnectionManager.GetHubContext<OutputHub>();
                c.Clients.All.write(txt);
            }
        }
    }

    OutputHub.cs:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using Microsoft.AspNet.SignalR;
    
    namespace Playground.lab.PowerShellExecution.Asynchroneous
    {
        public class OutputHub : Hub
        {
            public void write(String txt)
            {
                Clients.All.write(txt);
            }
    
        }
    }

    Wednesday, November 29, 2017 12:29 PM
  • User61956409 posted

    Hi Magnus,

    there is also a problem with the postback caused by the button click.

    Yes, if you click the button, it causes page postback, the client will disconnect old connection and re-establish a new connection with different connectionid to hub server.

    With Regards,

    Fei Han

    <sub></sub><sup></sup>

    Thursday, November 30, 2017 6:33 AM
  • User260076833 posted

    Hello Fei Han,

    thank you! But how can you solve this? How have you tested it?

    And why does the alert trace fire only once?

    Thanks
    Magnus

    Thursday, November 30, 2017 3:45 PM
  • User61956409 posted

    Hi Magnus,

    I put the “Execute” button inside a UpdatePanel Control for prevent from refreshing the whole page, I can get alert message every time when I click the button.

    <asp:UpdatePanel ID="UpdatePanel1" runat="server">
        <ContentTemplate>
            <asp:Button ID="btn_Execute" runat="server" Text="Execute" OnClick="btn_Execute_Click" />
        </ContentTemplate>
    </asp:UpdatePanel>
    

    My test result:

    With Regards,

    Fei Han

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Friday, December 1, 2017 8:59 AM
  • User260076833 posted

    Dear Fei Han,

    that solved my problem! When using the UpdatePanel it worked as expected!
    Thank you very much!

    However, I see that I have not reached my goal yet. I always wanted to execute a real "script", for example the contents of an arbitrary *.ps1 file. But it seems that the approach in my example can only be used to execute single commands.

    For example, I added this as input:

    Write-Host "This is the date:"
    get-date

    This only prints the date. The first command is omitted for some reason.
    Could it be that you can only execute single commands with my approach?

    I started another thread for this different issue...

    Thanks
    Magnus

    Saturday, December 2, 2017 1:30 PM