/*
* Copyright 2011 Matthias van der Vlies
*
* 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 core;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.persistence.Transient;
import models.Application;
import play.Logger;
import play.Play;
import play.jobs.Every;
import play.jobs.Job;
/**
* Process management for all spawned subprocesses.
*/
@Every("10s")
public class ProcessManager extends Job {
public static final String PROCESS_START_POSTFIX = "-start";
/**
* Process type
*/
public enum ProcessType {
PLAY, // a Play! framework application
COMMAND // a normal command e.g. git
}
/**
* Map of spawned subprocesses
*/
private static Map<String, Process> processes = new HashMap<String, Process>();
private static List<String> keptPids = new LinkedList<String>();
/**
* Execute a new subprocess from a given path
* @param pid Program ID
* @param command The command to execute
* @param workingPath The path to execute the command from (may be null)
*/
public static synchronized Process executeProcess(final String pid, final String command, File workingPath, boolean keepPid) throws Exception {
synchronized (processes) {
// we don't allow multiple pids running at the same time
if(processes.containsKey(pid)) {
throw new Exception("pid: " + pid + " already in use");
}
final Process process = Runtime.getRuntime().exec(command, null, workingPath);
storePid(pid, keepPid, process);
return process;
}
}
private static void storePid(final String pid, boolean keepPid,
final Process process) {
if(keepPid) {
Logger.info("Stored pid %s in keep", pid);
keptPids.add(pid);
}
processes.put(pid, process);
}
// number of loops to wait for process to change status
public static final int MAXIMUM_WAIT_TIME = 60;
@Override
public void doJob() throws Exception {
manageList();
final List<Application> applications = Application.all().fetch();
// check not running applications that should be running
for(final Application application : applications) {
final boolean isRunning = isProcessRunning(application.pid + "/" + (application.subfolder == null ? "" : application.subfolder), ProcessType.PLAY);
if(application.enabled && application.checkedOut && !isRunning) {
application.start(false, false);
}
else if(!application.enabled && isRunning) {
final String pid = application.pid + PROCESS_START_POSTFIX;
if(!processes.containsKey(pid) && !keptPids.contains(pid)) {
Logger.info("It appears %s (PID: %s) is running while it should not", application.pid, pid);
// there is no process currently booting so kill the Play! instance
application.stop();
}
}
}
}
/**
* Get full path to the Play! binary
*/
@Transient
public static String getFullPlayPath() {
final String path = Play.configuration.getProperty("path.play");
// return setting from application.conf or assume command is on the instance's path
return path == null || path.isEmpty() ? "play" : path;
}
public static String executeCommand(final String pid,
final String command, final StringBuffer output, final boolean keepPid) throws Exception {
return executeCommand(pid, command, output, true, null, keepPid);
}
public static String executeCommand(final String pid,
final String command, final StringBuffer output, final File workingPath, final boolean keepPid) throws Exception {
return executeCommand(pid, command, output, true, workingPath, keepPid);
}
/**
* Execute a command
* @param pid The program ID
* @param command The command to execute
* @param log Log to logger?
* @param workingPath Path to execute the command from
* @param Keep pid in process map for manual removal?
*/
public static synchronized String executeCommand(final String pid,
final String command, final StringBuffer output, boolean log, final File workingPath, final boolean keepPid) throws Exception {
if(log) {
Logger.info("Running command %s (PID: %s)", command, pid);
}
final Process process = executeProcess(pid, command, workingPath, keepPid);
final BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
final BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
boolean hasErrors = false;
// asynchronous waiting here
while(isProcessRunning(pid, ProcessType.COMMAND)) {
hasErrors = readCommandOutput(output, log, reader, errorReader,
hasErrors);
Thread.sleep(10);
}
// Run log interceptors once more to flush all buffers
hasErrors = readCommandOutput(output, log, reader, errorReader,
hasErrors);
if(log) {
Logger.info("Process: %s has stopped with exit value: %s keep: %s", pid, process.exitValue(), keepPid);
}
reader.close();
errorReader.close();
if(!keepPid) {
// remove pid
synchronized (processes) {
processes.remove(pid);
}
}
if (process.exitValue() != 0 || hasErrors) {
throw new Exception("command failed, exit value: " + process.exitValue());
}
return output.toString();
}
private static boolean readCommandOutput(final StringBuffer output,
boolean log, final BufferedReader reader,
final BufferedReader errorReader, boolean hasErrors)
throws IOException {
readCommandOutput(log, reader, output, false);
// check if the command produced error output
if(readCommandOutput(log, errorReader, output, true)) {
hasErrors = true;
}
return hasErrors;
}
/**
* Read stdout and stderr
* @param log Log to Play! logger?
* @param reader Used for reading from the process
* @param output Output buffer to store output in
*/
private static boolean readCommandOutput(boolean log,
final BufferedReader reader, final StringBuffer output, boolean error)
throws IOException {
boolean hasErrors = false;
// only fetch data if there is any!
if(!reader.ready()) {
return false;
}
String line = reader.readLine();
while (line != null) {
if(log) {
if(error) {
Logger.error("%s", line);
hasErrors = true;
}
else {
Logger.info("%s", line);
}
}
output.append(line + "\n");
line = reader.readLine();
}
return hasErrors;
}
/**
* Manage the list of spawned subprocesses
*/
private static void manageList() {
/* pids to remove */
final List<String> pids = new LinkedList<String>();
for(final Entry<String, Process> entry : processes.entrySet()) {
try {
final Process process = entry.getValue();
final String pid = entry.getKey();
final int status = process.exitValue();
if(!keptPids.contains(pid)) {
Logger.debug("Process with pid %s (%s) is not running anymore, removing from process list.", pid, status);
// not in kept pids list, so remove it
pids.add(pid);
}
}
catch(IllegalThreadStateException e) {
// still running! so ignore
}
}
synchronized (processes) {
// remove all pids that have stopped
for(final String pid : pids) {
processes.remove(pid);
}
}
}
/**
* Remove a kept pid from the process list
*/
public static void removeKeptPid(final String pid) {
synchronized (processes) {
processes.remove(pid);
}
synchronized (keptPids) {
keptPids.remove(pid);
}
Logger.info("Removed kept pid: %s", pid);
}
public static boolean isKeptPidAvailable(final String pid) {
synchronized (keptPids) {
return keptPids.contains(pid);
}
}
/**
* Check whether a process is running
* @param pid The program ID
* @param type The application type
*/
public static boolean isProcessRunning(final String pid, final ProcessType type) throws Exception {
if(type == ProcessType.COMMAND) {
final Process process = processes.get(pid);
if(process != null) {
try {
process.exitValue(); // throws IllegalThreadStateException when task is still running
return false;
}
catch(IllegalThreadStateException e) {
return true;
}
}
else {
return false;
}
}
else if(type == ProcessType.PLAY) {
try {
// If the container was killed, we are still able to re-attach to the still running "childs"
executeCommand(pid + "-check-" + System.currentTimeMillis(), getFullPlayPath() + " pid .",
new StringBuffer(), false,
new File("apps/" + pid), false);
return true;
} catch (Exception e) {
return false;
}
}
else {
throw new Exception("Unhandeld process type: " + type);
}
}
}