using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.Logging; using Remotely.Agent.Interfaces; using Remotely.Shared.Extensions; using Remotely.Shared.Models; using Remotely.Shared.Primitives; using Remotely.Shared.Services; using Remotely.Shared.Utilities; using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace Remotely.Agent.Services.Linux; public class AppLauncherLinux : IAppLauncher { private readonly ConnectionInfo _connectionInfo; private readonly ILogger _logger; private readonly IProcessInvoker _processInvoker; private readonly string _rcBinaryPath = Path.Combine( AppDomain.CurrentDomain.BaseDirectory, "Desktop", EnvironmentHelper.DesktopExecutableFileName); public AppLauncherLinux( IConfigService configService, IProcessInvoker processInvoker, ILogger logger) { _processInvoker = processInvoker; _connectionInfo = configService.GetConnectionInfo(); _logger = logger; } public async Task LaunchChatService(string pipeName, string userConnectionId, string requesterName, string orgName, string orgId, HubConnection hubConnection) { try { if (!File.Exists(_rcBinaryPath)) { await hubConnection.SendAsync("DisplayMessage", "Chat executable not found on target device.", "Executable not found on device.", "bg-danger", userConnectionId); } // Start Desktop app. await hubConnection.SendAsync("DisplayMessage", $"Starting chat service.", "Starting chat service.", "bg-success", userConnectionId); var args = _rcBinaryPath + $" --mode Chat" + $" --host \"{_connectionInfo.Host}\"" + $" --pipe-name {pipeName}" + $" --requester-name \"{requesterName}\"" + $" --org-name \"{orgName}\"" + $" --org-id \"{orgId}\""; return StartLinuxDesktopApp(args); } catch (Exception ex) { _logger.LogError(ex, "Error while starting chat."); await hubConnection.SendAsync("DisplayMessage", "Chat service failed to start on target device.", "Failed to start chat service.", "bg-danger", userConnectionId); } return -1; } public async Task LaunchRemoteControl(int targetSessionId, string sessionId, string accessKey, string userConnectionId, string requesterName, string orgName, string orgId, HubConnection hubConnection) { try { if (!File.Exists(_rcBinaryPath)) { await hubConnection.SendAsync("DisplayMessage", "Remote control executable not found on target device.", "Executable not found on device.", "bg-danger", userConnectionId); return; } // Start Desktop app. await hubConnection.SendAsync("DisplayMessage", "Starting remote control.", "Starting remote control.", "bg-success", userConnectionId); var args = _rcBinaryPath + $" --mode Unattended" + $" --host {_connectionInfo.Host}" + $" --requester-name \"{requesterName}\"" + $" --org-name \"{orgName}\"" + $" --org-id \"{orgId}\"" + $" --session-id \"{sessionId}\"" + $" --access-key \"{accessKey}\""; StartLinuxDesktopApp(args); } catch (Exception ex) { _logger.LogError(ex, "Error while launching remote control."); await hubConnection.SendAsync("DisplayMessage", "Remote control failed to start on target device.", "Failed to start remote control.", "bg-danger", userConnectionId); } } public async Task RestartScreenCaster(string[] viewerIds, string sessionId, string accessKey, string userConnectionId, string requesterName, string orgName, string orgId, HubConnection hubConnection, int targetSessionID = -1) { try { var args = _rcBinaryPath + $" --mode Unattended" + $" --host {_connectionInfo.Host}" + $" --requester-name \"{requesterName}\"" + $" --org-name \"{orgName}\"" + $" --org-id \"{orgId}\"" + $" --session-id \"{sessionId}\"" + $" --access-key \"{accessKey}\""; StartLinuxDesktopApp(args); } catch (Exception ex) { await hubConnection.SendAsync("SendConnectionFailedToViewers", viewerIds); _logger.LogError(ex, "Error while restarting screen caster."); throw; } } private int StartLinuxDesktopApp(string args) { var xdisplay = ":0"; var xauthority = string.Empty; var xResult = TryGetXAuth("Xorg"); if (!xResult.IsSuccess) { // If running Wayland, this still ends up still being unusable. // This X server will only provide a black screen with any apps // launched within the display it's using, but the display won't // show anything being rendered by the Wayland compositor. It's // better than simply crashing, though, so I'll leave it here // until Wayland support is added. xResult = TryGetXAuth("Xwayland"); } if (xResult.IsSuccess) { xdisplay = xResult.Value.XDisplay; xauthority = xResult.Value.XAuthority; } else { _logger.LogError("Failed to get X server auth."); } var whoString = _processInvoker.InvokeProcessOutput("who", "")?.Trim(); var username = ""; if (!string.IsNullOrWhiteSpace(whoString)) { try { var whoLines = whoString.Split('\n', StringSplitOptions.RemoveEmptyEntries); var whoLine = whoLines.FirstOrDefault(x => Regex.IsMatch( x.Split(" ", StringSplitOptions.RemoveEmptyEntries).Last(), @"\(:[\d]*\)")); if (!string.IsNullOrWhiteSpace(whoLine)) { var whoSplit = whoLine.Split(' ', StringSplitOptions.RemoveEmptyEntries); username = whoSplit[0]; xdisplay = whoSplit.Last().TrimStart('(').TrimEnd(')'); xauthority = $"/home/{username}/.Xauthority"; args = $"-u {username} {args}"; } } catch (Exception ex) { _logger.LogError(ex, "Error while getting current X11 user."); } } var psi = new ProcessStartInfo() { FileName = "sudo", Arguments = args }; psi.Environment.Add("DISPLAY", xdisplay); psi.Environment.Add("XAUTHORITY", xauthority); _logger.LogInformation( "Attempting to launch screen caster with username {username}, xauthority {xauthority}, display {display}, and args {args}.", username, xauthority, xdisplay, args); return Process.Start(psi)?.Id ?? throw new InvalidOperationException("Failed to launch desktop app."); } private Result TryGetXAuth(string xServerProcess) { try { var xdisplay = ":0"; var xauthority = string.Empty; var xprocess = _processInvoker .InvokeProcessOutput("ps", $"-C {xServerProcess} -f") ?.Split(Environment.NewLine) ?.FirstOrDefault(x => x.Contains(" -auth ")); if (string.IsNullOrWhiteSpace(xprocess)) { _logger.LogInformation("{xServerProcess} process not found.", xServerProcess); return Result.Fail($"{xServerProcess} process not found."); } _logger.LogInformation("Resolved X server process: {xprocess}", xprocess); var xprocSplit = xprocess .Split(" ", StringSplitOptions.RemoveEmptyEntries) .ToList(); var xProcIndex = xprocSplit.IndexWhere(x => x.EndsWith(xServerProcess)); if (xProcIndex > -1 && xprocSplit[xProcIndex + 1].StartsWith(":")) { xdisplay = xprocSplit[xProcIndex + 1]; } var authIndex = xprocSplit.IndexOf("-auth"); if (authIndex > -1) { xauthority = xprocSplit[authIndex + 1]; } return Result.Ok(new XAuthInfo(xdisplay, xauthority)); } catch (Exception ex) { _logger.LogError(ex, "Error while geting X auth for {xServerProcess}.", xServerProcess); return Result.Fail($"Error while geting X auth for {xServerProcess}."); } } private record XAuthInfo(string XDisplay, string XAuthority); }