From 298fad4c4e11c2afb923b378d8944968a7d18ee1 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Sun, 11 Apr 2021 21:50:28 +0000 Subject: [PATCH] Merged PR 6: Linux capture fixes. --- Agent/Agent.csproj | 4 +- Desktop.Core/Desktop.Core.csproj | 6 +- .../Interfaces/IFileTransferService.cs | 3 +- Desktop.Core/Services/DtoMessageHandler.cs | 36 ++--- Desktop.Core/Services/ScreenCaster.cs | 28 ++-- Desktop.Core/Services/Viewer.cs | 49 +++++-- Desktop.Core/Services/WebRtcSession.cs | 10 +- Desktop.Core/Utilities/ImageUtils.cs | 41 +++--- Desktop.Core/ViewModels/FileUpload.cs | 4 +- Desktop.Linux/App.xaml.cs | 4 +- Desktop.Linux/Controls/MessageBox.axaml | 2 +- Desktop.Linux/Desktop.Linux.csproj | 2 - Desktop.Linux/Native/LibX11.cs | 35 +++++ Desktop.Linux/Native/libXrandr.cs | 68 +++++++++ .../Services/FileTransferServiceLinux.cs | 4 +- .../Services/KeyboardMouseInputLinux.cs | 8 +- Desktop.Linux/Services/ScreenCapturerLinux.cs | 136 +++++++++++------- .../ViewModels/FileTransferWindowViewModel.cs | 3 +- .../ViewModels/MainWindowViewModel.cs | 1 + Desktop.Win/Desktop.Win.csproj | 2 - .../Services/FileTransferServiceWin.cs | 4 +- Desktop.Win/Services/KeyboardMouseInputWin.cs | 87 +++++++---- Desktop.Win/Services/ScreenCapturerWin.cs | 117 ++++++++------- .../ViewModels/FileTransferWindowViewModel.cs | 3 +- README.md | 38 +++-- Server/Components/Devices/ChatCard.razor.cs | 9 +- Server/Pages/ServerLogs.razor | 2 +- Server/Server.csproj | 23 +-- Server/wwwroot/Content/Install-Ubuntu-x64.sh | 1 + Shared/Shared.csproj | 4 +- Shared/Utilities/Logger.cs | 6 +- Tests.LoadTester/Tests.LoadTester.csproj | 2 +- Tests/ManualTests.cs | 8 +- Tests/Tests.csproj | 6 +- 34 files changed, 488 insertions(+), 268 deletions(-) create mode 100644 Desktop.Linux/Native/libXrandr.cs diff --git a/Agent/Agent.csproj b/Agent/Agent.csproj index 20fd935cc..c390acb9d 100644 --- a/Agent/Agent.csproj +++ b/Agent/Agent.csproj @@ -23,11 +23,11 @@ - + - + diff --git a/Desktop.Core/Desktop.Core.csproj b/Desktop.Core/Desktop.Core.csproj index a054a0af4..66f9c71f4 100644 --- a/Desktop.Core/Desktop.Core.csproj +++ b/Desktop.Core/Desktop.Core.csproj @@ -40,15 +40,13 @@ - - + + - - diff --git a/Desktop.Core/Interfaces/IFileTransferService.cs b/Desktop.Core/Interfaces/IFileTransferService.cs index 05117af72..d82f3a563 100644 --- a/Desktop.Core/Interfaces/IFileTransferService.cs +++ b/Desktop.Core/Interfaces/IFileTransferService.cs @@ -1,6 +1,7 @@ using Remotely.Desktop.Core.Services; using Remotely.Desktop.Core.ViewModels; using System; +using System.Threading; using System.Threading.Tasks; namespace Remotely.Desktop.Core.Interfaces @@ -11,6 +12,6 @@ public interface IFileTransferService Task ReceiveFile(byte[] buffer, string fileName, string messageId, bool endOfFile, bool startOfFile); void OpenFileTransferWindow(Viewer viewer); - Task UploadFile(FileUpload file, Viewer viewer, Action progressUpdateCallback); + Task UploadFile(FileUpload file, Viewer viewer, CancellationToken cancelToken, Action progressUpdateCallback); } } diff --git a/Desktop.Core/Services/DtoMessageHandler.cs b/Desktop.Core/Services/DtoMessageHandler.cs index 048a661fb..1be60778a 100644 --- a/Desktop.Core/Services/DtoMessageHandler.cs +++ b/Desktop.Core/Services/DtoMessageHandler.cs @@ -12,7 +12,7 @@ namespace Remotely.Desktop.Core.Services { public interface IDtoMessageHandler { - Task ParseMessage(Services.Viewer viewer, byte[] message); + Task ParseMessage(Viewer viewer, byte[] message); } public class DtoMessageHandler : IDtoMessageHandler { @@ -31,7 +31,7 @@ public DtoMessageHandler(IKeyboardMouseInput keyboardMouseInput, private IClipboardService ClipboardService { get; } private IFileTransferService FileTransferService { get; } private IKeyboardMouseInput KeyboardMouseInput { get; } - public async Task ParseMessage(Services.Viewer viewer, byte[] message) + public async Task ParseMessage(Viewer viewer, byte[] message) { try { @@ -154,25 +154,19 @@ await FileTransferService.ReceiveFile(dto.Buffer, dto.StartOfFile); } - private async Task GetWindowsSessions(Services.Viewer viewer) + private async Task GetWindowsSessions(Viewer viewer) { await viewer.SendWindowsSessions(); } - private void HandleFrameReceived(Services.Viewer viewer) + private void HandleFrameReceived(Viewer viewer) { - for (int i = 0; i < 5; i++) + while (viewer.PendingSentFrames.Count > 0 && + !viewer.IsStalled && + viewer.IsConnected) { - if (viewer.PendingSentFrames.TryDequeue(out var frame)) + if (viewer.PendingSentFrames.TryDequeue(out _)) { - var roundtrip = (DateTimeOffset.Now - frame.Timestamp).TotalSeconds; - var bps = frame.FrameSize / (roundtrip / 2); - - if (bps > viewer.PeakBytesPerSecond) - { - viewer.PeakBytesPerSecond = bps; - Debug.WriteLine($"Peak Mbps: {bps / 1024 / 1024 * 8}"); - } break; } } @@ -198,19 +192,19 @@ private void KeyUp(byte[] message) KeyboardMouseInput.SendKeyUp(dto.Key); } - private void MouseDown(byte[] message, Services.Viewer viewer) + private void MouseDown(byte[] message, Viewer viewer) { var dto = MessagePackSerializer.Deserialize(message); KeyboardMouseInput.SendMouseButtonAction(dto.Button, ButtonAction.Down, dto.PercentX, dto.PercentY, viewer); } - private void MouseMove(byte[] message, Services.Viewer viewer) + private void MouseMove(byte[] message, Viewer viewer) { var dto = MessagePackSerializer.Deserialize(message); KeyboardMouseInput.SendMouseMove(dto.PercentX, dto.PercentY, viewer); } - private void MouseUp(byte[] message, Services.Viewer viewer) + private void MouseUp(byte[] message, Viewer viewer) { var dto = MessagePackSerializer.Deserialize(message); KeyboardMouseInput.SendMouseButtonAction(dto.Button, ButtonAction.Up, dto.PercentX, dto.PercentY, viewer); @@ -222,12 +216,12 @@ private void MouseWheel(byte[] message) KeyboardMouseInput.SendMouseWheel(-(int)dto.DeltaY); } - private void OpenFileTransferWindow(Services.Viewer viewer) + private void OpenFileTransferWindow(Viewer viewer) { FileTransferService.OpenFileTransferWindow(viewer); } - private void SelectScreen(byte[] message, Services.Viewer viewer) + private void SelectScreen(byte[] message, Viewer viewer) { var dto = MessagePackSerializer.Deserialize(message); viewer.Capturer.SetSelectedScreen(dto.DisplayName); @@ -238,7 +232,7 @@ private void SetKeyStatesUp() KeyboardMouseInput.SetKeyStatesUp(); } - private void Tap(byte[] message, Services.Viewer viewer) + private void Tap(byte[] message, Viewer viewer) { var dto = MessagePackSerializer.Deserialize(message); KeyboardMouseInput.SendMouseButtonAction(0, ButtonAction.Down, dto.PercentX, dto.PercentY, viewer); @@ -257,7 +251,7 @@ private void ToggleBlockInput(byte[] message) KeyboardMouseInput.ToggleBlockInput(dto.ToggleOn); } - private void ToggleWebRtcVideo(byte[] message, Services.Viewer viewer) + private void ToggleWebRtcVideo(byte[] message, Viewer viewer) { var dto = MessagePackSerializer.Deserialize(message); viewer.ToggleWebRtcVideo(dto.ToggleOn); diff --git a/Desktop.Core/Services/ScreenCaster.cs b/Desktop.Core/Services/ScreenCaster.cs index c1bff643e..c4b2e4ade 100644 --- a/Desktop.Core/Services/ScreenCaster.cs +++ b/Desktop.Core/Services/ScreenCaster.cs @@ -14,7 +14,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using SkiaSharp; namespace Remotely.Desktop.Core.Services { @@ -55,6 +54,8 @@ public async Task BeginScreenCasting(ScreenCastRequest screenCastRequest) viewer.Name = screenCastRequest.RequesterName; viewer.ViewerConnectionID = screenCastRequest.ViewerID; + var screenBounds = viewer.Capturer.CurrentScreenBounds; + Logger.Write($"Starting screen cast. Requester: {viewer.Name}. " + $"Viewer ID: {viewer.ViewerConnectionID}. App Mode: {_conductor.Mode}"); @@ -78,8 +79,7 @@ await viewer.SendScreenData( viewer.Capturer.SelectedScreen, viewer.Capturer.GetDisplayNames().ToArray()); - await viewer.SendScreenSize(viewer.Capturer.CurrentScreenBounds.Width, - viewer.Capturer.CurrentScreenBounds.Height); + await viewer.SendScreenSize(screenBounds.Width, screenBounds.Height); await viewer.SendCursorChange(_cursorIconWatcher.GetCurrentCursor()); @@ -96,11 +96,11 @@ await viewer.SendScreenSize(viewer.Capturer.CurrentScreenBounds.Width, { await viewer.SendScreenCapture(new CaptureFrame() { - EncodedImageBytes = ImageUtils.EncodeWithSkia(initialFrame, SKEncodedImageFormat.Webp, _maxQuality), - Left = viewer.Capturer.CurrentScreenBounds.Left, - Top = viewer.Capturer.CurrentScreenBounds.Top, - Width = viewer.Capturer.CurrentScreenBounds.Width, - Height = viewer.Capturer.CurrentScreenBounds.Height + EncodedImageBytes = ImageUtils.EncodeJpeg(initialFrame, _maxQuality), + Left = screenBounds.Left, + Top = screenBounds.Top, + Width = screenBounds.Width, + Height = screenBounds.Height }); } } @@ -172,23 +172,23 @@ await viewer.SendScreenCapture(new CaptureFrame() if (viewer.Capturer.CaptureFullscreen) { // Recalculate Bps. - viewer.PeakBytesPerSecond = 0; - encodedImageBytes = ImageUtils.EncodeWithSkia(clone, SKEncodedImageFormat.Jpeg, _maxQuality); + viewer.AverageBytesPerSecond = 0; + encodedImageBytes = ImageUtils.EncodeJpeg(clone, _maxQuality); } else { - if (viewer.PeakBytesPerSecond > 0) + if (viewer.AverageBytesPerSecond > 0) { var expectedSize = clone.Height * clone.Width * 4 * .1; - var timeToSend = expectedSize / viewer.PeakBytesPerSecond; + var timeToSend = expectedSize / viewer.AverageBytesPerSecond; currentQuality = Math.Max(_minQuality, Math.Min(_maxQuality, (int)(.1 / timeToSend * _maxQuality))); if (currentQuality < _maxQuality - 10) { refreshNeeded = true; + Debug.WriteLine($"Quality Reduced: {currentQuality}"); } - Debug.WriteLine($"Current Quality: {currentQuality}"); } - encodedImageBytes = ImageUtils.EncodeWithSkia(clone, SKEncodedImageFormat.Jpeg, currentQuality); + encodedImageBytes = ImageUtils.EncodeJpeg(clone, currentQuality); } viewer.Capturer.CaptureFullscreen = false; diff --git a/Desktop.Core/Services/Viewer.cs b/Desktop.Core/Services/Viewer.cs index f2df27973..e482bf642 100644 --- a/Desktop.Core/Services/Viewer.cs +++ b/Desktop.Core/Services/Viewer.cs @@ -11,11 +11,16 @@ using System.IO; using System.Linq; using System.Threading.Tasks; +using System.Diagnostics; +using System.Threading; namespace Remotely.Desktop.Core.Services { public class Viewer : IDisposable { + private long _bytesSent; + private TimeSpan _timeSpentSending = TimeSpan.Zero; + public Viewer(ICasterSocket casterSocket, IScreenCapturer screenCapturer, IClipboardService clipboardService, @@ -64,7 +69,7 @@ public bool IsUsingWebRtcVideo public string Name { get; set; } - public double PeakBytesPerSecond { get; set; } + public double AverageBytesPerSecond { get; set; } public ConcurrentQueue PendingSentFrames { get; } = new(); public WebRtcSession RtcSession { get; set; } @@ -150,7 +155,7 @@ await SendToViewer(() => RtcSession.SendDto(dto), () => CasterSocket.SendDtoToViewer(dto, ViewerConnectionID)); } - public async Task SendFile(FileUpload fileUpload, Action progressUpdateCallback) + public async Task SendFile(FileUpload fileUpload, CancellationToken cancelToken, Action progressUpdateCallback) { try { @@ -163,13 +168,18 @@ public async Task SendFile(FileUpload fileUpload, Action progressUpdateC StartOfFile = true }; - await SendToViewer(() => RtcSession.SendDto(fileDto), - () => CasterSocket.SendDtoToViewer(fileDto, ViewerConnectionID)); + await SendToViewer(async () => await RtcSession.SendDto(fileDto), + async () => await CasterSocket.SendDtoToViewer(fileDto, ViewerConnectionID)); using var fs = File.OpenRead(fileUpload.FilePath); using var br = new BinaryReader(fs); while (fs.Position < fs.Length) { + if (cancelToken.IsCancellationRequested) + { + return; + } + fileDto = new FileDto() { Buffer = br.ReadBytes(50_000), @@ -178,8 +188,8 @@ await SendToViewer(() => RtcSession.SendDto(fileDto), }; await SendToViewer( - () => RtcSession.SendDto(fileDto), - () => CasterSocket.SendDtoToViewer(fileDto, ViewerConnectionID)); + async () => await RtcSession.SendDto(fileDto), + async () => await CasterSocket.SendDtoToViewer(fileDto, ViewerConnectionID)); progressUpdateCallback((double)fs.Position / fs.Length); } @@ -192,8 +202,8 @@ await SendToViewer( StartOfFile = false }; - await SendToViewer(() => RtcSession.SendDto(fileDto), - () => CasterSocket.SendDtoToViewer(fileDto, ViewerConnectionID)); + await SendToViewer(async () => await RtcSession.SendDto(fileDto), + async () => await CasterSocket.SendDtoToViewer(fileDto, ViewerConnectionID)); progressUpdateCallback(1); } @@ -219,6 +229,8 @@ public async Task SendScreenCapture(CaptureFrame screenFrame) var width = screenFrame.Width; var height = screenFrame.Height; + var sw = Stopwatch.StartNew(); + for (var i = 0; i < screenFrame.EncodedImageBytes.Length; i += 50_000) { var dto = new CaptureFrameDto() @@ -231,8 +243,8 @@ public async Task SendScreenCapture(CaptureFrame screenFrame) ImageBytes = screenFrame.EncodedImageBytes.Skip(i).Take(50_000).ToArray() }; - await SendToViewer(() => RtcSession.SendDto(dto), - () => CasterSocket.SendDtoToViewer(dto, ViewerConnectionID)); + await SendToViewer(async () => await RtcSession.SendDto(dto), + async () => await CasterSocket.SendDtoToViewer(dto, ViewerConnectionID)); } var endOfFrameDto = new CaptureFrameDto() @@ -244,8 +256,19 @@ await SendToViewer(() => RtcSession.SendDto(dto), EndOfFrame = true }; - await SendToViewer(() => RtcSession.SendDto(endOfFrameDto), - () => CasterSocket.SendDtoToViewer(endOfFrameDto, ViewerConnectionID)); + await SendToViewer( + async () => await RtcSession.SendDto(endOfFrameDto), + async () => await CasterSocket.SendDtoToViewer(endOfFrameDto, ViewerConnectionID)); + + sw.Stop(); + + _bytesSent += screenFrame.EncodedImageBytes.Length; + _timeSpentSending += sw.Elapsed; + + + AverageBytesPerSecond = _bytesSent / _timeSpentSending.TotalSeconds; + + Debug.WriteLine($"Mbps: {AverageBytesPerSecond / 1024 / 1024 * 8}"); } public async Task SendScreenData(string selectedScreen, string[] displayNames) @@ -280,7 +303,7 @@ public void ThrottleIfNeeded() { TaskHelper.DelayUntil(() => !PendingSentFrames.TryPeek(out var result) || DateTimeOffset.Now - result.Timestamp < TimeSpan.FromSeconds(1), - TimeSpan.MaxValue); + TimeSpan.FromSeconds(10)); } public void ToggleWebRtcVideo(bool toggleOn) diff --git a/Desktop.Core/Services/WebRtcSession.cs b/Desktop.Core/Services/WebRtcSession.cs index b9f201312..26a2fd1a4 100644 --- a/Desktop.Core/Services/WebRtcSession.cs +++ b/Desktop.Core/Services/WebRtcSession.cs @@ -7,6 +7,7 @@ using System.Drawing; using System.Linq; using System.Threading.Tasks; +using System.Threading; namespace Remotely.Desktop.Core.Services { @@ -109,7 +110,14 @@ public async Task Init(IceServerModel[] iceServers) public Task SendDto(T dto) where T : BaseDto { - return Task.Run(() => CaptureChannel.SendMessage(MessagePackSerializer.Serialize(dto))); + return Task.Run(() => + { + CaptureChannel.SendMessage(MessagePackSerializer.Serialize(dto)); + while (CurrentBuffer > 64_000) + { + Thread.Sleep(10); + } + }); } public async Task SetRemoteDescription(string type, string sdp) diff --git a/Desktop.Core/Utilities/ImageUtils.cs b/Desktop.Core/Utilities/ImageUtils.cs index 7fa57ef6b..688248f1e 100644 --- a/Desktop.Core/Utilities/ImageUtils.cs +++ b/Desktop.Core/Utilities/ImageUtils.cs @@ -1,6 +1,4 @@ -using SkiaSharp; -using SkiaSharp.Views.Desktop; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Drawing; @@ -14,18 +12,31 @@ namespace Remotely.Desktop.Core.Utilities { public class ImageUtils { - public static byte[] EncodeWithSkia(Bitmap bitmap, SKEncodedImageFormat format, int quality) + private static ImageCodecInfo _jpegEncoder = ImageCodecInfo.GetImageEncoders().FirstOrDefault(x => x.FormatID == ImageFormat.Jpeg.Guid); + + //public static byte[] EncodeWithSkia(Bitmap bitmap, SKEncodedImageFormat format, int quality) + //{ + // using var ms = new MemoryStream(); + // var info = new SKImageInfo(bitmap.Width, bitmap.Height); + // var skBitmap = new SKBitmap(info); + // using (var pixmap = skBitmap.PeekPixels()) + // { + // bitmap.ToSKPixmap(pixmap); + // } + + // skBitmap.Encode(ms, format, quality); + + // return ms.ToArray(); + //} + + public static byte[] EncodeJpeg(Bitmap bitmap, int quality) { using var ms = new MemoryStream(); - var info = new SKImageInfo(bitmap.Width, bitmap.Height); - var skBitmap = new SKBitmap(info); - using (var pixmap = skBitmap.PeekPixels()) + using var encoderParams = new EncoderParameters(1) { - bitmap.ToSKPixmap(pixmap); - } - - skBitmap.Encode(ms, format, quality); - + Param = new[] { new EncoderParameter(Encoder.Quality, quality) } + }; + bitmap.Save(ms, _jpegEncoder, encoderParams); return ms.ToArray(); } @@ -152,11 +163,7 @@ public static Bitmap GetImageDiff(Bitmap currentFrame, Bitmap previousFrame, boo { throw new Exception("Bitmaps are not of equal dimensions."); } - if (!Bitmap.IsAlphaPixelFormat(currentFrame.PixelFormat) || !Bitmap.IsAlphaPixelFormat(previousFrame.PixelFormat) || - !Bitmap.IsCanonicalPixelFormat(currentFrame.PixelFormat) || !Bitmap.IsCanonicalPixelFormat(previousFrame.PixelFormat)) - { - throw new Exception("Bitmaps must be 32 bits per pixel and contain alpha channel."); - } + var width = currentFrame.Width; var height = currentFrame.Height; diff --git a/Desktop.Core/ViewModels/FileUpload.cs b/Desktop.Core/ViewModels/FileUpload.cs index 1b97a95d7..e8e8dbc22 100644 --- a/Desktop.Core/ViewModels/FileUpload.cs +++ b/Desktop.Core/ViewModels/FileUpload.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Threading; namespace Remotely.Desktop.Core.ViewModels { @@ -7,6 +8,8 @@ public class FileUpload : ViewModelBase private string _filePath; private double _percentProgress; + public CancellationTokenSource CancellationTokenSource { get; } = new CancellationTokenSource(); + public string DisplayName => Path.GetFileName(FilePath); public string FilePath @@ -19,7 +22,6 @@ public string FilePath { _filePath = value; FirePropertyChanged(); - FirePropertyChanged(); } } public double PercentProgress diff --git a/Desktop.Linux/App.xaml.cs b/Desktop.Linux/App.xaml.cs index 1c4003ce1..546485b2c 100644 --- a/Desktop.Linux/App.xaml.cs +++ b/Desktop.Linux/App.xaml.cs @@ -120,7 +120,9 @@ private async Task Startup() } else { - this.RunWithMainWindow(); + await Dispatcher.UIThread.InvokeAsync(() => { + this.RunWithMainWindow(); + }); } } diff --git a/Desktop.Linux/Controls/MessageBox.axaml b/Desktop.Linux/Controls/MessageBox.axaml index 501d08ae0..cf288fcb3 100644 --- a/Desktop.Linux/Controls/MessageBox.axaml +++ b/Desktop.Linux/Controls/MessageBox.axaml @@ -5,7 +5,7 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" xmlns:vm="clr-namespace:Remotely.Desktop.Linux.ViewModels;assembly=Remotely_Desktop" x:Class="Remotely.Desktop.Linux.Controls.MessageBox" - Icon="{Binding Icon}" + Icon="{Binding WindowIcon}" Title="{Binding Caption}" SizeToContent="WidthAndHeight" MinWidth="300" MinHeight="100" WindowStartupLocation="CenterScreen"> diff --git a/Desktop.Linux/Desktop.Linux.csproj b/Desktop.Linux/Desktop.Linux.csproj index 15940af33..165b92c67 100644 --- a/Desktop.Linux/Desktop.Linux.csproj +++ b/Desktop.Linux/Desktop.Linux.csproj @@ -55,8 +55,6 @@ - - diff --git a/Desktop.Linux/Native/LibX11.cs b/Desktop.Linux/Native/LibX11.cs index 76bd60a7a..33ccf3849 100644 --- a/Desktop.Linux/Native/LibX11.cs +++ b/Desktop.Linux/Native/LibX11.cs @@ -82,6 +82,14 @@ public static unsafe class LibX11 [DllImport("libX11")] public static extern void XDestroyImage(IntPtr ximage); + [DllImport("libX11")] + public static extern void XNoOp(IntPtr display); + + [DllImport("libX11")] + public static extern void XFree(IntPtr data); + + [DllImport("libX11")] + public static extern int XGetWindowAttributes(IntPtr display, IntPtr window, out XWindowAttributes windowAttributes); public struct XImage { @@ -103,5 +111,32 @@ public struct XImage public ulong blue_mask; public IntPtr obdata; /* hook for the object routines to hang on */ } + + public struct XWindowAttributes + { + public int x; + public int y; + public int width; + public int height; + public int border_width; + public int depth; + public IntPtr visual; + public IntPtr root; + public int @class; + public int bit_gravity; + public int win_gravity; + public int backing_store; + public ulong backing_planes; + public ulong backing_pixel; + public bool save_under; + public IntPtr colormap; + public bool map_installed; + public int map_state; + public long all_event_masks; + public long your_event_mask; + public long do_not_propagate_mask; + public bool override_redirect; + public IntPtr screen; + } } } diff --git a/Desktop.Linux/Native/libXrandr.cs b/Desktop.Linux/Native/libXrandr.cs new file mode 100644 index 000000000..22132c729 --- /dev/null +++ b/Desktop.Linux/Native/libXrandr.cs @@ -0,0 +1,68 @@ +/* + * Copyright © 2000 Compaq Computer Corporation, Inc. + * Copyright © 2002 Hewlett-Packard Company, Inc. + * Copyright © 2006 Intel Corporation + * Copyright © 2008 Red Hat, Inc. + * + * Permission to use, copy, modify, distribute, and sell this software and its + * documentation for any purpose is hereby granted without fee, provided that + * the above copyright notice appear in all copies and that both that copyright + * notice and this permission notice appear in supporting documentation, and + * that the name of the copyright holders not be used in advertising or + * publicity pertaining to distribution of the software without specific, + * written prior permission. The copyright holders make no representations + * about the suitability of this software for any purpose. It is provided "as + * is" without express or implied warranty. + * + * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, + * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO + * EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY SPECIAL, INDIRECT OR + * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, + * DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER + * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE + * OF THIS SOFTWARE. + * + * Author: Jim Gettys, HP Labs, Hewlett-Packard, Inc. + * Keith Packard, Intel Corporation + */ + + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Remotely.Desktop.Linux.Native +{ + public static class LibXrandr + { + [StructLayout(LayoutKind.Sequential)] + public struct XRRMonitorInfo + { + // Atom + public IntPtr name; + public bool primary; + public bool automatic; + public int noutput; + public int x; + public int y; + public int width; + public int height; + public int mwidth; + public int mheight; + // RROutput* + public IntPtr outputs; + } + + [DllImport("libXrandr")] + public static extern IntPtr XRRGetMonitors(IntPtr display, IntPtr window, bool get_active, out int monitors); + + [DllImport("libXrandr")] + public static extern void XRRFreeMonitors(IntPtr monitors); + + [DllImport("libXrandr")] + public static extern IntPtr XRRAllocateMonitor(IntPtr display, int output); + } +} diff --git a/Desktop.Linux/Services/FileTransferServiceLinux.cs b/Desktop.Linux/Services/FileTransferServiceLinux.cs index 3d2f9c06c..527dd623d 100644 --- a/Desktop.Linux/Services/FileTransferServiceLinux.cs +++ b/Desktop.Linux/Services/FileTransferServiceLinux.cs @@ -117,11 +117,11 @@ public async Task ReceiveFile(byte[] buffer, string fileName, string messageId, } } - public async Task UploadFile(FileUpload fileUpload, Viewer viewer, Action progressUpdateCallback) + public async Task UploadFile(FileUpload fileUpload, Viewer viewer, CancellationToken cancelToken, Action progressUpdateCallback) { try { - await viewer.SendFile(fileUpload, progressUpdateCallback); + await viewer.SendFile(fileUpload, cancelToken, progressUpdateCallback); } catch (Exception ex) { diff --git a/Desktop.Linux/Services/KeyboardMouseInputLinux.cs b/Desktop.Linux/Services/KeyboardMouseInputLinux.cs index af1f85977..f1bfbfcdf 100644 --- a/Desktop.Linux/Services/KeyboardMouseInputLinux.cs +++ b/Desktop.Linux/Services/KeyboardMouseInputLinux.cs @@ -90,10 +90,12 @@ public void SendMouseMove(double percentX, double percentY, Viewer viewer) try { InitDisplay(); + + var screenBounds = viewer.Capturer.CurrentScreenBounds; LibXtst.XTestFakeMotionEvent(Display, - viewer.Capturer.GetSelectedScreenIndex(), - (int)(viewer.Capturer.CurrentScreenBounds.Width * percentX), - (int)(viewer.Capturer.CurrentScreenBounds.Height * percentY), + LibX11.XDefaultScreen(Display), + screenBounds.X + (int)(screenBounds.Width * percentX), + screenBounds.Y + (int)(screenBounds.Height * percentY), 0); LibX11.XSync(Display, false); } diff --git a/Desktop.Linux/Services/ScreenCapturerLinux.cs b/Desktop.Linux/Services/ScreenCapturerLinux.cs index cbaafab53..a6d932e4a 100644 --- a/Desktop.Linux/Services/ScreenCapturerLinux.cs +++ b/Desktop.Linux/Services/ScreenCapturerLinux.cs @@ -7,14 +7,15 @@ using System.Drawing.Imaging; using System.Linq; using System.Runtime.InteropServices; +using System.Text.Json; using System.Threading; namespace Remotely.Desktop.Linux.Services { public class ScreenCapturerLinux : IScreenCapturer { - private readonly SemaphoreSlim _screenCaptureLock = new(1,1); - private readonly Dictionary _x11Screens = new(); + private readonly object _screenBoundsLock = new(); + private readonly Dictionary _x11Screens = new(); public ScreenCapturerLinux() { Display = LibX11.XOpenDisplay(null); @@ -33,34 +34,27 @@ public void Dispose() LibX11.XCloseDisplay(Display); GC.SuppressFinalize(this); } - - public IEnumerable GetDisplayNames() => _x11Screens.Keys; + public IEnumerable GetDisplayNames() => _x11Screens.Keys.Select(x => x.ToString()); public Bitmap GetNextFrame() { - try - { - _screenCaptureLock.Wait(); - - return GetX11Screen(); - } - catch (Exception ex) + lock (_screenBoundsLock) { - Logger.Write(ex); - Init(); - return null; - } - finally - { - _screenCaptureLock.Release(); + try + { + return GetX11Capture(); + } + catch (Exception ex) + { + Logger.Write(ex); + Init(); + return null; + } } } - public int GetScreenCount() - { - return LibX11.XScreenCount(Display); - } + public int GetScreenCount() => _x11Screens.Count; - public int GetSelectedScreenIndex() => _x11Screens[SelectedScreen]; + public int GetSelectedScreenIndex() => int.Parse(SelectedScreen ?? "0"); public Rectangle GetVirtualScreenBounds() { @@ -83,12 +77,33 @@ public void Init() { CaptureFullscreen = true; _x11Screens.Clear(); + + var monitorsPtr = LibXrandr.XRRGetMonitors(Display, LibX11.XDefaultRootWindow(Display), true, out var monitorCount); + + var monitorInfoSize = Marshal.SizeOf(); + + for (var i = 0; i < monitorCount; i++) + { + var monitorPtr = new IntPtr(monitorsPtr.ToInt64() + i * monitorInfoSize); + var monitorInfo = Marshal.PtrToStructure(monitorPtr); + + Logger.Write($"Found monitor: " + + $"{monitorInfo.width}," + + $"{monitorInfo.height}," + + $"{monitorInfo.x}, " + + $"{monitorInfo.y}"); + + _x11Screens.Add(i.ToString(), monitorInfo); + } + + LibXrandr.XRRFreeMonitors(monitorsPtr); - for (var i = 0; i < GetScreenCount(); i++) + if (string.IsNullOrWhiteSpace(SelectedScreen) || + !_x11Screens.ContainsKey(SelectedScreen)) { - _x11Screens.Add(i.ToString(), i); + SelectedScreen = _x11Screens.Keys.First(); + RefreshCurrentScreenBounds(); } - SetSelectedScreen(_x11Screens.Keys.First()); } catch (Exception ex) { @@ -97,40 +112,49 @@ public void Init() } public void SetSelectedScreen(string displayName) { - if (displayName == SelectedScreen) - { - return; - } - try + lock (_screenBoundsLock) { - if (_x11Screens.ContainsKey(displayName)) + try { - SelectedScreen = displayName; + Logger.Write($"Setting display to {displayName}."); + if (displayName == SelectedScreen) + { + return; + } + if (_x11Screens.ContainsKey(displayName)) + { + SelectedScreen = displayName; + } + else + { + SelectedScreen = _x11Screens.Keys.First(); + } + + RefreshCurrentScreenBounds(); + } - else + catch (Exception ex) { - SelectedScreen = _x11Screens.Keys.First(); + Logger.Write(ex); } - var width = LibX11.XDisplayWidth(Display, _x11Screens[SelectedScreen]); - var height = LibX11.XDisplayHeight(Display, _x11Screens[SelectedScreen]); - CurrentScreenBounds = new Rectangle(0, 0, width, height); - CaptureFullscreen = true; - ScreenChanged?.Invoke(this, CurrentScreenBounds); - - } - catch (Exception ex) - { - Logger.Write(ex); } } - private Bitmap GetX11Screen() + private Bitmap GetX11Capture() { var currentFrame = new Bitmap(CurrentScreenBounds.Width, CurrentScreenBounds.Height, PixelFormat.Format32bppArgb); - var window = LibX11.XRootWindow(Display, _x11Screens[SelectedScreen]); + var window = LibX11.XDefaultRootWindow(Display); + + var imagePointer = LibX11.XGetImage(Display, + window, + CurrentScreenBounds.X, + CurrentScreenBounds.Y, + CurrentScreenBounds.Width, + CurrentScreenBounds.Height, + ~0, + 2); - var imagePointer = LibX11.XGetImage(Display, window, 0, 0, CurrentScreenBounds.Width, CurrentScreenBounds.Height, ~0, 2); var image = Marshal.PtrToStructure(imagePointer); var bd = currentFrame.LockBits(new Rectangle(0, 0, CurrentScreenBounds.Width, CurrentScreenBounds.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); @@ -147,9 +171,25 @@ private Bitmap GetX11Screen() } currentFrame.UnlockBits(bd); + Marshal.DestroyStructure(imagePointer); LibX11.XDestroyImage(imagePointer); return currentFrame; } + + private void RefreshCurrentScreenBounds() + { + var screen = _x11Screens[SelectedScreen]; + + Logger.Write($"Setting new screen bounds: " + + $"{screen.width}," + + $"{screen.height}," + + $"{screen.x}, " + + $"{screen.y}"); + + CurrentScreenBounds = new Rectangle(screen.x, screen.y, screen.width, screen.height); + CaptureFullscreen = true; + ScreenChanged?.Invoke(this, CurrentScreenBounds); + } } } diff --git a/Desktop.Linux/ViewModels/FileTransferWindowViewModel.cs b/Desktop.Linux/ViewModels/FileTransferWindowViewModel.cs index 73a244962..ebe059e50 100644 --- a/Desktop.Linux/ViewModels/FileTransferWindowViewModel.cs +++ b/Desktop.Linux/ViewModels/FileTransferWindowViewModel.cs @@ -89,7 +89,7 @@ await Dispatcher.UIThread.InvokeAsync(() => FileUploads.Add(fileUpload); }); - await _fileTransferService.UploadFile(fileUpload, _viewer, async progress => + await _fileTransferService.UploadFile(fileUpload, _viewer, fileUpload.CancellationTokenSource.Token, async progress => { await Dispatcher.UIThread.InvokeAsync(() => { @@ -103,6 +103,7 @@ await Dispatcher.UIThread.InvokeAsync(() => if (param is FileUpload fileUpload) { FileUploads.Remove(fileUpload); + fileUpload.CancellationTokenSource.Cancel(); } }); } diff --git a/Desktop.Linux/ViewModels/MainWindowViewModel.cs b/Desktop.Linux/ViewModels/MainWindowViewModel.cs index 02c228a90..ab1b85f9c 100644 --- a/Desktop.Linux/ViewModels/MainWindowViewModel.cs +++ b/Desktop.Linux/ViewModels/MainWindowViewModel.cs @@ -243,6 +243,7 @@ private async Task InstallDependencies() { FileName = "sudo", Arguments = "bash -c \"apt-get -y install libx11-dev ; " + + "apt-get -y install libxrandr-dev ; " + "apt-get -y install libc6-dev ; " + "apt-get -y install libgdiplus ; " + "apt-get -y install libxtst-dev ; " + diff --git a/Desktop.Win/Desktop.Win.csproj b/Desktop.Win/Desktop.Win.csproj index ab8bf1080..8fef046d7 100644 --- a/Desktop.Win/Desktop.Win.csproj +++ b/Desktop.Win/Desktop.Win.csproj @@ -39,8 +39,6 @@ - - diff --git a/Desktop.Win/Services/FileTransferServiceWin.cs b/Desktop.Win/Services/FileTransferServiceWin.cs index 8186f10a8..d5addd0a2 100644 --- a/Desktop.Win/Services/FileTransferServiceWin.cs +++ b/Desktop.Win/Services/FileTransferServiceWin.cs @@ -118,11 +118,11 @@ public async Task ReceiveFile(byte[] buffer, string fileName, string messageId, } } - public async Task UploadFile(FileUpload fileUpload, Viewer viewer, Action progressUpdateCallback) + public async Task UploadFile(FileUpload fileUpload, Viewer viewer, CancellationToken cancelToken, Action progressUpdateCallback) { try { - await viewer.SendFile(fileUpload, progressUpdateCallback); + await viewer.SendFile(fileUpload, cancelToken, progressUpdateCallback); } catch (Exception ex) { diff --git a/Desktop.Win/Services/KeyboardMouseInputWin.cs b/Desktop.Win/Services/KeyboardMouseInputWin.cs index 3839493f3..fb3465ea9 100644 --- a/Desktop.Win/Services/KeyboardMouseInputWin.cs +++ b/Desktop.Win/Services/KeyboardMouseInputWin.cs @@ -14,7 +14,8 @@ namespace Remotely.Desktop.Win.Services { public class KeyboardMouseInputWin : IKeyboardMouseInput { - private volatile bool inputBlocked; + private volatile bool _inputBlocked; + private Thread _inputProcessingThread; private CancellationTokenSource CancelTokenSource { get; set; } @@ -22,15 +23,19 @@ public class KeyboardMouseInputWin : IKeyboardMouseInput public Tuple GetAbsolutePercentFromRelativePercent(double percentX, double percentY, IScreenCapturer capturer) { - var absoluteX = (capturer.CurrentScreenBounds.Width * percentX) + capturer.CurrentScreenBounds.Left - capturer.GetVirtualScreenBounds().Left; - var absoluteY = (capturer.CurrentScreenBounds.Height * percentY) + capturer.CurrentScreenBounds.Top - capturer.GetVirtualScreenBounds().Top; + var screenBounds = capturer.CurrentScreenBounds; + + var absoluteX = (screenBounds.Width * percentX) + screenBounds.Left - capturer.GetVirtualScreenBounds().Left; + var absoluteY = (screenBounds.Height * percentY) + screenBounds.Top - capturer.GetVirtualScreenBounds().Top; return new Tuple(absoluteX / capturer.GetVirtualScreenBounds().Width, absoluteY / capturer.GetVirtualScreenBounds().Height); } public Tuple GetAbsolutePointFromRelativePercent(double percentX, double percentY, IScreenCapturer capturer) { - var absoluteX = (capturer.CurrentScreenBounds.Width * percentX) + capturer.CurrentScreenBounds.Left; - var absoluteY = (capturer.CurrentScreenBounds.Height * percentY) + capturer.CurrentScreenBounds.Top; + var screenBounds = capturer.CurrentScreenBounds; + + var absoluteX = (screenBounds.Width * percentX) + screenBounds.Left; + var absoluteY = (screenBounds.Height * percentY) + screenBounds.Top; return new Tuple(absoluteX, absoluteY); } @@ -41,8 +46,6 @@ public void Init() App.Current.Exit -= App_Exit; App.Current.Exit += App_Exit; }); - - StartInputProcessingThread(); } public void SendKeyDown(string key) @@ -225,10 +228,20 @@ public void ToggleBlockInput(bool toggleOn) { InputActions.Enqueue(() => { - inputBlocked = toggleOn; + _inputBlocked = toggleOn; var result = BlockInput(toggleOn); Logger.Write($"Result of ToggleBlockInput set to {toggleOn}: {result}"); + + if (!toggleOn) + { + CancelTokenSource.Cancel(); + } }); + + if (toggleOn) + { + StartInputProcessingThread(); + } } private void App_Exit(object sender, System.Windows.ExitEventArgs e) @@ -301,51 +314,65 @@ private VirtualKey ConvertJavaScriptKeyToVirtualKey(string key) } private void StartInputProcessingThread() { - CancelTokenSource?.Cancel(); - CancelTokenSource?.Dispose(); - + try + { + CancelTokenSource?.Cancel(); + CancelTokenSource?.Dispose(); + } + catch { } // After BlockInput is enabled, only simulated input coming from the same thread // will work. So we have to start a new thread that runs continuously and // processes a queue of input events. - var newThread = new Thread(() => + _inputProcessingThread = new Thread(() => { Logger.Write($"New input processing thread started on thread {Thread.CurrentThread.ManagedThreadId}."); CancelTokenSource = new CancellationTokenSource(); - if (inputBlocked) + if (_inputBlocked) { ToggleBlockInput(true); } CheckQueue(CancelTokenSource.Token); }); - newThread.SetApartmentState(ApartmentState.STA); - newThread.Start(); + _inputProcessingThread.SetApartmentState(ApartmentState.STA); + _inputProcessingThread.Start(); } private void TryOnInputDesktop(Action inputAction) { - InputActions.Enqueue(() => + if (!_inputBlocked) { - try + if (!Win32Interop.SwitchToInputDesktop()) { - if (!Win32Interop.SwitchToInputDesktop()) + Logger.Write("Desktop switch failed while sending input."); + } + inputAction(); + } + else + { + InputActions.Enqueue(() => + { + try { - Logger.Write("Desktop switch failed during input processing."); + if (!Win32Interop.SwitchToInputDesktop()) + { + Logger.Write("Desktop switch failed during input processing."); - // Thread likely has hooks in current desktop. SendKeys will create one with no way to unhook it. - // Start a new thread for processing input. - StartInputProcessingThread(); - return; + // Thread likely has hooks in current desktop. SendKeys will create one with no way to unhook it. + // Start a new thread for processing input. + StartInputProcessingThread(); + return; + } + inputAction(); } - inputAction(); - } - catch (Exception ex) - { - Logger.Write(ex); - } - }); + catch (Exception ex) + { + Logger.Write(ex); + } + }); + } } } } diff --git a/Desktop.Win/Services/ScreenCapturerWin.cs b/Desktop.Win/Services/ScreenCapturerWin.cs index b31c235ae..e78b9387a 100644 --- a/Desktop.Win/Services/ScreenCapturerWin.cs +++ b/Desktop.Win/Services/ScreenCapturerWin.cs @@ -43,7 +43,7 @@ public class ScreenCapturerWin : IScreenCapturer { private readonly Dictionary _bitBltScreens = new(); private readonly Dictionary _directxScreens = new(); - private readonly SemaphoreSlim _screenCaptureLock = new(1,1); + private readonly object _screenBoundsLock = new(); public ScreenCapturerWin() { @@ -53,8 +53,14 @@ public ScreenCapturerWin() public event EventHandler ScreenChanged; + private enum GetDirectXFrameResult + { + Success, + Failure, + Timeout, + } + public bool CaptureFullscreen { get; set; } = true; - public Rectangle CurrentScreenBounds { get; private set; } = Screen.PrimaryScreen.Bounds; public bool NeedsInit { get; set; } = true; public string SelectedScreen { get; private set; } = Screen.PrimaryScreen.DeviceName; public void Dispose() @@ -67,56 +73,62 @@ public void Dispose() } catch { } } + public Rectangle CurrentScreenBounds { get; private set; } = Screen.PrimaryScreen.Bounds; + public IEnumerable GetDisplayNames() => Screen.AllScreens.Select(x => x.DeviceName); public Bitmap GetNextFrame() { - try + lock (_screenBoundsLock) { - _screenCaptureLock.Wait(); - - if (NeedsInit) - { - Logger.Write("Init needed in GetNextFrame."); - Init(); - } - - // Sometimes DX will result in a timeout, even when there are changes - // on the screen. I've observed this when a laptop lid is closed, or - // on some machines that aren't connected to a monitor. This will - // have it fall back to BitBlt in those cases. - // TODO: Make DX capture work with changed screen orientation. - if (_directxScreens.ContainsKey(SelectedScreen) && - SystemInformation.ScreenOrientation != ScreenOrientation.Angle270 && - SystemInformation.ScreenOrientation != ScreenOrientation.Angle90) + try { - var (result, frame) = GetDirectXFrame(); + if (NeedsInit) + { + Logger.Write("Init needed in GetNextFrame."); + Init(); + } - if (result == GetDirectXFrameResult.Success || - result == GetDirectXFrameResult.Timeout) + // Sometimes DX will result in a timeout, even when there are changes + // on the screen. I've observed this when a laptop lid is closed, or + // on some machines that aren't connected to a monitor. This will + // have it fall back to BitBlt in those cases. + // TODO: Make DX capture work with changed screen orientation. + if (_directxScreens.ContainsKey(SelectedScreen) && + SystemInformation.ScreenOrientation != ScreenOrientation.Angle270 && + SystemInformation.ScreenOrientation != ScreenOrientation.Angle90) { - return frame; + var (result, frame) = GetDirectXFrame(); + + if (result == GetDirectXFrameResult.Success) + { + return frame; + } } - } - return GetBitBltFrame(); + return GetBitBltFrame(); + } + catch (Exception e) + { + Logger.Write(e); + NeedsInit = true; + } + return null; } - catch (Exception e) - { - Logger.Write(e); - NeedsInit = true; - } - finally - { - _screenCaptureLock.Release(); - } - return null; + } public int GetScreenCount() => Screen.AllScreens.Length; - public int GetSelectedScreenIndex() => _bitBltScreens[SelectedScreen]; + public int GetSelectedScreenIndex() + { + if (_bitBltScreens.TryGetValue(SelectedScreen, out var index)) + { + return index; + } + return 0; + } public Rectangle GetVirtualScreenBounds() => SystemInformation.VirtualScreen; @@ -133,20 +145,23 @@ public void Init() public void SetSelectedScreen(string displayName) { - if (displayName == SelectedScreen) + lock (_screenBoundsLock) { - return; - } + if (displayName == SelectedScreen) + { + return; + } - if (_bitBltScreens.ContainsKey(displayName)) - { - SelectedScreen = displayName; - } - else - { - SelectedScreen = _bitBltScreens.Keys.First(); + if (_bitBltScreens.ContainsKey(displayName)) + { + SelectedScreen = displayName; + } + else + { + SelectedScreen = _bitBltScreens.Keys.First(); + } + RefreshCurrentScreenBounds(); } - RefreshCurrentScreenBounds(); } private void ClearDirectXOutputs() @@ -182,14 +197,6 @@ private Bitmap GetBitBltFrame() return null; } - - private enum GetDirectXFrameResult - { - Success, - Failure, - Timeout, - } - private (GetDirectXFrameResult result, Bitmap frame) GetDirectXFrame() { try diff --git a/Desktop.Win/ViewModels/FileTransferWindowViewModel.cs b/Desktop.Win/ViewModels/FileTransferWindowViewModel.cs index 8a835b60c..989e5558b 100644 --- a/Desktop.Win/ViewModels/FileTransferWindowViewModel.cs +++ b/Desktop.Win/ViewModels/FileTransferWindowViewModel.cs @@ -111,7 +111,7 @@ public async Task UploadFile(string filePath) FileUploads.Add(fileUpload); }); - await _fileTransferService.UploadFile(fileUpload, _viewer, (double progress) => + await _fileTransferService.UploadFile(fileUpload, _viewer, fileUpload.CancellationTokenSource.Token, (double progress) => { App.Current.Dispatcher.Invoke(() => fileUpload.PercentProgress = progress); }); @@ -122,6 +122,7 @@ await _fileTransferService.UploadFile(fileUpload, _viewer, (double progress) => if (param is FileUpload fileUpload) { FileUploads.Remove(fileUpload); + fileUpload.CancellationTokenSource.Cancel(); } }); } diff --git a/README.md b/README.md index 6017dfc75..683fe6fc6 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ It's *highly* encouraged that you get comfortable building and deploying from so ## Build Instructions (GitHub) GitHub Actions allows you to build and deploy Remotely for free from their cloud servers. The definitions for the build processes are located in `/.github/workflows/` folder. -After forking the repo, follow the instructions in the workflow YML file. The easiest workflow to use is the Build.yml worfklow, and I'd recommend starting with that one. It will produce a build artifact (ZIP package) identical to what is on the Releases page, only the clients will have your server URL hard-coded. +After forking the repo, follow the instructions in the workflow YML file. The easiest workflow to use is the Build.yml worfklow, and I'd recommend starting with that one. It will produce a build artifact (ZIP package) identical to what was on the Releases page, only the clients will have your server URL hard-coded. ### Instructions for using the Build workflow: - Fork the repo if you haven't already. @@ -49,18 +49,17 @@ After forking the repo, follow the instructions in the workflow YML file. The e - If you're going to host on Windows, change the Server Runtime Identifier to `win-x64`. - Click "Run workflow". - When it's finished, there will be a build artifact for download that contains the server and clients. -- Download the ZIP file and extract the files to the location where your site will be hosted (e.g. `/var/www/remotely`). -- Run the install script located in the folder (e.g. `Ubuntu_Server_Install.sh`). + ## Hosting a Server (Windows) -* Create a site in IIS that will run Remotely. -* Run Install-RemotelyServer.ps1 (as an administrator), which is on the [Releases page](https://github.com/lucent-sea/Remotely/releases/latest) and in the [Utilities folder in source control](https://raw.githubusercontent.com/lucent-sea/Remotely/master/Utilities/Install-RemotelyServer.ps1). - * Alternatively, you can build from source and copy the server files to the site folder. +- Download the ZIP file and extract the files to the location where your site will be hosted (e.g. `/var/www/remotely`). +- Run the install script located in the folder (e.g. `Ubuntu_Server_Install.sh`). +- In the site's content directory, make a copy of the `appsettings.json` file and name it `appsettings.Production.json`. + - The server will use this new file for reading/writing its settings, and it won't be overwritten by future ugprades. * Download and install the latest .NET Runtime (not the SDK) with the Hosting Bundle. * Link: https://dotnet.microsoft.com/download/dotnet-core/current/runtime * This includes the Hosting Bundle for Windows, which allows you to run ASP.NET Core in IIS. * Important: If you installed .NET Runtime before installing all the required IIS features, you may need to run a repair on the .NET Runtime installation. -* Change values in appsettings.json for your environment. Make a copy named `appsettings.Production.json` (see Configuration section below). * By default, SQLite is used for the database. * The "Remotely.db" database file is automatically created in the root folder of your site. * You can browse and modify the contents using [DB Browser for SQLite](https://sqlitebrowser.org/). @@ -73,18 +72,19 @@ After forking the repo, follow the instructions in the workflow YML file. The e ## Hosting a Server (Ubuntu) * **IMPORTANT**: Recently, the default web server was switched from Nginx to Caddy Server. They cannot both be used on the same box. If you want to continue using Nginx, you'll need to set up the configuration manually. See the `Example_Nginx_Config.txt` file in the `Utilities` folder for an example. * Ubuntu 20.04, 19.04, and 18.04 have been tested. -* Run Ubuntu_Server_Install.sh (with sudo), which is on the [Releases page](https://github.com/lucent-sea/Remotely/releases/latest) and in the [Utilities folder in source control](https://raw.githubusercontent.com/lucent-sea/Remotely/master/Utilities/Ubuntu_Server_Install.sh). - * The script is designed to install Remotely and Caddy on the same server, running Ubuntu 20.04. You'll need to manually set up other configurations. - * A helpful user supplied an example Apache configuration, which can be found in the Utilities folder. - * The script will prompt for the "App root" location, which is the above directory where the server files are located. - * The script installs the .NET runtime, as well as other dependencies. - * Certbot is used in this script and will install an SSL certificate for your site. Your server needs to have a public domain name that is accessible from the internet for this to work. - * More information: https://letsencrypt.org/, https://certbot.eff.org/ - * Alternatively, you can build from source (using RuntimeIdentifier "linux-x64" for the server) and copy the server files to the site folder. * Change values in appsettings.json for your environment. Make a copy named `appsettings.Production.json` (see Configuration section below). * Documentation for hosting behind Nginx can be found here: https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-nginx * There is no default account. You must create the first one via the Register page, which will create an account that is both a server and organization admin. +## Upgrading +* To upgrade a server, do any of the below to copy the new Server application files. + * Run one of the GitHub Actions workflows, then copy the ZIP contents to the site's content folder. + * Build from source as described above and `rsync`/`robocopy` the output files to the server directory. + * Build from source and deploy to IIS (e.g. `dotnet publish /p:PublishProfile=MyProfile`) +* For Linux, you'll need to restart the Remotely service in systemd after overwriting the files. +* For Windows, you'll need to shut down the site's Application Pool in IIS before copying the files. + * Windows won't let you overwrite files that are in use. +* The only things that shouldn't be overwritten are the database DB file (if using SQLite) and the `appsettings.Production.json`. These files should never exist in the publish output. ## Hosting Scenarios There are countless ways to host an ASP.NET Core app, and I can't document or automate all of them. For hosting scenarios aside from the above two, please refer to Microsoft's documentation. @@ -113,13 +113,6 @@ The first account created will be an admin for both the server and the organizat An organization admin has access to the Organization page and server log entries specific to his/her organization. A server admin has access to the Server Config page and can see server log entries that don't belong to an organization. -## Upgrading -* To upgrade a server, do any of the below to copy the new Server application files. - * Run one of the GitHub Actions workflows. - * Build from source as described above and `rsync`/`robocopy` the output files to the server directory. - * Build from source and deploy to IIS (e.g. `dotnet publish /p:PublishProfile=MyProfile`) -* For Linux, you'll also need to restart the Remotely service in systemd after overwriting the files. -* The only things that can't be overwritten are the database DB file (if using SQLite) and the `appsettings.Production.json`. These files should never exist in the publish output. ## Branding Within the Account section, there is a tab for branding, which will apply to the quick support clients and Windows installer. @@ -182,6 +175,7 @@ You can change database by changing `DBProvider` in `ApplicationOptions` to `SQL * Linux: Only Ubuntu 18.04+ is tested. * For the Ubuntu's "quick support" client, you must first install the following dependencies: * libx11-dev + * libxrandr-dev * libc6-dev * libgdiplus * libxtst-dev diff --git a/Server/Components/Devices/ChatCard.razor.cs b/Server/Components/Devices/ChatCard.razor.cs index 124f878d6..b2174cf07 100644 --- a/Server/Components/Devices/ChatCard.razor.cs +++ b/Server/Components/Devices/ChatCard.razor.cs @@ -34,6 +34,11 @@ public void Dispose() GC.SuppressFinalize(this); } + protected override void OnAfterRender(bool firstRender) + { + JsInterop.ScrollToEnd(_chatMessagesWindow); + base.OnAfterRender(firstRender); + } protected override Task OnInitializedAsync() { AppState.PropertyChanged += AppState_PropertyChanged; @@ -85,9 +90,9 @@ private void CircuitConnection_MessageReceived(object sender, Models.CircuitEven session.MissedChats++; } - JsInterop.ScrollToEnd(_chatMessagesWindow); - InvokeAsync(StateHasChanged); + + JsInterop.ScrollToEnd(_chatMessagesWindow); } } } diff --git a/Server/Pages/ServerLogs.razor b/Server/Pages/ServerLogs.razor index 5d2e4b45f..cf0dcab5e 100644 --- a/Server/Pages/ServerLogs.razor +++ b/Server/Pages/ServerLogs.razor @@ -34,7 +34,7 @@
Type:
- @foreach (var eventType in Enum.GetValues(typeof(EventType))) { diff --git a/Server/Server.csproj b/Server/Server.csproj index 524624962..803daa92e 100644 --- a/Server/Server.csproj +++ b/Server/Server.csproj @@ -18,19 +18,26 @@ - - - - - - - + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + diff --git a/Server/wwwroot/Content/Install-Ubuntu-x64.sh b/Server/wwwroot/Content/Install-Ubuntu-x64.sh index 419aba80c..5e45bfd33 100644 --- a/Server/wwwroot/Content/Install-Ubuntu-x64.sh +++ b/Server/wwwroot/Content/Install-Ubuntu-x64.sh @@ -32,6 +32,7 @@ apt-get -y install dotnet-runtime-5.0 rm packages-microsoft-prod.deb apt-get -y install libx11-dev +apt-get -y install libxrandr-dev apt-get -y install unzip apt-get -y install libc6-dev apt-get -y install libgdiplus diff --git a/Shared/Shared.csproj b/Shared/Shared.csproj index 84b2fd6f8..877a8edd0 100644 --- a/Shared/Shared.csproj +++ b/Shared/Shared.csproj @@ -10,9 +10,9 @@ - + - + diff --git a/Shared/Utilities/Logger.cs b/Shared/Utilities/Logger.cs index 780947b85..27111975f 100644 --- a/Shared/Utilities/Logger.cs +++ b/Shared/Utilities/Logger.cs @@ -25,7 +25,7 @@ public static void Debug(string message, [CallerMemberName] string callerName = { CheckLogFileExists(); - File.AppendAllText(LogPath, $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t[Debug]\t-[{callerName}]\t{message}{Environment.NewLine}"); + File.AppendAllText(LogPath, $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t[Debug]\t[{callerName}]\t{message}{Environment.NewLine}"); } System.Diagnostics.Debug.WriteLine(message); @@ -62,7 +62,7 @@ public static void Write(string message, EventType eventType = EventType.Info, [ WriteLock.Wait(); CheckLogFileExists(); - File.AppendAllText(LogPath, $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t[{eventType}]\t-[{callerName}]\t{message}{Environment.NewLine}"); + File.AppendAllText(LogPath, $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t[{eventType}]\t[{callerName}]\t{message}{Environment.NewLine}"); Console.WriteLine(message); } catch { } @@ -84,7 +84,7 @@ public static void Write(Exception ex, EventType eventType = EventType.Error, [C while (exception != null) { - File.AppendAllText(LogPath, $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t[{eventType}]\t-[{callerName}]\t{exception?.Message}\t{exception?.StackTrace}\t{exception?.Source}{Environment.NewLine}"); + File.AppendAllText(LogPath, $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t[{eventType}]\t[{callerName}]\t{exception?.Message}\t{exception?.StackTrace}\t{exception?.Source}{Environment.NewLine}"); Console.WriteLine(exception.Message); exception = exception.InnerException; } diff --git a/Tests.LoadTester/Tests.LoadTester.csproj b/Tests.LoadTester/Tests.LoadTester.csproj index e923c7e38..8987b5a85 100644 --- a/Tests.LoadTester/Tests.LoadTester.csproj +++ b/Tests.LoadTester/Tests.LoadTester.csproj @@ -8,7 +8,7 @@ - + diff --git a/Tests/ManualTests.cs b/Tests/ManualTests.cs index 285760132..e6aea3bc2 100644 --- a/Tests/ManualTests.cs +++ b/Tests/ManualTests.cs @@ -74,10 +74,10 @@ public async Task EncodingSizeTests() { await Task.Delay(5000); var screen = _capturer.GetNextFrame(); - var a = ImageUtils.EncodeWithSkia(screen, SkiaSharp.SKEncodedImageFormat.Jpeg, 70); - Debug.WriteLine("JPEG Size: " + a.Length.ToString("N0")); - var b = ImageUtils.EncodeWithSkia(screen, SkiaSharp.SKEncodedImageFormat.Webp, 70); - Debug.WriteLine("WEBP Size: " + b.Length.ToString("N0")); + //var a = ImageUtils.EncodeWithSkia(screen, SkiaSharp.SKEncodedImageFormat.Jpeg, 70); + //Debug.WriteLine("JPEG Size: " + a.Length.ToString("N0")); + //var b = ImageUtils.EncodeWithSkia(screen, SkiaSharp.SKEncodedImageFormat.Webp, 70); + //Debug.WriteLine("WEBP Size: " + b.Length.ToString("N0")); } [TestMethod] diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 5bcef4b6d..c7959f149 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -23,9 +23,9 @@ - - - + + +