[build] drops obsolete perm. gen. option
[idea/community.git] / platform / xdebugger-impl / src / com / intellij / xdebugger / impl / actions / AttachToLocalProcessAction.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.xdebugger.impl.actions;
17
18 import com.intellij.execution.ExecutionException;
19 import com.intellij.execution.process.OSProcessUtil;
20 import com.intellij.execution.process.ProcessInfo;
21 import com.intellij.execution.runners.ExecutionUtil;
22 import com.intellij.internal.statistic.UsageTrigger;
23 import com.intellij.internal.statistic.beans.ConvertUsagesUtil;
24 import com.intellij.openapi.actionSystem.AnAction;
25 import com.intellij.openapi.actionSystem.AnActionEvent;
26 import com.intellij.openapi.application.ApplicationManager;
27 import com.intellij.openapi.extensions.Extensions;
28 import com.intellij.openapi.progress.PerformInBackgroundOption;
29 import com.intellij.openapi.progress.ProgressIndicator;
30 import com.intellij.openapi.progress.Task;
31 import com.intellij.openapi.project.Project;
32 import com.intellij.openapi.ui.popup.*;
33 import com.intellij.openapi.ui.popup.util.BaseListPopupStep;
34 import com.intellij.openapi.util.Key;
35 import com.intellij.openapi.util.Pair;
36 import com.intellij.openapi.util.UserDataHolder;
37 import com.intellij.openapi.util.UserDataHolderBase;
38 import com.intellij.openapi.util.text.StringUtil;
39 import com.intellij.openapi.wm.ToolWindowId;
40 import com.intellij.ui.popup.list.ListPopupImpl;
41 import com.intellij.util.containers.ContainerUtil;
42 import com.intellij.util.containers.MultiMap;
43 import com.intellij.util.containers.hash.LinkedHashMap;
44 import com.intellij.util.ui.StatusText;
45 import com.intellij.xdebugger.XDebuggerBundle;
46 import com.intellij.xdebugger.attach.XLocalAttachDebugger;
47 import com.intellij.xdebugger.attach.XLocalAttachDebuggerProvider;
48 import com.intellij.xdebugger.attach.XLocalAttachGroup;
49 import org.intellij.lang.annotations.MagicConstant;
50 import org.jetbrains.annotations.NotNull;
51 import org.jetbrains.annotations.Nullable;
52
53 import javax.swing.*;
54 import javax.swing.event.ListSelectionEvent;
55 import javax.swing.event.ListSelectionListener;
56 import java.awt.event.InputEvent;
57 import java.util.*;
58
59 public class AttachToLocalProcessAction extends AnAction {
60   private static final Key<LinkedHashMap<String, HistoryItem>> HISTORY_KEY = Key.create("AttachToLocalProcessAction.HISTORY_KEY");
61
62   public AttachToLocalProcessAction() {
63     super(XDebuggerBundle.message("xdebugger.attach.toLocal.action"),
64           XDebuggerBundle.message("xdebugger.attach.toLocal.action.description"), null);
65   }
66
67   @Override
68   public void update(AnActionEvent e) {
69     super.update(e);
70
71     Project project = getEventProject(e);
72     boolean enabled = project != null && Extensions.getExtensions(XLocalAttachDebuggerProvider.EP).length > 0;
73     e.getPresentation().setEnabledAndVisible(enabled);
74   }
75
76   @Override
77   public void actionPerformed(AnActionEvent e) {
78     final Project project = getEventProject(e);
79     if (project == null) return;
80
81     XLocalAttachDebuggerProvider[] providers = Extensions.getExtensions(XLocalAttachDebuggerProvider.EP);
82
83     new Task.Backgroundable(project, XDebuggerBundle.message("xdebugger.attach.toLocal.action.collectingProcesses"), true, PerformInBackgroundOption.DEAF) {
84       @Override
85       public void run(@NotNull ProgressIndicator indicator) {
86         ProcessInfo[] processList = OSProcessUtil.getProcessList();
87         List<AttachItem> items = collectAttachItems(project, processList, indicator, providers);
88         ApplicationManager.getApplication().invokeLater(() -> {
89           if (project.isDisposed()) {
90             return;
91           }
92           ProcessListStep step = new ProcessListStep(items, project);
93
94           final ListPopup popup = JBPopupFactory.getInstance().createListPopup(step);
95           final JList mainList = ((ListPopupImpl) popup).getList();
96
97           ListSelectionListener listener = event -> {
98             if (event.getValueIsAdjusting()) return;
99
100             Object item = ((JList) event.getSource()).getSelectedValue();
101
102             // if a sub-list is closed, fallback to the selected value from the main list
103             if (item == null) {
104               item = mainList.getSelectedValue();
105             }
106
107             if (item instanceof AttachItem) {
108               String debuggerName = ((AttachItem)item).getSelectedDebugger().getDebuggerDisplayName();
109               debuggerName = StringUtil.shortenTextWithEllipsis(debuggerName, 50, 0);
110               ((ListPopupImpl)popup).setCaption(XDebuggerBundle.message("xdebugger.attach.toLocal.popup.title", debuggerName));
111             }
112           };
113           popup.addListSelectionListener(listener);
114
115           // force first valueChanged event
116           listener.valueChanged(new ListSelectionEvent(mainList, mainList.getMinSelectionIndex(), mainList.getMaxSelectionIndex(), false));
117
118           popup.showCenteredInCurrentWindow(project);
119         });
120       }
121     }.queue();
122   }
123
124   @NotNull
125   public static List<AttachItem> collectAttachItems(@NotNull final Project project,
126                                                     @NotNull ProcessInfo[] processList,
127                                                     @NotNull ProgressIndicator indicator,
128                                                     @NotNull XLocalAttachDebuggerProvider... providers) {
129     MultiMap<XLocalAttachGroup, Pair<ProcessInfo, ArrayList<XLocalAttachDebugger>>> groupWithItems = new MultiMap<>();
130
131     UserDataHolderBase dataHolder = new UserDataHolderBase();
132     for (ProcessInfo eachInfo : processList) {
133
134       MultiMap<XLocalAttachGroup, XLocalAttachDebugger> groupsWithDebuggers = new MultiMap<>();
135       for (XLocalAttachDebuggerProvider eachProvider : providers) {
136         indicator.checkCanceled();
137         groupsWithDebuggers.putValues(eachProvider.getAttachGroup(), eachProvider.getAvailableDebuggers(project, eachInfo, dataHolder));
138       }
139
140       for (XLocalAttachGroup eachGroup : groupsWithDebuggers.keySet()) {
141         Collection<XLocalAttachDebugger> debuggers = groupsWithDebuggers.get(eachGroup);
142         if (!debuggers.isEmpty()) {
143           groupWithItems.putValue(eachGroup, Pair.create(eachInfo, new ArrayList<>(debuggers)));
144         }
145       }
146     }
147
148     ArrayList<XLocalAttachGroup> sortedGroups = new ArrayList<>(groupWithItems.keySet());
149     sortedGroups.sort(Comparator.comparingInt(XLocalAttachGroup::getOrder));
150
151     List<AttachItem> currentItems = new ArrayList<>();
152     for (final XLocalAttachGroup eachGroup : sortedGroups) {
153       List<Pair<ProcessInfo, ArrayList<XLocalAttachDebugger>>> sortedItems
154           = new ArrayList<>(groupWithItems.get(eachGroup));
155       sortedItems.sort((a, b) -> eachGroup.compare(project, a.first, b.first, dataHolder));
156
157       boolean first = true;
158       for (Pair<ProcessInfo, ArrayList<XLocalAttachDebugger>> eachItem : sortedItems) {
159         currentItems.add(new AttachItem(eachGroup, first, eachItem.first, eachItem.second, dataHolder));
160         first = false;
161       }
162     }
163
164     List<AttachItem> currentHistoryItems = new ArrayList<>();
165     List<HistoryItem> history = getHistory(project);
166     for (int i = history.size() - 1; i >= 0; i--) {
167       HistoryItem eachHistoryItem = history.get(i);
168       for (AttachItem eachCurrentItem : currentItems) {
169         boolean isSuitableItem = eachHistoryItem.getGroup().equals(eachCurrentItem.getGroup()) &&
170             eachHistoryItem.getProcessInfo().getCommandLine().equals(eachCurrentItem.getProcessInfo().getCommandLine());
171         if (!isSuitableItem) continue;
172
173         List<XLocalAttachDebugger> debuggers = eachCurrentItem.getDebuggers();
174         int selectedDebugger = -1;
175         for (int j = 0; j < debuggers.size(); j++) {
176           XLocalAttachDebugger eachDebugger = debuggers.get(j);
177           if (eachDebugger.getDebuggerDisplayName().equals(eachHistoryItem.getDebuggerName())) {
178             selectedDebugger = j;
179             break;
180           }
181         }
182         if (selectedDebugger == -1) continue;
183
184         currentHistoryItems.add(new AttachItem(eachCurrentItem.getGroup(),
185             currentHistoryItems.isEmpty(),
186             XDebuggerBundle.message("xdebugger.attach.toLocal.popup.recent"),
187             eachCurrentItem.getProcessInfo(),
188             debuggers,
189             selectedDebugger,
190             dataHolder));
191       }
192     }
193
194     currentHistoryItems.addAll(currentItems);
195     return currentHistoryItems;
196   }
197
198   public static void addToHistory(@NotNull Project project, @NotNull AttachItem item) {
199     LinkedHashMap<String, HistoryItem> history = project.getUserData(HISTORY_KEY);
200     if (history == null) {
201       project.putUserData(HISTORY_KEY, history = new LinkedHashMap<>());
202     }
203     ProcessInfo processInfo = item.getProcessInfo();
204     history.remove(processInfo.getCommandLine());
205     history.put(processInfo.getCommandLine(), new HistoryItem(processInfo, item.getGroup(),
206                                                               item.getSelectedDebugger().getDebuggerDisplayName()));
207     while (history.size() > 4) {
208       history.remove(history.keySet().iterator().next());
209     }
210   }
211
212   @NotNull
213   public static List<HistoryItem> getHistory(@NotNull Project project) {
214     LinkedHashMap<String, HistoryItem> history = project.getUserData(HISTORY_KEY);
215     return history == null ? Collections.emptyList()
216                            : Collections.unmodifiableList(new ArrayList<>(history.values()));
217   }
218
219   public static class HistoryItem {
220     @NotNull private final ProcessInfo myProcessInfo;
221     @NotNull private final XLocalAttachGroup myGroup;
222     @NotNull private final String myDebuggerName;
223
224     public HistoryItem(@NotNull ProcessInfo processInfo,
225                        @NotNull XLocalAttachGroup group,
226                        @NotNull String debuggerName) {
227       myProcessInfo = processInfo;
228       myGroup = group;
229       myDebuggerName = debuggerName;
230     }
231
232     @NotNull
233     public ProcessInfo getProcessInfo() {
234       return myProcessInfo;
235     }
236
237     @NotNull
238     public XLocalAttachGroup getGroup() {
239       return myGroup;
240     }
241
242     @NotNull
243     public String getDebuggerName() {
244       return myDebuggerName;
245     }
246
247     @Override
248     public boolean equals(Object o) {
249       if (this == o) return true;
250       if (o == null || getClass() != o.getClass()) return false;
251
252       HistoryItem item = (HistoryItem)o;
253
254       if (!myProcessInfo.equals(item.myProcessInfo)) return false;
255       if (!myGroup.equals(item.myGroup)) return false;
256       if (!myDebuggerName.equals(item.myDebuggerName)) return false;
257
258       return true;
259     }
260
261     @Override
262     public int hashCode() {
263       int result = myProcessInfo.hashCode();
264       result = 31 * result + myGroup.hashCode();
265       result = 31 * result + myDebuggerName.hashCode();
266       return result;
267     }
268   }
269
270   public static class AttachItem {
271     @NotNull private final XLocalAttachGroup myGroup;
272     private final boolean myIsFirstInGroup;
273     @NotNull private final String myGroupName;
274     @NotNull private UserDataHolder myDataHolder;
275     @NotNull private final ProcessInfo myProcessInfo;
276     @NotNull private final List<XLocalAttachDebugger> myDebuggers;
277     private final int mySelectedDebugger;
278     @NotNull private final List<AttachItem> mySubItems;
279
280     public AttachItem(@NotNull XLocalAttachGroup group,
281                       boolean isFirstInGroup,
282                       @NotNull ProcessInfo info,
283                       @NotNull List<XLocalAttachDebugger> debuggers,
284                       @NotNull UserDataHolder dataHolder) {
285       this(group, isFirstInGroup, group.getGroupName(), info, debuggers, 0, dataHolder);
286     }
287
288     public AttachItem(@NotNull XLocalAttachGroup group,
289                       boolean isFirstInGroup,
290                       @NotNull String groupName,
291                       @NotNull ProcessInfo info,
292                       @NotNull List<XLocalAttachDebugger> debuggers,
293                       int selectedDebugger,
294                       @NotNull UserDataHolder dataHolder) {
295       myGroupName = groupName;
296       myDataHolder = dataHolder;
297       assert !debuggers.isEmpty() : "debugger list should not be empty";
298       assert selectedDebugger >= 0 && selectedDebugger < debuggers.size() : "wrong selected debugger index";
299
300       myGroup = group;
301       myIsFirstInGroup = isFirstInGroup;
302       myProcessInfo = info;
303       myDebuggers = debuggers;
304       mySelectedDebugger = selectedDebugger;
305
306       if (debuggers.size() > 1) {
307         mySubItems = ContainerUtil.map(debuggers, debugger -> new AttachItem(myGroup, false, myProcessInfo, Collections.singletonList(debugger), dataHolder));
308       }
309       else {
310         mySubItems = Collections.emptyList();
311       }
312     }
313
314     @NotNull
315     public ProcessInfo getProcessInfo() {
316       return myProcessInfo;
317     }
318
319     @NotNull
320     public XLocalAttachGroup getGroup() {
321       return myGroup;
322     }
323
324     @Nullable
325     public String getSeparatorTitle() {
326       return myIsFirstInGroup ? myGroupName : null;
327     }
328
329     @Nullable
330     public Icon getIcon(@NotNull Project project) {
331       return myGroup.getProcessIcon(project, myProcessInfo, myDataHolder);
332     }
333
334     @NotNull
335     public String getText(@NotNull Project project) {
336       String shortenedText = StringUtil.shortenTextWithEllipsis(myGroup.getProcessDisplayText(project, myProcessInfo, myDataHolder), 200, 0);
337       return myProcessInfo.getPid() + " " + shortenedText;
338     }
339
340     @NotNull
341     public List<XLocalAttachDebugger> getDebuggers() {
342       return myDebuggers;
343     }
344
345     @NotNull
346     public XLocalAttachDebugger getSelectedDebugger() {
347       return myDebuggers.get(mySelectedDebugger);
348     }
349
350     @NotNull
351     public List<AttachItem> getSubItems() {
352       return mySubItems;
353     }
354
355     public void startDebugSession(@NotNull Project project) {
356       XLocalAttachDebugger debugger = getSelectedDebugger();
357       UsageTrigger.trigger(ConvertUsagesUtil.ensureProperKey("debugger.attach.local"));
358       UsageTrigger.trigger(ConvertUsagesUtil.ensureProperKey("debugger.attach.local." + debugger.getDebuggerDisplayName()));
359
360       try {
361         debugger.attachDebugSession(project, myProcessInfo);
362       }
363       catch (ExecutionException e) {
364         ExecutionUtil.handleExecutionError(project, ToolWindowId.DEBUG, myProcessInfo.getExecutableName(), e);
365       }
366     }
367   }
368
369   private static class MyBasePopupStep extends BaseListPopupStep<AttachItem> {
370     @NotNull final Project myProject;
371
372     public MyBasePopupStep(@NotNull Project project,
373                            @Nullable String title,
374                            List<? extends AttachItem> values) {
375       super(title, values);
376       myProject = project;
377     }
378
379     @Override
380     public boolean isSpeedSearchEnabled() {
381       return true;
382     }
383
384     @Override
385     public boolean isAutoSelectionEnabled() {
386       return false;
387     }
388
389     @Override
390     public boolean hasSubstep(AttachItem selectedValue) {
391       return !selectedValue.getSubItems().isEmpty();
392     }
393
394     @Override
395     public PopupStep onChosen(AttachItem selectedValue, boolean finalChoice) {
396       addToHistory(myProject, selectedValue);
397       selectedValue.startDebugSession(myProject);
398       return FINAL_CHOICE;
399     }
400   }
401
402   private static class ProcessListStep extends MyBasePopupStep implements ListPopupStepEx<AttachItem> {
403     public ProcessListStep(@NotNull List<AttachItem> items, @NotNull Project project) {
404       super(project, XDebuggerBundle.message("xdebugger.attach.toLocal.popup.title.default"), items);
405     }
406
407     @Nullable
408     @Override
409     public ListSeparator getSeparatorAbove(AttachItem value) {
410       String separatorTitle = value.getSeparatorTitle();
411       return separatorTitle == null ? null : new ListSeparator(separatorTitle);
412     }
413
414     @Override
415     public Icon getIconFor(AttachItem value) {
416       return value.getIcon(myProject);
417     }
418
419     @NotNull
420     @Override
421     public String getTextFor(AttachItem value) {
422       return value.getText(myProject);
423     }
424
425     @Nullable
426     @Override
427     public String getTooltipTextFor(AttachItem value) {
428       return value.getText(myProject);
429     }
430
431     @Override
432     public void setEmptyText(@NotNull StatusText emptyText) {
433       emptyText.setText(XDebuggerBundle.message("xdebugger.attach.toLocal.popup.emptyText"));
434     }
435
436     @Override
437     public PopupStep onChosen(AttachItem selectedValue, boolean finalChoice) {
438       if (finalChoice) {
439         return super.onChosen(selectedValue, true);
440       }
441       return new DebuggerListStep(selectedValue.getSubItems(), selectedValue.mySelectedDebugger);
442     }
443
444     @Override
445     public PopupStep onChosen(AttachItem selectedValue,
446                               boolean finalChoice,
447                               @MagicConstant(flagsFromClass = InputEvent.class) int eventModifiers) {
448       return onChosen(selectedValue, finalChoice);
449     }
450
451     private class DebuggerListStep extends MyBasePopupStep {
452       public DebuggerListStep(List<AttachItem> items, int selectedItem) {
453         super(ProcessListStep.this.myProject,
454               XDebuggerBundle.message("xdebugger.attach.toLocal.popup.selectDebugger.title"), items);
455         setDefaultOptionIndex(selectedItem);
456       }
457
458       @NotNull
459       @Override
460       public String getTextFor(AttachItem value) {
461         return value.getSelectedDebugger().getDebuggerDisplayName();
462       }
463     }
464   }
465 }