initial commit

This commit is contained in:
Jo 2025-01-07 02:06:59 +01:00
parent 6715289efe
commit 788c3389af
37645 changed files with 2526849 additions and 80 deletions

View file

@ -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);
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 504a37785439743bba6a7259d90d6652
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -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;
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 66a7a5932b5cd4dceb8261d66734b440
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -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);
}
}
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fe5d67dc3f01349a8ae810148424c3b8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -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; }
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f6fc6153494d34438861807b8e2ded93
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -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; }
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9d5c72ca5a9734f80b3a76cdb4232e21
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: