使用Windows脚本宿主从WshShell.Exec捕获输出

时间:2010-01-16 01:46:40

标签: stdout javascript wsh

我编写了以下两个函数,并从Windows Script Host中运行的JavaScript调用第二个函数(“callAndWait”)。我的总体意图是从另一个调用一个命令行程序。也就是说,我正在使用cscript运行初始脚本,然后尝试从该脚本运行其他东西(Ant)。

function readAllFromAny(oExec)
{
     if (!oExec.StdOut.AtEndOfStream)
          return oExec.StdOut.ReadLine();

     if (!oExec.StdErr.AtEndOfStream)
          return "STDERR: " + oExec.StdErr.ReadLine();

     return -1;
}

// Execute a command line function....
function callAndWait(execStr) {
 var oExec = WshShell.Exec(execStr);
  while (oExec.Status == 0)
 {
  WScript.Sleep(100);
  var output;
  while ( (output = readAllFromAny(oExec)) != -1) {
   WScript.StdOut.WriteLine(output);
  }
 }

}

不幸的是,当我运行我的程序时,我没有得到关于被调用程序正在做什么的立即反馈。相反,输出似乎进入适合和开始,有时等到原始程序完成,有时似乎已经死锁。我真正想要做的是让生成的进程实际上与调用进程共享相同的StdOut,但我没有看到这样做的方法。只是设置oExec.StdOut = WScript.StdOut不起作用。

是否有另一种方法来生成将共享StdOut& StdErr的启动过程?我尝试使用“WshShell.Run(),但这给了我一个”权限被拒绝“错误。这是有问题的,因为我不想告诉我的客户改变他们的Windows环境配置只是为了运行我的程序。

我该怎么办?

5 个答案:

答案 0 :(得分:11)

您无法以这种方式从脚本引擎中读取StdErr和StdOut,因为没有像Code Master Bob所说的非阻塞IO。如果被调用的进程在您尝试从StdOut读取时填充了StdErr上的缓冲区(大约4KB),反之亦然,那么您将死锁/挂起。等待StdOut时你会饿死它会阻止你等待你从StdErr读取。

实际的解决方案是将StdErr重定向到StdOut,如下所示:

sCommandLine = """c:\Path\To\prog.exe"" Argument1 argument2"
Dim oExec
Set oExec = WshShell.Exec("CMD /S /C "" " & sCommandLine & " 2>&1 """)

换句话说,传递给CreateProcess的是:

CMD /S /C " "c:\Path\To\prog.exe" Argument1 argument2 2>&1 "

这将调用CMD.EXE,它解释命令行。 /S /C调用一个特殊的解析规则,以便删除第一个和最后一个引用,其余部分使用 as-is 并由CMD.EXE执行。所以CMD.EXE执行这个:

"c:\Path\To\prog.exe" Argument1 argument2 2>&1

咒语2>&1prog.exe的StdErr重定向到StdOut。 CMD.EXE将传播退出代码。

现在,您可以通过阅读StdOut并忽略StdErr来取得成功。

缺点是StdErr和StdOut输出混合在一起。只要它们是可识别的,你就可以使用它。

答案 1 :(得分:4)

在这种情况下可能有用的另一种技术是将命令的标准错误流重定向到标准输出。 通过在前面添加“%comspec%/ c”并在execStr字符串的末尾添加“2>& 1”来完成此操作。 也就是说,更改您运行的命令:

zzz

为:

%comspec% /c zzz 2>&1 

“2>& 1”是重定向指令,其使得StdErr输出(文件描述符2)被写入StdOut流(文件描述符1)。 您需要包含“%comspec%/ c”部分,因为它是命令解释器,它了解命令行重定向。见http://technet.microsoft.com/en-us/library/ee156605.aspx
使用“%comspec%”而不是“cmd”可以为更广泛的Windows版本提供可移植性。 如果您的命令包含带引号的字符串参数,那么将它们设置为正确可能很棘手: “/ c”之后cmd如何处理引号的规范似乎不完整。

这样,您的脚本只需要读取StdOut流,并且将同时接收标准输出和标准错误。 我用“net stop wuauserv”将其用于成功写入StdOut(如果服务正在运行) 和StdErr失败(如果服务已经停止)。

答案 2 :(得分:2)

首先,你的循环被打破,因为它总是首先尝试从oExec.StdOut读取。如果没有实际输出,那么它将一直挂起,直到有。在StdErr变为真(可能是孩子终止)之前,您不会看到任何StdOut.atEndOfStream输出。不幸的是,脚本引擎中没有非阻塞I / O的概念。这意味着如果缓冲区中没有数据,则调用read并立即返回。因此,可能没有办法让这个循环按你的意愿工作。其次,WShell.Run没有提供任何属性或方法来访问子进程的标准I / O.它在一个单独的窗口中创建子节点,与父节点完全隔离,但返回代码除外。但是,如果你想要的只是能够看到孩子的输出,那么这可能是可以接受的。您还可以与孩子(输入)互动,但只能通过新窗口(参见SendKeys)。

至于使用ReadAll(),这会更糟糕,因为它会在返回之前收集流中的所有输入,因此在流关闭之前您根本看不到任何内容。我不知道为什么exampleReadAll置于构建字符串的循环中,单个if (!WScript.StdIn.AtEndOfStream)应足以避免异常。

另一种替代方法可能是在WMI中使用进程创建方法。如何处理标准I / O并不清楚,似乎没有任何方法可以将特定流分配为StdIn / Out / Err。唯一的希望是孩子会从父母那里继承这些,但这就是你想要的,不是吗? (这个评论基于一个想法和一些研究,但没有实际的测试。)

基本上,脚本系统不是为复杂的进程间通信/同步而设计的。

注意:使用Script 5.6版在Windows XP Sp2上执行了确认上述测试。参考当前(5.8)手册表明没有变化。

答案 3 :(得分:1)

是的,当涉及终端输出时,Exec功能似乎被打破了。

我一直在使用类似的函数function ConsumeStd(e) {WScript.StdOut.Write(e.StdOut.ReadAll());WScript.StdErr.Write(e.StdErr.ReadAll());},我在类似于你的循环中调用它。不确定检查EOF并逐行阅读是好还是坏。

答案 4 :(得分:1)

您可能遇到了此Microsoft Support网站上描述的死锁问题。

一个建议是始终从stdoutstderr同时阅读。 您可以将readAllFromAny更改为:

function readAllFromAny(oExec)
{
  var output = "";

  if (!oExec.StdOut.AtEndOfStream)
    output = output + oExec.StdOut.ReadLine();

  if (!oExec.StdErr.AtEndOfStream)
    output = output + "STDERR: " + oExec.StdErr.ReadLine();

  return output ? output : -1;
}