When trying to use stdin and stdout in C# (Unity) to pipe to a Python process, I get about a dozen or so transactions and the process breaks and the error "ObjectDisposedException: The object was used after being disposed."
After trying several of the more obvious things, I'm bringing the problem here perhaps someone know just the right technique. Thanks in advance.
Here's the C# Startup code:
Process pyProcess; // <=== fixed
ProcessStartInfo pyStartInfo;
public StreamReader pyStreamReader;
public StreamWriter pyStreamWriter;
public void startPython()
{
// Create new process start info
pyStartInfo = new ProcessStartInfo(pyPath)
{
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
Arguments = pyApp + " " + pyArgs
};
pyProcess = new Process { StartInfo = pyStartInfo };
pyProcess.Start();
pyStreamReader = pyProcess.StandardOutput;
pyStreamWriter = pyProcess.StandardInput;
pyStreamWriter.WriteLine("Hello!");
string str = pyStreamReader.ReadLine();
Debug.LogFormat(str + "\n");
}
void Start()
{
if(testPython == true)
startPython();
Here is the fragment that generates data sent to python at each update...
if (controller.testPython)
{
string str, python;
str = String.Format("data to send");
pyStreamWriter.DiscardBufferedData(); #<==== fixed
pyStreamWriter.WriteLine(str);
python = pyStreamReader.ReadLine();
Debug.LogFormat("python says: " + python + "\n");
}
And here is the simplified python process that's echoing the data
while True:
cmd = input() # read a command from c#
print(cmd) # process the cmd, here we just echo it back to c#
After a little experimentation, I discovered that adding
pyStreamReader.DiscardBufferedData();
before
pyStreamWriter.WriteLine(str);
solves the main problem and this simple form of piping seems to work, at least for hundreds of transactions that I observed.
I also had to declare pyProcess outside the scope so the code so that its handle is not released. That resolved the ObjectDisposed exception.
Related
I need to use standard input/output on process, so I created simple app "test":
var line = String.Empty;
do
{
Console.Write($"previous input ==> {line}, type next input> ");
line = Console.ReadLine();
}
while (!String.IsNullOrWhiteSpace(line) && line != "quit");
Console.WriteLine("End");
which receives something on standard input and writes on output. Then I created new app which needs to start that app "test" and use standard iput/output like:
var process = new Process
{
EnableRaisingEvents = false,
StartInfo = new ProcessStartInfo
{
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true,
Arguments = Arguments,
CreateNoWindow = true,
FileName = Name,
WindowStyle = ProcessWindowStyle.Hidden,
WorkingDirectory = WorkingDirectory
},
};
process.Start();
String? input;
do
{
Thread.Sleep(10000); // Sleep to be sure that "test" app generated output
var line = String.Empty;
while (process.StandardOutput.Peek() > -1)
line += (char)process.StandardOutput.Read();
Console.Write($"[Standard Output]{line}\t[New Input]");
input = Console.ReadLine();
process.StandardInput.WriteLine(input);
}
while (input != "quit");
The problem is that I get this as output:
[Standard Output]previous input ==> , type next input> [New Input]test
[Standard Output] [New Input]
The "process.StandardOutput.Peek()" second time is returning -1 and there exist output of "test" app. Is it possible to get next what is generated on standard output by "test" app from app that started that process.
I need to get second output generated from "test" app, so I expect to see line:
[Standard Output]previous input ==> test, type next input> [New Input]
If your platform is Windows, try PeekNamedPipe.
static string ReadAvailableString(StreamReader reader)
{
[DllImport("kernel32.dll")]
static extern bool PeekNamedPipe(
SafeFileHandle hNamedPipe,
IntPtr lpBuffer,
int nBufferSize,
IntPtr lpBytesRead,
out int lpTotalBytesAvail,
IntPtr lpBytesLeftThisMessage
);
var stream = (FileStream)reader.BaseStream;
if( !PeekNamedPipe(stream.SafeFileHandle, IntPtr.Zero, 0, IntPtr.Zero, out var totalbytesAvail, IntPtr.Zero) || totalbytesAvail<=0 )
return String.Empty;
Span<byte> buf = stackalloc byte[totalbytesAvail];
stream.Read(buf);
return reader.CurrentEncoding.GetString(buf);
}
Here is an example of getting StandardOutput.
var output = ReadAvailableString(process.StandardOutput);
Problem is Peek is non-blocking call which does not wait for data to become available. You start new process and then immediately proceed checking its standard output with Peek, but it might be nothing there yet. This is what you observe. Instead - you should read until some stopping point, but in this case there is no such point, so you can introduce it - use Console.WriteLine instead of Console.Write here:
Console.WriteLine($"previous input ==> {line}, type next input> ");
Now on receiving end you can read until you meet newline character:
line = process.StandardOutput.ReadLine();
Note that this is blocking read. It will read until newline, and if data is not available yet - it will wait until it's there. Now "messages" in your communication have clear boundaries.
I would even say that you should forget that Peek() exist and never use it. I've never used it in my practice and all usages I ever saw lead to bugs like this.
After trying a lot of things it seams that Process.StandardOutput.Peek is not working. You can use the "PeekNamedPipe" (answer from radian) for Windows OS.
I managed to work it on Windows and Linux using CliWrap (https://github.com/Tyrrrz/CliWrap). The problem here is that for input stream you need Stream that has blocking read() method, so I creted/implemented one for me. I will not put here the implementation of that stream, you can use any stream that satisfy that condition. So, here is the final version of above example using CliWrap library
var stdOutBuffer = new StringBuilder();
var stdErrBuffer = new StringBuilder();
// The CliStream is my own implementation of Stream class.
// The CliWrap library is calling method: int Read(byte[] buffer, int offset, int count)
// with parameters offset = 0 and count = 131072
// it is important that this method is blocking if nothing is in stream (it done it using Semaphores)
// if it is not blocking then you will have some unexpected behaviour
var stream = new CliStream();
var cmd = CliWrap.Cli.Wrap(ExecName)
.WithArguments(Arguments)
.WithStandardErrorPipe(CliWrap.PipeTarget.ToStringBuilder(stdErrBuffer))
.WithStandardOutputPipe(CliWrap.PipeTarget.ToStringBuilder(stdOutBuffer))
.WithStandardInputPipe(CliWrap.PipeSource.FromStream(stream))
.WithWorkingDirectory(WorkingDirectory);
cmd.ExecuteAsync();
String? input;
do
{
Thread.Sleep(10000); // Sleep to be sure that "test" app generated output
Console.Write($"[Standard Output]{stdOutBuffer}\t[New Input]");
stdOutBuffer.Clear();
input = Console.ReadLine();
var buffer = Encoding.UTF8.GetBytes(input + Environment.NewLine);
stream.Write(buffer);
}
while (input != "quit");
Thanks to everyone for contributing.
I have been working on converting a GUI script from another language to C# in VS2017 for a customer. With help from the folks here I am 95% of the way there, but have run into a couple of snags; just not sure I am doing things in the best way. I'm including just the relevant portions of code below, please let me know if I am not providing enough:
The majority of the code is centered on the wpf form, which collects data for low level technicians to batch deploy a number of Virtual Machines into the VMware environment. This number could easily range into the dozens or even a hundred VMs at once. The information for each VM is specified in the form, then collected in a listview. Once the listview is fully populated it is exported to a csv. Up to this point everything works just fine.
I've next been working on actually launching the powershell/powerCLI script (also working) and capturing output. The log file is opened with a specific reader application the customer uses, which updates in real time, and the captured output is fed to the log. It is important for the technicians to see the output from the code line by line so they can react if there is an issue.
I started with something like this as a test:
string sPSScript = "C:\\Users\\Admin\\Desktop\\TestC#.ps1";
string logFile = "C:\\Users\\Admin\\Desktop\\My.log";
string logReader = "C:\\Users\\Admin\\Documents\\CMTrace.exe";
string standard_output;
System.Diagnostics.Process PSScript = new System.Diagnostics.Process();
PSScript.StartInfo.FileName =
Environment.GetFolderPath(Environment.SpecialFolder.SystemX86) +
"\\WindowsPowerShell\\v1.0\\powershell.exe";
PSScript.StartInfo.Arguments = "-command . '" + sPSScript + "' " +
vCenter.Text;
PSScript.StartInfo.RedirectStandardOutput = true;
PSScript.StartInfo.UseShellExecute = false;
PSScript.Start();
System.Diagnostics.Process LogFile = new System.Diagnostics.Process();
LogFile.StartInfo.FileName = logReader;
LogFile.StartInfo.Arguments = logFile;
LogFile.Start(); while ((standard_output =
PSScript.StandardOutput.ReadLine()) != null)
{
if (standard_output != "")
{
using (StreamWriter file = new StreamWriter(logFile, append: true))
{
file.WriteLine(standard_output);
}
}
}
While this writes to the log file in real time as expected, it creates 100 instances of the logReader application. I understand why, since I am declaring a new StreamWriter object through every pass, but am unsure how better to go about this.
I tried creating the file outside the loop, like this:
StreamWriter file = new StreamWriter(logFile, append: true) { };
System.Diagnostics.Process LogFile = new System.Diagnostics.Process();
LogFile.StartInfo.FileName = logReader;
LogFile.StartInfo.Arguments = logFile;
System.Diagnostics.Process PSScript = new System.Diagnostics.Process();
PSScript.StartInfo.FileName = Environment.GetFolderPath(Environment.SpecialFolder.SystemX86) + "\\WindowsPowerShell\\v1.0\\powershell.exe";
PSScript.StartInfo.Arguments = "-command . '" + sPSScript + "' " + vCenter.Text;
PSScript.StartInfo.RedirectStandardOutput = true;
PSScript.StartInfo.UseShellExecute = false;
LogFile.Start();
PSScript.Start();
System.Threading.Thread.Sleep(1500);
while ((standard_output = PSScript.StandardOutput.ReadLine()) != null)
{
if (standard_output != "")
{
file.WriteLine(standard_output);
}
}
It doesn't create multiple instances, but it also does not update the log file in real time as the previous code does. It only updates once the script runs, and then only partially. The script produces ~1000 lines of output, and I consistently see only about 840 written to the log file.
I thought about doing something like this:
FileStream logFS;
logFS = new FileStream(logFile, FileMode.Append);
but it appears the only options available to me to write to the file are expecting a byte array.
I am sure that I am missing something stupid simple in this, but would appreciate any suggestions on the easiest way to create the log file, open it in the reader, and then update it with the standard output from the powershell script.
why did the previous code writes in real time?
because you are wrapping it with using. And at the end of using block its gonna call dispose which calls .Flush to write to disk
Your second code block calls WriteLine but never called Flush so it writes to the disk whenever the buffer is full. Just add a .Flush call after WriteLine and you will have real time logging
I run into a strange thing and can't find the reason why this is happening.
I have a service.exe where I collect data from configuration registry and then start n processes.
Sample Code:
_mProcess.StartInfo = new ProcessStartInfo
{
FileName = Command,
Arguments = Argument,
WorkingDirectory = WorkDir
};
_mProcess.Start();
Pid = _mProcess.Id;
My Pid contains the process id.
Now I added UseShellExecute = false to get the StandardOutput.
New sample code:
_mProcess.StartInfo = new ProcessStartInfo
{
FileName = Command,
Arguments = Argument,
WorkingDirectory = WorkDir //,
//CreateNoWindow = true,
UseShellExecute = false,
//RedirectStandardOutput = true,
RedirectStandardError = true
//RedirectStandardInput = true
};
_mProcess.Start();
Pid = _mProcess.Id;
using (var reader = _mProcess.StandardError)
{
_logger.ToLog("", Company, reader.ReadToEnd(), "RCluster.log", "service");
}
In this case the process return back any error which I can store to my log file.
Problem: This code is part of a method to start a process which I call many times (depends on my configuration).
So with this code the first process is called, the following processes are not.
Somehow the service wait for the first service now. I thought this happens only with WaitForExit.
So how I can get standard error output but not make the process block my main task to continue?
#Gusman: Add you comment as answer, you brought me to the correct answer. Then I can accept your comment as answer.
To the down-voter: Explanation why would be appreciated.
To all: I added some code to start the new process as a thread. In this case it make sense to start it in another thread to grab `StandardError messages but do not block the main process (which is a service in my case that starts many sub processes).
// start as new thread to prevent blocking
var ths = new ThreadStart(() =>
{
mProcess.Start();
Pid = mProcess.Id;
// write pid file
File.WriteAllText(RubyDir + #"\tmp\pids\" + Port + #".pid", Pid.ToString());
using (var reader = mProcess.StandardError)
{
var errorMsg = reader.ReadToEnd();
if (errorMsg.Length > 0) _logger.ToLog("", Company, errorMsg, "SOLR.log", "service");
}
});
var th = new Thread(ths);
th.Start();
I am generating C++ code via C#, for some reason after applying astyle my generated code compiles. So is there a way I can invoke astyle from within my C# windows application?
Astyle is a command line tool, so using Process class you can call it externally and ask it to format the C++ source file.
I have done similar projects in the past, such as
http://alex.codeplex.com
I finally figured it out a few days ago, so thought i would share my function to astyle via c#
'
private void astyleDirectory(string target_path)
{
System.Diagnostics.Process pProcess = new System.Diagnostics.Process();
//Enter Path to get Astyle.exe here
pProcess.StartInfo.FileName=System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) + #"\Astyle.exe";
pProcess.StartInfo.Arguments = "--options=none --style=ansi --recursive *.h *.cpp";
pProcess.StartInfo.UseShellExecute = false;
pProcess.StartInfo.RedirectStandardOutput = true;
pProcess.StartInfo.RedirectStandardError = true;
pProcess.StartInfo.WorkingDirectory = System.IO.Path.GetDirectoryName(target_path);
try
{
pProcess.Start();
string strOutput = pProcess.StandardOutput.ReadToEnd();
string strError = pProcess.StandardError.ReadToEnd();
pProcess.WaitForExit();
}
catch { }
}
'
I need some advice regarding the use of a command line utility from a C#/ASP.NET web application.
I found a 3rd party utility for converting files to CSV format. The utility works perfectly and it can be used from the command line.
I have been looking on the web for examples on how to execute the command line utility and found this example.
The problem is this is not very good. When I try to us the example code with my utility, I get a prompt asking me to install the utility on the client machine. This is not what I want. I do not want the user to see what is going on in the background.
Is it possible to execute the command server side and processing the file from there?
Any help would be greatly appreciated.
I've done something like this several times in the past, and here's what's worked for me:
Create an IHttpHandler implementation (easiest to do as an .ashx file) to handle a convert. Within the handler, use System.Diagnostics.Process and ProcessStartInfo to run your command line utility. You should be able to redirect the standard output to the output stream of your HTTP response. Here's some code:
public class ConvertHandler : IHttpHandler
{
#region IHttpHandler Members
bool IHttpHandler.IsReusable
{
get { return false; }
}
void IHttpHandler.ProcessRequest(HttpContext context)
{
var jobID = Guid.NewGuid();
// retrieve the posted csv file
var csvFile = context.Request.Files["csv"];
// save the file to disk so the CMD line util can access it
var filePath = Path.Combine("csv", String.Format("{0:n}.csv", jobID));
csvFile.SaveAs(filePath);
var psi = new ProcessStartInfo("mycsvutil.exe", String.Format("-file {0}", filePath))
{
WorkingDirectory = Environment.CurrentDirectory,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using (var process = new Process { StartInfo = psi })
{
// delegate for writing the process output to the response output
Action<Object, DataReceivedEventArgs> dataReceived = ((sender, e) =>
{
if (e.Data != null) // sometimes a random event is received with null data, not sure why - I prefer to leave it out
{
context.Response.Write(e.Data);
context.Response.Write(Environment.NewLine);
context.Response.Flush();
}
});
process.OutputDataReceived += new DataReceivedEventHandler(dataReceived);
process.ErrorDataReceived += new DataReceivedEventHandler(dataReceived);
// use text/plain so line breaks and any other whitespace formatting is preserved
context.Response.ContentType = "text/plain";
// start the process and start reading the standard and error outputs
process.Start();
process.BeginErrorReadLine();
process.BeginOutputReadLine();
// wait for the process to exit
process.WaitForExit();
// an exit code other than 0 generally means an error
if (process.ExitCode != 0)
{
context.Response.StatusCode = 500;
}
}
}
#endregion
}
The command is running server side. Any code is running on the server. The code in the example that you give works. You just need to make sure that the utility is set up properly on the server and that you have permissions to the directory/file.