blob: a8be9a92b96090a0d7928e8f32165cf4cf4c23f7 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
namespace NativeClientVSAddIn
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using EnvDTE;
using EnvDTE80;
using Microsoft.VisualStudio.VCProjectEngine;
/// <summary>
/// This class contains functions and utilities which are run when the user
/// presses F5 or otherwise starts debugging from within Visual Studio.
/// </summary>
public class PluginDebuggerHelper
{
/// <summary>
/// This is the initial number of milliseconds to wait between
/// checking for plug-in processes to attach the debugger to.
/// </summary>
private const int InitialPluginCheckFrequency = 1000;
/// <summary>
/// After a plug-in has been found, we slow the frequency of checking
/// for new ones. This value is in milliseconds.
/// </summary>
private const int RelaxedPluginCheckFrequency = 5000;
/// <summary>
/// The web server port to default to if the user does not specify one.
/// </summary>
private const int DefaultWebServerPort = 5103;
/// <summary>
/// The main visual studio object through which all Visual Studio functions are executed.
/// </summary>
private DTE2 dte_;
/// <summary>
/// Indicates the PluginDebuggerHelper is configured properly to run.
/// </summary>
private bool isProperlyInitialized_ = false;
/// <summary>
/// Directory of the plug-in project we are debugging.
/// </summary>
private string pluginProjectDirectory_;
/// <summary>
/// Directory where the plug-in assembly is placed.
/// </summary>
private string pluginOutputDirectory_;
/// <summary>
/// Path to the actual plug-in assembly.
/// </summary>
private string pluginAssembly_;
/// <summary>
/// Path to the NaCl IRT.
/// </summary>
private string irtPath_;
/// <summary>
/// Path to the project's nmf file.
/// </summary>
private string manifestPath_;
/// <summary>
/// Root directory of the installed NaCl SDK.
/// </summary>
private string sdkRootDirectory_;
/// <summary>
/// If debugging a .nexe this is the nacl-gdb process object.
/// </summary>
private System.Diagnostics.Process gdbProcess_;
/// <summary>
/// Path to NaCl-GDB executable.
/// </summary>
private string gdbPath_;
/// <summary>
/// Path to the gdb initialization file that we auto-generate from the VS project.
/// </summary>
private string gdbInitFileName_;
/// <summary>
/// The platform that the start-up project is currently configured with (NaCl or PPAPI).
/// </summary>
private ProjectPlatformType projectPlatformType_;
/// <summary>
/// When debugging is started this is the web server process object.
/// </summary>
private System.Diagnostics.Process webServer_;
/// <summary>
/// Visual Studio output window pane that captures output from the web server.
/// </summary>
private OutputWindowPane webServerOutputPane_;
/// <summary>
/// Path to the web server executable.
/// </summary>
private string webServerExecutable_;
/// <summary>
/// Arguments to be passed to the web server executable to start it.
/// </summary>
private string webServerArguments_;
/// <summary>
/// Timer object that periodically calls a function to look for the plug-in process to debug.
/// </summary>
private Timer pluginFinderTimer_;
/// <summary>
/// List of process IDs which we should not attempt to attach the debugger to. Mainly this
/// list contains process IDs of processes we have already attached to.
/// </summary>
private List<uint> pluginFinderForbiddenPids_;
/// <summary>
/// Process searcher class which allows us to query the system for running processes.
/// </summary>
private ProcessSearcher processSearcher_;
/// <summary>
/// The main process of chrome that was started by Visual Studio during debugging.
/// </summary>
private System.Diagnostics.Process debuggedChromeMainProcess_;
/// <summary>
/// Constructs the PluginDebuggerHelper.
/// Object is not usable until LoadProjectSettings() is called.
/// </summary>
/// <param name="dte">Automation object from Visual Studio.</param>
public PluginDebuggerHelper(DTE2 dte)
{
if (dte == null)
{
throw new ArgumentNullException("dte");
}
dte_ = dte;
// Every second, check for a new instance of the plug-in to attach to.
// Note that although the timer itself runs on a separate thread, the event
// is fired from the main UI thread during message processing, thus we do not
// need to worry about threading issues.
pluginFinderTimer_ = new Timer();
pluginFinderTimer_.Tick += new EventHandler(FindAndAttachToPlugin);
pluginFinderForbiddenPids_ = new List<uint>();
processSearcher_ = new ProcessSearcher();
}
/// <summary>
/// An event indicating a target plug-in was found on the system.
/// </summary>
public event EventHandler<PluginFoundEventArgs> PluginFoundEvent;
/// <summary>
/// Specifies the type of plug-in being run in this debug session.
/// </summary>
private enum ProjectPlatformType
{
/// <summary>
/// Represents all non-pepper/non-nacl platform types.
/// </summary>
Other,
/// <summary>
/// Indicates project platform is a trusted plug-in (nexe).
/// </summary>
NaCl,
/// <summary>
/// Indicates project platform is an untrusted plug-in.
/// </summary>
Pepper
}
/// <summary>
/// Initializes the PluginDebuggerHelper with the current project settings
/// If project settings are unsupported for NaCl/Pepper debugging then
/// the object is not initialized and we return false.
/// </summary>
/// <returns>True if the object is successfully initialized, false otherwise.</returns>
public bool LoadProjectSettings()
{
isProperlyInitialized_ = false;
string platformToolset;
// We require that there is only a single start-up project.
// If multiple start-up projects are specified then we use the first and
// leave a warning message in the Web Server output pane.
Array startupProjects = dte_.Solution.SolutionBuild.StartupProjects as Array;
if (startupProjects == null || startupProjects.Length == 0)
{
throw new ArgumentOutOfRangeException("startupProjects.Length");
}
else if (startupProjects.Length > 1)
{
WebServerWriteLine(Strings.WebServerMultiStartProjectWarning);
}
// Get the first start-up project object.
List<Project> projList = dte_.Solution.Projects.OfType<Project>().ToList();
string startProjectName = startupProjects.GetValue(0) as string;
Project startProject = projList.Find(proj => proj.UniqueName == startProjectName);
// Get the current platform type. If not nacl/pepper then fail.
string activePlatform = startProject.ConfigurationManager.ActiveConfiguration.PlatformName;
if (string.Compare(activePlatform, Strings.PepperPlatformName, true) == 0)
{
projectPlatformType_ = ProjectPlatformType.Pepper;
PluginFoundEvent += new EventHandler<PluginFoundEventArgs>(AttachVSDebugger);
}
else if (string.Compare(activePlatform, Strings.NaClPlatformName, true) == 0)
{
projectPlatformType_ = ProjectPlatformType.NaCl;
PluginFoundEvent += new EventHandler<PluginFoundEventArgs>(AttachNaClGDB);
}
else
{
projectPlatformType_ = ProjectPlatformType.Other;
return false;
}
// We only support certain project types (e.g. C/C++ projects). Otherwise we fail.
if (!Utility.IsVisualCProject(startProject))
{
return false;
}
// Extract necessary information from specific project type.
VCConfiguration config = Utility.GetActiveVCConfiguration(startProject);
IVCRulePropertyStorage general = config.Rules.Item("ConfigurationGeneral");
VCLinkerTool linker = config.Tools.Item("VCLinkerTool");
VCProject vcproj = (VCProject)startProject.Object;
sdkRootDirectory_ = general.GetEvaluatedPropertyValue("VSNaClSDKRoot");
platformToolset = general.GetEvaluatedPropertyValue("PlatformToolset");
pluginOutputDirectory_ = config.Evaluate(config.OutputDirectory);
pluginAssembly_ = config.Evaluate(linker.OutputFile);
pluginProjectDirectory_ = vcproj.ProjectDirectory; // Macros not allowed here.
if (projectPlatformType_ == ProjectPlatformType.NaCl)
{
irtPath_ = general.GetEvaluatedPropertyValue("NaClIrtPath");
manifestPath_ = general.GetEvaluatedPropertyValue("NaClManifestPath");
}
if (string.IsNullOrEmpty(sdkRootDirectory_))
{
MessageBox.Show(Strings.SDKPathNotSetError);
return false;
}
sdkRootDirectory_ = sdkRootDirectory_.TrimEnd("/\\".ToArray<char>());
// TODO(tysand): Move this code getting port to where the web server is started.
int webServerPort;
if (!int.TryParse(general.GetEvaluatedPropertyValue("NaClWebServerPort"), out webServerPort))
{
webServerPort = DefaultWebServerPort;
}
webServerExecutable_ = "python.exe";
webServerArguments_ = string.Format(
"{0}\\examples\\httpd.py --no_dir_check {1}", sdkRootDirectory_, webServerPort);
gdbPath_ = Path.Combine(
sdkRootDirectory_, "toolchain", platformToolset, @"bin\x86_64-nacl-gdb.exe");
debuggedChromeMainProcess_ = null;
isProperlyInitialized_ = true;
return true;
}
/// <summary>
/// This function should be called to start the PluginDebuggerHelper functionality.
/// </summary>
public void StartDebugging()
{
if (!isProperlyInitialized_)
{
throw new Exception(Strings.NotInitializedMessage);
}
StartWebServer();
pluginFinderTimer_.Interval = InitialPluginCheckFrequency;
pluginFinderTimer_.Start();
}
/// <summary>
/// This function should be called to stop the PluginDebuggerHelper functionality.
/// </summary>
public void StopDebugging()
{
isProperlyInitialized_ = false;
pluginFinderTimer_.Stop();
pluginFinderForbiddenPids_.Clear();
// Remove all event handlers from the plug-in found event.
if (PluginFoundEvent != null)
{
foreach (Delegate del in PluginFoundEvent.GetInvocationList())
{
PluginFoundEvent -= (EventHandler<PluginFoundEventArgs>)del;
}
}
Utility.EnsureProcessKill(ref webServer_);
WebServerWriteLine(Strings.WebServerStopMessage);
CleanUpGDBProcess();
}
/// <summary>
/// This function cleans up the started GDB process.
/// </summary>
private void CleanUpGDBProcess()
{
Utility.EnsureProcessKill(ref gdbProcess_);
if (!string.IsNullOrEmpty(gdbInitFileName_) && File.Exists(gdbInitFileName_))
{
File.Delete(gdbInitFileName_);
gdbInitFileName_ = null;
}
}
/// <summary>
/// This is called periodically by the Visual Studio UI thread to look for our plug-in process
/// and attach the debugger to it. The call is triggered by the pluginFinderTimer_ object.
/// </summary>
/// <param name="unused">The parameter is not used.</param>
/// <param name="unused1">The parameter is not used.</param>
private void FindAndAttachToPlugin(object unused, EventArgs unused1)
{
StringComparison ignoreCase = StringComparison.InvariantCultureIgnoreCase;
// Set the main chrome process that was started by visual studio. If it's not chrome
// or not found then we have no business attaching to any plug-ins so return.
if (debuggedChromeMainProcess_ == null)
{
foreach (Process proc in dte_.Debugger.DebuggedProcesses)
{
if (proc.Name.EndsWith(Strings.ChromeProcessName, ignoreCase))
{
debuggedChromeMainProcess_ = System.Diagnostics.Process.GetProcessById(proc.ProcessID);
break;
}
}
return;
}
// Get the list of all descendants of the main chrome process.
uint mainChromeProcId = (uint)debuggedChromeMainProcess_.Id;
List<ProcessInfo> chromeDescendants = processSearcher_.GetDescendants(mainChromeProcId);
// If we didn't start with debug flags then we should not attach.
string mainChromeFlags = chromeDescendants.Find(p => p.ID == mainChromeProcId).CommandLine;
if (projectPlatformType_ == ProjectPlatformType.NaCl &&
!mainChromeFlags.Contains(Strings.NaClDebugFlag))
{
return;
}
// From the list of descendants, find the plug-in by it's command line arguments and
// process name as well as not being attached to already.
List<ProcessInfo> plugins;
switch (projectPlatformType_)
{
case ProjectPlatformType.Pepper:
string identifierFlagTarget =
string.Format(Strings.PepperProcessPluginFlagFormat, pluginAssembly_);
plugins = chromeDescendants.FindAll(p =>
p.Name.Equals(Strings.ChromeProcessName, ignoreCase) &&
p.CommandLine.Contains(Strings.ChromeRendererFlag, ignoreCase) &&
p.CommandLine.Contains(identifierFlagTarget, ignoreCase) &&
!pluginFinderForbiddenPids_.Contains(p.ID));
break;
case ProjectPlatformType.NaCl:
plugins = chromeDescendants.FindAll(p =>
p.Name.Equals(Strings.NaClProcessName, ignoreCase) &&
p.CommandLine.Contains(Strings.NaClLoaderFlag, ignoreCase) &&
!pluginFinderForbiddenPids_.Contains(p.ID));
break;
default:
return;
}
// Attach to all plug-ins that we found.
foreach (ProcessInfo process in plugins)
{
// If we are attaching to a plug-in, add it to the forbidden list to ensure we
// don't try to attach again later.
pluginFinderForbiddenPids_.Add(process.ID);
PluginFoundEvent.Invoke(this, new PluginFoundEventArgs(process.ID));
// Slow down the frequency of checks for new plugins.
pluginFinderTimer_.Interval = RelaxedPluginCheckFrequency;
}
}
/// <summary>
/// Attaches the visual studio debugger to a given process ID.
/// </summary>
/// <param name="src">The parameter is not used.</param>
/// <param name="args">Contains the process ID to attach to.</param>
private void AttachVSDebugger(object src, PluginFoundEventArgs args)
{
foreach (EnvDTE.Process proc in dte_.Debugger.LocalProcesses)
{
if (proc.ProcessID == args.ProcessID)
{
proc.Attach();
break;
}
}
}
/// <summary>
/// Attaches the NaCl GDB debugger to the NaCl plug-in process. Handles loading symbols
/// and breakpoints from Visual Studio.
/// </summary>
/// <param name="src">The parameter is not used.</param>
/// <param name="args">
/// Contains the process ID to attach to, unused since debug stub is already attached.
/// </param>
private void AttachNaClGDB(object src, PluginFoundEventArgs args)
{
// Clean up any pre-existing GDB process (can happen if user reloads page).
CleanUpGDBProcess();
gdbInitFileName_ = Path.GetTempFileName();
string pluginAssemblyEscaped = pluginAssembly_.Replace("\\", "\\\\");
string irtPathEscaped = irtPath_.Replace("\\", "\\\\");
// Create the initialization file to read in on GDB start.
StringBuilder contents = new StringBuilder();
if (!string.IsNullOrEmpty(manifestPath_))
{
string manifestEscaped = manifestPath_.Replace("\\", "\\\\");
contents.AppendFormat("nacl-manifest {0}\n", manifestEscaped);
}
else
{
contents.AppendFormat("file \"{0}\"\n", pluginAssemblyEscaped);
}
contents.AppendFormat("nacl-irt {0}\n", irtPathEscaped);
contents.AppendFormat("target remote localhost:{0}\n", 4014);
// Insert breakpoints from Visual Studio project.
foreach (Breakpoint bp in dte_.Debugger.Breakpoints)
{
if (!bp.Enabled)
{
continue;
}
if (bp.LocationType == dbgBreakpointLocationType.dbgBreakpointLocationTypeFile)
{
contents.AppendFormat("b {0}:{1}\n", Path.GetFileName(bp.File), bp.FileLine);
}
else if (bp.LocationType == dbgBreakpointLocationType.dbgBreakpointLocationTypeFunction)
{
contents.AppendFormat("b {0}\n", bp.FunctionName);
}
else
{
WebServerWriteLine(
string.Format(Strings.UnsupportedBreakpointTypeFormat, bp.LocationType.ToString()));
}
}
contents.AppendLine("continue");
File.WriteAllText(gdbInitFileName_, contents.ToString());
// Start NaCl-GDB.
try
{
gdbProcess_ = new System.Diagnostics.Process();
gdbProcess_.StartInfo.UseShellExecute = true;
gdbProcess_.StartInfo.FileName = gdbPath_;
gdbProcess_.StartInfo.Arguments = string.Format("-x {0}", gdbInitFileName_);
gdbProcess_.StartInfo.WorkingDirectory = pluginProjectDirectory_;
gdbProcess_.Start();
}
catch (Exception e)
{
MessageBox.Show(
string.Format("NaCl-GDB Start Failed. {0}. Path: {1}", e.Message, gdbPath_));
}
}
/// <summary>
/// Spins up the web server process to host our plug-in.
/// </summary>
private void StartWebServer()
{
// Add a panel to the output window which is used to capture output
// from the web server hosting the plugin.
if (webServerOutputPane_ == null)
{
webServerOutputPane_ = dte_.ToolWindows.OutputWindow.OutputWindowPanes.Add(
Strings.WebServerOutputWindowTitle);
}
webServerOutputPane_.Clear();
WebServerWriteLine(Strings.WebServerStartMessage);
try
{
webServer_ = new System.Diagnostics.Process();
webServer_.StartInfo.CreateNoWindow = true;
webServer_.StartInfo.UseShellExecute = false;
webServer_.StartInfo.RedirectStandardOutput = true;
webServer_.StartInfo.RedirectStandardError = true;
webServer_.StartInfo.FileName = webServerExecutable_;
webServer_.StartInfo.Arguments = webServerArguments_;
webServer_.StartInfo.WorkingDirectory = pluginProjectDirectory_;
webServer_.OutputDataReceived += WebServerMessageReceive;
webServer_.ErrorDataReceived += WebServerMessageReceive;
webServer_.Start();
webServer_.BeginOutputReadLine();
webServer_.BeginErrorReadLine();
}
catch (Exception e)
{
WebServerWriteLine(Strings.WebServerStartFail);
WebServerWriteLine("Exception: " + e.Message);
}
}
/// <summary>
/// Receives output from the web server process to display in the Visual Studio UI.
/// </summary>
/// <param name="sender">The parameter is not used.</param>
/// <param name="e">Contains the data to display.</param>
private void WebServerMessageReceive(object sender, System.Diagnostics.DataReceivedEventArgs e)
{
WebServerWriteLine(e.Data);
}
/// <summary>
/// Helper function to write data to the Web Server Output Pane.
/// </summary>
/// <param name="message">Message to write.</param>
private void WebServerWriteLine(string message)
{
if (webServerOutputPane_ != null)
{
webServerOutputPane_.OutputString(message + "\n");
}
}
/// <summary>
/// The event arguments when a plug-in is found.
/// </summary>
public class PluginFoundEventArgs : EventArgs
{
/// <summary>
/// Construct the PluginFoundEventArgs.
/// </summary>
/// <param name="pid">Process ID of the found plug-in.</param>
public PluginFoundEventArgs(uint pid)
{
this.ProcessID = pid;
}
/// <summary>
/// Gets or sets process ID of the found plug-in.
/// </summary>
public uint ProcessID { get; set; }
}
}
}