1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5using System;
6using System.Collections.Generic;
7using System.ComponentModel;
8using System.Data;
9using System.Diagnostics;
10using System.Drawing;
11using System.IO;
12using System.Linq;
13using System.Management;
14using System.Text;
15using System.Threading.Tasks;
16using System.Windows.Forms;
17
18using ChromeDebug.LowLevel;
19
20namespace ChromeDebug {
21  // The form that is displayed to allow the user to select processes to attach to.  Note that we
22  // cannot interact with the DTE object from here (I assume this is because the dialog is running
23  // on a different thread, although I don't fully understand), so any access to the DTE object
24  // will have to be done through events that get posted back to the main package thread.
25  public partial class AttachDialog : Form {
26    private class ProcessViewItem : ListViewItem {
27      public ProcessViewItem() {
28        Category = ProcessCategory.Other;
29        MachineType = LowLevelTypes.MachineType.UNKNOWN;
30      }
31
32      public string Exe;
33      public int ProcessId;
34      public int SessionId;
35      public string Title;
36      public string DisplayCmdLine;
37      public string[] CmdLineArgs;
38      public ProcessCategory Category;
39      public LowLevelTypes.MachineType MachineType;
40
41      public ProcessDetail Detail;
42    }
43
44    private Dictionary<ProcessCategory, List<ProcessViewItem>> loadedProcessTable = null;
45    private Dictionary<ProcessCategory, ListViewGroup> processGroups = null;
46    private List<int> selectedProcesses = null;
47
48    public AttachDialog() {
49      InitializeComponent();
50
51      loadedProcessTable = new Dictionary<ProcessCategory, List<ProcessViewItem>>();
52      processGroups = new Dictionary<ProcessCategory, ListViewGroup>();
53      selectedProcesses = new List<int>();
54
55      // Create and initialize the groups and process lists only once. On a reset
56      // we don't clear the groups manually, clearing the list view should clear the
57      // groups. And we don't clear the entire processes_ dictionary, only the
58      // individual buckets inside the dictionary.
59      foreach (object value in Enum.GetValues(typeof(ProcessCategory))) {
60        ProcessCategory category = (ProcessCategory)value;
61
62        ListViewGroup group = new ListViewGroup(category.ToGroupTitle());
63        processGroups[category] = group;
64        listViewProcesses.Groups.Add(group);
65
66        loadedProcessTable[category] = new List<ProcessViewItem>();
67      }
68    }
69
70    // Provides an iterator that evaluates to the process ids of the entries that are selected
71    // in the list view.
72    public IEnumerable<int> SelectedItems {
73      get {
74        foreach (ProcessViewItem item in listViewProcesses.SelectedItems)
75          yield return item.ProcessId;
76      }
77    }
78
79    private void AttachDialog_Load(object sender, EventArgs e) {
80      RepopulateListView();
81    }
82
83    // Remove command line arguments that we aren't interested in displaying as part of the command
84    // line of the process.
85    private string[] FilterCommandLine(string[] args) {
86      Func<string, int, bool> AllowArgument = delegate(string arg, int index) {
87        if (index == 0)
88          return false;
89        return !arg.StartsWith("--force-fieldtrials", StringComparison.CurrentCultureIgnoreCase);
90      };
91
92      // The force-fieldtrials command line option makes the command line view useless, so remove
93      // it.  Also remove args[0] since that is the process name.
94      args = args.Where(AllowArgument).ToArray();
95      return args;
96    }
97
98    private void ReloadNativeProcessInfo() {
99      foreach (List<ProcessViewItem> list in loadedProcessTable.Values) {
100        list.Clear();
101      }
102
103      Process[] processes = Process.GetProcesses();
104      foreach (Process p in processes) {
105        ProcessViewItem item = new ProcessViewItem();
106        try {
107          item.Detail = new ProcessDetail(p.Id);
108          if (item.Detail.CanReadPeb && item.Detail.CommandLine != null) {
109            item.CmdLineArgs = Utility.SplitArgs(item.Detail.CommandLine);
110            item.DisplayCmdLine = GetFilteredCommandLineString(item.CmdLineArgs);
111          }
112          item.MachineType = item.Detail.MachineType;
113        }
114        catch (Exception) {
115          // Generally speaking, an exception here means the process is privileged and we cannot
116          // get any information about the process.  For those processes, we will just display the
117          // information that the Framework gave us in the Process structure.
118        }
119
120        // If we don't have the machine type, its privilege level is high enough that we won't be
121        // able to attach a debugger to it anyway, so skip it.
122        if (item.MachineType == LowLevelTypes.MachineType.UNKNOWN)
123          continue;
124
125        item.ProcessId = p.Id;
126        item.SessionId = p.SessionId;
127        item.Title = p.MainWindowTitle;
128        item.Exe = p.ProcessName;
129        if (item.CmdLineArgs != null)
130          item.Category = DetermineProcessCategory(item.Detail.Win32ProcessImagePath,
131                                                   item.CmdLineArgs);
132
133        Icon icon = item.Detail.SmallIcon;
134        List<ProcessViewItem> items = loadedProcessTable[item.Category];
135        item.Group = processGroups[item.Category];
136        items.Add(item);
137      }
138    }
139
140    // Filter the command line arguments to remove extraneous arguments that we don't wish to
141    // display.
142    private string GetFilteredCommandLineString(string[] args) {
143      if (args == null || args.Length == 0)
144        return string.Empty;
145
146      args = FilterCommandLine(args);
147      return string.Join(" ", args, 0, args.Length);
148    }
149
150    // Using a heuristic based on the command line, tries to determine what type of process this
151    // is.
152    private ProcessCategory DetermineProcessCategory(string imagePath, string[] cmdline) {
153      if (cmdline == null || cmdline.Length == 0)
154        return ProcessCategory.Other;
155
156      string file = Path.GetFileName(imagePath);
157      if (file.Equals("delegate_execute.exe", StringComparison.CurrentCultureIgnoreCase))
158        return ProcessCategory.DelegateExecute;
159      else if (file.Equals("chrome.exe", StringComparison.CurrentCultureIgnoreCase)) {
160          if (cmdline.Contains("--type=renderer"))
161              return ProcessCategory.Renderer;
162          else if (cmdline.Contains("--type=plugin") || cmdline.Contains("--type=ppapi"))
163              return ProcessCategory.Plugin;
164          else if (cmdline.Contains("--type=gpu-process"))
165              return ProcessCategory.Gpu;
166          else if (cmdline.Contains("--type=service"))
167              return ProcessCategory.Service;
168          else if (cmdline.Any(arg => arg.StartsWith("-ServerName")))
169              return ProcessCategory.MetroViewer;
170          else
171              return ProcessCategory.Browser;
172      } else
173        return ProcessCategory.Other;
174    }
175
176    private void InsertCategoryItems(ProcessCategory category) {
177      foreach (ProcessViewItem item in loadedProcessTable[category]) {
178        item.Text = item.Exe;
179        item.SubItems.Add(item.ProcessId.ToString());
180        item.SubItems.Add(item.Title);
181        item.SubItems.Add(item.MachineType.ToString());
182        item.SubItems.Add(item.SessionId.ToString());
183        item.SubItems.Add(item.DisplayCmdLine);
184        listViewProcesses.Items.Add(item);
185
186        Icon icon = item.Detail.SmallIcon;
187        if (icon != null) {
188          item.ImageList.Images.Add(icon);
189          item.ImageIndex = item.ImageList.Images.Count - 1;
190        }
191      }
192    }
193
194    private void AutoResizeColumns() {
195      // First adjust to the width of the headers, since it's fast.
196      listViewProcesses.AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize);
197
198      // Save the widths so we can use them again later.
199      List<int> widths = new List<int>();
200      foreach (ColumnHeader header in listViewProcesses.Columns)
201        widths.Add(header.Width);
202
203      // Now let Windows do the slow adjustment based on the content.
204      listViewProcesses.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent);
205
206      // Finally, iterate over each column, and resize those columns that just got smaller.
207      int total = 0;
208      for (int i = 0; i < listViewProcesses.Columns.Count; ++i) {
209        // Resize to the largest of the two, but don't let it go over a pre-defined maximum.
210        int max = Math.Max(listViewProcesses.Columns[i].Width, widths[i]);
211        int capped = Math.Min(max, 300);
212
213        // We do still want to fill up the available space in the list view however, so if we're
214        // under then we can fill.
215        int globalMinWidth = listViewProcesses.Width - SystemInformation.VerticalScrollBarWidth;
216        if (i == listViewProcesses.Columns.Count - 1 && (total + capped) < (globalMinWidth - 4))
217          capped = globalMinWidth - total - 4;
218
219        total += capped;
220        listViewProcesses.Columns[i].Width = capped;
221      }
222    }
223
224    private void RepopulateListView() {
225      listViewProcesses.Items.Clear();
226      listViewProcesses.SmallImageList = new ImageList();
227      listViewProcesses.SmallImageList.ImageSize = new Size(16, 16);
228
229      ReloadNativeProcessInfo();
230
231      InsertCategoryItems(ProcessCategory.Browser);
232      InsertCategoryItems(ProcessCategory.Renderer);
233      InsertCategoryItems(ProcessCategory.Gpu);
234      InsertCategoryItems(ProcessCategory.Plugin);
235      InsertCategoryItems(ProcessCategory.MetroViewer);
236      InsertCategoryItems(ProcessCategory.Service);
237      InsertCategoryItems(ProcessCategory.DelegateExecute);
238      if (!checkBoxOnlyChrome.Checked)
239        InsertCategoryItems(ProcessCategory.Other);
240
241      AutoResizeColumns();
242    }
243
244    private void buttonRefresh_Click(object sender, EventArgs e) {
245      RepopulateListView();
246    }
247
248    private void buttonAttach_Click(object sender, EventArgs e) {
249      System.Diagnostics.Debug.WriteLine("Closing dialog.");
250      this.Close();
251    }
252
253    private void checkBoxOnlyChrome_CheckedChanged(object sender, EventArgs e) {
254      if (!checkBoxOnlyChrome.Checked)
255        InsertCategoryItems(ProcessCategory.Other);
256      else {
257        foreach (ProcessViewItem item in loadedProcessTable[ProcessCategory.Other]) {
258          listViewProcesses.Items.Remove(item);
259        }
260      }
261    }
262  }
263}
264