blob: 392a14b78bbf3840ca76bb08186fcf9773e1be51 [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.debugger.impl;
import com.intellij.debugger.*;
import com.intellij.debugger.apiAdapters.TransportServiceWrapper;
import com.intellij.debugger.engine.*;
import com.intellij.debugger.settings.DebuggerSettings;
import com.intellij.debugger.ui.GetJPDADialog;
import com.intellij.debugger.ui.breakpoints.BreakpointManager;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.ExecutionResult;
import com.intellij.execution.configurations.JavaParameters;
import com.intellij.execution.configurations.RemoteConnection;
import com.intellij.execution.configurations.RunProfileState;
import com.intellij.execution.process.KillableColoredProcessHandler;
import com.intellij.execution.process.ProcessAdapter;
import com.intellij.execution.process.ProcessEvent;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.openapi.components.StoragePathMacros;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.colors.EditorColorsListener;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.JavaSdk;
import com.intellij.openapi.projectRoots.JavaSdkVersion;
import com.intellij.openapi.projectRoots.JdkUtil;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.projectRoots.ex.JavaSdkUtil;
import com.intellij.openapi.startup.StartupManager;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiClass;
import com.intellij.util.EventDispatcher;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import org.jdom.Element;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.*;
import java.util.jar.Attributes;
@State(name = "DebuggerManager", storages = {@Storage(file = StoragePathMacros.WORKSPACE_FILE)})
public class DebuggerManagerImpl extends DebuggerManagerEx implements PersistentStateComponent<Element> {
private static final Logger LOG = Logger.getInstance("#com.intellij.debugger.impl.DebuggerManagerImpl");
private final Project myProject;
private final HashMap<ProcessHandler, DebuggerSession> mySessions = new HashMap<ProcessHandler, DebuggerSession>();
private final BreakpointManager myBreakpointManager;
private final List<NameMapper> myNameMappers = ContainerUtil.createLockFreeCopyOnWriteList();
private final List<Function<DebugProcess, PositionManager>> myCustomPositionManagerFactories =
new ArrayList<Function<DebugProcess, PositionManager>>();
private final EventDispatcher<DebuggerManagerListener> myDispatcher = EventDispatcher.create(DebuggerManagerListener.class);
private final MyDebuggerStateManager myDebuggerStateManager = new MyDebuggerStateManager();
private final DebuggerContextListener mySessionListener = new DebuggerContextListener() {
@Override
public void changeEvent(DebuggerContextImpl newContext, int event) {
final DebuggerSession session = newContext.getDebuggerSession();
if (event == DebuggerSession.EVENT_PAUSE && myDebuggerStateManager.myDebuggerSession != session) {
// if paused in non-active session; switch current session
myDebuggerStateManager.setState(newContext, session != null? session.getState() : DebuggerSession.STATE_DISPOSED, event, null);
return;
}
if (myDebuggerStateManager.myDebuggerSession == session) {
myDebuggerStateManager.fireStateChanged(newContext, event);
}
if (event == DebuggerSession.EVENT_ATTACHED) {
myDispatcher.getMulticaster().sessionAttached(session);
}
else if (event == DebuggerSession.EVENT_DETACHED) {
myDispatcher.getMulticaster().sessionDetached(session);
}
else if (event == DebuggerSession.EVENT_DISPOSE) {
dispose(session);
if (myDebuggerStateManager.myDebuggerSession == session) {
myDebuggerStateManager
.setState(DebuggerContextImpl.EMPTY_CONTEXT, DebuggerSession.STATE_DISPOSED, DebuggerSession.EVENT_DISPOSE, null);
}
}
}
};
@NonNls private static final String DEBUG_KEY_NAME = "idea.xdebug.key";
@Override
public void addClassNameMapper(final NameMapper mapper) {
myNameMappers.add(mapper);
}
@Override
public void removeClassNameMapper(final NameMapper mapper) {
myNameMappers.remove(mapper);
}
@Override
public String getVMClassQualifiedName(@NotNull final PsiClass aClass) {
for (NameMapper nameMapper : myNameMappers) {
final String qName = nameMapper.getQualifiedName(aClass);
if (qName != null) {
return qName;
}
}
return aClass.getQualifiedName();
}
@Override
public void addDebuggerManagerListener(DebuggerManagerListener listener) {
myDispatcher.addListener(listener);
}
@Override
public void removeDebuggerManagerListener(DebuggerManagerListener listener) {
myDispatcher.removeListener(listener);
}
public DebuggerManagerImpl(Project project, StartupManager startupManager, EditorColorsManager colorsManager) {
myProject = project;
myBreakpointManager = new BreakpointManager(myProject, startupManager, this);
if (!project.isDefault()) {
colorsManager.addEditorColorsListener(new EditorColorsListener() {
@Override
public void globalSchemeChange(EditorColorsScheme scheme) {
getBreakpointManager().updateBreakpointsUI();
}
}, project);
}
}
@Override
public DebuggerSession getSession(DebugProcess process) {
ApplicationManager.getApplication().assertIsDispatchThread();
for (final DebuggerSession debuggerSession : getSessions()) {
if (process == debuggerSession.getProcess()) return debuggerSession;
}
return null;
}
@Override
public Collection<DebuggerSession> getSessions() {
synchronized (mySessions) {
final Collection<DebuggerSession> values = mySessions.values();
return values.isEmpty() ? Collections.<DebuggerSession>emptyList() : new ArrayList<DebuggerSession>(values);
}
}
@Override
public void disposeComponent() {
}
@Override
public void initComponent() {
}
@Override
public void projectClosed() {
}
@Override
public void projectOpened() {
myBreakpointManager.init();
}
@Nullable
@Override
public Element getState() {
Element state = new Element("state");
myBreakpointManager.writeExternal(state);
return state;
}
@Override
public void loadState(Element state) {
myBreakpointManager.readExternal(state);
}
public void writeExternal(Element element) throws WriteExternalException {
myBreakpointManager.writeExternal(element);
}
@Override
@Nullable
public DebuggerSession attachVirtualMachine(@NotNull DebugEnvironment environment) throws ExecutionException {
ApplicationManager.getApplication().assertIsDispatchThread();
final DebugProcessEvents debugProcess = new DebugProcessEvents(myProject);
debugProcess.addDebugProcessListener(new DebugProcessAdapter() {
@Override
public void processAttached(final DebugProcess process) {
process.removeDebugProcessListener(this);
for (Function<DebugProcess, PositionManager> factory : myCustomPositionManagerFactories) {
final PositionManager positionManager = factory.fun(process);
if (positionManager != null) {
process.appendPositionManager(positionManager);
}
}
for (PositionManagerFactory factory : Extensions.getExtensions(PositionManagerFactory.EP_NAME, myProject)) {
final PositionManager manager = factory.createPositionManager(debugProcess);
if (manager != null) {
process.appendPositionManager(manager);
}
}
}
@Override
public void processDetached(final DebugProcess process, final boolean closedByUser) {
debugProcess.removeDebugProcessListener(this);
}
@Override
public void attachException(final RunProfileState state,
final ExecutionException exception,
final RemoteConnection remoteConnection) {
debugProcess.removeDebugProcessListener(this);
}
});
DebuggerSession session = new DebuggerSession(environment.getSessionName(), debugProcess);
final ExecutionResult executionResult = session.attach(environment);
if (executionResult == null) {
return null;
}
session.getContextManager().addListener(mySessionListener);
getContextManager()
.setState(DebuggerContextUtil.createDebuggerContext(session, session.getContextManager().getContext().getSuspendContext()),
session.getState(), DebuggerSession.EVENT_CONTEXT, null);
final ProcessHandler processHandler = executionResult.getProcessHandler();
synchronized (mySessions) {
mySessions.put(processHandler, session);
}
if (!(processHandler instanceof RemoteDebugProcessHandler)) {
// add listener only to non-remote process handler:
// on Unix systems destroying process does not cause VMDeathEvent to be generated,
// so we need to call debugProcess.stop() explicitly for graceful termination.
// RemoteProcessHandler on the other hand will call debugProcess.stop() as a part of destroyProcess() and detachProcess() implementation,
// so we shouldn't add the listener to avoid calling stop() twice
processHandler.addProcessListener(new ProcessAdapter() {
@Override
public void processWillTerminate(ProcessEvent event, boolean willBeDestroyed) {
final DebugProcessImpl debugProcess = getDebugProcess(event.getProcessHandler());
if (debugProcess != null) {
// if current thread is a "debugger manager thread", stop will execute synchronously
// it is KillableColoredProcessHandler responsibility to terminate VM
debugProcess.stop(willBeDestroyed && !(event.getProcessHandler() instanceof KillableColoredProcessHandler));
// wait at most 10 seconds: the problem is that debugProcess.stop() can hang if there are troubles in the debuggee
// if processWillTerminate() is called from AWT thread debugProcess.waitFor() will block it and the whole app will hang
if (!DebuggerManagerThreadImpl.isManagerThread()) {
debugProcess.waitFor(10000);
}
}
}
});
}
myDispatcher.getMulticaster().sessionCreated(session);
return session;
}
@Override
public DebugProcessImpl getDebugProcess(final ProcessHandler processHandler) {
synchronized (mySessions) {
DebuggerSession session = mySessions.get(processHandler);
return session != null ? session.getProcess() : null;
}
}
@SuppressWarnings("UnusedDeclaration")
@Nullable
public DebuggerSession getDebugSession(final ProcessHandler processHandler) {
synchronized (mySessions) {
return mySessions.get(processHandler);
}
}
@Override
public void addDebugProcessListener(final ProcessHandler processHandler, final DebugProcessListener listener) {
DebugProcessImpl debugProcess = getDebugProcess(processHandler);
if (debugProcess != null) {
debugProcess.addDebugProcessListener(listener);
}
else {
processHandler.addProcessListener(new ProcessAdapter() {
@Override
public void startNotified(ProcessEvent event) {
DebugProcessImpl debugProcess = getDebugProcess(processHandler);
if (debugProcess != null) {
debugProcess.addDebugProcessListener(listener);
}
processHandler.removeProcessListener(this);
}
});
}
}
@Override
public void removeDebugProcessListener(final ProcessHandler processHandler, final DebugProcessListener listener) {
DebugProcessImpl debugProcess = getDebugProcess(processHandler);
if (debugProcess != null) {
debugProcess.removeDebugProcessListener(listener);
}
else {
processHandler.addProcessListener(new ProcessAdapter() {
@Override
public void startNotified(ProcessEvent event) {
DebugProcessImpl debugProcess = getDebugProcess(processHandler);
if (debugProcess != null) {
debugProcess.removeDebugProcessListener(listener);
}
processHandler.removeProcessListener(this);
}
});
}
}
@Override
public boolean isDebuggerManagerThread() {
return DebuggerManagerThreadImpl.isManagerThread();
}
@Override
@NotNull
public String getComponentName() {
return "DebuggerManager";
}
@Override
public BreakpointManager getBreakpointManager() {
return myBreakpointManager;
}
@Override
public DebuggerContextImpl getContext() {
return getContextManager().getContext();
}
@Override
public DebuggerStateManager getContextManager() {
return myDebuggerStateManager;
}
@Override
public void registerPositionManagerFactory(final Function<DebugProcess, PositionManager> factory) {
myCustomPositionManagerFactories.add(factory);
}
@Override
public void unregisterPositionManagerFactory(final Function<DebugProcess, PositionManager> factory) {
myCustomPositionManagerFactories.remove(factory);
}
private static boolean hasWhitespace(String string) {
int length = string.length();
for (int i = 0; i < length; i++) {
if (Character.isWhitespace(string.charAt(i))) {
return true;
}
}
return false;
}
/* Remoting */
private static void checkTargetJPDAInstalled(JavaParameters parameters) throws ExecutionException {
final Sdk jdk = parameters.getJdk();
if (jdk == null) {
throw new ExecutionException(DebuggerBundle.message("error.jdk.not.specified"));
}
final JavaSdkVersion version = JavaSdk.getInstance().getVersion(jdk);
String versionString = jdk.getVersionString();
if (version == JavaSdkVersion.JDK_1_0 || version == JavaSdkVersion.JDK_1_1) {
throw new ExecutionException(DebuggerBundle.message("error.unsupported.jdk.version", versionString));
}
if (SystemInfo.isWindows && version == JavaSdkVersion.JDK_1_2) {
final VirtualFile homeDirectory = jdk.getHomeDirectory();
if (homeDirectory == null || !homeDirectory.isValid()) {
throw new ExecutionException(DebuggerBundle.message("error.invalid.jdk.home", versionString));
}
//noinspection HardCodedStringLiteral
File dllFile = new File(
homeDirectory.getPath().replace('/', File.separatorChar) + File.separator + "bin" + File.separator + "jdwp.dll"
);
if (!dllFile.exists()) {
GetJPDADialog dialog = new GetJPDADialog();
dialog.show();
throw new ExecutionException(DebuggerBundle.message("error.debug.libraries.missing"));
}
}
}
/**
* for Target JDKs versions 1.2.x - 1.3.0 the Classic VM should be used for debugging
*/
private static boolean shouldForceClassicVM(Sdk jdk) {
if (SystemInfo.isMac) {
return false;
}
if (jdk == null) return false;
String version = JdkUtil.getJdkMainAttribute(jdk, Attributes.Name.IMPLEMENTATION_VERSION);
if (version != null) {
if (version.compareTo("1.4") >= 0) {
return false;
}
if (version.startsWith("1.2") && SystemInfo.isWindows) {
return true;
}
version += ".0";
if (version.startsWith("1.3.0") && SystemInfo.isWindows) {
return true;
}
if ((version.startsWith("1.3.1_07") || version.startsWith("1.3.1_08")) && SystemInfo.isWindows) {
return false; // fixes bug for these JDKs that it cannot start with -classic option
}
}
return DebuggerSettings.getInstance().FORCE_CLASSIC_VM;
}
@SuppressWarnings({"HardCodedStringLiteral"})
public static RemoteConnection createDebugParameters(final JavaParameters parameters,
final boolean debuggerInServerMode,
int transport, final String debugPort,
boolean checkValidity)
throws ExecutionException {
if (checkValidity) {
checkTargetJPDAInstalled(parameters);
}
final boolean useSockets = transport == DebuggerSettings.SOCKET_TRANSPORT;
String address = "";
if (StringUtil.isEmptyOrSpaces(debugPort)) {
try {
address = DebuggerUtils.getInstance().findAvailableDebugAddress(useSockets);
}
catch (ExecutionException e) {
if (checkValidity) {
throw e;
}
}
}
else {
address = debugPort;
}
final TransportServiceWrapper transportService = TransportServiceWrapper.getTransportService(useSockets);
final String debugAddress = debuggerInServerMode && useSockets ? "127.0.0.1:" + address : address;
String debuggeeRunProperties = "transport=" + transportService.transportId() + ",address=" + debugAddress;
if (debuggerInServerMode) {
debuggeeRunProperties += ",suspend=y,server=n";
}
else {
debuggeeRunProperties += ",suspend=n,server=y";
}
if (hasWhitespace(debuggeeRunProperties)) {
debuggeeRunProperties = "\"" + debuggeeRunProperties + "\"";
}
final String _debuggeeRunProperties = debuggeeRunProperties;
ApplicationManager.getApplication().runReadAction(new Runnable() {
@Override
@SuppressWarnings({"HardCodedStringLiteral"})
public void run() {
JavaSdkUtil.addRtJar(parameters.getClassPath());
final Sdk jdk = parameters.getJdk();
final boolean forceClassicVM = shouldForceClassicVM(jdk);
final boolean forceNoJIT = shouldForceNoJIT(jdk);
final String debugKey = System.getProperty(DEBUG_KEY_NAME, "-Xdebug");
final boolean needDebugKey = shouldAddXdebugKey(jdk) || !"-Xdebug".equals(debugKey) /*the key is non-standard*/;
if (forceClassicVM || forceNoJIT || needDebugKey || !isJVMTIAvailable(jdk)) {
parameters.getVMParametersList().replaceOrPrepend("-Xrunjdwp:", "-Xrunjdwp:" + _debuggeeRunProperties);
}
else {
// use newer JVMTI if available
parameters.getVMParametersList().replaceOrPrepend("-Xrunjdwp:", "");
parameters.getVMParametersList().replaceOrPrepend("-agentlib:jdwp=", "-agentlib:jdwp=" + _debuggeeRunProperties);
}
if (forceNoJIT) {
parameters.getVMParametersList().replaceOrPrepend("-Djava.compiler=", "-Djava.compiler=NONE");
parameters.getVMParametersList().replaceOrPrepend("-Xnoagent", "-Xnoagent");
}
if (needDebugKey) {
parameters.getVMParametersList().replaceOrPrepend(debugKey, debugKey);
}
else {
// deliberately skip outdated parameter because it can disable full-speed debugging for some jdk builds
// see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6272174
parameters.getVMParametersList().replaceOrPrepend("-Xdebug", "");
}
parameters.getVMParametersList().replaceOrPrepend("-classic", forceClassicVM ? "-classic" : "");
}
});
return new RemoteConnection(useSockets, "127.0.0.1", address, debuggerInServerMode);
}
private static boolean shouldForceNoJIT(Sdk jdk) {
if (DebuggerSettings.getInstance().DISABLE_JIT) {
return true;
}
if (jdk != null) {
final String version = JdkUtil.getJdkMainAttribute(jdk, Attributes.Name.IMPLEMENTATION_VERSION);
if (version != null && (version.startsWith("1.2") || version.startsWith("1.3"))) {
return true;
}
}
return false;
}
private static boolean shouldAddXdebugKey(Sdk jdk) {
if (jdk == null) {
return true; // conservative choice
}
if (DebuggerSettings.getInstance().DISABLE_JIT) {
return true;
}
//if (ApplicationManager.getApplication().isUnitTestMode()) {
// need this in unit tests to avoid false alarms when comparing actual output with expected output
//return true;
//}
final String version = JdkUtil.getJdkMainAttribute(jdk, Attributes.Name.IMPLEMENTATION_VERSION);
return version == null ||
//version.startsWith("1.5") ||
version.startsWith("1.4") ||
version.startsWith("1.3") ||
version.startsWith("1.2") ||
version.startsWith("1.1") ||
version.startsWith("1.0");
}
private static boolean isJVMTIAvailable(Sdk jdk) {
if (jdk == null) {
return false; // conservative choice
}
final String version = JdkUtil.getJdkMainAttribute(jdk, Attributes.Name.IMPLEMENTATION_VERSION);
if (version == null) {
return false;
}
return !(version.startsWith("1.4") ||
version.startsWith("1.3") ||
version.startsWith("1.2") ||
version.startsWith("1.1") ||
version.startsWith("1.0"));
}
public static RemoteConnection createDebugParameters(final JavaParameters parameters,
GenericDebuggerRunnerSettings settings,
boolean checkValidity)
throws ExecutionException {
return createDebugParameters(parameters, settings.LOCAL, settings.getTransport(), settings.DEBUG_PORT, checkValidity);
}
private static class MyDebuggerStateManager extends DebuggerStateManager {
private DebuggerSession myDebuggerSession;
@Override
public DebuggerContextImpl getContext() {
return myDebuggerSession == null ? DebuggerContextImpl.EMPTY_CONTEXT : myDebuggerSession.getContextManager().getContext();
}
@Override
public void setState(final DebuggerContextImpl context, int state, int event, String description) {
ApplicationManager.getApplication().assertIsDispatchThread();
myDebuggerSession = context.getDebuggerSession();
if (myDebuggerSession != null) {
myDebuggerSession.getContextManager().setState(context, state, event, description);
}
else {
fireStateChanged(context, event);
}
}
}
private void dispose(DebuggerSession session) {
ProcessHandler processHandler = session.getProcess().getProcessHandler();
synchronized (mySessions) {
DebuggerSession removed = mySessions.remove(processHandler);
LOG.assertTrue(removed != null);
myDispatcher.getMulticaster().sessionRemoved(session);
}
}
}