Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions Runtime/Scripts/Internal/MonoBehaviourContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,40 @@ private static void Init()
/// Runs a coroutine from a non-MonoBehaviour context, invoking the callback when the
/// coroutine completes.
/// </summary>
/// <remarks>
/// If the singleton has already been destroyed (e.g. during play mode exit or
/// application shutdown) the coroutine cannot be started on a MonoBehaviour, so it is
/// drained synchronously instead. This keeps cleanup side effects working without
/// throwing a <see cref="MissingReferenceException"/>.
/// </remarks>
internal static void RunCoroutine(IEnumerator coroutine, Action onComplete = null)
{
// Unity's overloaded == treats a destroyed object as null.
if (_instance == null)
{
DrainCoroutine(coroutine);
onComplete?.Invoke();
return;
}

_instance.StartCoroutine(WrapCoroutine(coroutine, onComplete));
}

/// <summary>
/// Synchronously runs a coroutine to completion, ignoring time-based yield instructions
/// and recursing into nested enumerators. Used as a fallback when no MonoBehaviour is
/// available to host the coroutine.
/// </summary>
private static void DrainCoroutine(IEnumerator coroutine)
{
if (coroutine == null) return;
while (coroutine.MoveNext())
{
if (coroutine.Current is IEnumerator nested)
DrainCoroutine(nested);
}
}

private static IEnumerator WrapCoroutine(IEnumerator coroutine, Action onComplete = null)
{
yield return coroutine;
Expand Down
62 changes: 62 additions & 0 deletions Tests/EditMode/MonoBehaviourContextTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System;
using System.Collections;
using System.Reflection;
using NUnit.Framework;
using UnityEngine;

namespace LiveKit.EditModeTests
{
public class MonoBehaviourContextTests
{
// Regression test for the MissingReferenceException thrown when the scene is stopped:
// Unity tears down the DontDestroyOnLoad LiveKitSDK GameObject (backing the
// MonoBehaviourContext singleton) before MeetManager.OnDestroy() runs, then
// MicrophoneSource.Stop() calls RunCoroutine on the destroyed instance.
//
// The real play-mode-exit teardown ordering can't be controlled from a test, but the
// throwing call (_instance.StartCoroutine) fires synchronously the moment its host is
// destroyed. So we reproduce the precondition deterministically: point _instance at a
// destroyed object via DestroyImmediate, then call RunCoroutine. Against the old code
// this throws MissingReferenceException; against the fix it drains the coroutine
// synchronously and invokes onComplete.
[Test]
public void RunCoroutine_AfterInstanceDestroyed_DrainsSynchronously_DoesNotThrow()
{
var instanceField = typeof(MonoBehaviourContext)
.GetField("_instance", BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(instanceField, "Expected private static MonoBehaviourContext._instance field");

// Save the global singleton so destroying our stand-in can't disturb other tests.
var original = instanceField.GetValue(null);
try
{
var go = new GameObject("temp-monobehaviourcontext");
var ctx = go.AddComponent<MonoBehaviourContext>();
instanceField.SetValue(null, ctx);

// Synchronous destroy: _instance now reports == null via Unity's overloaded ==.
UnityEngine.Object.DestroyImmediate(go);

bool ran = false;
bool completed = false;

Assert.DoesNotThrow(
() => MonoBehaviourContext.RunCoroutine(Body(() => ran = true), () => completed = true),
"RunCoroutine must not throw when the singleton has already been destroyed");

Assert.IsTrue(ran, "coroutine body should have been drained synchronously");
Assert.IsTrue(completed, "onComplete should fire after the drained coroutine");
}
finally
{
instanceField.SetValue(null, original);
}
}

private static IEnumerator Body(Action onStep)
{
onStep();
yield return null;
}
}
}
2 changes: 2 additions & 0 deletions Tests/EditMode/MonoBehaviourContextTests.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading