Chromium Code Reviews| Index: visual_studio/NativeClientVSAddIn/NativeClientVSAddIn/PluginDebuggerHelper.cs |
| diff --git a/visual_studio/NativeClientVSAddIn/NativeClientVSAddIn/PluginDebuggerHelper.cs b/visual_studio/NativeClientVSAddIn/NativeClientVSAddIn/PluginDebuggerHelper.cs |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..854574b8bc2e96eafe8b39ca577b14609b1337d1 |
| --- /dev/null |
| +++ b/visual_studio/NativeClientVSAddIn/NativeClientVSAddIn/PluginDebuggerHelper.cs |
| @@ -0,0 +1,455 @@ |
| +// 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. |
| + |
| +using EnvDTE; |
| +using EnvDTE80; |
| +using System; |
| +using System.Collections.Generic; |
| +using System.IO; |
| +using System.Linq; |
| +using System.Text; |
| +using System.Resources; |
| +using System.Windows.Forms; |
| + |
| +using Microsoft.VisualStudio.Shell; |
|
elijahtaylor1
2012/07/11 20:56:18
ABC order of 'using' directives, I don't mind if y
Petr Hosek
2012/07/12 00:25:46
You can use 'Remove and Sort' feature from 'Organi
tysand
2012/07/12 23:56:15
Done.
tysand
2012/07/12 23:56:15
Done.
|
| +using Microsoft.VisualStudio.VCProject; |
| +using Microsoft.VisualStudio.VCProjectEngine; |
| + |
| +namespace NativeClientVSAddIn |
| +{ |
| + /// <summary> |
| + /// This class contains functions and utilities which are run when the user |
| + /// "presses F5" or otherwise starts debugging from within Visual Studio. |
|
elijahtaylor1
2012/07/11 20:56:18
quotes not needed
tysand
2012/07/12 23:56:15
Done.
|
| + /// </summary> |
| + class PluginDebuggerHelper |
| + { |
| + /// <summary> |
| + /// Constructs the PluginDebuggerHelper. |
| + /// Object is not useable until InitializeFromProjectSettings() is called. |
|
elijahtaylor1
2012/07/11 20:56:18
s/useable/usable/
tysand
2012/07/12 23:56:15
Done.
|
| + /// </summary> |
| + /// <param name="dte">Automation object from Visual Studio</param> |
| + public PluginDebuggerHelper(DTE2 dte) |
| + { |
| + if (dte == null) |
| + { |
| + throw new ArgumentNullException("dte"); |
| + } |
| + |
| + dte_ = dte; |
| + IsDebuggerRunning = false; |
| + |
| + // Every second, check for a new instance of the plug-in to attach to |
| + pluginFinderTimer_ = new Timer(); |
|
noelallen1
2012/07/11 22:16:46
Who is providing Timer? Is this a system Timer, i
tysand
2012/07/12 23:56:15
This is a System.Windows.Forms.Timer. It works by
|
| + pluginFinderTimer_.Tick += new EventHandler(FindAndAttachToPlugin); |
| + pluginFinderForbiddenPids_ = new List<uint>(); |
| + processSearcher_ = new ProcessSearcher(); |
| + } |
| + |
| + /// <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 InitializeFromProjectSettings() |
| + { |
| + isProperlyInitialized_ = false; |
| + |
| + // We require that there is only a single start-up project |
|
elijahtaylor1
2012/07/11 20:56:18
This doesn't seem to be the case... it looks like
tysand
2012/07/12 23:56:15
Yes, you can specify multiple start-up projects in
|
| + 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 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 |
| + String activePlatform = startProject.ConfigurationManager.ActiveConfiguration.PlatformName; |
| + if (String.Compare(activePlatform, Strings.PepperPlatformName, true) == 0) |
| + { |
| + projectPlatformType_ = ProjectPlatformType.Pepper; |
| + debuggerType_ = DebuggerType.VS; |
| + PluginFoundEvent += new EventHandler<PluginFoundEventArgs>(AttachVSDebugger); |
| + } |
| + else if (String.Compare(activePlatform, Strings.NaClPlatformName, true) == 0) |
| + { |
| + projectPlatformType_ = ProjectPlatformType.NaCl; |
| + debuggerType_ = DebuggerType.GDB; |
| + PluginFoundEvent += new EventHandler<PluginFoundEventArgs>(AttachNaClGDB); |
| + } |
| + else |
| + { |
| + projectPlatformType_ = ProjectPlatformType.Other; |
| + return false; |
| + } |
| + |
| + // We only support certain project types (e.g. C/C++ projects) |
| + // If supported, extract necessary information from specific project type |
| + if (Utility.IsVisualCProject(startProject)) |
|
elijahtaylor1
2012/07/11 20:56:18
should this be an exception/error if this is not t
tysand
2012/07/12 23:56:15
No, the idea is that we return true if everything
|
| + { |
| + VCConfiguration config = Utility.GetActiveVCConfiguration(startProject); |
| + VCProject vcproj = (VCProject)startProject.Object; |
|
Petr Hosek
2012/07/12 00:25:46
I would rather use `startProject.Object as VCProje
tysand
2012/07/12 23:56:15
The function IsVisualCProject should ensure that t
|
| + VCLinkerTool linker = config.Tools.Item("VCLinkerTool"); |
| + pluginProjectDirectory_ = vcproj.ProjectDirectory; // Macros not allowed here |
| + pluginAssembly_ = config.Evaluate(linker.OutputFile); |
| + pluginOutputDirectory_ = config.Evaluate(config.OutputDirectory); |
| + isProperlyInitialized_ = true; |
| + } |
| + |
|
noelallen1
2012/07/11 22:16:46
Why track isPropertyInitialized, why not return fa
tysand
2012/07/12 23:56:15
Done.
|
| + int webServerPort = 5103; |
|
elijahtaylor1
2012/07/11 20:56:18
consider using a user parameter or a random/increa
tysand
2012/07/12 23:56:15
This is a future todo.
On 2012/07/11 20:56:18, eli
|
| + sdkRootDirectory_ = Environment.GetEnvironmentVariable(Strings.SDKPathEnvironmentVariable); |
| + sdkRootDirectory_ = sdkRootDirectory_.TrimEnd("/\\".ToArray<char>()); |
| + |
| + webServerExecutable_ = "python.exe"; |
| + webServerArguments_ = String.Format("{0}\\examples\\httpd.py --no_dir_check {1}", |
| + sdkRootDirectory_, webServerPort); |
| + |
| + gdbPath_ = sdkRootDirectory_ + @"customGDB\gdb.exe"; |
| + irtPath_ = sdkRootDirectory_ + @"\tools\irt_x86_64.nexe"; |
|
elijahtaylor1
2012/07/11 20:56:18
why is this hardcoded to 64-bit? What about 32 bi
tysand
2012/07/12 23:56:15
This code is a placeholder for the real nacl-gdb,
|
| + |
| + return isProperlyInitialized_; |
| + } |
| + |
| + /// <summary> |
| + /// This function should be called to start the PluginDebuggerHelper functionality |
| + /// </summary> |
| + public void StartDebugging() |
| + { |
|
noelallen1
2012/07/11 22:16:46
Should this throw or return false?
tysand
2012/07/12 23:56:15
Probably. I'm changing it to throw.
On 2012/07/11
|
| + if (!isProperlyInitialized_) |
| + { |
| + return; |
| + } |
| + |
| + StartWebServer(); |
| + pluginFinderTimer_.Interval = 1000; |
|
elijahtaylor1
2012/07/11 20:56:18
comment on units?
Petr Hosek
2012/07/12 00:25:46
How about using a constant?
tysand
2012/07/12 23:56:15
Done.
tysand
2012/07/12 23:56:15
Done.
|
| + pluginFinderTimer_.Start(); |
| + IsDebuggerRunning = true; |
| + } |
| + |
| + /// <summary> |
| + /// This function should be called to stop the PluginDebuggerHelper functionality |
| + /// </summary> |
| + public void StopDebugging() |
| + { |
|
noelallen1
2012/07/11 22:16:46
Are there really two sets of state you care about?
tysand
2012/07/12 23:56:15
Initially IsDebuggerRunning was used externally by
|
| + IsDebuggerRunning = false; |
| + 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; |
| + } |
| + } |
| + |
| + if (webServer_ != null) |
| + { |
| + webServer_.Kill(); |
| + webServer_ = null; |
| + } |
| + |
| + KillGDBProcess(); |
| + } |
| + |
| + /// <summary> |
| + /// Cleans up the started GDB process |
| + /// </summary> |
| + private void KillGDBProcess() |
| + { |
| + if (gdbProcess_ != null) |
| + { |
| + if (!gdbProcess_.HasExited) |
| + { |
| + gdbProcess_.Kill(); |
| + } |
| + |
| + if (!String.IsNullOrEmpty(gdbInitFileName_) && File.Exists(gdbInitFileName_)) |
| + { |
| + File.Delete(gdbInitFileName_); |
| + } |
| + |
| + gdbProcess_ = null; |
| + } |
| + } |
| + |
| + /// <summary> |
| + /// 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">Unused</param> |
| + /// <param name="unused1">Unused</param> |
| + private void FindAndAttachToPlugin(Object unused, EventArgs unused1) |
| + { |
| + String processNameTarget; |
| + String typeFlagTarget; |
| + String identifierFlagTarget; |
| + switch(projectPlatformType_) |
| + { |
| + case ProjectPlatformType.Pepper: |
| + processNameTarget = Strings.PepperProcessName; |
| + typeFlagTarget = Strings.PepperProcessTypeFlag; |
| + identifierFlagTarget = |
| + String.Format(Strings.PepperProcessPluginFlagFormat, pluginAssembly_); |
| + break; |
| + case ProjectPlatformType.NaCl: |
| + processNameTarget = Strings.NaClProcessName; |
| + typeFlagTarget = Strings.NaClProcessTypeFlag; |
| + identifierFlagTarget = Strings.NaClDebugFlag; |
| + break; |
| + default: |
| + return; |
| + } |
| + |
| + /* To identify our target plug-in we look for: it's process name (e.g. chrome.exe), |
|
elijahtaylor1
2012/07/11 20:56:18
s/it's/its/
tysand
2012/07/12 23:56:15
Done.
|
| + * identifying command line arguments (e.g. --type=renderer), not already attached |
| + * to by us, and must be a descendant process of this instance of Visual Studio */ |
| + List<ProcessInfo> results = processSearcher_.GetResultsByName(processNameTarget); |
| + uint currentProcessId = (uint)System.Diagnostics.Process.GetCurrentProcess().Id; |
| + StringComparison ignoreCase = StringComparison.InvariantCultureIgnoreCase; |
| + foreach (ProcessInfo process in results) |
| + { |
| + if (!pluginFinderForbiddenPids_.Contains(process.ID) && |
| + !String.IsNullOrEmpty(process.CommandLine) && |
| + process.CommandLine.Contains(typeFlagTarget, ignoreCase) && |
| + process.CommandLine.Contains(identifierFlagTarget, ignoreCase) && |
| + Utility.IsDescendantOfProcess(processSearcher_, process.ID, currentProcessId)) |
| + { |
| + pluginFinderForbiddenPids_.Add(process.ID); |
|
elijahtaylor1
2012/07/11 20:56:18
I'm unclear what the "forbidden" list is for, comm
tysand
2012/07/12 23:56:15
Comment added: If we are attaching to a plug-in, a
|
| + PluginFoundEvent.Invoke(this, new PluginFoundEventArgs(process.ID)); |
| + |
| + // Slow down the frequency of checks for new plugins to 5 seconds |
| + pluginFinderTimer_.Interval = 5000; |
|
elijahtaylor1
2012/07/11 20:56:18
Why do we still need to check for connections if w
tysand
2012/07/12 23:56:15
Although contrived, there could be multiple proces
|
| + } |
| + } |
| + } |
| + |
| + /// <summary> |
| + /// Attaches the visual studio debugger to a given process ID |
| + /// </summary> |
| + /// <param name="src">Object the event came from</param> |
| + /// <param name="args">Contains the process ID to attach to</param> |
| + private void AttachVSDebugger(object src, PluginFoundEventArgs args) |
|
elijahtaylor1
2012/07/11 20:56:18
src unused
tysand
2012/07/12 23:56:15
Done.
|
| + { |
| + foreach (EnvDTE.Process proc in dte_.Debugger.LocalProcesses) |
| + { |
| + if (proc.ProcessID == args.ProcessID) |
| + { |
| + proc.Attach(); |
|
elijahtaylor1
2012/07/11 20:56:18
break
noelallen1
2012/07/11 22:16:46
Should you break or return. Can the same ProcessI
tysand
2012/07/12 23:56:15
Done.
|
| + } |
| + } |
| + } |
| + |
| + /// <summary> |
| + /// Will do magic to attach GDB to NaCl process properly, load symbols, and load breakpoints |
|
elijahtaylor1
2012/07/11 20:56:18
Ha, can you change this comment to something more
tysand
2012/07/12 23:56:15
Heh yep I'll change it now. But the magic should g
|
| + /// </summary> |
| + /// <param name="src">Object the event came from</param> |
| + /// <param name="args"> |
| + /// Contains the process ID to attach to, unused since debug stub already attached |
| + /// </param> |
| + private void AttachNaClGDB(object src, PluginFoundEventArgs args) |
|
elijahtaylor1
2012/07/11 20:56:18
src unused
tysand
2012/07/12 23:56:15
Done.
|
| + { |
| + // NOTE: The settings listed here are a placeholder until nacl-gdb is ready |
|
elijahtaylor1
2012/07/11 20:56:18
Can you be more specific? what settings?
tysand
2012/07/12 23:56:15
Done.
|
| + |
| + // Clean up any pre-existing GDB process (can happen if user reloads page) |
| + KillGDBProcess(); |
| + |
| + gdbInitFileName_ = Path.GetTempFileName(); |
| + String projectDir = pluginProjectDirectory_.TrimEnd('\\'); |
| + String pluginAssemblyEscaped = pluginAssembly_.Replace("\\", "\\\\"); |
| + String irtPathEscaped = irtPath_.Replace("\\", "\\\\"); |
| + |
| + // Create the initialization file to read in on GDB start |
| + StringBuilder contents = new StringBuilder(); |
| + contents.AppendFormat("target remote localhost:{0}", 4014); |
|
elijahtaylor1
2012/07/11 20:56:18
comment on 4014 constant, maybe set a constant var
elijahtaylor1
2012/07/11 20:56:18
comment on 4014, maybe use a constant variable in
tysand
2012/07/12 23:56:15
Done.
|
| + contents.AppendLine(); |
| + contents.Append("set architecture i386:x86-64"); |
|
elijahtaylor1
2012/07/11 20:56:18
same comment as above with assuming 64-bit
tysand
2012/07/12 23:56:15
Done.
|
| + contents.AppendLine(); |
| + contents.AppendFormat("cd {0}", projectDir); |
| + contents.AppendLine(); |
| + contents.AppendFormat("add-symbol-file {0} 0xC00020080", pluginAssemblyEscaped); |
|
elijahtaylor1
2012/07/11 20:56:18
same comment as above for hardcoded constants
noelallen1
2012/07/11 22:16:46
Are you sure this value is fixed? Can't it change
tysand
2012/07/12 23:56:15
The value technically shouldn't be fixed in some c
|
| + contents.AppendLine(); |
| + contents.AppendFormat("add-symbol-file {0} 0xC0fc00080", irtPathEscaped); |
|
elijahtaylor1
2012/07/11 20:56:18
same comment as above for hardcoded constants
tysand
2012/07/12 23:56:15
Done.
|
| + contents.AppendLine(); |
| + |
| + // Insert breakpoints from Visual Studio project |
| + foreach (Breakpoint bp in dte_.Debugger.Breakpoints) |
| + { |
| + if (bp.Enabled && |
| + bp.LocationType == dbgBreakpointLocationType.dbgBreakpointLocationTypeFile) |
| + { |
| + contents.AppendFormat("b {0}:{1}", Path.GetFileName(bp.File), bp.FileLine); |
| + contents.AppendLine(); |
| + } |
| + else if (bp.Enabled && |
| + bp.LocationType == dbgBreakpointLocationType.dbgBreakpointLocationTypeFunction) |
| + { |
| + contents.AppendFormat("b {0}", bp.FunctionName); |
| + contents.AppendLine(); |
| + } |
|
elijahtaylor1
2012/07/11 20:56:18
are there any other types of breakpoints we can or
tysand
2012/07/12 23:56:15
Done.
|
| + } |
| + |
| + 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 plugin |
| + /// </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">Unused</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) |
| + { |
| + webServerOutputPane_.OutputString(message + "\n"); |
| + webServerOutputPane_.Activate(); |
| + } |
| + |
| + /// <summary> |
| + /// Specifies the debugger to use when debugging |
| + /// </summary> |
| + private enum DebuggerType |
| + { |
| + GDB, |
| + VS, |
| + WinGDB |
|
noelallen1
2012/07/11 22:16:46
is WinGDB unused?
tysand
2012/07/12 23:56:15
Done.
|
| + } |
| + |
| + /// <summary> |
| + /// Specifies the type of plug-in being run in this debug session |
| + /// </summary> |
| + private enum ProjectPlatformType |
| + { |
| + Other, |
| + NaCl, |
| + Pepper |
| + } |
| + |
| + private DTE2 dte_; |
| + private bool isProperlyInitialized_ = false; |
| + private String pluginProjectDirectory_; |
| + private String pluginOutputDirectory_; |
| + private String pluginAssembly_; |
| + |
| + private String irtPath_; |
| + private String sdkRootDirectory_; |
| + |
| + private System.Diagnostics.Process gdbProcess_; |
| + private String gdbPath_; |
| + private String gdbInitFileName_; |
| + |
| + private DebuggerType debuggerType_; |
| + private ProjectPlatformType projectPlatformType_; |
| + |
| + private System.Diagnostics.Process webServer_ = null; |
| + private OutputWindowPane webServerOutputPane_ = null; |
| + private String webServerExecutable_; |
| + private String webServerArguments_; |
| + |
| + private Timer pluginFinderTimer_; |
| + private List<uint> pluginFinderForbiddenPids_; |
| + |
| + private IProcessSearcher processSearcher_; |
| + |
| + public event EventHandler<PluginFoundEventArgs> PluginFoundEvent; |
|
elijahtaylor1
2012/07/11 20:56:18
member variable naming
tysand
2012/07/12 23:56:15
Microsoft style on events is PascalCase because I
|
| + |
| + /// <summary> |
| + /// Indicates true if the debugger is running (even during breakpoints) |
| + /// </summary> |
| + public bool IsDebuggerRunning { get; private set; } |
|
elijahtaylor1
2012/07/11 20:56:18
property naming
tysand
2012/07/12 23:56:15
Done.
|
| + } |
| + |
| + /// <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) |
| + { |
| + ProcessID = pid; |
| + } |
| + |
| + /// <summary> |
| + /// Process ID of the found plug-in |
| + /// </summary> |
| + public uint ProcessID { get; set; } |
|
elijahtaylor1
2012/07/11 20:56:18
property naming
tysand
2012/07/12 23:56:15
Done.
|
| + } |
| +} |