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 *--------------------------------------------------------------------------------------------*/
5using System;
6using System.Collections.Generic;
7using System.IO;
8using System.Linq;
9using System.Net;
10using System.Net.Sockets;
11using Microsoft.Unity.VisualStudio.Editor.Messaging;
12using Microsoft.Unity.VisualStudio.Editor.Testing;
13using UnityEditor;
14using UnityEngine;
15using MessageType = Microsoft.Unity.VisualStudio.Editor.Messaging.MessageType;
16
17namespace Microsoft.Unity.VisualStudio.Editor
18{
19 [InitializeOnLoad]
20 internal class VisualStudioIntegration
21 {
22 class Client
23 {
24 public IPEndPoint EndPoint { get; set; }
25 public double LastMessage { get; set; }
26 }
27
28 private static Messager _messager;
29
30 private static readonly Queue<Message> _incoming = new Queue<Message>();
31 private static readonly Dictionary<IPEndPoint, Client> _clients = new Dictionary<IPEndPoint, Client>();
32 private static readonly object _incomingLock = new object();
33 private static readonly object _clientsLock = new object();
34
35 static VisualStudioIntegration()
36 {
37 if (!VisualStudioEditor.IsEnabled)
38 return;
39
40 RunOnceOnUpdate(() =>
41 {
42 // Despite using ReuseAddress|!ExclusiveAddressUse, we can fail here:
43 // - if another application is using this port with exclusive access
44 // - or if the firewall is not properly configured
45 var messagingPort = MessagingPort();
46
47 try
48 {
49 _messager = Messager.BindTo(messagingPort);
50 _messager.ReceiveMessage += ReceiveMessage;
51 }
52 catch (SocketException)
53 {
54 // We'll have a chance to try to rebind on next domain reload
55 Debug.LogWarning($"Unable to use UDP port {messagingPort} for VS/Unity messaging. You should check if another process is already bound to this port or if your firewall settings are compatible.");
56 }
57
58 RunOnShutdown(Shutdown);
59 });
60
61 EditorApplication.update += OnUpdate;
62
63 CheckLegacyAssemblies();
64 }
65
66 private static void CheckLegacyAssemblies()
67 {
68 var checkList = new HashSet<string>(new[] { KnownAssemblies.UnityVS, KnownAssemblies.Messaging, KnownAssemblies.Bridge });
69
70 try
71 {
72 var assemblies = AppDomain
73 .CurrentDomain
74 .GetAssemblies()
75 .Where(a => checkList.Contains(a.GetName().Name));
76
77 foreach (var assembly in assemblies)
78 {
79 // for now we only want to warn against local assemblies, do not check externals.
80 var relativePath = FileUtility.MakeRelativeToProjectPath(assembly.Location);
81 if (relativePath == null)
82 continue;
83
84 Debug.LogWarning($"Project contains legacy assembly that could interfere with the Visual Studio Package. You should delete {relativePath}");
85 }
86 }
87 catch (Exception)
88 {
89 // abandon legacy check
90 }
91 }
92
93 private static void RunOnceOnUpdate(Action action)
94 {
95 var callback = null as EditorApplication.CallbackFunction;
96
97 callback = () =>
98 {
99 EditorApplication.update -= callback;
100 action();
101 };
102
103 EditorApplication.update += callback;
104 }
105
106 private static void RunOnShutdown(Action action)
107 {
108 // Mono on OSX has all kinds of quirks on AppDomain shutdown
109#if UNITY_EDITOR_WIN
110 AppDomain.CurrentDomain.DomainUnload += (_, __) => action();
111#endif
112 }
113
114 private static int DebuggingPort()
115 {
116 return 56000 + (System.Diagnostics.Process.GetCurrentProcess().Id % 1000);
117 }
118
119 private static int MessagingPort()
120 {
121 return DebuggingPort() + 2;
122 }
123
124 private static void ReceiveMessage(object sender, MessageEventArgs args)
125 {
126 OnMessage(args.Message);
127 }
128
129 private static void OnUpdate()
130 {
131 lock (_incomingLock)
132 {
133 while (_incoming.Count > 0)
134 {
135 ProcessIncoming(_incoming.Dequeue());
136 }
137 }
138
139 lock (_clientsLock)
140 {
141 foreach (var client in _clients.Values.ToArray())
142 {
143 // EditorApplication.timeSinceStartup: The time since the editor was started, in seconds, not reset when starting play mode.
144 if (EditorApplication.timeSinceStartup - client.LastMessage > 4)
145 _clients.Remove(client.EndPoint);
146 }
147 }
148 }
149
150 private static void AddMessage(Message message)
151 {
152 lock (_incomingLock)
153 {
154 _incoming.Enqueue(message);
155 }
156 }
157
158 private static void ProcessIncoming(Message message)
159 {
160 lock (_clientsLock)
161 {
162 CheckClient(message);
163 }
164
165 switch (message.Type)
166 {
167 case MessageType.Ping:
168 Answer(message, MessageType.Pong);
169 break;
170 case MessageType.Play:
171 Shutdown();
172 EditorApplication.isPlaying = true;
173 break;
174 case MessageType.Stop:
175 EditorApplication.isPlaying = false;
176 break;
177 case MessageType.Pause:
178 EditorApplication.isPaused = true;
179 break;
180 case MessageType.Unpause:
181 EditorApplication.isPaused = false;
182 break;
183 case MessageType.Build:
184 // Not used anymore
185 break;
186 case MessageType.Refresh:
187 Refresh();
188 break;
189 case MessageType.Version:
190 Answer(message, MessageType.Version, PackageVersion());
191 break;
192 case MessageType.UpdatePackage:
193 // Not used anymore
194 break;
195 case MessageType.ProjectPath:
196 Answer(message, MessageType.ProjectPath, Path.GetFullPath(Path.Combine(Application.dataPath, "..")));
197 break;
198 case MessageType.ExecuteTests:
199 TestRunnerApiListener.ExecuteTests(message.Value);
200 break;
201 case MessageType.RetrieveTestList:
202 TestRunnerApiListener.RetrieveTestList(message.Value);
203 break;
204 case MessageType.ShowUsage:
205 UsageUtility.ShowUsage(message.Value);
206 break;
207 }
208 }
209
210 private static void CheckClient(Message message)
211 {
212 var endPoint = message.Origin;
213
214 if (!_clients.TryGetValue(endPoint, out var client))
215 {
216 client = new Client
217 {
218 EndPoint = endPoint,
219 LastMessage = EditorApplication.timeSinceStartup
220 };
221
222 _clients.Add(endPoint, client);
223 }
224 else
225 {
226 client.LastMessage = EditorApplication.timeSinceStartup;
227 }
228 }
229
230 internal static string PackageVersion()
231 {
232 var package = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(VisualStudioIntegration).Assembly);
233 return package.version;
234 }
235
236 private static void Refresh()
237 {
238 // If the user disabled auto-refresh in Unity, do not try to force refresh the Asset database
239 if (!EditorPrefs.GetBool("kAutoRefresh", true))
240 return;
241
242 if (UnityInstallation.IsInSafeMode)
243 return;
244
245 RunOnceOnUpdate(AssetDatabase.Refresh);
246 }
247
248 private static void OnMessage(Message message)
249 {
250 AddMessage(message);
251 }
252
253 private static void Answer(Client client, MessageType answerType, string answerValue)
254 {
255 Answer(client.EndPoint, answerType, answerValue);
256 }
257
258 private static void Answer(Message message, MessageType answerType, string answerValue = "")
259 {
260 var targetEndPoint = message.Origin;
261
262 Answer(
263 targetEndPoint,
264 answerType,
265 answerValue);
266 }
267
268 private static void Answer(IPEndPoint targetEndPoint, MessageType answerType, string answerValue)
269 {
270 _messager?.SendMessage(targetEndPoint, answerType, answerValue);
271 }
272
273 private static void Shutdown()
274 {
275 if (_messager == null)
276 return;
277
278 _messager.ReceiveMessage -= ReceiveMessage;
279 _messager.Dispose();
280 _messager = null;
281 }
282
283 internal static void BroadcastMessage(MessageType type, string value)
284 {
285 lock (_clientsLock)
286 {
287 foreach (var client in _clients.Values.ToArray())
288 {
289 Answer(client, type, value);
290 }
291 }
292 }
293 }
294}