-
-
Save jbevain/7bf8851752d7f0a829bdc3ce0175a50b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Globalization; | |
using System.IO; | |
using System.Linq; | |
using System.Net; | |
using System.Net.Sockets; | |
using System.Net.NetworkInformation; | |
using System.Text; | |
using System.Text.RegularExpressions; | |
using System.Threading; | |
using System.Reflection; | |
using SyntaxTree.VisualStudio.Unity.Messaging; | |
namespace SyntaxTree.VisualStudio.Unity.Messaging | |
{ | |
public class UdpSocket : Socket | |
{ | |
public const int BufferSize = 1024 * 8; | |
internal UdpSocket() | |
: base(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp) | |
{ | |
SetIOControl(); | |
} | |
public void Bind(IPAddress address, int port = 0) | |
{ | |
Bind(new IPEndPoint(address ?? IPAddress.Any, port)); | |
} | |
private void SetIOControl() | |
{ | |
if (!Platform.OnWindows()) | |
return; | |
try | |
{ | |
const int SIO_UDP_CONNRESET = -1744830452; | |
IOControl(SIO_UDP_CONNRESET, new byte[] { 0 }, new byte[0]); | |
} | |
catch | |
{ | |
} | |
} | |
public static byte[] BufferFor(IAsyncResult result) | |
{ | |
return (byte[])result.AsyncState; | |
} | |
public static EndPoint Any() | |
{ | |
return new IPEndPoint(IPAddress.Any, 0); | |
} | |
} | |
class MulticastTransceiver : IDisposable | |
{ | |
private readonly UdpSocket _socket; | |
private readonly IPEndPoint _multicastEndPoint; | |
private readonly object _disposeLock = new object(); | |
private volatile bool _disposed; | |
public MulticastTransceiver(IPAddress address, int port, IPAddress iface = null, int ttl = 4) | |
{ | |
_socket = new UdpSocket(); | |
_multicastEndPoint = new IPEndPoint(address, port); | |
_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, false); | |
_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); | |
_socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, ttl); | |
var membershipOption = iface != null ? new MulticastOption(address, iface) : new MulticastOption(address); | |
_socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, membershipOption); | |
if (iface != null) | |
_socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastInterface, iface.GetAddressBytes()); | |
_socket.Bind(IPAddress.Any, port); | |
} | |
public event EventHandler<MulticastEventArgs> Receive; | |
public void BeginReceive() | |
{ | |
var buffer = new byte[UdpSocket.BufferSize]; | |
var any = UdpSocket.Any(); | |
try | |
{ | |
lock (_disposeLock) | |
{ | |
if (_disposed) | |
return; | |
_socket.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref any, ReceiveCallback, buffer); | |
} | |
} | |
catch (SocketException) | |
{ | |
BeginReceive(); | |
} | |
catch (ObjectDisposedException) | |
{ | |
} | |
} | |
private void ReceiveCallback(IAsyncResult result) | |
{ | |
try | |
{ | |
var endPoint = UdpSocket.Any(); | |
int length; | |
lock (_disposeLock) | |
{ | |
if (_disposed) | |
return; | |
length = _socket.EndReceiveFrom(result, ref endPoint); | |
} | |
var buffer = UdpSocket.BufferFor(result); | |
var bytes = new byte[length]; | |
Buffer.BlockCopy(buffer, 0, bytes, 0, length); | |
if (Receive != null) | |
Receive(this, new MulticastEventArgs((IPEndPoint)endPoint, bytes)); | |
} | |
catch (SocketException) | |
{ | |
} | |
catch (ObjectDisposedException) | |
{ | |
return; | |
} | |
BeginReceive(); | |
} | |
public void Send(byte[] bytes) | |
{ | |
try | |
{ | |
lock (_disposeLock) | |
{ | |
if (_disposed) | |
return; | |
_socket.BeginSendTo(bytes, 0, bytes.Length, SocketFlags.None, _multicastEndPoint, SendCallback, state: null); | |
} | |
} | |
catch (SocketException) | |
{ | |
} | |
catch (ObjectDisposedException) | |
{ | |
} | |
} | |
private void SendCallback(IAsyncResult result) | |
{ | |
try | |
{ | |
lock (_disposeLock) | |
{ | |
if (_disposed) | |
return; | |
_socket.EndSendTo(result); | |
} | |
} | |
catch (SocketException) | |
{ | |
} | |
catch (ObjectDisposedException) | |
{ | |
} | |
} | |
public void Dispose() | |
{ | |
lock (_disposeLock) | |
{ | |
_socket.Close(); | |
_disposed = true; | |
} | |
} | |
} | |
public class UnityPlayerProbe : IDisposable | |
{ | |
private const string PlayerStringFormat = @"\[IP\] (?<Ip>(\d|\.)+) \[Port\] (?<Port>\d+) \[Flags\] (?<Flags>\d+) \[Guid\] (?<Guid>\d+) \[EditorId\] (?<EditorId>\d+) \[Version\] (?<Version>\d+) \[Id\] (?<Id>.*)\((?<Machine>.*)\)(:(?<DebuggerPort>\d+))? \[Debug\] (?<Debug>0|1)"; | |
private const string AnnounceStringFormat = @"[IP] {0} [Port] {1} [Flags] {2} [Guid] {3} [EditorId] {4} [Version] {5} [Id] {6}({7}){8} [Debug] {9}"; | |
private static readonly IPAddress PlayerMultiCastGroup = IPAddress.Parse("225.0.0.222"); | |
private static readonly int[] PlayerMulticastPorts = { 54997, 34997, 57997, 58997 }; | |
public event EventHandler<UnityPlayerEventArgs> ReceiveResponse; | |
private readonly List<MulticastTransceiver> _transceivers; | |
private UnityPlayerProbe() | |
{ | |
_transceivers = new List<MulticastTransceiver>(); | |
CreateTransceivers(); | |
foreach (var transceiver in _transceivers) | |
{ | |
transceiver.Receive += (_, a) => ReceiveMulticast(a.Bytes); | |
transceiver.BeginReceive(); | |
} | |
} | |
private void CreateTransceivers() | |
{ | |
foreach (var iface in Platform.IPAddresses()) | |
{ | |
if (iface.AddressFamily != AddressFamily.InterNetwork) | |
continue; | |
CreateTransceiver(iface); | |
} | |
} | |
private void CreateTransceiver(IPAddress iface) | |
{ | |
foreach (var port in PlayerMulticastPorts) | |
{ | |
try | |
{ | |
_transceivers.Add(new MulticastTransceiver(PlayerMultiCastGroup, port, iface)); | |
} | |
catch (Exception) | |
{ | |
} | |
} | |
} | |
public static bool TryCreate(out UnityPlayerProbe probe) | |
{ | |
try | |
{ | |
probe = new UnityPlayerProbe(); | |
return true; | |
} | |
catch (Exception) | |
{ | |
probe = null; | |
return false; | |
} | |
} | |
private static ushort GetDefaultDebuggerPort(uint guid) | |
{ | |
return (ushort)(56000 + (guid % 1000)); | |
} | |
internal static string GetAnnounceString(UnityPlayer player) | |
{ | |
var id = player.Id; | |
var debuggerPort = string.Empty; | |
if (GetDefaultDebuggerPort(player.Guid) != player.DebuggerPort) | |
debuggerPort = string.Concat(":", player.DebuggerPort.ToString(CultureInfo.InvariantCulture)); | |
var announce = string.Format(CultureInfo.InvariantCulture, AnnounceStringFormat, | |
player.Ip, player.Port, (int) player.Flags, player.Guid, player.EditorId, player.Version, | |
id, player.Machine, debuggerPort, Convert.ToInt32(player.Debug)); | |
return announce; | |
} | |
public void Announce(UnityPlayer player) | |
{ | |
var announce = GetAnnounceString(player); | |
var bytes = Encoding.UTF8.GetBytes(announce); | |
foreach (var transceiver in _transceivers) | |
transceiver.Send(bytes); | |
} | |
private void ReceiveMulticast(byte[] bytes) | |
{ | |
var playerString = Encoding.UTF8.GetString(bytes); | |
var player = ParsePlayerResponse(playerString); | |
if (player != null && ReceiveResponse != null) | |
ReceiveResponse(this, new UnityPlayerEventArgs(player)); | |
} | |
internal static UnityPlayer ParsePlayerResponse(string playerString) | |
{ | |
var matchs = new Regex(PlayerStringFormat).Matches(playerString); | |
if (matchs.Count != 1) | |
return null; | |
try | |
{ | |
var match = matchs[0]; | |
var dpString = GroupValue(match, "DebuggerPort"); | |
var guid = ParserHelper.TryParseOrDefault<uint>(GroupValue(match, "Guid")); | |
var debuggerPort = GetDefaultDebuggerPort(guid); | |
if (!string.IsNullOrEmpty(dpString)) | |
debuggerPort = ParserHelper.TryParseOrDefault<ushort>(dpString); | |
return new UnityPlayer | |
{ | |
Ip = GroupValue(match, "Ip"), | |
Port = ParserHelper.TryParseOrDefault<ushort>(GroupValue(match, "Port")), | |
Flags = (UnityPlayerFlags)ParserHelper.TryParseOrDefault<uint>(GroupValue(match, "Flags"), (uint) (UnityPlayerFlags.DevelopmentBuild | UnityPlayerFlags.ScriptDebugging)), | |
Guid = guid, | |
EditorId = ParserHelper.TryParseOrDefault<uint>(GroupValue(match, "EditorId")), | |
Version = ParserHelper.TryParseOrDefault<uint>(GroupValue(match, "Version")), | |
Id = GroupValue(match, "Id"), | |
Machine = GroupValue(match, "Machine"), | |
DebuggerPort = debuggerPort, | |
Debug = ParserHelper.TryParseOrDefault<int>(GroupValue(match, "Debug")) != 0, | |
}; | |
} | |
catch | |
{ | |
return null; | |
} | |
} | |
private static string GroupValue(Match match, string group) | |
{ | |
return match.Groups[group].Value; | |
} | |
public void Dispose() | |
{ | |
foreach (var transceiver in _transceivers) | |
transceiver.Dispose(); | |
} | |
} | |
public class UnityPlayerEventArgs : EventArgs | |
{ | |
public UnityPlayer Player { get; private set; } | |
public UnityPlayerEventArgs(UnityPlayer player) | |
{ | |
Player = player; | |
} | |
} | |
class MulticastEventArgs : EventArgs | |
{ | |
public IPEndPoint EndPoint { get; private set; } | |
public byte[] Bytes { get; private set; } | |
public MulticastEventArgs(IPEndPoint endPoint, byte[] bytes) | |
{ | |
EndPoint = endPoint; | |
Bytes = bytes; | |
} | |
} | |
public class UnityPlayer | |
{ | |
public string Ip { get; set; } | |
public ushort Port { get; set; } | |
public ushort DebuggerPort { get; set; } | |
public UnityPlayerFlags Flags { get; set; } | |
public uint Guid { get; set; } | |
public uint EditorId { get; set; } | |
public uint Version { get; set; } | |
public string Id { get; set; } | |
public string Machine { get; set; } | |
public bool Debug { get; set; } | |
public UnityProcess ToProcess() | |
{ | |
return new UnityProcess | |
{ | |
Address = Ip == "0.0.0.0" ? "127.0.0.1" : Ip, | |
DebuggerPort = DebuggerPort, | |
ProjectName = Id, | |
Machine = Machine, | |
Type = UnityProcessType.Player, | |
}; | |
} | |
} | |
[Flags] | |
public enum UnityPlayerFlags | |
{ | |
ScriptDebugging = 1, | |
DevelopmentBuild = 2, | |
//Streamed = ?, | |
//OfflineDeployment = ?, | |
} | |
public class UnityProcess | |
{ | |
public int ProcessId { get; set; } | |
public int DebuggerPort { get; set; } | |
public string Address { get; set; } | |
public string Machine { get; set; } | |
public int MessagerPort { get; set; } | |
public string ProjectName { get; set; } | |
public UnityProcessType Type { get; set; } | |
public UnityProcessDiscoveryType DiscoveryType { get; set; } | |
public UnityProcess() | |
{ | |
DiscoveryType = UnityProcessDiscoveryType.Automatic; | |
} | |
public override string ToString() | |
{ | |
return string.Format(CultureInfo.InvariantCulture, string.IsNullOrEmpty(ProjectName) ? "{0}" : "{0} ({1})", Machine ?? Address, ProjectName); | |
} | |
public bool TryGetMessengerEndPoint(out IPEndPoint endPoint) | |
{ | |
return TryParseEndPoint(Address, MessagerPort, out endPoint); | |
} | |
public bool TryGetDebuggerEndPoint(out IPEndPoint endPoint) | |
{ | |
return TryParseEndPoint(Address, DebuggerPort, out endPoint); | |
} | |
public static bool TryParseEndPoint(string connection, out IPEndPoint endPoint) | |
{ | |
endPoint = null; | |
if (string.IsNullOrEmpty(connection)) | |
return false; | |
const char separator = ':'; | |
var index = connection.IndexOf(separator); | |
if (index == -1) | |
return false; | |
return TryParseEndPoint(connection.Substring(0, index), connection.Substring(index + 1), out endPoint); | |
} | |
public static bool TryParseEndPoint(string iphost, string port, out IPEndPoint endPoint) | |
{ | |
endPoint = null; | |
int iport; | |
if (!int.TryParse(port, out iport)) | |
return false; | |
return TryParseEndPoint(iphost, iport, out endPoint); | |
} | |
private static bool TryParseEndPoint(string iphost, int port, out IPEndPoint endPoint) | |
{ | |
endPoint = null; | |
if (Platform.IsLocal(iphost)) | |
return TryParseEndPoint(IPAddress.Loopback, port, out endPoint); | |
IPAddress ip; | |
if (IPAddress.TryParse(iphost, out ip)) | |
return TryParseEndPoint(ip, port, out endPoint); | |
try | |
{ | |
var entry = Dns.GetHostEntry(iphost); | |
var address = entry.AddressList.FirstOrDefault(a => a.AddressFamily == AddressFamily.InterNetwork); | |
return TryParseEndPoint(address, port, out endPoint); | |
} | |
catch (Exception) | |
{ | |
return false; | |
} | |
} | |
private static bool TryParseEndPoint(IPAddress ip, int port, out IPEndPoint endPoint) | |
{ | |
endPoint = null; | |
try | |
{ | |
endPoint = new IPEndPoint(ip, port); | |
return true; | |
} | |
catch (Exception) | |
{ | |
return false; | |
} | |
} | |
} | |
public enum UnityProcessType | |
{ | |
Editor = 1, | |
Player = 16, | |
} | |
public enum UnityProcessDiscoveryType | |
{ | |
Manual = 1, | |
Automatic = 16, | |
AutoConnect = 48, | |
} | |
public static class Platform | |
{ | |
public static bool OnWindows() | |
{ | |
return Environment.OSVersion.Platform == PlatformID.Win32NT; | |
} | |
private static Type MonoRuntime() | |
{ | |
return Type.GetType("Mono.Runtime"); | |
} | |
private static string MonoDisplayName() | |
{ | |
var type = MonoRuntime(); | |
return (string)type.GetMethod("GetDisplayName", BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, null); | |
} | |
public static bool OnMono() | |
{ | |
return MonoRuntime() != null; | |
} | |
public static bool OnOldMono() | |
{ | |
return OnMono() && MonoVersion > new Version(2, 6); | |
} | |
public static bool OnOldMonoOnMac() | |
{ | |
return OnOldMono() && !OnWindows(); | |
} | |
private static Version _monoVersion; | |
public static Version MonoVersion | |
{ | |
get | |
{ | |
if (_monoVersion != null) | |
return _monoVersion; | |
if (!OnMono()) | |
throw new InvalidOperationException(); | |
return _monoVersion = ParseMonoVersion(MonoDisplayName()); | |
} | |
} | |
private static Version ParseMonoVersion(string monoVersion) | |
{ | |
var match = new Regex(@"(?<v>(\d+\.\d+))\.(.*)").Match(monoVersion); | |
return match.Success ? new Version(match.Groups["v"].Value) : null; | |
} | |
public static bool HasNativeUvsSupport(Version unityVersion) | |
{ | |
if (OnWindows()) | |
return unityVersion >= new Version(5, 2); | |
return unityVersion >= new Version(5, 6); | |
} | |
public static IPAddress[] IPAddresses() | |
{ | |
var interfaces = OnOldMonoOnMac() | |
? SafeGetAllNetworkInterfaces() | |
: SafeGetAllNetworkInterfaces().Where(i => i.OperationalStatus == OperationalStatus.Up && i.SupportsMulticast).ToArray(); | |
return interfaces | |
.SelectMany(i => i.GetIPProperties().UnicastAddresses) | |
.Where(a => a.Address.AddressFamily == AddressFamily.InterNetwork) | |
.Where(a => !IPAddress.IsLoopback(a.Address)) | |
.Select(SafeAddress) | |
.Where(a => a != null) | |
.ToArray(); | |
} | |
private static NetworkInterface[] SafeGetAllNetworkInterfaces() | |
{ | |
try | |
{ | |
return NetworkInterface.GetAllNetworkInterfaces(); | |
} | |
catch (Exception) | |
{ | |
return new NetworkInterface[0]; | |
} | |
} | |
private static IPAddress SafeAddress(UnicastIPAddressInformation information) | |
{ | |
try | |
{ | |
return information.Address; | |
} | |
catch (Exception) | |
{ | |
return null; | |
} | |
} | |
public static bool IsLocal(string address) | |
{ | |
try | |
{ | |
if (address == "127.0.0.1") | |
return true; | |
return Dns.GetHostAddresses(Dns.GetHostName()).Any(ip => ip.ToString() == address); | |
} | |
catch | |
{ | |
return false; | |
} | |
} | |
} | |
public static class ParserHelper | |
{ | |
public static T TryParseOrDefault<T>(string valueString, T defaultValue = default(T)) where T : IConvertible | |
{ | |
try | |
{ | |
return (T)Convert.ChangeType(valueString, typeof(T), CultureInfo.InvariantCulture); | |
} | |
catch (Exception) | |
{ | |
return defaultValue; | |
} | |
} | |
} | |
} | |
class Program | |
{ | |
static int Main(string[] args) | |
{ | |
UnityPlayerProbe probe; | |
if (!UnityPlayerProbe.TryCreate(out probe)) | |
{ | |
Console.WriteLine("Could not create probe"); | |
return 1; | |
} | |
probe.ReceiveResponse += (s, ea) => | |
{ | |
var dbg = ea.Player.Debug ? "debuggable" : "not debuggable"; | |
Console.WriteLine("Player {0} at: {1}:{2}", dbg, ea.Player.Ip, ea.Player.DebuggerPort); | |
}; | |
Console.WriteLine("Press Enter to quit"); | |
Console.ReadLine(); | |
return 0; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment