暂无描述
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

VisualStudioForWindowsInstallation.cs 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. #if UNITY_EDITOR_WIN
  6. using System;
  7. using System.Collections.Concurrent;
  8. using System.Collections.Generic;
  9. using System.Diagnostics;
  10. using System.IO;
  11. using System.Linq;
  12. using System.Text.RegularExpressions;
  13. using Microsoft.Win32;
  14. using Unity.CodeEditor;
  15. using UnityEditor;
  16. using UnityEngine;
  17. using Debug = UnityEngine.Debug;
  18. using IOPath = System.IO.Path;
  19. namespace Microsoft.Unity.VisualStudio.Editor
  20. {
  21. internal class VisualStudioForWindowsInstallation : VisualStudioInstallation
  22. {
  23. // C# language version support for Visual Studio
  24. private static readonly VersionPair[] WindowsVersionTable =
  25. {
  26. // VisualStudio 2022
  27. new VersionPair(17,4, /* => */ 11,0),
  28. new VersionPair(17,0, /* => */ 10,0),
  29. // VisualStudio 2019
  30. new VersionPair(16,8, /* => */ 9,0),
  31. new VersionPair(16,0, /* => */ 8,0),
  32. // VisualStudio 2017
  33. new VersionPair(15,7, /* => */ 7,3),
  34. new VersionPair(15,5, /* => */ 7,2),
  35. new VersionPair(15,3, /* => */ 7,1),
  36. new VersionPair(15,0, /* => */ 7,0),
  37. };
  38. private static string _vsWherePath = null;
  39. private static readonly IGenerator _generator = new LegacyStyleProjectGeneration();
  40. public override bool SupportsAnalyzers
  41. {
  42. get
  43. {
  44. return Version >= new Version(16, 3);
  45. }
  46. }
  47. public override Version LatestLanguageVersionSupported
  48. {
  49. get
  50. {
  51. return GetLatestLanguageVersionSupported(WindowsVersionTable);
  52. }
  53. }
  54. private static string ReadRegistry(RegistryKey hive, string keyName, string valueName)
  55. {
  56. try
  57. {
  58. var unitykey = hive.OpenSubKey(keyName);
  59. var result = (string)unitykey?.GetValue(valueName);
  60. return result;
  61. }
  62. catch (Exception)
  63. {
  64. return null;
  65. }
  66. }
  67. private string GetWindowsBridgeFromRegistry()
  68. {
  69. var keyName = $"Software\\Microsoft\\Microsoft Visual Studio {Version.Major}.0 Tools for Unity";
  70. const string valueName = "UnityExtensionPath";
  71. var bridge = ReadRegistry(Registry.CurrentUser, keyName, valueName);
  72. if (string.IsNullOrEmpty(bridge))
  73. bridge = ReadRegistry(Registry.LocalMachine, keyName, valueName);
  74. return bridge;
  75. }
  76. private string GetExtensionPath()
  77. {
  78. const string extensionName = "Visual Studio Tools for Unity";
  79. const string extensionAssembly = "SyntaxTree.VisualStudio.Unity.dll";
  80. var vsDirectory = IOPath.GetDirectoryName(Path);
  81. var vstuDirectory = IOPath.Combine(vsDirectory, "Extensions", "Microsoft", extensionName);
  82. if (File.Exists(IOPath.Combine(vstuDirectory, extensionAssembly)))
  83. return vstuDirectory;
  84. return null;
  85. }
  86. public override string[] GetAnalyzers()
  87. {
  88. var vstuPath = GetExtensionPath();
  89. if (string.IsNullOrEmpty(vstuPath))
  90. return Array.Empty<string>();
  91. var analyzers = GetAnalyzers(vstuPath);
  92. if (analyzers?.Length > 0)
  93. return analyzers;
  94. var bridge = GetWindowsBridgeFromRegistry();
  95. if (File.Exists(bridge))
  96. return GetAnalyzers(IOPath.Combine(IOPath.GetDirectoryName(bridge), ".."));
  97. return Array.Empty<string>();
  98. }
  99. public override IGenerator ProjectGenerator
  100. {
  101. get
  102. {
  103. return _generator;
  104. }
  105. }
  106. private static bool IsCandidateForDiscovery(string path)
  107. {
  108. return File.Exists(path) && Regex.IsMatch(path, "devenv.exe$", RegexOptions.IgnoreCase);
  109. }
  110. public static bool TryDiscoverInstallation(string editorPath, out IVisualStudioInstallation installation)
  111. {
  112. installation = null;
  113. if (string.IsNullOrEmpty(editorPath))
  114. return false;
  115. if (!IsCandidateForDiscovery(editorPath))
  116. return false;
  117. // On windows we use the executable directly, so we can query extra information
  118. if (!File.Exists(editorPath))
  119. return false;
  120. // VS preview are not using the isPrerelease flag so far
  121. // On Windows FileDescription contains "Preview", but not on Mac
  122. var vi = FileVersionInfo.GetVersionInfo(editorPath);
  123. var version = new Version(vi.ProductVersion);
  124. var isPrerelease = vi.IsPreRelease || string.Concat(editorPath, "/" + vi.FileDescription).ToLower().Contains("preview");
  125. installation = new VisualStudioForWindowsInstallation()
  126. {
  127. IsPrerelease = isPrerelease,
  128. Name = $"{FormatProductName(vi.FileDescription)} [{version.ToString(3)}]",
  129. Path = editorPath,
  130. Version = version
  131. };
  132. return true;
  133. }
  134. public static string FormatProductName(string productName)
  135. {
  136. if (string.IsNullOrEmpty(productName))
  137. return string.Empty;
  138. return productName.Replace("Microsoft ", string.Empty);
  139. }
  140. public static IEnumerable<IVisualStudioInstallation> GetVisualStudioInstallations()
  141. {
  142. foreach (var installation in QueryVsWhere())
  143. yield return installation;
  144. }
  145. #region VsWhere Json Schema
  146. #pragma warning disable CS0649
  147. [Serializable]
  148. internal class VsWhereResult
  149. {
  150. public VsWhereEntry[] entries;
  151. public static VsWhereResult FromJson(string json)
  152. {
  153. return JsonUtility.FromJson<VsWhereResult>("{ \"" + nameof(VsWhereResult.entries) + "\": " + json + " }");
  154. }
  155. public IEnumerable<VisualStudioInstallation> ToVisualStudioInstallations()
  156. {
  157. foreach (var entry in entries)
  158. {
  159. yield return new VisualStudioForWindowsInstallation
  160. {
  161. Name = $"{FormatProductName(entry.displayName)} [{entry.catalog.productDisplayVersion}]",
  162. Path = entry.productPath,
  163. IsPrerelease = entry.isPrerelease,
  164. Version = Version.Parse(entry.catalog.buildVersion)
  165. };
  166. }
  167. }
  168. }
  169. [Serializable]
  170. internal class VsWhereEntry
  171. {
  172. public string displayName;
  173. public bool isPrerelease;
  174. public string productPath;
  175. public VsWhereCatalog catalog;
  176. }
  177. [Serializable]
  178. internal class VsWhereCatalog
  179. {
  180. public string productDisplayVersion; // non parseable like "16.3.0 Preview 3.0"
  181. public string buildVersion;
  182. }
  183. #pragma warning restore CS3021
  184. #endregion
  185. private static IEnumerable<VisualStudioInstallation> QueryVsWhere()
  186. {
  187. var progpath = _vsWherePath;
  188. if (string.IsNullOrWhiteSpace(progpath))
  189. return Enumerable.Empty<VisualStudioInstallation>();
  190. const string arguments = "-prerelease -format json";
  191. // We've seen issues with json parsing in utf8 mode and with specific non-UTF code pages like 949 (Korea)
  192. // So try with utf8 first, then fallback to non utf8 in case of an issue
  193. // See https://github.com/microsoft/vswhere/issues/264
  194. try
  195. {
  196. return QueryVsWhere(progpath, $"{arguments} -utf8");
  197. }
  198. catch
  199. {
  200. return QueryVsWhere(progpath, $"{arguments}");
  201. }
  202. }
  203. private static IEnumerable<VisualStudioInstallation> QueryVsWhere(string progpath, string arguments)
  204. {
  205. var result = ProcessRunner.StartAndWaitForExit(progpath, arguments);
  206. if (!result.Success)
  207. throw new Exception($"Failure while running vswhere: {result.Error}");
  208. // Do not catch any JsonException here, this will be handled by the caller
  209. return VsWhereResult
  210. .FromJson(result.Output)
  211. .ToVisualStudioInstallations();
  212. }
  213. private enum COMIntegrationState
  214. {
  215. Running,
  216. DisplayProgressBar,
  217. ClearProgressBar,
  218. Exited
  219. }
  220. public override void CreateExtraFiles(string projectDirectory)
  221. {
  222. // See https://devblogs.microsoft.com/setup/configure-visual-studio-across-your-organization-with-vsconfig/
  223. // We create a .vsconfig file to make sure our ManagedGame workload is installed
  224. try
  225. {
  226. var vsConfigFile = IOPath.Combine(projectDirectory.NormalizePathSeparators(), ".vsconfig");
  227. if (File.Exists(vsConfigFile))
  228. return;
  229. const string content = @"{
  230. ""version"": ""1.0"",
  231. ""components"": [
  232. ""Microsoft.VisualStudio.Workload.ManagedGame""
  233. ]
  234. }
  235. ";
  236. File.WriteAllText(vsConfigFile, content);
  237. }
  238. catch (IOException)
  239. {
  240. }
  241. }
  242. public override bool Open(string path, int line, int column, string solution)
  243. {
  244. var progpath = FileUtility.GetPackageAssetFullPath("Editor", "COMIntegration", "Release", "COMIntegration.exe");
  245. if (string.IsNullOrWhiteSpace(progpath))
  246. return false;
  247. string absolutePath = "";
  248. if (!string.IsNullOrWhiteSpace(path))
  249. {
  250. absolutePath = IOPath.GetFullPath(path);
  251. }
  252. // We remove all invalid chars from the solution filename, but we cannot prevent the user from using a specific path for the Unity project
  253. // So process the fullpath to make it compatible with VS
  254. if (!string.IsNullOrWhiteSpace(solution))
  255. {
  256. solution = $"\"{solution}\"";
  257. solution = solution.Replace("^", "^^");
  258. }
  259. var psi = ProcessRunner.ProcessStartInfoFor(progpath, $"\"{CodeEditor.CurrentEditorInstallation}\" {solution} \"{absolutePath}\" {line}");
  260. psi.StandardOutputEncoding = System.Text.Encoding.Unicode;
  261. psi.StandardErrorEncoding = System.Text.Encoding.Unicode;
  262. // inter thread communication
  263. var messages = new BlockingCollection<COMIntegrationState>();
  264. var asyncStart = AsyncOperation<ProcessRunnerResult>.Run(
  265. () => ProcessRunner.StartAndWaitForExit(psi, onOutputReceived: data => OnOutputReceived(data, messages)),
  266. e => new ProcessRunnerResult {Success = false, Error = e.Message, Output = string.Empty},
  267. () => messages.Add(COMIntegrationState.Exited)
  268. );
  269. MonitorCOMIntegration(messages);
  270. var result = asyncStart.Result;
  271. if (!result.Success && !string.IsNullOrWhiteSpace(result.Error))
  272. Debug.LogError($"Error while starting Visual Studio: {result.Error}");
  273. return result.Success;
  274. }
  275. private static void MonitorCOMIntegration(BlockingCollection<COMIntegrationState> messages)
  276. {
  277. var displayingProgress = false;
  278. COMIntegrationState state;
  279. do
  280. {
  281. state = messages.Take();
  282. switch (state)
  283. {
  284. case COMIntegrationState.ClearProgressBar:
  285. EditorUtility.ClearProgressBar();
  286. displayingProgress = false;
  287. break;
  288. case COMIntegrationState.DisplayProgressBar:
  289. EditorUtility.DisplayProgressBar("Opening Visual Studio", "Starting up Visual Studio, this might take some time.", .5f);
  290. displayingProgress = true;
  291. break;
  292. }
  293. } while (state != COMIntegrationState.Exited);
  294. // Make sure the progress bar is properly cleared in case of COMIntegration failure
  295. if (displayingProgress)
  296. EditorUtility.ClearProgressBar();
  297. }
  298. private static readonly COMIntegrationState[] ProgressBarCommands = {COMIntegrationState.DisplayProgressBar, COMIntegrationState.ClearProgressBar};
  299. private static void OnOutputReceived(string data, BlockingCollection<COMIntegrationState> messages)
  300. {
  301. if (data == null)
  302. return;
  303. foreach (var cmd in ProgressBarCommands)
  304. {
  305. if (data.IndexOf(cmd.ToString(), StringComparison.OrdinalIgnoreCase) >= 0)
  306. messages.Add(cmd);
  307. }
  308. }
  309. public static void Initialize()
  310. {
  311. _vsWherePath = FileUtility.GetPackageAssetFullPath("Editor", "VSWhere", "vswhere.exe");
  312. }
  313. }
  314. }
  315. #endif