当通过PsExec传递时,程序输出丢失

15

(这是我的同事在其他地方发布的问题,但我想在这里发布它,以便能够接触到不同的受众。)

大家好, 我正在测试编写一个小型Java应用程序的可能性,该应用程序将使用Psexec启动远程作业。在测试将Java程序的stdin和stdout绑定到psexec时,我遇到了一个奇怪的错误。

我的测试程序是一个基本的echo程序。它启动一个线程从stdin读取,然后直接将读取输出管道传回stdout。当在本地机器上运行而不是从psexec运行时,它运行得非常好。完全按照预期。

然而,当我从PsExec调用它时,第一次将输入直接传送到stdout时,它就丢失了。使这个错误真正奇怪的是,只有第一次将输入直接传送到stdout时才会丢失。如果将输入字符串附加到另一个字符串中,则可以正常工作。无论是字符串文字还是字符串变量。但是,如果将输入字符串直接发送到stdout,则无法通过。第二次将其发送到stdout时,它可以正常通过-并且此后每次都可以。

我完全不知道发生了什么。我尝试过测试我能想到的每种可能的错误。我已经没有主意了。我错过了什么还是这只是psexec内部的问题?

以下是相关的代码,它包含三个类(其中一个实现了一个单函数接口)。

主类:

public class Main {
    public static void main(String[] args) {
        System.out.println("Starting up.");

        CReader input = new CReader(new BufferedReader(
            new InputStreamReader(System.in)));
        CEcho echo = new CEcho();

        input.addInputStreamListener(echo);
        input.start();

        System.out.println("Successfully started up.  Awaiting input.");
    }
}

CReader类是从标准输入读取的线程:

public class CReader extends Thread {
    private ArrayList<InputStreamListener> listeners = 
        new ArrayList<InputStreamListener>();
    private boolean exit = false;
    private Reader in;

    public CReader(Reader in) {
        this.in = in;
    }

    public void addInputStreamListener(InputStreamListener listener) {
        listeners.add(listener);
    }

    public void fireInputRecieved(String input) {

        if(input.equals("quit"))
        exit = true;

        System.out.println("Input string has made it to fireInputRecieved: "
            + input);

        for(int index = 0; index < listeners.size(); index++)
            listeners.get(index).inputRecieved(input);
    }

    @Override
    public void run() {

        StringBuilder sb = new StringBuilder();
        int current = 0, last = 0;

        while (!exit) {
            try {
                current = in.read();
            }
            catch (IOException e) {
                System.out.println("Encountered IOException.");
            }

            if (current == -1) {
                break;
            }

            else if (current == (int) '\r') {
                if(sb.toString().length() == 0) {
                    // Extra \r, don't return empty string.
                    continue;
                }
                fireInputRecieved(new String(sb.toString()));
                sb = new StringBuilder();
            }

            else if(current == (int) '\n') {
                if(sb.toString().length() == 0) {
                    // Extra \n, don't return empty string.
                    continue;
                }
                fireInputRecieved(new String(sb.toString()));
                sb = new StringBuilder();
            }
            else {
                System.out.println("Recieved character: " + (char)current);
                sb.append((char) current);
                last = current;
            }
        }       
    }
}

CEcho类是将其输出到标准输出的类:

public class CEcho implements InputStreamListener {
    public void inputRecieved(String input) {
        System.out.println("\n\nSTART INPUT RECIEVED");
        System.out.println("The input that has been recieved is: "+input);
        System.out.println("It is a String, that has been copied from a " +
            "StringBuilder's toString().");
        System.out.println("Outputting it cleanly to standard out: ");
        System.out.println(input);
        System.out.println("Outputting it cleanly to standard out again: ");
        System.out.println(input);
        System.out.println("Finished example outputs of input: "+input);
        System.out.println("END INPUT RECIEVED\n\n");
    }
}

最后,这是程序的输出:

>psexec \\remotecomputer "C:\Program Files\Java\jre1.6.0_05\bin\java.exe" -jar "C:\Documents and Settings\testProram.jar"
PsExec v1.96 - 远程执行进程 版权所有 (C) 2001-2009 Mark Russinovich Sysinternals - www.sysinternals.com
正在启动。 成功启动。等待输入。 Test 收到字符:T 收到字符:e 收到字符:s 收到字符:t 输入字符串已经到达fireInputRecieved: Test
开始接收输入 已接收到的输入是:Test 它是一个字符串,已从StringBuilder的toString()复制。 将其干净地输出到标准输出:
再次干净地输出到标准输出: Test 完成输入示例的输出:Test 结束接收输入

