Java tutorial
/* * Copyright (C) 2017 Google 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.google.gapid.server; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import java.io.BufferedReader; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.logging.Logger; /** * Manages the invocation and bookkeeping of a child process. */ public abstract class ChildProcess<T> { private static final Logger LOG = Logger.getLogger(ChildProcess.class.getName()); protected final String name; private Thread serverThread; protected Process process; ChildProcess(String name) { this.name = name; } protected abstract Exception prepare(ProcessBuilder pb); public boolean isRunning() { return serverThread != null && serverThread.isAlive(); } public ListenableFuture<T> start() { final ProcessBuilder pb = new ProcessBuilder(); // Use the base directory as the working directory for the server. pb.directory(GapiPaths.base()); Exception prepareError = prepare(pb); if (prepareError != null) { return Futures.immediateFailedFuture(prepareError); } final SettableFuture<T> result = SettableFuture.create(); serverThread = new Thread(ChildProcess.class.getName() + "-" + name) { @Override public void run() { runProcess(result, pb); } }; serverThread.start(); return result; } protected void runProcess(final SettableFuture<T> result, final ProcessBuilder pb) { try { // This will throw IOException if the executable is not found. LOG.log(INFO, "Starting " + name + " as " + pb.command()); process = pb.start(); } catch (IOException e) { LOG.log(WARNING, "IO Error running process", e); result.setException(e); return; } int exitCode = -1; try (OutputHandler<T> stdout = createStdoutHandler(); OutputHandler<T> stderr = createStderrHandler()) { stdout.start(process.getInputStream(), result); stderr.start(process.getErrorStream(), result); exitCode = process.waitFor(); stderr.join(); stdout.join(); stderr.finish(result); stdout.finish(result); if (!result.isDone()) { result.setException(new Exception(name + " has exited")); } } catch (InterruptedException e) { LOG.log(INFO, "Killing " + name); result.setException(e); process.destroy(); } finally { onExit(exitCode); } } protected void onExit(int code) { if (code != 0) { LOG.log(WARNING, "The " + name + " process exited with a non-zero exit value: " + code); } else { LOG.log(INFO, name + " exited cleanly"); } shutdown(); } protected OutputHandler<T> createStdoutHandler() { return new LoggingStringHandler<T>(LOG, name, false, null); } protected OutputHandler<T> createStderrHandler() { return new LoggingStringHandler<T>(LOG, name, true, null); } public void shutdown() { LOG.log(INFO, "Shutting down " + name); serverThread.interrupt(); } /** * Handler for the child process' standard and error output. The handler is responsible for * producing the process result returned as a future from {@link ChildProcess#start()}. This is * typically done by parsing the output produces by the process. */ protected static abstract class OutputHandler<T> implements Closeable { private Thread thread; protected abstract void run(InputStream in, SettableFuture<T> result); public void start(InputStream in, SettableFuture<T> result) { close(); thread = new Thread(() -> run(in, result), getClass().getName()); thread.start(); } @SuppressWarnings("unused") public void finish(SettableFuture<T> result) throws InterruptedException { // Do nothing by default. } public void join() throws InterruptedException { if (thread != null) { thread.join(5000); } } @Override public void close() { if (thread != null) { thread.interrupt(); thread = null; } } } /** * String line based {@link ChildProcess.OutputHandler}. */ protected static class StringHandler<T> extends OutputHandler<T> { private final Parser<T> parser; public static interface Parser<T> { public T parse(String line) throws IOException; } public StringHandler(Parser<T> parser) { this.parser = parser; } @Override public void run(InputStream in, SettableFuture<T> result) { try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8))) { for (String line; (line = reader.readLine()) != null;) { T object = parser.parse(line); if (object != null) { result.set(object); } } } catch (IOException e) { result.setException(e); } } } /** * {@link ChildProcess.OutputHandler} that forward the output to the log. */ protected static class LoggingStringHandler<T> extends StringHandler<T> { public LoggingStringHandler(Logger logger, String name, boolean warn, Parser<T> parser) { super(line -> { if (warn) { logger.log(WARNING, name + ": " + line); } else { logger.log(INFO, name + ": " + line); } if (parser != null) { return parser.parse(line); } return null; }); } } /** * {@link ChildProcess.OutputHandler} for binary (non text) output. */ protected static class BinaryHandler<T> extends OutputHandler<T> { private final Parser<T> parser; public static interface Parser<T> { public T parse(InputStream in) throws IOException; } public BinaryHandler(Parser<T> parser) { this.parser = parser; } @Override public void run(InputStream in, SettableFuture<T> result) { try { T object = parser.parse(in); if (object != null) { result.set(object); } } catch (IOException e) { result.setException(e); } } } }