Răsfoiți Sursa

Initial working version of remoting

Nikita Tsukanov 8 ani în urmă
părinte
comite
646db5b914

+ 2 - 1
samples/ControlCatalog/MainWindow.xaml.cs

@@ -6,11 +6,12 @@ namespace ControlCatalog
 {
     public class MainWindow : Window
     {
+        public static bool DebugMode = false;
         public MainWindow()
         {
             this.InitializeComponent();
             this.AttachDevTools();
-            Renderer.DrawDirtyRects = Renderer.DrawFps = true;
+            Renderer.DrawDirtyRects = Renderer.DrawFps = DebugMode;
         }
 
         private void InitializeComponent()

+ 42 - 1
samples/RemoteTest/Program.cs

@@ -1,4 +1,13 @@
 using System;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Remote;
+using Avalonia.Remote.Protocol;
+using Avalonia.Threading;
+using ControlCatalog;
 
 namespace RemoteTest
 {
@@ -6,7 +15,39 @@ namespace RemoteTest
     {
         static void Main(string[] args)
         {
-            Console.WriteLine("Hello World!");
+            AppBuilder.Configure<App>().UsePlatformDetect().SetupWithoutStarting();
+
+            var l = new TcpListener(IPAddress.Loopback, 0);
+            l.Start();
+            var port = ((IPEndPoint) l.LocalEndpoint).Port;
+            l.Stop();
+            
+            var transport = new BsonTcpTransport();
+            transport.Listen(IPAddress.Loopback, port, sc =>
+            {
+                Dispatcher.UIThread.InvokeAsync(() =>
+                {
+                    new RemoteServer(sc).Content = new MainView();
+                });
+            });
+
+            var cts = new CancellationTokenSource();
+            transport.Connect(IPAddress.Loopback, port).ContinueWith(t =>
+            {
+                Dispatcher.UIThread.InvokeAsync(() =>
+                {
+                    var window = new Window()
+                    {
+                        Content = new RemoteWidget(t.Result)
+                    };
+                    window.Closed += delegate { cts.Cancel(); };
+                    window.Show();
+                });
+            });
+            Dispatcher.UIThread.MainLoop(cts.Token);
+
+
+
         }
     }
 }

+ 1 - 1
samples/RemoteTest/RemoteTest.csproj

@@ -2,7 +2,7 @@
 
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFramework>netcoreapp2.0</TargetFramework>
+    <TargetFramework>netcoreapp1.1</TargetFramework>
   </PropertyGroup>
 
   <ItemGroup>

+ 1 - 0
src/Avalonia.Base/AvaloniaObject.cs

@@ -56,6 +56,7 @@ namespace Avalonia
         /// </summary>
         public AvaloniaObject()
         {
+            CheckAccess();
             foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegistered(this))
             {
                 object value = property.IsDirect ?

+ 7 - 6
src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs

@@ -6,12 +6,13 @@ using System.Threading.Tasks;
 using Avalonia.Input;
 using Avalonia.Input.Raw;
 using Avalonia.Platform;
+using Avalonia.Rendering;
 
 namespace Avalonia.Controls.Embedding.Offscreen
 {
-    abstract class OffscreenTopLevelImplBase : ITopLevelImpl
+    public abstract class OffscreenTopLevelImplBase : ITopLevelImpl
     {
-        private double _scaling;
+        private double _scaling = 1;
         private Size _clientSize;
         public IInputRoot InputRoot { get; private set; }
 
@@ -20,6 +21,8 @@ namespace Avalonia.Controls.Embedding.Offscreen
             //No-op
         }
 
+        public IRenderer CreateRenderer(IRenderRoot root) => new ImmediateRenderer(root);
+
         public abstract void Invalidate(Rect rect);
         public abstract IEnumerable<object> Surfaces { get; }
 
@@ -51,15 +54,13 @@ namespace Avalonia.Controls.Embedding.Offscreen
 
         public virtual Point PointToClient(Point point) => point;
 
-        public Point PointToScreen(Point point)
-        {
-            throw new NotImplementedException();
-        }
+        public virtual Point PointToScreen(Point point) => point;
 
         public virtual void SetCursor(IPlatformHandle cursor)
         {
         }
 
         public Action Closed { get; set; }
+        public abstract IMouseDevice MouseDevice { get; }
     }
 }

+ 22 - 4
src/Avalonia.Controls/Remote/RemoteServer.cs

@@ -3,19 +3,37 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
+using Avalonia.Controls.Embedding;
+using Avalonia.Controls.Remote.Server;
+using Avalonia.Platform;
 using Avalonia.Remote.Protocol;
 
 namespace Avalonia.Controls.Remote
 {
     public class RemoteServer
     {
-        private readonly IAvaloniaRemoteTransport _transport;
+        private EmbeddableControlRoot _topLevel;
 
-        public RemoteServer(IAvaloniaRemoteTransport transport)
+        class TopLevelImpl : RemoteServerTopLevelImpl, IEmbeddableWindowImpl
         {
-            _transport = transport;
+            public TopLevelImpl(IAvaloniaRemoteTransportConnection transport) : base(transport)
+            {
+            }
+
+            public event Action LostFocus;
+        }
+        
+        public RemoteServer(IAvaloniaRemoteTransportConnection transport)
+        {
+            _topLevel = new EmbeddableControlRoot(new TopLevelImpl(transport));
+            _topLevel.Prepare();
+            //TODO: Somehow react on closed connection?
         }
 
-        public object Content { get; set; }
+        public object Content
+        {
+            get => _topLevel.Content;
+            set => _topLevel.Content = value;
+        }
     }
 }

+ 79 - 0
src/Avalonia.Controls/Remote/RemoteWidget.cs

@@ -0,0 +1,79 @@
+using System;
+using System.Runtime.InteropServices;
+using Avalonia.Input;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using Avalonia.Remote.Protocol;
+using Avalonia.Remote.Protocol.Viewport;
+using Avalonia.Threading;
+using PixelFormat = Avalonia.Platform.PixelFormat;
+
+namespace Avalonia.Controls.Remote
+{
+    public class RemoteWidget : Control
+    {
+        private readonly IAvaloniaRemoteTransportConnection _connection;
+        private FrameMessage _lastFrame;
+        private WritableBitmap _bitmap;
+        public RemoteWidget(IAvaloniaRemoteTransportConnection connection)
+        {
+            _connection = connection;
+            _connection.OnMessage += msg => Dispatcher.UIThread.InvokeAsync(() => OnMessage(msg));
+            _connection.Send(new ClientSupportedPixelFormatsMessage
+            {
+                Formats = new[]
+                {
+                    Avalonia.Remote.Protocol.Viewport.PixelFormat.Bgra8888,
+                    Avalonia.Remote.Protocol.Viewport.PixelFormat.Rgba8888,
+                }
+            });
+        }
+
+        private void OnMessage(object msg)
+        {
+            if (msg is FrameMessage frame)
+            {
+                _connection.Send(new FrameReceivedMessage
+                {
+                    SequenceId = frame.SequenceId
+                });
+                _lastFrame = frame;
+                InvalidateVisual();
+            }
+            
+        }
+
+        protected override void ArrangeCore(Rect finalRect)
+        {
+            _connection.Send(new ClientViewportAllocatedMessage
+            {
+                Width = finalRect.Width,
+                Height = finalRect.Height,
+                DpiX = 96,
+                DpiY = 96 //TODO: Somehow detect the actual DPI
+            });
+            base.ArrangeCore(finalRect);
+        }
+
+        public override void Render(DrawingContext context)
+        {
+            if (_lastFrame != null)
+            {
+                var fmt = (PixelFormat) _lastFrame.Format;
+                if (_bitmap == null || _bitmap.PixelWidth != _lastFrame.Width ||
+                    _bitmap.PixelHeight != _lastFrame.Height)
+                    _bitmap = new WritableBitmap(_lastFrame.Width, _lastFrame.Height, fmt);
+                using (var l = _bitmap.Lock())
+                {
+                    var lineLen = (fmt == PixelFormat.Rgb565 ? 2 : 4) * _lastFrame.Width;
+                    for (var y = 0; y < _lastFrame.Height; y++)
+                        Marshal.Copy(_lastFrame.Data, y * _lastFrame.Stride,
+                            new IntPtr(l.Address.ToInt64() + l.RowBytes * y), lineLen);
+                }
+                context.DrawImage(_bitmap, 1, new Rect(0, 0, _bitmap.PixelWidth, _bitmap.PixelHeight),
+                    new Rect(Bounds.Size));
+            }
+            base.Render(context);
+        }
+    }
+}

+ 88 - 19
src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs

@@ -6,6 +6,8 @@ using System.Text;
 using System.Threading.Tasks;
 using Avalonia.Controls.Embedding.Offscreen;
 using Avalonia.Controls.Platform.Surfaces;
+using Avalonia.Input;
+using Avalonia.Layout;
 using Avalonia.Platform;
 using Avalonia.Remote.Protocol;
 using Avalonia.Remote.Protocol.Viewport;
@@ -15,17 +17,20 @@ using ProtocolPixelFormat = Avalonia.Remote.Protocol.Viewport.PixelFormat;
 
 namespace Avalonia.Controls.Remote.Server
 {
-    class RemoteServerTopLevelImpl : OffscreenTopLevelImplBase, IFramebufferPlatformSurface
+    public class RemoteServerTopLevelImpl : OffscreenTopLevelImplBase, IFramebufferPlatformSurface
     {
-        private readonly IAvaloniaRemoteTransport _transport;
+        private readonly IAvaloniaRemoteTransportConnection _transport;
         private LockedFramebuffer _framebuffer;
         private object _lock = new object();
-        private long _lastSentFrame;
+        private long _lastSentFrame = -1;
         private long _lastReceivedFrame = -1;
+        private long _nextFrameNumber = 1;
+        private ClientViewportAllocatedMessage _pendingAllocation;
         private bool _invalidated;
+        private Vector _dpi = new Vector(96, 96);
         private ProtocolPixelFormat[] _supportedFormats;
 
-        public RemoteServerTopLevelImpl(IAvaloniaRemoteTransport transport)
+        public RemoteServerTopLevelImpl(IAvaloniaRemoteTransportConnection transport)
         {
             _transport = transport;
             _transport.OnMessage += OnMessage;
@@ -35,24 +40,64 @@ namespace Avalonia.Controls.Remote.Server
         {
             lock (_lock)
             {
-                var lastFrame = obj as FrameReceivedMessage;
-                if (lastFrame != null)
+                if (obj is FrameReceivedMessage lastFrame)
                 {
                     lock (_lock)
                     {
                         _lastReceivedFrame = lastFrame.SequenceId;
                     }
-                    Dispatcher.UIThread.InvokeAsync(CheckNeedsRender);
+                    Dispatcher.UIThread.InvokeAsync(RenderIfNeeded);
+                }
+                if (obj is ClientSupportedPixelFormatsMessage supportedFormats)
+                {
+                    lock (_lock)
+                        _supportedFormats = supportedFormats.Formats;
+                    Dispatcher.UIThread.InvokeAsync(RenderIfNeeded);
+                }
+                if (obj is MeasureViewportMessage measure)
+                    Dispatcher.UIThread.InvokeAsync(() =>
+                    {
+                        var m = Measure(new Size(measure.Width, measure.Height));
+                        _transport.Send(new MeasureViewportMessage
+                        {
+                            Width = m.Width,
+                            Height = m.Height
+                        });
+                    });
+                if (obj is ClientViewportAllocatedMessage allocated)
+                {
+                    lock (_lock)
+                    {
+                        if (_pendingAllocation == null)
+                            Dispatcher.UIThread.InvokeAsync(() =>
+                            {
+                                ClientViewportAllocatedMessage allocation;
+                                lock (_lock)
+                                {
+                                    allocation = _pendingAllocation;
+                                    _pendingAllocation = null;
+                                }
+                                _dpi = new Vector(allocation.DpiX, allocation.DpiY);
+                                ClientSize = new Size(allocation.Width, allocation.Height);
+                                RenderIfNeeded();
+                            });
+
+                        _pendingAllocation = allocated;
+                    }
                 }
-                var supportedFormats = obj as ClientSupportedPixelFormatsMessage;
-                if (supportedFormats != null)
-                    _supportedFormats = supportedFormats.Formats;
             }
         }
 
+        protected virtual Size Measure(Size constaint)
+        {
+            var l = (ILayoutable) InputRoot;
+            l.Measure(constaint);
+            return l.DesiredSize;
+        }
+
         public override IEnumerable<object> Surfaces => new[] { this };
         
-        FrameMessage RenderFrame(int width, int height, Size dpi, ProtocolPixelFormat? format)
+        FrameMessage RenderFrame(int width, int height, ProtocolPixelFormat? format)
         {
             var fmt = format ?? ProtocolPixelFormat.Rgba8888;
             var bpp = fmt == ProtocolPixelFormat.Rgb565 ? 2 : 4;
@@ -60,7 +105,7 @@ namespace Avalonia.Controls.Remote.Server
             var handle = GCHandle.Alloc(data, GCHandleType.Pinned);
             try
             {
-                _framebuffer = new LockedFramebuffer(handle.AddrOfPinnedObject(), width, height, width * bpp, dpi, (PixelFormat)fmt,
+                _framebuffer = new LockedFramebuffer(handle.AddrOfPinnedObject(), width, height, width * bpp, _dpi, (PixelFormat)fmt,
                     null);
                 Paint?.Invoke(new Rect(0, 0, width, height));
             }
@@ -69,7 +114,14 @@ namespace Avalonia.Controls.Remote.Server
                 _framebuffer = null;
                 handle.Free();
             }
-            return new FrameMessage();
+            return new FrameMessage
+            {
+                Data = data,
+                Format = (ProtocolPixelFormat) format,
+                Width = width,
+                Height = height,
+                Stride = width * bpp,
+            };
         }
 
         public ILockedFramebuffer Lock()
@@ -79,23 +131,40 @@ namespace Avalonia.Controls.Remote.Server
             return _framebuffer;
         }
 
-        void CheckNeedsRender()
+        void RenderIfNeeded()
         {
-            ProtocolPixelFormat[] formats;
             lock (_lock)
             {
-                if (_lastReceivedFrame != _lastSentFrame && !_invalidated)
+                if (_lastReceivedFrame != _lastSentFrame || !_invalidated || _supportedFormats == null)
                     return;
-                formats = _supportedFormats;
+
             }
+            if (ClientSize.Width < 1 || ClientSize.Height < 1)
+                return;
+            var format = ProtocolPixelFormat.Rgba8888;
+            foreach(var fmt in _supportedFormats)
+                if (fmt <= ProtocolPixelFormat.MaxValue)
+                {
+                    format = fmt;
+                    break;
+                }
             
-            //var frame = RenderFrame()
+            var frame = RenderFrame((int) ClientSize.Width, (int) ClientSize.Height, format);
+            lock (_lock)
+            {
+                _lastSentFrame = _nextFrameNumber++;
+                frame.SequenceId = _lastSentFrame;
+                _invalidated = false;
+            }
+            _transport.Send(frame);
         }
 
         public override void Invalidate(Rect rect)
         {
             _invalidated = true;
-            Dispatcher.UIThread.InvokeAsync(CheckNeedsRender);
+            Dispatcher.UIThread.InvokeAsync(RenderIfNeeded);
         }
+
+        public override IMouseDevice MouseDevice { get; } = new MouseDevice();
     }
 }

+ 10 - 9
src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj

@@ -1,9 +1,10 @@
-<Project Sdk="Microsoft.NET.Sdk">
-    <PropertyGroup>
-        <TargetFramework>netstandard1.3</TargetFramework>
-        <DefineConstants>AVALONIA_REMOTE_PROTOCOL;$(DefineConstants)</DefineConstants>
-    </PropertyGroup>
-    <ItemGroup>
-        <Compile Include="..\Avalonia.Input\Key.cs" />
-    </ItemGroup>
-</Project>
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <TargetFramework>netstandard1.3</TargetFramework>
+    <DefineConstants>AVALONIA_REMOTE_PROTOCOL;$(DefineConstants)</DefineConstants>
+  </PropertyGroup>
+  <ItemGroup>
+    <PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
+    <Compile Include="..\Avalonia.Input\Key.cs" />
+  </ItemGroup>
+</Project>

+ 155 - 2
src/Avalonia.Remote.Protocol/BsonStreamTransport.cs

@@ -1,7 +1,160 @@
-namespace Avalonia.Remote.Protocol
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using Newtonsoft.Json;
+using System.Threading.Tasks;
+using Newtonsoft.Json.Bson;
+
+namespace Avalonia.Remote.Protocol
 {
-    public class BsonStreamTransport
+    public class BsonStreamTransportConnection : IAvaloniaRemoteTransportConnection
     {
+        private readonly IMessageTypeResolver _resolver;
+        private readonly Stream _inputStream;
+        private readonly Stream _outputStream;
+        private readonly Action _disposeCallback;
+        private readonly CancellationToken _cancel;
+        private readonly CancellationTokenSource _cancelSource;
+        private readonly MemoryStream _outputBlock = new MemoryStream();
+        private readonly object _lock = new object();
+        private bool _writeOperationPending;
+        private bool _readingAlreadyStarted;
+        private bool _writerIsBroken;
+        static readonly JsonSerializer Serializer = new JsonSerializer();        
+        private static readonly byte[] ZeroLength = new byte[4];
+
+        public BsonStreamTransportConnection(IMessageTypeResolver resolver, Stream inputStream, Stream outputStream, Action disposeCallback)
+        {
+            _resolver = resolver;
+            _inputStream = inputStream;
+            _outputStream = outputStream;
+            _disposeCallback = disposeCallback;
+            _cancelSource = new CancellationTokenSource();
+            _cancel = _cancelSource.Token;
+        }
+
+        public void Dispose()
+        {
+            _cancelSource.Cancel();
+            _disposeCallback?.Invoke();
+        }
         
+        public void StartReading()
+        {
+            lock (_lock)
+            {
+                if(_readingAlreadyStarted)
+                    throw new InvalidOperationException("Reading has already started");
+                _readingAlreadyStarted = true;
+                Task.Run(Reader, _cancel);
+            }
+        }
+
+        async Task ReadExact(byte[] buffer)
+        {
+            int read = 0;
+            while (read != buffer.Length)
+            {
+                var readNow = await _inputStream.ReadAsync(buffer, read, buffer.Length - read, _cancel)
+                    .ConfigureAwait(false);
+                if (readNow == 0)
+                    throw new EndOfStreamException();
+                read += readNow;
+            }
+        }
+
+        async Task Reader()
+        {
+            Task.Yield();
+            try
+            {
+                while (true)
+                {
+                    var infoBlock = new byte[20];
+                    await ReadExact(infoBlock).ConfigureAwait(false);
+                    var length = BitConverter.ToInt32(infoBlock, 0);
+                    var guidBytes = new byte[16];
+                    Buffer.BlockCopy(infoBlock, 4, guidBytes, 0, 16);
+                    var guid = new Guid(guidBytes);
+                    var buffer = new byte[length];
+                    await ReadExact(buffer).ConfigureAwait(false);
+                    if (Environment.GetEnvironmentVariable("WTF") == "WTF")
+                    {
+
+                        using (var f = System.IO.File.Create("/tmp/wtf2.bin"))
+                        {
+                            f.Write(infoBlock, 0, infoBlock.Length);
+                            f.Write(buffer, 0, buffer.Length);
+                        }
+                    }
+                    var message = Serializer.Deserialize(new BsonReader(new MemoryStream(buffer)), _resolver.GetByGuid(guid));
+                    OnMessage?.Invoke(message);
+                }
+            }
+            catch (Exception e)
+            {
+                FireException(e);
+            }
+        }
+
+
+        public async Task Send(object data)
+        {
+            lock (_lock)
+            {
+                if(_writerIsBroken) //Ignore further calls, since there is no point of writing to "broken" stream
+                    return;
+                if (_writeOperationPending)
+                    throw new InvalidOperationException("Previous send operation was not finished");
+                _writeOperationPending = true;
+            }
+            try
+            {
+                var guid = _resolver.GetGuid(data.GetType()).ToByteArray();
+                _outputBlock.Seek(0, SeekOrigin.Begin);
+                _outputBlock.SetLength(0);
+                _outputBlock.Write(ZeroLength, 0, 4);
+                _outputBlock.Write(guid, 0, guid.Length);
+                var writer = new BsonWriter(_outputBlock);
+                Serializer.Serialize(writer, data);
+                _outputBlock.Seek(0, SeekOrigin.Begin);
+                var length = BitConverter.GetBytes((int)_outputBlock.Length - 20);
+                _outputBlock.Write(length, 0, length.Length);
+                _outputBlock.Seek(0, SeekOrigin.Begin);
+
+                try
+                {
+                    await _outputBlock.CopyToAsync(_outputStream, 0x1000, _cancel).ConfigureAwait(false);
+                }
+                catch (Exception e) //We are only catching "network"-related exceptions here
+                {
+                    lock (_lock)
+                    {
+                        _writerIsBroken = true;
+                    }
+                    FireException(e);
+                }
+            }
+            finally
+            {
+                lock (_lock)
+                {
+                    _writeOperationPending = false;
+                }
+            }
+        }
+
+        void FireException(Exception e)
+        {
+            var cancel = e as OperationCanceledException;
+            if (cancel?.CancellationToken == _cancel)
+                return;
+            OnException?.Invoke(e);
+        }
+
+
+        public event Action<object> OnMessage;
+        public event Action<Exception> OnException;
     }
 }

+ 27 - 0
src/Avalonia.Remote.Protocol/BsonTcpTransport.cs

@@ -0,0 +1,27 @@
+using System;
+using System.IO;
+using System.Reflection;
+
+namespace Avalonia.Remote.Protocol
+{
+    public class BsonTcpTransport : TcpTransportBase
+    {
+        public BsonTcpTransport(IMessageTypeResolver resolver) : base(resolver)
+        {
+        }
+
+        public BsonTcpTransport() : this(new DefaultMessageTypeResolver(typeof(BsonTcpTransport).GetTypeInfo().Assembly))
+        {
+            
+        }
+
+        protected override IAvaloniaRemoteTransportConnection CreateTransport(IMessageTypeResolver resolver,
+            Stream stream, Action dispose)
+        {
+            var t = new BsonStreamTransportConnection(resolver, stream, stream, dispose);
+            var wrap = new TransportConnectionWrapper(t);
+            t.StartReading();
+            return wrap;
+        }
+    }
+}

+ 72 - 0
src/Avalonia.Remote.Protocol/EventStash.cs

@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Generic;
+
+namespace Avalonia.Remote.Protocol
+{
+    public class EventStash<T>
+    {
+        private readonly Action<Exception> _exceptionHandler;
+        private List<T> _stash;
+        private Action<T> _delegate;
+
+        public EventStash(Action<Exception> exceptionHandler = null)
+        {
+            _exceptionHandler = exceptionHandler;
+        }
+        
+        public void Add(Action<T> handler)
+        {
+            List<T> stash;
+            lock (this)
+            {
+                var needsReplay = _delegate == null;
+                _delegate += handler;
+                if(!needsReplay)
+                    return;
+
+                lock (this)
+                {
+                    stash = _stash;
+                    if(_stash == null)
+                        return;
+                    _stash = null;
+                }
+            }
+            foreach (var m in stash)
+            {
+                if (_exceptionHandler != null)
+                    try
+                    {
+                        _delegate?.Invoke(m);
+                    }
+                    catch (Exception e)
+                    {
+                        _exceptionHandler(e);
+                    }
+                else
+                    _delegate?.Invoke(m);
+            }
+        }
+        
+        
+        public void Remove(Action<T> handler)
+        {
+            lock (this)
+                _delegate -= handler;
+        }
+
+        public void Fire(T ev)
+        {
+            if (_delegate == null)
+            {
+                lock (this)
+                {
+                    _stash = _stash ?? new List<T>();
+                    _stash.Add(ev);
+                }
+            }
+            else
+                _delegate?.Invoke(ev);
+        }
+    }
+}

+ 1 - 1
src/Avalonia.Remote.Protocol/ITransport.cs

@@ -6,7 +6,7 @@ using System.Threading.Tasks;
 
 namespace Avalonia.Remote.Protocol
 {
-    public interface IAvaloniaRemoteTransport
+    public interface IAvaloniaRemoteTransportConnection : IDisposable
     {
         Task Send(object data);
         event Action<object> OnMessage;

+ 84 - 0
src/Avalonia.Remote.Protocol/TcpTransportBase.cs

@@ -0,0 +1,84 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading.Tasks;
+
+namespace Avalonia.Remote.Protocol
+{
+    public abstract class TcpTransportBase
+    {
+        private readonly IMessageTypeResolver _resolver;
+
+        public TcpTransportBase(IMessageTypeResolver resolver)
+        {
+            _resolver = resolver;
+        }
+        
+        protected abstract IAvaloniaRemoteTransportConnection CreateTransport(IMessageTypeResolver resolver,
+            Stream stream, Action disposeCallback);
+
+        class DisposableServer : IDisposable
+        {
+            private readonly TcpListener _l;
+
+            public DisposableServer(TcpListener l)
+            {
+                _l = l;
+            }
+            public void Dispose()
+            {
+                try
+                {
+                    _l.Stop();
+                }
+                catch
+                {
+                    //Ignore
+                }
+            }
+        }
+        
+        public IDisposable Listen(IPAddress address, int port, Action<IAvaloniaRemoteTransportConnection> cb)
+        {
+            var server = new TcpListener(address, port);
+            async void AcceptNew()
+            {
+                try
+                {
+                    var cl = await server.AcceptTcpClientAsync();
+                    AcceptNew();
+                    Task.Run(async () =>
+                    {
+                        try
+                        {
+                            var tcs = new TaskCompletionSource<int>();
+                            var t = CreateTransport(_resolver, cl.GetStream(), () => tcs.TrySetResult(0));
+                            cb(t);
+                            await tcs.Task;
+                        }
+                        finally
+                        {
+                            cl.Dispose();
+                        }
+
+                    });
+                }
+                catch
+                {
+                    //Ignore and stop
+                }
+            }
+            server.Start();
+            AcceptNew();
+            return new DisposableServer(server);
+        }
+
+        public async Task<IAvaloniaRemoteTransportConnection> Connect(IPAddress address, int port)
+        {
+            var c = new TcpClient();
+            await c.ConnectAsync(address, port);
+            return CreateTransport(_resolver, c.GetStream(), c.Dispose);
+        }
+    }
+}

+ 96 - 2
src/Avalonia.Remote.Protocol/TransportConnectionWrapper.cs

@@ -1,7 +1,101 @@
-namespace Avalonia.Remote.Protocol
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Avalonia.Remote.Protocol
 {
-    public class TransportConnectionWrapper
+    public class TransportConnectionWrapper : IAvaloniaRemoteTransportConnection
     {
+        private readonly IAvaloniaRemoteTransportConnection _conn;
+        private EventStash<object> _onMessage;
+        private EventStash<Exception> _onException = new EventStash<Exception>();
         
+        private Queue<SendOperation> _sendQueue = new Queue<SendOperation>();
+        private object _lock =new object();
+        private TaskCompletionSource<int> _signal;
+        private bool _workerIsAlive;
+        public TransportConnectionWrapper(IAvaloniaRemoteTransportConnection conn)
+        {
+            _conn = conn;
+            _onMessage = new EventStash<object>(_onException.Fire);
+            _conn.OnException +=_onException.Fire;
+            conn.OnMessage +=  _onMessage.Fire;
+
+        }
+
+        class SendOperation
+        {
+            public object Message { get; set; }
+            public TaskCompletionSource<int> Tcs { get; set; }
+        }
+        
+        public void Dispose() => _conn.Dispose();
+
+        async void Worker()
+        {
+            while (true)
+            {
+                SendOperation wi = null;
+                lock (_lock)
+                {
+                    if (_sendQueue.Count != 0)
+                        wi = _sendQueue.Dequeue();
+                }
+                if (wi == null)
+                {
+                    var signal = new TaskCompletionSource<int>();
+                    lock (_lock)
+                        _signal = signal;
+                    await signal.Task.ConfigureAwait(false);
+                    continue;
+                }
+                try
+                {
+                    await _conn.Send(wi.Message).ConfigureAwait(false);
+                    wi.Tcs.TrySetResult(0);
+                }
+                catch (Exception e)
+                {
+                    wi.Tcs.TrySetException(e);
+                }
+            }
+            
+        }
+        
+        public Task Send(object data)
+        {
+            var tcs = new TaskCompletionSource<int>();
+            lock (_lock)
+            {
+                if (!_workerIsAlive)
+                {
+                    _workerIsAlive = true;
+                    Worker();
+                }
+                _sendQueue.Enqueue(new SendOperation
+                {
+                    Message = data,
+                    Tcs = tcs
+                });
+                if (_signal != null)
+                {
+                    _signal.SetResult(0);
+                    _signal = null;
+                }
+            }
+            return tcs.Task;
+        }
+        
+        public event Action<object> OnMessage
+        {
+            add => _onMessage.Add(value);
+            remove => _onMessage.Remove(value);
+        }
+
+        public event Action<Exception> OnException
+        {
+            add => _onException.Add(value);
+            remove => _onException.Remove(value);
+        }
     }
 }

+ 4 - 1
src/Avalonia.Remote.Protocol/ViewportMessages.cs

@@ -10,7 +10,8 @@ namespace Avalonia.Remote.Protocol.Viewport
     {
         Rgb565,
         Rgba8888,
-        Bgra8888
+        Bgra8888,
+        MaxValue = Bgra8888
     }
 
     [AvaloniaRemoteMessageGuid("6E3C5310-E2B1-4C3D-8688-01183AA48C5B")]
@@ -25,6 +26,8 @@ namespace Avalonia.Remote.Protocol.Viewport
     {
         public double Width { get; set; }
         public double Height { get; set; }
+        public double DpiX { get; set; }
+        public double DpiY { get; set; }
     }
 
     [AvaloniaRemoteMessageGuid("63481025-7016-43FE-BADC-F2FD0F88609E")]