Skip to content

Instantly share code, notes, and snippets.

@jbevain
Created October 16, 2018 19:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jbevain/7bf8851752d7f0a829bdc3ce0175a50b to your computer and use it in GitHub Desktop.
Save jbevain/7bf8851752d7f0a829bdc3ce0175a50b to your computer and use it in GitHub Desktop.
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