OsxCliController.cs 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. using System;
  2. using System.Diagnostics;
  3. using System.IO;
  4. using System.Threading.Tasks;
  5. using SingularityGroup.HotReload.Editor.Semver;
  6. using Debug = UnityEngine.Debug;
  7. namespace SingularityGroup.HotReload.Editor.Cli {
  8. class OsxCliController : ICliController {
  9. Process process;
  10. public string BinaryFileName => "HotReload.app.zip";
  11. public string PlatformName => "osx-x64";
  12. public bool CanOpenInBackground => false;
  13. /// In MacOS 13 Ventura, our app cannot launch a terminal window.
  14. /// We use a custom app that launches HotReload server and shows it's output (just like a terminal would).
  15. // Including MacOS 12 Monterey as well so I can dogfood it -Troy
  16. private static bool UseCustomConsoleApp() => MacOSVersion.Value.Major >= 12;
  17. // dont use static because null comparison on SemVersion is broken
  18. private static readonly Lazy<SemVersion> MacOSVersion = new Lazy<SemVersion>(() => {
  19. //UnityHelper.OperatingSystem; // in Unity 2018 it returns 10.16 on monterey (no idea why)
  20. //Environment.OSVersion returns unix version like 21.x
  21. var startinfo = new ProcessStartInfo {
  22. FileName = "/usr/bin/sw_vers",
  23. Arguments = "-productVersion",
  24. UseShellExecute = false,
  25. RedirectStandardOutput = true,
  26. CreateNoWindow = true,
  27. };
  28. var process = Process.Start(startinfo);
  29. string osVersion = process.StandardOutput.ReadToEnd().Trim();
  30. SemVersion macosVersion;
  31. if (SemVersion.TryParse(osVersion, out macosVersion)) {
  32. return macosVersion;
  33. }
  34. // should never happen
  35. Log.Warning("Failed to detect MacOS version, if Hot Reload fails to start, please contact support.");
  36. return SemVersion.None;
  37. });
  38. public async Task Start(StartArgs args) {
  39. // Unzip the .app.zip to temp folder .app
  40. var appExecutablePath = $"{args.executableTargetDir}/HotReload.app/Contents/MacOS/HotReload";
  41. var cliExecutablePath = $"{args.executableTargetDir}/HotReload.app/Contents/Resources/CodePatcherCLI";
  42. // ensure running on threadpool
  43. await ThreadUtility.SwitchToThreadPool();
  44. // executableTargetDir is versioned, so only need to extract once.
  45. if (!File.Exists(appExecutablePath)) {
  46. try {
  47. // delete only the extracted app folder (must not delete downloaded zip which is in same folder)
  48. Directory.Delete(args.executableTargetDir + "/HotReload.app", true);
  49. } catch (IOException) {
  50. // ignore directory not found
  51. }
  52. Directory.CreateDirectory(args.executableTargetDir);
  53. UnzipMacOsPackage($"{args.executableTargetDir}/{BinaryFileName}", args.executableTargetDir + "/");
  54. }
  55. try {
  56. // Always stop first because rarely it has happened that the server process was still running after custom console closed.
  57. // Note: this will also stop Hot Reload started by other Unity projects.
  58. await Stop();
  59. } catch {
  60. // ignored
  61. }
  62. if (UseCustomConsoleApp()) {
  63. await StartCustomConsole(args, appExecutablePath);
  64. } else {
  65. await StartTerminal(args, cliExecutablePath);
  66. }
  67. }
  68. public Task StartCustomConsole(StartArgs args, string executablePath) {
  69. process = Process.Start(new ProcessStartInfo {
  70. // Path to the HotReload.app
  71. FileName = executablePath,
  72. Arguments = args.cliArguments,
  73. UseShellExecute = false,
  74. });
  75. return Task.CompletedTask;
  76. }
  77. public Task StartTerminal(StartArgs args, string executablePath) {
  78. var pidFilePath = CliUtils.GetPidFilePath(args.hotreloadTempDir);
  79. // To run in a Terminal window (so you can see compiler logs), we must put the arguments into a script file
  80. // and run the script in Terminal. Terminal.app does not forward the arguments passed to it via `open --args`.
  81. // *.command files are opened with the user's default terminal app.
  82. var executableScriptPath = Path.Combine(Path.GetTempPath(), "Start_HotReloadServer.command");
  83. // You don't need to copy the cli executable on mac
  84. // omit hashbang line, let shell use the default interpreter (easier than detecting your default shell beforehand)
  85. File.WriteAllText(executableScriptPath, $"echo $$ > \"{pidFilePath}\"" +
  86. $"\ncd \"{Environment.CurrentDirectory}\"" + // set cwd because 'open' launches script with $HOME as cwd.
  87. $"\n\"{executablePath}\" {args.cliArguments} || read");
  88. CliUtils.Chmod(executableScriptPath); // make it executable
  89. CliUtils.Chmod(executablePath); // make it executable
  90. Directory.CreateDirectory(args.hotreloadTempDir);
  91. Directory.CreateDirectory(args.executableTargetDir);
  92. Directory.CreateDirectory(args.cliTempDir);
  93. process = Process.Start(new ProcessStartInfo {
  94. FileName = "open",
  95. Arguments = $"{(args.createNoWindow ? "-gj" : "")} '{executableScriptPath}'",
  96. UseShellExecute = true,
  97. });
  98. if (process.WaitForExit(1000)) {
  99. if (process.ExitCode != 0) {
  100. Log.Warning("Failed to the run the start server command. ExitCode={0}\nFilepath: {1}", process.ExitCode, executableScriptPath);
  101. }
  102. }
  103. else {
  104. process.EnableRaisingEvents = true;
  105. process.Exited += (_, __) => {
  106. if (process.ExitCode != 0) {
  107. Log.Warning("Failed to the run the start server command. ExitCode={0}\nFilepath: {1}", process.ExitCode, executableScriptPath);
  108. }
  109. };
  110. }
  111. return Task.CompletedTask;
  112. }
  113. public async Task Stop() {
  114. // kill HotReload server process (on mac it has different pid to the window which started it)
  115. await RequestHelper.KillServer();
  116. // process.CloseMainWindow throws if proc already exited.
  117. // We rely on the pid file for killing the trampoline script (in-case script is just starting and HotReload server not running yet)
  118. process = null;
  119. CliUtils.KillLastKnownHotReloadProcess();
  120. }
  121. static void UnzipMacOsPackage(string zipPath, string unzippedFolderPath) {
  122. //Log.Info("UnzipMacOsPackage called with {0}\n workingDirectory = {1}", zipPath, unzippedFolderPath);
  123. if (!zipPath.EndsWith(".zip")) {
  124. throw new ArgumentException($"Expected to end with .zip, but it was: {zipPath}", nameof(zipPath));
  125. }
  126. if (!File.Exists(zipPath)) {
  127. throw new ArgumentException($"zip file not found {zipPath}", nameof(zipPath));
  128. }
  129. var processStartInfo = new ProcessStartInfo {
  130. FileName = "unzip",
  131. Arguments = $"-o \"{zipPath}\"",
  132. WorkingDirectory = unzippedFolderPath, // unzip extracts to working directory by default
  133. UseShellExecute = true,
  134. CreateNoWindow = true
  135. };
  136. Process process = Process.Start(processStartInfo);
  137. process.WaitForExit();
  138. if (process.ExitCode != 0) {
  139. throw new Exception($"unzip failed with ExitCode {process.ExitCode}");
  140. }
  141. //Log.Info($"did unzip to {unzippedFolderPath}");
  142. // Move the .app folder to unzippedFolderPath
  143. // find the .app directory which is now inside unzippedFolderPath directory
  144. var foundDirs = Directory.GetDirectories(unzippedFolderPath, "*.app", SearchOption.AllDirectories);
  145. var done = false;
  146. var destDir = unzippedFolderPath + "HotReload.app";
  147. foreach (var dir in foundDirs) {
  148. if (dir.EndsWith(".app")) {
  149. done = true;
  150. if (dir == destDir) {
  151. // already in the right place
  152. break;
  153. }
  154. Directory.Move(dir, destDir);
  155. //Log.Info("Moved to " + destDir);
  156. break;
  157. }
  158. }
  159. if (!done) {
  160. throw new Exception("Failed to find .app directory and move it to " + destDir);
  161. }
  162. //Log.Info($"did unzip to {unzippedFolderPath}");
  163. }
  164. }
  165. }