diff options
author | Stefan Radomski <github@mintwerk.de> | 2014-03-11 14:46:52 (GMT) |
---|---|---|
committer | Stefan Radomski <github@mintwerk.de> | 2014-03-11 14:46:52 (GMT) |
commit | c0b56c2df6adaaa94fe493c37fc30081ea7f4d4d (patch) | |
tree | 39c73ba64b0d228b3c53913ea4e7ab6dda6ad5c1 | |
parent | 325ecf075e1bfee5a830ad345c126802b8b55fcf (diff) | |
parent | c34e0ce034586a05308e552cbbdff48beec7dd96 (diff) | |
download | uscxml-c0b56c2df6adaaa94fe493c37fc30081ea7f4d4d.zip uscxml-c0b56c2df6adaaa94fe493c37fc30081ea7f4d4d.tar.gz uscxml-c0b56c2df6adaaa94fe493c37fc30081ea7f4d4d.tar.bz2 |
Merge pull request #20 from sradomski/master
Integrated debugger into browser (use -d command line parameter)
-rw-r--r-- | CMakeLists.txt | 9 | ||||
-rw-r--r-- | README.md | 6 | ||||
-rw-r--r-- | apps/uscxml-browser.cpp | 19 | ||||
-rw-r--r-- | apps/uscxml-debug.cpp | 81 | ||||
-rw-r--r-- | apps/uscxml-debugger.html | 2832 | ||||
-rw-r--r-- | src/uscxml/DOMUtils.cpp | 23 | ||||
-rw-r--r-- | src/uscxml/DOMUtils.h | 2 | ||||
-rw-r--r-- | src/uscxml/Interpreter.cpp | 45 | ||||
-rw-r--r-- | src/uscxml/Interpreter.h | 36 | ||||
-rw-r--r-- | src/uscxml/Message.cpp | 7 | ||||
-rw-r--r-- | src/uscxml/debug/Breakpoint.cpp | 65 | ||||
-rw-r--r-- | src/uscxml/debug/Breakpoint.h | 29 | ||||
-rw-r--r-- | src/uscxml/debug/DebugSession.cpp | 378 | ||||
-rw-r--r-- | src/uscxml/debug/DebugSession.h | 99 | ||||
-rw-r--r-- | src/uscxml/debug/Debugger.cpp | 114 | ||||
-rw-r--r-- | src/uscxml/debug/Debugger.h | 60 | ||||
-rw-r--r-- | src/uscxml/debug/DebuggerServlet.cpp | 330 | ||||
-rw-r--r-- | src/uscxml/debug/DebuggerServlet.h | 69 | ||||
-rw-r--r-- | src/uscxml/plugins/ioprocessor/basichttp/BasicHTTPIOProcessor.cpp | 2 | ||||
-rw-r--r-- | src/uscxml/server/HTTPServer.cpp | 2 | ||||
-rw-r--r-- | test/samples/uscxml/test-dirmon.scxml | 2 | ||||
-rw-r--r-- | test/samples/w3c/draft/calc.scxml | 158 |
22 files changed, 3908 insertions, 460 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index f1ba3bd..7b2e89e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -937,15 +937,6 @@ if (NOT CMAKE_CROSSCOMPILING) set_target_properties(uscxml-browser PROPERTIES FOLDER "Apps") install_executable(TARGETS uscxml-browser COMPONENT tools) - add_executable(uscxml-debug apps/uscxml-debug.cpp) - target_link_libraries(uscxml-debug uscxml) - if (NOT CMAKE_CROSSCOMPILING) - set_target_properties(uscxml-debug PROPERTIES COTIRE_ADD_UNITY_BUILD FALSE) -# cotire(uscxml-browser) - endif() - set_target_properties(uscxml-debug PROPERTIES FOLDER "Apps") - install_executable(TARGETS uscxml-debug COMPONENT tools) - if (PROTOBUF_FOUND AND OFF) file(GLOB W3C-MMI-COMMON ${PROJECT_SOURCE_DIR}/apps/w3c-mmi/*.cpp ${PROJECT_SOURCE_DIR}/apps/w3c-mmi/*.h) @@ -44,12 +44,6 @@ uSCXML still fails the following ecmascript tests: <table> <tr><th>Test#</th><th>Status</th><th>Description</th><th>Comment</th></tr> <tr> - <td><tt><a href="https://github.com/tklab-tud/uscxml/blob/master/test/samples/w3c/ecma/test301.scxml">301</a></tt></td> - <td><tt>Failed</tt></td> - <td>"the processor should reject this document because it can't download the script"</td> - <td>uSCXML continues processing as if there was no <tt><script></tt> element.</td> - </tr> - <tr> <td><tt><a href="https://github.com/tklab-tud/uscxml/blob/master/test/samples/w3c/ecma/test329.scxml">329</a></tt></td> <td><tt>Failed</tt></td> <td>"test that none of the system variables can be modified"</td> diff --git a/apps/uscxml-browser.cpp b/apps/uscxml-browser.cpp index 4188132..81d8883 100644 --- a/apps/uscxml-browser.cpp +++ b/apps/uscxml-browser.cpp @@ -1,7 +1,7 @@ #include "uscxml/config.h" #include "uscxml/Interpreter.h" #include "uscxml/DOMUtils.h" -#include "uscxml/debug/SCXMLDotWriter.h" +#include "uscxml/debug/DebuggerServlet.h" #include <glog/logging.h> #ifdef HAS_SIGNAL_H @@ -142,6 +142,12 @@ int main(int argc, char** argv) { } HTTPServer::getInstance(options.httpPort, options.wsPort, sslConf); + DebuggerServlet* debugger; + if (options.withDebugger) { + debugger = new DebuggerServlet(); + HTTPServer::getInstance()->registerServlet("/debug", debugger); + } + // instantiate and configure interpreters std::list<Interpreter> interpreters; std::map<std::string, InterpreterOptions*>::iterator confIter = options.interpreters.begin(); @@ -160,9 +166,8 @@ int main(int argc, char** argv) { VerboseMonitor* vm = new VerboseMonitor(); interpreter.addMonitor(vm); } - if (options.useDot) { - SCXMLDotWriter* dotWriter = new SCXMLDotWriter(); - interpreter.addMonitor(dotWriter); + if (options.withDebugger) { + interpreter.addMonitor(debugger); } interpreters.push_back(interpreter); @@ -194,5 +199,11 @@ int main(int argc, char** argv) { } } + if (options.withDebugger) { + // idle and wait for CTRL+C or debugging events + while(true) + tthread::this_thread::sleep_for(tthread::chrono::seconds(1)); + } + return EXIT_SUCCESS; }
\ No newline at end of file diff --git a/apps/uscxml-debug.cpp b/apps/uscxml-debug.cpp deleted file mode 100644 index e155ace..0000000 --- a/apps/uscxml-debug.cpp +++ /dev/null @@ -1,81 +0,0 @@ -#include "uscxml/config.h" -#include "uscxml/Interpreter.h" -#include "uscxml/DOMUtils.h" -#include "uscxml/UUID.h" -#include "uscxml/debug/SCXMLDotWriter.h" -#include "uscxml/debug/Breakpoint.h" -#include "uscxml/debug/Debugger.h" -#include "uscxml/debug/DebuggerServlet.h" -#include <glog/logging.h> -#include <time.h> // mktime - -#include <boost/algorithm/string.hpp> -#include <map> - -#ifdef HAS_SIGNAL_H -#include <signal.h> -#endif - -#ifdef HAS_EXECINFO_H -#include <execinfo.h> -#endif - -#ifdef HAS_DLFCN_H -#include <dlfcn.h> -#endif - -using namespace uscxml; - -//class Debugger : public HTTPServlet { -//public: -// -// std::string _url; -// Interpreter _interpreter; -// -// HTTPServer::Request _clientConn; // a request the client renews everytime -// concurrency::BlockingQueue<Data> _sendQueue; // queue of things we have to return to the client -// -// tthread::recursive_mutex _mutex; -// tthread::condition_variable _resumeCond; -// -// std::set<Breakpoint> _breakPoints; -// std::string _sessionId; -// -// DebuggerMonitor _monitor; -// -// virtual ~Debugger() { -// } -// -// // callbacks from http requests -// -// -// // helpers -// -// -// -// -//}; - - -int main(int argc, char** argv) { - using namespace uscxml; - - InterpreterOptions options = InterpreterOptions::fromCmdLine(argc, argv); - DebuggerServlet debuggerServlet; - - // setup logging - google::InitGoogleLogging(argv[0]); - google::AddLogSink(&debuggerServlet); - - // setup HTTP server - HTTPServer::getInstance(18088, 18089, NULL); - - - HTTPServer::getInstance()->registerServlet("/", &debuggerServlet); - - while(true) - tthread::this_thread::sleep_for(tthread::chrono::seconds(1)); - - - return EXIT_SUCCESS; -}
\ No newline at end of file diff --git a/apps/uscxml-debugger.html b/apps/uscxml-debugger.html new file mode 100644 index 0000000..daf63e0 --- /dev/null +++ b/apps/uscxml-debugger.html @@ -0,0 +1,2832 @@ +<!doctype html> +<html> +<head> + <!-- <link href="http://localhost/~sradomski/jsPlumb/font-awesome.css" rel="stylesheet"> --> + <!-- <link rel="stylesheet" href="http://localhost/~sradomski/jsPlumb/demo-all.css"> --> + <!-- <link rel="stylesheet" href="http://localhost/~sradomski/jsPlumb/demo.css"> --> + + <!-- script src="https://google-code-prettify.googlecode.com/svn/loader/prettify.js?autoload=false"></script --> + <!--script src="https://google-code-prettify.googlecode.com/svn/loader/run_prettify.js?autoload=false&lang=xml" defer="defer"></script--> + + <script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script> + <script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/jquery-ui.min.js"></script> + <script src="http://ajax.aspnetcdn.com/ajax/jquery.validate/1.11.1/jquery.validate.min.js"></script> + <script src="http://ajax.aspnetcdn.com/ajax/jquery.validate/1.11.1/additional-methods.min.js"></script> + <script src="http://netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script> + + <script src="http://cdn.jsdelivr.net/ace/1.1.01/min/ace.js" charset="utf-8"></script> + <script src="http://vkbeautify.googlecode.com/files/vkbeautify.0.99.00.beta.js" charset="utf-8"></script> + <script src="http://cdnjs.cloudflare.com/ajax/libs/typeahead.js/0.9.3/typeahead.min.js" charset="utf-8"></script> + <script src="http://dean.edwards.name/base/Base.js" charset="utf-8"></script> + <!-- alternatively http://maxailloud.github.io/confirm-bootstrap/#liveDemo --> + + <script src="https://rawgithub.com/jibe914/Bootstrap-Confirmation/master/bootstrap3-confirmation.js" charset="utf-8"></script> + <!--script src="https://raw.githubusercontent.com/jibe914/Bootstrap-Confirmation/master/bootstrap3-confirmation.js" charset="utf-8"></script--> + <!-- script src="https://raw.github.com/Tavicu/bootstrap-confirmation/master/bootstrap-confirmation.js" charset="utf-8"></script --> + + + <script src="https://rawgithub.com/msurguy/ladda-bootstrap/master/dist/spin.js" charset="utf-8"></script> + <script src="https://rawgithub.com/msurguy/ladda-bootstrap/master/dist/ladda.js" charset="utf-8"></script> + <script src="https://rawgithub.com/prettycode/Object.identical.js/master/Object.identical.js" charset="utf-8"></script> + + + <!-- <link rel="stylesheet" href="jQuery-File-Upload/css/style.css"> + <link rel="stylesheet" href="jQuery-File-Upload/css/jquery.fileupload.css"> --> + + + + <link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/themes/smoothness/jquery-ui.css" /> + <link rel="stylesheet" href="http://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css"> + <!-- <link rel="stylesheet" href="http://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap-theme.min.css"> --> + <!-- <link rel="stylesheet" href="http://twitter.github.io/typeahead.js/css/main.css" /> --> + <link rel="stylesheet" href="https://raw.githubusercontent.com/hyspace/typeahead.js-bootstrap3.less/master/typeahead.css" /> + <link rel="stylesheet" href="https://rawgithub.com/msurguy/ladda-bootstrap/master/dist/ladda-themeless.min.css" /> + + <!-- script src="jquery-ui-1.10.4.uscxml/js/jquery-1.10.2.js"></script> + <script src="http://cdnjs.cloudflare.com/ajax/libs/select2/3.4.5/select2.min.js" charset="utf-8"></script> + <link rel="stylesheet" href="http://twitter.github.io/typeahead.js/css/main.css" /> + <script src="Base.js"></script> + <script src="jquery-ui-1.10.4.uscxml/js/jquery-ui-1.10.4.custom.js"></script> + <script src="http://localhost/~sradomski/ace/ace.js" charset="utf-8"></script> + <script src="http://localhost/~sradomski/vkbeautify.0.99.00.beta.js" charset="utf-8"></script> + <link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/select2/3.4.5/select2.min.css"> + + <script src='http://localhost/~sradomski/jsPlumb/jquery.min.js'></script> + <script src='http://localhost/~sradomski/jsPlumb/jquery-ui.min.js'></script> + <script src='http://localhost/~sradomski/jsPlumb/jquery.ui.touch-punch.min.js'></script> + <script src="//cdnjs.cloudflare.com/ajax/libs/ace/1.1.01/ace.js"></script> + <script src='http://localhost/~sradomski/jsPlumb/jquery.jsPlumb-1.5.5.js'></script> + <script src="http://localhost/~sradomski/ace/ace.js" charset="utf-8"></script> + <script src="http://localhost/~sradomski/jquery.dropdown.min.js" charset="utf-8"></script> + <script src="http://localhost/~sradomski/bootstrap/js/bootstrap.min.js"></script --> + + <!-- link href="jquery.dropdown.css" rel="stylesheet" / --> + <!-- <link rel="stylesheet" href="//code.jquery.com/ui/1.10.4/themes/dark-hive/jquery-ui.css"> --> + <!-- link rel="stylesheet" href="//code.jquery.com/ui/1.10.4/themes/smoothness/jquery-ui.css" --> + + <style type="text/css"> + + body { +/* font-family: "Trebuchet MS", "Helvetica", "Arial", "Verdana", "sans-serif";*/ + } + + .windowlet { + position: absolute; + top: 3em; + left: 5em; + box-shadow: 5px 2px 19px #aaa; + -o-box-shadow: 5px 2px 19px #aaa; + -webkit-box-shadow: 5px 2px 19px #aaa; + -moz-box-shadow: 5px 2px 19px #aaa; + } + + .windowlet .panel-body { + padding: 8px; + } + + .windowlet textarea { + resize:vertical; + } + + .debug { + position:fixed !important; + width: 320px; + min-width: 250px; + } + + .debug > .panel > .panel-group { +/* overflow: hidden;*/ + margin-bottom: 0px; + } + + .debug > .panel { + margin-bottom: 0; + } + + .debug > .panel > .panel-footer { + padding: 5px 5px; + font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace; + font-size: 10px; + } + + .panel-group .panel { +/* border: 1px solid transparent;*/ + overflow: visible; + } + + .debug-header { + cursor: move; +/* padding: 10px 15px 5px 15px; +*/ } + + .subtitle { + font-size: 0.7em; + font-weight: normal; + } + + /* nested panels */ + .panel .panel .panel-heading { + padding: 8px 15px; + } + + .panel .panel-group .panel { + border-radius: 0px; + } + + .panel .panel .panel-title { + font-size: 100%; /* Override explicit font-size from bootstrap */ + } + + .panel .panel { + font-size: 90%; + } + + .panel-group .panel+.panel { + margin-top: 0px; + } + + .panel-heading { + position: relative; + } + .heading-decoration { + position: absolute; + right: 40px; + top: 4px; + } + + .heading-decoration .badge { + float: left; + margin-top: 2px; + margin-right: 5px; + } + + .panel-heading .panel-toggle:after { + /* symbol for "opening" panels */ + font-family: 'Glyphicons Halflings'; /* essential for enabling glyphicon */ + content: "\e114"; /* adjust as needed, taken from bootstrap.css */ + float: right; /* adjust as needed */ + color: grey; /* adjust as needed */ + } + .panel-heading.collapsed .panel-toggle:after { + /* symbol for "collapsed" panels */ + content: "\e080"; /* adjust as needed, taken from bootstrap.css */ + } + + /* see http://stackoverflow.com/questions/18059161/css-issue-on-twitter-typeahead-with-bootstrap-3 */ + .twitter-typeahead .tt-hint { + display: block; + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.428571429; + border: 1px solid transparent; + } + + .form-group .twitter-typeahead { + width: 100%; + } + + .messages-content textarea.messages { + font-size: 10px; + cursor: auto; + white-space: pre; + overflow: auto; + font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace; + height: 16em; + } + + ul.dropdown-menu li.server-session { + padding-left: 0.7em; + } + + ul.recent-documents span.recent-document-features { + padding-left: 0.7em; + } + + .tooltip-inner { + background-color: #333; + } + .tooltip.bottom .tooltip-arrow { + border-bottom-color: #333; + background-color: #FFF transparent; + } + + .dom { + font-size: 10px; + font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace; + color: #666; + } + + .dom .active-state { + color: #000; + } + + .dom .xpath-selected { + color: #A00; + font-weight: bold; + } + + </style> + + <script type="text/javascript"> + + var SCXMLEditor = Base.extend({ + constructor: function(params) { + $.extend(this, params); + + this.debugger = new SCXMLEditor.Debugger.USCXML({ + debuggerURL: "http://localhost:8088", + scxmlEditor: this, + }); + + this.xmlView = new SCXMLEditor.XMLView({ + destNode: $("body").find(".dom")[0], + }); + + $(this.debugger.getNode()).appendTo($("body")); + }, + + scxmlDOM: undefined, + scxmlURL: undefined, + scxmlRoot: undefined, + scxmlStates: {}, + + setDocumentNameFromURL: function(url) { + var tmp = $('<a>', { href:url } )[0]; + $(this.debugger.debugDocumentNameNode).html(tmp.pathname.split('/').pop()); + $(this.debugger.saveBreakpointsNode).closest("li").removeClass("disabled"); + }, + + loadURL: function(scxmlURL) { + this.scxmlURL = scxmlURL; + this.setDocumentNameFromURL(scxmlURL); + + if (scxmlURL in this.debugger.recentDocuments) { + // load breakpoints + if ('breakpoints' in this.debugger.recentDocuments[scxmlURL]) { + for (var index in this.debugger.recentDocuments[scxmlURL].breakpoints) { + var breakpointData = this.debugger.recentDocuments[scxmlURL].breakpoints[index]; + var breakpoint = new SCXMLEditor.Debugger.Breakpoint({ + data: breakpointData + }); + + var serializedBreakpoint = JSON.stringify(breakpoint.toWireFormat()); + var alreadyExists = false; + + // check if such a breakpoint already exists + for (var index in this.debugger.breakpoints) { + var otherBreakpoint = this.debugger.breakpoints[index]; + if (serializedBreakpoint === JSON.stringify(otherBreakpoint.toWireFormat())) { + alreadyExists = true; + break; + } + } + + if (!alreadyExists) + this.debugger.addBreakpoint(breakpoint); + } + } + } + + // get instance into scope chain + var scxmlEdit = this; + $.ajax({ + type: "GET", + dataType: "text xml", + url: scxmlURL, + success: function (dom) { + scxmlEdit.loadDOM(dom); + }, + error: function(data) { + console.log(data); + } + }); + }, + + loadDOM: function(scxmlDOM) { + this.scxmlDOM = scxmlDOM; + this.scxmlRoot = $(scxmlDOM).find("scxml")[0]; + + // notify the debugger + this.debugger.loadDOM(scxmlDOM); + + // var domText = SCXMLEditor.xmlToString(this.scxmlDOM); + // $("body").find("pre.dom").text(domText); + if (this.xmlView) { + this.xmlView.setXML(this.scxmlDOM); + this.xmlView.prettyPrint(); + } + + // add 'nested' class to states of nested scxml documents + $(this.scxmlRoot).find("scxml").find("state, final, parallel, history").addClass("nested"); + $(this.getNode()).appendTo(this.containerNode); + }, + + getNode: function() { + this.node = document.createElement('div'); + this.node.className = "scxml-editor"; + this.node.innerHTML = '\ + <div class="state-tree"></div>\ + <div class="tools"></div>\ + '; + + this.stateTreeNode = $(this.node).children("div.state-tree"); + this.toolsNode = $(this.node).children("div.tools"); + + // var rootState = new SCXMLEditor.PortletStateView({ + // scxmlState: this.scxmlRoot, + // scxmlEditor: this + // }); + // this.rootNode = rootState.getNode(); + // $(this.rootNode).appendTo(this.stateTreeNode); + // $(this.rootNode).draggable(); + + return this.node; + }, + + getAllChildStates: function() { + return $(this.scxmlRoot).find("state, parallel, final, history").not(".nested"); + }, + + getAllStateIds: function() { + var stateIds = []; + var self = this; + $.each(this.getAllChildStates(), function(index, state) { + stateIds.push(SCXMLEditor.getStateId(state)); + }); + return stateIds; + } + + }); + + SCXMLEditor.getStateId = function(state) { + return $(state).attr("id") || $(state).attr("name") || "scxml"; + } + + SCXMLEditor.getStateType = function(state) { + return state.nodeName.toLowerCase(); + } + + SCXMLEditor.xmlToString = function (xmlNode) { + var text = xmlNode.xml || (new XMLSerializer()).serializeToString(xmlNode); + return vkbeautify.xml(text); + } + + SCXMLEditor.XMLView = Base.extend({ + constructor: function(params) { + $.extend(this, params); + }, + + prettyPrint: function(destNode) { + var self = this; + + function escapeText(text) { + return $('<div/>').text(text).html(); + } + function prettyIndent(xmlNode, indentation) { + var outputText = ''; + var indenter = ''; + + for (var i = 0; i < indentation; i++) { + indenter += " "; + } + var isActive = false; + var isXPathSelected = false; + + switch(xmlNode.nodeType) { + case Node.ELEMENT_NODE: + if (xmlNode.nodeName.toLowerCase() === "state") { + var stateId = xmlNode.getAttribute("id"); + if ($.inArray(stateId, self.activeStates) >= 0) { + outputText += '<span class="active-state">'; + isActive = true; + } + } + + if (xpathNodeSet && xpathNodeSet.snapshotLength > 0) { + for (var i = 0; i < xpathNodeSet.snapshotLength; i++) { + var currNode = xpathNodeSet.snapshotItem(i); + // console.log("Comparing:"); + // console.log(currNode); + // console.log(xmlNode); + if (currNode === xmlNode) { + // console.log("currNode:"); + // console.log(currNode); + outputText += '<span class="xpath-selected">'; + isXPathSelected = true; + break; + } + } + } + + outputText += indenter; + outputText += '<' + xmlNode.nodeName; + for (var j = 0; j < xmlNode.attributes.length; j++) { + var attribute = xmlNode.attributes.item(j); + outputText += " " + attribute.nodeName + "=\"" + attribute.nodeValue + "\""; + } + if (!xmlNode.hasChildNodes()) { + outputText += ' />'; + outputText += '<br />'; + } else { + outputText += '>'; + outputText += '<br />'; + } + break; + case Node.COMMENT_NODE: + outputText += indenter; + outputText += '<!-- ' + escapeText(xmlNode.nodeValue.trim()) + ' -->'; + outputText += '<br />'; + break; + case Node.TEXT_NODE: + var text = xmlNode.nodeValue.trim(); + if (text.length > 0) { + outputText += indenter; + outputText += escapeText(text); + outputText += '<br />'; + } + break; + case Node.ATTRIBUTE_NODE: + case Node.CDATA_SECTION_NODE: + case Node.ENTITY_REFERENCE_NODE: + case Node.ENTITY_NODE: + case Node.PROCESSING_INSTRUCTION_NODE: + case Node.DOCUMENT_NODE: + case Node.DOCUMENT_TYPE_NODE: + case Node.DOCUMENT_FRAGMENT_NODE: + case Node.NOTATION_NODE: + default: + } + + var child = xmlNode.firstChild; + while(child) { + outputText += prettyIndent(child, indentation + 1); + child = child.nextSibling; + } + + switch(xmlNode.nodeType) { + case Node.ELEMENT_NODE: + if (xmlNode.hasChildNodes()) { + outputText += indenter; + outputText += '</' + xmlNode.nodeName; + outputText += '>'; + outputText += '<br />'; + } + break; + default: + } + + if (isActive) { + outputText += '</span>'; + } + if (isXPathSelected) { + outputText += '</span>'; + } + + return outputText; + } + + if (typeof this.xmlNode == "string") { + this.xmlNode = $.parseXML(this.xmlNode); + } + + // this.xpath = "//*[local-name() = \"state\"][@id=\"s0\"]/*[local-name() = \"onentry\"][1]/*[local-name() = \"foreach\"][1]"; + // this.xpath = "//*[local-name() = \"state\"][@id=\"s0\"]/*[local-name() = \"onentry\"][1]/*[local-name() = \"foreach\"][1]"; + var xpathNodeSet = undefined; + + // console.log("xpath:"); + // console.log(this.xpath); + + if (this.xpath) { + try { + xpathNodeSet = this.xmlNode.evaluate( + this.xpath, + this.xmlNode, + function (prefix) { + switch (prefix) { + case 'scxml': + default: + return 'http://www.w3.org/2005/07/scxml'; + } + return null; + }, + XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, + null); + for (var i = 0; i < xpathNodeSet.snapshotLength; i++) { + var currNode = xpathNodeSet.snapshotItem(i); + // console.log("Got node:"); + // console.log(currNode); + } + + } catch(e) { + console.log(e); + } + } + + + var xmlMarkup = '<font color=#888>' + prettyIndent(this.xmlNode, 0) + '</font>'; + + + if (destNode) { + $(destNode).html(xmlMarkup); + } else { + $(this.destNode).html(xmlMarkup); + } + }, + + setDestinationNode: function(destNode) { + this.destNode = destNode; + }, + + setXML: function(xmlNode) { + this.xmlNode = xmlNode; + }, + + setElementXPath: function(xpath) { + this.xpath = xpath; + }, + + setActiveStates: function(states) { + this.activeStates = states; + }, + }); + + // base class of all views onto a state + SCXMLEditor.StateView = Base.extend({ + constructor: function(params) { + $.extend(this, params); + this.scxmlStateType = SCXMLEditor.getStateType(this.scxmlState); + this.scxmlStateId = SCXMLEditor.getStateId(this.scxmlState); + + $(this.scxmlState).bind("DOMSubtreeModified", this.scxmlStateSubtreeModified) + }, + + node: undefined, + scxmlStateType: undefined, + scxmlStateId: undefined, + scxmlState: undefined, + + getNode: function() { + console.log("SCXMLEditor.StateView.getNode()"); + }, + + scxmlStateSubtreeModified: function() { + console.log("SCXMLEditor.StateView.scxmlStateSubtreeModified()"); + }, + + isRoot: function() { + return $(this.scxmlState).prop("tagName").toLowerCase() === "scxml"; + }, + + getDirectChildStates: function() { + return $(this.scxmlState).children("state, parallel, final, history").not(".nested"); + }, + + getAllChildStates: function() { + return $(this.scxmlState).find("state, parallel, final, history").not(".nested"); + }, + + createTransitions: function() { + + } + }); + + // view a state in a small windowlet + SCXMLEditor.ToolsStateView = SCXMLEditor.StateView.extend({ + constructor: function(params) { + this.base(params); + }, + + getNode: function() { + + var tools = this; + + this.node = document.createElement('div'); + this.node.className = 'windowlet tools ui-widget-content'; + this.node.innerHTML = '\ + <div class="ui-widget-header">' + this.getStateId() + '<span class="ui-icon ui-icon-close"></span></div>\ + '; + + $(this.node).children( ".ui-widget-header" ).children( ".ui-icon-close" ).click(function() { + $(tools.node).remove(); + }); + + $(this.node).resizable(); + $(this.node).draggable(); + $(this.node).css("position", "absolute"); + $(this.node).appendTo(this.containerNode); + + return this.node; + }, + + scxmlStateSubtreeModified: function() { + console.log("SCXMLEditor.StateView.scxmlStateSubtreeModified()"); + }, + + }); + + SCXMLEditor.PortletStateView = SCXMLEditor.StateView.extend({ + constructor: function(params) { + this.base(params); + }, + + getNode: function() { + + this.node = document.createElement('div'); + this.node.className = 'portlet'; + + this.node.innerHTML = '\ + <div class="portlet-header"><span class="stateId">' + this.scxmlStateId + '</span></div>\ + <div class="portlet-content"></div>\ + '; + + this.portletHeader = $(this.node).children("div.portlet-header")[0]; + this.portletContent = $(this.node).children("div.portlet-content")[0]; + + var scxmlState = this.scxmlState; + + var settingsButton = document.createElement('button'); + settingsButton.innerHTML = 'Settings'; + $(settingsButton).button({ + icons: { + primary: "ui-icon-gear", + secondary: "ui-icon-triangle-1-s" + }, + text: false + }); + + var settingsMenu = document.createElement('ul'); + settingsMenu.innerHTML = '\ + <li><a href="#"><span class="ui-icon ui-icon-plus"></span>Add nested State</a></li>\ + <li><a href="#"><span class="ui-icon ui-icon-arrow-4-diag"></span>Move this State</a></li>\ + <li><a href="#"><span class="ui-icon ui-icon-trash"></span>Remove this State</a></li>\ + <li>-</li>\ + <li><a href="#"><span class="ui-icon ui-icon-transfer-e-w"></span>Transitions</a></li>\ + <li><a href="#"><span class="ui-icon ui-icon-gear"></span>Executable Content</a></li>\ + <li><a href="#"><span class="ui-icon ui-icon-tag"></span>Datamodel</a></li>\ + <li><a href="#"><span class="ui-icon ui-icon-document"></span>Invokers</a></li>\ + <li><a href="#"><span class="ui-icon ui-icon-zoomin"></span>States</a></li>\ + '; + + $(settingsMenu).menu().hide(); + + $(settingsButton).click(function() { + $(settingsMenu).show().position({ + my: "left top", + at: "left top", + of: this + }); + $(settingsMenu).find("a").click(function event() { + $(settingsMenu).hide(); + }); + $(settingsMenu).mouseleave(function(event) { + $(settingsMenu).hide(); + }); + return false; + }) + + $(settingsMenu).prependTo(this.portletHeader); + $(settingsButton).prependTo(this.portletHeader); + + var childs = this.getDirectChildStates(); + var nrChilds = childs.length + 1; + + var nrCols = Math.ceil(Math.sqrt(nrChilds)); + var nrRows = Math.ceil(nrChilds / nrCols); + + // console.log(nrCols + ' / ' + nrRows); + for (var col = 0; col < nrCols; col++) { + var colNode = document.createElement('div'); + colNode.className = "column"; + + for (var row = 0; row < nrRows; row++) { + if (childs.length > col * nrRows + row) { + var child = childs[col * nrRows + row]; + var childPortlet = new SCXMLEditor.PortletStateView({ + scxmlState: child, + scxmlEditor: this.scxmlEditor + }); + $(childPortlet.getNode()).appendTo(colNode); + } + } + $(colNode).appendTo(this.portletContent); + + $(colNode).sortable({ + connectWith: ".column", + handle: ".portlet-header", + cancel: ".portlet-toggle", + placeholder: "portlet-placeholder ui-corner-all" + }); + } + + // see https://api.jqueryui.com/theming/icons/ + $(this.node) + .addClass( "ui-widget ui-widget-content ui-helper-clearfix ui-corner-all" ) + .children( ".portlet-header" ) + .addClass( "ui-widget-header ui-corner-all" ) + .prepend( "<span class='ui-icon ui-icon-minusthick portlet-toggle'></span>") + .prepend( "<span class='ui-icon ui-icon-newwin portlet-detach'></span>") + .append( " "); + + $(this.node).children( ".portlet-header" ).children( ".portlet-toggle" ).click(function() { + var icon = $(this); + icon.toggleClass( "ui-icon-minusthick ui-icon-plusthick" ); + icon.closest( ".portlet" ).children( ".portlet-content" ).toggle(); + }); + + $(this.node).children( ".portlet-header" ).children( ".portlet-detach" ).click(function() { + var detach = $(this); + detach.toggleClass( "ui-icon-arrowthickstop-1-s ui-icon-newwin" ); + this.tools = new SCXMLEditor.ToolsStateView({ + scxmlState: scxmlState, + scxmlEditor: this.scxmlEditor + }); + $(this.tools.getNode()).appendTo($("body")); // TODO ought to be relative? + }); + return this.node; + + }, + }) + + SCXMLEditor.Debugger = Base.extend({ + constructor: function(params) { + this.breakpointsEnabled = true; + $.extend(this, params); + }, + + breakpoints: [], + currEvent: {}, + + loadDOM: function(scxmlDOM) { + // called by the editor, inherit to e.g. activate buttons + }, + + getNode: function() { + + var debug = this; + this.node = document.createElement('div'); + this.node.className = 'windowlet debug'; + $(this.node).css("position", "absolute"); + + // data-parent="#accordion" missing from <a> to show multiple panels + this.node.innerHTML = '\ + <div class="panel panel-default">\ + <div class="panel-heading debug-header">\ + <h4 class="panel-title">Debugger<span class="toolbar pull-right"></span></h4>\ + <!--span class="subtitle document-name">No Document loaded</span -->\ + </div>\ + <div class="panel-group">\ +<!-- Breakpoints -->\ + <div class="breakpoints panel panel-default">\ + <div class="panel-heading" data-toggle="collapse" data-target="#collapseBreakpoints">\ + <span class="panel-toggle"></span>\ + <h4 class="panel-title">Breakpoints<br/><span class="subtitle current-breakpoint"></span>\ + <i class="heading-decoration pull-right">\ + <div class="btn-group">\ + <button title="Deactivate all Breakpoints" class="deactivate btn btn-default btn-xs btn-primary"><span class="glyphicon glyphicon-off"></span></button>\ + </div>\ + <div class="btn-group">\ + <button title="Add a new Breakpoint" class="new btn btn-default btn-xs"><span class="glyphicon glyphicon-plus"></span></button>\ + <button \ + title="Remove all Breakpoints" class="delete-all btn btn-default btn-xs"><span class="glyphicon glyphicon-remove"></span></button>\ + </div>\ + </i>\ + </h4>\ + </div>\ + <div id="collapseBreakpoints" class="breakpoints-content panel-collapse collapse">\ + </div>\ + </div>\ +<!-- Current Event -->\ + <!--div class="event panel panel-default">\ + <div class="panel-heading" data-toggle="collapse" data-target="#collapseEvent">\ + <h4 class="panel-title">Current Event\ + <i class="heading-decoration pull-right">\ + <div class="btn-group">\ + <button title="Insert a new Event" class="new btn btn-default btn-xs"><span class="glyphicon glyphicon-plus"></span></button>\ + <button title="Edit current Event" class="edit btn btn-default btn-xs"><span class="glyphicon glyphicon-pencil"></span></button>\ + <button title="View Event in Window" class="view btn btn-default btn-xs"><span class="glyphicon glyphicon-search"></span></button>\ + </div>\ + </i>\ + <span class="panel-toggle"></span></h4>\ + </div>\ + <div id="collapseEvent" class="panel-collapse collapse">\ + <div class="panel-body event-content"></div>\ + </div>\ + </div-->\ +<!-- Active States -->\ + <!-- div class="states panel panel-default">\ + <div class="panel-heading" data-toggle="collapse" data-target="#collapseStates">\ + <h4 class="panel-title">Active States<span class="panel-toggle"></span></h4>\ + </div>\ + <div id="collapseStates" class="panel-collapse collapse">\ + <div class="panel-body states-content">\ + Anim pariatur cliche reprehenderit, enim eiusmod high life\ + </div>\ + </div>\ + </div -->\ +<!-- Datamodel -->\ + <div class="datamodel panel panel-default">\ + <div class="panel-heading" data-toggle="collapse" data-target="#collapseDatamodel">\ + <h4 class="panel-title">Datamodel\ + <!--i class="heading-decoration pull-right">\ + <div class="btn-group">\ + <button title="Clear Log" class="clear btn btn-default btn-xs"><span class="glyphicon glyphicon-trash"></span></button>\ + <button title="View Log in Window" class="view btn btn-default btn-xs"><span class="glyphicon glyphicon-search"></span></button>\ + </div>\ + </i-->\ + <span class="panel-toggle"></span></h4>\ + </div>\ + <div id="collapseDatamodel" class="panel-collapse collapse">\ + <div class="panel-body datamodel-content"></div>\ + </div>\ + </div>\ +<!-- Assets -->\ + <!--div class="assets panel panel-default">\ + <div class="panel-heading" data-toggle="collapse" data-target="#collapseAssets">\ + <h4 class="panel-title">Assets\ + <i class="heading-decoration pull-right">\ + <div class="btn-group">\ + <button title="New Asset" class="view btn btn-default btn-xs"><span class="glyphicon glyphicon-plus"></span></button>\ + </div>\ + </i>\ + <span class="panel-toggle"></span></h4>\ + </div>\ + <div id="collapseAssets" class="panel-collapse collapse">\ + <div class="panel-body assets-content"></div>\ + </div>\ + </div-->\ +<!-- Message -->\ + <div class="messages panel panel-default">\ + <div class="panel-heading" data-toggle="collapse" data-target="#collapseMessages">\ + <h4 class="panel-title">Messages\ + <i class="heading-decoration pull-right">\ + <span class="badge"></span>\ + <div class="btn-group">\ + <button title="Clear Log" class="clear btn btn-default btn-xs"><span class="glyphicon glyphicon-trash"></span></button>\ + <button title="Text Wrap" class="textwrap btn btn-default btn-xs"><span class="glyphicon glyphicon-align-left"></span></button>\ + </div>\ + </i>\ + <span class="panel-toggle"></span></h4>\ + </div>\ + <div id="collapseMessages" class="panel-collapse collapse in">\ + <div class="panel-body messages-content">\ + <textarea class="form-control messages" wrap="off" readonly></textarea>\ + </div>\ + </div>\ + </div>\ + </div>\ + <div class="panel-footer">\ + <span class="status-indicators"></span>\ + <span class="pull-right document-name"></span>\ + </div>\ + </div>\ + '; + + $(this.node).find("div.panel-collapse").not(".in").prev().addClass("collapsed"); + + + // get some nodes + this.debugFooterNode = $(this.node).find(".panel-footer")[0]; + this.statusIndicatorNode = $(this.node).find(".status-indicators")[0]; + this.debugDocumentNameNode = $(this.node).find(".document-name")[0]; + this.currentBreakpointNode = $(this.node).find(".current-breakpoint")[0]; + this.toolbarNode = $(this.node).find(".toolbar")[0]; + + this.messagesNode = $(this.node).find("div.messages-content")[0]; + this.messagesBadgeNode = $(this.node).find("div.messages").find("span.badge")[0]; + this.clearMessagesButton = $(this.node).find("div.messages").find("button.clear")[0]; + this.textwrapMessagesButton = $(this.node).find("div.messages").find("button.textwrap")[0]; + this.messagesTextarea = $(this.node).find("div.messages-content").find("textarea")[0]; + + this.breakpointNode = $(this.node).find("div.breakpoints-content")[0]; + this.deleteAllBreakpointsButton = $(this.node).find("div.breakpoints").find("button.delete-all")[0]; + this.disableBreakpointsButton = $(this.node).find("div.breakpoints").find("button.deactivate")[0]; + this.addNewBreakpointsButton = $(this.node).find("div.breakpoints").find("button.new")[0]; + + this.currEventNode = $(this.node).find("div.event-content")[0]; + this.insertEventButton = $(this.node).find("div.event").find("button.new")[0]; + this.editEventButton = $(this.node).find("div.event").find("button.edit")[0]; + this.viewEventButton = $(this.node).find("div.event").find("button.view")[0]; + + this.dataModelNode = $(this.node).find("div.datamodel-content")[0]; + this.clearDataModelLogButton = $(this.node).find("div.datamodel").find("button.clear")[0]; + this.viewDataModelLogButton = $(this.node).find("div.datamodel").find("button.view")[0]; + + this.initBreakpoints(); + this.initToolBar(); +// this.initCurrEvent(); + this.initDataModel(); + this.initMessages(); + + // enable tooltips + $(this.node).find("button").tooltip({ + placement: 'bottom', + container: 'body', + delay: { show: 300, hide: 100 } + }); + + $(this.node).resizable({grid: [1, 100000]}); + $(this.node).draggable({ + handle: $(this.node).find("div.debug-header") + }); + + return this.node; + }, + + initMessages: function() { + var debug = this; + + $(this.clearMessagesButton).prop('disabled', true); + + $(this.node).find('#collapseMessages') + .unbind('show.bs.collapse') + .unbind('shown.bs.collapse') + .on('show.bs.collapse', function () { + $(debug.messagesBadgeNode).html(""); // remove number from badge + }).on('shown.bs.collapse', function () { + debug.messagesTextarea.scrollTop = debug.messagesTextarea.scrollHeight; // scroll to bottom (won't work when hidden) + }); + + $(this.messagesTextarea) + .unbind('mouseenter') + .unbind('mouseleave') + .mouseenter(function () { + $(this).addClass("dont-scroll"); + }) + .mouseleave(function () { + $(this).removeClass("dont-scroll"); + }); + + $(this.clearMessagesButton) + .unbind('mouseenter') + .unbind('mouseleave') + .unbind('click') + .mouseenter(function() { + $(this).addClass("btn-warning"); + }).mouseleave(function() { + $(this).removeClass("btn-warning"); + }).click(function() { + debug.messagesTextarea.value = ""; + $(debug.messagesBadgeNode).html(""); + $(this).prop('disabled', true); + $(this).removeClass("btn-warning"); + $(this).tooltip("hide"); + return false; + }); + + $(this.textwrapMessagesButton).unbind('click') + .click(function() { + if ($(debug.messagesTextarea).attr("wrap") == "on") { + $(this).removeClass("active"); + $(debug.messagesTextarea).attr("wrap", "off"); + } else { + $(this).addClass("active"); + $(debug.messagesTextarea).attr("wrap", "on"); + } + return false; + }); + + + }, + + initToolBar: function() { + var debug = this; + + this.toolbarNode.innerHTML = '\ + <div class="btn-group">\ + <button class="control btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown">\ + <span class="glyphicon glyphicon-cog"></span>\ + <span class="caret"></span>\ + </button>\ + <ul class="dropdown-menu">\ + <li><a class="load-from-url" href="#"><i class="glyphicon glyphicon-import"></i> Load Document</a></li>\ + <li><a class="save-breakpoints" href="#"><i class="glyphicon glyphicon-floppy-save"></i> Save Breakpoints</a></li>\ + </ul>\ + </div>\ + <div class="btn-group">\ + <button title="Start Execution" class="start btn btn-default btn-xs"><span class="glyphicon glyphicon-play"></span></button>\ + <div class="btn-group" data-toggle="buttons-checkbox">\ + <button title="Pause Execution" class="pause btn btn-default btn-xs"><span class="glyphicon glyphicon-pause"></span></button>\ + </div>\ + <button title="Step Execution" class="step btn btn-default btn-xs"><span class="glyphicon glyphicon-step-forward"></span></button>\ + </div>\ + ' + this.controlButtonNode = $(this.toolbarNode).children("div.btn-group").children("button.control")[0]; + this.controlDropdownNode = $(this.toolbarNode).children("div.btn-group").children("ul.dropdown-menu")[0]; + this.loadFromURLNode = $(this.toolbarNode).find("a.load-from-url")[0]; + this.saveBreakpointsNode = $(this.toolbarNode).find("a.save-breakpoints")[0]; + this.startButtonNode = $(this.toolbarNode).children("div.btn-group").children("button.start")[0]; + this.pauseButtonNode = $(this.toolbarNode).children("div.btn-group").find("button.pause")[0]; + this.stepButtonNode = $(this.toolbarNode).children("div.btn-group").children("button.step")[0]; + this.globalDropDownNode = $(this.toolbarNode).find(".dropdown-menu")[0]; + + $(this.loadFromURLNode).unbind('click').click(function() { + debug.showLoadDialog(); + return false; + }); + + $(this.saveBreakpointsNode) + .unbind('click').click(function() { + debug.recentDocuments[debug.scxmlEditor.scxmlURL].breakpoints = []; + for (var key in debug.breakpoints) { + debug.recentDocuments[debug.scxmlEditor.scxmlURL].breakpoints.push(debug.breakpoints[key].toWireFormat()); + } + localStorage.recentDocuments = JSON.stringify(debug.recentDocuments); + $(debug.controlButtonNode).effect('highlight', {color: '#3AA453'}, 600); + return false; + }) + .closest("li").addClass("disabled"); + + + $(this.startButtonNode).prop('disabled', true); + $(this.pauseButtonNode).prop('disabled', true); + $(this.stepButtonNode).prop('disabled', true); + + }, + + showLoadDialog: function() { + var debug = this; + + // do we already have a modal div? + $("body").children("div#loadFromURLModal").remove(); + + // load from URL form - modal window + this.loadFromURLModalNode = $('\ + <div class="modal fade" id="loadFromURLModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">\ + <div class="modal-dialog">\ + <form action="" id="loadFromURLModalForm">\ + <div class="modal-content">\ + <div class="modal-header">\ + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>\ + <h4 class="modal-title" id="myModalLabel">Load SCXML from URL</h4>\ + </div>\ + <div class="modal-body">\ + <div class="form-group">\ + <div class="input-group">\ + <span class="input-group-btn">\ + <button title="Recent Documents" type="button" class="btn dropdown-toggle" data-toggle="dropdown">\ + <span class="glyphicon glyphicon-list-alt"></span>\ + </button>\ + <ul class="recent-documents dropdown-menu" role="menu" />\ + </span>\ + <input id="loadFromURLModalInput" name="scxmlUrl" class="form-control" placeholder="URL of SCXML document">\ + </div>\ + </div>\ + </div>\ + <div class="modal-footer">\ + <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>\ + <button type="submit" class="btn btn-primary">Load</button>\ + </div>\ + </div>\ + </form>\ + </div>\ + </div>\ + '); + + this.recentDocumentsNode = $(this.loadFromURLModalNode).find("ul.recent-documents")[0]; + $(this.recentDocumentsNode).prev().attr('disabled', true); + + this.recentDocuments = {}; + if (localStorage.recentDocuments) { + this.recentDocuments = JSON.parse(localStorage.recentDocuments); + for (var key in this.recentDocuments) { + if ((key.substring(0, 4) != "http") && (key.substring(0, 4) != "file")) { + // no idea, but it's not a document + delete this.recentDocuments[key]; + continue; + } + var recentURL = key; // not sure, but key is not available in callback + // we found a document, enable "recent" button + $(this.recentDocumentsNode).prev().attr('disabled', false); + + var recentDocumentItem = $('\ + <li><a href="#"><!-- i class="glyphicon glyphicon-floppy-open" style="margin-right: 0.7em" / -->' + key + '<span class="recent-document-features"></span></a></li>\ + '); + var recentDocumentFeatures = $(recentDocumentItem).find("span.recent-document-features"); + $(recentDocumentItem).children("a").click(function() { + $(debug.loadFromURLModalNode).find("input#loadFromURLModalInput").val(recentURL); + $(debug.recentDocumentsNode).dropdown("toggle"); + return false; + }); + if ('breakpoints' in this.recentDocuments[key] && this.recentDocuments[key].breakpoints.length > 0) { + $(recentDocumentFeatures).append('<i title="Has Breakpoints" class="glyphicon glyphicon-th-list">'); + } + + $(this.recentDocumentsNode).append(recentDocumentItem); + } + } + + $("body").append(this.loadFromURLModalNode); + + var fromURLValidator = $(this.loadFromURLModalNode).find("form#loadFromURLModalForm").validate({ // initialize the plugin + rules: { scxmlUrl: { required: true, url2: true }}, + highlight: function(element) { + $(element).closest('.form-group').removeClass('has-success').addClass('has-error'); + }, + success: function(element) { + element.text('OK!').addClass('valid').closest('.input-group').removeClass('has-error').addClass('has-success'); + }, + submitHandler: function (form) { + $(debug.loadFromURLModalNode).modal("hide"); + return true; + } + }); + + $(this.loadFromURLModalNode).on('hide.bs.modal', function(e) { + if (!fromURLValidator.valid()) { + $(debug.loadFromURLModalNode).find(".form-group").removeClass('has-success').removeClass('has-error'); + $(debug.loadFromURLModalNode).find("input#loadFromURLModalInput").val(""); + } else { + var scxmlURL = $(debug.loadFromURLModalNode).find("input#loadFromURLModalInput").val(); + if (!(scxmlURL in debug.recentDocuments)) { + debug.recentDocuments[scxmlURL] = {}; + } + localStorage.recentDocuments = JSON.stringify(debug.recentDocuments); + debug.scxmlEditor.loadURL(scxmlURL); + } + fromURLValidator.resetForm(); + }); + + $(this.loadFromURLModalNode).modal(); + + }, + + initDataModel: function() { + var debug = this; + + $(this.clearDataModelLogButton).prop('disabled', true); + $(this.viewDataModelLogButton).prop('disabled', true); + + // add functionality to buttons + $(this.clearDataModelLogButton).mouseenter(function() { + $(this).addClass("btn-warning"); + }).mouseleave(function() { + $(this).removeClass("btn-warning"); + }).click(function() { + + return false; + }); + + this.dataModelListingNode = document.createElement('div'); + + this.dataModelEditor = ace.edit(this.dataModelListingNode, {}); + this.dataModelEditor.getSession().setUseWorker(false); + this.dataModelListingNode.style.fontSize='10px'; + this.dataModelEditor.setTheme("ace/theme/tomorrow"); + this.dataModelEditor.getSession().setMode("ace/mode/javascript"); + this.dataModelEditor.setReadOnly(true); + // this.dataModelEditor.getSession().setUseWrapMode(true); + this.dataModelEditor.setShowPrintMargin(false); + this.dataModelEditor.renderer.setShowGutter(false); + // this.dataModelEditor.getSession().setValue(vkbeautify.json(initialContent)); + $(this.dataModelListingNode).css("height", ((this.dataModelEditor.session.getLength() + 10) * 1.5) + "em"); + + $(this.dataModelNode).append(this.dataModelListingNode); + $(this.dataModelNode).append('\ + <div class="input-group input-group-sm">\ + <span class="input-group-btn">\ + <button title="Quick Command" type="button" class="datamodel-suggest btn dropdown-toggle" data-toggle="dropdown">\ + <span class="glyphicon glyphicon-list-alt"></span>\ + </button>\ + <ul class="suggested-commands dropdown-menu" role="menu">\ + <li><a href="#">_event</a></li>\ + <li><a href="#">_name</a></li>\ + <li><a href="#">_sessionid</a></li>\ + <li><a href="#">_invokers</a></li>\ + <li><a href="#">_ioprocessors</a></li>\ + <li><a href="#">_x</a></li>\ + </ul>\ + </span>\ + <input type="text" class="datamodel-eval form-control">\ + <div class="input-group-btn">\ + <button title="Evaluate as String" type="button" class="datamodel-eval btn btn-default"><span class="glyphicon glyphicon-play"></span></button>\ + </div>\ + </div>\ + '); + + this.dataModelInputNode = $(this.dataModelNode).find("input.datamodel-eval")[0]; + this.dataModelInputButton = $(this.dataModelNode).find("button.datamodel-eval")[0]; + this.dataModelSuggestButton = $(this.dataModelNode).find("button.datamodel-suggest")[0]; + + $(this.dataModelSuggestButton).prop('disabled', true); + + $(this.dataModelNode).find("ul.suggested-commands").find("li") + .unbind('click').click(function() { + var expression = $(this).children("a").text(); + $(debug.dataModelInputNode).val(expression); + debug.evalOnDataModel(expression); + $(this).closest("ul").dropdown('toggle'); + return false; + }); + + $(this.dataModelInputNode) + .prop('disabled', true) + .unbind('keypress').keypress(function(e) { + if (e.which == 13) + debug.evalOnDataModel($(this).val()); + }); + + $(this.dataModelInputButton) + .prop('disabled', true) + .unbind('click').click(function() { + debug.evalOnDataModel($(debug.dataModelInputNode).val()); + return false; + }); + + // $(this.dataModelListingNode).resizable({handles: "n, s"}); + // $(this.dataModelListingNode).resizable({grid: [100000, 1], alsoResize: $(this.node)}); + // $(this.dataModelListingNode).unbind('resize').on('resize', function() { + // debug.dataModelEditor.resize(); + // }); + }, + + initBreakpoints: function() { + var debug = this; + + $(this.deleteAllBreakpointsButton) + .unbind('mouseenter') + .unbind('mouseleave') + .unbind('click'); + $(this.deleteAllBreakpointsButton).mouseenter(function() { + $(this).addClass("btn-danger"); + }).mouseleave(function() { + $(this).removeClass("btn-danger"); + }).click(function() { + return false; + }); + + $(this.deleteAllBreakpointsButton).confirmation({ + singleton: true, + container: "body", + btnOkClass: "btn btn-danger", + btnOkLabel: "Delete All", + btnOkIcon: "glyphicon glyphicon-trash", + onConfirm: function() { + while (debug.breakpoints.length > 0) { + debug.removeBreakpoint(debug.breakpoints[0]); + } + } + }); + + $(this.disableBreakpointsButton).unbind('click'); + $(this.disableBreakpointsButton).click(function() { + $(this).toggleClass("btn-primary"); + if ($(this).hasClass("btn-primary")) { + debug.enableAllBreakpoints(); + } else { + debug.disableAllBreakpoints(); + } + return false; + }); + + $(this.addNewBreakpointsButton).unbind('click'); + $(this.addNewBreakpointsButton).click(function() { + debug.addBreakpoint(); + return false; + }); + + this.breakpointNode.innerHTML = '\ + <table class="table table-hover">\ + </table>\ + '; + + for (var index in this.breakpoints) { + $(this.breakpointNode).children("table").append(this.breakpoints[index].getNode()); + } + }, + + initCurrEvent: function() { + var debug = this; + + $(this.insertEventButton).prop('disabled', true); + $(this.editEventButton).prop('disabled', true); + $(this.viewEventButton).prop('disabled', true); + + var initialContent = ''; + + var editor = ace.edit(this.currEventNode, {}); + editor.getSession().setUseWorker(false); + this.currEventNode.style.fontSize='10px'; + editor.setTheme("ace/theme/tomorrow"); + editor.getSession().setMode("ace/mode/javascript"); + editor.setReadOnly(true); + editor.setShowPrintMargin(false); + editor.renderer.setShowGutter(false); + try { + editor.getSession().setValue(vkbeautify.json(initialContent)); + } catch(e) { + editor.getSession().setValue(initialContent); + } + $(this.currEventNode).css("height", (editor.session.getLength() * 1.5) + "em"); + + this.eventViewModal = $('\ + <div class="modal fade bs-example-modal-lg" id="eventViewModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">\ + <div class="modal-dialog modal-lg">\ + <form action="" id="eventViewModalForm">\ + <div class="modal-content">\ + <div class="modal-header">\ + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>\ + <h4 class="modal-title" id="myModalLabel">View / Edit Event</h4>\ + </div>\ + <div class="modal-body">\ + <div class="editor"></div>\ + </div>\ + <div class="modal-footer">\ + <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>\ + <button type="submit" class="btn btn-primary">Apply</button>\ + </div>\ + </div>\ + </form>\ + </div>\ + </div>\ + '); + + + $(this.eventViewModal).modal({ + show: false + }); + + // $(this.eventViewModal).find("div.modal-dialog").resizable(); + // $(this.eventViewModal).find("div.modal-dialog").draggable(); + + $("body").append(this.eventViewModal); + + // add functionality to buttons + $(this.viewEventButton).click(function() { + $(debug.eventViewModal).find(".editor"); + + var modalEditorNode = $(debug.eventViewModal).find(".editor")[0]; + + var modalEditor = ace.edit(modalEditorNode, {}); + modalEditorNode.style.fontSize='10px'; + // safari has the cursor somewhat off +// modalEditorNode.style.font='inherit!important'; + modalEditor.setTheme("ace/theme/tomorrow"); + modalEditor.getSession().setMode("ace/mode/javascript"); + modalEditor.setReadOnly(false); + modalEditor.setShowPrintMargin(false); + modalEditor.renderer.setShowGutter(true); + modalEditor.getSession().setValue(vkbeautify.json(initialContent)); + + $(modalEditorNode).css("min-height", (editor.session.getLength() * 1.5) + "em"); + //$(modalEditorNode).css("height", "80%"); + + $(debug.eventViewModal).modal("show"); + return false; + }); + + }, + + log: function(messages) { + var self = this; + + $(this.clearMessagesButton).prop('disabled', false); + + if (!self.messagesTextarea) { + console.log(messages); + return; + } + + function logSingleMsg(msg) { + // only show badge when when collapsed + if (!$(self.messagesNode).parent().hasClass("in")) { + var bagdeContent = parseInt($(self.messagesBadgeNode).html()) || 0; + $(self.messagesBadgeNode).html(bagdeContent + 1); + } + + if (typeof(msg) === 'string') { + self.messagesTextarea.value += msg.trim() + "\n"; + } else if (typeof(msg) === 'object') { + if ('formatted' in msg) { + self.messagesTextarea.value += msg.formatted.trim() + "\n"; + } else if('message' in msg) { + self.messagesTextarea.value += msg.message.trim() + "\n"; + } else { + try { + self.messagesTextarea.value += vkbeautify.json(msg) + "\n"; + } catch(e) { + self.messagesTextarea.value += JSON.stringify(msg).trim() + "\n"; + } + } + } else { + //not sure what to do here + self.messagesTextarea.value += msg; + } + } + + if (messages instanceof Array) { + for (var key in messages) { + logSingleMsg(messages[key]); + } + } else { + logSingleMsg(messages); + } + + // scroll to bottom + if (!$(self.messagesTextarea).hasClass("dont-scroll")) + self.messagesTextarea.scrollTop = self.messagesTextarea.scrollHeight; + }, + + // find a breakpoint by value and return its index + findBreakpoint: function(breakpoint) { + for (var index in this.breakpoints) { + var currBreakpoint = this.breakpoints[index]; + + // $("body").append("<pre>" + JSON.stringify(breakpoint.toWireFormat()) + "</pre><br />"); + // $("body").append("<pre>" + JSON.stringify(currBreakpoint.toWireFormat()) + "</pre><br />"); + if (Object.identical(breakpoint.toWireFormat(), currBreakpoint.toWireFormat())) { + return index; + } + } + return -1; + }, + + editBreakpoint: function(breakpoint) { + breakpoint.showEditDialog() + }, + + editedBreakpoint: function(breakpoint, oldData) { + // override to update your debugger + }, + + skipToBreakpoint: function(breakpoint) { + }, + + addBreakpoint: function(breakpoint) { + var debug = this; + if (breakpoint) { + breakpoint.scxmlEditor = this.scxmlEditor; + breakpoint.scxmlDebugger = this; + + if (this.findBreakpoint(breakpoint) < 0) { + // add as new breakpoint + this.breakpoints.push(breakpoint); + var breakpointNode = breakpoint.getNode(); + if (!debug.breakpointsEnabled) { + $(breakpointNode).find("button.enable").prop('disabled', true); + $(breakpointNode).find("button.skipto").prop('disabled', true); + } + $(this.breakpointNode).children("table").append(breakpointNode); + } + } else { + // show dialog to add a breakpoint + var newBreakpoint = new SCXMLEditor.Debugger.Breakpoint({ + scxmlEditor: this.scxmlEditor, + scxmlDebugger: this + }); + debug.editBreakpoint(newBreakpoint); + } + }, + + removeBreakpoint: function(breakpoint) { + var debug = this; + var breakPointIndex = this.findBreakpoint(breakpoint); + if (breakPointIndex >= 0) { + this.breakpoints.splice(breakPointIndex, 1); + $(this.breakpointNode).children("table").find("tr").slice(breakPointIndex, breakPointIndex + 1).detach(); + } + }, + + highlightBreakpoint: function(breakpoint) { + var breakPointIndex = this.findBreakpoint(breakpoint); + if (breakPointIndex < 0) + return; + + // open breakpoint collapsible + $(this.node).find("div.breakpoints").find("div.panel-collapse").addClass("in"); + + // highlight breakpoint + $(this.breakpointNode) + .children("table") + .find("tr").slice(breakPointIndex, breakPointIndex + 1) + .addClass("info"); + + }, + + unhighlightAllBreakpoints: function() { + $(this.breakpointNode) + .children("table") + .find("tr").removeClass("info"); + }, + + enableBreakpoint: function(breakpoint) { + var breakPointIndex = this.findBreakpoint(breakpoint); + if (breakPointIndex < 0) + return; + + $(this.breakpointNode) + .children("table") + .find("tr").slice(breakPointIndex, breakPointIndex + 1) + .find("button.enable").addClass('btn-primary'); + + $(this.breakpointNode) + .children("table") + .find("tr").slice(breakPointIndex, breakPointIndex + 1) + .find("button.skipto") + .prop('disabled', false); + + }, + + disableBreakpoint: function(breakpoint) { + var breakPointIndex = this.findBreakpoint(breakpoint); + if (breakPointIndex < 0) + return; + + $(this.breakpointNode) + .children("table") + .find("tr").slice(breakPointIndex, breakPointIndex + 1) + .find("button.enable").removeClass('btn-primary'); + + $(this.breakpointNode) + .children("table") + .find("tr").slice(breakPointIndex, breakPointIndex + 1) + .find("button.skipto") + .prop('disabled', true); + }, + + disableAllBreakpoints: function() { + this.breakpointsEnabled = false; + $(this.breakpointNode).find("button.enable").prop('disabled', true); + $(this.breakpointNode).find("button.skipto").prop('disabled', true); + }, + + enableAllBreakpoints: function() { + this.breakpointsEnabled = true; + + $(this.breakpointNode).find("button.enable").each(function() { + $(this).prop('disabled', false); + if ($(this).hasClass("btn-primary")) { + $(this).parent().children("button.skipto").prop('disabled', false); + } + }); + }, + + evalOnDataModel: function() { + }, + + attachDebug: function(sessionId) { + }, + + prepareDebug: function() { + this.scxmlText = SCXMLEditor.xmlToString(this.scxmlEditor.scxmlDOM); + }, + + startDebug: function() { + this.unhighlightAllBreakpoints(); + }, + + stopDebug: function() { + this.unhighlightAllBreakpoints(); + }, + + stepDebug: function() { + this.unhighlightAllBreakpoints(); + }, + + }); + + + SCXMLEditor.Debugger.Breakpoint = Base.extend({ + constructor: function(params) { + $.extend(this, params); + this.seqNr = SCXMLEditor.Debugger.Breakpoint.seqNr++; + }, + + data: {}, // all data describing this breakpoint + editNodes: {}, // html nodes of form-groups during editing - needed to retain input - TODO + + breakpointTypes: { + "categories": [ + { id: "state", label: "States" }, + { id: "event", label: "Events" }, + { id: "transition", label: "Transitions" }, + { id: "executable", label: "Executable Content" }, + { id: "invoke", label: "Invokers" }, + { id: "configuration", label: "Configuration" }, + ], + "types": [ + { id: "state-before-enter", + label: "Before Entering State", category: "state", + hint: "Stop execution <b>before</b> a given state is <b>entered</b>." + }, + { id: "state-after-enter", + label: "After Entering State", category: "state", + hint: "Stops execution <b>after</b> a given state is <b>entered</b>." + }, + { id: "state-before-exit", + label: "Before Exiting State", category: "state", + hint: "Stops execution <b>before</b> a given state was <b>exited</b>." + }, + { id: "state-after-exit", + label: "After Exiting State", category: "state", + hint: "Stops execution <b>after</b> a given state was <b>exited</b>." + }, + { id: "event-before", + label: "Before Processing Event", category: "event", + hint: "Stops execution <b>before</b> a given event is processed." + }, + { id: "transition-before", + label: "Before Taking Transition", category: "transition", + hint: "Stops execution <b>before</b> a given transition is taken." + }, + { id: "transition-after", + label: "After Taking Transition", category: "transition", + hint: "Stops execution <b>after</b> a given transition was taken." + }, + { id: "invoker-before-invoke", + label: "Before Starting Invoker", category: "invoke", + hint: "Stop execution <b>before</b> an invoker is <b>started</b>." + }, + { id: "invoker-after-invoke", + label: "After Starting Invoker", category: "invoke", + hint: "Stops execution <b>after</b> an invoker is <b>started</b>." + }, + { id: "invoker-before-cancel", + label: "Before Cancelling Invoker", category: "invoke", + hint: "Stop execution <b>before</b> an invoker is <b>cancelled</b>." + }, + { id: "invoker-after-cancel", + label: "After Cancelling Invoker", category: "invoke", + hint: "Stops execution <b>after</b> an invoker is <b>cancelled</b>." + }, + { id: "executable-before", + label: "Before Executing Content", category: "executable", + hint: "Stops <b>before</b> executing content." + }, + { id: "executable-after", + label: "After Executing Content", category: "executable", + hint: "Stops <b>after</b> executing content." + }, + { id: "microstep-before", + label: "Before Microstep", category: "configuration", + hint: "Stops execution <b>before</b> processing a microstep." + }, + { id: "microstep-after", + label: "After Microstep", category: "configuration", + hint: "Stops execution <b>after</b> after a microstep was processed." + }, + { id: "stable-on", + label: "On Stable Configuration", category: "configuration", + hint: "Stops execution when the interpreter reached a stable configuration and waits for events." + } + ] + }, + + isEnabled: function() { + return true; + }, + + toWireFormat: function() { + var jsonData = jQuery.extend(true, {}, this.data); + + if ('breakpointType' in this.data) { + var typeComponents = this.data.breakpointType.split("-"); + jsonData.subject = typeComponents[0]; + jsonData.when = typeComponents[1]; + if (typeComponents.length > 2) { + jsonData.action = typeComponents[2]; + } else { + delete jsonData.action; + } + } + delete jsonData.breakpointType; + + return jsonData; + }, + + showEditDialog: function() { + var breakpoint = this; + + // do we already have a modal div? + $("body").children("div#editBreakpointModal").remove(); + + this.editModalNode = $('\ + <div class="modal fade" id="editBreakpointModal" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">\ + <div class="modal-dialog">\ + <form action="" class="form-horizontal" role="form">\ + <div class="modal-content">\ + <div class="modal-header">\ + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>\ + <h4 class="modal-title" id="myModalLabel"></h4>\ + </div>\ + <div class="modal-body">\ + <div class="form-group">\ + <label for="breakpointType" class="col-sm-2 control-label">Type</label>\ + <div class="col-sm-10 type-select"></div>\ + </div>\ + <div class="col-sm-2"></div><div class="col-sm-10 alert alert-info"></div>\ + </div>\ + <div class="modal-footer">\ + <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>\ + <button type="submit" class="btn btn-primary">Apply</button>\ + </div>\ + </div>\ + </form>\ + </div>\ + </div>\ + '); + + $(this.editModalNode).find("div.modal-body").find("div.type-select").append(this._typeSelectNode()); + $("body").append(this.editModalNode); + + this.editTypeInfoNode = $(this.editModalNode).find("div.alert-info")[0]; + this.editTypeNode = $(this.editModalNode).find("div.modal-body")[0]; + + var stateNames = this.scxmlEditor.getAllStateIds(); + + var editTypeSelectNode = $(this.editTypeNode).find("div.type-select").children("select"); + if ('breakpointType' in this.data) { + $(editTypeSelectNode).val(this.data.breakpointType); + $(this.editModalNode).find("h4.modal-title").text("Edit Breakpoint"); + } else { + $(this.editModalNode).find("h4.modal-title").text("Add Breakpoint"); + } + + $(this.editModalNode).find("form").removeData(); // important as closures persist with dom + $(this.editModalNode).find("form").validate({ + debug: true, + highlight: function(element) { + $(element).closest('.form-group').removeClass('has-success has-warning').addClass('has-error'); + }, + success: function(element) { + var state = element.closest('.form-group').find("input[name=stateId], input[name=toStateId], input[name=fromStateId]").val(); + if (state && $.inArray(state, stateNames) < 0) { + element.text('Warning: No such state!').addClass('valid').closest('.form-group').removeClass('has-success has-error').addClass('has-warning'); + return; + } + element.text('OK!').addClass('valid').closest('.form-group').removeClass('has-error has-warning').addClass('has-success'); + }, + submitHandler: function (form) { + $(breakpoint.editModalNode).modal("hide"); + + // reset + var oldData = jQuery.extend(true, {}, breakpoint.data); + breakpoint.data = {}; + + // copy over form fields + $(breakpoint.editTypeNode).find("textarea[name], input[name], select[name]").each(function() { + var name = $(this).attr("name"); + var value = $(this).val(); + if (name.length > 0 && value.length > 0) { + breakpoint.data[name] = value; + } + }) + console.log(breakpoint.data); + if (breakpoint.node) { + breakpoint._updateLabelNode(); + breakpoint._updateConditionNode(); + } + if (oldData.length > 0) { + breakpoint.scxmlDebugger.editedBreakpoint(breakpoint, oldData); + } else { + breakpoint.scxmlDebugger.addBreakpoint(breakpoint); + } + return true; + } + }); + + $(breakpoint.editTypeNode).children(".form-group").not(':first').remove(); + $(breakpoint.editTypeNode).children("input[type=hidden]").remove(); // hidden fields + + + $(editTypeSelectNode).change(function () { + $(breakpoint.editTypeNode).children(".form-group").not(':first').remove(); + $(breakpoint.editTypeNode).children("input[type=hidden]").remove(); // hidden fields + breakpoint._updateEditTypeDOM($(editTypeSelectNode).val()); + }); + this._updateEditTypeDOM($(editTypeSelectNode).val()); + + $(this.editModalNode).modal(); + }, + + _updateEditTypeDOM: function(typeId) { + var breakpoint = this; + var breakpointType = $.grep(this.breakpointTypes["types"], function(e){ return e.id == typeId; })[0]; + this.editTypeInfoNode.innerHTML = breakpointType.hint; + + // $(this.editTypeNode).append('\ + // <input type="hidden" name="label" value="' + breakpointType.label + '" />\ + // '); + + var stateNames = this.scxmlEditor.getAllStateIds(); + + switch(true) { + case /state-.*/.test(typeId): + var editTypeStateFormGroupNode = $('\ + <div class="form-group">\ + <label for="stateId" class="col-sm-2 control-label">State</label>\ + <div class="col-sm-10">\ + <input class="form-control" type="text" name="stateId" placeholder="The id of the state"/>\ + </div>\ + </div>\ + '); + $(this.editTypeNode).append(editTypeStateFormGroupNode); + $(editTypeStateFormGroupNode).find("input[name=stateId]") + .val('stateId' in breakpoint.data ? breakpoint.data.stateId : "") + .typeahead({local: stateNames}) + .rules('add', 'required'); + + break; + case /event-.*/.test(typeId): + var editTypeEventFormGroupNode = $('\ + <div class="form-group">\ + <label for="eventName" class="col-sm-2 control-label">Event</label>\ + <div class="col-sm-10">\ + <input class="form-control" type="text" name="eventName" placeholder="The name or prefix of the event"/>\ + </div>\ + </div>\ + '); + $(this.editTypeNode).append(editTypeEventFormGroupNode); + $(editTypeEventFormGroupNode).find("input[name=eventName]") + .val('eventName' in breakpoint.data ? breakpoint.data.eventName : "") + .rules('add', 'required'); + break; + case /transition-.*/.test(typeId): + var editTypeTransitionFormGroupNode = $('\ + <div class="form-group">\ + <label for="transSource" class="col-sm-2 control-label">From State</label>\ + <div class="col-sm-10">\ + <input class="form-control" type="text" name="transSource" placeholder="The id of the source state"/>\ + </div>\ + </div>\ + <div class="form-group">\ + <label for="transTarget" class="col-sm-2 control-label">To State</label>\ + <div class="col-sm-10">\ + <input class="form-control" type="text" name="transTarget" placeholder="The id of the destination state"/>\ + </div>\ + </div>\ + '); + $(this.editTypeNode).append(editTypeTransitionFormGroupNode); + $(editTypeTransitionFormGroupNode).find("input[name=transSource]") + .val('transSource' in breakpoint.data ? breakpoint.data.transSource : "") + .typeahead({local: stateNames}); + $(editTypeTransitionFormGroupNode).find("input[name=transTarget]") + .val('transTarget' in breakpoint.data ? breakpoint.data.transTarget : "") + .typeahead({local: stateNames}); + break + case /invoker-.*/.test(typeId): + var editTypeInvokerFormGroupNode = $('\ + <div class="form-group">\ + <label for="invokeId" class="col-sm-2 control-label">Invoke ID</label>\ + <div class="col-sm-10">\ + <input class="form-control" type="text" name="invokeId" placeholder="The id of the invoker"/>\ + </div>\ + </div>\ + <div class="form-group">\ + <label for="invokeType" class="col-sm-2 control-label">Invoke Type</label>\ + <div class="col-sm-10">\ + <input class="form-control" type="text" name="invokeType" placeholder="The type of the invoker"/>\ + </div>\ + </div>\ + '); + $(this.editTypeNode).append(editTypeInvokerFormGroupNode); + $(editTypeInvokerFormGroupNode).find("input[name=invokeId]") + .val('invokeId' in breakpoint.data ? breakpoint.data.invokeId : ""); + $(editTypeInvokerFormGroupNode).find("input[name=invokeType]") + .val('invokeType' in breakpoint.data ? breakpoint.data.invokeType : ""); + break + + case /executable-.*/.test(typeId): + var editTypeExecutableFormGroupNode = $('\ + <div class="form-group">\ + <label for="executableName" class="col-sm-2 control-label">Element Name</label>\ + <div class="col-sm-10">\ + <input class="form-control" type="text" name="executableName" placeholder="The executable\'s element name"/>\ + </div>\ + </div>\ + <div class="form-group">\ + <label for="executableXPath" class="col-sm-2 control-label">Element XPath</label>\ + <div class="col-sm-10">\ + <input class="form-control" type="text" name="executableXPath" placeholder="The executable\'s XPath expression"/>\ + </div>\ + </div>\ + '); + $(this.editTypeNode).append(editTypeExecutableFormGroupNode); + $(editTypeExecutableFormGroupNode).find("input[name=executableName]") + .val('executableName' in breakpoint.data ? breakpoint.data.executableName : ""); + $(editTypeExecutableFormGroupNode).find("input[name=executableXPath]") + .val('executableXPath' in breakpoint.data ? breakpoint.data.executableXPath : ""); + break; + default: + break; + } + + var editTypeConditionFormGroupNode = $('\ + <div class="form-group">\ + <label for="condition" class="col-sm-2 control-label">Condition</label>\ + <div class="col-sm-10">\ + <textarea class="form-control" rows="4" style="resize:vertical;" name="condition" placeholder="An optional boolean expression evaluated on the datamodel. Breakpoint is only triggered if true." />\ + </div>\ + </div>\ + '); + $(editTypeConditionFormGroupNode).find("textarea[name=condition]") + .val('condition' in breakpoint.data ? breakpoint.data.condition : "") + + $(this.editTypeNode).append(editTypeConditionFormGroupNode); + }, + + getNode: function() { + var self = this; + this.node = $('\ + <tr>\ + <td>\ + <button title="Enable this Breakpoint" class="enable btn btn-default btn-xs btn-primary"><span class="glyphicon glyphicon-ok"></span></button><br />\ + <button title="Skip to this Breakpoint" class="skipto btn btn-default btn-xs"><span class="glyphicon glyphicon-fast-forward"></span></button>\ + </td>\ + <td width="100%">\ + <span class="breakpoint-label"></span>\ + <span class="breakpoint-condition"></span>\ + </td>\ + <td>\ + <button title="Edit this Breakpoint" class="edit btn btn-default btn-xs"><span class="glyphicon glyphicon-wrench"></span></button><br />\ + <button title="Remove this Breakpoint" class="delete btn btn-default btn-xs"><span class="glyphicon glyphicon-trash"></span></button>\ + </td>\ + </tr>\ + '); + + this.labelNode = $(this.node).find("span.breakpoint-label")[0]; + this.conditionNode = $(this.node).find("span.breakpoint-condition")[0]; + this.enableButtonNode = $(this.node).find("button.enable")[0]; + this.skipToButtonNode = $(this.node).find("button.skipto")[0]; + this.deleteButtonNode = $(this.node).find("button.delete")[0]; + this.editButtonNode = $(this.node).find("button.edit")[0]; + + $(this.enableButtonNode) + .unbind('click') + .click(function() { + if ($(this).hasClass("btn-primary")) { + self.scxmlDebugger.disableBreakpoint(self); + } else { + self.scxmlDebugger.enableBreakpoint(self); + } + return false; + }); + + $(this.deleteButtonNode) + .unbind('mouseenter') + .unbind('mouseleave') + .unbind('click') + .mouseenter(function() { + $(this).addClass("btn-danger"); + }) + .mouseleave(function() { + $(this).removeClass("btn-danger"); + }) + .click(function() { + self.scxmlDebugger.removeBreakpoint(self); + return false; + }); + + $(this.editButtonNode) + .unbind('click').click(function() { + self.showEditDialog() + return false; + }); + + $(this.skipToButtonNode) + .unbind('click').click(function() { + self.scxmlDebugger.skipToBreakpoint(self); + return false; + }); + + this._updateLabelNode(); + this._updateConditionNode(); + + return this.node; + }, + + toDescription: function() { + var wireFormat = this.toWireFormat(); + + var breakpointString = ''; + breakpointString += ('when' in wireFormat ? wireFormat.when + ' ' : ''); + breakpointString += ('action' in wireFormat ? wireFormat.action + ' ' : ''); + breakpointString += ('subject' in wireFormat ? wireFormat.subject + ' ' : ''); + + breakpointString += (wireFormat.subject === 'state' ? wireFormat.stateId + ' ' : ''); + breakpointString += (wireFormat.subject === 'event' ? wireFormat.eventName + ' ' : ''); + if (wireFormat.subject === 'transition') { + breakpointString += ('transSource' in wireFormat ? wireFormat.transSource + ' ' : '* '); + breakpointString += '⇒ '; + breakpointString += ('transTarget' in wireFormat ? wireFormat.transTarget + ' ' : '* '); + } + + return breakpointString; + }, + + _updateLabelNode: function() { + this.labelNode.innerHTML = 'Halt ' + this.toDescription(); + }, + + _updateConditionNode: function() { + if (this.data.condition) { + this.conditionNode.innerHTML = "<br/>But only when <code>" + this.data.condition + "</code>"; + } else { + this.conditionNode.innerHTML = ''; + } + }, + + _typeSelectNode: function() { + // establish optgroups for selecting the breakpoint type + var optGroups = {}; + for (var index in this.breakpointTypes.types) { + var bp = this.breakpointTypes.types[index]; + var optGroup = bp.category; + if (!(optGroup in optGroups)) optGroups[optGroup] = []; + optGroups[optGroup].push(bp); + } + + var breakpointTypeSelectNode = document.createElement('select'); + breakpointTypeSelectNode.className = 'form-control'; + $(breakpointTypeSelectNode).attr("name", "breakpointType"); + + for (var optGroup in this.breakpointTypes.categories) { + var cat = this.breakpointTypes.categories[optGroup]; + var optGroupNode = document.createElement('optgroup'); + $(optGroupNode).attr('label', cat.label); + + for(var type in optGroups[cat.id]) { + var breakpointType = optGroups[cat.id][type]; + var optionNode = document.createElement('option'); + $(optionNode).attr('value', breakpointType.id); + optionNode.innerHTML = breakpointType.label; + $(optionNode).appendTo(optGroupNode); + } + $(optGroupNode).appendTo(breakpointTypeSelectNode); + } + + return breakpointTypeSelectNode; + }, + + }); + SCXMLEditor.Debugger.Breakpoint.seqNr = 0; + + SCXMLEditor.Debugger.USCXML = SCXMLEditor.Debugger.extend({ + constructor: function(params) { + this.base(params); + + this.isConnected = false; // we have a connection to the uscxml server + this.isPrepared = false; // we uploaded a document + this.isRunning = false; // the interpreter on the server is running + this.isPaused = false; // the interpreter is running but paused + this.hasDOMLoaded = false; // the client has a DOM loaded + this.isAttached = false; // we are attached to a server session + this.isPolling = false; // we have a unreplied poll request standing + this.sessionId = ""; + }, + + _updateDebugControls: function() { + // all buttons are disabled per base class + $(this.startButtonNode).prop('disabled', true) + $(this.pauseButtonNode).prop('disabled', true); + $(this.stepButtonNode).prop('disabled', true); + + var status = ""; + status += (this.isConnected ? "CON " : "con "); + status += (this.isPolling ? "POL " : "pol "); + status += (this.isPrepared ? "PRP " : "prp "); + status += (this.isRunning ? "RUN " : "run "); + status += (this.isPaused ? "PAU " : "pau "); + status += (this.hasDOMLoaded ? "DOM " : "dom "); + status += (this.isAttached ? "ATC " : "atc "); + + $(this.statusIndicatorNode).html(status); + + // some sanity + if (!this.isConnected) { + $("body").find(".dom").text(""); + $(this.currentBreakpointNode).html(""); + this.isPaused = false; + this.isRunning = false; + this.isAttached = false; + this.isPrepared = false; + } + + if (!this.isRunning) { + this.isPaused = false; + $(this.currentBreakpointNode).html(""); + } + + // toggle button icons + if (this.isRunning) { // switch play and stop + $(this.startButtonNode).children("span.glyphicon").removeClass("glyphicon-play").addClass("glyphicon-stop"); + $(this.startButtonNode).prop('title', 'Stop Execution'); + } else { + $(this.startButtonNode).children("span.glyphicon").removeClass("glyphicon-stop").addClass("glyphicon-play"); + $(this.startButtonNode).prop('title', 'Start Execution'); + } + + if (!this.isPaused) { // switch pause and resume + $(this.pauseButtonNode).removeClass("active"); + $(this.pauseButtonNode).prop('title', 'Pause Execution'); + this.unhighlightAllBreakpoints(); + $(this.currentBreakpointNode).html(""); + } else { + $(this.pauseButtonNode).addClass("active"); + $(this.pauseButtonNode).prop('title', 'Resume Execution'); + } + + if (this.isConnected && (this.hasDOMLoaded || this.isAttached)) { + // are we connected, not running and have a DOM or are attached? + if (!this.isAttached) { + $(this.startButtonNode).prop('disabled', false); + } + if (this.isRunning) { + $(this.pauseButtonNode).prop('disabled', false); + $(this.stepButtonNode).prop('disabled', false); + } else { + $(this.startButtonNode).prop('disabled', false) + $(this.pauseButtonNode).prop('disabled', true); + $(this.stepButtonNode).prop('disabled', false); + } + return; + } else { + $("body").find(".dom").text(""); + } + }, + + loadDOM: function(scxmlDOM) { + this.hasDOMLoaded = true; + this._updateDebugControls(); + }, + + getNode: function() { + this.base(); + var debug = this; + + // a new menu item + $(this.controlDropdownNode).append('\ + <li role="presentation" class="divider"></li>\ + <li class="disabled"><a href="#"><i class="glyphicon glyphicon-resize-small"></i> Attach to Instance</a></li>\ + '); + + // the connect bar + $(this.node).find(".debug-header").after('\ + <div class="panel-body input-group input-group-sm">\ + <input type="text" class="form-control connect" placeholder="uSCMXL Interpreter URL">\ + <span class="input-group-btn">\ + <button title="Connect" class="btn btn-default ladda-button connect" data-spinner-color="#000" data-style="zoom-in">\ + <span class="ladda-label"><span class="ui-icon ui-icon-unlocked"></span></span></button>\ + </span>\ + </div>\ + '); + + this.connectInputNode = $(this.node).find("input.connect"); + this.connectButtonNode = $(this.connectInputNode).next().children("button.connect")[0]; + + if (this.debuggerURL) { + $(this.connectInputNode).val(this.debuggerURL); + } else { + $(this.connectInputNode).attr("placeholder", "URL of uscxml-debug"); + } + + $(this.connectInputNode).focus(function () { + $(debug.connectButtonNode) + .removeClass("btn-warning") + .find("span.ui-icon") + .removeClass("ui-icon-alert ui-icon-locked") + .addClass("ui-icon-unlocked"); + }); + + $(this.connectInputNode).unbind('keypress').keypress(function(e) { + if (e.which == 13) + $(debug.connectButtonNode).trigger('click'); + }); + + + $(this.connectButtonNode).unbind("click"); + $(this.connectButtonNode).click(function() { + debug.connectSpinner = Ladda.create(this); + debug.connectSpinner.start(); + if (debug.isConnected) { + debug.disconnect(); + } else { + debug.connect($(debug.connectInputNode).val()); + } + }); + + // debug controls + $(this.startButtonNode) + .unbind('click').click(function () { + $(this).tooltip("hide"); + if ($(this).children(".glyphicon").hasClass("glyphicon-play")) { + debug.startDebug(); + } else { + debug.stopDebug(); + } + }); + + $(this.pauseButtonNode) + .unbind('click').click(function() { + $(this).tooltip("hide"); + if (debug.isPaused) { + debug.resumeDebug(); + } else { + debug.pauseDebug(); + } + }); + + $(this.stepButtonNode) + .unbind('click').click(function() { + $(this).tooltip("hide"); + debug.stepDebug(); + }); + + this._updateDebugControls(); + + return this.node; + }, + + _indicateLockedConnection: function() { + $(this.connectButtonNode) + .removeClass("btn-warning") + .find("span.ui-icon") + .removeClass("ui-icon-alert ui-icon-unlocked") + .addClass("ui-icon-locked"); + $(this.connectInputNode).attr('disabled', true); + $(this.dataModelInputNode).prop('disabled', false); + $(this.dataModelInputButton).prop('disabled', false); + $(this.dataModelSuggestButton).prop('disabled', false); + }, + + _indicateFailedConnection: function() { + $(this.connectButtonNode) + .addClass("btn-warning") + .find("span.ui-icon") + .removeClass("ui-icon-locked ui-icon-unlocked") + .addClass("ui-icon-alert"); + }, + + _indicateUnlockedConnection: function() { + $(this.connectButtonNode) + .removeClass("btn-warning") + .find("span.ui-icon") + .removeClass("ui-icon-locked ui-icon-alert") + .addClass("ui-icon-unlocked"); + $(this.connectInputNode).attr('disabled', false); + $(this.dataModelInputNode).prop('disabled', true); + $(this.dataModelInputButton).prop('disabled', true); + $(this.dataModelSuggestButton).prop('disabled', true); + }, + + connect: function(debuggerURL) { + if (!debuggerURL || debuggerURL.length == 0) + return; + + this.debuggerURL = debuggerURL; + + var self = this; + + this.requestWithJSON('/debug/connect', { + onFailure: function(dataObj, statusString, xhrObj) { + self.isConnected = false; + self._indicateFailedConnection(); + }, + onSuccess: function(dataObj, statusString, xhrObj) { + self.isConnected = true; + self._indicateLockedConnection(); + + self.log("Client successfully connected to " + self.debuggerURL); + self.sessionId = dataObj.session; + // send all breakpoints when connected + if (self.breakpoints.length > 0) { + for (var index in self.breakpoints) { + self.addBreakpoint(self.breakpoints[index]); + } + } + // register disconnect handler + $(window).unload(function() { + self.disconnect(); + }); + // update list of sessions from server + self.updateServerSessions(); + // and start long-poller + self.pollServer(); + }, + onComplete: function(dataObj, statusString) { + self.connectSpinner.stop(); + } + }); + }, + + disconnect: function() { + if (!this.isConnected) + return; + var self = this; + + this.requestWithJSON('/debug/disconnect', { + async: false, + onComplete: function(dataObj, statusString) { + self.isConnected = false; + self.isPrepared = false; + self._indicateUnlockedConnection(); + self.connectSpinner.stop(); + + // remove all previous sessions + $(self.controlDropdownNode).children("li.server-session").remove(); + self.debuggerURL = ''; + } + }); + }, + + updateServerSessions: function() { + this.base(); + if (!this.isConnected) + return; + + var self = this; + + this.requestWithJSON('/debug/sessions', { + onSuccess: function(dataObj, statusString, xhrObj) { + $(self.controlButtonNode).effect('highlight', {color: '#4489CA'}, 600); + + // remove all previous sessions + $(self.controlDropdownNode).children("li.server-session").remove(); + for (var key in dataObj.sessions) { + $(self.controlDropdownNode).append('\ + <li class="server-session" \ + session-url="' + dataObj.sessions[key].source + '"\ + session-name="' + dataObj.sessions[key].name + '"\ + session-id="' + dataObj.sessions[key].id + '"\ + ><a href="#"><i class="glyphicon glyphicon-link"></i> ' + dataObj.sessions[key].source + '</a></li>\ + '); + } + $(self.controlDropdownNode).find("li").unbind('click').click(function() { + var session = { + sessionId: $(this).attr("session-id"), + sessionName: $(this).attr("session-name"), + sessionURL: $(this).attr("session-url"), + }; + self.attachDebug(session); + + $(self.controlDropdownNode).dropdown('toggle'); + return false; + }); + } + }); + }, + + + addBreakpoint: function(breakpoint) { + this.base(breakpoint); + if (!this.isConnected) + return; + + if (!breakpoint) // it's perfectly fine to call this without a breakpoint - will add a new one + return; + + this.requestWithJSON('/debug/breakpoint/add', { + data: breakpoint.toWireFormat() + }); + }, + + removeBreakpoint: function(breakpoint) { + this.base(breakpoint); + if (!this.isConnected) + return; + this.requestWithJSON('/debug/breakpoint/remove', { + data: breakpoint.toWireFormat() + }); + }, + + editedBreakpoint: function(breakpoint, oldData) { + this.base(breakpoint, oldData); + if (!this.isConnected) + return; + + var oldBreakpoint = new SCXMLEditor.Debugger.Breakpoint({ data : oldData }); + this.requestWithJSON('/debug/breakpoint/remove', { + data: oldBreakpoint.toWireFormat() + }); + this.requestWithJSONBlocking('/debug/breakpoint/add', { + data: breakpoint.toWireFormat() + }); + }, + + skipToBreakpoint: function(breakpoint) { + this.base(breakpoint); + if (!this.isConnected) + return; + + this.requestWithJSON('/debug/breakpoint/skipto', { + data: breakpoint.toWireFormat() + }); + + }, + + enableBreakpoint: function(breakpoint) { + this.base(breakpoint); + if (!this.isConnected) + return; + + this.requestWithJSON('/debug/breakpoint/enable', { + data: breakpoint.toWireFormat() + }); + }, + + disableBreakpoint: function(breakpoint) { + this.base(breakpoint); + if (!this.isConnected) + return; + + this.requestWithJSON('/debug/breakpoint/disable', { + data: breakpoint.toWireFormat() + }); + }, + + enableAllBreakpoints: function() { + this.base(); + if (!this.isConnected) + return; + + this.requestWithJSON('/debug/breakpoint/enable/all'); + }, + + disableAllBreakpoints: function() { + this.base(); + if (!this.isConnected) + return; + + this.requestWithJSON('/debug/breakpoint/disable/all'); + + }, + + + attachDebug: function(session) { + this.base(); + if (!this.isConnected) + return; + + var self = this; + + var data = { + attach: session.sessionId, + }; + + this.requestWithJSON('/debug/attach', { + data: data, + onSuccess: function(dataObj, statusString, xhrObj) { + self.isPrepared = true; + self.isRunning = true; + self.isAttached = true; + + if (self.scxmlEditor.xmlView) { + self.scxmlEditor.xmlView.setXML(dataObj.xml) + self.scxmlEditor.xmlView.prettyPrint(); + } + + // var domText = vkbeautify.xml(dataObj.xml); + // $("body").find("pre.dom").text(domText); + self.scxmlEditor.setDocumentNameFromURL(session.sessionURL); + self._updateDebugControls(); + } + }); + + }, + + prepareDebug: function() { + this.base(); + if (!this.isConnected) + return; + + if (this.isAttached) + return; + + var self = this; + + var data = { + url: this.scxmlEditor.scxmlURL, + xml: this.scxmlText + }; + + this.requestWithJSON('/debug/prepare', { + data: data, + onSuccess: function(dataObj, statusString, xhrObj) { + self.isPrepared = true; + } + }); + }, + + startDebug: function() { + this.base(); + if (!this.isConnected) + return; + + if (!this.isPrepared) + this.prepareDebug(); + + var self = this; + + this.requestWithJSON('/debug/start', { + onSuccess: function(dataObj, statusString, xhrObj) { + self.isRunning = true; + } + }); + }, + + stopDebug: function() { + this.base(); + if (!this.isConnected) + return; + + var self = this; + + this.requestWithJSON('/debug/stop', { + onSuccess: function(dataObj, statusString, xhrObj) { + self.isRunning = false; + self.isPrepared = false; + } + }); + }, + + pauseDebug: function() { + this.base(); + if (!this.isConnected) + return; + + var self = this; + + this.requestWithJSON('/debug/pause', { + onSuccess: function(dataObj, statusString, xhrObj) { + self.isPaused = true; + self._updateDebugControls(); + } + }); + }, + + resumeDebug: function() { + this.base(); + if (!this.isConnected) + return; + + var self = this; + + this.requestWithJSON('/debug/resume', { + onSuccess: function(dataObj, statusString, xhrObj) { + self.isPaused = false; + self._updateDebugControls(); + } + }); + }, + + stepDebug: function() { + this.base(); + if (!this.isConnected) + return; + + if (!this.isPrepared) + this.prepareDebug(); + + var self = this; + + this.requestWithJSON('/debug/step', { + onSuccess: function(dataObj, statusString, xhrObj) { + self.isRunning = true; + self.isPaused = true; + self._updateDebugControls(); + } + }); + }, + + evalOnDataModel: function(expression) { + this.base(); + if (!this.isConnected) + return; + + var self = this; + + var data = {}; + data.expression = expression; + + this.requestWithJSON('/debug/eval', { + data: data, + onSuccess: function(dataObj, statusString, xhrObj) { + var content = JSON.stringify(dataObj.eval); + self.dataModelEditor.getSession().setValue(vkbeautify.json(content)); + + }, + }); + + }, + + // server will return 'status' with JSON response, dispatch accordingly + requestWithJSON: function(url, params) { + var self = this; + if (!params) + params = {}; + + params.method = ('method' in params ? params.method : "POST"); + params.async = ('async' in params ? params.async : true); + params.onSuccess = ('onSuccess' in params ? params.onSuccess : function() {}); + params.onFailure = ('onFailure' in params ? params.onFailure : function(dataObj) { self.log(dataObj); }); + params.onComplete = ('onComplete' in params ? params.onComplete : function() {}); + params.onError = ('onError' in params ? params.onError : function() {}); + params.returnType = ('returnType' in params ? params.returnType : "json"); + params.sendType = ('sendType' in params ? params.sendType : "application/json; charset=utf-8"); + params.data = ('data' in params ? params.data : {}); + + if (params.sendType.match("^application/json")) { + if (this.sessionId.length > 0) // automatically add session + params.data.session = this.sessionId; + params.data = JSON.stringify(params.data); + } + + $.ajax({ + type: params.method, + dataType: params.returnType, + async: params.async, + data: params.data, + contentType: params.sendType, + url: this.debuggerURL + url, + success: function (dataObj, statusString, xhrObj) { + if ("status" in dataObj && dataObj.status === 'success') { + params.onSuccess(dataObj, statusString, xhrObj); + } else { + params.onFailure(dataObj, statusString, xhrObj); + } + }, + error: function(xhrObj, statusString, errorString) { + self.log("Error from " + self.debuggerURL + url); + self.log(statusString + "\n\t" + errorString); + self.log(xhrObj); + + self.isConnected = false; + self._indicateFailedConnection(); + params.onError(errorString, statusString, xhrObj); + }, + complete: function(xhrObj, statusString) { + params.onComplete(xhrObj, statusString); + self._updateDebugControls(); + } + }); + }, + + handleBreakpointReturn: function(serverReturn) { + var updateViewer = false; + if ('qualified' in serverReturn) { + var qualifiedBreakpoint = new SCXMLEditor.Debugger.Breakpoint({ data : serverReturn.qualified }); + var breakpointString = 'Halted ' + qualifiedBreakpoint.toDescription(); + $(this.currentBreakpointNode).html(breakpointString); + + if ('xpath' in serverReturn.qualified) { + if (this.scxmlEditor.xmlView) { + this.scxmlEditor.xmlView.setElementXPath(serverReturn.qualified.xpath); + } + } else { + this.scxmlEditor.xmlView.setElementXPath(""); + } + } + + if ('basicStates' in serverReturn) { + if (this.scxmlEditor.xmlView) { + this.scxmlEditor.xmlView.setActiveStates(serverReturn['basicStates']); + } + } + + if (this.scxmlEditor.xmlView) { + this.scxmlEditor.xmlView.prettyPrint(); + } + + if ('breakpoint' in serverReturn) { + var breakpoint = new SCXMLEditor.Debugger.Breakpoint({ data : serverReturn.breakpoint }); + this.highlightBreakpoint(breakpoint); + } + + }, + + pollServer: function() { + this.isPolling = false; + this._updateDebugControls(); + + if (!this.isConnected) + return; + + if (this.sessionId.length == 0) + return; + + var self = this; + this.isPolling = true; + self._updateDebugControls(); + + $.ajax({ + type: "POST", + dataType: "json", + async: true, + timeout: 100000000, + data: JSON.stringify({session: this.sessionId}), + contentType: "application/json", + url: this.debuggerURL + '/debug/poll', + success: function (dataObj, statusString, xhrObj) { + try { + if ('status' in dataObj) { + if (dataObj.status !== 'success') { + self.log("Error from " + self.debuggerURL + "/debug/poll"); + self.log(dataObj); + return; + } + } + if ('replyType' in dataObj) { + //self.log("Received " + dataObj.replyType); + if (dataObj.replyType === "log") { + self.log(dataObj); + } else if (dataObj.replyType === "finished") { + self.isRunning = false; + self.isPrepared = false; + } else if (dataObj.replyType === "breakpoint") { + self.isPaused = true; + self.handleBreakpointReturn(dataObj); + } else { + self.log("Unhandled reply from /pollServer"); + self.log(dataObj); + } + } else { + self.log("Untyped reply from /pollServer"); + self.log(dataObj); + } + } catch(e) { + self.log("Exception when dispatching server push:"); + self.log(e); + } + // dispatch server push here + self.pollServer(); + }, + error: function(xhrObj, statusString, errorString) { + if (xhrObj.statusText === "timeout") { + self.pollServer(); // just repoll + return; + } + + self.log("Error from /pollServer"); + self.log(statusString + "\n\t" + errorString); + self.log(xhrObj); + + self.isConnected = false; + self._indicateFailedConnection(); + }, + complete: function(xhrObj, statusString) { + self._updateDebugControls(); + } + }); + + }, + + }); + + // see http://nealvs.wordpress.com/2010/07/12/how-to-compare-two-javascript-objects-for-equality/ + function deepEquals(x, y) { + for (var p in y) { + if (typeof(y[p]) !== typeof(x[p])) return false; + if ((y[p] === null) !== (x[p] === null)) return false; + switch (typeof(y[p])) { + case 'undefined': + if (typeof(x[p]) != 'undefined') return false; + break; + case 'object': + if (y[p] !== null && x[p] !== null && (y[p].constructor.toString() !== x[p].constructor.toString() || !y[p].equals(x[p]))) return false; + break; + case 'function': + if (p != 'equals' && y[p].toString() != x[p].toString()) return false; + break; + default: + if (y[p] !== x[p]) return false; + } + } + return true; + } + + $(document).ready(function() { + + + // see http://stackoverflow.com/questions/18754020/bootstrap-3-with-jquery-validation-plugin + $.validator.setDefaults({ + highlight: function(element) { + $(element).closest('.form-group').addClass('has-error'); + }, + unhighlight: function(element) { + $(element).closest('.form-group').removeClass('has-error'); + }, + errorElement: 'span', + errorClass: 'help-block', + errorPlacement: function(error, element) { + if(element.parent('.input-group').length) { + error.insertAfter(element.parent()); + } else { + error.insertAfter(element); + } + } + }); + + var scxmlEdit = new SCXMLEditor({ + containerNode: $("#scxml-editor")[0] + }); + + }); + + </script> +</head> +<body> + <div id="scxml-editor"></div> + <div class="messages"></div> + <div class="dom"></pre> +</body> +</html> diff --git a/src/uscxml/DOMUtils.cpp b/src/uscxml/DOMUtils.cpp index c38b087..3cf986b 100644 --- a/src/uscxml/DOMUtils.cpp +++ b/src/uscxml/DOMUtils.cpp @@ -25,9 +25,14 @@ namespace uscxml { -std::string DOMUtils::xPathForNode(const Arabica::DOM::Node<std::string>& node) { +std::string DOMUtils::xPathForNode(const Arabica::DOM::Node<std::string>& node, const std::string& ns) { std::string xPath; - + std::string nsPrefix; + + if (ns.size() > 0) { + nsPrefix = ns + ":"; + } + if (!node || node.getNodeType() != Arabica::DOM::Node_base::ELEMENT_NODE) return xPath; @@ -37,12 +42,16 @@ std::string DOMUtils::xPathForNode(const Arabica::DOM::Node<std::string>& node) case Arabica::DOM::Node_base::ELEMENT_NODE: { if (HAS_ATTR(curr, "id")) { // we assume ids to be unique and return immediately - xPath.insert(0, "//" + TAGNAME(curr) + "[@id=\"" + ATTR(curr, "id") + "\"]"); + if (ns == "*") { + xPath.insert(0, "//*[local-name() = \"" + TAGNAME(curr) + "\"][@id=\"" + ATTR(curr, "id") + "\"]"); + } else { + xPath.insert(0, "//" + nsPrefix + TAGNAME(curr) + "[@id=\"" + ATTR(curr, "id") + "\"]"); + } return xPath; } else { // check previous siblings to count our index Arabica::DOM::Node<std::string> sibling = curr.getPreviousSibling(); - int index = 1; + int index = 1; // xpath indices start at 1 while(sibling) { if (sibling.getNodeType() == Arabica::DOM::Node_base::ELEMENT_NODE) { if (iequals(TAGNAME(sibling), TAGNAME(curr))) { @@ -51,7 +60,11 @@ std::string DOMUtils::xPathForNode(const Arabica::DOM::Node<std::string>& node) } sibling = sibling.getPreviousSibling(); } - xPath.insert(0, "/" + TAGNAME(curr) + "[" + toStr(index) + "]"); + if (ns == "*") { + xPath.insert(0, "/*[local-name() = \"" + TAGNAME(curr) + "\"][" + toStr(index) + "]"); + } else { + xPath.insert(0, "/" + nsPrefix + TAGNAME(curr) + "[" + toStr(index) + "]"); + } } break; } diff --git a/src/uscxml/DOMUtils.h b/src/uscxml/DOMUtils.h index 731b332..596bb36 100644 --- a/src/uscxml/DOMUtils.h +++ b/src/uscxml/DOMUtils.h @@ -35,7 +35,7 @@ namespace uscxml { class USCXML_API DOMUtils { public: - static std::string xPathForNode(const Arabica::DOM::Node<std::string>& node); + static std::string xPathForNode(const Arabica::DOM::Node<std::string>& node, const std::string& ns = ""); static std::list<Arabica::DOM::Node<std::string> > getElementsByType(const Arabica::DOM::Node<std::string>& root, Arabica::DOM::Node_base::Type type); }; diff --git a/src/uscxml/Interpreter.cpp b/src/uscxml/Interpreter.cpp index 2e867ba..bfee23a 100644 --- a/src/uscxml/Interpreter.cpp +++ b/src/uscxml/Interpreter.cpp @@ -98,7 +98,7 @@ void InterpreterOptions::printUsageAndExit(const char* progName) { printf("\n"); printf("Options\n"); printf("\t-v : be verbose\n"); - printf("\t-d : write each configuration as a dot file\n"); + printf("\t-d : enable debugging via HTTP\n"); printf("\t-lN : Set loglevel to N\n"); printf("\t-tN : port for HTTP server\n"); printf("\t-sN : port for HTTPS server\n"); @@ -120,7 +120,7 @@ InterpreterOptions InterpreterOptions::fromCmdLine(int argc, char** argv) { optind = 0; struct option longOptions[] = { {"verbose", no_argument, 0, 'v'}, - {"dot", no_argument, 0, 'd'}, + {"debug", no_argument, 0, 'd'}, {"port", required_argument, 0, 't'}, {"ssl-port", required_argument, 0, 's'}, {"ws-port", required_argument, 0, 'w'}, @@ -134,11 +134,6 @@ InterpreterOptions InterpreterOptions::fromCmdLine(int argc, char** argv) { }; opterr = 0; - if (argc < 2) { - options.error = "No SCXML document to evaluate"; - return options; - } - InterpreterOptions* currOptions = &options; // parse global options @@ -183,7 +178,7 @@ InterpreterOptions InterpreterOptions::fromCmdLine(int argc, char** argv) { currOptions->pluginPath = optarg; break; case 'd': - currOptions->useDot = true; + currOptions->withDebugger = true; break; case 'c': currOptions->certificate = optarg; @@ -228,7 +223,7 @@ InterpreterOptions InterpreterOptions::fromCmdLine(int argc, char** argv) { DONE_PARSING_CMD: - if (options.interpreters.size() == 0) + if (options.interpreters.size() == 0 && !options.withDebugger) options.error = "No SCXML document to evaluate"; return options; @@ -328,6 +323,7 @@ Interpreter Interpreter::fromURI(const std::string& uri) { // try to establish URI root for relative src attributes in document if (interpreter) { interpreter._impl->_baseURI = URL::asBaseURL(absUrl); + interpreter._impl->_sourceURI = absUrl; } else { LOG(ERROR) << "Cannot create interpreter from URI '" << absUrl.asString() << "'"; } @@ -1310,16 +1306,26 @@ void InterpreterImpl::executeContent(const Arabica::DOM::Node<std::string>& cont if (content.getNodeType() != Node_base::ELEMENT_NODE) return; - if (false) { - } else if (iequals(TAGNAME(content), _xmlNSPrefix + "onentry") || - iequals(TAGNAME(content), _xmlNSPrefix + "onexit") || - iequals(TAGNAME(content), _xmlNSPrefix + "finalize") || - iequals(TAGNAME(content), _xmlNSPrefix + "transition")) { + if (iequals(TAGNAME(content), _xmlNSPrefix + "onentry") || + iequals(TAGNAME(content), _xmlNSPrefix + "onexit") || + iequals(TAGNAME(content), _xmlNSPrefix + "finalize") || + iequals(TAGNAME(content), _xmlNSPrefix + "transition")) { // --- CONVENIENCE LOOP -------------------------- NodeList<std::string> executable = content.getChildNodes(); for (int i = 0; i < executable.getLength(); i++) { executeContent(executable.item(i), rethrow); } + return; + } + + // --- MONITOR: beforeExecutingContent ------------------------------ + for(monIter_t monIter = _monitors.begin(); monIter != _monitors.end(); monIter++) { + try { + (*monIter)->beforeExecutingContent(shared_from_this(), content); + } USCXML_MONITOR_CATCH_BLOCK(beforeExecutingContent) + } + + if (false) { } else if (iequals(TAGNAME(content), _xmlNSPrefix + "raise")) { // --- RAISE -------------------------- if (HAS_ATTR(content, "event")) { @@ -1453,6 +1459,9 @@ void InterpreterImpl::executeContent(const Arabica::DOM::Node<std::string>& cont } } catch (Event exception) { // script failed to download + if (exception.name == "error.communication") { + throw exception; // terminate test329 + } receive(exception); return; } @@ -1526,6 +1535,14 @@ void InterpreterImpl::executeContent(const Arabica::DOM::Node<std::string>& cont } execContent.exitElement(content); } + + // --- MONITOR: afterExecutingContent ------------------------------ + for(monIter_t monIter = _monitors.begin(); monIter != _monitors.end(); monIter++) { + try { + (*monIter)->afterExecutingContent(shared_from_this(), content); + } USCXML_MONITOR_CATCH_BLOCK(afterExecutingContent) + } + } void InterpreterImpl::returnDoneEvent(const Arabica::DOM::Node<std::string>& state) { diff --git a/src/uscxml/Interpreter.h b/src/uscxml/Interpreter.h index 94c5d74..f6b1783 100644 --- a/src/uscxml/Interpreter.h +++ b/src/uscxml/Interpreter.h @@ -98,7 +98,7 @@ enum Capabilities { class USCXML_API InterpreterOptions { public: - bool useDot; + bool withDebugger; bool verbose; bool withHTTP; bool withHTTPS; @@ -126,7 +126,7 @@ public: protected: InterpreterOptions() : - useDot(false), + withDebugger(false), verbose(false), withHTTP(true), withHTTPS(true), @@ -171,12 +171,19 @@ public: _monitors.erase(monitor); } - void setBaseURI(std::string baseURI) { - _baseURI = URL(baseURI); + void setSourceURI(std::string sourceURI) { + _sourceURI = URL(sourceURI); + + URL baseURI(sourceURI); + URL::toBaseURL(baseURI); + _baseURI = baseURI; } URL getBaseURI() { return _baseURI; } + URL getSourceURI() { + return _sourceURI; + } void setCmdLineOptions(std::map<std::string, std::string> params); Data getCmdLineOptions() { @@ -213,6 +220,11 @@ public: return _nameSpaceInfo[ns] + ":"; return ""; } + + Arabica::XPath::NodeSet<std::string> getNodeSetForXPath(const std::string& xpathExpr) { + return _xpath.evaluate(xpathExpr, _scxml).asNodeSet(); + } + void setNameSpaceInfo(const std::map<std::string, std::string> nameSpaceInfo); std::map<std::string, std::string> getNameSpaceInfo() { return _nameSpaceInfo; @@ -339,6 +351,7 @@ protected: tthread::recursive_mutex _pluginMutex; URL _baseURI; + URL _sourceURI; Arabica::DOM::Document<std::string> _document; Arabica::DOM::Element<std::string> _scxml; Arabica::XPath::XPath<std::string> _xpath; @@ -478,8 +491,11 @@ public: return _impl->removeMonitor(monitor); } - void setBaseURI(std::string baseURI) { - return _impl->setBaseURI(baseURI); + void setSourceURI(std::string sourceURI) { + return _impl->setSourceURI(sourceURI); + } + URL getSourceURI() { + return _impl->getSourceURI(); } URL getBaseURI() { return _impl->getBaseURI(); @@ -527,7 +543,10 @@ public: std::string getXMLPrefixForNS(const std::string& ns) { return _impl->getXMLPrefixForNS(ns); } - + Arabica::XPath::NodeSet<std::string> getNodeSetForXPath(const std::string& xpathExpr) { + return _impl->getNodeSetForXPath(xpathExpr); + } + void inline receiveInternal(const Event& event) { return _impl->receiveInternal(event); } @@ -709,6 +728,9 @@ public: virtual void beforeExitingState(Interpreter interpreter, const Arabica::DOM::Element<std::string>& state) {} virtual void afterExitingState(Interpreter interpreter, const Arabica::DOM::Element<std::string>& state) {} + virtual void beforeExecutingContent(Interpreter interpreter, const Arabica::DOM::Node<std::string>& content) {} + virtual void afterExecutingContent(Interpreter interpreter, const Arabica::DOM::Node<std::string>& content) {} + virtual void beforeUninvoking(Interpreter interpreter, const Arabica::DOM::Element<std::string>& invokeElem, const std::string& invokeid) {} virtual void afterUninvoking(Interpreter interpreter, const Arabica::DOM::Element<std::string>& invokeElem, const std::string& invokeid) {} diff --git a/src/uscxml/Message.cpp b/src/uscxml/Message.cpp index aeb0027..33dca84 100644 --- a/src/uscxml/Message.cpp +++ b/src/uscxml/Message.cpp @@ -430,6 +430,7 @@ Data Data::fromJSON(const std::string& jsonString) { std::string value = trimmed.substr(t[currTok].start, t[currTok].end - t[currTok].start); if (dataStack.back()->type == Data::VERBATIM) { boost::replace_all(value, "\\\"", "\""); + boost::replace_all(value, "\\n", "\n"); } dataStack.back()->atom = value; dataStack.pop_back(); @@ -728,10 +729,11 @@ std::string Data::toJSON(const Data& data) { // escape string if (false) { } else if (data.atom[i] == '"') { - os << '\\'; - os << data.atom[i]; + os << "\\\""; } else if (data.atom[i] == '\n') { os << "\\n"; + } else if (data.atom[i] == '\t') { + os << "\\t"; } else { os << data.atom[i]; } @@ -746,6 +748,7 @@ std::string Data::toJSON(const Data& data) { std::string xmlSer = xmlSerSS.str(); boost::replace_all(xmlSer, "\"", "\\\""); boost::replace_all(xmlSer, "\n", "\\n"); + boost::replace_all(xmlSer, "\t", "\\t"); os << "\"" << xmlSer << "\""; } else { if (data.type == Data::VERBATIM) { diff --git a/src/uscxml/debug/Breakpoint.cpp b/src/uscxml/debug/Breakpoint.cpp index f64efad..58cf295 100644 --- a/src/uscxml/debug/Breakpoint.cpp +++ b/src/uscxml/debug/Breakpoint.cpp @@ -19,10 +19,12 @@ #include "uscxml/debug/Breakpoint.h" #include "uscxml/Interpreter.h" +#include "uscxml/DOMUtils.h" namespace uscxml { Breakpoint::Breakpoint(const Data& data) { + enabled = true; subject = UNDEF_SUBJECT; when = UNDEF_WHEN; action = UNDEF_ACTION; @@ -65,7 +67,7 @@ Breakpoint::Breakpoint(const Data& data) { subject = EVENT; } else if (data["subject"].atom == "invoker") { subject = INVOKER; - } else if (data["subject"].atom == "exec") { + } else if (data["subject"].atom == "executable") { subject = EXECUTABLE; } } @@ -82,6 +84,12 @@ Breakpoint::Breakpoint(const Data& data) { if (data.hasKey("eventName")) eventName = data["eventName"].atom; + if (data.hasKey("executableName")) + executableName = data["executableName"].atom; + + if (data.hasKey("executableXPath")) + executableXPath = data["executableXPath"].atom; + if (data.hasKey("stateId")) stateId = data["stateId"].atom; @@ -116,7 +124,7 @@ Data Breakpoint::toData() const { data.compound["subject"] = Data("invoker", Data::VERBATIM); break; case EXECUTABLE: - data.compound["subject"] = Data("exec", Data::VERBATIM); + data.compound["subject"] = Data("executable", Data::VERBATIM); break; default: break; @@ -162,6 +170,16 @@ Data Breakpoint::toData() const { if (eventName.length() > 0) data.compound["eventName"] = Data(eventName, Data::VERBATIM); + if (executableName.length() > 0) + data.compound["executableName"] = Data(executableName, Data::VERBATIM); + + if (executableXPath.length() > 0) { + data.compound["executableXPath"] = Data(executableXPath, Data::VERBATIM); + } + + if (element) + data.compound["xpath"] = Data(DOMUtils::xPathForNode(element, "*"), Data::VERBATIM); + if (stateId.length() > 0) data.compound["stateId"] = Data(stateId, Data::VERBATIM); @@ -177,34 +195,31 @@ Data Breakpoint::toData() const { return data; } -bool Breakpoint::matches(const Breakpoint& other) const { +bool Breakpoint::matches(Interpreter interpreter, const Breakpoint& other) const { // would we match the given breakpoint? if (subject != UNDEF_SUBJECT && - other.subject != UNDEF_SUBJECT && other.subject != subject) return false; // subject does not match if (when != UNDEF_WHEN && - other.when != UNDEF_WHEN && other.when != when) return false; // time does not match if (action != UNDEF_ACTION && - other.action != UNDEF_ACTION && other.action != action) return false; // action does not match // when we have a qualifier it has to match - if(invokeId.length() > 0 && !InterpreterImpl::nameMatch(invokeId, other.invokeId)) { + if(invokeId.length() > 0 && invokeId != other.invokeId) { return false; } - if(invokeType.length() > 0 && !InterpreterImpl::nameMatch(invokeType, other.invokeType)) { + if(invokeType.length() > 0 && invokeType != other.invokeType) { return false; } - if(stateId.length() > 0 && !InterpreterImpl::nameMatch(stateId, other.stateId)) { + if(stateId.length() > 0 && stateId != other.stateId) { return false; } @@ -212,19 +227,39 @@ bool Breakpoint::matches(const Breakpoint& other) const { return false; } - if(transSource.length() > 0 && !InterpreterImpl::nameMatch(transSource, other.transSource)) { + if(executableName.length() > 0 && executableName != other.executableName) { return false; } - if(transTarget.length() > 0 && !InterpreterImpl::nameMatch(transTarget, other.transTarget)) { + if(executableXPath.length()) { + Arabica::XPath::NodeSet<std::string> nodes; + try { + nodes = interpreter.getNodeSetForXPath(executableXPath); + } catch (...) { + return false; + } + return Interpreter::isMember(other.element, nodes); + } + + if(transSource.length() > 0 && transSource != other.transSource) { return false; } - - return true; -} + if(transTarget.length() > 0 && transTarget != other.transTarget) { + return false; + } -bool Breakpoint::isValid() { + if (condition.length() > 0) { + try { + DataModel dm = interpreter.getDataModel(); + if (!dm || !dm.evalAsBool(condition)) { + return false; + } + } catch (...) { + return false; + } + } + return true; } diff --git a/src/uscxml/debug/Breakpoint.h b/src/uscxml/debug/Breakpoint.h index b2861d8..cd6fea5 100644 --- a/src/uscxml/debug/Breakpoint.h +++ b/src/uscxml/debug/Breakpoint.h @@ -21,6 +21,7 @@ #define BREAKPOINT_H_VR7K7T1X #include "uscxml/Message.h" +#include "uscxml/Interpreter.h" namespace uscxml { @@ -39,35 +40,49 @@ public: UNDEF_ACTION, ENTER, EXIT, INVOKE, UNINVOKE }; - Breakpoint() {} + Breakpoint() { + subject = UNDEF_SUBJECT; + when = UNDEF_WHEN; + action = UNDEF_ACTION; + } Breakpoint(const Data& data); // would we match the given breakpoint as well? - bool matches(const Breakpoint& other) const; - - bool isValid(); - + bool matches(Interpreter interpreter, const Breakpoint& other) const; + Data toData() const; bool operator<(const Breakpoint& other) const { - return (origData < other.origData); + return (toData() < other.toData()); } + operator bool() { + return (subject != UNDEF_SUBJECT || + when != UNDEF_WHEN || + action != UNDEF_ACTION); + } + + mutable bool enabled; + When when; Subject subject; Action action; + Arabica::DOM::Element<std::string> element; + std::string invokeId; std::string invokeType; std::string eventName; + std::string executableName; + std::string executableXPath; + std::string stateId; std::string transSource; std::string transTarget; std::string condition; - Data origData; }; } diff --git a/src/uscxml/debug/DebugSession.cpp b/src/uscxml/debug/DebugSession.cpp new file mode 100644 index 0000000..46b414f --- /dev/null +++ b/src/uscxml/debug/DebugSession.cpp @@ -0,0 +1,378 @@ +/** + * @file + * @author 2012-2014 Stefan Radomski (stefan.radomski@cs.tu-darmstadt.de) + * @copyright Simplified BSD + * + * @cond + * This program is free software: you can redistribute it and/or modify + * it under the terms of the FreeBSD license as published by the FreeBSD + * project. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the FreeBSD license along with this + * program. If not, see <http://www.opensource.org/licenses/bsd-license>. + * @endcond + */ + +#include "uscxml/debug/DebugSession.h" +#include "uscxml/debug/Debugger.h" + +namespace uscxml { + +void DebugSession::checkBreakpoints(const std::list<Breakpoint> qualifiedBreakpoints) { + std::list<Breakpoint>::const_iterator qualifiedBreakpointIter = qualifiedBreakpoints.begin(); + + if (!_breakpointsEnabled) + return; + + while(qualifiedBreakpointIter != qualifiedBreakpoints.end()) { + const Breakpoint& qualifiedBreakpoint = *qualifiedBreakpointIter++; + + // check if one of the user-supplied breakpoints match + bool userBreakpointMatched = false; + Data replyData; + + if (_skipTo) { + if (_skipTo.matches(_interpreter, qualifiedBreakpoint)) { + replyData.compound["breakpoint"] = _skipTo.toData(); + replyData.compound["qualified"] = qualifiedBreakpoint.toData(); + breakExecution(replyData); + _skipTo = Breakpoint(); + } + continue; + } + + std::set<Breakpoint>::const_iterator breakpointIter = _breakPoints.begin(); + while(breakpointIter != _breakPoints.end()) { + const Breakpoint& breakpoint = *breakpointIter++; + if (!breakpoint.enabled) + continue; + if (breakpoint.matches(_interpreter, qualifiedBreakpoint)) { + // do we have a condition? + + replyData.compound["breakpoint"] = breakpoint.toData(); + replyData.compound["qualified"] = qualifiedBreakpoint.toData(); + + userBreakpointMatched = true; + breakExecution(replyData); + } + } + if (_isStepping && !userBreakpointMatched) { + replyData.compound["qualified"] = qualifiedBreakpoint.toData(); + breakExecution(replyData); + + } + } +} + +void DebugSession::breakExecution(Data replyData) { + tthread::lock_guard<tthread::recursive_mutex> lock(_mutex); + + Arabica::XPath::NodeSet<std::string> basicConf = _interpreter.getBasicConfiguration(); + for (int i = 0; i < basicConf.size(); i++) { + Arabica::DOM::Element<std::string> element = Arabica::DOM::Element<std::string>(basicConf[i]); + if (element.hasAttribute("id")) { + replyData.compound["basicStates"].array.push_back(Data(element.getAttribute("id"), Data::VERBATIM)); + } + } + + Arabica::XPath::NodeSet<std::string> activeConf = _interpreter.getConfiguration(); + for (int i = 0; i < activeConf.size(); i++) { + Arabica::DOM::Element<std::string> element = Arabica::DOM::Element<std::string>(activeConf[i]); + if (element.hasAttribute("id")) { + replyData.compound["activeStates"].array.push_back(Data(element.getAttribute("id"), Data::VERBATIM)); + } + } + + replyData.compound["replyType"] = Data("breakpoint", Data::VERBATIM); + _debugger->pushData(shared_from_this(), replyData); + _resumeCond.wait(_mutex); +} + +Data DebugSession::debugPrepare(const Data& data) { + Data replyData; + + if (!data.hasKey("xml") && !data.hasKey("url")) { + replyData.compound["status"] = Data("failure", Data::VERBATIM); + replyData.compound["reason"] = Data("No XML or URL given", Data::VERBATIM); + return replyData; + } + + debugStop(data); + + _isAttached = false; + + if (data.hasKey("xml")) { + _interpreter = Interpreter::fromXML(data["xml"].atom); + } else if (data.hasKey("url")) { + _interpreter = Interpreter::fromURI(data["url"].atom); + } else { + _interpreter = Interpreter(); + } + + if (_interpreter) { + // register ourself as a monitor + _interpreter.addMonitor(_debugger); + _debugger->attachSession(_interpreter, shared_from_this()); + if (data.hasKey("url")) { + // this allows to resolve relative external reources + _interpreter.setSourceURI(data["url"].atom); + } + replyData.compound["status"] = Data("success", Data::VERBATIM); + } else { + replyData.compound["status"] = Data("failure", Data::VERBATIM); + } + + return replyData; +} + +Data DebugSession::debugAttach(const Data& data) { + Data replyData; + _isAttached = true; + + if (!data.hasKey("attach")) { + replyData.compound["status"] = Data("failure", Data::VERBATIM); + replyData.compound["reason"] = Data("No id to attach to given", Data::VERBATIM); + return replyData; + } + + std::string interpreterId = data["attach"].atom; + bool interpreterFound = false; + + // find interpreter for sessionid + std::map<std::string, boost::weak_ptr<InterpreterImpl> > instances = Interpreter::getInstances(); + for (std::map<std::string, boost::weak_ptr<InterpreterImpl> >::iterator instIter = instances.begin(); + instIter != instances.end(); + instIter++) { + + boost::shared_ptr<InterpreterImpl> instance = instIter->second.lock(); + if (instance && instance->getSessionId() == interpreterId) { + _interpreter = instance; + _debugger->attachSession(_interpreter, shared_from_this()); + interpreterFound = true; + break; + } + } + + if (!interpreterFound) { + replyData.compound["status"] = Data("failure", Data::VERBATIM); + replyData.compound["reason"] = Data("No interpreter with given id found", Data::VERBATIM); + } else { + replyData.compound["xml"].node = _interpreter.getDocument(); + replyData.compound["status"] = Data("success", Data::VERBATIM); + } + + return replyData; +} + +Data DebugSession::debugDetach(const Data& data) { + Data replyData; + _isAttached = false; + + _debugger->detachSession(_interpreter); + replyData.compound["status"] = Data("success", Data::VERBATIM); + return replyData; +} + +Data DebugSession::debugStart(const Data& data) { + Data replyData; + + if (_isAttached) { + replyData.compound["reason"] = Data("Already started when attached", Data::VERBATIM); + replyData.compound["status"] = Data("failure", Data::VERBATIM); + } else if (!_interpreter) { + replyData.compound["reason"] = Data("No interpreter attached or loaded", Data::VERBATIM); + replyData.compound["status"] = Data("failure", Data::VERBATIM); + } else { + _interpreter.start(); + replyData.compound["status"] = Data("success", Data::VERBATIM); + } + + return replyData; +} + +Data DebugSession::debugStop(const Data& data) { + Data replyData; + + if (_interpreter) { + // detach from old intepreter + _debugger->detachSession(_interpreter); + } + + if (_interpreter && !_isAttached) + _interpreter.stop(); + // unblock + _resumeCond.notify_all(); + + _skipTo = Breakpoint(); + replyData.compound["status"] = Data("success", Data::VERBATIM); + + // calls destructor + _interpreter = Interpreter(); + + return replyData; +} + +Data DebugSession::debugStep(const Data& data) { + tthread::lock_guard<tthread::recursive_mutex> lock(_mutex); + + stepping(true); + _resumeCond.notify_one(); + + Data replyData; + if (_interpreter) { + // register ourself as a monitor + if (!_interpreter.isRunning()) + _interpreter.start(); + replyData.compound["status"] = Data("success", Data::VERBATIM); + } else { + replyData.compound["status"] = Data("failure", Data::VERBATIM); + } + return replyData; +} + +Data DebugSession::debugResume(const Data& data) { + tthread::lock_guard<tthread::recursive_mutex> lock(_mutex); + + stepping(false); + + Data replyData; + replyData.compound["status"] = Data("success", Data::VERBATIM); + + _resumeCond.notify_one(); + return replyData; +} + + +Data DebugSession::debugPause(const Data& data) { + tthread::lock_guard<tthread::recursive_mutex> lock(_mutex); + + _skipTo = Breakpoint(); + stepping(true); + + Data replyData; + replyData.compound["status"] = Data("success", Data::VERBATIM); + + return replyData; +} + +Data DebugSession::skipToBreakPoint(const Data& data) { + tthread::lock_guard<tthread::recursive_mutex> lock(_mutex); + + _skipTo = Breakpoint(data); + + Data replyData; + replyData.compound["status"] = Data("success", Data::VERBATIM); + + _resumeCond.notify_one(); + return replyData; +} + +Data DebugSession::addBreakPoint(const Data& data) { + Breakpoint breakpoint(data); + + Data replyData; + if (_breakPoints.find(breakpoint) == _breakPoints.end()) { + _breakPoints.insert(breakpoint); + replyData.compound["status"] = Data("success", Data::VERBATIM); + + } else { + replyData.compound["reason"] = Data("Breakpoint already exists", Data::VERBATIM); + replyData.compound["status"] = Data("failure", Data::VERBATIM); + } + return replyData; +} + +Data DebugSession::removeBreakPoint(const Data& data) { + Breakpoint breakpoint(data); + + Data replyData; + if (_breakPoints.find(breakpoint) != _breakPoints.end()) { + _breakPoints.erase(breakpoint); + replyData.compound["status"] = Data("success", Data::VERBATIM); + } else { + replyData.compound["reason"] = Data("No such breakpoint", Data::VERBATIM); + replyData.compound["status"] = Data("failure", Data::VERBATIM); + } + return replyData; +} + +Data DebugSession::enableBreakPoint(const Data& data) { + Breakpoint breakpoint(data); + + Data replyData; + if (_breakPoints.find(breakpoint) != _breakPoints.end()) { + _breakPoints.find(breakpoint)->enabled = true; + replyData.compound["status"] = Data("success", Data::VERBATIM); + } else { + replyData.compound["reason"] = Data("No such breakpoint", Data::VERBATIM); + replyData.compound["status"] = Data("failure", Data::VERBATIM); + } + + return replyData; +} +Data DebugSession::disableBreakPoint(const Data& data) { + Breakpoint breakpoint(data); + + Data replyData; + if (_breakPoints.find(breakpoint) != _breakPoints.end()) { + _breakPoints.find(breakpoint)->enabled = false; + replyData.compound["status"] = Data("success", Data::VERBATIM); + } else { + replyData.compound["reason"] = Data("No such breakpoint", Data::VERBATIM); + replyData.compound["status"] = Data("failure", Data::VERBATIM); + } + + return replyData; +} +Data DebugSession::enableAllBreakPoints() { + Data replyData; + + _breakpointsEnabled = true; + replyData.compound["status"] = Data("success", Data::VERBATIM); + + return replyData; +} +Data DebugSession::disableAllBreakPoints() { + Data replyData; + + _breakpointsEnabled = false; + replyData.compound["status"] = Data("success", Data::VERBATIM); + + return replyData; +} + +Data DebugSession::debugEval(const Data& data) { + Data replyData; + + if (!data.hasKey("expression")) { + replyData.compound["status"] = Data("failure", Data::VERBATIM); + replyData.compound["reason"] = Data("No expression given", Data::VERBATIM); + return replyData; + } + + std::string expr = data["expression"].atom; + + if (!_interpreter) { + replyData.compound["status"] = Data("failure", Data::VERBATIM); + replyData.compound["reason"] = Data("No interpreter running", Data::VERBATIM); + } else if (!_interpreter.getDataModel()) { + replyData.compound["status"] = Data("failure", Data::VERBATIM); + replyData.compound["reason"] = Data("No datamodel available", Data::VERBATIM); + } else { + try { + replyData.compound["eval"] = _interpreter.getDataModel().getStringAsData(expr); + } catch (Event e) { + replyData.compound["eval"] = e.data; + replyData.compound["eval"].compound["error"] = Data(e.name, Data::VERBATIM); + } + replyData.compound["status"] = Data("success", Data::VERBATIM); + } + return replyData; +} + + +}
\ No newline at end of file diff --git a/src/uscxml/debug/DebugSession.h b/src/uscxml/debug/DebugSession.h new file mode 100644 index 0000000..12f1d93 --- /dev/null +++ b/src/uscxml/debug/DebugSession.h @@ -0,0 +1,99 @@ +/** + * @file + * @author 2012-2014 Stefan Radomski (stefan.radomski@cs.tu-darmstadt.de) + * @copyright Simplified BSD + * + * @cond + * This program is free software: you can redistribute it and/or modify + * it under the terms of the FreeBSD license as published by the FreeBSD + * project. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the FreeBSD license along with this + * program. If not, see <http://www.opensource.org/licenses/bsd-license>. + * @endcond + */ + +#ifndef DEBUGSESSION_H_M8YHEGV6 +#define DEBUGSESSION_H_M8YHEGV6 + +#include "uscxml/debug/Breakpoint.h" +#include "uscxml/Interpreter.h" +#include <time.h> + +namespace uscxml { + +class Debugger; + +class USCXML_API DebugSession : public boost::enable_shared_from_this<DebugSession> { +public: + DebugSession() { + _isStepping = false; + _isAttached = false; + _breakpointsEnabled = true; + _markedForDeletion = false; + _debugger = NULL; + } + + void stepping(bool enable) { + _isStepping = enable; + } + + void checkBreakpoints(const std::list<Breakpoint> qualifiedBreakpoints); + + Data debugPrepare(const Data& data); + Data debugAttach(const Data& data); + Data debugDetach(const Data& data); + Data debugStart(const Data& data); + Data debugStop(const Data& data); + Data debugStep(const Data& data); + Data debugResume(const Data& data); + Data debugPause(const Data& data); + Data skipToBreakPoint(const Data& data); + Data addBreakPoint(const Data& data); + Data removeBreakPoint(const Data& data); + Data enableBreakPoint(const Data& data); + Data disableBreakPoint(const Data& data); + Data enableAllBreakPoints(); + Data disableAllBreakPoints(); + Data debugEval(const Data& data); + + void setDebugger(Debugger* debugger) { + _debugger = debugger; + } + + Interpreter getInterpreter() { + return _interpreter; + } + + void markForDeletion(bool mark) { + _markedForDeletion = mark; + } + +protected: + void breakExecution(Data replyData); + + bool _isStepping; + bool _isAttached; + bool _breakpointsEnabled; + + tthread::condition_variable _resumeCond; + tthread::recursive_mutex _runMutex; + tthread::recursive_mutex _mutex; + + bool _markedForDeletion; + Debugger* _debugger; + Interpreter _interpreter; + std::set<Breakpoint> _breakPoints; + Breakpoint _skipTo; + +}; + + +} + + +#endif /* end of include guard: DEBUGSESSION_H_M8YHEGV6 */ diff --git a/src/uscxml/debug/Debugger.cpp b/src/uscxml/debug/Debugger.cpp index aa97a22..3a19228 100644 --- a/src/uscxml/debug/Debugger.cpp +++ b/src/uscxml/debug/Debugger.cpp @@ -23,9 +23,13 @@ namespace uscxml { void Debugger::afterCompletion(Interpreter interpreter) { + boost::shared_ptr<DebugSession> session = getSession(interpreter); + if (!session) + return; + Data msg; msg.compound["replyType"] = Data("finished", Data::VERBATIM); - pushData(msg); + pushData(session, msg); } std::list<Breakpoint> getQualifiedStateBreakpoints(Interpreter interpreter, const Arabica::DOM::Element<std::string>& state, Breakpoint breakpointTemplate) { @@ -33,6 +37,7 @@ std::list<Breakpoint> getQualifiedStateBreakpoints(Interpreter interpreter, cons Breakpoint bp = breakpointTemplate; // copy base as template bp.stateId = ATTR(state, "id"); + bp.element = state; bp.subject = Breakpoint::STATE; breakpoints.push_back(bp); @@ -44,6 +49,7 @@ std::list<Breakpoint> getQualifiedInvokeBreakpoints(Interpreter interpreter, con Breakpoint bp = breakpointTemplate; // copy base as template bp.subject = Breakpoint::INVOKER; + bp.element = invokeElem; bp.invokeId = invokeId; if (HAS_ATTR(invokeElem, "type")) { @@ -67,6 +73,7 @@ std::list<Breakpoint> getQualifiedTransBreakpoints(Interpreter interpreter, cons Arabica::DOM::Element<std::string> target(targets[j]); Breakpoint bp = breakpointTemplate; // copy base as template + bp.element = transition; bp.transSource = ATTR(source, "id"); bp.transTarget = ATTR(target, "id"); bp.subject = Breakpoint::TRANSITION; @@ -83,6 +90,12 @@ void Debugger::beforeTakingTransition(Interpreter interpreter, const Arabica::DO void Debugger::afterTakingTransition(Interpreter interpreter, const Arabica::DOM::Element<std::string>& transition) { handleTransition(interpreter, transition, Breakpoint::AFTER); } +void Debugger::beforeExecutingContent(Interpreter interpreter, const Arabica::DOM::Node<std::string>& content) { + handleExecutable(interpreter, Arabica::DOM::Element<std::string>(content), Breakpoint::BEFORE); +} +void Debugger::afterExecutingContent(Interpreter interpreter, const Arabica::DOM::Node<std::string>& content) { + handleExecutable(interpreter, Arabica::DOM::Element<std::string>(content), Breakpoint::AFTER); +} void Debugger::beforeExitingState(Interpreter interpreter, const Arabica::DOM::Element<std::string>& state) { handleState(interpreter, state, Breakpoint::BEFORE, Breakpoint::EXIT); } @@ -120,9 +133,34 @@ void Debugger::beforeProcessingEvent(Interpreter interpreter, const Event& event handleEvent(interpreter, event, Breakpoint::BEFORE); } +void Debugger::handleExecutable(Interpreter interpreter, + const Arabica::DOM::Element<std::string>& execContentElem, + Breakpoint::When when) { + if (!interpreter.isRunning()) + return; + boost::shared_ptr<DebugSession> session = getSession(interpreter); + if (!session) + return; + + std::list<Breakpoint> breakpoints; + + Breakpoint breakpoint; + breakpoint.when = when; + breakpoint.element = execContentElem; + breakpoint.executableName = execContentElem.getLocalName(); + breakpoint.subject = Breakpoint::EXECUTABLE; + breakpoints.push_back(breakpoint); + + session->checkBreakpoints(breakpoints); + +} + void Debugger::handleEvent(Interpreter interpreter, const Event& event, Breakpoint::When when) { if (!interpreter.isRunning()) return; + boost::shared_ptr<DebugSession> session = getSession(interpreter); + if (!session) + return; std::list<Breakpoint> breakpoints; @@ -132,13 +170,16 @@ void Debugger::handleEvent(Interpreter interpreter, const Event& event, Breakpoi breakpoint.subject = Breakpoint::EVENT; breakpoints.push_back(breakpoint); - checkBreakpoints(interpreter, breakpoints); + session->checkBreakpoints(breakpoints); } void Debugger::handleStable(Interpreter interpreter, Breakpoint::When when) { if (!interpreter.isRunning()) return; + boost::shared_ptr<DebugSession> session = getSession(interpreter); + if (!session) + return; std::list<Breakpoint> breakpoints; @@ -147,13 +188,16 @@ void Debugger::handleStable(Interpreter interpreter, Breakpoint::When when) { breakpoint.subject = Breakpoint::STABLE; breakpoints.push_back(breakpoint); - checkBreakpoints(interpreter, breakpoints); + session->checkBreakpoints(breakpoints); } void Debugger::handleMicrostep(Interpreter interpreter, Breakpoint::When when) { if (!interpreter.isRunning()) return; - + boost::shared_ptr<DebugSession> session = getSession(interpreter); + if (!session) + return; + std::list<Breakpoint> breakpoints; Breakpoint breakpoint; @@ -161,86 +205,50 @@ void Debugger::handleMicrostep(Interpreter interpreter, Breakpoint::When when) { breakpoint.subject = Breakpoint::MICROSTEP; breakpoints.push_back(breakpoint); - checkBreakpoints(interpreter, breakpoints); + session->checkBreakpoints(breakpoints); } void Debugger::handleTransition(Interpreter interpreter, const Arabica::DOM::Element<std::string>& transition, Breakpoint::When when) { if (!interpreter.isRunning()) return; + boost::shared_ptr<DebugSession> session = getSession(interpreter); + if (!session) + return; Breakpoint breakpointTemplate; breakpointTemplate.when = when; std::list<Breakpoint> qualifiedBreakpoints = getQualifiedTransBreakpoints(interpreter, transition, breakpointTemplate); - checkBreakpoints(interpreter, qualifiedBreakpoints); + session->checkBreakpoints(qualifiedBreakpoints); } void Debugger::handleState(Interpreter interpreter, const Arabica::DOM::Element<std::string>& state, Breakpoint::When when, Breakpoint::Action action) { if (!interpreter.isRunning()) return; + boost::shared_ptr<DebugSession> session = getSession(interpreter); + if (!session) + return; Breakpoint breakpointTemplate; breakpointTemplate.when = when; breakpointTemplate.action = action; std::list<Breakpoint> qualifiedBreakpoints = getQualifiedStateBreakpoints(interpreter, state, breakpointTemplate); - checkBreakpoints(interpreter, qualifiedBreakpoints); + session->checkBreakpoints(qualifiedBreakpoints); } void Debugger::handleInvoke(Interpreter interpreter, const Arabica::DOM::Element<std::string>& invokeElem, const std::string& invokeId, Breakpoint::When when, Breakpoint::Action action) { if (!interpreter.isRunning()) return; + boost::shared_ptr<DebugSession> session = getSession(interpreter); + if (!session) + return; Breakpoint breakpointTemplate; breakpointTemplate.when = when; breakpointTemplate.action = action; std::list<Breakpoint> qualifiedBreakpoints = getQualifiedInvokeBreakpoints(interpreter, invokeElem, invokeId, breakpointTemplate); - checkBreakpoints(interpreter, qualifiedBreakpoints); - -} - -void Debugger::checkBreakpoints(Interpreter interpreter, const std::list<Breakpoint> qualifiedBreakpoints) { - std::list<Breakpoint>::const_iterator qualifiedBreakpointIter = qualifiedBreakpoints.begin(); - while(qualifiedBreakpointIter != qualifiedBreakpoints.end()) { - const Breakpoint& qualifiedBreakpoint = *qualifiedBreakpointIter++; - - // check if one of the user-supplied breakpoints match - bool userBreakpointMatched = false; - std::set<Breakpoint>::const_iterator breakpointIter = _breakPoints.begin(); - while(breakpointIter != _breakPoints.end()) { - const Breakpoint& breakpoint = *breakpointIter++; - if (breakpoint.matches(qualifiedBreakpoint)) { - Data replyData; - replyData.compound["breakpoint"] = breakpoint.toData(); - replyData.compound["qualified"] = qualifiedBreakpoint.toData(); - - userBreakpointMatched = true; - hitBreakpoint(interpreter, replyData); - } - } - if (_isStepping && !userBreakpointMatched) { - Data replyData; - replyData.compound["qualified"] = qualifiedBreakpoint.toData(); - hitBreakpoint(interpreter, replyData); - } - } -} + session->checkBreakpoints(qualifiedBreakpoints); -void Debugger::send(google::LogSeverity severity, const char* full_filename, - const char* base_filename, int line, - const struct ::tm* tm_time, - const char* message, size_t message_len) { - - // _sendQueue is thread-safe, not sure about ToString though - - LogMessage msg(severity, - full_filename, - base_filename, - line, - tm_time, - std::string(message, message_len), - ToString(severity, base_filename, line, tm_time, message, message_len)); - msg.compound["replyType"] = Data("log", Data::VERBATIM); - pushData(msg); } diff --git a/src/uscxml/debug/Debugger.h b/src/uscxml/debug/Debugger.h index dfc197d..c49e90f 100644 --- a/src/uscxml/debug/Debugger.h +++ b/src/uscxml/debug/Debugger.h @@ -23,45 +23,40 @@ #include "uscxml/Message.h" #include "uscxml/Interpreter.h" #include "uscxml/debug/Breakpoint.h" - -#include <glog/logging.h> +#include "uscxml/debug/DebugSession.h" namespace uscxml { -class USCXML_API Debugger : public InterpreterMonitor, public google::LogSink { +class USCXML_API Debugger : public InterpreterMonitor { public: Debugger() { - _isStepping = false; } virtual ~Debugger() {} - - class LogMessage : public Data { - public: - LogMessage(google::LogSeverity severity, const char* full_filename, - const char* base_filename, int line, - const struct ::tm* tm_time, - std::string message, std::string formatted) { - - compound["severity"] = severity; - compound["fullFilename"] = Data(full_filename, Data::VERBATIM); - compound["baseFilename"] = Data(base_filename, Data::VERBATIM); - compound["line"] = line; - compound["message"] = Data(message, Data::VERBATIM); - compound["time"] = Data(mktime((struct ::tm*)tm_time), Data::INTERPRETED); - compound["formatted"] = Data(formatted, Data::VERBATIM); - } - }; - virtual void pushData(Data pushData) = 0; - virtual void hitBreakpoint(const Interpreter& interpreter, Data data) = 0; + virtual void attachSession(Interpreter interpreter, boost::shared_ptr<DebugSession> session) { + tthread::lock_guard<tthread::recursive_mutex> lock(_sessionMutex); + _sessionForInterpreter[interpreter] = session; + } + + virtual void detachSession(Interpreter interpreter) { + tthread::lock_guard<tthread::recursive_mutex> lock(_sessionMutex); + _sessionForInterpreter.erase(interpreter); + } - void stepping(bool enable) { - _isStepping = enable; + virtual boost::shared_ptr<DebugSession> getSession(Interpreter interpreter) { + tthread::lock_guard<tthread::recursive_mutex> lock(_sessionMutex); + if (_sessionForInterpreter.find(interpreter) != _sessionForInterpreter.end()) + return _sessionForInterpreter[interpreter]; + return boost::shared_ptr<DebugSession>(); } + virtual void pushData(boost::shared_ptr<DebugSession> session, Data pushData) = 0; + // InterpreterMonitor virtual void beforeProcessingEvent(Interpreter interpreter, const Event& event); virtual void beforeMicroStep(Interpreter interpreter); + virtual void beforeExecutingContent(Interpreter interpreter, const Arabica::DOM::Node<std::string>& content); + virtual void afterExecutingContent(Interpreter interpreter, const Arabica::DOM::Node<std::string>& content); virtual void beforeExitingState(Interpreter interpreter, const Arabica::DOM::Element<std::string>& state); virtual void afterExitingState(Interpreter interpreter, const Arabica::DOM::Element<std::string>& state); virtual void beforeUninvoking(Interpreter interpreter, const Arabica::DOM::Element<std::string>& invokeElem, const std::string& invokeid); @@ -79,12 +74,6 @@ public: virtual void beforeCompletion(Interpreter interpreter) {} virtual void afterCompletion(Interpreter interpreter); - // Logsink - virtual void send(google::LogSeverity severity, const char* full_filename, - const char* base_filename, int line, - const struct ::tm* tm_time, - const char* message, size_t message_len); - protected: void handleTransition(Interpreter interpreter, @@ -99,14 +88,15 @@ protected: const std::string& invokeId, Breakpoint::When when, Breakpoint::Action action); + void handleExecutable(Interpreter interpreter, + const Arabica::DOM::Element<std::string>& execContentElem, + Breakpoint::When when); void handleStable(Interpreter interpreter, Breakpoint::When when); void handleMicrostep(Interpreter interpreter, Breakpoint::When when); void handleEvent(Interpreter interpreter, const Event& event, Breakpoint::When when); - void checkBreakpoints(Interpreter interpreter, const std::list<Breakpoint> qualifiedBreakpoints); - bool _isStepping; - std::set<Breakpoint> _breakPoints; - + tthread::recursive_mutex _sessionMutex; + std::map<Interpreter, boost::shared_ptr<DebugSession> > _sessionForInterpreter; }; } diff --git a/src/uscxml/debug/DebuggerServlet.cpp b/src/uscxml/debug/DebuggerServlet.cpp index e179b8c..55ced75 100644 --- a/src/uscxml/debug/DebuggerServlet.cpp +++ b/src/uscxml/debug/DebuggerServlet.cpp @@ -23,25 +23,30 @@ namespace uscxml { -void DebuggerServlet::pushData(Data pushData) { +void DebuggerServlet::pushData(boost::shared_ptr<DebugSession> session, Data pushData) { std::cout << "trying to push " << pushData["replyType"].atom << std::endl; - _sendQueue.push(pushData); - serverPushData(); + + if (!session) { + if (_sendQueues.size() > 0) // logging is not aware of its interpreter + _sendQueues.begin()->second.push(pushData); + } else { + _sendQueues[session].push(pushData); + } + + serverPushData(session); } -void DebuggerServlet::serverPushData() { - tthread::lock_guard<tthread::recursive_mutex> lock(_mutex); - - if (_sendQueue.isEmpty()) +void DebuggerServlet::serverPushData(boost::shared_ptr<DebugSession> session) { + if (_sendQueues[session].isEmpty()) return; - if (!_clientConn) + if (!_clientConns[session]) return; - Data reply = _sendQueue.pop(); + Data reply = _sendQueues[session].pop(); std::cout << "pushing " << reply["replyType"].atom << std::endl; - returnData(_clientConn, reply); - _clientConn = HTTPServer::Request(); + returnData(_clientConns[session], reply); + _clientConns[session] = HTTPServer::Request(); } void DebuggerServlet::returnData(const HTTPServer::Request& request, Data replyData) { @@ -56,17 +61,6 @@ void DebuggerServlet::returnData(const HTTPServer::Request& request, Data replyD reply.headers["Content-Type"] = "application/json"; HTTPServer::reply(reply); } - -void DebuggerServlet::hitBreakpoint(const Interpreter& interpreter, - Data data) { - tthread::lock_guard<tthread::recursive_mutex> lock(_mutex); - - data.compound["replyType"] = Data("breakpoint", Data::VERBATIM); - pushData(data); - - _resumeCond.wait(_mutex); - tthread::this_thread::sleep_for(tthread::chrono::milliseconds(200)); -} bool DebuggerServlet::isCORS(const HTTPServer::Request& request) { return (request.data["type"].atom == "options" && @@ -101,220 +95,164 @@ bool DebuggerServlet::httpRecvRequest(const HTTPServer::Request& request) { std::cout << request.data["path"] << ": " << request.data["content"] << std::endl; + Data replyData; + // process request that don't need a session if (false) { - } else if (boost::starts_with(request.data["path"].atom, "/poll")) { - processPoll(request); - } else if (boost::starts_with(request.data["path"].atom, "/connect")) { + } else if (boost::starts_with(request.data["path"].atom, "/debug/connect")) { processConnect(request); - } else if (boost::starts_with(request.data["path"].atom, "/disconnect")) { - processDisconnect(request); - } else if (boost::starts_with(request.data["path"].atom, "/sessions")) { + return true; + } else if (boost::starts_with(request.data["path"].atom, "/debug/sessions")) { processListSessions(request); - } else if (boost::starts_with(request.data["path"].atom, "/breakpoint/add")) { - processAddBreakPoint(request); - } else if (boost::starts_with(request.data["path"].atom, "/breakpoint/remove")) { - processRemoveBreakPoint(request); + return true; + } + + // get session or return error + if (false) { + } else if (!request.data["content"].hasKey("session")) { + replyData.compound["status"] = Data("failure", Data::VERBATIM); + replyData.compound["reason"] = Data("No session given", Data::VERBATIM); + } else if (_sessionForId.find(request.data["content"]["session"].atom) == _sessionForId.end()) { + replyData.compound["status"] = Data("failure", Data::VERBATIM); + replyData.compound["reason"] = Data("No such session", Data::VERBATIM); + } + if (replyData) { + returnData(request, replyData); + return true; + } + + boost::shared_ptr<DebugSession> session = _sessionForId[request.data["content"]["session"].atom]; + + if (false) { + } else if (boost::starts_with(request.data["path"].atom, "/debug/poll")) { + // save long-standing client poll + _clientConns[session] = request; + serverPushData(session); + + } else if (boost::starts_with(request.data["path"].atom, "/debug/disconnect")) { + processDisconnect(request); + + } else if (boost::starts_with(request.data["path"].atom, "/debug/breakpoint/enable/all")) { + replyData = session->enableAllBreakPoints(); + } else if (boost::starts_with(request.data["path"].atom, "/debug/breakpoint/disable/all")) { + replyData = session->disableAllBreakPoints(); + } else if (boost::starts_with(request.data["path"].atom, "/debug/breakpoint/skipto")) { + replyData = session->skipToBreakPoint(request.data["content"]); + } else if (boost::starts_with(request.data["path"].atom, "/debug/breakpoint/add")) { + replyData = session->addBreakPoint(request.data["content"]); + } else if (boost::starts_with(request.data["path"].atom, "/debug/breakpoint/remove")) { + replyData = session->removeBreakPoint(request.data["content"]); + } else if (boost::starts_with(request.data["path"].atom, "/debug/breakpoint/enable")) { + replyData = session->enableBreakPoint(request.data["content"]); + } else if (boost::starts_with(request.data["path"].atom, "/debug/breakpoint/disable")) { + replyData = session->disableBreakPoint(request.data["content"]); + } else if (boost::starts_with(request.data["path"].atom, "/debug/stop")) { + replyData = session->debugStop(request.data["content"]); } else if (boost::starts_with(request.data["path"].atom, "/debug/prepare")) { - processDebugPrepare(request); + replyData = session->debugPrepare(request.data["content"]); + } else if (boost::starts_with(request.data["path"].atom, "/debug/attach")) { + replyData = session->debugAttach(request.data["content"]); } else if (boost::starts_with(request.data["path"].atom, "/debug/start")) { - processDebugStart(request); - } else if (boost::starts_with(request.data["path"].atom, "/debug/stop")) { - processDebugStop(request); + replyData = session->debugStart(request.data["content"]); } else if (boost::starts_with(request.data["path"].atom, "/debug/step")) { - processDebugStep(request); + replyData = session->debugStep(request.data["content"]); } else if (boost::starts_with(request.data["path"].atom, "/debug/pause")) { - processDebugPause(request); + replyData = session->debugPause(request.data["content"]); } else if (boost::starts_with(request.data["path"].atom, "/debug/resume")) { - processDebugResume(request); + replyData = session->debugResume(request.data["content"]); } else if (boost::starts_with(request.data["path"].atom, "/debug/eval")) { - processDebugEval(request); + replyData = session->debugEval(request.data["content"]); } - return true; -} - -void DebuggerServlet::processPoll(const HTTPServer::Request& request) { - tthread::lock_guard<tthread::recursive_mutex> lock(_mutex); - _clientConn = request; - serverPushData(); -} - -void DebuggerServlet::processDebugPrepare(const HTTPServer::Request& request) { - tthread::lock_guard<tthread::recursive_mutex> lock(_mutex); - -// std::cout << "clearing all pushes" << std::endl; -// _sendQueue.clear(); - - // this will call the destructor if _interpreter already is set - _resumeCond.notify_all(); - _interpreter = Interpreter::fromXML(request.data["content"].atom); - - Data replyData; - if (_interpreter) { - // register ourself as a monitor - _interpreter.addMonitor(this); - replyData.compound["status"] = Data("success", Data::VERBATIM); - } else { - replyData.compound["status"] = Data("failure", Data::VERBATIM); + if (replyData) { + returnData(request, replyData); + return true; } - returnData(request, replyData); + + return true; } -void DebuggerServlet::processDebugStart(const HTTPServer::Request& request) { +// someone connected, create a new session +void DebuggerServlet::processConnect(const HTTPServer::Request& request) { tthread::lock_guard<tthread::recursive_mutex> lock(_mutex); + std::string sessionId = UUID::getUUID(); + + _sessionForId[sessionId] = boost::shared_ptr<DebugSession>(new DebugSession()); + _sessionForId[sessionId]->setDebugger(this); Data replyData; - if (_interpreter) { - // register ourself as a monitor - _interpreter.start(); - replyData.compound["status"] = Data("success", Data::VERBATIM); - } else { - replyData.compound["status"] = Data("failure", Data::VERBATIM); - } + replyData.compound["session"] = Data(sessionId, Data::VERBATIM); + replyData.compound["status"] = Data("success", Data::VERBATIM); returnData(request, replyData); } -void DebuggerServlet::processDebugStop(const HTTPServer::Request& request) { -// tthread::lock_guard<tthread::recursive_mutex> lock(_mutex); - - stepping(false); +void DebuggerServlet::processDisconnect(const HTTPServer::Request& request) { + tthread::lock_guard<tthread::recursive_mutex> lock(_mutex); Data replyData; - if (_interpreter) { - _interpreter.stop(); - _resumeCond.notify_all(); // unblock breakpoints - _interpreter.join(); - _interpreter = Interpreter(); // empty interpreter, calls destructor - replyData.compound["status"] = Data("success", Data::VERBATIM); - } else { - replyData.compound["status"] = Data("failure", Data::VERBATIM); - replyData.compound["reason"] = Data("Interpreter already stopped", Data::VERBATIM); - } - returnData(request, replyData); -} -void DebuggerServlet::processDebugEval(const HTTPServer::Request& request) { - Data replyData; - if (!_interpreter) { - replyData.compound["status"] = Data("failure", Data::VERBATIM); - replyData.compound["reason"] = Data("No interpreter running", Data::VERBATIM); - } else if (!_interpreter.getDataModel()) { + if (!request.data["content"].hasKey("session")) { replyData.compound["status"] = Data("failure", Data::VERBATIM); - replyData.compound["reason"] = Data("No datamodel available", Data::VERBATIM); - } else if (!request.data["content"].hasKey("expression")) { - replyData.compound["status"] = Data("failure", Data::VERBATIM); - replyData.compound["reason"] = Data("No expression given", Data::VERBATIM); - } else { - std::string expr = request.data["content"]["expression"].atom; - try { - replyData.compound["eval"] = _interpreter.getDataModel().getStringAsData(expr); - } catch (Event e) { - replyData.compound["eval"] = e.data; - replyData.compound["eval"].compound["error"] = Data(e.name, Data::VERBATIM); - } - replyData.compound["status"] = Data("success", Data::VERBATIM); + replyData.compound["reason"] = Data("No session given", Data::VERBATIM); + returnData(request, replyData); } - returnData(request, replyData); -} -void DebuggerServlet::processDebugStep(const HTTPServer::Request& request) { - tthread::lock_guard<tthread::recursive_mutex> lock(_mutex); + std::string sessionId = request.data["content"]["session"].atom; - stepping(true); - _resumeCond.notify_one(); - - Data replyData; - if (_interpreter && !_interpreter.isRunning()) { - // register ourself as a monitor - _interpreter.start(); - replyData.compound["status"] = Data("success", Data::VERBATIM); - } else { + if (_sessionForId.find(sessionId) == _sessionForId.end()) { replyData.compound["status"] = Data("failure", Data::VERBATIM); + replyData.compound["reason"] = Data("No such session", Data::VERBATIM); + } else { + replyData.compound["status"] = Data("success", Data::VERBATIM); + detachSession(_sessionForId[sessionId]->getInterpreter()); + _sessionForId[sessionId]->debugStop(request.data["content"]); + _clientConns.erase(_sessionForId[sessionId]); + _sendQueues.erase(_sessionForId[sessionId]); + _sessionForId.erase(sessionId); } - returnData(request, replyData); - -} - -void DebuggerServlet::processDebugResume(const HTTPServer::Request& request) { - tthread::lock_guard<tthread::recursive_mutex> lock(_mutex); - - stepping(false); - - Data replyData; - replyData.compound["status"] = Data("success", Data::VERBATIM); - returnData(request, replyData); - - _resumeCond.notify_one(); -} - -void DebuggerServlet::processDebugPause(const HTTPServer::Request& request) { - tthread::lock_guard<tthread::recursive_mutex> lock(_mutex); - - Data replyData; - replyData.compound["status"] = Data("success", Data::VERBATIM); - returnData(request, replyData); -} - -void DebuggerServlet::processConnect(const HTTPServer::Request& request) { - tthread::lock_guard<tthread::recursive_mutex> lock(_mutex); - _sessionId = UUID::getUUID(); - _breakPoints.clear(); -// _sendQueue.clear(); - Data replyData; - replyData.compound["session"] = Data(_sessionId, Data::VERBATIM); - replyData.compound["status"] = Data("success", Data::VERBATIM); returnData(request, replyData); } void DebuggerServlet::processListSessions(const HTTPServer::Request& request) { Data replyData; - - // TODO: return actual data - Data sessionData; - sessionData.compound["name"] = Data("Not actually a Session", Data::VERBATIM); - sessionData.compound["id"] = Data("23452523-wg23g2g2-234t2g-23g2g", Data::VERBATIM); - replyData.compound["sessions"].array.push_back(sessionData); - - sessionData.compound["name"] = Data("But returned from the server!", Data::VERBATIM); - sessionData.compound["id"] = Data("swfgsgfw-g232vqvq-234t2g-23g2g", Data::VERBATIM); - replyData.compound["sessions"].array.push_back(sessionData); - - replyData.compound["status"] = Data("success", Data::VERBATIM); - returnData(request, replyData); -} - -void DebuggerServlet::processDisconnect(const HTTPServer::Request& request) { - Data replyData; - replyData.compound["status"] = Data("success", Data::VERBATIM); - returnData(request, replyData); -} - -void DebuggerServlet::processAddBreakPoint(const HTTPServer::Request& request) { - Breakpoint breakPoint(request.data["content"]); - Data replyData; - if (breakPoint.isValid()) { - replyData.compound["status"] = Data("success", Data::VERBATIM); - - if (_breakPoints.find(breakPoint) == _breakPoints.end()) { - _breakPoints.insert(breakPoint); + + std::map<std::string, boost::weak_ptr<InterpreterImpl> > instances = Interpreter::getInstances(); + for (std::map<std::string, boost::weak_ptr<InterpreterImpl> >::iterator instIter = instances.begin(); + instIter != instances.end(); + instIter++) { + + boost::shared_ptr<InterpreterImpl> instance = instIter->second.lock(); + if (instance) { + Data sessionData; + sessionData.compound["name"] = Data(instance->getName(), Data::VERBATIM); + sessionData.compound["id"] = Data(instance->getSessionId(), Data::VERBATIM); + sessionData.compound["source"] = Data(instance->getSourceURI().asString(), Data::VERBATIM); + sessionData.compound["xml"].node = instance->getDocument(); + + replyData.compound["sessions"].array.push_back(sessionData); } - } else { - replyData.compound["status"] = Data("failure", Data::VERBATIM); } + + replyData.compound["status"] = Data("success", Data::VERBATIM); returnData(request, replyData); } -void DebuggerServlet::processRemoveBreakPoint(const HTTPServer::Request& request) { - Breakpoint breakPoint(request.data["content"]); - Data replyData; - if (_breakPoints.erase(breakPoint) > 0) { - replyData.compound["status"] = Data("success", Data::VERBATIM); - } else { - replyData.compound["message"] = Data("No such breakpoint", Data::VERBATIM); - replyData.compound["status"] = Data("failure", Data::VERBATIM); - } - returnData(request, replyData); +void DebuggerServlet::send(google::LogSeverity severity, const char* full_filename, + const char* base_filename, int line, + const struct ::tm* tm_time, + const char* message, size_t message_len) { + + // _sendQueue is thread-safe, not sure about ToString though + + LogMessage msg(severity, + full_filename, + base_filename, + line, + tm_time, + std::string(message, message_len), + ToString(severity, base_filename, line, tm_time, message, message_len)); + msg.compound["replyType"] = Data("log", Data::VERBATIM); + pushData(boost::shared_ptr<DebugSession>(), msg); } diff --git a/src/uscxml/debug/DebuggerServlet.h b/src/uscxml/debug/DebuggerServlet.h index 5cd0be9..ae5178f 100644 --- a/src/uscxml/debug/DebuggerServlet.h +++ b/src/uscxml/debug/DebuggerServlet.h @@ -22,6 +22,7 @@ #include "uscxml/Common.h" #include "getopt.h" +#include <glog/logging.h> #include "uscxml/server/HTTPServer.h" #include "uscxml/Interpreter.h" @@ -31,8 +32,25 @@ namespace uscxml { -class USCXML_API DebuggerServlet : public Debugger, public HTTPServlet { +class USCXML_API DebuggerServlet : public Debugger, public HTTPServlet, public google::LogSink { public: + class LogMessage : public Data { + public: + LogMessage(google::LogSeverity severity, const char* full_filename, + const char* base_filename, int line, + const struct ::tm* tm_time, + std::string message, std::string formatted) { + + compound["severity"] = severity; + compound["fullFilename"] = Data(full_filename, Data::VERBATIM); + compound["baseFilename"] = Data(base_filename, Data::VERBATIM); + compound["line"] = line; + compound["message"] = Data(message, Data::VERBATIM); + compound["time"] = Data(mktime((struct ::tm*)tm_time), Data::INTERPRETED); + compound["formatted"] = Data(formatted, Data::VERBATIM); + } + }; + virtual ~DebuggerServlet() {} // from Debugger @@ -46,36 +64,43 @@ public: _url = url; } - void pushData(Data pushData); + void pushData(boost::shared_ptr<DebugSession> session, Data pushData); void returnData(const HTTPServer::Request& request, Data replyData); - void hitBreakpoint(const Interpreter& interpreter, - Data data); - - void processDebugEval(const HTTPServer::Request& request); - void processDebugPrepare(const HTTPServer::Request& request); - void processDebugStart(const HTTPServer::Request& request); - void processDebugStop(const HTTPServer::Request& request); - void processDebugStep(const HTTPServer::Request& request); - void processDebugResume(const HTTPServer::Request& request); - void processDebugPause(const HTTPServer::Request& request); + void processDisconnect(const HTTPServer::Request& request); void processConnect(const HTTPServer::Request& request); void processListSessions(const HTTPServer::Request& request); - void processDisconnect(const HTTPServer::Request& request); - void processAddBreakPoint(const HTTPServer::Request& request); - void processRemoveBreakPoint(const HTTPServer::Request& request); - void processPoll(const HTTPServer::Request& request); + +// void processDebugPrepare(const HTTPServer::Request& request); +// void processDebugAttach(const HTTPServer::Request& request); +// void processDebugStart(const HTTPServer::Request& request); +// void processDebugStop(const HTTPServer::Request& request); + +// void processDebugEval(const HTTPServer::Request& request); +// void processDebugStart(const HTTPServer::Request& request); +// void processDebugStop(const HTTPServer::Request& request); +// void processDebugStep(const HTTPServer::Request& request); +// void processDebugResume(const HTTPServer::Request& request); +// void processDebugPause(const HTTPServer::Request& request); +// void processAddBreakPoint(const HTTPServer::Request& request); +// void processRemoveBreakPoint(const HTTPServer::Request& request); +// void processPoll(const HTTPServer::Request& request); + // Logsink + virtual void send(google::LogSeverity severity, const char* full_filename, + const char* base_filename, int line, + const struct ::tm* tm_time, + const char* message, size_t message_len); + protected: - void serverPushData(); + void serverPushData(boost::shared_ptr<DebugSession>); - Interpreter _interpreter; - std::string _sessionId; std::string _url; - HTTPServer::Request _clientConn; - tthread::condition_variable _resumeCond; + std::map<boost::shared_ptr<DebugSession>, HTTPServer::Request> _clientConns; + std::map<boost::shared_ptr<DebugSession>, concurrency::BlockingQueue<Data> > _sendQueues; + std::map<std::string, boost::shared_ptr<DebugSession> > _sessionForId; + tthread::recursive_mutex _mutex; - concurrency::BlockingQueue<Data> _sendQueue; }; } diff --git a/src/uscxml/plugins/ioprocessor/basichttp/BasicHTTPIOProcessor.cpp b/src/uscxml/plugins/ioprocessor/basichttp/BasicHTTPIOProcessor.cpp index 51e1e28..085d919 100644 --- a/src/uscxml/plugins/ioprocessor/basichttp/BasicHTTPIOProcessor.cpp +++ b/src/uscxml/plugins/ioprocessor/basichttp/BasicHTTPIOProcessor.cpp @@ -263,7 +263,7 @@ void BasicHTTPIOProcessor::downloadCompleted(const URL& url) { std::map<std::string, std::pair<URL, SendRequest> >::iterator reqIter = _sendRequests.begin(); while(reqIter != _sendRequests.end()) { if (reqIter->second.first == url) { - // test 513 + // test513 std::string statusCode = url.getStatusCode(); if (statusCode.length() > 0) { std::string statusPrefix = statusCode.substr(0,1); diff --git a/src/uscxml/server/HTTPServer.cpp b/src/uscxml/server/HTTPServer.cpp index 6c26811..69096fc 100644 --- a/src/uscxml/server/HTTPServer.cpp +++ b/src/uscxml/server/HTTPServer.cpp @@ -105,7 +105,7 @@ HTTPServer::HTTPServer(unsigned short port, unsigned short wsPort, SSLConfig* ss LOG(ERROR) << "HTTP server cannot bind to tcp/" << _port; } } - + _wsPort = wsPort; if (_wsPort > 0) { _wsHandle = evws_bind_socket(_evws, _wsPort); diff --git a/test/samples/uscxml/test-dirmon.scxml b/test/samples/uscxml/test-dirmon.scxml index 872757f..1fba300 100644 --- a/test/samples/uscxml/test-dirmon.scxml +++ b/test/samples/uscxml/test-dirmon.scxml @@ -2,7 +2,7 @@ <script src="http://uscxml.tk.informatik.tu-darmstadt.de/scripts/dump.js" /> <state id="start"> <invoke type="dirmon"> - <param name="dir" expr="'.'" /> + <param name="dir" expr="'./audio'" /> <param name="recurse" expr="'true'" /> <param name="reportexisting" expr="'true'" /> <param name="suffix" expr="'.scxml'" /> diff --git a/test/samples/w3c/draft/calc.scxml b/test/samples/w3c/draft/calc.scxml new file mode 100644 index 0000000..e854401 --- /dev/null +++ b/test/samples/w3c/draft/calc.scxml @@ -0,0 +1,158 @@ +<?xml version="1.0" ?> +<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" + initial="on" datamodel="ecmascript" name="calc"> + <datamodel> + <data id="long_expr" /> + <data id="short_expr" expr="0" /> + <data id="res" /> + </datamodel> + <state id="wrapper" initial="on"> + <state id="on" initial="ready"> + <onentry> + <send event="DISPLAY.UPDATE" /> + </onentry> + <state id="ready" initial="begin"> + <state id="begin"> + <transition event="OPER.MINUS" target="negated1" /> + <onentry> + <send event="DISPLAY.UPDATE" /> + </onentry> + </state> + <state id="result"> + </state> + <transition event="OPER" target="opEntered" /> + <transition event="DIGIT.0" target="zero1"> + <assign location="short_expr" expr="''" /> + </transition> + <transition event="DIGIT" target="int1"> + <assign location="short_expr" expr="''" /> + </transition> + <transition event="POINT" target="frac1"> + <assign location="short_expr" expr="''" /> + </transition> + </state> + <state id="negated1"> + <onentry> + <assign location="short_expr" expr="'-'" /> + <send event="DISPLAY.UPDATE" /> + </onentry> + <transition event="DIGIT.0" target="zero1" /> + <transition event="DIGIT" target="int1" /> + <transition event="POINT" target="frac1" /> + </state> + <state id="operand1"> + <state id="zero1"> + <transition event="DIGIT" cond="_event.name != 'DIGIT.0'" target="int1" /> + <transition event="POINT" target="frac1" /> + </state> + <state id="int1"> + <transition event="POINT" target="frac1" /> + <transition event="DIGIT"> + <assign location="short_expr" expr="short_expr+_event.name.substr(_event.name.lastIndexOf('.')+1)" /> + <send event="DISPLAY.UPDATE" /> + </transition> + <onentry> + <assign location="short_expr" expr="short_expr+_event.name.substr(_event.name.lastIndexOf('.')+1)" /> + <send event="DISPLAY.UPDATE" /> + </onentry> + </state> + <state id="frac1"> + <onentry> + <assign location="short_expr" expr="short_expr+'.'" /> + <send event="DISPLAY.UPDATE" /> + </onentry> + <transition event="DIGIT"> + <assign location="short_expr" expr="short_expr+_event.name.substr(_event.name.lastIndexOf('.')+1)" /> + <send event="DISPLAY.UPDATE" /> + </transition> + </state> + <transition event="OPER" target="opEntered" /> + </state> + <state id="opEntered"> + <transition event="OPER.MINUS" target="negated2" /> + <transition event="POINT" target="frac2" /> + <transition event="DIGIT.0" target="zero2" /> + <transition event="DIGIT" target="int2" /> + <onentry> + <raise event="CALC.SUB" /> + <send target="_internal" event="OP.INSERT"> + <param name="operator" expr="_event.name" /> + </send> + </onentry> + </state> + <state id="negated2"> + <onentry> + <assign location="short_expr" expr="'-'" /> + <send event="DISPLAY.UPDATE" /> + </onentry> + <transition event="DIGIT.0" target="zero2" /> + <transition event="DIGIT" target="int2" /> + <transition event="POINT" target="frac2" /> + </state> + <state id="operand2"> + <state id="zero2"> + <transition event="DIGIT" cond="_event.name != 'DIGIT.0'" target="int2" /> + <transition event="POINT" target="frac2" /> + </state> + <state id="int2"> + <transition event="DIGIT"> + <assign location="short_expr" expr="short_expr+_event.name.substr(_event.name.lastIndexOf('.')+1)" /> + <send event="DISPLAY.UPDATE" /> + </transition> + <onentry> + <assign location="short_expr" expr="short_expr+_event.name.substr(_event.name.lastIndexOf('.')+1)" /> + <send event="DISPLAY.UPDATE" /> + </onentry> + <transition event="POINT" target="frac2" /> + </state> + <state id="frac2"> + <onentry> + <assign location="short_expr" expr="short_expr +'.'" /> + <send event="DISPLAY.UPDATE" /> + </onentry> + <transition event="DIGIT"> + <assign location="short_expr" expr="short_expr +_event.name.substr(_event.name.lastIndexOf('.')+1)" /> + <send event="DISPLAY.UPDATE" /> + </transition> + </state> + <transition event="OPER" target="opEntered"> + <raise event="CALC.SUB" /> + <raise event="OP.INSERT" /> + </transition> + <transition event="EQUALS" target="result"> + <raise event="CALC.SUB" /> + <raise event="CALC.DO" /> + </transition> + </state> + <transition event="C" target="on" /> + </state> + <transition event="CALC.DO"> + <assign location="short_expr" expr="''+ res" /> + <assign location="long_expr" expr="''" /> + <assign location="res" expr="0" /> + </transition> + <transition event="CALC.SUB"> + <if cond="short_expr!=''"> + <assign location="long_expr" expr="long_expr+'('+short_expr+')'" /> + </if> + <assign location="res" expr="eval(long_expr)" /> + <assign location="short_expr" expr="''" /> + <send event="DISPLAY.UPDATE" /> + </transition> + <transition event="DISPLAY.UPDATE"> + <!--log level="0" label="'result'" expr=".short_expr==''?res:short_expr" / --> + </transition> + <transition event="OP.INSERT"> + <log level="0" expr="_event.data[0]" /> + <if cond="_event.data[0] == 'OPER.PLUS'"> + <assign location="long_expr" expr="long_expr+'+'" /> + <elseif cond="_event.data[0]=='OPER.MINUS'" /> + <assign location="long_expr" expr="long_expr+'-'" /> + <elseif cond="_event.data[0]=='OPER.STAR'" /> + <assign location="long_expr" expr="long_expr+'*'" /> + <elseif cond="_event.data[0]=='OPER.DIV'" /> + <assign location="long_expr" expr="long_expr+'/'" /> + </if> + </transition> + </state> +</scxml> |