Java程序运行、停止Shell脚本

碰到这样一个问题——用Java程序来控制shell脚本的运行和停止。具体来讲,这个Java程序至少要有三个功能:

  1. 运行Shell脚本;

  2. 等待Shell脚本执行结束;

  3. 停止运行中的Shell程序;

从功能需求来看,似乎是比较容易做到的。尽管没有写过类似功能的程序,Google一下,很快就有答案了。

用Runtime或者ProcessBuilder可以运行程序,而Process类的waitFor()和destroy()方法分别满足功能2和3。

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;


public class ShellRunner extends Thread
{
  private Process proc;
  private String dir;
  private String shell;

  public ShellRunner(String dir, String shell)
  {
    super();
    this.proc = null;
    this.dir = dir;
    this.shell = shell;
  }

  @Override
  public void run() {
    try
    {
      ProcessBuilder builder = new ProcessBuilder("sh", dir + shell);
      builder.directory(new File(dir));

      proc = builder.start();
      System.out.println("Running ...");
      int exitValue = proc.waitFor();
      System.out.println("Exit Value: " + exitValue);
    }
    catch (IOException e)
    {
      e.getLocalizedMessage();
    }
    catch (InterruptedException e)
    {
      e.getLocalizedMessage();
    }
  }

  public void kill()
  {
    if (this.getState() != State.TERMINATED) {
      proc.destroy();
    }
  }

  public static void main(String args[]) {
    ShellRunner runner = new ShellRunner("/tmp/", "run.sh");
    runner.start();

    InputStreamReader inputStreamReader = new InputStreamReader(System.in);
    BufferedReader reader = new BufferedReader(inputStreamReader);
    try
    {
      String line = null;
      while ( (line = reader.readLine()) != null ) {
        if (line.equals("kill")) {
          runner.kill();
        }
        else if (line.equals("break")) {
          break;
        }
        else {
          System.out.println(runner.getState());
        }
      }
      reader.close();
      inputStreamReader.close();
    }
    catch (IOException e)
    {
      e.printStackTrace();
    }

  }
}

跑一下上面这个测试程序,waitFor()方法可以正确等待shell程序退出,但是destroy()方法并没有结束shell脚本相关的进程。

为什么呢?

这是一个BUG

JDK-bug-4770092:Process.destroy() 不能结束孙子进程(grandchildren)。上述例子中,java程序的子进程是”sh run.sh”,而shell脚本中的任何命令都是”sh run.sh”这个进程的子进程(也可能有孙子进程,或者更远的后代进程)。所以shell脚本中执行的命令并不能随着 Process.destroy()结束。这是一个很老的BUG,但是出于各个平台兼容性的考虑,官方并不准备修复这个BUG。似乎依赖Java程序来完 成功能3的路已经断了。

现在剩下的问题可以归结为:如何结束一颗进程树上的所有进程?其中的某些进程可能已经退出,也就是说进程树的某些分支可能已经断开了。

一个比较自然的想法是:记录所有以”sh run.sh”这个进程为根进程的所有进程号,需要的时候统一kill。这需要一点Linux进程的相关知识:

  • Linux下每个进程有很多ID属性:PID(进程号)、PPID(父进程号)、PGID(进程组号)、SID(进程所在session的ID)

  • 子进程会继承父进程的进程组信息和会话信息

  • 一个进程只能创建从属于(和它自身)同一个会话的进程组,除非使用setsid系统调用的方式新建一个会话

  • 进程组不能在不同的会话中迁移,进程所属的进程组可以变,但是仅限于同一个会话中的进程组

还要一点Linux命令工具的知识:

  • 用 “kill -9 -1234”可以杀死进程组号为1234的所有进程

  • strace命令可以跟踪某个进程以及它fork出来的所有子进程产生的系统调用

有了这些知识,思路就比较清晰了:用strace跟踪setsid这个系统调用,记下所有伴随系统调用产生的SID;在需要杀死这棵进程树上的所有进程时,用ps命令把进程树上还没退出的进程组全部找出,一并kill。

形成代码就是:

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;


public class ShellRunner2 extends Thread
{
  private Process proc;
  private String dir;
  private String shell;
  private File tmpFile;

