├── .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