make ScratchLinkApp to take the place of MAUI app

Note that this builds and runs, but doesn't work: the Xamarin.Mac
implementation of `System.Net` always sets `IsWebSocketRequest` to
`false` and doesn't actually support WebSockets connections as a server.
This commit is contained in:
Christopher Willis-Ford 2022-07-07 18:25:47 -07:00
parent 2c9e4c600a
commit 8be1d8b021
7 changed files with 166 additions and 99 deletions

View file

@ -11,6 +11,7 @@ using System.Net.WebSockets;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using ScratchLink.Extensions; using ScratchLink.Extensions;
using ScratchLink.JsonRpc; using ScratchLink.JsonRpc;
@ -28,7 +29,7 @@ internal abstract class BLESession<TUUID> : Session
public BLESession(WebSocketContext context) public BLESession(WebSocketContext context)
: base(context) : base(context)
{ {
this.GattHelpers = IPlatformApplication.Current.Services.GetService<GattHelpers<TUUID>>(); this.GattHelpers = ScratchLinkApp.Current.Services.GetService<GattHelpers<TUUID>>();
this.AllowedServices = new (); this.AllowedServices = new ();
this.Handlers["discover"] = this.HandleDiscover; this.Handlers["discover"] = this.HandleDiscover;
this.Handlers["connect"] = this.HandleConnect; this.Handlers["connect"] = this.HandleConnect;

View file

@ -0,0 +1,150 @@
// <copyright file="ScratchLinkApp.cs" company="Scratch Foundation">
// Copyright (c) Scratch Foundation. All rights reserved.
// </copyright>
namespace ScratchLink;
using System;
using System.Diagnostics;
using System.Net;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;
using ScratchLink.BLE;
/// <summary>
/// Main entry point for Scratch Link and central service provider for dependency injection.
/// </summary>
public class ScratchLinkApp
{
private const int WebSocketPort = 20111;
private readonly SessionManager sessionManager;
private readonly WebSocketListener webSocketListener;
private ScratchLinkApp(IServiceProvider platformServicesProvider)
{
this.Services = platformServicesProvider;
if (Current != null)
{
throw new InvalidOperationException("Attempt to create a second app instance");
}
Current = this;
this.sessionManager = this.Services.GetService<SessionManager>();
this.webSocketListener = new ()
{
OnWebSocketConnection = (webSocketContext) =>
{
this.sessionManager.ClientDidConnect(webSocketContext);
},
OnOtherConnection = (context) =>
{
context.Response.Headers.Clear();
context.Response.SendChunked = false;
context.Response.StatusCode = 426; // Upgrade Required
context.Response.OutputStream.Write(Encoding.UTF8.GetBytes("WebSockets required"));
context.Response.OutputStream.Close();
},
};
}
/// <summary>
/// Gets the current app instance.
/// </summary>
public static ScratchLinkApp Current { get; private set; }
/// <summary>
/// Gets the platform-specific services provider.
/// This provides access to services like the session manager or GATT helpers.
/// </summary>
public IServiceProvider Services { get; private set; }
/// <summary>
/// Run the app.
/// </summary>
public void Run()
{
if (!HttpListener.IsSupported)
{
// TODO: pop up an error message
return;
}
this.webSocketListener.Start(new[]
{
string.Format("http://127.0.0.1:{0}/", WebSocketPort),
string.Format("http://localhost:{0}/", WebSocketPort),
});
}
private void HandleSessionDebug(WebSocketContext context)
{
var origin = context.Headers.Get("origin");
var socket = context.WebSocket;
Debug.Print("New connection");
socket.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None);
}
/// <summary>
/// Builds a Scratch Link app instance.
/// Fills the role of the .NET generic host or <c>MauiAppBuilder</c>.
/// </summary>
public class Builder
{
private string[] arguments;
private Type sessionManagerType;
private Type gattHelpersBaseType;
private Type gattHelpersType;
/// <summary>
/// Sets the arguments which will be passed to the app host.
/// Must be called before Build().
/// </summary>
/// <param name="arguments">Command line arguments from app invocation.</param>
public void SetArguments(string[] arguments)
{
this.arguments = arguments;
}
/// <summary>
/// Sets the type which will be used to build the platform-specific session manager.
/// </summary>
/// <typeparam name="TSessionManager">The platform-specific session manager type.</typeparam>
internal void SetSessionManager<TSessionManager>()
where TSessionManager : SessionManager
{
this.sessionManagerType = typeof(TSessionManager);
}
/// <summary>
/// Sets the types which will be used to build the BLE GATT helpers.
/// </summary>
/// <typeparam name="TGattHelpers">The platform-specific GATT helpers type.</typeparam>
/// <typeparam name="TUUID">The platform-specific type for BLE UUID values.</typeparam>
internal void SetGattHelpers<TGattHelpers, TUUID>()
where TGattHelpers : GattHelpers<TUUID>
{
this.gattHelpersBaseType = typeof(GattHelpers<TUUID>);
this.gattHelpersType = typeof(TGattHelpers);
}
/// <summary>
/// Builds a Scratch Link app host.
/// </summary>
/// <returns>A new Scratch Link app host.</returns>
internal ScratchLinkApp Build()
{
var serviceCollection = new ServiceCollection();
var serviceProviderOptions = new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true };
var servicesProvider = new DefaultServiceProviderFactory(serviceProviderOptions)
.CreateBuilder(serviceCollection)
.AddSingleton(typeof(SessionManager), this.sessionManagerType)
.AddSingleton(this.gattHelpersBaseType, this.gattHelpersType)
.BuildServiceProvider();
return new ScratchLinkApp(servicesProvider);
}
}
}

