/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.ignite.internal.util; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import org.apache.ignite.IgniteLogger; import org.apache.ignite.internal.util.lang.GridAbsClosure; import org.apache.ignite.internal.util.typedef.internal.U; import org.apache.ignite.lang.IgniteInClosure; import org.jetbrains.annotations.Nullable; /** * Wrapper class around the {@link Process} suited to run any Java class as separate java process. * <p> * This launcher supports simple interchange-with-a-process protocol to talk (in fact, listen) to process. * For the moment the only message in protocol is run process PID. Class-to-run should print it's PID * prefixed with {@code #PID_MSG_PREFIX} to tell the GridJavaProcess it's PID. * <p> * Protocol transport is any of (or both) <i>system.out</i> and/or <i>system.err</i>, * so any protocol message should be printed in the class-to-run. * <p> * NOTE 1: For the moment inner class running is not supported. * <p> * NOTE 2: This util class should work fine on Linux, Mac OS and Windows. */ public final class GridJavaProcess { /** Internal protocol message prefix saying that the next text in the outputted line is pid. */ public static final String PID_MSG_PREFIX = "my_pid_is:"; /** Logger */ private IgniteLogger log; /** Wrapped system process. */ private Process proc; /** Pid of wrapped process. Made as array to be changeable in nested static class. */ private volatile String pid = "-1"; /** system.out stream grabber for process in which user class is running. */ private ProcessStreamGrabber osGrabber; /** system.err stream grabber for process in which user class is running. */ private ProcessStreamGrabber esGrabber; /** Closure to be called when process termination is detected. */ private GridAbsClosure procKilledC; /** * Private constructor to promote factory method usage. */ private GridJavaProcess() { // No-op } /** * Executes main() method of the given class in a separate system process. * * @param cls Class with main() method to be run. * @param params main() method parameters. * @param printC Optional closure to be called each time wrapped process prints line to system.out or system.err. * @param procKilledC Optional closure to be called when process termination is detected. * @param log Log to use. * @return Wrapper around {@link Process} * @throws Exception If any problem occurred. */ public static GridJavaProcess exec(Class cls, String params, @Nullable IgniteLogger log, @Nullable IgniteInClosure<String> printC, @Nullable GridAbsClosure procKilledC) throws Exception { return exec(cls.getCanonicalName(), params, log, printC, procKilledC, null, null, null); } /** * Executes main() method of the given class in a separate system process. * * @param cls Class with main() method to be run. * @param params main() method parameters. * @param printC Optional closure to be called each time wrapped process prints line to system.out or system.err. * @param procKilledC Optional closure to be called when process termination is detected. * @param log Log to use. * @param jvmArgs JVM arguments to use. * @param cp Additional classpath. * @return Wrapper around {@link Process} * @throws Exception If any problem occurred. */ public static GridJavaProcess exec(Class cls, String params, @Nullable IgniteLogger log, @Nullable IgniteInClosure<String> printC, @Nullable GridAbsClosure procKilledC, @Nullable Collection<String> jvmArgs, @Nullable String cp) throws Exception { return exec(cls.getCanonicalName(), params, log, printC, procKilledC, null, jvmArgs, cp); } /** * Executes main() method of the given class in a separate system process. * * @param clsName Class with main() method to be run. * @param params main() method parameters. * @param log Log to use. * @param printC Optional closure to be called each time wrapped process prints line to system.out or system.err. * @param procKilledC Optional closure to be called when process termination is detected. * @param javaHome Java home location. The process will be started under given JVM. * @param jvmArgs JVM arguments to use. * @param cp Additional classpath. * @return Wrapper around {@link Process} * @throws Exception If any problem occurred. */ public static GridJavaProcess exec(String clsName, String params, @Nullable IgniteLogger log, @Nullable IgniteInClosure<String> printC, @Nullable GridAbsClosure procKilledC, @Nullable String javaHome, @Nullable Collection<String> jvmArgs, @Nullable String cp) throws Exception { GridJavaProcess gjProc = new GridJavaProcess(); gjProc.log = log; gjProc.procKilledC = procKilledC; List<String> procParams = params == null || params.isEmpty() ? Collections.<String>emptyList() : Arrays.asList(params.split(" ")); List<String> procCommands = new ArrayList<>(); String javaBin = (javaHome == null ? System.getProperty("java.home") : javaHome) + File.separator + "bin" + File.separator + "java"; procCommands.add(javaBin); procCommands.addAll(jvmArgs == null ? U.jvmArgs() : jvmArgs); if (jvmArgs == null || (!jvmArgs.contains("-cp") && !jvmArgs.contains("-classpath"))) { String classpath = System.getProperty("java.class.path"); String sfcp = System.getProperty("surefire.test.class.path"); if (sfcp != null) classpath += System.getProperty("path.separator") + sfcp; if (cp != null) classpath += System.getProperty("path.separator") + cp; procCommands.add("-cp"); procCommands.add(classpath); } procCommands.add(clsName); procCommands.addAll(procParams); ProcessBuilder builder = new ProcessBuilder(procCommands); builder.redirectErrorStream(true); Process proc = builder.start(); gjProc.osGrabber = gjProc.new ProcessStreamGrabber(proc.getInputStream(), printC); gjProc.esGrabber = gjProc.new ProcessStreamGrabber(proc.getErrorStream(), printC); gjProc.osGrabber.start(); gjProc.esGrabber.start(); gjProc.proc = proc; return gjProc; } /** * Kills the java process. * * @throws Exception If any problem occurred. */ public void kill() throws Exception { Process killProc = U.isWindows() ? Runtime.getRuntime().exec(new String[] {"taskkill", "/pid", pid, "/f", "/t"}) : Runtime.getRuntime().exec(new String[] {"kill", "-9", pid}); killProc.waitFor(); int exitVal = killProc.exitValue(); if (exitVal != 0) log.info(String.format("Abnormal exit value of %s for pid %s", exitVal, pid)); if (procKilledC != null) procKilledC.apply(); U.interrupt(osGrabber); U.interrupt(esGrabber); U.join(osGrabber, log); U.join(esGrabber, log); } /** * Kills process using {@link Process#destroy()}. */ public void killProcess() { proc.destroy(); if (procKilledC != null) procKilledC.apply(); U.interrupt(osGrabber); U.interrupt(esGrabber); U.join(osGrabber, log); U.join(esGrabber, log); } /** * Returns pid of the java process. * Wrapped java class should print it's PID to system.out or system.err to make wrapper know about it. * * @return Pid of the java process or -1 if pid is unknown. */ public int getPid() { return Integer.valueOf(pid); } /** * Exposes wrapped java Process. * * @return Wrapped java process. */ public Process getProcess() { return proc; } /** * Class which grabs sys.err and sys.out of the running process in separate thread * and implements the interchange-with-a-process protocol. */ private class ProcessStreamGrabber extends Thread { /** Stream to grab. */ private final InputStream streamToGrab; /** Closure to be called when process termination is detected. */ private final IgniteInClosure<String> printC; /** * Creates the ProcessStreamGrabber bounded to the given Process. * * @param streamToGrab Stream to grab. * @param printC Optional closure to be called each time wrapped process prints line to system.out or system.err. */ ProcessStreamGrabber(InputStream streamToGrab, @Nullable IgniteInClosure<String> printC) { this.streamToGrab = streamToGrab; this.printC = printC; } /** * Starts the ProcessStreamGrabber. */ @Override public void run() { try { BufferedReader br = new BufferedReader(new InputStreamReader(streamToGrab)); String line; while ((line = br.readLine()) != null && !isInterrupted()) { if (line.startsWith(PID_MSG_PREFIX)) pid = line.substring(PID_MSG_PREFIX.length()); else if (printC != null) printC.apply(line); } } catch (IOException e) { U.error(log, "Caught IOException while grabbing stream", e); try { // Check if process is still alive. proc.exitValue(); if (procKilledC != null) procKilledC.apply(); } catch (IllegalThreadStateException e1) { if (!interrupted()) U.error(log, "Failed to get exit value from process.", e1); } } } /** * Interrupts a thread and closes process streams. */ @Override public void interrupt() { super.interrupt(); // Close all Process streams to free allocated resources, see http://kylecartmell.com/?p=9. U.closeQuiet(proc.getErrorStream()); U.closeQuiet(proc.getInputStream()); U.closeQuiet(proc.getOutputStream()); } } }