blob: 11df707ae4a6374f9e726bb8176eb969d184a77e [file] [log] [blame]
/*
* Copyright 2000-2009 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.ui;
import com.intellij.CommonBundle;
import com.intellij.debugger.DebuggerBundle;
import com.intellij.debugger.DebuggerManager;
import com.intellij.debugger.DebuggerManagerEx;
import com.intellij.debugger.impl.DebuggerManagerAdapter;
import com.intellij.debugger.impl.DebuggerSession;
import com.intellij.debugger.impl.HotSwapFile;
import com.intellij.debugger.impl.HotSwapManager;
import com.intellij.debugger.settings.DebuggerSettings;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.compiler.CompilationStatusListener;
import com.intellij.openapi.compiler.CompileContext;
import com.intellij.openapi.compiler.CompilerManager;
import com.intellij.openapi.compiler.CompilerTopics;
import com.intellij.openapi.compiler.ex.CompilerPathsEx;
import com.intellij.openapi.components.ProjectComponent;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.util.PairFunction;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.messages.MessageBus;
import com.intellij.util.messages.MessageBusConnection;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.util.JpsPathUtil;
import javax.swing.*;
import java.io.File;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
/**
* User: lex
* Date: Oct 2, 2003
* Time: 6:00:55 PM
*/
public class HotSwapUIImpl extends HotSwapUI implements ProjectComponent {
private final List<HotSwapVetoableListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();
private boolean myAskBeforeHotswap = true;
private final Project myProject;
private boolean myPerformHotswapAfterThisCompilation = true;
public HotSwapUIImpl(final Project project, final MessageBus bus, DebuggerManager debugManager) {
myProject = project;
((DebuggerManagerEx)debugManager).addDebuggerManagerListener(new DebuggerManagerAdapter() {
private MessageBusConnection myConn = null;
private int mySessionCount = 0;
@Override
public void sessionAttached(DebuggerSession session) {
if (mySessionCount++ == 0) {
myConn = bus.connect();
myConn.subscribe(CompilerTopics.COMPILATION_STATUS, new MyCompilationStatusListener());
}
}
@Override
public void sessionDetached(DebuggerSession session) {
mySessionCount = Math.max(0, mySessionCount - 1);
if (mySessionCount == 0) {
final MessageBusConnection conn = myConn;
if (conn != null) {
Disposer.dispose(conn);
myConn = null;
}
}
}
});
}
public void projectOpened() {
}
public void projectClosed() {
}
@NotNull
public String getComponentName() {
return "HotSwapUI";
}
public void initComponent() {
}
public void disposeComponent() {
}
public void addListener(HotSwapVetoableListener listener) {
myListeners.add(listener);
}
public void removeListener(HotSwapVetoableListener listener) {
myListeners.remove(listener);
}
private boolean shouldDisplayHangWarning(DebuggerSettings settings, List<DebuggerSession> sessions) {
if (!settings.HOTSWAP_HANG_WARNING_ENABLED) {
return false;
}
// todo: return false if yourkit agent is inactive
for (DebuggerSession session : sessions) {
if (session.isPaused()) {
return true;
}
}
return false;
}
private void hotSwapSessions(final List<DebuggerSession> sessions, @Nullable final Map<String, List<String>> generatedPaths) {
final boolean shouldAskBeforeHotswap = myAskBeforeHotswap;
myAskBeforeHotswap = true;
// need this because search with PSI is perormed during hotswap
PsiDocumentManager.getInstance(myProject).commitAllDocuments();
final DebuggerSettings settings = DebuggerSettings.getInstance();
final String runHotswap = settings.RUN_HOTSWAP_AFTER_COMPILE;
final boolean shouldDisplayHangWarning = shouldDisplayHangWarning(settings, sessions);
if (shouldAskBeforeHotswap && DebuggerSettings.RUN_HOTSWAP_NEVER.equals(runHotswap)) {
return;
}
final boolean shouldPerformScan = generatedPaths == null;
final HotSwapProgressImpl findClassesProgress;
if (shouldPerformScan) {
findClassesProgress = new HotSwapProgressImpl(myProject);
}
else {
boolean createProgress = false;
for (DebuggerSession session : sessions) {
if (session.isModifiedClassesScanRequired()) {
createProgress = true;
break;
}
}
findClassesProgress = createProgress ? new HotSwapProgressImpl(myProject) : null;
}
ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
public void run() {
final Map<DebuggerSession, Map<String, HotSwapFile>> modifiedClasses;
if (shouldPerformScan) {
modifiedClasses = scanForModifiedClassesWithProgress(sessions, findClassesProgress, false);
}
else {
final List<DebuggerSession> toScan = new ArrayList<DebuggerSession>();
final List<DebuggerSession> toUseGenerated = new ArrayList<DebuggerSession>();
for (DebuggerSession session : sessions) {
(session.isModifiedClassesScanRequired() ? toScan : toUseGenerated).add(session);
session.setModifiedClassesScanRequired(false);
}
modifiedClasses = new HashMap<DebuggerSession, Map<String, HotSwapFile>>();
if (!toUseGenerated.isEmpty()) {
modifiedClasses.putAll(HotSwapManager.findModifiedClasses(toUseGenerated, generatedPaths));
}
if (!toScan.isEmpty()) {
modifiedClasses.putAll(scanForModifiedClassesWithProgress(toScan, findClassesProgress, !true));
}
}
final Application application = ApplicationManager.getApplication();
if (modifiedClasses.isEmpty()) {
final String message = DebuggerBundle.message("status.hotswap.uptodate");
HotSwapProgressImpl.NOTIFICATION_GROUP.createNotification(message, NotificationType.INFORMATION).notify(myProject);
return;
}
application.invokeLater(new Runnable() {
public void run() {
if (shouldAskBeforeHotswap && !DebuggerSettings.RUN_HOTSWAP_ALWAYS.equals(runHotswap)) {
final RunHotswapDialog dialog = new RunHotswapDialog(myProject, sessions, shouldDisplayHangWarning);
dialog.show();
if (!dialog.isOK()) {
for (DebuggerSession session : modifiedClasses.keySet()) {
session.setModifiedClassesScanRequired(true);
}
return;
}
final Set<DebuggerSession> toReload = new HashSet<DebuggerSession>(dialog.getSessionsToReload());
for (DebuggerSession session : modifiedClasses.keySet()) {
if (!toReload.contains(session)) {
session.setModifiedClassesScanRequired(true);
}
}
modifiedClasses.keySet().retainAll(toReload);
}
else {
if (shouldDisplayHangWarning) {
final int answer = Messages.showCheckboxMessageDialog(
DebuggerBundle.message("hotswap.dialog.hang.warning"),
DebuggerBundle.message("hotswap.dialog.title"),
new String[]{"Perform &Reload Classes", "&Skip Reload Classes"},
CommonBundle.message("dialog.options.do.not.show"),
false, 1, 1, Messages.getWarningIcon(),
new PairFunction<Integer, JCheckBox, Integer>() {
@Override
public Integer fun(Integer exitCode, JCheckBox cb) {
settings.HOTSWAP_HANG_WARNING_ENABLED = !cb.isSelected();
return exitCode == DialogWrapper.OK_EXIT_CODE ? exitCode : DialogWrapper.CANCEL_EXIT_CODE;
}
}
);
if (answer == DialogWrapper.CANCEL_EXIT_CODE) {
for (DebuggerSession session : modifiedClasses.keySet()) {
session.setModifiedClassesScanRequired(true);
}
return;
}
}
}
if (!modifiedClasses.isEmpty()) {
final HotSwapProgressImpl progress = new HotSwapProgressImpl(myProject);
application.executeOnPooledThread(new Runnable() {
public void run() {
reloadModifiedClasses(modifiedClasses, progress);
}
});
}
}
}, ModalityState.NON_MODAL);
}
});
}
private static Map<DebuggerSession, Map<String, HotSwapFile>> scanForModifiedClassesWithProgress(final List<DebuggerSession> sessions,
final HotSwapProgressImpl progress,
final boolean scanWithVFS) {
final Ref<Map<DebuggerSession, Map<String, HotSwapFile>>> result = Ref.create(null);
ProgressManager.getInstance().runProcess(new Runnable() {
public void run() {
try {
result.set(HotSwapManager.scanForModifiedClasses(sessions, progress, scanWithVFS));
}
finally {
progress.finished();
}
}
}, progress.getProgressIndicator());
return result.get();
}
private static void reloadModifiedClasses(final Map<DebuggerSession, Map<String, HotSwapFile>> modifiedClasses, final HotSwapProgressImpl progress) {
ProgressManager.getInstance().runProcess(new Runnable() {
public void run() {
HotSwapManager.reloadModifiedClasses(modifiedClasses, progress);
progress.finished();
}
}, progress.getProgressIndicator());
}
public void reloadChangedClasses(final DebuggerSession session, boolean compileBeforeHotswap) {
dontAskHotswapAfterThisCompilation();
if (compileBeforeHotswap) {
CompilerManager.getInstance(session.getProject()).make(null);
}
else {
if (session.isAttached()) {
hotSwapSessions(Collections.singletonList(session), null);
}
}
}
public void dontPerformHotswapAfterThisCompilation() {
myPerformHotswapAfterThisCompilation = false;
}
public void dontAskHotswapAfterThisCompilation() {
myAskBeforeHotswap = false;
}
private class MyCompilationStatusListener implements CompilationStatusListener {
private final AtomicReference<Map<String, List<String>>>
myGeneratedPaths = new AtomicReference<Map<String, List<String>>>(new HashMap<String, List<String>>());
private final THashSet<File> myOutputRoots;
private MyCompilationStatusListener() {
myOutputRoots = new THashSet<File>(FileUtil.FILE_HASHING_STRATEGY);
for (final String path : CompilerPathsEx.getOutputPaths(ModuleManager.getInstance(myProject).getModules())) {
myOutputRoots.add(new File(path));
}
}
public void fileGenerated(String outputRoot, String relativePath) {
if (StringUtil.endsWith(relativePath, ".class") && JpsPathUtil.isUnder(myOutputRoots, new File(outputRoot))) {
// collect only classes
final Map<String, List<String>> map = myGeneratedPaths.get();
List<String> paths = map.get(outputRoot);
if (paths == null) {
paths = new ArrayList<String>();
map.put(outputRoot, paths);
}
paths.add(relativePath);
}
}
public void compilationFinished(boolean aborted, int errors, int warnings, CompileContext compileContext) {
final Map<String, List<String>> generated = myGeneratedPaths.getAndSet(new HashMap<String, List<String>>());
if (myProject.isDisposed()) {
return;
}
if (errors == 0 && !aborted && myPerformHotswapAfterThisCompilation) {
for (HotSwapVetoableListener listener : myListeners) {
if (!listener.shouldHotSwap(compileContext)) {
return;
}
}
final List<DebuggerSession> sessions = new ArrayList<DebuggerSession>();
Collection<DebuggerSession> debuggerSessions = DebuggerManagerEx.getInstanceEx(myProject).getSessions();
for (final DebuggerSession debuggerSession : debuggerSessions) {
if (debuggerSession.isAttached() && debuggerSession.getProcess().canRedefineClasses()) {
sessions.add(debuggerSession);
}
}
if (!sessions.isEmpty()) {
hotSwapSessions(sessions, generated);
}
}
myPerformHotswapAfterThisCompilation = true;
}
}
}