View file

@ -27,5 +27,6 @@
<Compile Include="$(MSBuildThisFileDirectory)Session.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Session.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SessionManager.cs" /> <Compile Include="$(MSBuildThisFileDirectory)SessionManager.cs" />
<Compile Include="$(MSBuildThisFileDirectory)WebSocketListener.cs" /> <Compile Include="$(MSBuildThisFileDirectory)WebSocketListener.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ScratchLinkApp.cs" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -5,7 +5,9 @@
namespace ScratchLink.Mac; namespace ScratchLink.Mac;
using AppKit; using AppKit;
using CoreBluetooth;
using Foundation; using Foundation;
using ScratchLink.Mac.BLE;
/// <summary> /// <summary>
/// Scratch Link's implementation of the NSApplicationDelegate protocol. /// Scratch Link's implementation of the NSApplicationDelegate protocol.
@ -19,7 +21,14 @@ public class AppDelegate : NSApplicationDelegate
/// <param name="notification">A notification named <c>didFinishLaunchingNotification</c>.</param> /// <param name="notification">A notification named <c>didFinishLaunchingNotification</c>.</param>
public override void DidFinishLaunching(NSNotification notification) public override void DidFinishLaunching(NSNotification notification)
{ {
// Insert code here to initialize your application var appBuilder = new ScratchLinkApp.Builder();
appBuilder.SetArguments(new NSProcessInfo().Arguments);
appBuilder.SetSessionManager<MacSessionManager>();
appBuilder.SetGattHelpers<MacGattHelpers, CBUUID>();
var app = appBuilder.Build();
app.Run();
} }
/// <summary> /// <summary>

View file

@ -105,6 +105,9 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<AdditionalFiles Include="$(SolutionDir)stylecop.json" /> <AdditionalFiles Include="$(SolutionDir)stylecop.json" />
<PackageReference Include="Fleck">
<Version>1.2.0</Version>
</PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="System.Text.Json"> <PackageReference Include="System.Text.Json">

View file

@ -1,25 +0,0 @@
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:ScratchLink"
x:Class="ScratchLink.ScratchLinkApp">
<Application.Resources>
<ResourceDictionary>
<Color x:Key="PrimaryColor">#512bdf</Color>
<Color x:Key="SecondaryColor">White</Color>
<Style TargetType="Label">
<Setter Property="TextColor" Value="{DynamicResource PrimaryColor}" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
</Style>
<Style TargetType="Button">
<Setter Property="TextColor" Value="{DynamicResource SecondaryColor}" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="BackgroundColor" Value="{DynamicResource PrimaryColor}" />
<Setter Property="Padding" Value="14,10" />
</Style>
</ResourceDictionary>
</Application.Resources>
</Application>

View file

@ -1,72 +0,0 @@
// <copyright file="ScratchLinkApp.xaml.cs" company="Scratch Foundation">
// Copyright (c) Scratch Foundation. All rights reserved.
// </copyright>
namespace ScratchLink;
using ScratchLink.Resources.Strings;
using System.Net;
using System.Net.WebSockets;
/// <summary>
/// The <see cref="ScratchLinkApp"/> class contains the cross-platform entry point for the application.
/// </summary>
public partial class ScratchLinkApp : Application
{
private const int WebSocketPort = 20111;
private readonly SessionManager sessionManager;
private readonly WebSocketListener webSocketListener;
/// <summary>
/// Initializes a new instance of the <see cref="ScratchLinkApp"/> class.
/// This is the cross-platform entry point.
/// </summary>
public ScratchLinkApp()
{
this.InitializeComponent();
this.MainPage = new MainPage();
if (!HttpListener.IsSupported)
{
// TODO: this doesn't work right
this.MainPage.DisplayAlert("Error", AppResource.HTTP_Listener_not_supported, "Quit").ContinueWith(task =>
{
this.Quit();
});
return;
}
this.sessionManager = IPlatformApplication.Current.Services.GetService<SessionManager>();
this.webSocketListener = new ()
{
OnWebSocketConnection = (webSocketContext) =>
{
this.sessionManager.ClientDidConnect(webSocketContext);
},
OnOtherConnection = (context) =>
{
throw new NotImplementedException();
},
};
this.webSocketListener.Start(new[]
{
string.Format("http://127.0.0.1:{0}/", WebSocketPort),
string.Format("http://localhost:{0}/", WebSocketPort),
});
}
private void HandleSessionDebug(WebSocketContext context)
{
var origin = context.Headers.Get("origin");
var socket = context.WebSocket;
this.Dispatcher.Dispatch(() =>
{
this.MainPage.DisplayAlert("New connection", string.Format("Path: {0}\nOrigin: {1}", context.RequestUri.AbsolutePath, origin), "OK");
});
socket.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None);
}
}