PageRenderTime 249ms CodeModel.GetById 27ms RepoModel.GetById 1ms app.codeStats 0ms

/tests/TestUtilities/VsIdeHostAdapter.cs

https://github.com/jeroldhaas/VisualFSharpPowerTools
C# | 1241 lines | 793 code | 150 blank | 298 comment | 99 complexity | 3a660e262f0a7ce2b23970379fd641ee MD5 | raw file
Possible License(s): Apache-2.0
  1. /* ****************************************************************************
  2. *
  3. * Copyright (c) Microsoft Corporation.
  4. *
  5. * This source code is subject to terms and conditions of the Apache License, Version 2.0. A
  6. * copy of the license can be found in the License.html file at the root of this distribution. If
  7. * you cannot locate the Apache License, Version 2.0, please send an email to
  8. * vspython@microsoft.com. By using this source code in any fashion, you are agreeing to be bound
  9. * by the terms of the Apache License, Version 2.0.
  10. *
  11. * You must not remove this notice, or any other, from this software.
  12. *
  13. * ***************************************************************************/
  14. using System;
  15. using System.Collections;
  16. using System.Collections.Generic;
  17. using System.Diagnostics;
  18. using System.Diagnostics.Contracts;
  19. using System.Globalization;
  20. using System.IO;
  21. using System.Runtime.InteropServices;
  22. using System.Runtime.Remoting.Channels;
  23. using System.Runtime.Remoting.Channels.Ipc;
  24. using System.Runtime.Serialization.Formatters;
  25. using System.Security.Principal;
  26. using System.Text.RegularExpressions;
  27. using System.Xml;
  28. using EnvDTE;
  29. using Microsoft.TC.TestHostAdapters;
  30. using Microsoft.VisualStudio.OLE.Interop;
  31. using Microsoft.VisualStudio.TestTools.Common;
  32. using Microsoft.VisualStudio.TestTools.Common.Xml;
  33. using Microsoft.VisualStudio.TestTools.Execution;
  34. using Microsoft.VisualStudio.TestTools.TestAdapter;
  35. using Microsoft.Win32;
  36. using IBindCtx = Microsoft.VisualStudio.OLE.Interop.IBindCtx;
  37. using IEnumMoniker = Microsoft.VisualStudio.OLE.Interop.IEnumMoniker;
  38. using IMessageFilter = Microsoft.VisualStudio.OLE.Interop.IMessageFilter;
  39. using IMoniker = Microsoft.VisualStudio.OLE.Interop.IMoniker;
  40. using IRunningObjectTable = Microsoft.VisualStudio.OLE.Interop.IRunningObjectTable;
  41. using Process = System.Diagnostics.Process;
  42. namespace TestUtilities
  43. {
  44. /// <summary>
  45. /// Vs Ide Host Adapter: Agent side.
  46. /// This wraps ITestAdapter and looks like ITestAdapter for the Agent.
  47. /// Internally it delegates to original test adapter hosted by Visual Studio IDE.
  48. /// </summary>
  49. public class VsIdeHostAdapter : ITestAdapter
  50. {
  51. public const string DynamicHostAdapterName = "TC Dynamic";
  52. public const string VsAddinName = "TcVsIdeTestHost";
  53. private IRunContext _runContext;
  54. private TestRunConfiguration _runConfig;
  55. private string _workingDir;
  56. private ITestAdapter _hostSide;
  57. private object _hostSideLock = new object();
  58. private IVsIdeTestHostAddin _testHostAddin;
  59. private IChannel _clientChannel;
  60. private IChannel _serverChannel;
  61. private VisualStudioIde _vsIde;
  62. private string _vsRegistryHive; // This is like 10.0 or 10.0Exp.
  63. private RetryMessageFilter _comMessageFilter;
  64. private static readonly TimeSpan _ideStartupTimeout = TimeSpan.FromMinutes(2);
  65. private static readonly TimeSpan _addinWaitTimeout = TimeSpan.FromMinutes(1);
  66. private static readonly TimeSpan _debuggerTimeout = TimeSpan.FromMinutes(3);
  67. private static readonly TimeSpan _baseSleepDuration = TimeSpan.FromMilliseconds(250);
  68. private static readonly TimeSpan _baseSleepDoubleDuration = TimeSpan.FromMilliseconds(500);
  69. /// <summary>
  70. /// Constructor. Called by Agent via Activator.CreateInstance and should not have any parameters.
  71. /// </summary>
  72. public VsIdeHostAdapter()
  73. {
  74. }
  75. /// <summary>
  76. /// The Host Side of Vs Ide Host Adapter.
  77. /// </summary>
  78. private ITestAdapter HostSide
  79. {
  80. get
  81. {
  82. return _hostSide;
  83. }
  84. }
  85. /// <summary>
  86. /// ITestAdapter method: called to initialize run context for this adapter.
  87. /// </summary>
  88. /// <param name="runContext">The run context to be used for this run</param>
  89. void ITestAdapter.Initialize(IRunContext runContext)
  90. {
  91. Contract.Assert(runContext != null);
  92. _runContext = runContext;
  93. _runConfig = _runContext.RunConfig.TestRun.RunConfiguration;
  94. _workingDir = _runContext.RunContextVariables.GetStringValue("TestDeploymentDir");
  95. Contract.Assert(_runConfig != null);
  96. Contract.Assert(!string.IsNullOrEmpty(_workingDir));
  97. SetupChannels();
  98. // Install COM message filter to retry COM calls when VS IDE is busy, e.g. when getting the addin from VS IDE.
  99. // This prevents RPC_E_CALL_REJECTED error when VS IDE is busy.
  100. _comMessageFilter = new RetryMessageFilter();
  101. InitHostSide();
  102. }
  103. /// <summary>
  104. /// IBaseAdapter method: called to execute a test.
  105. /// </summary>
  106. /// <param name="testElement">The test object to run</param>
  107. /// <param name="testContext">The Test conext for this test invocation</param>
  108. void IBaseAdapter.Run(ITestElement testElement, ITestContext testContext)
  109. {
  110. if (testElement.TestCategories.Contains(new TestCategoryItem("RestartVS"))) {
  111. CleanupHostSide();
  112. InitHostSide();
  113. }
  114. _hostSide.Run(testElement, testContext);
  115. }
  116. /// <summary>
  117. /// IBaseAdapter method: called when the test run is complete.
  118. /// </summary>
  119. void IBaseAdapter.Cleanup()
  120. {
  121. try
  122. {
  123. CleanupHostSide();
  124. // Uninstall COM message filter.
  125. _comMessageFilter.Dispose();
  126. }
  127. finally
  128. {
  129. CleanupChannels();
  130. }
  131. }
  132. /// <summary>
  133. /// IBaseAdapter method: called when the user stops the test run.
  134. /// </summary>
  135. void IBaseAdapter.StopTestRun()
  136. {
  137. HostSide.StopTestRun();
  138. }
  139. /// <summary>
  140. /// IBaseAdapter method: called when the test run is aborted.
  141. /// </summary>
  142. void IBaseAdapter.AbortTestRun()
  143. {
  144. HostSide.AbortTestRun();
  145. }
  146. /// <summary>
  147. /// IBaseAdapter method: called when the user pauses the test run.
  148. /// </summary>
  149. void IBaseAdapter.PauseTestRun()
  150. {
  151. HostSide.PauseTestRun();
  152. }
  153. /// <summary>
  154. /// IBaseAdapter method: called when the user resumes a paused test run.
  155. /// </summary>
  156. void IBaseAdapter.ResumeTestRun()
  157. {
  158. HostSide.ResumeTestRun();
  159. }
  160. /// <summary>
  161. /// ITestAdapter method: called when a message is sent from the UI or the controller.
  162. /// </summary>
  163. /// <param name="obj">The message object</param>
  164. void ITestAdapter.ReceiveMessage(object obj)
  165. {
  166. HostSide.ReceiveMessage(obj);
  167. }
  168. /// <summary>
  169. /// ITestAdapter method: called just before the test run finishes and
  170. /// gives the adapter a chance to do any clean-up.
  171. /// </summary>
  172. /// <param name="runContext">The run context for this run</param>
  173. void ITestAdapter.PreTestRunFinished(IRunContext runContext)
  174. {
  175. HostSide.PreTestRunFinished(runContext);
  176. }
  177. /// <summary>
  178. /// Creates VS IDE and HostSide inside it.
  179. /// This can be called multiple times in the run if specified to restart VS between tests.
  180. /// </summary>
  181. private void InitHostSide()
  182. {
  183. Contract.Assert(_runContext != null);
  184. try
  185. {
  186. _vsRegistryHive = GetRegistryHive();
  187. lock (_hostSideLock)
  188. {
  189. // Get the "host side" of the host adapter.
  190. CreateHostSide();
  191. // Call Initialize for the host side.
  192. ((ITestAdapter)HostSide).Initialize(_runContext);
  193. // If test run was started under debugger, attach debugger.
  194. CheckAttachDebugger();
  195. }
  196. }
  197. catch (Exception ex)
  198. {
  199. // Report the error to the agent.
  200. SendResult(string.Format(CultureInfo.InvariantCulture, "VsIdeHostAdapter: Failed to initialize the host side: {0}", ex.ToString()), TestOutcome.Error, true);
  201. throw;
  202. }
  203. }
  204. /// <summary>
  205. /// Determine which registry hive to use for Visual Studio:
  206. /// If using RunConfig, get it from RunConfig
  207. /// Else get it from environment.
  208. /// </summary>
  209. /// <returns></returns>
  210. private string GetRegistryHive()
  211. {
  212. // Note that Run Config Data can be null, e.g. when executing using HostType attribute.
  213. TestRunConfiguration runConfig = _runContext.RunConfig.TestRun.RunConfiguration;
  214. string configKey;
  215. if (TryGetRegistryHiveFromConfig(runConfig, out configKey))
  216. {
  217. return configKey;
  218. }
  219. if (System.Environment.GetEnvironmentVariable("RUN_NO_EXP") != null)
  220. return VSUtility.Version;
  221. // Default to the experimental hive for the development evnironment.
  222. return VSUtility.Version + "Exp";
  223. }
  224. /// <summary>
  225. /// Starts new Visual Studio process and obtains host side from it.
  226. /// </summary>
  227. private void CreateHostSide()
  228. {
  229. Contract.Assert(!string.IsNullOrEmpty(_workingDir));
  230. Contract.Assert(_hostSide == null);
  231. Contract.Assert(_vsIde == null);
  232. // Start devenv.
  233. _vsIde = new VisualStudioIde(new VsIdeStartupInfo(_vsRegistryHive, _workingDir));
  234. _vsIde.ErrorHandler += HostProcessErrorHandler;
  235. Stopwatch timer = Stopwatch.StartNew();
  236. do
  237. {
  238. try
  239. {
  240. _vsIde.Dte.MainWindow.Visible = true; // This could be in TestRunConfig options for this host type.
  241. break;
  242. }
  243. catch (Exception)
  244. {
  245. }
  246. System.Threading.Thread.Sleep(_baseSleepDuration);
  247. } while (timer.Elapsed < _ideStartupTimeout);
  248. _hostSide = GetHostSideFromAddin();
  249. }
  250. /// <summary>
  251. /// Obtain host side from the addin.
  252. /// </summary>
  253. /// <returns></returns>
  254. private ITestAdapter GetHostSideFromAddin()
  255. {
  256. // Find the Addin.
  257. // Note: After VS starts addin needs some time to load, so we try a few times.
  258. AddIn addinLookingFor = null;
  259. Stopwatch timer = Stopwatch.StartNew();
  260. try
  261. {
  262. do
  263. {
  264. try
  265. {
  266. // There is no index-by-name API, so we have to check all addins.
  267. foreach (AddIn addin in _vsIde.Dte.AddIns)
  268. {
  269. if (addin.Name.StartsWith(VsAddinName))
  270. {
  271. addinLookingFor = addin;
  272. break;
  273. }
  274. }
  275. }
  276. catch (Exception)
  277. {
  278. // Catch all exceptions to prevent intermittent failures such as COMException (0x8001010A)
  279. // while VS has not started yet. Just retry again until we timeout.
  280. }
  281. if (addinLookingFor != null)
  282. {
  283. break;
  284. }
  285. System.Threading.Thread.Sleep(_baseSleepDuration);
  286. } while (timer.Elapsed < _addinWaitTimeout);
  287. }
  288. finally
  289. {
  290. timer.Stop();
  291. }
  292. if (addinLookingFor == null)
  293. {
  294. throw new VsIdeTestHostException("Timed out getting Vs Ide Test Host Add-in from Visual Studio. Please make sure that the Add-in is installed and started when VS starts (use Tools->Add-in Manager).");
  295. }
  296. _testHostAddin = (IVsIdeTestHostAddin)addinLookingFor.Object;
  297. ITestAdapter hostSide = _testHostAddin.GetHostSide();
  298. Contract.Assert(hostSide != null);
  299. return hostSide;
  300. }
  301. /// <summary>
  302. /// Check if we need to attach debugger and attach it.
  303. /// </summary>
  304. private void CheckAttachDebugger()
  305. {
  306. Contract.Assert(_runConfig != null);
  307. if (_runConfig.IsExecutedUnderDebugger)
  308. {
  309. Contract.Assert(_vsIde != null);
  310. DebugTargetInfo debugInfo = new DebugTargetInfo();
  311. debugInfo.ProcessId = _vsIde.Process.Id;
  312. ExecutionUtilities.DebugTarget(_runContext.ResultSink, _runContext.RunConfig.TestRun.Id, debugInfo, _debuggerTimeout);
  313. }
  314. }
  315. /// <summary>
  316. /// Clean up the host side.
  317. /// We do the following steps. Each step is important and must be done whenever previous step throws or not.
  318. /// - Call HostSide.Cleanup.
  319. /// - Ask Runner IDE to detach debugger (if attached).
  320. /// - Dispose m_vsIde.
  321. /// This should not throw.
  322. /// </summary>
  323. private void CleanupHostSide()
  324. {
  325. lock (_hostSideLock)
  326. {
  327. Contract.Assert(HostSide != null);
  328. if (HostSide != null)
  329. {
  330. try
  331. {
  332. HostSide.Cleanup();
  333. }
  334. catch (Exception ex) // We don't know what this can throw in advance.
  335. {
  336. SendResult(string.Format(CultureInfo.InvariantCulture, "Warning: VsIdeHostAdapter failed to call ITestAdapter.Cleanup: {0}", ex), TestOutcome.Warning);
  337. }
  338. }
  339. try
  340. {
  341. Contract.Assert(_vsIde != null);
  342. _vsIde.Dispose();
  343. }
  344. catch (InvalidComObjectException)
  345. {
  346. // This exception is always thrown when quitting VS. Nothing
  347. // is gained by reporting it for every run.
  348. }
  349. catch (Exception ex)
  350. {
  351. SendResult(string.Format(CultureInfo.InvariantCulture, "Warning: VsIdeHostAdapter: error shutting down VS IDE: {0}", ex), TestOutcome.Warning);
  352. }
  353. _vsIde = null;
  354. _hostSide = null; // Note: host side lifetime is controlled by the addin.
  355. }
  356. }
  357. /// <summary>
  358. /// Set up remoting communication channels.
  359. /// </summary>
  360. private void SetupChannels()
  361. {
  362. string channelPrefix = "EqtVsIdeHostAdapter_" + Guid.NewGuid().ToString();
  363. // Server channel is required for callbacks from client side.
  364. // Actually it is not required when running from vstesthost as vstesthost already sets up the channels
  365. // but since we have /noisolation (~ no vstesthost) mode we need to create this channel.
  366. BinaryServerFormatterSinkProvider serverProvider = new BinaryServerFormatterSinkProvider();
  367. serverProvider.TypeFilterLevel = TypeFilterLevel.Full; // Enable remoting objects as arguments.
  368. Hashtable props = new Hashtable();
  369. string serverChannelName = channelPrefix + "_ServerChannel";
  370. props["name"] = serverChannelName;
  371. props["portName"] = serverChannelName; // Must be different from client's port.
  372. // Default IpcChannel security is: allow for all users who can authorize on this machine.
  373. props["authorizedGroup"] = WindowsIdentity.GetCurrent().Name;
  374. _serverChannel = new IpcServerChannel(props, serverProvider);
  375. ChannelServices.RegisterChannel(_serverChannel, false);
  376. _clientChannel = new IpcClientChannel(channelPrefix + "_ClientChannel", new BinaryClientFormatterSinkProvider());
  377. ChannelServices.RegisterChannel(_clientChannel, false);
  378. }
  379. /// <summary>
  380. /// Clean up remoting communication channels.
  381. /// </summary>
  382. private void CleanupChannels()
  383. {
  384. if (_clientChannel != null)
  385. {
  386. ChannelServices.UnregisterChannel(_clientChannel);
  387. _clientChannel = null;
  388. }
  389. if (_serverChannel != null)
  390. {
  391. ChannelServices.UnregisterChannel(_serverChannel);
  392. _serverChannel = null;
  393. }
  394. }
  395. /// <summary>
  396. /// Error handler for the Visual Studio process exited event.
  397. /// </summary>
  398. /// <param name="errorMessage">The error message.</param>
  399. /// <param name="outcome">The outcome for the test.</param>
  400. /// <param name="abortTestRun">Whether test run needs to be aborted.</param>
  401. private void HostProcessErrorHandler(string errorMessage, TestOutcome outcome, bool abortTestRun)
  402. {
  403. Contract.Assert(_runContext != null);
  404. SendResult(errorMessage, outcome, abortTestRun);
  405. }
  406. /// <summary>
  407. /// Helper method to send message to the Agent.
  408. /// </summary>
  409. /// <param name="messageText">The text for the message.</param>
  410. /// <param name="outcome">The outcome for the test.</param>
  411. private void SendResult(string messageText, TestOutcome outcome)
  412. {
  413. SendResult(messageText, outcome, false);
  414. }
  415. /// <summary>
  416. /// Sends run level message to the result sink.
  417. /// </summary>
  418. /// <param name="message">Text for the message.</param>
  419. /// <param name="outcome">Outcome for the message. Affects test run outcome.</param>
  420. /// <param name="abortTestRun">If true, we use TMK.Panic, otherwise TMK.TextMessage</param>
  421. private void SendResult(string messageText, TestOutcome outcome, bool abortTestRun)
  422. {
  423. Contract.Assert(!abortTestRun || outcome == TestOutcome.Error,
  424. "SendResult: When abortTestRun = true, Outcome should be Error.");
  425. TestRunTextResultMessage message = new TestRunTextResultMessage(
  426. Environment.MachineName,
  427. _runContext.RunConfig.TestRun.Id,
  428. messageText,
  429. TestMessageKind.TextMessage);
  430. message.Outcome = outcome;
  431. _runContext.ResultSink.AddResult(message);
  432. if (abortTestRun)
  433. {
  434. _runContext.ResultSink.AddResult(new RunStateEvent(_runContext.RunConfig.TestRun.Id,
  435. RunState.Aborting,
  436. Environment.MachineName));
  437. }
  438. }
  439. private bool TryGetRegistryHiveFromConfig(TestRunConfiguration runConfig, out string hive)
  440. {
  441. const string VSHiveElement = "VSHive";
  442. hive = null;
  443. if (runConfig == null)
  444. {
  445. return false;
  446. }
  447. string configFileName = runConfig.Storage;
  448. if (string.IsNullOrEmpty(configFileName))
  449. {
  450. return false;
  451. }
  452. if (!File.Exists(configFileName))
  453. {
  454. // This will happen in the case with no file where a default file is created.
  455. if (!configFileName.StartsWith("default", StringComparison.OrdinalIgnoreCase))
  456. {
  457. SendResult(string.Format(CultureInfo.InvariantCulture, "VsIdeHostAdapter: Unable to find config file: {0}", configFileName), TestOutcome.Warning, false);
  458. }
  459. return false;
  460. }
  461. try
  462. {
  463. using (var configXml = new XmlTextReader(configFileName))
  464. {
  465. while (configXml.Read())
  466. {
  467. if (configXml.NodeType == XmlNodeType.Element && configXml.Name == VSHiveElement)
  468. {
  469. configXml.Read();
  470. if (configXml.NodeType == XmlNodeType.Text)
  471. {
  472. hive = configXml.Value;
  473. return true;
  474. }
  475. }
  476. }
  477. }
  478. }
  479. catch (Exception ex)
  480. {
  481. // Report the error to the agent.
  482. SendResult(string.Format(CultureInfo.InvariantCulture, "VsIdeHostAdapter: Error reading config file: {0}", ex.ToString()), TestOutcome.Warning, false);
  483. }
  484. return false;
  485. }
  486. /// <summary>
  487. /// The information to start Visual Studio.
  488. /// </summary>
  489. internal class VsIdeStartupInfo
  490. {
  491. private string _registryHive;
  492. private string _workingDirectory;
  493. internal VsIdeStartupInfo(string registryHive, string workingDirectory)
  494. {
  495. // Note: registryHive can be null when using the attribute. That's OK, VsIde will figure out.
  496. Contract.Assert(!string.IsNullOrEmpty(workingDirectory));
  497. _registryHive = registryHive;
  498. _workingDirectory = workingDirectory;
  499. }
  500. /// <summary>
  501. /// Hive name under Microsoft.VisualStudio, like 10.0Exp.
  502. /// </summary>
  503. internal string RegistryHive
  504. {
  505. get { return _registryHive; }
  506. set { _registryHive = value; }
  507. }
  508. /// <summary>
  509. /// Working directory for devenv.exe process.
  510. /// </summary>
  511. internal string WorkingDirectory
  512. {
  513. get { return _workingDirectory; }
  514. }
  515. }
  516. /// <summary>
  517. /// This wraps Visual Studio DTE (automation object).
  518. /// </summary>
  519. internal class VisualStudioIde : IDisposable
  520. {
  521. /// <summary>
  522. /// Used for error reporting.
  523. /// </summary>
  524. /// <param name="errorMessage">Error message.</param>
  525. /// <param name="outcome">The outcome for the test due to this error.</param>
  526. /// <param name="abortTestRun">Whether the error causes test run to abort.</param>
  527. internal delegate void VsIdeHostErrorHandler(string errorMessage, TestOutcome outcome, bool abortTestRun);
  528. private const string BaseProgId = "VisualStudio.DTE";
  529. // HRESULTs for COM errors.
  530. private const int CallRejectedByCalleeErrorCode = -2147418111;
  531. /// <summary>
  532. /// Time to wait for the VS process to exit after resetting it's profile.
  533. /// </summary>
  534. private static readonly TimeSpan _ideFirstRunTimeout = TimeSpan.FromMinutes(2);
  535. /// <summary>
  536. /// How long to wait for IDE to appear in ROT.
  537. /// </summary>
  538. private static readonly TimeSpan _ideStartupTimeout = TimeSpan.FromMinutes(2);
  539. /// <summary>
  540. /// How long to wait before killing devenv.exe after Dispose() is called. During this time VS can e.g. save buffers to disk.
  541. /// </summary>
  542. private static readonly TimeSpan _ideExitTimeout = TimeSpan.FromSeconds(5);
  543. /// <summary>
  544. /// Timeout to wait while VS rejects calls.
  545. /// </summary>
  546. private static readonly TimeSpan _rejectedCallTimeout = TimeSpan.FromSeconds(30);
  547. private DTE _dte;
  548. private Process _process;
  549. private object _cleanupLock = new object();
  550. /// <summary>
  551. /// Constructor. Starts new instance of VS IDE.
  552. /// </summary>
  553. public VisualStudioIde(VsIdeStartupInfo info)
  554. {
  555. Contract.Assert(info != null);
  556. if (string.IsNullOrEmpty(info.RegistryHive))
  557. {
  558. info.RegistryHive = VsRegistry.GetDefaultVersion();
  559. if (string.IsNullOrEmpty(info.RegistryHive))
  560. {
  561. throw new VsIdeTestHostException(string.Format(CultureInfo.InvariantCulture, "Cannot find installation of Visual Studio in '{0}' registry hive.", info.RegistryHive));
  562. }
  563. }
  564. StartNewInstance(info);
  565. }
  566. /// <summary>
  567. /// Finalizer.
  568. /// </summary>
  569. ~VisualStudioIde()
  570. {
  571. Dispose(false);
  572. }
  573. public DTE Dte
  574. {
  575. get { return _dte; }
  576. }
  577. public Process Process
  578. {
  579. get { return _process; }
  580. }
  581. public event VsIdeHostErrorHandler ErrorHandler;
  582. private static void ResetVSSettings(string vsPath, string hiveSuffix)
  583. {
  584. Process process = new Process();
  585. process.StartInfo.UseShellExecute = false;
  586. process.StartInfo.FileName = vsPath;
  587. process.StartInfo.Arguments = "/resetsettings \"General.vssettings\" /Command \"File.Exit\"";
  588. if (!string.IsNullOrEmpty(hiveSuffix))
  589. {
  590. process.StartInfo.Arguments += " /RootSuffix " + hiveSuffix;
  591. }
  592. // Launch VS for the "first time"
  593. if (!process.Start())
  594. {
  595. throw new VsIdeTestHostException("Failed to start Visual Studio process.");
  596. }
  597. // Wait for the process to exit
  598. if (!process.WaitForExit((int)_ideFirstRunTimeout.TotalMilliseconds))
  599. {
  600. // Kill the process and raise an exception
  601. process.Kill();
  602. string message = string.Format(
  603. CultureInfo.InvariantCulture,
  604. "First run Visual Studio process, used for resetting settings did not exit after {0} seconds.",
  605. _ideFirstRunTimeout.TotalSeconds);
  606. throw new TimeoutException(message);
  607. }
  608. }
  609. /// <summary>
  610. /// Create a Visual Studio process.
  611. /// </summary>
  612. /// <param name="info">Startup information.</param>
  613. private void StartNewInstance(VsIdeStartupInfo startupInfo)
  614. {
  615. Contract.Assert(startupInfo != null);
  616. Contract.Assert(_process == null, "VisualStudioIde.StartNewInstance: _process should be null!");
  617. Process process = new Process();
  618. process.StartInfo.UseShellExecute = false;
  619. if (startupInfo.WorkingDirectory != null)
  620. {
  621. process.StartInfo.WorkingDirectory = startupInfo.WorkingDirectory;
  622. }
  623. // Note that this needs to be partial (not $-terminated) as we partially match/replace.
  624. Regex versionRegex = new Regex(@"^[0-9]+\.[0-9]+");
  625. string hiveVersion = versionRegex.Match(startupInfo.RegistryHive).Value;
  626. string hiveSuffix = versionRegex.Replace(startupInfo.RegistryHive, string.Empty);
  627. if (!string.IsNullOrEmpty(hiveSuffix)) {
  628. process.StartInfo.Arguments = "/RootSuffix " + hiveSuffix + " /Log";
  629. } else {
  630. process.StartInfo.Arguments = "/Log";
  631. }
  632. process.StartInfo.FileName = VsRegistry.GetVsLocation(hiveVersion);
  633. Contract.Assert(!string.IsNullOrEmpty(process.StartInfo.FileName));
  634. // Prevent the settings popup on startup
  635. if (!VsRegistry.UserSettingsArchiveExists(startupInfo.RegistryHive))
  636. {
  637. ResetVSSettings(process.StartInfo.FileName, hiveSuffix);
  638. if (!VsRegistry.UserSettingsArchiveExists(startupInfo.RegistryHive))
  639. {
  640. throw new VsIdeTestHostException("Unable to reset VS settings.");
  641. }
  642. }
  643. process.Exited += new EventHandler(ProcessExited);
  644. process.EnableRaisingEvents = true;
  645. if (!process.Start())
  646. {
  647. throw new VsIdeTestHostException("Failed to start Visual Studio process.");
  648. }
  649. _process = process;
  650. string progId = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", VisualStudioIde.BaseProgId, hiveVersion);
  651. _dte = GetDteFromRot(progId, _process.Id);
  652. if (_dte == null)
  653. {
  654. throw new VsIdeTestHostException("Failed to get the DTE object from Visual Studio process. Please make sure that the Add-in is registered in Tools->Add-in Manager to load in when Visual Studio starts.");
  655. }
  656. }
  657. /// <summary>
  658. /// Obtains Visual Studio automation object from Running Object Table.
  659. /// </summary>
  660. /// <param name="progId">DTE's prog id.</param>
  661. /// <param name="processId">Visual Studio process id to obtain the automation object for.</param>
  662. /// <returns>Visual Studio automation object.</returns>
  663. public static DTE GetDteFromRot(string progId, int processId)
  664. {
  665. Contract.Assert(!string.IsNullOrEmpty(progId));
  666. EnvDTE.DTE dte;
  667. string moniker = string.Format(CultureInfo.InvariantCulture, "!{0}:{1}", progId, processId);
  668. // It takes some time after process started to register in ROT.
  669. Stopwatch sw = Stopwatch.StartNew();
  670. do
  671. {
  672. dte = GetDteFromRot(moniker);
  673. if (dte != null)
  674. {
  675. break;
  676. }
  677. System.Threading.Thread.Sleep(_baseSleepDoubleDuration);
  678. } while (sw.Elapsed < _ideStartupTimeout);
  679. if (dte == null)
  680. {
  681. throw new VsIdeTestHostException("Timed out getting VS.DTE from COM Running Object Table.");
  682. }
  683. return dte;
  684. }
  685. /// <summary>
  686. /// Obtains Visual Studio automation object from Running Object Table.
  687. /// </summary>
  688. /// <param name="monikerName">The moniker to use as a filter when looking in Running Object Table.</param>
  689. /// <returns></returns>
  690. private static DTE GetDteFromRot(string monikerName)
  691. {
  692. Contract.Assert(!string.IsNullOrEmpty(monikerName));
  693. IRunningObjectTable rot;
  694. IEnumMoniker monikerEnumerator;
  695. object dte = null;
  696. try
  697. {
  698. NativeMethods.GetRunningObjectTable(0, out rot);
  699. rot.EnumRunning(out monikerEnumerator);
  700. monikerEnumerator.Reset();
  701. uint fetched = 0;
  702. IMoniker[] moniker = new IMoniker[1];
  703. while (monikerEnumerator.Next(1, moniker, out fetched) == 0)
  704. {
  705. IBindCtx bindingContext;
  706. NativeMethods.CreateBindCtx(0, out bindingContext);
  707. string name;
  708. moniker[0].GetDisplayName(bindingContext, null, out name);
  709. if (name == monikerName)
  710. {
  711. object returnObject;
  712. rot.GetObject(moniker[0], out returnObject);
  713. dte = (object)returnObject;
  714. break;
  715. }
  716. }
  717. }
  718. catch
  719. {
  720. return null;
  721. }
  722. return (DTE)dte;
  723. }
  724. /// <summary>
  725. /// Called when Visual Studio process exits.
  726. /// </summary>
  727. /// <param name="sender">The sender of the event.</param>
  728. /// <param name="args">Event arguments.</param>
  729. private void ProcessExited(object sender, EventArgs args)
  730. {
  731. lock (_cleanupLock)
  732. {
  733. _process.EnableRaisingEvents = false;
  734. _process.Exited -= new EventHandler(ProcessExited);
  735. if (ErrorHandler != null)
  736. {
  737. ErrorHandler("Visual Studio process exited unexpectedly.", TestOutcome.Error, true);
  738. }
  739. }
  740. }
  741. /// <summary>
  742. /// Implements Idisposable.Dispose.
  743. /// </summary>
  744. public void Dispose()
  745. {
  746. Dispose(true);
  747. GC.SuppressFinalize(this);
  748. }
  749. /// <summary>
  750. /// The other part of the .NET Dispose pattern.
  751. /// </summary>
  752. /// <param name="disposingNotFinalizing">Whether this is explicit dispose, not auto-finalization of the object.</param>
  753. private void Dispose(bool explicitDispose)
  754. {
  755. if (!explicitDispose)
  756. {
  757. // When called from finalizer, just clean up. Don't lock. Don't throw.
  758. KillProcess();
  759. }
  760. else
  761. {
  762. lock (_cleanupLock)
  763. {
  764. if (_process.EnableRaisingEvents)
  765. {
  766. _process.EnableRaisingEvents = false;
  767. _process.Exited -= new EventHandler(ProcessExited);
  768. }
  769. try
  770. {
  771. if (_dte != null)
  772. {
  773. // Visual Studio sometimes rejects the call to Quit() so we need to retry it.
  774. Stopwatch sw = Stopwatch.StartNew();
  775. bool timedOut = true;
  776. do
  777. {
  778. try
  779. {
  780. _dte.Quit();
  781. timedOut = false;
  782. break;
  783. }
  784. catch (COMException ex)
  785. {
  786. if (ex.ErrorCode == CallRejectedByCalleeErrorCode)
  787. {
  788. System.Threading.Thread.Sleep(_baseSleepDoubleDuration);
  789. }
  790. else
  791. {
  792. throw;
  793. }
  794. }
  795. catch (Exception)
  796. {
  797. throw;
  798. }
  799. } while (sw.Elapsed < _rejectedCallTimeout);
  800. if (timedOut)
  801. {
  802. throw new VsIdeTestHostException("Warning: timed out calling Dte.Quit: all the calls were rejected. Visual Studio process will be killed.");
  803. }
  804. }
  805. }
  806. finally
  807. {
  808. KillProcess();
  809. }
  810. }
  811. }
  812. }
  813. /// <summary>
  814. /// Waits for Visual Studio process to exit and if it does not in s_ideExitTimeout time, kills it.
  815. /// </summary>
  816. private void KillProcess()
  817. {
  818. if (_process != null)
  819. {
  820. // wait for the specified time for the IDE to exit.
  821. // If it hasn't, kill the process so we can proceed to the next test.
  822. Stopwatch sw = Stopwatch.StartNew();
  823. while (!_process.HasExited && (sw.Elapsed < _ideExitTimeout))
  824. {
  825. System.Threading.Thread.Sleep(_baseSleepDuration);
  826. }
  827. if (!_process.HasExited)
  828. {
  829. _process.Kill();
  830. }
  831. _process = null;
  832. }
  833. }
  834. }
  835. /// <summary>
  836. /// COM message filter class to prevent RPC_E_CALL_REJECTED error while DTE is busy.
  837. /// The filter is used by COM to handle incoming/outgoing messages while waiting for response from a synchonous call.
  838. /// </summary>
  839. /// <seealso cref="http://msdn.microsoft.com/library/en-us/com/html/e12d48c0-5033-47a8-bdcd-e94c49857248.asp"/>
  840. [ComVisible(true)]
  841. internal class RetryMessageFilter : IMessageFilter, IDisposable
  842. {
  843. private const uint RetryCall = 99;
  844. private const uint CancelCall = unchecked((uint)-1); // For COM this must be -1 but IMessageFilter.RetryRejectedCall returns uint.
  845. private IMessageFilter _oldFilter;
  846. /// <summary>
  847. /// Constructor.
  848. /// </summary>
  849. public RetryMessageFilter()
  850. {
  851. // Register the filter.
  852. NativeMethods.CoRegisterMessageFilter(this, out _oldFilter);
  853. }
  854. /// <summary>
  855. /// FInalizer.
  856. /// </summary>
  857. ~RetryMessageFilter()
  858. {
  859. Dispose();
  860. }
  861. /// <summary>
  862. /// Implements IDisposable.Dispose.
  863. /// </summary>
  864. public void Dispose()
  865. {
  866. // Unregister the filter.
  867. IMessageFilter ourFilter;
  868. NativeMethods.CoRegisterMessageFilter(_oldFilter, out ourFilter);
  869. GC.SuppressFinalize(this);
  870. }
  871. /// <summary>
  872. /// Provides an ability to filter or reject incoming calls (or callbacks) to an object or a process.
  873. /// Called by COM prior to each method invocation originating outside the current process.
  874. /// </summary>
  875. public uint HandleInComingCall(uint dwCallType, IntPtr htaskCaller, uint dwTickCount, INTERFACEINFO[] lpInterfaceInfo)
  876. {
  877. // Let current process try process the call.
  878. return (uint)SERVERCALL.SERVERCALL_ISHANDLED;
  879. }
  880. /// <summary>
  881. /// An ability to choose to retry or cancel the outgoing call or switch to the task specified by threadIdCallee.
  882. /// Called by COM immediately after receiving SERVERCALL_RETRYLATER or SERVERCALL_REJECTED
  883. /// from the IMessageFilter::HandleIncomingCall method on the callee's IMessageFilter interface.
  884. /// </summary>
  885. /// <returns>
  886. /// -1: The call should be canceled. COM then returns RPC_E_CALL_REJECTED from the original method call.
  887. /// 0..99: The call is to be retried immediately.
  888. /// 100 and above: COM will wait for this many milliseconds and then retry the call.
  889. /// </returns>
  890. public uint RetryRejectedCall(IntPtr htaskCallee, uint dwTickCount, uint dwRejectType)
  891. {
  892. if (dwRejectType == (uint)SERVERCALL.SERVERCALL_RETRYLATER)
  893. {
  894. // The server called by this process is busy. Ask COM to retry the outgoing call.
  895. return RetryCall;
  896. }
  897. else
  898. {
  899. // Ask COM to cancel the call and return RPC_E_CALL_REJECTED from the original method call.
  900. return CancelCall;
  901. }
  902. }
  903. /// <summary>
  904. /// Called by COM when a Windows message appears in a COM application's message queue
  905. /// while the application is waiting for a reply to an outgoing remote call.
  906. /// </summary>
  907. /// <returns>
  908. /// Tell COM whether: to process the message without interrupting the call,
  909. /// to continue waiting, or to cancel the operation.
  910. /// </returns>
  911. public uint MessagePending(IntPtr htaskCallee, uint dwTickCount, uint dwPendingType)
  912. {
  913. // Continue waiting for the reply, and do not dispatch the message unless it is a task-switching or window-activation message.
  914. // A subsequent message will trigger another call to IMessageFilter::MessagePending.
  915. return (uint)PENDINGMSG.PENDINGMSG_WAITDEFPROCESS;
  916. }
  917. }
  918. /// <summary>
  919. /// The data for this host type to extend Run Config with.
  920. /// - Registry Hive, like 10.0Exp.
  921. /// - Session id for debugging.
  922. /// </summary>
  923. [Serializable]
  924. internal class RunConfigData : IHostSpecificRunConfigurationData, IXmlTestStore, IXmlTestStoreCustom
  925. {
  926. private const string RegistryHiveAttributeName = "registryHive";
  927. private const string XmlNamespaceUri = "http://microsoft.com/schemas/TC/TCTestHostAdapters";
  928. private const string XmlElementName = "VsIdeTestHostRunConfig";
  929. /// <summary>
  930. /// The registry hive of VS to use for the VS instance to start.
  931. /// This field is persisted in the .TestRunConfig file.
  932. /// </summary>
  933. private string _registryHive;
  934. /// <summary>
  935. /// Constructor.
  936. /// </summary>
  937. /// <param name="registryHive">The registry hive to use settings from for new Visual Studio instance.</param>
  938. internal RunConfigData(string registryHive)
  939. {
  940. _registryHive = registryHive; // null is OK. null means get latest version.
  941. }
  942. /// <summary>
  943. /// The description of this host to use in Run Config dialog.
  944. /// </summary>
  945. public string RunConfigurationInformation
  946. {
  947. get
  948. {
  949. return "VsIdeHostAdapter Test Host Configuration Data";
  950. }
  951. }
  952. /// <summary>
  953. /// Implements ICloneable.Clone.
  954. /// </summary>
  955. public object Clone()
  956. {
  957. return new RunConfigData(_registryHive);
  958. }
  959. /// <summary>
  960. /// The registry hive to use settings from for new Visual Studio instance.
  961. /// </summary>
  962. internal string RegistryHive
  963. {
  964. get { return _registryHive; }
  965. set { _registryHive = value; }
  966. }
  967. public void Load(XmlElement element, XmlTestStoreParameters parameters)
  968. {
  969. this.RegistryHive = element.GetAttribute(RegistryHiveAttributeName);
  970. }
  971. public void Save(XmlElement element, XmlTestStoreParameters parameters)
  972. {
  973. element.SetAttribute(RegistryHiveAttributeName, this.RegistryHive);
  974. }
  975. public string ElementName
  976. {
  977. get { return XmlElementName; }
  978. }
  979. public string NamespaceUri
  980. {
  981. get { return XmlNamespaceUri; }
  982. }
  983. }
  984. /// <summary>
  985. /// Helper class for VS registry.
  986. /// Used by both Host Adapter and UI side.
  987. /// </summary>
  988. internal static class VsRegistry
  989. {
  990. private const string ProcessName = "devenv.exe";
  991. internal const string VSRegistryRoot = @"SOFTWARE\Microsoft\VisualStudio";
  992. /// <summary>
  993. /// Obtains all installed Visual Studio versions.
  994. /// </summary>
  995. internal static List<string> GetVersions()
  996. {
  997. List<string> versions = new List<string>();
  998. GetVersionsHelper(versions);
  999. return versions;
  1000. }
  1001. /// <summary>
  1002. /// Returns max version without suffix.
  1003. /// </summary>
  1004. internal static string GetDefaultVersion()
  1005. {
  1006. return VSUtility.Version;
  1007. }
  1008. internal static bool UserSettingsArchiveExists(string registryHive)
  1009. {
  1010. // This must be a key that does not get set if you start up, hit the no settings prompt and select
  1011. // "Exit Visual Studio", but does get set if you select a default
  1012. #if DEV12_OR_LATER
  1013. const string SettingsMarkerKey = @"General\\ToolsOptions";
  1014. #else
  1015. const string SettingsMarkerKey = @"StartPage";
  1016. #endif
  1017. string versionKeyName = VSRegistryRoot + @"\" + registryHive;
  1018. using (RegistryKey hive = Registry.CurrentUser.OpenSubKey(versionKeyName))
  1019. {
  1020. if (hive == null)
  1021. {
  1022. return false;
  1023. }
  1024. using (RegistryKey settingsMarker = hive.OpenSubKey(SettingsMarkerKey))
  1025. {
  1026. return settingsMarker != null;
  1027. }
  1028. }
  1029. }
  1030. /// <summary>
  1031. /// Returns location of devenv.exe on disk.
  1032. /// </summary>
  1033. /// <param name="registryHive">The registry hive (version) of Visual Studio to get location for.</param>
  1034. internal static string GetVsLocation(string registryHive)
  1035. {
  1036. Contract.Assert(!string.IsNullOrEmpty(registryHive));
  1037. string versionKeyName = VSRegistryRoot + @"\" + registryHive;
  1038. string installDir = null;
  1039. using (RegistryKey key = Registry.LocalMachine.OpenSubKey(versionKeyName))
  1040. {
  1041. if (key != null)
  1042. {
  1043. object value = key.GetValue("InstallDir");
  1044. installDir = value as String;
  1045. }
  1046. }
  1047. if (string.IsNullOrEmpty(installDir))
  1048. {
  1049. throw new VsIdeTestHostException(string.Format(CultureInfo.InvariantCulture, "Cannot find installation of Visual Studio in '{0}' registry hive.", registryHive));
  1050. }
  1051. return Path.Combine(installDir, ProcessName);
  1052. }
  1053. /// <summary>
  1054. /// Obtains installed Visual Studio versions and default version.
  1055. /// </summary>
  1056. /// <param name="versions">If null, this is ignored.</param>
  1057. /// <returns>Returns default version = max version without suffix.</returns>
  1058. private static string GetVersionsHelper(List<string> versions)
  1059. {
  1060. // Default is the latest version without suffix, like 10.0.
  1061. string defaultVersion = null;
  1062. Regex versionNoSuffixRegex = new Regex(@"^[0-9]+\.[0-9]+$");
  1063. // Note that the version does not have to be numeric only: can be 10.0Exp.
  1064. using (RegistryKey vsKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\VisualStudio"))
  1065. {
  1066. foreach (string versionKeyName in vsKey.GetSubKeyNames())
  1067. {
  1068. // If there's no InstallDir subkey we skip this key.
  1069. using (RegistryKey versionKey = vsKey.OpenSubKey(versionKeyName))
  1070. {
  1071. if (versionKey.GetValue("InstallDir") == null)
  1072. {
  1073. continue;
  1074. }
  1075. if (versions != null)
  1076. {
  1077. versions.Add(versionKeyName);
  1078. }
  1079. }
  1080. if (versionNoSuffixRegex.Match(versionKeyName).Success &&
  1081. string.Compare(versionKeyName, defaultVersion, StringComparison.OrdinalIgnoreCase) > 0) // null has the smallest value.
  1082. {
  1083. defaultVersion = versionKeyName;
  1084. }
  1085. }
  1086. }
  1087. return defaultVersion;
  1088. }
  1089. }
  1090. }
  1091. }