/******************************************************************************* * Copyright (c) 2000, 2016 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation * Yevgen Kogan - Bug 403475 - Hot Code Replace drops too much frames in some cases *******************************************************************************/ package org.eclipse.jdt.internal.debug.core.hcr; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceChangeEvent; import org.eclipse.core.resources.IResourceChangeListener; import org.eclipse.core.resources.IResourceDelta; import org.eclipse.core.resources.IResourceDeltaVisitor; import org.eclipse.core.resources.IWorkspace; import org.eclipse.core.resources.IncrementalProjectBuilder; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.ListenerList; import org.eclipse.core.runtime.MultiStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.debug.core.DebugEvent; import org.eclipse.debug.core.DebugException; import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.IDebugEventSetListener; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchListener; import org.eclipse.debug.core.ILaunchManager; import org.eclipse.debug.core.model.IDebugTarget; import org.eclipse.debug.core.model.IThread; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IJavaModelMarker; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IMethod; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.Signature; import org.eclipse.jdt.core.ToolFactory; import org.eclipse.jdt.core.util.IClassFileReader; import org.eclipse.jdt.core.util.ISourceAttribute; import org.eclipse.jdt.debug.core.IJavaDebugTarget; import org.eclipse.jdt.debug.core.IJavaHotCodeReplaceListener; import org.eclipse.jdt.debug.core.IJavaStackFrame; import org.eclipse.jdt.debug.core.IJavaThread; import org.eclipse.jdt.debug.core.JDIDebugModel; import org.eclipse.jdt.internal.core.util.Util; import org.eclipse.jdt.internal.debug.core.JDIDebugPlugin; import org.eclipse.jdt.internal.debug.core.JavaDebugUtils; import org.eclipse.jdt.internal.debug.core.model.JDIDebugTarget; import org.eclipse.jdt.internal.debug.core.model.JDIStackFrame; import org.eclipse.jdt.internal.debug.core.model.JDIThread; import com.ibm.icu.text.MessageFormat; import com.sun.jdi.IncompatibleThreadStateException; import com.sun.jdi.ReferenceType; import com.sun.jdi.VirtualMachine; /** * The hot code replace manager listens for changes to class files and notifies * running debug targets of the changes. * <p> * Currently, replacing .jar files has no effect on running targets. */ public class JavaHotCodeReplaceManager implements IResourceChangeListener, ILaunchListener, IDebugEventSetListener { /** * Singleton */ private static JavaHotCodeReplaceManager fgInstance = null; /** * The class file extension */ private static final String CLASS_FILE_EXTENSION = "class"; //$NON-NLS-1$ /** * The list of <code>IJavaHotCodeReplaceListeners</code> which this hot code * replace manager will notify about hot code replace attempts. */ private ListenerList<IJavaHotCodeReplaceListener> fHotCodeReplaceListeners = new ListenerList<>(); /** * The lists of hot swap targets which support HCR and those which don't */ private ArrayList<JDIDebugTarget> fHotSwapTargets = new ArrayList<>(1); private ArrayList<JDIDebugTarget> fNoHotSwapTargets = new ArrayList<>(1); /** * A mapping of the last time projects were built. * <ol> * <li>key: project (IProject)</li> * <li>value: build date (ProjectBuildTime)</li> * </ol> */ private Map<IProject, ProjectBuildTime> fProjectBuildTimes = new HashMap<>(); private static Date fStartupDate = new Date(); /** * Cache of compilation unit deltas renewed on each HCR attempt. */ private Map<ICompilationUnit, CompilationUnitDelta> fDeltaCache = new HashMap<>(); /** * Utility object used for tracking build times of projects. The HCR manager * receives notification of builds AFTER the build has occurred but BEFORE * the classfile resource changed deltas are fired. Thus, when the current * build time is set, we need to hang onto the last build time so that we * can use the last build time for comparing changes to compilation units * (for smart drop to frame). */ class ProjectBuildTime { private Date fCurrentDate = new Date(); private Date fPreviousDate = new Date(); public void setCurrentBuildDate(Date date) { fPreviousDate = fCurrentDate; fCurrentDate = date; } public void setLastBuildDate(Date date) { fPreviousDate = date; if (fPreviousDate.getTime() > fCurrentDate.getTime()) { // If the previous date is set later than the current // date, move the current date up to the previous. fCurrentDate = fPreviousDate; } } /** * Returns the last build time */ public Date getLastBuildDate() { return fPreviousDate; } } /** * Visitor for resource deltas. */ protected ChangedClassFilesVisitor fClassfileVisitor = new ChangedClassFilesVisitor(); /** * Creates a new HCR manager */ private JavaHotCodeReplaceManager() { } /** * Returns the singleton HCR manager */ public static synchronized JavaHotCodeReplaceManager getDefault() { if (fgInstance == null) { fgInstance = new JavaHotCodeReplaceManager(); } return fgInstance; } /** * Registers this HCR manager as a resource change listener. This method is * called by the JDI debug model plug-in on startup. */ public void startup() { DebugPlugin.getDefault().getLaunchManager().addLaunchListener(this); DebugPlugin.getDefault().addDebugEventListener(this); } /** * unregisters this HCR manager as a resource change listener. Removes all * hot code replace listeners. This method is called by the JDI debug model * plug-in on shutdown. */ public void shutdown() { DebugPlugin.getDefault().getLaunchManager().removeLaunchListener(this); DebugPlugin.getDefault().removeDebugEventListener(this); getWorkspace().removeResourceChangeListener(this); fHotCodeReplaceListeners = new ListenerList<>(); synchronized (this) { fHotSwapTargets.clear(); fNoHotSwapTargets.clear(); } } /** * Returns the workspace. */ protected IWorkspace getWorkspace() { return ResourcesPlugin.getWorkspace(); } /** * Returns the launch manager. */ protected ILaunchManager getLaunchManager() { return DebugPlugin.getDefault().getLaunchManager(); } /** * @see IResourceChangeListener#resourceChanged(org.eclipse.core.resources.IResourceChangeEvent) */ @Override public void resourceChanged(IResourceChangeEvent event) { List<IProject> projects = getBuiltProjects(event); if (!projects.isEmpty()) { updateProjectBuildTime(projects); } synchronized (this) { if (fHotSwapTargets.isEmpty() && fNoHotSwapTargets.isEmpty()) { // If there are no targets to notify, only update the build // times. return; } } ChangedClassFilesVisitor visitor = getChangedClassFiles(event); if (visitor != null) { List<IResource> resources = visitor.getChangedClassFiles(); List<String> names = visitor.getQualifiedNamesList(); if (!resources.isEmpty()) { notifyTargets(resources, names); } } } /** * Returns all projects which this event says may have been built. */ protected List<IProject> getBuiltProjects(IResourceChangeEvent event) { IResourceDelta delta = event.getDelta(); if (event.getType() != IResourceChangeEvent.POST_BUILD || delta == null || event.getBuildKind() == 0) { return Collections.EMPTY_LIST; } if (event.getBuildKind() == IncrementalProjectBuilder.AUTO_BUILD && !ResourcesPlugin.getWorkspace().isAutoBuilding()) { // If this is an auto build and the workspace is not autobuilding, // no projects will actually be compiled. return Collections.EMPTY_LIST; } Object source = event.getSource(); if (source instanceof IProject) { List<IProject> list = new ArrayList<>(); list.add((IProject) source); return list; } else if (source instanceof IWorkspace) { IProject[] allProjects = ((IWorkspace) source).getRoot() .getProjects(); return Arrays.asList(allProjects); } return Collections.EMPTY_LIST; } /** * If the given event contains a build notification, update the last build * time of the corresponding project */ private void updateProjectBuildTime(List<IProject> projects) { Date currentDate = new Date(); ProjectBuildTime buildTime = null; for(IProject project : projects) { buildTime = fProjectBuildTimes.get(project); if (buildTime == null) { buildTime = new ProjectBuildTime(); fProjectBuildTimes.put(project, buildTime); } buildTime.setCurrentBuildDate(currentDate); } } /** * Returns the last known build time for the given project. If no build time * is known for the given project, the last known build time for the project * is set to the hot code replace manager's startup time. */ protected long getLastProjectBuildTime(IProject project) { ProjectBuildTime time = fProjectBuildTimes .get(project); if (time == null) { time = new ProjectBuildTime(); time.setLastBuildDate(fStartupDate); fProjectBuildTimes.put(project, time); } return time.getLastBuildDate().getTime(); } /** * Notifies the targets of the changed types */ private void notifyTargets(final List<IResource> resources, final List<String> qualifiedNames) { final List<JDIDebugTarget> hotSwapTargets = getHotSwapTargets(); final List<JDIDebugTarget> noHotSwapTargets = getNoHotSwapTargets(); if (!hotSwapTargets.isEmpty()) { Runnable runnable = new Runnable() { @Override public void run() { doHotCodeReplace(hotSwapTargets, resources, qualifiedNames); } }; DebugPlugin.getDefault().asyncExec(runnable); } if (!noHotSwapTargets.isEmpty()) { Runnable runnable = new Runnable() { @Override public void run() { notifyUnsupportedHCR(noHotSwapTargets, resources, qualifiedNames); } }; DebugPlugin.getDefault().asyncExec(runnable); } } /** * Filters elements out of the given collections of resources and qualified * names if there is no type corresponding type loaded in the given debug * target. This method allows us to avoid bogus HCR attempts and * "HCR failed" notifications. * * @param target * the debug target * @param resources * the list of resources to filter * @param qualifiedNames * the list of qualified names to filter, which corresponds to * the list of resources on a one-to-one-basis */ private void filterUnloadedTypes(JDIDebugTarget target, List<IResource> resources, List<String> qualifiedNames) { for (int i = 0, numElements = qualifiedNames.size(); i < numElements; i++) { String name = qualifiedNames.get(i); List<ReferenceType> list = target.jdiClassesByName(name); if (list.isEmpty()) { // If no classes with the given name are loaded in the VM, don't // waste // cycles trying to replace. qualifiedNames.remove(i); resources.remove(i); // Decrement the index and number of elements to compensate for // item removal i--; numElements--; } } } /** * Notify the given targets that HCR failed for classes with the given fully * qualified names. */ protected void notifyUnsupportedHCR(List<JDIDebugTarget> targets, List<IResource> resources, List<String> qualifiedNames) { Iterator<JDIDebugTarget> iter = targets.iterator(); JDIDebugTarget target = null; while (iter.hasNext()) { target = iter.next(); if (target.isAvailable()) { // Make a local copy of the resources/names to swap so we can // filter // unloaded types on a per-target basis. List<IResource> resourcesToReplace = new ArrayList<>(resources); List<String> qualifiedNamesToReplace = new ArrayList<>(qualifiedNames); filterUnloadedTypes(target, resourcesToReplace, qualifiedNamesToReplace); if (!qualifiedNamesToReplace.isEmpty()) { // Don't notify if the changed types aren't loaded. fireHCRFailed(target, null); notifyFailedHCR(target, qualifiedNamesToReplace); } } else { // Targets should be unregistered when they terminate, // but this is a fall-back. deregisterTarget(target); } } } protected void notifyFailedHCR(JDIDebugTarget target, List<String> qualifiedNames) { if (target.isAvailable()) { target.addOutOfSynchTypes(qualifiedNames); target.fireChangeEvent(DebugEvent.STATE); } } /** * Returns the currently registered debug targets that support hot code * replace. */ protected synchronized List<JDIDebugTarget> getHotSwapTargets() { return new ArrayList<>(fHotSwapTargets); } /** * Returns the currently registered debug targets that do not support hot * code replace. */ protected synchronized List<JDIDebugTarget> getNoHotSwapTargets() { return new ArrayList<>(fNoHotSwapTargets); } /** * Perform a hot code replace with the given resources. For a JDK 1.4 * compliant VM this involves: * <ol> * <li>Popping all frames from all thread stacks which will be affected by * reloading the given resources</li> * <li>Telling the VirtualMachine to redefine the affected classes</li> * <li>Performing a step-into operation on all threads which were affected * by the class redefinition. This returns execution to the first (deepest) * affected method on the stack</li> * </ol> * For a J9 compliant VM this involves: * <ol> * <li>Telling the VirtualMachine to redefine the affected classes</li> * <li>Popping all frames from all thread stacks which were affected by * reloading the given resources and then performing a step-into operation * on all threads which were affected by the class redefinition.</li> * </ol> * * @param targets * the targets in which to perform HCR * @param resources * the resources which correspond to the changed classes */ private void doHotCodeReplace(List<JDIDebugTarget> targets, List<IResource> resources, List<String> qualifiedNames) { // Check whether hot code replace is enabled if (!Platform.getPreferencesService().getBoolean( JDIDebugPlugin.getUniqueIdentifier(), JDIDebugPlugin.PREF_ENABLE_HCR, true, null)) { return; // disabled } MultiStatus ms = new MultiStatus( JDIDebugPlugin.getUniqueIdentifier(), DebugException.TARGET_REQUEST_FAILED, "At least one target failed to drop to frame after successful hot code replace.", null); //$NON-NLS-1$ Iterator<JDIDebugTarget> iter = targets.iterator(); while (iter.hasNext()) { JDIDebugTarget target = iter.next(); if (!target.isAvailable()) { deregisterTarget(target); continue; } // Make a local copy of the resources/names to swap so we can filter // unloaded types on a per-target basis. List<IResource> resourcesToReplace = new ArrayList<>(resources); List<String> qualifiedNamesToReplace = new ArrayList<>(qualifiedNames); // Make sure we only try to replace types from related projects filterUnrelatedResources(target, resourcesToReplace, qualifiedNamesToReplace); if (qualifiedNamesToReplace.isEmpty()) { // If none of the changed types are related to our target, do nothing. continue; } filterUnloadedTypes(target, resourcesToReplace, qualifiedNamesToReplace); if (qualifiedNamesToReplace.isEmpty()) { // If none of the changed types are loaded, do nothing. continue; } List<IThread> poppedThreads = new ArrayList<>(); target.setIsPerformingHotCodeReplace(true); try { boolean framesPopped = false; if (target.canPopFrames()) { // JDK 1.4 drop to frame support: // JDK 1.4 spec is faulty around methods that have // been rendered obsolete after class redefinition. // Thus, pop the frames that contain affected methods // *before* the class redefinition to avoid problems. try { attemptPopFrames(target, resourcesToReplace, qualifiedNamesToReplace, poppedThreads); framesPopped = true; // No exception occurred } catch (DebugException de) { if (shouldLogHCRException(de)) { ms.merge(de.getStatus()); } } } target.removeOutOfSynchTypes(qualifiedNamesToReplace); if (target.supportsJDKHotCodeReplace()) { redefineTypesJDK(target, resourcesToReplace, qualifiedNamesToReplace); } else if (target.supportsJ9HotCodeReplace()) { redefineTypesJ9(target, qualifiedNamesToReplace); } if (containsObsoleteMethods(target)) { fireObsoleteMethods(target); } try { if (target.canPopFrames() && framesPopped) { // Second half of JDK 1.4 drop to frame support: // All affected frames have been popped and the classes // have been reloaded. Step into the first changed // frame of each affected thread. // must re-set 'is doing HCR' to be able to step target.setIsPerformingHotCodeReplace(false); attemptStepIn(poppedThreads); } else { // J9 drop to frame support: // After redefining classes, drop to frame attemptDropToFrame(target, resourcesToReplace, qualifiedNamesToReplace); } } catch (DebugException de) { if (shouldLogHCRException(de)) { ms.merge(de.getStatus()); } } fireHCRSucceeded(target); } catch (DebugException de) { // target update failed fireHCRFailed(target, de); } // also re-set 'is doing HCR' here in case HCR failed target.setIsPerformingHotCodeReplace(false); target.fireChangeEvent(DebugEvent.CONTENT); } if (!ms.isOK()) { JDIDebugPlugin.log(ms); } fDeltaCache.clear(); } private void filterUnrelatedResources(JDIDebugTarget target, List<IResource> resourcesToReplace, List<String> qualifiedNamesToReplace) { Iterator<IResource> resources = resourcesToReplace.iterator(); Iterator<String> names = qualifiedNamesToReplace.iterator(); while (resources.hasNext()) { boolean supported = target.supportsResource(() -> names.next(), resources.next()); if (!supported) { resources.remove(); names.remove(); } } } /** * Returns whether the given exception, which occurred during HCR, should be * logged. We anticipate that we can get IncompatibleThreadStateExceptions * if the user happens to resume a thread at just the right moment. Since * this has no ill effects for HCR, we don't log these exceptions. */ private boolean shouldLogHCRException(DebugException exception) { return !(exception.getStatus().getException() instanceof IncompatibleThreadStateException || exception.getStatus().getCode() == IJavaThread.ERR_INCOMPATIBLE_THREAD_STATE || exception .getStatus().getCode() == IJavaThread.ERR_THREAD_NOT_SUSPENDED); } /** * Replaces the given types in the given J9 debug target. A fully qualified * name of each type must be supplied. * * Breakpoints are reinstalled automatically when the new types are loaded. * * @exception DebugException * if this method fails. Reasons include: * <ul> * <li>Failure communicating with the VM. The * DebugException's status code contains the underlying * exception responsible for the failure.</li> * <li>The target VM was unable to reload a type due to a * shape change</li> * </ul> */ private void redefineTypesJ9(JDIDebugTarget target, List<String> qualifiedNames) throws DebugException { String[] typeNames = qualifiedNames .toArray(new String[qualifiedNames.size()]); if (target.supportsJ9HotCodeReplace()) { target.setHCROccurred(true); org.eclipse.jdi.hcr.VirtualMachine vm = (org.eclipse.jdi.hcr.VirtualMachine) target .getVM(); if (vm == null) { target.requestFailed( JDIDebugHCRMessages.JavaHotCodeReplaceManager_Hot_code_replace_failed___VM_disconnected__1, null); } int result = org.eclipse.jdi.hcr.VirtualMachine.RELOAD_FAILURE; try { result = vm.classesHaveChanged(typeNames); } catch (RuntimeException e) { target.targetRequestFailed( MessageFormat.format(JDIDebugHCRMessages.JavaHotCodeReplaceManager_exception_replacing_types, e.toString()), e); } switch (result) { case org.eclipse.jdi.hcr.VirtualMachine.RELOAD_SUCCESS: break; case org.eclipse.jdi.hcr.VirtualMachine.RELOAD_IGNORED: target.targetRequestFailed( JDIDebugHCRMessages.JavaHotCodeReplaceManager_hcr_ignored, null); break; case org.eclipse.jdi.hcr.VirtualMachine.RELOAD_FAILURE: target.targetRequestFailed( JDIDebugHCRMessages.JavaHotCodeReplaceManager_hcr_failed, null); target.addOutOfSynchTypes(qualifiedNames); break; } } else { target.notSupported(JDIDebugHCRMessages.JavaHotCodeReplaceManager_does_not_support_hcr); target.addOutOfSynchTypes(qualifiedNames); } } /** * Replaces the given types in the given JDK-compliant debug target. * * This method is to be used for JDK hot code replace. */ private void redefineTypesJDK(JDIDebugTarget target, List<IResource> resources, List<String> qualifiedNames) throws DebugException { if (target.supportsJDKHotCodeReplace()) { target.setHCROccurred(true); Map<ReferenceType, byte[]> typesToBytes = getTypesToBytes(target, resources, qualifiedNames); try { VirtualMachine vm = target.getVM(); if (vm == null) { target.requestFailed( JDIDebugHCRMessages.JavaHotCodeReplaceManager_Hot_code_replace_failed___VM_disconnected__2, null); } vm.redefineClasses(typesToBytes); } catch (UnsupportedOperationException exception) { String detail = exception.getMessage(); if (detail != null) { redefineTypesFailedJDK( target, qualifiedNames, MessageFormat.format( JDIDebugHCRMessages.JavaHotCodeReplaceManager_hcr_unsupported_operation, detail), exception); } else { redefineTypesFailedJDK( target, qualifiedNames, JDIDebugHCRMessages.JavaHotCodeReplaceManager_hcr_unsupported_redefinition, exception); } } catch (NoClassDefFoundError exception) { redefineTypesFailedJDK( target, qualifiedNames, JDIDebugHCRMessages.JavaHotCodeReplaceManager_hcr_bad_bytes, exception); } catch (VerifyError exception) { redefineTypesFailedJDK( target, qualifiedNames, JDIDebugHCRMessages.JavaHotCodeReplaceManager_hcr_verify_error, exception); } catch (UnsupportedClassVersionError exception) { redefineTypesFailedJDK( target, qualifiedNames, JDIDebugHCRMessages.JavaHotCodeReplaceManager_hcr_unsupported_class_version, exception); } catch (ClassFormatError exception) { redefineTypesFailedJDK( target, qualifiedNames, JDIDebugHCRMessages.JavaHotCodeReplaceManager_hcr_class_format_error, exception); } catch (ClassCircularityError exception) { redefineTypesFailedJDK( target, qualifiedNames, JDIDebugHCRMessages.JavaHotCodeReplaceManager_hcr_class_circularity_error, exception); } catch (RuntimeException exception) { redefineTypesFailedJDK( target, qualifiedNames, JDIDebugHCRMessages.JavaHotCodeReplaceManager_hcr_failed, exception); } target.reinstallBreakpointsIn(resources, qualifiedNames); } else { target.notSupported(JDIDebugHCRMessages.JavaHotCodeReplaceManager_does_not_support_hcr); } } /** * Error handling for JDK hot code replace. * * The given exception occurred when redefinition was attempted for the * given types. */ private void redefineTypesFailedJDK(JDIDebugTarget target, List<String> qualifiedNames, String message, Throwable exception) throws DebugException { target.addOutOfSynchTypes(qualifiedNames); target.jdiRequestFailed(message, exception); } /** * Returns a mapping of class files to the bytes that make up those class * files. * * @param target * the debug target to query * @param resources * the classfiles * @param qualifiedNames * the fully qualified type names corresponding to the * classfiles. The typeNames correspond to the resources on a * one-to-one basis. * @return a mapping of class files to bytes key: class file value: the * bytes which make up that classfile */ private Map<ReferenceType, byte[]> getTypesToBytes(JDIDebugTarget target, List<IResource> resources, List<String> qualifiedNames) { Map<ReferenceType, byte[]> typesToBytes = new HashMap<>(resources.size()); Iterator<IResource> resourceIter = resources.iterator(); Iterator<String> nameIter = qualifiedNames.iterator(); IResource resource; String name; while (resourceIter.hasNext()) { resource = resourceIter.next(); name = nameIter.next(); List<ReferenceType> classes = target.jdiClassesByName(name); byte[] bytes = null; try { bytes = Util.getResourceContentsAsByteArray((IFile) resource); } catch (JavaModelException jme) { continue; } for(ReferenceType type : classes) { typesToBytes.put(type, bytes); } } return typesToBytes; } /** * Return the listeners to notify for the given target. Target specific * listeners take precedence over generic listeners registered with the * debug model plug-in. * * @param target * Java debug target * @return hot code replace listeners */ private ListenerList<IJavaHotCodeReplaceListener> getHotCodeReplaceListeners(IJavaDebugTarget target) { ListenerList<IJavaHotCodeReplaceListener> listeners = null; if (target instanceof JDIDebugTarget) { listeners = ((JDIDebugTarget) target).getHotCodeReplaceListeners(); } if (listeners == null || listeners.size() == 0) { listeners = fHotCodeReplaceListeners; } return listeners; } /** * Notifies listeners that a hot code replace attempt succeeded */ private void fireHCRSucceeded(IJavaDebugTarget target) { ListenerList<IJavaHotCodeReplaceListener> listeners = getHotCodeReplaceListeners(target); for (IJavaHotCodeReplaceListener listener : listeners) { listener.hotCodeReplaceSucceeded(target); } } /** * Notifies listeners that a hot code replace attempt failed with the given * exception */ private void fireHCRFailed(JDIDebugTarget target, DebugException exception) { ListenerList<IJavaHotCodeReplaceListener> listeners = getHotCodeReplaceListeners(target); for (IJavaHotCodeReplaceListener listener : listeners) { listener.hotCodeReplaceFailed(target, exception); } } /** * Notifies listeners that obsolete methods remain on the stack */ private void fireObsoleteMethods(JDIDebugTarget target) { ListenerList<IJavaHotCodeReplaceListener> listeners = getHotCodeReplaceListeners(target); for (IJavaHotCodeReplaceListener listener : listeners) { listener.obsoleteMethods(target); } } /** * Looks for the deepest affected stack frame in the stack and forces a drop * to frame. Does this for all of the active stack frames in the target. * * @param target * the debug target in which frames are to be dropped * @param replacedClassNames * the classes that have been redefined */ protected void attemptDropToFrame(JDIDebugTarget target, List<IResource> resources, List<String> replacedClassNames) throws DebugException { List<JDIStackFrame> dropFrames = getAffectedFrames(target.getThreads(), resources, replacedClassNames); // All threads that want to drop to frame are able. Proceed with the // drop JDIStackFrame dropFrame = null; Iterator<JDIStackFrame> iter = dropFrames.iterator(); while (iter.hasNext()) { try { dropFrame = iter.next(); dropFrame.dropToFrame(); } catch (DebugException de) { notifyFailedDrop( ((JDIThread) dropFrame.getThread()) .computeStackFrames(), replacedClassNames); } } } /** * Looks for the deepest affected stack frame in the stack and forces a drop * to frame. Does this for all of the active stack frames in the target. * * @param target * the debug target in which frames are to be dropped * @param replacedClassNames * the classes that have been redefined * @param poppedThreads * a list of the threads in which frames were popped.This * parameter may have entries added by this method */ protected void attemptPopFrames(JDIDebugTarget target, List<IResource> resources, List<String> replacedClassNames, List<IThread> poppedThreads) throws DebugException { List<JDIStackFrame> popFrames = getAffectedFrames(target.getThreads(), resources, replacedClassNames); // All threads that want to drop to frame are able. Proceed with the // drop JDIStackFrame popFrame = null; Iterator<JDIStackFrame> iter = popFrames.iterator(); while (iter.hasNext()) { try { popFrame = iter.next(); popFrame.popFrame(); poppedThreads.add(popFrame.getThread()); } catch (DebugException de) { poppedThreads.remove(popFrame.getThread()); notifyFailedDrop( ((JDIThread) popFrame.getThread()).computeStackFrames(), replacedClassNames); } } } /** * Returns whether or not the given target contains stack frames with * obsolete methods. */ protected boolean containsObsoleteMethods(JDIDebugTarget target) throws DebugException { IThread[] threads = target.getThreads(); List<IJavaStackFrame> frames = null; for (IThread thread : threads) { frames = ((JDIThread) thread).computeNewStackFrames(); for(IJavaStackFrame frame : frames) { if(frame.isObsolete()) { return true; } } } return false; } /** * Returns a list of frames which should be popped in the given threads. */ protected List<JDIStackFrame> getAffectedFrames(IThread[] threads, List<IResource> resourceList, List<String> replacedClassNames) throws DebugException { JDIThread thread = null; JDIStackFrame affectedFrame = null; List<JDIStackFrame> popFrames = new ArrayList<>(); int numThreads = threads.length; IResource[] resources = new IResource[resourceList.size()]; resourceList.toArray(resources); for (int i = 0; i < numThreads; i++) { thread = (JDIThread) threads[i]; if (thread.isSuspended()) { affectedFrame = getAffectedFrame(thread, replacedClassNames); if (affectedFrame == null) { // No frame to drop to in this thread continue; } if (affectedFrame.supportsDropToFrame()) { popFrames.add(affectedFrame); } else { // if any thread that should drop does not support the drop, // do not drop in any threads. for (int j = 0; j < numThreads; j++) { notifyFailedDrop( ((JDIThread) threads[i]).computeStackFrames(), replacedClassNames); } throw new DebugException( new Status( IStatus.ERROR, JDIDebugModel.getPluginIdentifier(), DebugException.NOT_SUPPORTED, JDIDebugHCRMessages.JavaHotCodeReplaceManager_Drop_to_frame_not_supported, null)); } } } return popFrames; } /** * Returns the stack frame that should be dropped to in the given thread * after a hot code replace. This is calculated by determining if the * threads contain stack frames that reside in one of the given replaced * class names. If possible, only stack frames whose methods were directly * affected (and not simply all frames in affected types) will be returned. */ protected JDIStackFrame getAffectedFrame(JDIThread thread, List<String> replacedClassNames) throws DebugException { List<IJavaStackFrame> frames = thread.computeStackFrames(); JDIStackFrame affectedFrame = null; JDIStackFrame frame = null; ICompilationUnit compilationUnit = null; CompilationUnitDelta delta = null; IProject project = null; for (int j = 0; j < frames.size(); j++) { frame = (JDIStackFrame) frames.get(j); if (containsChangedType(frame, replacedClassNames)) { // smart drop to frame support compilationUnit = getCompilationUnit(frame); // if we can't find the source, then do type-based drop if (compilationUnit != null) { try { project = compilationUnit.getCorrespondingResource() .getProject(); delta = getDelta(compilationUnit, getLastProjectBuildTime(project)); String typeName = frame.getDeclaringTypeName(); typeName = typeName.replace('$', '.'); if (!delta.hasChanged(typeName, frame.getName(), frame.getSignature())) { continue; } } catch (CoreException exception) { // If smart drop to frame fails, just do type-based drop } } if (frame.supportsDropToFrame()) { affectedFrame = frame; break; } // The frame we wanted to drop to cannot be popped. // Set the affected frame to the next lowest pop-able // frame on the stack. while (j > 0) { j--; frame = (JDIStackFrame) frames.get(j); if (frame.supportsDropToFrame()) { affectedFrame = frame; break; } } break; } } return affectedFrame; } /** * Returns the delta object for the given compilation unit * * @param cu * compilation unit * @param time * time to compare to (i.e. compare to first version before this * time) * @return delta object */ private CompilationUnitDelta getDelta(ICompilationUnit cu, long time) throws CoreException { CompilationUnitDelta delta = fDeltaCache.get(cu); if (delta == null) { delta = new CompilationUnitDelta(cu, time); fDeltaCache.put(cu, delta); } return delta; } /** * Returns whether the given frame's declaring type was changed based on the * given list of changed class names. */ protected boolean containsChangedType(JDIStackFrame frame, List<String> replacedClassNames) throws DebugException { String declaringTypeName = frame.getDeclaringTypeName(); // Check if the frame's declaring type was changed if (replacedClassNames.contains(declaringTypeName)) { return true; } // Check if one of the frame's declaring type's inner classes have // changed Iterator<String> iter = replacedClassNames.iterator(); int index; String className = null; while (iter.hasNext()) { className = iter.next(); index = className.indexOf('$'); if (index > -1 && declaringTypeName.equals(className.substring(0, index))) { return true; } } return false; } /** * Performs a "step into" operation on the given threads. */ protected void attemptStepIn(List<IThread> threads) throws DebugException { Iterator<IThread> iter = threads.iterator(); while (iter.hasNext()) { ((JDIThread) iter.next()).stepInto(); } } /** * Returns the compilation unit associated with this Java stack frame. * Returns <code>null</code> for a binary stack frame. */ protected ICompilationUnit getCompilationUnit(IJavaStackFrame frame) { ILaunch launch = frame.getLaunch(); if (launch == null) { return null; } try { IJavaElement sourceElement = JavaDebugUtils.resolveJavaElement(frame, launch); if (sourceElement instanceof IType) { return ((IType) sourceElement).getCompilationUnit(); } if (sourceElement instanceof ICompilationUnit) { return (ICompilationUnit) sourceElement; } return null; } catch (CoreException e) { return null; } } /** * Returns the method in which this stack frame is suspended or * <code>null</code> if none can be found */ public IMethod getMethod(JDIStackFrame frame, ICompilationUnit unit) throws CoreException { String declaringTypeName = frame.getDeclaringTypeName(); String methodName = frame.getMethodName(); String[] arguments = null; try { arguments = Signature.getParameterTypes(frame.getSignature()); } catch (IllegalArgumentException exception) { // If Signature can't parse the signature, we can't // create the method return null; } String typeName = getUnqualifiedName(declaringTypeName); int index = typeName.indexOf('$'); IType type = null; if (index > 0) { String remaining = typeName.substring(index + 1); typeName = typeName.substring(0, index); type = unit.getType(typeName); while (remaining != null) { index = remaining.indexOf('$'); if (index > 0) { typeName = remaining.substring(0, index); remaining = remaining.substring(index + 1); } else { typeName = remaining; remaining = null; } type = type.getType(typeName); } } else { type = unit.getType(typeName); } if (type != null) { return type.getMethod(methodName, arguments); } return null; } /** * Given a fully qualified name, return the unqualified name. */ protected String getUnqualifiedName(String qualifiedName) { int index = qualifiedName.lastIndexOf('.'); return qualifiedName.substring(index + 1); } /** * Notify the given frames that a drop to frame has failed after an HCR with * the given class names. */ private void notifyFailedDrop(List<IJavaStackFrame> frames, List<String> replacedClassNames) throws DebugException { for(IJavaStackFrame frame : frames) { if (replacedClassNames.contains(frame.getDeclaringTypeName())) { ((JDIStackFrame)frame).setOutOfSynch(true); } } } /** * Returns the class file visitor after visiting the resource change. The * visitor contains the changed class files and qualified type names. * Returns <code>null</code> if the visitor encounters an exception, or the * delta is not a POST_BUILD. */ protected ChangedClassFilesVisitor getChangedClassFiles( IResourceChangeEvent event) { IResourceDelta delta = event.getDelta(); if (event.getType() != IResourceChangeEvent.POST_BUILD || delta == null) { return null; } fClassfileVisitor.reset(); try { delta.accept(fClassfileVisitor); } catch (CoreException e) { JDIDebugPlugin.log(e); return null; // quiet failure } return fClassfileVisitor; } /** * A visitor which collects changed class files. */ class ChangedClassFilesVisitor implements IResourceDeltaVisitor { /** * The collection of changed class files. */ protected List<IResource> fFiles = null; /** * Collection of qualified type names, corresponding to class files. */ protected List<String> fNames = null; /** * Answers whether children should be visited. * <p> * If the associated resource is a class file which has been changed, * record it. */ @Override public boolean visit(IResourceDelta delta) { if (delta == null || 0 == (delta.getKind() & IResourceDelta.CHANGED)) { return false; } IResource resource = delta.getResource(); if (resource != null) { switch (resource.getType()) { case IResource.FILE: if (0 == (delta.getFlags() & IResourceDelta.CONTENT)) { return false; } if (CLASS_FILE_EXTENSION.equals(resource.getFullPath() .getFileExtension())) { IPath localLocation = resource.getLocation(); if (localLocation != null) { String path = localLocation.toOSString(); IClassFileReader reader = ToolFactory .createDefaultClassFileReader( path, IClassFileReader.CLASSFILE_ATTRIBUTES); if (reader != null) { // this name is slash-delimited String qualifiedName = new String( reader.getClassName()); boolean hasBlockingErrors = false; try { if (!Platform.getPreferencesService().getBoolean( JDIDebugPlugin.getUniqueIdentifier(), JDIDebugModel.PREF_HCR_WITH_COMPILATION_ERRORS, true, null)) { // If the user doesn't want to replace // classfiles containing // compilation errors, get the source // file associated with // the class file and query it for // compilation errors IJavaProject pro = JavaCore .create(resource.getProject()); ISourceAttribute sourceAttribute = reader .getSourceFileAttribute(); String sourceName = null; if (sourceAttribute != null) { sourceName = new String( sourceAttribute .getSourceFileName()); } IResource sourceFile = getSourceFile( pro, qualifiedName, sourceName); if (sourceFile != null) { IMarker[] problemMarkers = null; problemMarkers = sourceFile .findMarkers( IJavaModelMarker.JAVA_MODEL_PROBLEM_MARKER, true, IResource.DEPTH_INFINITE); for (IMarker problemMarker : problemMarkers) { if (problemMarker.getAttribute( IMarker.SEVERITY, -1) == IMarker.SEVERITY_ERROR) { hasBlockingErrors = true; break; } } } } } catch (CoreException e) { JDIDebugPlugin.log(e); } if (!hasBlockingErrors) { fFiles.add(resource); // dot-delimit the name fNames.add(qualifiedName.replace('/', '.')); } } } } return false; default: return true; } } return true; } /** * Resets the file collection to empty */ public void reset() { fFiles = new ArrayList<>(); fNames = new ArrayList<>(); } /** * Answers a collection of changed class files or <code>null</code> */ public List<IResource> getChangedClassFiles() { return fFiles; } /** * Returns a collection of qualified type names corresponding to the * changed class files. * * @return List */ public List<String> getQualifiedNamesList() { return fNames; } /** * Returns the source file associated with the given type, or * <code>null</code> if no source file could be found. * * @param project * the java project containing the classfile * @param qualifiedName * fully qualified name of the type, slash delimited * @param sourceAttribute * debug source attribute, or <code>null</code> if none */ private IResource getSourceFile(IJavaProject project, String qualifiedName, String sourceAttribute) { String name = null; IJavaElement element = null; try { if (sourceAttribute == null) { element = JavaDebugUtils .findElement(qualifiedName, project); } else { int i = qualifiedName.lastIndexOf('/'); if (i > 0) { name = qualifiedName.substring(0, i + 1); name = name + sourceAttribute; } else { name = sourceAttribute; } element = project.findElement(new Path(name)); } if (element instanceof ICompilationUnit) { ICompilationUnit cu = (ICompilationUnit) element; return cu.getCorrespondingResource(); } } catch (CoreException e) { } return null; } } /** * Adds the given listener to the collection of hot code replace listeners. * Listeners are notified when hot code replace attempts succeed or fail. */ public void addHotCodeReplaceListener(IJavaHotCodeReplaceListener listener) { fHotCodeReplaceListeners.add(listener); } /** * Removes the given listener from the collection of hot code replace * listeners. Once a listener is removed, it will no longer be notified of * hot code replace attempt successes or failures. */ public void removeHotCodeReplaceListener( IJavaHotCodeReplaceListener listener) { fHotCodeReplaceListeners.remove(listener); } /** * @see ILaunchListener#launchRemoved(ILaunch) */ @Override public void launchRemoved(ILaunch launch) { IDebugTarget[] debugTargets = launch.getDebugTargets(); for (IDebugTarget debugTarget : debugTargets) { IJavaDebugTarget jt = debugTarget .getAdapter(IJavaDebugTarget.class); if (jt != null) { deregisterTarget((JDIDebugTarget) jt); } } } /** * Begin listening for resource changes when a launch is registered with a * hot swap-able target. * * @see org.eclipse.debug.core.ILaunchListener#launchAdded(org.eclipse.debug.core.ILaunch) */ @Override public void launchAdded(ILaunch launch) { IDebugTarget[] debugTargets = launch.getDebugTargets(); for (IDebugTarget debugTarget : debugTargets) { IJavaDebugTarget jt = debugTarget .getAdapter(IJavaDebugTarget.class); if (jt != null) { JDIDebugTarget target = (JDIDebugTarget) jt; if (target.supportsHotCodeReplace()) { addHotSwapTarget(target); } else if (target.isAvailable()) { addNonHotSwapTarget(target); } } } synchronized (this) { if (!fHotSwapTargets.isEmpty() || !fNoHotSwapTargets.isEmpty()) { getWorkspace().addResourceChangeListener(this, IResourceChangeEvent.POST_BUILD); } } } /** * Begin listening for resource changes when a launch is registered with a * hot swap-able target. * * @see ILaunchListener#launchChanged(ILaunch) */ @Override public void launchChanged(ILaunch launch) { launchAdded(launch); } /* * (non-Javadoc) * * @see * org.eclipse.debug.core.IDebugEventSetListener#handleDebugEvents(org.eclipse * .debug.core.DebugEvent[]) */ @Override public void handleDebugEvents(DebugEvent[] events) { for (DebugEvent event : events) { if (event.getKind() == DebugEvent.TERMINATE) { Object source = event.getSource(); if (source instanceof IAdaptable && source instanceof IDebugTarget) { IJavaDebugTarget jt = ((IAdaptable) source) .getAdapter(IJavaDebugTarget.class); if (jt != null) { deregisterTarget((JDIDebugTarget) jt); } } } } } protected void deregisterTarget(JDIDebugTarget target) { // Remove the target from its hot swap target cache. if (!fHotSwapTargets.remove(target)) { fNoHotSwapTargets.remove(target); } ILaunch[] launches = DebugPlugin.getDefault().getLaunchManager() .getLaunches(); // If there are no more active JDIDebugTargets, stop // listening to resource changes. for (ILaunch launche : launches) { IDebugTarget[] targets = launche.getDebugTargets(); for (IDebugTarget debugTarget : targets) { IJavaDebugTarget jt = debugTarget .getAdapter(IJavaDebugTarget.class); if (jt != null) { if (((JDIDebugTarget) jt).isAvailable()) { return; } } } } } /** * Adds the given target to the list of hot-swap-able targets. Has no effect * if the target is already registered. * * @param target * a target that supports hot swap */ protected synchronized void addHotSwapTarget(JDIDebugTarget target) { if (!fHotSwapTargets.contains(target)) { fHotSwapTargets.add(target); } } /** * Adds the given target to the list of non hot-swap-able targets. Has no * effect if the target is already registered. * * @param target * a target that does not support hot swap */ protected synchronized void addNonHotSwapTarget(JDIDebugTarget target) { if (!fNoHotSwapTargets.contains(target)) { fNoHotSwapTargets.add(target); } } }