constructor reference: don't ignore constructor parameters during method reference...
[idea/community.git] / java / debugger / impl / src / com / intellij / debugger / impl / HotSwapManager.java
1 // Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.debugger.impl;
3
4 import com.intellij.debugger.DebuggerBundle;
5 import com.intellij.debugger.engine.DebuggerManagerThreadImpl;
6 import com.intellij.debugger.engine.events.DebuggerCommandImpl;
7 import com.intellij.ide.actions.ActionsCollector;
8 import com.intellij.openapi.application.ReadAction;
9 import com.intellij.openapi.project.Project;
10 import com.intellij.openapi.roots.OrderEnumerator;
11 import com.intellij.openapi.util.Pair;
12 import com.intellij.openapi.util.SystemInfo;
13 import com.intellij.openapi.util.io.FileUtil;
14 import com.intellij.openapi.util.text.StringUtil;
15 import com.intellij.util.containers.JBIterable;
16 import gnu.trove.THashMap;
17 import org.jetbrains.annotations.NotNull;
18
19 import java.io.File;
20 import java.util.*;
21
22 public class HotSwapManager {
23   private final Map<DebuggerSession, Long> myTimeStamps = new HashMap<>();
24   private static final String CLASS_EXTENSION = ".class";
25   private final Project myProject;
26
27   public HotSwapManager(@NotNull Project project) {
28     myProject = project;
29     project.getMessageBus().connect().subscribe(DebuggerManagerListener.TOPIC, new DebuggerManagerListener() {
30       @Override
31       public void sessionCreated(DebuggerSession session) {
32         myTimeStamps.put(session, Long.valueOf(System.currentTimeMillis()));
33       }
34
35       @Override
36       public void sessionRemoved(DebuggerSession session) {
37         myTimeStamps.remove(session);
38       }
39     });
40   }
41
42   private long getTimeStamp(DebuggerSession session) {
43     Long tStamp = myTimeStamps.get(session);
44     return tStamp != null ? tStamp.longValue() : 0;
45   }
46
47   void setTimeStamp(DebuggerSession session, long tStamp) {
48     myTimeStamps.put(session, Long.valueOf(tStamp));
49   }
50
51   public Map<String, HotSwapFile> scanForModifiedClasses(final DebuggerSession session, final HotSwapProgress progress) {
52     DebuggerManagerThreadImpl.assertIsManagerThread();
53
54     final long timeStamp = getTimeStamp(session);
55     final Map<String, HotSwapFile> modifiedClasses = new HashMap<>();
56
57     List<String> outputPaths = ReadAction.compute(
58       () -> JBIterable.of(OrderEnumerator.orderEntries(myProject).classes().getRoots())
59         .filterMap(o -> o.isDirectory() && !o.getFileSystem().isReadOnly() ? o.getPath() : null)
60         .toList());
61     for (String path : outputPaths) {
62       String rootPath = FileUtil.toCanonicalPath(path);
63       collectModifiedClasses(new File(path), rootPath, rootPath + "/", modifiedClasses, progress, timeStamp);
64     }
65
66     return modifiedClasses;
67   }
68
69   private static boolean collectModifiedClasses(File file, String filePath, String rootPath, Map<String, HotSwapFile> container, HotSwapProgress progress, long timeStamp) {
70     if (progress.isCancelled()) {
71       return false;
72     }
73     final File[] files = file.listFiles();
74     if (files != null) {
75       for (File child : files) {
76         if (!collectModifiedClasses(child, filePath + "/" + child.getName(), rootPath, container, progress, timeStamp)) {
77           return false;
78         }
79       }
80     }
81     else { // not a dir
82       if (SystemInfo.isFileSystemCaseSensitive? StringUtil.endsWith(filePath, CLASS_EXTENSION) : StringUtil.endsWithIgnoreCase(filePath, CLASS_EXTENSION)) {
83         if (file.lastModified() > timeStamp) {
84           progress.setText(DebuggerBundle.message("progress.hotswap.scanning.path", filePath));
85           final String qualifiedName = filePath.substring(rootPath.length(), filePath.length() - CLASS_EXTENSION.length()).replace('/', '.');
86           container.put(qualifiedName, new HotSwapFile(file));
87         }
88       }
89     }
90     return true;
91   }
92
93   public static HotSwapManager getInstance(Project project) {
94     return project.getComponent(HotSwapManager.class);
95   }
96
97   private void reloadClasses(DebuggerSession session, Map<String, HotSwapFile> classesToReload, HotSwapProgress progress) {
98     final long newSwapTime = System.currentTimeMillis();
99     new ReloadClassesWorker(session, progress).reloadClasses(classesToReload);
100     if (progress.isCancelled()) {
101       session.setModifiedClassesScanRequired(true);
102     }
103     else {
104       setTimeStamp(session, newSwapTime);
105     }
106   }
107
108   public static Map<DebuggerSession, Map<String, HotSwapFile>> findModifiedClasses(List<? extends DebuggerSession> sessions, Map<String, Collection<String>> generatedPaths) {
109     final Map<DebuggerSession, Map<String, HotSwapFile>> result = new HashMap<>();
110     List<Pair<DebuggerSession, Long>> sessionWithStamps = new ArrayList<>();
111     for (DebuggerSession session : sessions) {
112       sessionWithStamps.add(new Pair<>(session, getInstance(session.getProject()).getTimeStamp(session)));
113     }
114     for (Map.Entry<String, Collection<String>> entry : generatedPaths.entrySet()) {
115       final File root = new File(entry.getKey());
116       for (String relativePath : entry.getValue()) {
117         if (SystemInfo.isFileSystemCaseSensitive? StringUtil.endsWith(relativePath, CLASS_EXTENSION) : StringUtil.endsWithIgnoreCase(relativePath, CLASS_EXTENSION)) {
118           final String qualifiedName = relativePath.substring(0, relativePath.length() - CLASS_EXTENSION.length()).replace('/', '.');
119           final HotSwapFile hotswapFile = new HotSwapFile(new File(root, relativePath));
120           final long fileStamp = hotswapFile.file.lastModified();
121
122           for (Pair<DebuggerSession, Long> pair : sessionWithStamps) {
123             final DebuggerSession session = pair.first;
124             if (fileStamp > pair.second) {
125               result.computeIfAbsent(session, k -> new HashMap<>()).put(qualifiedName, hotswapFile);
126             }
127           }
128         }
129       }
130     }
131     return result;
132   }
133
134
135   @NotNull
136   public static Map<DebuggerSession, Map<String, HotSwapFile>> scanForModifiedClasses(@NotNull List<? extends DebuggerSession> sessions,
137                                                                                       @NotNull HotSwapProgress swapProgress) {
138     final Map<DebuggerSession, Map<String, HotSwapFile>> modifiedClasses = new THashMap<>();
139     final MultiProcessCommand scanClassesCommand = new MultiProcessCommand();
140     swapProgress.setCancelWorker(() -> scanClassesCommand.cancel());
141     for (DebuggerSession debuggerSession : sessions) {
142       if (!debuggerSession.isAttached()) {
143         continue;
144       }
145
146       scanClassesCommand.addCommand(debuggerSession.getProcess(), new DebuggerCommandImpl() {
147         @Override
148         protected void action() {
149           swapProgress.setDebuggerSession(debuggerSession);
150           Map<String, HotSwapFile> sessionClasses = getInstance(swapProgress.getProject()).scanForModifiedClasses(debuggerSession, swapProgress);
151           if (!sessionClasses.isEmpty()) {
152             modifiedClasses.put(debuggerSession, sessionClasses);
153           }
154         }
155       });
156     }
157
158     swapProgress.setTitle(DebuggerBundle.message("progress.hotswap.scanning.classes"));
159     scanClassesCommand.run();
160
161     if (swapProgress.isCancelled()) {
162       for (DebuggerSession session : sessions) {
163         session.setModifiedClassesScanRequired(true);
164       }
165       return Collections.emptyMap();
166     }
167     else {
168       return modifiedClasses;
169     }
170   }
171
172   public static void reloadModifiedClasses(@NotNull Map<DebuggerSession, Map<String, HotSwapFile>> modifiedClasses, @NotNull HotSwapProgress reloadClassesProgress) {
173     MultiProcessCommand reloadClassesCommand = new MultiProcessCommand();
174     reloadClassesProgress.setCancelWorker(() -> reloadClassesCommand.cancel());
175     for (DebuggerSession debuggerSession : modifiedClasses.keySet()) {
176       reloadClassesCommand.addCommand(debuggerSession.getProcess(), new DebuggerCommandImpl() {
177         @Override
178         protected void action() {
179           reloadClassesProgress.setDebuggerSession(debuggerSession);
180           getInstance(reloadClassesProgress.getProject()).reloadClasses(
181             debuggerSession, modifiedClasses.get(debuggerSession), reloadClassesProgress
182           );
183         }
184
185         @Override
186         protected void commandCancelled() {
187           debuggerSession.setModifiedClassesScanRequired(true);
188         }
189       });
190     }
191
192     reloadClassesProgress.setTitle(DebuggerBundle.message("progress.hotswap.reloading"));
193     reloadClassesCommand.run();
194     ActionsCollector.getInstance().record("Reload Classes", HotSwapManager.class);
195   }
196 }