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