001    package toolbus.viewer;
002    
003    import java.io.IOException;
004    import java.lang.management.ManagementFactory;
005    import java.lang.management.MemoryMXBean;
006    import java.lang.management.ThreadInfo;
007    import java.lang.management.ThreadMXBean;
008    import java.util.Collections;
009    import java.util.HashMap;
010    import java.util.Iterator;
011    import java.util.LinkedList;
012    import java.util.List;
013    import java.util.Map;
014    
015    import toolbus.SocketConnectionHandler;
016    import toolbus.StateElement;
017    import toolbus.TBTermFactory;
018    import toolbus.ToolBus;
019    import toolbus.exceptions.ToolBusException;
020    import toolbus.logging.ILogger;
021    import toolbus.logging.IToolBusLoggerConstants;
022    import toolbus.logging.LoggerFactory;
023    import toolbus.parsercup.PositionInformation;
024    import toolbus.process.ProcessCall;
025    import toolbus.process.ProcessInstance;
026    import toolbus.process.debug.ExecutionResult;
027    import toolbus.tool.ToolInstance;
028    import toolbus.util.collections.ConcurrentHashSet;
029    import aterm.ATerm;
030    import aterm.ATermList;
031    
032    /**
033     * A specialized version of the ToolBus, which executes the process logic in debug mode. Viewer
034     * implementations need to use this ToolBus instead of the regular one.
035     * 
036     * @author Arnold Lankamp
037     */
038    public class DebugToolBus extends ToolBus{
039            private volatile int currentState = IViewerConstants.UNKNOWN_STATE;
040            
041            private volatile boolean running = false;
042            private final Object processLock = new Object();
043            private volatile boolean workHasArrived = false;
044            
045            private volatile boolean doRun = false;
046            private volatile boolean doStep = false;
047            
048            private volatile boolean breakWhileStepping;
049            
050            private final IViewer viewer;
051            
052            private final ConcurrentHashSet<Integer> processInstanceBreakPoints;
053            private final ConcurrentHashSet<String> processBreakPoints;
054            private final ConcurrentHashSet<StateElement> stateElementBreakPoints;
055            private final Map<String, List<Integer>> sourceCodeBreakPoints;
056            
057            private final PerformanceInformationHandler performanceInformationRetriever;
058            
059            private long nextTime;
060            
061            /**
062             * Constructor.
063             * 
064             * @param args
065             *            The arguments the ToolBus need to start (like include path and main script name).
066             * @param viewer
067             *            The viewer that is attached to this ToolBus.
068             * @param performanceMonitor
069             *            The tool performance monitor to use (optional argument; pass null in case
070             *            monitoring is not required).
071             */
072            public DebugToolBus(String[] args, IViewer viewer, IPerformanceMonitor performanceMonitor){
073                    super(args);
074                    
075                    this.viewer = viewer;
076                    
077                    this.performanceInformationRetriever = (performanceMonitor != null) ? new PerformanceInformationHandler(performanceMonitor): null;
078                    
079                    processInstanceBreakPoints = new ConcurrentHashSet<Integer>();
080                    processBreakPoints = new ConcurrentHashSet<String>();
081                    stateElementBreakPoints = new ConcurrentHashSet<StateElement>();
082                    sourceCodeBreakPoints = new HashMap<String, List<Integer>>();
083                    
084                    nextTime = 0;
085                    
086                    breakWhileStepping = true;
087            }
088            
089            /**
090             * @see toolbus.ToolBus#addProcess(ProcessCall)
091             */
092            public ProcessInstance addProcess(ProcessCall call) throws ToolBusException{
093                    ProcessInstance pi = super.addProcess(call);
094                    viewer.processInstanceStarted(pi);
095                    return pi;
096            }
097            
098            /**
099             * Terminates the ToolBus.
100             * 
101             * @see toolbus.ToolBus#shutdown(ATerm)
102             */
103            public void shutdown(ATerm msg){
104                    viewer.toolbusTerminating();
105                    
106                    super.shutdown(msg);
107                    
108                    running = false;
109                    
110                    synchronized(processLock){
111                            processLock.notify();
112                    }
113            }
114            
115            /**
116             * @see toolbus.ToolBus#setNextTime(long)
117             */
118            public void setNextTime(long next){
119                    //System.err.println("setNextTime: " + next);
120                    long currentTime = getRunTime();
121                    if(nextTime < currentTime || (next < nextTime && next > currentTime)){
122                            nextTime = next;
123                    }
124                    //System.err.println("setNextTime: set to " + nextTime);
125            }
126            
127            public void prepare(){
128                    if(nerrors > 0){
129                            System.err.println("ToolBus cannot continue execution due to errors in Tscript");
130                            return;
131                    }
132                    
133                    viewer.toolbusStarting();
134    
135                    connectionHandler = new SocketConnectionHandler(this);
136                    
137                    try{
138                            int userSpecifiedPort = propertyManager.getUserSpecifiedPort();
139                            if(userSpecifiedPort == -1){
140                                    connectionHandler.initialize();
141                            }else{
142                                    connectionHandler.initialize(userSpecifiedPort);
143                            }
144                    }catch(IOException ioex){
145                            LoggerFactory.log("Unable initialize the ToolBus connection handler.", ioex, ILogger.FATAL, IToolBusLoggerConstants.COMMUNICATION);
146                            throw new RuntimeException(ioex);
147                    }
148                    portNumber = connectionHandler.getPort();
149                    
150                    Thread tbConnectionHandler = new Thread(connectionHandler);
151                    tbConnectionHandler.setName("ToolBus connection handler");
152                    tbConnectionHandler.start();
153                    
154                    System.out.println("The ToolBus server allocated port (" + portNumber + ")");
155            }
156            
157            /**
158             * The main process loop of the ToolBus.
159             * 
160             * @see toolbus.ToolBus#execute()
161             */
162            public void execute(){
163                    ProcessInstance pi = null;
164                    ProcessInstanceIterator processesIterator = new ProcessInstanceIterator(processes);
165                    running = true;
166                    try{
167                            boolean work = false;
168                            boolean reset = false;
169                            long currentNextTime = 0;
170                            workHasArrived = true; // This is just to get things started.
171                            
172                            PROCESSLOOP: do{
173                                    synchronized(processLock){
174                                            while((!workHasArrived && running) || !(doStep || doRun)){
175                                                    fireStateChange(IViewerConstants.WAITING_STATE);
176                                                    
177                                                    long blockTime = nextTime - getRunTime(); // Recalculate the delay before sleeping.
178                                                    if(blockTime > 0){
179                                                            try{
180                                                                    processLock.wait(blockTime);
181                                                            }catch(InterruptedException irex){
182                                                                    // Just ignore this, it's not harmfull.
183                                                            }
184                                                            workHasArrived = true;
185                                                    }else if(currentNextTime != nextTime){ // If the nextTime changed and the blockTime is zero or less, don't block as there might be work to do.
186                                                            workHasArrived = true;
187                                                            currentNextTime = nextTime;
188                                                    }else{
189                                                            try{
190                                                                    processLock.wait();
191                                                            }catch(InterruptedException irex){
192                                                                    // Just ignore this, it's not harmfull.
193                                                            }
194                                                            
195                                                            if(performanceInformationRetriever != null) performanceInformationRetriever.handleRetrievedData(); // Handle the arrived performance stats before continueing to execute the process logic.
196                                                    }
197                                                    
198                                                    if(shuttingDown) return; // Stop executing if a shutdown is triggered.
199                                            }
200                                            currentNextTime = nextTime;
201                                            
202                                            if(doStep) fireStateChange(IViewerConstants.STEPPING_STATE);
203                                            else if(doRun) fireStateChange(IViewerConstants.RUNNING_STATE);
204                                            else fireStateChange(IViewerConstants.UNKNOWN_STATE);
205                                    }
206                                    
207                                    WORKLOOP: do{
208                                            work = false;
209                                            workHasArrived = false;
210                                            
211                                            if(!processesIterator.hasNext()){
212                                                    processesIterator.reset();
213                                                    Collections.rotate(processes, 1);
214                                                    
215                                                    reset = true;
216                                            }else{
217                                                    reset = false; // We might start iterating in the middle of the collections, so ensure we do at least a full one if we're running.
218                                            }
219                                            
220                                            while(processesIterator.hasNext() && (doRun || doStep)){
221                                                    if(shuttingDown){
222                                                            return;
223                                                    }
224                                                    
225                                                    pi = processesIterator.next();
226                                                    ExecutionResult executionResult = pi.debugStep();
227                                                    boolean stepSuccessFull = (executionResult != null);
228                                                    boolean wasStep = doStep;
229                                                    
230                                                    if(wasStep && stepSuccessFull){
231                                                            doStep = false; // Consume the step.
232                                                    }
233                                                    
234                                                    String processName = pi.getProcessName();
235                                                    int processId = pi.getProcessId();
236                                                    if(!wasStep || breakWhileStepping){
237                                                            if(stepSuccessFull){
238                                                                    if(processInstanceBreakPoints.contains(new Integer(processId)) || processBreakPoints.contains(processName)){
239                                                                            viewer.processBreakPointHit(pi);
240                                                                    }
241                                                                    StateElement executedStateElement = executionResult.stateElement; // Ignore the Eclipse warning here, since it's bogus.
242                                                                    if(stateElementBreakPoints.contains(executedStateElement)){
243                                                                            viewer.stateElementBreakPointHit(executedStateElement);
244                                                                    }
245                                                                    PositionInformation posInfo = executedStateElement.getPosInfo();
246                                                                    if(posInfo != null){
247                                                                            String filename = posInfo.getFileName();
248                                                                            List<Integer> lineNumbers = sourceCodeBreakPoints.get(filename);
249                                                                            if(lineNumbers != null){
250                                                                                    int beginLineNumber = posInfo.getBeginLine();
251                                                                                    int endLineNumber = posInfo.getEndLine();
252                                                                                    
253                                                                                    Iterator<Integer> lineNumberIterator = lineNumbers.iterator();
254                                                                                    synchronized(lineNumbers){
255                                                                                            while(lineNumberIterator.hasNext()){
256                                                                                                    int lineNumber = lineNumberIterator.next().intValue();
257                                                                                                    if(lineNumber >= beginLineNumber && lineNumber <= endLineNumber){
258                                                                                                            viewer.sourceBreakPointHit(executedStateElement);
259                                                                                                            break;
260                                                                                                    }
261                                                                                            }
262                                                                                    }
263                                                                            }
264                                                                    }
265                                                            }
266                                                    }
267                                                    
268                                                    if(pi.isTerminated()){
269                                                            processesIterator.remove();
270                                                            pi.terminate();
271                                                            
272                                                            viewer.processInstanceTerminated(pi);
273                                                            
274                                                            if(processes.size() == 0) break PROCESSLOOP;
275                                                    }
276                                                    work |= stepSuccessFull;
277                                                    
278                                                    if(stepSuccessFull){
279                                                            if(wasStep){
280                                                                    viewer.stepExecuted(pi, executionResult.stateElement, executionResult.partners); // Ignore the Eclipse warning here, since it's bogus.
281                                                                    break WORKLOOP;
282                                                            }
283                                                    }
284                                            }
285                                            
286                                            if(performanceInformationRetriever != null) performanceInformationRetriever.handleRetrievedData(); // Handle the arrived performance stats before continueing the process.
287                                    }while((doRun && (work || !reset)) || (doStep && !reset));
288                                    workHasArrived |= work; // If we did something, set this to ensure we can release the lock when needed.
289                            }while(running);
290                    }catch(ToolBusException e){
291                            error("Process " + (pi != null ? pi.getProcessName() : "?"), e.getMessage());
292                            e.printStackTrace();
293                    }
294                    
295                    if(!shuttingDown) shutdown(tbfactory.make("ToolBus halted"));
296            }
297            
298            /**
299             * @see toolbus.ToolBus#workArrived()
300             */
301            public void workArrived(ToolInstance toolInstance, byte operation){
302                    if(performanceInformationRetriever != null){
303                            performanceInformationRetriever.toolCommunicationTriggered(toolInstance, operation);
304                    }
305                    
306                    if(!workHasArrived){
307                            synchronized(processLock){
308                                    fireStateChange(IViewerConstants.READY_STATE);
309                                    
310                                    workHasArrived = true;
311                                    
312                                    processLock.notify();
313                            }
314                    }
315            }
316            
317            /**
318             * Gathers performance statistics related to JVM the current ToolBus is running in.
319             * 
320             * @return Performance statictics.
321             */
322            public ATerm getToolBusPerformanceStats(){
323                    // Type stuff (for standard compliance only).
324                    ATerm toolData = tbfactory.makeAppl(tbfactory.makeAFun("unsupported-operation", 0, false));
325                    
326                    // Memory stuff
327                    MemoryMXBean mmxb = ManagementFactory.getMemoryMXBean();
328                    long heapMemoryUsage = mmxb.getHeapMemoryUsage().getUsed();
329                    long nonHeapMemoryUsage = mmxb.getNonHeapMemoryUsage().getUsed();
330                    
331                    ATerm heapUsage = tbfactory.makeAppl(tbfactory.makeAFun("heap-usage", 1, false), tbfactory.makeInt(((int) (heapMemoryUsage / 1024))));
332                    ATerm nonHeapUsage = tbfactory.makeAppl(tbfactory.makeAFun("non-heap-usage", 1, false), tbfactory.makeInt(((int) (nonHeapMemoryUsage / 1024))));
333                    
334                    ATerm memory = tbfactory.makeAppl(tbfactory.makeAFun("memory-usage", 2, false), heapUsage, nonHeapUsage);
335                    
336                    // Thread stuff
337                    ThreadMXBean tmxb = ManagementFactory.getThreadMXBean();
338                    
339                    ATerm threads;
340                    
341                    long[] threadIds = tmxb.getAllThreadIds();
342                    int nrOfThreads = threadIds.length;
343                    try{
344                            ATermList threadsList = tbfactory.makeList();
345                            for(int i = 0; i < nrOfThreads; i++){
346                                    ThreadInfo ti = tmxb.getThreadInfo(threadIds[i]);
347                                    if(ti != null){
348                                            String threadName = ti.getThreadName();
349                                            long userTime = tmxb.getThreadUserTime(threadIds[i]);
350                                            long systemTime = tmxb.getThreadCpuTime(threadIds[i]) - userTime;
351                                            
352                                            if((userTime + systemTime) <= 0) continue;
353                                            
354                                            ATerm userTimeTerm = tbfactory.makeAppl(tbfactory.makeAFun("user-time", 1, false), tbfactory.makeInt(((int) (userTime / 1000000))));
355                                            ATerm systemTimeTerm = tbfactory.makeAppl(tbfactory.makeAFun("system-time", 1, false), tbfactory.makeInt(((int) (systemTime / 1000000))));
356                                            ATerm thread = tbfactory.makeAppl(tbfactory.makeAFun(threadName, 2, false), userTimeTerm, systemTimeTerm);
357                                            
358                                            threadsList = tbfactory.makeList(thread, threadsList);
359                                    }
360                            }
361                            
362                            threads = tbfactory.makeAppl(tbfactory.makeAFun("threads", 1, false), threadsList);
363                    }catch(UnsupportedOperationException uoex){
364                            threads = tbfactory.make("threads(unsupported-operation)");
365                            LoggerFactory.log("Thread time profiling is not supported by this JVM.", ILogger.ERROR, IToolBusLoggerConstants.EXECUTE);
366                    }
367                    
368                    return tbfactory.makeAppl(tbfactory.makeAFun("performance-stats", 3, false), toolData, memory, threads);
369            }
370            
371            /**
372             * Notifies the debug toolbus that it should execute normally.
373             */
374            public void doRun(){
375                    synchronized(processLock){
376                            workHasArrived = true; // This should not be needed; it's just here to ensure we will escape the wait loop.
377                            
378                            doStep = false;
379                            doRun = true;
380                            
381                            fireStateChange(IViewerConstants.RUNNING_STATE);
382                            
383                            processLock.notify();
384                    }
385            }
386            
387            /**
388             * Notifies the debug toolbus that it should suspend its execution.
389             */
390            public void doStop(){
391                    synchronized(processLock){
392                            fireStateChange(IViewerConstants.STOPPING_STATE);
393                            
394                            doRun = false;
395                            doStep = false;
396                            
397                            processLock.notify(); // This causes the state to be updated properly.
398                    }
399            }
400            
401            /**
402             * Notifies the debug toolbus that it should execute one step.
403             */
404            public void doStep(){
405                    synchronized(processLock){
406                            workHasArrived = true; // This should not be needed; it's just here to ensure we will escape the wait loop.
407                            
408                            doRun = false;
409                            doStep = true;
410                            
411                            fireStateChange(IViewerConstants.STEPPING_STATE);
412                            
413                            processLock.notify();
414                    }
415            }
416            
417            /**
418             * Requests the termination of the ToolBus.
419             */
420            public void doTerminate(){
421                    shutdown(TBTermFactory.getInstance().EmptyList);
422            }
423            
424            /**
425             * Sends a notification to the attached Viewer that the ToolBus's state changed.
426             * 
427             * @param state
428             *            The state we changed to.
429             */
430            private void fireStateChange(int state){
431                    if(state != currentState){
432                            currentState = state;
433                            viewer.updateState(state);
434                    }
435            }
436            
437            /**
438             * Tells the debug toolbus whether or not it should fire 'breakPointHit' events while stepping.
439             * This parameter is set to true by default.
440             * 
441             * @param breakWhileStepping
442             *            The flag that indicates whether or not to break on breakpoints while stepping.
443             */
444            public void setBreakWhileStepping(boolean breakWhileStepping){
445                    this.breakWhileStepping = breakWhileStepping;
446            }
447            
448            /**
449             * Adds a breakpoint for the process instance with the given id. When the debug toolbus executes
450             * a state element in the associated process instance, the attached Viewer will be notified.
451             * 
452             * @param processId
453             *            The id of the process instance we want to add a breakpoint for.
454             */
455            public void addProcessInstanceBreakPoint(int processId){
456                    processInstanceBreakPoints.put(new Integer(processId));
457            }
458            
459            /**
460             * Removes the breakpoint on the process instance with the given id (if present).
461             * 
462             * @param processId
463             *            The id of the process instance we want to remove the breakpoint for.
464             */
465            public void removeProcessInstanceBreakPoint(int processId){
466                    processInstanceBreakPoints.remove(new Integer(processId));
467            }
468            
469            /**
470             * Adds a breakpoint for all the process instances whos type is identified by the given name.
471             * When the debug toolbus executes a state element in one of those process instances, the
472             * attached Viewer will be notified.
473             * 
474             * @param processName
475             *            The name that identifies the type of process instances we want to add breakpoints
476             *            for.
477             */
478            public void addProcessBreakPoint(String processName){
479                    processBreakPoints.put(processName);
480            }
481            
482            /**
483             * Removes the breakpoints for the process instances whos type is identified by the given name
484             * (if present).
485             * 
486             * @param processName
487             *            The name that identifies the type of process instance we want to remove the
488             *            breakpoints for.
489             */
490            public void removeProcessBreakPoint(String processName){
491                    processBreakPoints.remove(processName);
492            }
493            
494            /**
495             * Adds a breakpoint on the given state element. When the debug toolbus executes the given state
496             * element, the attached Viewer will be notified.
497             * 
498             * @param stateElement
499             *            The state element we want to add a breakpoint for.
500             */
501            public void addStateElementBreakPoint(StateElement stateElement){
502                    stateElementBreakPoints.put(stateElement);
503            }
504            
505            /**
506             * Removes the breakpoint from the given state element (if present).
507             * 
508             * @param stateElement
509             *            The state element we want to remove the breakpoint from.
510             */
511            public void removeStateElementBreakPoint(StateElement stateElement){
512                    stateElementBreakPoints.remove(stateElement);
513            }
514            
515            /**
516             * Adds a breakpoint on the given sourcecode coordinates. When the debug toolbus executes a
517             * state element which's position information matches the sourcecode coordiates, the attached
518             * Viewer will be notified.
519             * 
520             * @param filename
521             *            The name of the sourcefile
522             * @param lineNumber
523             *            The line number to add the breakpoint on. Note that sourcecode line numbers start
524             *            counting at 0.
525             */
526            public void addSourceCodeBreakPoint(String filename, int lineNumber){
527                    synchronized(sourceCodeBreakPoints){
528                            List<Integer> lineNumbers = sourceCodeBreakPoints.get(filename);
529                            if(lineNumbers == null){
530                                    lineNumbers = new LinkedList<Integer>();
531                                    sourceCodeBreakPoints.put(filename, lineNumbers);
532                            }
533                            synchronized(lineNumbers){
534                                    lineNumbers.add(Integer.valueOf(lineNumber));
535                            }
536                    }
537            }
538            
539            /**
540             * Removes the breakpoint from the given source code coordinates (if present).
541             * 
542             * @param filename
543             *            The name of the sourcefile
544             * @param lineNumber
545             *            The line number to remove the breakpoint for.
546             */
547            public void removeSourceCodeBreakPoint(String filename, int lineNumber){
548                    synchronized(sourceCodeBreakPoints){
549                            List<Integer> lineNumbers = sourceCodeBreakPoints.get(filename);
550                            synchronized(lineNumbers){
551                                    lineNumbers.remove(Integer.valueOf(lineNumber));
552                            }
553                            if(lineNumbers.isEmpty()){
554                                    sourceCodeBreakPoints.remove(filename);
555                            }
556                    }
557            }
558            
559            /**
560             * Initiates the monitoring of the tool associated with the given tool key (in case performance
561             * monitoring is enabled for this debug ToolBus).
562             * 
563             * @param toolKey
564             *            The tool key associated with the tool we want to monitor.
565             */
566            public void startMonitoringTool(ATerm toolKey){
567                    performanceInformationRetriever.startMonitoringTool(toolKey);
568            }
569            
570            /**
571             * Stops monitoring the tool associated with the given tool key.
572             * 
573             * @param toolKey
574             *            The tool key associated with the tool which we want to stop monitoring.
575             */
576            public void stopMonitoringTool(ATerm toolKey){
577                    performanceInformationRetriever.startMonitoringTool(toolKey);
578            }
579            
580            /**
581             * Initiates the monitoring of the given tool type (in case performance monitoring is enabled
582             * for this debug ToolBus).
583             * 
584             * @param toolName
585             *            The tool type of tool we want to monitor.
586             */
587            public void startMonitorToolType(String toolName){
588                    performanceInformationRetriever.startMonitorToolType(toolName);
589            }
590            
591            /**
592             * Stops monitoring tools of the given type.
593             * 
594             * @param toolName
595             *            The type of tool which we want to stop monitoring.
596             */
597            public void stopMonitoringToolType(String toolName){
598                    performanceInformationRetriever.stopMonitoringToolType(toolName);
599            }
600    }