You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

296 lines
7.5 KiB
C#

1 year ago
/*---------------------------------------------------------------------------------------------
* 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<Message> _incoming = new Queue<Message>();
private static readonly Dictionary<IPEndPoint, Client> _clients = new Dictionary<IPEndPoint, Client>();
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<string>(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);
}
}
}
}
}