├── .gitignore ├── Cbonnell.DotNetExpect.Test ├── Cbonnell.DotNetExpect.Test.csproj ├── ChildProcessOptionsTest.cs ├── ChildProcessTest.cs ├── Properties │ └── AssemblyInfo.cs ├── TestEnvironment.cs └── TestUtilities.cs ├── Cbonnell.DotNetExpect ├── Cbonnell.DotNetExpect.csproj ├── Cbonnell.DotNetExpect.nuspec ├── Cbonnell.DotNetExpect.sln ├── ChildProcess.cs ├── ChildProcessOptions.cs ├── CommandResult.cs ├── ConsoleInterface.cs ├── OperationFailedException.cs ├── Properties │ └── AssemblyInfo.cs ├── ProxyProcess.cs └── ProxyProcessManager.cs ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | [Dd]ebug/ 11 | [Dd]ebugPublic/ 12 | [Rr]elease/ 13 | [Rr]eleases/ 14 | x64/ 15 | x86/ 16 | build/ 17 | bld/ 18 | [Bb]in/ 19 | [Oo]bj/ 20 | 21 | # Roslyn cache directories 22 | *.ide/ 23 | 24 | # MSTest test Results 25 | [Tt]est[Rr]esult*/ 26 | [Bb]uild[Ll]og.* 27 | 28 | #NUNIT 29 | *.VisualState.xml 30 | TestResult.xml 31 | 32 | # Build Results of an ATL Project 33 | [Dd]ebugPS/ 34 | [Rr]eleasePS/ 35 | dlldata.c 36 | 37 | *_i.c 38 | *_p.c 39 | *_i.h 40 | *.ilk 41 | *.meta 42 | *.obj 43 | *.pch 44 | *.pdb 45 | *.pgc 46 | *.pgd 47 | *.rsp 48 | *.sbr 49 | *.tlb 50 | *.tli 51 | *.tlh 52 | *.tmp 53 | *.tmp_proj 54 | *.log 55 | *.vspscc 56 | *.vssscc 57 | .builds 58 | *.pidb 59 | *.svclog 60 | *.scc 61 | 62 | # Chutzpah Test files 63 | _Chutzpah* 64 | 65 | # Visual C++ cache files 66 | ipch/ 67 | *.aps 68 | *.ncb 69 | *.opensdf 70 | *.sdf 71 | *.cachefile 72 | 73 | # Visual Studio profiler 74 | *.psess 75 | *.vsp 76 | *.vspx 77 | 78 | # TFS 2012 Local Workspace 79 | $tf/ 80 | 81 | # Guidance Automation Toolkit 82 | *.gpState 83 | 84 | # ReSharper is a .NET coding add-in 85 | _ReSharper*/ 86 | *.[Rr]e[Ss]harper 87 | *.DotSettings.user 88 | 89 | # JustCode is a .NET coding addin-in 90 | .JustCode 91 | 92 | # TeamCity is a build add-in 93 | _TeamCity* 94 | 95 | # DotCover is a Code Coverage Tool 96 | *.dotCover 97 | 98 | # NCrunch 99 | _NCrunch_* 100 | .*crunch*.local.xml 101 | 102 | # MightyMoose 103 | *.mm.* 104 | AutoTest.Net/ 105 | 106 | # Web workbench (sass) 107 | .sass-cache/ 108 | 109 | # Installshield output folder 110 | [Ee]xpress/ 111 | 112 | # DocProject is a documentation generator add-in 113 | DocProject/buildhelp/ 114 | DocProject/Help/*.HxT 115 | DocProject/Help/*.HxC 116 | DocProject/Help/*.hhc 117 | DocProject/Help/*.hhk 118 | DocProject/Help/*.hhp 119 | DocProject/Help/Html2 120 | DocProject/Help/html 121 | 122 | # Click-Once directory 123 | publish/ 124 | 125 | # Publish Web Output 126 | *.[Pp]ublish.xml 127 | *.azurePubxml 128 | # TODO: Comment the next line if you want to checkin your web deploy settings 129 | # but database connection strings (with potential passwords) will be unencrypted 130 | *.pubxml 131 | *.publishproj 132 | 133 | # NuGet Packages 134 | *.nupkg 135 | # The packages folder can be ignored because of Package Restore 136 | **/packages/* 137 | # except build/, which is used as an MSBuild target. 138 | !**/packages/build/ 139 | # If using the old MSBuild-Integrated Package Restore, uncomment this: 140 | #!**/packages/repositories.config 141 | 142 | # Windows Azure Build Output 143 | csx/ 144 | *.build.csdef 145 | 146 | # Windows Store app package directory 147 | AppPackages/ 148 | 149 | # Others 150 | sql/ 151 | *.Cache 152 | ClientBin/ 153 | [Ss]tyle[Cc]op.* 154 | ~$* 155 | *~ 156 | *.dbmdl 157 | *.dbproj.schemaview 158 | *.pfx 159 | *.publishsettings 160 | node_modules/ 161 | 162 | # RIA/Silverlight projects 163 | Generated_Code/ 164 | 165 | # Backup & report files from converting an old project file 166 | # to a newer Visual Studio version. Backup files are not needed, 167 | # because we have git ;-) 168 | _UpgradeReport_Files/ 169 | Backup*/ 170 | UpgradeLog*.XML 171 | UpgradeLog*.htm 172 | 173 | # SQL Server files 174 | *.mdf 175 | *.ldf 176 | 177 | # Business Intelligence projects 178 | *.rdl.data 179 | *.bim.layout 180 | *.bim_*.settings 181 | 182 | # Microsoft Fakes 183 | FakesAssemblies/ 184 | -------------------------------------------------------------------------------- /Cbonnell.DotNetExpect.Test/Cbonnell.DotNetExpect.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {71ED942A-A513-4B8B-8381-2050A7636652} 8 | Library 9 | Properties 10 | Cbonnell.DotNetExpect.Test 11 | Cbonnell.DotNetExpect.Test 12 | v3.5 13 | 512 14 | Client 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {0b25d3a5-eb12-42b6-887d-193bc0770d88} 52 | Cbonnell.DotNetExpect 53 | 54 | 55 | 56 | 63 | -------------------------------------------------------------------------------- /Cbonnell.DotNetExpect.Test/ChildProcessOptionsTest.cs: -------------------------------------------------------------------------------- 1 | /* 2 | DotNetExpect 3 | Copyright (c) Corey Bonnell, All rights reserved. 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 3.0 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library. */ 17 | 18 | using NUnit.Framework; 19 | using System; 20 | using System.Linq; 21 | 22 | namespace Cbonnell.DotNetExpect.Test 23 | { 24 | [TestFixture] 25 | public class ChildProcessOptionsTest 26 | { 27 | [SetUp] 28 | public void SetUp() 29 | { 30 | TestUtilities.EnsureProxyExit(); 31 | } 32 | 33 | [Test] 34 | public void DefaultValuesExpected() 35 | { 36 | ChildProcessOptions options = new ChildProcessOptions(); 37 | Assert.AreEqual(Environment.NewLine, options.NewLine); 38 | Assert.IsTrue(options.AttachConsole); 39 | Assert.AreEqual(10 * 1000, options.AttachConsoleTimeoutMilliseconds); 40 | Assert.AreEqual(60 * 1000, options.TimeoutMilliseconds); 41 | Assert.IsTrue(options.ClearConsole); 42 | } 43 | 44 | [Test] 45 | public void DefaultConsoleClearsAfterMatch() 46 | { 47 | using (ChildProcess childProc = new ChildProcess(TestEnvironment.CMD_EXE_NAME)) 48 | { 49 | string content = childProc.Read(TestEnvironment.PROMPT_CHAR.ToString()); 50 | childProc.WriteLine("echo \"hello world\""); 51 | content = childProc.Read(TestEnvironment.PROMPT_CHAR.ToString()); 52 | Assert.AreEqual(1, content.Count((c) => c.Equals(TestEnvironment.PROMPT_CHAR))); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Cbonnell.DotNetExpect.Test/ChildProcessTest.cs: -------------------------------------------------------------------------------- 1 | /* 2 | DotNetExpect 3 | Copyright (c) Corey Bonnell, All rights reserved. 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 3.0 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library. */ 17 | 18 | using NUnit.Framework; 19 | using System; 20 | using System.Diagnostics; 21 | using System.Text.RegularExpressions; 22 | using System.Threading; 23 | 24 | namespace Cbonnell.DotNetExpect.Test 25 | { 26 | [TestFixture] 27 | public class ChildProcessTest 28 | { 29 | [SetUp] 30 | public void SetUp() 31 | { 32 | TestUtilities.EnsureProxyExit(); 33 | } 34 | 35 | [Test] 36 | [ExpectedException(typeof(ArgumentNullException))] 37 | public void ConstructorNullFilePath() 38 | { 39 | new ChildProcess(null); 40 | } 41 | 42 | [Test] 43 | [ExpectedException(typeof(ArgumentNullException))] 44 | public void ConstructorNullArguments() 45 | { 46 | new ChildProcess(TestEnvironment.DUMMY_EXE_NAME, null, new ChildProcessOptions()); 47 | } 48 | 49 | [Test] 50 | [ExpectedException(typeof(ArgumentNullException))] 51 | public void ConstructorNullWorkingDirectory() 52 | { 53 | new ChildProcess(TestEnvironment.DUMMY_EXE_NAME, TestEnvironment.DUMMY_ARGUMENTS, null, new ChildProcessOptions()); 54 | } 55 | 56 | [Test] 57 | [ExpectedException(typeof(ArgumentNullException))] 58 | public void ConstructorNullOptions() 59 | { 60 | new ChildProcess(TestEnvironment.DUMMY_EXE_NAME, TestEnvironment.DUMMY_ARGUMENTS, Environment.CurrentDirectory, null); 61 | } 62 | 63 | [Test] 64 | [ExpectedException(typeof(OperationFailedException))] 65 | public void ProcessNoExist() 66 | { 67 | using (ChildProcess childProc = new ChildProcess(Guid.NewGuid().ToString() + ".exe")) { } 68 | } 69 | 70 | [Test] 71 | [ExpectedException(typeof(ArgumentNullException))] 72 | public void ReadNullRegex() 73 | { 74 | using (ChildProcess childProc = new ChildProcess(TestEnvironment.CMD_EXE_NAME)) 75 | { 76 | childProc.Match(null); 77 | } 78 | } 79 | 80 | [Test] 81 | [ExpectedException(typeof(ArgumentNullException))] 82 | public void WriteNullString() 83 | { 84 | using (ChildProcess childProc = new ChildProcess(TestEnvironment.CMD_EXE_NAME)) 85 | { 86 | childProc.WriteLine(null); 87 | } 88 | } 89 | 90 | [Test] 91 | public void SimpleReadUntilPrompt() 92 | { 93 | using (ChildProcess childProc = new ChildProcess(TestEnvironment.CMD_EXE_NAME)) 94 | { 95 | string content = childProc.Read(TestEnvironment.PROMPT_CHAR.ToString()); 96 | Console.WriteLine(content); 97 | Assert.IsTrue(content.EndsWith(TestEnvironment.PROMPT_CHAR.ToString())); 98 | } 99 | } 100 | 101 | [Test] 102 | public void SimpleMatch() 103 | { 104 | using (ChildProcess childProc = new ChildProcess(TestEnvironment.CMD_EXE_NAME)) 105 | { 106 | childProc.WriteLine("dir"); 107 | Match m = childProc.Match(new Regex(@"Volume Serial Number is (?[0-9A-F]{4}-[0-9A-F]{4})")); 108 | Console.WriteLine("Primary volume serial number: {0}", m.Groups["VolumeSerial"].Value); 109 | Assert.IsTrue(m.Success); 110 | } 111 | } 112 | 113 | [Test] 114 | public void SimpleRead() 115 | { 116 | using (ChildProcess childProc = new ChildProcess(TestEnvironment.CMD_EXE_NAME)) 117 | { 118 | string content = childProc.Read(Environment.CurrentDirectory + ">"); 119 | Assert.IsTrue(content.Contains(Environment.CurrentDirectory + ">")); 120 | } 121 | } 122 | 123 | [Test] 124 | [ExpectedException(typeof(TimeoutException))] 125 | public void ReadTimeout() 126 | { 127 | using (ChildProcess childProc = new ChildProcess(TestEnvironment.CMD_EXE_NAME, new ChildProcessOptions() { TimeoutMilliseconds = 0 })) 128 | { 129 | childProc.Read(Guid.NewGuid().ToString()); 130 | } 131 | } 132 | 133 | [Test] 134 | [ExpectedException(typeof(TimeoutException))] 135 | public void MatchTimeout() 136 | { 137 | using (ChildProcess childProc = new ChildProcess(TestEnvironment.CMD_EXE_NAME, new ChildProcessOptions() { TimeoutMilliseconds = 0 })) 138 | { 139 | childProc.Match(new Regex(Guid.NewGuid().ToString())); 140 | } 141 | } 142 | 143 | [Test] 144 | [ExpectedException(typeof(ObjectDisposedException))] 145 | public void ObjectDisposed() 146 | { 147 | ChildProcess childProc; 148 | using (childProc = new ChildProcess(TestEnvironment.CMD_EXE_NAME)) { } 149 | childProc.ClearConsole(); 150 | } 151 | 152 | [Test] 153 | public void VerifyChildDeadOnDispose() 154 | { 155 | Process p = null; 156 | try 157 | { 158 | using (ChildProcess childProc = new ChildProcess(TestEnvironment.CMD_EXE_NAME)) 159 | { 160 | p = Process.GetProcessById(childProc.ChildProcessId); 161 | } 162 | TestUtilities.WaitForProcessExitAndThrow(p); 163 | } 164 | finally 165 | { 166 | if (p != null) 167 | { 168 | p.Dispose(); 169 | } 170 | } 171 | } 172 | 173 | [Test] 174 | public void VerifyProxyDeadOnDispose() 175 | { 176 | Process[] powerShells = null; 177 | 178 | try 179 | { 180 | using (ChildProcess childProc = new ChildProcess(TestEnvironment.CMD_EXE_NAME)) 181 | { 182 | powerShells = Process.GetProcessesByName(TestEnvironment.PROXY_PROCESS_NAME); 183 | Assert.AreEqual(1, powerShells.Length); 184 | } 185 | TestUtilities.WaitForProcessExitAndThrow(powerShells[0]); 186 | } 187 | finally 188 | { 189 | if (powerShells != null) 190 | { 191 | Array.ForEach(powerShells, (p) => p.Dispose()); 192 | } 193 | } 194 | } 195 | 196 | [Test] 197 | public void TestExistingProcess() 198 | { 199 | using (Process p = new Process()) 200 | { 201 | try 202 | { 203 | p.StartInfo.FileName = TestEnvironment.CMD_EXE_NAME; 204 | p.StartInfo.UseShellExecute = false; 205 | p.StartInfo.CreateNoWindow = true; 206 | p.Start(); 207 | using (ChildProcess childProc = new ChildProcess(p.Id)) 208 | { 209 | string prompt = childProc.Read(TestEnvironment.PROMPT_CHAR.ToString()); 210 | Console.WriteLine(prompt); 211 | } 212 | } 213 | finally 214 | { 215 | p.Kill(); 216 | } 217 | } 218 | } 219 | 220 | [Test] 221 | public void TestExistingProcessStillAliveAfterDispose() 222 | { 223 | using (Process p = new Process()) 224 | { 225 | try 226 | { 227 | p.StartInfo.FileName = TestEnvironment.CMD_EXE_NAME; 228 | p.StartInfo.UseShellExecute = false; 229 | p.StartInfo.CreateNoWindow = true; 230 | p.Start(); 231 | using (ChildProcess childProc = new ChildProcess(p.Id)) { } 232 | 233 | Assert.IsFalse(p.HasExited); 234 | } 235 | finally 236 | { 237 | p.Kill(); 238 | } 239 | } 240 | } 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /Cbonnell.DotNetExpect.Test/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Cbonnell.DotNetExpect.Test")] 9 | [assembly: AssemblyDescription("Tests for Cbonnell.DotNetExpect")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Cbonnell")] 12 | [assembly: AssemblyProduct("Cbonnell.DotNetExpect.Test")] 13 | [assembly: AssemblyCopyright("Copyright © Corey Bonnell 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("0dc8eed2-c1eb-480e-93f0-e9fa807f4cad")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /Cbonnell.DotNetExpect.Test/TestEnvironment.cs: -------------------------------------------------------------------------------- 1 | /* 2 | DotNetExpect 3 | Copyright (c) Corey Bonnell, All rights reserved. 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 3.0 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library. */ 17 | 18 | using System.Text.RegularExpressions; 19 | 20 | namespace Cbonnell.DotNetExpect.Test 21 | { 22 | internal static class TestEnvironment 23 | { 24 | public const string CMD_EXE_NAME = "cmd.exe"; 25 | public const string DUMMY_EXE_NAME = "myprocess.exe"; 26 | public const string DUMMY_ARGUMENTS = "foo hoge"; 27 | 28 | public const string PROXY_PROCESS_NAME = "powershell"; 29 | 30 | public const char PROMPT_CHAR = '>'; 31 | 32 | public static readonly Regex CMD_PROMPT_REGEX = new Regex(TestEnvironment.PROMPT_CHAR.ToString()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Cbonnell.DotNetExpect.Test/TestUtilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace Cbonnell.DotNetExpect.Test 8 | { 9 | internal static class TestUtilities 10 | { 11 | public const int PROCESS_EXIT_TIMEOUT_MS = 10 * 1000; 12 | 13 | // Wait for any proxies to exit before running each test 14 | public static void EnsureProxyExit() 15 | { 16 | Process[] powerShells = Process.GetProcessesByName(TestEnvironment.PROXY_PROCESS_NAME); 17 | try 18 | { 19 | foreach (Process p in powerShells) 20 | { 21 | TestUtilities.WaitForProcessExitAndThrow(p); 22 | } 23 | } 24 | finally 25 | { 26 | Array.ForEach(powerShells, (p) => p.Dispose()); 27 | } 28 | } 29 | 30 | public static void WaitForProcessExitAndThrow(Process p) 31 | { 32 | if (!p.WaitForExit(TestUtilities.PROCESS_EXIT_TIMEOUT_MS)) 33 | { 34 | throw new TimeoutException(); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Cbonnell.DotNetExpect/Cbonnell.DotNetExpect.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {0B25D3A5-EB12-42B6-887D-193BC0770D88} 8 | Library 9 | Properties 10 | Cbonnell.DotNetExpect 11 | Cbonnell.DotNetExpect 12 | v3.5 13 | 512 14 | Client 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | bin\Debug\Cbonnell.DotNetExpect.XML 25 | 26 | 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | bin\Release\Cbonnell.DotNetExpect.XML 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 61 | -------------------------------------------------------------------------------- /Cbonnell.DotNetExpect/Cbonnell.DotNetExpect.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $id$ 5 | $version$ 6 | $title$ 7 | $author$ 8 | $author$ 9 | https://raw.githubusercontent.com/CBonnell/dotnetexpect/master/LICENSE 10 | https://github.com/cbonnell/dotnetexpect 11 | false 12 | $description$ 13 | Copyright 2015 $author$ 14 | Expect Console Automation 15 | 16 | 17 | -------------------------------------------------------------------------------- /Cbonnell.DotNetExpect/Cbonnell.DotNetExpect.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.30501.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cbonnell.DotNetExpect", "Cbonnell.DotNetExpect.csproj", "{0B25D3A5-EB12-42B6-887D-193BC0770D88}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cbonnell.DotNetExpect.Test", "..\Cbonnell.DotNetExpect.Test\Cbonnell.DotNetExpect.Test.csproj", "{71ED942A-A513-4B8B-8381-2050A7636652}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {0B25D3A5-EB12-42B6-887D-193BC0770D88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {0B25D3A5-EB12-42B6-887D-193BC0770D88}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {0B25D3A5-EB12-42B6-887D-193BC0770D88}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {0B25D3A5-EB12-42B6-887D-193BC0770D88}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {71ED942A-A513-4B8B-8381-2050A7636652}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {71ED942A-A513-4B8B-8381-2050A7636652}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {71ED942A-A513-4B8B-8381-2050A7636652}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {71ED942A-A513-4B8B-8381-2050A7636652}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /Cbonnell.DotNetExpect/ChildProcess.cs: -------------------------------------------------------------------------------- 1 | /* 2 | DotNetExpect 3 | Copyright (c) Corey Bonnell, All rights reserved. 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 3.0 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library. */ 17 | 18 | using System; 19 | using System.Collections.Generic; 20 | using System.Linq; 21 | using System.Text; 22 | using System.Text.RegularExpressions; 23 | 24 | namespace Cbonnell.DotNetExpect 25 | { 26 | /// 27 | /// Represents a child process whose console input and output can be accessed. 28 | /// 29 | public class ChildProcess : IDisposable 30 | { 31 | private ProxyProcessManager proxy = new ProxyProcessManager(); 32 | 33 | /// 34 | /// Creates a new instance of . 35 | /// 36 | /// The path to the child process. The semantics in terms of how relative paths are handled are the same as with set to false. 37 | public ChildProcess(string filePath) : this(filePath, String.Empty) { } 38 | 39 | /// 40 | /// Creates a new instance of . 41 | /// 42 | /// The path to the child process. The semantics in terms of how relative paths are handled are the same as with set to false. 43 | /// The to use when accessing the console input and output of the child process. 44 | public ChildProcess(string filePath, ChildProcessOptions options) : this(filePath, String.Empty, options) { } 45 | 46 | /// 47 | /// Creates a new instance of . 48 | /// 49 | /// The path to the child process. The semantics in terms of how relative paths are handled are the same as with set to false. 50 | /// The command line arguments for the child process. 51 | public ChildProcess(string filePath, string arguments) : this(filePath, arguments, Environment.CurrentDirectory) { } 52 | 53 | /// 54 | /// Creates a new instance of . 55 | /// 56 | /// The path to the child process. The semantics in terms of how relative paths are handled are the same as with set to false. 57 | /// The command line arguments for the child process. 58 | /// The to use when accessing the console input and output of the child process. 59 | public ChildProcess(string filePath, string arguments, ChildProcessOptions options) : this(filePath, arguments, Environment.CurrentDirectory, options) { } 60 | 61 | /// 62 | /// Creates a new instance of . 63 | /// 64 | /// The path to the child process. The semantics in terms of how relative paths are handled are the same as with set to false. 65 | /// The command line arguments for the child process. 66 | /// The working directory for the child process. 67 | public ChildProcess(string filePath, string arguments, string workingDirectory) : this(filePath, arguments, workingDirectory, new ChildProcessOptions()) { } 68 | 69 | /// 70 | /// Creates a new instance of . 71 | /// 72 | /// The path to the child process. The semantics in terms of how relative paths are handled are the same as with set to false. 73 | /// The command line arguments for the child process. 74 | /// The working directory for the child process. 75 | /// The to use when accessing the console input and output of the child process. 76 | public ChildProcess(string filePath, string arguments, string workingDirectory, ChildProcessOptions options) 77 | { 78 | if (filePath == null) 79 | { 80 | throw new ArgumentNullException("filePath"); 81 | } 82 | if (arguments == null) 83 | { 84 | throw new ArgumentNullException("arguments"); 85 | } 86 | if (workingDirectory == null) 87 | { 88 | throw new ArgumentNullException("workingDirectory"); 89 | } 90 | if (options == null) 91 | { 92 | throw new ArgumentNullException("options"); 93 | } 94 | 95 | this.Options = options; 96 | 97 | try 98 | { 99 | this.proxy.Start(); 100 | this.proxy.CommandPipeWriter.Write((byte)ProxyCommand.StartProcess); 101 | this.proxy.CommandPipeWriter.Write(filePath); 102 | this.proxy.CommandPipeWriter.Write(arguments); 103 | this.proxy.CommandPipeWriter.Write(workingDirectory); 104 | this.proxy.CommandPipeWriter.Flush(); 105 | 106 | this.readResponseAndThrow(); 107 | } 108 | catch (Exception) 109 | { 110 | this.Dispose(); 111 | throw; 112 | } 113 | } 114 | 115 | /// 116 | /// Creates a new instance of . 117 | /// 118 | /// The process ID of the console process. 119 | /// This constructor is used to manipulate console I/O for a process that is already running. 120 | public ChildProcess(int pid) : this(pid, new ChildProcessOptions()) { } 121 | 122 | /// 123 | /// Creates a new instance of . 124 | /// 125 | /// The process ID of the console process. 126 | /// The to use when accessing the console input and output of the child process. 127 | /// This constructor is used to manipulate console I/O for a process that is already running. 128 | public ChildProcess(int pid, ChildProcessOptions options) 129 | { 130 | if (options == null) 131 | { 132 | throw new ArgumentNullException("options"); 133 | } 134 | 135 | this.Options = options; 136 | 137 | try 138 | { 139 | this.proxy.Start(); 140 | this.proxy.CommandPipeWriter.Write((byte)ProxyCommand.SetPid); 141 | this.proxy.CommandPipeWriter.Write(pid); 142 | this.proxy.CommandPipeWriter.Flush(); 143 | 144 | this.readResponseAndThrow(); 145 | } 146 | catch (Exception) 147 | { 148 | this.Dispose(); 149 | throw; 150 | } 151 | } 152 | 153 | /// 154 | /// Reads the child process's console output (screen buffer) until the content of the screen output matches the specified . 155 | /// 156 | /// The regular expression to match the console screen output against. 157 | /// A that was returned after matching the console output with the specified . 158 | public Match Match(Regex regex) 159 | { 160 | if (regex == null) 161 | { 162 | throw new ArgumentNullException("regex"); 163 | } 164 | 165 | this.checkDisposedAndThrow(); 166 | 167 | return this.readLoopWithTimeout((s) => regex.Match(s), (m) => m.Success, this.Options.TimeoutMilliseconds); 168 | } 169 | 170 | /// 171 | /// Reads the child process's console output (screen buffer) until the content of the screen output contains the expected data. 172 | /// 173 | /// The string for which to search. 174 | /// The console output. 175 | public string Read(string expectedData) 176 | { 177 | if (expectedData == null) 178 | { 179 | throw new ArgumentNullException("expectedData"); 180 | } 181 | 182 | this.checkDisposedAndThrow(); 183 | 184 | return this.readLoopWithTimeout((s) => s, (s) => s.Contains(expectedData), this.Options.TimeoutMilliseconds); 185 | } 186 | 187 | /// 188 | /// Writes the specified string to the child process's console. 189 | /// 190 | /// The string data to write to the child process's console. 191 | public void Write(string data) 192 | { 193 | if (data == null) 194 | { 195 | throw new ArgumentNullException("data"); 196 | } 197 | 198 | this.checkDisposedAndThrow(); 199 | 200 | if (this.Options.AttachConsole) 201 | { 202 | this.attachConsole(); 203 | } 204 | 205 | this.proxy.CommandPipeWriter.Write((byte)ProxyCommand.WriteConsole); 206 | this.proxy.CommandPipeWriter.Write(data); 207 | this.proxy.CommandPipeWriter.Flush(); 208 | this.readResponseAndThrow(); 209 | } 210 | 211 | /// 212 | /// Writes the specified string to the child process's console, first appending the value of to the string. 213 | /// 214 | /// The string data to write to the child process's console. 215 | public void WriteLine(string data) 216 | { 217 | if (data == null) 218 | { 219 | throw new ArgumentNullException("data"); 220 | } 221 | 222 | data += this.Options.NewLine; 223 | 224 | this.Write(data); 225 | } 226 | 227 | /// 228 | /// Kills the child process. 229 | /// 230 | /// The exit code of the killed child process. 231 | /// After this method is called, this object is disposed no further operations can be performed on this object. 232 | public int Kill() 233 | { 234 | this.checkDisposedAndThrow(); 235 | 236 | this.proxy.CommandPipeWriter.Write((byte)ProxyCommand.KillProcess); 237 | this.proxy.CommandPipeWriter.Flush(); 238 | this.readResponseAndThrow(); 239 | int exitCode = this.proxy.CommandPipeReader.ReadInt32(); // read the exit code 240 | 241 | this.Dispose(); 242 | 243 | return exitCode; 244 | } 245 | 246 | /// 247 | /// Clears the child process's console output. 248 | /// 249 | public void ClearConsole() 250 | { 251 | this.checkDisposedAndThrow(); 252 | 253 | this.proxy.CommandPipeWriter.Write((byte)ProxyCommand.ClearConsole); 254 | this.proxy.CommandPipeWriter.Flush(); 255 | this.readResponseAndThrow(); 256 | } 257 | 258 | /// 259 | /// Retrieves the process ID of the spawned child process. 260 | /// 261 | public int ChildProcessId 262 | { 263 | get 264 | { 265 | this.checkDisposedAndThrow(); 266 | 267 | this.proxy.CommandPipeWriter.Write((byte)ProxyCommand.GetPid); 268 | this.proxy.CommandPipeWriter.Flush(); 269 | this.readResponseAndThrow(); 270 | 271 | return this.proxy.CommandPipeReader.ReadInt32(); 272 | } 273 | } 274 | 275 | /// 276 | /// Retrieves the exit code of a child process that has exited (stopped executing). 277 | /// 278 | public int ChildExitCode 279 | { 280 | get 281 | { 282 | this.checkDisposedAndThrow(); 283 | 284 | this.proxy.CommandPipeWriter.Write((byte)ProxyCommand.GetProcessExitCode); 285 | this.proxy.CommandPipeWriter.Flush(); 286 | this.readResponseAndThrow(); 287 | 288 | return this.proxy.CommandPipeReader.ReadInt32(); 289 | } 290 | } 291 | 292 | /// 293 | /// Retrieves whether or not a child process has exited (stopped executing). 294 | /// 295 | public bool HasChildExited 296 | { 297 | get 298 | { 299 | this.checkDisposedAndThrow(); 300 | 301 | this.proxy.CommandPipeWriter.Write((byte)ProxyCommand.GetHasProcessExited); 302 | this.proxy.CommandPipeWriter.Flush(); 303 | this.readResponseAndThrow(); 304 | 305 | return this.proxy.CommandPipeReader.ReadBoolean(); 306 | } 307 | } 308 | 309 | /// 310 | /// Retrieves the used by the current instance of . 311 | /// 312 | public ChildProcessOptions Options 313 | { 314 | get; 315 | private set; 316 | } 317 | 318 | /// 319 | /// Disposes the current instance of . 320 | /// 321 | public void Dispose() 322 | { 323 | if (this.proxy != null) 324 | { 325 | this.proxy.Dispose(); 326 | this.proxy = null; 327 | } 328 | } 329 | 330 | private void checkDisposedAndThrow() 331 | { 332 | if (this.proxy == null) 333 | { 334 | throw new ObjectDisposedException(this.GetType().FullName); 335 | } 336 | } 337 | 338 | private TReturn readLoopWithTimeout(Converter string2TypeConverter, Predicate isCompleteDelegate, int timeoutMilliseconds) 339 | { 340 | TimeSpan timeoutSpan = timeoutMilliseconds >= 0 ? TimeSpan.FromMilliseconds(timeoutMilliseconds) : default(TimeSpan); 341 | 342 | DateTime startTime = DateTime.Now; 343 | TReturn returnValue = default(TReturn); 344 | while (timeoutMilliseconds < 0 || DateTime.Now < startTime + timeoutSpan) 345 | { 346 | string output = this.readConsoleOutput(); 347 | returnValue = string2TypeConverter.Invoke(output); 348 | if (isCompleteDelegate.Invoke(returnValue)) 349 | { 350 | break; 351 | } 352 | 353 | // if we're here, then default the return value and loop back because it doesn't satisfy our condition 354 | returnValue = default(TReturn); 355 | } 356 | 357 | // if we're here then we've either satisified the condition or we timed out 358 | // compare the return value to the default for the return type to determine if we timed out or not 359 | if (Object.Equals(returnValue, default(TReturn))) 360 | { 361 | throw new TimeoutException(); 362 | } 363 | 364 | if (this.Options.ClearConsole) 365 | { 366 | this.ClearConsole(); 367 | } 368 | 369 | return returnValue; 370 | } 371 | 372 | private string readConsoleOutput() 373 | { 374 | if (this.Options.AttachConsole) 375 | { 376 | this.attachConsole(); 377 | } 378 | 379 | this.proxy.CommandPipeWriter.Write((byte)ProxyCommand.ReadConsole); 380 | this.proxy.CommandPipeWriter.Flush(); 381 | this.readResponseAndThrow(); 382 | 383 | return this.proxy.CommandPipeReader.ReadString(); 384 | } 385 | 386 | private void attachConsole() 387 | { 388 | this.proxy.CommandPipeWriter.Write((byte)ProxyCommand.AttachConsole); 389 | this.proxy.CommandPipeWriter.Write(this.Options.AttachConsoleTimeoutMilliseconds); 390 | this.proxy.CommandPipeWriter.Flush(); 391 | this.readResponseAndThrow(); 392 | } 393 | 394 | private void readResponseAndThrow() 395 | { 396 | CommandResult result = (CommandResult)this.proxy.CommandPipeReader.ReadByte(); 397 | if (result != CommandResult.Success) 398 | { 399 | throw new OperationFailedException(result); 400 | } 401 | } 402 | } 403 | } 404 | -------------------------------------------------------------------------------- /Cbonnell.DotNetExpect/ChildProcessOptions.cs: -------------------------------------------------------------------------------- 1 | /* 2 | DotNetExpect 3 | Copyright (c) Corey Bonnell, All rights reserved. 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 3.0 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library. */ 17 | 18 | using System; 19 | 20 | namespace Cbonnell.DotNetExpect 21 | { 22 | /// 23 | /// Contains the various options for interacting with a . 24 | /// 25 | public class ChildProcessOptions 26 | { 27 | private const int MSEC_PER_SEC = 1000; 28 | 29 | /// 30 | /// Instantiates a new instance of . 31 | /// 32 | public ChildProcessOptions() 33 | { 34 | this.AttachConsole = true; 35 | this.NewLine = Environment.NewLine; 36 | this.AttachConsoleTimeoutMilliseconds = 10 * ChildProcessOptions.MSEC_PER_SEC; 37 | this.TimeoutMilliseconds = 60 * ChildProcessOptions.MSEC_PER_SEC; 38 | this.ClearConsole = true; 39 | } 40 | 41 | /// 42 | /// Whether or not to attach to the child process console on each input/output operation. This is sometimes required, as many command line applications create their own console. 43 | /// 44 | /// The default value is true. 45 | public bool AttachConsole 46 | { 47 | get; 48 | set; 49 | } 50 | 51 | private string newLine; 52 | /// 53 | /// The string to append to each specified string when calling . This is generally useful so that one does not need to manually add newline/carriage return characters to each string to be written. 54 | /// 55 | /// The default value is . 56 | public string NewLine 57 | { 58 | get 59 | { 60 | return this.newLine; 61 | } 62 | set 63 | { 64 | if (value == null) 65 | { 66 | throw new ArgumentNullException("value"); 67 | } 68 | this.newLine = value; 69 | } 70 | } 71 | 72 | private int attachConsoleTimeoutMilliseconds; 73 | /// 74 | /// The amount of time to attempt to attach to a child process's console. This value may need to be adjusted if the child process takes a considerable amount of time to initialize before allocating its own console. 75 | /// 76 | /// The default value is 10,000 millseconds (10 seconds). 77 | public int AttachConsoleTimeoutMilliseconds 78 | { 79 | get 80 | { 81 | return this.attachConsoleTimeoutMilliseconds; 82 | } 83 | set 84 | { 85 | if (value < 0) 86 | { 87 | throw new ArgumentOutOfRangeException("value"); 88 | } 89 | this.attachConsoleTimeoutMilliseconds = value; 90 | } 91 | } 92 | 93 | private int timeoutMilliseconds; 94 | /// 95 | /// The amount of time to wait for matching input when calling or before a is thrown. 96 | /// 97 | /// The default value is 60,000 milliseconds (60 seconds, or 1 minute). A negative value denotes that there is no timeout (will wait forever for matching input). 98 | public int TimeoutMilliseconds 99 | { 100 | get 101 | { 102 | return this.timeoutMilliseconds; 103 | } 104 | set 105 | { 106 | this.timeoutMilliseconds = value; 107 | } 108 | } 109 | 110 | /// 111 | /// Whether or not to clear the console of all content after successfully reading console content. This is useful so that previously read content is not returned on subsequent Reads. 112 | /// 113 | /// The default value is true. 114 | public bool ClearConsole 115 | { 116 | get; 117 | set; 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Cbonnell.DotNetExpect/CommandResult.cs: -------------------------------------------------------------------------------- 1 | /* 2 | DotNetExpect 3 | Copyright (c) Corey Bonnell, All rights reserved. 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 3.0 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library. */ 17 | 18 | 19 | namespace Cbonnell.DotNetExpect 20 | { 21 | /// 22 | /// Represents the possible results that can be returned from a command invoked on a child process. 23 | /// 24 | public enum CommandResult 25 | { 26 | /// 27 | /// The command succeeded. 28 | /// 29 | Success = 0, 30 | /// 31 | /// A non-specific error occurred. 32 | /// 33 | GeneralFailure, 34 | /// 35 | /// An error occurred when attempting to spawn the child process. 36 | /// 37 | /// Generally this error is returned when the specified application does not exist. 38 | CouldNotSpawnChild, 39 | /// 40 | /// The the child process was already spawned for the current instance of . 41 | /// 42 | ChildAlreadySpawned, 43 | /// 44 | /// A call to retrieve the child process's exit code was attempted while the child process was still executing. 45 | /// 46 | ChildHasNotExited, 47 | /// 48 | /// The child process has not yet been spawned for the current instance . 49 | /// 50 | ChildNotSpawned, 51 | /// 52 | /// A process with the specified PID does not exist. 53 | /// 54 | ProcessDoesNotExist, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Cbonnell.DotNetExpect/ConsoleInterface.cs: -------------------------------------------------------------------------------- 1 | /* 2 | DotNetExpect 3 | Copyright (c) Corey Bonnell, All rights reserved. 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 3.0 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library. */ 17 | 18 | using Microsoft.Win32.SafeHandles; 19 | using System; 20 | using System.ComponentModel; 21 | using System.Runtime.InteropServices; 22 | using System.Text; 23 | 24 | namespace Cbonnell.DotNetExpect 25 | { 26 | internal static class ConsoleInterface 27 | { 28 | private const char SPACE_CHAR = ' '; 29 | 30 | #region Public Interface 31 | 32 | public static string ReadConsole() 33 | { 34 | char[,] output = ConsoleInterface.ReadConsoleRaw(); 35 | StringBuilder buffer = new StringBuilder(); 36 | 37 | for (int y = 0; y < output.GetLength(1); y++) 38 | { 39 | char[] lineChars = new char[output.GetLength(0)]; 40 | for (int i = 0; i < lineChars.Length; i++) 41 | { 42 | lineChars[i] = output[i, y]; 43 | } 44 | buffer.AppendLine(new String(lineChars).TrimEnd()); 45 | } 46 | 47 | return buffer.ToString().TrimEnd(); 48 | } 49 | 50 | public static char[,] ReadConsoleRaw() 51 | { 52 | NativeInterface.CONSOLE_SCREEN_BUFFER_INFO screenBufferInfo = ConsoleInterface.getScreenBufferInfo(); 53 | 54 | NativeInterface.SMALL_RECT readRegion = default(NativeInterface.SMALL_RECT); 55 | readRegion.Right = screenBufferInfo.dwSize.X; 56 | readRegion.Bottom = (short)(screenBufferInfo.dwCursorPosition.Y + 1); // read all lines from the top of the screen buffer to the current cursor position (inclusive) 57 | 58 | NativeInterface.CHAR_INFO[] charInfos = new NativeInterface.CHAR_INFO[screenBufferInfo.dwSize.X * screenBufferInfo.dwSize.Y]; 59 | using (SafeHandle hConsoleOutput = ConsoleInterface.getConsoleOutputHandle()) 60 | { 61 | ConsoleInterface.callWin32Func(() => NativeInterface.ReadConsoleOutput(hConsoleOutput, charInfos, screenBufferInfo.dwSize, default(NativeInterface.COORD), ref readRegion)); 62 | } 63 | 64 | char[,] output = new char[screenBufferInfo.dwSize.X, readRegion.Bottom]; 65 | for (int y = 0; y < readRegion.Bottom; y++) 66 | { 67 | for (int x = 0; x < screenBufferInfo.dwSize.X; x++) 68 | { 69 | output[x, y] = charInfos[y * screenBufferInfo.dwSize.X + x].UnicodeChar; 70 | } 71 | } 72 | 73 | return output; 74 | } 75 | 76 | public static void WriteConsole(string data) 77 | { 78 | using (SafeHandle hConsoleInput = ConsoleInterface.getConsoleInputHandle()) 79 | { 80 | // we need 2x the number of characters because we need to simulate key press and key release events for each character 81 | NativeInterface.INPUT_RECORD[] inputRecords = new NativeInterface.INPUT_RECORD[data.Length * 2]; 82 | for (int i = 0; i < data.Length; i++) 83 | { 84 | NativeInterface.KEY_EVENT_RECORD keyPressEvent = default(NativeInterface.KEY_EVENT_RECORD); 85 | keyPressEvent.bKeyDown = true; 86 | keyPressEvent.wRepeatCount = 1; 87 | keyPressEvent.UnicodeChar = data[i]; 88 | 89 | NativeInterface.KEY_EVENT_RECORD keyReleaseEvent = keyPressEvent; // same values for all fields as the key press event, but with the bKeyDown field as "false" 90 | keyReleaseEvent.bKeyDown = false; 91 | 92 | inputRecords[i * 2].EventType = NativeInterface.KEY_EVENT; 93 | inputRecords[i * 2].Event = keyPressEvent; 94 | inputRecords[i * 2 + 1].EventType = NativeInterface.KEY_EVENT; 95 | inputRecords[i * 2 + 1].Event = keyReleaseEvent; 96 | } 97 | 98 | int recordsWritten; 99 | ConsoleInterface.callWin32Func(() => NativeInterface.WriteConsoleInput(hConsoleInput, inputRecords, inputRecords.Length, out recordsWritten)); 100 | } 101 | } 102 | 103 | public static void AttachToConsole(int processId, TimeSpan timeout) 104 | { 105 | ConsoleInterface.callWin32Func(() => NativeInterface.FreeConsole()); 106 | DateTime startTime = DateTime.Now; 107 | do 108 | { 109 | if (NativeInterface.AttachConsole(processId)) 110 | { 111 | return; 112 | } 113 | } while (startTime + timeout > DateTime.Now); 114 | throw new TimeoutException(); 115 | } 116 | 117 | public static void ClearConsole() 118 | { 119 | using (SafeHandle hConsoleOutput = ConsoleInterface.getConsoleOutputHandle()) 120 | { 121 | // get dimensions of the screen buffer 122 | NativeInterface.CONSOLE_SCREEN_BUFFER_INFO screenBufferInfo = ConsoleInterface.getScreenBufferInfo(); 123 | 124 | int charsToWrite = screenBufferInfo.dwSize.X * screenBufferInfo.dwSize.Y; 125 | int charsWritten; 126 | ConsoleInterface.callWin32Func(() => NativeInterface.FillConsoleOutputCharacter(hConsoleOutput, ConsoleInterface.SPACE_CHAR, charsToWrite, default(NativeInterface.COORD), out charsWritten)); 127 | ConsoleInterface.callWin32Func(() => NativeInterface.SetConsoleCursorPosition(hConsoleOutput, default(NativeInterface.COORD))); 128 | } 129 | } 130 | 131 | #endregion Public Interface 132 | 133 | #region Helper Methods 134 | 135 | private static void callWin32Func(Func func) 136 | { 137 | if (!func.Invoke()) 138 | { 139 | throw new Win32Exception(); 140 | } 141 | } 142 | 143 | private static SafeHandle getConsoleHandle(int nStdHandle) 144 | { 145 | IntPtr hConsole = NativeInterface.GetStdHandle(nStdHandle); 146 | SafeHandle handle = new ConsoleHandle(hConsole, true); 147 | if (handle.IsInvalid) 148 | { 149 | throw new Win32Exception(); 150 | } 151 | 152 | return handle; 153 | } 154 | 155 | private static SafeHandle getConsoleInputHandle() 156 | { 157 | return ConsoleInterface.getConsoleHandle(NativeInterface.STD_INPUT_HANDLE); 158 | } 159 | 160 | private static SafeHandle getConsoleOutputHandle() 161 | { 162 | return ConsoleInterface.getConsoleHandle(NativeInterface.STD_OUTPUT_HANDLE); 163 | } 164 | 165 | private static NativeInterface.CONSOLE_SCREEN_BUFFER_INFO getScreenBufferInfo() 166 | { 167 | using (SafeHandle hConsoleOutput = ConsoleInterface.getConsoleOutputHandle()) 168 | { 169 | NativeInterface.CONSOLE_SCREEN_BUFFER_INFO screenBufferInfo = default(NativeInterface.CONSOLE_SCREEN_BUFFER_INFO); 170 | ConsoleInterface.callWin32Func(() => NativeInterface.GetConsoleScreenBufferInfo(hConsoleOutput, out screenBufferInfo)); 171 | return screenBufferInfo; 172 | } 173 | } 174 | 175 | #endregion Helper Methods 176 | 177 | private class ConsoleHandle : SafeHandleMinusOneIsInvalid 178 | { 179 | public ConsoleHandle(IntPtr hConsole, bool ownsHandle) 180 | : base(ownsHandle) 181 | { 182 | this.SetHandle(hConsole); 183 | } 184 | 185 | protected override bool ReleaseHandle() 186 | { 187 | return true; // don't close console handles as that will be done for us at process termination time 188 | } 189 | } 190 | 191 | private static class NativeInterface 192 | { 193 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] 194 | public struct CHAR_INFO 195 | { 196 | public char UnicodeChar; 197 | public short Attributes; 198 | } 199 | 200 | [StructLayout(LayoutKind.Sequential)] 201 | public struct SMALL_RECT 202 | { 203 | public short Left; 204 | public short Top; 205 | public short Right; 206 | public short Bottom; 207 | } 208 | 209 | [StructLayout(LayoutKind.Sequential)] 210 | public struct COORD 211 | { 212 | public short X; 213 | public short Y; 214 | } 215 | 216 | [StructLayout(LayoutKind.Sequential)] 217 | public struct INPUT_RECORD 218 | { 219 | public short EventType; 220 | public KEY_EVENT_RECORD Event; 221 | } 222 | 223 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] 224 | public struct KEY_EVENT_RECORD 225 | { 226 | public bool bKeyDown; 227 | public short wRepeatCount; 228 | public short wVirtualKeyCode; 229 | public short wVirtualScanCode; 230 | public char UnicodeChar; 231 | public int dwControlKeyState; 232 | } 233 | 234 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] 235 | public struct CONSOLE_SCREEN_BUFFER_INFO 236 | { 237 | public NativeInterface.COORD dwSize; 238 | public NativeInterface.COORD dwCursorPosition; 239 | public short wAttributes; 240 | public NativeInterface.SMALL_RECT srWindow; 241 | public NativeInterface.COORD dwMaximumWindowSize; 242 | } 243 | 244 | [DllImport("kernel32", SetLastError = true)] 245 | public static extern bool SetConsoleMode(SafeHandle hConsoleHandle, int dwMode); 246 | 247 | [DllImport("kernel32", SetLastError = true)] 248 | public static extern bool GetConsoleMode(SafeHandle hConsoleHandle, out int dwMode); 249 | 250 | [DllImport("kernel32", SetLastError = true)] 251 | public static extern bool ReadConsoleOutput(SafeHandle hConsoleHandle, [Out] CHAR_INFO[] lpBuffer, COORD dwBufferSize, COORD dwBufferCoord, ref SMALL_RECT lpReadRegion); 252 | 253 | [DllImport("kernel32", SetLastError = true)] 254 | public static extern IntPtr GetStdHandle(int nStdHandle); 255 | 256 | [DllImport("kernel32", SetLastError = true)] 257 | public static extern bool FreeConsole(); 258 | 259 | [DllImport("kernel32", SetLastError = true)] 260 | public static extern bool AttachConsole(int dwProcessId); 261 | 262 | [DllImport("kernel32", SetLastError = true)] 263 | public static extern int GetConsoleProcessList([Out] int[] lpdwProcessList, int dwProcessCount); 264 | 265 | [DllImport("kernel32", SetLastError = true)] 266 | public static extern bool WriteConsoleInput(SafeHandle hConsoleInput, [In] INPUT_RECORD[] lpBuffer, int nLength, out int lpNumberOfEventsWritten); 267 | 268 | [DllImport("kernel32", CharSet = CharSet.Unicode, SetLastError = true)] 269 | public static extern bool FillConsoleOutputCharacter(SafeHandle hConsoleOutput, char cCharacter, int nLength, NativeInterface.COORD dwWriteCoord, out int lpNumberOfCharsWritten); 270 | 271 | [DllImport("kernel32", SetLastError = true)] 272 | public static extern bool GetConsoleScreenBufferInfo(SafeHandle hConsoleOutput, out NativeInterface.CONSOLE_SCREEN_BUFFER_INFO result); 273 | 274 | [DllImport("kernel32", SetLastError = true)] 275 | public static extern bool SetConsoleCursorPosition(SafeHandle hConsoleOutput, NativeInterface.COORD dwCursorPosition); 276 | 277 | public const int ENABLE_ECHO_INPUT = 0x4; 278 | 279 | public const int STD_INPUT_HANDLE = -10; 280 | public const int STD_OUTPUT_HANDLE = -11; 281 | 282 | public const int KEY_EVENT = 0x1; 283 | 284 | } 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /Cbonnell.DotNetExpect/OperationFailedException.cs: -------------------------------------------------------------------------------- 1 | /* 2 | DotNetExpect 3 | Copyright (c) Corey Bonnell, All rights reserved. 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 3.0 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library. */ 17 | 18 | using System; 19 | 20 | namespace Cbonnell.DotNetExpect 21 | { 22 | /// 23 | /// Represents an error that occurred when accessing a child process's console input or output. 24 | /// 25 | public class OperationFailedException : Exception 26 | { 27 | private const string MESSAGE_FMT = "An operation failed with the following error: {0}"; 28 | 29 | internal OperationFailedException(CommandResult reason) 30 | : base(String.Format(OperationFailedException.MESSAGE_FMT, reason)) 31 | { 32 | this.Reason = reason; 33 | } 34 | 35 | /// 36 | /// Retrieves the reason for the error. 37 | /// 38 | public CommandResult Reason 39 | { 40 | get; 41 | private set; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Cbonnell.DotNetExpect/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | /* 2 | DotNetExpect 3 | Copyright (c) Corey Bonnell, All rights reserved. 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 3.0 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library. */ 17 | 18 | using System.Reflection; 19 | using System.Runtime.InteropServices; 20 | 21 | // General Information about an assembly is controlled through the following 22 | // set of attributes. Change these attribute values to modify the information 23 | // associated with an assembly. 24 | [assembly: AssemblyTitle("Cbonnell.DotNetExpect")] 25 | [assembly: AssemblyDescription("An Expect-inspired .NET library for accessing console I/O of processes.")] 26 | [assembly: AssemblyConfiguration("")] 27 | [assembly: AssemblyCompany("Cbonnell")] 28 | [assembly: AssemblyProduct("Cbonnell.DotNetExpect")] 29 | [assembly: AssemblyCopyright("Copyright © Corey Bonnell 2015")] 30 | [assembly: AssemblyTrademark("")] 31 | [assembly: AssemblyCulture("")] 32 | 33 | // Setting ComVisible to false makes the types in this assembly not visible 34 | // to COM components. If you need to access a type in this assembly from 35 | // COM, set the ComVisible attribute to true on that type. 36 | [assembly: ComVisible(false)] 37 | 38 | // The following GUID is for the ID of the typelib if this project is exposed to COM 39 | [assembly: Guid("f71b6a09-e6ea-4d51-90df-25c9709d2cdd")] 40 | 41 | // Version information for an assembly consists of the following four values: 42 | // 43 | // Major Version 44 | // Minor Version 45 | // Build Number 46 | // Revision 47 | // 48 | // You can specify all the values or you can default the Build and Revision Numbers 49 | // by using the '*' as shown below: 50 | // [assembly: AssemblyVersion("1.0.*")] 51 | [assembly: AssemblyVersion("1.0.0.0")] 52 | [assembly: AssemblyFileVersion("1.0.0.0")] 53 | -------------------------------------------------------------------------------- /Cbonnell.DotNetExpect/ProxyProcess.cs: -------------------------------------------------------------------------------- 1 | /* 2 | DotNetExpect 3 | Copyright (c) Corey Bonnell, All rights reserved. 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 3.0 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library. */ 17 | 18 | using System; 19 | using System.Diagnostics; 20 | using System.IO; 21 | using System.IO.Pipes; 22 | 23 | namespace Cbonnell.DotNetExpect 24 | { 25 | internal enum ProxyCommand 26 | { 27 | StartProcess = 0, 28 | SetPid, 29 | KillProcess, 30 | ClearConsole, 31 | ReadConsole, 32 | WriteConsole, 33 | AttachConsole, 34 | GetPid, 35 | GetProcessExitCode, 36 | GetHasProcessExited, 37 | } 38 | 39 | internal class ProxyProcess 40 | { 41 | private readonly NamedPipeClientStream commandPipe; 42 | private readonly BinaryReader commandReader; 43 | private readonly BinaryWriter commandWriter; 44 | private Process process = null; 45 | private bool spawnedProcess = false; 46 | 47 | public ProxyProcess(string commandPipeName) 48 | { 49 | this.commandPipe = new NamedPipeClientStream(commandPipeName); 50 | this.commandReader = new BinaryReader(this.commandPipe); 51 | this.commandWriter = new BinaryWriter(this.commandPipe); 52 | } 53 | 54 | public void Run() 55 | { 56 | this.commandPipe.Connect(); 57 | 58 | bool shouldContinue = true; 59 | try 60 | { 61 | while (shouldContinue) 62 | { 63 | int opcode = this.commandReader.ReadByte(); 64 | CommandResult result = CommandResult.GeneralFailure; 65 | bool shouldWriteResult = true; // individual opcodes can set this to false to return specialized data 66 | 67 | switch ((ProxyCommand)opcode) 68 | { 69 | case ProxyCommand.StartProcess: 70 | if (this.process == null) 71 | { 72 | this.process = new Process(); 73 | this.process.StartInfo.FileName = this.commandReader.ReadString(); 74 | this.process.StartInfo.Arguments = this.commandReader.ReadString(); 75 | this.process.StartInfo.WorkingDirectory = this.commandReader.ReadString(); 76 | this.process.StartInfo.UseShellExecute = false; 77 | this.process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; 78 | try 79 | { 80 | this.process.Start(); 81 | this.spawnedProcess = true; 82 | result = CommandResult.Success; 83 | } 84 | catch (Exception) 85 | { 86 | result = CommandResult.CouldNotSpawnChild; 87 | this.process.Dispose(); 88 | this.process = null; 89 | } 90 | } 91 | else 92 | { 93 | result = CommandResult.ChildAlreadySpawned; 94 | } 95 | break; 96 | case ProxyCommand.SetPid: 97 | if (this.process == null) 98 | { 99 | try 100 | { 101 | int pid = this.commandReader.ReadInt32(); 102 | this.process = Process.GetProcessById(pid); 103 | result = CommandResult.Success; 104 | } 105 | catch (Exception) 106 | { 107 | result = CommandResult.ProcessDoesNotExist; 108 | } 109 | } 110 | break; 111 | case ProxyCommand.KillProcess: 112 | if (this.process != null) 113 | { 114 | this.process.Kill(); 115 | this.process.WaitForExit(); 116 | 117 | this.commandWriter.Write((byte)CommandResult.Success); 118 | this.commandWriter.Write(this.process.ExitCode); 119 | 120 | this.process.Dispose(); 121 | this.process = null; 122 | 123 | shouldWriteResult = false; 124 | shouldContinue = false; 125 | } 126 | else 127 | { 128 | result = CommandResult.ChildNotSpawned; 129 | } 130 | break; 131 | case ProxyCommand.AttachConsole: 132 | int timeoutMs = this.commandReader.ReadInt32(); 133 | 134 | if (this.process != null) 135 | { 136 | ConsoleInterface.AttachToConsole(this.process.Id, TimeSpan.FromMilliseconds(timeoutMs)); 137 | result = CommandResult.Success; 138 | } 139 | else 140 | { 141 | result = CommandResult.ChildNotSpawned; 142 | } 143 | break; 144 | case ProxyCommand.ReadConsole: 145 | string outputData = ConsoleInterface.ReadConsole(); 146 | shouldWriteResult = false; 147 | this.commandWriter.Write((byte)CommandResult.Success); 148 | this.commandWriter.Write(outputData); 149 | break; 150 | case ProxyCommand.WriteConsole: 151 | string inputData = this.commandReader.ReadString(); 152 | ConsoleInterface.WriteConsole(inputData); 153 | result = CommandResult.Success; 154 | break; 155 | case ProxyCommand.ClearConsole: 156 | ConsoleInterface.ClearConsole(); 157 | result = CommandResult.Success; 158 | break; 159 | case ProxyCommand.GetHasProcessExited: 160 | if (this.process != null) 161 | { 162 | this.commandWriter.Write((byte)CommandResult.Success); 163 | this.commandWriter.Write(this.process.HasExited); 164 | shouldWriteResult = false; 165 | } 166 | else 167 | { 168 | result = CommandResult.ChildNotSpawned; 169 | } 170 | break; 171 | case ProxyCommand.GetPid: 172 | if (this.process != null) 173 | { 174 | this.commandWriter.Write((byte)CommandResult.Success); 175 | this.commandWriter.Write(this.process.Id); 176 | shouldWriteResult = false; 177 | } 178 | else 179 | { 180 | result = CommandResult.ChildNotSpawned; 181 | } 182 | break; 183 | case ProxyCommand.GetProcessExitCode: 184 | if (this.process != null) 185 | { 186 | if (this.process.HasExited) 187 | { 188 | this.commandWriter.Write((byte)CommandResult.Success); 189 | this.commandWriter.Write(this.process.ExitCode); 190 | shouldWriteResult = false; 191 | } 192 | else 193 | { 194 | result = CommandResult.ChildHasNotExited; 195 | } 196 | } 197 | else 198 | { 199 | result = CommandResult.ChildNotSpawned; 200 | } 201 | break; 202 | default: 203 | result = CommandResult.GeneralFailure; 204 | break; 205 | } 206 | 207 | if (shouldWriteResult) 208 | { 209 | this.commandWriter.Write((byte)result); 210 | } 211 | this.commandWriter.Flush(); 212 | } 213 | } 214 | finally // ensure that the child process is killed if we exit, but only if we spawned the process 215 | { 216 | if (this.process != null && this.spawnedProcess) 217 | { 218 | this.process.Kill(); 219 | } 220 | } 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /Cbonnell.DotNetExpect/ProxyProcessManager.cs: -------------------------------------------------------------------------------- 1 | /* 2 | DotNetExpect 3 | Copyright (c) Corey Bonnell, All rights reserved. 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 3.0 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library. */ 17 | 18 | using System; 19 | using System.ComponentModel; 20 | using System.Diagnostics; 21 | using System.IO; 22 | using System.IO.Pipes; 23 | using System.Reflection; 24 | using System.Runtime.InteropServices; 25 | 26 | namespace Cbonnell.DotNetExpect 27 | { 28 | internal class ProxyProcessManager : IDisposable 29 | { 30 | private const string PIPE_NAME_FMT = "Cbonnell.DotNetExpect.{0}"; 31 | private const string POWERSHELL_COMMAND_LINE = "powershell.exe -Command \"$asm = [System.Reflection.Assembly]::LoadFile('{0}'); $proxyType = $asm.GetType('Cbonnell.DotNetExpect.ProxyProcess'); $proxy = [System.Activator]::CreateInstance($proxyType, @('{1}')); $proxy.Run() | Out-Null;\""; 32 | 33 | private readonly NamedPipeServerStream commandPipe; 34 | private readonly BinaryReader commandPipeReader; 35 | private readonly BinaryWriter commandPipeWriter; 36 | private readonly string commandPipeName; 37 | 38 | private Process proxyProcess = null; 39 | 40 | public ProxyProcessManager() 41 | { 42 | this.commandPipeName = String.Format(ProxyProcessManager.PIPE_NAME_FMT, Guid.NewGuid()); 43 | this.commandPipe = new NamedPipeServerStream(this.commandPipeName); 44 | this.commandPipeReader = new BinaryReader(this.commandPipe); 45 | this.commandPipeWriter = new BinaryWriter(this.commandPipe); 46 | } 47 | 48 | public BinaryReader CommandPipeReader 49 | { 50 | get 51 | { 52 | return this.commandPipeReader; 53 | } 54 | } 55 | 56 | public BinaryWriter CommandPipeWriter 57 | { 58 | get 59 | { 60 | return this.commandPipeWriter; 61 | } 62 | } 63 | 64 | public void Start() 65 | { 66 | NativeMethods.STARTUPINFO startInfo = default(NativeMethods.STARTUPINFO); 67 | startInfo.cb = Marshal.SizeOf(startInfo); 68 | startInfo.dwFlags = NativeMethods.STARTF_USE_SHOWWINDOW; 69 | startInfo.wShowWindow = NativeMethods.SW_HIDE; 70 | 71 | string commandLine = String.Format(ProxyProcessManager.POWERSHELL_COMMAND_LINE, Assembly.GetExecutingAssembly().Location, this.commandPipeName); 72 | 73 | NativeMethods.PROCESS_INFORMATION procInfo; 74 | if (!NativeMethods.CreateProcess(null, commandLine, IntPtr.Zero, IntPtr.Zero, false, NativeMethods.CREATE_NEW_CONSOLE, IntPtr.Zero, null, ref startInfo, out procInfo)) 75 | { 76 | throw new Win32Exception(); 77 | } 78 | 79 | NativeMethods.CloseHandle(procInfo.hProcess); 80 | NativeMethods.CloseHandle(procInfo.hThread); 81 | this.proxyProcess = Process.GetProcessById(procInfo.dwProcessId); 82 | this.commandPipe.WaitForConnection(); 83 | } 84 | 85 | public void Dispose() 86 | { 87 | this.commandPipe.Dispose(); 88 | if (this.proxyProcess != null) 89 | { 90 | this.proxyProcess.Dispose(); 91 | } 92 | } 93 | 94 | private static class NativeMethods 95 | { 96 | [StructLayout(LayoutKind.Sequential)] 97 | public struct PROCESS_INFORMATION 98 | { 99 | public IntPtr hProcess; 100 | public IntPtr hThread; 101 | public int dwProcessId; 102 | public int dwThreadId; 103 | } 104 | 105 | [StructLayout(LayoutKind.Sequential)] 106 | public struct STARTUPINFO 107 | { 108 | public int cb; 109 | public string lpReserved; 110 | public string lpDesktop; 111 | public string lpTitle; 112 | public int dwX; 113 | public int dwY; 114 | public int dwXSize; 115 | public int dwYSize; 116 | public int dwXCountChars; 117 | public int dwYCountChars; 118 | public int dwFillAttribute; 119 | public int dwFlags; 120 | public short wShowWindow; 121 | public short cbReserved2; 122 | public IntPtr lpReserved2; 123 | public IntPtr hStdInput; 124 | public IntPtr hStdOutput; 125 | public IntPtr hStdError; 126 | } 127 | 128 | [DllImport("kernel32", SetLastError = true)] 129 | public static extern bool CreateProcess(string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); 130 | 131 | [DllImport("kernel32", SetLastError = true)] 132 | public static extern bool CloseHandle(IntPtr hObject); 133 | 134 | public const int STARTF_USE_SHOWWINDOW = 0x1; 135 | public const int SW_HIDE = 0; 136 | public const int CREATE_NEW_CONSOLE = 0x10; 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DotNetExpect 2 | 3 | 4 | ### What is DotNetExpect? 5 | Inspired by the design of the [Expect library](http://en.wikipedia.org/wiki/Expect), DotNetExpect is a .NET library that provides console input/control of console-based Windows applications. Unlike other solutions that can only redirect the standard streams of child processes, DotNetExpect directly accesses the console screen buffer of applications so that regardless of the mechanism the application uses to write to the console (either by writing to the standard streams or by invoking low-level console output functions), DotNetExpect is able to work with a much wider range of console applications. 6 | 7 | DotNetExpect is designed with the intention of exposing a simple, easy to use API so that automation of console applications becomes a trivial task. 8 | 9 | ### How do I use DotNetExpect? 10 | The DotNetExpect library exposes the primary class of the library interface, which is named `ChildProcess`. All operations regarding the creation/termination of console applications as well as reading and writing to an application's console is performed using this class. 11 | 12 | Here is a short example showing how the Windows Telnet client application can be automated to log into a Linux machine and retrieve a directory listing: 13 | 14 | ```csharp 15 | // Create a new instance of ChildProcess. This will spawn telnet.exe and connect its console to the library. 16 | // ChildProcess implements IDisposable, so generally you will want to instantiate this class in a using statement. 17 | using (ChildProcess childProc = new ChildProcess("telnet.exe", "192.168.1.1")) 18 | { 19 | // Wait for the login prompt to appear 20 | childProc.Read("login:"); 21 | 22 | // Write the user name with which to login to the console input 23 | childProc.WriteLine("root"); 24 | 25 | // Wait for the password prompt to appear 26 | childProc.Read("Password:"); 27 | 28 | // Write the password 29 | childProc.WriteLine("MySecretPassword"); 30 | 31 | // Wait for the root shell prompt to appear 32 | childProc.Read("#"); 33 | 34 | // Issue the "ls" command to output the directory contents 35 | childProc.WriteLine("ls"); 36 | 37 | // Wait until the root shell prompt appears and return the directory contents 38 | string dirContents = childProc.Read("#"); 39 | 40 | // Display the directory contents to our console 41 | Console.WriteLine(dirContents); 42 | } 43 | ``` 44 | 45 | Many other properties and methods are exposed on the `ChildProcess` class, including regular expression output matching and process management (such as terminating a child process and retrieving its exit code, etc.). Documentation is provided for each public member of the library and the library was designed to leverage Visual Studio's IntelliSense functionality for discoverability and ease of use. 46 | 47 | ### How do I obtain DotNetExpect? 48 | DotNetExpect is available as a [NuGet package](https://www.nuget.org/packages/Cbonnell.DotNetExpect/). Alternatively, the library can be built from source. 49 | 50 | ### How do I build DotNetExpect? 51 | DotNetExpect is Visual Studio 2013 project, so it is possible to build the project by merely opening up the solution (.sln) file for the library and building it or invoking MSBuild from the command line. 52 | 53 | ### What are the system requirements for using DotNetExpect? 54 | DotNetExpect is written against .NET framework 3.5 (client profile), so that version of the framework or higher must be installed. DotNetExpect uses PowerShell as a proxy process (see the section "How does DotNetExpect work?" below for details), so PowerShell must be installed and on the system PATH. Any version of PowerShell should work. 55 | 56 | ### What is the license for DotNetExpect? 57 | DotNetExpect is licensed under the LGPL version 3.0. 58 | 59 | ### Are there tests available? 60 | A small suite of unit and integration tests is included in the Visual Studio solution. These tests are written against NUnit 2.6.4, so you will need to have NUnit installed to build and run these tests. 61 | 62 | ### Are there any limitations of DotNetExpect? 63 | The chief limitation of DotNetExpect is that the output of console applications may be lost if huge amounts of data are being written to the console. This is due to how DotNetExpect interacts with console applications to capture thier output. The contents of the console application's screen buffer are constantly being captured by the library, but there may be instances where some output is lost due to the output "scrolling out" of the buffer before it can be captured. 64 | 65 | A second limitation is that "console-like" applications, such as PuTTY, cannot be manipulated using this library. This is due to PuTTY (and some other applications) actually not being a console application, but rather a Windows graphical application that happens to look like a console application. Therefore, the Console API functions that DotNetExpect uses have no effect on these types of applications. 66 | 67 | ### How do I report bugs or request enhancements? 68 | Please [create an issue](https://github.com/CBonnell/dotnetexpect/issues). 69 | 70 | ### How does DotNetExpect work? 71 | When an instance of `ChildProcess` is created, a randomly-named named pipe is created and a "proxy process" is launched. The proxy process that is created is PowerShell, which then loads the DotNetExpect assembly and invokes the code in the DotNetExpect library that runs the proxy process server. The proxy process server connects to the named pipe and services commands sent from the library. Whenever a method or property is called on `ChildProcess`, that request is sent via the named pipe to the proxy process, which then services the request. 72 | 73 | The rationale for using a proxy process is so that graphical applications (which do not have a console) or console applications that still want to have console input/output of their own will function normally, as the proxy process is created with its own console entirely separate from the parent (calling) process. It is the proxy process that actually spawns the child process and shares its console with the child process. PowerShell was chosen due to its ability to dynamically load and call into .NET assemblies, obviating the need to author a separate executable for the proxy process. In this regard, PowerShell acts as a host process for running .NET code. 74 | 75 | 76 | 77 | --------------------------------------------------------------------------------