/*
* Copyright (c) 2013-2017 Cinchapi Inc.
*
* Licensed 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 com.cinchapi.concourse.server.io.process;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;
import com.cinchapi.common.process.ProcessTerminationListener;
import com.cinchapi.common.process.ProcessWatcher;
import com.cinchapi.concourse.server.io.FileSystem;
import com.cinchapi.concourse.util.Platform;
import com.cinchapi.concourse.util.Processes;
import com.cinchapi.concourse.util.TLists;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
/**
* An {@link JavaApp} takes a dynamic set of instructions, written as a formal
* Java class with a main method, and runs them in a separate {@link Process}.
*
* @author Jeff Nelson
*/
public class JavaApp extends Process {
/**
* Return the injected source code that goes into the main method of the
* JavaApp's source to help detect when the host process has terminated.
*
* @return the injected source code
*/
private static String getHostWatcherCodeInjectionMainBlock() {
// @formatter:off
return ""
+ " try {\n"
+ " watchThreadStarted.await();\n"
+ " }\n"
+ " catch(InterruptedException e) {\n"
+ " throw new RuntimeException(e);\n"
+ " }\n";
// @formatter:on
}
/**
* Return the injected source code that goes into a static block immediately
* after the main class declaration to help detect when the host process has
* terminated.
*
* @param pid the pid of the host process to watch for termination
* @return the injected source code
*/
private static String getHostWatcherCodeInjectionStaticBlock(String pid) {
// @formatter:off
return ""
+ "static java.util.concurrent.CountDownLatch watchThreadStarted = new java.util.concurrent.CountDownLatch(1);\n"
+ "static {\n"
+ " Thread main = Thread.currentThread();"
+ " Thread t = new Thread(new Runnable() {\n"
+ " @Override\n"
+ " public void run() {\n"
+ " "+ProcessWatcher.class.getName()+" watcher = new "+ProcessWatcher.class.getName()+"();\n"
+ " java.util.concurrent.atomic.AtomicBoolean terminated = new java.util.concurrent.atomic.AtomicBoolean(false);\n"
+ " watcher.watch(\""+pid+"\", new "+ProcessTerminationListener.class.getName()+"() {\n"
+ " @Override\n"
+ " public void onTermination() {\n"
+ " terminated.set(true);\n"
+ " }\n"
+ " });\n"
+ " watchThreadStarted.countDown();\n"
+ " while(!terminated.get() && main.isAlive()) {\n"
+ " Thread.yield();\n"
+ " continue;\n"
+ " }"
+ " System.exit(0);\n"
+ " }\n"
+ " });\n"
+ " t.setDaemon(true);\n"
+ " t.start();\n"
+ "}";
// @formatter:on
}
/**
* Given the input {@code source}, inject the code necessary to watch the
* host process for termination.
*
* @param source the input source
* @return the source after additional code has been injected
*/
private static String injectHostWatcherCode(String source) {
String pid = Processes.getCurrentPid();
StringBuilder builder = new StringBuilder(source);
// First, inject a static block of code right after the class
// declaration
int index = builder.indexOf("{");
builder.insert(index + 1,
"\n" + getHostWatcherCodeInjectionStaticBlock(pid));
// Inject a block of code at the beginning of the main method to use a
// ProcessWatcher to detect when the host process terminates
index = builder.indexOf("{", builder.indexOf("static void main"));
builder.insert(index + 1, System.lineSeparator()
+ getHostWatcherCodeInjectionMainBlock());
return builder.toString();
}
/**
* Inject the necessary jars onto the input classpath to ensure that the
* host watcher code can compile.
*
* @param classpath the input classpath
* @return the updated classpath with additional jars
*/
private static String injectHostWatcherJarsOntoClasspath(String classpath) {
final File f = new File(ProcessWatcher.class.getProtectionDomain()
.getCodeSource().getLocation().getPath());
return classpath += CLASSPATH_SEPARATOR + f.getAbsolutePath();
}
/**
* Make sure that the {@code source} has all the necessary components and
* return the name of the main class.
*
* @param source the source code
* @return the name of the main class
*/
private static String validateSource(String source) {
Preconditions.checkArgument(source.contains("static void main(String"),
"Source must contain a main method");
String[] toks = source.split("public class");
Preconditions.checkArgument(toks.length >= 2,
"Source must define a top-level public class");
String clazz = toks[1].trim().split(" ")[0].trim();
return clazz;
}
/**
* The separator character to use between different elements on the
* classpath.
*/
public static final String CLASSPATH_SEPARATOR = Platform.isWindows() ? ";"
: ":";
/**
* The amount of time between each check to determine if the app shutdown
* prematurely.
*/
@VisibleForTesting
protected static int PREMATURE_SHUTDOWN_CHECK_INTERVAL_IN_MILLIS = 3000;
/**
* The classpath to use when launching the external JVM process.
*/
private final String classpath;
/**
* A flag that indicates whether the app has been compiled.
*/
private boolean compiled = false;
/**
* A reflective reference to a field that my container information about
* whether {@link #process} has exited.
*/
private Field hasExited = null;
/**
* The absolute path to the java binary that is used to launch the JVM.
*/
private final String javaBinary;
/**
* The name of the top-level class that contains the main method. This is
* extracted from the source in the {@link #validateSource(String)} method.
*/
private final String mainClass;
/**
* Options to pass to the JVM.
*/
private final String[] options;
/**
* The underlying process that controls the app.
*/
private Process process;
/**
* The system dependent separator.
*/
private final String separator;
/**
* The absolute path to the location where the source is stored on disk.
*/
private final String sourceFile;
/**
* A scheduled executor to watch for premature shutdowns, if
* {@link #onPrematureShutdown(PrematureShutdownHandler)} is configured.
*/
private ScheduledExecutorService watcher;
/**
* The temporary directory where the source is saved and compiled and the
* java process is launched.
*/
private final String workspace;
/**
* Construct a new instance.
*
* @param source
*/
public JavaApp(String source) {
this(System.getProperty("java.class.path"), source);
}
/**
* Construct a new instance.
*
* @param classpath
* @param source
* @param options
*/
public JavaApp(String classpath, String source, ArrayList<String> options) {
this(classpath, source,
(String[]) options.toArray(new String[options.size()]));
}
/**
* Construct a new instance.
*
* @param classpath
* @param source
* @param options
*/
public JavaApp(String classpath, String source, String... options) {
source = injectHostWatcherCode(source);
classpath = injectHostWatcherJarsOntoClasspath(classpath);
this.mainClass = validateSource(source);
this.classpath = classpath;
this.separator = System.getProperty("file.separator");
this.javaBinary = System.getProperty("java.home") + separator + "bin"
+ separator + "java";
this.workspace = Files.createTempDir().getAbsolutePath();
this.options = options;
// Save the source to a temporary file
this.sourceFile = workspace + separator + mainClass + ".java";
FileSystem.write(source, sourceFile);
// Add Shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
JavaApp.this.destroy();
}
}));
}
/**
* Attempt to compile the app and throw an {@link Exception} if an error
* occurs.
*/
public void compile() {
if(!compiled) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
int exit;
if(classpath.length() > 0) {
StandardJavaFileManager fileManager = compiler
.getStandardFileManager(null, null, null);
List<File> cpList = Lists.newArrayList();
String[] parts = classpath.split(CLASSPATH_SEPARATOR);
for (String part : parts) {
cpList.add(new File(part));
}
try {
fileManager.setLocation(StandardLocation.CLASS_PATH,
cpList);
}
catch (IOException e) {
throw Throwables.propagate(e);
}
Iterable<? extends JavaFileObject> compilationUnits = fileManager
.getJavaFileObjectsFromStrings(
Arrays.asList(sourceFile));
CompilationTask task = compiler.getTask(null, fileManager, null,
Lists.newArrayList("-classpath", classpath), null,
compilationUnits);
exit = task.call() ? 0 : 1;
}
else {
exit = compiler.run(null, null, null, sourceFile);
}
if(exit == 0) {
compiled = true;
}
else {
throw new RuntimeException("Could not compile source");
}
}
}
@Override
public void destroy() {
if(watcher != null) {
watcher.shutdownNow();
}
if(process != null) {
process.destroy();
}
}
@Override
public int exitValue() {
return process.exitValue();
}
@Override
public InputStream getErrorStream() {
return process.getErrorStream();
}
@Override
public InputStream getInputStream() {
return process.getInputStream();
}
@Override
public OutputStream getOutputStream() {
return process.getOutputStream();
}
/**
* Return {@code true} if the app is running.
*
* @return {@code true} of the app is running
*/
public boolean isRunning() {
if(process != null) {
if(hasExited != null) {
try {
return !hasExited.getBoolean(process);
}
catch (ReflectiveOperationException e) {
throw Throwables.propagate(e);
}
}
else {
try {
process.exitValue();
return false;
}
catch (IllegalThreadStateException e) {
// Hate using an exception for control flow, but Java gives
// us no choice in the matter here :-/
return true;
}
}
}
return false;
}
/**
* Submit a {@link PrematureShutdownHandler task} to be executed whenever
* this process prematurely shuts down.
*
* @param handler the task to execute
*/
public void onPrematureShutdown(final PrematureShutdownHandler handler) {
watcher = Executors.newSingleThreadScheduledExecutor();
watcher.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
if(!isRunning()) {
handler.run(process.getInputStream(),
process.getErrorStream());
destroy();
}
}
}, PREMATURE_SHUTDOWN_CHECK_INTERVAL_IN_MILLIS,
PREMATURE_SHUTDOWN_CHECK_INTERVAL_IN_MILLIS,
TimeUnit.MILLISECONDS);
}
/**
* Run the app. Use the standard {@link Process} methods to do things like
* {@link #waitFor() waiting for} the app the finish, getting the
* {@link #exitValue() exit code} and getting the contents of the
* {@link #getInputStream() output} and {@link #getErrorStream() error}
* streams.
*/
public void run() {
compile();
List<String> args = Lists.newArrayList(javaBinary, "-cp",
classpath + ":.");
for (String option : options) {
args.add(option);
}
args.add(mainClass);
ProcessBuilder builder = new ProcessBuilder(
TLists.toArrayCasted(args, String.class));
builder.directory(new File(workspace));
try {
process = builder.start();
try {
hasExited = process.getClass().getDeclaredField("hasExited");
hasExited.setAccessible(true);
}
catch (ReflectiveOperationException e) {}
}
catch (IOException e) {
throw Throwables.propagate(e);
}
}
/**
* Attempt to compile the app. Return {@code true} if successful and
* {@code false} otherwise.
*
* @return {@code true} if the app successfully compiles
*/
public boolean tryCompile() {
try {
compile();
return true;
}
catch (RuntimeException e) {
return false;
}
}
@Override
public int waitFor() throws InterruptedException {
return process.waitFor();
}
}