/* * Copyright (c) 2007, 2012, 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 com.sun.max.ins; import static com.sun.max.ins.MaxineInspector.*; import static com.sun.max.tele.MaxInspectionMode.*; import static com.sun.max.tele.MaxProcessState.*; import java.io.*; import java.net.*; import java.util.*; import javax.swing.*; import com.sun.cri.ci.*; import com.sun.max.ins.InspectionPreferences.ExternalViewerType; import com.sun.max.ins.gui.*; import com.sun.max.ins.util.*; import com.sun.max.ins.view.*; import com.sun.max.program.*; import com.sun.max.program.option.*; import com.sun.max.tele.*; import com.sun.max.tele.util.*; import com.sun.max.vm.actor.holder.*; import com.sun.max.vm.actor.member.*; import com.sun.max.vm.classfile.*; import com.sun.max.vm.heap.*; /** * Holds the user interaction state for the inspection of a VM, which is accessed via a surrogate implementing {@link MaxVM}. */ public final class Inspection implements InspectionHolder { private static final int TRACE_VALUE = 1; /** * @return a string suitable for tagging all trace lines; mention the thread if it isn't the AWT event handler. */ private String tracePrefix() { if (java.awt.EventQueue.isDispatchThread()) { return "[Inspection] "; } return "[Inspection: " + Thread.currentThread().getName() + "] "; } private final MaxVM vm; private final String bootImageFileName; private final OptionSet options; private final InspectorNameDisplay nameDisplay; private final InspectionFocus focus; private final InspectionPreferences preferences; private static final String SETTINGS_FILE_NAME = "maxine.ins"; private final InspectionSettings settings; private final InspectionActions inspectionActions; private final InspectionViews inspectionViews; private InspectorMainFrame inspectorMainFrame; public Inspection(MaxVM vm, OptionSet options) { Trace.begin(TRACE_VALUE, tracePrefix() + "Initializing"); final long startTimeMillis = System.currentTimeMillis(); this.vm = vm; this.options = options; this.bootImageFileName = vm.bootImageFile().getAbsolutePath().toString(); this.nameDisplay = new InspectorNameDisplay(this); this.focus = new InspectionFocus(this); this.settings = new InspectionSettings(this, new File(vm.programFile().getParentFile(), SETTINGS_FILE_NAME)); this.preferences = new InspectionPreferences(this, settings); this.inspectionActions = new InspectionActions(this); this.inspectionViews = new InspectionViews(this); BreakpointPersistenceManager.initialize(this); inspectionActions.refresh(true); vm.addVMStateListener(new VMStateListener()); vm.breakpointManager().addListener(new BreakpointListener()); if (vm.watchpointManager() != null) { vm.watchpointManager().addListener(new WatchpointListener()); } inspectorMainFrame = new InspectorMainFrame(this, MaxineInspector.NAME, nameDisplay, settings, inspectionActions); if (vm.state().processState() == UNKNOWN) { // Inspector is working with a boot image only, no process exists. inspectionViews.activateInitialViews(); } else { try { // Choose an arbitrary thread as the "current" thread. If the Inspector is // creating the process to be debugged (as opposed to attaching to it), then there // should only be one thread. final List<MaxThread> threads = vm().state().threads(); MaxThread nonJavaThread = null; for (MaxThread thread : threads) { if (thread.isJava()) { focus.setThread(thread); nonJavaThread = null; break; } nonJavaThread = thread; } if (nonJavaThread != null) { focus.setThread(nonJavaThread); } inspectionViews.activateInitialViews(); focus.setCodeLocation(focus.thread().ipLocation()); } catch (Throwable throwable) { InspectorWarning.message(null, "Error during initialization", throwable); throwable.printStackTrace(); System.exit(1); } } refreshAll(false); inspectorMainFrame.refresh(true); inspectorMainFrame.setVisible(true); Trace.end(TRACE_VALUE, tracePrefix() + "Initializing", startTimeMillis); } public Inspection inspection() { return this; } public MaxVM vm() { return vm; } public InspectorGUI gui() { return inspectorMainFrame; } public InspectionFocus focus() { return focus; } public InspectionViews views() { return inspectionViews; } public InspectionActions actions() { return inspectionActions; } public InspectionPreferences preference() { return preferences; } public OptionSet options() { return options; } /** * Updates the string appearing the outermost window frame: program name, process state, boot image filename. */ public String currentInspectionTitle() { final StringBuilder sb = new StringBuilder(50); sb.append(NAME); sb.append(" (mode=").append(vm().inspectionMode().toString()).append(")"); if (vm().inspectionMode() != IMAGE) { sb.append(" VM Process "); final MaxVMState vmState = vm().state(); if (vmState == null) { sb.append(UNKNOWN.label()); } else { sb.append(vmState.processState().label()); if (vmState.heapPhase() != HeapPhase.MUTATING) { sb.append(", heap=" + vmState.heapPhase().label()); } if (vmState.isInEviction()) { sb.append(", in Eviction"); } } } return sb.toString(); } public InspectionSettings settings() { return settings; } /** * @return Inspection utility for generating standard, human-intelligible names for entities in the inspection * environment. */ public InspectorNameDisplay nameDisplay() { return nameDisplay; } /** * @return Is the Inspector in debugging mode with a legitimate process? */ public boolean hasProcess() { final MaxProcessState processState = vm().state().processState(); return !(processState == UNKNOWN || processState == TERMINATED); } /** * Is the VM running, as of the most recent direct (synchronous) notification by the VM? * * @return VM state == {@link MaxProcessState#RUNNING}. */ public boolean isVMRunning() { return vm().state().processState() == RUNNING; } /** * Is the VM available to start running, as of the most recent direct (synchronous) notification by the VM? * * @return VM state == {@link MaxProcessState#STOPPED}. */ public boolean isVMReady() { return vm().state().processState() == STOPPED; } private MaxVMState lastVMStateProcessed = null; /** * Gets a copy of the current set of inspection listeners. This is useful for iterating * over the listeners where the body of the loop may change {@link #inspectionListeners}. */ private InspectionListener[] copyInspectionListeners() { return inspectionListeners.toArray(new InspectionListener[inspectionListeners.size()]); } /** * Handles reported changes in the {@linkplain MaxVM#state() VM process state}. * Must only be run in AWT event thread. */ private void processVMStateChange() { // Ensure that we're just looking at one state while making decisions, even // though display elements may find the VM in a newer state by the time they // attempt to update their state. final MaxVMState vmState = vm().state(); final String stateDescription = vm.state().toString(); final TimedTrace tracer = new TimedTrace(TRACE_VALUE, tracePrefix() + "process new VM state=" + stateDescription); tracer.begin(); inspectorMainFrame.refresh(true); if (!vmState.newerThan(lastVMStateProcessed)) { Trace.line(1, tracePrefix() + "redundant state change=" + stateDescription); } lastVMStateProcessed = vmState; switch (vmState.processState()) { case STOPPED: updateAfterVMStopped(); break; case RUNNING: break; case TERMINATED: Trace.line(1, tracePrefix() + " - VM process terminated"); // Clear any possibly misleading view state. focus().clearAll(); // Give all process-sensitive views a chance to shut down for (InspectionListener listener : copyInspectionListeners()) { listener.vmProcessTerminated(); } // Clear any possibly misleading view state. focus().clearAll(); // Be sure all process-sensitive actions are disabled. inspectionActions.refresh(false); break; case NONE: case UNKNOWN: break; } inspectorMainFrame.refresh(true); inspectionActions.refresh(true); tracer.end(); } /** * Handles reported changes in the {@linkplain MaxVM#state() VM state}. * Updates state synchronously, then posts an event for follow-up on the AST event thread */ private final class VMStateListener implements MaxVMStateListener { public void stateChanged(final MaxVMState vmState) { Trace.line(TRACE_VALUE, tracePrefix() + "notified vmState=" + vmState); for (MaxThread thread : vmState.threadsStarted()) { Trace.line(TRACE_VALUE, tracePrefix() + "started: " + thread); } for (MaxThread thread : vmState.threadsDied()) { Trace.line(TRACE_VALUE, tracePrefix() + "died: " + thread); } if (java.awt.EventQueue.isDispatchThread()) { processVMStateChange(); } else { Tracer tracer = null; if (Trace.hasLevel(TRACE_VALUE)) { tracer = new Tracer("scheduled " + vmState); } Trace.begin(TRACE_VALUE, tracer); SwingUtilities.invokeLater(new Runnable() { public void run() { processVMStateChange(); } @Override public String toString() { return "processVMStateChange"; } }); Trace.end(TRACE_VALUE, tracer); } } } /** * Propagates reported breakpoint changes in the VM. * Ensures that notification is handled only on the * AWT event thread. */ private final class BreakpointListener implements MaxBreakpointListener { public void breakpointsChanged() { Runnable runnable = new Runnable() { public void run() { Trace.begin(TRACE_VALUE, tracePrefix() + "breakpoint state change notification"); for (InspectionListener listener : copyInspectionListeners()) { listener.breakpointStateChanged(); } Trace.end(TRACE_VALUE, tracePrefix() + "breakpoint state change notification"); } }; runNotification(runnable); } public void breakpointToBeDeleted(final MaxBreakpoint breakpoint, final String reason) { Runnable runnable = new Runnable() { public void run() { Trace.begin(TRACE_VALUE, tracePrefix() + "breakpoint being deleted notification"); for (InspectionListener listener : copyInspectionListeners()) { listener.breakpointToBeDeleted(breakpoint, reason); } Trace.end(TRACE_VALUE, tracePrefix() + "breakpoint being deleted notification"); } }; runNotification(runnable); } private void runNotification(Runnable runnable) { if (java.awt.EventQueue.isDispatchThread()) { runnable.run(); } else { SwingUtilities.invokeLater(runnable); } } } /** * Propagates reported watchpoint changes in the VM. * Ensures that notification is handled only on the * AWT event thread. */ private final class WatchpointListener implements MaxWatchpointListener { public void watchpointsChanged() { Runnable runnable = new Runnable() { public void run() { Trace.begin(TRACE_VALUE, tracePrefix() + "watchpoint state change notification"); for (InspectionListener listener : copyInspectionListeners()) { listener.watchpointSetChanged(); } Trace.end(TRACE_VALUE, tracePrefix() + "watchpoint state change notification"); } }; if (java.awt.EventQueue.isDispatchThread()) { runnable.run(); } else { SwingUtilities.invokeLater(runnable); } } } private InspectorAction currentAction = null; /** * Holds the action currently being performed; null when finished. */ public InspectorAction currentAction() { return currentAction; } void setCurrentAction(InspectorAction action) { currentAction = action; } /** * @return default title for any messages: defaults to name of current {@link InspectorAction} if one is current, * otherwise the generic name of the Inspector. */ public String currentActionTitle() { return currentAction != null ? currentAction.name() : MaxineInspector.NAME; } private Set<InspectionListener> inspectionListeners = CiUtil.newIdentityHashSet(); /** * Adds a listener for view update when VM state changes. */ public void addInspectionListener(InspectionListener listener) { Trace.line(TRACE_VALUE, tracePrefix() + "adding inspection listener: " + listener); inspectionListeners.add(listener); } /** * Removes a listener for view update, for example when an Inspector is disposed or when the default notification * mechanism is being overridden. */ public void removeInspectionListener(InspectionListener listener) { Trace.line(TRACE_VALUE, tracePrefix() + "removing inspection listener: " + listener); inspectionListeners.remove(listener); } /** * Update all views by reading from VM state as needed. * * @param force suspend caching behavior; reload state unconditionally. */ public void refreshAll(boolean force) { // Additional listeners may come and go during the update cycle, which can be ignored. for (InspectionListener listener : copyInspectionListeners()) { listener.vmStateChanged(force); } inspectionActions.refresh(force); } /** * Updates all views, assuming that display and style parameters may have changed; forces state reload from the * VM. */ void updateViewConfiguration() { for (InspectionListener listener : inspectionListeners) { Trace.line(TRACE_VALUE, tracePrefix() + "updateViewConfiguration: " + listener); listener.viewConfigurationChanged(); } inspectionActions.redisplay(); inspectorMainFrame.redisplay(); } /** * This is the main update loop for all inspection state after a period of VM execution. * Determines what happened in VM execution that just concluded. Then updates all view state as needed. */ public void updateAfterVMStopped() { gui().showInspectorBusy(true); // Clear any breakpoint selection; if we're at a breakpoint, it will be highlighted. // This also avoids a regrettable event bug, where the breakpoint view decides // on update to send the method viewer to the currently selected breakpoint, even // if it has nothing to do with where we are. focus().setBreakpoint(null); if (!focus().thread().isLive()) { // Our most recent thread focus died; pick a new one to maintain the // invariant, even if another one gets set eventually. focus().setThread(vm().state().threads().get(0)); } try { // Notify all listeners (Inspectors, menu items, etc.() that // there has been a significant VM state change. refreshAll(false); // Make visible the code at the IP of the thread that triggered the breakpoint // or the memory location that triggered a watchpoint final MaxWatchpointEvent watchpointEvent = vm().state().watchpointEvent(); if (watchpointEvent != null) { focus().setThread(watchpointEvent.thread()); focus().setWatchpoint(watchpointEvent.watchpoint()); focus().setAddress(watchpointEvent.address()); } else if (!vm().state().breakpointEvents().isEmpty()) { final MaxThread thread = vm().state().breakpointEvents().get(0).thread(); if (thread != null) { focus().setThread(thread); } else { // If there was no selection based on breakpoint, then check the thread that was selected before the // change. InspectorError.check(focus().thread().isLive(), "Selected thread no longer valid"); } } // Reset focus to new IP. final MaxThread focusThread = focus().thread(); focus().setStackFrame(focusThread.stack().top(), false); } catch (Throwable throwable) { InspectorError.unexpected("could not update view", throwable).display(this); } finally { gui().showInspectorBusy(false); } } /** * Make a standard announcement that an action has failed because the Inspector * was unable to acquire the lock on the VM. * * @param attemptedAction description of what was being attempted */ public void announceVMBusyFailure(String attemptedAction) { gui().errorMessage(attemptedAction + " failed: VM Busy"); } /** * Saves any persistent state, then shuts down VM process if needed and inspection. */ public void quit() { for (InspectionListener listener : inspectionListeners) { Trace.line(TRACE_VALUE, tracePrefix() + "inspection quitting: " + listener); listener.inspectionEnding(); } settings().quit(); try { if (vm().state().processState() != TERMINATED) { vm().terminateVM(); } } catch (Exception exception) { InspectorWarning.message(null, "error during VM termination: " + exception); } finally { Trace.line(1, tracePrefix() + " exiting, Goodbye"); System.exit(0); } } /** * If an external viewer has been {@linkplain InspectionPreferences#setExternalViewer(ExternalViewerType) configured}, attempt to view a * source file location corresponding to a given bytecode location. The view attempt is only made if an existing * source file and source line number can be derived from the given bytecode location. * * @param codePos specifies a bytecode position in a class method actor * @return true if a file viewer was opened */ public boolean viewSourceExternally(CiCodePos codePos) { if (preferences.externalViewerType() == ExternalViewerType.NONE) { return false; } final ClassMethodActor classMethodActor = (ClassMethodActor) codePos.method; final CodeAttribute codeAttribute = classMethodActor.codeAttribute(); final int lineNumber = codeAttribute.lineNumberTable().findLineNumber(codePos.bci); if (lineNumber == -1) { return false; } return viewSourceExternally(classMethodActor.holder(), lineNumber); } /** * If an external viewer has been {@linkplain InspectionPreferences#setExternalViewer(ExternalViewerType) configured}, attempt to view a * source file location corresponding to a given class actor and line number. The view attempt is only made if an * existing source file and source line number can be derived from the given bytecode location. * * @param classActor the class whose source file is to be viewed * @param lineNumber the line number at which the viewer should position the current focus point * @return true if a file viewer was opened */ public boolean viewSourceExternally(ClassActor classActor, int lineNumber) { if (preferences.externalViewerType() == ExternalViewerType.NONE) { return false; } final File javaSourceFile = vm().findJavaSourceFile(classActor); if (javaSourceFile == null) { return false; } switch (preferences.externalViewerType()) { case PROCESS: { final String config = preferences.externalViewerConfig().get(ExternalViewerType.PROCESS); if (config != null) { final String command = config.replaceAll("\\$file", javaSourceFile.getAbsolutePath()).replaceAll("\\$line", String.valueOf(lineNumber)); try { Trace.line(1, tracePrefix() + "Opening file by executing " + command); Runtime.getRuntime().exec(command); } catch (IOException ioException) { InspectorWarning.message(this, "Error opening file by executing " + command, ioException); return false; } } break; } case SOCKET: { final String hostname = null; final String portString = preferences.externalViewerConfig().get(ExternalViewerType.SOCKET); if (portString != null) { try { final int port = Integer.parseInt(portString); final Socket fileViewer = new Socket(hostname, port); final String command = javaSourceFile.getAbsolutePath() + "|" + lineNumber; Trace.line(1, tracePrefix() + "Opening file '" + command + "' via localhost:" + portString); final OutputStream fileViewerStream = fileViewer.getOutputStream(); fileViewerStream.write(command.getBytes()); fileViewerStream.flush(); fileViewer.close(); } catch (IOException ioException) { InspectorWarning.message(this, "Error opening file via localhost:" + portString + ": " + ioException); return false; } } break; } default: { InspectorError.unknownCase(); } } return true; } /** * An object that delays evaluation of a trace message for controller actions. */ private class Tracer { private final String message; /** * An object that delays evaluation of a trace message. * * @param message identifies what is being traced */ public Tracer(String message) { this.message = message; } @Override public String toString() { return tracePrefix() + message; } } }