using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using SingularityGroup.HotReload.DTO; using SingularityGroup.HotReload.Editor.Cli; using SingularityGroup.HotReload.Editor.Demo; using SingularityGroup.HotReload.EditorDependencies; using SingularityGroup.HotReload.RuntimeDependencies; using UnityEditor; using UnityEngine; using Debug = UnityEngine.Debug; using Task = System.Threading.Tasks.Task; using System.Reflection; using System.Runtime.CompilerServices; using SingularityGroup.HotReload.Newtonsoft.Json; using UnityEditor.Compilation; [assembly: InternalsVisibleTo("SingularityGroup.HotReload.IntegrationTests")] namespace SingularityGroup.HotReload.Editor { internal class Config { public bool patchEditModeOnlyOnEditorFocus; public string[] assetBlacklist; public bool changePlaymodeTint; public bool disableCompilingFromEditorScripts; public bool enableInspectorFreezeFix; } [InitializeOnLoad] internal static class EditorCodePatcher { const string sessionFilePath = PackageConst.LibraryCachePath + "/sessionId.txt"; const string patchesFilePath = PackageConst.LibraryCachePath + "/patches.json"; internal static readonly ServerDownloader serverDownloader; internal static bool _compileError; internal static bool _applyingFailed; internal static bool _appliedPartially; static Timer timer; static bool init; internal static UnityLicenseType licenseType { get; private set; } internal static bool LoginNotRequired => PackageConst.IsAssetStoreBuild && licenseType != UnityLicenseType.UnityPro; internal static bool compileError => _compileError; internal static PatchStatus patchStatus = PatchStatus.None; internal static event Action OnPatchHandled; internal static Config config; static bool quitting; static EditorCodePatcher() { if(init) { //Avoid infinite recursion in case the static constructor gets accessed via `InitPatchesBlocked` below return; } if (File.Exists(PackageConst.ConfigFileName)) { config = JsonConvert.DeserializeObject(File.ReadAllText(PackageConst.ConfigFileName)); } else { config = new Config(); } init = true; UnityHelper.Init(); //Use synchonization context if possible because it's more reliable. ThreadUtility.InitEditor(); if (!EditorWindowHelper.IsHumanControllingUs()) { return; } serverDownloader = new ServerDownloader(); timer = new Timer(OnIntervalThreaded, (Action) OnIntervalMainThread, 500, 500); UpdateHost(); licenseType = UnityLicenseHelper.GetLicenseType(); var compileChecker = CompileChecker.Create(); compileChecker.onCompilationFinished += OnCompilationFinished; EditorApplication.delayCall += InstallUtility.CheckForNewInstall; AddEditorFocusChangedHandler(OnEditorFocusChanged); // When domain reloads, this is a good time to ensure server has up-to-date project information if (ServerHealthCheck.I.IsServerHealthy) { EditorApplication.delayCall += TryPrepareBuildInfo; } HotReloadSuggestionsHelper.Init(); // reset in case last session didn't shut down properly CheckEditorSettings(); EditorApplication.quitting += ResetSettingsOnQuit; AssemblyReloadEvents.beforeAssemblyReload += () => { HotReloadTimelineHelper.PersistTimeline(); }; CompilationPipeline.compilationFinished += obj => { // reset in case package got removed // if it got removed, it will not be enabled again // if it wasn't removed, settings will get handled by OnIntervalMainThread AutoRefreshSettingChecker.Reset(); ScriptCompilationSettingChecker.Reset(); PlaymodeTintSettingChecker.Reset(); HotReloadRunTab.recompiling = false; CompileMethodDetourer.Reset(); }; DetectEditorStart(); DetectVersionUpdate(); SingularityGroup.HotReload.Demo.Demo.I = new EditorDemo(); RecordActiveDaysForRateApp(); if (EditorApplication.isPlayingOrWillChangePlaymode) { CodePatcher.I.InitPatchesBlocked(patchesFilePath); HotReloadTimelineHelper.InitPersistedEvents(); } #pragma warning disable CS0612 // Type or member is obsolete if (HotReloadPrefs.RateAppShownLegacy) { HotReloadPrefs.RateAppShown = true; } if (!File.Exists(HotReloadPrefs.showOnStartupPath)) { var showOnStartupLegacy = HotReloadPrefs.GetShowOnStartupEnum(); HotReloadPrefs.ShowOnStartup = showOnStartupLegacy; } #pragma warning restore CS0612 // Type or member is obsolete HotReloadState.ShowingRedDot = false; if (DateTime.Now < new DateTime(2023, 11, 1)) { HotReloadSuggestionsHelper.SetSuggestionsShown(HotReloadSuggestionKind.UnityBestDevelopmentToolAward2023); } else { HotReloadSuggestionsHelper.SetSuggestionInactive(HotReloadSuggestionKind.UnityBestDevelopmentToolAward2023); } EditorApplication.playModeStateChanged += state => { if (state == PlayModeStateChange.EnteredEditMode && HotReloadPrefs.AutoRecompileUnsupportedChangesOnExitPlayMode) { if (TryRecompileUnsupportedChanges()) { HotReloadState.RecompiledUnsupportedChangesOnExitPlaymode = true; } } }; } public static void ResetSettingsOnQuit() { quitting = true; AutoRefreshSettingChecker.Reset(); ScriptCompilationSettingChecker.Reset(); PlaymodeTintSettingChecker.Reset(); HotReloadCli.StopAsync().Forget(); CompileMethodDetourer.Reset(); } public static bool autoRecompileUnsupportedChangesSupported; static void AddEditorFocusChangedHandler(Action handler) { var eventInfo = typeof(EditorApplication).GetEvent("focusChanged", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); var addMethod = eventInfo?.GetAddMethod(true) ?? eventInfo?.GetAddMethod(false); if (addMethod != null) { addMethod.Invoke(null, new object[]{ handler }); } autoRecompileUnsupportedChangesSupported = addMethod != null; } private static void OnEditorFocusChanged(bool hasFocus) { if (hasFocus && !HotReloadPrefs.AutoRecompileUnsupportedChangesImmediately) { TryRecompileUnsupportedChanges(); } } public static bool TryRecompileUnsupportedChanges() { if (!HotReloadPrefs.AutoRecompileUnsupportedChanges || HotReloadTimelineHelper.UnsupportedChangesCount == 0 && (!HotReloadPrefs.AutoRecompilePartiallyUnsupportedChanges || HotReloadTimelineHelper.PartiallySupportedChangesCount == 0) || _compileError || EditorApplication.isPlaying && !HotReloadPrefs.AutoRecompileUnsupportedChangesInPlayMode ) { return false; } if (HotReloadPrefs.ShowCompilingUnsupportedNotifications) { EditorWindowHelper.ShowNotification(EditorWindowHelper.NotificationStatus.NeedsRecompile); } HotReloadRunTab.Recompile(); return true; } private static DateTime lastPrepareBuildInfo = DateTime.UtcNow; /// Post state for player builds. /// Only check build target because user can change build settings whenever. internal static void TryPrepareBuildInfo() { // Note: we post files state even when build target is wrong // because you might connect with a build downloaded onto the device. if ((DateTime.UtcNow - lastPrepareBuildInfo).TotalSeconds > 5) { lastPrepareBuildInfo = DateTime.UtcNow; HotReloadCli.PrepareBuildInfoAsync().Forget(); } } internal static void RecordActiveDaysForRateApp() { var unixDay = (int)(DateTimeOffset.UtcNow.ToUnixTimeSeconds() / 86400); var activeDays = GetActiveDaysForRateApp(); if (activeDays.Count < Constants.DaysToRateApp && activeDays.Add(unixDay.ToString())) { HotReloadPrefs.ActiveDays = string.Join(",", activeDays); } } internal static HashSet GetActiveDaysForRateApp() { if (string.IsNullOrEmpty(HotReloadPrefs.ActiveDays)) { return new HashSet(); } return new HashSet(HotReloadPrefs.ActiveDays.Split(',')); } // CheckEditorStart distinguishes between domain reload and first editor open // We have some separate logic on editor start (InstallUtility.HandleEditorStart) private static void DetectEditorStart() { var editorId = EditorAnalyticsSessionInfo.id; var currVersion = PackageConst.Version; Task.Run(() => { try { var lines = File.Exists(sessionFilePath) ? File.ReadAllLines(sessionFilePath) : Array.Empty(); long prevSessionId = -1; string prevVersion = null; if (lines.Length >= 2) { long.TryParse(lines[1], out prevSessionId); } if (lines.Length >= 3) { prevVersion = lines[2].Trim(); } var updatedFromVersion = (prevSessionId != -1 && currVersion != prevVersion) ? prevVersion : null; if (prevSessionId != editorId && prevSessionId != 0) { // back to mainthread ThreadUtility.RunOnMainThread(() => { InstallUtility.HandleEditorStart(updatedFromVersion); var newEditorId = EditorAnalyticsSessionInfo.id; if (newEditorId != 0) { Task.Run(() => { try { // editorId isn't available on first domain reload, must do it here File.WriteAllLines(sessionFilePath, new[] { "1", // serialization version newEditorId.ToString(), currVersion, }); } catch (IOException) { // ignore } }); } }); } } catch (IOException) { // ignore } catch (Exception e) { ThreadUtility.LogException(e); } }); } private static void DetectVersionUpdate() { if (serverDownloader.CheckIfDownloaded(HotReloadCli.controller)) { return; } ServerHealthCheck.instance.CheckHealth(); if (!ServerHealthCheck.I.IsServerHealthy) { return; } var restartServer = EditorUtility.DisplayDialog("Hot Reload", $"When updating Hot Reload, the server must be restarted for the update to take effect." + "\nDo you want to restart it now?", "Restart server", "Don't restart"); if (restartServer) { RestartCodePatcher().Forget(); } } private static void UpdateHost() { RequestHelper.SetServerInfo(new PatchServerInfo(RequestHelper.defaultServerHost, HotReloadState.ServerPort, null, Path.GetFullPath("."))); } static void OnIntervalThreaded(object o) { ServerHealthCheck.instance.CheckHealth(); ThreadUtility.RunOnMainThread((Action)o); if (serverDownloader.Progress >= 1f) { serverDownloader.CheckIfDownloaded(HotReloadCli.controller); } } private static bool _requestingFlushErrors; private static long _lastErrorFlush; private static async Task RequestFlushErrors() { _requestingFlushErrors = true; try { await RequestFlushErrorsCore(); } finally { _requestingFlushErrors = false; } } private static async Task RequestFlushErrorsCore() { var pollFrequency = 500; // Delay until we've hit the poll request frequency var waitMs = (int)Mathf.Clamp(pollFrequency - ((DateTime.Now.Ticks / (float)TimeSpan.TicksPerMillisecond) - _lastErrorFlush), 0, pollFrequency); await Task.Delay(waitMs); await FlushErrors(); _lastErrorFlush = DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond; } static async Task FlushErrors() { var response = await RequestHelper.RequestFlushErrors(); if (response == null) { return; } foreach (var responseWarning in response.warnings) { if (responseWarning.Contains("Scripts have compile errors")) { Log.Error(responseWarning); } else { Log.Warning(responseWarning); } if (responseWarning.Contains("Multidimensional arrays are not supported")) { await ThreadUtility.SwitchToMainThread(); HotReloadSuggestionsHelper.SetSuggestionsShown(HotReloadSuggestionKind.MultidimensionalArrays); } } foreach (var responseError in response.errors) { Log.Error(responseError); } } internal static bool firstPatchAttempted; static void OnIntervalMainThread() { HotReloadSuggestionsHelper.Check(); // Moved from RequestServerInfo to avoid GC allocations when HR is not active // Repaint if the running Status has changed since the layout changes quite a bit if (running != ServerHealthCheck.I.IsServerHealthy) { if (HotReloadWindow.Current) { HotReloadRunTab.RepaintInstant(); } running = ServerHealthCheck.I.IsServerHealthy; } if (!running) { startupCompletedAt = null; } if (!running && !StartedServerRecently()) { // Reset startup progress startupProgress = null; } if(ServerHealthCheck.I.IsServerHealthy) { // NOTE: avoid calling this method when HR is not running to avoid allocations RequestServerInfo(); TryPrepareBuildInfo(); if (!requestingCompile && (!config.patchEditModeOnlyOnEditorFocus || Application.isPlaying || UnityEditorInternal.InternalEditorUtility.isApplicationActive)) { RequestHelper.PollMethodPatches(HotReloadState.LastPatchId, resp => HandleResponseReceived(resp)); } RequestHelper.PollPatchStatus(resp => { patchStatus = resp.patchStatus; if (patchStatus == PatchStatus.Compiling) { startWaitingForCompile = null; } if (patchStatus == PatchStatus.Patching) { firstPatchAttempted = true; if (HotReloadPrefs.ShowPatchingNotifications) { EditorWindowHelper.ShowNotification(EditorWindowHelper.NotificationStatus.Patching, maxDuration: 10); } } else if (HotReloadPrefs.ShowPatchingNotifications) { EditorWindowHelper.RemoveNotification(); } }, patchStatus); if (HotReloadPrefs.AllAssetChanges) { RequestHelper.PollAssetChanges(HandleAssetChange); } } if (!ServerHealthCheck.I.IsServerHealthy) { stopping = false; } if (startupProgress?.Item1 == 1) { starting = false; } if (!_requestingFlushErrors && Running) { RequestFlushErrors().Forget(); } CheckEditorSettings(); } static void CheckEditorSettings() { if (quitting) { return; } CheckAutoRefresh(); CheckScriptCompilation(); CheckPlaymodeTint(); CheckAssetDatabaseRefresh(); } static void CheckAutoRefresh() { if (HotReloadPrefs.AllowDisableUnityAutoRefresh && ServerHealthCheck.I.IsServerHealthy) { AutoRefreshSettingChecker.Apply(); AutoRefreshSettingChecker.Check(); } else { AutoRefreshSettingChecker.Reset(); } } static void CheckScriptCompilation() { if (HotReloadPrefs.AllowDisableUnityAutoRefresh && ServerHealthCheck.I.IsServerHealthy) { ScriptCompilationSettingChecker.Apply(); ScriptCompilationSettingChecker.Check(); } else { ScriptCompilationSettingChecker.Reset(); } } static string[] assetExtensionBlacklist = new[] { ".cs", // TODO add setting to allow scenes to get hot reloaded for users who collaborate (their scenes change externally) ".unity", // safer to ignore meta files completely until there's a use-case ".meta", // debug files ".mdb", ".pdb", // ".shader", //use assetBlacklist instead }; public static string[] compileFiles = new[] { ".asmdef", ".asmref", ".rsp", }; public static string[] plugins = new[] { // native plugins ".dll", ".bundle", ".dylib", ".so", // plugin scripts ".cpp", ".h", ".aar", ".jar", ".a", ".java" }; static void HandleAssetChange(string assetPath) { // ignore directories if (Directory.Exists(assetPath)) { return; } foreach (var compileFile in compileFiles) { if (assetPath.EndsWith(compileFile, StringComparison.Ordinal)) { HotReloadTimelineHelper.CreateErrorEventEntry($"errors: AssemblyFileEdit: Editing assembly files requires recompiling in Unity. in {assetPath}", entryType: EntryType.Foldout); _applyingFailed = true; if (HotReloadPrefs.AutoRecompileUnsupportedChangesImmediately || UnityEditorInternal.InternalEditorUtility.isApplicationActive) { TryRecompileUnsupportedChanges(); } return; } } // Add plugin changes to unsupported changes list foreach (var plugin in plugins) { if (assetPath.EndsWith(plugin, StringComparison.Ordinal)) { HotReloadTimelineHelper.CreateErrorEventEntry($"errors: NativePluginEdit: Editing native plugins requires recompiling in Unity. in {assetPath}", entryType: EntryType.Foldout); _applyingFailed = true; if (HotReloadPrefs.AutoRecompileUnsupportedChangesImmediately || UnityEditorInternal.InternalEditorUtility.isApplicationActive) { TryRecompileUnsupportedChanges(); } return; } } // ignore file extensions that trigger domain reload if (!HotReloadPrefs.IncludeShaderChanges) { if (assetPath.EndsWith(".shader", StringComparison.Ordinal)) { return; } } foreach (var blacklisted in assetExtensionBlacklist) { if (assetPath.EndsWith(blacklisted, StringComparison.Ordinal)) { return; } } if (config?.assetBlacklist != null) { foreach (var blacklisted in config.assetBlacklist) { if (assetPath.EndsWith(blacklisted, StringComparison.Ordinal)) { return; } } } var relativePath = GetRelativePath(assetPath, Path.GetFullPath("Assets")); var relativePathPackages = GetRelativePath(assetPath, Path.GetFullPath("Packages")); // ignore files outside assets and packages folders if (relativePath.StartsWith("..", StringComparison.Ordinal) && relativePathPackages.StartsWith("..", StringComparison.Ordinal) ) { return; } try { if (!File.Exists(assetPath)) { AssetDatabase.DeleteAsset(relativePath); } else { AssetDatabase.ImportAsset(relativePath, ImportAssetOptions.ForceUpdate); } } catch (Exception e){ Log.Warning($"Refreshing asset at path: {assetPath} failed due to exception: {e}"); } } public static string GetRelativePath(string filespec, string folder) { Uri pathUri = new Uri(filespec); Uri folderUri = new Uri(folder); return Uri.UnescapeDataString(folderUri.MakeRelativeUri(pathUri).ToString().Replace('/', Path.DirectorySeparatorChar)); } static void CheckPlaymodeTint() { if (config.changePlaymodeTint && ServerHealthCheck.I.IsServerHealthy && Application.isPlaying) { PlaymodeTintSettingChecker.Apply(); PlaymodeTintSettingChecker.Check(); } else { PlaymodeTintSettingChecker.Reset(); } } static void CheckAssetDatabaseRefresh() { if (config.disableCompilingFromEditorScripts && ServerHealthCheck.I.IsServerHealthy) { CompileMethodDetourer.Apply(); } else { CompileMethodDetourer.Reset(); } } static void HandleResponseReceived(MethodPatchResponse response) { HandleRemovedUnityMethods(response.removedMethod); RegisterPatchesResult patchResult = null; if (response.patches?.Length > 0) { LogBurstHint(response); patchResult = CodePatcher.I.RegisterPatches(response, persist: true); CodePatcher.I.SaveAppliedPatches(patchesFilePath).Forget(); } var partiallySupportedChangesFiltered = new List(response.partiallySupportedChanges ?? Array.Empty()); partiallySupportedChangesFiltered.RemoveAll(x => !HotReloadTimelineHelper.GetPartiallySupportedChangePref(x)); var failuresDeduplicated = new HashSet(response.failures ?? Array.Empty()); _compileError = response.failures?.Any(failure => failure.Contains("error CS")) ?? false; _applyingFailed = response.failures?.Length > 0 || patchResult?.patchFailures.Count > 0; _appliedPartially = !_applyingFailed && partiallySupportedChangesFiltered.Count > 0; if (_compileError) { HotReloadTimelineHelper.EventsTimeline.RemoveAll(e => e.alertType == AlertType.CompileError); foreach (var failure in failuresDeduplicated) { if (failure.Contains("error CS")) { HotReloadTimelineHelper.CreateErrorEventEntry(failure); } } } else if (_applyingFailed) { if (partiallySupportedChangesFiltered.Count > 0) { foreach (var responsePartiallySupportedChange in partiallySupportedChangesFiltered) { HotReloadTimelineHelper.CreatePartiallyAppliedEventEntry(responsePartiallySupportedChange, entryType: EntryType.Child); } } foreach (var failure in failuresDeduplicated) { HotReloadTimelineHelper.CreateErrorEventEntry(failure, entryType: EntryType.Child); } if (patchResult?.patchFailures.Count > 0) { foreach (var failure in patchResult.patchFailures) { SMethod method = failure.Item1; string error = failure.Item2; HotReloadTimelineHelper.CreatePatchFailureEventEntry(error, methodName: GetMethodName(method), methodSimpleName: method.simpleName, entryType: EntryType.Child); } } HotReloadTimelineHelper.CreateReloadFinishedWithWarningsEventEntry(); HotReloadSuggestionsHelper.SetSuggestionsShown(HotReloadSuggestionKind.UnsupportedChanges); if (HotReloadPrefs.AutoRecompileUnsupportedChangesImmediately || UnityEditorInternal.InternalEditorUtility.isApplicationActive) { TryRecompileUnsupportedChanges(); } } else if (_appliedPartially) { foreach (var responsePartiallySupportedChange in partiallySupportedChangesFiltered) { HotReloadTimelineHelper.CreatePartiallyAppliedEventEntry(responsePartiallySupportedChange, entryType: EntryType.Child, detailed: false); } HotReloadTimelineHelper.CreateReloadPartiallyAppliedEventEntry(); if (HotReloadPrefs.AutoRecompileUnsupportedChangesImmediately || UnityEditorInternal.InternalEditorUtility.isApplicationActive) { TryRecompileUnsupportedChanges(); } } else { HotReloadTimelineHelper.CreateReloadFinishedEventEntry(); } // When patching different assembly, compile error will get removed, even though it's still there // It's a shortcut we take for simplicity if (!_compileError) { HotReloadTimelineHelper.EventsTimeline.RemoveAll(x => x.alertType == AlertType.CompileError); } if (HotReloadWindow.Current) { HotReloadWindow.Current.Repaint(); } HotReloadState.LastPatchId = response.id; OnPatchHandled?.Invoke(); } static string GetMethodName(SMethod method) { var spaceIndex = method.displayName.IndexOf(" ", StringComparison.Ordinal); if (spaceIndex > 0) { return method.displayName.Substring(spaceIndex); } return method.displayName; } static void HandleRemovedUnityMethods(SMethod[] removedMethods) { if (removedMethods == null) { return; } foreach(var sMethod in removedMethods) { try { var candidates = CodePatcher.I.SymbolResolver.Resolve(sMethod.assemblyName.Replace(".dll", "")); var asm = candidates[0]; var module = asm.GetLoadedModules()[0]; var oldMethod = module.ResolveMethod(sMethod.metadataToken); UnityEventHelper.RemoveUnityEventMethod(oldMethod); } catch(Exception ex) { Log.Warning("Encountered exception in RemoveUnityEventMethod: {0} {1}", ex.GetType().Name, ex.Message); } } } [Conditional("UNITY_2022_2_OR_NEWER")] static void LogBurstHint(MethodPatchResponse response) { if(HotReloadPrefs.LoggedBurstHint) { return; } foreach (var patch in response.patches) { if(patch.unityJobs.Length > 0) { Debug.LogWarning("A unity job was hot reloaded. " + "This will cause a harmless warning that can be ignored. " + $"More info about this can be found here: {Constants.TroubleshootingURL}"); HotReloadPrefs.LoggedBurstHint = true; break; } } } private static DateTime? startWaitingForCompile; static void OnCompilationFinished() { ServerHealthCheck.instance.CheckHealth(); if(ServerHealthCheck.I.IsServerHealthy) { startWaitingForCompile = DateTime.UtcNow; firstPatchAttempted = false; RequestCompile().Forget(); } Task.Run(() => File.Delete(patchesFilePath)); HotReloadTimelineHelper.ClearPersistance(); } static bool requestingCompile; static async Task RequestCompile() { requestingCompile = true; try { await RequestHelper.RequestClearPatches(); await ProjectGeneration.ProjectGeneration.GenerateSlnAndCsprojFiles(Application.dataPath); await RequestHelper.RequestCompile(); } finally { requestingCompile = false; } } private static bool stopping; private static bool starting; private static DateTime? startupCompletedAt; private static Tuple startupProgress; internal static bool Started => ServerHealthCheck.I.IsServerHealthy && DownloadProgress == 1 && StartupProgress?.Item1 == 1; internal static bool Starting => (StartedServerRecently() || ServerHealthCheck.I.IsServerHealthy) && !Started && starting && patchStatus != PatchStatus.CompileError; internal static bool Stopping => stopping && Running; internal static bool Compiling => DateTime.UtcNow - startWaitingForCompile < TimeSpan.FromSeconds(5) || patchStatus == PatchStatus.Compiling || HotReloadRunTab.recompiling; internal static Tuple StartupProgress => startupProgress; /// /// We have a button to stop the Hot Reload server.
/// Store task to ensure only one stop attempt at a time. ///
private static DateTime? serverStartedAt; private static DateTime? serverStoppedAt; private static DateTime? serverRestartedAt; private static bool StartedServerRecently() { return DateTime.UtcNow - serverStartedAt < ServerHealthCheck.HeartBeatTimeout; } internal static bool StoppedServerRecently() { return DateTime.UtcNow - serverStoppedAt < ServerHealthCheck.HeartBeatTimeout || (!StartedServerRecently() && (startupProgress?.Item1 ?? 0) == 0); } internal static bool RestartedServerRecently() { return DateTime.UtcNow - serverRestartedAt < ServerHealthCheck.HeartBeatTimeout; } private static bool requestingStart; private static async Task StartCodePatcher(LoginData loginData = null) { if (requestingStart || StartedServerRecently()) { return; } stopping = false; starting = true; var exposeToNetwork = HotReloadPrefs.ExposeServerToLocalNetwork; var allAssetChanges = HotReloadPrefs.AllAssetChanges; var disableConsoleWindow = HotReloadPrefs.DisableConsoleWindow; CodePatcher.I.ClearPatchedMethods(); try { requestingStart = true; startupProgress = Tuple.Create(0f, "Starting Hot Reload"); serverStartedAt = DateTime.UtcNow; await HotReloadCli.StartAsync(exposeToNetwork, allAssetChanges, disableConsoleWindow, loginData).ConfigureAwait(false); } catch (Exception ex) { ThreadUtility.LogException(ex); } finally { requestingStart = false; } } private static bool requestingStop; internal static async Task StopCodePatcher() { stopping = true; starting = false; if (requestingStop) { return; } CodePatcher.I.ClearPatchedMethods(); HotReloadSuggestionsHelper.SetSuggestionInactive(HotReloadSuggestionKind.EditorsWithoutHRRunning); try { requestingStop = true; await HotReloadCli.StopAsync().ConfigureAwait(false); serverStoppedAt = DateTime.UtcNow; await ThreadUtility.SwitchToMainThread(); startupProgress = null; } catch (Exception ex) { ThreadUtility.LogException(ex); } finally { requestingStop = false; } } private static bool requestingRestart; internal static async Task RestartCodePatcher() { if (requestingRestart) { return; } try { requestingRestart = true; await StopCodePatcher(); await DownloadAndRun(); serverRestartedAt = DateTime.UtcNow; } finally { requestingRestart = false; } } private static bool requestingDownloadAndRun; internal static float DownloadProgress => serverDownloader.Progress; internal static bool DownloadRequired => DownloadProgress < 1f; internal static bool DownloadStarted => serverDownloader.Started; internal static bool RequestingDownloadAndRun => requestingDownloadAndRun; internal static async Task DownloadAndRun(LoginData loginData = null) { if (requestingDownloadAndRun) { return false; } stopping = false; requestingDownloadAndRun = true; try { if (DownloadRequired) { var ok = await serverDownloader.PromptForDownload(); if (!ok) { return false; } } await StartCodePatcher(loginData); return true; } finally { requestingDownloadAndRun = false; } } private const int SERVER_POLL_FREQUENCY_ON_STARTUP_MS = 500; private const int SERVER_POLL_FREQUENCY_AFTER_STARTUP_MS = 2000; private static int GetPollFrequency() { return (startupProgress != null && startupProgress.Item1 < 1) || StartedServerRecently() ? SERVER_POLL_FREQUENCY_ON_STARTUP_MS : SERVER_POLL_FREQUENCY_AFTER_STARTUP_MS; } internal static bool RequestingLoginInfo { get; set; } [CanBeNull] internal static LoginStatusResponse Status { get; private set; } internal static void HandleStatus(LoginStatusResponse resp) { Attribution.RegisterLogin(resp); bool consumptionsChanged = Status?.freeSessionRunning != resp.freeSessionRunning || Status?.freeSessionEndTime != resp.freeSessionEndTime; bool expiresAtChanged = Status?.licenseExpiresAt != resp.licenseExpiresAt; if (resp.consumptionsUnavailableReason == ConsumptionsUnavailableReason.UnrecoverableError && Status?.consumptionsUnavailableReason != ConsumptionsUnavailableReason.UnrecoverableError ) { Log.Error("Free charges unavailabe. Please contact support if the issue persists."); } if (!RequestingLoginInfo && resp.requestError == null) { Status = resp; } if (resp.lastLicenseError == null) { // If we got success, we should always show an error next time it comes up HotReloadPrefs.ErrorHidden = false; } var oldStartupProgress = startupProgress; var newStartupProgress = Tuple.Create( resp.startupProgress, string.IsNullOrEmpty(resp.startupStatus) ? "Starting Hot Reload" : resp.startupStatus); startupProgress = newStartupProgress; // ReSharper disable once CompareOfFloatsByEqualityOperator if (startupCompletedAt == null && newStartupProgress.Item1 == 1f) { startupCompletedAt = DateTime.UtcNow; } if (oldStartupProgress == null || Math.Abs(oldStartupProgress.Item1 - newStartupProgress.Item1) > 0 || oldStartupProgress.Item2 != newStartupProgress.Item2 || consumptionsChanged || expiresAtChanged ) { // Send project files state now that server can receive requests (only needed for player builds) TryPrepareBuildInfo(); } } internal static async Task RequestLogin(string email, string password) { RequestingLoginInfo = true; try { int i = 0; while (!Running && i < 100) { await Task.Delay(100); i++; } Status = await RequestHelper.RequestLogin(email, password, 10); // set to false so new error is shown HotReloadPrefs.ErrorHidden = false; if (Status?.isLicensed == true) { HotReloadPrefs.LicenseEmail = email; HotReloadPrefs.LicensePassword = Status.initialPassword ?? password; } } finally { RequestingLoginInfo = false; } } private static bool requestingServerInfo; private static long lastServerPoll; private static bool running; internal static bool Running => ServerHealthCheck.I.IsServerHealthy; internal static void RequestServerInfo() { if (requestingServerInfo) { return; } RequestServerInfoAsync().Forget(); } private static async Task RequestServerInfoAsync() { requestingServerInfo = true; try { await RequestServerInfoCore(); } finally { requestingServerInfo = false; } } private static async Task RequestServerInfoCore() { var pollFrequency = GetPollFrequency(); // Delay until we've hit the poll request frequency var waitMs = (int)Mathf.Clamp(pollFrequency - ((DateTime.Now.Ticks / (float)TimeSpan.TicksPerMillisecond) - lastServerPoll), 0, pollFrequency); await Task.Delay(waitMs); if (!ServerHealthCheck.I.IsServerHealthy) { return; } var resp = await RequestHelper.GetLoginStatus(30); HandleStatus(resp); lastServerPoll = DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond; } } }