JDK-4770092 : (process) Process.destroy does not kill multiple child processes
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.lang
  • Affected Version: 1.3.1
  • Priority: P4
  • Status: Closed
  • Resolution: Won't Fix
  • OS: windows_nt
  • CPU: x86
  • Submitted: 2002-10-28
  • Updated: 2017-05-12
  • Resolved: 2017-05-12
Description
Name: nt126004			Date: 10/28/2002


FULL PRODUCT VERSION :
  Bug reproduced in 1.4, 1.3.1_01,_03, and _04.

java version "1.3.1_04"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.3.1_04-b02)
Java HotSpot(TM) Client VM (build 1.3.1_04-b02, mixed mode)


FULL OPERATING SYSTEM VERSION :

Windows NT 4.00.1381

A DESCRIPTION OF THE PROBLEM :

My basic requirement is to run two process sequentially in
the same environment, for instance to run a setenv.bat
script and followed by a java app.  The reason for this is
to set up the system environment variables in a script
(using Runtime.exec() passing environment array is not an
option).

I have tried the following

Runtime.exec("setenv.bat && java ...");
Runtime.exec("cmd.exe /K setenv.bat && java ...");
Runtime.exec("run.bat");  // Where run.bat is "setenv.bat &&
java ..."

I have a thread waiting on the return process with
Process.waitFor.  In my main thread, I call
Process.destroy().  Process.waitFor() returns with exit code
1, but the java process continues.

If I kill the java process externally, Process.waitFor()
will also return.

I can not find any workaround for this, and it seriously
limits my ability to launch external applications.


STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Prepare by creating the batch files specified in the source
section and modifying the path to the batch files both
within the batch files and in the java source file.

1. java ExecTest
The system.out should stop after 5 seconds.

2. java ExecTest and
The system.out will continue after the destroy.

3. java ExecTest script
The system.out will continue after the destroy.


EXPECTED VERSUS ACTUAL BEHAVIOR :
The child processes should be killed, just as if you had hit
control-c.  Instead, it continues (as evidenced by the
printing to the command line).  However, Process.waitFor
DOES return.  I can find no way to successfully kill the
child processes.


REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
// FILE: foo.bat
rem This just makes sure the batch file returns 0 exit code
// END OF FILE: foo.bat

// FILE: test.bat - contains a few alternatives, none of which work
rem Application is not properly killed in any of these cases:

rem Option 1
c:\foo.bat && java com.bluemartini.dev.ExecTestWait

rem Option 2
rem java com.bluemartini.dev.ExecTestWait

rem Option 3
rem call c:\foo.bat
rem java com.bluemartini.dev.ExecTestWait
// END OF FILE: test.bat

// FILE: ExecTest.java
import java.util.*;
import java.io.*;

public class ExecTest {
    private static final int RUN_JAVA = 0;
    private static final int RUN_SCRIPT = 1;
    private static final int RUN_AND = 2;

    private static int state = RUN_JAVA;

    public static void main(String[] args) throws Exception {
        if (args.length > 0) {
            String sState = args[0];
            if (sState.equals("and")) {
                state = RUN_AND;
            } else if (sState.equals("script")) {
                state = RUN_SCRIPT;
            } else if (sState.equals("java")) {
                state = RUN_JAVA;
            }
        }
        new ExecTest();
    }

