| /******************************************************************************* |
| * Copyright (c) 2000, 2006 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 |
| *******************************************************************************/ |
| 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.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.ISourceLocator; |
| 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 fHotCodeReplaceListeners= new ListenerList(); |
| |
| /** |
| * The lists of hot swap targets which support HCR and those which don't |
| */ |
| private ArrayList fHotSwapTargets= new ArrayList(1); |
| private ArrayList 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 fProjectBuildTimes= new HashMap(); |
| private static Date fStartupDate= new Date(); |
| |
| /** |
| * Cache of compilation unit deltas renewed on each HCR attempt. |
| */ |
| private Map 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 plugin on startup. |
| */ |
| public void startup() { |
| DebugPlugin.getDefault().getLaunchManager().addLaunchListener(this); |
| DebugPlugin.getDefault().addDebugEventListener(this); |
| } |
| |
| /** |
| * Deregisters this HCR manager as a resource change listener. Removes all hot |
| * code replace listeners. This method is called by the JDI debug model plugin |
| * on shutdown. |
| */ |
| public void shutdown() { |
| DebugPlugin.getDefault().getLaunchManager().removeLaunchListener(this); |
| DebugPlugin.getDefault().removeDebugEventListener(this); |
| getWorkspace().removeResourceChangeListener(this); |
| fHotCodeReplaceListeners = new ListenerList(); |
| fHotSwapTargets= null; |
| fNoHotSwapTargets= null; |
| } |
| /** |
| * 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) |
| */ |
| public void resourceChanged(IResourceChangeEvent event) { |
| List projects= getBuiltProjects(event); |
| if (!projects.isEmpty()) { |
| updateProjectBuildTime(projects); |
| } |
| 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 resources = visitor.getChangedClassFiles(); |
| List names = visitor.getQualifiedNamesList(); |
| if (!resources.isEmpty()) { |
| notifyTargets(resources, names); |
| } |
| } |
| } |
| |
| /** |
| * Returns all projects which this event says may have been built. |
| */ |
| protected List 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 list= new ArrayList(); |
| list.add(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 projects) { |
| Iterator iter= projects.iterator(); |
| IProject project= null; |
| Date currentDate= new Date(); |
| ProjectBuildTime buildTime= null; |
| while (iter.hasNext()) { |
| project= (IProject) iter.next(); |
| buildTime= (ProjectBuildTime)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= (ProjectBuildTime)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 resources, final List qualifiedNames) { |
| final List hotSwapTargets= getHotSwapTargets(); |
| final List noHotSwapTargets= getNoHotSwapTargets(); |
| if (!hotSwapTargets.isEmpty()) { |
| Runnable runnable= new Runnable() { |
| public void run() { |
| doHotCodeReplace(hotSwapTargets, resources, qualifiedNames); |
| } |
| }; |
| DebugPlugin.getDefault().asyncExec(runnable); |
| } |
| if (!noHotSwapTargets.isEmpty()) { |
| Runnable runnable= new Runnable() { |
| 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 tyep 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 resources, List qualifiedNames) { |
| for (int i= 0, numElements= qualifiedNames.size(); i < numElements; i++) { |
| String name= (String) qualifiedNames.get(i); |
| List 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 targets, List resources, List qualifiedNames) { |
| Iterator iter= targets.iterator(); |
| JDIDebugTarget target= null; |
| while (iter.hasNext()) { |
| target= (JDIDebugTarget) 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 resourcesToReplace= new ArrayList(resources); |
| List 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 fallback. |
| deregisterTarget(target); |
| } |
| } |
| } |
| |
| protected void notifyFailedHCR(JDIDebugTarget target, List qualifiedNames) { |
| if (target.isAvailable()) { |
| target.addOutOfSynchTypes(qualifiedNames); |
| target.fireChangeEvent(DebugEvent.STATE); |
| } |
| } |
| |
| /** |
| * Returns the currently registered debug targets that support |
| * hot code replace. |
| */ |
| protected List getHotSwapTargets() { |
| return (List) fHotSwapTargets.clone(); |
| } |
| |
| /** |
| * Returns the currently registered debug targets that do |
| * not support hot code replace. |
| */ |
| protected List getNoHotSwapTargets() { |
| return (List) fNoHotSwapTargets.clone(); |
| } |
| |
| /** |
| * 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 targets, List resources, List qualifiedNames) { |
| 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 iter= targets.iterator(); |
| while (iter.hasNext()) { |
| JDIDebugTarget target= (JDIDebugTarget) 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 resourcesToReplace= new ArrayList(resources); |
| List qualifiedNamesToReplace= new ArrayList(qualifiedNames); |
| filterUnloadedTypes(target, resourcesToReplace, qualifiedNamesToReplace); |
| if (qualifiedNamesToReplace.isEmpty()) { |
| // If none of the changed types are loaded, do nothing. |
| continue; |
| } |
| |
| List 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 incase HCR failed |
| target.setIsPerformingHotCodeReplace(false); |
| target.fireChangeEvent(DebugEvent.CONTENT); |
| } |
| if (!ms.isOK()) { |
| JDIDebugPlugin.log(ms); |
| } |
| fDeltaCache.clear(); |
| } |
| |
| /** |
| * 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 qualifiedNames) throws DebugException { |
| String[] typeNames = (String[]) 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, new String[] {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 resources, List qualifiedNames) throws DebugException { |
| if (target.supportsJDKHotCodeReplace()) { |
| target.setHCROccurred(true); |
| Map 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, new String[] {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 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 getTypesToBytes(JDIDebugTarget target, List resources, List qualifiedNames) { |
| Map typesToBytes= new HashMap(resources.size()); |
| Iterator resourceIter= resources.iterator(); |
| Iterator nameIter= qualifiedNames.iterator(); |
| IResource resource; |
| String name; |
| while (resourceIter.hasNext()) { |
| resource= (IResource) resourceIter.next(); |
| name= (String) nameIter.next(); |
| List classes= target.jdiClassesByName(name); |
| byte[] bytes= null; |
| try { |
| bytes= Util.getResourceContentsAsByteArray((IFile) resource); |
| } catch (JavaModelException jme) { |
| continue; |
| } |
| Iterator classIter= classes.iterator(); |
| while (classIter.hasNext()) { |
| ReferenceType type= (ReferenceType) classIter.next(); |
| typesToBytes.put(type, bytes); |
| } |
| } |
| return typesToBytes; |
| } |
| |
| /** |
| * Notifies listeners that a hot code replace attempt succeeded |
| */ |
| private void fireHCRSucceeded(IJavaDebugTarget target) { |
| Object[] listeners= fHotCodeReplaceListeners.getListeners(); |
| for (int i=0; i<listeners.length; i++) { |
| ((IJavaHotCodeReplaceListener)listeners[i]).hotCodeReplaceSucceeded(target); |
| } |
| } |
| |
| /** |
| * Notifies listeners that a hot code replace attempt failed with the given exception |
| */ |
| private void fireHCRFailed(JDIDebugTarget target, DebugException exception) { |
| Object[] listeners= fHotCodeReplaceListeners.getListeners(); |
| for (int i=0; i<listeners.length; i++) { |
| ((IJavaHotCodeReplaceListener)listeners[i]).hotCodeReplaceFailed(target, exception); |
| } |
| } |
| |
| /** |
| * Notifies listeners that obsolete methods remain on the stack |
| */ |
| private void fireObsoleteMethods(JDIDebugTarget target) { |
| Object[] listeners= fHotCodeReplaceListeners.getListeners(); |
| for (int i=0; i<listeners.length; i++) { |
| ((IJavaHotCodeReplaceListener)listeners[i]).obsoleteMethods(target); |
| } |
| } |
| |
| /** |
| * Looks for the deepest effected 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 resources, List replacedClassNames) throws DebugException { |
| List dropFrames= getAffectedFrames(target.getThreads(), resources, replacedClassNames); |
| |
| // All threads that want to drop to frame are able. Proceed with the drop |
| JDIStackFrame dropFrame= null; |
| Iterator iter= dropFrames.iterator(); |
| while (iter.hasNext()) { |
| try { |
| dropFrame= ((JDIStackFrame)iter.next()); |
| dropFrame.dropToFrame(); |
| } catch (DebugException de) { |
| notifyFailedDrop(((JDIThread)dropFrame.getThread()).computeStackFrames(), replacedClassNames); |
| } |
| } |
| } |
| |
| /** |
| * Looks for the deepest effected 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 resources, List replacedClassNames, List poppedThreads) throws DebugException { |
| List popFrames= getAffectedFrames(target.getThreads(), resources, replacedClassNames); |
| |
| // All threads that want to drop to frame are able. Proceed with the drop |
| JDIStackFrame popFrame= null; |
| Iterator iter= popFrames.iterator(); |
| while (iter.hasNext()) { |
| try { |
| popFrame= ((JDIStackFrame)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 frames= null; |
| Iterator iter= null; |
| for (int i= 0, numThreads= threads.length; i < numThreads; i++) { |
| frames= ((JDIThread)threads[i]).computeNewStackFrames(); |
| iter= frames.iterator(); |
| while (iter.hasNext()) { |
| if (((JDIStackFrame)iter.next()).isObsolete()) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Returns a list of frames which should be popped in the given threads. |
| */ |
| protected List getAffectedFrames(IThread[] threads, List resourceList, List replacedClassNames) throws DebugException { |
| JDIThread thread= null; |
| JDIStackFrame affectedFrame= null; |
| List 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 replacedClassNames) throws DebugException { |
| List frames= thread.computeStackFrames(); |
| JDIStackFrame affectedFrame= null; |
| JDIStackFrame frame= null; |
| ICompilationUnit compilationUnit= null; |
| CompilationUnitDelta delta= null; |
| IProject project= null; |
| for (int j= frames.size() - 1; j >= 0; 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)); |
| if (!delta.hasChanged(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 poppable |
| // 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 = (CompilationUnitDelta) 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 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 iter= replacedClassNames.iterator(); |
| int index; |
| String className= null; |
| while (iter.hasNext()) { |
| className= (String) 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 threads) throws DebugException { |
| Iterator 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; |
| } |
| ISourceLocator locator= launch.getSourceLocator(); |
| if (locator == null) { |
| return null; |
| } |
| IJavaDebugTarget target = (IJavaDebugTarget) frame.getDebugTarget(); |
| String def = target.getDefaultStratum(); |
| target.setDefaultStratum("Java"); //$NON-NLS-1$ |
| Object sourceElement= locator.getSourceElement(frame); |
| target.setDefaultStratum(def); |
| if (!(sourceElement instanceof IJavaElement) && sourceElement instanceof IAdaptable) { |
| sourceElement = ((IAdaptable)sourceElement).getAdapter(IJavaElement.class); |
| } |
| if (sourceElement instanceof IType) { |
| return ((IType)sourceElement).getCompilationUnit(); |
| } |
| if (sourceElement instanceof ICompilationUnit) { |
| return (ICompilationUnit)sourceElement; |
| } |
| 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 frames, List replacedClassNames) throws DebugException { |
| JDIStackFrame frame; |
| Iterator iter= frames.iterator(); |
| while (iter.hasNext()) { |
| frame= (JDIStackFrame) iter.next(); |
| if (replacedClassNames.contains(frame.getDeclaringTypeName())) { |
| 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 fFiles= null; |
| |
| /** |
| * Collection of qualified type names, corresponding to class files. |
| */ |
| protected List fNames= null; |
| |
| /** |
| * Answers whether children should be visited. |
| * <p> |
| * If the associated resource is a class file which |
| * has been changed, record it. |
| */ |
| 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 (!JDIDebugModel.getPreferences().getBoolean(JDIDebugModel.PREF_HCR_WITH_COMPILATION_ERRORS)) { |
| // 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 (int i= 0; i < problemMarkers.length; i++) { |
| if (problemMarkers[i].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 getChangedClassFiles() { |
| return fFiles; |
| } |
| |
| /** |
| * Returns a collection of qualified type names corresponding to the |
| * changed class files. |
| * |
| * @return List |
| */ |
| public List 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) |
| */ |
| public void launchRemoved(ILaunch launch) { |
| IDebugTarget[] debugTargets= launch.getDebugTargets(); |
| for (int i = 0; i < debugTargets.length; i++) { |
| IJavaDebugTarget jt = (IJavaDebugTarget)debugTargets[i].getAdapter(IJavaDebugTarget.class); |
| if (jt != null) { |
| deregisterTarget((JDIDebugTarget)jt); |
| } |
| } |
| } |
| |
| /** |
| * Begin listening for resource changes when a launch is |
| * registered with a hot swapable target. |
| * |
| * @see org.eclipse.debug.core.ILaunchListener#launchAdded(org.eclipse.debug.core.ILaunch) |
| */ |
| public void launchAdded(ILaunch launch) { |
| IDebugTarget[] debugTargets= launch.getDebugTargets(); |
| for (int i = 0; i < debugTargets.length; i++) { |
| IJavaDebugTarget jt = (IJavaDebugTarget)debugTargets[i].getAdapter(IJavaDebugTarget.class); |
| if (jt != null) { |
| JDIDebugTarget target = (JDIDebugTarget)jt; |
| if (target.supportsHotCodeReplace()) { |
| addHotSwapTarget(target); |
| } else if (target.isAvailable()){ |
| addNonHotSwapTarget(target); |
| } |
| } |
| } |
| if (!fHotSwapTargets.isEmpty() || !fNoHotSwapTargets.isEmpty()) { |
| getWorkspace().addResourceChangeListener(this, IResourceChangeEvent.POST_BUILD); |
| } |
| } |
| |
| /** |
| * Begin listening for resource changes when a launch is |
| * registered with a hot swapable target. |
| * |
| * @see ILaunchListener#launchChanged(ILaunch) |
| */ |
| public void launchChanged(ILaunch launch) { |
| launchAdded(launch); |
| } |
| |
| /* (non-Javadoc) |
| * @see org.eclipse.debug.core.IDebugEventSetListener#handleDebugEvents(org.eclipse.debug.core.DebugEvent[]) |
| */ |
| public void handleDebugEvents(DebugEvent[] events) { |
| for (int i = 0; i < events.length; i++) { |
| DebugEvent event = events[i]; |
| if (event.getKind() == DebugEvent.TERMINATE) { |
| Object source = event.getSource(); |
| if (source instanceof IAdaptable && source instanceof IDebugTarget) { |
| IJavaDebugTarget jt = (IJavaDebugTarget)((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 (int i= 0; i < launches.length; i++) { |
| IDebugTarget[] targets = launches[i].getDebugTargets(); |
| for (int j = 0; j < targets.length; j++) { |
| IDebugTarget debugTarget = targets[j]; |
| IJavaDebugTarget jt = (IJavaDebugTarget)debugTarget.getAdapter(IJavaDebugTarget.class); |
| if (jt != null) { |
| if (((JDIDebugTarget)jt).isAvailable()) { |
| return; |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Adds the given target to the list of hot-swappable targets. |
| * Has no effect if the target is already registered. |
| * |
| * @param target a target that supports hot swap |
| */ |
| protected void addHotSwapTarget(JDIDebugTarget target) { |
| if (!fHotSwapTargets.contains(target)) { |
| fHotSwapTargets.add(target); |
| } |
| } |
| |
| /** |
| * Adds the given target to the list of non hot-swappable targets. |
| * Has no effect if the target is already registered. |
| * |
| * @param target a target that does not support hot swap |
| */ |
| protected void addNonHotSwapTarget(JDIDebugTarget target) { |
| if (!fNoHotSwapTargets.contains(target)) { |
| fNoHotSwapTargets.add(target); |
| } |
| } |
| |
| } |
| |