HttpHostingService.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Net;
  5. using System.Net.Sockets;
  6. using UnityEditor.AddressableAssets.Settings;
  7. using UnityEngine;
  8. using UnityEngine.AddressableAssets;
  9. namespace UnityEditor.AddressableAssets.HostingServices
  10. {
  11. // ReSharper disable once ClassWithVirtualMembersNeverInherited.Global
  12. /// <summary>
  13. /// HTTP implementation of hosting service.
  14. /// </summary>
  15. public class HttpHostingService : BaseHostingService
  16. {
  17. /// <summary>
  18. /// Options for standard Http result codes
  19. /// </summary>
  20. protected enum ResultCode
  21. {
  22. /// <summary>
  23. /// Use to indicate that the request succeeded.
  24. /// </summary>
  25. Ok = 200,
  26. /// <summary>
  27. /// Use to indicate that the requested resource could not be found.
  28. /// </summary>
  29. NotFound = 404
  30. }
  31. internal class FileUploadOperation
  32. {
  33. HttpListenerContext m_Context;
  34. byte[] m_ReadByteBuffer;
  35. FileStream m_ReadFileStream;
  36. long m_TotalBytesRead;
  37. bool m_IsDone = false;
  38. public bool IsDone => m_IsDone;
  39. public FileUploadOperation(HttpListenerContext context, string filePath)
  40. {
  41. m_Context = context;
  42. m_Context.Response.ContentType = "application/octet-stream";
  43. m_ReadByteBuffer = new byte[k_FileReadBufferSize];
  44. try
  45. {
  46. m_ReadFileStream = File.OpenRead(filePath);
  47. }
  48. catch (Exception e)
  49. {
  50. m_IsDone = true;
  51. Debug.LogException(e);
  52. throw;
  53. }
  54. m_Context.Response.ContentLength64 = m_ReadFileStream.Length;
  55. }
  56. public void Update(double diff, int bytesPerSecond)
  57. {
  58. if (m_Context == null || m_ReadFileStream == null)
  59. return;
  60. int countToRead = (int)(bytesPerSecond * diff);
  61. try
  62. {
  63. while (countToRead > 0)
  64. {
  65. int count = countToRead > m_ReadByteBuffer.Length ? m_ReadByteBuffer.Length : countToRead;
  66. int read = m_ReadFileStream.Read(m_ReadByteBuffer, 0, count);
  67. m_Context.Response.OutputStream.Write(m_ReadByteBuffer, 0, read);
  68. m_TotalBytesRead += read;
  69. countToRead -= count;
  70. if (m_TotalBytesRead == m_ReadFileStream.Length)
  71. {
  72. Stop();
  73. break;
  74. }
  75. }
  76. }
  77. catch (Exception e)
  78. {
  79. string url = m_Context.Request.Url.ToString();
  80. Stop();
  81. if (e.InnerException != null && e.InnerException is SocketException &&
  82. e.InnerException.Message == "The socket has been shut down")
  83. {
  84. Addressables.LogWarning($"Connection lost: {url}. The socket has been shut down.");
  85. }
  86. else
  87. {
  88. Addressables.LogException(e);
  89. throw;
  90. }
  91. }
  92. }
  93. public void Stop()
  94. {
  95. if (m_IsDone)
  96. {
  97. Debug.LogError("FileUploadOperation has already completed.");
  98. return;
  99. }
  100. m_IsDone = true;
  101. m_ReadFileStream.Dispose();
  102. m_ReadFileStream = null;
  103. m_Context.Response.OutputStream.Close();
  104. m_Context = null;
  105. }
  106. }
  107. const string k_HostingServicePortKey = "HostingServicePort";
  108. const int k_FileReadBufferSize = 64 * 1024;
  109. private const int k_OneGBPS = 1024 * 1024 * 1024;
  110. const string k_UploadSpeedKey = "HostingServiceUploadSpeed";
  111. int m_UploadSpeed;
  112. double m_LastFrameTime;
  113. List<FileUploadOperation> m_ActiveUploads = new List<FileUploadOperation>();
  114. static readonly IPEndPoint k_DefaultLoopbackEndpoint = new IPEndPoint(IPAddress.Loopback, 0);
  115. int m_ServicePort;
  116. readonly List<string> m_ContentRoots;
  117. readonly Dictionary<string, string> m_ProfileVariables;
  118. GUIContent m_UploadSpeedGUI =
  119. new GUIContent("Upload Speed (Kb/s)", "Speed in Kb/s the hosting service will upload content. 0 for no limit");
  120. // ReSharper disable once MemberCanBePrivate.Global
  121. /// <summary>
  122. /// The actual Http listener used by this service
  123. /// </summary>
  124. protected HttpListener MyHttpListener { get; set; }
  125. /// <summary>
  126. /// The port number on which the service is listening
  127. /// </summary>
  128. // ReSharper disable once MemberCanBePrivate.Global
  129. public int HostingServicePort
  130. {
  131. get
  132. {
  133. return m_ServicePort;
  134. }
  135. protected set
  136. {
  137. if (value > 0)
  138. m_ServicePort = value;
  139. }
  140. }
  141. /// <summary>
  142. /// The upload speed that files were be served at, in kbps
  143. /// </summary>
  144. public int UploadSpeed
  145. {
  146. get => m_UploadSpeed;
  147. set => m_UploadSpeed = value > 0 ? value > int.MaxValue / 1024 ? int.MaxValue / 1024 : value : 0;
  148. }
  149. /// <summary>
  150. /// Files that are currently being uploaded
  151. /// </summary>
  152. internal List<FileUploadOperation> ActiveOperations => m_ActiveUploads;
  153. /// <inheritdoc/>
  154. public override bool IsHostingServiceRunning
  155. {
  156. get { return MyHttpListener != null && MyHttpListener.IsListening; }
  157. }
  158. /// <inheritdoc/>
  159. public override List<string> HostingServiceContentRoots
  160. {
  161. get { return m_ContentRoots; }
  162. }
  163. /// <inheritdoc/>
  164. public override Dictionary<string, string> ProfileVariables
  165. {
  166. get
  167. {
  168. m_ProfileVariables[k_HostingServicePortKey] = HostingServicePort.ToString();
  169. m_ProfileVariables[DisambiguateProfileVar(k_HostingServicePortKey)] = HostingServicePort.ToString();
  170. return m_ProfileVariables;
  171. }
  172. }
  173. /// <summary>
  174. /// Create a new <see cref="HttpHostingService"/>
  175. /// </summary>
  176. public HttpHostingService()
  177. {
  178. m_ProfileVariables = new Dictionary<string, string>();
  179. m_ContentRoots = new List<string>();
  180. MyHttpListener = new HttpListener();
  181. }
  182. /// <summary>
  183. /// Destroys a <see cref="HttpHostingService"/>
  184. /// </summary>
  185. ~HttpHostingService()
  186. {
  187. StopHostingService();
  188. }
  189. /// <inheritdoc/>
  190. public override void StartHostingService()
  191. {
  192. if (IsHostingServiceRunning)
  193. return;
  194. if (HostingServicePort <= 0)
  195. {
  196. HostingServicePort = GetAvailablePort();
  197. }
  198. else if (!IsPortAvailable(HostingServicePort))
  199. {
  200. LogError("Port {0} is in use, cannot start service!", HostingServicePort);
  201. return;
  202. }
  203. if (HostingServiceContentRoots.Count == 0)
  204. {
  205. throw new Exception(
  206. "ContentRoot is not configured; cannot start service. This can usually be fixed by modifying the BuildPath for any new groups and/or building content.");
  207. }
  208. ConfigureHttpListener();
  209. MyHttpListener.Start();
  210. MyHttpListener.BeginGetContext(HandleRequest, null);
  211. EditorApplication.update += EditorUpdate;
  212. var count = HostingServiceContentRoots.Count;
  213. Log("Started. Listening on port {0}. Hosting {1} folder{2}.", HostingServicePort, count, count > 1 ? "s" : string.Empty);
  214. foreach (var root in HostingServiceContentRoots)
  215. {
  216. Log("Hosting : {0}", root);
  217. }
  218. }
  219. private void EditorUpdate()
  220. {
  221. if (m_LastFrameTime == 0)
  222. m_LastFrameTime = EditorApplication.timeSinceStartup - Time.unscaledDeltaTime;
  223. double diff = EditorApplication.timeSinceStartup - m_LastFrameTime;
  224. int speed = m_UploadSpeed * 1024;
  225. int bps = speed > 0 ? speed : k_OneGBPS;
  226. Update(diff, bps);
  227. m_LastFrameTime = EditorApplication.timeSinceStartup;
  228. }
  229. internal void Update(double deltaTime, int bytesPerSecond)
  230. {
  231. for (int i = m_ActiveUploads.Count - 1; i >= 0; --i)
  232. {
  233. m_ActiveUploads[i].Update(deltaTime, bytesPerSecond);
  234. if (m_ActiveUploads[i].IsDone)
  235. m_ActiveUploads.RemoveAt(i);
  236. }
  237. }
  238. /// <summary>
  239. /// Temporarily stops the service from receiving requests.
  240. /// </summary>
  241. public override void StopHostingService()
  242. {
  243. if (!IsHostingServiceRunning) return;
  244. Log("Stopping");
  245. MyHttpListener.Stop();
  246. // Abort() is the method we want instead of Close(), because the former frees up resources without
  247. // disposing the object.
  248. MyHttpListener.Abort();
  249. EditorApplication.update -= EditorUpdate;
  250. foreach (FileUploadOperation operation in m_ActiveUploads)
  251. operation.Stop();
  252. m_ActiveUploads.Clear();
  253. }
  254. /// <inheritdoc/>
  255. public override void OnGUI()
  256. {
  257. EditorGUILayout.BeginHorizontal();
  258. {
  259. var newPort = EditorGUILayout.DelayedIntField("Port", HostingServicePort);
  260. if (newPort != HostingServicePort)
  261. {
  262. if (IsPortAvailable(newPort))
  263. {
  264. ResetListenPort(newPort);
  265. var settings = AddressableAssetSettingsDefaultObject.Settings;
  266. if (settings != null)
  267. settings.SetDirty(AddressableAssetSettings.ModificationEvent.HostingServicesManagerModified, this, false, true);
  268. }
  269. else
  270. LogError("Cannot listen on port {0}; port is in use", newPort);
  271. }
  272. if (GUILayout.Button("Reset", GUILayout.ExpandWidth(false)))
  273. ResetListenPort();
  274. //GUILayout.Space(rect.width / 2f);
  275. }
  276. EditorGUILayout.EndHorizontal();
  277. UploadSpeed = EditorGUILayout.IntField(m_UploadSpeedGUI, UploadSpeed);
  278. }
  279. /// <inheritdoc/>
  280. public override void OnBeforeSerialize(KeyDataStore dataStore)
  281. {
  282. dataStore.SetData(k_HostingServicePortKey, HostingServicePort);
  283. dataStore.SetData(k_UploadSpeedKey, m_UploadSpeed);
  284. base.OnBeforeSerialize(dataStore);
  285. }
  286. /// <inheritdoc/>
  287. public override void OnAfterDeserialize(KeyDataStore dataStore)
  288. {
  289. HostingServicePort = dataStore.GetData(k_HostingServicePortKey, 0);
  290. UploadSpeed = dataStore.GetData(k_UploadSpeedKey, 0);
  291. base.OnAfterDeserialize(dataStore);
  292. }
  293. /// <summary>
  294. /// Listen on a new port then next time the server starts. If the server is already running, it will be stopped
  295. /// and restarted automatically.
  296. /// </summary>
  297. /// <param name="port">Specify a port to listen on. Default is 0 to choose any open port</param>
  298. // ReSharper disable once MemberCanBePrivate.Global
  299. public void ResetListenPort(int port = 0)
  300. {
  301. var isRunning = IsHostingServiceRunning;
  302. StopHostingService();
  303. HostingServicePort = port;
  304. if (isRunning)
  305. StartHostingService();
  306. }
  307. /// <summary>
  308. /// Handles any configuration necessary for <see cref="MyHttpListener"/> before listening for connections.
  309. /// </summary>
  310. protected virtual void ConfigureHttpListener()
  311. {
  312. try
  313. {
  314. MyHttpListener.Prefixes.Clear();
  315. MyHttpListener.Prefixes.Add("http://+:" + HostingServicePort + "/");
  316. }
  317. catch (Exception e)
  318. {
  319. Debug.LogException(e);
  320. }
  321. }
  322. /// <summary>
  323. /// Asynchronous callback to handle a client connection request on <see cref="MyHttpListener"/>. This method is
  324. /// recursive in that it will call itself immediately after receiving a new incoming request to listen for the
  325. /// next connection.
  326. /// </summary>
  327. /// <param name="ar">Asynchronous result from previous request. Pass null to listen for an initial request</param>
  328. /// <exception cref="ArgumentOutOfRangeException">thrown when the request result code is unknown</exception>
  329. protected virtual void HandleRequest(IAsyncResult ar)
  330. {
  331. if (!IsHostingServiceRunning)
  332. return;
  333. var c = MyHttpListener.EndGetContext(ar);
  334. MyHttpListener.BeginGetContext(HandleRequest, null);
  335. var relativePath = c.Request.Url.LocalPath.Substring(1);
  336. var fullPath = FindFileInContentRoots(relativePath);
  337. var result = fullPath != null ? ResultCode.Ok : ResultCode.NotFound;
  338. var info = fullPath != null ? new FileInfo(fullPath) : null;
  339. var size = info != null ? info.Length.ToString() : "-";
  340. var remoteAddress = c.Request.RemoteEndPoint != null ? c.Request.RemoteEndPoint.Address : null;
  341. var timestamp = DateTime.Now.ToString("o");
  342. Log("{0} - - [{1}] \"{2}\" {3} {4}", remoteAddress, timestamp, fullPath, (int)result, size);
  343. switch (result)
  344. {
  345. case ResultCode.Ok:
  346. ReturnFile(c, fullPath);
  347. break;
  348. case ResultCode.NotFound:
  349. Return404(c);
  350. break;
  351. default:
  352. throw new ArgumentOutOfRangeException();
  353. }
  354. }
  355. /// <summary>
  356. /// Searches for the given relative path within the configured content root directores.
  357. /// </summary>
  358. /// <param name="relativePath"></param>
  359. /// <returns>The full system path to the file if found, or null if file could not be found</returns>
  360. protected virtual string FindFileInContentRoots(string relativePath)
  361. {
  362. relativePath = relativePath.TrimStart('/');
  363. relativePath = relativePath.TrimStart('\\');
  364. foreach (var root in HostingServiceContentRoots)
  365. {
  366. var fullPath = Path.Combine(root, relativePath).Replace('\\','/');
  367. if (File.Exists(fullPath))
  368. return fullPath;
  369. }
  370. return null;
  371. }
  372. /// <summary>
  373. /// Sends a file to the connected HTTP client
  374. /// </summary>
  375. /// <param name="context"></param>
  376. /// <param name="filePath"></param>
  377. /// <param name="readBufferSize"></param>
  378. protected virtual void ReturnFile(HttpListenerContext context, string filePath, int readBufferSize = k_FileReadBufferSize)
  379. {
  380. if (m_UploadSpeed > 0)
  381. {
  382. m_ActiveUploads.Add(new FileUploadOperation(context, filePath));
  383. }
  384. else
  385. {
  386. context.Response.ContentType = "application/octet-stream";
  387. var buffer = new byte[readBufferSize];
  388. using (var fs = File.OpenRead(filePath))
  389. {
  390. context.Response.ContentLength64 = fs.Length;
  391. int read;
  392. while ((read = fs.Read(buffer, 0, buffer.Length)) > 0)
  393. context.Response.OutputStream.Write(buffer, 0, read);
  394. }
  395. context.Response.OutputStream.Close();
  396. }
  397. }
  398. /// <summary>
  399. /// Sets the status code to 404 on the given <c>HttpListenerContext</c> object.
  400. /// </summary>
  401. /// <param name="context">The object to modify.</param>
  402. protected virtual void Return404(HttpListenerContext context)
  403. {
  404. context.Response.StatusCode = 404;
  405. context.Response.Close();
  406. }
  407. /// <summary>
  408. /// Tests to see if the given port # is already in use
  409. /// </summary>
  410. /// <param name="port">port number to test</param>
  411. /// <returns>true if there is not a listener on the port</returns>
  412. protected static bool IsPortAvailable(int port)
  413. {
  414. try
  415. {
  416. if (port <= 0)
  417. return false;
  418. using (var client = new TcpClient())
  419. {
  420. var result = client.BeginConnect(IPAddress.Loopback, port, null, null);
  421. var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(500));
  422. if (!success)
  423. return true;
  424. client.EndConnect(result);
  425. }
  426. }
  427. catch
  428. {
  429. return true;
  430. }
  431. return false;
  432. }
  433. /// <summary>
  434. /// Find an open network listen port on the local system
  435. /// </summary>
  436. /// <returns>a system assigned port, or 0 if none are available</returns>
  437. protected static int GetAvailablePort()
  438. {
  439. using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 0))
  440. {
  441. socket.Bind(k_DefaultLoopbackEndpoint);
  442. var endPoint = socket.LocalEndPoint as IPEndPoint;
  443. return endPoint != null ? endPoint.Port : 0;
  444. }
  445. }
  446. }
  447. }