Skip to content

Commit

Permalink
feat: add socket transport, remove grpc transport
Browse files Browse the repository at this point in the history
  • Loading branch information
rdavisau committed Apr 13, 2022
1 parent bfec146 commit 127c72c
Show file tree
Hide file tree
Showing 38 changed files with 1,278 additions and 655 deletions.
4 changes: 2 additions & 2 deletions 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
Expand Down
17 changes: 9 additions & 8 deletions readme.md
Expand Up @@ -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

Expand Down Expand Up @@ -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.
43 changes: 43 additions & 0 deletions 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<Outcome> LoadAssembly(LoadDynamicAssemblyRequest request);

[Post("/eval")]
Task<Outcome> Exec(ExecuteCommandRequest request);

[Post("/synchronize-dependencies")]
Task<Outcome> SynchronizeDependencies(CachedAssemblyState cachedAssemblyState);
}

public interface ITbcHost
{
[Post("/add-assembly-reference")]
Task<Outcome> AddAssemblyReference(AssemblyReference reference);

[Post("/add-assembly-reference")]
Task<Outcome> AddManyAssemblyReferences(ManyAssemblyReferences references);

[Post("/execute-command")]
Task<Outcome> ExecuteCommand(ExecuteCommandRequest request);

[Post("/heartbeat")]
Task<Outcome> Heartbeat(HeartbeatRequest request);
}

public interface ITbcConnectable
{
[Post("/connect")]
Task<ConnectResponse> Connect(ConnectRequest req);
}

public interface ITbcConnectableTarget : ITbcTarget, ITbcConnectable { }

public interface ITbcProtocol : ITbcHost, ITbcTarget {}
128 changes: 128 additions & 0 deletions 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<THandler>
{
private readonly Action<string> _log;
private readonly HttpListener _listener;

public Dictionary<string, (Type ParameterType, Func<object, Task<object>>, Type ReturnType)>
_handlerOperations = new();

public HttpServer (int listenPort, THandler handler, Action<string>? 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<object> 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<PostAttribute>(), x))
.Where(x => x.Item1 != null)
.ToDictionary(x => x.Item1.Path, x => (x.x.GetParameters()[0].ParameterType,
new Func<object, Task<object>>(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}");
}
}

3 changes: 3 additions & 0 deletions src/components/tbc.core/IsExternalInit.cs
@@ -0,0 +1,3 @@
namespace System.Runtime.CompilerServices;

internal static class IsExternalInit {}

0 comments on commit 127c72c

Please sign in to comment.