-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
AttachedCommand is the Command used when attaching to already running process.
- Loading branch information
Konrad Kruczyński
committed
Apr 5, 2019
1 parent
91f45f9
commit 1ccd64f
Showing
9 changed files
with
459 additions
and
112 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
using System; | ||
using System.Threading; | ||
using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
|
||
namespace Medallion.Shell.Tests | ||
{ | ||
[TestClass] | ||
public class AttachingTests | ||
{ | ||
[TestMethod] | ||
public void TestAttachingToExistingProcess() | ||
{ | ||
var processCommand = Command.Run("SampleCommand", new[] { "sleep", "10000" }); | ||
var processId = processCommand.ProcessId; | ||
Command.TryAttachToProcess(processId, out _) | ||
.ShouldEqual(true, "Attaching to process failed."); | ||
processCommand.Kill(); | ||
} | ||
|
||
[TestMethod] | ||
public void TestWaitingForAttachedProcessExit() | ||
{ | ||
var processCommand = Command.Run("SampleCommand", new[] { "sleep", "100" }); | ||
Command.TryAttachToProcess(processCommand.ProcessId, out var attachedCommand) | ||
.ShouldEqual(true, "Attaching to process failed."); | ||
var commandResult = attachedCommand.Task; | ||
commandResult.IsCompleted.ShouldEqual(false, "Task has finished too early."); | ||
Thread.Sleep(300); | ||
commandResult.IsCompleted.ShouldEqual(true, "Task has not finished on time."); | ||
} | ||
|
||
[TestMethod] | ||
public void TestGettingExitCodeFromAttachedProcess() | ||
{ | ||
var processCommand = Command.Run("SampleCommand", new[] { "exit", "16" }); | ||
Command.TryAttachToProcess(processCommand.ProcessId, out var attachedCommand) | ||
.ShouldEqual(true, "Attaching to process failed."); | ||
var task = attachedCommand.Task; | ||
task.Wait(1000).ShouldEqual(true, "Task has not finished on time."); | ||
task.Result.ExitCode.ShouldEqual(16, "Exit code was not correct."); | ||
} | ||
|
||
[TestMethod] | ||
public void TestAttachingToNonExistingProcess() | ||
{ | ||
var processCommand = Command.Run("SampleCommand", new[] { "exit", "0" }); | ||
var processId = processCommand.ProcessId; | ||
processCommand.Task.Wait(1000).ShouldEqual(true, "Process has not exited, test is inconclusive."); | ||
Command.TryAttachToProcess(processId, out _) | ||
.ShouldEqual(false, "Attaching succeeded although process has already exited."); | ||
} | ||
|
||
[TestMethod] | ||
public void TestKillingAttachedProcess() | ||
{ | ||
var processCommand = Command.Run("SampleCommand", new[] { "sleep", "10000" }); | ||
var processId = processCommand.ProcessId; | ||
Command.TryAttachToProcess( | ||
processId, | ||
options => options.DisposeOnExit(false), | ||
out var attachedCommand) | ||
.ShouldEqual(true, "Attaching to process failed."); | ||
|
||
attachedCommand.Kill(); | ||
attachedCommand.Process.HasExited | ||
.ShouldEqual(true, "The process is still alive after Kill() has finished."); | ||
} | ||
|
||
[TestMethod] | ||
public void TestAttachingWithAlreadyCancelledToken() | ||
{ | ||
var cancellationTokenSource = new CancellationTokenSource(); | ||
cancellationTokenSource.Cancel(); | ||
var processCommand = Command.Run("SampleCommand", new[] { "sleep", "10000" }); | ||
var processId = processCommand.ProcessId; | ||
Command.TryAttachToProcess( | ||
processId, | ||
options => options.CancellationToken(cancellationTokenSource.Token).DisposeOnExit(false), | ||
out var attachedCommand); | ||
attachedCommand.Process.HasExited.ShouldEqual(true, "The process wasn't killed."); | ||
} | ||
|
||
[TestMethod] | ||
public void TestTimeout() | ||
{ | ||
const string expectedTimeoutexceptionDidNotOccur = "Expected TimeoutException did not occur."; | ||
|
||
var processCommand = Command.Run("SampleCommand", new[] { "sleep", "10000" }); | ||
Thread.Sleep(200); | ||
var processId = processCommand.ProcessId; | ||
Command.TryAttachToProcess( | ||
processId, | ||
options => options.Timeout(TimeSpan.FromMilliseconds(150)), | ||
out var attachedCommand); | ||
|
||
// the timeout is counted from the moment we attached to the process so it shouldn't throw at this moment | ||
attachedCommand.Task.Wait(100); | ||
|
||
// but should eventually throw | ||
var exception = UnitTestHelpers.AssertThrows<AggregateException>( | ||
() => attachedCommand.Task.Wait(150), | ||
expectedTimeoutexceptionDidNotOccur); | ||
|
||
(exception.InnerException.InnerException is TimeoutException).ShouldEqual(true, expectedTimeoutexceptionDidNotOccur); | ||
} | ||
} | ||
} |
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; | ||
using System.Collections.Generic; | ||
using System.Collections.ObjectModel; | ||
using System.ComponentModel; | ||
using System.Diagnostics; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Medallion.Shell.Streams; | ||
|
||
namespace Medallion.Shell | ||
{ | ||
internal sealed class AttachedCommand : Command | ||
{ | ||
private const string StreamPropertyExceptionMessage = | ||
"This property cannot be used when attaching to already running process."; | ||
private readonly Process process; | ||
private readonly Task<CommandResult> commandResultTask; | ||
private readonly bool disposeOnExit; | ||
private readonly Lazy<ReadOnlyCollection<Process>> processes; | ||
|
||
internal AttachedCommand( | ||
Process process, | ||
bool throwOnError, | ||
TimeSpan timeout, | ||
CancellationToken cancellationToken, | ||
bool disposeOnExit) | ||
{ | ||
this.process = process; | ||
this.disposeOnExit = disposeOnExit; | ||
var processMonitoringTask = CreateProcessMonitoringTask(process); | ||
var processTask = ProcessHelper.CreateProcessTask(this.process, processMonitoringTask, throwOnError, timeout, cancellationToken); | ||
|
||
this.commandResultTask = processTask.ContinueWith(continuedTask => | ||
{ | ||
if (disposeOnExit) | ||
{ | ||
this.process.Dispose(); | ||
} | ||
return new CommandResult(continuedTask.Result, this); | ||
}); | ||
|
||
this.processes = new Lazy<ReadOnlyCollection<Process>>(() => new ReadOnlyCollection<Process>(new[] { this.process })); | ||
} | ||
|
||
public override Process Process | ||
{ | ||
get | ||
{ | ||
this.ThrowIfDisposed(); | ||
Throw<InvalidOperationException>.If( | ||
this.disposeOnExit, | ||
ProcessHelper.ProcessNotAccessibleWithDisposeOnExitEnabled | ||
); | ||
return this.process; | ||
} | ||
} | ||
|
||
public override IReadOnlyList<Process> Processes | ||
{ | ||
get | ||
{ | ||
this.ThrowIfDisposed(); | ||
return this.processes.Value; | ||
} | ||
} | ||
|
||
public override int ProcessId | ||
{ | ||
get | ||
{ | ||
this.ThrowIfDisposed(); | ||
|
||
return this.process.Id; | ||
} | ||
} | ||
|
||
public override IReadOnlyList<int> ProcessIds => new ReadOnlyCollection<int>(new[] { this.ProcessId }); | ||
|
||
public override ProcessStreamWriter StandardInput => throw new InvalidOperationException(StreamPropertyExceptionMessage); | ||
|
||
public override ProcessStreamReader StandardOutput => throw new InvalidOperationException(StreamPropertyExceptionMessage); | ||
|
||
public override ProcessStreamReader StandardError => throw new InvalidOperationException(StreamPropertyExceptionMessage); | ||
|
||
public override void Kill() | ||
{ | ||
this.ThrowIfDisposed(); | ||
|
||
ProcessHelper.TryKillProcess(this.process); | ||
} | ||
|
||
public override Task<CommandResult> Task => this.commandResultTask; | ||
|
||
protected override void DisposeInternal() | ||
{ | ||
this.Process.Dispose(); | ||
} | ||
|
||
private static Task CreateProcessMonitoringTask(Process process) | ||
{ | ||
var taskBuilder = new TaskCompletionSource<bool>(); | ||
|
||
// EnableRaisingEvents will throw if the process has already exited; to account for | ||
// that race condition we return a simple blocking task in that case | ||
try | ||
{ | ||
process.EnableRaisingEvents = true; | ||
} | ||
catch (InvalidOperationException) | ||
{ | ||
return System.Threading.Tasks.Task.Run(() => process.WaitForExit()); | ||
} | ||
|
||
process.Exited += (sender, e) => taskBuilder.TrySetResult(false); | ||
|
||
// we must account for the race condition where the process exits between enabling events and | ||
// subscribing to Exited. Therefore, we do exit check after the subscription to account | ||
// for this | ||
if (process.HasExited) | ||
{ | ||
taskBuilder.TrySetResult(false); | ||
} | ||
|
||
return taskBuilder.Task; | ||
} | ||
} | ||
} |
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.