A game about forced loneliness, made by TACStudios
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