Editor/Core/ConnectionTeardownCoordinator.cs
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Sockets;
using System.Threading.Tasks;
namespace TeamCreate;
public static class CollaborationDiagnostics
{
public const int MaxDiagnosticLength = 512;
public static string Bounded(string? value)
{
return Bounded(value, 512);
}
public static string Bounded(string? value, int maxLength)
{
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
if (maxLength < 0)
{
maxLength = 0;
}
return (value.Length <= maxLength) ? value : value.Substring(0, maxLength);
}
public static string DescribeExternalFailure(string context, Exception? error)
{
string? text = error?.Message;
return Bounded(context + ((text != null && text.Length > 0) ? text : "unknown error"));
}
}
public sealed record CollaborationFailureOutcome(string? Error, string? Status);
public static class CollaborationFailureRules
{
/// <summary>
/// Whether a failure should write a recovery archive. The archive exists to keep local work that a
/// reconnect might discard; a join that never got a welcome never touched a scene, so there is nothing
/// to keep, and an archive per refused attempt only buries the ones that matter.
/// </summary>
public static bool ShouldArchive(bool everConnected, bool hasPendingEdits)
{
return everConnected || hasPendingEdits;
}
// A join that never reached the host has nothing that "stopped": no scene was ever synchronized.
// Saying so would send a person looking for lost work that does not exist.
public static CollaborationFailureOutcome Compose(string? existingError, string error, string? recoveryWriteFailure, bool everConnected = true)
{
if (!string.IsNullOrEmpty(existingError))
{
return new CollaborationFailureOutcome(existingError, null);
}
string text = CollaborationDiagnostics.Bounded(error);
if (!string.IsNullOrEmpty(recoveryWriteFailure))
{
text = CollaborationDiagnostics.Bounded(text + " Recovery write also failed: " + recoveryWriteFailure);
}
return new CollaborationFailureOutcome(text, CollaborationDiagnostics.Bounded((everConnected ? "Synchronization stopped: " : "Could not connect: ") + text));
}
}
public sealed record ResourceDisposalResult(bool Succeeded, string? Failure);
public static class ResourceDisposalRules
{
public static ResourceDisposalResult DisposePair(IDisposable? first, string firstName, IDisposable? second, string secondName)
{
List<string> list = new List<string>();
DisposeOne(first, firstName, list);
DisposeOne(second, secondName, list);
if (list.Count == 0)
{
return new ResourceDisposalResult(Succeeded: true, null);
}
return new ResourceDisposalResult(Succeeded: false, CollaborationDiagnostics.Bounded(string.Join(" | ", list)));
}
private static void DisposeOne(IDisposable? resource, string what, ICollection<string> failures)
{
if (resource == null)
{
return;
}
try
{
resource.Dispose();
}
catch (Exception error)
{
failures.Add(CollaborationDiagnostics.Bounded(CollaborationDiagnostics.DescribeExternalFailure(what + " could not be disposed: ", error), 256));
}
}
}
public sealed record SessionStartGuardRequest(PlayModeTransitionResult LifecycleValidation, bool ConnectionBusy, bool ConnectionTeardownPending, bool ConnectionTeardownFailed, string? TerminalTeardownDiagnostic);
public sealed record SessionStartCommitRequest(PlayModeTransitionResult Validation, Func<bool> Prepare);
public enum CollaborationTransportState
{
None,
Connecting,
Live,
Failed,
Disconnected
}
public static class CollaborationTransportStateRules
{
public static CollaborationTransportState Detect(bool attached, bool live, bool failed, bool connecting)
{
if (connecting)
{
return CollaborationTransportState.Connecting;
}
if (failed)
{
return CollaborationTransportState.Failed;
}
if (!attached)
{
return CollaborationTransportState.None;
}
return live ? CollaborationTransportState.Live : CollaborationTransportState.Disconnected;
}
}
public sealed record SessionReconnectGuardRequest(PlayModeLifecycle Lifecycle, bool SessionEstablished, bool ConnectionAttemptPending, bool ConnectionBusy, bool ConnectionTeardownPending = false, bool ConnectionTeardownFailed = false, string? TerminalTeardownDiagnostic = null, bool ConnectionLive = false, CollaborationTransportState? TransportState = null, bool SourceBaselineMatches = true, bool SessionLeft = false, bool HostedSession = false);
public enum SessionFailureClass
{
Terminal,
Transient
}
public sealed record AutoRecoveryInput(bool AutoRecoverEnabled, bool ErrorRecorded, SessionFailureClass FailureClass, bool TerminalTeardown, bool HostedSession, bool UnsavedChanges, bool ReconnectAccepted, bool AttemptInFlight, int AttemptsUsed, DateTime Now, DateTime NextAttemptAt);
public static class AutoRecoveryRules
{
public const int MaxAttempts = 3;
public static SessionFailureClass Classify(Exception error)
{
if (error == null)
{
return SessionFailureClass.Terminal;
}
if (error is AggregateException ex)
{
Exception? innerException = ex.InnerException;
return (innerException != null) ? Classify(innerException) : SessionFailureClass.Terminal;
}
return (error is IOException || error is SocketException || error is TimeoutException) ? SessionFailureClass.Transient : SessionFailureClass.Terminal;
}
public static TimeSpan Backoff(int attemptsUsed)
{
if (1 == 0)
{
}
TimeSpan result;
if (attemptsUsed <= 0)
{
result = TimeSpan.FromSeconds(1L);
}
else
{
result = ((attemptsUsed != 1) ? TimeSpan.FromSeconds(10L) : TimeSpan.FromSeconds(4L));
}
if (1 == 0)
{
}
return result;
}
public static bool Eligible(AutoRecoveryInput input)
{
if (!input.AutoRecoverEnabled)
{
return false;
}
if (!input.ErrorRecorded)
{
return false;
}
if (input.FailureClass != SessionFailureClass.Transient)
{
return false;
}
if (input.TerminalTeardown)
{
return false;
}
if (input.HostedSession)
{
return false;
}
if (input.UnsavedChanges)
{
return false;
}
if (!input.ReconnectAccepted)
{
return false;
}
if (input.AttemptInFlight)
{
return false;
}
if (input.AttemptsUsed >= 3)
{
return false;
}
if (input.Now < input.NextAttemptAt)
{
return false;
}
return true;
}
public static bool PromptOffered(bool autoRecoverEnabled, bool errorRecorded, SessionFailureClass failureClass, bool terminalTeardown, bool reconnectAccepted, bool promptShown)
{
return (((!autoRecoverEnabled & errorRecorded) && failureClass == SessionFailureClass.Transient && !terminalTeardown) & reconnectAccepted) && !promptShown;
}
}
public sealed record SessionReconnectCommitRequest(PlayModeTransitionResult Validation, Func<bool> Prepare);
public enum ReconnectCommitStage
{
RejectedBeforePrepare,
FailedDuringPrepare,
Committed
}
public sealed record SessionReconnectCommitOutcome(PlayModeTransitionResult Result, ReconnectCommitStage Stage, bool PreviousResourcesDisposed)
{
public bool Accepted => Result.Accepted;
public string? ActionableError => Result.ActionableError;
}
public static class CollaborationSessionResourceRules
{
public static bool IsBusy(bool wireAttached, bool connecting, bool hostServer, bool failStop)
{
return wireAttached | connecting | hostServer | failStop;
}
}
public sealed record RecoveryPathOutcome(string? Published, bool Succeeded);
public static class RecoveryPathRules
{
public static string CreatePath(string folder, string stamp)
{
return CollaborationDiagnostics.Bounded(Path.Combine(folder, stamp + ".json"));
}
public static RecoveryPathOutcome PublishAfterWrite(string path, Action write)
{
try
{
write();
return new RecoveryPathOutcome(path, Succeeded: true);
}
catch (Exception)
{
return new RecoveryPathOutcome(null, Succeeded: false);
}
}
}
public sealed record ConnectionPromotionRequest(bool SourcePacketsAdmissible, bool TeardownBusy, bool ConnectCompleted);
public static class ConnectionPromotionRules
{
public static bool MayPromote(ConnectionPromotionRequest request)
{
return request.ConnectCompleted && !request.TeardownBusy && request.SourcePacketsAdmissible;
}
public static bool ShouldPromote(bool sourcePacketsAdmissible, bool canPromoteConnection)
{
return MayPromote(new ConnectionPromotionRequest(sourcePacketsAdmissible, !canPromoteConnection, ConnectCompleted: true));
}
}
public sealed record SessionLeaveGuardRequest(bool LifecycleTransitionInProgress);
public sealed record SessionLeaveRequest(ConnectionTeardownCoordinator Coordinator, bool LifecycleTransitionInProgress, Func<bool> HasUnacknowledgedWork, Func<string, bool> Preserve, Action Announce, Func<bool> DisposeResources);
public sealed record SessionLeaveOutcome(PlayModeTransitionResult Guard, bool PreserveAttempted, bool PreserveFailed, bool ResourcesDisposed, bool DisposalFailed, string? Failure)
{
public bool Accepted => Guard.Accepted;
public string? ActionableError => Guard.ActionableError;
}
public static class SessionLeaveRules
{
public const string PreserveFailedDiagnostic = "Left with unacknowledged edits, but the local recovery archive could not be written.";
public static SessionLeaveOutcome Run(SessionLeaveRequest request)
{
if (request is null)
{
throw new ArgumentNullException("request");
}
PlayModeTransitionResult playModeTransitionResult = request.Coordinator.TryBeginLeave(new SessionLeaveGuardRequest(request.LifecycleTransitionInProgress));
if (!playModeTransitionResult.Accepted)
{
return new SessionLeaveOutcome(playModeTransitionResult, PreserveAttempted: false, PreserveFailed: false, ResourcesDisposed: false, DisposalFailed: false, null);
}
bool preserveAttempted = false;
string? text = null;
try
{
if (request.HasUnacknowledgedWork())
{
preserveAttempted = true;
request.Preserve("Left with unacknowledged edits");
}
}
catch (Exception error)
{
preserveAttempted = true;
text = CollaborationTransportDiagnostics.BoundedExceptionDiagnostic("Left with unacknowledged edits, but the local recovery archive could not be written. ", error);
}
try
{
request.Announce();
}
catch
{
}
bool flag = false;
bool flag2 = false;
try
{
flag = request.DisposeResources();
}
catch (Exception error2)
{
flag = true;
flag2 = true;
if (text == null)
{
text = CollaborationTransportDiagnostics.BoundedExceptionDiagnostic("Connection teardown failed while leaving: ", error2);
}
}
return new SessionLeaveOutcome(playModeTransitionResult, preserveAttempted, text != null, ResourcesDisposed: true, flag2 || !flag, CollaborationDiagnostics.Bounded(text));
}
}
public enum ConnectionTeardownOutcome
{
None,
Detached,
ReusedPending,
CompletedWithoutConnection,
TerminalFailure
}
public sealed record ConnectionTeardownResult(ConnectionTeardownOutcome Outcome, string? Failure);
public sealed record TeardownCompletionObservation(bool Observed, bool TeardownFailed, string? TeardownFailure, bool AttemptRecovery, bool EnableDeferredRecovery, HotloadTeardownRecoveryStatus Status, string? Error);
public sealed class ConnectionTeardownCoordinator
{
private readonly ConnectionTeardownGate gate = new ConnectionTeardownGate();
public bool LeaveRequested { get; private set; }
public bool DeferredHotloadRecovery { get; private set; }
public bool TeardownCompletionPending { get; private set; }
public bool TeardownCompleted { get; private set; }
public string? TeardownFailure { get; private set; }
public bool HotloadArchiveRecorded { get; private set; }
public string? TerminalDiagnostic { get; private set; }
public bool IsPending => gate.IsPending;
public bool HasTerminalFailure => gate.HasTerminalFailure || TerminalDiagnostic != null;
public bool IsFailStop => IsPending || HasTerminalFailure;
public bool CanPromoteConnection => !IsFailStop && !TeardownCompletionPending;
private static TeardownCompletionObservation NotObserved => new TeardownCompletionObservation(Observed: false, TeardownFailed: false, null, AttemptRecovery: false, EnableDeferredRecovery: false, HotloadTeardownRecoveryStatus.NotRequired, null);
public void DeferHotloadRecovery()
{
DeferredHotloadRecovery = true;
}
public void ClearDeferredHotloadRecovery()
{
DeferredHotloadRecovery = false;
}
public PlayModeTransitionResult ValidateStart(SessionStartGuardRequest request)
{
if (!string.IsNullOrWhiteSpace(request.TerminalTeardownDiagnostic))
{
return Reject(request.TerminalTeardownDiagnostic);
}
if (request.ConnectionTeardownPending)
{
return Reject("A connection teardown is still pending. Leave the collaboration session before starting a new one.");
}
if (TeardownCompletionPending)
{
return Reject("A completed connection teardown is waiting to be observed; Start cannot clear it.");
}
if (request.ConnectionTeardownFailed)
{
return Reject("A previous connection teardown failed. This editor session remains fail-stop; restart the editor to recover.");
}
if (request.ConnectionBusy)
{
return Reject("Leave the current connection first");
}
if (!request.LifecycleValidation.Accepted)
{
return Reject(request.LifecycleValidation.ActionableError);
}
return PlayModeTransitionResult.Ok();
}
public PlayModeTransitionResult CommitStart()
{
if (TeardownCompletionPending)
{
return Reject("A completed connection teardown is waiting to be observed; Start cannot clear it.");
}
return CommitStartCore();
}
private PlayModeTransitionResult CommitStartCore()
{
LeaveRequested = false;
DeferredHotloadRecovery = false;
TeardownCompletionPending = false;
TeardownCompleted = false;
TeardownFailure = null;
return PlayModeTransitionResult.Ok();
}
public PlayModeTransitionResult BeginStart(SessionStartCommitRequest request)
{
if (request is null || !request.Validation.Accepted)
{
return request?.Validation ?? Reject("Start validation did not complete.");
}
try
{
if (!request.Prepare())
{
return Reject("Start preparation failed; no collaboration bookkeeping was committed.");
}
}
catch (Exception error)
{
return Reject(CollaborationTransportDiagnostics.BoundedExceptionDiagnostic("Start preparation failed: ", error));
}
return CommitStart();
}
public PlayModeTransitionResult TryBeginStart(SessionStartGuardRequest request)
{
PlayModeTransitionResult playModeTransitionResult = ValidateStart(request);
if (!playModeTransitionResult.Accepted)
{
return playModeTransitionResult;
}
return CommitStart();
}
public PlayModeTransitionResult ValidateReconnect(SessionReconnectGuardRequest request)
{
if (!string.IsNullOrWhiteSpace(request.TerminalTeardownDiagnostic))
{
return Reject(request.TerminalTeardownDiagnostic);
}
if (request.ConnectionTeardownPending)
{
return Reject("A connection teardown is still pending. Wait for it to complete or Leave before reconnecting.");
}
if (TeardownCompletionPending)
{
return Reject("A completed connection teardown is waiting to be observed; Reconnect cannot clear it.");
}
if (request.ConnectionTeardownFailed)
{
return Reject("A previous connection teardown failed. This editor session remains fail-stop; restart the editor to recover.");
}
if (request.Lifecycle == null)
{
return Reject("Runtime collaboration lifecycle is missing; recovery is not safe.");
}
PlayModeLifecycleState state = request.Lifecycle.State;
if ((state == PlayModeLifecycleState.RuntimeStarting || state == PlayModeLifecycleState.RuntimeBlocked) ? true : false)
{
return Reject("Stop play mode and complete hotload recovery with Leave before reconnecting.");
}
if (request.Lifecycle.HasTerminalTeardownFailure)
{
return Reject("A connection teardown failed; this editor session remains fail-stop.");
}
if (request.SessionLeft)
{
return Reject("No session to recover — start or join one.");
}
if (request.HostedSession)
{
return Reject("A hosted session cannot be recovered in place; leave and host the session again.");
}
if (!request.SessionEstablished)
{
return Reject("Join a session first");
}
if (!request.SourceBaselineMatches)
{
return Reject("Source, assets or project settings changed. Sync in Git and start a new session.");
}
if (request.ConnectionAttemptPending)
{
return Reject("A connection attempt is already pending");
}
if (request.ConnectionLive)
{
return Reject("Already connected");
}
if (!request.TransportState.HasValue)
{
return Reject(request.ConnectionBusy ? "Reconnect requires an explicitly failed or disconnected transport." : "Reconnect requires a transport state.");
}
CollaborationTransportState? transportState = request.TransportState;
bool flag;
if (transportState.HasValue)
{
CollaborationTransportState valueOrDefault = transportState.GetValueOrDefault();
if ((uint)(valueOrDefault - 3) <= 1u)
{
flag = true;
goto IL_01fc;
}
}
flag = false;
goto IL_01fc;
IL_01fc:
if (!flag)
{
return Reject("Reconnect requires an explicitly failed or disconnected transport.");
}
return PlayModeTransitionResult.Ok();
}
public SessionReconnectCommitOutcome BeginReconnect(SessionReconnectCommitRequest request)
{
if (request is null || !request.Validation.Accepted)
{
PlayModeTransitionResult result = request?.Validation ?? Reject("Reconnect validation did not complete.");
return new SessionReconnectCommitOutcome(result, ReconnectCommitStage.RejectedBeforePrepare, PreviousResourcesDisposed: false);
}
try
{
if (!request.Prepare())
{
return new SessionReconnectCommitOutcome(Reject("Reconnect preparation reported failure; the previous connection was already closed."), ReconnectCommitStage.FailedDuringPrepare, PreviousResourcesDisposed: true);
}
}
catch (Exception error)
{
return new SessionReconnectCommitOutcome(Reject(CollaborationTransportDiagnostics.BoundedExceptionDiagnostic("Reconnect preparation failed after the previous connection was closed: ", error)), ReconnectCommitStage.FailedDuringPrepare, PreviousResourcesDisposed: true);
}
return new SessionReconnectCommitOutcome(CommitReconnect(), ReconnectCommitStage.Committed, PreviousResourcesDisposed: true);
}
public PlayModeTransitionResult CommitReconnect()
{
if (TeardownCompletionPending)
{
return Reject("A completed connection teardown is waiting to be observed; Reconnect cannot clear it.");
}
return CommitReconnectCore();
}
private PlayModeTransitionResult CommitReconnectCore()
{
LeaveRequested = false;
DeferredHotloadRecovery = false;
return PlayModeTransitionResult.Ok();
}
public PlayModeTransitionResult TryBeginReconnect(SessionReconnectGuardRequest request)
{
PlayModeTransitionResult playModeTransitionResult = ValidateReconnect(request);
if (!playModeTransitionResult.Accepted)
{
return playModeTransitionResult;
}
return CommitReconnect();
}
public PlayModeTransitionResult ValidateLeave(SessionLeaveGuardRequest request)
{
if (request.LifecycleTransitionInProgress)
{
return Reject("Stop the in-progress play-mode lifecycle before leaving; no source or runtime state was discarded");
}
return PlayModeTransitionResult.Ok();
}
public PlayModeTransitionResult TryBeginLeave(SessionLeaveGuardRequest request)
{
if (request.LifecycleTransitionInProgress)
{
return Reject("Stop the in-progress play-mode lifecycle before leaving; no source or runtime state was discarded");
}
LeaveRequested = true;
return PlayModeTransitionResult.Ok();
}
public PlayModeTransitionResult ApplyArchiveMarker(PlayModeTransitionResult marker)
{
if (marker is null || !marker.Accepted)
{
HotloadArchiveRecorded = false;
return marker ?? PlayModeTransitionResult.Rejected("Hotload recovery archive was not recorded.");
}
HotloadArchiveRecorded = true;
return marker;
}
public ConnectionTeardownResult BeginDetachedTeardown<T>(Task<T>? pendingConnect, bool recoverWhenComplete) where T : IDisposable
{
if (gate.HasTerminalFailure || TerminalDiagnostic != null)
{
RecordTerminalTeardownFailure(gate.Failure ?? TerminalDiagnostic);
return new ConnectionTeardownResult(ConnectionTeardownOutcome.TerminalFailure, TerminalDiagnostic);
}
if (gate.IsPending)
{
if (recoverWhenComplete)
{
DeferredHotloadRecovery = true;
}
return new ConnectionTeardownResult(ConnectionTeardownOutcome.ReusedPending, null);
}
if (TeardownCompletionPending)
{
if (recoverWhenComplete)
{
DeferredHotloadRecovery = true;
}
return new ConnectionTeardownResult(ConnectionTeardownOutcome.ReusedPending, null);
}
if (recoverWhenComplete)
{
DeferredHotloadRecovery = true;
}
if (pendingConnect != null)
{
TeardownCompletionPending = true;
gate.Begin(pendingConnect);
return new ConnectionTeardownResult(ConnectionTeardownOutcome.Detached, null);
}
gate.CompleteWithoutConnection();
TeardownCompletionPending = true;
return new ConnectionTeardownResult(ConnectionTeardownOutcome.CompletedWithoutConnection, null);
}
public void RecordTerminalTeardownFailure(string? failure)
{
string text = (string.IsNullOrWhiteSpace(failure) ? "unknown teardown error" : failure);
if (!text.StartsWith("Connection teardown failed;", StringComparison.Ordinal))
{
TerminalDiagnostic = CollaborationDiagnostics.Bounded("Connection teardown failed; this editor session remains offline and fail-stop: " + text);
}
}
public TeardownCompletionObservation ObserveTeardownCompletion(bool requiresHotloadRecovery, bool connectionPending)
{
if (gate.IsPending)
{
return NotObserved;
}
if (TeardownCompletionPending)
{
if (!gate.TryTakeCompletion(out string? teardownFailure))
{
return NotObserved;
}
TeardownCompletionPending = false;
TeardownCompleted = true;
TeardownFailure = teardownFailure;
}
else if (!TeardownCompleted && TerminalDiagnostic == null)
{
return NotObserved;
}
if (TeardownFailure != null)
{
RecordTerminalTeardownFailure(TeardownFailure);
}
string? text = TeardownFailure ?? TerminalDiagnostic;
bool teardownFailed = text != null;
HotloadTeardownRecoveryDecision hotloadTeardownRecoveryDecision = HotloadTeardownRecovery.Decide(new HotloadTeardownRecoveryRequest(LeaveRequested, requiresHotloadRecovery, HotloadArchiveRecorded, HotloadArchiveRecorded, TeardownPending: false, text, teardownFailed, DeferredHotloadRecovery));
if (hotloadTeardownRecoveryDecision.EnableDeferredRecovery)
{
DeferredHotloadRecovery = true;
}
bool flag = connectionPending && hotloadTeardownRecoveryDecision.AttemptRecovery;
return new TeardownCompletionObservation(Observed: true, teardownFailed, text, hotloadTeardownRecoveryDecision.AttemptRecovery && !flag, hotloadTeardownRecoveryDecision.EnableDeferredRecovery, flag ? HotloadTeardownRecoveryStatus.WaitingForTeardown : hotloadTeardownRecoveryDecision.Status, hotloadTeardownRecoveryDecision.Error);
}
private static PlayModeTransitionResult Reject(string? error)
{
return PlayModeTransitionResult.Rejected(CollaborationDiagnostics.Bounded(error));
}
}