chromium/tools/win/ChromeDebug/ChromeDebug/AttachDialog.cs

// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Management;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

using ChromeDebug.LowLevel;

namespace ChromeDebug {
  // The form that is displayed to allow the user to select processes to attach to.  Note that we
  // cannot interact with the DTE object from here (I assume this is because the dialog is running
  // on a different thread, although I don't fully understand), so any access to the DTE object
  // will have to be done through events that get posted back to the main package thread.
  public partial class AttachDialog : Form {
    private class ProcessViewItem : ListViewItem {
      public ProcessViewItem() {
        Category = ProcessCategory.Other;
        MachineType = LowLevelTypes.MachineType.UNKNOWN;
      }

      public string Exe;
      public int ProcessId;
      public int SessionId;
      public string Title;
      public string DisplayCmdLine;
      public string[] CmdLineArgs;
      public ProcessCategory Category;
      public LowLevelTypes.MachineType MachineType;

      public ProcessDetail Detail;
    }

    private Dictionary<ProcessCategory, List<ProcessViewItem>> loadedProcessTable = null;
    private Dictionary<ProcessCategory, ListViewGroup> processGroups = null;
    private List<int> selectedProcesses = null;

    public AttachDialog() {
      InitializeComponent();

      loadedProcessTable = new Dictionary<ProcessCategory, List<ProcessViewItem>>();
      processGroups = new Dictionary<ProcessCategory, ListViewGroup>();
      selectedProcesses = new List<int>();

      // Create and initialize the groups and process lists only once. On a reset
      // we don't clear the groups manually, clearing the list view should clear the
      // groups. And we don't clear the entire processes_ dictionary, only the
      // individual buckets inside the dictionary.
      foreach (object value in Enum.GetValues(typeof(ProcessCategory))) {
        ProcessCategory category = (ProcessCategory)value;

        ListViewGroup group = new ListViewGroup(category.ToGroupTitle());
        processGroups[category] = group;
        listViewProcesses.Groups.Add(group);

        loadedProcessTable[category] = new List<ProcessViewItem>();
      }
    }

    // Provides an iterator that evaluates to the process ids of the entries that are selected
    // in the list view.
    public IEnumerable<int> SelectedItems {
      get {
        foreach (ProcessViewItem item in listViewProcesses.SelectedItems)
          yield return item.ProcessId;
      }
    }

    private void AttachDialog_Load(object sender, EventArgs e) {
      RepopulateListView();
    }

    // Remove command line arguments that we aren't interested in displaying as part of the command
    // line of the process.
    private string[] FilterCommandLine(string[] args) {
      Func<string, int, bool> AllowArgument = delegate(string arg, int index) {
        if (index == 0)
          return false;
        return !arg.StartsWith("--force-fieldtrials", StringComparison.CurrentCultureIgnoreCase);
      };

      // The force-fieldtrials command line option makes the command line view useless, so remove
      // it.  Also remove args[0] since that is the process name.
      args = args.Where(AllowArgument).ToArray();
      return args;
    }

    private void ReloadNativeProcessInfo() {
      foreach (List<ProcessViewItem> list in loadedProcessTable.Values) {
        list.Clear();
      }

      Process[] processes = Process.GetProcesses();
      foreach (Process p in processes) {
        ProcessViewItem item = new ProcessViewItem();
        try {
          item.Detail = new ProcessDetail(p.Id);
          if (item.Detail.CanReadPeb && item.Detail.CommandLine != null) {
            item.CmdLineArgs = Utility.SplitArgs(item.Detail.CommandLine);
            item.DisplayCmdLine = GetFilteredCommandLineString(item.CmdLineArgs);
          }
          item.MachineType = item.Detail.MachineType;
        }
        catch (Exception) {
          // Generally speaking, an exception here means the process is privileged and we cannot
          // get any information about the process.  For those processes, we will just display the
          // information that the Framework gave us in the Process structure.
        }

        // If we don't have the machine type, its privilege level is high enough that we won't be
        // able to attach a debugger to it anyway, so skip it.
        if (item.MachineType == LowLevelTypes.MachineType.UNKNOWN)
          continue;

        item.ProcessId = p.Id;
        item.SessionId = p.SessionId;
        item.Title = p.MainWindowTitle;
        item.Exe = p.ProcessName;
        if (item.CmdLineArgs != null)
          item.Category = DetermineProcessCategory(item.Detail.Win32ProcessImagePath, 
                                                   item.CmdLineArgs);

        Icon icon = item.Detail.SmallIcon;
        List<ProcessViewItem> items = loadedProcessTable[item.Category];
        item.Group = processGroups[item.Category];
        items.Add(item);
      }
    }

