using System;
using System.Diagnostics;
using System.IO;
using System.Net;
#if UNITY_EDITOR_WIN
using System.Net.NetworkInformation;
#else
using System.Net.Sockets;
#endif
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;
///
/// Public API: Starts the Hot Reload server. Must be on the main thread
///
public static Task StartAsync() {
return StartAsync(
isReleaseMode: RequestHelper.IsReleaseMode(),
exposeServerToNetwork: HotReloadPrefs.ExposeServerToLocalNetwork,
allAssetChanges: HotReloadPrefs.AllAssetChanges,
createNoWindow: HotReloadPrefs.DisableConsoleWindow,
detailedErrorReporting: !HotReloadPrefs.DisableDetailedErrorReporting
);
}
internal static async Task StartAsync(bool exposeServerToNetwork, bool allAssetChanges, bool createNoWindow, bool isReleaseMode, bool detailedErrorReporting, LoginData loginData = null) {
var port = await Prepare().ConfigureAwait(false);
await ThreadUtility.SwitchToThreadPool();
StartArgs args;
if (TryGetStartArgs(UnityHelper.DataPath, exposeServerToNetwork, allAssetChanges, createNoWindow, isReleaseMode, detailedErrorReporting, loginData, port, out args)) {
await controller.Start(args);
}
}
///
/// Public API: Stops the Hot Reload server
///
///
/// This is a no-op in case the server is not running
///
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, bool isReleaseMode, bool detailedErrorReporting, 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(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}"" -r {isReleaseMode} -detailed-error-reporting {detailedErrorReporting}";
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 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));
}
}
}