C#执行外部程序用到的是Process
进程类,打开一个进程,可以指定进程的启动信息StartInfo
(启动的程序名、输入输出是否重定向、是否显示UI界面、一些必要参数等)。
相关代码在网上可以找到很多,本篇在参考这些代码的基础上,进行一些修改,尤其是后面探讨交互执行时的情况。
交互执行输入信息,完成程序的执行,是一个相对很必要的情况。
需要添加命名空间System.Diagnostics
。
在CMCode
项目中添加文件夹ExecApplications
,存放执行外部程序的相关文件代码。
定义一个Process执行外部程序的输出类
public class ExecResult
{
public string Output { get; set; }
/// <summary>
/// 程序正常执行后的错误输出,需要根据实际内容判断是否成功。如果Output为空但Error不为空,则基本可以说明发生了问题或错误,但是可以正常执行结束
/// </summary>
public string Error { get; set; }
/// <summary>
/// 执行发生的异常,表示程序没有正常执行并结束
/// </summary>
public Exception ExceptError { get; set; }
}
调用cmd执行命令行
调用cmd直接执行
如下为执行cmd的帮助类,提供了异步Async方法、同步方法和一次执行多个命令的方法。主要代码部分都有注释。
获取Windows系统环境特殊文件夹路径的方法:Environment.GetFolderPath(Environment.SpecialFolder.SystemX86)
,获取C:\Windows\System32\
/// <summary>
/// 执行cmd命令
/// </summary>
public static class ExecCMD
{
#region 异步方法
/// <summary>
/// 执行cmd命令 返回cmd窗口显示的信息
/// 多命令请使用批处理命令连接符:
/// <![CDATA[
/// &:同时执行两个命令
/// |:将上一个命令的输出,作为下一个命令的输入
/// &&:当&&前的命令成功时,才执行&&后的命令
/// ||:当||前的命令失败时,才执行||后的命令]]>
/// </summary>
///<param name="command">执行的命令</param>
///<param name="workDirectory">工作目录</param>
/// <returns>cmd命令执行窗口的输出</returns>
public static async Task<ExecResult> RunAsync(string command,string workDirectory=null)
{
command = command.Trim().TrimEnd('&') + "&exit"; //说明:不管命令是否成功均执行exit命令,否则当调用ReadToEnd()方法时,会处于假死状态
string cmdFileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.SystemX86), "cmd.exe");// @"C:\Windows\System32\cmd.exe";
using (Process p = new Process())
{
var result = new ExecResult();
try
{
p.StartInfo.FileName = cmdFileName;
p.StartInfo.UseShellExecute = false; //是否使用操作系统shell启动
p.StartInfo.RedirectStandardInput = true; //接受来自调用程序的输入信息
p.StartInfo.RedirectStandardOutput = true; //由调用程序获取输出信息
p.StartInfo.RedirectStandardError = true; //重定向标准错误输出
p.StartInfo.CreateNoWindow = true; //不显示程序窗口
if (!string.IsNullOrWhiteSpace(workDirectory))
{
p.StartInfo.WorkingDirectory = workDirectory;
}
p.Start();//启动程序
//向cmd窗口写入命令
p.StandardInput.WriteLine(command);
p.StandardInput.AutoFlush = true;
// 若要使用StandardError,必须设置ProcessStartInfo.UseShellExecute为false,并且必须设置 ProcessStartInfo.RedirectStandardError 为 true。 否则,从 StandardError 流中读取将引发异常。
//获取cmd的输出信息
result.Output = await p.StandardOutput.ReadToEndAsync();
result.Error = await p.StandardError.ReadToEndAsync();
p.WaitForExit();//等待程序执行完退出进程。应在最后调用
p.Close();
}
catch (Exception ex)
{
result.ExceptError = ex;
}
return result;
}
}
/// <summary>
/// 执行多个cmd命令 返回cmd窗口显示的信息
/// 此处执行的多条命令并不是交互执行的信息,是多条独立的命令。也可以使用&连接多条命令为一句执行
/// </summary>
///<param name="command">执行的命令</param>
/// <returns>cmd命令执行窗口的输出</returns>
/// <returns>工作目录</returns>
public static async Task<ExecResult> RunAsync(string[] commands,string workDirectory=null)
{
if (commands == null)
{
throw new ArgumentNullException();
}
if (commands.Length == 0)
{
return default(ExecResult);
}
return await Task.Run(() =>
{
commands[commands.Length - 1] = commands[commands.Length - 1].Trim().TrimEnd('&') + "&exit"; //说明:不管命令是否成功均执行exit命令
string cmdFileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.SystemX86), "cmd.exe");// @"C:\Windows\System32\cmd.exe";
using (Process p = new Process())
{
var result = new ExecResult();
try
{
p.StartInfo.FileName = cmdFileName;
p.StartInfo.UseShellExecute = false; //是否使用操作系统shell启动
p.StartInfo.RedirectStandardInput = true; //接受来自调用程序的输入信息
p.StartInfo.RedirectStandardOutput = true; //由调用程序获取输出信息
p.StartInfo.RedirectStandardError = true; //重定向标准错误输出
p.StartInfo.CreateNoWindow = true; //不显示程序窗口
if (!string.IsNullOrWhiteSpace(workDirectory))
{
p.StartInfo.WorkingDirectory = workDirectory;
}
// 接受输出的方式逐次执行每条
//var output = string.Empty;
var inputI = 1;
p.OutputDataReceived += (sender, e) =>
{
// cmd中的输出会包含换行;其他应用可以考虑接收数据是添加换行 Environment.NewLine
result.Output +=$"{ e.Data}{Environment.NewLine}" ;// 获取输出
if (inputI >= commands.Length)
{
return;
}
if (e.Data.Contains(commands[inputI - 1]))
{
p.StandardInput.WriteLine(commands[inputI]);
}
inputI++;
};
p.ErrorDataReceived+= (sender, e) =>
{
result.Error += $"{ e.Data}{Environment.NewLine}";// 获取输出
if (inputI>= commands.Length)
{
return;
}
if (e.Data.Contains(commands[inputI - 1]))
{
p.StandardInput.WriteLine(commands[inputI]);
}
inputI++;
};
p.Start();//启动程序
// 开始异步读取输出流
p.BeginOutputReadLine();
p.BeginErrorReadLine();
//向cmd窗口写入命令
p.StandardInput.WriteLine(commands[0]);
p.StandardInput.AutoFlush = true;
p.WaitForExit();//等待程序执行完退出进程。应在最后调用
p.Close();
}
catch (Exception ex)
{
result.ExceptError = ex;
}
return result;
}
});
}
#endregion
/// <summary>
/// 执行cmd命令 返回cmd窗口显示的信息
/// 多命令请使用批处理命令连接符:
/// <![CDATA[
/// &:同时执行两个命令
/// |:将上一个命令的输出,作为下一个命令的输入
/// &&:当&&前的命令成功时,才执行&&后的命令
/// ||:当||前的命令失败时,才执行||后的命令]]>
/// </summary>
///<param name="command">执行的命令</param>
///<param name="workDirectory">工作目录</param>
public static ExecResult Run(string command, string workDirectory = null)
{
command = command.Trim().TrimEnd('&') + "&exit"; //说明:不管命令是否成功均执行exit命令,否则当调用ReadToEnd()方法时,会处于假死状态
string cmdFileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.SystemX86), "cmd.exe");// @"C:\Windows\System32\cmd.exe";
using (Process p = new Process())
{
var result = new ExecResult();
try
{
p.StartInfo.FileName = cmdFileName;
p.StartInfo.UseShellExecute = false; //是否使用操作系统shell启动,设置为false可以重定向输入输出错误流;同时会影响WorkingDirectory的值
p.StartInfo.RedirectStandardInput = true; //接受来自调用程序的输入信息
p.StartInfo.RedirectStandardOutput = true; //由调用程序获取输出信息
p.StartInfo.RedirectStandardError = true; //重定向标准错误输出
p.StartInfo.CreateNoWindow = true; //不显示程序窗口
if (!string.IsNullOrWhiteSpace(workDirectory))
{
p.StartInfo.WorkingDirectory = workDirectory;
}
p.Start();//启动程序
//向cmd窗口写入命令
p.StandardInput.WriteLine(command);
p.StandardInput.AutoFlush = true;
//获取cmd的输出信息
result.Output = p.StandardOutput.ReadToEnd();
result.Error = p.StandardError.ReadToEnd();
p.WaitForExit();//等待程序执行完退出进程。应在最后调用
p.Close();
}
catch (Exception ex)
{
result.ExceptError = ex;
}
return result;
}
}
}
调用cmd执行交互命令
很多情况下,命令的执行都需要输入交互信息。在C#调用cmd执行时,处理交互信息却并不是直接输入那么简单(可自行测试...)
cmd中执行获取输出一般意味着命令(或新程序)已经执行结束,而交互命令则需要未执行完等待输入。
因此,大多数情况下交互信息都会新开一个窗口进行输入,这就不受当前Process的控制了,通常是等待使用者输入。这时需要额外操作新开窗口输入信息。
当然,也会有在当前主线程等待输入的情况,这时直接输入就可以了。
总之,交互命令或信息的输入,要根据实际情况来出来,很难抽象为一个统一的方法。
下面以PostgreSQL的createuser
命令(位于其bin
目录)为例,借助Win32 API窗口句柄相关的EnumWindows
方法遍历查找新开的cmd窗口,通过SendMessage
输入密码和回车,最后关闭窗口(之前已经介绍过窗口句柄的操作,相关方法作为WndHelper
帮助类直接使用,如有需要请查看之前文章,不再重复)。完成用户的新建过程。
由于是新打开了窗口,尝试连续输入、或异步读取输出都无法与新窗口交互,只能借助窗口句柄操作,也可以改为UI自动化输入信息。
PostgreSQL命令行创建用户和数据库,也可以查看之前的介绍。
// 用户密码
var userName = "myuser1";
var userPwd = "mypwd1";
// bin路径
var psqlBinPath = Path.Combine("C:\PostgreSQL", "pgsql", "bin");
// 执行createuser.exe
var result_cuser = await RunCMDPSqlCreateUserAsync(Path.Combine(psqlBinPath, "createuser.exe")+$" -P {userName}", userPwd);
如下为命令行执行时新窗口交互输入密码口令的实现。有打开窗口需要时间,因此查找窗口前等待一段时间,同时,输入口令和确认口令时也有个等待时间。
执行完成后需要关闭新开的窗口,否则会一直处于等待中。
需要注意的是,新开的窗口的标题应该包含执行的命令,但是其显示的内容命令中多了一个空格,因此,查找上也是额外处理的。传入的命令
C:\PostgreSQL\pgsql\bin\createuser.exe -P myuser1
,在新窗口标题中变为了C:\PostgreSQL\pgsql\bin\createuser.exe -P myuser1
。
/// <summary>
/// 执行cmd命令行创建用户,交互输入密码
/// </summary>
///<param name="createCommand">执行的命令</param>
/// <param name="userPwd">交互输入的口令密码</param>
/// <returns>cmd命令执行的输出</returns>
public static async System.Threading.Tasks.Task<ExecResult> RunCMDPSqlCreateUserAsync(string createCommand, string userPwd)
{
createCommand = createCommand.Trim().TrimEnd('&');
string cmdFileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "cmd.exe");// @"C:\Windows\System32\cmd.exe";
using (Process p = new Process())
{
var result = new ExecResult();
try
{
p.StartInfo.FileName = cmdFileName;
p.StartInfo.UseShellExecute = false; //是否使用操作系统shell启动
p.StartInfo.RedirectStandardInput = true; //接受来自调用程序的输入信息
p.StartInfo.RedirectStandardOutput = true; //由调用程序获取输出信息
p.StartInfo.RedirectStandardError = true; //重定向标准错误输出
p.StartInfo.CreateNoWindow = false; //需要显示程序窗口(操作新打开的确认口令窗口)
p.Start();//启动程序
p.StandardInput.AutoFlush = false;
//向cmd窗口写入命令
p.StandardInput.WriteLine(createCommand);
p.StandardInput.Flush();
// 等待一会,等待新窗口打开
Thread.Sleep(500);
// 命令行窗口包标题-P前包含两个空格,和传入的不符,因此无法查找到 @"管理员: C:\WINDOWS\system32\cmd.exe - C:\PostgreSQL\pgsql\bin\createuser.exe -P myuser".Contains(command);
var windows = WndHelper.FindAllWindows(x => x.Title.Contains(@"C:\PostgreSQL\pgsql\bin\createuser.exe")); // x.Title.Contains(command)
var window = windows[0];
WndHelper.SendText(window.Hwnd, userPwd);
WndHelper.SendEnter(window.Hwnd);
// 等待
Thread.Sleep(100);
WndHelper.SendText(window.Hwnd, userPwd);
WndHelper.SendEnter(window.Hwnd);
// 等待 需要等待多一点再关闭,否则可能创建不成功
Thread.Sleep(500);
// 处理完后关闭窗口,否则后面一直阻塞
WndHelper.CloseWindow(window.Hwnd);
// 或者发送 exit 和回车
// 不能直接使用标准输出,会一直等待;需要关闭新打开的窗口(或执行完exti退出)
result.Output = await p.StandardOutput.ReadToEndAsync();
result.Error = await p.StandardError.ReadToEndAsync();
p.WaitForExit();//等待程序执行完退出进程。应在最后调用
p.Close();
}
catch (Exception ex)
{
result.ExceptError = ex;
}
return result;
}
}
执行结果如下:
为了实现这个交互查找了很多资料,后来在大佬的提醒下,显示窗口查看一下问题。从而确认是新开了窗口。未显示窗口时查看异步输出信息,会一直等待没有确认口令的提示:
显示窗口时,可以看到新的窗口有提示确认口令的信息,这个信息和原Process接受到的是分开的,如果通过Process与此新窗口交互。
cmd命令作为参数执行时需要指定/K
在执行cmd命令时,也可以将命令作为cmd的启动参数执行。
但是,如果执行下面的命令,将会看到没有任何效果(只打开cmd窗口)。
Process.Start(@"C:\WINDOWS\system32\cmd.exe", "ping 127.0.0.1");
命令作为cmd的启动参数执行时,需要在最开始指定/K
参数。
Process.Start(@"C:\WINDOWS\system32\cmd.exe", "/K ping 127.0.0.1");
将会正确执行。
或者,使用ProcessStartInfo
对象。
Process process = new Process();
ProcessStartInfo startInfo = new ProcessStartInfo();
//startInfo.WindowStyle = ProcessWindowStyle.Hidden;
startInfo.FileName = "cmd.exe";
startInfo.Arguments = "/K ping 127.0.0.1";
process.StartInfo = startInfo;
process.Start();
cmd
/k
的含义是执行后面的命令,并且执行完毕后保留窗口。