  public ShellRunner2(String dir, String shell) throws IOException
  {
    super();
    this.proc = null;
    this.dir = dir;
    this.shell = shell;
    this.tmpFile = createTempFile(dir, shell);
  }

  @Override
  public void run() {
    try
    {
      ProcessBuilder builder = new ProcessBuilder("sh", "-c",
          "strace -o " + tmpFile.getPath() + " -f -e trace=setsid setsid sh " + dir + shell);
      builder.directory(new File(dir));

      proc = builder.start();
      System.out.println("Running ...");
      int exitValue = proc.waitFor();
      System.out.println("Exit Value: " + exitValue);
    }
    catch (IOException e)
    {
      e.getLocalizedMessage();
    }
    catch (InterruptedException e)
    {
      e.getLocalizedMessage();
    }
  }

  public void kill()
  {
    if (this.getState() != State.TERMINATED) {
      try
      {
        ProcessBuilder builder = new ProcessBuilder("sh", "-c",
            "ps -o sid,pgid ax | " +
            "grep $(grep -e \"setsid()\" -e \"<... setsid resumed>\" " +
              tmpFile.getPath() +
            " | awk '{printf \" -e\" $NF}') | awk {'print $NF'} | " +
            "sort | uniq | sed 's/^/-/g' | xargs kill -9 2>/dev/null");
        builder.directory(new File(dir));
        Process proc = builder.start();
        proc.waitFor();
      }
      catch (IOException e)
      {
        e.printStackTrace();
      }
      catch (InterruptedException e)
      {
        e.printStackTrace();
      }
    }
  }

  public static void main(String args[]) throws IOException {
    ShellRunner2 runner = new ShellRunner2("/tmp/", "a.sh");
    runner.start();

    InputStreamReader inputStreamReader = new InputStreamReader(System.in);
    BufferedReader reader = new BufferedReader(inputStreamReader);
    try
    {
      String line = null;
      while ( (line = reader.readLine()) != null ) {
        if (line.equals("kill")) {
          runner.kill();
        }
        else if (line.equals("break")) {
          break;
        }
        else {
          System.out.println(runner.getState());
        }
      }
      reader.close();
      inputStreamReader.close();
    }
    catch (IOException e)
    {
      e.printStackTrace();
    }

  }

  private File createTempFile(String dir, String prefix) throws IOException {
    String name = "." + prefix + "-" + System.currentTimeMillis();
    File tempFile = new File(dir, name);
    if (tempFile.createNewFile()) {
      return tempFile;
    }
    throw new IOException("Failed to create file " + tempFile.getPath() + ".");
  }
}

设计一个针对性的测试:

下面这个程序fork子程序并设置PGID,测试过成中编译为可执行文件pgid_test。

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main(int argc, void *argv[]) {
  pid_t pid;
  int slp = 10;

  if (argc > 1) {
      int tmp = atoi(argv[1]);
      if (tmp > 0 && tmp < 120) {
          slp = tmp;
      }
  }

  if ((pid = fork()) != 0) {
      if (setpgid(pid, 0) != 0) {
          fprintf(stderr, "setpgid() error - %s", strerror(errno));
      }
  }

  sleep(slp);
  
  return 0;
}

{a,b,c,d}.sh这几个shell脚本之间的调用模拟了一个进程树的生成,并且这些进程有着不同的SID或者PGID。

a.sh

sh b.sh &(sleep 15)&sleep 10

b.sh

(sleep 12)&sh c.sh &

c.sh

./pgid_test 20 &setsid sh d.sh &setsid sh d.sh &

d.sh

./pgid_test 30 &(sleep 30; echo "bad killer " `date` >> /tmp/bad_killer) &

运行java程序,调用kill()后,所有子进程都被成功结束了。

这个方法并不那么优美,也有可能存在问题:

  1. strace的输出sesion id的形式可能不止那么两种;不同版本的strace的输出可能不一样。

  2. shell程序可能以其他用户身份启动了一些进程,而kill又没有权限杀死那些进程。

  3. 在ps命令执行之后,kill执行之前,正好有setsid调用,这个调用产生的session的相关进程会被漏掉。这个问题可以通过多次执行解决。


你可能感兴趣的:(Java程序运行、停止Shell脚本)