All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.rapidoid.process.ProcessHandle Maven / Gradle / Ivy

There is a newer version: 5.5.5
Show newest version
package org.rapidoid.process;

import org.rapidoid.annotation.Authors;
import org.rapidoid.annotation.Since;
import org.rapidoid.collection.Coll;
import org.rapidoid.commons.Arr;
import org.rapidoid.commons.RapidoidInfo;
import org.rapidoid.group.AbstractManageable;
import org.rapidoid.group.ManageableBean;
import org.rapidoid.lambda.Lmbd;
import org.rapidoid.lambda.Operation;
import org.rapidoid.log.GlobalCfg;
import org.rapidoid.log.Log;
import org.rapidoid.log.LogLevel;
import org.rapidoid.u.U;
import org.rapidoid.util.Msc;
import org.rapidoid.util.SlidingWindowList;
import org.rapidoid.util.Wait;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.*;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CancellationException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/*
 * #%L
 * rapidoid-commons
 * %%
 * Copyright (C) 2014 - 2017 Nikolche Mihajlovski and contributors
 * %%
 * 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.
 * #L%
 */

@Authors("Nikolche Mihajlovski")
@Since("5.3.0")
@ManageableBean(kind = "processes")
public class ProcessHandle extends AbstractManageable {

	private final static Set ALL = Coll.synchronizedSet();

	private final static ProcessCrawlerThread CRAWLER = new ProcessCrawlerThread(ALL);

	private final ProcessParams params;

	private final String id;

	private final BlockingQueue input = new ArrayBlockingQueue<>(100);
	private final BlockingQueue output = null;
	private final BlockingQueue error = null;

	private final List outBuffer;
	private final List errBuffer;
	private final List outAndErrBuffer;

	private final AtomicBoolean doneReadingOut = new AtomicBoolean();
	private final AtomicBoolean doneReadingErr = new AtomicBoolean();

	private final int terminationTimeout;

	private volatile Process process;

	private volatile Date startedAt;
	private volatile Date finishedAt;

	static {
		Runtime.getRuntime().addShutdownHook(new Thread() {
			public void run() {
				terminateProcesses();
			}
		});
	}

	private static void terminateProcesses() {
		for (ProcessHandle proc : Coll.copyOf(ALL)) {
			proc.terminate();
		}
	}

	ProcessHandle(ProcessParams params) {
		this.params = params;
		this.id = params.id() != null ? params.id() : UUID.randomUUID().toString();

		this.outBuffer = Collections.synchronizedList(new SlidingWindowList(params.maxLogLines()));
		this.errBuffer = Collections.synchronizedList(new SlidingWindowList(params.maxLogLines()));
		this.outAndErrBuffer = Collections.synchronizedList(new SlidingWindowList(params.maxLogLines()));

		this.terminationTimeout = params.terminationTimeout();

		// keep reference to the handle, used by the crawler internally
		ALL.add(this);

		// register to the process group, if configured
		if (params.group() != null) {
			params.group().add(this);
		}

		setupIO();
	}

	private void setupIO() {
		Thread inputProcessor = new ProcessIOThread(this) {
			@Override
			void doIO() {
				writeAll(input, process.getOutputStream());
			}
		};

		inputProcessor.setDaemon(true);
		inputProcessor.start();

		Thread errorProcessor = new ProcessIOThread(this) {
			@Override
			void doIO() {
				try {
					BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
					readInto(reader, error, errBuffer, outAndErrBuffer);
				} finally {
					doneReadingErr.set(true);
				}
			}
		};

		errorProcessor.setDaemon(true);
		errorProcessor.start();

		Thread outputProcessor = new ProcessIOThread(this) {
			@Override
			void doIO() {
				try {
					BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
					readInto(reader, output, outBuffer, outAndErrBuffer);
				} finally {
					doneReadingOut.set(true);
				}
			}
		};

		outputProcessor.setDaemon(true);
		outputProcessor.start();
	}

	private static void writeAll(BlockingQueue input, OutputStream output) {
		while (!Thread.interrupted()) {
			try {
				Object obj = input.take();

				if (obj instanceof String) {
					String s = (String) obj;
					output.write(s.getBytes());

				} else if (obj instanceof byte[]) {
					byte[] b = (byte[]) obj;
					output.write(b);

				} else {
					throw U.rte("Unsupported input object type: " + obj);
				}

				output.flush();
			} catch (Exception e) {
				Log.error("Cannot write!", e);
			}
		}
	}

	private long readInto(BufferedReader reader, BlockingQueue dest, List... buffers) {
		long total = 0;

		try {
			String line;
			while ((line = reader.readLine()) != null) {
				try {
					if (dest != null) {
						dest.put(line);
					}

					if (params.printingOutput()) {
						U.print(params.linePrefix() + line);
					}

					for (List buffer : buffers) {
						buffer.add(line);
					}

					total++;
				} catch (InterruptedException e) {
					throw new CancellationException();
				}
			}
		} catch (IOException e) {
			// can't read anymore (e.g. the stream was closed)
		}

		return total;
	}

	public synchronized BlockingQueue input() {
		return input;
	}

	public synchronized BlockingQueue output() {
		return output;
	}

