/**
* Copyright 2015 StreamSets Inc.
*
* Licensed under 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 com.streamsets.datacollector.util;
import com.google.common.base.Joiner;
import com.google.common.collect.EvictingQueue;
import com.google.common.collect.ImmutableList;
import com.streamsets.pipeline.api.impl.Utils;
import com.streamsets.pipeline.lib.util.ThreadUtil;
import com.streamsets.pipeline.util.SystemProcess;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
public class SystemProcessImpl implements SystemProcess {
private static final Logger LOG = LoggerFactory.getLogger(SystemProcessImpl.class);
private static final AtomicLong fileCounter = new AtomicLong(0);
private static final int OUT_FILE_LIMIT = 50;
static final String OUT_EXT = ".out";
static final String ERR_EXT = ".err";
private static final Method DESTROY_FORCIBLY;
static {
Method destroyForcibly;
try {
destroyForcibly = Process.class.getDeclaredMethod("destroyForcibly");
} catch (NoSuchMethodException e) {
destroyForcibly = null;
}
DESTROY_FORCIBLY = destroyForcibly;
}
protected ImmutableList<String> args;
private final File tempDir;
private final File input = new File("/dev/null");
private final File output;
private final File error;
private SimpleFileTailer outputTailer;
private SimpleFileTailer errorTailer;
private Process delegate;
SystemProcessImpl(String name, File tempDir, File logDir) {
String id = nextId();
this.tempDir = tempDir;
output = new File(logDir, Utils.format("{}-{}{}", name, id, OUT_EXT));
error = new File(logDir, Utils.format("{}-{}{}", name, id, ERR_EXT));
}
public SystemProcessImpl(String name, File tempDir, List<String> args) {
clean(tempDir, OUT_FILE_LIMIT);
String id = nextId();
output = new File(tempDir, Utils.format("{}-{}{}", name, id, OUT_EXT));
error = new File(tempDir, Utils.format("{}-{}{}", name, id, ERR_EXT));
this.tempDir = tempDir;
this.args = ImmutableList.copyOf(args);
}
/**
* @return a unique number which shorts in descending order
*/
private static String nextId() {
NumberFormat numberFormat = NumberFormat.getInstance();
numberFormat.setMinimumIntegerDigits(10);
numberFormat.setGroupingUsed(false);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss");
return Utils.format("{}-{}", dateFormat.format(new Date()), numberFormat.format(fileCounter.incrementAndGet()));
}
static void clean(File tempDir, int limit) {
String[] files = tempDir.list((dir, name) -> name.endsWith(OUT_EXT) || name.endsWith(ERR_EXT));
if (files != null && files.length > limit) {
List<String> fileList = new ArrayList<>(files.length);
fileList.addAll(Arrays.asList(files));
Collections.sort(fileList);
while (fileList.size() > limit) {
File file = new File(tempDir, fileList.remove(0));
if (!FileUtils.deleteQuietly(file)) {
LOG.warn("Could not delete: {}", file);
}
}
}
}
@Override
public void start() throws IOException {
start(new HashMap<String, String>());
}
@Override
public void start(Map<String, String> env) throws IOException {
Utils.checkState(output.createNewFile(), Utils.formatL("Could not create output file: {}", output));
Utils.checkState(error.createNewFile(), Utils.formatL("Could not create error file: {}", error));
Utils.checkState(delegate == null, "start can only be called once");
LOG.info("Standard output for process written to file: " + output);
LOG.info("Standard error for process written to file: " + error);
ProcessBuilder processBuilder = new ProcessBuilder()
.redirectInput(input)
.redirectOutput(output)
.redirectError(error)
.directory(tempDir).command(args);
processBuilder.environment().putAll(env);
LOG.info("Starting: " + args);
delegate = processBuilder.start();
ThreadUtil.sleep(100); // let it start
outputTailer = new SimpleFileTailer(output);
errorTailer = new SimpleFileTailer(error);
}
@Override
public String getCommand() {
return Joiner.on(" ").join(args);
}
@Override
public boolean isAlive() {
return delegate != null && isAlive(delegate);
}
@Override
public void cleanup() {
if (outputTailer != null) {
outputTailer.close();
}
if (errorTailer != null) {
errorTailer.close();
}
kill(5000);
}
@Override
public Collection<String> getAllOutput() {
if (outputTailer != null) {
return outputTailer.getAllData();
}
return new ArrayList<>();
}
@Override
public Collection<String> getAllError() {
if (errorTailer != null) {
return errorTailer.getAllData();
}
return new ArrayList<>();
}
@Override
public Collection<String> getOutput() {
if (outputTailer != null) {
return outputTailer.getData();
}
return new ArrayList<>();
}
@Override
public Collection<String> getError() {
if (errorTailer != null) {
return errorTailer.getData();
}
return new ArrayList<>();
}
@Override
public void kill(long timeoutBeforeForceKill) {
if (outputTailer != null) {
outputTailer.close();
}
if (errorTailer != null) {
errorTailer.close();
}
if (delegate != null && isAlive(delegate)) {
delegate.destroy();
long start = System.currentTimeMillis();
while (isAlive(delegate) && (System.currentTimeMillis() - start) > timeoutBeforeForceKill) {
if (!ThreadUtil.sleep(100)) {
break;
}
}
if (isAlive(delegate)) {
if (DESTROY_FORCIBLY != null) {
try {
DESTROY_FORCIBLY.invoke(delegate);
} catch (Exception e) {
LOG.error("Error trying to call destroyForcibly on {}: {}", delegate, e, e);
}
}
}
}
}
@Override
public String toString() {
return Utils.format("SystemProcess: {} ", Joiner.on(" ").join(args));
}
@Override
public int exitValue() {
return delegate.exitValue();
}
@Override
public boolean waitFor(long timeout, TimeUnit unit) {
return waitFor(delegate, timeout, unit);
}
/**
* Java 1.7 does not have Process.isAlive
*/
private static boolean isAlive(Process process) {
try {
process.exitValue();
return false;
} catch(IllegalThreadStateException e) {
return true;
}
}
/**
* Java 1.7 does not have Process.waitFor(timeout)
*/
private static boolean waitFor(Process process, long timeout, TimeUnit unit) {
long startTime = System.nanoTime();
long rem = unit.toNanos(timeout);
do {
try {
process.exitValue();
return true;
} catch(IllegalThreadStateException ex) {
if (rem > 0)
ThreadUtil.sleep(
Math.min(TimeUnit.NANOSECONDS.toMillis(rem) + 1, 100));
}
rem = unit.toNanos(timeout) - (System.nanoTime() - startTime);
} while (rem > 0);
return false;
}
private static class SimpleFileTailer {
private final File file;
private final EvictingQueue<String> history;
private final RandomAccessFile randomAccessFile;
private final byte[] inbuf;
public SimpleFileTailer(File file) {
this.file = file;
this.history = EvictingQueue.create(2500);
this.inbuf = new byte[8192 * 8];
try {
this.randomAccessFile = new RandomAccessFile(file, "r");
} catch (FileNotFoundException e) {
throw new RuntimeException(Utils.format("Unexpected error reading output file '{}': {}", file, e), e);
}
}
public void close() {
IOUtils.closeQuietly(randomAccessFile);
}
public List<String> getData() {
List<String> result = new ArrayList<>();
try {
readLines(randomAccessFile, result);
} catch (IOException e) {
throw new RuntimeException(Utils.format("Error reading from '{}': {}", file, e), e);
}
history.addAll(result);
return result;
}
public Collection<String> getAllData() {
EvictingQueue<String> result = EvictingQueue.create(2500);
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(file));
String line;
while ((line = reader.readLine()) != null) {
result.add(line);
}
} catch (IOException e) {
String msg = Utils.format("Error reading from command output file '{}': {}", file, e);
throw new RuntimeException(msg, e);
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException ex) {
// ignored
}
}
}
return result;
}
/**
* Read new lines.
*
* @param reader The file to read
* @return The new position after the lines have been read
* @throws java.io.IOException if an I/O error occurs.
*/
private long readLines(final RandomAccessFile reader, List<String> result) throws IOException {
ByteArrayOutputStream lineBuf = new ByteArrayOutputStream(64);
long pos = reader.getFilePointer();
long rePos = pos; // position to re-read
int num;
boolean seenCR = false;
while (((num = reader.read(inbuf)) != -1)) {
for (int i = 0; i < num; i++) {
final byte ch = inbuf[i];
switch (ch) {
case '\n':
seenCR = false; // swallow CR before LF
result.add(new String(lineBuf.toByteArray(), StandardCharsets.UTF_8));
lineBuf.reset();
rePos = pos + i + 1;
break;
case '\r':
if (seenCR) {
lineBuf.write('\r');
}
seenCR = true;
break;
default:
if (seenCR) {
seenCR = false; // swallow final CR
result.add(new String(lineBuf.toByteArray(), StandardCharsets.UTF_8));
lineBuf.reset();
rePos = pos + i + 1;
}
lineBuf.write(ch);
}
}
pos = reader.getFilePointer();
}
IOUtils.closeQuietly(lineBuf); // not strictly necessary
reader.seek(rePos); // Ensure we can re-read if necessary
return rePos;
}
}
}