/******************************************************************************* * 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; } }