	public synchronized BlockingQueue error() {
		return error;
	}

	public synchronized Process process() {
		return process;
	}

	public synchronized ProcessParams params() {
		return params;
	}

	public synchronized boolean isAlive() {
		return process != null && exitCode() == null;
	}

	public void receive(Operation outputProcessor, Operation errorProcessor) {
		String s;

		int grace = 1;

		do {
			if (outputProcessor != null) {
				while ((s = output().poll()) != null) {
					Lmbd.call(outputProcessor, s);
				}
			}

			if (errorProcessor != null) {
				while ((s = error().poll()) != null) {
					Lmbd.call(errorProcessor, s);
				}
			}

			U.sleep(10);

		} while (isAlive() || !doneReadingOut.get() || !doneReadingErr.get() || (--grace) >= 0);
	}

	public void print() {
		for (String line : outAndError()) {
			U.print(line);
		}
	}

	public void log(LogLevel level) {
		for (String line : outAndError()) {
			Log.log("PROCESS", level, line);
		}
	}

	public List out() {
		return outBuffer;
	}

	public List err() {
		return errBuffer;
	}

	public List outAndError() {
		return outAndErrBuffer;
	}

	synchronized void startProcess(ProcessParams params) {

		Log.info("Starting process", "command", params.command());

		ProcessBuilder builder = new ProcessBuilder().command(params.command());

		if (params.in() != null) {
			builder.directory(params.in());
		}

		removeRapidoidConfig(builder.environment());

		addExtraEnvInfo(builder.environment());

		Date startingAt = new Date();

		Process process;
		try {
			process = builder.start();
		} catch (IOException e) {
			throw U.rte("Cannot start process: " + U.join(" ", params.command()));
		}

		this.startedAt = startingAt;
		this.finishedAt = null;
		this.doneReadingErr.set(false);
		this.doneReadingOut.set(false);

		attach(process);

		synchronized (CRAWLER) {
			if (CRAWLER.getState() == Thread.State.NEW) CRAWLER.start();
		}
	}

	private void addExtraEnvInfo(Map env) {
		env.put(GlobalCfg.MANAGED_BY, RapidoidInfo.nameAndInfo());
	}

	private void removeRapidoidConfig(Map env) {
		Iterator> it = env.entrySet().iterator();
		while (it.hasNext()) {

			Map.Entry e = it.next();
			String key = e.getKey().toUpperCase();

			if (key.startsWith("RAPIDOID_") || key.startsWith("RAPIDOID.")) {
				it.remove();
			}
		}
	}

	private void attach(Process process) {
		this.process = process;
	}

	private synchronized Process requireProcess() {
		U.must(process != null, "The handle must have a process attached!");
		return process;
	}

	public ProcessHandle waitFor() {
		try {
			requireProcess().waitFor();
		} catch (InterruptedException e) {
			throw new CancellationException();
		}

		Wait.until(doneReadingOut);
		Wait.until(doneReadingErr);

		return this;
	}

	public ProcessHandle waitFor(long timeout, TimeUnit unit) {
		try {
			requireProcess().waitFor(timeout, unit);
		} catch (InterruptedException e) {
			throw new CancellationException();
		}

		// FIXME timeout
		Wait.until(doneReadingOut);
		Wait.until(doneReadingErr);

		return this;
	}

	public ProcessHandle destroy() {
		if (process != null) process.destroy();
		return this;
	}

	public ProcessHandle destroyForcibly() {
		if (process != null) process.destroyForcibly();
		return this;
	}

	public synchronized String cmd() {
		return params.command()[0];
	}

	public synchronized String[] args() {
		return Arr.sub(params.command(), 1, params().command().length);
	}

	public synchronized Integer exitCode() {
		try {
			return process != null ? process.exitValue() : null;
		} catch (IllegalThreadStateException e) {
			return null;
		}
	}

	public synchronized long duration() {
		if (this.startedAt == null) return 0;

		Date until = this.finishedAt;
		if (until == null) until = new Date();

		return until.getTime() - this.startedAt.getTime();
	}

	synchronized void onTerminated() {
		finishedAt = new Date();
	}

	public synchronized Date startedAt() {
		return startedAt;
	}

	public synchronized Date finishedAt() {
		return finishedAt;
	}

	@Override
	public synchronized String id() {
		return id;
	}

	@Override
	public synchronized List getManageableActions() {
		List actions = U.list("?Restart");

		if (isAlive()) {
			actions.add("!Terminate");
		}

		return actions;
	}

	public synchronized Processes group() {
		return params.group();
	}

	public synchronized ProcessHandle restart() {
		terminate();

		startProcess(params);

		return this;
	}

	public synchronized ProcessHandle terminate() {
		destroy();

		long t = U.time();
		while (isAlive()) {
			U.sleep(1);

			if (Msc.timedOut(t, terminationTimeout)) {
				destroyForcibly();
				break;
			}
		}

		t = U.time();
		while (isAlive()) {
			U.sleep(1);

			if (Msc.timedOut(t, terminationTimeout)) {
				throw U.rte("Couldn't terminate the process!");
			}
		}

		Log.info("Terminated process", "id", id());
		return this;
	}

}