JMadKernelImpl.java
// @formatter:off
/*******************************************************************************
* This file is part of JMad. Copyright (c) 2008-2011, CERN. All rights reserved. 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.
******************************************************************************/
// @formatter:on
package cern.accsoft.steering.jmad.kernel;
import static com.google.common.base.Preconditions.checkState;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import cern.accsoft.steering.jmad.JMadException;
import cern.accsoft.steering.jmad.bin.MadxBin;
import cern.accsoft.steering.jmad.domain.result.Result;
import cern.accsoft.steering.jmad.domain.result.ResultType;
import cern.accsoft.steering.jmad.io.DynapOutputParser;
import cern.accsoft.steering.jmad.io.MatchOutputParser;
import cern.accsoft.steering.jmad.io.StrengthFileParser;
import cern.accsoft.steering.jmad.io.TfsFileParser;
import cern.accsoft.steering.jmad.io.TrackOutputParser;
import cern.accsoft.steering.jmad.util.FileMonitor;
import cern.accsoft.steering.jmad.util.FileMonitor.ProcessTerminatedUnexpectedlyException;
import cern.accsoft.steering.jmad.util.FileUtil;
import cern.accsoft.steering.jmad.util.JMadPreferences;
import cern.accsoft.steering.jmad.util.ProcTools;
import cern.accsoft.steering.jmad.util.ProcessTerminationMonitor;
import cern.accsoft.steering.jmad.util.StringUtil;
import cern.accsoft.steering.jmad.util.TempFileUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* this is the implementation of the {@link JMadKernel} which controls one MadX-Process.
*
* @author Kajetan Fuchsberger (kajetan.fuchsberger at cern.ch)
*/
public class JMadKernelImpl implements JMadKernel, JMadKernelConfig {
private static final int MAX_REPORTED_OUTPUT_LINES = 10;
private static final int MAX_REPORTED_ERROR_LINES = 10;
/**
* the logger for the class
*/
private static final Logger LOGGER = LoggerFactory.getLogger(JMadKernelImpl.class);
/**
* This value is returned by the waitUntilReady - method. It is only used internally.
*/
private static final int EXIT_VALUE_DESTROYED = -9999;
/**
* the command which is used to stop madx.
*/
private static final String CMD_STOP = "stop;";
/*
* various file names which are used to communicate with MadX and for logging
*/
private static final String FILENAME_READY = "madx-ready.out";
private static final String FILENAME_RESULT = "madx-result.out";
private static final String FILENAME_LOG_IN = "madx-input.log";
private static final String FILENAME_LOG_OUT = "madx-output.log";
private static final String FILENAME_LOG_ERROR = "madx-error.log";
private File readyFile = null;
private File resultFile = null;
private File madxInputLogFile = null;
private File madxOutputLogFile = null;
private File madxErrorLogFile = null;
/**
* wait this amount of ms, when deletion failed and retry
*/
private static final int RETRY_DELAY_IN_MILLISECONDS = 100;
/**
* how often to retry deleting a file before throwing an exception?
*/
private static final int RETRY_ATTEMPTS = 3;
/**
* the (optional) timeout in ms which the kernel waits until Madx finishes the command/task. If the timeout is set
* to null, then the kernel waits forever. Null is the default.
*/
private Long timeout = null;
/**
* the process for madx
*/
private Process process = null;
/**
* the stream for providing madx with input
*/
private PrintWriter input = null;
/**
* the logger, where the inputs to madx are logged
*/
private BufferedWriter inputLogWriter = null;
/**
* The preferences to be injected
*/
private JMadPreferences preferences;
/**
* the file util to be injected
*/
private TempFileUtil fileUtil;
/**
* the class which takes care of the madx-binary
*/
private MadxBin madxBin;
/**
* does not delete the outputFile after executing a command/task. Keeping the output-file is especially useful for
* debugging.
*/
private boolean keepOutputFile = true;
/**
* if true, then the temp dirs are deleted when cleaning up, if false they are kept. If this is null (default) then
* the settings are taken from the preferences.
*/
private Boolean cleanupDirs = null;
/**
* the listeners to the kernel
*/
private final List<JMadKernelListener> listeners = new ArrayList<>();
private final ExecutorService logFileWriteExecutor = Executors.newCachedThreadPool();
@Override
public void start() throws JMadException {
checkState(fileUtil != null, "fileUtil not injected. Fix Spring configuration");
checkState(madxBin != null, "madxBin not injected. Fix Spring configuration");
/*
* First we have to get temp-files related to this kernel
*/
readyFile = fileUtil.getOutputFile(this, FILENAME_READY);
resultFile = fileUtil.getOutputFile(this, FILENAME_RESULT);
madxInputLogFile = fileUtil.getOutputFile(this, FILENAME_LOG_IN);
madxOutputLogFile = fileUtil.getOutputFile(this, FILENAME_LOG_OUT);
madxErrorLogFile = fileUtil.getOutputFile(this, FILENAME_LOG_ERROR);
deleteReadyFile();
madxInputLogFile.delete();
try {
process = madxBin.execute();
input = new PrintWriter(process.getOutputStream());
inputLogWriter = new BufferedWriter(new FileWriter(madxInputLogFile));
logFileWriteExecutor.submit(() -> Files.copy(process.getInputStream(), madxOutputLogFile.toPath()));
logFileWriteExecutor.submit(() -> Files.copy(process.getErrorStream(), madxErrorLogFile.toPath()));
fireStartedKernel();
} catch (IOException e) {
throw new JMadException("Error while executing madx.", e);
}
}
@Override
public int stop() throws JMadException {
int exitValue = 0;
writeCommand(CMD_STOP);
try {
if (timeout == null) {
LOGGER.debug("No timeout set. Waiting until madx-process terminates.");
exitValue = process.waitFor();
} else {
ProcessTerminationMonitor processTerminationMonitor = new ProcessTerminationMonitor(process);
processTerminationMonitor.start();
processTerminationMonitor.join(timeout);
if (processTerminationMonitor.isAlive()) {
processTerminationMonitor.interrupt();
process.destroy();
exitValue = EXIT_VALUE_DESTROYED;
LOGGER.warn("Waiting for terminating madx timed out! (timeout={} ms)", timeout);
} else {
exitValue = process.exitValue();
}
}
if (exitValue == 0) {
LOGGER.debug("madx terminated correctly with exit-value {}.", exitValue);
} else if (exitValue == EXIT_VALUE_DESTROYED) {
LOGGER.debug("Tried to destroy madx-process. -> set exitValue to {}", exitValue);
} else {
LOGGER.warn("madx terminated with exit-value {}.", exitValue);
}
deleteReadyFile();
closeInputLogger();
} catch (InterruptedException e) {
throw new JMadException("Error while trying to stop MadX", e);
}
/* delete the dir corresponding to the kernel. */
if (isCleanupDirs() && (fileUtil != null)) {
fileUtil.cleanup(this);
}
fireStoppedKernel();
return exitValue;
}
private void deleteReadyFile() {
readyFile.delete();
}
private void closeInputLogger() {
try {
inputLogWriter.flush();
inputLogWriter.close();
} catch (IOException e) {
LOGGER.error("Error while flushing input logger.", e);
}
}
@Override
public Result execute(JMadExecutable executable) throws JMadException {
resultFile.delete();
executable.setOutputFile(resultFile);
/* execute the commands and wait. */
writeCommand(executable.compose());
waitUntilReady();
/* parse result */
Result result = null;
if ((executable.getResultType() != null) && (ResultType.NO_RESULT != executable.getResultType())) {
LOGGER.debug("parsing madx output-file ({})", resultFile.getAbsolutePath());
try {
if (ResultType.TFS_RESULT == executable.getResultType()) {
TfsFileParser parser = new TfsFileParser(resultFile);
parser.parse();
result = parser.getResult();
} else if (ResultType.VALUES_RESULT == executable.getResultType()) {
StrengthFileParser parser = new StrengthFileParser(resultFile);
parser.parse(false);
result = parser.getResult();
} else if (ResultType.MATCH_RESULT == executable.getResultType()) {
MatchOutputParser parser = new MatchOutputParser(executable.getOutputFile());
parser.parse();
result = parser.getResult();
} else if (ResultType.TRACK_RESULT == executable.getResultType()) {
TrackOutputParser parser = new TrackOutputParser(executable.getOutputFile());
parser.parse();
result = parser.getResult();
} else if (ResultType.DYNAP_RESULT == executable.getResultType()) {
DynapOutputParser parser = new DynapOutputParser(executable.getOutputFile());
parser.parse();
result = parser.getResult();
}
} catch (Exception e) {
throw new JMadException("File '" + resultFile.getAbsolutePath() + "' could not be parsed."
+ "\nProbably madx did not produce it?" + "\n\n" + madxOutputMessage());
}
if (!this.keepOutputFile) {
if (!resultFile.delete()) {
throw new JMadException("Could not delete result file '" + resultFile.getAbsolutePath());
}
LOGGER.debug("deleted madx output file ({})", resultFile.getAbsolutePath());
}
}
return result;
}
/**
* writes the command(s) as String to MadX-input. This method does not wait for the end of the execution and does
* not return any result. Use with care!
*
* @param command the command to be executed by madx
* @throws JMadException
*/
/* package visibility for testing! */
void writeCommand(String command) throws JMadException { // NOPMD by kaifox on 6/25/10 4:01 PM
String commandString = command + "\n";
if (!isMadxRunning()) {
throw new JMadException("MadX is not running -> cannot write commands.");
}
LOGGER.debug("writing command(s) to madx:\n{}", commandString);
input.println(commandString);
input.flush();
/* also log in separate file for simple executing in madx */
try {
inputLogWriter.write(commandString);
inputLogWriter.flush();
} catch (IOException e) {
LOGGER.warn("Error while logging commands!", e);
}
}
@Override
public boolean isMadxRunning() {
return ProcTools.isRunning(process);
}
/**
* writes a file through madx and waits until it exists or reaching timeout (if set).
*
* @throws JMadException
* @see #getTimeout()
* @see #setTimeout(Long)
*/
/* package visibility for testing! */
void waitUntilReady() throws JMadException {
if (!isMadxRunning()) {
throw new JMadException("MadX is not running!");
}
writeCommand("\nsystem, \"echo > " + readyFile.getAbsolutePath() + "\"; // wait until ready\n");
/* wait for the file, which tells us, that madx finished */
FileMonitor fileMonitor = new FileMonitor(readyFile, process);
boolean fileCreated = false;
try {
fileCreated = fileMonitor.waitForFile(timeout);
} catch (ProcessTerminatedUnexpectedlyException e) {
closeInputLogger();
throwTerminatedException(e);
}
if (!fileCreated) {
throw new WaitForMadxTimedOutException("madx command timed out! (timeout=" + timeout + "ms).");
}
deleteReadyFileWithRetries();
}
private void throwTerminatedException(ProcessTerminatedUnexpectedlyException e) throws MadxTerminatedException {
throw new MadxTerminatedException("Madx terminated unexpectedly.\n\n" + madxOutputMessage(), e);
}
private String madxOutputMessage() {
return fileSnippet("output", madxOutputLogFile, MAX_REPORTED_OUTPUT_LINES) //
+ fileSnippet("error output", madxErrorLogFile, MAX_REPORTED_ERROR_LINES) //
+ "\nFull MadX Input Log: '" + madxInputLogFile.getAbsolutePath() + "'" //
+ "\nFull MadX Output Log: '" + madxOutputLogFile.getAbsolutePath() + "'" //
+ "\nFull MadX Error Log: '" + madxErrorLogFile.getAbsolutePath() + "'\n";
}
private String fileSnippet(String fileQualifier, File file, int maxLines) {
List<String> lastMadxErrorLines = FileUtil.tail(file, maxLines);
return "MadX " + fileQualifier + "(Max last " + maxLines + " lines):\n---\n'" + StringUtil
.join(lastMadxErrorLines, "\n") + "'.\n---\n";
}
private void deleteReadyFileWithRetries() throws JMadException {
if (!readyFile.delete()) {
boolean deleted = false;
for (int i = 0; i < RETRY_ATTEMPTS; i++) {
LOGGER.debug("deletion of file '" + readyFile.getAbsolutePath() + "' failed. Retrying again in "
+ RETRY_DELAY_IN_MILLISECONDS + "ms");
try {
Thread.sleep(RETRY_DELAY_IN_MILLISECONDS);
} catch (InterruptedException e) {
LOGGER.error("Error while waiting for retry ...", e);
}
if (readyFile.delete()) {
deleted = true;
break;
}
}
if (!deleted) {
throw new JMadException("error while deleting file '" + readyFile.getAbsolutePath() + "'");
}
}
}
/**
* When the class is destroyed it takes care, that MadX is closed in a proper way.
*
* @throws Throwable if the finalization fails
*/
@Override
protected void finalize() throws Throwable {
if (isMadxRunning()) {
LOGGER.warn("Madx is still running! - trying to stop...");
try {
stop();
} catch (JMadException e) {
LOGGER.error("Error while trying to stop MadX.", e);
}
}
super.finalize();
}
@Override
public void addListener(JMadKernelListener listener) {
this.listeners.add(listener);
}
@Override
public void removeListener(JMadKernelListener listener) {
this.listeners.remove(listener);
}
/**
* notifies the listeners, that the kernel has started
*/
private void fireStartedKernel() {
for (JMadKernelListener listener : this.listeners) {
listener.startedKernel(this.process);
}
}
/**
* notifies all listeners, that the kernel has stopped.
*/
private void fireStoppedKernel() {
for (JMadKernelListener listener : this.listeners) {
listener.stoppedKernel();
}
}
//
// Methods which allow special configuration
//
/**
* @return the actual set timeout
*/
@Override
public Long getTimeout() {
return timeout;
}
/**
* sets the actual timeout in ms. If null then the kernel waits forever for Madx.
*
* @param timeout the timeout to set
*/
@Override
public void setTimeout(Long timeout) {
this.timeout = timeout;
}
/**
* sets the flag, if the output-file shall be kept or deleted after task/command execution.
*
* @param keepOutputFile true if the output-file shall be kept, false otherwise
*/
@Override
public void setKeepOutputFile(boolean keepOutputFile) {
this.keepOutputFile = keepOutputFile;
}
/**
* @return true, if the outputfile shall be kept or false if it shall be deleted after command/task execution.
*/
@Override
public boolean isKeepOutputFile() {
return keepOutputFile;
}
@Override
public void setCleanupDirs(boolean cleanupDirs) {
this.cleanupDirs = cleanupDirs;
}
@Override
public boolean isCleanupDirs() {
if (this.cleanupDirs != null) {
return cleanupDirs;
}
return preferences.isCleanupKernelFiles();
}
@Override
public File getOutputFile() {
return this.resultFile;
}
public void setPreferences(JMadPreferences preferences) {
this.preferences = preferences;
}
public void setFileUtil(TempFileUtil fileUtil) {
this.fileUtil = fileUtil;
}
public void setMadxBin(MadxBin madxBin) {
this.madxBin = madxBin;
}
}