如何在Java中使用ProcessBuilder运行nohup命令并保持后台运行?

新手上路,请多包涵

如何在java中调用ProcessBuilder运行nohup...&格式的bash命令?

我现在有一个简单的bash脚本t.sh。这个脚本维护一个计数器,每过1s,该计数器会+1,并且将当前该计数器的值输出到标准输出上。

#!/bin/bash

counter=0

while true; do
    counter=$((counter + 1))
    echo $counter
    sleep 1
done

我可以直接使用nohup bash t.sh & 来在linux环境下运行该脚本,该脚本会在nohup.out中输出下面的预期结果

1
2
3
......

现在,我想要在Java中通过ProcessBuilder来运行该脚本。我可以使用下列方法

    @Test
    void testNohupInOne() throws IOException, InterruptedException {
        // String cmd = "nohup bash t.sh &";
        String cmd = "bash t.sh";

        String[] cmds = new String[] { "bash", "-c", cmd };
        ProcessBuilder processBuilder = new ProcessBuilder(cmds);
        Process process = processBuilder.start();
        
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
            reader.lines().forEach(line -> {
                System.out.println(line);
            });
        }

        process.waitFor();
    }

上述方法可以正常在java程序的标准输出上,输出和直接在linux终端中使用nohup bash t.sh & 后在nohup.out中一样的结果。

然而,如果我们将java中使用的命令替换为bash t.sh,即将上面testNohupInOne方法的第一行取消注释,而第二行添加注释

String cmd = "nohup bash t.sh &";
// String cmd = "bash t.sh";

那么,java程序只会输出1

1

这似乎意味着,java程序会立刻结束nohup...&格式的命令的运行,而非如同预期一般将其作为后台进程持续运行。
如果我们将bash脚本中counter的初始值从0改为3,则java程序的输出会对应的变为仅有4一行。

我想要知道,为什么java程序会产生这样的行为?如果我想要在java程序中通过ProcessBuilder运行nohup...&格式的bash命令,是否有比较优雅的实现方式?

阅读 1.1k
avatarAI BotBETA

问题解释

在Java中使用ProcessBuilder时,你遇到的问题是由于对nohup和后台运行(&)命令的理解不足导致的。当你在bash中直接执行nohup bash t.sh &时,这个命令被shell处理,它负责将进程放到后台并管理nohup的输出重定向。但是,当你在Java的ProcessBuilder中尝试这样做时,&字符并不被Java或bash -c直接理解为一个后台运行的指示符,因为bash -c通常期望的是一个完整的命令字符串,而不是一个会被shell解析的命令行。

Java中优雅的实现方式

为了在Java中通过ProcessBuilder优雅地运行一个命令(如nohup bash t.sh)并保持其在后台运行,你可以考虑以下几种方法:

  1. 直接运行不带&的nohup命令
    由于Java的ProcessBuilder会阻塞等待子进程结束(除非你另外管理子进程的线程),你可以简单地去掉&,然后让Java程序继续执行其他任务,而不需要等待这个子进程结束。

    String cmd = "nohup bash t.sh > nohup.out 2>&1 &";
    String[] cmds = {"bash", "-c", cmd.replace("&", "")}; // 移除&,因为bash -c不期望它
    ProcessBuilder processBuilder = new ProcessBuilder(cmds);
    Process process = processBuilder.start();
    
    // 如果你不需要等待这个进程结束,可以直接继续你的程序
    // 如果需要等待,可以使用 process.waitFor(); 但这通常不是后台运行的意图

注意:这里去掉了&,但在实际bash脚本中,你可能仍然希望使用它(虽然在这个特定的bash -c调用中不被需要)。如果你确实需要后台运行并且不关心nohup.out的输出,你可以让bash脚本自己处理后台化(通过脚本内部的&),但在ProcessBuilder调用中不这样做。

  1. 使用Runtime.getRuntime().exec()
    虽然ProcessBuilder是更推荐的方式,但在某些情况下,你可能想使用Runtime.getRuntime().exec(),特别是如果你需要更直接地模拟shell行为。然而,这通常不推荐,因为它更难于正确处理和避免潜在的安全风险。
  2. 分离子进程管理
    如果你的Java应用确实需要管理多个这样的后台进程,并且需要它们独立于Java进程的生命周期,你可能需要将这些命令放入到shell脚本中,并通过Java调用这个shell脚本来启动它们。然后,Java程序可以继续执行其他任务,而不需要等待这些后台进程。

结论

对于大多数情况,简单地去掉&并让Java程序继续执行其他任务,而nohupbash脚本在后台运行,是一个简单且有效的解决方案。如果你确实需要Java程序与这些后台进程交互(例如,检查它们的状态或终止它们),你可能需要实现更复杂的进程管理逻辑。

1 个回答

机器人回答的可靠,我建议你通过Runtime.getRuntime().exec():,另外把脚本改一下

#!/bin/bash

counter=0

: > /tmp/res.dat

while true; do
    counter=$((counter + 1))
    echo $counter >> ~/tmp/res.dat
    sleep 1
done

因为你使用echo输出,这是在标准输出的,nohup会创建一个nohup.out文件接收你的输出,&将任务丢在了后台,你通过ProcessBuilder的流读取结果时,任务已经在后台运行了。

你可以先启动程序,然后去监听/tmp/res.dat的变化,这样结果就是准确的

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