/* * Copyright (c) 2001, 2003, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code 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. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, * CA 94065 USA or visit www.oracle.com if you need additional information or * have any questions. * */ package sun.jvm.hotspot.bugspot; import java.awt.*; import java.awt.event.*; import java.io.*; import java.net.*; import java.util.*; import javax.swing.*; import javax.swing.filechooser.*; import sun.jvm.hotspot.debugger.*; import sun.jvm.hotspot.debugger.cdbg.*; import sun.jvm.hotspot.debugger.posix.*; import sun.jvm.hotspot.debugger.win32.*; import sun.jvm.hotspot.livejvm.*; import sun.jvm.hotspot.memory.*; import sun.jvm.hotspot.oops.*; import sun.jvm.hotspot.runtime.*; import sun.jvm.hotspot.ui.*; import sun.jvm.hotspot.utilities.*; /** The BugSpot component. This is embeddable in an application by virtue of its being a JComponent. It (currently) requires the use of a menu bar which can be fetched via getMenuBar(). This is intended ultimately to replace HSDB. */ public class BugSpot extends JPanel { public BugSpot() { super(); Runtime.getRuntime().addShutdownHook(new java.lang.Thread() { public void run() { detachDebugger(); } }); } /** Turn on or off MDI (Multiple Document Interface) mode. When MDI is enabled, the BugSpot component contains a JDesktopPane and all windows are JInternalFrames. When disabled, only the menu bar is relevant. */ public void setMDIMode(boolean onOrOff) { mdiMode = onOrOff; } /** Indicates whether MDI mode is enabled. */ public boolean getMDIMode() { return mdiMode; } /** Build user interface widgets. This must be called before adding the BugSpot component to its parent. */ public void build() { setLayout(new BorderLayout()); menuBar = new JMenuBar(); attachMenuItems = new java.util.ArrayList(); detachMenuItems = new java.util.ArrayList(); debugMenuItems = new java.util.ArrayList(); suspendDebugMenuItems = new java.util.ArrayList(); resumeDebugMenuItems = new java.util.ArrayList(); // // File menu // JMenu menu = createMenu("File", 'F', 0); JMenuItem item; item = createMenuItem("Open source file...", new ActionListener() { public void actionPerformed(ActionEvent e) { openSourceFile(); } }, KeyEvent.VK_O, InputEvent.CTRL_MASK, 'O', 0); menu.add(item); detachMenuItems.add(item); menu.addSeparator(); item = createMenuItem("Attach to process...", new ActionListener() { public void actionPerformed(ActionEvent e) { showAttachDialog(); } }, 'A', 0); menu.add(item); attachMenuItems.add(item); item = createMenuItem("Detach", new ActionListener() { public void actionPerformed(ActionEvent e) { detach(); } }, 'D', 0); menu.add(item); detachMenuItems.add(item); // Disable detach menu items at first setMenuItemsEnabled(detachMenuItems, false); menu.addSeparator(); menu.add(createMenuItem("Exit", new ActionListener() { public void actionPerformed(ActionEvent e) { detach(); System.exit(0); } }, 'x', 1)); menuBar.add(menu); // // Debug menu // debugMenu = createMenu("Debug", 'D', 0); item = createMenuItem("Go", new ActionListener() { public void actionPerformed(ActionEvent e) { if (!attached) return; if (!isSuspended()) return; resume(); } }, KeyEvent.VK_F5, 0, 'G', 0); debugMenu.add(item); resumeDebugMenuItems.add(item); item = createMenuItem("Break", new ActionListener() { public void actionPerformed(ActionEvent e) { if (!attached) { System.err.println("Not attached"); return; } if (isSuspended()) { System.err.println("Already suspended"); return; } suspend(); } }, 'B', 0); debugMenu.add(item); suspendDebugMenuItems.add(item); debugMenu.addSeparator(); item = createMenuItem("Threads...", new ActionListener() { public void actionPerformed(ActionEvent e) { showThreadsDialog(); } }, 'T', 0); debugMenu.add(item); debugMenuItems.add(item); // FIXME: belongs under "View -> Debug Windows" item = createMenuItem("Memory", new ActionListener() { public void actionPerformed(ActionEvent e) { showMemoryDialog(); } }, 'M', 0); debugMenu.add(item); debugMenuItems.add(item); debugMenu.setEnabled(false); menuBar.add(debugMenu); if (mdiMode) { desktop = new JDesktopPane(); add(desktop, BorderLayout.CENTER); } fixedWidthFont = GraphicsUtilities.lookupFont("Courier"); debugEventTimer = new javax.swing.Timer(100, new ActionListener() { public void actionPerformed(ActionEvent e) { pollForDebugEvent(); } }); } public JMenuBar getMenuBar() { return menuBar; } public void showAttachDialog() { setMenuItemsEnabled(attachMenuItems, false); final FrameWrapper attachDialog = newFrame("Attach to process"); attachDialog.getContentPane().setLayout(new BorderLayout()); attachDialog.setClosable(true); attachDialog.setResizable(true); JPanel panel = new JPanel(); panel.setLayout(new BorderLayout()); panel.setBorder(GraphicsUtilities.newBorder(5)); attachDialog.setBackground(panel.getBackground()); JPanel listPanel = new JPanel(); listPanel.setLayout(new BorderLayout()); final ProcessListPanel plist = new ProcessListPanel(getLocalDebugger()); panel.add(plist, BorderLayout.CENTER); JCheckBox check = new JCheckBox("Update list continuously"); check.addItemListener(new ItemListener() { public void itemStateChanged(ItemEvent e) { if (e.getStateChange() == ItemEvent.SELECTED) { plist.start(); } else { plist.stop(); } } }); listPanel.add(plist, BorderLayout.CENTER); listPanel.add(check, BorderLayout.SOUTH); panel.add(listPanel, BorderLayout.CENTER); attachDialog.getContentPane().add(panel, BorderLayout.CENTER); attachDialog.setClosingActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { plist.stop(); setMenuItemsEnabled(attachMenuItems, true); } }); ActionListener attacher = new ActionListener() { public void actionPerformed(ActionEvent e) { plist.stop(); attachDialog.setVisible(false); removeFrame(attachDialog); ProcessInfo info = plist.getSelectedProcess(); if (info != null) { attach(info.getPid()); } } }; Box hbox = Box.createHorizontalBox(); hbox.add(Box.createGlue()); JButton button = new JButton("OK"); button.addActionListener(attacher); hbox.add(button); hbox.add(Box.createHorizontalStrut(20)); button = new JButton("Cancel"); button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { plist.stop(); attachDialog.setVisible(false); removeFrame(attachDialog); setMenuItemsEnabled(attachMenuItems, true); } }); hbox.add(button); hbox.add(Box.createGlue()); panel = new JPanel(); panel.setBorder(GraphicsUtilities.newBorder(5)); panel.add(hbox); attachDialog.getContentPane().add(panel, BorderLayout.SOUTH); addFrame(attachDialog); attachDialog.pack(); attachDialog.setSize(400, 300); GraphicsUtilities.centerInContainer(attachDialog.getComponent(), getParentDimension(attachDialog.getComponent())); attachDialog.show(); } public void showThreadsDialog() { final FrameWrapper threadsDialog = newFrame("Threads"); threadsDialog.getContentPane().setLayout(new BorderLayout()); threadsDialog.setClosable(true); threadsDialog.setResizable(true); ThreadListPanel threads = new ThreadListPanel(getCDebugger(), getAgent().isJavaMode()); threads.addListener(new ThreadListPanel.Listener() { public void setFocus(ThreadProxy thread, JavaThread jthread) { setCurrentThread(thread); // FIXME: print this to GUI, bring some windows to foreground System.err.println("Focus changed to thread " + thread); } }); threads.setBorder(GraphicsUtilities.newBorder(5)); threadsDialog.getContentPane().add(threads); addFrame(threadsDialog); threadsDialog.pack(); GraphicsUtilities.reshapeToAspectRatio(threadsDialog.getComponent(), 3.0f, 0.9f, getParentDimension(threadsDialog.getComponent())); GraphicsUtilities.centerInContainer(threadsDialog.getComponent(), getParentDimension(threadsDialog.getComponent())); threadsDialog.show(); } public void showMemoryDialog() { final FrameWrapper memoryDialog = newFrame("Memory"); memoryDialog.getContentPane().setLayout(new BorderLayout()); memoryDialog.setClosable(true); memoryDialog.setResizable(true); memoryDialog.getContentPane().add(new MemoryViewer(getDebugger(), (getDebugger().getMachineDescription().getAddressSize() == 8)), BorderLayout.CENTER); addFrame(memoryDialog); memoryDialog.pack(); GraphicsUtilities.reshapeToAspectRatio(memoryDialog.getComponent(), 1.0f, 0.7f, getParentDimension(memoryDialog.getComponent())); GraphicsUtilities.centerInContainer(memoryDialog.getComponent(), getParentDimension(memoryDialog.getComponent())); memoryDialog.show(); } /** Changes the editor factory this debugger uses to display source code. Specified factory may be null, in which case the default factory is used. */ public void setEditorFactory(EditorFactory fact) { if (fact != null) { editorFact = fact; } else { editorFact = new DefaultEditorFactory(); } } //---------------------------------------------------------------------- // Internals only below this point // private WorkerThread workerThread; private boolean mdiMode; private JVMDebugger localDebugger; private BugSpotAgent agent = new BugSpotAgent(); private JMenuBar menuBar; /** List */ private java.util.List attachMenuItems; private java.util.List detachMenuItems; private java.util.List debugMenuItems; private java.util.List suspendDebugMenuItems; private java.util.List resumeDebugMenuItems; private FrameWrapper stackFrame; private VariablePanel localsPanel; private StackTracePanel stackTracePanel; private FrameWrapper registerFrame; private RegisterPanel registerPanel; // Used for mixed-language stack traces private Map threadToJavaThreadMap; private JMenu debugMenu; // MDI mode only: desktop pane private JDesktopPane desktop; // Attach/detach state private boolean attached; // Suspension (combined Java/C++) state private boolean suspended; // Fixed-width font private Font fixedWidthFont; // Breakpoint setting // Maps Strings to List/**/ private Map sourceFileToLineNumberInfoMap; // Maps Strings (file names) to Sets of Integers (line numbers) private Map fileToBreakpointMap; // Debug events private javax.swing.Timer debugEventTimer; // Java debug events private boolean javaEventPending; static class BreakpointResult { private boolean success; private boolean set; private int lineNo; private String why; /** For positive results */ BreakpointResult(boolean success, boolean set, int lineNo) { this(success, set, lineNo, null); } /** For negative results */ BreakpointResult(boolean success, boolean set, int lineNo, String why) { this.success = success; this.set = set; this.lineNo = lineNo; this.why = why; } public boolean succeeded() { return success; } public boolean set() { return set; } /** Line at which the breakpoint was actually set; only valid if succeeded() returns true */ public int getLine() { return lineNo; } public String getWhy() { return why; } } // Editors for source code. File name-to-Editor mapping. private Map editors; private EditorFactory editorFact = new DefaultEditorFactory(); private EditorCommands editorComm = new EditorCommands() { public void windowClosed(Editor editor) { editors.remove(editor.getSourceFileName()); } public void toggleBreakpointAtLine(Editor editor, int lineNumber) { // FIXME: handle "lazy" breakpoints where the source file has // been opened with some other mechanism (File -> Open) and we // don't have debug information pointing to that file yet // FIXME: NOT FINISHED BreakpointResult res = handleBreakpointToggle(editor, lineNumber); if (res.succeeded()) { if (res.set()) { editor.showBreakpointAtLine(res.getLine()); } else { editor.clearBreakpointAtLine(res.getLine()); } } else { String why = res.getWhy(); if (why == null) { why = ""; } else { why = ": " + why; } showMessageDialog("Unable to toggle breakpoint" + why, "Unable to toggle breakpoint", JOptionPane.WARNING_MESSAGE); } } }; private void attach(final int pid) { try { getAgent().attach(pid); setMenuItemsEnabled(detachMenuItems, true); setMenuItemsEnabled(suspendDebugMenuItems, false); setMenuItemsEnabled(resumeDebugMenuItems, true); debugMenu.setEnabled(true); attached = true; suspended = true; if (getAgent().isJavaMode()) { System.err.println("Java HotSpot(TM) virtual machine detected."); } else { System.err.println("(No Java(TM) virtual machine detected)"); } // Set up editor map editors = new HashMap(); // Initialize breakpoints fileToBreakpointMap = new HashMap(); // Create combined stack trace and local variable panel JPanel framePanel = new JPanel(); framePanel.setLayout(new BorderLayout()); framePanel.setBorder(GraphicsUtilities.newBorder(5)); localsPanel = new VariablePanel(); JTabbedPane tab = new JTabbedPane(); tab.addTab("Locals", localsPanel); tab.setTabPlacement(JTabbedPane.BOTTOM); framePanel.add(tab, BorderLayout.CENTER); JPanel stackPanel = new JPanel(); stackPanel.setLayout(new BoxLayout(stackPanel, BoxLayout.X_AXIS)); stackPanel.add(new JLabel("Context:")); stackPanel.add(Box.createHorizontalStrut(5)); stackTracePanel = new StackTracePanel(); stackTracePanel.addListener(new StackTracePanel.Listener() { public void frameChanged(CFrame fr, JavaVFrame jfr) { setCurrentFrame(fr, jfr); } }); stackPanel.add(stackTracePanel); framePanel.add(stackPanel, BorderLayout.NORTH); stackFrame = newFrame("Stack"); stackFrame.getContentPane().setLayout(new BorderLayout()); stackFrame.getContentPane().add(framePanel, BorderLayout.CENTER); stackFrame.setResizable(true); stackFrame.setClosable(false); addFrame(stackFrame); stackFrame.setSize(400, 200); GraphicsUtilities.moveToInContainer(stackFrame.getComponent(), 0.0f, 1.0f, 0, 20); stackFrame.show(); // Create register panel registerPanel = new RegisterPanel(); registerPanel.setFont(fixedWidthFont); registerFrame = newFrame("Registers"); registerFrame.getContentPane().setLayout(new BorderLayout()); registerFrame.getContentPane().add(registerPanel, BorderLayout.CENTER); addFrame(registerFrame); registerFrame.setResizable(true); registerFrame.setClosable(false); registerFrame.setSize(225, 200); GraphicsUtilities.moveToInContainer(registerFrame.getComponent(), 1.0f, 0.0f, 0, 0); registerFrame.show(); resetCurrentThread(); } catch (DebuggerException e) { final String errMsg = formatMessage(e.getMessage(), 80); setMenuItemsEnabled(attachMenuItems, true); showMessageDialog("Unable to connect to process ID " + pid + ":\n\n" + errMsg, "Unable to Connect", JOptionPane.WARNING_MESSAGE); getAgent().detach(); } } private synchronized void detachDebugger() { if (!attached) { return; } if (isSuspended()) { resume(); // Necessary for JVMDI resumption } getAgent().detach(); // FIXME: clear out breakpoints (both Java and C/C++) from target // process sourceFileToLineNumberInfoMap = null; fileToBreakpointMap = null; threadToJavaThreadMap = null; editors = null; attached = false; } private synchronized void detach() { detachDebugger(); setMenuItemsEnabled(attachMenuItems, true); setMenuItemsEnabled(detachMenuItems, false); debugMenu.setEnabled(false); if (mdiMode) { // FIXME: is this sufficient, or will I have to do anything else // to the components to kill them off? What about WorkerThreads? desktop.removeAll(); desktop.invalidate(); desktop.validate(); desktop.repaint(); } // FIXME: keep track of all windows and close them even in non-MDI // mode debugEventTimer.stop(); } // Returns a Debugger for processes on the local machine. This is // only used to fetch the process list. private Debugger getLocalDebugger() { if (localDebugger == null) { String os = PlatformInfo.getOS(); String cpu = PlatformInfo.getCPU(); if (os.equals("win32")) { if (!cpu.equals("x86")) { throw new DebuggerException("Unsupported CPU \"" + cpu + "\" for Windows"); } localDebugger = new Win32DebuggerLocal(new MachineDescriptionIntelX86(), true); } else if (os.equals("linux")) { if (!cpu.equals("x86")) { throw new DebuggerException("Unsupported CPU \"" + cpu + "\" for Linux"); } // FIXME: figure out how to specify path to debugger module throw new RuntimeException("FIXME: figure out how to specify path to debugger module"); // localDebugger = new PosixDebuggerLocal(new MachineDescriptionIntelX86(), true); } else { // FIXME: port to Solaris throw new DebuggerException("Unsupported OS \"" + os + "\""); } // FIXME: we require that the primitive type sizes be configured // in order to use basic functionality in class Address such as // the fetching of floating-point values. There are a lot of // assumptions in the current code that Java floats and doubles // are of equivalent size to C values. The configurability of the // primitive type sizes hasn't seemed necessary and in this kind // of debugging scenario (namely, debugging arbitrary C++ // processes) it appears difficult to support that kind of // flexibility. localDebugger.configureJavaPrimitiveTypeSizes(1, 1, 2, 8, 4, 4, 8, 2); } return localDebugger; } private BugSpotAgent getAgent() { return agent; } private Debugger getDebugger() { return getAgent().getDebugger(); } private CDebugger getCDebugger() { return getAgent().getCDebugger(); } private void resetCurrentThread() { setCurrentThread((ThreadProxy) getCDebugger().getThreadList().get(0)); } private void setCurrentThread(ThreadProxy t) { // Create stack trace // FIXME: add ability to intermix C/Java frames java.util.List trace = new ArrayList(); CFrame fr = getCDebugger().topFrameForThread(t); while (fr != null) { trace.add(new StackTraceEntry(fr, getCDebugger())); try { fr = fr.sender(); } catch (AddressException e) { e.printStackTrace(); showMessageDialog("Error while walking stack; stack trace will be truncated\n(see console for details)", "Error walking stack", JOptionPane.WARNING_MESSAGE); fr = null; } } JavaThread jthread = javaThreadForProxy(t); if (jthread != null) { // Java mode, and we have a Java thread. // Find all Java frames on the stack. We currently do this in a // manner which involves minimal interaction between the Java // and C/C++ debugging systems: any C frame which has a PC in an // unknown location (i.e., not in any DSO) is assumed to be a // Java frame. We merge stack segments of unknown frames with // segments of Java frames beginning with native methods. java.util.List javaTrace = new ArrayList(); VFrame vf = jthread.getLastJavaVFrameDbg(); while (vf != null) { if (vf.isJavaFrame()) { javaTrace.add(new StackTraceEntry((JavaVFrame) vf)); vf = vf.sender(); } } // Merge stack traces java.util.List mergedTrace = new ArrayList(); int c = 0; int j = 0; while (c < trace.size()) { StackTraceEntry entry = (StackTraceEntry) trace.get(c); if (entry.isUnknownCFrame()) { boolean gotJavaFrame = false; while (j < javaTrace.size()) { StackTraceEntry javaEntry = (StackTraceEntry) javaTrace.get(j); JavaVFrame jvf = javaEntry.getJavaFrame(); Method m = jvf.getMethod(); if (!m.isNative() || !gotJavaFrame) { gotJavaFrame = true; mergedTrace.add(javaEntry); ++j; } else { break; // Reached native method; have intervening C frames } } if (gotJavaFrame) { // Skip this sequence of unknown frames, as we've // successfully identified it as Java frames while (c < trace.size() && entry.isUnknownCFrame()) { ++c; if (c < trace.size()) { entry = (StackTraceEntry) trace.get(c); } } continue; } } // If we get here, we either have an unknown frame we didn't // know how to categorize or we have a known C frame. Add it // to the trace. mergedTrace.add(entry); ++c; } trace = mergedTrace; } stackTracePanel.setTrace(trace); registerPanel.update(t); } private void setCurrentFrame(CFrame fr, JavaVFrame jfr) { localsPanel.clear(); if (fr != null) { localsPanel.update(fr); // FIXME: load source file if we can find it, otherwise display disassembly LoadObject lo = getCDebugger().loadObjectContainingPC(fr.pc()); if (lo != null) { CDebugInfoDataBase db = lo.getDebugInfoDataBase(); if (db != null) { LineNumberInfo info = db.lineNumberForPC(fr.pc()); if (info != null) { System.err.println("PC " + fr.pc() + ": Source file \"" + info.getSourceFileName() + "\", line number " + info.getLineNumber() + ", PC range [" + info.getStartPC() + ", " + info.getEndPC() + ")"); // OK, here we go... showLineNumber(null, info.getSourceFileName(), info.getLineNumber()); } else { System.err.println("(No line number information for PC " + fr.pc() + ")"); // Dump line number information for database db.iterate(new LineNumberVisitor() { public void doLineNumber(LineNumberInfo info) { System.err.println(" Source file \"" + info.getSourceFileName() + "\", line number " + info.getLineNumber() + ", PC range [" + info.getStartPC() + ", " + info.getEndPC() + ")"); } }); } } } } else { if (Assert.ASSERTS_ENABLED) { Assert.that(jfr != null, "Must have either C or Java frame"); } localsPanel.update(jfr); // See whether we can locate source file and line number // FIXME: infer pathmap entries from user's locating of this // source file // FIXME: figure out what to do for native methods. Possible to // go to line number for the native method declaration? Method m = jfr.getMethod(); Symbol sfn = ((InstanceKlass) m.getMethodHolder()).getSourceFileName(); if (sfn != null) { int bci = jfr.getBCI(); int lineNo = m.getLineNumberFromBCI(bci); if (lineNo >= 0) { // FIXME: show disassembly otherwise showLineNumber(packageName(m.getMethodHolder().getName().asString()), sfn.asString(), lineNo); } } } } private String packageName(String str) { int idx = str.lastIndexOf('/'); if (idx < 0) { return ""; } return str.substring(0, idx).replace('/', '.'); } private JavaThread javaThreadForProxy(ThreadProxy t) { if (!getAgent().isJavaMode()) { return null; } if (threadToJavaThreadMap == null) { threadToJavaThreadMap = new HashMap(); Threads threads = VM.getVM().getThreads(); for (JavaThread thr = threads.first(); thr != null; thr = thr.next()) { threadToJavaThreadMap.put(thr.getThreadProxy(), thr); } } return (JavaThread) threadToJavaThreadMap.get(t); } private static JMenu createMenu(String name, char mnemonic, int mnemonicPos) { JMenu menu = new JMenu(name); menu.setMnemonic(mnemonic); menu.setDisplayedMnemonicIndex(mnemonicPos); return menu; } private static JMenuItem createMenuItem(String name, ActionListener l) { JMenuItem item = new JMenuItem(name); item.addActionListener(l); return item; } private static JMenuItem createMenuItemInternal(String name, ActionListener l, int accelerator, int modifiers) { JMenuItem item = createMenuItem(name, l); item.setAccelerator(KeyStroke.getKeyStroke(accelerator, modifiers)); return item; } private static JMenuItem createMenuItem(String name, ActionListener l, int accelerator) { return createMenuItemInternal(name, l, accelerator, 0); } private static JMenuItem createMenuItem(String name, ActionListener l, char mnemonic, int mnemonicPos) { JMenuItem item = createMenuItem(name, l); item.setMnemonic(mnemonic); item.setDisplayedMnemonicIndex(mnemonicPos); return item; } private static JMenuItem createMenuItem(String name, ActionListener l, int accelerator, int acceleratorMods, char mnemonic, int mnemonicPos) { JMenuItem item = createMenuItemInternal(name, l, accelerator, acceleratorMods); item.setMnemonic(mnemonic); item.setDisplayedMnemonicIndex(mnemonicPos); return item; } /** Punctuates the given string with \n's where necessary to not exceed the given number of characters per line. Strips extraneous whitespace. */ private static String formatMessage(String message, int charsPerLine) { StringBuffer buf = new StringBuffer(message.length()); StringTokenizer tokenizer = new StringTokenizer(message); int curLineLength = 0; while (tokenizer.hasMoreTokens()) { String tok = tokenizer.nextToken(); if (curLineLength + tok.length() > charsPerLine) { buf.append('\n'); curLineLength = 0; } else { if (curLineLength != 0) { buf.append(' '); ++curLineLength; } } buf.append(tok); curLineLength += tok.length(); } return buf.toString(); } private void setMenuItemsEnabled(java.util.List items, boolean enabled) { for (Iterator iter = items.iterator(); iter.hasNext(); ) { ((JMenuItem) iter.next()).setEnabled(enabled); } } private void showMessageDialog(final String message, final String title, final int jOptionPaneKind) { SwingUtilities.invokeLater(new Runnable() { public void run() { if (mdiMode) { JOptionPane.showInternalMessageDialog(desktop, message, title, jOptionPaneKind); } else { JOptionPane.showMessageDialog(null, message, title, jOptionPaneKind); } } }); } private FrameWrapper newFrame(String title) { if (mdiMode) { return new JInternalFrameWrapper(new JInternalFrame(title)); } else { return new JFrameWrapper(new JFrame(title)); } } private void addFrame(FrameWrapper frame) { if (mdiMode) { desktop.add(frame.getComponent()); } } private void removeFrame(FrameWrapper frame) { if (mdiMode) { desktop.remove(frame.getComponent()); desktop.invalidate(); desktop.validate(); desktop.repaint(); } // FIXME: do something when not in MDI mode } private Dimension getParentDimension(Component c) { if (mdiMode) { return desktop.getSize(); } else { return Toolkit.getDefaultToolkit().getScreenSize(); } } // Default editor implementation class DefaultEditor implements Editor { private DefaultEditorFactory factory; private FrameWrapper editorFrame; private String filename; private SourceCodePanel code; private boolean shown; private Object userData; public DefaultEditor(DefaultEditorFactory fact, String filename, final EditorCommands comm) { this.filename = filename; this.factory = fact; editorFrame = newFrame(filename); code = new SourceCodePanel(); // FIXME: when font changes, change font in editors as well code.setFont(fixedWidthFont); editorFrame.getContentPane().add(code); editorFrame.setClosable(true); editorFrame.setResizable(true); editorFrame.setClosingActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { comm.windowClosed(DefaultEditor.this); removeFrame(editorFrame); editorFrame.dispose(); factory.editorClosed(DefaultEditor.this); } }); editorFrame.setActivatedActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { factory.makeEditorCurrent(DefaultEditor.this); code.requestFocus(); } }); code.setEditorCommands(comm, this); } public boolean openFile() { return code.openFile(filename); } public String getSourceFileName() { return filename; } public int getCurrentLineNumber() { return code.getCurrentLineNumber(); } public void showLineNumber(int lineNo) { if (!shown) { addFrame(editorFrame); GraphicsUtilities.reshapeToAspectRatio(editorFrame.getComponent(), 1.0f, 0.85f, getParentDimension(editorFrame.getComponent())); editorFrame.show(); shown = true; } code.showLineNumber(lineNo); editorFrame.toFront(); } public void highlightLineNumber(int lineNo) { code.highlightLineNumber(lineNo); } public void showBreakpointAtLine(int lineNo) { code.showBreakpointAtLine(lineNo); } public boolean hasBreakpointAtLine(int lineNo) { return code.hasBreakpointAtLine(lineNo); } public void clearBreakpointAtLine(int lineNo) { code.clearBreakpointAtLine(lineNo); } public void clearBreakpoints() { code.clearBreakpoints(); } public void setUserData(Object o) { userData = o; } public Object getUserData() { return userData; } public void toFront() { editorFrame.toFront(); factory.makeEditorCurrent(this); } } class DefaultEditorFactory implements EditorFactory { private LinkedList/**/ editors = new LinkedList(); public Editor openFile(String filename, EditorCommands commands) { DefaultEditor editor = new DefaultEditor(this, filename, editorComm); if (!editor.openFile()) { return null; } return editor; } public Editor getCurrentEditor() { if (editors.isEmpty()) { return null; } return (Editor) editors.getFirst(); } void editorClosed(Editor editor) { editors.remove(editor); } void makeEditorCurrent(Editor editor) { editors.remove(editor); editors.addFirst(editor); } } // Helper class for loading .java files; show only those with // correct file name which are also in the correct package static class JavaFileFilter extends javax.swing.filechooser.FileFilter { private String packageName; private String fileName; JavaFileFilter(String packageName, String fileName) { this.packageName = packageName; this.fileName = fileName; } public boolean accept(File f) { if (f.isDirectory()) { return true; } // This rejects most files if (!f.getName().equals(fileName)) { return false; } // Ensure selected file is in the correct package PackageScanner scanner = new PackageScanner(); String pkg = scanner.scan(f); if (!pkg.equals(packageName)) { return false; } return true; } public String getDescription() { return "Java source files"; } } // Auxiliary information used only for Java source files static class JavaUserData { private String packageName; // External format private String sourceFileName; /** Source file name is equivalent to that found in the .java file; i.e., not a full path */ JavaUserData(String packageName, String sourceFileName) { this.packageName = packageName; this.sourceFileName = sourceFileName; } String packageName() { return packageName; } String sourceFileName() { return sourceFileName; } } // Opens a source file. This makes it available for the setting of // lazy breakpoints. private void openSourceFile() { JFileChooser chooser = new JFileChooser(); chooser.setDialogTitle("Open source code file"); chooser.setMultiSelectionEnabled(false); if (chooser.showOpenDialog(null) != JFileChooser.APPROVE_OPTION) { return; } File chosen = chooser.getSelectedFile(); if (chosen == null) { return; } // See whether we have a Java source file. If so, derive a package // name for it. String path = chosen.getPath(); String name = null; JavaUserData data = null; if (path.endsWith(".java")) { PackageScanner scanner = new PackageScanner(); String pkg = scanner.scan(chosen); // Now knowing both the package name and file name, we can put // this in the editor map and use it for setting breakpoints // later String fileName = chosen.getName(); name = pkg + "." + fileName; data = new JavaUserData(pkg, fileName); } else { // FIXME: need pathmap mechanism name = path; } Editor editor = (Editor) editors.get(name); if (editor == null) { editor = editorFact.openFile(path, editorComm); if (editor == null) { showMessageDialog("Unable to open file \"" + path + "\" -- unexpected error.", "Unable to open file", JOptionPane.WARNING_MESSAGE); return; } editors.put(name, editor); if (data != null) { editor.setUserData(data); } } else { editor.toFront(); } editor.showLineNumber(1); // Show breakpoints as well if we have any for this file Set set = (Set) fileToBreakpointMap.get(editor.getSourceFileName()); if (set != null) { for (Iterator iter = set.iterator(); iter.hasNext(); ) { editor.showBreakpointAtLine(((Integer) iter.next()).intValue()); } } } // Package name may be null, in which case the file is assumed to be // a C source file. Otherwise it is assumed to be a Java source file // and certain filtering rules will be applied. private void showLineNumber(String packageName, String fileName, int lineNumber) { String name; if (packageName == null) { name = fileName; } else { name = packageName + "." + fileName; } Editor editor = (Editor) editors.get(name); if (editor == null) { // See whether file exists File file = new File(fileName); String realFileName = fileName; if (!file.exists()) { // User must specify path to file JFileChooser chooser = new JFileChooser(); chooser.setDialogTitle("Please locate " + fileName); chooser.setMultiSelectionEnabled(false); if (packageName != null) { chooser.setFileFilter(new JavaFileFilter(packageName, fileName)); } int res = chooser.showOpenDialog(null); if (res != JFileChooser.APPROVE_OPTION) { // FIXME: show disassembly instead return; } // FIXME: would like to infer more from the selection; i.e., // a pathmap leading up to this file File chosen = chooser.getSelectedFile(); if (chosen == null) { return; } realFileName = chosen.getPath(); } // Now instruct editor factory to open file editor = editorFact.openFile(realFileName, editorComm); if (editor == null) { showMessageDialog("Unable to open file \"" + realFileName + "\" -- unexpected error.", "Unable to open file", JOptionPane.WARNING_MESSAGE); return; } // Got an editor; put it in map editors.put(name, editor); // If Java source file, add additional information for later if (packageName != null) { editor.setUserData(new JavaUserData(packageName, fileName)); } } // Got editor; show line editor.showLineNumber(lineNumber); editor.highlightLineNumber(lineNumber); // Show breakpoints as well if we have any for this file Set set = (Set) fileToBreakpointMap.get(editor.getSourceFileName()); if (set != null) { for (Iterator iter = set.iterator(); iter.hasNext(); ) { editor.showBreakpointAtLine(((Integer) iter.next()).intValue()); } } } // // Suspend/resume // private boolean isSuspended() { return suspended; } private synchronized void suspend() { setMenuItemsEnabled(resumeDebugMenuItems, true); setMenuItemsEnabled(suspendDebugMenuItems, false); BugSpotAgent agent = getAgent(); if (agent.canInteractWithJava() && !agent.isJavaSuspended()) { agent.suspendJava(); } agent.suspend(); // FIXME: call VM.getVM().fireVMSuspended() resetCurrentThread(); debugEventTimer.stop(); suspended = true; } private synchronized void resume() { // Note: we don't wipe out the cached state like the // sourceFileToLineNumberInfoMap since it is too expensive to // recompute. Instead we recompute it if any DLLs are loaded or // unloaded. threadToJavaThreadMap = null; setMenuItemsEnabled(resumeDebugMenuItems, false); setMenuItemsEnabled(suspendDebugMenuItems, true); registerPanel.clear(); // FIXME: call VM.getVM().fireVMResumed() BugSpotAgent agent = getAgent(); agent.resume(); if (agent.canInteractWithJava()) { if (agent.isJavaSuspended()) { agent.resumeJava(); } if (javaEventPending) { javaEventPending = false; // Clear it out before resuming polling for events agent.javaEventContinue(); } } agent.enableJavaInteraction(); suspended = false; debugEventTimer.start(); } // // Breakpoints // private synchronized BreakpointResult handleBreakpointToggle(Editor editor, int lineNumber) { // Currently we only use user data in editors to indicate Java // source files. If this changes then this code will need to // change. JavaUserData data = (JavaUserData) editor.getUserData(); String filename = editor.getSourceFileName(); if (data == null) { // C/C++ code // FIXME: as noted above in EditorCommands.toggleBreakpointAtLine, // this needs more work to handle "lazy" breakpoints in files // which we don't know about in the debug information yet CDebugger dbg = getCDebugger(); ProcessControl prctl = dbg.getProcessControl(); if (prctl == null) { return new BreakpointResult(false, false, 0, "Process control not enabled"); } boolean mustSuspendAndResume = (!prctl.isSuspended()); try { if (mustSuspendAndResume) { prctl.suspend(); } // Search debug info for all DSOs LineNumberInfo info = getLineNumberInfo(filename, lineNumber); if (info != null) { Set bpset = (Set) fileToBreakpointMap.get(filename); if (bpset == null) { bpset = new HashSet(); fileToBreakpointMap.put(filename, bpset); } Integer key = new Integer(info.getLineNumber()); if (bpset.contains(key)) { // Clear breakpoint at this line's PC prctl.clearBreakpoint(info.getStartPC()); bpset.remove(key); return new BreakpointResult(true, false, info.getLineNumber()); } else { // Set breakpoint at this line's PC System.err.println("Setting breakpoint at PC " + info.getStartPC()); prctl.setBreakpoint(info.getStartPC()); bpset.add(key); return new BreakpointResult(true, true, info.getLineNumber()); } } else { return new BreakpointResult(false, false, 0, "No debug information for this source file and line"); } } finally { if (mustSuspendAndResume) { prctl.resume(); } } } else { BugSpotAgent agent = getAgent(); if (!agent.canInteractWithJava()) { String why; if (agent.isJavaInteractionDisabled()) { why = "Can not toggle Java breakpoints while stopped because\nof C/C++ debug events (breakpoints, single-stepping)"; } else { why = "Could not talk to SA's JVMDI module to enable Java\nprogramming language breakpoints (run with -Xdebug -Xrunsa)"; } return new BreakpointResult(false, false, 0, why); } Set bpset = (Set) fileToBreakpointMap.get(filename); if (bpset == null) { bpset = new HashSet(); fileToBreakpointMap.put(filename, bpset); } boolean mustResumeAndSuspend = isSuspended(); try { if (mustResumeAndSuspend) { agent.resume(); } ServiceabilityAgentJVMDIModule.BreakpointToggleResult res = getAgent().toggleJavaBreakpoint(data.sourceFileName(), data.packageName(), lineNumber); if (res.getSuccess()) { Integer key = new Integer(res.getLineNumber()); boolean addRemRes = false; if (res.getWasSet()) { addRemRes = bpset.add(key); System.err.println("Setting breakpoint at " + res.getMethodName() + res.getMethodSignature() + ", bci " + res.getBCI() + ", line " + res.getLineNumber()); } else { addRemRes = bpset.remove(key); System.err.println("Clearing breakpoint at " + res.getMethodName() + res.getMethodSignature() + ", bci " + res.getBCI() + ", line " + res.getLineNumber()); } if (Assert.ASSERTS_ENABLED) { Assert.that(addRemRes, "Inconsistent Java breakpoint state with respect to target process"); } return new BreakpointResult(true, res.getWasSet(), res.getLineNumber()); } else { return new BreakpointResult(false, false, 0, res.getErrMsg()); } } finally { if (mustResumeAndSuspend) { agent.suspend(); resetCurrentThread(); } } } } // Must call only when suspended private LineNumberInfo getLineNumberInfo(String filename, int lineNumber) { Map map = getSourceFileToLineNumberInfoMap(); java.util.List infos = (java.util.List) map.get(filename); if (infos == null) { return null; } // Binary search for line number return searchLineNumbers(infos, lineNumber, 0, infos.size()); } // Must call only when suspended private Map getSourceFileToLineNumberInfoMap() { if (sourceFileToLineNumberInfoMap == null) { // Build from debug info java.util.List loadObjects = getCDebugger().getLoadObjectList(); final Map map = new HashMap(); for (Iterator iter = loadObjects.iterator(); iter.hasNext(); ) { LoadObject lo = (LoadObject) iter.next(); CDebugInfoDataBase db = lo.getDebugInfoDataBase(); if (db != null) { db.iterate(new LineNumberVisitor() { public void doLineNumber(LineNumberInfo info) { String name = info.getSourceFileName(); if (name != null) { java.util.List val = (java.util.List) map.get(name); if (val == null) { val = new ArrayList(); map.put(name, val); } val.add(info); } } }); } } // Sort all lists for (Iterator iter = map.values().iterator(); iter.hasNext(); ) { java.util.List list = (java.util.List) iter.next(); Collections.sort(list, new Comparator() { public int compare(Object o1, Object o2) { LineNumberInfo l1 = (LineNumberInfo) o1; LineNumberInfo l2 = (LineNumberInfo) o2; int n1 = l1.getLineNumber(); int n2 = l2.getLineNumber(); if (n1 < n2) return -1; if (n1 == n2) return 0; return 1; } }); } sourceFileToLineNumberInfoMap = map; } return sourceFileToLineNumberInfoMap; } private LineNumberInfo searchLineNumbers(java.util.List infoList, int lineNo, int lowIdx, int highIdx) { if (highIdx < lowIdx) return null; if (lowIdx == highIdx) { // Base case: see whether start PC is less than or equal to addr if (checkLineNumber(infoList, lineNo, lowIdx)) { return (LineNumberInfo) infoList.get(lowIdx); } else { return null; } } else if (lowIdx == highIdx - 1) { if (checkLineNumber(infoList, lineNo, lowIdx)) { return (LineNumberInfo) infoList.get(lowIdx); } else if (checkLineNumber(infoList, lineNo, highIdx)) { return (LineNumberInfo) infoList.get(highIdx); } else { return null; } } int midIdx = (lowIdx + highIdx) >> 1; LineNumberInfo info = (LineNumberInfo) infoList.get(midIdx); if (lineNo < info.getLineNumber()) { // Always move search down return searchLineNumbers(infoList, lineNo, lowIdx, midIdx); } else if (lineNo == info.getLineNumber()) { return info; } else { // Move search up return searchLineNumbers(infoList, lineNo, midIdx, highIdx); } } private boolean checkLineNumber(java.util.List infoList, int lineNo, int idx) { LineNumberInfo info = (LineNumberInfo) infoList.get(idx); return (info.getLineNumber() >= lineNo); } // // Debug events // private synchronized void pollForDebugEvent() { ProcessControl prctl = getCDebugger().getProcessControl(); if (prctl == null) { return; } DebugEvent ev = prctl.debugEventPoll(); if (ev != null) { DebugEvent.Type t = ev.getType(); if (t == DebugEvent.Type.LOADOBJECT_LOAD || t == DebugEvent.Type.LOADOBJECT_UNLOAD) { // Conservatively clear cached debug info state sourceFileToLineNumberInfoMap = null; // FIXME: would be very useful to have "stop on load/unload" // events // FIXME: must do work at these events to implement lazy // breakpoints prctl.debugEventContinue(); } else if (t == DebugEvent.Type.BREAKPOINT) { // Note: Visual C++ only notifies on breakpoints it doesn't // know about // FIXME: put back test // if (!prctl.isBreakpointSet(ev.getPC())) { showMessageDialog("Breakpoint reached at PC " + ev.getPC(), "Breakpoint reached", JOptionPane.INFORMATION_MESSAGE); // } agent.disableJavaInteraction(); suspend(); prctl.debugEventContinue(); } else if (t == DebugEvent.Type.SINGLE_STEP) { agent.disableJavaInteraction(); suspend(); prctl.debugEventContinue(); } else if (t == DebugEvent.Type.ACCESS_VIOLATION) { showMessageDialog("Access violation attempting to " + (ev.getWasWrite() ? "write" : "read") + " address " + ev.getAddress() + " at PC " + ev.getPC(), "Access Violation", JOptionPane.WARNING_MESSAGE); agent.disableJavaInteraction(); suspend(); prctl.debugEventContinue(); } else { String info = "Unknown debug event encountered"; if (ev.getUnknownEventDetail() != null) { info = info + ": " + ev.getUnknownEventDetail(); } showMessageDialog(info, "Unknown debug event", JOptionPane.INFORMATION_MESSAGE); suspend(); prctl.debugEventContinue(); } return; } // No C++ debug event; poll for Java debug event if (getAgent().canInteractWithJava()) { if (!javaEventPending) { if (getAgent().javaEventPending()) { suspend(); // This does a lot of work and we want to have the page // cache available to us as it runs sun.jvm.hotspot.livejvm.Event jev = getAgent().javaEventPoll(); if (jev != null) { javaEventPending = true; if (jev.getType() == sun.jvm.hotspot.livejvm.Event.Type.BREAKPOINT) { BreakpointEvent bpev = (BreakpointEvent) jev; showMessageDialog("Breakpoint reached in method\n" + bpev.methodID().method().externalNameAndSignature() + ",\nbci " + bpev.location(), "Breakpoint reached", JOptionPane.INFORMATION_MESSAGE); } else if (jev.getType() == sun.jvm.hotspot.livejvm.Event.Type.EXCEPTION) { ExceptionEvent exev = (ExceptionEvent) jev; showMessageDialog(exev.exception().getKlass().getName().asString() + "\nthrown in method\n" + exev.methodID().method().externalNameAndSignature() + "\nat BCI " + exev.location(), "Exception thrown", JOptionPane.INFORMATION_MESSAGE); } else { Assert.that(false, "Should not reach here"); } } } } } } }