initial commit
This commit is contained in:
parent
6715289efe
commit
788c3389af
37645 changed files with 2526849 additions and 80 deletions
|
|
@ -0,0 +1,173 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor.AddressableAssets.Settings;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityEditor.AddressableAssets.HostingServices
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for hosting services.
|
||||
/// </summary>
|
||||
public abstract class BaseHostingService : IHostingService
|
||||
{
|
||||
const string k_HostingServiceContentRootKey = "ContentRoot";
|
||||
const string k_IsHostingServiceRunningKey = "IsEnabled";
|
||||
const string k_DescriptiveNameKey = "DescriptiveName";
|
||||
internal const string k_InstanceIdKey = "InstanceId";
|
||||
|
||||
internal bool WasEnabled { get; set; }
|
||||
|
||||
void OnPlayModeStateChanged(PlayModeStateChange obj) => SaveEnabledState();
|
||||
void OnQuitting() => SaveEnabledState();
|
||||
void SaveEnabledState() => WasEnabled = IsHostingServiceRunning;
|
||||
|
||||
internal void OnEnable()
|
||||
{
|
||||
EditorApplication.quitting += OnQuitting;
|
||||
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
|
||||
if (!IsHostingServiceRunning && WasEnabled)
|
||||
StartHostingService();
|
||||
}
|
||||
|
||||
internal void OnDisable()
|
||||
{
|
||||
SaveEnabledState();
|
||||
EditorApplication.quitting -= OnQuitting;
|
||||
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
|
||||
StopHostingService();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// List of content roots for hosting service.
|
||||
/// </summary>
|
||||
public abstract List<string> HostingServiceContentRoots { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Dictionary of profile variables defined by the hosting service.
|
||||
/// </summary>
|
||||
public abstract Dictionary<string, string> ProfileVariables { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current running status of the hosting service.
|
||||
/// </summary>
|
||||
public abstract bool IsHostingServiceRunning { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Starts the hosting service.
|
||||
/// </summary>
|
||||
public abstract void StartHostingService();
|
||||
|
||||
/// <summary>
|
||||
/// Stops the hosting service.
|
||||
/// </summary>
|
||||
public abstract void StopHostingService();
|
||||
|
||||
/// <summary>
|
||||
/// Render the hosting service GUI.
|
||||
/// </summary>
|
||||
public abstract void OnGUI();
|
||||
|
||||
ILogger m_Logger = Debug.unityLogger;
|
||||
|
||||
/// <summary>
|
||||
/// Get and set the logger for the hosting service.
|
||||
/// </summary>
|
||||
public ILogger Logger
|
||||
{
|
||||
get { return m_Logger; }
|
||||
set { m_Logger = value ?? Debug.unityLogger; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes a profile variable lookup ID based on string key
|
||||
/// </summary>
|
||||
/// <param name="key">the key to look up </param>
|
||||
/// <returns>The variable lookup ID.</returns>
|
||||
protected virtual string DisambiguateProfileVar(string key)
|
||||
{
|
||||
return string.Format("{0}.ID_{1}", key, InstanceId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual string DescriptiveName { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual int InstanceId { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual string EvaluateProfileString(string key)
|
||||
{
|
||||
string retVal;
|
||||
ProfileVariables.TryGetValue(key, out retVal);
|
||||
return retVal;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void OnBeforeSerialize(KeyDataStore dataStore)
|
||||
{
|
||||
dataStore.SetData(k_HostingServiceContentRootKey, string.Join(";", HostingServiceContentRoots.ToArray()));
|
||||
dataStore.SetData(k_IsHostingServiceRunningKey, WasEnabled);
|
||||
dataStore.SetData(k_DescriptiveNameKey, DescriptiveName);
|
||||
dataStore.SetData(k_InstanceIdKey, InstanceId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void OnAfterDeserialize(KeyDataStore dataStore)
|
||||
{
|
||||
var contentRoots = dataStore.GetData(k_HostingServiceContentRootKey, string.Empty);
|
||||
HostingServiceContentRoots.AddRange(contentRoots.Split(';'));
|
||||
WasEnabled = dataStore.GetData(k_IsHostingServiceRunningKey, false);
|
||||
DescriptiveName = dataStore.GetDataString(k_DescriptiveNameKey, string.Empty);
|
||||
InstanceId = dataStore.GetData(k_InstanceIdKey, -1);
|
||||
}
|
||||
|
||||
static T[] ArrayPush<T>(T[] arr, T val)
|
||||
{
|
||||
var newArr = new T[arr.Length + 1];
|
||||
Array.Copy(arr, newArr, arr.Length);
|
||||
newArr[newArr.Length - 1] = val;
|
||||
return newArr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs a formatted message to the Logger specifically on this service. <see cref="Logger"/>
|
||||
/// </summary>
|
||||
/// <param name="logType">Severity of the log</param>
|
||||
/// <param name="format">The base string</param>
|
||||
/// <param name="args">The parameters to be formatted into the base string</param>
|
||||
protected void LogFormat(LogType logType, string format, object[] args)
|
||||
{
|
||||
Logger.LogFormat(logType, format, ArrayPush(args, this));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs an info severity formatted message to the Logger specifically on this service. <see cref="Logger"/>
|
||||
/// </summary>
|
||||
/// <param name="format">The base string</param>
|
||||
/// <param name="args">The parameters to be formatted into the base string</param>
|
||||
protected void Log(string format, params object[] args)
|
||||
{
|
||||
LogFormat(LogType.Log, format, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs an warning severity formatted message to the Logger specifically on this service. <see cref="Logger"/>
|
||||
/// </summary>
|
||||
/// <param name="format">The base string</param>
|
||||
/// <param name="args">The parameters to be formatted into the base string</param>
|
||||
protected void LogWarning(string format, params object[] args)
|
||||
{
|
||||
LogFormat(LogType.Warning, format, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs an error severity formatted message to the Logger specifically on this service. <see cref="Logger"/>
|
||||
/// </summary>
|
||||
/// <param name="format">The base string</param>
|
||||
/// <param name="args">The parameters to be formatted into the base string</param>
|
||||
protected void LogError(string format, params object[] args)
|
||||
{
|
||||
LogFormat(LogType.Error, format, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 504a37785439743bba6a7259d90d6652
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,596 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using UnityEditor.AddressableAssets.Build.DataBuilders;
|
||||
using UnityEditor.AddressableAssets.Settings;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
// ReSharper disable DelegateSubtraction
|
||||
|
||||
namespace UnityEditor.AddressableAssets.HostingServices
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages the hosting services.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class HostingServicesManager : ISerializationCallbackReceiver
|
||||
{
|
||||
internal const string KPrivateIpAddressKey = "PrivateIpAddress";
|
||||
|
||||
internal string GetPrivateIpAddressKey(int id = 0)
|
||||
{
|
||||
if (id == 0)
|
||||
return KPrivateIpAddressKey;
|
||||
return $"{KPrivateIpAddressKey}_{id}";
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
internal class HostingServiceInfo
|
||||
{
|
||||
[SerializeField]
|
||||
internal string classRef;
|
||||
|
||||
[SerializeField]
|
||||
internal KeyDataStore dataStore;
|
||||
}
|
||||
|
||||
[FormerlySerializedAs("m_hostingServiceInfos")]
|
||||
[SerializeField]
|
||||
List<HostingServiceInfo> m_HostingServiceInfos;
|
||||
|
||||
[FormerlySerializedAs("m_settings")]
|
||||
[SerializeField]
|
||||
AddressableAssetSettings m_Settings;
|
||||
|
||||
[FormerlySerializedAs("m_nextInstanceId")]
|
||||
[SerializeField]
|
||||
int m_NextInstanceId;
|
||||
|
||||
[FormerlySerializedAs("m_registeredServiceTypeRefs")]
|
||||
[SerializeField]
|
||||
List<string> m_RegisteredServiceTypeRefs;
|
||||
|
||||
readonly Type[] m_BuiltinServiceTypes =
|
||||
{
|
||||
typeof(HttpHostingService)
|
||||
};
|
||||
|
||||
Dictionary<IHostingService, HostingServiceInfo> m_HostingServiceInfoMap;
|
||||
ILogger m_Logger;
|
||||
List<Type> m_RegisteredServiceTypes;
|
||||
|
||||
[SerializeField]
|
||||
int m_PingTimeoutInMilliseconds = 5000;
|
||||
/// <summary>
|
||||
/// Timeout in milliseconds for filtering ip addresses for the hosting service
|
||||
/// </summary>
|
||||
internal int PingTimeoutInMilliseconds
|
||||
{
|
||||
get { return m_PingTimeoutInMilliseconds; }
|
||||
set { m_PingTimeoutInMilliseconds = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Key/Value pairs valid for profile variable substitution
|
||||
/// </summary>
|
||||
public Dictionary<string, string> GlobalProfileVariables { get; private set; }
|
||||
|
||||
internal static readonly string k_GlobalProfileVariablesCountKey = $"com.unity.addressables.{nameof(GlobalProfileVariables)}Count";
|
||||
|
||||
internal static string GetSessionStateKey(int id)
|
||||
{
|
||||
return $"com.unity.addressables.{nameof(GlobalProfileVariables)}{id}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct logging output of all managed services
|
||||
/// </summary>
|
||||
public ILogger Logger
|
||||
{
|
||||
get { return m_Logger; }
|
||||
set
|
||||
{
|
||||
m_Logger = value ?? Debug.unityLogger;
|
||||
foreach (var svc in HostingServices)
|
||||
svc.Logger = m_Logger;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Static method for use in starting up the HostingServicesManager in batch mode.
|
||||
/// </summary>
|
||||
/// <param name="settings"> </param>
|
||||
public static void BatchMode(AddressableAssetSettings settings)
|
||||
{
|
||||
if (settings == null)
|
||||
{
|
||||
Debug.LogError("Could not load Addressable Assets settings - aborting.");
|
||||
return;
|
||||
}
|
||||
|
||||
var manager = settings.HostingServicesManager;
|
||||
if (manager == null)
|
||||
{
|
||||
Debug.LogError("Could not load HostingServicesManager - aborting.");
|
||||
return;
|
||||
}
|
||||
|
||||
manager.StartAllServices();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Static method for use in starting up the HostingServicesManager in batch mode. This method
|
||||
/// without parameters will find and use the default <see cref="AddressableAssetSettings"/> object.
|
||||
/// </summary>
|
||||
public static void BatchMode()
|
||||
{
|
||||
BatchMode(AddressableAssetSettingsDefaultObject.Settings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether or not this HostingServiceManager is initialized
|
||||
/// </summary>
|
||||
public bool IsInitialized
|
||||
{
|
||||
get { return m_Settings != null; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return an enumerable list of all configured <see cref="IHostingService"/> objects
|
||||
/// </summary>
|
||||
public ICollection<IHostingService> HostingServices
|
||||
{
|
||||
get { return m_HostingServiceInfoMap.Keys; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an array of all <see cref="IHostingService"/> types that have been used by the manager, or are known
|
||||
/// built-in types available for use.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Type[] RegisteredServiceTypes
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_RegisteredServiceTypes.Count == 0)
|
||||
m_RegisteredServiceTypes.AddRange(m_BuiltinServiceTypes);
|
||||
|
||||
return m_RegisteredServiceTypes.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The id value that will be assigned to the next <see cref="IHostingService"/> add to the manager.
|
||||
/// </summary>
|
||||
public int NextInstanceId
|
||||
{
|
||||
get { return m_NextInstanceId; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="HostingServicesManager"/>
|
||||
/// </summary>
|
||||
public HostingServicesManager()
|
||||
{
|
||||
GlobalProfileVariables = new Dictionary<string, string>();
|
||||
m_HostingServiceInfos = new List<HostingServiceInfo>();
|
||||
m_HostingServiceInfoMap = new Dictionary<IHostingService, HostingServiceInfo>();
|
||||
m_RegisteredServiceTypes = new List<Type>();
|
||||
m_RegisteredServiceTypeRefs = new List<string>();
|
||||
m_Logger = Debug.unityLogger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize manager with the given <see cref="AddressableAssetSettings"/> object.
|
||||
/// </summary>
|
||||
/// <param name="settings"></param>
|
||||
public void Initialize(AddressableAssetSettings settings)
|
||||
{
|
||||
if (IsInitialized) return;
|
||||
m_Settings = settings;
|
||||
RefreshGlobalProfileVariables();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="IHostingService.StopHostingService"/> on all managed <see cref="IHostingService"/> instances
|
||||
/// where <see cref="IHostingService.IsHostingServiceRunning"/> is true
|
||||
/// </summary>
|
||||
public void StopAllServices()
|
||||
{
|
||||
foreach (var svc in HostingServices)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (svc.IsHostingServiceRunning)
|
||||
svc.StopHostingService();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
m_Logger.LogFormat(LogType.Error, e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="IHostingService.StartHostingService"/> on all managed <see cref="IHostingService"/> instances
|
||||
/// where <see cref="IHostingService.IsHostingServiceRunning"/> is false
|
||||
/// </summary>
|
||||
public void StartAllServices()
|
||||
{
|
||||
foreach (var svc in HostingServices)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!svc.IsHostingServiceRunning)
|
||||
svc.StartHostingService();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
m_Logger.LogFormat(LogType.Error, e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a new hosting service instance of the given type. The <paramref name="serviceType"/> must implement the
|
||||
/// <see cref="IHostingService"/> interface, or an <see cref="ArgumentException"/> is thrown.
|
||||
/// </summary>
|
||||
/// <param name="serviceType">A <see cref="Type"/> object for the service. Must implement <see cref="IHostingService"/></param>
|
||||
/// <param name="name">A descriptive name for the new service instance.</param>
|
||||
/// <returns></returns>
|
||||
public IHostingService AddHostingService(Type serviceType, string name)
|
||||
{
|
||||
var svc = Activator.CreateInstance(serviceType) as IHostingService;
|
||||
if (svc == null)
|
||||
throw new ArgumentException("Provided type does not implement IHostingService", "serviceType");
|
||||
|
||||
if (!m_RegisteredServiceTypes.Contains(serviceType))
|
||||
m_RegisteredServiceTypes.Add(serviceType);
|
||||
|
||||
var info = new HostingServiceInfo
|
||||
{
|
||||
classRef = TypeToClassRef(serviceType),
|
||||
dataStore = new KeyDataStore()
|
||||
};
|
||||
|
||||
svc.Logger = m_Logger;
|
||||
svc.DescriptiveName = name;
|
||||
svc.InstanceId = m_NextInstanceId;
|
||||
svc.HostingServiceContentRoots.AddRange(GetAllContentRoots());
|
||||
m_Settings.profileSettings.RegisterProfileStringEvaluationFunc(svc.EvaluateProfileString);
|
||||
|
||||
m_HostingServiceInfoMap.Add(svc, info);
|
||||
m_Settings.SetDirty(AddressableAssetSettings.ModificationEvent.HostingServicesManagerModified, this, true, true);
|
||||
AddressableAssetUtility.OpenAssetIfUsingVCIntegration(m_Settings);
|
||||
|
||||
m_NextInstanceId++;
|
||||
return svc;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the given <see cref="IHostingService"/>, unregisters callbacks, and removes it from management. This
|
||||
/// function does nothing if the service is not being managed by this <see cref="HostingServicesManager"/>
|
||||
/// </summary>
|
||||
/// <param name="svc"></param>
|
||||
public void RemoveHostingService(IHostingService svc)
|
||||
{
|
||||
if (!m_HostingServiceInfoMap.ContainsKey(svc))
|
||||
return;
|
||||
|
||||
svc.StopHostingService();
|
||||
m_Settings.profileSettings.UnregisterProfileStringEvaluationFunc(svc.EvaluateProfileString);
|
||||
m_HostingServiceInfoMap.Remove(svc);
|
||||
m_Settings.SetDirty(AddressableAssetSettings.ModificationEvent.HostingServicesManagerModified, this, true, true);
|
||||
AddressableAssetUtility.OpenAssetIfUsingVCIntegration(m_Settings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Should be called by parent <see cref="ScriptableObject"/> instance Awake method
|
||||
/// </summary>
|
||||
internal void OnAwake()
|
||||
{
|
||||
RefreshGlobalProfileVariables();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Should be called by parent <see cref="ScriptableObject"/> instance OnEnable method
|
||||
/// </summary>
|
||||
public void OnEnable()
|
||||
{
|
||||
Debug.Assert(IsInitialized);
|
||||
|
||||
m_Settings.OnModification -= OnSettingsModification;
|
||||
m_Settings.OnModification += OnSettingsModification;
|
||||
m_Settings.profileSettings.RegisterProfileStringEvaluationFunc(EvaluateGlobalProfileVariableKey);
|
||||
|
||||
// GetAllContentRoots can return unpredictable results when there are no hosting services
|
||||
if (HostingServices.Count > 0)
|
||||
{
|
||||
var contentRoots = GetAllContentRoots();
|
||||
foreach (var svc in HostingServices)
|
||||
{
|
||||
|
||||
svc.Logger = m_Logger;
|
||||
m_Settings.profileSettings.RegisterProfileStringEvaluationFunc(svc.EvaluateProfileString);
|
||||
var baseSvc = svc as BaseHostingService;
|
||||
svc.HostingServiceContentRoots.Clear();
|
||||
svc.HostingServiceContentRoots.AddRange(contentRoots);
|
||||
baseSvc?.OnEnable();
|
||||
}
|
||||
}
|
||||
|
||||
LoadSessionStateKeysIfExists();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Should be called by parent <see cref="ScriptableObject"/> instance OnDisable method
|
||||
/// </summary>
|
||||
public void OnDisable()
|
||||
{
|
||||
Debug.Assert(IsInitialized);
|
||||
|
||||
// ReSharper disable once DelegateSubtraction
|
||||
m_Settings.OnModification -= OnSettingsModification;
|
||||
m_Settings.profileSettings.UnregisterProfileStringEvaluationFunc(EvaluateGlobalProfileVariableKey);
|
||||
foreach (var svc in HostingServices)
|
||||
{
|
||||
svc.Logger = null;
|
||||
m_Settings.profileSettings.UnregisterProfileStringEvaluationFunc(svc.EvaluateProfileString);
|
||||
(svc as BaseHostingService)?.OnDisable();
|
||||
}
|
||||
|
||||
SaveSessionStateKeys();
|
||||
}
|
||||
|
||||
internal void LoadSessionStateKeysIfExists()
|
||||
{
|
||||
int numKeys = SessionState.GetInt(k_GlobalProfileVariablesCountKey, 0);
|
||||
if (numKeys > 0)
|
||||
GlobalProfileVariables.Clear();
|
||||
|
||||
for (int i = 0; i < numKeys; i++)
|
||||
{
|
||||
string profileVar = SessionState.GetString(GetSessionStateKey(i), string.Empty);
|
||||
if (!string.IsNullOrEmpty(profileVar))
|
||||
GlobalProfileVariables.Add(GetPrivateIpAddressKey(i), profileVar);
|
||||
}
|
||||
}
|
||||
|
||||
internal void SaveSessionStateKeys()
|
||||
{
|
||||
int prevNumKeys = SessionState.GetInt(k_GlobalProfileVariablesCountKey, 0);
|
||||
SessionState.SetInt(k_GlobalProfileVariablesCountKey, GlobalProfileVariables.Count);
|
||||
|
||||
int profileVarIdx = 0;
|
||||
foreach (KeyValuePair<string, string> pair in GlobalProfileVariables)
|
||||
{
|
||||
SessionState.SetString(GetSessionStateKey(profileVarIdx), pair.Value);
|
||||
profileVarIdx++;
|
||||
}
|
||||
|
||||
EraseSessionStateKeys(profileVarIdx, prevNumKeys);
|
||||
}
|
||||
|
||||
internal static void EraseSessionStateKeys()
|
||||
{
|
||||
int numKeys = SessionState.GetInt(k_GlobalProfileVariablesCountKey, 0);
|
||||
EraseSessionStateKeys(0, numKeys);
|
||||
SessionState.EraseInt(k_GlobalProfileVariablesCountKey);
|
||||
}
|
||||
|
||||
static void EraseSessionStateKeys(int min, int max)
|
||||
{
|
||||
for (int i = min; i < max; i++)
|
||||
{
|
||||
SessionState.EraseString(GetSessionStateKey(i));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Ensure object is ready for serialization, and calls <see cref="IHostingService.OnBeforeSerialize"/> methods
|
||||
/// on all managed <see cref="IHostingService"/> instances
|
||||
/// </summary>
|
||||
public void OnBeforeSerialize()
|
||||
{
|
||||
// https://docs.unity3d.com/ScriptReference/EditorWindow.OnInspectorUpdate.html
|
||||
// Because the manager is a serialized field in the Addressables settings, this method is called
|
||||
// at 10 frames per second when the settings are opened in the inspector...
|
||||
// Be careful what you put in there...
|
||||
|
||||
m_HostingServiceInfos.Clear();
|
||||
foreach (var svc in HostingServices)
|
||||
{
|
||||
var info = m_HostingServiceInfoMap[svc];
|
||||
m_HostingServiceInfos.Add(info);
|
||||
svc.OnBeforeSerialize(info.dataStore);
|
||||
}
|
||||
|
||||
m_RegisteredServiceTypeRefs.Clear();
|
||||
foreach (var type in m_RegisteredServiceTypes)
|
||||
m_RegisteredServiceTypeRefs.Add(TypeToClassRef(type));
|
||||
}
|
||||
|
||||
/// <summary> Ensure object is ready for serialization, and calls <see cref="IHostingService.OnBeforeSerialize"/> methods
|
||||
/// on all managed <see cref="IHostingService"/> instances
|
||||
/// </summary>
|
||||
public void OnAfterDeserialize()
|
||||
{
|
||||
m_HostingServiceInfoMap = new Dictionary<IHostingService, HostingServiceInfo>();
|
||||
foreach (var svcInfo in m_HostingServiceInfos)
|
||||
{
|
||||
IHostingService svc = CreateHostingServiceInstance(svcInfo.classRef);
|
||||
|
||||
if (svc == null) continue;
|
||||
svc.OnAfterDeserialize(svcInfo.dataStore);
|
||||
m_HostingServiceInfoMap.Add(svc, svcInfo);
|
||||
}
|
||||
|
||||
m_RegisteredServiceTypes = new List<Type>();
|
||||
foreach (var typeRef in m_RegisteredServiceTypeRefs)
|
||||
{
|
||||
var type = Type.GetType(typeRef, false);
|
||||
if (type == null) continue;
|
||||
m_RegisteredServiceTypes.Add(type);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refresh values in the global profile variables table.
|
||||
/// </summary>
|
||||
public void RefreshGlobalProfileVariables()
|
||||
{
|
||||
var vars = GlobalProfileVariables;
|
||||
vars.Clear();
|
||||
|
||||
var ipAddressList = FilterValidIPAddresses(NetworkInterface.GetAllNetworkInterfaces()
|
||||
.Where(n => n.NetworkInterfaceType != NetworkInterfaceType.Loopback && n.OperationalStatus == OperationalStatus.Up)
|
||||
.SelectMany(n => n.GetIPProperties().UnicastAddresses)
|
||||
.Where(a => a.Address.AddressFamily == AddressFamily.InterNetwork)
|
||||
.Select(a => a.Address).ToList());
|
||||
|
||||
if (ipAddressList.Count > 0)
|
||||
{
|
||||
vars.Add(KPrivateIpAddressKey, ipAddressList[0].ToString());
|
||||
|
||||
if (ipAddressList.Count > 1)
|
||||
{
|
||||
for (var i = 1; i < ipAddressList.Count; i++)
|
||||
vars.Add(KPrivateIpAddressKey + "_" + i, ipAddressList[i].ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Internal for unit tests
|
||||
internal string EvaluateGlobalProfileVariableKey(string key)
|
||||
{
|
||||
string retVal;
|
||||
GlobalProfileVariables.TryGetValue(key, out retVal);
|
||||
return retVal;
|
||||
}
|
||||
|
||||
void OnSettingsModification(AddressableAssetSettings s, AddressableAssetSettings.ModificationEvent evt, object obj)
|
||||
{
|
||||
switch (evt)
|
||||
{
|
||||
case AddressableAssetSettings.ModificationEvent.GroupAdded:
|
||||
case AddressableAssetSettings.ModificationEvent.GroupRemoved:
|
||||
case AddressableAssetSettings.ModificationEvent.GroupSchemaAdded:
|
||||
case AddressableAssetSettings.ModificationEvent.GroupSchemaRemoved:
|
||||
case AddressableAssetSettings.ModificationEvent.GroupSchemaModified:
|
||||
case AddressableAssetSettings.ModificationEvent.ActiveProfileSet:
|
||||
case AddressableAssetSettings.ModificationEvent.BuildSettingsChanged:
|
||||
case AddressableAssetSettings.ModificationEvent.ProfileModified:
|
||||
var profileRemoteBuildPath = m_Settings.profileSettings.GetValueByName(m_Settings.activeProfileId, AddressableAssetSettings.kRemoteBuildPath);
|
||||
if (profileRemoteBuildPath != null && (profileRemoteBuildPath.Contains('[') || !CurrentContentRootsContain(profileRemoteBuildPath)))
|
||||
ConfigureAllHostingServices();
|
||||
break;
|
||||
case AddressableAssetSettings.ModificationEvent.BatchModification:
|
||||
ConfigureAllHostingServices();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool CurrentContentRootsContain(string root)
|
||||
{
|
||||
foreach (var svc in HostingServices)
|
||||
{
|
||||
if (!svc.HostingServiceContentRoots.Contains(root))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ConfigureAllHostingServices()
|
||||
{
|
||||
if (HostingServices.Count > 0)
|
||||
{
|
||||
var contentRoots = GetAllContentRoots();
|
||||
|
||||
foreach (var svc in HostingServices)
|
||||
{
|
||||
svc.HostingServiceContentRoots.Clear();
|
||||
svc.HostingServiceContentRoots.AddRange(contentRoots);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string[] GetAllContentRoots()
|
||||
{
|
||||
Debug.Assert(IsInitialized);
|
||||
|
||||
var contentRoots = new List<string>();
|
||||
foreach (var group in m_Settings.groups)
|
||||
{
|
||||
if (group != null)
|
||||
{
|
||||
foreach (var schema in group.Schemas)
|
||||
{
|
||||
var configProvider = schema as IHostingServiceConfigurationProvider;
|
||||
if (configProvider != null)
|
||||
{
|
||||
var groupRoot = configProvider.HostingServicesContentRoot;
|
||||
if (groupRoot != null && !contentRoots.Contains(groupRoot))
|
||||
contentRoots.Add(groupRoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return contentRoots.ToArray();
|
||||
}
|
||||
|
||||
IHostingService CreateHostingServiceInstance(string classRef)
|
||||
{
|
||||
try
|
||||
{
|
||||
var objType = Type.GetType(classRef, true);
|
||||
var svc = (IHostingService)Activator.CreateInstance(objType);
|
||||
return svc;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
m_Logger.LogFormat(LogType.Error, "Could not create IHostingService from class ref '{0}'", classRef);
|
||||
m_Logger.LogFormat(LogType.Error, e.Message);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static string TypeToClassRef(Type t)
|
||||
{
|
||||
return string.Format("{0}, {1}", t.FullName, t.Assembly.GetName().Name);
|
||||
}
|
||||
|
||||
// For unit tests
|
||||
internal AddressableAssetSettings Settings
|
||||
{
|
||||
get { return m_Settings; }
|
||||
}
|
||||
|
||||
private List<IPAddress> FilterValidIPAddresses(List<IPAddress> ipAddresses)
|
||||
{
|
||||
List<IPAddress> validIpList = new List<IPAddress>();
|
||||
if (PingTimeoutInMilliseconds < 0)
|
||||
{
|
||||
m_Logger.LogFormat(LogType.Error, "Cannot filter IP addresses. Timeout must be a non-negative integer.");
|
||||
return validIpList;
|
||||
}
|
||||
|
||||
foreach (IPAddress address in ipAddresses)
|
||||
{
|
||||
var sender = new System.Net.NetworkInformation.Ping();
|
||||
var reply = sender.Send(address.ToString(), PingTimeoutInMilliseconds);
|
||||
if (reply.Status == IPStatus.Success)
|
||||
{
|
||||
validIpList.Add(address);
|
||||
}
|
||||
}
|
||||
|
||||
return validIpList;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 66a7a5932b5cd4dceb8261d66734b440
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,595 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using UnityEditor.AddressableAssets.Settings;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
|
||||
namespace UnityEditor.AddressableAssets.HostingServices
|
||||
{
|
||||
// ReSharper disable once ClassWithVirtualMembersNeverInherited.Global
|
||||
/// <summary>
|
||||
/// HTTP implementation of hosting service.
|
||||
/// </summary>
|
||||
public class HttpHostingService : BaseHostingService
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for standard Http result codes
|
||||
/// </summary>
|
||||
protected enum ResultCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Use to indicate that the request succeeded.
|
||||
/// </summary>
|
||||
Ok = 200,
|
||||
|
||||
/// <summary>
|
||||
/// Use to indicate that the requested resource could not be found.
|
||||
/// </summary>
|
||||
NotFound = 404
|
||||
}
|
||||
|
||||
internal interface IHttpContext
|
||||
{
|
||||
Uri GetRequestUrl();
|
||||
void SetResponseContentType(string contentType);
|
||||
void SetResponseContentLength(long contentLength);
|
||||
Stream GetResponseOutputStream();
|
||||
}
|
||||
|
||||
// this class exists to make testing with mocks simpler
|
||||
internal class HttpListenerContextWrapper : IHttpContext
|
||||
{
|
||||
HttpListenerContext m_Context;
|
||||
public HttpListenerContextWrapper(HttpListenerContext context)
|
||||
{
|
||||
m_Context = context;
|
||||
}
|
||||
public Uri GetRequestUrl()
|
||||
{
|
||||
return m_Context.Request.Url;
|
||||
}
|
||||
|
||||
public void SetResponseContentType(string contentType)
|
||||
{
|
||||
m_Context.Response.ContentType = contentType;
|
||||
}
|
||||
|
||||
public void SetResponseContentLength(long contentLength)
|
||||
{
|
||||
m_Context.Response.ContentLength64 = contentLength;
|
||||
}
|
||||
|
||||
public Stream GetResponseOutputStream()
|
||||
{
|
||||
return m_Context.Response.OutputStream;
|
||||
}
|
||||
}
|
||||
|
||||
internal class FileUploadOperation
|
||||
{
|
||||
IHttpContext m_Context;
|
||||
byte[] m_ReadByteBuffer;
|
||||
FileStream m_ReadFileStream;
|
||||
long m_TotalBytesRead;
|
||||
bool m_IsDone;
|
||||
private Timer m_UpdateTimer;
|
||||
private int m_UploadSpeed;
|
||||
private TimeSpan m_SleepTime = TimeSpan.FromMilliseconds(250);
|
||||
private Action m_Cleanup;
|
||||
public bool IsDone => m_IsDone;
|
||||
|
||||
|
||||
|
||||
public FileUploadOperation(HttpListenerContext context, string filePath, int uploadSpeed, Action cleanup) : this(new HttpListenerContextWrapper(context), filePath, uploadSpeed, cleanup)
|
||||
{
|
||||
}
|
||||
internal FileUploadOperation(IHttpContext context, string filePath, int uploadSpeed, Action cleanup)
|
||||
{
|
||||
m_Context = context;
|
||||
m_UploadSpeed = uploadSpeed;
|
||||
m_Cleanup = cleanup;
|
||||
m_ReadByteBuffer = new byte[k_FileReadBufferSize];
|
||||
try
|
||||
{
|
||||
m_ReadFileStream = File.OpenRead(filePath);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
m_IsDone = true;
|
||||
m_Cleanup();
|
||||
Debug.LogException(e);
|
||||
throw;
|
||||
}
|
||||
|
||||
m_Context.SetResponseContentType("application/octet-stream");
|
||||
m_Context.SetResponseContentLength(m_ReadFileStream.Length);
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
m_UpdateTimer = new Timer(
|
||||
callback: this.Update,
|
||||
state: null,
|
||||
dueTime: TimeSpan.Zero,
|
||||
period: m_SleepTime);
|
||||
}
|
||||
|
||||
public void Update(object stateInfo)
|
||||
{
|
||||
if (m_Context == null || m_ReadFileStream == null)
|
||||
return;
|
||||
|
||||
int countToRead = (int)(m_UploadSpeed * m_SleepTime.TotalSeconds);
|
||||
|
||||
try
|
||||
{
|
||||
while (countToRead > 0)
|
||||
{
|
||||
int count = countToRead > m_ReadByteBuffer.Length ? m_ReadByteBuffer.Length : countToRead;
|
||||
int read = m_ReadFileStream.Read(m_ReadByteBuffer, 0, count);
|
||||
m_Context.GetResponseOutputStream().Write(m_ReadByteBuffer, 0, read);
|
||||
m_TotalBytesRead += read;
|
||||
countToRead -= count;
|
||||
|
||||
if (m_TotalBytesRead == m_ReadFileStream.Length)
|
||||
{
|
||||
Stop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
string url = m_Context.GetRequestUrl().ToString();
|
||||
Stop();
|
||||
if (e.InnerException != null && e.InnerException is SocketException &&
|
||||
e.InnerException.Message == "The socket has been shut down")
|
||||
{
|
||||
Addressables.LogWarning($"Connection lost: {url}. The socket has been shut down.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Addressables.LogException(e);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (m_IsDone)
|
||||
{
|
||||
Debug.LogError("FileUploadOperation has already completed.");
|
||||
return;
|
||||
}
|
||||
|
||||
m_IsDone = true;
|
||||
// when running tests this may be null
|
||||
if (m_UpdateTimer != null)
|
||||
{
|
||||
m_UpdateTimer.Dispose();
|
||||
}
|
||||
|
||||
m_ReadFileStream.Dispose();
|
||||
m_ReadFileStream = null;
|
||||
m_Context.GetResponseOutputStream().Flush();
|
||||
m_Context.GetResponseOutputStream().Close();
|
||||
m_Context = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
m_Cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const string k_HostingServicePortKey = "HostingServicePort";
|
||||
const int k_FileReadBufferSize = 64 * 1024;
|
||||
|
||||
internal const int k_OneGBPS = 1024 * 1024 * 1024;
|
||||
const string k_UploadSpeedKey = "HostingServiceUploadSpeed";
|
||||
int m_UploadSpeed;
|
||||
double m_LastFrameTime;
|
||||
|
||||
internal List<FileUploadOperation> m_ActiveUploads = new List<FileUploadOperation>();
|
||||
|
||||
static readonly IPEndPoint k_DefaultLoopbackEndpoint = new IPEndPoint(IPAddress.Loopback, 0);
|
||||
int m_ServicePort;
|
||||
readonly List<string> m_ContentRoots;
|
||||
readonly Dictionary<string, string> m_ProfileVariables;
|
||||
|
||||
GUIContent m_UploadSpeedGUI =
|
||||
new GUIContent("Upload Speed (Kb/s)", "Speed in Kb/s the hosting service will upload content. 0 for no limit");
|
||||
|
||||
GUIContent m_PortNumberGUI =
|
||||
new GUIContent("Port", "Port number used by the service");
|
||||
|
||||
GUIContent m_ResetPortGUI =
|
||||
new GUIContent("Reset", "Selects the next available port. Value will remain unchanged if no other port is available");
|
||||
|
||||
// ReSharper disable once MemberCanBePrivate.Global
|
||||
/// <summary>
|
||||
/// The actual Http listener used by this service
|
||||
/// </summary>
|
||||
protected HttpListener MyHttpListener { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The port number on which the service is listening
|
||||
/// </summary>
|
||||
// ReSharper disable once MemberCanBePrivate.Global
|
||||
public int HostingServicePort
|
||||
{
|
||||
get { return m_ServicePort; }
|
||||
protected set
|
||||
{
|
||||
if (value > 0)
|
||||
m_ServicePort = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The upload speed that files were be served at, in kbps
|
||||
/// </summary>
|
||||
public int UploadSpeed
|
||||
{
|
||||
get => m_UploadSpeed;
|
||||
set => m_UploadSpeed = value > 0 ? value > int.MaxValue / 1024 ? int.MaxValue / 1024 : value : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Files that are currently being uploaded
|
||||
/// </summary>
|
||||
internal List<FileUploadOperation> ActiveOperations => m_ActiveUploads;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsHostingServiceRunning
|
||||
{
|
||||
get { return MyHttpListener != null && MyHttpListener.IsListening; }
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override List<string> HostingServiceContentRoots
|
||||
{
|
||||
get { return m_ContentRoots; }
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Dictionary<string, string> ProfileVariables
|
||||
{
|
||||
get
|
||||
{
|
||||
m_ProfileVariables[k_HostingServicePortKey] = HostingServicePort.ToString();
|
||||
m_ProfileVariables[DisambiguateProfileVar(k_HostingServicePortKey)] = HostingServicePort.ToString();
|
||||
return m_ProfileVariables;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="HttpHostingService"/>
|
||||
/// </summary>
|
||||
public HttpHostingService()
|
||||
{
|
||||
m_ProfileVariables = new Dictionary<string, string>();
|
||||
m_ContentRoots = new List<string>();
|
||||
MyHttpListener = new HttpListener();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Destroys a <see cref="HttpHostingService"/>
|
||||
/// </summary>
|
||||
~HttpHostingService()
|
||||
{
|
||||
StopHostingService();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void StartHostingService()
|
||||
{
|
||||
if (IsHostingServiceRunning)
|
||||
return;
|
||||
|
||||
if (HostingServicePort <= 0)
|
||||
{
|
||||
HostingServicePort = GetAvailablePort();
|
||||
if (HostingServicePort == 0)
|
||||
{
|
||||
LogError("Failed to get an available port, cannot start service!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (!IsPortAvailable(HostingServicePort))
|
||||
{
|
||||
LogError("Port {0} is in use, cannot start service!", HostingServicePort);
|
||||
return;
|
||||
}
|
||||
|
||||
if (HostingServiceContentRoots.Count == 0)
|
||||
{
|
||||
throw new Exception(
|
||||
"ContentRoot is not configured; cannot start service. This can usually be fixed by modifying the BuildPath for any new groups and/or building content.");
|
||||
}
|
||||
|
||||
ConfigureHttpListener();
|
||||
MyHttpListener.Start();
|
||||
MyHttpListener.BeginGetContext(HandleRequest, null);
|
||||
|
||||
var count = HostingServiceContentRoots.Count;
|
||||
Log("Started. Listening on port {0}. Hosting {1} folder{2}.", HostingServicePort, count, count > 1 ? "s" : string.Empty);
|
||||
foreach (var root in HostingServiceContentRoots)
|
||||
{
|
||||
Log("Hosting : {0}", root);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Temporarily stops the service from receiving requests.
|
||||
/// </summary>
|
||||
public override void StopHostingService()
|
||||
{
|
||||
if (!IsHostingServiceRunning) return;
|
||||
Log("Stopping");
|
||||
MyHttpListener.Stop();
|
||||
// Abort() is the method we want instead of Close(), because the former frees up resources without
|
||||
// disposing the object.
|
||||
MyHttpListener.Abort();
|
||||
|
||||
foreach (FileUploadOperation operation in m_ActiveUploads)
|
||||
operation.Stop();
|
||||
m_ActiveUploads.Clear();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnGUI()
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
{
|
||||
var newPort = EditorGUILayout.DelayedIntField(m_PortNumberGUI, HostingServicePort);
|
||||
if (newPort != HostingServicePort)
|
||||
{
|
||||
if (IsPortAvailable(newPort))
|
||||
{
|
||||
ResetListenPort(newPort);
|
||||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||||
if (settings != null)
|
||||
settings.SetDirty(AddressableAssetSettings.ModificationEvent.HostingServicesManagerModified, this, false, true);
|
||||
}
|
||||
else
|
||||
LogError("Cannot listen on port {0}; port is in use", newPort);
|
||||
}
|
||||
|
||||
if (GUILayout.Button(m_ResetPortGUI, GUILayout.ExpandWidth(false)))
|
||||
ResetListenPort();
|
||||
|
||||
//GUILayout.Space(rect.width / 2f);
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
UploadSpeed = EditorGUILayout.IntField(m_UploadSpeedGUI, UploadSpeed);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnBeforeSerialize(KeyDataStore dataStore)
|
||||
{
|
||||
dataStore.SetData(k_HostingServicePortKey, HostingServicePort);
|
||||
dataStore.SetData(k_UploadSpeedKey, m_UploadSpeed);
|
||||
base.OnBeforeSerialize(dataStore);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnAfterDeserialize(KeyDataStore dataStore)
|
||||
{
|
||||
HostingServicePort = dataStore.GetData(k_HostingServicePortKey, 0);
|
||||
UploadSpeed = dataStore.GetData(k_UploadSpeedKey, 0);
|
||||
base.OnAfterDeserialize(dataStore);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Listen on a new port the next time the server starts. If the server is already running, it will be stopped
|
||||
/// and restarted automatically.
|
||||
/// </summary>
|
||||
/// <param name="port">Specify a port to listen on. Default is 0 to choose any open port</param>
|
||||
// ReSharper disable once MemberCanBePrivate.Global
|
||||
public void ResetListenPort(int port = 0)
|
||||
{
|
||||
var isRunning = IsHostingServiceRunning;
|
||||
bool autoPickPort = port == 0;
|
||||
var newPort = autoPickPort ? GetAvailablePort() : port;
|
||||
StopHostingService();
|
||||
|
||||
if (autoPickPort)
|
||||
{
|
||||
var oldPort = HostingServicePort;
|
||||
HostingServicePort = newPort;
|
||||
if (HostingServicePort == 0)
|
||||
{
|
||||
HostingServicePort = oldPort;
|
||||
LogError("No other port available. Unable to change hosting port.");
|
||||
}
|
||||
}
|
||||
else
|
||||
HostingServicePort = newPort;
|
||||
|
||||
if (isRunning)
|
||||
StartHostingService();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles any configuration necessary for <see cref="MyHttpListener"/> before listening for connections.
|
||||
/// </summary>
|
||||
protected virtual void ConfigureHttpListener()
|
||||
{
|
||||
try
|
||||
{
|
||||
MyHttpListener.Prefixes.Clear();
|
||||
MyHttpListener.Prefixes.Add("http://+:" + HostingServicePort + "/");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronous callback to handle a client connection request on <see cref="MyHttpListener"/>. This method is
|
||||
/// recursive in that it will call itself immediately after receiving a new incoming request to listen for the
|
||||
/// next connection.
|
||||
/// </summary>
|
||||
/// <param name="ar">Asynchronous result from previous request. Pass null to listen for an initial request</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">thrown when the request result code is unknown</exception>
|
||||
protected virtual void HandleRequest(IAsyncResult ar)
|
||||
{
|
||||
|
||||
if (!IsHostingServiceRunning)
|
||||
return;
|
||||
|
||||
// finish this request
|
||||
var c = MyHttpListener.EndGetContext(ar);
|
||||
|
||||
// start waiting for the next request
|
||||
MyHttpListener.BeginGetContext(HandleRequest, null);
|
||||
|
||||
var relativePath = c.Request.Url.LocalPath.Substring(1);
|
||||
|
||||
var fullPath = FindFileInContentRoots(relativePath);
|
||||
var result = fullPath != null ? ResultCode.Ok : ResultCode.NotFound;
|
||||
var info = fullPath != null ? new FileInfo(fullPath) : null;
|
||||
var size = info != null ? info.Length.ToString() : "-";
|
||||
var remoteAddress = c.Request.RemoteEndPoint != null ? c.Request.RemoteEndPoint.Address : null;
|
||||
var timestamp = DateTime.Now.ToString("o");
|
||||
|
||||
Log("{0} - - [{1}] \"{2}\" {3} {4}", remoteAddress, timestamp, fullPath, (int)result, size);
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case ResultCode.Ok:
|
||||
ReturnFile(c, fullPath);
|
||||
break;
|
||||
case ResultCode.NotFound:
|
||||
Return404(c);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for the given relative path within the configured content root directores.
|
||||
/// </summary>
|
||||
/// <param name="relativePath"></param>
|
||||
/// <returns>The full system path to the file if found, or null if file could not be found</returns>
|
||||
protected virtual string FindFileInContentRoots(string relativePath)
|
||||
{
|
||||
relativePath = relativePath.TrimStart('/');
|
||||
relativePath = relativePath.TrimStart('\\');
|
||||
foreach (var root in HostingServiceContentRoots)
|
||||
{
|
||||
var fullPath = Path.Combine(root, relativePath).Replace('\\', '/');
|
||||
if (File.Exists(fullPath))
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a file to the connected HTTP client
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="filePath"></param>
|
||||
/// <param name="readBufferSize"></param>
|
||||
protected virtual void ReturnFile(HttpListenerContext context, string filePath, int readBufferSize = k_FileReadBufferSize)
|
||||
{
|
||||
if (m_UploadSpeed > 0)
|
||||
{
|
||||
// enqueue throttled download
|
||||
var op = new FileUploadOperation(context, filePath, m_UploadSpeed, Cleanup);
|
||||
op.Start();
|
||||
m_ActiveUploads.Add(op);
|
||||
return;
|
||||
}
|
||||
context.Response.ContentType = "application/octet-stream";
|
||||
|
||||
var buffer = new byte[readBufferSize];
|
||||
using (var fs = File.OpenRead(filePath))
|
||||
{
|
||||
context.Response.ContentLength64 = fs.Length;
|
||||
int read;
|
||||
while ((read = fs.Read(buffer, 0, buffer.Length)) > 0)
|
||||
context.Response.OutputStream.Write(buffer, 0, read);
|
||||
}
|
||||
|
||||
context.Response.OutputStream.Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the status code to 404 on the given <c>HttpListenerContext</c> object.
|
||||
/// </summary>
|
||||
/// <param name="context">The object to modify.</param>
|
||||
protected virtual void Return404(HttpListenerContext context)
|
||||
{
|
||||
context.Response.StatusCode = 404;
|
||||
context.Response.Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests to see if the given port # is already in use
|
||||
/// </summary>
|
||||
/// <param name="port">port number to test</param>
|
||||
/// <returns>true if there is not a listener on the port</returns>
|
||||
protected static bool IsPortAvailable(int port)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (port <= 0)
|
||||
return false;
|
||||
|
||||
using (var client = new TcpClient())
|
||||
{
|
||||
var result = client.BeginConnect(IPAddress.Loopback, port, null, null);
|
||||
var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(500));
|
||||
if (!success)
|
||||
return true;
|
||||
|
||||
client.EndConnect(result);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find an open network listen port on the local system
|
||||
/// </summary>
|
||||
/// <returns>a system assigned port, or 0 if none are available</returns>
|
||||
protected static int GetAvailablePort()
|
||||
{
|
||||
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 0))
|
||||
{
|
||||
socket.Bind(k_DefaultLoopbackEndpoint);
|
||||
|
||||
var endPoint = socket.LocalEndPoint as IPEndPoint;
|
||||
return endPoint != null ? endPoint.Port : 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void Cleanup()
|
||||
{
|
||||
for (int i = m_ActiveUploads.Count - 1; i >= 0; --i)
|
||||
{
|
||||
if (m_ActiveUploads[i].IsDone) {
|
||||
m_ActiveUploads.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: fe5d67dc3f01349a8ae810148424c3b8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor.AddressableAssets.Settings;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityEditor.AddressableAssets.HostingServices
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IHostingService"/> implementations serve Addressable content from the Unity Editor to players running
|
||||
/// locally or on devices with network access to the Editor.
|
||||
/// </summary>
|
||||
public interface IHostingService
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the list of root directories being served by this hosting service
|
||||
/// </summary>
|
||||
List<string> HostingServiceContentRoots { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get a map of all profile variables and their current values
|
||||
/// </summary>
|
||||
Dictionary<string, string> ProfileVariables { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get a boolean that indicates if this hosting service is running
|
||||
/// </summary>
|
||||
bool IsHostingServiceRunning { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Start the hosting service
|
||||
/// </summary>
|
||||
void StartHostingService();
|
||||
|
||||
/// <summary>
|
||||
/// Stop the hosting service
|
||||
/// </summary>
|
||||
void StopHostingService();
|
||||
|
||||
/// <summary>
|
||||
/// Called by the HostingServicesManager before a domain reload, giving the hosting service
|
||||
/// an opportunity to persist state information.
|
||||
/// </summary>
|
||||
/// <param name="dataStore">A key/value pair data store for use in persisting state information</param>
|
||||
void OnBeforeSerialize(KeyDataStore dataStore);
|
||||
|
||||
/// <summary>
|
||||
/// Called immediatley following a domain reload by the HostingServicesManager, for restoring state information
|
||||
/// after the service is recreated.
|
||||
/// </summary>
|
||||
/// <param name="dataStore">A key/value pair data store for use in restoring state information</param>
|
||||
void OnAfterDeserialize(KeyDataStore dataStore);
|
||||
|
||||
/// <summary>
|
||||
/// Expand special variables from Addressable profiles
|
||||
/// </summary>
|
||||
/// <param name="key">Key name to match</param>
|
||||
/// <returns>replacement string value for key, or null if no match</returns>
|
||||
string EvaluateProfileString(string key);
|
||||
|
||||
/// <summary>
|
||||
/// The ILogger instance to use for debug log output
|
||||
/// </summary>
|
||||
ILogger Logger { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Draw configuration GUI elements
|
||||
/// </summary>
|
||||
// ReSharper disable once InconsistentNaming
|
||||
void OnGUI();
|
||||
|
||||
/// <summary>
|
||||
/// Set by the HostingServicesManager, primarily used to disambiguate multiple instances of the same service
|
||||
/// in the GUI.
|
||||
/// </summary>
|
||||
string DescriptiveName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// uniquely identifies this service within the scope of the HostingServicesManager
|
||||
/// </summary>
|
||||
int InstanceId { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f6fc6153494d34438861807b8e2ded93
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
using System;
|
||||
using UnityEditor.AddressableAssets.Settings;
|
||||
|
||||
namespace UnityEditor.AddressableAssets.HostingServices
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for providing configuration data for <see cref="IHostingService"/> implementations
|
||||
/// </summary>
|
||||
public interface IHostingServiceConfigurationProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the Hosting Service content root path for the given <see cref="AddressableAssetGroup"/>
|
||||
/// </summary>
|
||||
string HostingServicesContentRoot { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9d5c72ca5a9734f80b3a76cdb4232e21
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Loading…
Add table
Add a link
Reference in a new issue