PackageUpdateChecker.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. using System;
  2. using System.IO;
  3. using System.Net;
  4. using System.Net.Http;
  5. using System.Threading;
  6. using System.Threading.Tasks;
  7. using SingularityGroup.HotReload.Editor.Semver;
  8. using SingularityGroup.HotReload.Newtonsoft.Json;
  9. using SingularityGroup.HotReload.Newtonsoft.Json.Linq;
  10. using UnityEditor;
  11. using UnityEngine;
  12. using UnityEngine.Networking;
  13. namespace SingularityGroup.HotReload.Editor {
  14. internal class PackageUpdateChecker {
  15. const string persistedFile = PackageConst.LibraryCachePath + "/updateChecker.json";
  16. readonly JsonSerializer jsonSerializer = JsonSerializer.CreateDefault();
  17. SemVersion newVersionDetected;
  18. bool started;
  19. bool warnedVersionCheckFailed;
  20. private static TimeSpan RetryInterval => TimeSpan.FromSeconds(30);
  21. private static TimeSpan CheckInterval => TimeSpan.FromHours(1);
  22. private static readonly HttpClient client = HttpClientUtils.CreateHttpClient();
  23. private static string _lastRemotePackageVersion;
  24. public static string lastRemotePackageVersion => _lastRemotePackageVersion;
  25. public async void StartCheckingForNewVersion() {
  26. if(started) {
  27. return;
  28. }
  29. started = true;
  30. for (;;) {
  31. try {
  32. await PerformVersionCheck();
  33. if(newVersionDetected != null) {
  34. break;
  35. }
  36. } catch(Exception ex) {
  37. Log.Warning("encountered exception when checking for new Hot Reload package version:\n{0}", ex);
  38. }
  39. await Task.Delay(RetryInterval);
  40. }
  41. }
  42. public bool TryGetNewVersion(out SemVersion version) {
  43. var currentVersion = SemVersion.Parse(PackageConst.Version, strict: true);
  44. return !ReferenceEquals(version = newVersionDetected, null) && newVersionDetected > currentVersion;
  45. }
  46. async Task PerformVersionCheck() {
  47. var state = await LoadPersistedState();
  48. var currentVersion = SemVersion.Parse(PackageConst.Version, strict: true);
  49. if(state != null) {
  50. _lastRemotePackageVersion = state.lastRemotePackageVersion;
  51. var newVersion = SemVersion.Parse(state.lastRemotePackageVersion);
  52. if(newVersion > currentVersion) {
  53. newVersionDetected = newVersion;
  54. return;
  55. }
  56. if(DateTime.UtcNow - state.lastVersionCheck < CheckInterval) {
  57. return;
  58. }
  59. }
  60. var response = await GetLatestPackageVersion();
  61. if(response.err != null) {
  62. if(response.statusCode == 0 || response.statusCode == 404) {
  63. // probably no internet, fail silently and retry
  64. } else if (!warnedVersionCheckFailed) {
  65. Log.Warning("version check failed: {0}", response.err);
  66. warnedVersionCheckFailed = true;
  67. }
  68. } else {
  69. var newVersion = response.data;
  70. if (response.data > currentVersion) {
  71. newVersionDetected = newVersion;
  72. }
  73. await Task.Run(() => PersistState(response.data));
  74. }
  75. }
  76. void PersistState(SemVersion newVersion) {
  77. // ReSharper disable once AssignNullToNotNullAttribute
  78. var fi = new FileInfo(persistedFile);
  79. fi.Directory.Create();
  80. using (var streamWriter = new StreamWriter(fi.OpenWrite()))
  81. using (var writer = new JsonTextWriter(streamWriter)) {
  82. jsonSerializer.Serialize(writer, new State {
  83. lastVersionCheck = DateTime.UtcNow,
  84. lastRemotePackageVersion = newVersion.ToString()
  85. });
  86. }
  87. }
  88. Task<State> LoadPersistedState() {
  89. return Task.Run(() => {
  90. var fi = new FileInfo(persistedFile);
  91. if(!fi.Exists) {
  92. return null;
  93. }
  94. using(var streamReader = fi.OpenText())
  95. using(var reader = new JsonTextReader(streamReader)) {
  96. return jsonSerializer.Deserialize<State>(reader);
  97. }
  98. });
  99. }
  100. static async Task<Response<SemVersion>> GetLatestPackageVersion() {
  101. string versionUrl;
  102. if (PackageConst.IsAssetStoreBuild) {
  103. // version updates are synced with asset store
  104. versionUrl = "https://d2tc55zjhw51ly.cloudfront.net/releases/latest/asset-store-version.json";
  105. } else {
  106. versionUrl = "https://gitlab.hotreload.net/root/hot-reload-releases/-/raw/production/package.json";
  107. }
  108. try {
  109. using(var resp = await client.GetAsync(versionUrl).ConfigureAwait(false)) {
  110. if(resp.StatusCode != HttpStatusCode.OK) {
  111. return Response.FromError<SemVersion>($"Request failed with statusCode: {resp.StatusCode} {resp.ReasonPhrase}");
  112. }
  113. var json = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);
  114. var o = await JObject.LoadAsync(new JsonTextReader(new StringReader(json))).ConfigureAwait(false);
  115. SemVersion newVersion;
  116. JToken value;
  117. if (!o.TryGetValue("version", out value)) {
  118. return Response.FromError<SemVersion>("Invalid package.json");
  119. } else if(!SemVersion.TryParse(value.Value<string>(), out newVersion, strict: true)) {
  120. return Response.FromError<SemVersion>($"Invalid version in package.json: '{value.Value<string>()}'");
  121. } else {
  122. return Response.FromResult(newVersion);
  123. }
  124. }
  125. } catch(Exception ex) {
  126. return Response.FromError<SemVersion>($"{ex.GetType().Name} {ex.Message}");
  127. }
  128. }
  129. public async Task UpdatePackageAsync(SemVersion newVersion) {
  130. //Package can be updated by updating the git url via the package manager
  131. if(EditorUtility.DisplayDialog($"Update To v{newVersion}", $"By pressing 'Update' the Hot Reload package will be updated to v{newVersion}", "Update", "Cancel")) {
  132. var resp = await GetLatestPackageVersion();
  133. if(resp.err == null && resp.data > newVersion) {
  134. newVersion = resp.data;
  135. }
  136. if(await IsUsingGitRepo()) {
  137. var err = UpdateGitUrlInManifest(newVersion);
  138. if(err != null) {
  139. Log.Warning("Encountered issue when updating Hot Reload: {0}", err);
  140. } else {
  141. //Delete state to force another version check after the package is installed
  142. File.Delete(persistedFile);
  143. #if UNITY_2020_3_OR_NEWER
  144. UnityEditor.PackageManager.Client.Resolve();
  145. #else
  146. CompileMethodDetourer.Reset();
  147. AssetDatabase.Refresh();
  148. #endif
  149. }
  150. } else {
  151. var err = await UpdateUtility.Update(newVersion.ToString(), null, CancellationToken.None);
  152. if(err != null) {
  153. Log.Warning("Failed to update package: {0}", err);
  154. } else {
  155. CompileMethodDetourer.Reset();
  156. AssetDatabase.Refresh();
  157. }
  158. }
  159. //open changelog
  160. HotReloadPrefs.ShowChangeLog = true;
  161. HotReloadWindow.Current.SelectTab(typeof(HotReloadAboutTab));
  162. }
  163. }
  164. string UpdateGitUrlInManifest(SemVersion newVersion) {
  165. const string repoUrl = "git+https://gitlab.hotreload.net/root/hot-reload-releases.git";
  166. const string manifestJsonPath = "Packages/manifest.json";
  167. var repoUrlToNewVersion = $"{repoUrl}#{newVersion}";
  168. if(!File.Exists(manifestJsonPath)) {
  169. return "Unable to find manifest.json";
  170. }
  171. var root = JObject.Load(new JsonTextReader(new StringReader(File.ReadAllText(manifestJsonPath))));
  172. JObject deps;
  173. var err = TryGetManfestDeps(root, out deps);
  174. if(err != null) {
  175. return err;
  176. }
  177. deps[PackageConst.PackageName] = repoUrlToNewVersion;
  178. root["dependencies"] = deps;
  179. File.WriteAllText(manifestJsonPath, root.ToString(Formatting.Indented));
  180. return null;
  181. }
  182. static string TryGetManfestDeps(JObject root, out JObject deps) {
  183. JToken value;
  184. if(!root.TryGetValue("dependencies", out value)) {
  185. deps = null;
  186. return "no dependencies object found in manifest.json";
  187. }
  188. deps = value.Value<JObject>();
  189. if(deps == null) {
  190. return "dependencies object null in manifest.json";
  191. }
  192. return null;
  193. }
  194. static async Task<bool> IsUsingGitRepo() {
  195. var respose = await Task.Run(() => IsUsingGitRepoThreaded(PackageConst.PackageName));
  196. if(respose.err != null) {
  197. Log.Warning("Unable to find package. message: {0}", respose.err);
  198. return false;
  199. } else {
  200. return respose.data;
  201. }
  202. }
  203. static Response<bool> IsUsingGitRepoThreaded(string packageId) {
  204. var fi = new FileInfo("Packages/manifest.json");
  205. if(!fi.Exists) {
  206. return "Unable to find manifest.json";
  207. }
  208. using(var reader = fi.OpenText()) {
  209. var root = JObject.Load(new JsonTextReader(reader));
  210. JObject deps;
  211. var err = TryGetManfestDeps(root, out deps);
  212. if(err != null) {
  213. return "no dependencies specified in manifest.json";
  214. }
  215. JToken value;
  216. if(!deps.TryGetValue(packageId, out value)) {
  217. //Likely a local package directly in the packages folder of the unity project
  218. //or the package got moved into the Assets folder
  219. return Response.FromResult(false);
  220. }
  221. var pathToPackage = value.Value<string>();
  222. if(pathToPackage.StartsWith("git+", StringComparison.Ordinal)) {
  223. return Response.FromResult(true);
  224. }
  225. if(pathToPackage.StartsWith("https://", StringComparison.Ordinal)) {
  226. return Response.FromResult(true);
  227. }
  228. return Response.FromResult(false);
  229. }
  230. }
  231. class Response<T> {
  232. public readonly T data;
  233. public readonly string err;
  234. public readonly long statusCode;
  235. public Response(T data, string err, long statusCode) {
  236. this.data = data;
  237. this.err = err;
  238. this.statusCode = statusCode;
  239. }
  240. public static implicit operator Response<T>( string err) {
  241. return Response.FromError<T>(err);
  242. }
  243. }
  244. static class Response {
  245. public static Response<T> FromError<T>(string error) {
  246. return new Response<T>(default(T), error, -1);
  247. }
  248. public static Response<T> FromResult<T>(T result) {
  249. return new Response<T>(result, null, 200);
  250. }
  251. }
  252. class State {
  253. public DateTime lastVersionCheck;
  254. public string lastRemotePackageVersion;
  255. }
  256. }
  257. }