123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506 |
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Net;
- using System.Net.Sockets;
- 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 class FileUploadOperation
- {
- HttpListenerContext m_Context;
- byte[] m_ReadByteBuffer;
- FileStream m_ReadFileStream;
- long m_TotalBytesRead;
- bool m_IsDone = false;
- public bool IsDone => m_IsDone;
- public FileUploadOperation(HttpListenerContext context, string filePath)
- {
- m_Context = context;
- m_Context.Response.ContentType = "application/octet-stream";
- m_ReadByteBuffer = new byte[k_FileReadBufferSize];
- try
- {
- m_ReadFileStream = File.OpenRead(filePath);
- }
- catch (Exception e)
- {
- m_IsDone = true;
- Debug.LogException(e);
- throw;
- }
- m_Context.Response.ContentLength64 = m_ReadFileStream.Length;
- }
- public void Update(double diff, int bytesPerSecond)
- {
- if (m_Context == null || m_ReadFileStream == null)
- return;
- int countToRead = (int)(bytesPerSecond * diff);
- 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.Response.OutputStream.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.Request.Url.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()
- {
- if (m_IsDone)
- {
- Debug.LogError("FileUploadOperation has already completed.");
- return;
- }
- m_IsDone = true;
- m_ReadFileStream.Dispose();
- m_ReadFileStream = null;
- m_Context.Response.OutputStream.Close();
- m_Context = null;
- }
- }
- const string k_HostingServicePortKey = "HostingServicePort";
- const int k_FileReadBufferSize = 64 * 1024;
- private const int k_OneGBPS = 1024 * 1024 * 1024;
- const string k_UploadSpeedKey = "HostingServiceUploadSpeed";
- int m_UploadSpeed;
- double m_LastFrameTime;
- 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");
- // 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();
- }
- 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);
- EditorApplication.update += EditorUpdate;
- 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);
- }
- }
- private void EditorUpdate()
- {
- if (m_LastFrameTime == 0)
- m_LastFrameTime = EditorApplication.timeSinceStartup - Time.unscaledDeltaTime;
- double diff = EditorApplication.timeSinceStartup - m_LastFrameTime;
- int speed = m_UploadSpeed * 1024;
- int bps = speed > 0 ? speed : k_OneGBPS;
- Update(diff, bps);
- m_LastFrameTime = EditorApplication.timeSinceStartup;
- }
- internal void Update(double deltaTime, int bytesPerSecond)
- {
- for (int i = m_ActiveUploads.Count - 1; i >= 0; --i)
- {
- m_ActiveUploads[i].Update(deltaTime, bytesPerSecond);
- if (m_ActiveUploads[i].IsDone)
- m_ActiveUploads.RemoveAt(i);
- }
- }
- /// <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();
- EditorApplication.update -= EditorUpdate;
- foreach (FileUploadOperation operation in m_ActiveUploads)
- operation.Stop();
- m_ActiveUploads.Clear();
- }
- /// <inheritdoc/>
- public override void OnGUI()
- {
- EditorGUILayout.BeginHorizontal();
- {
- var newPort = EditorGUILayout.DelayedIntField("Port", 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("Reset", 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 then 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;
- StopHostingService();
- HostingServicePort = port;
- 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;
- var c = MyHttpListener.EndGetContext(ar);
- 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)
- {
- m_ActiveUploads.Add(new FileUploadOperation(context, filePath));
- }
- else
- {
- 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;
- }
- }
- }
- }
|