using System.Linq; using System.Threading; using SysThread = System.Threading.Thread; using WhiteMagic; using WhiteMagic.Native; using WhiteMagic.Thread; using Xunit; namespace WhiteMagicTest.Thread; /// /// Tests for scoped thread freeze via and . /// public sealed class FrozenThreadTests { [Fact] public void Freeze_suspends_selected_workers_until_disposed() { using var magic = Magic.OpenInProcess(); var factory = new ThreadFactory(magic.Memory); using var cts1 = new CancellationTokenSource(); using var cts2 = new CancellationTokenSource(); var started1 = new ManualResetEventSlim(false); var started2 = new ManualResetEventSlim(false); int osThreadId1 = 0; int osThreadId2 = 0; var worker1 = new SysThread(() => { osThreadId1 = (int)NativeMethods.GetCurrentThreadId(); started1.Set(); while (!cts1.IsCancellationRequested) SysThread.Sleep(10); }); var worker2 = new SysThread(() => { osThreadId2 = (int)NativeMethods.GetCurrentThreadId(); started2.Set(); while (!cts2.IsCancellationRequested) SysThread.Sleep(10); }); worker1.Start(); worker2.Start(); started1.Wait(); started2.Wait(); int[] targetIds = [osThreadId1, osThreadId2]; try { var selected = factory.Enumerate().Where(t => targetIds.Contains(t.Id)).ToList(); Assert.Equal(2, selected.Count); using (factory.Freeze(selected)) { cts1.Cancel(); cts2.Cancel(); Assert.False(worker1.Join(100)); Assert.False(worker2.Join(100)); } Assert.True(worker1.Join(1000)); Assert.True(worker2.Join(1000)); } finally { if (worker1.IsAlive) { cts1.Cancel(); using var t = new RemoteThread(magic.Memory, osThreadId1); t.Resume(); worker1.Join(1000); } if (worker2.IsAlive) { cts2.Cancel(); using var t = new RemoteThread(magic.Memory, osThreadId2); t.Resume(); worker2.Join(1000); } } } [Fact] public void Dispose_resumes_only_frozen_threads_leaving_external_suspends_intact() { using var magic = Magic.OpenInProcess(); var factory = new ThreadFactory(magic.Memory); using var cts = new CancellationTokenSource(); var started = new ManualResetEventSlim(false); int osThreadId = 0; var worker = new SysThread(() => { osThreadId = (int)NativeMethods.GetCurrentThreadId(); started.Set(); while (!cts.IsCancellationRequested) SysThread.Sleep(10); }); worker.Start(); started.Wait(); try { // Suspend the worker externally first. using (var external = new RemoteThread(magic.Memory, osThreadId)) { external.Suspend(); var selected = factory.Enumerate().Where(t => t.Id == osThreadId).ToList(); using (factory.Freeze(selected)) { // Frozen scope adds one more suspend count. } // After the freeze scope disposes, the worker was resumed once. // Because it was already externally suspended, it should still be suspended. cts.Cancel(); Assert.False(worker.Join(100)); external.Resume(); } Assert.True(worker.Join(1000)); } finally { if (worker.IsAlive) { cts.Cancel(); using var t = new RemoteThread(magic.Memory, osThreadId); t.Resume(); worker.Join(1000); } } } [Fact] public void Exception_in_body_still_resumes_frozen_threads() { using var magic = Magic.OpenInProcess(); var factory = new ThreadFactory(magic.Memory); using var cts = new CancellationTokenSource(); var started = new ManualResetEventSlim(false); int osThreadId = 0; var worker = new SysThread(() => { osThreadId = (int)NativeMethods.GetCurrentThreadId(); started.Set(); while (!cts.IsCancellationRequested) SysThread.Sleep(10); }); worker.Start(); started.Wait(); try { var selected = factory.Enumerate().Where(t => t.Id == osThreadId).ToList(); Assert.Throws(new Action(() => { using (factory.Freeze(selected)) { throw new InvalidOperationException("Intentional failure inside freeze scope."); } })); cts.Cancel(); Assert.True(worker.Join(1000)); } finally { if (worker.IsAlive) { cts.Cancel(); using var t = new RemoteThread(magic.Memory, osThreadId); t.Resume(); worker.Join(1000); } } } }