Skip to content

Commit

Permalink
Add ProcessId/ProcessIds properties to command. Fix #16
Browse files Browse the repository at this point in the history
  • Loading branch information
madelson committed Jun 29, 2017
1 parent f012181 commit 98e8016
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 5 deletions.
60 changes: 60 additions & 0 deletions MedallionShell.Tests/GeneralTest.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Management;
using System.Reflection;
using System.Text;
using System.Threading;
Expand Down Expand Up @@ -314,6 +316,64 @@ public void TestGetOutputAndErrorLines()
CollectionAssert.AreEquivalent(lines, outputLines);
}

[TestMethod]
public void TestProcessAndProcessId()
{
void testHelper(bool disposeOnExit)
{
var shell = new Shell(o => o.DisposeOnExit(disposeOnExit));
var command1 = shell.Run("SampleCommand", "pipe", "--id1");
var command2 = shell.Run("SampleCommand", "pipe", "--id2");
var pipeCommand = command1.PipeTo(command2);
try
{
if (disposeOnExit)
{
// invalid due to DisposeOnExit()
UnitTestHelpers.AssertThrows<InvalidOperationException>(() => command1.Process.ToString())
.Message.ShouldContain("dispose on exit");
UnitTestHelpers.AssertThrows<InvalidOperationException>(() => command2.Processes.Count())
.Message.ShouldContain("dispose on exit");
UnitTestHelpers.AssertThrows<InvalidOperationException>(() => pipeCommand.Processes.Count())
.Message.ShouldContain("dispose on exit");
}
else
{
command1.Process.StartInfo.Arguments.ShouldContain("--id1");
command1.Processes.SequenceEqual(new[] { command1.Process });
command2.Process.StartInfo.Arguments.ShouldContain("--id2");
command2.Processes.SequenceEqual(new[] { command2.Process }).ShouldEqual(true);
pipeCommand.Process.ShouldEqual(command2.Process);
pipeCommand.Processes.SequenceEqual(new[] { command1.Process, command2.Process }).ShouldEqual(true);
}

// https://stackoverflow.com/questions/2633628/can-i-get-command-line-arguments-of-other-processes-from-net-c
string getCommandLine(int processId)
{
using (var searcher = new ManagementObjectSearcher("SELECT CommandLine FROM Win32_Process WHERE ProcessId = " + processId))
{
return searcher.Get().Cast<ManagementBaseObject>().Single()["CommandLine"].ToString();
}
}

getCommandLine(command1.ProcessId).ShouldContain("--id1");
command1.ProcessIds.SequenceEqual(new[] { command1.ProcessId }).ShouldEqual(true);
getCommandLine(command2.ProcessId).ShouldContain("--id2");
command2.ProcessIds.SequenceEqual(new[] { command2.ProcessId }).ShouldEqual(true);
pipeCommand.ProcessId.ShouldEqual(command2.ProcessId);
pipeCommand.ProcessIds.SequenceEqual(new[] { command1.ProcessId, command2.ProcessId }).ShouldEqual(true);
}
finally
{
command1.RedirectFrom(new[] { "data" });
pipeCommand.Wait();
}
}

testHelper(disposeOnExit: true);
testHelper(disposeOnExit: false);
}

private IEnumerable<string> ErrorLines()
{
yield return "1";
Expand Down
1 change: 1 addition & 0 deletions MedallionShell.Tests/MedallionShell.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
<Reference Include="System" />
<Reference Include="System.ComponentModel.Composition" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.Management" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Numerics" />
<Reference Include="System.Xml" />
Expand Down
11 changes: 11 additions & 0 deletions MedallionShell.Tests/UnitTestHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,16 @@ public static void AssertIsInstanceOf<T>(object value, string message = null)
{
Assert.IsInstanceOfType(value, typeof(T), message);
}

public static string ShouldContain(this string haystack, string needle, string message = null)
{
Assert.IsNotNull(haystack, $"Expected: contains '{needle}'. Was: NULL{(message != null ? $" ({message})" : string.Empty)}");
if (!haystack.Contains(needle))
{
Assert.Fail($"Expected: contains '{needle}'. Was: '{haystack}'{(message != null ? $" ({message})" : string.Empty)}");
}

return haystack;
}
}
}
24 changes: 19 additions & 5 deletions MedallionShell/Command.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,31 @@ public abstract class Command : IDisposable
internal Command() { }

