HotReloadCli.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. using System;
  2. using System.Diagnostics;
  3. using System.IO;
  4. using System.Net;
  5. #if UNITY_EDITOR_WIN
  6. using System.Net.NetworkInformation;
  7. #else
  8. using System.Net.Sockets;
  9. #endif
  10. using System.Threading.Tasks;
  11. using SingularityGroup.HotReload.Newtonsoft.Json;
  12. using UnityEditor;
  13. namespace SingularityGroup.HotReload.Editor.Cli {
  14. [InitializeOnLoad]
  15. public static class HotReloadCli {
  16. internal static readonly ICliController controller;
  17. //InitializeOnLoad ensures controller gets initialized on unity thread
  18. static HotReloadCli() {
  19. controller =
  20. #if UNITY_EDITOR_OSX
  21. new OsxCliController();
  22. #elif UNITY_EDITOR_LINUX
  23. new LinuxCliController();
  24. #elif UNITY_EDITOR_WIN
  25. new WindowsCliController();
  26. #else
  27. new FallbackCliController();
  28. #endif
  29. }
  30. public static bool CanOpenInBackground => controller.CanOpenInBackground;
  31. /// <summary>
  32. /// Public API: Starts the Hot Reload server. Must be on the main thread
  33. /// </summary>
  34. public static Task StartAsync() {
  35. return StartAsync(
  36. isReleaseMode: RequestHelper.IsReleaseMode(),
  37. exposeServerToNetwork: HotReloadPrefs.ExposeServerToLocalNetwork,
  38. allAssetChanges: HotReloadPrefs.AllAssetChanges,
  39. createNoWindow: HotReloadPrefs.DisableConsoleWindow,
  40. detailedErrorReporting: !HotReloadPrefs.DisableDetailedErrorReporting
  41. );
  42. }
  43. internal static async Task StartAsync(bool exposeServerToNetwork, bool allAssetChanges, bool createNoWindow, bool isReleaseMode, bool detailedErrorReporting, LoginData loginData = null) {
  44. var port = await Prepare().ConfigureAwait(false);
  45. await ThreadUtility.SwitchToThreadPool();
  46. StartArgs args;
  47. if (TryGetStartArgs(UnityHelper.DataPath, exposeServerToNetwork, allAssetChanges, createNoWindow, isReleaseMode, detailedErrorReporting, loginData, port, out args)) {
  48. await controller.Start(args);
  49. }
  50. }
  51. /// <summary>
  52. /// Public API: Stops the Hot Reload server
  53. /// </summary>
  54. /// <remarks>
  55. /// This is a no-op in case the server is not running
  56. /// </remarks>
  57. public static Task StopAsync() {
  58. return controller.Stop();
  59. }
  60. class Config {
  61. #pragma warning disable CS0649
  62. public bool useBuiltInProjectGeneration;
  63. #pragma warning restore CS0649
  64. }
  65. static bool TryGetStartArgs(string dataPath, bool exposeServerToNetwork, bool allAssetChanges, bool createNoWindow, bool isReleaseMode, bool detailedErrorReporting, LoginData loginData, int port, out StartArgs args) {
  66. string serverDir;
  67. if(!CliUtils.TryFindServerDir(out serverDir)) {
  68. Log.Warning($"Failed to start the Hot Reload Server. " +
  69. $"Unable to locate the 'Server' directory. " +
  70. $"Make sure the 'Server' directory is " +
  71. $"somewhere in the Assets folder inside a 'HotReload' folder or in the HotReload package");
  72. args = null;
  73. return false;
  74. }
  75. Config config;
  76. if (File.Exists(PackageConst.ConfigFileName)) {
  77. config = JsonConvert.DeserializeObject<Config>(File.ReadAllText(PackageConst.ConfigFileName));
  78. } else {
  79. config = new Config();
  80. }
  81. var hotReloadTmpDir = CliUtils.GetHotReloadTempDir();
  82. var cliTempDir = CliUtils.GetCliTempDir();
  83. // Versioned path so that we only need to extract the binary once. User can have multiple projects
  84. // on their machine using different HotReload versions.
  85. var executableTargetDir = CliUtils.GetExecutableTargetDir();
  86. Directory.CreateDirectory(executableTargetDir); // ensure exists
  87. var executableSourceDir = Path.Combine(serverDir, controller.PlatformName);
  88. var unityProjDir = Path.GetDirectoryName(dataPath);
  89. string slnPath;
  90. if (config.useBuiltInProjectGeneration) {
  91. var info = new DirectoryInfo(Path.GetFullPath("."));
  92. slnPath = Path.Combine(Path.GetFullPath("."), info.Name + ".sln");
  93. if (!File.Exists(slnPath)) {
  94. Log.Warning($"Failed to start the Hot Reload Server. Cannot find solution file. Please disable \"useBuiltInProjectGeneration\" in settings to enable custom project generation.");
  95. args = null;
  96. return false;
  97. }
  98. Log.Info("Using default project generation. If you encounter any problem with Unity's default project generation consider disabling it to use custom project generation.");
  99. try {
  100. Directory.Delete(ProjectGeneration.ProjectGeneration.tempDir, true);
  101. } catch(Exception ex) {
  102. Log.Exception(ex);
  103. }
  104. } else {
  105. slnPath = ProjectGeneration.ProjectGeneration.GetSolutionFilePath(dataPath);
  106. }
  107. if (!File.Exists(slnPath)) {
  108. Log.Warning($"No .sln file found. Open any c# file to generate it so Hot Reload can work properly");
  109. }
  110. var searchAssemblies = string.Join(";", CodePatcher.I.GetAssemblySearchPaths());
  111. var cliArguments = $@"-u ""{unityProjDir}"" -s ""{slnPath}"" -t ""{cliTempDir}"" -a ""{searchAssemblies}"" -ver ""{PackageConst.Version}"" -proc ""{Process.GetCurrentProcess().Id}"" -assets ""{allAssetChanges}"" -p ""{port}"" -r {isReleaseMode} -detailed-error-reporting {detailedErrorReporting}";
  112. if (loginData != null) {
  113. cliArguments += $@" -email ""{loginData.email}"" -pass ""{loginData.password}""";
  114. }
  115. if (exposeServerToNetwork) {
  116. // server will listen on local network interface (default is localhost only)
  117. cliArguments += " -e true";
  118. }
  119. args = new StartArgs {
  120. hotreloadTempDir = hotReloadTmpDir,
  121. cliTempDir = cliTempDir,
  122. executableTargetDir = executableTargetDir,
  123. executableSourceDir = executableSourceDir,
  124. cliArguments = cliArguments,
  125. unityProjDir = unityProjDir,
  126. createNoWindow = createNoWindow,
  127. };
  128. return true;
  129. }
  130. private static int DiscoverFreePort() {
  131. var maxAttempts = 10;
  132. for (int attempt = 0; attempt < maxAttempts; attempt++) {
  133. var port = RequestHelper.defaultPort + attempt;
  134. if (IsPortInUse(port)) {
  135. continue;
  136. }
  137. return port;
  138. }
  139. // we give up at this point
  140. return RequestHelper.defaultPort + maxAttempts;
  141. }
  142. public static bool IsPortInUse(int port) {
  143. // Note that there is a racecondition that a port gets occupied after checking.
  144. // However, it will very rare someone will run into this.
  145. #if UNITY_EDITOR_WIN
  146. IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
  147. IPEndPoint[] activeTcpListeners = ipGlobalProperties.GetActiveTcpListeners();
  148. foreach (IPEndPoint endPoint in activeTcpListeners) {
  149. if (endPoint.Port == port) {
  150. return true;
  151. }
  152. }
  153. return false;
  154. #else
  155. try {
  156. using (TcpClient tcpClient = new TcpClient()) {
  157. tcpClient.Connect(IPAddress.Loopback, port); // Try to connect to the specified port
  158. return true;
  159. }
  160. } catch (SocketException) {
  161. return false;
  162. } catch (Exception e) {
  163. Log.Exception(e);
  164. // act as if the port is allocated
  165. return true;
  166. }
  167. #endif
  168. }
  169. static async Task<int> Prepare() {
  170. await ThreadUtility.SwitchToMainThread();
  171. var dataPath = UnityHelper.DataPath;
  172. await ProjectGeneration.ProjectGeneration.EnsureSlnAndCsprojFiles(dataPath);
  173. await PrepareBuildInfoAsync();
  174. PrepareSystemPathsFile();
  175. var port = DiscoverFreePort();
  176. HotReloadState.ServerPort = port;
  177. RequestHelper.SetServerPort(port);
  178. return port;
  179. }
  180. static bool didLogWarning;
  181. internal static async Task PrepareBuildInfoAsync() {
  182. await ThreadUtility.SwitchToMainThread();
  183. var buildInfoInput = await BuildInfoHelper.GetGenerateBuildInfoInput();
  184. await Task.Run(() => {
  185. try {
  186. var buildInfo = BuildInfoHelper.GenerateBuildInfoThreaded(buildInfoInput);
  187. PrepareBuildInfo(buildInfo);
  188. } catch (Exception e) {
  189. if (!didLogWarning) {
  190. Log.Warning($"Preparing build info failed! On-device functionality might not work. Exception: {e}");
  191. didLogWarning = true;
  192. } else {
  193. Log.Debug($"Preparing build info failed! On-device functionality might not work. Exception: {e}");
  194. }
  195. }
  196. });
  197. }
  198. internal static void PrepareBuildInfo(BuildInfo buildInfo) {
  199. // When starting server make sure it starts with correct player data state.
  200. // (this fixes issue where Unity is in background and not sending files state).
  201. // Always write player data because you can be on any build target and want to connect with a downloaded android build.
  202. var json = buildInfo.ToJson();
  203. var cliTempDir = CliUtils.GetCliTempDir();
  204. Directory.CreateDirectory(cliTempDir);
  205. File.WriteAllText(Path.Combine(cliTempDir, "playerdata.json"), json);
  206. }
  207. static void PrepareSystemPathsFile() {
  208. #pragma warning disable CS0618 // obsolete since 2023
  209. var lvl = PlayerSettings.GetApiCompatibilityLevel(EditorUserBuildSettings.selectedBuildTargetGroup);
  210. #pragma warning restore CS0618
  211. #if UNITY_2020_3_OR_NEWER
  212. var dirs = UnityEditor.Compilation.CompilationPipeline.GetSystemAssemblyDirectories(lvl);
  213. #else
  214. var t = typeof(UnityEditor.Editor).Assembly.GetType("UnityEditor.Scripting.ScriptCompilation.MonoLibraryHelpers");
  215. var m = t.GetMethod("GetSystemReferenceDirectories");
  216. var dirs = m.Invoke(null, new object[] { lvl });
  217. #endif
  218. Directory.CreateDirectory(PackageConst.LibraryCachePath);
  219. File.WriteAllText(PackageConst.LibraryCachePath + "/systemAssemblies.json", JsonConvert.SerializeObject(dirs));
  220. }
  221. }
  222. }