2025-07-08 10:46:31 +00:00
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 ;
/// <summary>
/// Public API: Starts the Hot Reload server. Must be on the main thread
/// </summary>
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 ) ;
}
}
/// <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 , 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 < 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}"" -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 < 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 ) ) ;
}
}
}