CapersProject/Packages/com.singularitygroup.hotreload/Editor/EditorCodePatcher.cs

913 lines
40 KiB
C#

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<Config>(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<bool> 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<string> GetActiveDaysForRateApp() {
if (string.IsNullOrEmpty(HotReloadPrefs.ActiveDays)) {
return new HashSet<string>();
}
return new HashSet<string>(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<string>();
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<PartiallySupportedChange>(response.partiallySupportedChanges ?? Array.Empty<PartiallySupportedChange>());
partiallySupportedChangesFiltered.RemoveAll(x => !HotReloadTimelineHelper.GetPartiallySupportedChangePref(x));
var failuresDeduplicated = new HashSet<string>(response.failures ?? Array.Empty<string>());
_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<float, string> 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<float, string> StartupProgress => startupProgress;
/// <summary>
/// We have a button to stop the Hot Reload server.<br/>
/// Store task to ensure only one stop attempt at a time.
/// </summary>
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<bool> 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;
}
}
}