diff --git a/MedallionShell.Tests/GeneralTest.cs b/MedallionShell.Tests/GeneralTest.cs index a4b641e..5edfc3c 100644 --- a/MedallionShell.Tests/GeneralTest.cs +++ b/MedallionShell.Tests/GeneralTest.cs @@ -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; @@ -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(() => command1.Process.ToString()) + .Message.ShouldContain("dispose on exit"); + UnitTestHelpers.AssertThrows(() => command2.Processes.Count()) + .Message.ShouldContain("dispose on exit"); + UnitTestHelpers.AssertThrows(() => 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().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 ErrorLines() { yield return "1"; diff --git a/MedallionShell.Tests/MedallionShell.Tests.csproj b/MedallionShell.Tests/MedallionShell.Tests.csproj index c7a8d55..4c57e1d 100644 --- a/MedallionShell.Tests/MedallionShell.Tests.csproj +++ b/MedallionShell.Tests/MedallionShell.Tests.csproj @@ -59,6 +59,7 @@ + diff --git a/MedallionShell.Tests/UnitTestHelpers.cs b/MedallionShell.Tests/UnitTestHelpers.cs index 384047a..840066b 100644 --- a/MedallionShell.Tests/UnitTestHelpers.cs +++ b/MedallionShell.Tests/UnitTestHelpers.cs @@ -43,5 +43,16 @@ public static void AssertIsInstanceOf(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; + } } } diff --git a/MedallionShell/Command.cs b/MedallionShell/Command.cs index 54b01b7..7270e7e 100644 --- a/MedallionShell/Command.cs +++ b/MedallionShell/Command.cs @@ -20,17 +20,31 @@ public abstract class Command : IDisposable internal Command() { } /// - /// The associated with this . In a multi-process command, - /// this will be the final in the chain. NOTE: this property cannot be accessed when using - /// the DisposeOnExit option + /// The associated with this . In a multi-process command, + /// this will be the final in the chain. NOTE: this property cannot be accessed when using + /// the option /// public abstract Process Process { get; } /// - /// All es associated with this . NOTE: this property cannot be accessed when using - /// the DisposeOnExit option + /// All es associated with this . NOTE: this property cannot be accessed when using + /// the option /// public abstract IReadOnlyList Processes { get; } + /// + /// The PID of the process associated with this . In a multi-process command, + /// this will be the PID of the final in the chain. NOTE: unlike + /// the property, this property is compatible with the + /// option + /// + public abstract int ProcessId { get; } + /// + /// All PIDs of the es associated with this . NOTE: unlike + /// the property, this property is compatible with the + /// option + /// + public abstract IReadOnlyList ProcessIds { get; } + /// /// Writes to the process's standard input /// diff --git a/MedallionShell/IoCommand.cs b/MedallionShell/IoCommand.cs index 4663289..0b0c11e 100644 --- a/MedallionShell/IoCommand.cs +++ b/MedallionShell/IoCommand.cs @@ -33,6 +33,9 @@ public override IReadOnlyList Processes get { return this.command.Processes; } } + public override int ProcessId => this.command.ProcessId; + public override IReadOnlyList ProcessIds => this.command.ProcessIds; + public override Streams.ProcessStreamWriter StandardInput { get { return this.command.StandardInput; } diff --git a/MedallionShell/PipedCommand.cs b/MedallionShell/PipedCommand.cs index ed2d169..7f86eb6 100644 --- a/MedallionShell/PipedCommand.cs +++ b/MedallionShell/PipedCommand.cs @@ -40,6 +40,14 @@ public override IReadOnlyList 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 processIds; + public override IReadOnlyList ProcessIds + { + get { return this.processIds ?? (this.processIds = this.first.ProcessIds.Concat(this.second.ProcessIds).ToList().AsReadOnly()); } + } + public override Task Task { get { return this.task; } diff --git a/MedallionShell/ProcessCommand.cs b/MedallionShell/ProcessCommand.cs index 2ff1c8b..185e26d 100644 --- a/MedallionShell/ProcessCommand.cs +++ b/MedallionShell/ProcessCommand.cs @@ -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; @@ -51,6 +52,15 @@ internal ProcessCommand( 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); } @@ -95,6 +105,28 @@ public override IReadOnlyList Processes get { return this.processes ?? (this.processes = new ReadOnlyCollection(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 processIds; + public override IReadOnlyList ProcessIds + { + get { return this.processIds ?? (this.processIds = new ReadOnlyCollection(new[] { this.ProcessId })); } + } + private readonly ProcessStreamWriter standardInput; public override ProcessStreamWriter StandardInput {