/* * 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 { private static final Logger LOG = Logger.getInstance("#com.intellij.debugger.impl.DebuggerManagerImpl"); private final Project myProject; private final HashMap mySessions = new HashMap(); private final BreakpointManager myBreakpointManager; private final List myNameMappers = ContainerUtil.createLockFreeCopyOnWriteList(); private final List> myCustomPositionManagerFactories = new ArrayList>(); private final EventDispatcher 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 getSessions() { synchronized (mySessions) { final Collection values = mySessions.values(); return values.isEmpty() ? Collections.emptyList() : new ArrayList(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 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 factory) { myCustomPositionManagerFactories.add(factory); } @Override public void unregisterPositionManagerFactory(final Function 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); } } }