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