-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Added the ProcessTracker class, with its tests and documentation
- Loading branch information
Showing
7 changed files
with
463 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
using System.Diagnostics; | ||
|
||
namespace MindControl; | ||
|
||
/// <summary> | ||
/// Provides a <see cref="ProcessMemory"/> for a process identified by its name. | ||
/// The tracker is able to re-attach to a process with the same name after it has been closed and reopened. | ||
/// </summary> | ||
public class ProcessTracker : IDisposable | ||
{ | ||
private readonly string _processName; | ||
private readonly SemaphoreSlim _instanceSemaphore = new(1, 1); | ||
private ProcessMemory? _processMemory; | ||
|
||
/// <summary> | ||
/// Gets the name of the process tracked by this instance. | ||
/// </summary> | ||
public string ProcessName => _processName; | ||
|
||
/// <summary> | ||
/// Gets a value indicating if the process is currently attached. | ||
/// </summary> | ||
public bool IsAttached => _processMemory?.IsAttached == true; | ||
|
||
/// <summary> | ||
/// Event raised when attaching to the target process. | ||
/// </summary> | ||
public event EventHandler? Attached; | ||
|
||
/// <summary> | ||
/// Event raised when detached from the target process. | ||
/// </summary> | ||
public event EventHandler? Detached; | ||
|
||
/// <summary> | ||
/// Builds a tracker for the process with the given name. | ||
/// </summary> | ||
/// <param name="processName">Name of the process to track.</param> | ||
public ProcessTracker(string processName) | ||
{ | ||
_processName = processName; | ||
} | ||
|
||
/// <summary> | ||
/// Gets the <see cref="ProcessMemory"/> instance for the currently attached process. | ||
/// If no process is attached yet, make an attempt to attach to the target process by its name. | ||
/// Returns null if no process with the target name can be found. | ||
/// </summary> | ||
public ProcessMemory? GetProcessMemory() | ||
{ | ||
// Use a semaphore here to make sure we never attach twice or attach while detaching. | ||
_instanceSemaphore.Wait(); | ||
try | ||
{ | ||
if (_processMemory?.IsAttached != true) | ||
{ | ||
_processMemory = AttemptToAttachProcess(); | ||
|
||
if (_processMemory != null) | ||
{ | ||
_processMemory.ProcessDetached += OnProcessDetached; | ||
Attached?.Invoke(this, EventArgs.Empty); | ||
} | ||
} | ||
} | ||
finally | ||
{ | ||
_instanceSemaphore.Release(); | ||
} | ||
return _processMemory; | ||
} | ||
|
||
/// <summary> | ||
/// Callback. Called when the process memory detaches. | ||
/// </summary> | ||
private void OnProcessDetached(object? sender, EventArgs e) => Detach(); | ||
|
||
/// <summary> | ||
/// Makes sure the memory instance is detached. | ||
/// </summary> | ||
private void Detach() | ||
{ | ||
// Reserve the semaphore to prevent simultaneous detach/attach operations. | ||
_instanceSemaphore.Wait(); | ||
|
||
try | ||
{ | ||
if (_processMemory != null) | ||
{ | ||
_processMemory.ProcessDetached -= OnProcessDetached; | ||
_processMemory?.Dispose(); | ||
Detached?.Invoke(this, EventArgs.Empty); | ||
} | ||
} | ||
catch | ||
{ | ||
// Swallow the exception - we don't care about something happening while detaching. | ||
} | ||
|
||
_processMemory = null; | ||
_instanceSemaphore.Release(); | ||
} | ||
|
||
/// <summary> | ||
/// Attempts to locate and attach the target process, and returns the resulting process memory instance. | ||
/// </summary> | ||
private ProcessMemory? AttemptToAttachProcess() | ||
{ | ||
var process = GetTargetProcess(); | ||
return process == null ? null : new ProcessMemory(process); | ||
} | ||
|
||
/// <summary> | ||
/// Gets the first process running with the target name. Returns null if no process with the given name is found. | ||
/// </summary> | ||
private Process? GetTargetProcess() => Process.GetProcessesByName(_processName).MinBy(p => p.Id); | ||
|
||
/// <summary> | ||
/// Releases the underlying process memory if required. | ||
/// </summary> | ||
public void Dispose() | ||
{ | ||
Detach(); | ||
_instanceSemaphore.Dispose(); | ||
GC.SuppressFinalize(this); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
53 changes: 53 additions & 0 deletions
53
test/MindControl.Test/ProcessMemoryTests/ProcessMemoryAttachTest.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
using NUnit.Framework; | ||
|
||
namespace MindControl.Test.ProcessMemoryTests; | ||
|
||
/// <summary> | ||
/// Tests the features of the <see cref="ProcessMemory"/> class related to attaching to a process. | ||
/// </summary> | ||
[FixtureLifeCycle(LifeCycle.InstancePerTestCase)] | ||
public class ProcessMemoryAttachTest : ProcessMemoryTest | ||
{ | ||
/// <summary> | ||
/// This test only ensures that the setup works, i.e. that opening a process as a | ||
/// <see cref="MindControl.ProcessMemory"/> instance won't throw an exception. | ||
/// </summary> | ||
[Test] | ||
public void OpenProcessTest() { } | ||
|
||
/// <summary> | ||
/// Tests that the Dispose method detaches from the process and raises the relevant event. | ||
/// </summary> | ||
[Test] | ||
public void DisposeTest() | ||
{ | ||
var hasRaisedEvent = false; | ||
TestProcessMemory!.ProcessDetached += (_, _) => { hasRaisedEvent = true; }; | ||
TestProcessMemory.Dispose(); | ||
Assert.Multiple(() => | ||
{ | ||
Assert.That(hasRaisedEvent, Is.True); | ||
Assert.That(TestProcessMemory.IsAttached, Is.False); | ||
}); | ||
} | ||
|
||
/// <summary> | ||
/// Tests that the <see cref="ProcessMemory.ProcessDetached"/> event is raised when the process exits. | ||
/// </summary> | ||
[Test] | ||
public void ProcessDetachedOnExitTest() | ||
{ | ||
var hasRaisedEvent = false; | ||
TestProcessMemory!.ProcessDetached += (_, _) => { hasRaisedEvent = true; }; | ||
|
||
// Go to the end of the process, then wait for a bit to make sure the process exits before we assert the results | ||
ProceedUntilProcessEnds(); | ||
Thread.Sleep(1000); | ||
|
||
Assert.Multiple(() => | ||
{ | ||
Assert.That(hasRaisedEvent, Is.True); | ||
Assert.That(TestProcessMemory.IsAttached, Is.False); | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.