first commit
This commit is contained in:
128
Packages/com.singularitygroup.hotreload/Editor/CLI/CliUtils.cs
Normal file
128
Packages/com.singularitygroup.hotreload/Editor/CLI/CliUtils.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using SingularityGroup.HotReload.Newtonsoft.Json;
|
||||
using UnityEngine;
|
||||
using System;
|
||||
|
||||
namespace SingularityGroup.HotReload.Editor.Cli {
|
||||
internal static class CliUtils {
|
||||
static readonly string projectIdentifier = GetProjectIdentifier();
|
||||
|
||||
class Config {
|
||||
public bool singleInstance;
|
||||
}
|
||||
|
||||
public static string GetProjectIdentifier() {
|
||||
if (File.Exists(PackageConst.ConfigFileName)) {
|
||||
var config = JsonConvert.DeserializeObject<Config>(File.ReadAllText(PackageConst.ConfigFileName));
|
||||
if (config.singleInstance) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
var path = Path.GetDirectoryName(UnityHelper.DataPath);
|
||||
var name = new DirectoryInfo(path).Name;
|
||||
using (SHA256 sha256 = SHA256.Create()) {
|
||||
byte[] inputBytes = Encoding.UTF8.GetBytes(path);
|
||||
byte[] hashBytes = sha256.ComputeHash(inputBytes);
|
||||
var hash = BitConverter.ToString(hashBytes).Replace("-", "").Substring(0, 6).ToUpper();
|
||||
return $"{name}-{hash}";
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetTempDownloadFilePath(string osxFileName) {
|
||||
if (UnityHelper.Platform == RuntimePlatform.OSXEditor) {
|
||||
// project specific temp directory that is writeable on MacOS (Path.GetTempPath() wasn't when run through HotReload.app)
|
||||
return Path.GetFullPath(PackageConst.LibraryCachePath + $"/HotReloadServerTemp/{osxFileName}");
|
||||
} else {
|
||||
return Path.GetTempFileName();
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetHotReloadTempDir() {
|
||||
if (UnityHelper.Platform == RuntimePlatform.OSXEditor) {
|
||||
// project specific temp directory that is writeable on MacOS (Path.GetTempPath() wasn't when run through HotReload.app)
|
||||
return Path.GetFullPath(PackageConst.LibraryCachePath + "/HotReloadServerTemp");
|
||||
} else {
|
||||
if (projectIdentifier != null) {
|
||||
return Path.Combine(Path.GetTempPath(), "HotReloadTemp", projectIdentifier);
|
||||
} else {
|
||||
return Path.Combine(Path.GetTempPath(), "HotReloadTemp");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetAppDataPath() {
|
||||
# if (UNITY_EDITOR_OSX)
|
||||
var baseDir = "/Users/Shared";
|
||||
# elif (UNITY_EDITOR_LINUX)
|
||||
var baseDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
# else
|
||||
var baseDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
#endif
|
||||
return Path.Combine(baseDir, "singularitygroup-hotreload");
|
||||
}
|
||||
|
||||
public static string GetExecutableTargetDir() {
|
||||
if (PackageConst.IsAssetStoreBuild) {
|
||||
return Path.Combine(GetAppDataPath(), "asset-store", $"executables_{PackageConst.ServerVersion.Replace('.', '-')}");
|
||||
}
|
||||
return Path.Combine(GetAppDataPath(), $"executables_{PackageConst.ServerVersion.Replace('.', '-')}");
|
||||
}
|
||||
|
||||
public static string GetCliTempDir() {
|
||||
return Path.Combine(GetHotReloadTempDir(), "MethodPatches");
|
||||
}
|
||||
|
||||
public static void Chmod(string targetFile, string flags = "+x") {
|
||||
// ReSharper disable once PossibleNullReferenceException
|
||||
Process.Start(new ProcessStartInfo("chmod", $"{flags} \"{targetFile}\"") {
|
||||
UseShellExecute = false,
|
||||
}).WaitForExit(2000);
|
||||
}
|
||||
|
||||
public static bool TryFindServerDir(out string path) {
|
||||
const string serverBasePath = "Packages/com.singularitygroup.hotreload/Server";
|
||||
if(Directory.Exists(serverBasePath)) {
|
||||
path = Path.GetFullPath(serverBasePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
//Not found in packages. Try to find in assets folder.
|
||||
//fast path - this is the expected folder
|
||||
const string alternativeExecutablePath = "Assets/HotReload/Server";
|
||||
if(Directory.Exists(alternativeExecutablePath)) {
|
||||
path = Path.GetFullPath(alternativeExecutablePath);
|
||||
return true;
|
||||
}
|
||||
//slow path - try to find the server directory somewhere in the assets folder
|
||||
var candidates = Directory.GetDirectories("Assets", "HotReload", SearchOption.AllDirectories);
|
||||
foreach(var candidate in candidates) {
|
||||
var serverDir = Path.Combine(candidate, "Server");
|
||||
if(Directory.Exists(serverDir)) {
|
||||
path = Path.GetFullPath(serverDir);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
path = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static string GetPidFilePath(string hotreloadTempDir) {
|
||||
return Path.GetFullPath(Path.Combine(hotreloadTempDir, "server.pid"));
|
||||
}
|
||||
|
||||
public static void KillLastKnownHotReloadProcess() {
|
||||
var pidPath = GetPidFilePath(GetHotReloadTempDir());
|
||||
try {
|
||||
var pid = int.Parse(File.ReadAllText(pidPath));
|
||||
Process.GetProcessById(pid).Kill();
|
||||
}
|
||||
catch {
|
||||
//ignore
|
||||
}
|
||||
File.Delete(pidPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b0243b348dec4a308dc7b98e09842d2c
|
||||
timeCreated: 1673820875
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SingularityGroup.HotReload.Editor.Cli {
|
||||
class FallbackCliController : ICliController {
|
||||
public string BinaryFileName => "";
|
||||
public string PlatformName => "";
|
||||
public bool CanOpenInBackground => false;
|
||||
public Task Start(StartArgs args) => Task.CompletedTask;
|
||||
|
||||
public Task Stop() => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 090ed5d45f294f0d8799879206139bd6
|
||||
timeCreated: 1673824275
|
||||
@@ -0,0 +1,239 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
using SingularityGroup.HotReload.Newtonsoft.Json;
|
||||
using UnityEditor;
|
||||
|
||||
namespace SingularityGroup.HotReload.Editor.Cli {
|
||||
[InitializeOnLoad]
|
||||
public static class HotReloadCli {
|
||||
internal static readonly ICliController controller;
|
||||
|
||||
//InitializeOnLoad ensures controller gets initialized on unity thread
|
||||
static HotReloadCli() {
|
||||
controller =
|
||||
#if UNITY_EDITOR_OSX
|
||||
new OsxCliController();
|
||||
#elif UNITY_EDITOR_LINUX
|
||||
new LinuxCliController();
|
||||
#elif UNITY_EDITOR_WIN
|
||||
new WindowsCliController();
|
||||
#else
|
||||
new FallbackCliController();
|
||||
#endif
|
||||
}
|
||||
|
||||
public static bool CanOpenInBackground => controller.CanOpenInBackground;
|
||||
|
||||
/// <summary>
|
||||
/// Public API: Starts the Hot Reload server. Must be on the main thread
|
||||
/// </summary>
|
||||
public static Task StartAsync() {
|
||||
return StartAsync(
|
||||
exposeServerToNetwork: HotReloadPrefs.ExposeServerToLocalNetwork,
|
||||
allAssetChanges: HotReloadPrefs.AllAssetChanges,
|
||||
createNoWindow: HotReloadPrefs.DisableConsoleWindow
|
||||
);
|
||||
}
|
||||
|
||||
internal static async Task StartAsync(bool exposeServerToNetwork, bool allAssetChanges, bool createNoWindow, LoginData loginData = null) {
|
||||
var port = await Prepare().ConfigureAwait(false);
|
||||
await ThreadUtility.SwitchToThreadPool();
|
||||
StartArgs args;
|
||||
if (TryGetStartArgs(UnityHelper.DataPath, exposeServerToNetwork, allAssetChanges, createNoWindow, loginData, port, out args)) {
|
||||
await controller.Start(args);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Public API: Stops the Hot Reload server
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is a no-op in case the server is not running
|
||||
/// </remarks>
|
||||
public static Task StopAsync() {
|
||||
return controller.Stop();
|
||||
}
|
||||
|
||||
class Config {
|
||||
#pragma warning disable CS0649
|
||||
public bool useBuiltInProjectGeneration;
|
||||
#pragma warning restore CS0649
|
||||
}
|
||||
|
||||
static bool TryGetStartArgs(string dataPath, bool exposeServerToNetwork, bool allAssetChanges, bool createNoWindow, LoginData loginData, int port, out StartArgs args) {
|
||||
string serverDir;
|
||||
if(!CliUtils.TryFindServerDir(out serverDir)) {
|
||||
Log.Warning($"Failed to start the Hot Reload Server. " +
|
||||
$"Unable to locate the 'Server' directory. " +
|
||||
$"Make sure the 'Server' directory is " +
|
||||
$"somewhere in the Assets folder inside a 'HotReload' folder or in the HotReload package");
|
||||
args = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
Config config;
|
||||
if (File.Exists(PackageConst.ConfigFileName)) {
|
||||
config = JsonConvert.DeserializeObject<Config>(File.ReadAllText(PackageConst.ConfigFileName));
|
||||
} else {
|
||||
config = new Config();
|
||||
}
|
||||
var hotReloadTmpDir = CliUtils.GetHotReloadTempDir();
|
||||
var cliTempDir = CliUtils.GetCliTempDir();
|
||||
// Versioned path so that we only need to extract the binary once. User can have multiple projects
|
||||
// on their machine using different HotReload versions.
|
||||
var executableTargetDir = CliUtils.GetExecutableTargetDir();
|
||||
Directory.CreateDirectory(executableTargetDir); // ensure exists
|
||||
var executableSourceDir = Path.Combine(serverDir, controller.PlatformName);
|
||||
var unityProjDir = Path.GetDirectoryName(dataPath);
|
||||
string slnPath;
|
||||
if (config.useBuiltInProjectGeneration) {
|
||||
var info = new DirectoryInfo(Path.GetFullPath("."));
|
||||
slnPath = Path.Combine(Path.GetFullPath("."), info.Name + ".sln");
|
||||
if (!File.Exists(slnPath)) {
|
||||
Log.Warning($"Failed to start the Hot Reload Server. Cannot find solution file. Please disable \"useBuiltInProjectGeneration\" in settings to enable custom project generation.");
|
||||
args = null;
|
||||
return false;
|
||||
}
|
||||
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.");
|
||||
try {
|
||||
Directory.Delete(ProjectGeneration.ProjectGeneration.tempDir, true);
|
||||
} catch(Exception ex) {
|
||||
Log.Exception(ex);
|
||||
}
|
||||
} else {
|
||||
slnPath = ProjectGeneration.ProjectGeneration.GetSolutionFilePath(dataPath);
|
||||
}
|
||||
|
||||
if (!File.Exists(slnPath)) {
|
||||
Log.Warning($"No .sln file found. Open any c# file to generate it so Hot Reload can work properly");
|
||||
}
|
||||
|
||||
var searchAssemblies = string.Join(";", CodePatcher.I.GetAssemblySearchPaths());
|
||||
var cliArguments = $@"-u ""{unityProjDir}"" -s ""{slnPath}"" -t ""{cliTempDir}"" -a ""{searchAssemblies}"" -ver ""{PackageConst.Version}"" -proc ""{Process.GetCurrentProcess().Id}"" -assets ""{allAssetChanges}"" -p ""{port}""";
|
||||
if (loginData != null) {
|
||||
cliArguments += $@" -email ""{loginData.email}"" -pass ""{loginData.password}""";
|
||||
}
|
||||
if (exposeServerToNetwork) {
|
||||
// server will listen on local network interface (default is localhost only)
|
||||
cliArguments += " -e true";
|
||||
}
|
||||
args = new StartArgs {
|
||||
hotreloadTempDir = hotReloadTmpDir,
|
||||
cliTempDir = cliTempDir,
|
||||
executableTargetDir = executableTargetDir,
|
||||
executableSourceDir = executableSourceDir,
|
||||
cliArguments = cliArguments,
|
||||
unityProjDir = unityProjDir,
|
||||
createNoWindow = createNoWindow,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int DiscoverFreePort() {
|
||||
var maxAttempts = 10;
|
||||
for (int attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
var port = RequestHelper.defaultPort + attempt;
|
||||
if (IsPortInUse(port)) {
|
||||
continue;
|
||||
}
|
||||
return port;
|
||||
}
|
||||
// we give up at this point
|
||||
return RequestHelper.defaultPort + maxAttempts;
|
||||
}
|
||||
|
||||
public static bool IsPortInUse(int port) {
|
||||
// Note that there is a racecondition that a port gets occupied after checking.
|
||||
// However, it will very rare someone will run into this.
|
||||
#if UNITY_EDITOR_WIN
|
||||
IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
|
||||
IPEndPoint[] activeTcpListeners = ipGlobalProperties.GetActiveTcpListeners();
|
||||
|
||||
foreach (IPEndPoint endPoint in activeTcpListeners) {
|
||||
if (endPoint.Port == port) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
#else
|
||||
try {
|
||||
using (TcpClient tcpClient = new TcpClient()) {
|
||||
tcpClient.Connect(IPAddress.Loopback, port); // Try to connect to the specified port
|
||||
return true;
|
||||
}
|
||||
} catch (SocketException) {
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
Log.Exception(e);
|
||||
// act as if the port is allocated
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
static async Task<int> Prepare() {
|
||||
await ThreadUtility.SwitchToMainThread();
|
||||
|
||||
var dataPath = UnityHelper.DataPath;
|
||||
await ProjectGeneration.ProjectGeneration.EnsureSlnAndCsprojFiles(dataPath);
|
||||
await PrepareBuildInfoAsync();
|
||||
PrepareSystemPathsFile();
|
||||
|
||||
var port = DiscoverFreePort();
|
||||
HotReloadState.ServerPort = port;
|
||||
RequestHelper.SetServerPort(port);
|
||||
return port;
|
||||
}
|
||||
|
||||
static bool didLogWarning;
|
||||
internal static async Task PrepareBuildInfoAsync() {
|
||||
await ThreadUtility.SwitchToMainThread();
|
||||
var buildInfoInput = await BuildInfoHelper.GetGenerateBuildInfoInput();
|
||||
await Task.Run(() => {
|
||||
try {
|
||||
var buildInfo = BuildInfoHelper.GenerateBuildInfoThreaded(buildInfoInput);
|
||||
PrepareBuildInfo(buildInfo);
|
||||
} catch (Exception e) {
|
||||
if (!didLogWarning) {
|
||||
Log.Warning($"Preparing build info failed! On-device functionality might not work. Exception: {e}");
|
||||
didLogWarning = true;
|
||||
} else {
|
||||
Log.Debug($"Preparing build info failed! On-device functionality might not work. Exception: {e}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
internal static void PrepareBuildInfo(BuildInfo buildInfo) {
|
||||
// When starting server make sure it starts with correct player data state.
|
||||
// (this fixes issue where Unity is in background and not sending files state).
|
||||
// Always write player data because you can be on any build target and want to connect with a downloaded android build.
|
||||
var json = buildInfo.ToJson();
|
||||
var cliTempDir = CliUtils.GetCliTempDir();
|
||||
Directory.CreateDirectory(cliTempDir);
|
||||
File.WriteAllText(Path.Combine(cliTempDir, "playerdata.json"), json);
|
||||
}
|
||||
|
||||
static void PrepareSystemPathsFile() {
|
||||
#pragma warning disable CS0618 // obsolete since 2023
|
||||
var lvl = PlayerSettings.GetApiCompatibilityLevel(EditorUserBuildSettings.selectedBuildTargetGroup);
|
||||
#pragma warning restore CS0618
|
||||
#if UNITY_2020_3_OR_NEWER
|
||||
var dirs = UnityEditor.Compilation.CompilationPipeline.GetSystemAssemblyDirectories(lvl);
|
||||
#else
|
||||
var t = typeof(UnityEditor.Editor).Assembly.GetType("UnityEditor.Scripting.ScriptCompilation.MonoLibraryHelpers");
|
||||
var m = t.GetMethod("GetSystemReferenceDirectories");
|
||||
var dirs = m.Invoke(null, new object[] { lvl });
|
||||
#endif
|
||||
Directory.CreateDirectory(PackageConst.LibraryCachePath);
|
||||
File.WriteAllText(PackageConst.LibraryCachePath + "/systemAssemblies.json", JsonConvert.SerializeObject(dirs));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f756ed6b78d428b8b9f83a6544317fe
|
||||
timeCreated: 1673820326
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SingularityGroup.HotReload.Editor.Cli {
|
||||
interface ICliController {
|
||||
string BinaryFileName {get;}
|
||||
string PlatformName {get;}
|
||||
bool CanOpenInBackground {get;}
|
||||
|
||||
Task Start(StartArgs args);
|
||||
|
||||
Task Stop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8cba48e21f76483da3ba615915e731fd
|
||||
timeCreated: 1673820542
|
||||
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Debug = UnityEngine.Debug;
|
||||
|
||||
namespace SingularityGroup.HotReload.Editor.Cli {
|
||||
|
||||
class LinuxCliController : ICliController {
|
||||
Process process;
|
||||
|
||||
public string BinaryFileName => "CodePatcherCLI";
|
||||
public string PlatformName => "linux-x64";
|
||||
public bool CanOpenInBackground => true;
|
||||
|
||||
public Task Start(StartArgs args) {
|
||||
var startScript = Path.Combine(args.executableSourceDir, "hotreload-start-script.sh");
|
||||
if (!File.Exists(startScript)) {
|
||||
throw new FileNotFoundException(startScript);
|
||||
}
|
||||
File.WriteAllText(startScript, File.ReadAllText(startScript).Replace("\r\n", "\n"));
|
||||
CliUtils.Chmod(startScript);
|
||||
|
||||
var title = CodePatcher.TAG + "Server " + new DirectoryInfo(args.unityProjDir).Name;
|
||||
title = title.Replace(" ", "-");
|
||||
title = title.Replace("'", "");
|
||||
|
||||
var cliargsfile = Path.GetTempFileName();
|
||||
File.WriteAllText(cliargsfile,args.cliArguments);
|
||||
var codePatcherProc = Process.Start(new ProcessStartInfo {
|
||||
FileName = startScript,
|
||||
Arguments =
|
||||
$"--title \"{title}\""
|
||||
+ $" --executables-source-dir \"{args.executableSourceDir}\" "
|
||||
+ $" --executable-taget-dir \"{args.executableTargetDir}\""
|
||||
+ $" --pidfile \"{CliUtils.GetPidFilePath(args.hotreloadTempDir)}\""
|
||||
+ $" --cli-arguments-file \"{cliargsfile}\""
|
||||
+ $" --method-patch-dir \"{args.cliTempDir}\""
|
||||
+ $" --create-no-window \"{args.createNoWindow}\"",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
});
|
||||
if (codePatcherProc == null) {
|
||||
if (File.Exists(cliargsfile)) {
|
||||
File.Delete(cliargsfile);
|
||||
}
|
||||
throw new Exception("Could not start code patcher process.");
|
||||
}
|
||||
codePatcherProc.BeginErrorReadLine();
|
||||
codePatcherProc.BeginOutputReadLine();
|
||||
codePatcherProc.OutputDataReceived += (_, a) => {
|
||||
};
|
||||
// error data can also mean we kill the proc beningly
|
||||
codePatcherProc.ErrorDataReceived += (_, a) => {
|
||||
};
|
||||
process = codePatcherProc;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task Stop() {
|
||||
await RequestHelper.KillServer();
|
||||
try {
|
||||
// process.CloseMainWindow throws if proc already exited.
|
||||
// also we just rely on the pid file it is fine
|
||||
CliUtils.KillLastKnownHotReloadProcess();
|
||||
} catch {
|
||||
//ignored
|
||||
}
|
||||
process = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c894a69d595d4ada8cfa4afe23c68ab9
|
||||
timeCreated: 1673820131
|
||||
@@ -0,0 +1,189 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using SingularityGroup.HotReload.Editor.Semver;
|
||||
using Debug = UnityEngine.Debug;
|
||||
|
||||
namespace SingularityGroup.HotReload.Editor.Cli {
|
||||
class OsxCliController : ICliController {
|
||||
Process process;
|
||||
|
||||
public string BinaryFileName => "HotReload.app.zip";
|
||||
public string PlatformName => "osx-x64";
|
||||
public bool CanOpenInBackground => false;
|
||||
|
||||
/// In MacOS 13 Ventura, our app cannot launch a terminal window.
|
||||
/// We use a custom app that launches HotReload server and shows it's output (just like a terminal would).
|
||||
// Including MacOS 12 Monterey as well so I can dogfood it -Troy
|
||||
private static bool UseCustomConsoleApp() => MacOSVersion.Value.Major >= 12;
|
||||
|
||||
// dont use static because null comparison on SemVersion is broken
|
||||
private static readonly Lazy<SemVersion> MacOSVersion = new Lazy<SemVersion>(() => {
|
||||
//UnityHelper.OperatingSystem; // in Unity 2018 it returns 10.16 on monterey (no idea why)
|
||||
//Environment.OSVersion returns unix version like 21.x
|
||||
var startinfo = new ProcessStartInfo {
|
||||
FileName = "/usr/bin/sw_vers",
|
||||
Arguments = "-productVersion",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
var process = Process.Start(startinfo);
|
||||
|
||||
string osVersion = process.StandardOutput.ReadToEnd().Trim();
|
||||
|
||||
SemVersion macosVersion;
|
||||
if (SemVersion.TryParse(osVersion, out macosVersion)) {
|
||||
return macosVersion;
|
||||
}
|
||||
// should never happen
|
||||
Log.Warning("Failed to detect MacOS version, if Hot Reload fails to start, please contact support.");
|
||||
return SemVersion.None;
|
||||
});
|
||||
|
||||
public async Task Start(StartArgs args) {
|
||||
// Unzip the .app.zip to temp folder .app
|
||||
var appExecutablePath = $"{args.executableTargetDir}/HotReload.app/Contents/MacOS/HotReload";
|
||||
var cliExecutablePath = $"{args.executableTargetDir}/HotReload.app/Contents/Resources/CodePatcherCLI";
|
||||
|
||||
// ensure running on threadpool
|
||||
await ThreadUtility.SwitchToThreadPool();
|
||||
|
||||
// executableTargetDir is versioned, so only need to extract once.
|
||||
if (!File.Exists(appExecutablePath)) {
|
||||
try {
|
||||
// delete only the extracted app folder (must not delete downloaded zip which is in same folder)
|
||||
Directory.Delete(args.executableTargetDir + "/HotReload.app", true);
|
||||
} catch (IOException) {
|
||||
// ignore directory not found
|
||||
}
|
||||
Directory.CreateDirectory(args.executableTargetDir);
|
||||
UnzipMacOsPackage($"{args.executableTargetDir}/{BinaryFileName}", args.executableTargetDir + "/");
|
||||
}
|
||||
|
||||
try {
|
||||
// Always stop first because rarely it has happened that the server process was still running after custom console closed.
|
||||
// Note: this will also stop Hot Reload started by other Unity projects.
|
||||
await Stop();
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
|
||||
if (UseCustomConsoleApp()) {
|
||||
await StartCustomConsole(args, appExecutablePath);
|
||||
} else {
|
||||
await StartTerminal(args, cliExecutablePath);
|
||||
}
|
||||
}
|
||||
|
||||
public Task StartCustomConsole(StartArgs args, string executablePath) {
|
||||
process = Process.Start(new ProcessStartInfo {
|
||||
// Path to the HotReload.app
|
||||
FileName = executablePath,
|
||||
Arguments = args.cliArguments,
|
||||
UseShellExecute = false,
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StartTerminal(StartArgs args, string executablePath) {
|
||||
var pidFilePath = CliUtils.GetPidFilePath(args.hotreloadTempDir);
|
||||
// To run in a Terminal window (so you can see compiler logs), we must put the arguments into a script file
|
||||
// and run the script in Terminal. Terminal.app does not forward the arguments passed to it via `open --args`.
|
||||
// *.command files are opened with the user's default terminal app.
|
||||
var executableScriptPath = Path.Combine(Path.GetTempPath(), "Start_HotReloadServer.command");
|
||||
// You don't need to copy the cli executable on mac
|
||||
// omit hashbang line, let shell use the default interpreter (easier than detecting your default shell beforehand)
|
||||
File.WriteAllText(executableScriptPath, $"echo $$ > \"{pidFilePath}\"" +
|
||||
$"\ncd \"{Environment.CurrentDirectory}\"" + // set cwd because 'open' launches script with $HOME as cwd.
|
||||
$"\n\"{executablePath}\" {args.cliArguments} || read");
|
||||
|
||||
CliUtils.Chmod(executableScriptPath); // make it executable
|
||||
CliUtils.Chmod(executablePath); // make it executable
|
||||
|
||||
Directory.CreateDirectory(args.hotreloadTempDir);
|
||||
Directory.CreateDirectory(args.executableTargetDir);
|
||||
Directory.CreateDirectory(args.cliTempDir);
|
||||
|
||||
process = Process.Start(new ProcessStartInfo {
|
||||
FileName = "open",
|
||||
Arguments = $"{(args.createNoWindow ? "-gj" : "")} '{executableScriptPath}'",
|
||||
UseShellExecute = true,
|
||||
});
|
||||
|
||||
if (process.WaitForExit(1000)) {
|
||||
if (process.ExitCode != 0) {
|
||||
Log.Warning("Failed to the run the start server command. ExitCode={0}\nFilepath: {1}", process.ExitCode, executableScriptPath);
|
||||
}
|
||||
}
|
||||
else {
|
||||
process.EnableRaisingEvents = true;
|
||||
process.Exited += (_, __) => {
|
||||
if (process.ExitCode != 0) {
|
||||
Log.Warning("Failed to the run the start server command. ExitCode={0}\nFilepath: {1}", process.ExitCode, executableScriptPath);
|
||||
}
|
||||
};
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task Stop() {
|
||||
// kill HotReload server process (on mac it has different pid to the window which started it)
|
||||
await RequestHelper.KillServer();
|
||||
|
||||
// process.CloseMainWindow throws if proc already exited.
|
||||
// We rely on the pid file for killing the trampoline script (in-case script is just starting and HotReload server not running yet)
|
||||
process = null;
|
||||
CliUtils.KillLastKnownHotReloadProcess();
|
||||
}
|
||||
|
||||
static void UnzipMacOsPackage(string zipPath, string unzippedFolderPath) {
|
||||
//Log.Info("UnzipMacOsPackage called with {0}\n workingDirectory = {1}", zipPath, unzippedFolderPath);
|
||||
if (!zipPath.EndsWith(".zip")) {
|
||||
throw new ArgumentException($"Expected to end with .zip, but it was: {zipPath}", nameof(zipPath));
|
||||
}
|
||||
|
||||
if (!File.Exists(zipPath)) {
|
||||
throw new ArgumentException($"zip file not found {zipPath}", nameof(zipPath));
|
||||
}
|
||||
var processStartInfo = new ProcessStartInfo {
|
||||
FileName = "unzip",
|
||||
Arguments = $"-o \"{zipPath}\"",
|
||||
WorkingDirectory = unzippedFolderPath, // unzip extracts to working directory by default
|
||||
UseShellExecute = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
Process process = Process.Start(processStartInfo);
|
||||
process.WaitForExit();
|
||||
if (process.ExitCode != 0) {
|
||||
throw new Exception($"unzip failed with ExitCode {process.ExitCode}");
|
||||
}
|
||||
//Log.Info($"did unzip to {unzippedFolderPath}");
|
||||
// Move the .app folder to unzippedFolderPath
|
||||
|
||||
// find the .app directory which is now inside unzippedFolderPath directory
|
||||
var foundDirs = Directory.GetDirectories(unzippedFolderPath, "*.app", SearchOption.AllDirectories);
|
||||
var done = false;
|
||||
var destDir = unzippedFolderPath + "HotReload.app";
|
||||
foreach (var dir in foundDirs) {
|
||||
if (dir.EndsWith(".app")) {
|
||||
done = true;
|
||||
if (dir == destDir) {
|
||||
// already in the right place
|
||||
break;
|
||||
}
|
||||
Directory.Move(dir, destDir);
|
||||
//Log.Info("Moved to " + destDir);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!done) {
|
||||
throw new Exception("Failed to find .app directory and move it to " + destDir);
|
||||
}
|
||||
//Log.Info($"did unzip to {unzippedFolderPath}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ebeed1c29454bc78e5a9ee64f2c9def
|
||||
timeCreated: 1673821666
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace SingularityGroup.HotReload.Editor.Cli {
|
||||
class StartArgs {
|
||||
public string hotreloadTempDir;
|
||||
// aka method patch temp dir
|
||||
public string cliTempDir;
|
||||
public string executableTargetDir;
|
||||
public string executableSourceDir;
|
||||
public string cliArguments;
|
||||
public string unityProjDir;
|
||||
public bool createNoWindow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 43d69eb7ae8aef4428da83562105bfaa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SingularityGroup.HotReload.Editor.Cli {
|
||||
class WindowsCliController : ICliController {
|
||||
Process process;
|
||||
|
||||
public string BinaryFileName => "CodePatcherCLI.exe";
|
||||
public string PlatformName => "win-x64";
|
||||
public bool CanOpenInBackground => true;
|
||||
|
||||
public Task Start(StartArgs args) {
|
||||
process = Process.Start(new ProcessStartInfo {
|
||||
FileName = Path.GetFullPath(Path.Combine(args.executableTargetDir, "CodePatcherCLI.exe")),
|
||||
Arguments = args.cliArguments,
|
||||
UseShellExecute = !args.createNoWindow,
|
||||
CreateNoWindow = args.createNoWindow,
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task Stop() {
|
||||
await RequestHelper.KillServer();
|
||||
try {
|
||||
process?.CloseMainWindow();
|
||||
} catch {
|
||||
//ignored
|
||||
}
|
||||
process = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e5644af69ec7404a8039ff2833610d48
|
||||
timeCreated: 1673822169
|
||||
Reference in New Issue
Block a user