├── .gitignore ├── BuildAll.bat ├── BuildAll.ps1 ├── DUTRemoteTests ├── BasicRunTests.cs ├── ClientTests.cs ├── DUTRemoteTests.csproj ├── ExtensionTests.cs └── JobTests.cs ├── Doxyfile ├── LICENSE ├── MainDocPage.md ├── PluginExample ├── Class1.cs ├── PluginExample.csproj └── pubout │ └── PluginExample.dll ├── README.md ├── SECURITY.md ├── SetVersion.py ├── SimpleDUTClientLibrary ├── AssemblyRedirectResolver.cs ├── RpcClient.cs └── SimpleDUTClientLibrary.csproj ├── SimpleDUTCommonLibrary ├── GlobFunctions.cs ├── SimpleDUTCommonLibrary.csproj ├── TarFunctions.cs ├── TcpClientConnectWithTimeout.cs └── ZipFunctions.cs ├── SimpleDUTRemote.sln ├── SimpleDUTRemote ├── Functions.cs ├── HelperFunctions │ ├── LargeFileTransfers.cs │ ├── ReadWriteChecks.cs │ └── ThreadSafeStringBuilder.cs ├── JobSystem │ └── Job.cs └── SimpleDUTRemote.csproj ├── SimpleJsonRpc ├── BroadcastResponder.cs ├── SimpleJsonRpc.csproj ├── SimpleRpcMethod.cs └── SimpleRpcServer.cs ├── SimpleRemoteConsole ├── Nlog.config ├── Program.cs ├── ServiceInterop │ ├── NativeServiceWrapper.cs │ ├── Service.cs │ ├── ServiceInfo.cs │ ├── ServiceStatus.cs │ └── ServiceTableEntry.cs ├── SimpleRemoteConsole.csproj └── UserWarning.txt ├── assets └── TeamLogo.png ├── extra_docs ├── rpc_tutorial.md └── tutorial.md └── installer.iss /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | 33 | # Visual Studio 2015/2017 cache/options directory 34 | .vs/ 35 | # Uncomment if you have tasks that create the project's static files in wwwroot 36 | #wwwroot/ 37 | 38 | # Visual Studio 2017 auto generated files 39 | Generated\ Files/ 40 | 41 | # MSTest test Results 42 | [Tt]est[Rr]esult*/ 43 | [Bb]uild[Ll]og.* 44 | 45 | # NUnit 46 | *.VisualState.xml 47 | TestResult.xml 48 | nunit-*.xml 49 | 50 | # Build Results of an ATL Project 51 | [Dd]ebugPS/ 52 | [Rr]eleasePS/ 53 | dlldata.c 54 | 55 | # Benchmark Results 56 | BenchmarkDotNet.Artifacts/ 57 | 58 | # .NET Core 59 | project.lock.json 60 | project.fragment.lock.json 61 | artifacts/ 62 | 63 | # StyleCop 64 | StyleCopReport.xml 65 | 66 | # Files built by Visual Studio 67 | *_i.c 68 | *_p.c 69 | *_h.h 70 | *.ilk 71 | *.meta 72 | *.obj 73 | *.iobj 74 | *.pch 75 | *.pdb 76 | *.ipdb 77 | *.pgc 78 | *.pgd 79 | *.rsp 80 | *.sbr 81 | *.tlb 82 | *.tli 83 | *.tlh 84 | *.tmp 85 | *.tmp_proj 86 | *_wpftmp.csproj 87 | *.log 88 | *.vspscc 89 | *.vssscc 90 | .builds 91 | *.pidb 92 | *.svclog 93 | *.scc 94 | 95 | # Chutzpah Test files 96 | _Chutzpah* 97 | 98 | # Visual C++ cache files 99 | ipch/ 100 | *.aps 101 | *.ncb 102 | *.opendb 103 | *.opensdf 104 | *.sdf 105 | *.cachefile 106 | *.VC.db 107 | *.VC.VC.opendb 108 | 109 | # Visual Studio profiler 110 | *.psess 111 | *.vsp 112 | *.vspx 113 | *.sap 114 | 115 | # Visual Studio Trace Files 116 | *.e2e 117 | 118 | # TFS 2012 Local Workspace 119 | $tf/ 120 | 121 | # Guidance Automation Toolkit 122 | *.gpState 123 | 124 | # ReSharper is a .NET coding add-in 125 | _ReSharper*/ 126 | *.[Rr]e[Ss]harper 127 | *.DotSettings.user 128 | 129 | # JustCode is a .NET coding add-in 130 | .JustCode 131 | 132 | # TeamCity is a build add-in 133 | _TeamCity* 134 | 135 | # DotCover is a Code Coverage Tool 136 | *.dotCover 137 | 138 | # AxoCover is a Code Coverage Tool 139 | .axoCover/* 140 | !.axoCover/settings.json 141 | 142 | # Visual Studio code coverage results 143 | *.coverage 144 | *.coveragexml 145 | 146 | # NCrunch 147 | _NCrunch_* 148 | .*crunch*.local.xml 149 | nCrunchTemp_* 150 | 151 | # MightyMoose 152 | *.mm.* 153 | AutoTest.Net/ 154 | 155 | # Web workbench (sass) 156 | .sass-cache/ 157 | 158 | # Installshield output folder 159 | [Ee]xpress/ 160 | 161 | # DocProject is a documentation generator add-in 162 | DocProject/buildhelp/ 163 | DocProject/Help/*.HxT 164 | DocProject/Help/*.HxC 165 | DocProject/Help/*.hhc 166 | DocProject/Help/*.hhk 167 | DocProject/Help/*.hhp 168 | DocProject/Help/Html2 169 | DocProject/Help/html 170 | 171 | # Click-Once directory 172 | publish/ 173 | 174 | # Publish Web Output 175 | *.[Pp]ublish.xml 176 | *.azurePubxml 177 | # Note: Comment the next line if you want to checkin your web deploy settings, 178 | # but database connection strings (with potential passwords) will be unencrypted 179 | *.pubxml 180 | *.publishproj 181 | 182 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 183 | # checkin your Azure Web App publish settings, but sensitive information contained 184 | # in these scripts will be unencrypted 185 | PublishScripts/ 186 | 187 | # NuGet Packages 188 | *.nupkg 189 | # NuGet Symbol Packages 190 | *.snupkg 191 | # The packages folder can be ignored because of Package Restore 192 | **/[Pp]ackages/* 193 | # except build/, which is used as an MSBuild target. 194 | !**/[Pp]ackages/build/ 195 | # Uncomment if necessary however generally it will be regenerated when needed 196 | #!**/[Pp]ackages/repositories.config 197 | # NuGet v3's project.json files produces more ignorable files 198 | *.nuget.props 199 | *.nuget.targets 200 | 201 | # Microsoft Azure Build Output 202 | csx/ 203 | *.build.csdef 204 | 205 | # Microsoft Azure Emulator 206 | ecf/ 207 | rcf/ 208 | 209 | # Windows Store app package directories and files 210 | AppPackages/ 211 | BundleArtifacts/ 212 | Package.StoreAssociation.xml 213 | _pkginfo.txt 214 | *.appx 215 | *.appxbundle 216 | *.appxupload 217 | 218 | # Visual Studio cache files 219 | # files ending in .cache can be ignored 220 | *.[Cc]ache 221 | # but keep track of directories ending in .cache 222 | !?*.[Cc]ache/ 223 | 224 | # Others 225 | ClientBin/ 226 | ~$* 227 | *~ 228 | *.dbmdl 229 | *.dbproj.schemaview 230 | *.jfm 231 | *.pfx 232 | *.publishsettings 233 | orleans.codegen.cs 234 | 235 | # Including strong name files can present a security risk 236 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 237 | #*.snk 238 | 239 | # Since there are multiple workflows, uncomment next line to ignore bower_components 240 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 241 | #bower_components/ 242 | 243 | # RIA/Silverlight projects 244 | Generated_Code/ 245 | 246 | # Backup & report files from converting an old project file 247 | # to a newer Visual Studio version. Backup files are not needed, 248 | # because we have git ;-) 249 | _UpgradeReport_Files/ 250 | Backup*/ 251 | UpgradeLog*.XML 252 | UpgradeLog*.htm 253 | ServiceFabricBackup/ 254 | *.rptproj.bak 255 | 256 | # SQL Server files 257 | *.mdf 258 | *.ldf 259 | *.ndf 260 | 261 | # Business Intelligence projects 262 | *.rdl.data 263 | *.bim.layout 264 | *.bim_*.settings 265 | *.rptproj.rsuser 266 | *- [Bb]ackup.rdl 267 | *- [Bb]ackup ([0-9]).rdl 268 | *- [Bb]ackup ([0-9][0-9]).rdl 269 | 270 | # Microsoft Fakes 271 | FakesAssemblies/ 272 | 273 | # GhostDoc plugin setting file 274 | *.GhostDoc.xml 275 | 276 | # Node.js Tools for Visual Studio 277 | .ntvs_analysis.dat 278 | node_modules/ 279 | 280 | # Visual Studio 6 build log 281 | *.plg 282 | 283 | # Visual Studio 6 workspace options file 284 | *.opt 285 | 286 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 287 | *.vbw 288 | 289 | # Visual Studio LightSwitch build output 290 | **/*.HTMLClient/GeneratedArtifacts 291 | **/*.DesktopClient/GeneratedArtifacts 292 | **/*.DesktopClient/ModelManifest.xml 293 | **/*.Server/GeneratedArtifacts 294 | **/*.Server/ModelManifest.xml 295 | _Pvt_Extensions 296 | 297 | # Paket dependency manager 298 | .paket/paket.exe 299 | paket-files/ 300 | 301 | # FAKE - F# Make 302 | .fake/ 303 | 304 | # CodeRush personal settings 305 | .cr/personal 306 | 307 | # Python Tools for Visual Studio (PTVS) 308 | __pycache__/ 309 | *.pyc 310 | 311 | # Cake - Uncomment if you are using it 312 | # tools/** 313 | # !tools/packages.config 314 | 315 | # Tabs Studio 316 | *.tss 317 | 318 | # Telerik's JustMock configuration file 319 | *.jmconfig 320 | 321 | # BizTalk build output 322 | *.btp.cs 323 | *.btm.cs 324 | *.odx.cs 325 | *.xsd.cs 326 | 327 | # OpenCover UI analysis results 328 | OpenCover/ 329 | 330 | # Azure Stream Analytics local run output 331 | ASALocalRun/ 332 | 333 | # MSBuild Binary and Structured Log 334 | *.binlog 335 | 336 | # NVidia Nsight GPU debugger configuration file 337 | *.nvuser 338 | 339 | # MFractors (Xamarin productivity tool) working folder 340 | .mfractor/ 341 | 342 | # Local History for Visual Studio 343 | .localhistory/ 344 | 345 | # BeatPulse healthcheck temp database 346 | healthchecksdb 347 | 348 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 349 | MigrationBackup/ 350 | 351 | # Ionide (cross platform F# VS Code tools) working folder 352 | .ionide/ 353 | 354 | BuildOutput/ 355 | .vscode/ 356 | output/ 357 | 358 | #Keep the doc folder out of this branch 359 | doc/ -------------------------------------------------------------------------------- /BuildAll.bat: -------------------------------------------------------------------------------- 1 | REM Builds all of SimpleRemote and associated docs 2 | 3 | @ECHO OFF 4 | PowerShell.exe -Command "& '%~dpn0.ps1'" 5 | pause -------------------------------------------------------------------------------- /BuildAll.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft. All rights reserved. 2 | # Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | 5 | # build SimpleRemote and all associated docs 6 | 7 | # CONSTANTS 8 | $BASEDIR = $PSScriptRoot 9 | $OUTDIR = "$BASEDIR/output" 10 | $INNOINSTALL = "C:\Program Files (x86)\Inno Setup 5\ISCC.exe" 11 | 12 | # useful other variables 13 | $VER_SUFFIX = (Get-Date -Format FileDate) 14 | 15 | # clean output directory and projects 16 | if (Test-Path $OUTDIR) 17 | { 18 | rm -r $OUTDIR 19 | } 20 | mkdir $OUTDIR | out-null 21 | dotnet clean | out-null 22 | 23 | # build the common library package 24 | echo "Building common library package..." 25 | cd $BASEDIR\SimpleDUTCommonLibrary 26 | dotnet pack -o $OUTDIR/nuget -c Release | Out-Null 27 | 28 | # build the client 29 | echo "Building client library..." 30 | cd $BASEDIR\SimpleDUTClientLibrary 31 | dotnet publish -o $OUTDIR/SimpleRemoteClient --version-suffix $VER_SUFFIX -f netstandard2.0 -c Release | Out-Null 32 | dotnet pack -o $OUTDIR/nuget -c Release | Out-Null 33 | 34 | 35 | # build the rpc server nuget binary 36 | echo "Building RPC server package" 37 | cd $BASEDIR\SimpleJsonRpc 38 | dotnet pack -o $OUTDIR/nuget -c Release | Out-Null 39 | 40 | # build the server 41 | echo "Building server..." 42 | cd $BASEDIR\SimpleRemoteConsole 43 | dotnet publish -o $OUTDIR/SimpleRemoteServer-x64-netframework -f net47 -r win10-x64 -c Release | Out-Null 44 | dotnet publish -o $OUTDIR/SimpleRemoteServer-arm64 -f net6 -r win10-arm64 -c Release | Out-Null 45 | dotnet publish -o $OUTDIR/SimpleRemoteServer-x64 -f net6 -r win10-x64 -c Release | Out-Null 46 | 47 | # build docs 48 | if (gcm doxygen -ErrorAction SilentlyContinue) 49 | { 50 | echo "Building docs..." 51 | cd $BASEDIR 52 | doxygen 2>&1 | Out-Null 53 | cp -r .\doc\html $OUTDIR/htmldoc 54 | } 55 | else { 56 | echo "Doxygen not found, skipping doc build." 57 | } 58 | 59 | # build the installer 60 | if (Test-Path $INNOINSTALL) 61 | { 62 | echo "Building installer..." 63 | cd $BASEDIR 64 | & $INNOINSTALL /q installer.iss 65 | } 66 | else { 67 | echo "Inno install not found - skipping installer generation." 68 | } -------------------------------------------------------------------------------- /DUTRemoteTests/BasicRunTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using System.Threading.Tasks; 7 | using System.Threading; 8 | using System.Net.Sockets; 9 | using System.IO; 10 | using Newtonsoft.Json; 11 | using System.Collections.Generic; 12 | using SimpleDUTRemote; 13 | using System.Diagnostics; 14 | using System.Linq; 15 | using SimpleJsonRpc; 16 | 17 | namespace DUTRemoteTests 18 | { 19 | [TestClass] 20 | public class BasicRunTests 21 | { 22 | private SimpleRpcServer server; 23 | private Task serverTask; 24 | 25 | public BasicRunTests() 26 | { 27 | server = new SimpleRpcServer(); 28 | var rpcFunctions = new SimpleDUTRemote.Functions(); 29 | server.Register(rpcFunctions); 30 | serverTask = server.Start(); 31 | } 32 | 33 | [TestMethod] 34 | public void RunWithoutResult() 35 | { 36 | string output; 37 | using (var client = GetClient()) 38 | using (var rstream = new StreamReader(client.GetStream())) 39 | using (var wstream = new StreamWriter(client.GetStream())) 40 | { 41 | var request = new JsonRpcRequest(); 42 | request.method = "Run"; 43 | request.args = new List() { @"C:\Program Files\Internet Explorer\iexplore.exe" }; 44 | wstream.WriteLine(JsonConvert.SerializeObject(request) + "\r\n"); 45 | wstream.Flush(); 46 | 47 | output = rstream.ReadToEnd(); 48 | } 49 | 50 | var resp = JsonConvert.DeserializeObject(output); 51 | 52 | Assert.IsNull(resp.error); 53 | Assert.IsTrue((bool) resp.result == true); 54 | 55 | // confirm IE started 56 | var procList = Process.GetProcessesByName("iexplore"); 57 | Assert.IsTrue(procList.Length > 0, "No IE instance found."); 58 | Assert.IsTrue(procList.First().HasExited == false); 59 | 60 | // terminate it 61 | procList.First().Kill(); 62 | } 63 | 64 | [TestMethod] 65 | public void RunWithResult() 66 | { 67 | string output; 68 | using (var client = GetClient()) 69 | using (var rstream = new StreamReader(client.GetStream())) 70 | using (var wstream = new StreamWriter(client.GetStream())) 71 | { 72 | var request = new JsonRpcRequest(); 73 | request.method = "RunWithResult"; 74 | request.args = new List() { "systeminfo.exe" }; 75 | wstream.WriteLine(JsonConvert.SerializeObject(request) + "\r\n"); 76 | wstream.Flush(); 77 | 78 | output = rstream.ReadToEnd(); 79 | } 80 | 81 | var resp = JsonConvert.DeserializeObject(output); 82 | 83 | Assert.IsNull(resp.error); 84 | Assert.IsTrue(((string)resp.result).Length > 0, "Length of output from command is 0"); 85 | Assert.IsTrue(((string)resp.result).Contains("OS Name:"), "Result doesn't contain expected items."); 86 | } 87 | 88 | [TestMethod] 89 | public void RunWithResultAndExitCode() 90 | { 91 | string output; 92 | using (var client = GetClient()) 93 | using (var rstream = new StreamReader(client.GetStream())) 94 | using (var wstream = new StreamWriter(client.GetStream())) 95 | { 96 | var request = new JsonRpcRequest(); 97 | request.method = "RunWithResultAndExitCode"; 98 | request.args = new List() { "systeminfo.exe" }; 99 | wstream.WriteLine(JsonConvert.SerializeObject(request) + "\r\n"); 100 | wstream.Flush(); 101 | 102 | output = rstream.ReadToEnd(); 103 | } 104 | 105 | var resp = JsonConvert.DeserializeObject(output); 106 | var result = ((Newtonsoft.Json.Linq.JArray)resp.result).ToObject(); 107 | 108 | Assert.IsNull(resp.error); 109 | Assert.IsTrue(result.Length > 1, "Should return an array of strings"); 110 | Assert.IsTrue(result[0].Contains("0"), "Exit code was incorrect (nonzero)."); 111 | Assert.IsTrue(result[1].Contains("OS Name:"), "Result doesn't contain expected items."); 112 | } 113 | 114 | [TestMethod] 115 | public void KillProcess() 116 | { 117 | // start our process 118 | var p = Process.Start(@"C:\Program Files\Internet Explorer\iexplore.exe"); 119 | 120 | // confirm it's actually started 121 | Assert.IsFalse(p.HasExited); 122 | 123 | string output; 124 | using (var client = GetClient()) 125 | using (var rstream = new StreamReader(client.GetStream())) 126 | using (var wstream = new StreamWriter(client.GetStream())) 127 | { 128 | var request = new JsonRpcRequest(); 129 | request.method = "KillProcess"; 130 | request.args = new List() { "iexplore" }; 131 | wstream.WriteLine(JsonConvert.SerializeObject(request) + "\r\n"); 132 | wstream.Flush(); 133 | 134 | output = rstream.ReadToEnd(); 135 | } 136 | 137 | var resp = JsonConvert.DeserializeObject(output); 138 | 139 | Assert.IsNull(resp.error); 140 | Assert.IsTrue((bool) resp.result == true); 141 | 142 | // confirm IE terminated 143 | Assert.IsTrue(p.HasExited); 144 | } 145 | 146 | [TestMethod] 147 | public void RpcServer_StopServer() 148 | { 149 | var altServer = new SimpleRpcServer(); 150 | var rpcFunctions = new SimpleDUTRemote.Functions(); 151 | altServer.Register(rpcFunctions); 152 | var serverTask = altServer.Start(9000); 153 | Thread.Sleep(500); 154 | altServer.Stop(); 155 | 156 | var finished = serverTask.Wait(1000); 157 | 158 | Assert.IsTrue(finished, "Server did not stop."); 159 | } 160 | 161 | 162 | private TcpClient GetClient() 163 | { 164 | // return a connection to the current server 165 | var client = new TcpClient(); 166 | client.ConnectAsync("localhost", 8000).Wait(); 167 | 168 | return client; 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /DUTRemoteTests/ClientTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using SimpleDUTClientLibrary; 6 | using SimpleDUTRemote; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using System.Diagnostics; 12 | using System.Linq; 13 | using System.Threading; 14 | using System.IO; 15 | using System.Net.Sockets; 16 | using SimpleJsonRpc; 17 | using System.Security.Principal; 18 | using Newtonsoft.Json.Linq; 19 | 20 | namespace DUTRemoteTests 21 | { 22 | [TestClass] 23 | public class ClientTests 24 | { 25 | private SimpleRpcServer server; 26 | private Task serverTask; 27 | private RpcClient client; 28 | 29 | public ClientTests() 30 | { 31 | server = new SimpleRpcServer(); 32 | var rpcFunctions = new SimpleDUTRemote.Functions(); 33 | server.Register(rpcFunctions); 34 | serverTask = server.Start(); 35 | client = new RpcClient("127.0.0.1", 8000); 36 | 37 | // setup nlog tracking (off by default) 38 | // use this when debugging since vstest doesn't capture output. 39 | /* 40 | var config = new NLog.Config.LoggingConfiguration(); 41 | config.AddRuleForAllLevels(new NLog.Targets.FileTarget("logfile") 42 | { 43 | FileName = "client_test_log.txt" 44 | }); 45 | 46 | NLog.LogManager.Configuration = config; 47 | */ 48 | } 49 | 50 | [TestMethod] 51 | public void Client_RunAndKillProcess() 52 | { 53 | // test run 54 | 55 | bool status = client.Run(@"notepad.exe"); 56 | Assert.IsTrue(status); 57 | 58 | var sw = new Stopwatch(); 59 | sw.Start(); 60 | 61 | Process[] procList = new Process[0]; 62 | status = CheckIfTrueInTimelimit(() => 63 | { 64 | procList = Process.GetProcessesByName("notepad"); 65 | return (procList.Length > 0); 66 | }, 5000); 67 | 68 | Assert.IsTrue(status, "Did not spawn process within time limit."); 69 | 70 | // test process kill 71 | status = client.KillProcess("notepad"); 72 | Assert.IsTrue(status); 73 | 74 | status = CheckIfTrueInTimelimit(() => 75 | { 76 | procList = Process.GetProcessesByName("notepad"); 77 | return (procList.Length < 1); 78 | }, 5000); 79 | 80 | Assert.IsTrue(status, "Process did not exit."); 81 | } 82 | 83 | [TestMethod] 84 | public void Client_RunProcessWithOutput() 85 | { 86 | string result = client.RunJob("systeminfo.exe"); 87 | 88 | Assert.IsTrue(result.Length > 0); 89 | Assert.IsTrue(result.Contains("OS Name:"), "Result doesn't contain expected items."); 90 | } 91 | 92 | [TestMethod] 93 | public void Client_RunProcessWithOutput_WithTimeout() 94 | { 95 | // it should take longer than 1 second to complete, so we should get an aggregate exception 96 | var e = Assert.ThrowsException(() => client.RunJob("systeminfo.exe", timeout: 1)); 97 | 98 | Assert.IsInstanceOfType(e.InnerException, typeof(TimeoutException)); 99 | } 100 | 101 | [TestMethod] 102 | public void Client_RunProcessWithArgs() 103 | { 104 | string result = client.RunJob("powershell.exe", "-Help" ); 105 | 106 | Assert.IsTrue(result.Length > 0); 107 | Assert.IsTrue(result.Contains("Shows this message."), "Failed to call powershell with -Help argument."); 108 | } 109 | 110 | [TestMethod] 111 | public void Client_GetAllJobs() 112 | { 113 | var sysinfo = client.StartJob("systeminfo.exe"); 114 | var notepad = client.StartJob("notepad.exe"); 115 | 116 | while (!client.CheckJobCompletion(sysinfo)) 117 | { 118 | Task.Delay(1000).Wait(); 119 | } 120 | 121 | var jobs = client.GetAllJobs(); 122 | 123 | Assert.IsTrue(jobs.ContainsKey(notepad), "Missing notepad job id."); 124 | Assert.IsTrue(jobs.ContainsKey(sysinfo), "Midding system info job id."); 125 | 126 | Assert.IsTrue(jobs[notepad] == false, "Notepad marked as ended, even through it should still be running."); 127 | Assert.IsTrue(jobs[sysinfo] == true, "System Info marked as running, even through it should have completed."); 128 | 129 | client.StopJob(notepad); 130 | } 131 | 132 | [TestMethod] 133 | public void Client_StartJobWithNotification_CheckJobOutput() 134 | { 135 | AutoResetEvent evt = new AutoResetEvent(false); 136 | int completedJob = -1; 137 | Action cb = (jobid) => 138 | { 139 | completedJob = jobid; 140 | evt.Set(); 141 | }; 142 | 143 | int newJobId = client.StartJobWithNotification("systeminfo.exe", null, cb); 144 | 145 | Assert.IsTrue(newJobId > 0); 146 | 147 | Assert.IsTrue(evt.WaitOne(5000), "Callback took too long to be received."); 148 | Assert.IsTrue(completedJob == newJobId); 149 | 150 | Assert.IsTrue(client.CheckJobCompletion(completedJob), "Job declared complete does not appear to be complete on server."); 151 | 152 | string result = client.GetJobResult(completedJob); 153 | Assert.IsTrue(result.Length > 0); 154 | Assert.IsTrue(result.Contains("OS Name:"), "Result doesn't contain expected items."); 155 | } 156 | 157 | [TestMethod] 158 | public void Client_StartJobWithProgress_CheckJobOutput() 159 | { 160 | AutoResetEvent evt = new AutoResetEvent(false); 161 | int completedJob = -1; 162 | var completedMsgs = new List(); 163 | 164 | Action cb = (jobid) => 165 | { 166 | completedJob = jobid; 167 | evt.Set(); 168 | }; 169 | 170 | Action progressEvent = (msg) => completedMsgs.Add(msg); 171 | 172 | int newJobId = client.StartJobWithProgress("systeminfo.exe", null, progressEvent, cb); 173 | 174 | Assert.IsTrue(newJobId > 0); 175 | 176 | Assert.IsTrue(evt.WaitOne(5000), "Callback took too long to be received."); 177 | Assert.IsTrue(completedJob == newJobId); 178 | 179 | Assert.IsTrue(client.CheckJobCompletion(completedJob), "Job declared complete does not appear to be complete on server."); 180 | 181 | string result = client.GetJobResult(completedJob); 182 | Assert.IsTrue(result.Length == 0, "Result was greater than zero, even though we used StartJobWithProgress"); 183 | 184 | Assert.IsTrue(completedMsgs.Count() > 0, "Did not receive any progress messages."); 185 | var OSNameMsg = completedMsgs.Where((x) => x.StartsWith("OS Name:")); 186 | Assert.IsTrue(OSNameMsg.Count() > 0, "Did not receive a message with OS Name"); 187 | 188 | } 189 | 190 | [TestMethod] 191 | public void Client_StartJobWithArgsAndNotification() 192 | { 193 | AutoResetEvent evt = new AutoResetEvent(false); 194 | Action cb = (jobid) => 195 | { 196 | evt.Set(); 197 | }; 198 | 199 | int newJobId = client.StartJobWithNotification("powershell.exe", "-Help", cb); 200 | Assert.IsTrue(evt.WaitOne(5000), "Callback took too long to be received."); 201 | 202 | string result = client.GetJobResult(newJobId); 203 | 204 | Assert.IsTrue(result.Contains("Shows this message."), "Failed to start job with powershell using -Help argument."); 205 | } 206 | 207 | [TestMethod] 208 | public void Client_RunWithResultAsync() 209 | { 210 | var task = client.RunJobAsync("systeminfo.exe", null); 211 | var completed = task.Wait(5000); 212 | 213 | Assert.IsTrue(completed, "RunWithResultAsync task did not complete in a timely manner."); 214 | Assert.IsTrue(task.IsCompleted && !task.IsFaulted, "RunWithResultAsync task failed to complete."); 215 | 216 | var result = task.Result; 217 | Assert.IsTrue(result.Length > 0); 218 | Assert.IsTrue(result.Contains("OS Name:"), "RunWithResultAsync output doesn't contain expected items."); 219 | } 220 | 221 | [TestMethod] 222 | public void Client_RunWithResultAsyncEx() 223 | { 224 | var task = client.RunJobAsyncEx("systeminfo.exe", null); 225 | var completed = task.Wait(5000); 226 | 227 | Assert.IsTrue(completed, "RunJobAsyncEx task did not complete in a timely manner."); 228 | Assert.IsTrue(task.IsCompleted && !task.IsFaulted, "RunJobAsyncEx task failed to complete."); 229 | 230 | var result = task.Result; 231 | var jObj = JObject.Parse(result); 232 | 233 | var output = (string) jObj["output"]; 234 | var exitCode = (long) jObj["exitCode"]; 235 | 236 | Assert.IsTrue(output.Length > 0); 237 | Assert.IsTrue(output.Contains("OS Name:"), "RunJobAsyncEx output doesn't contain expected items."); 238 | Assert.IsTrue(exitCode == 0, "RunJobAsyncEx called process didn't return exit code 0."); 239 | } 240 | 241 | [TestMethod] 242 | public void Client_RunWithResultAsync_TestCancellation() 243 | { 244 | var tokenSource = new CancellationTokenSource(); 245 | var token = tokenSource.Token; 246 | 247 | var task = client.RunJobAsync("systeminfo.exe", cancellationToken: token); 248 | tokenSource.Cancel(); 249 | 250 | string result; 251 | var e = Assert.ThrowsException(() => result = task.Result); 252 | 253 | Assert.IsInstanceOfType(e.InnerException, typeof(OperationCanceledException)); 254 | 255 | 256 | } 257 | 258 | [TestMethod] 259 | public void Client_Upload_UseFile_UseRelativeSendPath() 260 | { 261 | // determine file paths 262 | var fileToSend = "sampleFileToSend"; 263 | var dirToRecv = Path.Combine(Path.GetTempPath(), "sampleRecvDir"); 264 | var fileToRecv = Path.Combine(dirToRecv, "sampleFileToSend"); 265 | 266 | Directory.CreateDirectory(dirToRecv); 267 | 268 | // create 100 MB random data, and write it to our file. 269 | byte[] data = new byte[100 * 1024 * 1024]; 270 | Random rng = new Random(); 271 | rng.NextBytes(data); 272 | File.WriteAllBytes(fileToSend, data); 273 | 274 | // perform the upload 275 | client.Upload(fileToSend, dirToRecv, true); 276 | 277 | Thread.Sleep(500); // give time for writes to disk to complete. 278 | byte[] receivedData = File.ReadAllBytes(fileToRecv); 279 | Assert.IsTrue(data.SequenceEqual(receivedData), "Data received and data sent do not match."); 280 | 281 | // if we get to this point, delete extra files 282 | File.Delete(fileToSend); 283 | File.Delete(fileToRecv); 284 | 285 | Directory.Delete(dirToRecv); 286 | } 287 | 288 | [TestMethod] 289 | public void Client_Upload_UseFile() 290 | { 291 | // determine file paths 292 | var fileToSend = Path.GetTempPath() + "sampleFileToSend"; 293 | var dirToRecv = Path.Combine(Path.GetTempPath(), "sampleRecvDir"); 294 | var fileToRecv = Path.Combine(dirToRecv, "sampleFileToSend"); 295 | 296 | Directory.CreateDirectory(dirToRecv); 297 | 298 | // create 100 MB random data, and write it to our file. 299 | byte[] data = new byte[100 * 1024 * 1024]; 300 | Random rng = new Random(); 301 | rng.NextBytes(data); 302 | File.WriteAllBytes(fileToSend, data); 303 | 304 | // perform the upload 305 | client.Upload(fileToSend, dirToRecv, true); 306 | 307 | Thread.Sleep(500); // give time for writes to disk to complete. 308 | byte[] receivedData = File.ReadAllBytes(fileToRecv); 309 | Assert.IsTrue(data.SequenceEqual(receivedData), "Data received and data sent do not match."); 310 | 311 | // if we get to this point, delete extra files 312 | File.Delete(fileToSend); 313 | File.Delete(fileToRecv); 314 | 315 | Directory.Delete(dirToRecv); 316 | } 317 | 318 | [TestMethod] 319 | public void Client_Upload_UseFile_CheckOverwrite() 320 | { 321 | // determine file paths 322 | var fileToSend = Path.GetTempPath() + "sampleFileToSend"; 323 | var dirToRecv = Path.Combine(Path.GetTempPath(), "sampleRecvDir"); 324 | var fileToRecv = Path.Combine(dirToRecv, "sampleFileToSend"); 325 | 326 | Directory.CreateDirectory(dirToRecv); 327 | 328 | // create 100 MB random data, and write it to our file. 329 | byte[] data = new byte[100 * 1024 * 1024]; 330 | Random rng = new Random(); 331 | rng.NextBytes(data); 332 | File.WriteAllBytes(fileToSend, data); 333 | 334 | // perform the upload 335 | client.Upload(fileToSend, dirToRecv, true); 336 | 337 | Thread.Sleep(500); // give time for writes to disk to complete. 338 | byte[] receivedData = File.ReadAllBytes(fileToRecv); 339 | Assert.IsTrue(data.SequenceEqual(receivedData), "Data received and data sent do not match."); 340 | 341 | // redo the upload with new data 342 | rng.NextBytes(data); 343 | File.WriteAllBytes(fileToSend, data); 344 | 345 | // upload again 346 | client.Upload(fileToSend, dirToRecv, true); 347 | 348 | Thread.Sleep(500); // give time for writes to disk to complete. 349 | receivedData = File.ReadAllBytes(fileToRecv); 350 | Assert.IsTrue(data.SequenceEqual(receivedData), "Data received (after overwrite) and data sent do not match."); 351 | 352 | // check the this fails if we try to upload again with overwrite off 353 | // and confirm existing file isn't altered 354 | var originalData = data; 355 | byte[] newdata = new byte[10 * 1024 * 1024]; 356 | rng.NextBytes(newdata); 357 | File.WriteAllBytes(fileToSend, data); 358 | 359 | // attempt upload - this should fail 360 | try 361 | { 362 | client.Upload(fileToSend, dirToRecv, false); 363 | Assert.Fail("Upload succeeded even when target already existed and overwrite was false."); 364 | } 365 | catch (Exception ex) 366 | { 367 | // confirm we're not catching the assertion failed 368 | if (ex is AssertFailedException) throw; 369 | 370 | // otherwsie, we're fine. 371 | } 372 | 373 | // confirm that the file on disk was not changed 374 | Assert.IsTrue(originalData.SequenceEqual(File.ReadAllBytes(fileToRecv)), "Existing file changed (even though overwrite was off)."); 375 | 376 | // if we get to this point, delete extra files 377 | File.Delete(fileToSend); 378 | File.Delete(fileToRecv); 379 | 380 | Directory.Delete(dirToRecv); 381 | } 382 | 383 | [TestMethod] 384 | public void Client_Upload_UseDirectory() 385 | { 386 | var sampleRoot = PrepareDirectoryTests(); 387 | var sendRoot = Path.Combine(sampleRoot, "sent"); 388 | var recvRoot = Path.Combine(sampleRoot, "received"); 389 | 390 | client.Upload(sendRoot, recvRoot, true); 391 | 392 | Thread.Sleep(1000); 393 | 394 | Assert.IsTrue(Directory.Exists(recvRoot), "Recieve root directory not created."); 395 | Assert.IsTrue(Directory.Exists(Path.Combine(recvRoot, "bar")), "Sub directory 'bar' not created."); 396 | 397 | Assert.IsTrue(File.ReadAllText(Path.Combine(recvRoot, "foo.txt")).Contains("the quick brown fox"), "Top level file is missing expected content."); 398 | Assert.IsTrue(File.ReadAllText(Path.Combine(recvRoot, "bar", "baz.txt")).Contains("he broke a new shoelace"), "Nested file is missing content."); 399 | } 400 | 401 | [TestMethod] 402 | public void Client_UploadFileAsync() 403 | { 404 | // determine file paths 405 | var fileToSend = Path.GetTempPath() + "sampleFileToSend"; 406 | var dirToRecv = Path.Combine(Path.GetTempPath(), "sampleRecvDir"); 407 | var fileToRecv = Path.Combine(dirToRecv, "sampleFileToSend"); 408 | 409 | Directory.CreateDirectory(dirToRecv); 410 | 411 | // create 100 MB random data, and write it to our file. 412 | byte[] data = new byte[100 * 1024 * 1024]; 413 | Random rng = new Random(); 414 | rng.NextBytes(data); 415 | File.WriteAllBytes(fileToSend, data); 416 | 417 | // perform the upload 418 | var task = client.UploadAsync(fileToSend, dirToRecv, true); 419 | 420 | Assert.IsTrue(task.Wait(10000), "UploadFileAsync took longer than expected."); 421 | Assert.IsTrue(task.IsCompleted && !task.IsFaulted, "UploadFileAsync task failed to complete without error."); 422 | 423 | Thread.Sleep(500); // give time for writes to disk to complete. 424 | byte[] receivedData = File.ReadAllBytes(fileToRecv); 425 | Assert.IsTrue(data.SequenceEqual(receivedData), "Data received and data sent do not match."); 426 | 427 | // if we get to this point, delete extra files 428 | File.Delete(fileToSend); 429 | File.Delete(fileToRecv); 430 | 431 | Directory.Delete(dirToRecv); 432 | } 433 | 434 | [TestMethod] 435 | public void Client_DownloadFile() 436 | { 437 | // determine file paths 438 | var fileToSend = Path.GetTempPath() + "sampleFileToSend"; 439 | var dirToRecv = Path.Combine(Path.GetTempPath(), "sampleRecvDir"); 440 | var fileToRecv = Path.Combine(dirToRecv, "sampleFileToSend"); 441 | 442 | Directory.CreateDirectory(dirToRecv); 443 | 444 | // create 100 MB random data, and write it to our file. 445 | byte[] data = new byte[100 * 1024 * 1024]; 446 | Random rng = new Random(); 447 | rng.NextBytes(data); 448 | File.WriteAllBytes(fileToSend, data); 449 | 450 | // perform the upload 451 | client.Download(fileToSend, dirToRecv, true); 452 | 453 | Thread.Sleep(500); // give time for writes to disk to complete. 454 | byte[] receivedData = File.ReadAllBytes(fileToRecv); 455 | Assert.IsTrue(data.SequenceEqual(receivedData), "Data received and data sent do not match."); 456 | 457 | // if we get to this point, delete extra files 458 | File.Delete(fileToSend); 459 | File.Delete(fileToRecv); 460 | 461 | Directory.Delete(dirToRecv); 462 | } 463 | 464 | [TestMethod] 465 | public void Client_DownloadFile_SpecifyPort() 466 | { 467 | 468 | // determine file paths 469 | var fileToSend = Path.GetTempPath() + "sampleFileToSend"; 470 | var dirToRecv = Path.Combine(Path.GetTempPath(), "sampleRecvDir"); 471 | var fileToRecv = Path.Combine(dirToRecv, "sampleFileToSend"); 472 | 473 | Directory.CreateDirectory(dirToRecv); 474 | 475 | // create 100 MB random data, and write it to our file. 476 | byte[] data = new byte[100 * 1024 * 1024]; 477 | Random rng = new Random(); 478 | rng.NextBytes(data); 479 | File.WriteAllBytes(fileToSend, data); 480 | 481 | // perform the upload 482 | client.Download(fileToSend, dirToRecv, true, 9099); 483 | 484 | Thread.Sleep(500); // give time for writes to disk to complete. 485 | byte[] receivedData = File.ReadAllBytes(fileToRecv); 486 | Assert.IsTrue(data.SequenceEqual(receivedData), "Data received and data sent do not match."); 487 | 488 | // if we get to this point, delete extra files 489 | File.Delete(fileToSend); 490 | File.Delete(fileToRecv); 491 | 492 | Directory.Delete(dirToRecv); 493 | } 494 | 495 | [TestMethod] 496 | public void Client_GetVersion() 497 | { 498 | var version = client.GetVersion(); 499 | 500 | Assert.IsTrue(version.Length > 0, "Version string isn't the right length"); 501 | Assert.IsTrue(version.Where((a) => a == '.').Count() >= 2, "Version string doesn't have right format"); 502 | } 503 | 504 | [TestMethod] 505 | public void Client_GetHeartbeat() 506 | { 507 | var res = client.GetHeartbeat(); 508 | Assert.IsTrue(res, "Heartbeat failed to return true."); 509 | } 510 | 511 | [TestMethod] 512 | public void Client_GetVersionUsingHostname() 513 | { 514 | RpcClient myclient = new RpcClient("localhost", 8000); 515 | var version = client.GetVersion(); 516 | 517 | Assert.IsTrue(version.Length > 0, "Version string isn't the right length"); 518 | Assert.IsTrue(version.Where((a) => a == '.').Count() >= 2, "Version string doesn't have right format"); 519 | } 520 | 521 | [TestMethod] 522 | public void Client_BadHostnameLookup() 523 | { 524 | RpcClient myclient; 525 | var exception = Assert.ThrowsException( () => myclient = new RpcClient("ThisIsAFakeHostName", 8000, 10000), 526 | "System didn't fail when resolving an invalid hostname."); 527 | 528 | } 529 | 530 | [TestMethod] 531 | public void Client_BadEndpointConnection() 532 | { 533 | // there is no server on port 9000 534 | RpcClient myclient = new RpcClient("localhost", 9000); 535 | 536 | var exception = Assert.ThrowsException(() => myclient.GetVersion(), 537 | "System didn't fail when connecting to an invalid endpoint."); 538 | } 539 | 540 | [TestMethod] 541 | public void Client_UploadDirectory() 542 | { 543 | var sampleRoot = PrepareDirectoryTests(); 544 | var sendRoot = Path.Combine(sampleRoot, "sent"); 545 | var recvRoot = Path.Combine(sampleRoot, "received"); 546 | Directory.CreateDirectory(recvRoot); 547 | 548 | client.Upload(sendRoot, recvRoot, true); 549 | 550 | Thread.Sleep(1000); 551 | 552 | //Assert.IsTrue(Directory.Exists(recvRoot), "Recieve root directory not created."); //test is only valid if we generate the top level folder (we don't right now) 553 | Assert.IsTrue(Directory.Exists(Path.Combine(recvRoot, "bar")), "Sub directory 'bar' not created."); 554 | 555 | Assert.IsTrue(File.ReadAllText(Path.Combine(recvRoot, "foo.txt")).Contains("the quick brown fox"), "Top level file is missing expected content."); 556 | Assert.IsTrue(File.ReadAllText(Path.Combine(recvRoot, "bar", "baz.txt")).Contains("he broke a new shoelace"), "Nested file is missing content."); 557 | } 558 | 559 | [TestMethod] 560 | public void Client_Download_UseDirectory() 561 | { 562 | var sampleRoot = PrepareDirectoryTests(); 563 | var sendRoot = Path.Combine(sampleRoot, "sent"); 564 | var recvRoot = Path.Combine(sampleRoot, "received"); 565 | Directory.CreateDirectory(recvRoot); 566 | 567 | client.Download(sendRoot, recvRoot, true); 568 | 569 | Thread.Sleep(1000); 570 | 571 | //Assert.IsTrue(Directory.Exists(recvRoot), "Recieve root directory not created."); //test is only valid if we generate the top level folder (we don't right now) 572 | Assert.IsTrue(Directory.Exists(Path.Combine(recvRoot, "bar")), "Sub directory 'bar' not created."); 573 | 574 | Assert.IsTrue(File.ReadAllText(Path.Combine(recvRoot, "foo.txt")).Contains("the quick brown fox"), "Top level file is missing expected content."); 575 | Assert.IsTrue(File.ReadAllText(Path.Combine(recvRoot, "bar", "baz.txt")).Contains("he broke a new shoelace"), "Nested file is missing content."); 576 | } 577 | 578 | [TestMethod] 579 | public void Client_Download_DownloadFileWithGlobExp() 580 | { 581 | // determine file paths 582 | var fileToSend = Path.GetTempPath() + "sampleFileToSend"; 583 | var dirToRecv = Path.Combine(Path.GetTempPath(), "sampleRecvDir"); 584 | var fileToRecv = Path.Combine(dirToRecv, "sampleFileToSend"); 585 | 586 | Directory.CreateDirectory(dirToRecv); 587 | 588 | // create 100 MB random data, and write it to our file. 589 | byte[] data = new byte[100 * 1024 * 1024]; 590 | Random rng = new Random(); 591 | rng.NextBytes(data); 592 | File.WriteAllBytes(fileToSend, data); 593 | 594 | // perform the upload 595 | client.Download(Path.GetTempPath() + "sampleFileTo*", dirToRecv, true); 596 | 597 | Thread.Sleep(500); // give time for writes to disk to complete. 598 | byte[] receivedData = File.ReadAllBytes(fileToRecv); 599 | Assert.IsTrue(data.SequenceEqual(receivedData), "Data received and data sent do not match."); 600 | 601 | // if we get to this point, delete extra files 602 | File.Delete(fileToSend); 603 | File.Delete(fileToRecv); 604 | 605 | Directory.Delete(dirToRecv); 606 | } 607 | 608 | [TestMethod] 609 | public void Client_Download_DownloadMultipleWithGlobExp() 610 | { 611 | var sampleRoot = PrepareDirectoryTests(); 612 | var sendRoot = Path.Combine(sampleRoot, "sent"); 613 | var recvRoot = Path.Combine(sampleRoot, "received"); 614 | Directory.CreateDirectory(recvRoot); 615 | 616 | // this gives us send/foo.txt, send/bar, and send/bar/baz.txt 617 | // we'll now create send/bat.txt and try to receive send/ba* 618 | File.WriteAllText(Path.Combine(sendRoot, "bat.txt"), "I am a file named bat.txt"); 619 | 620 | // try to download all of send/ba, which should include everything in bar and bat.txt 621 | // but not foo 622 | var bytesFetched = client.Download(Path.Combine(sendRoot, "ba*"), recvRoot, true); 623 | 624 | // confirm we received bar and bat, not foo 625 | Assert.IsTrue(File.Exists(Path.Combine(recvRoot, "bat.txt"))); 626 | Assert.IsTrue(Directory.Exists(Path.Combine(recvRoot, "bar"))); 627 | Assert.IsFalse(File.Exists(Path.Combine(recvRoot, "foo.txt"))); 628 | 629 | // confirm we received bar/baz 630 | Assert.IsTrue(File.Exists(Path.Combine(recvRoot, "bar", "baz.txt"))); 631 | 632 | // confirm sizes of what was sent vs received 633 | var totalBytes = Directory.GetFiles(recvRoot, "*", SearchOption.AllDirectories) 634 | .Select(x => (new FileInfo(x).Length)) 635 | .Sum(); 636 | Assert.IsTrue(bytesFetched == totalBytes); 637 | } 638 | 639 | [TestMethod] 640 | [DeploymentItem(@"PluginExample.dll")] 641 | public void Client_LoadAndRunExtension() 642 | { 643 | client.PluginLoad("testId", "PluginExample.SimpleTest", "PluginExample.dll"); 644 | var res = client.PluginCallMethod("testId", "SayHiToMe", "FOO"); 645 | Assert.IsTrue(res == "Hello FOO", "Result string was not correct."); 646 | } 647 | 648 | [TestMethod] 649 | [DeploymentItem(@"PluginExample.dll")] 650 | public void Client_LoadAndRunExtensionWithNullReturn() 651 | { 652 | client.PluginLoad("testId", "PluginExample.SimpleTest", "PluginExample.dll"); 653 | var res = client.PluginCallMethod("testId", "WriteToConsole"); 654 | Assert.IsNull(res, "Function returned a value, even though the function should have returned null"); 655 | } 656 | 657 | [TestMethod] 658 | [DeploymentItem(@"PluginExample.dll")] 659 | public void Client_InvalidExtensionLoad() 660 | { 661 | var ex = Assert.ThrowsException(() => client.PluginLoad("testId", "PluginExample.SimpleTest", "Not_A_Path.dll")); 662 | Assert.IsTrue(ex.Message.Contains("IOException"), "The exception from the server didn't have the expected IOException in the message."); 663 | } 664 | 665 | [TestMethod] 666 | public void Client_IsServerRunningAsAdmin() 667 | { 668 | var isTestRunningAsAdmin = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); 669 | Assert.AreEqual(isTestRunningAsAdmin, client.GetIsRunningAsAdmin(), "Server did not return a correct response for if it was running as admin."); 670 | } 671 | 672 | [TestMethod] 673 | public void Client_GetServerPath() 674 | { 675 | var thisAssemblyLocation = System.Reflection.Assembly.GetExecutingAssembly().Location; 676 | var reportedLocation = client.GetServerLocation(); 677 | 678 | Assert.AreEqual(Path.GetDirectoryName(reportedLocation), Path.GetDirectoryName(thisAssemblyLocation), 679 | "Entry assembly locations don't match."); 680 | } 681 | 682 | [TestMethod] 683 | public void Client_RunProcessAndKill_ExpectCompletionCallback() 684 | { 685 | // for this, we need a job that will last several seconds, and generate output. 686 | // so we're going to send a powershell script that just prints 0 to 4, each 687 | // with a one second gap. 688 | // Powershell: for ($i=0; $i -lt 5; $i++) { Write-Host "Message $i"; Start-Sleep -Seconds 1 } 689 | // Base64 Script (UTF16LE): ZgBvAHIAIAAoACQAaQA9ADAAOwAgACQAaQAgAC0AbAB0ACAANQA7ACAAJABpACsAKwApACAAewAgAFcAcgBpAHQAZQAtAEgAbwBzAHQAIAAiAE0AZQBzAHMAYQBnAGUAIAAkAGkAIgA7ACAAUwB0AGEAcgB0AC0AUwBsAGUAZQBwACAALQBTAGUAYwBvAG4AZABzACAAMQAgAH0A 690 | 691 | var powershellScript = "ZgBvAHIAIAAoACQAaQA9ADAAOwAgACQAaQAgAC0AbAB0ACAANQA7ACAAJABpACsAKwApACAAewAgAFcAcgBpAHQAZQAtAEgAbwBzAHQAIAAiAE0AZQBzAHMAYQBnAGUAIAAkAGkAIgA7ACAAUwB0AGEAcgB0AC0AUwBsAGUAZQBwACAALQBTAGUAYwBvAG4AZABzACAAMQAgAH0A"; 692 | 693 | AutoResetEvent evt = new AutoResetEvent(false); 694 | Action cb = (jobid) => 695 | { 696 | evt.Set(); 697 | }; 698 | 699 | int newJobId = client.StartJobWithNotification("powershell.exe", $"-EncodedCommand {powershellScript}", cb); 700 | Thread.Sleep(2000); 701 | var killJobStatus = client.StopJob(newJobId); 702 | Assert.IsTrue(killJobStatus, "Failed to kill remote job."); 703 | Assert.IsTrue(evt.WaitOne(10000), "Completion callback never received."); 704 | } 705 | 706 | 707 | private bool CheckIfTrueInTimelimit(Func test, int timeout = 5000) 708 | { 709 | var sw = new Stopwatch(); 710 | sw.Start(); 711 | 712 | do 713 | { 714 | if (test()) return true; 715 | } while (sw.ElapsedMilliseconds < timeout); 716 | 717 | return false; 718 | 719 | } 720 | 721 | public static string PrepareDirectoryTests() 722 | { 723 | var sampleBasePath = Path.Combine(Path.GetTempPath(), "TestDirUpDown"); 724 | var sendPath = Path.Combine(sampleBasePath, "sent"); 725 | 726 | if (Directory.Exists(sampleBasePath)) { (new DirectoryInfo(sampleBasePath)).Delete(true); } 727 | Directory.CreateDirectory(sendPath); 728 | Directory.CreateDirectory(Path.Combine(sendPath, "bar")); //nested directory for testing 729 | 730 | File.WriteAllText(Path.Combine(sendPath, "foo.txt"), "the quick brown fox jumped over the stream."); 731 | File.WriteAllText(Path.Combine(sendPath, "bar", "baz.txt"), "he broke a new shoelace that day"); 732 | 733 | return sampleBasePath; 734 | } 735 | } 736 | } 737 | -------------------------------------------------------------------------------- /DUTRemoteTests/DUTRemoteTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6 5 | true 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /DUTRemoteTests/ExtensionTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using System.Threading.Tasks; 7 | using System.Threading; 8 | using System.Net.Sockets; 9 | using System.IO; 10 | using Newtonsoft.Json; 11 | using System.Collections.Generic; 12 | using SimpleDUTRemote; 13 | using System.Diagnostics; 14 | using System.Linq; 15 | 16 | namespace DUTRemoteTests 17 | { 18 | [TestClass] 19 | public class ExtensionTests 20 | { 21 | [TestMethod] 22 | [DeploymentItem(@"PluginExample.dll")] 23 | public void Extensions_LoadAndCallDLL_ReturnTrue() 24 | { 25 | var functions = new Functions(); 26 | 27 | functions.PluginLoad("testId", "PluginExample.SimpleTest", "PluginExample.dll"); 28 | 29 | bool res = (bool) functions.PluginCallMethod("testId", "IsNumberEven", 2); 30 | 31 | Assert.IsTrue(res, "Test failed, plugin thought 2 is odd."); 32 | 33 | } 34 | 35 | [TestMethod] 36 | [DeploymentItem(@"PluginExample.dll")] 37 | public void Extensions_LoadAndCallDLL_WriteToConsole() 38 | { 39 | var functions = new Functions(); 40 | functions.PluginLoad("testId", "PluginExample.SimpleTest", "PluginExample.dll"); 41 | var res = functions.PluginCallMethod("testId", "WriteToConsole"); 42 | 43 | Assert.IsNull(res, "Result was not null, but the function is type void."); 44 | } 45 | 46 | [TestMethod] 47 | [DeploymentItem(@"PluginExample.dll")] 48 | public void Extensions_LoadAndCallDLL_ReturnString() 49 | { 50 | var functions = new Functions(); 51 | functions.PluginLoad("testId", "PluginExample.SimpleTest", "PluginExample.dll"); 52 | var res = (string) functions.PluginCallMethod("testId", "SayHiToMe", "FOO"); 53 | Assert.IsTrue(res == "Hello FOO", "Result string was not correct."); 54 | } 55 | 56 | } 57 | } -------------------------------------------------------------------------------- /DUTRemoteTests/JobTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using Newtonsoft.Json; 6 | using Newtonsoft.Json.Linq; 7 | using SimpleDUTRemote; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Diagnostics; 11 | using System.IO; 12 | using System.Linq; 13 | using System.Net; 14 | using System.Net.Sockets; 15 | using System.Text; 16 | using System.Threading.Tasks; 17 | using System.Threading; 18 | using SimpleJsonRpc; 19 | 20 | namespace DUTRemoteTests 21 | { 22 | [TestClass] 23 | public class JobTests 24 | { 25 | Functions remoteFunctions; 26 | 27 | public JobTests() 28 | { 29 | remoteFunctions = new Functions(); 30 | } 31 | 32 | [TestMethod] 33 | public void Job_StartJob() 34 | { 35 | var id = remoteFunctions.StartJob("notepad.exe"); 36 | Assert.IsFalse(remoteFunctions.IsJobComplete(id)); 37 | 38 | // confirm IE started 39 | var procList = Process.GetProcessesByName("notepad"); 40 | Assert.IsTrue(procList.Length > 0, "No notepad instance found."); 41 | Assert.IsTrue(procList.First().HasExited == false); 42 | 43 | // confirm job is marked as running 44 | Assert.IsTrue(!remoteFunctions.IsJobComplete(id), "Job factory thinks task already exited while it is active."); 45 | 46 | // terminate process 47 | remoteFunctions.StopJob(id); 48 | Assert.IsTrue(procList.First().HasExited, "Notepad is still running, even after terminating the job."); 49 | } 50 | 51 | [TestMethod] 52 | public void Job_CheckOutput() 53 | { 54 | var id = remoteFunctions.StartJob("systeminfo.exe"); 55 | 56 | Assert.IsTrue(!remoteFunctions.IsJobComplete(id), "Job marked as exitted before it could have reasonably completed."); 57 | 58 | Stopwatch sw = new Stopwatch(); 59 | sw.Start(); 60 | 61 | while (sw.ElapsedMilliseconds < 10 * 1000) 62 | { 63 | if (remoteFunctions.IsJobComplete(id)) { break; } 64 | Task.Delay(1000).Wait(); 65 | } 66 | sw.Stop(); 67 | 68 | Assert.IsTrue(remoteFunctions.IsJobComplete(id), "Job did not finish within reasonable time."); 69 | 70 | var result = remoteFunctions.GetJobResult(id); 71 | Assert.IsTrue(result.Length > 0, "Length from command is 0 characters - this is incorrect."); 72 | Assert.IsTrue(result.Contains("OS Name:"), "Result doesn't contain expected items."); 73 | } 74 | 75 | [TestMethod] 76 | public void Job_CheckCallback() 77 | { 78 | var self = IPAddress.Loopback; 79 | var port = 0; 80 | string receivedMessage = null; 81 | 82 | var server = new TcpListener(self, port); 83 | server.Start(1); 84 | port = ((IPEndPoint)server.Server.LocalEndPoint).Port; 85 | var client = server.AcceptTcpClientAsync(); 86 | 87 | var id = remoteFunctions.StartJobWithNotification(self.ToString(), port, "systeminfo.exe"); 88 | 89 | if (!client.Wait(10 * 1000)) 90 | { 91 | server.Stop(); 92 | Assert.Fail("Timed out waiting for callback."); 93 | } 94 | 95 | using (var reader = new StreamReader(client.Result.GetStream())) 96 | { 97 | receivedMessage = reader.ReadToEnd(); 98 | } 99 | client.Result.Dispose(); 100 | 101 | Assert.IsTrue(receivedMessage == $"JOB {id} COMPLETED", "Callback has wrong data"); 102 | Assert.IsTrue(remoteFunctions.IsJobComplete(id)); 103 | 104 | } 105 | 106 | [TestMethod] 107 | public void Job_CheckStreamingProgress_ExpectSuccess() 108 | { 109 | var old_files = Directory.GetFiles(Path.GetTempPath()).Where((x) => x.Contains("SimpleRemote-JobOutput-")); 110 | old_files.ToList().ForEach(x => File.Delete(x)); 111 | 112 | var self = IPAddress.Loopback; 113 | var completionPort = 0; 114 | var progressPort = 0; 115 | string receivedMessage = null; 116 | 117 | // completion server items 118 | var completionServer = new TcpListener(self, completionPort); 119 | completionServer.Start(1); 120 | completionPort = ((IPEndPoint)completionServer.Server.LocalEndPoint).Port; 121 | var completionClient = completionServer.AcceptTcpClientAsync(); 122 | 123 | // progress server items 124 | var progressServer = new TcpListener(self, progressPort); 125 | progressServer.Start(1); 126 | progressPort = ((IPEndPoint)progressServer.Server.LocalEndPoint).Port; 127 | var progressClient = progressServer.AcceptTcpClientAsync(); 128 | 129 | // start streaming job 130 | var id = remoteFunctions.StartJobWithProgress(self.ToString(), completionPort, progressPort, "systeminfo.exe"); 131 | 132 | // wait for completion 133 | 134 | if (!completionClient.Wait(10 * 1000)) 135 | { 136 | completionServer.Stop(); 137 | progressServer.Stop(); 138 | Assert.Fail("Timed out waiting for callback."); 139 | } 140 | 141 | remoteFunctions.GetJobResult(id); 142 | 143 | using (var reader = new StreamReader(progressClient.Result.GetStream())) 144 | { 145 | receivedMessage = reader.ReadToEnd(); 146 | } 147 | 148 | // clean all sockets. 149 | progressClient.Result.Dispose(); 150 | completionClient.Result.Dispose(); 151 | 152 | progressServer.Stop(); 153 | completionServer.Stop(); 154 | 155 | Assert.IsTrue(receivedMessage.Contains("OS Name:"), "Missing expected text from progress stream."); 156 | 157 | // confirm there's a log file. 158 | var files = Directory.GetFiles(Path.GetTempPath()).Where((x) => x.Contains("SimpleRemote-JobOutput-")); 159 | Assert.IsTrue(files.Count() == 1, "Incorrect number of fallback logs found."); 160 | var file = files.First(); 161 | 162 | var fileText = File.ReadAllText(file); 163 | Assert.IsTrue(fileText.Contains("OS Name:"), "Missing expected text from progress file."); 164 | Assert.IsTrue(fileText.Contains("systeminfo.exe"), "Missing called process name from progress file."); 165 | } 166 | 167 | [TestMethod] 168 | public void Job_CheckStreamingProgress_KillProcess() 169 | { 170 | var old_files = Directory.GetFiles(Path.GetTempPath()).Where((x) => x.Contains("SimpleRemote-JobOutput-")); 171 | old_files.ToList().ForEach(x => File.Delete(x)); 172 | 173 | var self = IPAddress.Loopback; 174 | var completionPort = 0; 175 | var progressPort = 0; 176 | string receivedMessage = null; 177 | 178 | // completion server items 179 | var completionServer = new TcpListener(self, completionPort); 180 | completionServer.Start(1); 181 | completionPort = ((IPEndPoint)completionServer.Server.LocalEndPoint).Port; 182 | var completionClient = completionServer.AcceptTcpClientAsync(); 183 | 184 | // progress server items 185 | var progressServer = new TcpListener(self, progressPort); 186 | progressServer.Start(1); 187 | progressPort = ((IPEndPoint)progressServer.Server.LocalEndPoint).Port; 188 | var progressClient = progressServer.AcceptTcpClientAsync(); 189 | 190 | // start streaming job 191 | var id = remoteFunctions.StartJobWithProgress(self.ToString(), completionPort, progressPort, "systeminfo.exe"); 192 | // wait 500ms and kill it 193 | Thread.Sleep(500); 194 | remoteFunctions.StopJob(id); 195 | 196 | // we should still get a completion callback (the process will die) 197 | 198 | if (!completionClient.Wait(5 * 1000)) 199 | { 200 | completionServer.Stop(); 201 | progressServer.Stop(); 202 | Assert.Fail("Missing completion callback."); 203 | } 204 | 205 | // clean all sockets. 206 | progressClient.Result.Dispose(); 207 | completionClient.Result.Dispose(); 208 | 209 | progressServer.Stop(); 210 | completionServer.Stop(); 211 | 212 | // confirm there's a log file. 213 | var files = Directory.GetFiles(Path.GetTempPath()).Where((x) => x.Contains("SimpleRemote-JobOutput-")); 214 | Assert.IsTrue(files.Count() == 1, "Incorrect number of fallback logs found."); 215 | var file = files.First(); 216 | 217 | var fileText = File.ReadAllText(file); 218 | Assert.IsTrue(fileText.Contains("systeminfo.exe"), "Missing called process name from progress file."); 219 | } 220 | 221 | [TestMethod] 222 | public void Job_CheckStreamingProgress_ExpectFileFallback_NoListener() 223 | { 224 | var old_files = Directory.GetFiles(Path.GetTempPath()).Where((x) => x.Contains("SimpleRemote-JobOutput-")); 225 | old_files.ToList().ForEach(x => File.Delete(x)); 226 | 227 | var self = IPAddress.Loopback; 228 | var completionPort = 0; 229 | var progressPort = 13000; 230 | string receivedMessage = null; 231 | 232 | // completion server items 233 | var completionServer = new TcpListener(self, completionPort); 234 | completionServer.Start(1); 235 | completionPort = ((IPEndPoint)completionServer.Server.LocalEndPoint).Port; 236 | var completionClient = completionServer.AcceptTcpClientAsync(); 237 | 238 | // progress server items 239 | // N/A - we're testing what happens when we're not listening 240 | 241 | // start streaming job 242 | var id = remoteFunctions.StartJobWithProgress(self.ToString(), completionPort, progressPort, "systeminfo.exe"); 243 | 244 | // wait for completion 245 | 246 | if (!completionClient.Wait(10 * 1000)) 247 | { 248 | completionServer.Stop(); 249 | Assert.Fail("Timed out waiting for callback."); 250 | } 251 | 252 | remoteFunctions.GetJobResult(id); 253 | 254 | // confirm there's a log file. 255 | var files = Directory.GetFiles(Path.GetTempPath()).Where((x) => x.Contains("SimpleRemote-JobOutput-")); 256 | Assert.IsTrue(files.Count() == 1, $"Incorrect number of fallback logs found: {files.Count()} instead of 1"); 257 | var file = files.First(); 258 | 259 | Assert.IsTrue(DateTime.Now.Subtract(File.GetLastWriteTime(file)).Seconds < 10, "File found is too old."); 260 | 261 | // read the log file 262 | receivedMessage = File.ReadAllText(file); 263 | 264 | // clean all sockets. 265 | completionClient.Result.Dispose(); 266 | completionServer.Stop(); 267 | 268 | Assert.IsTrue(receivedMessage.Contains("OS Name:"), "Missing expected text from progress file."); 269 | 270 | File.Delete(file); 271 | 272 | } 273 | 274 | [TestMethod] 275 | public void Job_CheckStreamingProgress_ExpectFileFallback_ClientClosedConnectionEarly() 276 | { 277 | var old_files = Directory.GetFiles(Path.GetTempPath()).Where((x) => x.Contains("SimpleRemote-JobOutput-")); 278 | old_files.ToList().ForEach(x => File.Delete(x)); 279 | 280 | var self = IPAddress.Loopback; 281 | var completionPort = 0; 282 | var progressPort = 0; 283 | string receivedMessage = null; 284 | 285 | // completion server items 286 | var completionServer = new TcpListener(self, completionPort); 287 | completionServer.Start(1); 288 | completionPort = ((IPEndPoint)completionServer.Server.LocalEndPoint).Port; 289 | var completionClient = completionServer.AcceptTcpClientAsync(); 290 | 291 | // progress server items 292 | var progressServer = new TcpListener(self, progressPort); 293 | progressServer.Start(1); 294 | progressPort = ((IPEndPoint)progressServer.Server.LocalEndPoint).Port; 295 | var progressClient = progressServer.AcceptSocketAsync(); 296 | 297 | // close the client as soon as it's connected. 298 | progressClient.ContinueWith((t) => t.Result.Close()); 299 | 300 | // start streaming job 301 | var id = remoteFunctions.StartJobWithProgress(self.ToString(), completionPort, progressPort, "systeminfo.exe"); 302 | 303 | // wait for completion 304 | if (!completionClient.Wait(10 * 1000)) 305 | { 306 | completionServer.Stop(); 307 | progressServer.Stop(); 308 | Assert.Fail("Timed out waiting for callback."); 309 | } 310 | 311 | remoteFunctions.GetJobResult(id); 312 | 313 | // confirm there's a fallback file. 314 | var files = Directory.GetFiles(Path.GetTempPath()).Where((x) => x.Contains("SimpleRemote-JobOutput-")); 315 | Assert.IsTrue(files.Count() == 1, $"Incorrect number of fallback logs found: {files.Count()} instead of 1"); 316 | var file = files.First(); 317 | 318 | Assert.IsTrue(DateTime.Now.Subtract(File.GetLastWriteTime(file)).Seconds < 10, "File found is too old."); 319 | 320 | // read the fallback file 321 | receivedMessage += File.ReadAllText(file); 322 | 323 | // clean all sockets. 324 | progressClient.Result.Dispose(); 325 | completionClient.Result.Dispose(); 326 | 327 | progressServer.Stop(); 328 | completionServer.Stop(); 329 | 330 | Assert.IsTrue(receivedMessage.Contains("OS Name:"), "Missing expected text from progress file."); 331 | } 332 | 333 | [TestMethod] 334 | public void Job_CheckCallback_AutodetectAddress_ExpectFail() 335 | { 336 | var self = IPAddress.Loopback; 337 | var port = 13000; 338 | string receivedMessage = null; 339 | 340 | Assert.ThrowsException(() => remoteFunctions.StartJobWithNotification(null, port, "systeminfo.exe")); 341 | } 342 | 343 | [TestMethod] 344 | public void Job_CheckCallback_AutodetectAddress_ExpectSuccess() 345 | { 346 | var port = 14000; 347 | string receivedMessage = null; 348 | 349 | // code to listen for callback 350 | var server = new TcpListener(IPAddress.Any, port); 351 | server.Start(1); 352 | var callbackClient = server.AcceptTcpClientAsync(); 353 | 354 | // start up the server for this specific test 355 | var rpcserver = new SimpleJsonRpc.SimpleRpcServer(); 356 | var rpcFunctions = new SimpleDUTRemote.Functions(); 357 | rpcserver.Register(rpcFunctions); 358 | var rpcserverTask = rpcserver.Start(); 359 | 360 | string output; 361 | var client = new TcpClient(); 362 | client.ConnectAsync("localhost", 8000).Wait(); 363 | 364 | using (client) 365 | using (var rstream = new StreamReader(client.GetStream())) 366 | using (var wstream = new StreamWriter(client.GetStream())) 367 | { 368 | var request = new JsonRpcRequest(); 369 | request.method = "StartJobWithNotification"; 370 | request.args = new List() { null, port, "systeminfo.exe" }; 371 | wstream.WriteLine(JsonConvert.SerializeObject(request) + "\r\n"); 372 | wstream.Flush(); 373 | 374 | output = rstream.ReadToEnd(); 375 | } 376 | 377 | JObject resp = JObject.Parse(output); 378 | int id = (int) resp["result"].ToObject(); 379 | 380 | 381 | if (!callbackClient.Wait(10 * 1000)) 382 | { 383 | server.Stop(); 384 | Assert.Fail("Timed out waiting for callback."); 385 | } 386 | 387 | using (var reader = new StreamReader(callbackClient.Result.GetStream())) 388 | { 389 | receivedMessage = reader.ReadToEnd(); 390 | } 391 | callbackClient.Result.Dispose(); 392 | 393 | Assert.IsTrue(receivedMessage == $"JOB {id} COMPLETED", "Callback has wrong data"); 394 | 395 | } 396 | 397 | 398 | 399 | [TestMethod] 400 | public void JobFactory_TestCallbackNoListener() 401 | { 402 | var self = IPAddress.Loopback; 403 | var port = 13000; 404 | string receivedMessage = null; 405 | 406 | // but don't create a listener - the connect method should time out, and continue 407 | 408 | var id = remoteFunctions.StartJobWithNotification(self.ToString(), port, "systeminfo.exe"); 409 | 410 | Stopwatch sw = new Stopwatch(); 411 | sw.Start(); 412 | 413 | while (sw.ElapsedMilliseconds < 10 * 1000) 414 | { 415 | if (remoteFunctions.IsJobComplete(id)) { break; } 416 | Task.Delay(1000).Wait(); 417 | } 418 | sw.Stop(); 419 | 420 | Assert.IsTrue(remoteFunctions.IsJobComplete(id)); 421 | 422 | var result = remoteFunctions.GetJobResult(id); 423 | 424 | Assert.IsTrue(result.Length > 0, "Length of output from command is 0"); 425 | Assert.IsTrue(result.Contains("OS Name:"), "Result doesn't contain expected items."); 426 | } 427 | 428 | [TestMethod] 429 | public void Job_GetAllJobs() 430 | { 431 | var notepadId = remoteFunctions.StartJob("notepad.exe"); 432 | var sysinfoId = remoteFunctions.StartJob("systeminfo.exe"); 433 | 434 | while (!remoteFunctions.IsJobComplete(sysinfoId)) 435 | { 436 | Task.Delay(1000).Wait(); 437 | } 438 | 439 | // sysinfo should now be done, notepad should still be up. 440 | var currentJobs = remoteFunctions.GetAllJobs(); 441 | 442 | Assert.IsTrue(currentJobs.ContainsKey(notepadId), "Missing notepad job id."); 443 | Assert.IsTrue(currentJobs.ContainsKey(sysinfoId), "Midding system info job id."); 444 | 445 | Assert.IsTrue(currentJobs[notepadId] == false, "Notepad marked as ended, even through it should still be running."); 446 | Assert.IsTrue(currentJobs[sysinfoId] == true, "System Info marked as running, even through it should have completed."); 447 | 448 | remoteFunctions.StopJob(notepadId); 449 | 450 | 451 | } 452 | 453 | 454 | } 455 | } 456 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /MainDocPage.md: -------------------------------------------------------------------------------- 1 | Simple DUT Remote is a framework designed to make automation easier for subsystems. 2 | It does this by providing a simple communications framework for communicating with devices under test. if you need to interact with your DUT as 3 | part of your testing, this framework can help you. 4 | 5 | # I want to get started right away! # 6 | There is a tutorial located here, which includes examples and how to control to the server via PowerShell: 7 | [Simple DUT Remote Tutorial](extra_docs/tutorial.md) 8 | 9 | Client DLL documentation is located here: SimpleDUTClientLibrary.RpcClient 10 | 11 | Server documentation is located within the SimpleDUTRemote namespace. Functions on the server that can be called via RPC are located here: SimpleDUTRemote.Functions 12 | 13 | If you're looking for documentation on the JSON-RPC server, which can be used on its own, additional 14 | information on that library is located here: [SimpleJsonRpc Library Docs](extra_docs/rpc_tutorial.md) 15 | 16 | # Building and Testing the Code # 17 | 18 | ## Building Everything ## 19 | If you want to build everything, just run `BuildAll.bat` from the project root directory - this will produce client binaries 20 | for x64, will generate server binaries for supported architectures, will build the documentation (if doxygen is installed), 21 | and will build an installer package (if InnoInstall is installed). 22 | 23 | `BuildAll.bat` relies on the command line .NET Core tools [that are available here](https://www.microsoft.com/net/core#windowscmd). 24 | 25 | ## Building individual items and running tests ## 26 | Most projects within this solution dual target the .NET Core Framework (v2.0) and .NET Framework (v4.6). 27 | Visual Studio 2017.3 has native support for both targets, and is a simple, graphical way to build the solution. 28 | 29 | Alternatively, you can use the command line .NET Core tools [that are available here](https://www.microsoft.com/net/core#windowscmd). 30 | 31 | This project relies on both the NLog and the Newtonsoft.Json frameworks for log management and json file parsing. When opening the project, 32 | Visual Studio should automatically fetch any packages that aren't on your system already. If using the command line, use the `dotnet restore` 33 | command. 34 | 35 | ### Generating Self Contained EXEs ### 36 | For .NET Core targets, you will want to compile as a self-contained deployment to generate an exe, which is done by specifying a runtime identifier (the `-r` option). 37 | In general, it is easier to do this from the command line. 38 | 39 | To compile to Windows 10-x64, run this from the same directory as the .csproj file: `dotnet publish -c Release -r win10-x64 -f net6`. 40 | 41 | For instructions on how to compile for other platforms, [follow the instructions here](https://docs.microsoft.com/en-us/dotnet/core/deploying/deploy-with-cli). 42 | 43 | ### Running Tests ### 44 | All functions on the server, and most functions on the client, have unit tests written for them. Unit tests are located in the 45 | DUTRemoteTests project. Use the Visual Studio Test Explorer to view and run unit tests, or use the `dotnet test` command. 46 | 47 | # Deploying # 48 | 49 | ## Setting up the Server (Device Under Test) ## 50 | If you used the `BuildAll.bat` script, it will generate an output directory with client and server binaries. Simply copy these to the desired devices. If you 51 | compiled manually, you should copy the directory generated by the `dotnet publish` command. 52 | 53 | Once you've copied the code, simply double click the exe. If you want to specify a port number for the server, you may provide it as the first argument when running the server. 54 | 55 | ### Security Warning ### 56 | SimpleRemote allows users to run programs and access files on the computer where it is run, and is a major security risk. It should be only run on test machines that are connected to secure networks. 57 | 58 | When you start the server for the first time, it will display a security warning, and prompt you to acknowledge the security risks of using SimpleRemote. Once you acknowledge the warning, the server will write a blank file `UserWarningAcknowledged` in the same directory as the server executable, and will not display the prompt on future runs. 59 | 60 | If you are deploying in a lab enviornment, acknowledge and accept the risks of using this software, and want to avoid manually acknowledging the prompt on each machine, you can either: 61 | - Place a blank file named `UserWarningAcknowledged` in the same directory as the server exe. 62 | - Start the server with the command line flag `--SuppressUserWarning`. 63 | 64 | ## Setting up the Client ## 65 | The client is a simple .NET DLL. Compile the SimpleDUTClientLibrary project in the release configuration, and copy the contents of the `netstandard2.0` output folder to your client machine. If you used the `BuildAll.bat` script, you will have a client folder in the `output` directory. 66 | 67 | Client documentation can be found here: SimpleDUTClientLibrary.RpcClient 68 | 69 | Additionally, the [Simple DUT Remote Tutorial](extra_docs/tutorial.md) covers how to use the client in PowerShell. 70 | 71 | # Logging # 72 | Logging is handled by the Nlog framework on the server, and can be configured without recompiling the application. Simply adjust the Nlog.conf file that is in the 73 | project with the exe (or main DLL) application. 74 | 75 | Status messages logged by the application are emitted at the INFO level. Timeouts and other non-critical issues are logged at WARN level. 76 | Exceptions are logged at the ERROR level. By default the logger will provide colored output to a terminal, but it can also be routed to files, 77 | or the Windows event log. 78 | 79 | [Details on how to configure can be found here](https://github.com/NLog/NLog/wiki/Tutorial#configuration). 80 | 81 | # Adding new functionality # 82 | Using the Plugin functions within SimpleDUTRemote.Functions, users can build custom actions into DLLs, and access the 83 | functionality directly from this communication library, without having to build the functions into an independent exe. 84 | 85 | This is a better solution for operations that need to be done repeatedly, and the overhead of starting and stopping a process 86 | repeatedly is unacceptable. 87 | 88 | There is an example plugin (located in the PluginExample directory) in this codebase. -------------------------------------------------------------------------------- /PluginExample/Class1.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | 6 | namespace PluginExample 7 | { 8 | public class SimpleTest 9 | { 10 | public bool IsNumberEven(int number) 11 | { 12 | return (number % 2 == 0); 13 | 14 | } 15 | public void WriteToConsole() 16 | { 17 | System.Console.WriteLine("Really, you shouldn't write to console in a plugin...");; 18 | } 19 | 20 | public string SayHiToMe(string myName) 21 | { 22 | return $"Hello {myName}"; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /PluginExample/PluginExample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /PluginExample/pubout/PluginExample.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/SimpleRemote/c8fbf151d68382bab3c3bdf50354587fa8ecbf91/PluginExample/pubout/PluginExample.dll -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | SimpleRemote is a framework designed to make device automation easier. 3 | It does this by providing a simple communications framework for interacting with devices under test. 4 | If you need to call programs or transfer files to devices as part of your testing, this framework can help you. 5 | 6 | This project also contains SimpleJsonRpc, which is a minimalist .NET Core JSON-RPC server. 7 | 8 | *Note: This project is also sometimes referred to as SimpleDUTRemote in the documentation - SimpleRemote and 9 | SimpleDUTRemote are the same thing, SimpleDUTRemote is just the older name for the tool.* 10 | 11 | # How do I start? 12 | This project is fully documented (with tutorials) in doxygen. To use, simply clone this code 13 | and run `doxygen` in the project directory. 14 | 15 | You can also view the pre-built [documentation online](https://microsoft.github.io/SimpleRemote/doc/html/index.html). 16 | 17 | Binaries for both the server and client are available on the GitHub Releases page, and nuget packages 18 | are available for the client and the SimpleJsonRpc libraries (under the `Microsoft.SurfaceAutomationTeam.SimpleRemote` 19 | namespace). 20 | 21 | If you already have doxygen and the .NET Core CLI tools, you can build everything by running 22 | the `BuildAll.bat` script included in this repositry. That will automatically build the client, 23 | server, supporting libraries, and all documentation, and place it in the `output` directory. 24 | 25 | # Why Should I Use This? 26 | This project was designed by the Surface team to help automate their hardware testing. The solution is 27 | incredibly lightweight - it has minimal power impact, generates minimal network traffic, and does not require 28 | any external servers. To deploy, simply run an exe and you're ready. 29 | 30 | You can think of this as shiny version of telnet and FTP, wrapped into one. 31 | 32 | # Security 33 | SimpleRemote allows users to run programs and access files on the computer where it is run, with no authentication whatsoever. It was desiged to be run on test machines on closed, lab networks. 34 | 35 | When you start the server for the first time, it will display a security warning, and prompt you to acknowledge the security risks of using SimpleRemote. Once you acknowledge the warning, the server will write a blank file `UserWarningAcknowledged` in the same directory as the server executable, and will not display the prompt on future runs. 36 | 37 | If you are deploying in a lab enviornment, acknowledge and accept the risks of using this software, and want to avoid manually acknowledging the prompt on each machine, you can either: 38 | - Place a blank file named `UserWarningAcknowledged` in the same directory as the server exe. 39 | - Start the server with the command line flag `--SuppressUserWarning`. 40 | 41 | # Tests 42 | Most functions provided by this tool have associated test cases, in the `DUTRemoteTests` project. To run, 43 | uses either Visual Studio's build in unit test runner, or run `dotnet test` from the `DUTRemoteTests` project 44 | directory. 45 | 46 | # Contributing 47 | 48 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 49 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 50 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 51 | 52 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 53 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 54 | provided by the bot. You will only need to do this once across all repos using our CLA. 55 | 56 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 57 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 58 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 59 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /SetVersion.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft. All rights reserved. 2 | # Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | """Set the version in all csproj and inno setup files.""" 5 | 6 | import os, os.path, re, sys 7 | 8 | # extensions to process 9 | EXTENSIONS = [".csproj", ".iss"] 10 | 11 | def main(): 12 | targetVer = sys.argv[1] 13 | fileList = GetFilesToModify() 14 | 15 | ModifyCsprojFiles(fileList, targetVer) 16 | ModifyIssFiles(fileList, targetVer) 17 | 18 | print("Done.") 19 | 20 | 21 | 22 | def GetFilesToModify(): 23 | fileList = [] 24 | for path, dirs, files in os.walk("."): 25 | f = [] 26 | for ext in EXTENSIONS: 27 | f.extend([x for x in files if os.path.splitext(x)[-1] == ext]) 28 | fileList.extend([os.path.join(path, x) for x in f]) 29 | return fileList 30 | 31 | def ModifyCsprojFiles(fileList, targetVersion): 32 | csprojFiles = [f for f in fileList if os.path.splitext(f)[-1] == ".csproj"] 33 | for csproj in csprojFiles: 34 | print("Updating file ", csproj) 35 | with open(csproj, "r+") as f: 36 | fileData = f.read() 37 | f.seek(0) 38 | fileData = re.sub(r"(.*?)", 39 | "{0}".format(targetVersion), fileData) 40 | fileData = re.sub(r"(.*?)", 41 | "{0}".format(targetVersion), fileData) 42 | f.write(fileData) 43 | 44 | def ModifyIssFiles(fileList, targetVersion): 45 | issFiles = [f for f in fileList if os.path.splitext(f)[-1] == ".iss"] 46 | for issFile in issFiles: 47 | print("Updating file ", issFile) 48 | with open(issFile, "r+") as f: 49 | fileData = f.read() 50 | f.seek(0) 51 | fileData = re.sub(r"(#define MyAppVersion).*", 52 | r'\1 "{0}"'.format(targetVersion), 53 | fileData) 54 | f.write(fileData) 55 | 56 | 57 | def _GetVersionPrefix(targetVersion): 58 | """Assuming semver, truncate the last number""" 59 | truncVer = targetVersion.split(".")[:-1] 60 | return ".".join(truncVer) 61 | 62 | if __name__ == "__main__": 63 | main() -------------------------------------------------------------------------------- /SimpleDUTClientLibrary/AssemblyRedirectResolver.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using System.Globalization; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Reflection; 11 | using System.Text; 12 | using System.Xml.Linq; 13 | 14 | namespace SimpleDUTClientLibrary 15 | { 16 | /// 17 | /// Assist in binding redirection when running the client as a DLL in PS or LabView. 18 | /// 19 | /// This code is coped directly from https://github.com/Microsoft/dotnet-apiport 20 | /// Specifically: dotnet-apiport/src/ApiPort.VisualStudio/AssemblyRedirectResolver.cs 21 | /// 22 | internal class AssemblyRedirectResolver 23 | { 24 | private readonly IDictionary _redirectsDictionary; 25 | 26 | public AssemblyRedirectResolver(string configFile) 27 | { 28 | var xml = XDocument.Load(configFile); 29 | Func getFullName = (name) => { return XName.Get(name, "urn:schemas-microsoft-com:asm.v1"); }; 30 | 31 | var redirects = from element in xml.Descendants(getFullName("dependentAssembly")) 32 | let identity = element.Element(getFullName("assemblyIdentity")) 33 | let redirect = element.Element(getFullName("bindingRedirect")) 34 | let name = identity.Attribute("name").Value 35 | let publicKey = identity.Attribute("publicKeyToken").Value 36 | let newVersion = redirect.Attribute("newVersion").Value 37 | select new AssemblyRedirect(name, newVersion, publicKey); 38 | 39 | _redirectsDictionary = redirects.ToDictionary(x => x.Name); 40 | } 41 | 42 | public AssemblyRedirectResolver(DirectoryInfo assemblyFolder) 43 | { 44 | var redirects = assemblyFolder.GetFiles("*.dll") 45 | .Select(dll => { 46 | AssemblyName name; 47 | try 48 | { 49 | name = AssemblyName.GetAssemblyName(dll.FullName); 50 | } 51 | catch 52 | { 53 | return null; 54 | } 55 | var publicKeyToken = name.GetPublicKeyToken().Aggregate("", (s, b) => s += b.ToString("x2", CultureInfo.InvariantCulture)); 56 | return new AssemblyRedirect(name.Name, name.Version.ToString(), publicKeyToken); 57 | }); 58 | 59 | _redirectsDictionary = redirects.Where(x => x != null).ToDictionary(x => x.Name); 60 | } 61 | 62 | public Assembly ResolveAssembly(string assemblyName, Assembly requestingAssembly) 63 | { 64 | // Use latest strong name & version when trying to load SDK assemblies 65 | var requestedAssembly = new AssemblyName(assemblyName); 66 | 67 | AssemblyRedirect redirectInformation; 68 | 69 | if (!_redirectsDictionary.TryGetValue(requestedAssembly.Name, out redirectInformation)) 70 | { 71 | return null; 72 | } 73 | 74 | Trace.WriteLine("Redirecting assembly load of " + assemblyName 75 | + ",\tloaded by " + (requestingAssembly == null ? "(unknown)" : requestingAssembly.FullName)); 76 | 77 | var alreadyLoadedAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => 78 | { 79 | var assm = a.GetName(); 80 | return string.Equals(assm.Name, requestedAssembly.Name, StringComparison.Ordinal) 81 | && redirectInformation.TargetVersion.Equals(assm.Version); 82 | }); 83 | 84 | if (alreadyLoadedAssembly != default(Assembly)) 85 | { 86 | return alreadyLoadedAssembly; 87 | } 88 | 89 | requestedAssembly.Version = redirectInformation.TargetVersion; 90 | requestedAssembly.SetPublicKeyToken(new AssemblyName("x, PublicKeyToken=" + redirectInformation.PublicKeyToken).GetPublicKeyToken()); 91 | requestedAssembly.CultureInfo = CultureInfo.InvariantCulture; 92 | 93 | return Assembly.Load(requestedAssembly); 94 | } 95 | } 96 | 97 | internal class AssemblyRedirect 98 | { 99 | public readonly string Name; 100 | 101 | public readonly string PublicKeyToken; 102 | 103 | public readonly Version TargetVersion; 104 | 105 | public AssemblyRedirect(string name, string version, string publicKeyToken) 106 | { 107 | Name = name; 108 | TargetVersion = new Version(version); 109 | PublicKeyToken = publicKeyToken; 110 | } 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /SimpleDUTClientLibrary/SimpleDUTClientLibrary.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Microsoft.SurfaceAutomationTeam.SimpleRemote.ClientLibrary 5 | Microsoft 6 | 1.4.2 7 | Microsoft 8 | SimpleRemote 9 | C# client library for Microsoft SimpleRemote 10 | (c) Microsoft Corporation. All rights reserved. 11 | MIT 12 | https://github.com/microsoft/SimpleRemote 13 | git 14 | https://github.com/microsoft/SimpleRemote 15 | https://raw.githubusercontent.com/microsoft/SimpleRemote/master/assets/TeamLogo.png 16 | TeamLogo.png 17 | rpc;SimpleRemote;device testing 18 | 19 | 20 | 21 | netstandard2.0 22 | 1.4.2 23 | true 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /SimpleDUTCommonLibrary/GlobFunctions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | using System.IO; 8 | 9 | namespace SimpleDUTCommonLibrary 10 | { 11 | public class GlobFunctions 12 | { 13 | public static string[] Glob(string path) 14 | { 15 | if (!(path.Contains("*") || path.Contains("?"))) 16 | { 17 | return new string[] { path }; //not a glob expression 18 | } 19 | 20 | // handle glob expression 21 | var parentDir = Path.GetDirectoryName(path); 22 | var globExp = Path.GetFileName(path); 23 | 24 | List entries = new List(); 25 | entries.AddRange(Directory.GetFiles(parentDir, globExp)); // add files that meet the glob expression 26 | 27 | // we now need to figure out what folders met the glob expression, and if so, add their elements recursively 28 | var folders = Directory.GetDirectories(parentDir, globExp); 29 | 30 | foreach (var dir in folders) 31 | { 32 | entries.Add(dir); 33 | entries.AddRange(Directory.GetFileSystemEntries(dir, "*", SearchOption.AllDirectories)); 34 | } 35 | 36 | return entries.ToArray(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /SimpleDUTCommonLibrary/SimpleDUTCommonLibrary.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Microsoft.SurfaceAutomationTeam.SimpleRemote.CommonLibrary 5 | Microsoft 6 | 1.4.2 7 | Microsoft 8 | SimpleRemote 9 | C# library used by both SimpleRemote Client and SimpleRemote Server 10 | (c) Microsoft Corporation. All rights reserved. 11 | MIT 12 | https://github.com/microsoft/SimpleRemote 13 | git 14 | https://github.com/microsoft/SimpleRemote 15 | https://raw.githubusercontent.com/microsoft/SimpleRemote/master/assets/TeamLogo.png 16 | TeamLogo.png 17 | rpc;SimpleRemote;device testing 18 | 19 | 20 | 21 | netstandard2.0 22 | true 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /SimpleDUTCommonLibrary/TarFunctions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Linq; 6 | using System.IO; 7 | using ICSharpCode.SharpZipLib.Tar; 8 | 9 | namespace SimpleDUTCommonLibrary 10 | { 11 | public class TarFunctions 12 | { 13 | public static long WriteFileOrDirectoryToStream(Stream stream, string targetPath, bool closeStreamWhenComplete = true) 14 | { 15 | long bytesSent = 0; 16 | TarArchive tar = TarArchive.CreateOutputTarArchive(stream); 17 | tar.IsStreamOwner = closeStreamWhenComplete; 18 | string[] fileList = null; 19 | string rootPath = null; 20 | 21 | // handle globs, directories, and individual files. 22 | if (targetPath.Contains("*") || targetPath.Contains("?")) 23 | { 24 | // we're handling a glob 25 | rootPath = Path.GetDirectoryName(targetPath); 26 | fileList = GlobFunctions.Glob(targetPath); 27 | 28 | } 29 | else if (File.GetAttributes(targetPath).HasFlag(FileAttributes.Directory)) 30 | { 31 | // handling a directory 32 | rootPath = targetPath; 33 | fileList = Directory.GetFileSystemEntries(targetPath, "*", SearchOption.AllDirectories); 34 | } 35 | else 36 | { 37 | // handling a single file 38 | rootPath = Path.GetDirectoryName(targetPath); 39 | fileList = new string[] { targetPath }; 40 | } 41 | 42 | foreach (var entry in fileList) 43 | { 44 | var tarEntry = TarEntry.CreateEntryFromFile(entry); 45 | 46 | // Work around for icsharpcode/SharpZipLib issues #334, #337, #338 47 | // This manually rebuilds the tar header entry name 48 | var newEntryName = entry; 49 | 50 | // remove the root path, if present and not an empty string, from the entry path 51 | if (!string.IsNullOrEmpty(rootPath) && newEntryName.StartsWith(rootPath, StringComparison.OrdinalIgnoreCase)) 52 | { 53 | newEntryName = newEntryName.Substring(rootPath.Length + 1); 54 | } 55 | 56 | // in the event this was a unc path name (started with \\), 57 | // remove leading '\' entries. 58 | while (newEntryName.StartsWith(@"\", StringComparison.Ordinal)) 59 | { 60 | newEntryName = newEntryName.Substring(1); 61 | } 62 | 63 | // switch all back slashes to forwrd slashes 64 | newEntryName = newEntryName.Replace(Path.DirectorySeparatorChar, '/'); 65 | 66 | // if this is a directory, it should have a trailing slash. 67 | if (File.GetAttributes(entry).HasFlag(FileAttributes.Directory)) 68 | { 69 | newEntryName += '/'; 70 | } 71 | 72 | // rewrite the header block name 73 | tarEntry.TarHeader.Name = newEntryName; 74 | 75 | // end work around for icsharpcode/SharpZipLib. 76 | 77 | tar.WriteEntry(tarEntry, false); 78 | 79 | // if it's a file, count it's size 80 | if (!File.GetAttributes(entry).HasFlag(FileAttributes.Directory)) 81 | bytesSent += (new FileInfo(entry)).Length; 82 | } 83 | 84 | // close the archive 85 | tar.Close(); 86 | 87 | // return our byte count 88 | return bytesSent; 89 | 90 | } 91 | 92 | public static long ReadFileOrDirectoryFromStream(Stream stream, string pathToWrite, 93 | bool overwrite = true, bool closeStreamWhenComplete = true) 94 | { 95 | var tar = new TarInputStream(stream); 96 | tar.IsStreamOwner = closeStreamWhenComplete; 97 | long bytesRead = 0; 98 | 99 | // we can't use the simple ExtractContents because we need to be overwrite aware, 100 | // so we iterate instead 101 | TarEntry entry; 102 | while ((entry = tar.GetNextEntry()) != null) 103 | { 104 | var extractPath = Path.Combine(pathToWrite, entry.Name); 105 | 106 | if (entry.IsDirectory) 107 | { 108 | // we don't have to worry about writing over directories 109 | Directory.CreateDirectory(extractPath); 110 | } 111 | else 112 | { 113 | // if overwrite is on, use Create FileMode. If not, use CreateNew, which will throw 114 | // an IO error if the file already exists. 115 | using (var fs = new FileStream(extractPath, overwrite ? FileMode.Create : FileMode.CreateNew)) 116 | { 117 | tar.CopyEntryContents(fs); 118 | bytesRead += entry.Size; 119 | } 120 | } 121 | 122 | } 123 | tar.Close(); 124 | 125 | return bytesRead; 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /SimpleDUTCommonLibrary/TcpClientConnectWithTimeout.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Net.Sockets; 5 | using System.Text; 6 | using System.Threading; 7 | 8 | namespace SimpleDUTCommonLibrary 9 | { 10 | public static class TcpClientConnectWithTimeout 11 | { 12 | /// 13 | /// Attempt to connect within a timeout. 14 | /// 15 | /// The TcpClient Object 16 | /// The hostname or IP address to connect to. 17 | /// The port to connect to. 18 | /// The timeout value in milliseconds 19 | /// True if the connection is successful, false if the connection attempt timed out or failed. 20 | public static bool ConnectWithTimeout(this TcpClient tcpClient, string target, int port, int millisecondsTimeout) 21 | { 22 | var asyncResult = tcpClient.BeginConnect(target, port, ar => { 23 | try { 24 | tcpClient.EndConnect(ar); 25 | } 26 | catch { 27 | // an exception occured, but this is ok - the connected propery of the client will be 28 | // false, which is what we use to check if a connection was successful. 29 | } 30 | }, null); 31 | 32 | // if the wait returned true, check if the connection succeeded (and return connection status), 33 | // otherwise, return false. 34 | return asyncResult.AsyncWaitHandle.WaitOne(millisecondsTimeout) 35 | ? tcpClient.Connected 36 | : false; 37 | } 38 | 39 | /// 40 | /// Attempt to connect within a timeout. 41 | /// 42 | /// The TcpClient Object 43 | /// The hostname or IP address to connect to. 44 | /// The port to connect to. 45 | /// The timeout value in milliseconds 46 | /// True if the connection is successful, false if the connection attempt timed out or failed. 47 | public static bool ConnectWithTimeout(this TcpClient tcpClient, IPAddress target, int port, int millisecondsTimeout) 48 | { 49 | var asyncResult = tcpClient.BeginConnect(target, port, ar => { 50 | try { 51 | tcpClient.EndConnect(ar); 52 | } 53 | catch { 54 | // an exception occured, but this is ok - the connected propery of the client will be 55 | // false, which is what we use to check if a connection was successful. 56 | } 57 | }, null); 58 | 59 | // if the wait returned true, check if the connection succeeded (and return connection status), 60 | // otherwise, return false. 61 | return asyncResult.AsyncWaitHandle.WaitOne(millisecondsTimeout) 62 | ? tcpClient.Connected 63 | : false; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /SimpleDUTCommonLibrary/ZipFunctions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.IO; 6 | using System.IO.Compression; 7 | using System.Linq; 8 | 9 | namespace SimpleDUTCommonLibrary 10 | { 11 | public static class ZipFunctions 12 | { 13 | public static long WriteDirectoryToStream(Stream stream, string pathToDirectory) 14 | { 15 | using (PositionWrapperStream wrappedStream = new PositionWrapperStream(stream)) 16 | using (ZipArchive zip = new ZipArchive(wrappedStream, ZipArchiveMode.Create)) 17 | { 18 | // track how much we've sent (ignoring zip overhead) 19 | long bytesSent = 0; 20 | 21 | // use URIs to calculate relative paths, as discussed in 22 | // https://stackoverflow.com/questions/9042861/how-to-make-an-absolute-path-relative-to-a-particular-folder 23 | Uri relativeRoot = new Uri(pathToDirectory.Last() == '\\' ? pathToDirectory : pathToDirectory + '\\', UriKind.Absolute); 24 | 25 | // get all entries in the path 26 | var entries = (new DirectoryInfo(pathToDirectory)).EnumerateFileSystemInfos("*", SearchOption.AllDirectories); 27 | 28 | // add entries to the zip file 29 | foreach (var entry in entries) 30 | { 31 | string entryName = relativeRoot.MakeRelativeUri(new Uri(entry.FullName, UriKind.Absolute)).ToString(); 32 | 33 | // if we're handling a directory, add a blank entry 34 | if (entry.Attributes.HasFlag(FileAttributes.Directory)) 35 | { 36 | // calculate the relative path as described here: 37 | zip.CreateEntry(entryName + "/"); 38 | } 39 | 40 | // we're handling a file 41 | else 42 | { 43 | zip.CreateEntryFromFile(entry.FullName, entryName, CompressionLevel.NoCompression); 44 | bytesSent += (new FileInfo(entry.FullName)).Length; 45 | } 46 | } 47 | 48 | // return the number of bytes sent 49 | return bytesSent; 50 | 51 | } 52 | } 53 | 54 | public static long ReadDirectoryFromStream(Stream stream, string pathToWrite, bool overwrite = true) 55 | { 56 | using (ZipArchive zip = new ZipArchive(stream, ZipArchiveMode.Read)) 57 | { 58 | Directory.CreateDirectory(pathToWrite); 59 | string targetPath; 60 | long bytesReceived = 0; 61 | 62 | foreach (ZipArchiveEntry entry in zip.Entries) 63 | { 64 | targetPath = Path.Combine(pathToWrite, entry.FullName); 65 | 66 | if (entry.FullName.EndsWith("/")) 67 | { 68 | // we're handling a directory entry 69 | // this is always safe, regardless if overwrite is on or not. 70 | Directory.CreateDirectory(targetPath); 71 | } 72 | else 73 | { 74 | // handle files 75 | entry.ExtractToFile(targetPath, overwrite); 76 | bytesReceived += entry.Length; 77 | } 78 | } 79 | 80 | return bytesReceived; 81 | } 82 | } 83 | 84 | /// 85 | /// Helper Class For Network Streams with Position Tracking 86 | /// 87 | /// 88 | /// This class exists due to a bug in the .NET Framework where ZipArchives 89 | /// try to access position of a stream, even in create mode. 90 | ///
Code stolen from: https://stackoverflow.com/questions/16585488/writing-to-ziparchive-using-the-httpcontext-outputstream 91 | ///
Bug: https://connect.microsoft.com/VisualStudio/feedback/details/816411/ziparchive-shouldnt-read-the-position-of-non-seekable-streams 92 | ///
93 | ///
This is fixed in .NET Core 2.0: https://github.com/dotnet/corefx/pull/12682 94 | ///
95 | private class PositionWrapperStream : Stream 96 | { 97 | private readonly Stream wrapped; 98 | 99 | private int pos = 0; 100 | 101 | public PositionWrapperStream(Stream wrapped) 102 | { 103 | this.wrapped = wrapped; 104 | } 105 | 106 | public override bool CanSeek { get { return false; } } 107 | 108 | public override bool CanWrite { get { return true; } } 109 | 110 | public override long Position 111 | { 112 | get { return pos; } 113 | set { throw new NotSupportedException(); } 114 | } 115 | 116 | public override bool CanRead => throw new NotImplementedException(); 117 | 118 | public override long Length => throw new NotImplementedException(); 119 | 120 | public override void Write(byte[] buffer, int offset, int count) 121 | { 122 | pos += count; 123 | wrapped.Write(buffer, offset, count); 124 | } 125 | 126 | public override void Flush() 127 | { 128 | wrapped.Flush(); 129 | } 130 | 131 | protected override void Dispose(bool disposing) 132 | { 133 | wrapped.Dispose(); 134 | base.Dispose(disposing); 135 | } 136 | 137 | public override int Read(byte[] buffer, int offset, int count) 138 | { 139 | throw new NotImplementedException(); 140 | } 141 | 142 | public override long Seek(long offset, SeekOrigin origin) 143 | { 144 | throw new NotImplementedException(); 145 | } 146 | 147 | public override void SetLength(long value) 148 | { 149 | throw new NotImplementedException(); 150 | } 151 | 152 | // all the other required methods can throw NotSupportedException 153 | 154 | } 155 | } 156 | 157 | 158 | } 159 | -------------------------------------------------------------------------------- /SimpleDUTRemote.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 15 3 | VisualStudioVersion = 15.0.26730.15 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleDUTRemote", "SimpleDUTRemote\SimpleDUTRemote.csproj", "{DD5FADA0-372D-49BD-96F2-49CAD3288DE0}" 6 | EndProject 7 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleRemoteConsole", "SimpleRemoteConsole\SimpleRemoteConsole.csproj", "{370224B4-7642-496A-80E8-EECA789757F6}" 8 | EndProject 9 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DUTRemoteTests", "DUTRemoteTests\DUTRemoteTests.csproj", "{24E17929-8B83-4924-B2F6-5454F25D96FD}" 10 | EndProject 11 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleDUTClientLibrary", "SimpleDUTClientLibrary\SimpleDUTClientLibrary.csproj", "{0194FE09-1726-4BA7-8D49-AAF4B9D33FA4}" 12 | EndProject 13 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F4803079-16D7-44E1-8410-5286494900EB}" 14 | ProjectSection(SolutionItems) = preProject 15 | BuildAll.bat = BuildAll.bat 16 | BuildAll.ps1 = BuildAll.ps1 17 | Doxyfile = Doxyfile 18 | MainDocPage.md = MainDocPage.md 19 | extra_docs\tutorial.md = extra_docs\tutorial.md 20 | EndProjectSection 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleDUTCommonLibrary", "SimpleDUTCommonLibrary\SimpleDUTCommonLibrary.csproj", "{5922E9FB-B1A4-4EF4-8B91-E4568E1120C8}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PluginExample", "PluginExample\PluginExample.csproj", "{3EFBE0D5-3D14-4F31-B5B8-677B299CF3D0}" 25 | EndProject 26 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extra Docs", "Extra Docs", "{4197B078-69DF-4478-8889-E0DC1EC8174B}" 27 | EndProject 28 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleJsonRpc", "SimpleJsonRpc\SimpleJsonRpc.csproj", "{AAC15B64-9F37-4FAB-8700-AEA268E16A37}" 29 | EndProject 30 | Global 31 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 32 | Debug|Any CPU = Debug|Any CPU 33 | Debug|x64 = Debug|x64 34 | Debug|x86 = Debug|x86 35 | Release|Any CPU = Release|Any CPU 36 | Release|x64 = Release|x64 37 | Release|x86 = Release|x86 38 | EndGlobalSection 39 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 40 | {DD5FADA0-372D-49BD-96F2-49CAD3288DE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {DD5FADA0-372D-49BD-96F2-49CAD3288DE0}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {DD5FADA0-372D-49BD-96F2-49CAD3288DE0}.Debug|x64.ActiveCfg = Debug|Any CPU 43 | {DD5FADA0-372D-49BD-96F2-49CAD3288DE0}.Debug|x64.Build.0 = Debug|Any CPU 44 | {DD5FADA0-372D-49BD-96F2-49CAD3288DE0}.Debug|x86.ActiveCfg = Debug|Any CPU 45 | {DD5FADA0-372D-49BD-96F2-49CAD3288DE0}.Debug|x86.Build.0 = Debug|Any CPU 46 | {DD5FADA0-372D-49BD-96F2-49CAD3288DE0}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {DD5FADA0-372D-49BD-96F2-49CAD3288DE0}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {DD5FADA0-372D-49BD-96F2-49CAD3288DE0}.Release|x64.ActiveCfg = Release|Any CPU 49 | {DD5FADA0-372D-49BD-96F2-49CAD3288DE0}.Release|x64.Build.0 = Release|Any CPU 50 | {DD5FADA0-372D-49BD-96F2-49CAD3288DE0}.Release|x86.ActiveCfg = Release|Any CPU 51 | {DD5FADA0-372D-49BD-96F2-49CAD3288DE0}.Release|x86.Build.0 = Release|Any CPU 52 | {370224B4-7642-496A-80E8-EECA789757F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {370224B4-7642-496A-80E8-EECA789757F6}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {370224B4-7642-496A-80E8-EECA789757F6}.Debug|x64.ActiveCfg = Debug|Any CPU 55 | {370224B4-7642-496A-80E8-EECA789757F6}.Debug|x64.Build.0 = Debug|Any CPU 56 | {370224B4-7642-496A-80E8-EECA789757F6}.Debug|x86.ActiveCfg = Debug|Any CPU 57 | {370224B4-7642-496A-80E8-EECA789757F6}.Debug|x86.Build.0 = Debug|Any CPU 58 | {370224B4-7642-496A-80E8-EECA789757F6}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {370224B4-7642-496A-80E8-EECA789757F6}.Release|Any CPU.Build.0 = Release|Any CPU 60 | {370224B4-7642-496A-80E8-EECA789757F6}.Release|x64.ActiveCfg = Release|Any CPU 61 | {370224B4-7642-496A-80E8-EECA789757F6}.Release|x64.Build.0 = Release|Any CPU 62 | {370224B4-7642-496A-80E8-EECA789757F6}.Release|x86.ActiveCfg = Release|Any CPU 63 | {370224B4-7642-496A-80E8-EECA789757F6}.Release|x86.Build.0 = Release|Any CPU 64 | {24E17929-8B83-4924-B2F6-5454F25D96FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 65 | {24E17929-8B83-4924-B2F6-5454F25D96FD}.Debug|Any CPU.Build.0 = Debug|Any CPU 66 | {24E17929-8B83-4924-B2F6-5454F25D96FD}.Debug|x64.ActiveCfg = Debug|Any CPU 67 | {24E17929-8B83-4924-B2F6-5454F25D96FD}.Debug|x64.Build.0 = Debug|Any CPU 68 | {24E17929-8B83-4924-B2F6-5454F25D96FD}.Debug|x86.ActiveCfg = Debug|Any CPU 69 | {24E17929-8B83-4924-B2F6-5454F25D96FD}.Debug|x86.Build.0 = Debug|Any CPU 70 | {24E17929-8B83-4924-B2F6-5454F25D96FD}.Release|Any CPU.ActiveCfg = Release|Any CPU 71 | {24E17929-8B83-4924-B2F6-5454F25D96FD}.Release|Any CPU.Build.0 = Release|Any CPU 72 | {24E17929-8B83-4924-B2F6-5454F25D96FD}.Release|x64.ActiveCfg = Release|Any CPU 73 | {24E17929-8B83-4924-B2F6-5454F25D96FD}.Release|x64.Build.0 = Release|Any CPU 74 | {24E17929-8B83-4924-B2F6-5454F25D96FD}.Release|x86.ActiveCfg = Release|Any CPU 75 | {24E17929-8B83-4924-B2F6-5454F25D96FD}.Release|x86.Build.0 = Release|Any CPU 76 | {0194FE09-1726-4BA7-8D49-AAF4B9D33FA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 77 | {0194FE09-1726-4BA7-8D49-AAF4B9D33FA4}.Debug|Any CPU.Build.0 = Debug|Any CPU 78 | {0194FE09-1726-4BA7-8D49-AAF4B9D33FA4}.Debug|x64.ActiveCfg = Debug|Any CPU 79 | {0194FE09-1726-4BA7-8D49-AAF4B9D33FA4}.Debug|x64.Build.0 = Debug|Any CPU 80 | {0194FE09-1726-4BA7-8D49-AAF4B9D33FA4}.Debug|x86.ActiveCfg = Debug|Any CPU 81 | {0194FE09-1726-4BA7-8D49-AAF4B9D33FA4}.Debug|x86.Build.0 = Debug|Any CPU 82 | {0194FE09-1726-4BA7-8D49-AAF4B9D33FA4}.Release|Any CPU.ActiveCfg = Release|Any CPU 83 | {0194FE09-1726-4BA7-8D49-AAF4B9D33FA4}.Release|Any CPU.Build.0 = Release|Any CPU 84 | {0194FE09-1726-4BA7-8D49-AAF4B9D33FA4}.Release|x64.ActiveCfg = Release|Any CPU 85 | {0194FE09-1726-4BA7-8D49-AAF4B9D33FA4}.Release|x64.Build.0 = Release|Any CPU 86 | {0194FE09-1726-4BA7-8D49-AAF4B9D33FA4}.Release|x86.ActiveCfg = Release|Any CPU 87 | {0194FE09-1726-4BA7-8D49-AAF4B9D33FA4}.Release|x86.Build.0 = Release|Any CPU 88 | {5922E9FB-B1A4-4EF4-8B91-E4568E1120C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 89 | {5922E9FB-B1A4-4EF4-8B91-E4568E1120C8}.Debug|Any CPU.Build.0 = Debug|Any CPU 90 | {5922E9FB-B1A4-4EF4-8B91-E4568E1120C8}.Debug|x64.ActiveCfg = Debug|Any CPU 91 | {5922E9FB-B1A4-4EF4-8B91-E4568E1120C8}.Debug|x64.Build.0 = Debug|Any CPU 92 | {5922E9FB-B1A4-4EF4-8B91-E4568E1120C8}.Debug|x86.ActiveCfg = Debug|Any CPU 93 | {5922E9FB-B1A4-4EF4-8B91-E4568E1120C8}.Debug|x86.Build.0 = Debug|Any CPU 94 | {5922E9FB-B1A4-4EF4-8B91-E4568E1120C8}.Release|Any CPU.ActiveCfg = Release|Any CPU 95 | {5922E9FB-B1A4-4EF4-8B91-E4568E1120C8}.Release|Any CPU.Build.0 = Release|Any CPU 96 | {5922E9FB-B1A4-4EF4-8B91-E4568E1120C8}.Release|x64.ActiveCfg = Release|Any CPU 97 | {5922E9FB-B1A4-4EF4-8B91-E4568E1120C8}.Release|x64.Build.0 = Release|Any CPU 98 | {5922E9FB-B1A4-4EF4-8B91-E4568E1120C8}.Release|x86.ActiveCfg = Release|Any CPU 99 | {5922E9FB-B1A4-4EF4-8B91-E4568E1120C8}.Release|x86.Build.0 = Release|Any CPU 100 | {3EFBE0D5-3D14-4F31-B5B8-677B299CF3D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 101 | {3EFBE0D5-3D14-4F31-B5B8-677B299CF3D0}.Debug|Any CPU.Build.0 = Debug|Any CPU 102 | {3EFBE0D5-3D14-4F31-B5B8-677B299CF3D0}.Debug|x64.ActiveCfg = Debug|Any CPU 103 | {3EFBE0D5-3D14-4F31-B5B8-677B299CF3D0}.Debug|x64.Build.0 = Debug|Any CPU 104 | {3EFBE0D5-3D14-4F31-B5B8-677B299CF3D0}.Debug|x86.ActiveCfg = Debug|Any CPU 105 | {3EFBE0D5-3D14-4F31-B5B8-677B299CF3D0}.Debug|x86.Build.0 = Debug|Any CPU 106 | {3EFBE0D5-3D14-4F31-B5B8-677B299CF3D0}.Release|Any CPU.ActiveCfg = Release|Any CPU 107 | {3EFBE0D5-3D14-4F31-B5B8-677B299CF3D0}.Release|Any CPU.Build.0 = Release|Any CPU 108 | {3EFBE0D5-3D14-4F31-B5B8-677B299CF3D0}.Release|x64.ActiveCfg = Release|Any CPU 109 | {3EFBE0D5-3D14-4F31-B5B8-677B299CF3D0}.Release|x64.Build.0 = Release|Any CPU 110 | {3EFBE0D5-3D14-4F31-B5B8-677B299CF3D0}.Release|x86.ActiveCfg = Release|Any CPU 111 | {3EFBE0D5-3D14-4F31-B5B8-677B299CF3D0}.Release|x86.Build.0 = Release|Any CPU 112 | {AAC15B64-9F37-4FAB-8700-AEA268E16A37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 113 | {AAC15B64-9F37-4FAB-8700-AEA268E16A37}.Debug|Any CPU.Build.0 = Debug|Any CPU 114 | {AAC15B64-9F37-4FAB-8700-AEA268E16A37}.Debug|x64.ActiveCfg = Debug|Any CPU 115 | {AAC15B64-9F37-4FAB-8700-AEA268E16A37}.Debug|x64.Build.0 = Debug|Any CPU 116 | {AAC15B64-9F37-4FAB-8700-AEA268E16A37}.Debug|x86.ActiveCfg = Debug|Any CPU 117 | {AAC15B64-9F37-4FAB-8700-AEA268E16A37}.Debug|x86.Build.0 = Debug|Any CPU 118 | {AAC15B64-9F37-4FAB-8700-AEA268E16A37}.Release|Any CPU.ActiveCfg = Release|Any CPU 119 | {AAC15B64-9F37-4FAB-8700-AEA268E16A37}.Release|Any CPU.Build.0 = Release|Any CPU 120 | {AAC15B64-9F37-4FAB-8700-AEA268E16A37}.Release|x64.ActiveCfg = Release|Any CPU 121 | {AAC15B64-9F37-4FAB-8700-AEA268E16A37}.Release|x64.Build.0 = Release|Any CPU 122 | {AAC15B64-9F37-4FAB-8700-AEA268E16A37}.Release|x86.ActiveCfg = Release|Any CPU 123 | {AAC15B64-9F37-4FAB-8700-AEA268E16A37}.Release|x86.Build.0 = Release|Any CPU 124 | EndGlobalSection 125 | GlobalSection(SolutionProperties) = preSolution 126 | HideSolutionNode = FALSE 127 | EndGlobalSection 128 | GlobalSection(ExtensibilityGlobals) = postSolution 129 | SolutionGuid = {1E67C7C9-1E3E-4017-925F-3B0418866904} 130 | EndGlobalSection 131 | EndGlobal 132 | -------------------------------------------------------------------------------- /SimpleDUTRemote/HelperFunctions/LargeFileTransfers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using System.Net.Sockets; 9 | using System.Net; 10 | using NLog; 11 | using System.IO; 12 | using System.IO.Compression; 13 | using System.Linq; 14 | using SimpleDUTCommonLibrary; 15 | 16 | namespace SimpleDUTRemote.HelperFunctions 17 | { 18 | public class LargeFileTransfers 19 | { 20 | private static Logger logger = LogManager.GetCurrentClassLogger(); 21 | 22 | public static void Upload(string path, TcpListener server, bool overwrite) 23 | { 24 | logger.Info($"Waiting for connection for directory transfer on port {((IPEndPoint)server.LocalEndpoint).Port}"); 25 | var asyncAccept = server.BeginAcceptTcpClient(null, null); 26 | 27 | #if DEBUG 28 | asyncAccept.AsyncWaitHandle.WaitOne(); 29 | #else 30 | 31 | if (!asyncAccept.AsyncWaitHandle.WaitOne(10 * 1000)) 32 | { 33 | logger.Warn($"Timed out waiting for client to begin uploading files. Closing port."); 34 | server.Stop(); 35 | return; 36 | } 37 | #endif 38 | var client = server.EndAcceptTcpClient(asyncAccept); 39 | server.Stop(); 40 | 41 | using (client) 42 | using (var stream = client.GetStream()) 43 | { 44 | try 45 | { 46 | //var bytesReceived = ZipFunctions.ReadDirectoryFromStream(stream, path, overwrite); 47 | var bytesReceived = TarFunctions.ReadFileOrDirectoryFromStream(stream, path, overwrite, 48 | closeStreamWhenComplete: false); 49 | logger.Info($"Successfully received {bytesReceived} bytes, written to {path}"); 50 | 51 | // Depending on the record size settings, tar can legally end up sending extra null blocks. 52 | // If it did, and we try to shutdown the socket without reading the remaining bytes, 53 | // it will trigger an RST packet. We can avoid this by clearing the inbound socket buffer. 54 | var dummyBuffer = new byte[1024]; 55 | while (stream.DataAvailable) { 56 | stream.Read(dummyBuffer, 0, 1024); 57 | } 58 | 59 | // attempt to send back byte count to the client before shutting down the connection 60 | try 61 | { 62 | var temp = Encoding.ASCII.GetBytes(bytesReceived.ToString() + "\r\n"); 63 | 64 | stream.Write(temp, 0, temp.Length); 65 | } 66 | catch (IOException e) 67 | { 68 | logger.Info("Upload was successful, but client disconnected before byte-count could be sent back."); 69 | } 70 | } 71 | catch (Exception e) 72 | { 73 | logger.Error(e, "Upload failed."); 74 | } 75 | 76 | } 77 | } 78 | 79 | public static void Download(string path, TcpListener server) 80 | { 81 | logger.Info($"Waiting for connection for directory transfer on port {((IPEndPoint)server.LocalEndpoint).Port}"); 82 | var asyncAccept = server.BeginAcceptTcpClient(null, null); 83 | 84 | #if DEBUG 85 | asyncAccept.AsyncWaitHandle.WaitOne(); 86 | #else 87 | 88 | if (!asyncAccept.AsyncWaitHandle.WaitOne(10 * 1000)) 89 | { 90 | logger.Warn($"Timed out waiting for client to begin downloading files. Closing port."); 91 | server.Stop(); 92 | return; 93 | } 94 | #endif 95 | var client = server.EndAcceptTcpClient(asyncAccept); 96 | server.Stop(); 97 | 98 | using (client) 99 | using (var stream = client.GetStream()) 100 | { 101 | 102 | try 103 | { 104 | //var bytesSent = ZipFunctions.WriteDirectoryToStream(stream, path); 105 | var bytesSent = TarFunctions.WriteFileOrDirectoryToStream(stream, path); 106 | logger.Info($"Successfully sent {bytesSent} bytes, read from {path}"); 107 | } 108 | catch (Exception e) 109 | { 110 | logger.Error(e, "Download failed"); 111 | return; 112 | } 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /SimpleDUTRemote/HelperFunctions/ReadWriteChecks.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | using System.IO; 8 | 9 | namespace SimpleDUTRemote 10 | { 11 | internal class ReadWriteChecks 12 | { 13 | /// 14 | /// Check if the given path exists, and can be read. Works for files and directories. 15 | /// 16 | /// Path to check. 17 | /// True if successful, false otherwise. 18 | internal static bool CheckReadFromFileOrDir(string path) 19 | { 20 | try 21 | { 22 | // handle glob expressions - if we see wildcards, just grab the parent directory 23 | if (path.Contains("*") || path.Contains("?")) 24 | { 25 | path = Path.GetDirectoryName(path); 26 | } 27 | 28 | if (IsDir(path)) 29 | { 30 | // try to list contents, will throw if does not exist 31 | var tmp = Directory.GetFileSystemEntries(path); 32 | 33 | // if this didn't throw, we assume we're safe. 34 | return true; 35 | } 36 | else 37 | { 38 | // this is a file, can we open it for reading? 39 | using (var fstream = File.Open(path, FileMode.Open)) 40 | { 41 | // if we're here, we can open the file without issue. 42 | return true; 43 | } 44 | } 45 | } 46 | catch (IOException) 47 | { 48 | return false; 49 | } 50 | 51 | } 52 | 53 | /// 54 | /// Check if the given path exists, and can be written. Works for directories only. 55 | /// 56 | /// Path to directory to check 57 | /// 58 | internal static bool CheckWriteToDir(string path) 59 | { 60 | try 61 | { 62 | if (!Directory.Exists(path)) 63 | { 64 | throw new FileNotFoundException($"Target directory {path} doesn't exist."); 65 | } 66 | 67 | // can we write a file here? 68 | using (var fstream = File.Create( 69 | Path.Combine(path, Path.GetRandomFileName()), 70 | 1024, 71 | FileOptions.DeleteOnClose)) 72 | { 73 | return true; 74 | } 75 | } 76 | catch (Exception e) 77 | { 78 | if (e is IOException || e is UnauthorizedAccessException) 79 | return false; 80 | else 81 | throw; 82 | } 83 | 84 | } 85 | 86 | private static bool IsDir(string path) 87 | { 88 | // check if path is a directory. 89 | // should also throw if path doesn't exist 90 | var fattrs = File.GetAttributes(path); 91 | return fattrs.HasFlag(FileAttributes.Directory); 92 | } 93 | 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /SimpleDUTRemote/HelperFunctions/ThreadSafeStringBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace SimpleDUTRemote.HelperFunctions 6 | { 7 | public class ThreadSafeStringBuilder 8 | { 9 | private StringBuilder sb; 10 | private object lockobj; 11 | 12 | public ThreadSafeStringBuilder() 13 | { 14 | sb = new StringBuilder(); 15 | lockobj = new object(); 16 | } 17 | 18 | public override string ToString() 19 | { 20 | lock (lockobj) 21 | { 22 | return sb.ToString(); 23 | } 24 | } 25 | 26 | public void AppendLine(string str) 27 | { 28 | lock (lockobj) 29 | { 30 | sb.AppendLine(str); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /SimpleDUTRemote/JobSystem/Job.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using NLog; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Collections.Concurrent; 8 | using System.Diagnostics; 9 | using System.IO; 10 | using System.Net; 11 | using System.Net.Sockets; 12 | using System.Text; 13 | using System.Threading; 14 | using System.Threading.Tasks; 15 | using SimpleDUTCommonLibrary; 16 | 17 | namespace SimpleDUTRemote.JobSystem 18 | { 19 | public class Job : IDisposable 20 | { 21 | public int jobId { get; private set; } 22 | private Process process; 23 | private static Logger logger = LogManager.GetLogger("JobSystem"); 24 | private JobCallbackInfo callbackInfo; 25 | private static int nextJobId = 0; 26 | 27 | // used to capture non-streaming output in memory 28 | private HelperFunctions.ThreadSafeStringBuilder output = null; 29 | 30 | // items for progress streaming (network) 31 | private StreamWriter progressStream; 32 | private const int NETWORK_TIMEOUT_MS = 5000; 33 | private Task streamingLoopTask; 34 | private BlockingCollection streamingCollection; 35 | 36 | // while streaming to network, we also stream to a backup file 37 | private const string OUTPUT_LOG_BASENAME = "SimpleRemote-JobOutput-"; 38 | private const string OUTPUT_LOG_TIME_FORMAT = "yyyy-MM-dd_HH-mm-ss"; 39 | private string outputLogFilename = null; 40 | private StreamWriter outputLogSteam = null; 41 | 42 | // class-level lock object to ensure stream cleanup is threadsafe. 43 | private object lockObj = new object(); 44 | 45 | public static Job CreateJob(Process p, JobCallbackInfo callback = null) 46 | { 47 | var newid = Interlocked.Increment(ref nextJobId); 48 | return new Job(newid, p, callback); 49 | } 50 | 51 | public Job(int id, Process process, JobCallbackInfo callback = null) 52 | { 53 | logger.Info("Spawning new job id {0}; will call {1}", id, process.StartInfo.FileName); 54 | jobId = id; 55 | this.process = process; 56 | 57 | // if a progress port was specified in callback info, prepare for streaming 58 | if (callback != null && callback.ProgressPort > 0) 59 | { 60 | var streamEp = new IPEndPoint(callback.Address, callback.ProgressPort); 61 | CreateProgressStream(streamEp); 62 | 63 | streamingCollection = new BlockingCollection(); 64 | 65 | process.OutputDataReceived += (s, a) => streamingCollection.Add(a.Data); 66 | process.ErrorDataReceived += (s, a) => streamingCollection.Add(a.Data); 67 | 68 | streamingLoopTask = Task.Factory.StartNew(StreamingLoopHandler); 69 | } 70 | else 71 | { 72 | output = new HelperFunctions.ThreadSafeStringBuilder(); 73 | process.OutputDataReceived += (s, a) => output.AppendLine(a.Data); 74 | process.ErrorDataReceived += (s, a) => output.AppendLine(a.Data); 75 | 76 | } 77 | 78 | // always log output 79 | process.OutputDataReceived += (s,a) => logger.Debug($"Job {this.jobId} std output: {a.Data}"); 80 | process.ErrorDataReceived += (s,a) => logger.Debug($"Job {this.jobId} std error: {a.Data}"); 81 | 82 | // add a logging message when a job finishes and ensure that streaming task stops if needed 83 | process.EnableRaisingEvents = true; 84 | process.Exited += (o, e) => logger.Info($"Job {id} finished executing."); 85 | process.Exited += (o, e) => streamingCollection?.Add(null); 86 | 87 | if (callback != null) 88 | { 89 | logger.Info("Registering job {0:d} for callbacks", id); 90 | 91 | // if callbacks are specified, we need to register for process events, and setup a handler 92 | callbackInfo = callback; 93 | 94 | // fire the callback handler in another thread (otherwise the callback might take a while and block other events waiting on exit) 95 | process.Exited += (o, e) => Task.Factory.StartNew(FireCompletionCallback); 96 | } 97 | 98 | process.Start(); 99 | 100 | process.BeginErrorReadLine(); 101 | process.BeginOutputReadLine(); 102 | } 103 | 104 | private void StreamingLoopHandler() 105 | { 106 | string nextLine; 107 | 108 | while ((nextLine = streamingCollection.Take()) != null) 109 | { 110 | try 111 | { 112 | outputLogSteam.WriteLine(nextLine); 113 | progressStream?.WriteLine(nextLine); 114 | } 115 | catch (IOException e) 116 | { 117 | if (!(e.InnerException is SocketException)) throw; 118 | 119 | logger.Error("Failed to stream progress from process - socket exception ocurred."); 120 | logger.Error("Logging will continue to the the file log."); 121 | progressStream.Dispose(); 122 | progressStream = null; 123 | } 124 | catch (ObjectDisposedException) 125 | { 126 | logger.Debug("Stream object was disposed while streaming output - this likely means this job was terminated."); 127 | return; // break out of the function if this happens. 128 | } 129 | } 130 | 131 | CloseStreams(); 132 | 133 | } 134 | 135 | private void FireCompletionCallback() 136 | { 137 | logger.Debug("Attmpeting to send TCP completion message for job {0}", this.jobId); 138 | 139 | // if the streaming system is active, don't send this until the streams have completed 140 | if (streamingLoopTask != null && !streamingLoopTask.IsCompleted) 141 | { 142 | streamingLoopTask.Wait(); 143 | } 144 | 145 | // Retry connection attempt with exponential backoff 146 | var retryAttempt = 0; 147 | bool clientConnected = false; 148 | 149 | while (retryAttempt < 5 && !clientConnected) 150 | { 151 | logger.Warn("Attmpeting to send TCP completion message for job {0}: Retry Attempt {1}", this.jobId, retryAttempt); 152 | using (var client = new TcpClient()) 153 | { 154 | // Delay and retry if the connection failed or network timeout was reached 155 | if (!client.ConnectWithTimeout(callbackInfo.Address, callbackInfo.Port, NETWORK_TIMEOUT_MS)) 156 | { 157 | // Backoff the retry delay time 158 | var newDelay = Math.Pow(2, retryAttempt); 159 | logger.Warn("Timeout or Task Fault, task states:\n\tConnection: Timed out\n\tTimeout: {0}\nRetry in: {1} seconds", NETWORK_TIMEOUT_MS, newDelay.ToString()); 160 | 161 | // Delay then retry connection 162 | Thread.Sleep(TimeSpan.FromSeconds(newDelay)); 163 | retryAttempt++; 164 | } 165 | else // Use the TCP Client and complete callback 166 | { 167 | logger.Debug("Task Completed, task states:\n\tConnection: OK\n\tTimeout: {0}", NETWORK_TIMEOUT_MS); 168 | clientConnected = true; 169 | 170 | using (var streamWriter = new StreamWriter(client.GetStream(), Encoding.ASCII)) 171 | { 172 | streamWriter.Write("JOB {0:d} COMPLETED", jobId); 173 | } 174 | } 175 | } 176 | } 177 | 178 | // Log connection success or failure 179 | if (clientConnected) 180 | { 181 | logger.Debug("Successfully sent job completion message for job {0}", jobId); 182 | } 183 | else 184 | { 185 | logger.Warn("Failed to contact client with completion message for job {0}", jobId); 186 | } 187 | } 188 | 189 | public bool IsDone() 190 | { 191 | return process.HasExited; 192 | } 193 | 194 | public string GetResult() 195 | { 196 | if (!this.IsDone()) 197 | { 198 | throw new InvalidOperationException("Process hasn't finished executing. Cannot get result."); 199 | } 200 | // wait until all streams have flushed 201 | // this call cannot have a timeout (see reference source - adding a timeout will cause streams 202 | // to be ignored), but we assume we're just waiting on logging, so any wait should be fast. 203 | logger.Debug($"Waiting for job {jobId} process output stream to flush"); 204 | process.WaitForExit(); 205 | logger.Debug($"Child process streams flushed on job {jobId}."); 206 | 207 | return output != null ? output.ToString() : String.Empty; 208 | } 209 | 210 | public int GetExitCode() 211 | { 212 | if (!this.IsDone()) 213 | { 214 | throw new InvalidOperationException("Process hasn't finished executing. Cannot get exit code."); 215 | } 216 | return process.ExitCode; 217 | } 218 | 219 | public void WaitForCompletion() 220 | { 221 | if (!this.IsDone()) 222 | { 223 | process.WaitForExit(); 224 | } 225 | } 226 | 227 | public void Kill() 228 | { 229 | logger.Info("Terminating job id: {0}", jobId); 230 | process.Kill(); 231 | } 232 | 233 | // connect to the client and create a network stream and create a filestream 234 | // to the file log. 235 | private bool CreateProgressStream(IPEndPoint streamEp) 236 | { 237 | 238 | bool connectionSuccessful = false; 239 | 240 | // setup file streaming regardless of what happens on the network; 241 | outputLogFilename = OUTPUT_LOG_BASENAME + DateTime.Now.ToString(OUTPUT_LOG_TIME_FORMAT) + ".txt"; 242 | outputLogFilename = Path.Combine(Path.GetTempPath(), outputLogFilename); 243 | outputLogSteam = new StreamWriter(new FileStream(outputLogFilename, FileMode.Create)); 244 | logger.Info("Recording process output to: {0} .", outputLogFilename); 245 | 246 | // make a note on the logfile what job this is and what was called. 247 | outputLogSteam.WriteLine($"SimpleRemote Job {jobId} Output - {DateTime.Now:g}"); 248 | outputLogSteam.WriteLine($"{process.StartInfo.FileName} {process.StartInfo.Arguments}"); 249 | outputLogSteam.WriteLine(); 250 | 251 | var progressClient = new TcpClient(); 252 | progressClient.SendTimeout = NETWORK_TIMEOUT_MS; 253 | 254 | try 255 | { 256 | if (!progressClient.ConnectWithTimeout(streamEp.Address, streamEp.Port, NETWORK_TIMEOUT_MS)) 257 | { 258 | // failed to connect due to timeout - log and proceed. 259 | connectionSuccessful = false; 260 | logger.Error("Failed to initiate network streaming progress - connection to client timed out "); 261 | 262 | } 263 | else 264 | { 265 | // connection successful 266 | logger.Debug("Successfully connected to progress listener."); 267 | progressStream = new StreamWriter(progressClient.GetStream()); 268 | connectionSuccessful = true; 269 | } 270 | } 271 | catch (Exception e) 272 | { 273 | if (e is SocketException || (e is AggregateException && e.InnerException is SocketException)) 274 | { 275 | connectionSuccessful = false; 276 | logger.Error("Failed to initiate network streaming progress - got socket exception while connecting"); 277 | } 278 | else throw; 279 | } 280 | 281 | return connectionSuccessful; 282 | } 283 | 284 | private void CloseStreams() 285 | { 286 | // ensure cleanup is threadsafe 287 | lock (lockObj) 288 | { 289 | try 290 | { 291 | // if necessary, shutdown network progress stream 292 | if (progressStream != null) 293 | { 294 | progressStream.Close(); //closes network streams 295 | progressStream = null; 296 | } 297 | } 298 | catch (IOException e) 299 | { 300 | if (!(e.InnerException is SocketException)) throw; 301 | logger.Error("A SocketException occurred while closing progress socket streams."); 302 | 303 | } 304 | finally 305 | { 306 | if (outputLogSteam != null) 307 | { 308 | outputLogSteam.Close(); //close file stream 309 | outputLogSteam = null; 310 | } 311 | } 312 | } 313 | } 314 | 315 | public void Dispose() 316 | { 317 | Dispose(true); 318 | GC.SuppressFinalize(this); 319 | } 320 | 321 | protected virtual void Dispose(bool safeToCleanManagedResources) 322 | { 323 | if (safeToCleanManagedResources) 324 | { 325 | CloseStreams(); 326 | 327 | // Don't dispose of the process handle here. If you do, the completion 328 | // callback won't fire if the process was killed (calling dispose will 329 | // break the callback). 330 | 331 | // See tests Job_CheckStreamingProgress_KillProcess and 332 | // Client_RunProcessAndKill_ExpectCompletionCallback to verify. 333 | 334 | //process.Dispose(); 335 | } 336 | } 337 | 338 | ~Job() 339 | { 340 | Dispose(false); 341 | } 342 | } 343 | 344 | public class JobCallbackInfo 345 | { 346 | public int Port; 347 | public IPAddress Address; 348 | 349 | public int ProgressPort; 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /SimpleDUTRemote/SimpleDUTRemote.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Library 5 | netstandard2.0 6 | 7 | library 8 | 9 | Subsystem Automation Taskforce 10 | 1.4.2 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /SimpleJsonRpc/BroadcastResponder.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using NLog; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Net.Sockets; 8 | using System.Text; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace SimpleJsonRpc 13 | { 14 | class BroadcastResponder 15 | { 16 | private static Logger logger = LogManager.GetCurrentClassLogger(); 17 | 18 | public static async Task StartBroadcastResponder(int rpcServerPort, int broadcasterResponderPort = 8001, CancellationToken? cancellationToken = null) 19 | { 20 | logger.Info("Started broadcast responder on port {0}", broadcasterResponderPort); 21 | CancellationToken token = cancellationToken ?? CancellationToken.None; 22 | 23 | using (var client = new UdpClient(broadcasterResponderPort)) 24 | { 25 | // setup client for broadcast 26 | client.EnableBroadcast = true; 27 | 28 | // fire task on cancelation so we can use await whenany 29 | var tcs = new TaskCompletionSource(); 30 | token.Register(() => tcs.TrySetCanceled()); 31 | var cancellationTask = tcs.Task; 32 | 33 | while (true) 34 | { 35 | var completedTask = await Task.WhenAny(client.ReceiveAsync(), cancellationTask); 36 | if (completedTask.IsCanceled){ 37 | logger.Info("Stopping broadcast responder."); 38 | break; 39 | } 40 | 41 | // if we're here, we got a UDP message. 42 | var udpResult = completedTask.Result; 43 | 44 | if (Encoding.ASCII.GetString(udpResult.Buffer) == "SimpleJsonRpc Ping") 45 | { 46 | var clientEndpoint = udpResult.RemoteEndPoint; 47 | byte[] resp = BitConverter.GetBytes(rpcServerPort); 48 | client.Send(resp, resp.Length, clientEndpoint); 49 | 50 | logger.Info($"Responded to broadcast packet from: {clientEndpoint.Address}"); 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /SimpleJsonRpc/SimpleJsonRpc.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Microsoft.SurfaceAutomationTeam.SimpleRemote.SimpleJsonRpc 5 | Microsoft 6 | 1.4.2 7 | Microsoft 8 | SimpleRemote 9 | A minimalist json-rpc server for .NET Standard. 10 | (c) Microsoft Corporation. All rights reserved. 11 | MIT 12 | https://github.com/microsoft/SimpleRemote 13 | git 14 | https://github.com/microsoft/SimpleRemote 15 | https://raw.githubusercontent.com/microsoft/SimpleRemote/master/assets/TeamLogo.png 16 | TeamLogo.png 17 | rpc;SimpleRemote;jsonrpc 18 | 19 | 20 | 21 | netstandard2.0 22 | true 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /SimpleJsonRpc/SimpleRpcMethod.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | 8 | namespace SimpleJsonRpc 9 | { 10 | [AttributeUsage(AttributeTargets.Method)] 11 | public class SimpleRpcMethod : System.Attribute 12 | { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /SimpleJsonRpc/SimpleRpcServer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Reflection; 8 | using NLog; 9 | using System.Net; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using System.Net.Sockets; 13 | using System.IO; 14 | using Newtonsoft.Json; 15 | 16 | namespace SimpleJsonRpc 17 | { 18 | /// 19 | /// Main class for the RPC system. 20 | /// 21 | /// The RpcServer class handles processing RPC requests. It can be used either as a stand-alone server, 22 | /// or with an existing solution (and just provide parsing, method lookup and execution, and result serialization). 23 | /// 24 | /// The general flow for using this class is: 25 | /// - Mark methods you want to call using the SimpleRpcMethod attribute. 26 | /// - Pass objects that have the attribute to the Register() function. 27 | /// - (For internal server) Call Start() to start the server. 28 | /// - (For other server) Call HandleJsonString() to handle inbound requests. 29 | /// 30 | /// This class handles parsing requests, looking up and running methods, and serializing results. If you're looking 31 | /// for the functions that you can call via RPC, please see you should review the documentation for the registered object. 32 | public class SimpleRpcServer 33 | { 34 | private Dictionary rpcMethods; 35 | private TcpListener serverListener; 36 | private static Logger logger = LogManager.GetCurrentClassLogger(); 37 | private CancellationTokenSource broadcastTaskCancelation; 38 | 39 | //track extra information for a request 40 | public static AsyncLocal currentClient { get; private set; } 41 | = new AsyncLocal(); 42 | 43 | public event Action Starting; 44 | public event Action Stopping; 45 | 46 | // tracking bool to ensure we don't raise exceptions from AcceptTcpClientAsync while 47 | // stopping. AcceptTcpClientAsync doesn't support cancelation in .NET Framework, so our 48 | // only way of shutting it down is to close the underlying socket. 49 | private bool isSocketShuttingDown = false; 50 | 51 | 52 | public SimpleRpcServer() 53 | { 54 | rpcMethods = new Dictionary(); 55 | broadcastTaskCancelation = new CancellationTokenSource(); 56 | } 57 | 58 | /// 59 | /// Start the internal json rpc server, on a specific port. Optionally start broadcast server. 60 | /// 61 | /// This starts a TcpListener on the given port, listens for inbound json rpc requests, 62 | /// reads to the first new line character, parses the request, calls the appropriate registered 63 | /// method based on the method name, and sends back the json rpc response. 64 | /// 65 | /// If broadcastPort is specified, it will create a UDP listener on the given port, 66 | /// and wait for broadcast packets with the message "SimpleJsonRpc Ping". When one is received, 67 | /// it will respond with the current json rpc server port. This is primarily used in lab test 68 | /// environments only. 69 | /// 70 | /// If you plan on using this behind another server, you may be better served by skipping 71 | /// this method and using HandleJsonString() directly. 72 | /// 73 | /// Port to listen for rpc requests 74 | /// IP to bind to - use null for all interfaces. 75 | /// Port to use to listen for broadcast pings. Use null to disable broadcast 76 | /// support. 77 | /// Task for the running server. 78 | public async Task Start(int serverPort = 8000, IPAddress ip = null, int? broadcastPort = null) 79 | { 80 | ip = ip ?? IPAddress.Any; 81 | serverListener = new TcpListener(ip, serverPort); 82 | serverListener.Start(); 83 | Starting?.Invoke(); 84 | logger.Info("RPC Server started."); 85 | logger.Info("Ready for client connection"); 86 | 87 | if (broadcastPort != null) 88 | { 89 | var token = broadcastTaskCancelation.Token; 90 | 91 | var broadcastTask = BroadcastResponder.StartBroadcastResponder(serverPort, 92 | broadcastPort.Value, token); 93 | } 94 | 95 | // this is our core loop - we need to: 96 | // 1. read the request, and parse it. 97 | // 2. call the requested method 98 | // 3. return the response 99 | try 100 | { 101 | while (true) 102 | { 103 | // 1a. get our connection and our instruction 104 | var client = await serverListener.AcceptTcpClientAsync(); 105 | 106 | logger.Info($"Client connected: {((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString()}"); 107 | 108 | // 1b-3. Handle our client in a seperate thread, so we don't block inbound 109 | // connections while doing work. 110 | // Also, suppress compiler warning that there isn't an await here. This is a fire-and-forget method. 111 | #pragma warning disable 4014 112 | Task.Factory.StartNew(() => HandleClient(client)); 113 | #pragma warning restore 4014 114 | } 115 | } 116 | catch (Exception e) 117 | { 118 | // In some versions of .NET this will be an ObjectDisposedError, while in others it will be 119 | // a SocketException. To avoid this, since our goal is to ignore this as long as it's the result 120 | // of a shutdown, we check the isSocketShuttingDown flag - if it's true, we ignore this exception. 121 | // Otherwise we re-throw. 122 | 123 | if (isSocketShuttingDown) { 124 | logger.Info("Server stopped"); 125 | } 126 | else { 127 | logger.Error(e); 128 | throw; 129 | } 130 | 131 | } 132 | 133 | } 134 | 135 | /// 136 | /// Stops the internal json rpc server. 137 | /// 138 | public void Stop() 139 | { 140 | isSocketShuttingDown = true; 141 | serverListener.Stop(); 142 | 143 | broadcastTaskCancelation?.Cancel(); 144 | 145 | Stopping?.Invoke(); 146 | 147 | 148 | } 149 | 150 | /// 151 | /// Register an object that has SimpleRpcMethod annotations. 152 | /// 153 | /// Register functions (with the SimpleRpcMethod annotation) with the rpc system. 154 | /// Once registered, inbound rpc requests can call methods that were annotated. 155 | /// 156 | /// This stores a copy of the object - any inbound rpc call that matches a registered 157 | /// method name will be called against the provided object. 158 | /// 159 | /// If you register several objects that have an annotated function with the same name, 160 | /// the last one registered will be called. 161 | /// Object with SimpleRpcMethod annotations. 162 | public void Register(object rpcObject) 163 | { 164 | Type t = rpcObject.GetType(); 165 | t.GetMethods() 166 | .Where(m => m.GetCustomAttributes(typeof(SimpleRpcMethod), true).Length > 0).ToList() 167 | .ForEach(x => 168 | { 169 | rpcMethods[x.Name] = (x, rpcObject); 170 | }); 171 | } 172 | 173 | /// 174 | /// Manually process an inbound json rpc request. 175 | /// 176 | /// If you aren't using the built in server, you can use this method to manually 177 | /// process a json-rpc string, and have it return the result string. 178 | /// Json rpc request (as a string) 179 | /// Json rpc result, as a string. 180 | public string HandleJsonString(string jsonString) 181 | { 182 | // report what we received in debug log. 183 | logger.Debug($"Received json: {jsonString}"); 184 | 185 | // parse into our JsonRpcRequest 186 | JsonRpcRequest request = JsonConvert.DeserializeObject(jsonString); 187 | 188 | // run the requested function 189 | // and build our response object 190 | JsonRpcResponse response = new JsonRpcResponse(); 191 | response.id = request.id; 192 | 193 | try 194 | { 195 | response.result = CallMethod(request.method, request.args.ToArray()); 196 | 197 | } 198 | catch (Exception e) 199 | { 200 | response.error = e.ToString(); 201 | logger.Error(e); 202 | } 203 | 204 | return JsonConvert.SerializeObject(response); 205 | } 206 | 207 | public void HandleClient(TcpClient client) 208 | { 209 | using (client) 210 | using (StreamReader reader = new StreamReader(client.GetStream())) 211 | using (StreamWriter writer = new StreamWriter(client.GetStream())) 212 | { 213 | currentClient.Value = (IPEndPoint)client.Client.RemoteEndPoint; 214 | 215 | // make sure if we time out we don't hold threads 216 | // while waiting for clients that may have stalled. 217 | // But we don't want this while debugging. 218 | #if (!DEBUG) 219 | client.ReceiveTimeout = 10 * 1000; 220 | #endif 221 | // 1b. Get our instructions 222 | // and drop any client that takes more than 10 seconds after connecting 223 | // to send. 224 | string jsonString; 225 | try { jsonString = reader.ReadLine(); } 226 | catch (IOException) 227 | { 228 | logger.Info("Connection timed out."); 229 | return; 230 | } 231 | 232 | // 2. Parse the string into a json rpc request and call the appropriate method. 233 | // then serialize the response back into a string. 234 | var resultString = HandleJsonString(jsonString); 235 | 236 | 237 | // 3. send back our response 238 | writer.WriteLine(resultString); 239 | writer.Flush(); 240 | 241 | // using block will close down connection 242 | logger.Info($"RPC call complete, closing connection to client {((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString()}"); 243 | 244 | currentClient.Value = null; 245 | } 246 | 247 | } 248 | 249 | private object CallMethod(string rpcMethodName, object[] methodArgs) 250 | { 251 | // locate the method 252 | if (!rpcMethods.ContainsKey(rpcMethodName)) 253 | { 254 | throw new ArgumentException($"Rpc method name: {rpcMethodName} not found in registered functions."); 255 | } 256 | 257 | MethodInfo method = rpcMethods[rpcMethodName].meth; 258 | var callableObject = rpcMethods[rpcMethodName].obj; 259 | 260 | // determine if the last item is a parameter array 261 | // see https://stackoverflow.com/questions/627656/determining-if-a-parameter-uses-params-using-reflection-in-c 262 | // and https://stackoverflow.com/questions/6484651/calling-a-function-using-reflection-that-has-a-params-parameter-methodbase 263 | var paramArray = method.GetParameters(); 264 | if (paramArray.Length > 0 && paramArray.Last().IsDefined(typeof(ParamArrayAttribute), false)) 265 | { 266 | // we need the object array to have a sub-array with anything after the number of normal parameters. 267 | // copy all the normal parameters into an object array 268 | object[] normalargs = new object[paramArray.Length]; 269 | Array.Copy(methodArgs, normalargs, paramArray.Length - 1); 270 | 271 | // copy all the extra args into a seperate array 272 | Array extraArgs = Array.CreateInstance(paramArray.Last().ParameterType.GetElementType(), methodArgs.Length - (paramArray.Length - 1)); 273 | Array.Copy(methodArgs, paramArray.Length - 1, extraArgs, 0, extraArgs.Length); 274 | 275 | // set the last item of the paramArray to to the extraArgs array 276 | normalargs[normalargs.Length - 1] = extraArgs; 277 | 278 | // rebind methodArgs 279 | methodArgs = normalargs; 280 | } 281 | 282 | // handle default args - add type missing for all arguments that are optional. 283 | // if we're handling parameter arrays, this isn't an issue since then every other arg was populated. 284 | if (paramArray.Length > methodArgs.Length) 285 | { 286 | object[] argsWithDefaults = new object[paramArray.Length]; 287 | Array.Copy(methodArgs, argsWithDefaults, methodArgs.Length); 288 | 289 | for (int i = methodArgs.Length; i < paramArray.Length; i++) 290 | { 291 | argsWithDefaults[i] = Type.Missing; 292 | } 293 | 294 | // rebind methodargs 295 | methodArgs = argsWithDefaults; 296 | } 297 | 298 | object result = method.Invoke(callableObject, methodArgs); 299 | return result; 300 | } 301 | 302 | } 303 | 304 | public class JsonRpcRequest 305 | { 306 | public string jsonrpc = "2.0"; 307 | public string method; 308 | 309 | [JsonProperty(PropertyName = "params")] 310 | public List args = new List(); 311 | 312 | public int id; 313 | } 314 | 315 | public class JsonRpcResponse 316 | { 317 | public string jsonrpc = "2.0"; 318 | 319 | [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] 320 | public object result; 321 | 322 | public int id; 323 | 324 | [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] 325 | public string error; 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /SimpleRemoteConsole/Nlog.config: -------------------------------------------------------------------------------- 1 |  2 | 8 | 9 | 12 | 13 | 14 | 18 | 19 | 20 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /SimpleRemoteConsole/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using SimpleDUTRemote; 5 | using System; 6 | using System.Net; 7 | using System.Threading; 8 | using NLog; 9 | using NLog.Config; 10 | using System.Linq; 11 | using System.Net.NetworkInformation; 12 | using System.Collections.Generic; 13 | using SimpleJsonRpc; 14 | using System.IO; 15 | using System.Reflection; 16 | using System.Threading.Tasks; 17 | using SimpleRemoteConsole.ServiceInterop; 18 | 19 | namespace SimpleRemoteConsole 20 | { 21 | class Program 22 | { 23 | static AutoResetEvent stopEvt = new AutoResetEvent(false); 24 | static Task serverTask; 25 | 26 | static void Main(string[] args) 27 | { 28 | if (args.Contains("--SuppressUserWarning") || args.Contains("--start-service")) 29 | { 30 | Console.WriteLine("User warning suppressed."); 31 | } 32 | else if (CheckUserWarning()) 33 | { 34 | Console.WriteLine("User warning acknowledged. Proceeding..."); 35 | } 36 | else 37 | { 38 | Console.WriteLine("Aborting - user declined to proceed after warning."); 39 | return; 40 | } 41 | 42 | const string svcName = "SimpleDUTRemote-Service"; 43 | List argsList = new List(args); 44 | ServiceInfo info = new ServiceInfo() 45 | { 46 | ServiceName = svcName, 47 | DisplayName = svcName, 48 | BinaryPath = Assembly.GetEntryAssembly().Location, 49 | ServiceArgs = argsList.ToArray(), 50 | StartHandler = InitializeServer, 51 | StopHandler = () => { }, 52 | StartType = StartType.SERVICE_DEMAND_START 53 | }; 54 | 55 | try 56 | { 57 | // installs and launches the service. It doesn't fail 58 | // if the service is already installed and does not 59 | // uninstall then reinstall the service; it will just launch 60 | // the existing service 61 | if (args.Contains("--install-service")) 62 | { 63 | // change the argument from install to start so 64 | // that when the service is started and this method 65 | // is re-entered, it takes the start service path 66 | argsList.Remove("--install-service"); 67 | argsList.Add("--start-service"); 68 | info.ServiceArgs = argsList.ToArray(); 69 | 70 | var svcStartIndex = argsList.IndexOf("--service-start-type"); 71 | if (svcStartIndex != -1 && argsList[svcStartIndex + 1].ToLower() == "auto") 72 | { 73 | info.StartType = StartType.SERVICE_AUTO_START; 74 | } 75 | 76 | Service svc = new Service(info); 77 | svc.CreateService(); 78 | } 79 | else if (args.Contains("--uninstall-service")) 80 | { 81 | Service svc = new Service(info); 82 | svc.RemoveService(); 83 | } 84 | else if (args.Contains("--start-service")) 85 | { 86 | Service svc = new Service(info); 87 | svc.StartService(); 88 | } 89 | // if there are no service related commands, we launch the server as usual 90 | else 91 | { 92 | InitializeServer(args); 93 | 94 | Console.CancelKeyPress += HandleCancelEvent; 95 | // wait for Ctrl+C 96 | stopEvt.WaitOne(); 97 | 98 | Console.WriteLine("Stopping Server."); 99 | } 100 | } 101 | catch (Exception e) 102 | { 103 | Console.WriteLine("Exception occurred: " + e.Message); 104 | Console.WriteLine("Exiting..."); 105 | } 106 | } 107 | 108 | static void InitializeServer(string[] args) 109 | { 110 | Logger logger = LogManager.GetCurrentClassLogger(); 111 | 112 | // determine server port number (default to 8000 unless --port is specified) 113 | int portNumber = 8000; 114 | int broadcastPort; 115 | 116 | if (args.Contains("--port")) 117 | { 118 | var portArgNumber = Array.IndexOf(args, "--port") + 1; 119 | int.TryParse(args[portArgNumber], out portNumber); 120 | } 121 | 122 | // determine broadcast port (default to server port + 1 unless --broadcastPort is specified) 123 | if (args.Contains("--broadcastPort")) 124 | { 125 | var bcastArgNumber = Array.IndexOf(args, "--broadcastPort") + 1; 126 | int.TryParse(args[bcastArgNumber], out broadcastPort); 127 | } 128 | else 129 | broadcastPort = portNumber + 1; 130 | 131 | Console.WriteLine("Starting Simple Remote on this system..."); 132 | Console.WriteLine($"You can connect on {Dns.GetHostName()}:{portNumber}"); 133 | foreach (var ip in GetLocalIPAddresses()) 134 | { 135 | Console.WriteLine($"You can also use {ip.Item2}:{portNumber} ({ip.Item1})"); 136 | } 137 | Console.WriteLine(); 138 | 139 | // create our object that has our functions 140 | var remotes = new Functions(); 141 | 142 | // create our server object and register our functions 143 | var server = new SimpleRpcServer(); 144 | server.Register(remotes); 145 | 146 | Console.WriteLine("Now ready for connections; press Ctrl+C to exit."); 147 | serverTask = server.Start(portNumber, null, broadcastPort); 148 | } 149 | 150 | // return list of tuples containing the interface name and the IP address as a string. 151 | static IEnumerable> GetLocalIPAddresses() 152 | { 153 | var interfaceAndUnicastCollectionTuples = NetworkInterface.GetAllNetworkInterfaces() 154 | .Where(x => x.OperationalStatus == OperationalStatus.Up) 155 | .Where(x => x.NetworkInterfaceType != NetworkInterfaceType.Loopback) 156 | .Select(x => Tuple.Create(x, x.GetIPProperties().UnicastAddresses)); 157 | 158 | List> ips = new List>(); 159 | 160 | // filter all ip addresses so we only collect IPv4 161 | foreach (var interfaceAndCollection in interfaceAndUnicastCollectionTuples) 162 | { 163 | foreach(var ip in interfaceAndCollection.Item2) 164 | { 165 | if (ip.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) 166 | ips.Add(Tuple.Create(interfaceAndCollection.Item1.Name, ip.Address.ToString())); 167 | } 168 | } 169 | 170 | return ips; 171 | } 172 | 173 | static bool CheckUserWarning() 174 | { 175 | var pathToThisExe = new FileInfo(Assembly.GetEntryAssembly().Location).DirectoryName; 176 | var pathToAckFile = Path.Combine(pathToThisExe, "UserWarningAcknowledged"); 177 | var pathToWarnTxt = Path.Combine(pathToThisExe, "UserWarning.txt"); 178 | 179 | if (File.Exists(pathToAckFile)) 180 | { 181 | // user acknowledged warning already (or preset it) 182 | return true; 183 | } 184 | else 185 | { 186 | // show the warning 187 | var warningText = File.ReadAllText(pathToWarnTxt); 188 | Console.Write(warningText); 189 | 190 | // get user response 191 | ConsoleKeyInfo resp; 192 | while (true) 193 | { 194 | resp = Console.ReadKey(); 195 | if (! new[] { 'Y', 'y', 'N', 'N'}.Contains(resp.KeyChar)) 196 | { 197 | Console.WriteLine(); 198 | Console.Write("Please enter Y or N: "); 199 | 200 | } 201 | else break; 202 | } 203 | 204 | if (resp.KeyChar == 'y' || resp.KeyChar == 'Y') 205 | { 206 | // user has acknowledged risk. Proceed and don't ask again 207 | File.Create(pathToAckFile).Dispose(); // close the file immediately. 208 | Console.WriteLine(); 209 | return true; 210 | } 211 | else 212 | { 213 | Console.WriteLine(); 214 | return false; 215 | } 216 | } 217 | } 218 | 219 | private static void HandleCancelEvent(object sender, ConsoleCancelEventArgs args) 220 | { 221 | // don't terminate the process, we'll do that with our own event. 222 | args.Cancel = true; 223 | 224 | // only stop on ctrl+c, not ctrl+break 225 | if (args.SpecialKey == ConsoleSpecialKey.ControlC) 226 | { 227 | stopEvt.Set(); 228 | } 229 | 230 | } 231 | } 232 | } -------------------------------------------------------------------------------- /SimpleRemoteConsole/ServiceInterop/NativeServiceWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace SimpleRemoteConsole.ServiceInterop 5 | { 6 | internal enum SCMPermissions 7 | { 8 | SC_MANAGER_ALL_ACCESS = 0xF003F 9 | } 10 | 11 | internal enum ServicePermissions 12 | { 13 | SERVICE_ALL_ACCESS = 0xF01FF, 14 | SERVICE_START = 0x10, 15 | SERVICE_STOP = 0x20, 16 | SERVICE_DELETE = 0x10000 17 | } 18 | 19 | internal enum ServiceErrorCodes 20 | { 21 | ERROR_SERVICE_EXISTS = 1073, 22 | ERROR_SERVICE_ALREADY_RUNNING = 1056, 23 | ERROR_SERVICE_NOT_ACTIVE = 1062, 24 | ERROR_SERVICE_CANNOT_ACCEPT_CTRL = 1061, 25 | ERROR_SHUTDOWN_IN_PROGRESS = 1115 26 | } 27 | 28 | internal static class NativeServiceWrapper 29 | { 30 | private const string AdvAPI32Lib = "advapi32.dll"; 31 | 32 | internal delegate void ServiceControlHandler(uint control, uint eventType, IntPtr eventData, IntPtr eventContext); 33 | internal delegate void ServiceMainFunction(int argc, IntPtr argv); 34 | 35 | #region Service Imports 36 | 37 | [DllImport(AdvAPI32Lib, ExactSpelling = true, SetLastError = true)] 38 | internal static extern bool CloseServiceHandle(IntPtr handle); 39 | 40 | [DllImport(AdvAPI32Lib, ExactSpelling = true, SetLastError = true, CharSet = CharSet.Unicode)] 41 | internal static extern IntPtr RegisterServiceCtrlHandlerExW(string serviceName, ServiceControlHandler serviceControlHandler, IntPtr context); 42 | 43 | [DllImport(AdvAPI32Lib, ExactSpelling = true, SetLastError = true)] 44 | internal static extern bool SetServiceStatus(IntPtr statusHandle, ref ServiceStatus pServiceStatus); 45 | 46 | [DllImport(AdvAPI32Lib, ExactSpelling = true, SetLastError = true, CharSet = CharSet.Unicode)] 47 | internal static extern IntPtr OpenSCManagerW(string machineName, string databaseName, uint dwAccess); 48 | 49 | [DllImport(AdvAPI32Lib, ExactSpelling = true, SetLastError = true, CharSet = CharSet.Unicode)] 50 | internal static extern IntPtr CreateServiceW( 51 | IntPtr serviceControlManager, 52 | string serviceName, 53 | string displayName, 54 | uint desiredControlAccess, 55 | ServiceType serviceType, 56 | StartType startType, 57 | ErrorControl errorSeverity, 58 | string binaryPath, 59 | string loadOrderGroup, 60 | IntPtr outUIntTagId, 61 | string dependencies, 62 | string serviceUserName, 63 | string servicePassword); 64 | 65 | [DllImport(AdvAPI32Lib, ExactSpelling = true, SetLastError = true, CharSet = CharSet.Unicode)] 66 | internal static extern IntPtr OpenServiceW(IntPtr serviceControlManager, string serviceName, uint desiredControlAccess); 67 | 68 | [DllImport(AdvAPI32Lib, ExactSpelling = true, SetLastError = true)] 69 | internal static extern bool StartServiceW(IntPtr service, uint argc, string[] wargv); 70 | 71 | [DllImport(AdvAPI32Lib, ExactSpelling = true, SetLastError = true, CharSet = CharSet.Unicode)] 72 | internal static extern bool StartServiceCtrlDispatcherW([MarshalAs(UnmanagedType.LPArray)] ServiceTableEntry[] serviceTable); 73 | 74 | [DllImport(AdvAPI32Lib, ExactSpelling = true, SetLastError = true)] 75 | internal static extern bool ControlService(IntPtr service, ServiceControls dwControl, ref ServiceStatus pServiceStatus); 76 | 77 | [DllImport(AdvAPI32Lib, ExactSpelling = true, SetLastError = true)] 78 | internal static extern bool DeleteService(IntPtr service); 79 | 80 | #endregion 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /SimpleRemoteConsole/ServiceInterop/Service.cs: -------------------------------------------------------------------------------- 1 | using NLog; 2 | using System; 3 | using System.ComponentModel; 4 | using System.Runtime.InteropServices; 5 | using System.Text; 6 | using System.Threading; 7 | 8 | namespace SimpleRemoteConsole.ServiceInterop 9 | { 10 | public enum ServiceType 11 | { 12 | SERVICE_FILE_SYSTEM_DRIVER = 0x2, 13 | SERVICE_KERNEL_DRIVER = 0x1, 14 | SERVICE_WIN32_OWN_PROCESS = 0x10, 15 | SERVICE_WIN32_SHARE_PROCESS = 0x20, 16 | SERVICE_USER_OWN_PROCESS = 0x50, 17 | SERVICE_USER_SHARE_PROCESS = 0x60 18 | } 19 | 20 | public enum StartType 21 | { 22 | SERVICE_AUTO_START = 0x2, 23 | SERVICE_BOOT_START = 0x0, 24 | SERVICE_DEMAND_START = 0x3, 25 | SERVICE_DISABLED = 0x4, 26 | SERVICE_SYSTEM_START = 0x1 27 | } 28 | 29 | public enum ErrorControl 30 | { 31 | SERVICE_ERROR_CRITICAL = 0x3, 32 | SERVICE_ERROR_IGNORE = 0x0, 33 | SERVICE_ERROR_NORMAL = 0x1, 34 | SERVICE_ERROR_SEVERE = 0x2 35 | } 36 | 37 | public class Service 38 | { 39 | private Logger logger; 40 | 41 | private NativeServiceWrapper.ServiceMainFunction svcMain; 42 | private NativeServiceWrapper.ServiceControlHandler svcCtrlHandler; 43 | 44 | private AutoResetEvent stopEvent = new AutoResetEvent(false); 45 | 46 | private uint serviceCheckPoint; 47 | private ServiceStatus svcStatus; 48 | private IntPtr hSvcStatus; 49 | 50 | private string serviceName; 51 | private string displayName; 52 | private string binaryPath; 53 | private string[] serviceArgs; 54 | private string username = null; 55 | private string password = null; 56 | private ServiceType serviceType = ServiceType.SERVICE_WIN32_OWN_PROCESS; 57 | private StartType startType = StartType.SERVICE_AUTO_START; 58 | private ErrorControl errorSeverity = ErrorControl.SERVICE_ERROR_NORMAL; 59 | private ServiceInfo.OnStart startHandler; 60 | private ServiceInfo.OnStop stopHandler; 61 | 62 | public Service(ServiceInfo serviceInfo) 63 | { 64 | logger = LogManager.GetCurrentClassLogger(); 65 | 66 | svcMain = InitializeService; 67 | svcCtrlHandler = SvcControlHandler; 68 | 69 | serviceName = serviceInfo.ServiceName; 70 | displayName = serviceInfo.DisplayName; 71 | binaryPath = serviceInfo.BinaryPath; 72 | serviceArgs = serviceInfo.ServiceArgs; 73 | username = serviceInfo.Username; 74 | password = serviceInfo.Password; 75 | serviceType = serviceInfo.ServiceType; 76 | startType = serviceInfo.StartType; 77 | errorSeverity = serviceInfo.ErrorSeverity; 78 | startHandler = serviceInfo.StartHandler; 79 | stopHandler = serviceInfo.StopHandler; 80 | 81 | serviceCheckPoint = 1; 82 | svcStatus = new ServiceStatus(); 83 | svcStatus.dwServiceType = (uint)serviceType; 84 | svcStatus.dwServiceSpecificExitCode = 0; 85 | hSvcStatus = IntPtr.Zero; 86 | } 87 | 88 | public void CreateService(bool start = true) 89 | { 90 | var hSCManager = IntPtr.Zero; 91 | var hService = IntPtr.Zero; 92 | 93 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 94 | { 95 | var errorMsg = "Services can only be installed on Windows platforms."; 96 | logger.Error("Create service failed: " + errorMsg); 97 | throw new PlatformNotSupportedException(); 98 | } 99 | 100 | try 101 | { 102 | hSCManager = NativeServiceWrapper.OpenSCManagerW(null, null, (uint)SCMPermissions.SC_MANAGER_ALL_ACCESS); 103 | if (hSCManager == IntPtr.Zero) 104 | { 105 | var error = Marshal.GetLastWin32Error(); 106 | var errorMsg = "Error when opening SCM: " + error; 107 | 108 | if (error == 0x5) 109 | { 110 | errorMsg = "Error when opening SCM: Access Denied. Please try again with administrator privileges."; 111 | } 112 | 113 | logger.Error("Create service failed: " + errorMsg); 114 | throw new Win32Exception(error, errorMsg); 115 | } 116 | 117 | var serviceCmd = new StringBuilder(); 118 | serviceCmd.AppendFormat("\"{0}\"", binaryPath); 119 | if (serviceArgs != null) 120 | { 121 | foreach (var arg in serviceArgs) 122 | { 123 | serviceCmd.Append(" "); 124 | serviceCmd.Append(arg); 125 | } 126 | } 127 | 128 | hService = NativeServiceWrapper.CreateServiceW(hSCManager, serviceName, displayName, (uint)ServicePermissions.SERVICE_ALL_ACCESS, serviceType, startType, errorSeverity, serviceCmd.ToString(), null, IntPtr.Zero, null, username, password); 129 | if (hService == IntPtr.Zero) 130 | { 131 | var error = Marshal.GetLastWin32Error(); 132 | if (error != (int)ServiceErrorCodes.ERROR_SERVICE_EXISTS) 133 | { 134 | var errorMsg = "Create service failed with error: " + error; 135 | logger.Error("Create service failed: " + errorMsg); 136 | throw new Win32Exception(error, errorMsg); 137 | } 138 | 139 | hService = NativeServiceWrapper.OpenServiceW(hSCManager, serviceName, (uint)ServicePermissions.SERVICE_START); 140 | if (hService == IntPtr.Zero) 141 | { 142 | error = Marshal.GetLastWin32Error(); 143 | var errorMsg = "Could not open the existing service. Error: " + error; 144 | logger.Error("Create service failed: " + errorMsg); 145 | throw new Win32Exception(error, errorMsg); 146 | } 147 | } 148 | 149 | if (start) 150 | { 151 | LaunchService(hSCManager, hService); 152 | } 153 | } 154 | finally 155 | { 156 | if (hService != IntPtr.Zero) 157 | { 158 | NativeServiceWrapper.CloseServiceHandle(hService); 159 | } 160 | 161 | if (hSCManager != IntPtr.Zero) 162 | { 163 | NativeServiceWrapper.CloseServiceHandle(hSCManager); 164 | } 165 | } 166 | } 167 | 168 | /// 169 | /// Launches the service and then returns. 170 | /// 171 | public void LaunchService() 172 | { 173 | var hSCManager = NativeServiceWrapper.OpenSCManagerW(null, null, (uint)SCMPermissions.SC_MANAGER_ALL_ACCESS); 174 | if (hSCManager == IntPtr.Zero) 175 | { 176 | var error = Marshal.GetLastWin32Error(); 177 | var errorMsg = "Error when opening SCM: " + error; 178 | 179 | if (error == 0x5) 180 | { 181 | errorMsg = "Error when opening SCM: Access Denied. Please try again with administrator privileges."; 182 | } 183 | 184 | logger.Error("Launch service failed: " + errorMsg); 185 | throw new Win32Exception(error, errorMsg); 186 | } 187 | 188 | try 189 | { 190 | LaunchService(hSCManager); 191 | } 192 | finally 193 | { 194 | NativeServiceWrapper.CloseServiceHandle(hSCManager); 195 | } 196 | } 197 | 198 | private void LaunchService(IntPtr hSCManager) 199 | { 200 | var hService = NativeServiceWrapper.OpenServiceW(hSCManager, serviceName, (uint)ServicePermissions.SERVICE_START); 201 | if (hService == IntPtr.Zero) 202 | { 203 | var error = Marshal.GetLastWin32Error(); 204 | var errorMsg = "Could not open the existing service. Error: " + error; 205 | logger.Error("Launch service failed: " + errorMsg); 206 | throw new Win32Exception(error, errorMsg); 207 | } 208 | 209 | try 210 | { 211 | LaunchService(hSCManager, hService); 212 | } 213 | finally 214 | { 215 | NativeServiceWrapper.CloseServiceHandle(hService); 216 | } 217 | } 218 | 219 | private void LaunchService(IntPtr hSCManager, IntPtr hService) 220 | { 221 | var svcArgs = serviceArgs ?? new string[0]; 222 | if (!NativeServiceWrapper.StartServiceW(hService, (uint)svcArgs.Length, svcArgs)) 223 | { 224 | var error = Marshal.GetLastWin32Error(); 225 | if (error != (int)ServiceErrorCodes.ERROR_SERVICE_ALREADY_RUNNING) 226 | { 227 | var errorMsg = "Error when starting service: " + error; 228 | logger.Error("Launch service failed: " + errorMsg); 229 | throw new Win32Exception(error, errorMsg); 230 | } 231 | } 232 | } 233 | 234 | public void RemoveService() 235 | { 236 | var hSCManager = IntPtr.Zero; 237 | var hService = IntPtr.Zero; 238 | 239 | try 240 | { 241 | hSCManager = NativeServiceWrapper.OpenSCManagerW(null, null, (uint)SCMPermissions.SC_MANAGER_ALL_ACCESS); 242 | if (hSCManager == IntPtr.Zero) 243 | { 244 | var error = Marshal.GetLastWin32Error(); 245 | var errorMsg = "Error when opening SCM: " + error; 246 | 247 | if (error == 0x5) 248 | { 249 | errorMsg = "Error when opening SCM: Access Denied. Please try again with administrator privileges."; 250 | } 251 | 252 | logger.Error("Remove service failed: " + errorMsg); 253 | throw new Win32Exception(error, errorMsg); 254 | } 255 | 256 | hService = NativeServiceWrapper.OpenServiceW(hSCManager, serviceName, (uint)ServicePermissions.SERVICE_DELETE | (uint)ServicePermissions.SERVICE_STOP); 257 | if (hService == IntPtr.Zero) 258 | { 259 | var error = Marshal.GetLastWin32Error(); 260 | var errorMsg = "Could not open the service for removal. Error: " + error; 261 | logger.Error("Remove service failed: " + errorMsg); 262 | throw new Win32Exception(error, errorMsg); 263 | } 264 | 265 | ServiceStatus status = new ServiceStatus(); 266 | if (!NativeServiceWrapper.ControlService(hService, ServiceControls.SERVICE_CONTROL_STOP, ref status)) 267 | { 268 | var error = Marshal.GetLastWin32Error(); 269 | if (error != (int)ServiceErrorCodes.ERROR_SERVICE_NOT_ACTIVE && 270 | error != (int)ServiceErrorCodes.ERROR_SERVICE_CANNOT_ACCEPT_CTRL && 271 | error != (int)ServiceErrorCodes.ERROR_SHUTDOWN_IN_PROGRESS) 272 | { 273 | var errorMsg = "Could not stop the service for removal. Error: " + error; 274 | logger.Error("Remove service failed: " + errorMsg); 275 | throw new Win32Exception(error, errorMsg); 276 | } 277 | } 278 | 279 | if (!NativeServiceWrapper.DeleteService(hService)) 280 | { 281 | var error = Marshal.GetLastWin32Error(); 282 | var errorMsg = "Could not delete the service. Error: " + error; 283 | logger.Error("Remove service failed: " + errorMsg); 284 | throw new Win32Exception(error, errorMsg); 285 | } 286 | } 287 | finally 288 | { 289 | if (hService != IntPtr.Zero) 290 | { 291 | NativeServiceWrapper.CloseServiceHandle(hService); 292 | } 293 | 294 | if (hSCManager != IntPtr.Zero) 295 | { 296 | NativeServiceWrapper.CloseServiceHandle(hSCManager); 297 | } 298 | } 299 | } 300 | 301 | /// 302 | /// Part of the service start workflow where the main function 303 | /// calls this function to signal that the service is should start. 304 | /// This function blocks until the service is stopped. 305 | /// 306 | public void StartService() 307 | { 308 | ServiceTableEntry[] dispatchTable = new ServiceTableEntry[2]; 309 | dispatchTable[0].serviceName = serviceName; 310 | dispatchTable[0].serviceMainFunction = Marshal.GetFunctionPointerForDelegate(svcMain); 311 | 312 | if (!NativeServiceWrapper.StartServiceCtrlDispatcherW(dispatchTable)) 313 | { 314 | var error = Marshal.GetLastWin32Error(); 315 | var errorMsg = "Could not start the service. Error: " + error; 316 | logger.Error("Start service control dispatcher failed: " + errorMsg); 317 | throw new Win32Exception(error, errorMsg); 318 | } 319 | } 320 | 321 | /// 322 | /// Called by SCM as the entry point of the service. Note that args 323 | /// should come from the program main since SCM will launch us with 324 | /// the args passed in like a normal command line program. 325 | /// 326 | /// The number of arguments passed. 327 | /// The string array of arguments. 328 | private void InitializeService(int argc, IntPtr argv) 329 | { 330 | logger.Info("Begin service initialization."); 331 | 332 | hSvcStatus = NativeServiceWrapper.RegisterServiceCtrlHandlerExW(serviceName, svcCtrlHandler, IntPtr.Zero); 333 | if (hSvcStatus == IntPtr.Zero) 334 | { 335 | var error = Marshal.GetLastWin32Error(); 336 | var errorMsg = "Could not register service control handler. Error: " + error; 337 | logger.Error("Initialize service failed: " + errorMsg); 338 | throw new Win32Exception(error, errorMsg); 339 | } 340 | 341 | ReportSvcStatus(CurrentState.SERVICE_START_PENDING, 0, 3000); 342 | 343 | Thread.Sleep(30000); 344 | var strArgs = "\"" + string.Join("\", \"", serviceArgs) + "\""; 345 | logger.Info("Initializing service with arguments: " + strArgs); 346 | 347 | var exitCode = 0; 348 | try 349 | { 350 | startHandler(serviceArgs); 351 | ReportSvcStatus(CurrentState.SERVICE_RUNNING, 0, 0); 352 | logger.Info("Service started successfully."); 353 | stopEvent.WaitOne(); 354 | } 355 | catch (Exception e) 356 | { 357 | exitCode = -1; 358 | ReportSvcStatus(CurrentState.SERVICE_STOPPED, exitCode, 0); 359 | logger.Error("Exception thrown from service start handler: " + e.Message); 360 | return; 361 | } 362 | 363 | logger.Info("Stopping service."); 364 | ReportSvcStatus(CurrentState.SERVICE_STOP_PENDING, 0, 3000); 365 | 366 | try 367 | { 368 | stopHandler(); 369 | } 370 | catch (Exception e) 371 | { 372 | exitCode = -1; 373 | logger.Error("Exception thrown from service stop handler: " + e.Message); 374 | } 375 | 376 | ReportSvcStatus(CurrentState.SERVICE_STOPPED, exitCode, 0); 377 | logger.Info("Service stopped successfully."); 378 | } 379 | 380 | private void SvcControlHandler(uint control, uint eventType, IntPtr eventData, IntPtr eventContext) 381 | { 382 | switch ((ServiceControls)control) 383 | { 384 | case ServiceControls.SERVICE_CONTROL_STOP: 385 | stopEvent.Set(); 386 | return; 387 | case ServiceControls.SERVICE_CONTROL_INTERROGATE: 388 | break; 389 | default: 390 | break; 391 | } 392 | } 393 | 394 | private void ReportSvcStatus(CurrentState state, int exitCode, uint waitHint) 395 | { 396 | if (svcStatus.dwCurrentState == (uint)CurrentState.SERVICE_STOPPED) 397 | { 398 | return; 399 | } 400 | 401 | serviceCheckPoint = 1; 402 | svcStatus.dwCurrentState = (uint)state; 403 | svcStatus.dwWin32ExitCode = exitCode; 404 | svcStatus.dwWaitHint = waitHint; 405 | 406 | if (state == CurrentState.SERVICE_START_PENDING) 407 | { 408 | svcStatus.dwControlsAccepted = 0; 409 | } 410 | else 411 | { 412 | svcStatus.dwControlsAccepted = (uint)ControlsAccepted.SERVICE_ACCEPT_STOP; 413 | } 414 | 415 | if (state == CurrentState.SERVICE_RUNNING || state == CurrentState.SERVICE_STOPPED) 416 | { 417 | svcStatus.dwCheckPoint = 0; 418 | } 419 | else 420 | { 421 | svcStatus.dwCheckPoint = serviceCheckPoint++; 422 | } 423 | 424 | NativeServiceWrapper.SetServiceStatus(hSvcStatus, ref svcStatus); 425 | } 426 | } 427 | } 428 | -------------------------------------------------------------------------------- /SimpleRemoteConsole/ServiceInterop/ServiceInfo.cs: -------------------------------------------------------------------------------- 1 | namespace SimpleRemoteConsole.ServiceInterop 2 | { 3 | public class ServiceInfo 4 | { 5 | public delegate void OnStart(string[] args); 6 | public delegate void OnStop(); 7 | 8 | public string ServiceName; 9 | public string DisplayName = null; 10 | public string BinaryPath; 11 | public string[] ServiceArgs; 12 | public string Username = null; 13 | public string Password = null; 14 | public ServiceType ServiceType = ServiceType.SERVICE_WIN32_OWN_PROCESS; 15 | public StartType StartType = StartType.SERVICE_AUTO_START; 16 | public ErrorControl ErrorSeverity = ErrorControl.SERVICE_ERROR_NORMAL; 17 | public OnStart StartHandler; 18 | public OnStop StopHandler; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SimpleRemoteConsole/ServiceInterop/ServiceStatus.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace SimpleRemoteConsole.ServiceInterop 4 | { 5 | [StructLayout(LayoutKind.Sequential)] 6 | internal struct ServiceStatus 7 | { 8 | public uint dwServiceType; 9 | public uint dwCurrentState; 10 | public uint dwControlsAccepted; 11 | public int dwWin32ExitCode; 12 | public uint dwServiceSpecificExitCode; 13 | public uint dwCheckPoint; 14 | public uint dwWaitHint; 15 | } 16 | 17 | internal enum CurrentState 18 | { 19 | SERVICE_CONTINUE_PENDING = 0x5, 20 | SERVICE_PAUSE_PENDING = 0x6, 21 | SERVICE_PAUSED = 0x7, 22 | SERVICE_RUNNING = 0x4, 23 | SERVICE_START_PENDING = 0x2, 24 | SERVICE_STOP_PENDING = 0x3, 25 | SERVICE_STOPPED = 0x1 26 | } 27 | 28 | internal enum ControlsAccepted 29 | { 30 | SERVICE_ACCEPT_NETBINDCHANGE = 0x10, 31 | SERVICE_ACCEPT_PARAMCHANGE = 0x8, 32 | SERVICE_ACCEPT_PAUSE_CONTINUE = 0x2, 33 | SERVICE_ACCEPT_PRESHUTDOWN = 0x100, 34 | SERVICE_ACCEPT_SHUTDOWN = 0x4, 35 | SERVICE_ACCEPT_STOP = 0x1 36 | } 37 | 38 | internal enum ServiceControls 39 | { 40 | SERVICE_CONTROL_STOP = 0x1, 41 | SERVICE_CONTROL_INTERROGATE = 0x4 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /SimpleRemoteConsole/ServiceInterop/ServiceTableEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace SimpleRemoteConsole.ServiceInterop 5 | { 6 | [StructLayout(LayoutKind.Sequential)] 7 | internal struct ServiceTableEntry 8 | { 9 | [MarshalAs(UnmanagedType.LPWStr)] 10 | internal string serviceName; 11 | 12 | internal IntPtr serviceMainFunction; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /SimpleRemoteConsole/SimpleRemoteConsole.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 1.4.2 5 | Exe 6 | net6;net47 7 | win10-x64;win10-arm64 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Always 21 | 22 | 23 | PreserveNewest 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /SimpleRemoteConsole/UserWarning.txt: -------------------------------------------------------------------------------- 1 | !!! WARNING !!! 2 | 3 | SimpleRemote is a test tool designed to make it easy for 4 | other devices to run programs and transfer files on this machine. 5 | 6 | IT HAS NO SECURITY WHATSOEVER. 7 | 8 | If used improperly, this can allow attackers to gain access to your 9 | system. It should only be used on test machines on secure networks. 10 | 11 | By proceeding, you are acknowledging that you understand these 12 | risks, and have taken appropriate steps to secure the machine. 13 | 14 | Proceed? [Y/N]: -------------------------------------------------------------------------------- /assets/TeamLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/SimpleRemote/c8fbf151d68382bab3c3bdf50354587fa8ecbf91/assets/TeamLogo.png -------------------------------------------------------------------------------- /extra_docs/rpc_tutorial.md: -------------------------------------------------------------------------------- 1 | # SimpleJsonRpc Overview # 2 | SimpleRemote uses a minimal .NET Standard JSON-RPC server library called SimpleJsonRpc. This library can be 3 | used separately from SimpleRemote. 4 | 5 | You can find the Doxygen documentation for the main rpc server class under SimpleJsonRpc.SimpleRpcServer 6 | 7 | This page provides a bit more detail on how to use the library outside of SimpleRemote. 8 | 9 | ## Declaring and Registering RPC Functions ## 10 | To allow a method to be called via RPC, you first need to add the annotation `SimpleRpcMethod`. 11 | 12 | Once you've created an instance of the SimpleRpcServer class, pass an instance of the class with 13 | annotated methods to the function SimpleJsonRpc.SimpleRpcServer.Register(). The register function 14 | will locate any methods with the `SimpleRpcMethod` annotation, and add them into a dictionary, mapping 15 | the method's name to that function within that class. 16 | 17 | *Note: The system also keeps a reference to your class instance - when you call an RPC method, it is 18 | called against the provided instance.* 19 | 20 | *Warning: The system tracks your function by name only - if you try to register two functions with the same 21 | name (even if they have different signatures) only the last one will be stored.* 22 | 23 | ## Running the Server ## 24 | You have two options for running the server. After you create an instance of SimpleRpcServer, you can 25 | either: 26 | - Have SimpleRpcServer open a server socket, and listen for inbound connections with the 27 | SimpleJsonRpc.SimpleRpcServer.Start() method. 28 | - Manually feed SimpleRpcServer json strings (which you've received somewhere else in your app), 29 | using the method SimpleJsonRpc.SimpleRpcServer.HandleJsonString(). 30 | 31 | The option only changes how the server gets the json request string - the logic afterwards is identical. 32 | 33 | Once a json string is received, the system will look in the registered function list for a matching function, 34 | and call it if present. 35 | 36 | ### Broadcast Support ### 37 | The RpcServer, if listening on its own socket, can open a second socket and listen for UDP broadcast packets. 38 | If an UDP datagram arrives with the message "SimpleJsonRpc Ping", the system will respond with the RpcServer's 39 | IPEndPoint information. 40 | 41 | This was desinged to help hardware test labs, where there might be a large number of devices under test 42 | running the server, all with potentially dynamic IP addresses. It's completely optional, and is disabled 43 | by default. You can turn it on by specifing a `broadcastPort` in the call to SimpleJsonRpc.SimpleRpcServer.Start() 44 | 45 | If you're using SimpleJsonRpc.SimpleRpcServer.HandleJsonString() to manually process rpc requests, then there's 46 | no option to turn on broadcast responses. 47 | 48 | ## Limitations ## 49 | SimpleJsonRpc does not support named parameters in JSON-RPC requests - all parameters must be passed as arguments 50 | in an array. -------------------------------------------------------------------------------- /extra_docs/tutorial.md: -------------------------------------------------------------------------------- 1 | # SimpleRemote Basic Tutorial # 2 | This covers the basic usage of SimpleRemote, and is designed as a companion to the API documentation 3 | provided by doxygen. 4 | 5 | *A quick warning about security - this tool allows you to remotely control a device without any kind of 6 | authentication. You should only use this tool on secured networks.* 7 | 8 | ## Deploy the Server to your DUT ## 9 | The first step is to launch the SimpleRemote server on your DUT. Do to this, copy the compiled binaries for 10 | your platform to the device, and launch the `SimpleRemoteConsole.exe` binary. 11 | 12 | Note that if need to call functions or system services that require administrator rights, you'll need 13 | to start the server as an administrator. 14 | 15 | Note - the first time you run the tool, you'll likely get a prompt asking if you want to allow the tool 16 | through the firewall. If you don't, and you have issues connecting, either manually create a firewall 17 | exception on the device, or you can completely disable firewalls on the device by calling: 18 | 19 | netsh advfirewall set allprofiles state off 20 | 21 | If you only want to open specific ports of the firewall, see the [Firewall Details](#Firewall-Details) 22 | section, located toward the end of this document. 23 | 24 | ## Using the .NET Client ## 25 | The easiest way to use the server is to use the .NET Client library, which is 26 | [documented here](@ref SimpleDUTClientLibrary.RpcClient). As a demonstration, we'll use the client 27 | in PowerShell to show how easy it is. 28 | 29 | *Warning for Nuget users: SimpleRemote's nuget packages require the PackageReference format, and will not work correctly if you're using the packages.config format (the default method for managing packges for .NET Framework projects). Please review the [article here](https://blog.nuget.org/20170316/NuGet-now-fully-integrated-into-MSBuild.html) for information on how to switch a project to use PackageReference format (see the section under 'What about other project types that are not .NET Core?').* 30 | 31 | Before we begin, we'll need to give PowerShell information about our RpcClient class. To do that, 32 | navigate to the directory containing the %SimpleDUTClientLibrary DLL, and open a PowerShell instance. Then call: 33 | 34 | Add-Type -Path SimpleDUTClientLibrary.dll 35 | 36 | Once you've done that, you can create a client instance by calling: 37 | 38 | $client = new-object SimpleDUTClientLibrary.RpcClient -argumentlist "127.0.0.1",8000 39 | 40 | In your case, you should replace "127.0.0.1" with the IP address if your device, and 8000 with your 41 | server's port number (it defaults to 8000, but can be changed with command line arguments). 42 | 43 | Note that simply creating a client object doesn't generate a connection. To do that, we'll need 44 | to run a method. To see the available methods on the client DLL, you can call: 45 | 46 | $client | Get-Member 47 | 48 | So, to launch notepad on the remote machine, and return immediately, you would call: 49 | 50 | $client.Run("notepad.exe") 51 | 52 | To run a command and get output, you would use the [RunJob](@ref SimpleDUTClientLibrary.RpcClient.RunJob) function: 53 | 54 | $client.RunJob("systeminfo.exe") 55 | 56 | Note that any returned standard out/standard error will have new line characters at the end of the returned string, as it would on the remote machine (this is something to be aware of if you plan on comparing the output to a known value). 57 | 58 | ## Getting More Information on the Server ## 59 | The server uses NLog to provide information back to the user. By default, the logger will only write messages 60 | to the terminal, and only show messages at level `Info` or higher. If you want to see more detailed information 61 | about what the server is doing, you can set the `minlevel` option in the `Nlog.conf` file (it's in the same 62 | folder as `SimpleRemoteConsole.exe`) to `Debug`. You may want to do this if you want to see the stdout and stderr 63 | of a called processes as it is generated. 64 | 65 | Nlog can also be configured to write to files, the Windows event log, and a number of other locations. To see more 66 | information on how to configure logging, see the [NLog Tutorial](https://github.com/NLog/NLog/wiki/Tutorial#configuration). 67 | 68 | ## Running as a Service ## 69 | You can install SimpleRemote as a service on any Windows system. Simple launch the SimpleRemoteConsole exe from an elevated command prompt, and include the arg `--install-service`, as well as any other flags you wish to use (such as specifying the port). By default, the service will not start automatically, unless you specify `--service-start-type auto` when installing the service. 70 | 71 | The service can be removed from the system my running `--uninstall-service` from an elevated command prompt. 72 | 73 | While running with a service is useful for some applications, note that the service will not be attached to a specific user session. As such, automating graphical applications may not work as expected. 74 | 75 | ## Firewall Details ## 76 | In some cases, you may only want to open specific ports on the DUT instead of turning off the firewall completely. If this the case, you'll need to open: 77 | 78 | - The simple remote communication port (TCP 8000 by default) 79 | - TCP port(s) for upload/download operations 80 | - Optional: The UDP broadcast port (UDP 8001 by default) 81 | 82 | By defult, when an upload or download is started, the system running SimpleRemote will 83 | choose a random port for the transfer. However, this can be undesirable if you want to limit 84 | SimpleRemote to certain ports. To control which port is used for an upload or download, 85 | specify a port argument to the Upload or Download function. 86 | 87 | *Note: Each transfer uses its own socket on the server machine. Do not 88 | attempt to do multiple transfers on the same port at the same time.* 89 | -------------------------------------------------------------------------------- /installer.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Setup Script Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | #define MyAppName "Simple Remote Server" 5 | #define MyAppVersion "1.4.2" 6 | #define MyAppPublisher "Microsoft Surface" 7 | #define MyAppURL "surface.com" 8 | #define MyAppExeName "SimpleRemoteConsole.exe" 9 | 10 | [Setup] 11 | ; NOTE: The value of AppId uniquely identifies this application. 12 | ; Do not use the same AppId value in installers for other applications. 13 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 14 | AppId={{160AFA98-B85F-4D9D-B577-7D841D18B261} 15 | AppName={#MyAppName} 16 | AppVersion={#MyAppVersion} 17 | ;AppVerName={#MyAppName} {#MyAppVersion} 18 | AppPublisher={#MyAppPublisher} 19 | AppPublisherURL={#MyAppURL} 20 | AppSupportURL={#MyAppURL} 21 | AppUpdatesURL={#MyAppURL} 22 | DefaultDirName={pf}\{#MyAppName} 23 | DisableProgramGroupPage=yes 24 | OutputBaseFilename=SimpleRemoteInstaller 25 | Compression=lzma 26 | SolidCompression=yes 27 | SourceDir="output" 28 | OutputDir="." 29 | ArchitecturesInstallIn64BitMode=x64 30 | 31 | [Languages] 32 | Name: "english"; MessagesFile: "compiler:Default.isl" 33 | 34 | [Files] 35 | Source: "SimpleRemoteServer-x64\*"; DestDir: "{app}"; Flags: ignoreversion 36 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 37 | 38 | [Icons] 39 | Name: "{commonprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" 40 | 41 | [Run] 42 | Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent 43 | 44 | 45 | 46 | 47 | --------------------------------------------------------------------------------