blob: 27facfb2d355e5201fe65c5d39796af914b2aa7f [file] [log] [blame]
/*******************************************************************************
* 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<JDIDebugTarget>(1);
private ArrayList<JDIDebugTarget> fNoHotSwapTargets = new ArrayList<JDIDebugTarget>(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<IProject, ProjectBuildTime>();
private static Date fStartupDate = new Date();
/**
* Cache of compilation unit deltas renewed on each HCR attempt.
*/
private Map<ICompilationUnit, CompilationUnitDelta> fDeltaCache = new HashMap<ICompilationUnit, CompilationUnitDelta>();
/**
* 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<IProject>();
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<IResource>(resources);
List<String> qualifiedNamesToReplace = new ArrayList<String>(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<JDIDebugTarget>(fHotSwapTargets);
}
/**
* Returns the currently registered debug targets that do not support hot
* code replace.
*/
protected synchronized List<JDIDebugTarget> getNoHotSwapTargets() {
return new ArrayList<JDIDebugTarget>(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<IResource>(resources);
List<String> qualifiedNamesToReplace = new ArrayList<String>(qualifiedNames);
filterUnloadedTypes(target, resourcesToReplace,
qualifiedNamesToReplace);
if (qualifiedNamesToReplace.isEmpty()) {
// If none of the changed types are loaded, do nothing.
continue;
}
List<IThread> poppedThreads = new ArrayList<IThread>();
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();
}
/**
* 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<ReferenceType, byte[]>(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<JDIStackFrame>();
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<IResource>();
fNames = new ArrayList<String>();
}
/**
* 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);
}
}
}