Files
kbe 9aef9c21e3 Add thread control surfaces
Adds RemoteThread, ThreadFactory (enumeration, main-thread selection, get-by-id), and FrozenThread scoped freeze. Supports suspend/resume, 32/64-bit context round-trip, TEB query, and reverse-order resume on dispose. Closes section 2 of add-thread-region-finder.
2026-07-22 16:04:30 +02:00

193 lines
5.5 KiB
C#

using System.Linq;
using System.Threading;
using SysThread = System.Threading.Thread;
using WhiteMagic;
using WhiteMagic.Native;
using WhiteMagic.Thread;
using Xunit;
namespace WhiteMagicTest.Thread;
/// <summary>
/// Tests for scoped thread freeze via <see cref="FrozenThread"/> and <see cref="ThreadFactory.Freeze"/>.
/// </summary>
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<InvalidOperationException>(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);
}
}
}
}