diff --git a/build/build.sh b/build/build.sh index 1b7afe1..065cdbd 100755 --- a/build/build.sh +++ b/build/build.sh @@ -1,5 +1,5 @@ -dotnet publish -c Release -o output/win -r win-x64 -p:PublishSingleFile=true -p:DebugType=None src/heads/tbc.host.console/tbc.host.console.csproj -dotnet publish -c Release -o output/macos -r osx-x64 -p:PublishSingleFile=true -p:DebugType=None src/heads/tbc.host.console/tbc.host.console.csproj +dotnet publish -c Release -o output/win -r win-x64 -f net6.0 -p:PublishSingleFile=true -p:DebugType=None src/heads/tbc.host.console/tbc.host.console.csproj +dotnet publish -c Release -o output/macos-x64 -r osx-x64 -f net6.0 -p:PublishSingleFile=true -p:DebugType=None src/heads/tbc.host.console/tbc.host.console.csproj dotnet build -c Release src/components/tbc.core/tbc.core.csproj dotnet build -c Release src/components/tbc.target/tbc.target.csproj diff --git a/readme.md b/readme.md index 4f8dae7..6b89a2c 100644 --- a/readme.md +++ b/readme.md @@ -4,10 +4,9 @@ tbc facilitates patch-based c# hot reload for people who like hard work (and dep it's alpha quality by me for me -[![Here's a video](https://i.imgur.com/IljtZDz.png)](https://ryandavisau.blob.core.windows.net/store/teebeecee.mp4?sp=r&st=2021-02-15T07:34:52Z&se=2026-01-31T15:34:52Z&spr=https&sv=2019-12-12&sr=b&sig=eW2DWnMw162dTqEN7DIAzrB6J6MdRWIBZKoRfO32XF8%3D "Here's a video") - -#### ⚠️ note: running on M1 -seems like the [grpc core](https://github.com/grpc/grpc/tree/master/src/csharp) peeps are unlikely to implement macos/arm64 support. The api for the new-and-now-recommended [grpc-dotnet](https://github.com/grpc/grpc-dotnet) is a little too different to for me to migrate to at the moment. In the meantime, you can run tbc.console using x64 dotnet/rosetta: `PROTOBUF_TOOLS_OS=macos PROTOBUF_TOOLS_CPU=x64 dotnet run -a x64 --framework net6.0`. Feels a tad slower than on intel - most notably on the first reload - but still zippy. +now with maui powers +[![Here's a maui video](https://i.imgur.com/AKkcXaZ.png)](https://ryandavisau.blob.core.windows.net/store/teebeecee-maui.mp4?sv=2020-08-04&st=2022-04-13T06%3A53%3A20Z&se=2069-04-20T06%3A53%3A00Z&sr=b&sp=r&sig=vVw2wFzbDjcpCk2eOLqcFpffHnQuGEBBK5EwhCVotcc%3D) +([here's an old video showing more stuff](https://ryandavisau.blob.core.windows.net/store/teebeecee.mp4?sp=r&st=2021-02-15T07:34:52Z&se=2026-01-31T15:34:52Z&spr=https&sv=2019-12-12&sr=b&sig=eW2DWnMw162dTqEN7DIAzrB6J6MdRWIBZKoRfO32XF8%3D "Here's a video")) # features @@ -118,12 +117,14 @@ Since your `IReloadManager` is itself reloadable (provided it derives from `Relo ## debugging -Since the incremental compiler builds directly off the source files you're working on, debugging reloaded code is possible. Nice! +Since the incremental compiler builds directly off the source files you're working on, debugging reloaded code is possible. Nice! +VS for Mac seems to like to show the break higher in the callstack (at the first non-reloaded component), but you can select the current frame. Rider breaks in the expected place. # alpha quality -I've only used this for myself but have used earlier incarnations on production-complexity apps. I've only tested on iOS. +I've only used this for myself but on several production-complexity-level apps. I've only tested on iOS. -Your mileage may vary. Messing with static classes probably won't work (`tree remove` them 🤠). Xaml files won't work (delete them 🤠🤠). +Your mileage may vary. Messing with static classes probably won't work (`tree remove` them 🤠). Xaml files won't work (delete them 🤠🤠). Something that needs to be source generated won't work. If source generators are more common in maui, I'd see if it can be added. -I used gRPC for the host/target interop. For some reason, to build with iOS you need to add [this](https://github.com/rdavisau/tbc/blob/main/src/samples/prism/tbc.sample.prism/tbc.sample.prism/tbc.sample.prism.iOS/tbc.sample.prism.iOS.csproj#L149-L170) to your csproj. [Maybe it will get fixed](https://github.com/grpc/grpc/issues/19172), maybe I'll swap gRPC for something else. +This used to use grpc.core for message interchange but it was not apple silicon friendly. I replaced grpc with a socket-based transport which hasn't yet had a huge amount of testing. +But now it's apple silicon friendly and with .NET maui, the simulator is apple silicon friendly too! Finally nirvana. diff --git a/src/components/tbc.core/Apis/TbcApis.cs b/src/components/tbc.core/Apis/TbcApis.cs new file mode 100644 index 0000000..51d7b94 --- /dev/null +++ b/src/components/tbc.core/Apis/TbcApis.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using Refit; +using Tbc.Core.Models; + +namespace Tbc.Core.Apis; + +public interface ITbcTarget +{ + + [Post("/load-assembly")] + Task LoadAssembly(LoadDynamicAssemblyRequest request); + + [Post("/eval")] + Task Exec(ExecuteCommandRequest request); + + [Post("/synchronize-dependencies")] + Task SynchronizeDependencies(CachedAssemblyState cachedAssemblyState); +} + +public interface ITbcHost +{ + [Post("/add-assembly-reference")] + Task AddAssemblyReference(AssemblyReference reference); + + [Post("/add-assembly-reference")] + Task AddManyAssemblyReferences(ManyAssemblyReferences references); + + [Post("/execute-command")] + Task ExecuteCommand(ExecuteCommandRequest request); + + [Post("/heartbeat")] + Task Heartbeat(HeartbeatRequest request); +} + +public interface ITbcConnectable +{ + [Post("/connect")] + Task Connect(ConnectRequest req); +} + +public interface ITbcConnectableTarget : ITbcTarget, ITbcConnectable { } + +public interface ITbcProtocol : ITbcHost, ITbcTarget {} diff --git a/src/components/tbc.core/Http/HttpServer.cs b/src/components/tbc.core/Http/HttpServer.cs new file mode 100644 index 0000000..67ab821 --- /dev/null +++ b/src/components/tbc.core/Http/HttpServer.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Refit; + +namespace Tbc.Core.Http; + +public class HttpServer +{ + private readonly Action _log; + private readonly HttpListener _listener; + + public Dictionary>, Type ReturnType)> + _handlerOperations = new(); + + public HttpServer (int listenPort, THandler handler, Action? log = default) + { + _log = log ?? Console.WriteLine; + + _listener = new HttpListener { Prefixes = { $"http://+:{listenPort}/" } }; + + SetHandlerOperations(handler); + } + + public Task Run() + { + _listener.Start(); + +#pragma warning disable CS4014 + Task.Run(async () => await RunRequestLoop()) + .ContinueWith(t => +#pragma warning restore CS4014 + { + Console.WriteLine("Request loop terminated:"); + Console.WriteLine(t); + }); + + return Task.CompletedTask; + } + + private async Task RunRequestLoop() + { + while (true) + { + var requestContext = await _listener.GetContextAsync().ConfigureAwait(false); + +#pragma warning disable CS4014 + Task.Run(async () => +#pragma warning restore CS4014 + { + var (req, resp) = (requestContext.Request, requestContext.Response); + + Console.WriteLine($"{req.HttpMethod} {req.Url}"); + + try + { + var ret = await HandleRequest(req.Url.AbsolutePath, req.InputStream); + await Write(ret, resp).ConfigureAwait(false); + } + catch (Exception ex) + { + _log(ex.ToString()); + resp.StatusCode = 500; + var errorBytes = Encoding.UTF8.GetBytes(ex.ToString()); + await resp.OutputStream.WriteAsync(errorBytes, 0, errorBytes.Length).ConfigureAwait(false); + resp.Close(); + } + }); + } + } + + private async Task HandleRequest (string path, Stream inputStream) + { + var (type, action, output) = _handlerOperations[path]; + + using var sr = new StreamReader(inputStream); + var json = await sr.ReadToEndAsync(); + var content = JsonSerializer.Deserialize(json, type, new JsonSerializerOptions(JsonSerializerDefaults.Web)); + var ret = await action(content); + + return ret; + } + + private static async Task Write(object ret, HttpListenerResponse resp) + { + var json = JsonSerializer.Serialize(ret); + var buffer = Encoding.UTF8.GetBytes(json); + resp.StatusCode = 200; + + using var ms = new MemoryStream(); + using (var zip = new GZipStream(ms, CompressionMode.Compress, true)) + zip.Write(buffer, 0, buffer.Length); + + buffer = ms.ToArray(); + + resp.AddHeader("Content-Encoding", "gzip"); + resp.ContentLength64 = buffer.Length; + await resp.OutputStream.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + await resp.OutputStream.FlushAsync(); + + resp.Close(); + } + + private void SetHandlerOperations(THandler handler) + { + _handlerOperations = typeof(THandler).GetMethods().Concat(handler.GetType().GetMethods()) + .Select(x => (x.GetCustomAttribute(), x)) + .Where(x => x.Item1 != null) + .ToDictionary(x => x.Item1.Path, x => (x.x.GetParameters()[0].ParameterType, + new Func>(async y => + { + var t = (Task)x.x.Invoke(handler, new[] { y }); + await t; + return t.GetType().GetProperty("Result")!.GetValue(t); + }), x.x.ReturnType)); + + foreach (var operation in _handlerOperations) + _log($"{operation.Key}: {operation.Value.Item1.Name} -> {operation.Value.Item3}"); + } +} + diff --git a/src/components/tbc.core/IsExternalInit.cs b/src/components/tbc.core/IsExternalInit.cs new file mode 100644 index 0000000..f53b57d --- /dev/null +++ b/src/components/tbc.core/IsExternalInit.cs @@ -0,0 +1,3 @@ +namespace System.Runtime.CompilerServices; + +internal static class IsExternalInit {} diff --git a/src/components/tbc.core/Models/OperationModels.cs b/src/components/tbc.core/Models/OperationModels.cs index fdf7ea6..a9e6d68 100644 --- a/src/components/tbc.core/Models/OperationModels.cs +++ b/src/components/tbc.core/Models/OperationModels.cs @@ -1,64 +1,186 @@ using System; using System.Collections.Generic; +using MessagePack; namespace Tbc.Core.Models; public record ConnectRequest { - public HostInfo HostInfo { get; set; } + public HostInfo HostInfo { get; set; } = default!; +} + +public record HostInfo +{ + public string Address { get; set; } = default!; + public int Port { get; set; } + + public string HttpAddress => $"http://{Address}:{Port}"; } public record ConnectResponse { - public string AssemblyName { get; set; } + public string AssemblyName { get; set; } = default!; +} + +public record HeartbeatRequest { } + +public interface ISocketMessage +{ + SocketMessageKind Kind { get; } + string RequestIdentifier { get; set; } + object Payload { get; } } +[MessagePackObject] +public record SocketRequest : ISocketMessage +{ + [Key(0)] + public string RequestIdentifier { get; set; } = default!; + + [Key(1)] + public T Payload { get; set; } = default!; + + SocketMessageKind ISocketMessage.Kind => SocketMessageKind.Request; + object ISocketMessage.Payload => Payload; +} + +[MessagePackObject] +public record SocketResponse : ISocketMessage +{ + [Key(0)] + public string RequestIdentifier { get; set; } = default!; + + [Key(1)] + public SocketRequestOutcome Outcome { get; set; } + + [Key(2)] + public T Data { get; set; } = default!; + + [Key(3)] + public string? ErrorData { get; set; } + + SocketMessageKind ISocketMessage.Kind => SocketMessageKind.Response; + object ISocketMessage.Payload => Data; + + public SocketResponse() { } + + public SocketResponse(string requestIdentifier, T data) + { + RequestIdentifier = requestIdentifier; + Data = data; + Outcome = SocketRequestOutcome.Success; + } +} + +public enum SocketRequestOutcome +{ + Success, + ProtocolNotRecognised, + RequestNotHandled, + Error +} + +public enum SocketMessageKind +{ + Unset, + Request, + Response +} + +public record ReceiveResult +{ + public ReceiveResultOutcome Outcome { get; set; } + public SocketMessageKind Kind { get; set; } + public ISocketMessage? Message { get; set; } + public ISocketMessage? Response { get; set; } + + public Exception? Exception { get; set; } = default!; +} + +public enum ReceiveResultOutcome +{ + Success, + ProtocolNotRecognised, + RequestNotHandled, + WaywardMessage, + Error, + Disconnect +} + +[MessagePackObject] public record LoadDynamicAssemblyRequest { - public byte[] PeBytes { get; set; } - public byte[] PdbBytes { get; set; } - public string AssemblyName { get; set; } - public string PrimaryTypeName { get; set; } + [Key(0)] + public byte[] PeBytes { get; set; } = default!; + + [Key(1)] + public byte[]? PdbBytes { get; set; } + + [Key(2)] + public string AssemblyName { get; set; } = default!; + + [Key(3)] + public string PrimaryTypeName { get; set; } = default!; } +[MessagePackObject] public record Outcome { + [Key(0)] public bool Success { get; set; } + + [Key(1)] public List Messages { get; set; } = new(); } +[MessagePackObject] public record OutcomeMessage { - public string Message { get; set; } -} - -public record HostInfo -{ - public string Address { get; set; } - public int Port { get; set; } + [Key(0)] + public string Message { get; set; } = default!; } +[MessagePackObject] public record ExecuteCommandRequest { - public string Command { get; set; } + [Key(0)] + public string Command { get; set; } = default!; + + [Key(1)] public List Args { get; set; } = new(); } +[MessagePackObject] public record CachedAssemblyState { + [Key(0)] public List CachedAssemblies { get; set; } = new(); } +[MessagePackObject] public record CachedAssembly { + [Key(0)] public string AssemblyName { get; set; } = default!; + [Key(1)] public DateTimeOffset ModificationTime { get; set; } } +[MessagePackObject] public record AssemblyReference { + [Key(0)] public string AssemblyName { get; set; } = default!; + [Key(1)] public string AssemblyLocation { get; set; } = default!; + [Key(2)] public DateTimeOffset ModificationTime { get; set; } = default!; + [Key(3)] public byte[] PeBytes { get; set; } = default!; } + +[MessagePackObject] +public record ManyAssemblyReferences +{ + [Key(0)] public List AssemblyReferences { get; set; } = new(); +} diff --git a/src/components/tbc.core/Protos/asm.proto b/src/components/tbc.core/Protos/asm.proto deleted file mode 100644 index fff46cc..0000000 --- a/src/components/tbc.core/Protos/asm.proto +++ /dev/null @@ -1,56 +0,0 @@ -syntax = "proto3"; - -option csharp_namespace = "Tbc.Protocol"; - -package inject; - -service AssemblyLoader { - rpc LoadAssembly (LoadDynamicAssemblyRequest) returns (Outcome); - rpc Exec (ExecuteCommandRequest) returns (Outcome); - - rpc SynchronizeDependencies(CachedAssemblyState) returns (stream AssemblyReference); - rpc RequestCommand(Unit) returns (stream ExecuteCommandRequest); -} - -message CachedAssemblyState -{ - repeated CachedAssembly cachedAssemblies = 1; -} - -message CachedAssembly -{ - string assemblyName = 1; - uint64 modificationTime = 2; -} - -message Outcome { - bool success = 1; - repeated Message messages = 2; -} - -message Message { - string message = 1; -} - -message AssemblyReference -{ - string assemblyName = 1; - string assemblyLocation = 2; - uint64 modificationTime = 3; - bytes peBytes = 4; -} - -message LoadDynamicAssemblyRequest { - bytes peBytes = 1; - bytes pdbBytes = 2; - string assemblyName = 3; - string primaryTypeName = 4; -} - -message ExecuteCommandRequest { - string command = 1; - repeated string args = 2; -} - -message Unit { -} \ No newline at end of file diff --git a/src/components/tbc.core/Socket/Abstractions/IRemoteEndpoint.cs b/src/components/tbc.core/Socket/Abstractions/IRemoteEndpoint.cs new file mode 100644 index 0000000..ba83343 --- /dev/null +++ b/src/components/tbc.core/Socket/Abstractions/IRemoteEndpoint.cs @@ -0,0 +1,9 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Tbc.Core.Socket.Abstractions; + +public interface IRemoteEndpoint +{ + public Task SendRequest(TRequest request, CancellationToken canceller = default); +} \ No newline at end of file diff --git a/src/components/tbc.core/Socket/Abstractions/ISendToRemote.cs b/src/components/tbc.core/Socket/Abstractions/ISendToRemote.cs new file mode 100644 index 0000000..33a7ac0 --- /dev/null +++ b/src/components/tbc.core/Socket/Abstractions/ISendToRemote.cs @@ -0,0 +1,6 @@ +namespace Tbc.Core.Socket.Abstractions; + +public interface ISendToRemote +{ + IRemoteEndpoint Remote { get; set; } +} \ No newline at end of file diff --git a/src/components/tbc.core/Socket/Abstractions/ISocketSerializer.cs b/src/components/tbc.core/Socket/Abstractions/ISocketSerializer.cs new file mode 100644 index 0000000..fe964f3 --- /dev/null +++ b/src/components/tbc.core/Socket/Abstractions/ISocketSerializer.cs @@ -0,0 +1,9 @@ +using System; + +namespace Tbc.Core.Socket.Abstractions; + +public interface ISocketSerializer +{ + byte[] Serialize(object data); + object Deserialize(Type type, byte[] data); +} \ No newline at end of file diff --git a/src/components/tbc.core/Socket/Extensions/StreamReaderExtensions.cs b/src/components/tbc.core/Socket/Extensions/StreamReaderExtensions.cs new file mode 100644 index 0000000..8e7db31 --- /dev/null +++ b/src/components/tbc.core/Socket/Extensions/StreamReaderExtensions.cs @@ -0,0 +1,18 @@ +using System.IO; +using System.Threading.Tasks; + +namespace Tbc.Core.Socket.Extensions; + +public static class StreamReaderExtensions +{ + public static async Task ReadExactly(this Stream stream, int length) + { + var buffer = new byte[length]; + var bytesRead = 0; + + while (bytesRead < length) + bytesRead += await stream.ReadAsync(buffer, bytesRead, length - bytesRead); + + return buffer; + } +} \ No newline at end of file diff --git a/src/components/tbc.core/Socket/Models/SocketHandlerOperation.cs b/src/components/tbc.core/Socket/Models/SocketHandlerOperation.cs new file mode 100644 index 0000000..ce90a8b --- /dev/null +++ b/src/components/tbc.core/Socket/Models/SocketHandlerOperation.cs @@ -0,0 +1,6 @@ +using System; +using System.Threading.Tasks; + +namespace Tbc.Core.Socket.Models; + +public record SocketHandlerOperation(string Name, Type RequestType, Type ResponseType, Func> Handler); \ No newline at end of file diff --git a/src/components/tbc.core/Socket/Serialization/Serializers/ClearTextSystemTextJsonSocketSerializer.cs b/src/components/tbc.core/Socket/Serialization/Serializers/ClearTextSystemTextJsonSocketSerializer.cs new file mode 100644 index 0000000..d52025b --- /dev/null +++ b/src/components/tbc.core/Socket/Serialization/Serializers/ClearTextSystemTextJsonSocketSerializer.cs @@ -0,0 +1,25 @@ +using System; +using System.Text; +using System.Text.Json; +using Tbc.Core.Socket.Abstractions; + +namespace Tbc.Core.Socket.Serialization.Serializers; + +public class ClearTextSystemTextJsonSocketSerializer : ISocketSerializer +{ + public byte[] Serialize(object data) + { + var bytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(data, _serializerOptions)); + return bytes; + } + + public object Deserialize(Type type, byte[] data) + { + var json = Encoding.UTF8.GetString(data); + var ret = JsonSerializer.Deserialize(json, type, _serializerOptions); + + return ret; + } + + private readonly JsonSerializerOptions _serializerOptions = new(JsonSerializerDefaults.Web); +} diff --git a/src/components/tbc.core/Socket/Serialization/Serializers/MessagePackSocketSerializer.cs b/src/components/tbc.core/Socket/Serialization/Serializers/MessagePackSocketSerializer.cs new file mode 100644 index 0000000..f53c439 --- /dev/null +++ b/src/components/tbc.core/Socket/Serialization/Serializers/MessagePackSocketSerializer.cs @@ -0,0 +1,17 @@ +using System; +using MessagePack; +using Tbc.Core.Socket.Abstractions; + +namespace Tbc.Core.Socket.Serialization.Serializers; + +public class MessagePackSocketSerializer : ISocketSerializer +{ + public byte[] Serialize(object data) + => MessagePackSerializer.Serialize(data.GetType(), data, _serializationOptions); + + public object Deserialize(Type type, byte[] data) + => MessagePackSerializer.Deserialize(type, data, _serializationOptions); + + private readonly MessagePackSerializerOptions _serializationOptions = + MessagePackSerializerOptions.Standard.WithCompression(MessagePackCompression.Lz4Block); +} \ No newline at end of file diff --git a/src/components/tbc.core/Socket/Serialization/Serializers/SystemTextJsonSocketSerializer.cs b/src/components/tbc.core/Socket/Serialization/Serializers/SystemTextJsonSocketSerializer.cs new file mode 100644 index 0000000..75c9f2a --- /dev/null +++ b/src/components/tbc.core/Socket/Serialization/Serializers/SystemTextJsonSocketSerializer.cs @@ -0,0 +1,36 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Text.Json; +using Tbc.Core.Socket.Abstractions; + +namespace Tbc.Core.Socket.Serialization.Serializers; + +public class SystemTextJsonSocketSerializer : ISocketSerializer +{ + public byte[] Serialize(object data) + { + using var ms = new MemoryStream(); + using var gz = new GZipStream(ms, CompressionLevel.Optimal); + using var writer = new Utf8JsonWriter(gz); + + JsonSerializer.Serialize(writer, data, _serializerOptions); + writer.Flush(); + gz.Flush(); + + ms.Seek(0, SeekOrigin.Begin); + return ms.ToArray(); + } + + public object Deserialize(Type type, byte[] data) + { + using var ms = new MemoryStream(data); + using var gz = new GZipStream(ms, CompressionMode.Decompress); + + var ret = JsonSerializer.Deserialize(gz, type, _serializerOptions); + + return ret; + } + + private readonly JsonSerializerOptions _serializerOptions = new(JsonSerializerDefaults.Web); +} diff --git a/src/components/tbc.core/Socket/Serialization/SocketSerializationFormat.cs b/src/components/tbc.core/Socket/Serialization/SocketSerializationFormat.cs new file mode 100644 index 0000000..77afb0d --- /dev/null +++ b/src/components/tbc.core/Socket/Serialization/SocketSerializationFormat.cs @@ -0,0 +1,8 @@ +namespace Tbc.Core.Socket.Serialization; + +public enum SocketSerializationFormat +{ + MessagePack, + Json, + CompressedJson +} \ No newline at end of file diff --git a/src/components/tbc.core/Socket/SocketServer.cs b/src/components/tbc.core/Socket/SocketServer.cs new file mode 100644 index 0000000..5105e19 --- /dev/null +++ b/src/components/tbc.core/Socket/SocketServer.cs @@ -0,0 +1,323 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Sockets; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Tbc.Core.Models; +using Tbc.Core.Socket.Abstractions; +using Tbc.Core.Socket.Extensions; +using Tbc.Core.Socket.Models; +using Tbc.Core.Socket.Serialization; +using Tbc.Core.Socket.Serialization.Serializers; + +namespace Tbc.Core.Socket; + +public class SocketServer : IRemoteEndpoint +{ + public string Identifier { get; } + public TcpClient Socket { get; private set; } + private Stream Stream => Socket.GetStream(); + public object Handler { get; } + + public Dictionary Protocol = new(); + public Dictionary HandlerOperations = new(); + + private readonly ConcurrentDictionary> _pendingRequests = new(); + private readonly Func? _onDisconnect; + private readonly Action _log; + private bool _finished; + + private SocketSerializationFormat _serializationFormat = SocketSerializationFormat.Json; + public ISocketSerializer Serializer { get; set; } + + public Action? OnReceived { get; set; } + + public SocketServer (TcpClient socket, object handler, string identifier, + Action? log = null, + Func? onDisconnect = null) + { + _log = log ?? Console.WriteLine; + _log = x => { Console.Write($"{Identifier}: "); log?.Invoke(x); }; + _onDisconnect = onDisconnect; + + Socket = socket; + Handler = handler; + Identifier = identifier; + + Socket.ReceiveBufferSize = 1024 * 1024 * 5; + Socket.SendBufferSize = 1024 * 1024 * 5; + + Serializer = _serializationFormat switch + { + SocketSerializationFormat.Json => new ClearTextSystemTextJsonSocketSerializer(), + SocketSerializationFormat.CompressedJson => new SystemTextJsonSocketSerializer(), + SocketSerializationFormat.MessagePack => new MessagePackSocketSerializer() + }; + + if (Handler is ISendToRemote str) + str.Remote = this; + + SetProtocol(); + SetHandlerOperations(); + } + + public async Task Run(CancellationToken ct = default) + { +#pragma warning disable CS4014 + Task.Run(async () => await RunRequestLoop(ct)) + .ContinueWith(t => +#pragma warning restore CS4014 + { + _log("Request loop terminated:"); + _log(t.Exception?.ToString() ?? "no exception"); + _finished = true; + }); + } + + private async Task RunRequestLoop(CancellationToken ct = default) + { + while (true) + { + var receiveResult = await ReceiveAndHandle(ct); + _log($"receive result: {receiveResult}"); + + if (receiveResult.Outcome == ReceiveResultOutcome.Disconnect) + await Terminate(); + } + } + + private async Task Terminate() + { + if (_onDisconnect is { } od) + await od.Invoke(); + + throw new FinishedException(); + } + + public class FinishedException : Exception {} + + private async Task ReceiveAndHandle(CancellationToken ct = default) + { + var receiveResult = await Receive(ct); + + OnReceived?.Invoke(receiveResult); + + if (receiveResult.Outcome != ReceiveResultOutcome.Success || receiveResult.Message is null) + return receiveResult; + + var requestIdentifier = receiveResult.Message.RequestIdentifier; + var data = receiveResult.Message.Payload; + + if (receiveResult.Message.Kind is SocketMessageKind.Request) + { + // invoke handler and return response + var receivedType = data.GetType(); + var operation = HandlerOperations.GetValueOrDefault(receivedType); + if (operation is null) + { + _log($"WARN: No handler for message: ({receivedType.Name}): {data}"); + return receiveResult with { Outcome = ReceiveResultOutcome.RequestNotHandled }; + } + + var result = await operation.Handler(data); + _log($"Result for operation {operation.Name}: {result}"); + + var resultType = result.GetType(); + var responseMessageEnvelopeType = typeof(SocketResponse<>).MakeGenericType(resultType); + var response = (ISocketMessage) Activator.CreateInstance(responseMessageEnvelopeType, requestIdentifier, result); + + await SendResponse(receiveResult.Message, response, ct); + + return receiveResult with { Response = response }; + } + else + { + // route to pending request + if (!_pendingRequests.TryRemove(requestIdentifier, out var matchingCompletionSource)) + { + _log($"Received result with identifier {requestIdentifier} ({data}) but there was no pending request"); + + return receiveResult with { Outcome = ReceiveResultOutcome.WaywardMessage }; + } + + matchingCompletionSource.SetResult(receiveResult); + + return receiveResult; + } + } + + public async Task SendRequest(TRequest request, CancellationToken ct = default) + { + if (request == null) throw new ArgumentNullException(nameof(request)); + + if (_finished) + { + _log("ignoring attempt to send message on finished socket server, please find the caller " + + "who did this and prevent them from doing this >:)"); + + return default!; + } + + var responseMessageId = + Protocol.Where(x => x.Value == request.GetType()).Select(x => x.Key).FirstOrDefault(); + + var identifier = Guid.NewGuid().ToString(); + var requestEnvelope = new SocketRequest { RequestIdentifier = identifier, Payload = request }; + var requestData = Serializer.Serialize(requestEnvelope); + var requestLength = requestData.Length; + + var final = Enumerable + .Concat(BitConverter.GetBytes((int)(SocketMessageKind.Request)), BitConverter.GetBytes(responseMessageId)) + .Concat(BitConverter.GetBytes(requestLength)).Concat(requestData) + .ToArray(); + + var tcs = new TaskCompletionSource(); + _pendingRequests.AddOrUpdate(identifier, _ => tcs, (_, _) => tcs); + + _log($"Sending {request.GetType().Name} ({final.Length:N0} bytes in envelope): {requestData}"); + + var stream = Stream; + + await stream.WriteAsync(final, 0, final.Length, ct); + await stream.FlushAsync(ct); + + var response = (ReceiveResult) await tcs.Task; + + if (response.Message is not SocketResponse expectedResponse) + { + _log($"Expected response of type {typeof(TResponse).Name} but received {response.GetType().Name}"); + throw new Exception("Unexpected response to request"); + } + + return expectedResponse.Data; + } + + public async Task SendResponse(ISocketMessage request, ISocketMessage response, CancellationToken ct = default) + { + var responseType = response.Payload.GetType(); + + var responseMessageId = + Protocol.Where(x => x.Value == responseType).Select(x => x.Key).FirstOrDefault(); + + var requestEnvelope = response; + var requestData = Serializer.Serialize(requestEnvelope); + var requestLength = requestData.Length; + + var final = Enumerable + .Concat(BitConverter.GetBytes((int)(SocketMessageKind.Response)), BitConverter.GetBytes(responseMessageId)) + .Concat(BitConverter.GetBytes(requestLength)).Concat(requestData) + .ToArray(); + + _log($"Sending {responseType.Name} ({final.Length:N0} bytes in envelope): {requestData}"); + + var stream = Stream; + + await stream.WriteAsync(final, 0, final.Length, ct); + await stream.FlushAsync(ct); + } + + private async Task Receive(CancellationToken ct) + { + // protocol is + // socket message kind + // int message id + // int message length + // length bytes of data + var incomingMessageLength = sizeof(int) + sizeof(int) + sizeof(int); + + var typeBuf = new byte[incomingMessageLength]; + var stream = Stream; + + var len = await stream.ReadAsync(typeBuf, 0, incomingMessageLength, ct); + if (len < incomingMessageLength) + { + return new ReceiveResult { Outcome = ReceiveResultOutcome.Disconnect }; + } + + var socketMessageKind = (SocketMessageKind)BitConverter.ToInt32(typeBuf, 0); + var messageId = BitConverter.ToInt32(typeBuf, 4); + var messageLength = BitConverter.ToInt32(typeBuf, 8); + + var buffer = await stream.ReadExactly(messageLength); + + var incomingType = Protocol.GetValueOrDefault(messageId); + if (incomingType is null) + { + _log($"Received unrecognised message with id: {messageId}"); + return new ReceiveResult { Outcome = ReceiveResultOutcome.ProtocolNotRecognised }; + } + + var envelopedIncomingType = + socketMessageKind switch + { + SocketMessageKind.Request => typeof(SocketRequest<>).MakeGenericType(incomingType), + SocketMessageKind.Response => typeof(SocketResponse<>).MakeGenericType(incomingType) + }; + + var message = (ISocketMessage) + Serializer.Deserialize(envelopedIncomingType, buffer); + + return new ReceiveResult + { + Kind = socketMessageKind, + Outcome = ReceiveResultOutcome.Success, + Message = message + }; + } + + private void SetProtocol() + { + Protocol = GetMethodsInInterfaceHierarchy(typeof(TProtocol)) + .SelectMany(x => new [] { x.GetParameters()[0].ParameterType, x.ReturnType.GetGenericArguments()[0] }) + .GroupBy(x => x) + .Select(x => x.First()) + .OrderBy(x => x.Name) + .Select((x,i) => new { Index = i + 1, Type = x }) + .ToDictionary(x => x.Index, x => x.Type); + + _log("Protocol:"); + foreach (var model in Protocol) + _log($"\t[{model.Key}]: {model.Value}"); + } + + // assume all handlers + // have one parameter + // return Task + private void SetHandlerOperations() + { + HandlerOperations = Handler.GetType().GetMethods().Concat(Handler.GetType().GetMethods()) + .Where(x => x.GetParameters().Any() && Protocol.ContainsValue(x.GetParameters()[0].ParameterType)) + .GroupBy(x => x.GetParameters()[0].ParameterType) + .Select(x => x.First()) + .Select(x => + new SocketHandlerOperation(x.Name, x.GetParameters()[0].ParameterType, x.ReturnType.GetGenericArguments()[0], + async y => + { + var t = (Task)x.Invoke(Handler, new[] { y }); + await t; return t.GetType().GetProperty("Result")!.GetValue(t); + })) + .ToDictionary(x => x.RequestType); + + _log("Handler Operations:"); + foreach (var operation in HandlerOperations.Values) + _log($"\t{operation.Name}: {operation.RequestType} -> {operation.ResponseType}"); + } + + static IEnumerable GetMethodsInInterfaceHierarchy(Type type) { + foreach (var method in type.GetMethods(BindingFlags.Instance | BindingFlags.Static | + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly)) { + yield return method; + } + if (type.IsInterface) { + foreach (var iface in type.GetInterfaces()) { + foreach (var method in GetMethodsInInterfaceHierarchy(iface)) { + yield return method; + } + } + } + } +} diff --git a/src/components/tbc.core/tbc.core.csproj b/src/components/tbc.core/tbc.core.csproj index fe8f26f..4b06218 100644 --- a/src/components/tbc.core/tbc.core.csproj +++ b/src/components/tbc.core/tbc.core.csproj @@ -4,16 +4,15 @@ netstandard2.1 Tbc.Core latest + true + true + embedded + enable - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/src/components/tbc.host/Components/CommandProcessor/Models/IHaveComponentsThatExposeCommands.cs b/src/components/tbc.host/Components/CommandProcessor/Models/IHaveComponentsThatExposeCommands.cs index 7ce882b..de5ee90 100644 --- a/src/components/tbc.host/Components/CommandProcessor/Models/IHaveComponentsThatExposeCommands.cs +++ b/src/components/tbc.host/Components/CommandProcessor/Models/IHaveComponentsThatExposeCommands.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Tbc.Protocol; +using Tbc.Core.Models; namespace Tbc.Host.Components.CommandProcessor.Models { @@ -14,4 +14,4 @@ public interface IWantToRequestCommands { Func RequestCommand { get; set; } } -} \ No newline at end of file +} diff --git a/src/components/tbc.host/Components/FileEnvironment/FileEnvironment.cs b/src/components/tbc.host/Components/FileEnvironment/FileEnvironment.cs index 1a30485..a0b6d7a 100644 --- a/src/components/tbc.host/Components/FileEnvironment/FileEnvironment.cs +++ b/src/components/tbc.host/Components/FileEnvironment/FileEnvironment.cs @@ -58,9 +58,10 @@ public async Task Run() await TryLoadLoadContext(); Task.Factory.StartNew(SetupReferenceTracking, TaskCreationOptions.LongRunning); - + FileWatcher .Changes + .Where(_ => !Terminated) .Select(IncrementalCompiler.StageFile) .Where(x => x != null) .SelectMany(SendAssemblyForReload) @@ -73,7 +74,7 @@ public async Task Run() await Client.WaitForTerminalState(); Terminated = true; - + Logger.LogWarning("FileEnvironment for client {@Client} terminating", Client); } @@ -184,7 +185,7 @@ public async Task SendAssemblyForReload(EmittedAssembly asm) : TryResolvePrimaryType(_primaryTypeHint) }; - return await Client.LoadAssemblyAsync(req); + return await Client.RequestClientLoadAssemblyAsync(req); } public string TryResolvePrimaryType(string typeHint) @@ -234,7 +235,7 @@ string IExposeCommands.Identifier foreach (var arg in args) req.Args.Add(arg); - var outcome = await Client.ExecAsync(req); + var outcome = await Client.RequestClientExecAsync(req); Logger.LogInformation("{@Outcome}", outcome); } diff --git a/src/components/tbc.host/Components/FileEnvironment/Models/IRemoteClientDefinition.cs b/src/components/tbc.host/Components/FileEnvironment/Models/IRemoteClientDefinition.cs index d7fc5c8..e0ad865 100644 --- a/src/components/tbc.host/Components/FileEnvironment/Models/IRemoteClientDefinition.cs +++ b/src/components/tbc.host/Components/FileEnvironment/Models/IRemoteClientDefinition.cs @@ -4,5 +4,7 @@ public interface IRemoteClientDefinition { public string Address { get; set; } public int Port { get; set; } + + public string HttpAddress => $"http://{Address}:{Port}"; } } diff --git a/src/components/tbc.host/Components/IncrementalCompiler/IncrementalCompiler.cs b/src/components/tbc.host/Components/IncrementalCompiler/IncrementalCompiler.cs index e256f60..b96773e 100644 --- a/src/components/tbc.host/Components/IncrementalCompiler/IncrementalCompiler.cs +++ b/src/components/tbc.host/Components/IncrementalCompiler/IncrementalCompiler.cs @@ -17,7 +17,7 @@ using Tbc.Host.Components.FileEnvironment.Models; using Tbc.Host.Components.FileWatcher.Models; using Tbc.Host.Components.IncrementalCompiler.Models; -using Tbc.Host.Components.TargetClient.GrpcCore; +using Tbc.Host.Components.TargetClient; using Tbc.Host.Config; using Tbc.Host.Extensions; @@ -28,7 +28,7 @@ public class IncrementalCompiler : ComponentBase, IIncremen private bool _disposing; private readonly AssemblyCompilationOptions _options; - private readonly GrpcCoreTargetClient _client; + private readonly ITargetClient _client; private readonly IFileSystem _fileSystem; @@ -49,7 +49,7 @@ public List StagedFiles public IncrementalCompiler( AssemblyCompilationOptions options, - IRemoteClientDefinition client, Func targetClientFactory, + IRemoteClientDefinition client, Func targetClientFactory, IFileSystem fileSystem, ILogger logger) : base(logger) { _options = options; @@ -118,7 +118,7 @@ public EmittedAssembly StageFile(ChangedFile file, bool silent = false) Environment.NewLine, result.Diagnostics .Where(x => x.Severity == DiagnosticSeverity.Error) - .Select(x => x.GetMessage()))); + .Select(x => $"{x.Location}: {x.GetMessage()}"))); return newC; }); diff --git a/src/components/tbc.host/Components/TargetClient/CanonicalChannelState.cs b/src/components/tbc.host/Components/TargetClient/CanonicalChannelState.cs index 2a081f6..83f0415 100644 --- a/src/components/tbc.host/Components/TargetClient/CanonicalChannelState.cs +++ b/src/components/tbc.host/Components/TargetClient/CanonicalChannelState.cs @@ -1,3 +1,5 @@ +namespace Tbc.Host.Components.TargetClient; + public enum CanonicalChannelState { /// diff --git a/src/components/tbc.host/Components/TargetClient/GrpcCore/GrpcCoreMappingExtensions.cs b/src/components/tbc.host/Components/TargetClient/GrpcCore/GrpcCoreMappingExtensions.cs deleted file mode 100644 index 49e3f9d..0000000 --- a/src/components/tbc.host/Components/TargetClient/GrpcCore/GrpcCoreMappingExtensions.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Linq; -using Google.Protobuf; -using Tbc.Core.Models; -using AssemblyReference = Tbc.Protocol.AssemblyReference; -using ExecuteCommandRequest = Tbc.Protocol.ExecuteCommandRequest; -using LoadDynamicAssemblyRequest = Tbc.Protocol.LoadDynamicAssemblyRequest; -using Outcome = Tbc.Protocol.Outcome; - -namespace Tbc.Host.Components.TargetClient.GrpcCore; - -public static class GrpcCoreMappingExtensions -{ - public static Tbc.Core.Models.Outcome ToCanonical(this Outcome o) - => new() { - Success = o.Success, - Messages = o.Messages.Select(x => new OutcomeMessage { Message = x.Message_ }).ToList() - }; - - public static Tbc.Core.Models.ExecuteCommandRequest ToCanonical(this ExecuteCommandRequest x) - => new() { Command = x.Command, Args = x.Args.ToList() }; - - public static Tbc.Core.Models.AssemblyReference ToCanonical(this AssemblyReference x) - => new() - { - AssemblyName = x.AssemblyName, - ModificationTime = DateTimeOffset.FromUnixTimeSeconds((long)x.ModificationTime), - AssemblyLocation = x.AssemblyLocation, - PeBytes = x.PeBytes.ToByteArray() - }; - - public static LoadDynamicAssemblyRequest ToCore(this Tbc.Core.Models.LoadDynamicAssemblyRequest req) - => new() - { - AssemblyName = req.AssemblyName, - PeBytes = ByteString.CopyFrom(req.PeBytes), - PdbBytes = ByteString.CopyFrom(req.PdbBytes), - PrimaryTypeName = req.PrimaryTypeName - }; - - public static ExecuteCommandRequest ToCore(this Tbc.Core.Models.ExecuteCommandRequest req) - => new() - { - Command = req.Command, - Args = { req.Args } - }; -} diff --git a/src/components/tbc.host/Components/TargetClient/GrpcCore/GrpcCoreTargetClient.cs b/src/components/tbc.host/Components/TargetClient/GrpcCore/GrpcCoreTargetClient.cs deleted file mode 100644 index a90288e..0000000 --- a/src/components/tbc.host/Components/TargetClient/GrpcCore/GrpcCoreTargetClient.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using System.Reactive.Threading.Tasks; -using System.Threading.Tasks; -using Grpc.Core; -using Microsoft.Extensions.Logging; -using Tbc.Host.Components.Abstractions; -using Tbc.Host.Components.FileEnvironment.Models; -using Tbc.Host.Extensions; -using Tbc.Protocol; -using CachedAssemblyState = Tbc.Protocol.CachedAssemblyState; - -namespace Tbc.Host.Components.TargetClient.GrpcCore -{ - public class GrpcCoreTargetClient : ComponentBase, ITargetClient - { - public IRemoteClientDefinition ClientDefinition { get; } - - private readonly Subject _channelStateSub = new(); - private CanonicalChannelState ToCanonicalChannelState(ChannelState state) - => Enum.Parse(state.ToString()); // CanonicalChannelState is a clone of ChannelState - - public IObservable ClientChannelState - => _channelStateSub - .Select(ToCanonicalChannelState) - .AsObservable(); - - private AssemblyLoader.AssemblyLoaderClient Loader { get; set; } - private Channel Channel { get; set; } - - public GrpcCoreTargetClient(IRemoteClientDefinition client, ILogger logger) : base(logger) - { - ClientDefinition = client; - } - - public async Task WaitForConnection() - { - var channel = new Channel( - ClientDefinition.Address, ClientDefinition.Port, - ChannelCredentials.Insecure, - new [] - { - new ChannelOption(ChannelOptions.MaxReceiveMessageLength, -1), - new ChannelOption(ChannelOptions.MaxSendMessageLength, -1) - }); - - await channel.ConnectAsync(); - - Task.Run(async () => await TrackChannelState(channel)); - - Channel = channel; - } - - private async Task TrackChannelState(Channel channel) - { - void LogState(ChannelState s) - => Logger.LogInformation("Client {Client} channel state is now '{ChannelState}'", ClientDefinition, s); - - var state = channel.State; - LogState(state); - - while (state != ChannelState.Shutdown && state != ChannelState.TransientFailure) - { - await channel.TryWaitForStateChangedAsync(channel.State); - - state = channel.State; - LogState(state); - - _channelStateSub.OnNext(state); - } - } - - public async Task> AssemblyReferences() - { - if (Channel?.State != ChannelState.Ready) - throw new Exception($"Channel state is '{Channel?.State}' but needed '{ChannelState.Ready}'"); - - while (true) - { - try - { - Loader ??= new AssemblyLoader.AssemblyLoaderClient(Channel); - - return Loader.SynchronizeDependencies(new CachedAssemblyState()).ResponseStream - .ReadAllAsync(x => x.ToCanonical()); - } - catch (Exception ex) - { - await Task.Delay(TimeSpan.FromSeconds(.33)); - } - } - } - - public async Task> CommandRequests() - { - if (Channel?.State != ChannelState.Ready) - throw new Exception($"Channel state is '{Channel?.State}' but needed '{ChannelState.Ready}'"); - - while (true) - { - try - { - return Loader.RequestCommand(new Unit()).ResponseStream.ReadAllAsync(x => x.ToCanonical()); - } - catch (Exception ex) - { - Console.WriteLine(ex); - await Task.Delay(TimeSpan.FromSeconds(.33)); - } - } - } - public override string ToString() - { - return $"{ClientDefinition.Address}:{ClientDefinition.Port} (Channel State: {Channel?.State})"; - } - - public void Dispose() - { - - } - - public Task WaitForTerminalState() => - ClientChannelState - .TakeUntil(x => x == CanonicalChannelState.Shutdown - || x == CanonicalChannelState.TransientFailure - || x == CanonicalChannelState.Idle) - .ToTask(); - - public Task ExecAsync(Tbc.Core.Models.ExecuteCommandRequest req) - => Loader.ExecAsync(req.ToCore()).ResponseAsync.ContinueWith(t => t.Result.ToCanonical()); - - public Task LoadAssemblyAsync(Tbc.Core.Models.LoadDynamicAssemblyRequest req) - => Loader.LoadAssemblyAsync(req.ToCore()).ResponseAsync.ContinueWith(t => t.Result.ToCanonical()); - } -} diff --git a/src/components/tbc.host/Components/TargetClient/ITargetClient.cs b/src/components/tbc.host/Components/TargetClient/ITargetClient.cs index 49fc8fc..596595d 100644 --- a/src/components/tbc.host/Components/TargetClient/ITargetClient.cs +++ b/src/components/tbc.host/Components/TargetClient/ITargetClient.cs @@ -16,6 +16,6 @@ public interface ITargetClient : IDisposable Task> AssemblyReferences(); Task> CommandRequests(); - Task ExecAsync(ExecuteCommandRequest req); - Task LoadAssemblyAsync(LoadDynamicAssemblyRequest req); + Task RequestClientExecAsync(ExecuteCommandRequest req); + Task RequestClientLoadAssemblyAsync(LoadDynamicAssemblyRequest req); } diff --git a/src/components/tbc.host/Components/TargetClient/SocketTargetClient.cs b/src/components/tbc.host/Components/TargetClient/SocketTargetClient.cs new file mode 100644 index 0000000..6c86e81 --- /dev/null +++ b/src/components/tbc.host/Components/TargetClient/SocketTargetClient.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net.Sockets; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Reactive.Threading.Tasks; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Tbc.Core.Apis; +using Tbc.Core.Models; +using Tbc.Core.Socket; +using Tbc.Host.Components.Abstractions; +using Tbc.Host.Components.FileEnvironment.Models; + +namespace Tbc.Host.Components.TargetClient; + +public class SocketTargetClient : ComponentBase, ITargetClient, ITbcHost +{ + public TcpClient TcpClient { get; private set; } + public SocketServer Target { get; set; } + + public SocketTargetClient(ILogger logger, IRemoteClientDefinition clientDefinition) : base(logger) + { + ClientDefinition = clientDefinition; + } + + public IRemoteClientDefinition ClientDefinition { get; } + public IObservable ClientChannelState + => _clientChannelState.AsObservable(); + + private readonly Subject _clientChannelState = new(); + private readonly Subject _incomingAssemblyReferences = new(); + private readonly Subject _incomingCommandRequests = new(); + + public async Task WaitForConnection() + { + _clientChannelState.OnNext(CanonicalChannelState.Connecting); + + var success = false; + while (!success) + { + _clientChannelState.OnNext(CanonicalChannelState.Connecting); + + try + { + TcpClient = new TcpClient(); + await TcpClient.ConnectAsync(ClientDefinition.Address, ClientDefinition.Port); + Target = new SocketServer(TcpClient, this, "host", x => Logger.LogInformation("{@Message}", x), + () => + { + try { TcpClient.Dispose(); } + catch (Exception ex) { Logger.LogError(ex, "On tcpclient dispose"); } + + _clientChannelState.OnNext(CanonicalChannelState.Shutdown); + return Task.CompletedTask; + }); + await Target.Run(); + + Logger.LogInformation("TcpClient connected: {@State}", TcpClient.Connected); + + _clientChannelState.OnNext(CanonicalChannelState.Ready); + + success = true; + } + catch (Exception ex) + { + _clientChannelState.OnNext(CanonicalChannelState.TransientFailure); + + success = false; + } + + if (!success) + await Task.Delay(TimeSpan.FromSeconds(.5)); + } + } + + public async Task> AssemblyReferences() + { + Task.Delay(TimeSpan.FromSeconds(.1)).ContinueWith(_ => Target.SendRequest(new CachedAssemblyState())); + + return _incomingAssemblyReferences.ToAsyncEnumerable(); + } + + public async Task> CommandRequests() + => _incomingCommandRequests.ToAsyncEnumerable(); + + public Task RequestClientExecAsync(ExecuteCommandRequest req) + => Target.SendRequest(req); + + public async Task RequestClientLoadAssemblyAsync(LoadDynamicAssemblyRequest req) + { + var sw = Stopwatch.StartNew(); + var result = await Target.SendRequest(req); + Logger.LogInformation("Round trip for LoadAssembly with primary type {PrimaryTypeName}, {Duration}ms", req.PrimaryTypeName, sw.ElapsedMilliseconds); + return result; + } + + public Task WaitForTerminalState() => + ClientChannelState + .TakeUntil(x => + x == CanonicalChannelState.Shutdown + || x == CanonicalChannelState.TransientFailure + || x == CanonicalChannelState.Idle) + .ToTask(); + + public async Task AddAssemblyReference(AssemblyReference reference) + { + _incomingAssemblyReferences.OnNext(reference); + + return new Outcome { Success = true }; + } + + public async Task AddManyAssemblyReferences(ManyAssemblyReferences references) + { + var sw = Stopwatch.StartNew(); + Logger.LogInformation("Begin loading {AssemblyCount} references", references.AssemblyReferences.Count); + + foreach (var asm in references.AssemblyReferences) + _incomingAssemblyReferences.OnNext(asm); + + Logger.LogInformation("{Elapsed:N0}ms to load {AssemblyCount} references", sw.ElapsedMilliseconds, references.AssemblyReferences.Count); + return new Outcome { Success = true }; + } + + public async Task ExecuteCommand(ExecuteCommandRequest request) + { + _incomingCommandRequests.OnNext(request); + + return new Outcome { Success = true }; + } + + public Task Heartbeat(HeartbeatRequest request) + => throw new NotImplementedException(); + + public void Dispose() + { + + } +} diff --git a/src/components/tbc.host/Extensions/AsyncStreamReaderExtensions.cs b/src/components/tbc.host/Extensions/AsyncStreamReaderExtensions.cs deleted file mode 100644 index 1b55725..0000000 --- a/src/components/tbc.host/Extensions/AsyncStreamReaderExtensions.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using Grpc.Core; - -namespace Tbc.Host.Extensions -{ - public static class AsyncStreamReaderExtensions - { - /// - /// Advances the stream reader to the next element in the sequence, returning the result asynchronously. - /// - /// The message type. - /// The stream reader. - /// - /// Task containing the result of the operation: true if the reader was successfully advanced - /// to the next element; false if the reader has passed the end of the sequence. - /// - public static Task MoveNext(this IAsyncStreamReader streamReader) - where T : class - { - if (streamReader == null) - { - throw new ArgumentNullException(nameof(streamReader)); - } - - return streamReader.MoveNext(CancellationToken.None); - } - - /// - /// - /// - /// - /// - /// - /// - public async static IAsyncEnumerable ReadAllAsync( - this IAsyncStreamReader streamReader, - [EnumeratorCancellation] CancellationToken cancellationToken = default - ) - { - if (streamReader == null) - { - throw new System.ArgumentNullException(nameof(streamReader)); - } - - while (await streamReader.MoveNext(cancellationToken)) - { - yield return streamReader.Current; - } - } - - public async static IAsyncEnumerable ReadAllAsync( - this IAsyncStreamReader streamReader, Func selector, - [EnumeratorCancellation] CancellationToken cancellationToken = default - ) - { - if (streamReader == null) - { - throw new System.ArgumentNullException(nameof(streamReader)); - } - - while (await streamReader.MoveNext(cancellationToken)) - { - yield return selector(streamReader.Current); - } - } - - public static IAsyncEnumerable ToAsyncEnumerable(this IEnumerable enumerable) => - new SynchronousAsyncEnumerable(enumerable); - - private class SynchronousAsyncEnumerable : IAsyncEnumerable - { - private readonly IEnumerable _enumerable; - - public SynchronousAsyncEnumerable(IEnumerable enumerable) => - _enumerable = enumerable; - - public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => - new SynchronousAsyncEnumerator(_enumerable.GetEnumerator()); - } - - private class SynchronousAsyncEnumerator : IAsyncEnumerator - { - private readonly IEnumerator _enumerator; - - public T Current => _enumerator.Current; - - public SynchronousAsyncEnumerator(IEnumerator enumerator) => - _enumerator = enumerator; - - public ValueTask DisposeAsync() => - new ValueTask(Task.CompletedTask); - - public ValueTask MoveNextAsync() => - new ValueTask(Task.FromResult(_enumerator.MoveNext())); - } - - public static async Task ForEachAsync(this IAsyncEnumerable enumerable, Action action) - { - await foreach (var item in enumerable) - { - action(item); - } - } - } -} diff --git a/src/components/tbc.host/tbc.host.csproj b/src/components/tbc.host/tbc.host.csproj index 570d3ce..006351b 100644 --- a/src/components/tbc.host/tbc.host.csproj +++ b/src/components/tbc.host/tbc.host.csproj @@ -3,32 +3,27 @@ netstandard2.1 Tbc.Host - preview + preview + true + true + embedded - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - + + + + + + + + + + - - - - - + diff --git a/src/components/tbc.target/AssemblyLoaderService.cs b/src/components/tbc.target/AssemblyLoaderService.cs deleted file mode 100644 index 5b1d2db..0000000 --- a/src/components/tbc.target/AssemblyLoaderService.cs +++ /dev/null @@ -1,194 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using Google.Protobuf; -using Grpc.Core; -using Tbc.Protocol; -using Tbc.Target.Interfaces; -using Tbc.Target.Requests; -using LoadDynamicAssemblyRequest = Tbc.Protocol.LoadDynamicAssemblyRequest; -using Outcome = Tbc.Protocol.Outcome; - -namespace Tbc.Target -{ - public class AssemblyLoaderService : Protocol.AssemblyLoader.AssemblyLoaderBase - { - private IReloadManager _reloadManager; - private readonly Action _log; - - public AssemblyLoaderService(IReloadManager reloadManager, Action log) - { - _reloadManager = reloadManager; - _log = log; - - OnReloadManager(reloadManager); - } - - private void OnReloadManager(IReloadManager reloadManager) - { - _reloadManager = reloadManager; - - if (reloadManager is INotifyReplacement inr) - inr.NotifyReplacement = rm => - { - inr.NotifyReplacement = null; - - _log($"Replacing reload manager: {rm}"); - - OnReloadManager(rm); - }; - - if (reloadManager is IRequestHostCommand irehc) - irehc.RequestHostCommand += req - => RequestHostCommandImpl?.Invoke(req); - } - - public override async Task LoadAssembly(LoadDynamicAssemblyRequest request, ServerCallContext context) - { - try - { - var peBytes = request.PeBytes.ToByteArray(); - var pdbBytes = request.PdbBytes.ToByteArray(); - - var asm = Assembly.Load(peBytes, pdbBytes); - var primaryType = !String.IsNullOrWhiteSpace(request.PrimaryTypeName) - ? asm.GetTypes().FirstOrDefault(x => x.Name.EndsWith(request.PrimaryTypeName)) - : null; - - var req = new ProcessNewAssemblyRequest - { - Assembly = asm, - PrimaryType = primaryType - }; - - var canonicalOutcome = await _reloadManager.ProcessNewAssembly(req); - - return new Outcome - { - Success = canonicalOutcome.Success, - Messages = { new Message - { - Message_ = canonicalOutcome.Messages.FirstOrDefault()?.Message ?? "no message" - } }, - }; - } - catch (Exception ex) - { - _log($"An error occurred when attempting to load assembly: {request.AssemblyName}"); - - return new Outcome - { - Success = false, - Messages = {new Message {Message_ = ex.ToString()}} - }; - } - } - - public override async Task Exec(ExecuteCommandRequest request, ServerCallContext context) - { - try - { - var canonicalOutcome = - await _reloadManager.ExecuteCommand(new CommandRequest(request.Command, request.Args.ToList())); - - return new Outcome - { - Success = canonicalOutcome.Success, - Messages = { new Message - { - Message_ = canonicalOutcome.Messages.FirstOrDefault()?.Message ?? "no message" - } }, - }; - } - catch (Exception ex) - { - _log($"An error occurred when attempting to exec command: {request.Command} with args {request.Args}"); - - return new Outcome - { - Success = false, - Messages = {new Message {Message_ = ex.ToString()}} - }; - } - } - - public override async Task SynchronizeDependencies(CachedAssemblyState cachedAssemblyState, IServerStreamWriter responseStream, ServerCallContext context) - { - var sw = Stopwatch.StartNew(); - - var cache = ToDictionary(cachedAssemblyState); - var currentAssemblies = AppDomain.CurrentDomain.GetAssemblies(); - - foreach (var asm in currentAssemblies) - await WriteIfNecessary(asm, responseStream); - - _log($"Finished sending current assemblies in {sw.Elapsed}"); - - AppDomain.CurrentDomain.AssemblyLoad += async (sender, args) - => await WriteIfNecessary(args.LoadedAssembly, responseStream); - - await new TaskCompletionSource(context.CancellationToken).Task; - } - - public override async Task RequestCommand(Unit request, IServerStreamWriter responseStream, ServerCallContext context) - { - RequestHostCommandImpl = async cmd => - { - var req = new ExecuteCommandRequest { Command = cmd.Command }; - - // woof - req.Args.AddRange(cmd.Args); - - await responseStream.WriteAsync(req); - }; - - await new TaskCompletionSource(context.CancellationToken).Task; - } - - private Func RequestHostCommandImpl; - - private Dictionary ToDictionary(CachedAssemblyState cachedAssemblyState) => - cachedAssemblyState.CachedAssemblies?.ToDictionary( - x => x.AssemblyName, - x => x.ModificationTime); - - private async Task WriteIfNecessary(Assembly asm, IServerStreamWriter responseStream) - { - _log($"Send {asm.FullName}?"); - var sw = Stopwatch.StartNew(); - - if (asm.IsDynamic || String.IsNullOrWhiteSpace(asm.Location)) - return; - - if (asm.FullName.StartsWith("r2,")) - return; - - try - { - await using var fs = new FileStream(asm.Location, FileMode.Open, FileAccess.Read); - - await responseStream.WriteAsync(new AssemblyReference - { - AssemblyName = asm.FullName, - AssemblyLocation = asm.Location, - ModificationTime = - (ulong) new DateTimeOffset(new FileInfo(asm.Location).LastWriteTimeUtc, TimeSpan.Zero) - .ToUnixTimeSeconds(), - - PeBytes = await ByteString.FromStreamAsync(fs), - }); - - _log($"Sent {asm.FullName} - {sw.Elapsed}"); - } - - catch (Exception ex) - { - _log(ex.ToString()); - } - } - } -} diff --git a/src/components/tbc.target/Extensions/OutcomeExtensions.cs b/src/components/tbc.target/Extensions/OutcomeExtensions.cs deleted file mode 100644 index cda6440..0000000 --- a/src/components/tbc.target/Extensions/OutcomeExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Linq; -using Tbc.Protocol; - -namespace Tbc.Target.Extensions -{ - public class TbcOutcome - { - public static Outcome Success() => - new Outcome {Success = true}; - - public static Outcome Failure(params string[] why) => - new Outcome - { - Success = false, - Messages = {why.Select(x => new Message {Message_ = x})} - }; - } -} \ No newline at end of file diff --git a/src/components/tbc.target/Implementation/AssemblyLoaderService.cs b/src/components/tbc.target/Implementation/AssemblyLoaderService.cs new file mode 100644 index 0000000..565ec46 --- /dev/null +++ b/src/components/tbc.target/Implementation/AssemblyLoaderService.cs @@ -0,0 +1,170 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Nito.AsyncEx; +using Tbc.Core.Apis; +using Tbc.Core.Models; +using Tbc.Core.Socket.Abstractions; +using Tbc.Target.Interfaces; +using Tbc.Target.Requests; +using AssemblyReference = Tbc.Core.Models.AssemblyReference; +using CachedAssemblyState = Tbc.Core.Models.CachedAssemblyState; +using ExecuteCommandRequest = Tbc.Core.Models.ExecuteCommandRequest; +using LoadDynamicAssemblyRequest = Tbc.Core.Models.LoadDynamicAssemblyRequest; +using Outcome = Tbc.Core.Models.Outcome; + +namespace Tbc.Target.Implementation; + +public class AssemblyLoaderService : ITbcTarget, ISendToRemote +{ + private IReloadManager _reloadManager; + private readonly Action _log; + public IRemoteEndpoint? Remote { get; set; } + + public AssemblyLoaderService(IReloadManager reloadManager, Action log) + { + _reloadManager = reloadManager; + _log = log; + + OnReloadManager(reloadManager); + } + + public async Task LoadAssembly(LoadDynamicAssemblyRequest request) + { + try + { + var peBytes = request.PeBytes; + var pdbBytes = request.PdbBytes; + + var asm = Assembly.Load(peBytes, pdbBytes); + var primaryType = !String.IsNullOrWhiteSpace(request.PrimaryTypeName) + ? asm.GetTypes().FirstOrDefault(x => x.Name.EndsWith(request.PrimaryTypeName)) + : null; + + var req = new ProcessNewAssemblyRequest + { + Assembly = asm, + PrimaryType = primaryType + }; + + return await _reloadManager.ProcessNewAssembly(req); + } + catch (Exception ex) + { + _log($"An error occurred when attempting to load assembly: {request.AssemblyName}"); + _log(ex.ToString()); + + return new Outcome + { + Success = false, + Messages = { new OutcomeMessage { Message = ex.ToString() } } + }; + } + } + + public async Task Exec(ExecuteCommandRequest request) + { + try + { + return await _reloadManager.ExecuteCommand(new CommandRequest(request.Command, request.Args.ToList())); + } + catch (Exception ex) + { + _log($"An error occurred when attempting to exec command: {request.Command} with args {request.Args}"); + + return new Outcome + { + Success = false, + Messages = { new OutcomeMessage { Message = ex.ToString() } } + }; + } + } + + public async Task SynchronizeDependencies(CachedAssemblyState cachedAssemblyState) + { + Task.Delay(TimeSpan.FromSeconds(.5)) + .ContinueWith(async _ => + { + var sw = Stopwatch.StartNew(); + var currentAssemblies = AppDomain.CurrentDomain.GetAssemblies().ToList(); + + foreach (var asm in currentAssemblies) + await WriteIfNecessary(asm); + + _log($"Finished sending current assemblies in {sw.Elapsed}"); + + AppDomain.CurrentDomain.AssemblyLoad += async (sender, args) + => await WriteIfNecessary(args.LoadedAssembly); + }); + + return new Outcome { Success = true }; + } + + public async Task RequestCommand(CommandRequest req) + => await Remote.SendRequest(new ExecuteCommandRequest + { + Command = req.Command, + Args = req.Args.ToList() + }); + + private readonly AsyncLock _mutex = new(); + + private async Task WriteIfNecessary(Assembly asm) + { + using (await _mutex.LockAsync()) + { + // It's safe to await while the lock is held + _log($"Send {asm.FullName}?"); + var sw = Stopwatch.StartNew(); + + if (asm.IsDynamic || String.IsNullOrWhiteSpace(asm.Location)) + return; + + if (asm.FullName.StartsWith("r2,")) + return; + + try + { + var @ref = new AssemblyReference + { + AssemblyName = asm.FullName, + AssemblyLocation = asm.Location, + ModificationTime = new DateTimeOffset(new FileInfo(asm.Location).LastWriteTimeUtc, TimeSpan.Zero), + PeBytes = await File.ReadAllBytesAsync(asm.Location) + }; + + _log($"Will send {asm.FullName} - {sw.Elapsed}"); + + await Remote.SendRequest(@ref); + + _log($"Sent {asm.FullName} - {sw.Elapsed}"); + } + catch (Exception ex) + { + Console.WriteLine(ex); + _log(ex.ToString()); + } + } + } + + private void OnReloadManager(IReloadManager reloadManager) + { + _reloadManager = reloadManager; + + if (reloadManager is INotifyReplacement inr) + inr.NotifyReplacement = rm => + { + inr.NotifyReplacement = null; + + _log($"Replacing reload manager: {rm}"); + + OnReloadManager(rm); + }; + + if (reloadManager is IRequestHostCommand irehc) + irehc.RequestHostCommand += RequestCommand; + } +} diff --git a/src/components/tbc.target/TargetServer.cs b/src/components/tbc.target/TargetServer.cs index 14856b3..699a52a 100644 --- a/src/components/tbc.target/TargetServer.cs +++ b/src/components/tbc.target/TargetServer.cs @@ -1,9 +1,11 @@ using System; -using System.Diagnostics; +using System.Net; +using System.Net.Sockets; using System.Threading.Tasks; -using Grpc.Core; -using Tbc.Protocol; +using Tbc.Core.Apis; +using Tbc.Core.Socket; using Tbc.Target.Config; +using Tbc.Target.Implementation; using Tbc.Target.Interfaces; namespace Tbc.Target @@ -14,7 +16,7 @@ public class TargetServer public TargetServer() : this(TargetConfiguration.Default()) { - + } public TargetServer(TargetConfiguration configuration) @@ -24,20 +26,34 @@ public TargetServer(TargetConfiguration configuration) public async Task Run(IReloadManager reloadManager, Action log = null) { - var server = new Server(new [] + log ??= Console.WriteLine; + + var listener = new TcpListener(IPAddress.Any, Configuration.ListenPort); + var handler = new AssemblyLoaderService(reloadManager, log); + + listener.Start(); + + Task.Run(async () => { - new ChannelOption(ChannelOptions.MaxReceiveMessageLength, 838860800), - new ChannelOption(ChannelOptions.MaxSendMessageLength, 838860800) + while (true) + { + var connection = await listener.AcceptTcpClientAsync(); + + try + { + var socketServer = new SocketServer(connection, handler, "client", log); + await socketServer.Run(); + } + catch (Exception ex) + { + log($"socket loop iteration faulted: {ex.ToString()}"); + } + } }) + .ContinueWith(t => { - Services = { Protocol.AssemblyLoader.BindService(new AssemblyLoaderService( - reloadManager, - log ?? (s => Debug.WriteLine(s)) - )) }, - Ports = { new ServerPort("0.0.0.0", Configuration.ListenPort, ServerCredentials.Insecure) } - }; - - server.Start(); + log($"socket loop faulted: {t.Exception}"); + }); } } } diff --git a/src/components/tbc.target/tbc.target.csproj b/src/components/tbc.target/tbc.target.csproj index a2c8c5c..c352299 100644 --- a/src/components/tbc.target/tbc.target.csproj +++ b/src/components/tbc.target/tbc.target.csproj @@ -6,23 +6,22 @@ true 1.0.14 latest + true + true + embedded + enable - + - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - + - + + + + + - + diff --git a/src/heads/tbc.host.console/Program.cs b/src/heads/tbc.host.console/Program.cs index 728c765..57a4ace 100644 --- a/src/heads/tbc.host.console/Program.cs +++ b/src/heads/tbc.host.console/Program.cs @@ -11,7 +11,6 @@ using Tbc.Host.Components.Abstractions; using Tbc.Host.Components.FileEnvironment.Models; using Tbc.Host.Components.TargetClient; -using Tbc.Host.Components.TargetClient.GrpcCore; using tbc.host.console.ConsoleHost; namespace tbc.host.console @@ -38,8 +37,7 @@ static async Task Main(string[] args) Configurator .ConfigureServices(configuration, withAssemblies: typeof(Program).Assembly, configure: c => c.RegisterDelegate>( - sp => client - => sp.GetRequiredService>()(client))) + sp => client => sp.GetRequiredService>()(client))) .Resolve() .Run(); } diff --git a/src/tbc.sln b/src/tbc.sln index cf71594..60e12b7 100644 --- a/src/tbc.sln +++ b/src/tbc.sln @@ -18,6 +18,10 @@ ProjectSection(SolutionItems) = preProject ..\build\build.yml = ..\build\build.yml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestSocketTarget", "test\TestSocketTarget\TestSocketTarget.csproj", "{0408CA13-C929-4D09-AF25-76BCE9BDB88D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{EF1C6677-3CBD-4344-A3BC-41653447D4DE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -40,11 +44,16 @@ Global {81CF0DFE-7625-4790-B571-E8CC27EB65BA}.Debug|Any CPU.Build.0 = Debug|Any CPU {81CF0DFE-7625-4790-B571-E8CC27EB65BA}.Release|Any CPU.ActiveCfg = Release|Any CPU {81CF0DFE-7625-4790-B571-E8CC27EB65BA}.Release|Any CPU.Build.0 = Release|Any CPU + {0408CA13-C929-4D09-AF25-76BCE9BDB88D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0408CA13-C929-4D09-AF25-76BCE9BDB88D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0408CA13-C929-4D09-AF25-76BCE9BDB88D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0408CA13-C929-4D09-AF25-76BCE9BDB88D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {95204880-130F-407E-8025-46A9B1CEFB15} = {F73C79A7-D67C-4F5F-9C36-7718CB18329D} {1BA46910-5E17-4F4C-9D11-DFCB3BAD52BA} = {F73C79A7-D67C-4F5F-9C36-7718CB18329D} {10DE80CE-9B3F-4A5A-9A8B-1E54D6F1721B} = {F73C79A7-D67C-4F5F-9C36-7718CB18329D} {81CF0DFE-7625-4790-B571-E8CC27EB65BA} = {A64A1E7B-195F-4570-BA85-46C7EEC27837} + {0408CA13-C929-4D09-AF25-76BCE9BDB88D} = {EF1C6677-3CBD-4344-A3BC-41653447D4DE} EndGlobalSection EndGlobal diff --git a/src/test/TestSocketTarget/Program.cs b/src/test/TestSocketTarget/Program.cs new file mode 100644 index 0000000..bc12eca --- /dev/null +++ b/src/test/TestSocketTarget/Program.cs @@ -0,0 +1,78 @@ +using System.Net; +using System.Net.Sockets; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Tbc.Core.Apis; +using Tbc.Core.Models; +using Tbc.Core.Socket; +using Tbc.Host.Components.FileEnvironment.Models; +using Tbc.Host.Components.TargetClient; +using Tbc.Target; +using Tbc.Target.Implementation; +using Tbc.Target.Requests; +using TestSocketTarget; + +var rm = new MyReloadManager(); +var listener = new TcpListener(IPAddress.Any, 0); +var handler = new AssemblyLoaderService(rm, Console.WriteLine); +listener.Start(); + +Task.Run(async () => +{ + while (true) + { + try + { + var connection = await listener.AcceptTcpClientAsync(); + var socketServer = new SocketServer(connection, handler, "client", Console.WriteLine); + await socketServer.Run(); + } + catch (Exception ex) + { + Console.WriteLine($"socket loop iteration faulted: {ex.ToString()}"); + await Task.Delay(TimeSpan.FromSeconds(.5)); + } + } +}); + +var ep = ((IPEndPoint)listener.LocalEndpoint); + +var serviceCollection = new ServiceCollection(); +serviceCollection.AddLogging(configure => configure.AddConsole()); +var serviceProvider = serviceCollection.BuildServiceProvider(); + +var logger = serviceProvider.GetRequiredService>(); + +var host = new SocketTargetClient( + logger, + new RemoteClient { Address = "localhost", Port = ep.Port }); + +host.ClientChannelState.Subscribe(x => Console.WriteLine(x)); + +Console.WriteLine("Waiting for connection"); +await host.WaitForConnection().ConfigureAwait(false); + +Console.WriteLine("Got connection"); + +Task.Run(async () => host.AssemblyReferences()); + +Console.ReadLine(); + + +namespace TestSocketTarget +{ + public class MyReloadManager : ReloadManagerBase + { + public override Task ProcessNewAssembly(ProcessNewAssemblyRequest req) + { + Console.WriteLine(req); + return Task.FromResult(new Outcome()); + } + + public override Task ExecuteCommand(CommandRequest req) + { + Console.WriteLine(req); + return Task.FromResult(new Outcome()); + } + } +} diff --git a/src/test/TestSocketTarget/TestSocketTarget.csproj b/src/test/TestSocketTarget/TestSocketTarget.csproj new file mode 100644 index 0000000..35475ed --- /dev/null +++ b/src/test/TestSocketTarget/TestSocketTarget.csproj @@ -0,0 +1,20 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + +