ServerDownloader.cs 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Net;
  5. using System.Net.Http;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. using SingularityGroup.HotReload.DTO;
  9. using SingularityGroup.HotReload.Editor.Cli;
  10. using SingularityGroup.HotReload.Newtonsoft.Json;
  11. using UnityEditor;
  12. using UnityEngine;
  13. namespace SingularityGroup.HotReload.Editor {
  14. internal class ServerDownloader : IProgress<float> {
  15. public float Progress {get; private set;}
  16. public bool Started {get; private set;}
  17. class Config {
  18. public Dictionary<string, string> customServerExecutables;
  19. }
  20. public string GetExecutablePath(ICliController cliController) {
  21. var targetDir = CliUtils.GetExecutableTargetDir();
  22. var targetPath = Path.Combine(targetDir, cliController.BinaryFileName);
  23. return targetPath;
  24. }
  25. public bool IsDownloaded(ICliController cliController) {
  26. return File.Exists(GetExecutablePath(cliController));
  27. }
  28. public bool CheckIfDownloaded(ICliController cliController) {
  29. if(TryUseUserDefinedBinaryPath(cliController, GetExecutablePath(cliController))) {
  30. Started = true;
  31. Progress = 1f;
  32. return true;
  33. } else if(IsDownloaded(cliController)) {
  34. Started = true;
  35. Progress = 1f;
  36. return true;
  37. } else {
  38. Started = false;
  39. Progress = 0f;
  40. return false;
  41. }
  42. }
  43. public async Task<bool> EnsureDownloaded(ICliController cliController, CancellationToken cancellationToken) {
  44. var targetDir = CliUtils.GetExecutableTargetDir();
  45. var targetPath = Path.Combine(targetDir, cliController.BinaryFileName);
  46. Started = true;
  47. if(File.Exists(targetPath)) {
  48. Progress = 1f;
  49. return true;
  50. }
  51. Progress = 0f;
  52. await ThreadUtility.SwitchToThreadPool(cancellationToken);
  53. Directory.CreateDirectory(targetDir);
  54. if(TryUseUserDefinedBinaryPath(cliController, targetPath)) {
  55. Progress = 1f;
  56. return true;
  57. }
  58. var tmpPath = CliUtils.GetTempDownloadFilePath("Server.tmp");
  59. var attempt = 0;
  60. bool sucess = false;
  61. HashSet<string> errors = null;
  62. while(!sucess) {
  63. try {
  64. if (File.Exists(targetPath)) {
  65. Progress = 1f;
  66. return true;
  67. }
  68. // Note: we are writing to temp file so if downloaded file is corrupted it will not cause issues until it's copied to target location
  69. var result = await DownloadUtility.DownloadFile(GetDownloadUrl(cliController), tmpPath, this, cancellationToken).ConfigureAwait(false);
  70. sucess = result.statusCode == HttpStatusCode.OK;
  71. } catch (Exception e) {
  72. var error = $"{e.GetType().Name}: {e.Message}";
  73. errors = (errors ?? new HashSet<string>());
  74. if (errors.Add(error)) {
  75. Log.Warning($"Download attempt failed. If the issue persists please reach out to customer support for assistance. Exception: {error}");
  76. }
  77. }
  78. if (!sucess) {
  79. await Task.Delay(ExponentialBackoff.GetTimeout(attempt), cancellationToken).ConfigureAwait(false);
  80. }
  81. Progress = 0;
  82. attempt++;
  83. }
  84. if (errors?.Count > 0) {
  85. var data = new EditorExtraData {
  86. { StatKey.Errors, new List<string>(errors) },
  87. };
  88. // sending telemetry requires server to be running so we only attempt after server is downloaded
  89. RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Error, StatFeature.Editor, StatEventType.Download), data).Forget();
  90. Log.Info("Download succeeded!");
  91. }
  92. const int ERROR_ALREADY_EXISTS = 0xB7;
  93. try {
  94. File.Move(tmpPath, targetPath);
  95. } catch(IOException ex) when((ex.HResult & 0x0000FFFF) == ERROR_ALREADY_EXISTS) {
  96. //another downloader came first
  97. try {
  98. File.Delete(tmpPath);
  99. } catch {
  100. //ignored
  101. }
  102. }
  103. Progress = 1f;
  104. return true;
  105. }
  106. static bool TryUseUserDefinedBinaryPath(ICliController cliController, string targetPath) {
  107. if (!File.Exists(PackageConst.ConfigFileName)) {
  108. return false;
  109. }
  110. var config = JsonConvert.DeserializeObject<Config>(File.ReadAllText(PackageConst.ConfigFileName));
  111. var customExecutables = config?.customServerExecutables;
  112. if (customExecutables == null) {
  113. return false;
  114. }
  115. string customBinaryPath;
  116. if(!customExecutables.TryGetValue(cliController.PlatformName, out customBinaryPath)) {
  117. return false;
  118. }
  119. if (!File.Exists(customBinaryPath)) {
  120. Log.Warning($"unable to find server binary for platform '{cliController.PlatformName}' at '{customBinaryPath}'. " +
  121. $"Will proceed with downloading the binary (default behavior)");
  122. return false;
  123. }
  124. try {
  125. var targetFile = new FileInfo(targetPath);
  126. bool copy = true;
  127. if (targetFile.Exists) {
  128. copy = File.GetLastWriteTimeUtc(customBinaryPath) > targetFile.LastWriteTimeUtc;
  129. }
  130. if (copy) {
  131. Directory.CreateDirectory(Path.GetDirectoryName(targetPath));
  132. File.Copy(customBinaryPath, targetPath, true);
  133. }
  134. return true;
  135. } catch(IOException ex) {
  136. Log.Warning("encountered exception when copying server binary in the specified custom executable path '{0}':\n{1}", customBinaryPath, ex);
  137. return false;
  138. }
  139. }
  140. static string GetDownloadUrl(ICliController cliController) {
  141. const string version = PackageConst.ServerVersion;
  142. var key = $"{DownloadUtility.GetPackagePrefix(version)}/server/{cliController.PlatformName}/{cliController.BinaryFileName}";
  143. return DownloadUtility.GetDownloadUrl(key);
  144. }
  145. void IProgress<float>.Report(float value) {
  146. Progress = value;
  147. }
  148. public Task<bool> PromptForDownload() {
  149. if (EditorUtility.DisplayDialog(
  150. title: "Install platform specific components",
  151. message: InstallDescription,
  152. ok: "Install",
  153. cancel: "More Info")
  154. ) {
  155. return EnsureDownloaded(HotReloadCli.controller, CancellationToken.None);
  156. }
  157. Application.OpenURL(Constants.AdditionalContentURL);
  158. return Task.FromResult(false);
  159. }
  160. public const string InstallDescription = "For Hot Reload to work, additional components specific to your operating system have to be installed";
  161. }
  162. class DownloadResult {
  163. public readonly HttpStatusCode statusCode;
  164. public readonly string error;
  165. public DownloadResult(HttpStatusCode statusCode, string error) {
  166. this.statusCode = statusCode;
  167. this.error = error;
  168. }
  169. }
  170. }