/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; using Microsoft.Unity.VisualStudio.Editor.Messaging; using Microsoft.Unity.VisualStudio.Editor.Testing; using UnityEditor; using UnityEngine; using MessageType = Microsoft.Unity.VisualStudio.Editor.Messaging.MessageType; namespace Microsoft.Unity.VisualStudio.Editor { [InitializeOnLoad] internal class VisualStudioIntegration { class Client { public IPEndPoint EndPoint { get; set; } public double LastMessage { get; set; } } private static Messager _messager; private static readonly Queue _incoming = new Queue(); private static readonly Dictionary _clients = new Dictionary(); private static readonly object _incomingLock = new object(); private static readonly object _clientsLock = new object(); static VisualStudioIntegration() { if (!VisualStudioEditor.IsEnabled) return; RunOnceOnUpdate(() => { // Despite using ReuseAddress|!ExclusiveAddressUse, we can fail here: // - if another application is using this port with exclusive access // - or if the firewall is not properly configured var messagingPort = MessagingPort(); try { _messager = Messager.BindTo(messagingPort); _messager.ReceiveMessage += ReceiveMessage; } catch (SocketException) { // We'll have a chance to try to rebind on next domain reload 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."); } RunOnShutdown(Shutdown); }); EditorApplication.update += OnUpdate; CheckLegacyAssemblies(); } private static void CheckLegacyAssemblies() { var checkList = new HashSet(new[] { KnownAssemblies.UnityVS, KnownAssemblies.Messaging, KnownAssemblies.Bridge }); try { var assemblies = AppDomain .CurrentDomain .GetAssemblies() .Where(a => checkList.Contains(a.GetName().Name)); foreach (var assembly in assemblies) { // for now we only want to warn against local assemblies, do not check externals. var relativePath = FileUtility.MakeRelativeToProjectPath(assembly.Location); if (relativePath == null) continue; Debug.LogWarning($"Project contains legacy assembly that could interfere with the Visual Studio Package. You should delete {relativePath}"); } } catch (Exception) { // abandon legacy check } } private static void RunOnceOnUpdate(Action action) { var callback = null as EditorApplication.CallbackFunction; callback = () => { EditorApplication.update -= callback; action(); }; EditorApplication.update += callback; } private static void RunOnShutdown(Action action) { // Mono on OSX has all kinds of quirks on AppDomain shutdown if (!VisualStudioEditor.IsWindows) return; AppDomain.CurrentDomain.DomainUnload += (_, __) => action(); } private static int DebuggingPort() { return 56000 + (System.Diagnostics.Process.GetCurrentProcess().Id % 1000); } private static int MessagingPort() { return DebuggingPort() + 2; } private static void ReceiveMessage(object sender, MessageEventArgs args) { OnMessage(args.Message); } private static void OnUpdate() { lock (_incomingLock) { while (_incoming.Count > 0) { ProcessIncoming(_incoming.Dequeue()); } } lock (_clientsLock) { foreach (var client in _clients.Values.ToArray()) { // EditorApplication.timeSinceStartup: The time since the editor was started, in seconds, not reset when starting play mode. if (EditorApplication.timeSinceStartup - client.LastMessage > 4) _clients.Remove(client.EndPoint); } } } private static void AddMessage(Message message) { lock (_incomingLock) { _incoming.Enqueue(message); } } private static void ProcessIncoming(Message message) { lock (_clientsLock) { CheckClient(message); } switch (message.Type) { case MessageType.Ping: Answer(message, MessageType.Pong); break; case MessageType.Play: Shutdown(); EditorApplication.isPlaying = true; break; case MessageType.Stop: EditorApplication.isPlaying = false; break; case MessageType.Pause: EditorApplication.isPaused = true; break; case MessageType.Unpause: EditorApplication.isPaused = false; break; case MessageType.Build: // Not used anymore break; case MessageType.Refresh: Refresh(); break; case MessageType.Version: Answer(message, MessageType.Version, PackageVersion()); break; case MessageType.UpdatePackage: // Not used anymore break; case MessageType.ProjectPath: Answer(message, MessageType.ProjectPath, Path.GetFullPath(Path.Combine(Application.dataPath, ".."))); break; case MessageType.ExecuteTests: TestRunnerApiListener.ExecuteTests(message.Value); break; case MessageType.RetrieveTestList: TestRunnerApiListener.RetrieveTestList(message.Value); break; case MessageType.ShowUsage: UsageUtility.ShowUsage(message.Value); break; } } private static void CheckClient(Message message) { var endPoint = message.Origin; if (!_clients.TryGetValue(endPoint, out var client)) { client = new Client { EndPoint = endPoint, LastMessage = EditorApplication.timeSinceStartup }; _clients.Add(endPoint, client); } else { client.LastMessage = EditorApplication.timeSinceStartup; } } internal static string PackageVersion() { var package = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(VisualStudioIntegration).Assembly); return package.version; } private static void Refresh() { // If the user disabled auto-refresh in Unity, do not try to force refresh the Asset database if (!EditorPrefs.GetBool("kAutoRefresh", true)) return; if (UnityInstallation.IsInSafeMode) return; RunOnceOnUpdate(AssetDatabase.Refresh); } private static void OnMessage(Message message) { AddMessage(message); } private static void Answer(Client client, MessageType answerType, string answerValue) { Answer(client.EndPoint, answerType, answerValue); } private static void Answer(Message message, MessageType answerType, string answerValue = "") { var targetEndPoint = message.Origin; Answer( targetEndPoint, answerType, answerValue); } private static void Answer(IPEndPoint targetEndPoint, MessageType answerType, string answerValue) { _messager?.SendMessage(targetEndPoint, answerType, answerValue); } private static void Shutdown() { if (_messager == null) return; _messager.ReceiveMessage -= ReceiveMessage; _messager.Dispose(); _messager = null; } internal static void BroadcastMessage(MessageType type, string value) { lock (_clientsLock) { foreach (var client in _clients.Values.ToArray()) { Answer(client, type, value); } } } } }