using System; using System.Collections.Generic; using System.IO; using System.Reflection; using UnityEngine; namespace Proxima { /// Proxima Inspector enables remote inspecting, debugging, and control of a Unity application. [HelpURL("https://www.unityproxima.com/docs")] public class ProximaInspector : MonoBehaviour { // The name displayed to show in the browser when connected. [SerializeField] private string _displayName; public string DisplayName { get => _displayName; set => _displayName = value; } // The port number to host the embedded Proxima server. [SerializeField] private int _port = 7759; public int Port { get => _port; set => _port = value; } // The password required to connect to Proxima. See unityproxima.com/docs/security for more information. [SerializeField] private string _password = ""; public string Password { get => _password; set => _password = value; } // Enables and disables HTTPS for encryption. See unityproxima.com/docs/security for more information. [SerializeField] private bool _useHttps = false; public bool UseHttps { get => _useHttps; set => _useHttps = value; } // Optional TLS certificate. By default Proxima uses Proxima/Resources/Proxima/ProximaEmbeddedCert.pfx. [SerializeField] private PfxAsset _certificate; public PfxAsset Certificate { get => _certificate; set => _certificate = value; } // Password for the TLS certificate. [SerializeField] private string _certificatePassword; public string CertificatePassword { get => _certificatePassword; set => _certificatePassword = value; } // Automatically starts the Proxima server when this component is enabled. [SerializeField] private bool _runOnEnable = true; public bool StartOnEnable { get => _runOnEnable; set => _runOnEnable = value; } // Maximum number of log messages to keep in memory. [SerializeField] private int _logBufferSize = 1000; public int LogBufferSize { get => _logBufferSize; set => ProximaLogCommands.SetLogCapacity(value); } // Instantiates Proxima/Resources/Proxima/ProximaStatusUI.prefab on startup. // This UI lets you see the current status of Proxima at the bottom of your screen. [SerializeField] private bool _instantiateStatusUI = true; public bool InstantiateStatusUI { get => _instantiateStatusUI; set => _instantiateStatusUI = value; } // Instantiates Proxima/Resources/Proxima/ProximaConnectUI.prefab on startup. // This UI appears when the user presses F2 and allows the user to start and stop the server // with a display name and password. [SerializeField] private bool _instantiateConnectUI = false; public bool InstantiateConnectUI { get => _instantiateConnectUI; set => _instantiateConnectUI = value; } // Adds the gameObject with the Proxima Inspector to the DontDestroyOnLoad scene, // which keeps connections alive when transitioning between scenes. [SerializeField] private bool _dontDestroyOnLoad = true; // When Proxima starts, sets Application.runInBackground to true. When Proxima stops, // sets Application.runInBackground back to its previous value. This allows Proxima // to work when connecting from a browseer on the same device, since normally Unity // will pause the app when focus is set to the browser. [SerializeField] private bool _setRunInBackground = true; // Stores the current status of Proxima and raises events when it changes. public ProximaStatus Status = new ProximaStatus(); public enum ServerTypes { Remote, Embedded, #if PROXIMA_DEMO Demo #endif } // Is the Proxima server embedded or hosted remotely? // This feature is a work in progress, and so is disabled. [SerializeField, HideInInspector] private ServerTypes _serverType = ServerTypes.Embedded; public ServerTypes ServerType { get => _serverType; set => _serverType = value; } /// URL of the remote Proxima Server. [SerializeField, HideInInspector] private string _serverUrl = ""; public string ServerUrl { get => _serverUrl; set => _serverUrl = value; } // Performance options public static int MaxGameObjectUpdatesPerFrame = 10; public static int MaxComponentUpdateFrequency = 10; private struct OpenStream { public ProximaConnection Connection; public string Id; public string Guid; public StreamInfo Info; } private class StreamInfo { public MethodInfo StartMethod; public MethodInfo StopMethod; public MethodInfo UpdateMethod; } private ProximaServer _server; private static List _inits = new List(); private static List _teardowns = new List(); private static Dictionary _commands = new Dictionary(StringComparer.OrdinalIgnoreCase); public static Dictionary Commands => _commands; private static bool _staticInitialized; private static Dictionary _streams = new Dictionary(StringComparer.OrdinalIgnoreCase); private Dictionary> _openStreams; private ProximaDispatcher _dispatcher; private ProximaStatusUI _statusUI; private ProximaConnectUI _connectUI; private bool _wasRunInBackgroundSet; void Awake() { if (!_staticInitialized) { RegisterBuiltInCommands(); ProximaLogCommands.SetLogCapacity(_logBufferSize); _staticInitialized = true; } _dispatcher = new ProximaDispatcher(this); if (string.IsNullOrEmpty(_displayName)) { _displayName = Application.companyName + "." + Application.productName + "." + Application.version; } } void OnEnable() { if (_dontDestroyOnLoad) { DontDestroyOnLoad(gameObject); } if (_runOnEnable) { Run(); } if (_instantiateStatusUI) { _statusUI = Instantiate(Resources.Load("Proxima/ProximaStatusUI")); _statusUI.ProximaInspector = this; _statusUI.transform.SetParent(transform); } if (_instantiateConnectUI) { _connectUI = Instantiate(Resources.Load("Proxima/ProximaConnectUI")); _connectUI.ProximaInspector = this; _connectUI.GetComponent().ProximaInspector = this; _connectUI.transform.SetParent(transform); } } void OnApplicationQuit() { Stop(); } void OnDestroy() { Stop(); } void OnDisable() { Stop(); if (_statusUI) { Destroy(_statusUI.gameObject); _statusUI = null; } if (_connectUI) { Destroy(_connectUI.gameObject); _connectUI = null; } } // Starts the Proxima Server with the current configuration. public void Run() { if (_server != null) { Log.Warning("Run was called, but Proxima is already running."); return; } if (string.IsNullOrWhiteSpace(_displayName)) { Status.SetError("Display name is required to start Proxima."); Log.Error("Display name is required to start Proxima."); return; } if (string.IsNullOrWhiteSpace(_password)) { Status.SetError("Password is required to start Proxima."); Log.Error("Password is required to start Proxima."); return; } foreach (var method in _inits) { method.Invoke(null, null); } if (_setRunInBackground) { _wasRunInBackgroundSet = Application.runInBackground; Application.runInBackground = true; } Status.Reset(); Status.SetRunning(true); var remoteServerType = Type.GetType("Proxima.ProximaRemoteServer"); var demoServerType = Type.GetType("Proxima.ProximaDemoServer"); if (remoteServerType != null && _serverType == ServerTypes.Remote) { _server = (ProximaServer)Activator.CreateInstance(remoteServerType, _dispatcher, Status, _serverUrl); } #if PROXIMA_DEMO else if (demoServerType != null && _serverType == ServerTypes.Demo) { _server = (ProximaServer)Activator.CreateInstance(demoServerType, _dispatcher, Status); } #endif else { #if UNITY_WEBGL && !UNITY_EDITOR _server = new ProximaWebGLServer(_dispatcher, Status); #else _server = new ProximaEmbeddedServer(_dispatcher, Status, _port, _useHttps, _certificate, _certificatePassword); #endif } try { _server.Start(_displayName, _password); } catch (Exception e) { if (e.InnerException != null) { e = e.InnerException; } Log.Exception(e); Status.SetError(e.Message); Status.SetRunning(false); Cleanup(); } } // Stops the Proxima Server, closing any connections. public void Stop() { if (_server != null) { Log.Info("Proxima shutting down."); foreach (var method in _teardowns) { method.Invoke(null, null); } } _server?.Stop(); Status.Reset(); Cleanup(); } private void Cleanup() { if (_server != null && _setRunInBackground) { Application.runInBackground = _wasRunInBackgroundSet; } _server = null; _openStreams = null; } void Update() { _dispatcher?.InvokeAll(); if (_server == null) { return; } if (_server.TryGetMessage(out var item)) { var (connection, message) = item; var response = HandleMessage(connection, message); if (response != null) { connection.SendMessage(response); } } UpdateStreams(); } private MemoryStream HandleMessage(ProximaConnection connection, string message) { ProximaRequest request; try { request = JsonUtility.FromJson(message); } catch (Exception ex) { Log.Error("Failed to parse request: " + ex.Message); return ProximaSerialization.ErrorResponse(message, "Invalid request."); } if (request.Type == ProximaRequestType.StartStream) { return HandleStreamStartRequest(connection, request); } else if (request.Type == ProximaRequestType.StopStream) { return HandleStreamStopRequest(connection, request); } else if (request.Type == ProximaRequestType.Command) { return HandleCommand(request); } else if (request.Type == ProximaRequestType.List) { return ProximaSerialization.DataResponse(request, ""); } else if (request.Type == ProximaRequestType.Select) { return ProximaSerialization.ErrorResponse(request, "Already selected."); } else { return ProximaSerialization.ErrorResponse(request, "Invalid request type."); } } private MemoryStream HandleStreamStartRequest(ProximaConnection connection, ProximaRequest request) { var stream = request.Cmd; if (!_streams.TryGetValue(stream, out var streamInfo)) { return ProximaSerialization.ErrorResponse(request, "Invalid stream."); } if (_openStreams == null) { _openStreams = new Dictionary>(StringComparer.OrdinalIgnoreCase); } if (!_openStreams.ContainsKey(stream)) { _openStreams.Add(stream, new List()); } var guid = Guid.NewGuid().ToString(); if (streamInfo.StartMethod != null) { var args = new string[request.Args.Length + 1]; args[0] = guid; Array.Copy(request.Args, 0, args, 1, request.Args.Length); if (!TryInvoke(streamInfo.StartMethod, args, out var data, out var error)) { return ProximaSerialization.ErrorResponse(request, error); } } var openStream = new OpenStream { Connection = connection, Id = request.Id, Guid = guid, Info = streamInfo }; _openStreams[stream].Add(openStream); return null; } private MemoryStream HandleStreamStopRequest(ProximaConnection connection, ProximaRequest request) { var stream = request.Cmd; if (!_streams.TryGetValue(stream, out var streamInfo)) { return ProximaSerialization.ErrorResponse(request, "Invalid stream name."); } if (_openStreams == null) { return ProximaSerialization.ErrorResponse(request, "Stream not open. (A)"); } if (!_openStreams.TryGetValue(stream, out var listeners)) { return ProximaSerialization.ErrorResponse(request, "Stream not open. (B)"); } var idx = listeners.FindIndex(os => os.Connection == connection && os.Id == request.Id); if (idx < 0) { return ProximaSerialization.ErrorResponse(request, "Stream not open. (C)"); } var guid = listeners[idx].Guid; listeners.RemoveAt(idx); if (!TryInvoke(streamInfo.StopMethod, new string[] { guid }, out var result, out var error)) { return ProximaSerialization.ErrorResponse(request, error); } return ProximaSerialization.DataResponse(request, ""); } private MemoryStream HandleCommand(ProximaRequest request) { if (!_commands.TryGetValue(request.Cmd, out var method)) { return ProximaSerialization.ErrorResponse(request, $"Method {request.Cmd} not found."); } if (!TryInvoke(method, request.Args, out var data, out var error)) { return ProximaSerialization.ErrorResponse(request, error); } return ProximaSerialization.DataResponse(request, data); } private static StreamInfo GetOrCreateStreamInfo(string name) { if (!_streams.TryGetValue(name, out var streamInfo)) { streamInfo = new StreamInfo(); _streams.Add(name, streamInfo); } return streamInfo; } private void RegisterBuiltInCommands() { RegisterCommands(); RegisterCommands(); RegisterCommands(); RegisterCommands(); ProximaFeatures.RegisterProFeatures(); } public static void RegisterCommands() { RegisterCommands(typeof(T)); } public static void RegisterCommands(Type type) { foreach (var method in type.GetRuntimeMethods()) { if (!method.IsStatic) continue; var initAttribute = method.GetCustomAttribute(); if (initAttribute != null) { Log.Verbose("Found init: " + type.Name + "." + method.Name); _inits.Add(method); } var teardownAttribute = method.GetCustomAttribute(); if (teardownAttribute != null) { Log.Verbose("Found teardown: " + type.Name + "." + method.Name); _teardowns.Add(method); } var commandAttribute = method.GetCustomAttribute(); if (commandAttribute != null) { if (_commands.ContainsKey(method.Name)) { throw new Exception($"Multiple Proxima commands found with name {type.Name}.{method.Name}."); } Log.Verbose("Found command: " + type.Name + "." + method.Name); _commands.Add(method.Name, method); if (!string.IsNullOrWhiteSpace(commandAttribute.Alias)) { Log.Verbose("Found command alias: " + commandAttribute.Alias); _commands.Add(commandAttribute.Alias, method); } } var streamStart = method.GetCustomAttribute(); if (streamStart != null) { var streamInfo = GetOrCreateStreamInfo(streamStart.Name); if (streamInfo.StartMethod != null) { throw new Exception($"Multiple Proxima stream start methods found for stream {type.Name}.{streamStart.Name}."); } Log.Verbose($"Found stream start: {type.Name}.{streamStart.Name}"); streamInfo.StartMethod = method; } var streamStop = method.GetCustomAttribute(); if (streamStop != null) { var streamInfo = GetOrCreateStreamInfo(streamStop.Name); if (streamInfo.StopMethod != null) { throw new Exception($"Multiple Proxima stream stop methods found for stream {type.Name}.{streamStop.Name}."); } Log.Verbose($"Found stream stop: {type.Name}.{streamStop.Name}"); streamInfo.StopMethod = method; } var streamUpdate = method.GetCustomAttribute(); if (streamUpdate != null) { var streamInfo = GetOrCreateStreamInfo(streamUpdate.Name); if (streamInfo.UpdateMethod != null) { throw new Exception($"Multiple Proxima stream update methods found for stream {type.Name}.{streamUpdate.Name}."); } Log.Verbose($"Found stream update: {type.Name}.{streamUpdate.Name}"); streamInfo.UpdateMethod = method; } } } private void UpdateStreams() { if (_openStreams != null) { foreach (var stream in _openStreams) { var listeners = stream.Value; // Close streams that disconnected foreach (var listener in listeners) { if (!listener.Connection.Open) { TryInvoke(listener.Info.StopMethod, new string[] { listener.Guid }); } } listeners.RemoveAll(os => !os.Connection.Open); foreach (var listener in listeners) { object data = null; string error = null; try { data = listener.Info.UpdateMethod?.Invoke(null, new object[] { listener.Guid }); } catch (Exception e) { Debug.LogException(e); error = e.Message; } if (!string.IsNullOrEmpty(error)) { listener.Connection.SendMessage(ProximaSerialization.ErrorResponse(listener.Id, error)); } else if (data != null) { listener.Connection.SendMessage(ProximaSerialization.DataResponse(listener.Id, data)); } } } } } private bool TryInvoke(MethodInfo method, string[] args) { return TryInvoke(method, args, out var result, out var error); } private bool TryInvoke(MethodInfo method, string[] args, out object result, out string error) { result = null; error = string.Empty; if (method == null) return true; var parameters = method.GetParameters(); var values = new object[parameters.Length]; for (int i = 0; i < parameters.Length; i++) { var parameter = parameters[i]; if (args == null || args.Length <= i) { if (parameters[i].HasDefaultValue) { values[i] = parameters[i].DefaultValue; continue; } error = $"Required argument {parameter.Name} not found."; return false; } if (typeof(IPropertyOrValue).IsAssignableFrom(parameter.ParameterType)) { values[i] = Activator.CreateInstance(parameter.ParameterType, new object[] { args[i] }); continue; } if (ProximaSerialization.TryDeserialize(parameter.ParameterType, args[i], out var value)) { values[i] = value; continue; } error = $"Unable to deserialize argument {parameter.Name} as {parameter.ParameterType.Name}."; return false; } try { if (method.ReturnType == typeof(void)) { method.Invoke(null, values); } else { result = method.Invoke(null, values); } } catch (Exception e) { Log.Exception(e); error = e.InnerException.Message; return false; } return true; } } }