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.
193 lines
5.5 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|