package lcm.spy; import javax.swing.*; import javax.swing.table.*; import java.awt.*; import java.awt.event.*; import java.io.*; import java.util.*; import lcm.util.*; import java.lang.reflect.*; import lcm.lcm.*; /** Spy main class. **/ public class Spy { LCM lcm; LCMTypeDatabase handlers; long startuTime; // time that lcm-spy started HashMap<String, ChannelData> channelMap = new HashMap<String, ChannelData>(); ArrayList<ChannelData> channelList = new ArrayList<ChannelData>(); ChannelTableModel _channelTableModel = new ChannelTableModel(); TableSorter channelTableModel = new TableSorter(_channelTableModel); JTable channelTable = new JTable(channelTableModel); ChartData chartData; ArrayList<SpyPlugin> plugins = new ArrayList<SpyPlugin>(); JButton clearButton = new JButton("Clear"); public Spy(String lcmurl) throws IOException { // sortedChannelTableModel.addMouseListenerToHeaderInTable(channelTable); channelTableModel.setTableHeader(channelTable.getTableHeader()); channelTableModel.setSortingStatus(0, TableSorter.ASCENDING); handlers = new LCMTypeDatabase(); TableColumnModel tcm = channelTable.getColumnModel(); tcm.getColumn(0).setMinWidth(140); tcm.getColumn(1).setMinWidth(140); tcm.getColumn(2).setMaxWidth(100); tcm.getColumn(3).setMaxWidth(100); tcm.getColumn(4).setMaxWidth(100); tcm.getColumn(5).setMaxWidth(100); tcm.getColumn(6).setMaxWidth(100); JFrame jif = new JFrame("LCM Spy"); jif.setLayout(new BorderLayout()); jif.add(channelTable.getTableHeader(), BorderLayout.PAGE_START); // XXX weird bug, if clearButton is added after JScrollPane, we get an error. jif.add(clearButton, BorderLayout.SOUTH); jif.add(new JScrollPane(channelTable), BorderLayout.CENTER); chartData = new ChartData(utime_now()); jif.setSize(800,600); jif.setLocationByPlatform(true); jif.setVisible(true); if(null == lcmurl) lcm = new LCM(); else lcm = new LCM(lcmurl); lcm.subscribeAll(new MySubscriber()); new HzThread().start(); clearButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { channelMap.clear(); channelList.clear(); channelTableModel.fireTableDataChanged(); } }); channelTable.addMouseListener(new MouseAdapter() { public void mouseClicked(MouseEvent e) { int mods=e.getModifiersEx(); if (e.getButton()==3) { showPopupMenu(e); } else if (e.getClickCount() == 2) { Point p = e.getPoint(); int row = rowAtPoint(p); ChannelData cd = channelList.get(row); boolean got_one = false; for (SpyPlugin plugin : plugins) { if (!got_one && plugin.canHandle(cd.fingerprint)) { // start the plugin (new PluginStarter(plugin, cd)).getAction().actionPerformed(null); got_one = true; } } if (!got_one) createViewer(channelList.get(row)); } } }); jif.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { System.out.println("Spy quitting"); System.exit(0); } }); ClassDiscoverer.findClasses(new PluginClassVisitor()); System.out.println("Found "+plugins.size()+" plugins"); for (SpyPlugin plugin : plugins) { System.out.println(" "+plugin); } } class PluginStarter { private SpyPlugin plugin; private ChannelData cd; private String name; public PluginStarter(SpyPlugin pluginIn, ChannelData cdIn) { plugin = pluginIn; cd = cdIn; Action thisAction = plugin.getAction(null, null); name = (String) thisAction.getValue("Name"); } public Action getAction() { return new PluginStarterAction(); } class PluginStarterAction extends AbstractAction { public PluginStarterAction() { super(name); } @Override public void actionPerformed(ActionEvent e) { // for historical reasons, plugins expect a JDesktopPane // here we create a JFrame, add a JDesktopPane, and start the // plugin by calling its actionPerformed method JFrame pluginFrame = new JFrame(cd.name); pluginFrame.setLayout(new BorderLayout()); JDesktopPane pluginJdp = new JDesktopPane(); pluginFrame.add(pluginJdp); pluginFrame.setSize(500, 400); pluginFrame.setLocationByPlatform(true); pluginFrame.setVisible(true); plugin.getAction(pluginJdp, cd).actionPerformed(null); } } } class PluginClassVisitor implements ClassDiscoverer.ClassVisitor { public void classFound(String jar, Class cls) { Class interfaces[] = cls.getInterfaces(); for (Class iface : interfaces) { if (iface.equals(SpyPlugin.class)) { try { Constructor c = cls.getConstructor(new Class[0]); SpyPlugin plugin = (SpyPlugin) c.newInstance(new Object[0]); plugins.add(plugin); } catch (Exception ex) { System.out.println("ex: "+ex); } } } } } void createViewer(ChannelData cd) { if (cd.viewerFrame != null && !cd.viewerFrame.isVisible()) { cd.viewerFrame.dispose(); cd.viewer = null; } if (cd.viewer == null) { cd.viewerFrame = new JFrame(cd.name); cd.viewer = new ObjectPanel(cd.name, chartData); // cd.viewer = new ObjectViewer(cd.name, cd.cls, null); cd.viewerFrame.setLayout(new BorderLayout()); // default scroll speed is too slow, so increase it JScrollPane viewerScrollPane = new JScrollPane(cd.viewer); viewerScrollPane.getVerticalScrollBar().setUnitIncrement(16); // we need to tell the viewer what its viewport is so that it can // make smart decisions about which elements are in view of the user // so it can avoid drawing items outside the view cd.viewer.setViewport(viewerScrollPane.getViewport()); cd.viewerFrame.add(viewerScrollPane, BorderLayout.CENTER); cd.viewer.setObject(cd.last, cd.last_utime); //jdp.add(cd.viewerFrame); cd.viewerFrame.setSize(650,400); cd.viewerFrame.setLocationByPlatform(true); cd.viewerFrame.setVisible(true); } else { cd.viewerFrame.setVisible(true); //cd.viewerFrame.moveToFront(); } } static final long utime_now() { return System.nanoTime()/1000; } class ChannelTableModel extends AbstractTableModel { public int getColumnCount() { return 8; } public int getRowCount() { return channelList.size(); } public Object getValueAt(int row, int col) { ChannelData cd = channelList.get(row); if (cd == null) return ""; switch (col) { case 0: return cd.name; case 1: if (cd.cls == null) return String.format("?? %016x", cd.fingerprint); String s = cd.cls.getName(); return s.substring(s.lastIndexOf('.')+1); case 2: return ""+cd.nreceived; case 3: return String.format("%6.2f", cd.hz); case 4: return String.format("%6.2f ms",1000.0/cd.hz); // cd.max_interval/1000.0); case 5: return String.format("%6.2f ms",(cd.max_interval - cd.min_interval)/1000.0); case 6: return String.format("%6.2f KB/s", (cd.bandwidth/1024.0)); case 7: return ""+cd.nerrors; } return "???"; } public String getColumnName(int col) { switch (col) { case 0: return "Channel"; case 1: return "Type"; case 2: return "Num Msgs"; case 3: return "Hz"; case 4: return "1/Hz"; case 5: return "Jitter"; case 6: return "Bandwidth"; case 7: return "Undecodable"; } return "???"; } } class MySubscriber implements LCMSubscriber { public void messageReceived(LCM lcm, String channel, LCMDataInputStream dins) { Object o = null; ChannelData cd = channelMap.get(channel); int msg_size = 0; try { msg_size = dins.available(); long fingerprint = (msg_size >=8) ? dins.readLong() : -1; dins.reset(); Class cls = handlers.getClassByFingerprint(fingerprint); if (cd == null) { cd = new ChannelData(); cd.name = channel; cd.cls = cls; cd.fingerprint = fingerprint; cd.row = channelList.size(); synchronized(channelList) { channelMap.put(channel, cd); channelList.add(cd); _channelTableModel.fireTableDataChanged(); } } else { if (cls != null && cd.cls != null && !cd.cls.equals(cls)) { System.out.println("WARNING: Class changed for channel "+channel); cd.nerrors++; } } long utime = utime_now(); long interval = utime - cd.last_utime; cd.hz_min_interval = Math.min(cd.hz_min_interval, interval); cd.hz_max_interval = Math.max(cd.hz_max_interval, interval); cd.hz_bytes += msg_size; cd.last_utime = utime; cd.nreceived++; o = cd.cls.getConstructor(DataInput.class).newInstance(dins); cd.last = o; if (cd.viewer != null) cd.viewer.setObject(o, cd.last_utime); } catch (NullPointerException ex) { cd.nerrors++; } catch (IOException ex) { cd.nerrors++; System.out.println("Spy.messageReceived ex: "+ex); } catch (NoSuchMethodException ex) { cd.nerrors++; System.out.println("Spy.messageReceived ex: "+ex); } catch (InstantiationException ex) { cd.nerrors++; System.out.println("Spy.messageReceived ex: "+ex); } catch (IllegalAccessException ex) { cd.nerrors++; System.out.println("Spy.messageReceived ex: "+ex); } catch (InvocationTargetException ex) { cd.nerrors++; // these are almost always spurious //System.out.println("ex: "+ex+"..."+ex.getTargetException()); } } } class HzThread extends Thread { public HzThread() { setDaemon(true); } public void run() { while (true) { long utime = utime_now(); synchronized(channelList) { for (ChannelData cd : channelList) { long diff_recv = cd.nreceived - cd.hz_last_nreceived; cd.hz_last_nreceived = cd.nreceived; long dutime = utime - cd.hz_last_utime; cd.hz_last_utime = utime; cd.hz = diff_recv / (dutime/1000000.0); cd.min_interval = cd.hz_min_interval; cd.max_interval = cd.hz_max_interval; cd.hz_min_interval = 9999; cd.hz_max_interval = 0; cd.bandwidth = cd.hz_bytes / (dutime/1000000.0); cd.hz_bytes = 0; } } int selrow = channelTable.getSelectedRow(); channelTableModel.fireTableDataChanged(); if (selrow >= 0) channelTable.setRowSelectionInterval(selrow, selrow); try { Thread.sleep(1000); } catch (InterruptedException ex) { } } } } class DefaultViewer extends AbstractAction { ChannelData cd; public DefaultViewer(ChannelData cd) { super("Structure Viewer..."); this.cd = cd; } public void actionPerformed(ActionEvent e) { createViewer(cd); } } int rowAtPoint(Point p) { int physicalRow = channelTable.rowAtPoint(p); return channelTableModel.modelIndex(physicalRow); } public void showPopupMenu(MouseEvent e) { Point p = e.getPoint(); int row = rowAtPoint(p); ChannelData cd = channelList.get(row); JPopupMenu jm = new JPopupMenu("Viewers"); int prow = channelTable.rowAtPoint(p); channelTable.setRowSelectionInterval(prow, prow); jm.add(new DefaultViewer(cd)); if (cd.cls != null) { for (SpyPlugin plugin : plugins) { if (plugin.canHandle(cd.fingerprint)) { jm.add(new PluginStarter(plugin, cd).getAction()); //jm.add(plugin.getAction(this_desktop_pane, cd)); } } } jm.show(channelTable, e.getX(), e.getY()); } public static void usage() { System.err.println("usage: lcm-spy [options]"); System.err.println(""); System.err.println("lcm-spy is the Lightweight Communications and Marshalling traffic "); System.err.println("inspection utility. It is a graphical tool for viewing messages received on "); System.err.println("an LCM network, and is analagous to tools like Ethereal/Wireshark and tcpdump"); System.err.println("in that it is able to inspect all LCM messages received and provide information"); System.err.println("and statistics on the channels used."); System.err.println(""); System.err.println("When given appropriate LCM type definitions, lcm-spy is able to"); System.err.println("automatically detect and decode messages, and can display the individual fields"); System.err.println("of recognized messages. lcm-spy is limited to displaying statistics for"); System.err.println("unrecognized messages."); System.err.println(""); System.err.println("Options:"); System.err.println(" -l, --lcm-url=URL Use the specified LCM URL"); System.err.println(" -h, --help Shows this help text and exits"); System.err.println(""); System.exit(1); } public static void main(String args[]) { // check if the JRE is supplied by gcj, and warn the user if it is. if(System.getProperty("java.vendor").indexOf("Free Software Foundation") >= 0) { System.err.println("WARNING: Detected gcj. lcm-spy is not known to work well with gcj."); System.err.println(" The Sun JRE is recommended."); } String lcmurl = null; for(int optind=0; optind<args.length; optind++) { String c = args[optind]; if(c.equals("-h") || c.equals("--help")) { usage(); } else if(c.equals("-l") || c.equals("--lcm-url") || c.startsWith("--lcm-url=")) { String optarg = null; if(c.startsWith("--lcm-url=")) { optarg=c.substring(10); } else if(optind < args.length) { optind++; optarg = args[optind]; } if(null == optarg) { usage(); } else { lcmurl = optarg; } } else { usage(); } } try { new Spy(lcmurl); } catch (IOException ex) { System.out.println(ex); } } }