    // Filter the command line arguments to remove extraneous arguments that we don't wish to
    // display.
    private string GetFilteredCommandLineString(string[] args) {
      if (args == null || args.Length == 0)
        return string.Empty;

      args = FilterCommandLine(args);
      return string.Join(" ", args, 0, args.Length);
    }

    // Using a heuristic based on the command line, tries to determine what type of process this
    // is.
    private ProcessCategory DetermineProcessCategory(string imagePath, string[] cmdline) {
      if (cmdline == null || cmdline.Length == 0)
        return ProcessCategory.Other;

      string file = Path.GetFileName(imagePath);
      if (file.Equals("delegate_execute.exe", StringComparison.CurrentCultureIgnoreCase))
        return ProcessCategory.DelegateExecute;
      else if (file.Equals("chrome.exe", StringComparison.CurrentCultureIgnoreCase)) {
          if (cmdline.Contains("--type=renderer"))
              return ProcessCategory.Renderer;
          else if (cmdline.Contains("--type=plugin") || cmdline.Contains("--type=ppapi"))
              return ProcessCategory.Plugin;
          else if (cmdline.Contains("--type=gpu-process"))
              return ProcessCategory.Gpu;
          else if (cmdline.Contains("--type=service"))
              return ProcessCategory.Service;
          else if (cmdline.Any(arg => arg.StartsWith("-ServerName")))
              return ProcessCategory.MetroViewer;
          else
              return ProcessCategory.Browser;
      } else
        return ProcessCategory.Other;
    }

    private void InsertCategoryItems(ProcessCategory category) {
      foreach (ProcessViewItem item in loadedProcessTable[category]) {
        item.Text = item.Exe;
        item.SubItems.Add(item.ProcessId.ToString());
        item.SubItems.Add(item.Title);
        item.SubItems.Add(item.MachineType.ToString());
        item.SubItems.Add(item.SessionId.ToString());
        item.SubItems.Add(item.DisplayCmdLine);
        listViewProcesses.Items.Add(item);

        Icon icon = item.Detail.SmallIcon;
        if (icon != null) {
          item.ImageList.Images.Add(icon);
          item.ImageIndex = item.ImageList.Images.Count - 1;
        }
      }
    }

    private void AutoResizeColumns() {
      // First adjust to the width of the headers, since it's fast.
      listViewProcesses.AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize);

      // Save the widths so we can use them again later.
      List<int> widths = new List<int>();
      foreach (ColumnHeader header in listViewProcesses.Columns)
        widths.Add(header.Width);

      // Now let Windows do the slow adjustment based on the content.
      listViewProcesses.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent);

      // Finally, iterate over each column, and resize those columns that just got smaller.
      int total = 0;
      for (int i = 0; i < listViewProcesses.Columns.Count; ++i) {
        // Resize to the largest of the two, but don't let it go over a pre-defined maximum.
        int max = Math.Max(listViewProcesses.Columns[i].Width, widths[i]);
        int capped = Math.Min(max, 300);

        // We do still want to fill up the available space in the list view however, so if we're
        // under then we can fill.
        int globalMinWidth = listViewProcesses.Width - SystemInformation.VerticalScrollBarWidth;
        if (i == listViewProcesses.Columns.Count - 1 && (total + capped) < (globalMinWidth - 4))
          capped = globalMinWidth - total - 4;

        total += capped;
        listViewProcesses.Columns[i].Width = capped;
      }
    }

    private void RepopulateListView() {
      listViewProcesses.Items.Clear();
      listViewProcesses.SmallImageList = new ImageList();
      listViewProcesses.SmallImageList.ImageSize = new Size(16, 16);

      ReloadNativeProcessInfo();

      InsertCategoryItems(ProcessCategory.Browser);
      InsertCategoryItems(ProcessCategory.Renderer);
      InsertCategoryItems(ProcessCategory.Gpu);
      InsertCategoryItems(ProcessCategory.Plugin);
      InsertCategoryItems(ProcessCategory.MetroViewer);
      InsertCategoryItems(ProcessCategory.Service);
      InsertCategoryItems(ProcessCategory.DelegateExecute);
      if (!checkBoxOnlyChrome.Checked)
        InsertCategoryItems(ProcessCategory.Other);

      AutoResizeColumns();
    }

    private void buttonRefresh_Click(object sender, EventArgs e) {
      RepopulateListView();
    }

    private void buttonAttach_Click(object sender, EventArgs e) {
      System.Diagnostics.Debug.WriteLine("Closing dialog.");
      this.Close();
    }

    private void checkBoxOnlyChrome_CheckedChanged(object sender, EventArgs e) {
      if (!checkBoxOnlyChrome.Checked)
        InsertCategoryItems(ProcessCategory.Other);
      else {
        foreach (ProcessViewItem item in loadedProcessTable[ProcessCategory.Other]) {
          listViewProcesses.Items.Remove(item);
        }
      }
    }
  }
}