Run a vb script using System.Diagnostics.Process but only partially successful in feeding in input text to the process' stdin
Asked Answered
A

2

9

Edit: first things first

The point of the vbscript is to act like a REPL or command prompt/bash environment, it is simplified to just reprinting the user input

So in other words the cscript process should stay alive and the user input for each pass should be sent to this process only.

And also it means that the internal state of the script should be kept for each pass (One pass = each time the "Send" button in the C# winform is clicked, or in the context of the vbscript, One pass = each time ^Z is input).

For example, if the vbscript is to be modified to demonstrate the state-keeping behavior, you can make the following mods:

  1. At line dim wsh,stmt,l... append it with : dim passcnt : passcnt=1
  2. At line wsh.Echo("Enter lines of strings, press ctrl-z..., replace the last closing bracket with & " (pass #" & passcnt & ")")
  3. At line wsh.Echo("End output") append the code : passcnt = passcnt + 1

Running the vbscript the console will show the pass number incremented on each pass.

  1. The C# winform can be modified in any way, as long as the above condition still holds.
  2. Try to observe what the script does by cscript ask_SO.vbs, it should make things clear enough

I think this is the most clear I am able to made it.


I would like to use stdout/stdin redirection of System.Diagnostics.Process to feed input texts to the following VBScript.

What the vbscript does is that it allows the user to input multiple lines of strings to the console, and when the ^z character is input, the script will just output everything ver batim to the console:

Sample Output

Microsoft (R) Windows Script Host Version 5.812
Copyright (C) Microsoft Corporation. All rights reserved.

Enter lines of strings, press ctrl-z when you are done (ctrl-c to quit):
I come with no wrapping or pretty pink bows.
got line
I am who I am, from my head to my toes.
got line
I tend to get loud when speaking my mind.
got line
Even a little crazy some of the time.
got line
I'm not a size 5 and don't care to be.
got line
You can be you and I can be me.
got line

got line
Source: https://www.familyfriendpoems.com/poem/be-proud-of-who-you-are
got line
^Z
=====================================
You have entered:
I come with no wrapping or pretty pink bows.
I am who I am, from my head to my toes.
I tend to get loud when speaking my mind.
Even a little crazy some of the time.
I'm not a size 5 and don't care to be.
You can be you and I can be me.

Source: https://www.familyfriendpoems.com/poem/be-proud-of-who-you-are

End output
Enter lines of strings, press ctrl-z when you are done (ctrl-c to quit):

After that, the user can input another chunk of text and repeat the process.

This is the script code:

ask_SO.vbs


dim wsh,stmt,l : set wsh = WScript


do
    wsh.Echo("Enter lines of strings, press ctrl-z when you are done (ctrl-c to quit):")
    'stmt=wsh.StdIn.ReadAll()
    do
        l=wsh.StdIn.ReadLine()
        wsh.echo("got line")
        stmt = stmt & l & vbcrlf
    loop while (not wsh.StdIn.AtEndOfStream)
    wsh.Echo("=====================================")
    wsh.Echo("You have entered:")
    wsh.Echo(stmt)
    wsh.Echo("End output")
loop

This is how to invoke the script:

cscript ask_SO.vbs

I came out with the following C# code (project type set to Console Application instead of Windows Forms):

frmPostSample

public class frmPostSample : Form

{
    Process proc_cscr;
    StreamWriter sw;
    public frmPostSample()
    {
        InitializeComponent2();
    }

    #region Copied from generated code
    private System.ComponentModel.IContainer components = null;

    /// <summary>
    /// Clean up any resources being used.
    /// </summary>
    /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
    protected override void Dispose(bool disposing)
    {
        if (disposing && (components != null))
        {
            components.Dispose();
        }
        base.Dispose(disposing);
    }
    private void InitializeComponent2()
    {
        this.txt_lines = new System.Windows.Forms.TextBox();
        this.Btn_Send = new System.Windows.Forms.Button();
        this.SuspendLayout();
        // 
        // txt_lines2
        // 
        this.txt_lines.Location = new System.Drawing.Point(41, 75);
        this.txt_lines.Multiline = true;
        this.txt_lines.Name = "txt_lines2";
        this.txt_lines.Size = new System.Drawing.Size(689, 298);
        this.txt_lines.TabIndex = 0;
        // 
        // Btn_Send2
        // 
        this.Btn_Send.Location = new System.Drawing.Point(695, 410);
        this.Btn_Send.Name = "Btn_Send2";
        this.Btn_Send.Size = new System.Drawing.Size(75, 23);
        this.Btn_Send.TabIndex = 1;
        this.Btn_Send.Text = "&Send";
        this.Btn_Send.UseVisualStyleBackColor = true;
        this.Btn_Send.Click += new System.EventHandler(this.Btn_Send_Click);
        // 
        // Form1
        // 
        this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
        this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
        this.ClientSize = new System.Drawing.Size(800, 450);
        this.Controls.Add(this.Btn_Send);
        this.Controls.Add(this.txt_lines);
        this.Name = "Form1";
        this.Text = "Form1";
        this.ResumeLayout(false);
        this.PerformLayout();

    }


    private System.Windows.Forms.TextBox txt_lines;
    private System.Windows.Forms.Button Btn_Send;

    #endregion
    private void Btn_Send_Click(object sender, EventArgs e)
    {
        if (proc_cscr == null)
        {
            if (!File.Exists("ask_SO.vbs"))
            {
                MessageBox.Show("Script file not exist");
                return;
            }
            ProcessStartInfo startInfo = new ProcessStartInfo();
            startInfo.FileName = "cscript";
            startInfo.Arguments = "//nologo ask_SO.vbs";

            startInfo.RedirectStandardInput = true;
            startInfo.RedirectStandardOutput = true;
            startInfo.UseShellExecute = false;


            proc_cscr = new Process();

            proc_cscr.StartInfo = startInfo;

            proc_cscr.Start();
            sw = proc_cscr.StandardInput;
        }
        OutPrint();


        foreach (var vbsline in txt_lines.Lines)
        {
            sw.WriteLine(vbsline);     // <-------- SW WRITELINE
            sw.Flush();
            OutPrint();


        }
        //sw.Flush();
        sw.Close();
        while (true)
        {
            var s2 = proc_cscr.StandardOutput.ReadLineAsync();
            s2.Wait();
            Console.WriteLine(s2.Result);
            if (proc_cscr.StandardOutput.Peek() == -1) break;
        }

    }
    private void OutPrint()
    {
        string l;
        while (proc_cscr.StandardOutput.Peek() != -1)
        {
            l = proc_cscr.StandardOutput.ReadLine();
            Console.WriteLine(l);
        }
    }
}

Run the program, and if you have correctly set the project type to "Console Application", a console window and a GUI Window should be shown. You just paste the text to the text input area and press send, and observe the result in the console window.

However, what the C# form behaves is not the same as directly running the script cscript ask_SO.vbs:

  1. The script can only accept one pass of input - the second pass throws the error "Cannot write to a closed TextWriter" at the line with comment SW WRITELINE - I know it is because I've closed the stdin stream, but otherwise I can't make the script go forward
  2. Also, I've got the error shown: ...\ask_SO.vbs(8, 9) Microsoft VBScript runtime error: Input past end of file.
  3. The "got line" echo is not shown immediately after the c# code write a line input to the stdin (again, at the line with comment SW WRITELINE).

I've searched online to find a solution, but most of the materials only shows input without using the ^z character, or in other words, only accepts one-pass input.

You can download the C# visual studio solution here (vbscript included - you just load the solution in visual studio 2019 and press F5 to run).

Note

The encoding I got from proc_cscr.StandardOutput.CurrentEncoding.BodyName and proc_cscr.StandardInput.Encoding.BodyName is big5, it is a DBCSCodePageEncoding, used for encoding Chinese characters.

I recognized that I need to mention this, when I tried the suggestion mentioned in an answer to write (char)26 to the stdin stream. As Encoding.GetEncoding("big5").GetBytes(new char[]{(char)26}) returns only one byte (two bytes for unicode: {byte[2]} [0]: 26 [1]: 0), I did a sw.Write((char)26);, and add a sw.flush() also. It still didn't work.

Aromatize answered 24/6, 2019 at 6:52 Comment(3)
You can try the vbscript code to understand my question quickly. If what I asked is not clear to anyone I can add more info in the questionAromatize
Ctrl+Z is probably not expected to be part of the standard input stream. It's a keyboard shortcut to force Microsoft Script Host to stop reading input. You'll probably have to send simulated keypresses to the child process in order to get that part to work.Southing
The solution is very simple. proc_cscr is not set to null when the script terminates and you run 2nd time your run code your never opens a process. So when you execute sw.Close(); you also have to do proc_cscr = null;Alternator
E
1

I do not think, this is possible to do.

Your point 3:

The "got line" echo is not shown immediately after the c# code write a line input to the stdin

This is because you have redirected output (startInfo.RedirectStandardOutput = true). If you redirect it, everything you write goes to the StandardOutput stream and you have to read it manually. So just do not redirect output and your got line messages will be immediate. If the output is not redirected, you can not use StandardOutput property (but you do not need it anyway).

The rest is more difficult. The thing is, it seems there is not a way how to send end of stream, because this is what stops your inner loop in vbs. The stream ends when you finish with it - technically when you close it, or finish your process. The character of value 26 is represented as end of stream (Ctrl + Z) somewhere. But it is not working here (I tried sw.Write(Convert.ToChar(26)).

I do not know if it is possible (I do not know vbs), but maybe you can change your logic there and not check for end of stream. Insted of it maybe read by bytes (characters) and check for specific char (for example that char(26)) to step out of the inner loop.

Exenterate answered 27/6, 2019 at 18:35 Comment(8)
RedirectStandardOutput=true won't be a problem. Don't use ReadLineAsync then Wait, instead just use the synchronous function ReadLine.Cullis
@Todd I trust that you have tried to run the source code? I changed these three lines: var s2 = proc_cscr.StandardOutput.ReadLineAsync(); s2.Wait(); Console.WriteLine(s2.Result); to one line Console.WriteLine(proc_cscr.StandardOutput.ReadLine()); and the pass seems to be broke (the input text couldn't be shown), and the next pass still throw the "textwriter closed" exceptionAromatize
btw, the encoding I got from proc_cscr.StandardOutput.CurrentEncoding.BodyName and proc_cscr.StandardInput.Encoding.BodyName is big5, it is a DBCSCodePageEncoding, used for encoding Chinese charactersAromatize
@Aromatize This was about using the correct convention. There can be problems/bugs using Wait, so rather than point them out, I recommended to simply use the synchronous call convention. I'm not saying this fixes your overall problem, but it certainly does eliminate a possible problem.Cullis
@Todd Point noted, anyway what I found out is that the synchronous ReadLine() seemed to have blocked the vbscript and the script couldn't go forward - and the C# execution is stuck at the ReadLine() call.Aromatize
I think whether the scripting environment is vbscript or not is not relevant, knowing that it is using standard output and input is enough.Aromatize
@Aromatize Oh I see, that's an unusual side effect. Using ReadLineAsync must perform the operation on another thread, and that must be necessary for the process interop in this case. I can't see why that's necessary, but as you say, that's what you're experiencing. However, given that your problem pertains to this part of the system, I suggest that gaining an understanding of why a synchronous ReadLine() behaves this way, will lead to your final answer.Cullis
@Aromatize According to this, you could be experiencing a deadlock - devblogs.microsoft.com/oldnewthing/?p=10223, one solution might be to use https://mcmap.net/q/1320034/-why-does-standardoutput-read-block-when-startinfo-redirectstandardinput-is-set-to-true p.OutputDataReceived += Subscription instead of the stream reader. Or perhaps just using ReadToEnd() in one go - at least to confirm that the script is outputting everything.Cullis
O
1

Your problem here is when you close the stream, cscript also terminates and you try to read from a dead process.

I've modified your sample to utilize async reading of cscript by calling BeginOutputReadLine and reading output in OutputDataReceived event. I've also added a WaitForExit which is required to ensure raising of async events.

By the way you really do not need to send CTRL+Z since it is just a character and it is not really the EOF marker. Console handler just handles that keystroke as EOF signal. Closing StandardInput does the trick.

var psi = new ProcessStartInfo
          {
              FileName = "cscript",
              RedirectStandardError = true,
              RedirectStandardOutput = true,
              RedirectStandardInput = true,
              UseShellExecute = false,
              //CreateNoWindow = true,
              WindowStyle = ProcessWindowStyle.Normal,
              Arguments = "//nologo ask_SO.vbs"
          };

var process = Process.Start(psi);
process.BeginOutputReadLine();

var buffer = new StringBuilder();

process.OutputDataReceived += (s, args) =>
{
    buffer.AppendLine(args.Data);
};

foreach (var line in textBox1.Lines)
{
    buffer.AppendLine(line);
    process.StandardInput.WriteLine(line);

    Thread.Sleep(50);
}

process.StandardInput.Flush();
process.StandardInput.Close();

process.WaitForExit();

output.Text = buffer.ToString();

EDIT: Updated to keep process alive

private Process process;

private void EnsureProcessStarted()
{
    if (null != process)
        return;

    var psi = new ProcessStartInfo
              {
                  FileName = "cscript",
                  RedirectStandardError = true,
                  RedirectStandardOutput = true,
                  RedirectStandardInput = true,
                  UseShellExecute = false,
                  //CreateNoWindow = true,
                  WindowStyle = ProcessWindowStyle.Normal,
                  Arguments = "//nologo ask_SO.vbs"
              };

    process = Process.Start(psi);
    process.OutputDataReceived += (s, args) => AppendLineToTextBox(args.Data);

    process.BeginOutputReadLine();

    // time to warm up
    Thread.Sleep(500);
}

private void AppendLineToTextBox(string line)
{
    if (string.IsNullOrEmpty(line))
        return;

    if (output.InvokeRequired)
    {
        output.Invoke(new Action<string>(AppendLineToTextBox), line);
        return;
    }

    output.AppendText(line);
    output.AppendText(Environment.NewLine);
}

private void SendLineToProcess(string text)
{
    EnsureProcessStarted();

    if (string.IsNullOrWhiteSpace(text))
    {
        process.StandardInput.Flush();
        process.StandardInput.Close();

        //process.WaitForExit(); causes a deadlock
        process = null;
    }
    else
    {
        AppendLineToTextBox(text); // local echo
        process.StandardInput.WriteLine(text);
        process.StandardInput.Flush();

        // time to process
        Thread.Sleep(50);
    }
}
Oporto answered 2/7, 2019 at 14:7 Comment(6)
Can you modify it so that it retains the original coding's behavior of keeping the cscript process alive, instead of launching it every time the send button is pressed?Aromatize
I've tested your code and I'm afraid that it doesn't seem to be able to get out of the do .... loop while (not wsh.StdIn.AtEndOfStream) loopAromatize
well if you send an empty line, it calls process.StandardInput.Close(); and AtEndOfStream returns true. Problem is outer loop. Since we close the stream new time we try to read using wsh.StdIn.ReadLine() causes error 62 (Reading past the end of stream) and application terminates.Oporto
Ok I see that you have thought of how to give signal to the script for committing the pass, but to me using an empty line or any other kind of pre defined token might work at first, but some time later it would bite you at a time you wouldn't expect (and for this case I prefer empty lines to be supported, you might say another string token can be used and that's why I would like to say these to you). On the other hand I prefer to follow standards, I think it would be more fool-proof.Aromatize
Also I did try to close the stdin and this caused an error like "writing to a textwriter already closed" on the second pass (you can see I have a sw.close() line being commented out, it was also mentioned in my question), but I will try the test of inputting an empty line input to your program tomorrow and see if it did really make a difference in comparison to my line of sw.close()Aromatize
When I leave the textbox empty and press "send", the code just set the process variable as empty, and the next "send" just relaunch a new process, which is not what I intend to get. I think you aren't expecting to get bounty points for free, are you?Aromatize

© 2022 - 2024 — McMap. All rights reserved.