/*******************************************************************************
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* 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 hr.fer.zemris.vhdllab.service.impl;
import hr.fer.zemris.vhdllab.applets.editor.newtb.enums.TimeScale;
import hr.fer.zemris.vhdllab.applets.editor.newtb.exceptions.UniformTestbenchParserException;
import hr.fer.zemris.vhdllab.applets.editor.newtb.model.Testbench;
import hr.fer.zemris.vhdllab.applets.editor.newtb.model.TestbenchParser;
import hr.fer.zemris.vhdllab.entity.File;
import hr.fer.zemris.vhdllab.entity.FileType;
import hr.fer.zemris.vhdllab.service.Simulator;
import hr.fer.zemris.vhdllab.service.WorkspaceService;
import hr.fer.zemris.vhdllab.service.exception.CompilationException;
import hr.fer.zemris.vhdllab.service.exception.NoAvailableProcessException;
import hr.fer.zemris.vhdllab.service.exception.SimulationException;
import hr.fer.zemris.vhdllab.service.exception.SimulatorTimeoutException;
import hr.fer.zemris.vhdllab.service.hierarchy.Hierarchy;
import hr.fer.zemris.vhdllab.service.hierarchy.HierarchyNode;
import hr.fer.zemris.vhdllab.service.result.CompilationMessage;
import hr.fer.zemris.vhdllab.service.result.Result;
import hr.fer.zemris.vhdllab.service.util.SecurityUtils;
import hr.fer.zemris.vhdllab.util.IOUtil;
import hr.fer.zemris.vhdllab.util.StringUtil;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteException;
import org.apache.commons.exec.ExecuteStreamHandler;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.exec.Executor;
import org.apache.commons.exec.PumpStreamHandler;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.UnhandledException;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
/**
* A wrapper for GHDL (http://ghdl.free.fr/).
*/
public class GhdlSimulator extends ServiceSupport implements Simulator {
/**
* Logger for this class
*/
private static final Logger LOG = Logger.getLogger(GhdlSimulator.class);
private static final int PROCESS_TIMEOUT = 2000;
private static final int MAX_SIMULTANEOUS_PROCESSES = 20;
private static final int ACQUIRE_TIMEOUT = 1;
private static final TimeUnit ACQUIRE_TIME_UNIT = TimeUnit.SECONDS;
private static final String PROP_COMPILATION_CMD = "compilation.cmd";
private static final String PROP_SIMULATION_CMD = "simulation.cmd";
private final Semaphore semaphore = new Semaphore(MAX_SIMULTANEOUS_PROCESSES);
private boolean leaveSimulationResults = true;
private Properties properties;
@Autowired
private WorkspaceService workspaceService;
private Properties getProperties() {
if (properties == null) {
properties = new Properties();
InputStream is = this.getClass().getClassLoader().getResourceAsStream("ghdl.properties");
try {
properties.load(is);
} catch (IOException e) {
throw new IllegalStateException(e);
}
IOUtils.closeQuietly(is);
}
return properties;
}
@Override
public List<CompilationMessage> compile(Integer fileId) throws CompilationException {
try {
if (!semaphore.tryAcquire(ACQUIRE_TIMEOUT, ACQUIRE_TIME_UNIT)) {
throw new NoAvailableProcessException(MAX_SIMULTANEOUS_PROCESSES);
}
} catch (InterruptedException e) {
throw new NoAvailableProcessException(e);
}
List<CompilationMessage> result;
try {
result = compileImpl(fileId);
} finally {
semaphore.release();
}
return result;
}
@Override
public Result simulate(Integer fileId) throws SimulationException {
try {
if (!semaphore.tryAcquire(ACQUIRE_TIMEOUT, ACQUIRE_TIME_UNIT)) {
throw new NoAvailableProcessException(MAX_SIMULTANEOUS_PROCESSES);
}
} catch (InterruptedException e) {
throw new NoAvailableProcessException(e);
}
Result result;
try {
result = simulateImpl(fileId);
} finally {
semaphore.release();
}
return result;
}
private List<CompilationMessage> compileImpl(Integer fileId) {
SimulationContext context = createContext(fileId);
context.compileOnly = true;
try {
doCompile(context);
} catch (IOException e) {
throw new CompilationException(e);
}
return listToCompilationMessages(context.result);
}
private Result simulateImpl(Integer fileId) {
SimulationContext context = createContext(fileId);
String waveform;
try {
doCompile(context);
prepairSimulationCommandLine(context);
context.result = executeProcess(context.commandLine, context.tempDirectory);
waveform = extractWaveform(context);
} catch (IOException e) {
throw new CompilationException(e);
} finally {
if (!leaveSimulationResults) {
FileUtils.deleteQuietly(context.tempDirectory);
}
}
return new Result(waveform, context.result);
}
@SuppressWarnings("synthetic-access")
private SimulationContext createContext(Integer fileId) {
SimulationContext context = new SimulationContext();
context.targetFile = loadFile(fileId);
return context;
}
private void doCompile(SimulationContext context) throws IOException {
try {
context.tempDirectory = createTempDirectory(context.targetFile.getId());
context.dependencies = orderFileNames(context.targetFile);
copyFiles(context.dependencies, context.targetFile.getProject().getId(), context.tempDirectory);
context.commandLine = prepairCompilationCommandLine(context.dependencies);
context.result = executeProcess(context.commandLine, context.tempDirectory);
} finally {
if (context.compileOnly) {
FileUtils.deleteQuietly(context.tempDirectory);
}
}
}
private List<String> orderFileNames(File file) {
Hierarchy hierarchy = workspaceService.extractHierarchy(file.getProject().getId());
List<File> ordered = new ArrayList<File>();
orderFileNames(ordered, hierarchy, hierarchy.getNode(file));
List<String> names = new ArrayList<String>(ordered.size());
for (File f : ordered) {
names.add(f.getName());
}
return names;
}
private void orderFileNames(List<File> ordered, Hierarchy hierarchy, HierarchyNode node) {
Set<String> missing = node.getMissingDependencies();
if (!missing.isEmpty()) {
String dep = missing.iterator().next();
throw new CompilationException(node.getFile().getName() + " uses " + dep + " however that circuit doesn't exist. Neither compilation nor simulation can be run.");
}
Set<HierarchyNode> dependencies = hierarchy.getDependenciesFor(node);
for (HierarchyNode n : dependencies) {
orderFileNames(ordered, hierarchy, n);
}
if (!ordered.contains(node.getFile())) {
ordered.add(node.getFile());
}
}
private java.io.File createTempDirectory(Integer fileId) throws IOException {
String suffix = "_by_user-" + SecurityUtils.getUser() + "__file_id-" + fileId;
java.io.File tempFile = java.io.File.createTempFile("ghd", suffix, getRootDir());
java.io.File tempDir = new java.io.File(tempFile.getParentFile(), "DIR" + tempFile.getName());
FileUtils.forceMkdir(tempDir);
FileUtils.deleteQuietly(tempFile);
LOG.debug("Created temp directory: " + tempDir.getPath());
return tempDir;
}
private java.io.File getRootDir() {
java.io.File root = null;
String tmpDir = getProperties().getProperty("tmp.dir");
if (tmpDir != null && !tmpDir.equals("${ghdl.tmp.dir}")) {
root = new java.io.File(tmpDir);
}
LOG.debug("Temp root dir resolved as: " + root);
return root;
}
private void copyFiles(List<String> dependencies, Integer projectId, java.io.File tempDirectory) throws IOException {
for (String dep : dependencies) {
File depFile = findProjectOrPredefinedFile(projectId, dep);
String data = depFile.getData();
if (!depFile.getType().equals(FileType.PREDEFINED)) {
data = metadataExtractor.generateVhdl(depFile).getData();
}
java.io.File fileOnDisk = new java.io.File(tempDirectory, dep + ".vhdl");
FileUtils.writeStringToFile(fileOnDisk, data, IOUtil.DEFAULT_ENCODING);
}
}
private CommandLine createCommandLine(String cmd) {
return CommandLine.parse(cmd);
}
private CommandLine prepairCompilationCommandLine(List<String> dependencies) {
String cmd = getProperties().getProperty(PROP_COMPILATION_CMD);
String[] files = new String[dependencies.size()];
for (int i = 0; i < dependencies.size(); i++) {
files[i] = dependencies.get(i) + ".vhdl";
}
cmd = cmd.replace("{files}", StringUtils.join(files, ' '));
return createCommandLine(cmd);
}
private void prepairSimulationCommandLine(SimulationContext context) {
String cmd = getProperties().getProperty(PROP_SIMULATION_CMD);
cmd = cmd.replace("{files}", context.targetFile.getName());
CommandLine cl = createCommandLine(cmd);
cl.addArgument("--vcd=simout.vcd");
Testbench tb = null;
try {
tb = TestbenchParser.parseXml(context.targetFile.getData());
} catch (UniformTestbenchParserException e) {
throw new SimulationException(e);
}
long simulationLength = tb.getSimulationLength();
if (simulationLength <= 0) {
simulationLength = 100;
} else {
simulationLength = simulationLength / TimeScale.getMultiplier(tb.getTimeScale());
simulationLength = (long) (simulationLength * 1.1);
}
cl.addArgument("--stop-time=" + simulationLength + tb.getTimeScale().toString().toLowerCase());
context.commandLine = cl;
}
private List<String> executeProcess(CommandLine cl, java.io.File tempDirectory) throws ExecuteException,
IOException {
if (LOG.isDebugEnabled()) {
LOG.debug("Executing process: " + cl.toString());
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ExecuteStreamHandler handler = new PumpStreamHandler(bos);
ExecuteWatchdog watchdog = new ExecuteWatchdog(PROCESS_TIMEOUT);
Executor executor = new DefaultExecutor();
executor.setWorkingDirectory(tempDirectory);
executor.setWatchdog(watchdog);
executor.setStreamHandler(handler);
/*
* It seems that when ExecuteWatchdog terminates process by invoking
* destroy method, process terminates with exit code 143. And since we
* manually ask watchdog if he killed the process, exit code 143 is
* marked as successful (just so our code can be executed).
*
* Exit code 1 in case of compilation error(s).
*/
executor.setExitValues(new int[] { 0, 1, 143 });
try {
executor.execute(cl);
} catch (ExecuteException e) {
LOG.warn("Process output dump:\n" + bos.toString());
throw e;
}
if (watchdog.killedProcess()) {
throw new SimulatorTimeoutException(PROCESS_TIMEOUT);
}
String output;
try {
output = bos.toString(IOUtil.DEFAULT_ENCODING);
} catch (UnsupportedEncodingException e) {
throw new UnhandledException(e);
}
if (StringUtils.isBlank(output)) {
return Collections.emptyList();
}
return Arrays.asList(StringUtil.splitToNewLines(output));
}
private List<CompilationMessage> listToCompilationMessages(List<String> errors) {
List<CompilationMessage> list = new ArrayList<CompilationMessage>(errors.size());
for (String e : errors) {
String[] line = parseGHDLErrorMessage(e);
if (line.length == 4) {
list
.add(new CompilationMessage(line[0], Integer.parseInt(line[1]), Integer.parseInt(line[2]),
line[3]));
} else {
list.add(new CompilationMessage(line[1]));
}
}
return list;
}
private String[] parseGHDLErrorMessage(String m) {
String msg = m;
String[] res = new String[] { msg, null, null, null };
for (int i = 0; i < 3; i++) {
int pos = msg.indexOf(':');
if (pos != -1) {
res[i] = msg.substring(0, pos);
msg = msg.substring(pos + 1);
res[i + 1] = msg;
} else {
return new String[] {
res[0],
(res[1] == null ? "" : res[1]) + (res[2] == null ? "" : ":" + res[2])
+ (res[3] == null ? "" : ":" + res[3]) };
}
}
if (res[0].toUpperCase().endsWith(".VHDL")) {
res[0] = res[0].substring(0, res[0].length() - 5);
} else if (res[0].toUpperCase().endsWith(".VHD")) {
res[0] = res[0].substring(0, res[0].length() - 4);
}
return res;
}
private String extractWaveform(SimulationContext context) throws IOException {
String waveform = "";
java.io.File simout = new java.io.File(context.tempDirectory, "simout.vcd");
if(simout.exists()) {
String vcd = FileUtils.readFileToString(new java.io.File(context.tempDirectory, "simout.vcd"),
IOUtil.DEFAULT_ENCODING);
VcdParser parser = new VcdParser(StringUtil.splitToNewLines(vcd));
parser.parse();
waveform = parser.getResultInString();
}
return waveform;
}
private static class SimulationContext {
/*
* Since this is a private class used by simulator exclusively there is
* no need for getters and setters.
*/
public boolean compileOnly;
public java.io.File tempDirectory;
public File targetFile;
public List<String> dependencies;
public CommandLine commandLine;
public List<String> result;
}
}