├── .gitignore ├── ReadMe.md ├── RemoteExecution.sln ├── RemoteExecution ├── Commands.cs ├── Logger.cs ├── RemoteCommand.cs ├── RemoteExecution.xproj ├── RemoteExecutionClient.cs ├── RemoteExecutionServer.cs ├── RemoteProxy.cs ├── RemotelyCreatedObject.cs ├── Serialization │ ├── SerializationHelper.cs │ └── TypeConverter.cs └── project.json └── Tests ├── TestClient ├── Program.cs ├── Properties │ └── launchSettings.json ├── TestClient.xproj └── project.json ├── TestComponent ├── TestComponent.xproj ├── TestType.cs └── project.json └── TestServer ├── Program.cs ├── TestServer.xproj └── project.json /.gitignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | ## From: https://raw.githubusercontent.com/github/gitignore/master/VisualStudio.gitignore 3 | ## Ignore Visual Studio temporary files, build results, and 4 | ## files generated by popular Visual Studio add-ons. 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | 26 | # Visual Studio 2015 cache/options directory 27 | .vs/ 28 | **/wwwroot/lib/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | 85 | # Visual Studio profiler 86 | *.psess 87 | *.vsp 88 | *.vspx 89 | *.sap 90 | 91 | # TFS 2012 Local Workspace 92 | $tf/ 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | *.DotSettings.user 101 | 102 | # JustCode is a .NET coding add-in 103 | .JustCode 104 | 105 | # TeamCity is a build add-in 106 | _TeamCity* 107 | 108 | # DotCover is a Code Coverage Tool 109 | *.dotCover 110 | 111 | # NCrunch 112 | _NCrunch_* 113 | .*crunch*.local.xml 114 | nCrunchTemp_* 115 | 116 | # MightyMoose 117 | *.mm.* 118 | AutoTest.Net/ 119 | 120 | # Web workbench (sass) 121 | .sass-cache/ 122 | 123 | # Installshield output folder 124 | [Ee]xpress/ 125 | 126 | # DocProject is a documentation generator add-in 127 | DocProject/buildhelp/ 128 | DocProject/Help/*.HxT 129 | DocProject/Help/*.HxC 130 | DocProject/Help/*.hhc 131 | DocProject/Help/*.hhk 132 | DocProject/Help/*.hhp 133 | DocProject/Help/Html2 134 | DocProject/Help/html 135 | 136 | # Click-Once directory 137 | publish/ 138 | 139 | # Publish Web Output 140 | *.[Pp]ublish.xml 141 | *.azurePubxml 142 | # TODO: Comment the next line if you want to checkin your web deploy settings 143 | # but database connection strings (with potential passwords) will be unencrypted 144 | *.pubxml 145 | *.publishproj 146 | 147 | # NuGet Packages 148 | *.nupkg 149 | # The packages folder can be ignored because of Package Restore 150 | **/packages/* 151 | # except build/, which is used as an MSBuild target. 152 | !**/packages/build/ 153 | # Uncomment if necessary however generally it will be regenerated when needed 154 | #!**/packages/repositories.config 155 | # NuGet v3's project.json files produces more ignoreable files 156 | *.nuget.props 157 | *.nuget.targets 158 | 159 | # Microsoft Azure Build Output 160 | csx/ 161 | *.build.csdef 162 | 163 | # Microsoft Azure Emulator 164 | ecf/ 165 | rcf/ 166 | 167 | # Microsoft Azure ApplicationInsights config file 168 | ApplicationInsights.config 169 | 170 | # Windows Store app package directory 171 | AppPackages/ 172 | BundleArtifacts/ 173 | 174 | # Visual Studio cache files 175 | # files ending in .cache can be ignored 176 | *.[Cc]ache 177 | # but keep track of directories ending in .cache 178 | !*.[Cc]ache/ 179 | 180 | # Others 181 | ClientBin/ 182 | ~$* 183 | *~ 184 | *.dbmdl 185 | *.dbproj.schemaview 186 | *.pfx 187 | *.publishsettings 188 | node_modules/ 189 | orleans.codegen.cs 190 | 191 | # RIA/Silverlight projects 192 | Generated_Code/ 193 | 194 | # Backup & report files from converting an old project file 195 | # to a newer Visual Studio version. Backup files are not needed, 196 | # because we have git ;-) 197 | _UpgradeReport_Files/ 198 | Backup*/ 199 | UpgradeLog*.XML 200 | UpgradeLog*.htm 201 | 202 | # SQL Server files 203 | *.mdf 204 | *.ldf 205 | 206 | # Business Intelligence projects 207 | *.rdl.data 208 | *.bim.layout 209 | *.bim_*.settings 210 | 211 | # Microsoft Fakes 212 | FakesAssemblies/ 213 | 214 | # GhostDoc plugin setting file 215 | *.GhostDoc.xml 216 | 217 | # Node.js Tools for Visual Studio 218 | .ntvs_analysis.dat 219 | 220 | # Visual Studio 6 build log 221 | *.plg 222 | 223 | # Visual Studio 6 workspace options file 224 | *.opt 225 | 226 | # Visual Studio LightSwitch build output 227 | **/*.HTMLClient/GeneratedArtifacts 228 | **/*.DesktopClient/GeneratedArtifacts 229 | **/*.DesktopClient/ModelManifest.xml 230 | **/*.Server/GeneratedArtifacts 231 | **/*.Server/ModelManifest.xml 232 | _Pvt_Extensions 233 | 234 | # Paket dependency manager 235 | .paket/paket.exe 236 | 237 | # FAKE - F# Make 238 | .fake/ -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | .NET Core Remote Communication and Marshaling 2 | ============================================== 3 | 4 | Overview 5 | -------- 6 | 7 | .NET remoting is one of the .NET Framework feature area that is [not available](https://docs.microsoft.com/en-us/dotnet/articles/core/porting/libraries#key-technologies-not-yet-available-on-the-net-standard-or-net-core) in .NET Core. 8 | 9 | Although remoting (as it exists in NetFX) is not available, many scenarios that call for remoting can be implemented with alternate approaches in .NET Core. This repo attempts to show a simple sample of that. Note that this repo demonstrates a fairly general approach to the problem. In specific cases, better (and simpler) solutions may be possible which target the goals of that scenario. This repo is just a general-purpose demo of how cross-process managed code invocation can be accomplished in .NET Core. 10 | 11 | The required technologies are: 12 | 13 | * [Named pipes](https://msdn.microsoft.com/en-us/library/bb546085(v=vs.110).aspx) for cross-process communication. 14 | * A serialization technology for marshaling managed objects. In this example, I use [Json.NET](http://www.newtonsoft.com/json). This means that the sample can only send parameters cross-process if their types are serializable/deserializable with Json.NET. All of the serialization logic is confined to a couple classes in the [serialization](https://github.com/mjrousos/NetCoreRemoting/tree/master/RemoteExecution/Serialization) directory, though, so it should be easy to swap out Json.NET for an alternate serialization provider, if needed. 15 | * A proxying technology. In order to keep the sample as simple as possible, it's not using real proxies - I just have a `RemoteProxy` class that takes a string parameter specifying which API to call on a remote object. A more fully-featured remoting story could use something like [Castle.DynamixProxy](https://github.com/castleproject/Core/blob/master/docs/dynamicproxy.md) to create proxy objects for use on the client. 16 | 17 | How it Works 18 | ------------ 19 | 20 | As mentioned above, the communication is based on named pipes. To start remoting, the server piece of the scenario must create a [`RemoteExecutionServer`](https://github.com/mjrousos/NetCoreRemoting/blob/master/RemoteExecution/RemoteExecutionServer.cs) object. Doing so will cause a named pipe to be setup for clients to connect to. Once that's done, the server's work is complete. 21 | 22 | Then, clients are able to create [`RemoteProxy`](https://github.com/mjrousos/NetCoreRemoting/blob/master/RemoteExecution/RemoteProxy.cs) objects (or use the [`RemoteExecutionClient`](https://github.com/mjrousos/NetCoreRemoting/blob/master/RemoteExecution/RemoteExecutionClient.cs) wrapper class). Creating this type causes the client to connect with the server via named pipes and send a request on that pipe (all requests are instances of [`RemoteCommand`](https://github.com/mjrousos/NetCoreRemoting/blob/master/RemoteExecution/RemoteCommand.cs)) for an instance of the type to be remoted to be created. 23 | 24 | On the server side, the necessary type will be created and it will be put in a Dictionary to be referenced later. The key (for looking up the object in the dictionary) is then returned to the `RemoteProxy` where it is stored. 25 | 26 | Later, when the client wants to call some API on the remote object, `RemoteProxy.InvokeAsync` (or one of a number of other similar APIs) can be called. This will cause another `RemoteCommand` to be created which will include: 27 | 28 | 1. The ID of the remote object that the API should be invoked on. 29 | 2. The name of the API as a string. 30 | 3. The parameters (if any) to pass to the API. 31 | 4. The types of those parameters (since they can be lost in serialization). 32 | 33 | This information is all serialized and then sent via the named pipe to the server. The server, as might be expected, deserializes the command and uses reflection to invoke the API requested, serializing the return object (if any) to be sent back to the proxy (which, in turn, presents it to its caller). 34 | 35 | Trying it Out 36 | ------------- 37 | 38 | There are some tests in the [Tests](https://github.com/mjrousos/NetCoreRemoting/tree/master/Tests) folder which allow for trying out the remoting mentioned here. To experiment with it, follow these steps: 39 | 40 | 1. Run the [TestServer](https://github.com/mjrousos/NetCoreRemoting/tree/master/Tests/TestServer) application to start listening for remote objects. 41 | 1. Before running, though, make sure that you've copied TestClient and TestComponent dlls next to the TestServer app. This is important because TestServer doesn't usually depend on those libraries, but the client will be requesting that types from them be created. Therefore, it's necessary for TestServer to be able to find the assemblies so that it can load and activate the required types. 42 | 2. Run the [TestClient](https://github.com/mjrousos/NetCoreRemoting/tree/master/Tests/TestClient) which will first make a series of hard-coded remote calls that I found useful for initial ad-hoc testing, and then create a remote instance of [`TestTypes.MessageHolder`](https://github.com/mjrousos/NetCoreRemoting/blob/master/Tests/TestComponent/TestType.cs) (a glorified wrapper around a queue) and allow the user to ask for different `MessageHolder` commands to be executed remotely. Of special note is the 'print' command which will cause information about the queue to be written to the console. When calling this, notice that the console output happens in the TestServer command prompt since that's the process in which all the `MessageHolder` code is executing. 43 | 44 | Demo 45 | ---- 46 | 47 | There's a brief demo video of this sample available [on YouTube](https://youtu.be/QwvYXrHM4E4). -------------------------------------------------------------------------------- /RemoteExecution.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "RemoteExecution", "RemoteExecution\RemoteExecution.xproj", "{FA090568-11AF-45B9-AFA8-40536677E8FC}" 7 | EndProject 8 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "TestClient", "Tests\TestClient\TestClient.xproj", "{6875513A-73E8-4CCE-8648-82494A33EFB2}" 9 | EndProject 10 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "TestComponent", "Tests\TestComponent\TestComponent.xproj", "{C81624AB-5A17-4385-8DC8-F3817E6E6552}" 11 | EndProject 12 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "TestServer", "Tests\TestServer\TestServer.xproj", "{29192211-AE15-4B68-B79C-DA6C2EF998D5}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C5B7FAEF-B884-4EE5-A52C-39FA0B530920}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {FA090568-11AF-45B9-AFA8-40536677E8FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {FA090568-11AF-45B9-AFA8-40536677E8FC}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {FA090568-11AF-45B9-AFA8-40536677E8FC}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {FA090568-11AF-45B9-AFA8-40536677E8FC}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {6875513A-73E8-4CCE-8648-82494A33EFB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {6875513A-73E8-4CCE-8648-82494A33EFB2}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {6875513A-73E8-4CCE-8648-82494A33EFB2}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {6875513A-73E8-4CCE-8648-82494A33EFB2}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {C81624AB-5A17-4385-8DC8-F3817E6E6552}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {C81624AB-5A17-4385-8DC8-F3817E6E6552}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {C81624AB-5A17-4385-8DC8-F3817E6E6552}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {C81624AB-5A17-4385-8DC8-F3817E6E6552}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {29192211-AE15-4B68-B79C-DA6C2EF998D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {29192211-AE15-4B68-B79C-DA6C2EF998D5}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {29192211-AE15-4B68-B79C-DA6C2EF998D5}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {29192211-AE15-4B68-B79C-DA6C2EF998D5}.Release|Any CPU.Build.0 = Release|Any CPU 38 | EndGlobalSection 39 | GlobalSection(SolutionProperties) = preSolution 40 | HideSolutionNode = FALSE 41 | EndGlobalSection 42 | GlobalSection(NestedProjects) = preSolution 43 | {6875513A-73E8-4CCE-8648-82494A33EFB2} = {C5B7FAEF-B884-4EE5-A52C-39FA0B530920} 44 | {C81624AB-5A17-4385-8DC8-F3817E6E6552} = {C5B7FAEF-B884-4EE5-A52C-39FA0B530920} 45 | {29192211-AE15-4B68-B79C-DA6C2EF998D5} = {C5B7FAEF-B884-4EE5-A52C-39FA0B530920} 46 | EndGlobalSection 47 | EndGlobal 48 | -------------------------------------------------------------------------------- /RemoteExecution/Commands.cs: -------------------------------------------------------------------------------- 1 | namespace RemoteExecution 2 | { 3 | internal enum Commands 4 | { 5 | NewObject, 6 | RetrieveObject, 7 | Invoke, 8 | GetProperty, 9 | SetProperty, 10 | CloseConnection 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /RemoteExecution/Logger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace RemoteExecution 5 | { 6 | public class Logger 7 | { 8 | object LoggingLock = new object(); 9 | 10 | public MessagePriority LogLevel { get; set; } = MessagePriority.Informational; 11 | 12 | public void Log(string message, MessagePriority priority = MessagePriority.Verbose) 13 | { 14 | if (priority <= LogLevel) 15 | { 16 | lock (LoggingLock) 17 | { 18 | ConsoleColor previousForeColor = Console.ForegroundColor; 19 | StringBuilder toWrite = new StringBuilder($"[{DateTime.Now.ToString("HH:MM:ss.ff")}] "); 20 | 21 | switch (priority) 22 | { 23 | case MessagePriority.Error: 24 | Console.ForegroundColor = ConsoleColor.Red; 25 | toWrite.Append("[ERROR] "); 26 | break; 27 | case MessagePriority.Warning: 28 | Console.ForegroundColor = ConsoleColor.Yellow; 29 | toWrite.Append("[Warning] "); 30 | break; 31 | case MessagePriority.Verbose: 32 | Console.ForegroundColor = ConsoleColor.Gray; 33 | break; 34 | case MessagePriority.Informational: 35 | Console.ForegroundColor = ConsoleColor.White; 36 | break; 37 | } 38 | 39 | toWrite.Append(message); 40 | 41 | Console.WriteLine(toWrite.ToString()); 42 | Console.ForegroundColor = previousForeColor; 43 | } 44 | } 45 | } 46 | } 47 | 48 | public enum MessagePriority 49 | { 50 | Error, 51 | Warning, 52 | Informational, 53 | Verbose 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /RemoteExecution/RemoteCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RemoteExecution 4 | { 5 | internal class RemoteCommand 6 | { 7 | // Common properties 8 | public int CommandVersion { get; set; } = 1; 9 | public Commands Command { get; set; } 10 | 11 | // Used for object retrieval or instance method invocation 12 | public Guid ObjectId { get; set; } 13 | 14 | // Used for method invocation 15 | public string MemberName { get; set; } 16 | 17 | // Used for method invocation or object creation 18 | public object[] Parameters { get; set; } 19 | 20 | // Newtonsoft.Json does not persist type information for 21 | // some primitive types, so that information needs to be tracked explicitly 22 | // for parameters. 23 | public Type[] ParameterTypes { get; set; } 24 | 25 | // Used for new object creation or static method execution 26 | public Type Type { get; set; } 27 | } 28 | } -------------------------------------------------------------------------------- /RemoteExecution/RemoteExecution.xproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 10 | fa090568-11af-45b9-afa8-40536677e8fc 11 | RemoteExecution 12 | .\obj 13 | .\bin\ 14 | v4.5.2 15 | 16 | 17 | 18 | 2.0 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /RemoteExecution/RemoteExecutionClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace RemoteExecution 7 | { 8 | public static class RemoteExecutionClient 9 | { 10 | /// 11 | /// Creates a new RemoteProxy for a given type T 12 | /// 13 | /// The type to create remotely 14 | /// The remote execution server's name (for named pipe communication) 15 | /// Parameters to pass to the type's constructor 16 | /// A proxy for managing the remote object 17 | public static RemoteProxy CreateRemoteInstance(string remoteServerName, params object[] paramaters) 18 | { 19 | return new RemoteProxy(typeof(T), remoteServerName, paramaters); 20 | } 21 | 22 | /// 23 | /// Creates a proxy to an existing remote object 24 | /// 25 | /// The remote execution server's name (for named pipe communication) 26 | /// The remote object's ID 27 | /// A proxy for managing the remote object 28 | public static RemoteProxy GetRemoteInstance(string remoteServerName, Guid objectId) 29 | { 30 | return new RemoteProxy(remoteServerName, objectId); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /RemoteExecution/RemoteExecutionServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.IO.Pipes; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace RemoteExecution 12 | { 13 | public class RemoteExecutionServer : IDisposable 14 | { 15 | // Dictionary for storing objects created remotely. Keyed by the guid that proxies will issue commands to the objects with 16 | ConcurrentDictionary Objects = new ConcurrentDictionary(); 17 | 18 | // All current pipe connection tasks 19 | ConcurrentBag ConnectionTasks = new ConcurrentBag(); 20 | 21 | CancellationTokenSource CTS = new CancellationTokenSource(); 22 | 23 | // Whether new connection listening tasks may be started 24 | // Will be set to false during shutdown 25 | volatile bool Active = true; 26 | 27 | // This lock protects access to the Active bool 28 | ReaderWriterLockSlim ConnectionsActiveLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); 29 | 30 | // The name of the remote execution server (which proxies use to connect) 31 | public string RemoteName { get; set; } 32 | 33 | // Centralized logging so that we can easily log in a more sophisticated way in the future 34 | public Logger Logger { get; } 35 | 36 | /// 37 | /// Create a new RemoteExecutionServer with an auto-generated name 38 | /// 39 | public RemoteExecutionServer() : this(Guid.NewGuid().ToString()) { } 40 | 41 | /// 42 | /// Create a new RemoteExecutionServer 43 | /// 44 | /// The name that clients and proxies will use to connect 45 | public RemoteExecutionServer(string name) 46 | { 47 | RemoteName = name; 48 | Logger = new Logger(); 49 | QueueListener(); 50 | } 51 | 52 | /// 53 | /// Starts listening for named pipe connections 54 | /// 55 | private void QueueListener() 56 | { 57 | // TODO - Clean up closed connections? 58 | 59 | // Make sure that we're not shutting down or anything like that 60 | ConnectionsActiveLock.EnterReadLock(); 61 | try 62 | { 63 | if (Active) 64 | { 65 | // If all is active, start listening 66 | ConnectionTasks.Add(ListenForRemoteConnection()); 67 | } 68 | } 69 | finally 70 | { 71 | ConnectionsActiveLock.ExitReadLock(); 72 | } 73 | } 74 | 75 | /// 76 | /// Listens for a remote connection via named pipes 77 | /// 78 | private async Task ListenForRemoteConnection() 79 | { 80 | using (var pipe = new NamedPipeServerStream(RemoteName, PipeDirection.InOut, -1, PipeTransmissionMode.Message, PipeOptions.Asynchronous)) 81 | { 82 | // Wait for a remote client to connect 83 | await pipe.WaitForConnectionAsync(CTS.Token); 84 | 85 | Logger.Log("Client has connected"); 86 | 87 | // Start a new listener since this one's now setting up an active connection 88 | QueueListener(); 89 | 90 | // Loop processing messages from client 91 | while (!CTS.IsCancellationRequested && pipe.IsConnected) 92 | { 93 | // Read message bytes 94 | var messageBytes = new List(); 95 | do 96 | { 97 | var buffer = new byte[1024]; 98 | var bytesRead = await pipe.ReadAsync(buffer, 0, buffer.Length, CTS.Token); 99 | messageBytes.AddRange(buffer.Take(bytesRead)); 100 | } 101 | while (!pipe.IsMessageComplete && !CTS.IsCancellationRequested); 102 | 103 | if (pipe.IsMessageComplete && messageBytes.Count > 0) 104 | { 105 | Logger.Log($"Received {messageBytes.Count} bytes from client", MessagePriority.Verbose); 106 | 107 | // Process the message 108 | object returnVal = null; 109 | bool shouldCloseConnection = false; 110 | ProcessMessage(messageBytes.ToArray(), ref returnVal, ref shouldCloseConnection); 111 | 112 | // Serialize any return value and write it to the pipe 113 | var returnString = SerializationHelper.Serialize(returnVal); 114 | var returnBytes = Encoding.UTF8.GetBytes(returnString); 115 | Logger.Log($"Writing {returnBytes.Length} bytes to client", MessagePriority.Verbose); 116 | await pipe.WriteAsync(returnBytes, 0, returnBytes.Length, CTS.Token); 117 | 118 | // If processing resulted in a request to disconnect, do so 119 | if (shouldCloseConnection) 120 | { 121 | Logger.Log("Disconnecting from client"); 122 | pipe.Disconnect(); 123 | } 124 | } 125 | } 126 | } 127 | } 128 | 129 | /// 130 | /// Deserializes a command from a message and acts on it 131 | /// 132 | private void ProcessMessage(byte[] messageBytes, ref object returnVal, ref bool shouldCloseConnection) 133 | { 134 | RemoteCommand command = null; 135 | var commandString = new string(Encoding.UTF8.GetChars(messageBytes)); 136 | 137 | try 138 | { 139 | // Deserialize command 140 | command = SerializationHelper.Deserialize(commandString); 141 | Logger.Log($"Received command {command.Command}", MessagePriority.Informational); 142 | Logger.Log($" Object ID: {command.ObjectId}", MessagePriority.Verbose); 143 | Logger.Log($" Type: {command.Type?.AssemblyQualifiedName}", MessagePriority.Verbose); 144 | Logger.Log($" Member Name: {command.MemberName}", MessagePriority.Verbose); 145 | Logger.Log($" Param count: {command.Parameters?.Length}", MessagePriority.Verbose); 146 | } 147 | catch (ArgumentException) 148 | { 149 | Logger.Log($"Invalid message not processed: {commandString}", MessagePriority.Warning); 150 | return; 151 | } 152 | 153 | switch (command.Command) 154 | { 155 | case Commands.CloseConnection: 156 | // Decrement the ref-count for the object referenced by the closing connection 157 | RemotelyCreatedObject remotelyCreatedObject; 158 | if (Objects.TryGetValue(command.ObjectId, out remotelyCreatedObject)) 159 | { 160 | if (0 >= Interlocked.Decrement(ref remotelyCreatedObject.RefCount)) 161 | { 162 | // If no references remain, end-of-life the object and remove it from the dictionary 163 | // 164 | // If we need to adjust remote object lifetime (should a future process be able to get an old object?) 165 | // we can change that behavior here. 166 | Logger.Log($"Removing object {command.ObjectId} which is no longer referenced remotely"); 167 | remotelyCreatedObject.Value = null; 168 | Objects.TryRemove(command.ObjectId, out remotelyCreatedObject); 169 | } 170 | } 171 | 172 | // Record that a disconnection has been requested 173 | shouldCloseConnection = true; 174 | break; 175 | case Commands.NewObject: 176 | if (command.Type != null) 177 | { 178 | object newObj = null; 179 | try 180 | { 181 | if (command.Parameters?.Length > 0) 182 | { 183 | // Map parameters to their proper types 184 | object[] ctorParameters = new object[command.Parameters.Length]; 185 | for (int i = 0; i < command.Parameters.Length; i++) 186 | { 187 | ctorParameters[i] = SerializationHelper.GetObjectAsType(command.Parameters[i], command.ParameterTypes[i]); 188 | } 189 | 190 | // Create object 191 | newObj = Activator.CreateInstance(command.Type, ctorParameters); 192 | } 193 | else 194 | { 195 | // If the type has a default ctor or is a value type, allow creation without parameters 196 | if (command.Type.GetTypeInfo().IsValueType || command.Type.GetTypeInfo().DeclaredConstructors.Any(c => c.GetParameters().Length == 0)) 197 | { 198 | newObj = Activator.CreateInstance(command.Type); 199 | } 200 | } 201 | } 202 | catch (Exception exc) 203 | { 204 | Logger.Log($"WARNING: Failed to create object of type {command.Type.AssemblyQualifiedName}: {exc.ToString()}", MessagePriority.Warning); 205 | } 206 | 207 | if (newObj != null) 208 | { 209 | // Register the object in our dictionary 210 | var newId = Guid.NewGuid(); 211 | Objects.TryAdd(newId, new RemotelyCreatedObject { RefCount = 1, Value = newObj }); 212 | 213 | // Set the return value to the object's ID (to be returned to the client) 214 | returnVal = newId; 215 | 216 | Logger.Log($"Created a new objet with ID {newId.ToString()} and type {command.Type.FullName}", MessagePriority.Informational); 217 | } 218 | } 219 | break; 220 | case Commands.RetrieveObject: 221 | // TODO - Look up an object and return its type. Increment its ref count in the dictionary 222 | // This functionality isn't strictly necessary, but mirrors traditional remoting's ability 223 | // to retrieve a remote object previously created by another remote client. 224 | break; 225 | case Commands.Invoke: 226 | MethodInfo method = null; 227 | object objectToInvokeOn = null; 228 | 229 | // Lookup method via Reflection 230 | if (command.Type != null) 231 | { 232 | // Static method 233 | method = command.Type.GetRuntimeMethod(command.MemberName, command.ParameterTypes); 234 | } 235 | else if (command.ObjectId != Guid.NewGuid()) 236 | { 237 | // Instance method 238 | RemotelyCreatedObject obj = null; 239 | if (Objects.TryGetValue(command.ObjectId, out obj)) 240 | { 241 | objectToInvokeOn = obj?.Value; 242 | method = objectToInvokeOn?.GetType().GetRuntimeMethod(command.MemberName, command.ParameterTypes); 243 | } 244 | } 245 | 246 | // Get parameters 247 | object[] parameters = null; 248 | if (command.Parameters?.Length > 0) 249 | { 250 | parameters = new object[command.Parameters.Length]; 251 | for (int i = 0; i < command.Parameters.Length; i++) 252 | { 253 | parameters[i] = SerializationHelper.GetObjectAsType(command.Parameters[i], command.ParameterTypes[i]); 254 | } 255 | } 256 | 257 | // Invoke method 258 | if (method != null) 259 | { 260 | Logger.Log($"Invoking {method.DeclaringType.Name}.{method.Name}"); 261 | returnVal = method.Invoke(objectToInvokeOn, parameters); 262 | Logger.Log($"Return value: {returnVal?.ToString()}", MessagePriority.Verbose); 263 | } 264 | 265 | break; 266 | case Commands.GetProperty: 267 | case Commands.SetProperty: 268 | PropertyInfo property = null; 269 | object objectToAccess = null; 270 | // Lookup property via Reflection 271 | if (command.Type != null) 272 | { 273 | // Static property 274 | property = command.Type.GetRuntimeProperty(command.MemberName); 275 | } 276 | else if (command.ObjectId != Guid.NewGuid()) 277 | { 278 | // Instance property 279 | RemotelyCreatedObject obj = null; 280 | if (Objects.TryGetValue(command.ObjectId, out obj)) 281 | { 282 | objectToAccess = obj?.Value; 283 | property = objectToAccess?.GetType().GetRuntimeProperty(command.MemberName); 284 | } 285 | } 286 | 287 | // Get parameters 288 | object parameter = null; 289 | if (command.Parameters?.Length > 0) 290 | { 291 | parameter = SerializationHelper.GetObjectAsType(command.Parameters[0], command.ParameterTypes[0]); 292 | } 293 | 294 | // Get/Set the property 295 | if (property != null) 296 | { 297 | if (command.Command == Commands.GetProperty) 298 | { 299 | Logger.Log($"Getting property {property.DeclaringType.Name}.{property.Name}"); 300 | returnVal = property.GetValue(objectToAccess); 301 | Logger.Log($"Return value: {returnVal?.ToString()}", MessagePriority.Verbose); 302 | } 303 | else if (command.Command == Commands.SetProperty) 304 | { 305 | Logger.Log($"Setting property {property.DeclaringType.Name}.{property.Name}"); 306 | property.SetValue(objectToAccess, parameter); 307 | } 308 | } 309 | 310 | break; 311 | default: 312 | break; 313 | } 314 | } 315 | 316 | public void Dispose() 317 | { 318 | // Prevent new tasks from beginning 319 | ConnectionsActiveLock.EnterWriteLock(); 320 | Active = false; 321 | ConnectionsActiveLock.ExitWriteLock(); 322 | 323 | // Cancel existing tasks and wait for them to finish 324 | CTS.Cancel(); 325 | Task.WaitAll(ConnectionTasks.ToArray(), 5000); 326 | } 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /RemoteExecution/RemoteProxy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO.Pipes; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace RemoteExecution 9 | { 10 | public class RemoteProxy : IDisposable 11 | { 12 | // ID and type of the remote object 13 | public Guid ID { get; } 14 | public Type Type { get; } 15 | 16 | // Connection information 17 | private string RemoteMachineName { get; } 18 | private string RemoteExecutionServerName { get; } 19 | 20 | // The pipe for communicating with the server 21 | NamedPipeClientStream Pipe; 22 | 23 | /// 24 | /// Create a new remote proxy for a given type 25 | /// 26 | /// The type to create remotely 27 | /// The name of the remote execution server (pipe name) 28 | /// Parameters to pass to the objects constructor 29 | public RemoteProxy(Type type, string remoteExecutionServerName, object[] parameters) : this(type, ".", remoteExecutionServerName, parameters) { } 30 | 31 | /// 32 | /// Create a new remote proxy for a given type 33 | /// 34 | /// The type to create remotely 35 | /// The machine name where the remote server is running 36 | /// The name of the remote execution server (pipe name) 37 | /// Parameters to pass to the objects constructor 38 | public RemoteProxy(Type type, string serverName, string remoteExecutionServerName, object[] parameters) 39 | { 40 | RemoteMachineName = serverName; 41 | RemoteExecutionServerName = remoteExecutionServerName; 42 | Type = type; 43 | 44 | // Setup the pipe 45 | InitializeAsync().Wait(); 46 | 47 | // Request remote object be created 48 | var command = new RemoteCommand 49 | { 50 | Command = Commands.NewObject, 51 | Type = type, 52 | Parameters = parameters, 53 | ParameterTypes = parameters?.Select(p => p?.GetType()).ToArray() 54 | }; 55 | 56 | // Send the command and receive the remote object's ID back 57 | ID = SendCommandAsync(command).Result; 58 | } 59 | 60 | /// 61 | /// Create a proxy for an existing remote object 62 | /// 63 | /// The name of the remote execution server (pipe name) 64 | /// The remote object's ID 65 | public RemoteProxy(string remoteExecutionServerName, Guid objectId) : this(".", remoteExecutionServerName, objectId) { } 66 | 67 | /// 68 | /// Create a proxy for an existing remote object 69 | /// 70 | /// The machine name where the remote server is running 71 | /// The name of the remote execution server (pipe name) 72 | /// The remote object's ID 73 | public RemoteProxy(string serverName, string remoteExecutionServerName, Guid objectId) 74 | { 75 | RemoteMachineName = serverName; 76 | RemoteExecutionServerName = remoteExecutionServerName; 77 | ID = objectId; 78 | 79 | // Setup the pipe 80 | InitializeAsync().Wait(); 81 | 82 | // Request remote object be created 83 | var command = new RemoteCommand 84 | { 85 | Command = Commands.RetrieveObject, 86 | ObjectId = ID 87 | }; 88 | 89 | // Send the command and receive the remote object's ID back 90 | Type = SendCommandAsync(command).Result; 91 | } 92 | 93 | /// 94 | /// Prepares named pipe communication with the server 95 | /// 96 | private async Task InitializeAsync() 97 | { 98 | // Setup pipe 99 | Pipe = new NamedPipeClientStream(RemoteMachineName, RemoteExecutionServerName, PipeDirection.InOut, PipeOptions.Asynchronous); 100 | 101 | // Connect and set ReadMode 102 | await Pipe.ConnectAsync(); 103 | Pipe.ReadMode = PipeTransmissionMode.Message; 104 | } 105 | 106 | /// 107 | /// Invokes a method on the remote object 108 | /// 109 | /// Name of method to invoke 110 | /// Method parameters 111 | public async Task InvokeAsync(string methodName, params object[] parameters) 112 | { 113 | await InvokeAsync(methodName, parameters); 114 | } 115 | 116 | /// 117 | /// Invokes a method on the remote object 118 | /// 119 | /// The expected type of the returned object 120 | /// Name of method to invoke 121 | /// Method parameters 122 | public async Task InvokeAsync(string methodName, params object[] parameters) 123 | { 124 | var command = new RemoteCommand 125 | { 126 | ObjectId = ID, 127 | Command = Commands.Invoke, 128 | MemberName = methodName, 129 | Parameters = parameters, 130 | ParameterTypes = parameters?.Select(p => p?.GetType()).ToArray() 131 | }; 132 | 133 | return await SendCommandAsync(command); 134 | } 135 | 136 | /// 137 | /// Invoke a static method remotely 138 | /// 139 | /// The type containing the target method 140 | /// The name of the method to invoke 141 | /// Method parameters 142 | public async Task InvokeStaticAsync(Type type, string methodName, params object[] parameters) 143 | { 144 | await InvokeStaticAsync(type, methodName, parameters); 145 | } 146 | 147 | /// 148 | /// Invoke a static method remotely 149 | /// 150 | /// The expected type of the returned object 151 | /// The type containing the target method 152 | /// The name of the method to invoke 153 | /// Method parameters 154 | public async Task InvokeStaticAsync(Type type, string methodName, params object[] parameters) 155 | { 156 | var command = new RemoteCommand 157 | { 158 | Type = type, 159 | Command = Commands.Invoke, 160 | MemberName = methodName, 161 | Parameters = parameters, 162 | ParameterTypes = parameters?.Select(p => p?.GetType()).ToArray() 163 | }; 164 | 165 | return await SendCommandAsync(command); 166 | } 167 | 168 | /// 169 | /// Get a property value from the remote object 170 | /// 171 | /// The expected type of the returned object 172 | /// The property name to access 173 | public async Task GetPropertyAsync(string propertyName) 174 | { 175 | var command = new RemoteCommand 176 | { 177 | ObjectId = ID, 178 | Command = Commands.GetProperty, 179 | MemberName = propertyName, 180 | }; 181 | 182 | return await SendCommandAsync(command); 183 | } 184 | 185 | /// 186 | /// Get a property value for a static property 187 | /// 188 | /// The expected type of the returned object 189 | /// The type to get the property from 190 | /// The property name to access 191 | public async Task GetStaticPropertyAsync(Type type, string propertyName) 192 | { 193 | var command = new RemoteCommand 194 | { 195 | Type = type, 196 | Command = Commands.GetProperty, 197 | MemberName = propertyName, 198 | }; 199 | 200 | return await SendCommandAsync(command); 201 | } 202 | 203 | /// 204 | /// Sets a property on the remote object 205 | /// 206 | /// The property name to set 207 | /// The value to set the property to 208 | public async Task SetPropertyAsync(string propertyName, object value) 209 | { 210 | var command = new RemoteCommand 211 | { 212 | ObjectId = ID, 213 | Command = Commands.SetProperty, 214 | MemberName = propertyName, 215 | Parameters = new[] { value }, 216 | ParameterTypes = new[] { value?.GetType() } 217 | }; 218 | 219 | await SendCommandAsync(command); 220 | } 221 | 222 | /// 223 | /// Sets a static property remotely 224 | /// 225 | /// The type to set the property on 226 | /// The name of the property to set 227 | /// The value to set the property to 228 | public async Task SetStaticPropertyAsync(Type type, string propertyName, object value) 229 | { 230 | var command = new RemoteCommand 231 | { 232 | Type = type, 233 | Command = Commands.SetProperty, 234 | MemberName = propertyName, 235 | Parameters = new[] { value }, 236 | ParameterTypes = new[] { value?.GetType() } 237 | }; 238 | 239 | await SendCommandAsync(command); 240 | } 241 | 242 | /// 243 | /// Sends a command to the remote server 244 | /// 245 | /// The expected type of the returned object 246 | /// The command to execute remotely 247 | /// The return value from executing the given command on the remote server 248 | private async Task SendCommandAsync(RemoteCommand command) 249 | { 250 | // If the pipe isn't setup yet, do so now 251 | if (Pipe == null || !Pipe.IsConnected) 252 | { 253 | await InitializeAsync(); 254 | } 255 | 256 | // Serialize the command into bytes 257 | var commandString = SerializationHelper.Serialize(command); 258 | var commandBytes = Encoding.UTF8.GetBytes(commandString); 259 | 260 | // Write the serialized command bytes to the named pipe 261 | await Pipe.WriteAsync(commandBytes, 0, commandBytes.Length); 262 | 263 | // Read the return value from the named pipe 264 | // The remote server should alwasy send something back - even if it's just a null response 265 | var returnBuffer = new byte[1024]; 266 | var returnBytes = new List(); 267 | do 268 | { 269 | var byteCount = await Pipe.ReadAsync(returnBuffer, 0, returnBuffer.Length); 270 | returnBytes.AddRange(returnBuffer.Take(byteCount)); 271 | returnBuffer = new byte[1024]; 272 | } while (!Pipe.IsMessageComplete); 273 | 274 | // Deserialize the returned object 275 | var returnString = Encoding.UTF8.GetString(returnBytes.ToArray()); 276 | return SerializationHelper.Deserialize(returnString); 277 | } 278 | 279 | public void Dispose() 280 | { 281 | if (Pipe != null && ID != Guid.Empty) 282 | { 283 | if (Pipe.IsConnected) 284 | { 285 | // If the pipe is setup and connected, request that it be disconnected 286 | // and the ref count on the remote object be decreased. 287 | SendCommandAsync(new RemoteCommand 288 | { 289 | Command = Commands.CloseConnection, 290 | ObjectId = ID, 291 | }).Wait(1000); // Wait up to a second before closing the pipe 292 | } 293 | Pipe.Dispose(); 294 | } 295 | } 296 | } 297 | } -------------------------------------------------------------------------------- /RemoteExecution/RemotelyCreatedObject.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace RemoteExecution 7 | { 8 | internal class RemotelyCreatedObject 9 | { 10 | public int RefCount; 11 | public object Value { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /RemoteExecution/Serialization/SerializationHelper.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Reflection; 6 | 7 | namespace RemoteExecution 8 | { 9 | /// 10 | /// A helper class for serializing and deserializing types to/from JSON 11 | /// for transport over named pipes 12 | /// 13 | internal static class SerializationHelper 14 | { 15 | private static bool initialized = false; 16 | 17 | private static JsonSerializerSettings GetJsonSettings() => 18 | new JsonSerializerSettings 19 | { 20 | TypeNameHandling = TypeNameHandling.Objects, 21 | NullValueHandling = NullValueHandling.Ignore, 22 | ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, 23 | PreserveReferencesHandling = PreserveReferencesHandling.Objects, 24 | Converters = new List() { 25 | // Use a custom type converter to massage type names in order 26 | // to allow limited cross-platform (NetFX <-> NetCore) interoperability 27 | new TypeConverter() 28 | } 29 | }; 30 | 31 | public static string Serialize(object o) 32 | { 33 | if (!initialized) 34 | { 35 | JsonConvert.DefaultSettings = SerializationHelper.GetJsonSettings; 36 | initialized = true; 37 | } 38 | 39 | var json = JsonConvert.SerializeObject(o); 40 | return TypeConverter.CleanSerializedString(json); 41 | } 42 | 43 | public static object Deserialize(string s) 44 | { 45 | if (!initialized) 46 | { 47 | JsonConvert.DefaultSettings = SerializationHelper.GetJsonSettings; 48 | initialized = true; 49 | } 50 | 51 | try 52 | { 53 | return JsonConvert.DeserializeObject(s); 54 | } 55 | catch (JsonException exc) 56 | { 57 | throw new ArgumentException("Invalid json string", exc); 58 | } 59 | } 60 | 61 | public static T Deserialize(string s) 62 | { 63 | if (!initialized) 64 | { 65 | JsonConvert.DefaultSettings = SerializationHelper.GetJsonSettings; 66 | initialized = true; 67 | } 68 | 69 | try 70 | { 71 | return JsonConvert.DeserializeObject(s); 72 | } 73 | catch (JsonException exc) 74 | { 75 | throw new ArgumentException("Invalid json string", exc); 76 | } 77 | } 78 | 79 | // A wrapper around Convert.ChangeType to handle more cases known to be interesting 80 | // with Json.Net serialization. 81 | // http://www.newtonsoft.com/json/help/html/SerializationGuide.htm 82 | internal static object GetObjectAsType(object obj, Type type) 83 | { 84 | if (obj == null) return null; 85 | 86 | if (type == null) return obj; 87 | 88 | if (obj is JArray) 89 | { 90 | return ((JArray)obj).ToObject(type); 91 | } 92 | 93 | if (obj is JObject) 94 | { 95 | return ((JObject)obj).ToObject(type); 96 | } 97 | 98 | if (obj is string && type == typeof(Guid)) 99 | { 100 | return new Guid(obj as string); 101 | } 102 | 103 | if (obj is string && type == typeof(byte[])) 104 | { 105 | return Convert.FromBase64String(obj as string); 106 | } 107 | 108 | if (obj is string && typeof(DateTime).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())) 109 | { 110 | return DateTime.ParseExact(obj as string, "YYYY-MM-DDTHH:mmZ", null); 111 | } 112 | 113 | if (obj is string && typeof(Type).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())) 114 | { 115 | return Type.GetType(obj as string); 116 | } 117 | 118 | 119 | return Convert.ChangeType(obj, type); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /RemoteExecution/Serialization/TypeConverter.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Reflection; 4 | using System.Linq; 5 | 6 | namespace RemoteExecution 7 | { 8 | /// 9 | /// Type converter to allow limited NetCore <->NetFX interoperability 10 | /// There are still plenty of cross-platform scenarios that don't work, but 11 | /// by stripping out obviously different base/core assembly names from type 12 | /// strings, some simple cases can be made to work. 13 | /// 14 | public class TypeConverter : JsonConverter 15 | { 16 | // .NET Core-specific core assemblies which don't have NetFX facades 17 | static string[] SkippedAssemblies = { 18 | "System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e", 19 | "System.Private.CoreLib" 20 | }; 21 | 22 | // Specify which types (System.Type) this type converter works on 23 | public override bool CanConvert(Type objectType) 24 | { 25 | if (objectType == null) return false; 26 | return typeof(Type).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo()); 27 | } 28 | 29 | // Retrieves a type from its string representation 30 | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 31 | { 32 | Type ret = null; 33 | if (reader.TokenType == JsonToken.String) 34 | { 35 | ret = Type.GetType(reader.Value?.ToString()); 36 | } 37 | return ret; 38 | } 39 | 40 | // Writes a type as a string 41 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 42 | { 43 | var type = (value as Type).GetTypeInfo(); 44 | string typeString = null; 45 | 46 | // Assume that types in System.Private.CoreLib can be found without their assembly name qualifier (since they are base types) 47 | // Don't include the assembly name since it changes from Framework-to-Framework 48 | if (SkippedAssemblies.Contains(type.Assembly.FullName, StringComparer.OrdinalIgnoreCase)) 49 | { 50 | typeString = type.FullName; 51 | } 52 | else 53 | { 54 | typeString = type.AssemblyQualifiedName; 55 | } 56 | 57 | // Even if FullName is used, assembly qualified names can show up in generic parameters. 58 | // This subsequent scrub of the string removes those. 59 | // Using this approach, the previous decision to use FullName or AssemblyQualifiedName 60 | // doesn't actually change anything, but I'm leaving it for the moment since it's the 61 | // more obvious approach. 62 | typeString = CleanSerializedString(typeString); 63 | 64 | writer.WriteValue(typeString); 65 | } 66 | 67 | // Helper method to strip skipped assembly names from general string representations of types 68 | public static string CleanSerializedString(string serializedString) 69 | { 70 | foreach (var assmName in SkippedAssemblies) 71 | { 72 | serializedString = serializedString.Replace($", {assmName}", string.Empty); 73 | } 74 | 75 | return serializedString; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /RemoteExecution/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.9.0-*", 3 | 4 | "dependencies": { 5 | "NETStandard.Library": "1.6.1", 6 | "Newtonsoft.Json": "9.0.1", 7 | "System.IO.Pipes": "4.3.0" 8 | }, 9 | 10 | "frameworks": { 11 | "netstandard1.3": { 12 | "imports": "dnxcore50" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/TestClient/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using RemoteExecution; 5 | using TestTypes; 6 | using System.Linq; 7 | 8 | // Simple test client to demonstrate remoting functionality with some test input 9 | namespace TestClient 10 | { 11 | public class Program 12 | { 13 | const string RemoteExecutionName = "MyRemoteServer"; 14 | 15 | public static void Main(string[] args) 16 | { 17 | Console.ForegroundColor = ConsoleColor.Cyan; 18 | Console.WriteLine("#########################################"); 19 | Console.WriteLine("# Test 1 - Creating some remote objects #"); 20 | Console.WriteLine("#########################################"); 21 | Console.ForegroundColor = ConsoleColor.White; 22 | Console.WriteLine(); 23 | 24 | 25 | 26 | // Test #1 27 | Console.WriteLine("Creating proxy objects..."); 28 | 29 | var proxy0 = RemoteExecutionClient.CreateRemoteInstance(RemoteExecutionName); 30 | Console.WriteLine($"Created remote object with ID: {proxy0.ID}"); 31 | 32 | var proxy1 = RemoteExecutionClient.CreateRemoteInstance(RemoteExecutionName, "Hello, world!".ToCharArray()); 33 | Console.WriteLine($"Created remote object with ID: {proxy1.ID}"); 34 | 35 | var proxy2 = RemoteExecutionClient.CreateRemoteInstance>(RemoteExecutionName); 36 | Console.WriteLine($"Created remote object with ID: {proxy2.ID}"); 37 | 38 | var proxy3 = RemoteExecutionClient.CreateRemoteInstance>(RemoteExecutionName, 5); 39 | Console.WriteLine($"Created remote object with ID: {proxy3.ID}"); 40 | proxy3.InvokeAsync("Add", 3).Wait(); 41 | proxy3.InvokeAsync("Add", 5).Wait(); 42 | var index = proxy3.InvokeAsync("IndexOf", 5).Result; 43 | var count = proxy3.GetPropertyAsync("Count").Result; 44 | Console.WriteLine($"Added items to remote list (count: {count}) and found index of '5' is: {index}"); 45 | 46 | var proxy4 = RemoteExecutionClient.CreateRemoteInstance(RemoteExecutionName, typeof(Program), RemoteExecutionName, null); 47 | Console.WriteLine($"Created remote object with ID: {proxy4.ID}"); 48 | 49 | 50 | 51 | // Test #2 52 | Console.WriteLine(); 53 | Console.ForegroundColor = ConsoleColor.Cyan; 54 | Console.WriteLine("###########################################"); 55 | Console.WriteLine("# Test 2 - Interactive remote custom type #"); 56 | Console.WriteLine("###########################################"); 57 | Console.ForegroundColor = ConsoleColor.White; 58 | Console.WriteLine(); 59 | 60 | var proxy = RemoteExecutionClient.CreateRemoteInstance(RemoteExecutionName, "MyMessages"); 61 | Console.WriteLine($"MessageHolder proxy created with ID: {proxy.ID}"); 62 | Console.WriteLine(); 63 | 64 | bool shouldQuit = false; 65 | 66 | while (!shouldQuit) 67 | { 68 | Console.WriteLine("Select option:"); 69 | Console.WriteLine(" E - Enqueue"); 70 | Console.WriteLine(" D - Dequeue"); 71 | Console.WriteLine(" L - Length"); 72 | Console.WriteLine(" M - Retrieve max length"); 73 | Console.WriteLine(" S - Set max length"); 74 | Console.WriteLine(" P - Print status (remotely)"); 75 | Console.WriteLine(" Q - End test"); 76 | Console.Write(" > "); 77 | 78 | var option = char.ToUpperInvariant(Console.ReadLine().FirstOrDefault()); 79 | 80 | switch (option) 81 | { 82 | case 'E': 83 | Console.Write("Message: "); 84 | var message = Console.ReadLine(); 85 | proxy.InvokeAsync("AddMessageToQueue", message).Wait(); 86 | Console.WriteLine("Message sent"); 87 | break; 88 | case 'D': 89 | Console.WriteLine("Retrieving message..."); 90 | var retMessage = proxy.InvokeAsync("RetrieveMessageFromQueue").Result; 91 | Console.WriteLine($"Message: {retMessage}"); 92 | break; 93 | case 'L': 94 | Console.WriteLine("Retrieving length..."); 95 | var length = proxy.GetPropertyAsync("Length").Result; 96 | Console.WriteLine($"Length: {length}"); 97 | break; 98 | case 'M': 99 | Console.WriteLine("Retrieving Max length..."); 100 | var maxLength = proxy.GetPropertyAsync("MaxLength").Result; 101 | Console.WriteLine($"Max length: {maxLength}"); 102 | break; 103 | case 'S': 104 | Console.Write("Max length: "); 105 | var newLength = byte.Parse(Console.ReadLine()); 106 | proxy.SetPropertyAsync("MaxLength", newLength).Wait(); 107 | Console.WriteLine("Message sent"); 108 | break; 109 | case 'P': 110 | Console.WriteLine("Sending log command..."); 111 | proxy.InvokeAsync("LogQueueToConsole").Wait(); 112 | Console.WriteLine("Sent"); 113 | break; 114 | case 'Q': 115 | shouldQuit = true; 116 | break; 117 | default: 118 | Console.WriteLine("Invalid option"); 119 | break; 120 | } 121 | Console.WriteLine(); 122 | } 123 | 124 | 125 | 126 | Console.WriteLine("- Test Done -"); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Tests/TestClient/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "TestClient": { 4 | "commandName": "Project" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /Tests/TestClient/TestClient.xproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 10 | 6875513a-73e8-4cce-8648-82494a33efb2 11 | TestClient 12 | .\obj 13 | .\bin\ 14 | v4.5.2 15 | 16 | 17 | 18 | 2.0 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Tests/TestClient/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0-*", 3 | "buildOptions": { 4 | "emitEntryPoint": true 5 | }, 6 | 7 | "dependencies": { 8 | "RemoteExecution": "0.9.0", 9 | "TestComponent": "1.0.0" 10 | }, 11 | 12 | "frameworks": { 13 | "netcoreapp1.0": { 14 | "imports": "dnxcore50", 15 | "dependencies": { 16 | "Microsoft.NETCore.App": { 17 | "type": "platform", 18 | "version": "1.0.1" 19 | } 20 | } 21 | }, 22 | "net461": { } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Tests/TestComponent/TestComponent.xproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 10 | c81624ab-5a17-4385-8dc8-f3817e6e6552 11 | TestComponent 12 | .\obj 13 | .\bin\ 14 | v4.5.2 15 | 16 | 17 | 18 | 2.0 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Tests/TestComponent/TestType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | 5 | namespace TestTypes 6 | { 7 | /// 8 | /// A simple type to be created and accessed cross-process 9 | /// 10 | public class MessageHolder 11 | { 12 | public string Name { get; } 13 | 14 | internal Queue MessageQueue { get; } = new Queue(); 15 | 16 | public byte Length => (byte)MessageQueue.Count; 17 | 18 | public byte MaxLength { get; set; } = 5; 19 | 20 | public MessageHolder(string name) 21 | { 22 | Name = name; 23 | } 24 | 25 | public void AddMessageToQueue(string message) 26 | { 27 | if (MessageQueue.Count < MaxLength) 28 | MessageQueue.Enqueue(message); 29 | } 30 | 31 | public string RetrieveMessageFromQueue() 32 | { 33 | return MessageQueue.Dequeue(); 34 | } 35 | 36 | /// 37 | /// A Console.WriteLine-using diagnostic to demonstrate where the object is running 38 | /// 39 | public void LogQueueToConsole() 40 | { 41 | Console.WriteLine($"Contents of queue '{Name}' in process {Process.GetCurrentProcess().Id}:\n\t{string.Join("\n\t", MessageQueue)}"); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /Tests/TestComponent/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0-*", 3 | 4 | "dependencies": { 5 | "NETStandard.Library": "1.6.0", 6 | "System.Diagnostics.Process": "4.1.0" 7 | }, 8 | 9 | "frameworks": { 10 | "netstandard1.3": { 11 | "imports": "dnxcore50" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Tests/TestServer/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using RemoteExecution; 4 | 5 | namespace TestServer 6 | { 7 | public class Program 8 | { 9 | const string RemoteExecutionName = "MyRemoteServer"; 10 | static RemoteExecutionServer RemoteServer; 11 | 12 | public static void Main(string[] args) 13 | { 14 | Console.WriteLine("Creating RemoteExecutionServer..."); 15 | 16 | RemoteServer = new RemoteExecutionServer(RemoteExecutionName); 17 | 18 | // Enable verbose logging 19 | RemoteServer.Logger.LogLevel = MessagePriority.Verbose; 20 | 21 | // Report server creation and wait 22 | Console.WriteLine($"Remote server created with name {RemoteServer.RemoteName}"); 23 | Console.WriteLine("Press enter to quit"); 24 | Console.WriteLine(); 25 | Console.ReadLine(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/TestServer/TestServer.xproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 10 | 29192211-ae15-4b68-b79c-da6c2ef998d5 11 | TestServer 12 | .\obj 13 | .\bin\ 14 | v4.5.2 15 | 16 | 17 | 18 | 2.0 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Tests/TestServer/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0-*", 3 | "buildOptions": { 4 | "emitEntryPoint": true 5 | }, 6 | 7 | "dependencies": { 8 | "RemoteExecution": "0.9.0" 9 | }, 10 | 11 | "frameworks": { 12 | "netcoreapp1.0": { 13 | "imports": "dnxcore50", 14 | "dependencies": { 15 | "Microsoft.NETCore.App": { 16 | "type": "platform", 17 | "version": "1.0.1" 18 | } 19 | } 20 | }, 21 | "net461": { } 22 | } 23 | } 24 | --------------------------------------------------------------------------------