001: /*
002: * <copyright>
003: *
004: * Copyright 2000-2004 BBNT Solutions, LLC
005: * under sponsorship of the Defense Advanced Research Projects
006: * Agency (DARPA).
007: *
008: * You can redistribute this software and/or modify it under the
009: * terms of the Cougaar Open Source License as published on the
010: * Cougaar Open Source Website (www.cougaar.org).
011: *
012: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
013: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
014: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
015: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
016: * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
017: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
018: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
019: * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
020: * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
021: * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
022: * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
023: *
024: * </copyright>
025: */
026:
027: package org.cougaar.tools.csmart.ui.console;
028:
029: import org.cougaar.tools.csmart.ui.viewer.CSMART;
030: import org.cougaar.tools.server.NodeEvent;
031: import org.cougaar.tools.server.OutputBundle;
032: import org.cougaar.tools.server.OutputListener;
033: import org.cougaar.util.log.Logger;
034:
035: import javax.swing.*;
036: import javax.swing.text.SimpleAttributeSet;
037: import javax.swing.text.StyleConstants;
038: import java.awt.*;
039: import java.io.BufferedWriter;
040: import java.io.FileWriter;
041: import java.io.IOException;
042: import java.io.ObjectInputStream;
043: import java.io.Writer;
044: import java.util.Date;
045: import java.util.Timer;
046: import java.util.TimerTask;
047:
048: /**
049: * Listener for "pushed" Node activities. Handles NodeStatus button color
050: * changes, writing content to log file, measuring Node "idleness".
051: * This is object called from the AppServer.
052: */
053: public class ConsoleNodeListener implements OutputListener {
054: private String logFileName;
055: private Writer logFile;
056: private SimpleAttributeSet[] atts;
057: public NodeStatusButton statusButton;
058: private long firsttime = 0l;
059: private ConsoleStyledDocument doc;
060: private ConsoleNodeOutputFilter filter = null;
061: private Object logFileLock = new Object();
062: private int nLogEvents = 0;
063: private TimerTask logFlushTask;
064: private transient Logger log;
065: private NodeModel nodeModel;
066:
067: public ConsoleNodeListener(NodeModel nodeModel) {
068: createLogger();
069: this .nodeModel = nodeModel;
070:
071: // status button for node on console display
072: this .statusButton = nodeModel.getStatusButton();
073:
074: // wrap and capture to a log
075: this .logFileName = nodeModel.getLogFileName();
076: // Create new log file, always appending to the end - so a Node restart
077: // can write to the end of the file, not losing data
078: try {
079: this .logFile = new BufferedWriter(new FileWriter(
080: logFileName, true));
081: } catch (IOException ie) {
082: this .logFile = null;
083: System.out.println("Couldn't create log file");
084: }
085:
086: // FIXME (problem Mark Barger saw once) test to make sure logFile will be writable
087:
088: // save the document that contains node output
089: this .doc = nodeModel.getDoc();
090:
091: // create our attributes
092: // stdout 0,0,0 (black)
093: // stderr 192,64,64 (dark red)
094: // Node created, Node destroyed, Agent Added 64,64,192 (dark blue)
095: atts = new SimpleAttributeSet[3];
096: atts[0] = new SimpleAttributeSet();
097: StyleConstants.setForeground(atts[0], Color.black);
098: atts[1] = new SimpleAttributeSet();
099: StyleConstants.setForeground(atts[1], new Color(192, 64, 64));
100: atts[2] = new SimpleAttributeSet();
101: StyleConstants.setForeground(atts[2], new Color(64, 64, 192));
102: // write the log file every 5 seconds
103: logFlushTask = new TimerTask() {
104: public void run() {
105: synchronized (logFileLock) {
106: try {
107: logFile.flush();
108: } catch (Exception e) {
109: }
110: }
111: }
112: };
113: new Timer().schedule(logFlushTask, new Date(), 5000);
114: }
115:
116: private void createLogger() {
117: log = CSMART.createLogger(this .getClass().getName());
118: }
119:
120: /**
121: * Pull out the idleness number from a NodeEvent message.<br>
122: * The idleness is:
123: * <pre>(actual time difference between wakeups) - (expected difference) / (expected difference)</pre>
124: *
125: * @param nodemsg a <code>String</code> NodeEvent message to parse
126: * @return a <code>double</code> idleness number, 0 on error
127: */
128: private double getIdleness(String nodemsg) {
129: // node message is idletime:time of snapshot
130: // where idletime is (actual time diff) - (expected time diff) / (expected time diff)
131: // as a double
132: // and time of snapshot is a long, as returned by System.currentTimeMillis()
133: if (nodemsg == null || nodemsg.equals("")) {
134: return 0.0;
135: }
136: String idle = nodemsg.substring(0, nodemsg.indexOf(':'));
137: double ret = 0.0;
138: try {
139: ret = Double.parseDouble(idle);
140: } catch (NumberFormatException e) {
141: if (log.isErrorEnabled()) {
142: log.error("Couldn't parse that idle string", e);
143: }
144: }
145: return ret;
146: }
147:
148: /**
149: * Get the timestamp of the idleness snapshot
150: *
151: * @param nodemsg a <code>String</code> NodeEvent message
152: * @return a <code>long</code> timestamp in milliseconds
153: */
154: private long getTimestamp(String nodemsg) {
155: // node message is idletime:time of snapshot
156: // where idletime is (actual time diff) - (expected time diff) / (expected time diff)
157: // as a double
158: // and time of snapshot is a long, as returned by System.currentTimeMillis()
159: if (nodemsg == null || nodemsg.equals("")) {
160: return 0L;
161: }
162: String time = nodemsg.substring(nodemsg.indexOf(':') + 1);
163: long ret = 0L;
164: try {
165: ret = Long.parseLong(time);
166: } catch (NumberFormatException e) {
167: if (log.isErrorEnabled()) {
168: log.error("Couldn't parse that time string", e);
169: }
170: }
171: return ret;
172: }
173:
174: /**
175: * Called by appserver to stream standard output and standard err
176: * to this listener.
177: * @param outputBundle the standard out and err
178: */
179: public void handleOutputBundle(OutputBundle outputBundle) {
180: // fix to use the new app-server APIs
181: // see the app-server example "GuiConsole"
182: java.util.List l = org.cougaar.tools.server.NodeEventTranslator
183: .toNodeEvents(outputBundle);
184: handleAll(l);
185: }
186:
187: /**
188: * Handle the specified list of node events (List of NodeEvent)
189: * from a node. Same as handle, but for a list of events.
190: */
191: private void handleAll(java.util.List nodeEvents) {
192: int n = nodeEvents.size();
193: if (n <= 0) {
194: return;
195: }
196:
197: // write a description of the event to the log file
198: synchronized (logFileLock) {
199: try {
200: int i = 0;
201: do {
202: NodeEvent nodeEvent = (NodeEvent) nodeEvents.get(i);
203: if (logFile != null)
204: logFile
205: .write(getNodeEventDescription(nodeEvent));
206: nLogEvents++;
207: if (nLogEvents > 100) {
208: if (logFile != null)
209: logFile.flush();
210: nLogEvents = 0;
211: }
212: } while (++i < n);
213: } catch (Exception e) {
214: if (log.isErrorEnabled()) {
215: log.error("Exception writing to Node log file "
216: + logFileName + ": ", e);
217: }
218: // FIXME: Is there any way to recover? Perhaps flush the log file,
219: // and then re-open it?
220: }
221: }
222:
223: // must use swing "invokeLater" to be thread-safe
224: // collects descriptions of the same type and batch writes them to display
225: // handles idle updates separately
226: try {
227: SwingUtilities
228: .invokeLater(new NodeEventHandler(nodeEvents));
229: } catch (RuntimeException e) {
230: }
231: }
232:
233: /**
234: * Thread for handling incoming NodeEvents. Trying to avoid
235: * declaring anything final.
236: **/
237: class NodeEventHandler implements Runnable {
238: java.util.List nodeEvents = null;
239: int n = 0;
240:
241: public NodeEventHandler(java.util.List nodeEvents) {
242: // nodeEvents
243: this .nodeEvents = nodeEvents;
244: this .n = nodeEvents.size();
245: }
246:
247: public void run() {
248: int prevType = -1;
249: String prevDescription = "";
250: int nextEventIndex = -1;
251: // get the first description which is not an idle update
252: for (int i = 0; i < n; i++) {
253: NodeEvent nodeEvent = (NodeEvent) nodeEvents.get(i);
254: int nodeEventType = nodeEvent.getType();
255: if (nodeEventType == NodeEvent.IDLE_UPDATE) {
256: String s = nodeEvent.getMessage();
257: handleIdleUpdate(getIdleness(s), getTimestamp(s));
258: } else {
259: updateStatus(nodeEvent);
260: if (filter != null
261: && !filter.includeEventInDisplay(nodeEvent))
262: continue; // don't append events user isn't interested in
263: prevType = nodeEventType;
264: prevDescription = getNodeEventDescription(nodeEvent);
265: nextEventIndex = i + 1;
266: break;
267: }
268: }
269:
270: if (nextEventIndex == -1)
271: return; // all the events were idle updates or ignored
272: // start batching descriptions
273: for (int j = nextEventIndex; j < n; j++) {
274: NodeEvent nodeEvent = (NodeEvent) nodeEvents.get(j);
275: int nodeEventType = nodeEvent.getType();
276: String description = getNodeEventDescription(nodeEvent);
277: updateStatus(nodeEvent);
278: if (nodeEventType == NodeEvent.IDLE_UPDATE) {
279: String s = nodeEvent.getMessage();
280: handleIdleUpdate(getIdleness(s), getTimestamp(s));
281: } else if (nodeEventType == prevType) {
282: prevDescription += description;
283: } else {
284: if (filter == null
285: || filter
286: .includeEventTypeInDisplay(prevType)) {
287: if (doc != null)
288: doc.appendString(prevDescription,
289: getNodeEventStyle(prevType));
290: }
291: prevDescription = description;
292: prevType = nodeEventType;
293: }
294: }
295: if (filter == null
296: || filter.includeEventTypeInDisplay(prevType)) {
297: if (doc != null)
298: doc.appendString(prevDescription,
299: getNodeEventStyle(prevType));
300: }
301: }
302: }
303:
304: /**
305: * Get Node Event description.
306: */
307: private final String getNodeEventDescription(
308: final NodeEvent nodeEvent) {
309: switch (nodeEvent.getType()) {
310: case NodeEvent.STANDARD_OUT:
311: case NodeEvent.STANDARD_ERR:
312: return nodeEvent.getMessage();
313: default:
314: return nodeEvent.toString();
315: }
316: }
317:
318: /**
319: * Returns attribute set for a style of output:
320: * stdout, stderr, heartbeat, or default.
321: */
322: private final SimpleAttributeSet getNodeEventStyle(final int type) {
323: switch (type) {
324: case NodeEvent.STANDARD_OUT:
325: return atts[0];
326: case NodeEvent.STANDARD_ERR:
327: return atts[1];
328: default:
329: return atts[2];
330: }
331: }
332:
333: private void handleIdleUpdate(double idleTime, long timestamp) {
334: double result = 1 / Math.log(idleTime);
335: result = (result + 1) * 50; // in range 0 to 100
336: if (statusButton != null) {
337: if (result <= 16)
338: statusButton.getMyModel().setStatus(
339: NodeStatusButton.STATUS_LOW_BUSY);
340: else if (result > 16 && result <= 33)
341: statusButton.getMyModel().setStatus(
342: NodeStatusButton.STATUS_MEDIUM_LOW_BUSY);
343: else if (result > 33 && result <= 50)
344: statusButton.getMyModel().setStatus(
345: NodeStatusButton.STATUS_MEDIUM_BUSY);
346: else if (result > 50 && result <= 67)
347: statusButton.getMyModel().setStatus(
348: NodeStatusButton.STATUS_MEDIUM_HIGH_BUSY);
349: else if (result > 67 && result <= 83)
350: statusButton.getMyModel().setStatus(
351: NodeStatusButton.STATUS_HIGH_BUSY);
352: else if (result > 83)
353: statusButton.getMyModel().setStatus(
354: NodeStatusButton.STATUS_BUSY);
355: }
356:
357: // reset times to 0. Maybe this looks better?
358: if (firsttime > 0) {
359: firsttime = timestamp;
360: }
361:
362: }
363:
364: private void updateStatus(NodeEvent nodeEvent) {
365: int nodeEventType = nodeEvent.getType();
366: if (nodeEventType == NodeEvent.PROCESS_CREATED) {
367: if (statusButton != null)
368: statusButton.getMyModel().setStatus(
369: NodeStatusButton.STATUS_NODE_CREATED);
370: } else if (nodeEventType == NodeEvent.PROCESS_DESTROYED) {
371: if (statusButton != null)
372: statusButton.getMyModel().setStatus(
373: NodeStatusButton.STATUS_NODE_DESTROYED);
374: nodeModel.stopped();
375: } else if (nodeEventType == NodeEvent.STANDARD_ERR) {
376: if (statusButton != null)
377: statusButton.getMyModel().setStatus(
378: NodeStatusButton.STATUS_STD_ERROR);
379: }
380: }
381:
382: /**
383: * Flush and close the log file.
384: */
385: public void closeLogFile() {
386: synchronized (logFileLock) {
387: try {
388: if (logFile != null) {
389: logFile.close();
390: logFile = null;
391: }
392: logFlushTask.cancel();
393: } catch (Exception e) {
394: if (log.isErrorEnabled()) {
395: log.error("Exception closing log file: ", e);
396: }
397: }
398: }
399: }
400:
401: /**
402: * Set the console node output filter used to filter events displayed.
403: */
404: public void setFilter(ConsoleNodeOutputFilter filter) {
405: this .filter = filter;
406: }
407:
408: /**
409: * Get the console node output filter used to filter events displayed.
410: */
411: public ConsoleNodeOutputFilter getFilter() {
412: return filter;
413: }
414:
415: /**
416: * When completely done with this guy, close out the log file,
417: * and set my pointers to null for gc.
418: **/
419: public void cleanUp() {
420: if (log.isDebugEnabled())
421: log.debug("Listener.cleanup");
422: closeLogFile();
423: // Don't kill the document cause we call this
424: // when the Node has stopped
425: doc = null;
426: filter = null;
427: synchronized (logFileLock) {
428: logFile = null;
429: }
430: statusButton = null;
431: }
432:
433: private void readObject(ObjectInputStream ois) throws IOException,
434: ClassNotFoundException {
435: ois.defaultReadObject();
436: createLogger();
437: }
438:
439: }
|