Java进程调用外部shell脚本

原文链接: http://freewind886.blog.163.com/blog/static/66192464201261462759238/


由于使用ProcessBuilder 发生了阻塞 ,根据方法4搞定,记录下!

前段时间实现一个小功能,在长时间运行的管理服务器master(Java进程)上增加一种调用shell脚本发送报警的方式(已有邮件和短信报警)。脚本名称和相对路径固定,每发送一次报警master就会调用一次脚本(可能会很频繁),报警内容是JSON格式的消息,以$1参数传入脚本。用户可以自定义shell脚本的内容,例如再调用python脚本将报警内容发送到指定的服务器。master只负责调用脚本传入报警信息,确保执行脚本的线程能合理退出,如果有异常也要打印错误日志方便问题排查。

方案1

class ExecuteThread extends Thread {     private String cmdString;

    ExecuteThread(String cmd) {         this.cmdString = cmd     }

    public void run() {         String[] cmdArray = { "/bin/bash", "-c", cmdString };         ProcessBuilder builder = new ProcessBuilder(cmdArray);         process = builder.start();

        // 获取错误输出         InputStream stderr = process.getErrorStream();

        // 使用Reader进行输入读取和打印         InputStreamReader isr = new InputStreamReader(stderr);         BufferedReader br = new BufferedReader(isr);         String line = null;         System.out.println("<ERROR>");         while ( (line = br.readLine()) != null)             System.out.println(line);

        // 获取执行返回值         int exitCode = process.waitFor();         if (exitCode != 0) {         // 进行错误处理         }     } }

方案1的问题在于当脚本本身有问题导致执行时间过长时,整个操作就会在读取输出的地方卡住(br.readLine()),整个执行外部脚本操作的线程就会卡住无法退出。

方案2
对方案1进行改进,将循环读取输出的操作挪到另一个线程中进行。
这篇文章对这种方式以及执行外部脚本相关的细节有很详细的说明,推荐细看:http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html

class ExecuteThread extends Thread {     private String cmdString;

    ExecuteThread(String cmd) {         this.cmdString = cmd     }

    public void run() {         String[] cmdArray = { "/bin/bash", "-c", cmdString };         ProcessBuilder builder = new ProcessBuilder(cmdArray);         process = builder.start();

        // 获取错误输出         InputStream stderr = process.getErrorStream();

        // 使用StreamGobbler进行输入读取和打印         new StreamGobbler(stderr).start();

        // 获取执行返回值         int exitCode = process.waitFor();         if (exitCode != 0) {         // 进行错误处理         }     } }

class StreamGobbler extends Thread {     private InputStream input;

    public StreamGobbler(InputStream input) {         this.input = input;     }

    public void run() {         InputStreamReader isr = new InputStreamReader(input);         BufferedReader br = new BufferedReader(isr);         String line = null;         System.out.println("<ERROR>");         while ( (line = br.readLine()) != null)             System.out.println(line);     } }

对于脚本执行超时的问题,方案2没有解决,执行输出读取的StreamGobbler线程还是会因为readLine()卡住,而ExecuteThread会由于process.waitFor()而卡住。
如果每调用一次脚本就多出2个无法退出的线程,那master迟早会因为资源耗尽而崩溃。

方案3
对于Process.waitFor()的阻塞,可以调用Process.destroy()解除,而对于readLine()的阻塞,则尝试使用Reader.close或InputStream.close()来解除。

class ExecuteThread extends Thread {     // 增加close()方法,外部判定任务执行超时后进行资源清理     // 其他部分代码不变     public void close() {         process.destroy();         stderr.close();     } }

// 使用ExecuteThread的静态方法 static void executeCmd(String cmd) {     ExecuteThread execThread = new ExecuteThread(cmd).start();     try {         execThread.join(timeoutInMillis);     } finally {         execThread.close();        } }

针对方案3进行了多次的测试,Process.destroy()可以解除Process.waitFor()的阻塞,而stderr.close()却无法让阻塞的readLine()中断退出。也就是说,当用户写了个有问题的脚本,每次都执行很长一段时间甚至不退出,那么每发送一次报警就多1个阻塞的StreamGobbler线程,方案2中存在的问题没有解决。

方案4
在这个线程线程退出的问题上,也尝试了不少的方案,最后找到一个比较丑的办法,在sun jdk1.5和1.6上测试通过。

class ExecuteThread extends Thread {     private String cmdString;     private volatile Process process;     private volatile FileChannel inputChannel;

    ExecuteThread(String cmd) {         this.cmdString = cmd     }

    public void run() {         String[] cmdArray = { "/bin/bash", "-c", cmdString };         ProcessBuilder builder = new ProcessBuilder(cmdArray);         process = builder.start();

 

       // 获取脚本错误输出        InputStream errorStream = process.getErrorStream();        if (errorStream instanceof FileInputStream) {         inputChannel = ((FileInputStream) errorStream).getChannel();        } else {        throw new Exception("无法将脚本子进程的输出流转为管道");        }

        // inputChannel.read(buffer)会因为inputChannel的关闭而退出,不会一直阻塞;         /* 以下的处理也可以用另一个线程来执行,这里放在同一个线程是使得在调用脚本出错(例如脚本文件被误删除)         迅速退出的情况下也能获取到相应的错误信息,避免ExecuteThread比读取输出线程结束得更快。

*/        ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream(32);         ByteBuffer buffer = ByteBuffer.allocate(32);         WritableByteChannel channelOut = Channels.newChannel(byteArrayStream);         try {           while (inputChannel.read(buffer) > -1) {               buffer.flip();              channelOut.write(buffer);              buffer.clear();             }         } catch (IOException ioe) {            // 当脚本执行超时,由于channel的关闭必然会抛出异常         }

        // 获取执行返回值         int exitCode = process.waitFor();         if (exitCode != 0) {             // 进行错误处理         }     }

    public void close() {         process.destroy();         inputChannel.close();     } }

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
可以使用JavaProcessBuilder类来远程调用shell脚本ProcessBuilder类允许您启动并控制外部进程。以下是一个示例代码,它使用ProcessBuilder类从远程服务器上运行shell脚本。 ``` import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; public class RemoteShellExecutor { public static void main(String[] args) throws IOException, InterruptedException { String host = "remote-host"; // 远程服务器IP地址或域名 String user = "remote-user"; // 远程服务器用户名 String password = "remote-password"; // 远程服务器密码 String command = "/path/to/remote/script.sh"; // 远程服务器上的shell脚本路径 // 创建ProcessBuilder对象 ProcessBuilder processBuilder = new ProcessBuilder(); // 设置远程SSH连接命令 processBuilder.command("sshpass", "-p", password, "ssh", user + "@" + host, command); // 启动进程 Process process = processBuilder.start(); // 读取进程输出 BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; while ((line = reader.readLine()) != null) { System.out.println(line); } // 等待进程结束 int exitCode = process.waitFor(); System.out.println("Process exited with code " + exitCode); } } ``` 在上面的示例代码中,我们使用ProcessBuilder类创建了一个SSH连接命令,并将其设置为要执行的命令。然后,我们启动进程,并使用一个缓冲读取器来读取进程输出。最后,我们等待进程结束,并打印进程退出代码。请注意,您需要在本地计算机上安装sshpass软件包,以便使用密码连接到远程服务器。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值