/*
* Copyright (c) 2009, Swedish Institute of Computer Science.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the Institute nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
package org.contikios.cooja.plugins;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GraphicsEnvironment;
import java.awt.Insets;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Observable;
import java.util.Observer;
import javax.script.ScriptException;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JDialog;
import javax.swing.JEditorPane;
import javax.swing.JFileChooser;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextArea;
import javax.swing.event.MenuEvent;
import javax.swing.event.MenuListener;
import javax.swing.filechooser.FileFilter;
import jsyntaxpane.DefaultSyntaxKit;
import jsyntaxpane.actions.DefaultSyntaxAction;
import org.apache.log4j.Logger;
import org.jdom.Element;
import org.contikios.cooja.ClassDescription;
import org.contikios.cooja.Cooja;
import org.contikios.cooja.PluginType;
import org.contikios.cooja.Simulation;
import org.contikios.cooja.VisPlugin;
import org.contikios.cooja.dialogs.MessageList;
import org.contikios.cooja.util.StringUtils;
@ClassDescription("Simulation script editor")
@PluginType(PluginType.SIM_CONTROL_PLUGIN)
public class ScriptRunner extends VisPlugin {
private static final long serialVersionUID = 7614358340336799109L;
private static Logger logger = Logger.getLogger(ScriptRunner.class);
static boolean headless;
{
headless = GraphicsEnvironment.isHeadless();
if (!headless) {
DefaultSyntaxKit.initKit();
}
}
final String[] EXAMPLE_SCRIPTS = new String[] {
"basic.js", "Various commands",
"helloworld.js", "Wait for 'Hello, world'",
"log_all.js", "Just log all printf()'s and timeout",
"shell.js", "Basic shell interaction",
"plugins.js", "Interact with surrounding Cooja plugins",
};
private Simulation simulation;
private LogScriptEngine engine;
private static BufferedWriter logWriter = null; /* For non-GUI tests */
private JEditorPane codeEditor;
private JTextArea logTextArea;
private JSplitPane centerPanel;
private JSyntaxLinkFile actionLinkFile = null;
private File linkedFile = null;
public ScriptRunner(Simulation simulation, Cooja gui) {
super("Simulation script editor", gui, false);
this.simulation = simulation;
this.engine = null;
/* Menus */
JMenuBar menuBar = new JMenuBar();
JMenu fileMenu = new JMenu("File");
JMenu editMenu = new JMenu("Edit");
JMenu runMenu = new JMenu("Run");
menuBar.add(fileMenu);
menuBar.add(editMenu);
menuBar.add(runMenu);
this.setJMenuBar(menuBar);
/* Example scripts */
final JMenu examplesMenu = new JMenu("Load example script");
for (int i=0; i < EXAMPLE_SCRIPTS.length; i += 2) {
final String file = EXAMPLE_SCRIPTS[i];
JMenuItem exampleItem = new JMenuItem(EXAMPLE_SCRIPTS[i+1]);
exampleItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
String script = loadScript(file);
if (script == null) {
JOptionPane.showMessageDialog(Cooja.getTopParentContainer(),
"Could not load example script: scripts/" + file,
"Could not load script", JOptionPane.ERROR_MESSAGE);
return;
}
updateScript(script);
}
});
examplesMenu.add(exampleItem);
}
fileMenu.add(examplesMenu);
{
/* XXX Workaround to configure jsyntaxpane */
JEditorPane e = new JEditorPane();
new JScrollPane(e);
e.setContentType("text/javascript");
if (e.getEditorKit() instanceof DefaultSyntaxKit) {
DefaultSyntaxKit kit = (DefaultSyntaxKit) e.getEditorKit();
kit.setProperty("PopupMenu", "copy-to-clipboard,-,find,find-next,goto-line,-,linkfile");
kit.setProperty("Action.linkfile", JSyntaxLinkFile.class.getName());
kit.setProperty("Action.execute-script", "jsyntaxpane.actions.ScriptRunnerAction");
}
}
/* Script area */
setLayout(new BorderLayout());
codeEditor = new JEditorPane();
logTextArea = new JTextArea(12,50);
logTextArea.setMargin(new Insets(5,5,5,5));
logTextArea.setEditable(true);
logTextArea.setCursor(null);
final JCheckBoxMenuItem activateMenuItem = new JCheckBoxMenuItem("Activate");
activateMenuItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent ev) {
try {
setScriptActive(!isActive());
} catch (Exception e) {
logger.fatal("Error: " + e.getMessage(), e);
}
}
});
runMenu.add(activateMenuItem);
final JMenuItem runTestMenuItem = new JMenuItem("Save simulation and run with script");
runMenu.add(runTestMenuItem);
runTestMenuItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
exportAndRun();
}
});
doLayout();
centerPanel = new JSplitPane(
JSplitPane.VERTICAL_SPLIT,
new JScrollPane(codeEditor),
new JScrollPane(logTextArea)
);
MenuListener toggleMenuItems = new MenuListener() {
public void menuSelected(MenuEvent e) {
activateMenuItem.setSelected(isActive());
runTestMenuItem.setEnabled(!isActive());
examplesMenu.setEnabled(!isActive());
}
public void menuDeselected(MenuEvent e) {
}
public void menuCanceled(MenuEvent e) {
}
};
fileMenu.addMenuListener(toggleMenuItems);
editMenu.addMenuListener(toggleMenuItems);
runMenu.addMenuListener(toggleMenuItems);
codeEditor.setContentType("text/javascript");
if (codeEditor.getEditorKit() instanceof DefaultSyntaxKit) {
DefaultSyntaxKit kit = (DefaultSyntaxKit) codeEditor.getEditorKit();
kit.setProperty("PopupMenu", "copy-to-clipboard,-,find,find-next,goto-line,-,linkfile");
kit.setProperty("Action.linkfile", JSyntaxLinkFile.class.getName());
kit.setProperty("Action.execute-script", "jsyntaxpane.actions.ScriptRunnerAction");
}
JPopupMenu p = codeEditor.getComponentPopupMenu();
if (p != null) {
for (Component c: p.getComponents()) {
if (!(c instanceof JMenuItem)) {
continue;
}
if (((JMenuItem) c).getAction() == null) {
continue;
}
Action a = ((JMenuItem) c).getAction();
if (a instanceof JSyntaxLinkFile) {
actionLinkFile = (JSyntaxLinkFile) a;
actionLinkFile.setMenuText("Link script to disk file");
actionLinkFile.putValue("ScriptRunner", this);
}
}
}
centerPanel.setOneTouchExpandable(true);
centerPanel.setResizeWeight(0.5);
JPanel buttonPanel = new JPanel(new BorderLayout());
JPanel southPanel = new JPanel(new BorderLayout());
southPanel.add(BorderLayout.EAST, buttonPanel);
getContentPane().add(BorderLayout.CENTER, centerPanel);
getContentPane().add(BorderLayout.SOUTH, southPanel);
setSize(600, 700);
Dimension maxSize = gui.getDesktopPane().getSize();
if (getWidth() > maxSize.getWidth()) {
setSize((int)maxSize.getWidth(), getHeight());
}
if (getHeight() > maxSize.getHeight()) {
setSize(getWidth(), (int)maxSize.getHeight());
}
/* Set default script */
String script = loadScript(EXAMPLE_SCRIPTS[0]);
if (script != null) {
updateScript(script);
}
}
public void startPlugin() {
/* start simulation */
if (!Cooja.isVisualized()) {
simulation.setSpeedLimit(null);
simulation.startSimulation();
}
}
public void setLinkFile(File source) {
linkedFile = source;
if (source == null) {
updateScript("");
if (actionLinkFile != null) {
actionLinkFile.setMenuText("Link script to disk file");
actionLinkFile.putValue("JavascriptSource", null);
}
codeEditor.setEditable(true);
} else {
updateScript(linkedFile);
Cooja.setExternalToolsSetting("SCRIPTRUNNER_LAST_SCRIPTFILE", source.getAbsolutePath());
if (actionLinkFile != null) {
actionLinkFile.setMenuText("Unlink script: " + source.getName());
actionLinkFile.putValue("JavascriptSource", source);
}
codeEditor.setEditable(false);
}
updateTitle();
}
public void setScriptActive(boolean active)
throws Exception {
if (active) {
/* setScriptActive(true) */
/* Free any resources */
setScriptActive(false);
/* Reload script from file */
if (linkedFile != null) {
String script = StringUtils.loadFromFile(linkedFile);
if (script == null) {
logger.fatal("Failed to load script from: " + linkedFile.getAbsolutePath());
} else {
updateScript(script);
}
}
/* Create new engine */
engine = new LogScriptEngine(simulation);
if (Cooja.isVisualized()) {
/* Attach visualized log observer */
engine.setScriptLogObserver(new Observer() {
public void update(Observable obs, Object obj) {
logTextArea.append((String) obj);
logTextArea.setCaretPosition(logTextArea.getText().length());
}
});
} else {
try {
/* Continously write test output to file */
if (logWriter == null) {
/* Warning: static variable, used by all active test editor plugins */
File logFile = new File("COOJA.testlog");
if (logFile.exists()) {
logFile.delete();
}
logWriter = new BufferedWriter(new FileWriter(logFile));
logWriter.write("Random seed: " + simulation.getRandomSeed() + "\n");
logWriter.flush();
}
engine.setScriptLogObserver(new Observer() {
public void update(Observable obs, Object obj) {
try {
if (logWriter != null) {
logWriter.write((String) obj);
logWriter.flush();
} else {
logger.fatal("No log writer: " + obj);
}
} catch (IOException e) {
logger.fatal("Error when writing to test log file: " + obj, e);
}
}
});
} catch (Exception e) {
logger.fatal("Create log writer error: ", e);
setScriptActive(false);
}
}
/* Activate engine */
try {
engine.activateScript(codeEditor.getText());
if (!headless) {
if (actionLinkFile != null) {
actionLinkFile.setEnabled(false);
}
logTextArea.setText("");
codeEditor.setEnabled(false);
updateTitle();
}
logger.info("Test script activated");
} catch (ScriptException e) {
logger.fatal("Test script error: ", e);
setScriptActive(false);
if (Cooja.isVisualized()) {
Cooja.showErrorDialog(Cooja.getTopParentContainer(),
"Script error", e, false);
}
} catch (RuntimeException e) {
logger.fatal("Test script error: ", e);
setScriptActive(false);
}
} else {
/* setScriptActive(false) */
if (engine != null) {
/* Deactivate script */
engine.deactivateScript();
engine.setScriptLogObserver(null);
engine = null;
}
if (logWriter != null) {
try {
logWriter.write(
"Test ended at simulation time: " +
(simulation!=null?simulation.getSimulationTime():"?") + "\n");
logWriter.flush();
logWriter.close();
} catch (IOException e) {
}
logWriter = null;
}
if (!headless) {
if (actionLinkFile != null) {
actionLinkFile.setEnabled(true);
}
codeEditor.setEnabled(true);
updateTitle();
}
logger.info("Test script deactivated");
}
}
private void updateTitle() {
String title = "Simulation script editor ";
if (linkedFile != null) {
title += "(" + linkedFile.getName() + ") ";
}
if (isActive()) {
title += "*active*";
}
setTitle(title);
}
private void exportAndRun() {
/* Save simulation config */
File configFile = simulation.getCooja().doSaveConfig(true);
if (configFile == null) {
return;
}
/* Start test in external process */
try {
JPanel progressPanel = new JPanel(new BorderLayout());
final JDialog progressDialog = new JDialog((Window)Cooja.getTopParentContainer(), (String) null);
progressDialog.setTitle("Running test...");
File coojaBuild;
File coojaJAR;
try {
coojaBuild = new File(Cooja.getExternalToolsSetting("PATH_CONTIKI"), "tools/cooja/build");
coojaJAR = new File(Cooja.getExternalToolsSetting("PATH_CONTIKI"), "tools/cooja/dist/cooja.jar");
coojaBuild = coojaBuild.getCanonicalFile();
coojaJAR = coojaJAR.getCanonicalFile();
} catch (IOException e) {
coojaBuild = new File(Cooja.getExternalToolsSetting("PATH_CONTIKI"), "tools/cooja/build");
coojaJAR = new File(Cooja.getExternalToolsSetting("PATH_CONTIKI"), "tools/cooja/dist/cooja.jar");
}
if (!coojaJAR.exists()) {
JOptionPane.showMessageDialog(Cooja.getTopParentContainer(),
"Can't start Cooja, cooja.jar not found:" +
"\n" + coojaJAR.getAbsolutePath()
+ "\n\nVerify that PATH_CONTIKI is correct in external tools settings.",
"cooja.jar not found", JOptionPane.ERROR_MESSAGE);
return;
}
final File logFile = new File(coojaBuild, "COOJA.testlog");
String command[] = {
"java",
"-Djava.awt.headless=true",
"-jar",
"../dist/cooja.jar",
"-nogui=" + configFile.getAbsolutePath()
};
/* User confirmation */
String s1 = "Start";
String s2 = "Cancel";
int n = JOptionPane.showOptionDialog(Cooja.getTopParentContainer(),
"Starting Cooja in " + coojaBuild.getPath() + ":\n" +
" " + command[0] + " " + command[1] + " " + command[2] + " " + command[3] + "\n",
"Starting Cooja without GUI", JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE, null, new Object[] { s1, s2 }, s1);
if (n != JOptionPane.YES_OPTION) {
return;
}
/* Start process */
final Process process = Runtime.getRuntime().exec(command, null, coojaBuild);
final BufferedReader input = new BufferedReader(new InputStreamReader(process.getInputStream()));
final BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream()));
/* GUI components */
final MessageList testOutput = new MessageList();
final AbstractAction abort = new AbstractAction() {
public void actionPerformed(ActionEvent e) {
process.destroy();
if (progressDialog.isDisplayable()) {
progressDialog.dispose();
}
}
};
abort.putValue(AbstractAction.NAME, "Abort test");
final JButton button = new JButton(abort);
progressPanel.add(BorderLayout.CENTER, new JScrollPane(testOutput));
progressPanel.add(BorderLayout.SOUTH, button);
progressPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
progressPanel.setVisible(true);
progressDialog.getContentPane().add(progressPanel);
progressDialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
progressDialog.getRootPane().setDefaultButton(button);
progressDialog.setSize(800, 300);
progressDialog.setLocationRelativeTo(ScriptRunner.this);
progressDialog.addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
abort.actionPerformed(null);
}
});
progressDialog.setVisible(true);
Thread readInput = new Thread(new Runnable() {
public void run() {
String line;
try {
while ((line = input.readLine()) != null) {
testOutput.addMessage(line, MessageList.NORMAL);
}
} catch (IOException e) {
}
testOutput.addMessage("", MessageList.NORMAL);
testOutput.addMessage("", MessageList.NORMAL);
testOutput.addMessage("", MessageList.NORMAL);
/* Parse log file, check if test succeeded */
try {
String log = StringUtils.loadFromFile(logFile);
if (log == null) {
throw new FileNotFoundException(logFile.getPath());
}
String[] lines = log.split("\n");
boolean testSucceeded = false;
for (String l: lines) {
if (l == null) {
line = "";
}
testOutput.addMessage(l, MessageList.NORMAL);
if (l.contains("TEST OK")) {
testSucceeded = true;
break;
}
}
if (testSucceeded) {
progressDialog.setTitle("Test run completed. Test succeeded.");
button.setText("Test OK");
} else {
progressDialog.setTitle("Test run completed. Test failed.");
button.setText("Test failed");
}
} catch (FileNotFoundException e) {
logger.fatal("No test output : " + logFile);
progressDialog.setTitle("Test run completed. Test failed! (no logfile)");
button.setText("Test failed");
}
}
});
Thread readError = new Thread(new Runnable() {
public void run() {
String line;
try {
while ((line = err.readLine()) != null) {
testOutput.addMessage(line, MessageList.ERROR);
}
} catch (IOException e) {
}
}
});
readInput.start();
readError.start();
} catch (IOException e) {
e.printStackTrace();
}
}
public boolean updateScript(File scriptFile) {
String script = StringUtils.loadFromFile(scriptFile);
if (script == null) {
return false;
}
updateScript(script);
return true;
}
private void updateScript(String script) {
if (script == null) {
return;
}
codeEditor.setText(script);
logTextArea.setText("");
}
public Collection<Element> getConfigXML() {
ArrayList<Element> config = new ArrayList<Element>();
Element element;
if (linkedFile != null) {
element = new Element("scriptfile");
element.setText(simulation.getCooja().createPortablePath(linkedFile).getPath().replace('\\', '/'));
config.add(element);
} else {
element = new Element("script");
element.setText(codeEditor.getText());
config.add(element);
}
element = new Element("active");
element.setText("" + isActive());
config.add(element);
return config;
}
public boolean isActive() {
return engine != null;
}
public void closePlugin() {
try {
setScriptActive(false);
} catch (Exception e) {
}
}
public boolean setConfigXML(Collection<Element> configXML, boolean visAvailable) {
for (Element element : configXML) {
String name = element.getName();
if ("script".equals(name)) {
if (!element.getText().isEmpty()) {
updateScript(element.getText());
}
} else if ("scriptfile".equals(name)) {
File file = simulation.getCooja().restorePortablePath(new File(element.getText().trim()));
setLinkFile(file);
} else if ("active".equals(name)) {
boolean active = Boolean.parseBoolean(element.getText());
if (Cooja.isVisualized()) {
try {
setScriptActive(active);
} catch (Exception e) {
logger.fatal("Error: " + e.getMessage(), e);
}
}
}
}
if (!Cooja.isVisualized()) {
/* Automatically activate script */
try {
setScriptActive(true);
} catch (Exception e) {
logger.fatal("Error: " + e.getMessage(), e);
}
}
return true;
}
private static String loadScript(String file) {
return StringUtils.loadFromURL(ScriptRunner.class.getResource("/scripts/" + file));
}
public static class JSyntaxLinkFile extends DefaultSyntaxAction {
private static Logger logger = Logger.getLogger(JSyntaxLinkFile.class);
public JSyntaxLinkFile() {
super("linkfile");
}
public void actionPerformed(ActionEvent e) {
JMenuItem menuItem = (JMenuItem) e.getSource();
Action action = menuItem.getAction();
ScriptRunner scriptRunner = (ScriptRunner) action.getValue("ScriptRunner");
File currentSource = (File) action.getValue("JavascriptSource");
if (currentSource != null) {
scriptRunner.setLinkFile(null);
return;
}
JFileChooser fileChooser = new JFileChooser();
String suggest = Cooja.getExternalToolsSetting("SCRIPTRUNNER_LAST_SCRIPTFILE", null);
if (suggest != null) {
fileChooser.setSelectedFile(new File(suggest));
} else {
fileChooser.setCurrentDirectory(new java.io.File("."));
}
fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
fileChooser.setDialogTitle("Select script file");
fileChooser.setFileFilter(new FileFilter() {
public boolean accept(File file) {
if (file.isDirectory()) { return true; }
if (file.getName().endsWith(".js")) {
return true;
}
return false;
}
public String getDescription() {
return "Javascript";
}
});
if (fileChooser.showOpenDialog(scriptRunner) != JFileChooser.APPROVE_OPTION) {
return;
}
scriptRunner.setLinkFile(fileChooser.getSelectedFile());
}
}
}