异步捕获多个进程的输出
Capture output from multiple processes asynchronously
我有一个 windows 表单应用程序,可以同时启动最多 X 个控制台进程。我从每一个中获取 JSON 输出(一个对象)并解析它以进行统计。我正在跟踪我启动了多少个进程并在 OutputDataReceived()
中捕获它们的输出。我使用进程 ID 作为键将输出附加到 ConcurrentBag<object>
中。
我的 JSON 经常会变成两个抛出解析错误的对象。我不确定如何在同一对象中得到来自两个不同进程的数据。就好像 OutputDataReceived()
事件正在从与其报告的 Id 不同的进程中获取数据。我尝试实现一些锁定但没有任何运气(这对我来说有点新,因为我来自经典 VB 背景)。
下面是一些相关代码:
private object _lockObj = new object();
private ConcurrentBag<ProcData> _procDatas;
// This is called up to X times
private void LaunchProc(int itemId)
{
var proc = new Process();
proc.StartInfo.CreateNoWindow = true;
proc.StartInfo.ErrorDialog = false;
proc.StartInfo.UseShellExecute = false;
proc.StartInfo.RedirectStandardError = true;
proc.StartInfo.RedirectStandardInput = true;
proc.StartInfo.RedirectStandardOutput = true;
proc.EnableRaisingEvents = true;
proc.Exited += proc_Exited;
proc.OutputDataReceived += proc_OutputDataReceived;
proc.ErrorDataReceived += proc_ErrorDataReceived;
proc.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
proc.StartInfo.FileName = "someapp.exe";
proc.StartInfo.Arguments = "/id=" + itemId;
proc.Start();
proc.BeginErrorReadLine();
proc.BeginOutputReadLine();
}
// I assume I'm screwing something up here since this is the only place where I set OutputData
void proc_OutputDataReceived(object sender, DataReceivedEventArgs e)
{
var proc = sender as System.Diagnostics.Process;
if (proc == null) return;
if (e.Data == null) return;
lock (_lockObj)
{
var item = _procDatas.FirstOrDefault(pi => pi.Id == proc.Id);
if (item == null)
_procDatas.Add(new ProcData() {Id = proc.Id, OutputData = e.Data});
else
item.OutputData += e.Data;
}
}
void proc_Exited(object sender, EventArgs e)
{
var proc = sender as System.Diagnostics.Process;
ProcMessage procMsg = null;
lock (_lockObj)
{
var procInfo = _procDatas.FirstOrDefault(pi => pi.Id == proc.Id);
// JSON is parsed here and error is thrown because of multiple objects (eg {"ProcessId":1,"Msg":"Success"}{"ProcessId":2,"Msg":"Success"})
procMsg = new ProcMessage(procInfo.OutputData);
}
}
public class ProcData
{
public int Id { get; set; }
public string OutputData { get; set; }
public string ErrorData { get; set; }
}
在此先感谢您帮助解决此问题或提出不同(更好)的方法。
ID of a Process can get reused。因此,我建议不要在进程退出后使用进程 ID 作为标识符。
相反,您可以使用 itemId
作为标识符,将流程输出与其相关联,并将流程和 itemId
封装在某个容器中,例如(经过轻微测试,似乎没问题):
public class ProcessExitedEventArgs<TKey> : EventArgs
{
public ProcessExitedEventArgs(TKey key, string[] output)
{
this.Key = key;
this.Output = output;
}
public TKey Key { get; private set; }
public string[] Output { get; private set; }
}
public delegate void ProcessExitedEventHandler<TKey>(object sender, ProcessExitedEventArgs<TKey> e);
public class ProcessLauncher<TKey>
{
public string FileName { get; private set; }
public string Arguments { get; private set; }
public TKey Key { get; private set; }
object locker = new object();
readonly List<string> output = new List<string>();
Process process = null;
bool launched = false;
public ProcessLauncher(string fileName, string arguments, TKey key)
{
this.FileName = fileName;
this.Arguments = arguments;
this.Key = key;
}
public event ProcessExitedEventHandler<TKey> Exited;
public bool Start()
{
lock (locker)
{
if (launched)
throw new InvalidOperationException();
launched = true;
process = new Process();
process.StartInfo.CreateNoWindow = true;
process.StartInfo.ErrorDialog = false;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.RedirectStandardInput = true;
process.StartInfo.RedirectStandardOutput = true;
process.EnableRaisingEvents = true;
process.Exited += new EventHandler(proc_Exited);
process.OutputDataReceived += proc_OutputDataReceived;
process.ErrorDataReceived += proc_ErrorDataReceived;
process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
process.StartInfo.FileName = FileName;
process.StartInfo.Arguments = Arguments;
try
{
var started = process.Start();
process.BeginErrorReadLine();
process.BeginOutputReadLine();
return started;
}
catch (Exception)
{
process.Dispose();
process = null;
throw;
}
}
}
void proc_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
if (e.Data != null)
{
// Fill in as appropriate.
Debug.WriteLine(string.Format("Error data received: {0}", e.Data));
}
}
void proc_OutputDataReceived(object sender, DataReceivedEventArgs e)
{
if (e.Data == null)
return;
lock (locker)
{
output.Add(e.Data);
}
}
void proc_Exited(object sender, EventArgs e)
{
lock (locker)
{
var exited = Exited;
if (exited != null)
{
exited(this, new ProcessExitedEventArgs<TKey>(Key, output.ToArray()));
// Prevent memory leaks by removing references to listeners.
Exited -= exited;
}
}
var process = Interlocked.Exchange(ref this.process, null);
if (process != null)
{
process.OutputDataReceived -= proc_OutputDataReceived;
process.ErrorDataReceived -= proc_ErrorDataReceived;
process.Exited -= proc_Exited;
process.Dispose();
}
}
}
(这个 class 也确保进程被释放。)然后像这样使用它:
public void LaunchProc(int itemId)
{
var launcher = new ProcessLauncher<int>("someapp.exe", "/id=" + itemId, itemId);
launcher.Exited += launcher_Exited;
launcher.Start();
}
void launcher_Exited(object sender, ProcessExitedEventArgs<int> e)
{
var itemId = e.Key;
var output = e.Output;
// Process output and associate it to itemId.
}
我有一个 windows 表单应用程序,可以同时启动最多 X 个控制台进程。我从每一个中获取 JSON 输出(一个对象)并解析它以进行统计。我正在跟踪我启动了多少个进程并在 OutputDataReceived()
中捕获它们的输出。我使用进程 ID 作为键将输出附加到 ConcurrentBag<object>
中。
我的 JSON 经常会变成两个抛出解析错误的对象。我不确定如何在同一对象中得到来自两个不同进程的数据。就好像 OutputDataReceived()
事件正在从与其报告的 Id 不同的进程中获取数据。我尝试实现一些锁定但没有任何运气(这对我来说有点新,因为我来自经典 VB 背景)。
下面是一些相关代码:
private object _lockObj = new object();
private ConcurrentBag<ProcData> _procDatas;
// This is called up to X times
private void LaunchProc(int itemId)
{
var proc = new Process();
proc.StartInfo.CreateNoWindow = true;
proc.StartInfo.ErrorDialog = false;
proc.StartInfo.UseShellExecute = false;
proc.StartInfo.RedirectStandardError = true;
proc.StartInfo.RedirectStandardInput = true;
proc.StartInfo.RedirectStandardOutput = true;
proc.EnableRaisingEvents = true;
proc.Exited += proc_Exited;
proc.OutputDataReceived += proc_OutputDataReceived;
proc.ErrorDataReceived += proc_ErrorDataReceived;
proc.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
proc.StartInfo.FileName = "someapp.exe";
proc.StartInfo.Arguments = "/id=" + itemId;
proc.Start();
proc.BeginErrorReadLine();
proc.BeginOutputReadLine();
}
// I assume I'm screwing something up here since this is the only place where I set OutputData
void proc_OutputDataReceived(object sender, DataReceivedEventArgs e)
{
var proc = sender as System.Diagnostics.Process;
if (proc == null) return;
if (e.Data == null) return;
lock (_lockObj)
{
var item = _procDatas.FirstOrDefault(pi => pi.Id == proc.Id);
if (item == null)
_procDatas.Add(new ProcData() {Id = proc.Id, OutputData = e.Data});
else
item.OutputData += e.Data;
}
}
void proc_Exited(object sender, EventArgs e)
{
var proc = sender as System.Diagnostics.Process;
ProcMessage procMsg = null;
lock (_lockObj)
{
var procInfo = _procDatas.FirstOrDefault(pi => pi.Id == proc.Id);
// JSON is parsed here and error is thrown because of multiple objects (eg {"ProcessId":1,"Msg":"Success"}{"ProcessId":2,"Msg":"Success"})
procMsg = new ProcMessage(procInfo.OutputData);
}
}
public class ProcData
{
public int Id { get; set; }
public string OutputData { get; set; }
public string ErrorData { get; set; }
}
在此先感谢您帮助解决此问题或提出不同(更好)的方法。
ID of a Process can get reused。因此,我建议不要在进程退出后使用进程 ID 作为标识符。
相反,您可以使用 itemId
作为标识符,将流程输出与其相关联,并将流程和 itemId
封装在某个容器中,例如(经过轻微测试,似乎没问题):
public class ProcessExitedEventArgs<TKey> : EventArgs
{
public ProcessExitedEventArgs(TKey key, string[] output)
{
this.Key = key;
this.Output = output;
}
public TKey Key { get; private set; }
public string[] Output { get; private set; }
}
public delegate void ProcessExitedEventHandler<TKey>(object sender, ProcessExitedEventArgs<TKey> e);
public class ProcessLauncher<TKey>
{
public string FileName { get; private set; }
public string Arguments { get; private set; }
public TKey Key { get; private set; }
object locker = new object();
readonly List<string> output = new List<string>();
Process process = null;
bool launched = false;
public ProcessLauncher(string fileName, string arguments, TKey key)
{
this.FileName = fileName;
this.Arguments = arguments;
this.Key = key;
}
public event ProcessExitedEventHandler<TKey> Exited;
public bool Start()
{
lock (locker)
{
if (launched)
throw new InvalidOperationException();
launched = true;
process = new Process();
process.StartInfo.CreateNoWindow = true;
process.StartInfo.ErrorDialog = false;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.RedirectStandardInput = true;
process.StartInfo.RedirectStandardOutput = true;
process.EnableRaisingEvents = true;
process.Exited += new EventHandler(proc_Exited);
process.OutputDataReceived += proc_OutputDataReceived;
process.ErrorDataReceived += proc_ErrorDataReceived;
process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
process.StartInfo.FileName = FileName;
process.StartInfo.Arguments = Arguments;
try
{
var started = process.Start();
process.BeginErrorReadLine();
process.BeginOutputReadLine();
return started;
}
catch (Exception)
{
process.Dispose();
process = null;
throw;
}
}
}
void proc_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
if (e.Data != null)
{
// Fill in as appropriate.
Debug.WriteLine(string.Format("Error data received: {0}", e.Data));
}
}
void proc_OutputDataReceived(object sender, DataReceivedEventArgs e)
{
if (e.Data == null)
return;
lock (locker)
{
output.Add(e.Data);
}
}
void proc_Exited(object sender, EventArgs e)
{
lock (locker)
{
var exited = Exited;
if (exited != null)
{
exited(this, new ProcessExitedEventArgs<TKey>(Key, output.ToArray()));
// Prevent memory leaks by removing references to listeners.
Exited -= exited;
}
}
var process = Interlocked.Exchange(ref this.process, null);
if (process != null)
{
process.OutputDataReceived -= proc_OutputDataReceived;
process.ErrorDataReceived -= proc_ErrorDataReceived;
process.Exited -= proc_Exited;
process.Dispose();
}
}
}
(这个 class 也确保进程被释放。)然后像这样使用它:
public void LaunchProc(int itemId)
{
var launcher = new ProcessLauncher<int>("someapp.exe", "/id=" + itemId, itemId);
launcher.Exited += launcher_Exited;
launcher.Start();
}
void launcher_Exited(object sender, ProcessExitedEventArgs<int> e)
{
var itemId = e.Key;
var output = e.Output;
// Process output and associate it to itemId.
}