OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012 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 | |
5 namespace NativeClientVSAddIn | |
6 { | |
7 using System; | |
8 using System.Collections.Generic; | |
9 using System.IO; | |
10 using System.Linq; | |
11 using System.Text; | |
12 using System.Windows.Forms; | |
13 | |
14 using EnvDTE; | |
15 using EnvDTE80; | |
16 using Microsoft.VisualStudio.VCProjectEngine; | |
17 | |
18 /// <summary> | |
19 /// This class contains functions and utilities which are run when the user | |
20 /// presses F5 or otherwise starts debugging from within Visual Studio. | |
21 /// </summary> | |
22 public class PluginDebuggerHelper | |
23 { | |
24 /// <summary> | |
25 /// This is the initial number of milliseconds to wait between | |
26 /// checking for plug-in processes to attach the debugger to. | |
27 /// </summary> | |
28 private const int InitialPluginCheckFrequency = 1000; | |
29 | |
30 /// <summary> | |
31 /// After a plug-in has been found, we slow the frequency of checking | |
32 /// for new ones. This value is in milliseconds. | |
33 /// </summary> | |
34 private const int RelaxedPluginCheckFrequency = 5000; | |
35 | |
36 /// <summary> | |
37 /// The web server port to default to if the user does not specify one. | |
38 /// </summary> | |
39 private const int DefaultWebServerPort = 5103; | |
40 | |
41 /// <summary> | |
42 /// The main visual studio object through which all Visual Studio functions
are executed. | |
43 /// </summary> | |
44 private DTE2 dte_; | |
45 | |
46 /// <summary> | |
47 /// Indicates the PluginDebuggerHelper is configured properly to run. | |
48 /// </summary> | |
49 private bool isProperlyInitialized_ = false; | |
50 | |
51 /// <summary> | |
52 /// Directory of the plug-in project we are debugging. | |
53 /// </summary> | |
54 private string pluginProjectDirectory_; | |
55 | |
56 /// <summary> | |
57 /// Directory where the plug-in assembly is placed. | |
58 /// </summary> | |
59 private string pluginOutputDirectory_; | |
60 | |
61 /// <summary> | |
62 /// Path to the actual plug-in assembly. | |
63 /// </summary> | |
64 private string pluginAssembly_; | |
65 | |
66 /// <summary> | |
67 /// Path to the NaCl IRT. | |
68 /// </summary> | |
69 private string irtPath_; | |
70 | |
71 /// <summary> | |
72 /// Path to the project's nmf file. | |
73 /// </summary> | |
74 private string manifestPath_; | |
75 | |
76 /// <summary> | |
77 /// Root directory of the installed NaCl SDK. | |
78 /// </summary> | |
79 private string sdkRootDirectory_; | |
80 | |
81 /// <summary> | |
82 /// If debugging a .nexe this is the nacl-gdb process object. | |
83 /// </summary> | |
84 private System.Diagnostics.Process gdbProcess_; | |
85 | |
86 /// <summary> | |
87 /// Path to NaCl-GDB executable. | |
88 /// </summary> | |
89 private string gdbPath_; | |
90 | |
91 /// <summary> | |
92 /// Path to the gdb initialization file that we auto-generate from the VS pr
oject. | |
93 /// </summary> | |
94 private string gdbInitFileName_; | |
95 | |
96 /// <summary> | |
97 /// The platform that the start-up project is currently configured with (NaC
l or PPAPI). | |
98 /// </summary> | |
99 private ProjectPlatformType projectPlatformType_; | |
100 | |
101 /// <summary> | |
102 /// When debugging is started this is the web server process object. | |
103 /// </summary> | |
104 private System.Diagnostics.Process webServer_; | |
105 | |
106 /// <summary> | |
107 /// Visual Studio output window pane that captures output from the web serve
r. | |
108 /// </summary> | |
109 private OutputWindowPane webServerOutputPane_; | |
110 | |
111 /// <summary> | |
112 /// Path to the web server executable. | |
113 /// </summary> | |
114 private string webServerExecutable_; | |
115 | |
116 /// <summary> | |
117 /// Arguments to be passed to the web server executable to start it. | |
118 /// </summary> | |
119 private string webServerArguments_; | |
120 | |
121 /// <summary> | |
122 /// Timer object that periodically calls a function to look for the plug-in
process to debug. | |
123 /// </summary> | |
124 private Timer pluginFinderTimer_; | |
125 | |
126 /// <summary> | |
127 /// List of process IDs which we should not attempt to attach the debugger t
o. Mainly this | |
128 /// list contains process IDs of processes we have already attached to. | |
129 /// </summary> | |
130 private List<uint> pluginFinderForbiddenPids_; | |
131 | |
132 /// <summary> | |
133 /// Process searcher class which allows us to query the system for running p
rocesses. | |
134 /// </summary> | |
135 private ProcessSearcher processSearcher_; | |
136 | |
137 /// <summary> | |
138 /// The main process of chrome that was started by Visual Studio during debu
gging. | |
139 /// </summary> | |
140 private System.Diagnostics.Process debuggedChromeMainProcess_; | |
141 | |
142 /// <summary> | |
143 /// Constructs the PluginDebuggerHelper. | |
144 /// Object is not usable until LoadProjectSettings() is called. | |
145 /// </summary> | |
146 /// <param name="dte">Automation object from Visual Studio.</param> | |
147 public PluginDebuggerHelper(DTE2 dte) | |
148 { | |
149 if (dte == null) | |
150 { | |
151 throw new ArgumentNullException("dte"); | |
152 } | |
153 | |
154 dte_ = dte; | |
155 | |
156 // Every second, check for a new instance of the plug-in to attach to. | |
157 // Note that although the timer itself runs on a separate thread, the even
t | |
158 // is fired from the main UI thread during message processing, thus we do
not | |
159 // need to worry about threading issues. | |
160 pluginFinderTimer_ = new Timer(); | |
161 pluginFinderTimer_.Tick += new EventHandler(FindAndAttachToPlugin); | |
162 pluginFinderForbiddenPids_ = new List<uint>(); | |
163 processSearcher_ = new ProcessSearcher(); | |
164 } | |
165 | |
166 /// <summary> | |
167 /// An event indicating a target plug-in was found on the system. | |
168 /// </summary> | |
169 public event EventHandler<PluginFoundEventArgs> PluginFoundEvent; | |
170 | |
171 /// <summary> | |
172 /// Specifies the type of plug-in being run in this debug session. | |
173 /// </summary> | |
174 private enum ProjectPlatformType | |
175 { | |
176 /// <summary> | |
177 /// Represents all non-pepper/non-nacl platform types. | |
178 /// </summary> | |
179 Other, | |
180 | |
181 /// <summary> | |
182 /// Indicates project platform is a trusted plug-in (nexe). | |
183 /// </summary> | |
184 NaCl, | |
185 | |
186 /// <summary> | |
187 /// Indicates project platform is an untrusted plug-in. | |
188 /// </summary> | |
189 Pepper | |
190 } | |
191 | |
192 /// <summary> | |
193 /// Initializes the PluginDebuggerHelper with the current project settings | |
194 /// If project settings are unsupported for NaCl/Pepper debugging then | |
195 /// the object is not initialized and we return false. | |
196 /// </summary> | |
197 /// <returns>True if the object is successfully initialized, false otherwise
.</returns> | |
198 public bool LoadProjectSettings() | |
199 { | |
200 isProperlyInitialized_ = false; | |
201 | |
202 string platformToolset; | |
203 | |
204 // We require that there is only a single start-up project. | |
205 // If multiple start-up projects are specified then we use the first and | |
206 // leave a warning message in the Web Server output pane. | |
207 Array startupProjects = dte_.Solution.SolutionBuild.StartupProjects as Arr
ay; | |
208 if (startupProjects == null || startupProjects.Length == 0) | |
209 { | |
210 throw new ArgumentOutOfRangeException("startupProjects.Length"); | |
211 } | |
212 else if (startupProjects.Length > 1) | |
213 { | |
214 WebServerWriteLine(Strings.WebServerMultiStartProjectWarning); | |
215 } | |
216 | |
217 // Get the first start-up project object. | |
218 List<Project> projList = dte_.Solution.Projects.OfType<Project>().ToList()
; | |
219 string startProjectName = startupProjects.GetValue(0) as string; | |
220 Project startProject = projList.Find(proj => proj.UniqueName == startProje
ctName); | |
221 | |
222 // Get the current platform type. If not nacl/pepper then fail. | |
223 string activePlatform = startProject.ConfigurationManager.ActiveConfigurat
ion.PlatformName; | |
224 if (string.Compare(activePlatform, Strings.PepperPlatformName, true) == 0) | |
225 { | |
226 projectPlatformType_ = ProjectPlatformType.Pepper; | |
227 PluginFoundEvent += new EventHandler<PluginFoundEventArgs>(AttachVSDebug
ger); | |
228 } | |
229 else if (string.Compare(activePlatform, Strings.NaClPlatformName, true) ==
0) | |
230 { | |
231 projectPlatformType_ = ProjectPlatformType.NaCl; | |
232 PluginFoundEvent += new EventHandler<PluginFoundEventArgs>(AttachNaClGDB
); | |
233 } | |
234 else | |
235 { | |
236 projectPlatformType_ = ProjectPlatformType.Other; | |
237 return false; | |
238 } | |
239 | |
240 // We only support certain project types (e.g. C/C++ projects). Otherwise
we fail. | |
241 if (!Utility.IsVisualCProject(startProject)) | |
242 { | |
243 return false; | |
244 } | |
245 | |
246 // Extract necessary information from specific project type. | |
247 VCConfiguration config = Utility.GetActiveVCConfiguration(startProject); | |
248 IVCRulePropertyStorage general = config.Rules.Item("ConfigurationGeneral")
; | |
249 VCLinkerTool linker = config.Tools.Item("VCLinkerTool"); | |
250 VCProject vcproj = (VCProject)startProject.Object; | |
251 | |
252 sdkRootDirectory_ = general.GetEvaluatedPropertyValue("VSNaClSDKRoot"); | |
253 platformToolset = general.GetEvaluatedPropertyValue("PlatformToolset"); | |
254 pluginOutputDirectory_ = config.Evaluate(config.OutputDirectory); | |
255 pluginAssembly_ = config.Evaluate(linker.OutputFile); | |
256 pluginProjectDirectory_ = vcproj.ProjectDirectory; // Macros not allowed
here. | |
257 | |
258 if (projectPlatformType_ == ProjectPlatformType.NaCl) | |
259 { | |
260 irtPath_ = general.GetEvaluatedPropertyValue("NaClIrtPath"); | |
261 manifestPath_ = general.GetEvaluatedPropertyValue("NaClManifestPath"); | |
262 } | |
263 | |
264 if (string.IsNullOrEmpty(sdkRootDirectory_)) | |
265 { | |
266 MessageBox.Show(Strings.SDKPathNotSetError); | |
267 return false; | |
268 } | |
269 | |
270 sdkRootDirectory_ = sdkRootDirectory_.TrimEnd("/\\".ToArray<char>()); | |
271 | |
272 // TODO(tysand): Move this code getting port to where the web server is st
arted. | |
273 int webServerPort; | |
274 if (!int.TryParse(general.GetEvaluatedPropertyValue("NaClWebServerPort"),
out webServerPort)) | |
275 { | |
276 webServerPort = DefaultWebServerPort; | |
277 } | |
278 | |
279 webServerExecutable_ = "python.exe"; | |
280 webServerArguments_ = string.Format( | |
281 "{0}\\examples\\httpd.py --no_dir_check {1}", sdkRootDirectory_, webSe
rverPort); | |
282 | |
283 gdbPath_ = Path.Combine( | |
284 sdkRootDirectory_, "toolchain", platformToolset, @"bin\x86_64-nacl-gdb
.exe"); | |
285 | |
286 debuggedChromeMainProcess_ = null; | |
287 | |
288 isProperlyInitialized_ = true; | |
289 return true; | |
290 } | |
291 | |
292 /// <summary> | |
293 /// This function should be called to start the PluginDebuggerHelper functio
nality. | |
294 /// </summary> | |
295 public void StartDebugging() | |
296 { | |
297 if (!isProperlyInitialized_) | |
298 { | |
299 throw new Exception(Strings.NotInitializedMessage); | |
300 } | |
301 | |
302 StartWebServer(); | |
303 pluginFinderTimer_.Interval = InitialPluginCheckFrequency; | |
304 pluginFinderTimer_.Start(); | |
305 } | |
306 | |
307 /// <summary> | |
308 /// This function should be called to stop the PluginDebuggerHelper function
ality. | |
309 /// </summary> | |
310 public void StopDebugging() | |
311 { | |
312 isProperlyInitialized_ = false; | |
313 pluginFinderTimer_.Stop(); | |
314 pluginFinderForbiddenPids_.Clear(); | |
315 | |
316 // Remove all event handlers from the plug-in found event. | |
317 if (PluginFoundEvent != null) | |
318 { | |
319 foreach (Delegate del in PluginFoundEvent.GetInvocationList()) | |
320 { | |
321 PluginFoundEvent -= (EventHandler<PluginFoundEventArgs>)del; | |
322 } | |
323 } | |
324 | |
325 Utility.EnsureProcessKill(ref webServer_); | |
326 WebServerWriteLine(Strings.WebServerStopMessage); | |
327 CleanUpGDBProcess(); | |
328 } | |
329 | |
330 /// <summary> | |
331 /// This function cleans up the started GDB process. | |
332 /// </summary> | |
333 private void CleanUpGDBProcess() | |
334 { | |
335 Utility.EnsureProcessKill(ref gdbProcess_); | |
336 if (!string.IsNullOrEmpty(gdbInitFileName_) && File.Exists(gdbInitFileName
_)) | |
337 { | |
338 File.Delete(gdbInitFileName_); | |
339 gdbInitFileName_ = null; | |
340 } | |
341 } | |
342 | |
343 /// <summary> | |
344 /// This is called periodically by the Visual Studio UI thread to look for o
ur plug-in process | |
345 /// and attach the debugger to it. The call is triggered by the pluginFinde
rTimer_ object. | |
346 /// </summary> | |
347 /// <param name="unused">The parameter is not used.</param> | |
348 /// <param name="unused1">The parameter is not used.</param> | |
349 private void FindAndAttachToPlugin(object unused, EventArgs unused1) | |
350 { | |
351 StringComparison ignoreCase = StringComparison.InvariantCultureIgnoreCase; | |
352 | |
353 // Set the main chrome process that was started by visual studio. If it's
not chrome | |
354 // or not found then we have no business attaching to any plug-ins so retu
rn. | |
355 if (debuggedChromeMainProcess_ == null) | |
356 { | |
357 foreach (Process proc in dte_.Debugger.DebuggedProcesses) | |
358 { | |
359 if (proc.Name.EndsWith(Strings.ChromeProcessName, ignoreCase)) | |
360 { | |
361 debuggedChromeMainProcess_ = System.Diagnostics.Process.GetProcessBy
Id(proc.ProcessID); | |
362 break; | |
363 } | |
364 } | |
365 | |
366 return; | |
367 } | |
368 | |
369 // Get the list of all descendants of the main chrome process. | |
370 uint mainChromeProcId = (uint)debuggedChromeMainProcess_.Id; | |
371 List<ProcessInfo> chromeDescendants = processSearcher_.GetDescendants(main
ChromeProcId); | |
372 | |
373 // If we didn't start with debug flags then we should not attach. | |
374 string mainChromeFlags = chromeDescendants.Find(p => p.ID == mainChromePro
cId).CommandLine; | |
375 if (projectPlatformType_ == ProjectPlatformType.NaCl && | |
376 !mainChromeFlags.Contains(Strings.NaClDebugFlag)) | |
377 { | |
378 return; | |
379 } | |
380 | |
381 // From the list of descendants, find the plug-in by it's command line arg
uments and | |
382 // process name as well as not being attached to already. | |
383 List<ProcessInfo> plugins; | |
384 switch (projectPlatformType_) | |
385 { | |
386 case ProjectPlatformType.Pepper: | |
387 string identifierFlagTarget = | |
388 string.Format(Strings.PepperProcessPluginFlagFormat, pluginAssembl
y_); | |
389 plugins = chromeDescendants.FindAll(p => | |
390 p.Name.Equals(Strings.ChromeProcessName, ignoreCase) && | |
391 p.CommandLine.Contains(Strings.ChromeRendererFlag, ignoreCase) && | |
392 p.CommandLine.Contains(identifierFlagTarget, ignoreCase) && | |
393 !pluginFinderForbiddenPids_.Contains(p.ID)); | |
394 break; | |
395 case ProjectPlatformType.NaCl: | |
396 plugins = chromeDescendants.FindAll(p => | |
397 p.Name.Equals(Strings.NaClProcessName, ignoreCase) && | |
398 p.CommandLine.Contains(Strings.NaClLoaderFlag, ignoreCase) && | |
399 !pluginFinderForbiddenPids_.Contains(p.ID)); | |
400 break; | |
401 default: | |
402 return; | |
403 } | |
404 | |
405 // Attach to all plug-ins that we found. | |
406 foreach (ProcessInfo process in plugins) | |
407 { | |
408 // If we are attaching to a plug-in, add it to the forbidden list to ens
ure we | |
409 // don't try to attach again later. | |
410 pluginFinderForbiddenPids_.Add(process.ID); | |
411 PluginFoundEvent.Invoke(this, new PluginFoundEventArgs(process.ID)); | |
412 | |
413 // Slow down the frequency of checks for new plugins. | |
414 pluginFinderTimer_.Interval = RelaxedPluginCheckFrequency; | |
415 } | |
416 } | |
417 | |
418 /// <summary> | |
419 /// Attaches the visual studio debugger to a given process ID. | |
420 /// </summary> | |
421 /// <param name="src">The parameter is not used.</param> | |
422 /// <param name="args">Contains the process ID to attach to.</param> | |
423 private void AttachVSDebugger(object src, PluginFoundEventArgs args) | |
424 { | |
425 foreach (EnvDTE.Process proc in dte_.Debugger.LocalProcesses) | |
426 { | |
427 if (proc.ProcessID == args.ProcessID) | |
428 { | |
429 proc.Attach(); | |
430 break; | |
431 } | |
432 } | |
433 } | |
434 | |
435 /// <summary> | |
436 /// Attaches the NaCl GDB debugger to the NaCl plug-in process. Handles loa
ding symbols | |
437 /// and breakpoints from Visual Studio. | |
438 /// </summary> | |
439 /// <param name="src">The parameter is not used.</param> | |
440 /// <param name="args"> | |
441 /// Contains the process ID to attach to, unused since debug stub is already
attached. | |
442 /// </param> | |
443 private void AttachNaClGDB(object src, PluginFoundEventArgs args) | |
444 { | |
445 // Clean up any pre-existing GDB process (can happen if user reloads page)
. | |
446 CleanUpGDBProcess(); | |
447 | |
448 gdbInitFileName_ = Path.GetTempFileName(); | |
449 string pluginAssemblyEscaped = pluginAssembly_.Replace("\\", "\\\\"); | |
450 string irtPathEscaped = irtPath_.Replace("\\", "\\\\"); | |
451 | |
452 // Create the initialization file to read in on GDB start. | |
453 StringBuilder contents = new StringBuilder(); | |
454 | |
455 if (!string.IsNullOrEmpty(manifestPath_)) | |
456 { | |
457 string manifestEscaped = manifestPath_.Replace("\\", "\\\\"); | |
458 contents.AppendFormat("nacl-manifest {0}\n", manifestEscaped); | |
459 } | |
460 else | |
461 { | |
462 contents.AppendFormat("file \"{0}\"\n", pluginAssemblyEscaped); | |
463 } | |
464 | |
465 contents.AppendFormat("nacl-irt {0}\n", irtPathEscaped); | |
466 contents.AppendFormat("target remote localhost:{0}\n", 4014); | |
467 | |
468 // Insert breakpoints from Visual Studio project. | |
469 foreach (Breakpoint bp in dte_.Debugger.Breakpoints) | |
470 { | |
471 if (!bp.Enabled) | |
472 { | |
473 continue; | |
474 } | |
475 | |
476 if (bp.LocationType == dbgBreakpointLocationType.dbgBreakpointLocationTy
peFile) | |
477 { | |
478 contents.AppendFormat("b {0}:{1}\n", Path.GetFileName(bp.File), bp.Fil
eLine); | |
479 } | |
480 else if (bp.LocationType == dbgBreakpointLocationType.dbgBreakpointLocat
ionTypeFunction) | |
481 { | |
482 contents.AppendFormat("b {0}\n", bp.FunctionName); | |
483 } | |
484 else | |
485 { | |
486 WebServerWriteLine( | |
487 string.Format(Strings.UnsupportedBreakpointTypeFormat, bp.LocationTy
pe.ToString())); | |
488 } | |
489 } | |
490 | |
491 contents.AppendLine("continue"); | |
492 File.WriteAllText(gdbInitFileName_, contents.ToString()); | |
493 | |
494 // Start NaCl-GDB. | |
495 try | |
496 { | |
497 gdbProcess_ = new System.Diagnostics.Process(); | |
498 gdbProcess_.StartInfo.UseShellExecute = true; | |
499 gdbProcess_.StartInfo.FileName = gdbPath_; | |
500 gdbProcess_.StartInfo.Arguments = string.Format("-x {0}", gdbInitFileNam
e_); | |
501 gdbProcess_.StartInfo.WorkingDirectory = pluginProjectDirectory_; | |
502 gdbProcess_.Start(); | |
503 } | |
504 catch (Exception e) | |
505 { | |
506 MessageBox.Show( | |
507 string.Format("NaCl-GDB Start Failed. {0}. Path: {1}", e.Message, gd
bPath_)); | |
508 } | |
509 } | |
510 | |
511 /// <summary> | |
512 /// Spins up the web server process to host our plug-in. | |
513 /// </summary> | |
514 private void StartWebServer() | |
515 { | |
516 // Add a panel to the output window which is used to capture output | |
517 // from the web server hosting the plugin. | |
518 if (webServerOutputPane_ == null) | |
519 { | |
520 webServerOutputPane_ = dte_.ToolWindows.OutputWindow.OutputWindowPanes.A
dd( | |
521 Strings.WebServerOutputWindowTitle); | |
522 } | |
523 | |
524 webServerOutputPane_.Clear(); | |
525 WebServerWriteLine(Strings.WebServerStartMessage); | |
526 | |
527 try | |
528 { | |
529 webServer_ = new System.Diagnostics.Process(); | |
530 webServer_.StartInfo.CreateNoWindow = true; | |
531 webServer_.StartInfo.UseShellExecute = false; | |
532 webServer_.StartInfo.RedirectStandardOutput = true; | |
533 webServer_.StartInfo.RedirectStandardError = true; | |
534 webServer_.StartInfo.FileName = webServerExecutable_; | |
535 webServer_.StartInfo.Arguments = webServerArguments_; | |
536 webServer_.StartInfo.WorkingDirectory = pluginProjectDirectory_; | |
537 webServer_.OutputDataReceived += WebServerMessageReceive; | |
538 webServer_.ErrorDataReceived += WebServerMessageReceive; | |
539 webServer_.Start(); | |
540 webServer_.BeginOutputReadLine(); | |
541 webServer_.BeginErrorReadLine(); | |
542 } | |
543 catch (Exception e) | |
544 { | |
545 WebServerWriteLine(Strings.WebServerStartFail); | |
546 WebServerWriteLine("Exception: " + e.Message); | |
547 } | |
548 } | |
549 | |
550 /// <summary> | |
551 /// Receives output from the web server process to display in the Visual Stu
dio UI. | |
552 /// </summary> | |
553 /// <param name="sender">The parameter is not used.</param> | |
554 /// <param name="e">Contains the data to display.</param> | |
555 private void WebServerMessageReceive(object sender, System.Diagnostics.DataR
eceivedEventArgs e) | |
556 { | |
557 WebServerWriteLine(e.Data); | |
558 } | |
559 | |
560 /// <summary> | |
561 /// Helper function to write data to the Web Server Output Pane. | |
562 /// </summary> | |
563 /// <param name="message">Message to write.</param> | |
564 private void WebServerWriteLine(string message) | |
565 { | |
566 if (webServerOutputPane_ != null) | |
567 { | |
568 webServerOutputPane_.OutputString(message + "\n"); | |
569 } | |
570 } | |
571 | |
572 /// <summary> | |
573 /// The event arguments when a plug-in is found. | |
574 /// </summary> | |
575 public class PluginFoundEventArgs : EventArgs | |
576 { | |
577 /// <summary> | |
578 /// Construct the PluginFoundEventArgs. | |
579 /// </summary> | |
580 /// <param name="pid">Process ID of the found plug-in.</param> | |
581 public PluginFoundEventArgs(uint pid) | |
582 { | |
583 this.ProcessID = pid; | |
584 } | |
585 | |
586 /// <summary> | |
587 /// Gets or sets process ID of the found plug-in. | |
588 /// </summary> | |
589 public uint ProcessID { get; set; } | |
590 } | |
591 } | |
592 } | |
OLD | NEW |