为了透明度(也为了避免如果我做了什么愚蠢的事情而责怪马克),我是最初发布这个问题的同事。我不知道我是否只是在做一些愚蠢的事情,还是这是psexec中一个合法的bug。我真的希望它是前者 - 因为如果是后者,我就要做更多的工作了。 - Daniel Bingham
是的...我希望有一种方法可以将它转移到Alcon...这真的是他的问题,只是他在我发布后才意识到SO有多棒。 - mwalling
澄清一下:输入是什么并不重要。我可以输入Argle-Bargle Glophy-Glyph,它仍然会丢失。因此,它似乎与输入变量的实际内容无关。 - Daniel Bingham
1
@mwalling,这个问题有更新了吗?我们知道PSEXEC在哪里读取输出并且如何避免它吗? - Sunny R Gupta
1
不好意思,我们已经在四年前转移了。 - mwalling
显示剩余3条评论
13个回答

4
你是否尝试将输出重定向到文件中(java... >c:\output.txt)?这样你可以仔细检查是否所有内容都已输出到stdout,或者可能被psexec吞噬了。

好主意。让我试试看会发生什么。 - Daniel Bingham
2
不错的调用,果然是psexec出了问题。好的,那么这就是psexec的一个问题。现在有没有绕过它的方法? - Daniel Bingham
1
我接受这个,因为他是第一个说psexec有问题的人,而且赏金需要去某个地方。 - mwalling

3

PsExec正在吞噬输出。下一个有趣的问题可能是它在哪里吞噬输出。您可以通过获取Wireshark的副本并检查该输出是否正在通过网络传输来进行检查。如果没有,那么它就被吞噬在远程端。如果有,它就是在本地被吞噬。

虽然我不确定接下来该怎么做,但收集更多信息肯定是一个好的方向...


2

我遇到了同样的问题,尝试了多种重定向组合。

以下是解决方法:

processBuilder.redirectErrorStream(true);
processBuilder.redirectOutput(Redirect.PIPE);
processBuilder.redirectInput(Redirect.INHERIT);

final Process process = processBuilder.start();

// Using Apache Commons IOUtils to get output in String
StringWriter writer = new StringWriter();
IOUtils.copy(process.getInputStream(), writer, StandardCharsets.UTF_8);
String result = writer.toString();
logger.info(result);

final int exitStatus = process.waitFor();

使用processBuilder.redirectInput的Redirect.INHERIT选项使我得到了缺失的远程命令输出。


1
也许你可以尝试将输入的副本传递给你的监听器:
 public void fireInputRecieved(String input) {

    if(input.equals("quit"))
    exit = true;

    String inputCopy = new String(input);
    System.out.println("Input string has made it to fireInputRecieved: "
        + input);



    for(int index = 0; index < listeners.size(); index++)
        listeners.get(index).inputRecieved(inputCopy);
}

我在监听器方面也遇到了类似的问题,如果我没有传递一个显式的副本,那么传递的变量最终会为空。


我曾经考虑过这个问题并尝试了类似的方法,但我们现在已经确定是psexec而不是Java在占用输出。谢谢! - Daniel Bingham

1

我也遇到了同样的问题,最近一直在阅读这篇帖子,期望能找到解决方案。然后我决定放弃psexec并寻找替代方案。所以,这就是事情的经过:PAExec。它非常完美地用于获取命令输出。


欢迎来到Stack Overflow!请查看此链接。在您的答案中包含一些摘录。 - Devraj Gadhavi

1

System.out没有自动刷新配置吗?尝试在第一次打印后使用 System.out.flush(),看看第一行是否出现而不再打印更多行。

(哦是的,严肃点,应该是"RECEIVED",而不是"RECIEVED")


没有什么不同。我很确定System.out默认配置为自动刷新。如果不是这样,其他println也不应该被发送。这个bug真正奇怪的地方在于,它只发生在你第一次尝试发送没有任何字符附加的输入时。而且只有在使用StringBuilder设置输入后才会出现。任何其他的打印尝试都可以正常工作。虽然这样做值得一试,但还是谢谢你。另外,那只是一个拼写错误。我通常在编码时不会过多考虑拼写。特别是当我被这样的bug分心时 :p - Daniel Bingham
@Alcon - 而且 应该是那个拼写不好的人! - mwalling
@mwalling,你才是拼写有问题的那一个 :p 我只是懒得改。 - Daniel Bingham

1

好的,我在周末考虑了一下,既然你要在不同的机器之间跳转,我想知道是否存在字符集问题?也许第一次它吃掉了字符串并处理了不同的代码页或字符集问题?Java通常是16位字符,而Windows则是8位带有代码页或utf-8。