    public ExecTest() throws Exception {
        // This behaves properly
        Process p;
        if (state == RUN_SCRIPT) {
            // ExecTestWait continues to run after destroy called
            p = Runtime.getRuntime().exec("c:\\test.bat");
        } else if (state == RUN_AND) {
            // ExecTestWait continues to run after destroy called
            p = Runtime.getRuntime().exec("c:\\foo.bat && java
com.bluemartini.dev.ExecTestWait");
        } else {
            // This behaves properly
            p = Runtime.getRuntime().exec("java com.bluemartini.dev.ExecTestWait");
        }

        ProcessWatcher pw = new ProcessWatcher(p) {
            public void outPrint(String s) {
                System.out.println("[" + Thread.currentThread().getName() +
":out] " + s);
            }
            public void errPrint(String s) {
                System.out.println("[" + Thread.currentThread().getName() +
":err] " + s);
            }

            public void processTerminated(int code) {
                System.out.println("[" + Thread.currentThread().getName() + "]
Process ended");
            }
        };

        Thread.sleep(5000);
        System.out.println("Killing process");
        p.destroy();
    }

    public interface TailPipeListener {
        void tailPipeOutput(String s);
    }

    public class ProcessWatcher extends Thread {

        Process proc_;
        Thread outputThread_;
        Thread errorThread_;
        boolean bInterrupt_ = false;

        TailPipe tailPipeOut_;
        TailPipe tailPipeErr_;

        public ProcessWatcher(Process p) {
            proc_ = p;

            tailPipeOut_ = new TailPipe(proc_, false);
            tailPipeOut_.addListener(new TailPipeListener() {
                public void tailPipeOutput(String s) {
                    outPrint(s);
                }
            });

            tailPipeErr_ = new TailPipe(proc_, true);
            tailPipeErr_.addListener(new TailPipeListener() {
                public void tailPipeOutput(String s) {
                    errPrint(s);
                }
            });

            setName("ProcessWatcher");
            start();
        }

        public void run() {
            try {
                tailPipeOut_.follow(200);
                tailPipeErr_.follow(200);
                int exitValue = proc_.waitFor();
                processTerminated(exitValue);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
//                bInterrupt_ = true;
//                tailPipeOut_.stopFollow();
//                tailPipeErr_.stopFollow();
            }
        }

        protected void processTerminated(int code) {
        }

        protected void outPrint(String s) {
        }

        protected void errPrint(String s) {
        }
    }

    public class TailPipe {
        private Vector vListeners_ = new Vector();
        private boolean bFollowing_ = false;
        private PipeThread followThread_;

        File file_;
        Process process_;
        boolean bErrorStream_;

        public TailPipe(File file) {
            file_ = file;
        }

        public TailPipe(Process process, boolean errorStream) {
            process_ = process;
            bErrorStream_ = errorStream;
        }

    //    public void tail(int lines) {
    //    }

    //    public void follow(int lines, int latency) {
    //        bFollowing_ = true;
    //        PipeThread thread = new PipeThread(latency);
    //        thread.start();
    //    }

        public void follow(int latency) throws IOException {
            try {
                followThread_ = new PipeThread(latency);
            } catch (Exception e) {
                if (e instanceof IOException) {
                    throw (IOException)e;
                } else {
                    e.printStackTrace();
                    throw new IOException(e.getMessage());
                }
            }
            bFollowing_ = true;
            followThread_.start();
        }

        public void stopFollow() {
            bFollowing_ = false;
            followThread_.interrupt(); // ### Do I really have to do this?
        }

        private class PipeThread extends Thread {
            int latency_ = -1;
            BufferedReader pipeIn_;
            char[] buf_ = new char[32768];

            public PipeThread(int latency) throws Exception {
                latency_ = latency;
                if (file_ != null) {
                    pipeIn_ = new BufferedReader(new FileReader(file_));
                } else if (process_ != null) {
                    if (bErrorStream_) {
                        pipeIn_ = new BufferedReader(new
InputStreamReader(process_.getErrorStream()));
                    } else {
                        pipeIn_ = new BufferedReader(new
InputStreamReader(process_.getInputStream()));
                    }
                }
            }
            public void run() {
                int charsRead;
                while (bFollowing_) {
                    try {
                        charsRead = pipeIn_.read(buf_);
                        fireTailPipeOutput(new String(buf_, 0, charsRead));
                    } catch(Exception e) {
                        try {
                            pipeIn_.close();
                        } catch (IOException ioe) {
                            ioe.printStackTrace();
                        }
                        bFollowing_ = false;
                    }
                    try {
                        Thread.sleep(latency_);
                    } catch (InterruptedException e) {
                    }
                }
                try {
                    pipeIn_.close();
                } catch (IOException ioe) {
                }
            }
        }

        private void fireTailPipeOutput(String s) {
            Enumeration enum = vListeners_.elements();
            while (enum.hasMoreElements()) {
                TailPipeListener ln = (TailPipeListener)enum.nextElement();
                ln.tailPipeOutput(s);
            }
        }

        public void addListener(TailPipeListener lnr) {
            vListeners_.addElement(lnr);
        }
    }
}
// END OF FILE: ExecTest.java

// FILE: ExecTestWait.java
public class ExecTestWait {
    public static void main(String[] args) throws Exception {
        System.out.println("Sleep process started ...");
        while(true) {
            System.out.println("sleeping");
            Thread.sleep(1000);
            Thread.yield();
        }
    }
}
// END OF FILE: ExecTestWait.java

---------- END SOURCE ----------

CUSTOMER WORKAROUND :
None found.
(Review ID: 165865) 
======================================================================

Comments
WORK AROUND For the particular problem addressed by the submitter, setting environment variables before running an external process, the new in 1.5.0 ProcessBuilder API is a perfect match.
27-09-2005

EVALUATION The following excellent SDN comment explains what's going on: --------------------------------------------------------------------- The fundamental problem here is that, unlike Unix, Windows does that maintain parent-child relationships between processes. A process can kill its own immediate children, but unless you make other arrangements to obtain the information, can't kill any 'grand-children' because it has no way of finding them. Ctrl-C types at a command prompt is just a character that the command processor interprets and not a signal sent from outside. When you 'destroy' a child command script, that process does not get the opportunity to terminate any child processes it may know about. Recent versions of WIndows (2000 or later) do provide a "Job" concept which acts as a container for processes. Killing a Job does terminate all processes associated with that job. However Jobs do not contain other jobs, so fully emulating the Unix behaviour is probably impossible. --------------------------------------------------------------------- Note that Unix emulation environments on Windows, like Cygwin, suffer from the same problem. Any fix would be difficult. Even if we could figure out how to fix this, we might choose not to do so for the usual reason -- compatibility.
27-09-2005