我正在尝试从我的java代码中执行一个脚本,如下所示:
Process p = Runtime.getRuntime().exec(cmdarray, envp, dir); // cmdarray is a String array
// consisting details of the script and its arguments
final Thread err = new Thread(...); // Start reading error stream
err.start();
final Thread out = new Thread(...); // Start reading output stream
out.start();
p.waitFor();
// Close resources
脚本的执行结束了(它的pid已不复存在),但java仍停留在进程的waitFor()
方法上!
是的,我正在2个独立的线程中读取输出和错误流。是的,他们最后加入(waitFor()
之后)。
该脚本基本上安装了几个RPM(如10个左右)并进行配置。所以脚本运行了60多秒。
看起来类似于以下内容:
#!/bin/sh
#exec 3>&1 >/var/log/some_log 2>&1
# If the above line is uncommented, Java recognizes that the
# process is over and terminates fine.
tar xzf a-package-having-rpms.tar.gz
cd unpacked-folder
(sh installer-script.sh) #This installs 10 odd rpms in a subshell and configures them
cd ..
rm -rf unpacked-folder
exit 0
令人震惊的是,如果我在脚本中放置以下行(在顶部),Java会理解脚本已经结束并且它会完美地终止该过程。
exec 3>&1 > /var/log/some_log 2>&1
对于记录,脚本不会生成任何输出。零个字符!所以把exec语句放在那里毫无意义!
但是,神奇地说,将exec语句放在脚本中会使java工作! 为什么?
如何在脚本中避免使用那种不合逻辑的exec语句?
如果您对installer-script.sh的内容感兴趣,那么:
#!/bin/sh
exec 3>&1 >>/var/log/another-log.log 2>&1
INSDIR=$PWD
RPMSDIR=$INSDIR/RPMS
cd $RPMSDIR
#
rpm -i java-3.7.5-1.x86_64.rpm
rpm -i --force perl-3.7.5-1.x86_64.rpm
rpm -i --nodeps mysql-libs-5.0.51a-1.vs.i386.rpm
rpm -i --nodeps mysql-5.0.51a-1.vs.i386.rpm
rpm -i --nodeps mysql-server-5.0.51a-1.vs.i386.rpm
rpm -i --nodeps perl-DBD-MySQL-3.0007-2.el5.i386.rpm
rpm -i --nodeps perl-XML-Parser-2.34-6.1.2.2.1.i386.rpm
.
.
.
现在,为什么Java需要exec命令才能知道进程已经结束? 我怎么能避免那个exec?,尤其是因为第一个脚本不会产生任何输出。
屏住呼吸等待答案!
答案 0 :(得分:7)
我的猜测是,Java不会认为脚本已经结束,直到它通过stdin / stdout / stderr传递给它的管道被子进程关闭。也就是说,stdin上没有更多活动的读者进程,stdout / stderr上没有更多活动的编写器进程。
当您在管道上阅读时,在没有更多进程打开管道输出之前,您不会收到文件结束指示。因此,如果进程分叉和新进程继承了一个打开的文件句柄,那么原始进程就会终止,仍然有一个打开文件的进程,读者仍然会等待。
与您正在编写的管道类似,在最后一个读取器关闭管道之前,您不会收到“断管”信号。
当您的脚本分离继承stdin / stdout / stderr的后台任务(如新安装的服务)时,通常会出现此问题。
通过使用exec,您明确地破坏了这些管道的继承链,以便后台进程不使用它们。
如果在Linux上,请检查/ proc / * / fd是否有任何新服务,看看他们的stdin / stdout / stderr是否与java进程传递给脚本的管道相同。
运行/etc/init.d/xxx脚本时经常会出现同样的情况:当您从命令行运行它们时它们会正常完成,但是当您从某种监视器运行它们时它们似乎会挂起。
修改强>
您说安装程序脚本包含以下行:
exec 3>&1 >>/var/log/another-log.log 2>&1
第一个术语3>& 1,将stdout克隆到文件描述符3(参见man bash中的Redirections)。据我所知,fd 3没有特别的意义。然后它通过打开/var/log/another-log.log替换stdout并通过克隆stdout替换stderr。请参阅bash手册页的重定向部分
这意味着新文件描述符3在最初作为STDOUT传入的管道上打开以进行写入。期望成为系统服务守护程序的程序通常会关闭文件描述符0(STDIN),1(STDOUT)和2(STDERR),但可能不会打扰任何其他文件描述符。此外,既然shell已打开FD-3,它会将该打开文件传递给它执行的任何命令,包括后台命令。
您知道安装程序打开FD 3是否有任何特殊原因?我的猜测是,如果您只是从安装程序中删除“3>& 1”术语,您的问题就会得到解决。这将允许您完全从脚本中删除exec
答案 1 :(得分:1)
可能的原因是安装软件包启动后台进程,使一个或两个管道(stdout / stderr)保持打开状态。因此您的线程不会终止。这意味着这些后台进程没有正确守护进程。正确的守护程序在初始化完成后用/ dev / null或日志文件替换其原始stdin / stdout / stderr。
答案 2 :(得分:0)
如果您在Linux / Unix上运行,请尝试不使用stdout / stderr。
上次我必须从Java运行外部脚本时,我不得不检查操作系统并仅在Windows上附加输出食物线程。
答案 3 :(得分:0)
我也会抽取标准输入。泵送所有三个标准流是个好主意。我无法解释它,但是在进程挂起之前我遇到了类似的问题,而且缺少stdin泵是问题所在。
答案 4 :(得分:0)
我没有在Linux上尝试过,但我在Windows中遇到了这个问题。我想我必须先关闭流程中的流程标准才能终止。
答案 5 :(得分:0)
听起来你理解问题的各个部分,但你仍然没有得到“大图片”。
是的,可以从Java程序调用shell脚本。事实上很容易! < =你已经知道了。
是的,通常情况下,您应该可以看到打印到stderr或stdout的任何内容。
是的,你应该能够将stderr和/或stdout重定向到一个文件。
你的问题显然是你没有看到你期望看到的东西,当你期望看到它时。这是正常的,这是预期的......而且,在更大的方案中,这是非常可取的。
我认为,核心问题是“I / O缓冲”。
缓冲是一件好事。
我强烈建议您: 1.创建一个简单的单线程命令行Java测试程序,该程序调用shell脚本。 < =您应该看到“预期结果”
使用线程详细说明测试程序。 根据程序的结构,您可能会或可能不会得到“预期结果”。
用C程序(或其他Java程序)替换shell脚本 < =这使得更容易使用“stderr”,这是UNBUFFERED,vs“stdout” 你也应该失去“exec”废话 - 这是一个红鲱鱼!
实验,看看你学到了什么。
PS: 并重复我的口头禅:“I / O缓冲是好的”; - )
PPS:
问:到目前为止,这个问题仅被查看了38次!
答:“你必须学会耐心,年轻的蚱蜢”; - )
问:这对我来说有点关键!答:“你也必须学习谦卑”; - )
答案 6 :(得分:-1)
您无法从Java执行脚本并等待它。您必须执行该脚本的解释器。 cmdarray应以“bash”开头,而不是“myscript.bash”。