/// <summary>
/// The <see cref="Process"/> associated with this <see cref="Command"/>. In a multi-process command,
/// this will be the final <see cref="Process"/> in the chain. NOTE: this property cannot be accessed when using
/// the DisposeOnExit option
/// The <see cref="System.Diagnostics.Process"/> associated with this <see cref="Command"/>. In a multi-process command,
/// this will be the final <see cref="System.Diagnostics.Process"/> in the chain. NOTE: this property cannot be accessed when using
/// the <see cref="Shell.Options.DisposeOnExit(bool)"/> option
/// </summary>
public abstract Process Process { get; }
/// <summary>
/// All <see cref="Process"/>es associated with this <see cref="Command"/>. NOTE: this property cannot be accessed when using
/// the DisposeOnExit option
/// All <see cref="System.Diagnostics.Process"/>es associated with this <see cref="Command"/>. NOTE: this property cannot be accessed when using
/// the <see cref="Shell.Options.DisposeOnExit(bool)"/> option
/// </summary>
public abstract IReadOnlyList<Process> Processes { get; }

/// <summary>
/// The PID of the process associated with this <see cref="Command"/>. In a multi-process command,
/// this will be the PID of the final <see cref="System.Diagnostics.Process"/> in the chain. NOTE: unlike
/// the <see cref="Process"/> property, this property is compatible with the <see cref="Shell.Options.DisposeOnExit(bool)"/>
/// option
/// </summary>
public abstract int ProcessId { get; }
/// <summary>
/// All PIDs of the <see cref="System.Diagnostics.Process"/>es associated with this <see cref="Command"/>. NOTE: unlike
/// the <see cref="Processes"/> property, this property is compatible with the
/// <see cref="Shell.Options.DisposeOnExit(bool)"/> option
/// </summary>
public abstract IReadOnlyList<int> ProcessIds { get; }

/// <summary>
/// Writes to the process's standard input
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions MedallionShell/IoCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ public override IReadOnlyList<System.Diagnostics.Process> Processes
get { return this.command.Processes; }
}

public override int ProcessId => this.command.ProcessId;
public override IReadOnlyList<int> ProcessIds => this.command.ProcessIds;

public override Streams.ProcessStreamWriter StandardInput
{
get { return this.command.StandardInput; }
Expand Down
8 changes: 8 additions & 0 deletions MedallionShell/PipedCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ public override IReadOnlyList<Process> Processes
get { return this.processes ?? (this.processes = this.first.Processes.Concat(this.second.Processes).ToList().AsReadOnly()); }
}

public override int ProcessId => this.second.ProcessId;

private IReadOnlyList<int> processIds;
public override IReadOnlyList<int> ProcessIds
{
get { return this.processIds ?? (this.processIds = this.first.ProcessIds.Concat(this.second.ProcessIds).ToList().AsReadOnly()); }
}

public override Task<CommandResult> Task
{
get { return this.task; }
Expand Down
32 changes: 32 additions & 0 deletions MedallionShell/ProcessCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -51,6 +52,15 @@ internal sealed class ProcessCommand : Command
this.standardInput = new ProcessStreamWriter(streamWriter);
}

// according to https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process.id?view=netcore-1.1#System_Diagnostics_Process_Id,
// this can throw PlatformNotSupportedException on some older windows systems in some StartInfo configurations. To be as
// robust as possible, we thus make this a best-effort attempt
try { this.processIdOrExceptionDispatchInfo = process.Id; }
catch (PlatformNotSupportedException processIdException)
{
this.processIdOrExceptionDispatchInfo = ExceptionDispatchInfo.Capture(processIdException);
}

this.task = this.CreateCombinedTask(processTask, ioTasks);
}

Expand Down Expand Up @@ -95,6 +105,28 @@ public override IReadOnlyList<Process> Processes
get { return this.processes ?? (this.processes = new ReadOnlyCollection<Process>(new[] { this.Process })); }
}

private readonly object processIdOrExceptionDispatchInfo;
public override int ProcessId
{
get
{
this.ThrowIfDisposed();

if (this.processIdOrExceptionDispatchInfo is ExceptionDispatchInfo exceptionDispatchInfo)
{
exceptionDispatchInfo.Throw();
}

return (int)this.processIdOrExceptionDispatchInfo;
}
}

private IReadOnlyList<int> processIds;
public override IReadOnlyList<int> ProcessIds
{
get { return this.processIds ?? (this.processIds = new ReadOnlyCollection<int>(new[] { this.ProcessId })); }
}

private readonly ProcessStreamWriter standardInput;
public override ProcessStreamWriter StandardInput
{
Expand Down

0 comments on commit 98e8016

Please sign in to comment.