本地和远程机器有不同的默认字符集的可能性大吗?如果你正在通过网络发送本地化数据,它可能会出现问题。


我想这是有可能的,但它们都是基于相同核心负载和运行相同JRE的Windows XP SP2版本。因此,这不应该是一个字符集差异问题。但是,当读入时,它可能会以某种方式转换为Java字符集,然后在输出时没有被转换回来。我可以尝试通过在输出时强制设置字符集来解决这个问题。但我仍然认为这不太可能是原因。 - Daniel Bingham
我担心PSExec对字符集有不同的理解。当你附加文本时,它可以无缝运行,但如果你原样打印它,第一次就会失败...也许PSExec必须扩展其字符集的概念,并在此过程中丢失字符串?诚然,这只是一个猜测,但一旦你开始在管道/端口之间传递字符串,这种情况就会发生。 - Mark
我会尝试强制使用几个字符集,看看会发生什么。 - Daniel Bingham
好的,尝试强制 BufferedReader 使用 Latin-1 和 US-ASCII 字符集,但都没有效果。这并不意味着它不是字符集问题,但如果这两个都不起作用,那我有点怀疑强制字符集会修复它:(除非还有其他地方需要强制。 - Daniel Bingham

1
当我运行psexec时,我看到它会生成一个子窗口来执行任务,但却不会将该程序的输出返回给其控制台窗口。我建议使用WMI或某种Windows进程API框架,以获得您在psexec中似乎缺乏的控制水平。毫无疑问,Java有一个类似于.Net的System.Diagnotics.Process类的等效物。

1

我并没有确切的答案,但是一些评论可能会有所帮助。

  • "传递副本"的想法不应该有影响,因为在失败之前,你的输出成功地打印了两次字符串,然后又成功了。
  • 自动刷新也不应该有影响,因为你已经提到过了。
  • Niko的建议有一定的价值,用于诊断目的。结合Mark的建议,让我想知道是否有一些看不见的控制字符介入了。如果你将字符的字节值作为诊断步骤打印出来会怎样?
  • 你“知道”这个值是“Test”(至少在你给我们的输出中是这样)。如果你直接将“Test”传递给失败的printLn语句会发生什么?
  • 在这种情况下,你想要获得尽可能多的信息。插入断点并分析字符。将字节发送到文件并在十六进制编辑器中打开它们。尽可能准确和精确地跟踪事物。
  • 想出奇怪的测试场景并尝试它们,即使它们不可能有所帮助。你永远不知道在分析绝望的想法的结果时会有什么好主意。

尝试在许多地方将"Test"直接传递给println并且都没有问题。同时,还尝试使用非test的输入,它们也没有回来。所以这与值"test"无关。使用输出捕获建议后,我们已经确定现在肯定是psexec吞噬了输出。问题是为什么。 - Daniel Bingham
根据这些评论,我必须假设你所输入的“测试”和你在代码中输入的“测试”之间存在差异。那个差异是什么? - John Fisher
你可以通过在inputReceived函数的顶部添加这行代码来更彻底地测试我的最后一条评论:'input = "Test"'。如果它总是有效,但是“Test”不能通过正常程序流程工作,那么接收到的文本字符串中可能隐藏着一些狡猾和恶意的东西,而编码的字符串中没有。 - John Fisher

1
我猜可能在那里有一个虚假的字节引导着T。根据JavaDocs,InputStreamReader将读取一个或多个字节,并将其解码为字符。
你可能在其中有一个转义序列或杂乱的字节,伪装成一个多字节字符。
快速检查一下 - 看看"current"是否大于128或小于33。
如果你使用CharArrayReader来获取单个字节而没有进行任何字符集转换,会怎样呢?
理论上,在第一次尝试使用println输出字符串时,它发送了某种转义字符,吃掉了剩下的字符串。在以后的打印中,Java或网络管道要么处理它,要么将其删除,因为它先前得到了该转义序列,也许以某种方式改变了处理方式。
作为一个无关的细微之处,sb.toString()返回一个新的String,所以调用"new String(sb.toString())"是不必要的。

新的String()只是我尝试找出问题所在的无数个步骤之一。我并不认为它会有什么区别,但我想尽可能地隔离输入和输出。你知道调试的过程,你到了一个点,你几乎愿意尝试任何事情,即使它并没有真正意义上的道理 ;) - Daniel Bingham
我会尝试检查并看看会发生什么。 - Daniel Bingham

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接