246 lines
11 KiB
C#
246 lines
11 KiB
C#
#if ENABLE_MONO && (DEVELOPMENT_BUILD || UNITY_EDITOR)
|
|
using System;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using UnityEngine;
|
|
|
|
namespace SingularityGroup.HotReload {
|
|
internal class ServerHandshake {
|
|
public static readonly ServerHandshake I = new ServerHandshake();
|
|
|
|
/// <summary>
|
|
/// Not verified as compatible yet - need to do handshake
|
|
/// </summary>
|
|
private PatchServerInfo pendingServer;
|
|
|
|
/// <summary>
|
|
/// Handshake is complete. Player can connect to this server.
|
|
/// </summary>
|
|
private PatchServerInfo verifiedServer;
|
|
|
|
private Task handshakeCheck;
|
|
|
|
private CancellationTokenSource cts = new CancellationTokenSource();
|
|
/// Track first handshake request after calling SetServerInfo.
|
|
/// Sometimes and it can take 10-30 seconds and succeed.
|
|
private TaskCompletionSource<Result> firstHandshake = new TaskCompletionSource<Result>();
|
|
|
|
/// <remarks>Server info should be well known or a strong guess, not just a random ip address.</remarks>
|
|
public Task<Result> SetServerInfo(PatchServerInfo serverInfo) {
|
|
if (verifiedServer != null && serverInfo == verifiedServer) {
|
|
return Task.FromResult(Result.Verified);
|
|
}
|
|
pendingServer = serverInfo;
|
|
if (serverInfo != null) {
|
|
Prompts.SetConnectionState(ConnectionSummary.Handshaking);
|
|
}
|
|
|
|
// disconnect
|
|
verifiedServer = null;
|
|
|
|
// cancel any ongoing RequestHandshake task
|
|
firstHandshake.TrySetCanceled(cts.Token);
|
|
firstHandshake = new TaskCompletionSource<Result>();
|
|
cts.Cancel();
|
|
cts = new CancellationTokenSource();
|
|
if (serverInfo == null) return Task.FromResult(Result.None);
|
|
return firstHandshake.Task;
|
|
}
|
|
|
|
/// Ensures a handshake request is running.
|
|
public void CheckHandshake() {
|
|
var serverToCheck = pendingServer;
|
|
if (verifiedServer == null && serverToCheck != null) {
|
|
if (handshakeCheck == null || handshakeCheck.IsCompleted) {
|
|
handshakeCheck = Task.Run(async () => {
|
|
try {
|
|
Log.Debug("Run RequestHandshake");
|
|
var results = await RequestHandshake(serverToCheck);
|
|
await ThreadUtility.SwitchToMainThread();
|
|
var decisionIsFinal = await VerifyResults(results, serverToCheck);
|
|
firstHandshake.TrySetResult(results); // VerifyResults() can also set it, this is the default fallback
|
|
if (decisionIsFinal) {
|
|
pendingServer = null;
|
|
}
|
|
} catch (Exception ex) {
|
|
Log.Exception(ex);
|
|
} finally {
|
|
// set as failed if wasnt set as true by above code
|
|
firstHandshake.TrySetResult(Result.None);
|
|
}
|
|
}, cts.Token);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify results of the handshake.
|
|
/// </summary>
|
|
/// <param name="results"></param>
|
|
/// <param name="server"></param>
|
|
/// <returns>True if the conclusion is final, otherwise false</returns>
|
|
/// <remarks>
|
|
/// Must be called on main thread because it uses Unity UI methods.
|
|
/// </remarks>
|
|
async Task<bool> VerifyResults(Result results, PatchServerInfo server) {
|
|
if (results.HasFlag(Result.QuietWarning)) {
|
|
// can handle here if needed later
|
|
}
|
|
if (results.HasFlag(Result.Verified)) {
|
|
if (!firstHandshake.Task.IsCompleted) {
|
|
Prompts.SetConnectionState(ConnectionSummary.Connecting);
|
|
}
|
|
OnVerified(server);
|
|
return true;
|
|
}
|
|
|
|
// handle objections in order of obviousness, most obvious goes first
|
|
if (results.HasFlag(Result.DifferentProject)) {
|
|
await Prompts.ShowQuestionDialog(new QuestionDialog.Config {
|
|
summary = "Hot Reload was started from a different project",
|
|
suggestion = "Please run Hot Reload from the matching Unity project",
|
|
continueButtonText = "OK",
|
|
cancelButtonText = null,
|
|
});
|
|
// they need to provide a new server info
|
|
Prompts.SetConnectionState(ConnectionSummary.Cancelled);
|
|
return true;
|
|
}
|
|
if (results.HasFlag(Result.DifferentCommit)) {
|
|
Prompts.SetConnectionState(ConnectionSummary.DifferencesFound);
|
|
bool yes = await Prompts.ShowQuestionDialog(new QuestionDialog.Config {
|
|
summary = "Editor and current build are on different commits",
|
|
suggestion = "This can cause errors when the build was made on an old commit.",
|
|
continueButtonText = "Connect",
|
|
});
|
|
if (yes) {
|
|
results |= Result.Verified;
|
|
Prompts.SetConnectionState(ConnectionSummary.Connecting);
|
|
firstHandshake.TrySetResult(results);
|
|
OnVerified(server);
|
|
} else {
|
|
Prompts.SetConnectionState(ConnectionSummary.Cancelled);
|
|
}
|
|
// cancel -> tell them to provide a new server
|
|
return true;
|
|
}
|
|
|
|
if (results.HasFlag(Result.TempError)) {
|
|
// retry might work, its not over yet
|
|
return false;
|
|
}
|
|
// at time of writing, code should never reach here. Adding new HandshakeResult flags should be handled above.
|
|
Log.Debug("UNEXPECTED: VerifyResults continued into untested code: {0}", results);
|
|
return true;
|
|
}
|
|
|
|
void OnVerified(PatchServerInfo serverToCheck) {
|
|
verifiedServer = serverToCheck;
|
|
}
|
|
|
|
public bool TryGetVerifiedServer(out PatchServerInfo serverInfo) {
|
|
// take verifiedServer
|
|
var server = Interlocked.Exchange(ref verifiedServer, null);
|
|
serverInfo = server;
|
|
return serverInfo != null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result of a handshake with the remote Hot Reload instance.
|
|
/// </summary>
|
|
[Flags]
|
|
public enum Result {
|
|
None = 0,
|
|
DifferentCommit = 1 << 0,
|
|
DifferentProject = 1 << 1,
|
|
|
|
/// <summary>
|
|
/// A temporary error occurred, retrying might work.
|
|
/// </summary>
|
|
TempError = 1 << 2,
|
|
|
|
/// <summary>
|
|
/// Hot Reload is compiling, so we should wait a bit before trying again.
|
|
/// </summary>
|
|
WaitForCompiling = 1 << 3,
|
|
|
|
[Obsolete("Not needed so far", true)]
|
|
Placeholder2 = 1 << 4,
|
|
|
|
// use when a warning is logged, but we're allowing Hot Reload to connect bcus it probably works.
|
|
QuietWarning = 1 << 5,
|
|
Verified = 1 << 6,
|
|
}
|
|
|
|
static async Task<Result> RequestHandshake(PatchServerInfo info) {
|
|
var buildInfo = PlayerEntrypoint.PlayerBuildInfo;
|
|
var results = Result.None;
|
|
var verified = true;
|
|
Log.Debug($"Comparing commits {buildInfo.commitHash} and {info.commitHash}");
|
|
if (buildInfo.IsDifferentCommit(info.commitHash)) {
|
|
results |= Result.DifferentCommit;
|
|
verified = false;
|
|
}
|
|
// Check for health before sending handshake request
|
|
// If health check fails UI updates faster
|
|
var healthy = await ServerHealthCheck.CheckHealthAsync(info);
|
|
if (!healthy) {
|
|
Log.Debug("Won't send handshake request because server is not healhy");
|
|
return results;
|
|
}
|
|
Log.Info("Request handshake to Hot Reload server with hostname: {0}", info.hostName);
|
|
//Log.Debug("Handshake with projectOmissionRegex: \"{0}\"", buildInfo.projectOmissionRegex);
|
|
var response = await RequestHelper.RequestHandshake(info, buildInfo.DefineSymbolsAsHashSet,
|
|
buildInfo.projectOmissionRegex);
|
|
if (response.error != null) {
|
|
verified = false;
|
|
Log.Debug($"RequestHandshake errored: {response.error}");
|
|
if (response.error == Result.WaitForCompiling.ToString()) {
|
|
// WaitForCompiling is a temp error
|
|
results |= Result.WaitForCompiling;
|
|
results |= Result.TempError;
|
|
} else {
|
|
results |= Result.TempError;
|
|
}
|
|
}
|
|
|
|
if (response.data == null) {
|
|
// need response data to continue
|
|
verified = false;
|
|
return results;
|
|
}
|
|
|
|
// handshake response is what we post to /files which is BuildInfo
|
|
var remoteBuildTarget = response.data[nameof(BuildInfo.activeBuildTarget)] as string;
|
|
var remoteCommitHash = response.data[nameof(BuildInfo.commitHash)] as string;
|
|
var remoteProjectIdentifier = response.data[nameof(BuildInfo.projectIdentifier)] as string;
|
|
if (buildInfo.IsDifferentCommit(remoteCommitHash)) {
|
|
Log.Debug($"RequestHandshake server is on different commit {response.error}");
|
|
results |= Result.DifferentCommit;
|
|
verified = false;
|
|
}
|
|
|
|
if (remoteProjectIdentifier != buildInfo.projectIdentifier) {
|
|
Log.Debug("RequestHandshake remote is using a different project identifier");
|
|
results |= Result.DifferentProject;
|
|
verified = false;
|
|
}
|
|
|
|
if (remoteBuildTarget == null) {
|
|
// Should never happen. Server responsed with an error when no BuildInfo at all.
|
|
Log.Warning("Server did not declare its current Unity activeBuildTarget in the handshake response. Will assume it is {0}.", buildInfo.activeBuildTarget);
|
|
results |= Result.QuietWarning;
|
|
} else if (remoteBuildTarget != buildInfo.activeBuildTarget) {
|
|
Log.Warning("Your Unity project is running on {0}. You may need to switch it to {1} for Hot Reload to work.", remoteBuildTarget, buildInfo.activeBuildTarget);
|
|
results |= Result.QuietWarning;
|
|
}
|
|
|
|
if (verified) {
|
|
results |= Result.Verified;
|
|
}
|
|
return results;
|
|
}
|
|
}
|
|
}
|
|
#endif
|