Extract remote control functionality into separate library. (#539)
* Convert server to new single-file startup model. * Add remote control implementations. * Implement IViewerAuthorizer. * Update hub endpoints. * Implement HubEventHandler. * Implement ViewerHubDataProvider. * Implement page data provider. * Implement RCL and refactor. * Update submodule. * Replace submodule with NuGet. * Update copy URL. * Update NuGet. * Remove deprecated WebRTC. * Remove deprecated WebRTC. * Update Immense.RemoteControl * Building out desktop projects. * Bring more services into submodule. * Update submodule. * Update submodule. * Refactoring for module. * Update submodule. * Update submodule * Got Windows desktop app running. * Refactor for submodule changes. * FIx unattended session start. * Switch desktop app out of console mode. * Fix tests. * Update publishing. * Remove ClickOnce middleware. * Remove ClickOnce remnants. * Update submodule * Add some logging. * Update Linux path. * Update submodule. * Add cleanup service for unattended sessions that failed to start. * Update submodule. * Fix chat. * Add ValidateExecutableReferencesMatchSelfContained property. * Add other submodule projects. Align checkbox. * Update submodule. Reduce deserialization in the browser, resulting in faster renders. * Update submodule. * Update submodule. * Update submodule. * Update submodule. * Add orgId back for branding. * Get branding loading in desktop apps. * Update submodule. * Create log dir. * Refactor version check on config page. * Update submodule. * Update submodule. * Change submodule URL. * Correct namespace. * Update submodule. * Checkout submodules recursively.
108
.editorconfig
@ -1,4 +1,4 @@
|
||||
[*.cs]
|
||||
[ "*.cs" ]
|
||||
|
||||
# CA1416: Validate platform compatibility
|
||||
dotnet_diagnostic.CA1416.severity = none
|
||||
@ -7,4 +7,108 @@ dotnet_diagnostic.CA1416.severity = none
|
||||
dotnet_diagnostic.CS1591.severity = none
|
||||
csharp_style_var_for_built_in_types=true:silent
|
||||
csharp_style_var_when_type_is_apparent=true:silent
|
||||
csharp_style_var_elsewhere=true:silent
|
||||
csharp_style_var_elsewhere=true:silent
|
||||
|
||||
# CA1822: Mark members as static
|
||||
dotnet_diagnostic.CA1822.severity = none
|
||||
csharp_using_directive_placement = outside_namespace:silent
|
||||
csharp_prefer_simple_using_statement = true:suggestion
|
||||
csharp_prefer_braces = true:silent
|
||||
csharp_style_namespace_declarations = block_scoped:silent
|
||||
csharp_style_prefer_method_group_conversion = true:silent
|
||||
csharp_style_prefer_top_level_statements = true:silent
|
||||
csharp_style_expression_bodied_methods = false:silent
|
||||
csharp_style_expression_bodied_constructors = false:silent
|
||||
csharp_style_expression_bodied_operators = false:silent
|
||||
csharp_style_expression_bodied_properties = true:silent
|
||||
csharp_style_expression_bodied_indexers = true:silent
|
||||
csharp_style_expression_bodied_accessors = true:silent
|
||||
csharp_style_expression_bodied_lambdas = true:silent
|
||||
csharp_style_expression_bodied_local_functions = false:silent
|
||||
csharp_indent_labels = one_less_than_current
|
||||
|
||||
[*.{cs,vb}]
|
||||
#### Naming styles ####
|
||||
|
||||
# Naming rules
|
||||
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
|
||||
|
||||
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
|
||||
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
|
||||
|
||||
# Symbol specifications
|
||||
|
||||
dotnet_naming_symbols.interface.applicable_kinds = interface
|
||||
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.interface.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
|
||||
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.types.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
||||
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.non_field_members.required_modifiers =
|
||||
|
||||
# Naming styles
|
||||
|
||||
dotnet_naming_style.begins_with_i.required_prefix = I
|
||||
dotnet_naming_style.begins_with_i.required_suffix =
|
||||
dotnet_naming_style.begins_with_i.word_separator =
|
||||
dotnet_naming_style.begins_with_i.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
|
||||
dotnet_style_prefer_auto_properties = true:silent
|
||||
dotnet_style_object_initializer = true:suggestion
|
||||
dotnet_style_collection_initializer = true:suggestion
|
||||
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
|
||||
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
|
||||
dotnet_style_prefer_conditional_expression_over_return = true:silent
|
||||
dotnet_style_explicit_tuple_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_tuple_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
|
||||
dotnet_style_prefer_compound_assignment = true:suggestion
|
||||
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||
tab_width = 4
|
||||
indent_size = 4
|
||||
end_of_line = crlf
|
||||
dotnet_style_prefer_simplified_interpolation = true:suggestion
|
||||
|
||||
# CA1822: Mark members as static
|
||||
dotnet_diagnostic.CA1822.severity = silent
|
||||
[*.cs]
|
||||
csharp_indent_labels = one_less_than_current
|
||||
csharp_using_directive_placement = outside_namespace:silent
|
||||
csharp_prefer_simple_using_statement = true:suggestion
|
||||
csharp_prefer_braces = true:silent
|
||||
csharp_style_namespace_declarations = block_scoped:silent
|
||||
csharp_style_prefer_method_group_conversion = true:silent
|
||||
csharp_style_prefer_top_level_statements = true:silent
|
||||
csharp_style_expression_bodied_methods = false:silent
|
||||
csharp_style_expression_bodied_constructors = false:silent
|
||||
csharp_style_expression_bodied_operators = false:silent
|
||||
csharp_style_expression_bodied_properties = true:silent
|
||||
csharp_style_expression_bodied_indexers = true:silent
|
||||
csharp_style_expression_bodied_accessors = true:silent
|
||||
csharp_style_expression_bodied_lambdas = true:silent
|
||||
csharp_style_expression_bodied_local_functions = false:silent
|
||||
csharp_prefer_static_local_function = true:suggestion
|
||||
5
.github/workflows/build.yml
vendored
@ -40,6 +40,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install .NET Core
|
||||
uses: actions/setup-dotnet@v1.9.0
|
||||
@ -109,7 +111,8 @@ jobs:
|
||||
with:
|
||||
# Comment out the below 'repository' line if you want to build from
|
||||
# your fork instead of the author's.
|
||||
repository: lucent-sea/Remotely
|
||||
repository: immense/Remotely
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
# Test the Server URL to make sure it's valid
|
||||
|
||||
1
.github/workflows/run_tests.yml
vendored
@ -27,6 +27,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
# Install the .NET Core workload
|
||||
|
||||
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "submodules/Immense.RemoteControl"]
|
||||
path = submodules/Immense.RemoteControl
|
||||
url = git@github.com:immense/RemoteControl.git
|
||||
@ -122,15 +122,15 @@
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</ApplicationDefinition>
|
||||
<Compile Include="..\Shared\AppConstants.cs">
|
||||
<Link>AppConstants.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\Shared\Models\ConnectionInfo.cs">
|
||||
<Link>Models\ConnectionInfo.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\Shared\Models\DeviceSetupOptions.cs">
|
||||
<Link>Models\DeviceSetupOptions.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\Shared\Utilities\AppConstants.cs">
|
||||
<Link>Utilities\AppConstants.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="Models\BrandingInfo.cs" />
|
||||
<Compile Include="Utilities\CommandLineParser.cs" />
|
||||
<Compile Include="Utilities\MessageBoxEx.cs" />
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
using Remotely.Agent.Installer.Win.Models;
|
||||
using Remotely.Agent.Installer.Win.Services;
|
||||
using Remotely.Agent.Installer.Win.Utilities;
|
||||
using Remotely.Shared.Utilities;
|
||||
using Remotely.Shared.Models;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
@ -18,6 +17,7 @@ using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Net;
|
||||
using Remotely.Shared;
|
||||
|
||||
namespace Remotely.Agent.Installer.Win.ViewModels
|
||||
{
|
||||
@ -34,7 +34,7 @@ namespace Remotely.Agent.Installer.Win.ViewModels
|
||||
|
||||
private int _progress;
|
||||
|
||||
private string _serverUrl;
|
||||
private string _serverUrl = AppConstants.ServerUrl;
|
||||
|
||||
private string _statusMessage;
|
||||
public MainWindowViewModel()
|
||||
|
||||
@ -23,16 +23,16 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="6.0.6" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="6.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.EventLog" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.6" />
|
||||
<PackageReference Include="Microsoft.PowerShell.SDK" Version="7.2.5" />
|
||||
<PackageReference Include="Microsoft.WSMan.Management" Version="7.2.5" />
|
||||
<PackageReference Include="Microsoft.WSMan.Runtime" Version="7.2.5" />
|
||||
<PackageReference Include="System.Management.Automation" Version="7.2.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.9" />
|
||||
<PackageReference Include="Microsoft.PowerShell.SDK" Version="7.2.6" />
|
||||
<PackageReference Include="Microsoft.WSMan.Management" Version="7.2.6" />
|
||||
<PackageReference Include="Microsoft.WSMan.Runtime" Version="7.2.6" />
|
||||
<PackageReference Include="System.Management.Automation" Version="7.2.6" />
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@ -6,8 +6,8 @@ namespace Remotely.Agent.Interfaces
|
||||
{
|
||||
public interface IAppLauncher
|
||||
{
|
||||
Task<int> LaunchChatService(string orgName, string requesterID, HubConnection hubConnection);
|
||||
Task LaunchRemoteControl(int targetSessionId, string requesterID, string serviceID, HubConnection hubConnection);
|
||||
Task RestartScreenCaster(List<string> viewerIDs, string serviceID, string requesterID, HubConnection hubConnection, int targetSessionID = -1);
|
||||
Task<int> LaunchChatService(string pipeName, string userConnectionId, string requesterName, string orgName, string orgId, HubConnection hubConnection);
|
||||
Task LaunchRemoteControl(int targetSessionId, string sessionId, string accessKey, string userConnectionId, string requesterName, string orgName, string orgId, HubConnection hubConnection);
|
||||
Task RestartScreenCaster(List<string> viewerIDs, string sessionId, string accessKey, string userConnectionId, string requesterName, string orgName, string orgId, HubConnection hubConnection, int targetSessionID = -1);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ using System.IO;
|
||||
using System.ServiceProcess;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace Remotely.Agent
|
||||
{
|
||||
@ -97,20 +98,10 @@ namespace Remotely.Agent
|
||||
SetWorkingDirectory();
|
||||
|
||||
|
||||
if (EnvironmentHelper.IsWindows &&
|
||||
if (OperatingSystem.IsWindows() &&
|
||||
Process.GetCurrentProcess().SessionId == 0)
|
||||
{
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ServiceBase.Run(new WindowsService());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex, "Failed to start service.", EventType.Warning);
|
||||
}
|
||||
});
|
||||
_ = Task.Run(StartService);
|
||||
}
|
||||
|
||||
await Services.GetRequiredService<IUpdater>().BeginChecking();
|
||||
@ -129,5 +120,18 @@ namespace Remotely.Agent
|
||||
var assemblyDir = Path.GetDirectoryName(assemblyPath);
|
||||
Directory.SetCurrentDirectory(assemblyDir);
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static void StartService()
|
||||
{
|
||||
try
|
||||
{
|
||||
ServiceBase.Run(new WindowsService());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex, "Failed to start service.", EventType.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,7 +65,7 @@ namespace Remotely.Agent.Services
|
||||
_connectionInfo = _configService.GetConnectionInfo();
|
||||
|
||||
_hubConnection = new HubConnectionBuilder()
|
||||
.WithUrl(_connectionInfo.Host + "/AgentHub")
|
||||
.WithUrl(_connectionInfo.Host + "/hubs/service")
|
||||
.AddMessagePackProtocol()
|
||||
.Build();
|
||||
|
||||
@ -178,10 +178,8 @@ namespace Remotely.Agent.Services
|
||||
|
||||
private void RegisterMessageHandlers()
|
||||
{
|
||||
// TODO: Remove possibility for circular dependencies in the future
|
||||
// by emitting these events so other services can listen for them.
|
||||
|
||||
_hubConnection.On("ChangeWindowsSession", async (string serviceID, string viewerID, int targetSessionID) =>
|
||||
_hubConnection.On("ChangeWindowsSession", async (string viewerConnectionId, string sessionId, string accessKey, string userConnectionId, string requesterName, string orgName, string orgId, int targetSessionID) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -191,7 +189,7 @@ namespace Remotely.Agent.Services
|
||||
return;
|
||||
}
|
||||
|
||||
await _appLauncher.RestartScreenCaster(new List<string>() { viewerID }, serviceID, viewerID, _hubConnection, targetSessionID);
|
||||
await _appLauncher.RestartScreenCaster(new List<string>() { viewerConnectionId }, sessionId, accessKey, userConnectionId, requesterName, orgName, orgId, _hubConnection, targetSessionID);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -199,7 +197,7 @@ namespace Remotely.Agent.Services
|
||||
}
|
||||
});
|
||||
|
||||
_hubConnection.On("Chat", async (string senderName, string message, string orgName, bool disconnected, string senderConnectionID) =>
|
||||
_hubConnection.On("Chat", async (string senderName, string message, string orgName, string orgId, bool disconnected, string senderConnectionID) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -209,7 +207,7 @@ namespace Remotely.Agent.Services
|
||||
return;
|
||||
}
|
||||
|
||||
await _chatService.SendMessage(senderName, message, orgName, disconnected, senderConnectionID, _hubConnection);
|
||||
await _chatService.SendMessage(senderName, message, orgName, orgId, disconnected, senderConnectionID, _hubConnection);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -342,7 +340,7 @@ namespace Remotely.Agent.Services
|
||||
}
|
||||
});
|
||||
|
||||
_hubConnection.On("RemoteControl", async (string requesterID, string serviceID) =>
|
||||
_hubConnection.On("RemoteControl", async (string sessionId, string accessKey, string userConnectionId, string requesterName, string orgName, string orgId) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -351,7 +349,7 @@ namespace Remotely.Agent.Services
|
||||
Logger.Write("Remote control attempted before server was verified.", EventType.Warning);
|
||||
return;
|
||||
}
|
||||
await _appLauncher.LaunchRemoteControl(-1, requesterID, serviceID, _hubConnection);
|
||||
await _appLauncher.LaunchRemoteControl(-1, sessionId, accessKey, userConnectionId, requesterName, orgName, orgId, _hubConnection);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -359,7 +357,7 @@ namespace Remotely.Agent.Services
|
||||
}
|
||||
});
|
||||
|
||||
_hubConnection.On("RestartScreenCaster", async (List<string> viewerIDs, string serviceID, string requesterID) =>
|
||||
_hubConnection.On("RestartScreenCaster", async (List<string> viewerIDs, string sessionId, string accessKey, string userConnectionId, string requesterName, string orgName, string orgId) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -368,7 +366,7 @@ namespace Remotely.Agent.Services
|
||||
Logger.Write("Remote control attempted before server was verified.", EventType.Warning);
|
||||
return;
|
||||
}
|
||||
await _appLauncher.RestartScreenCaster(viewerIDs, serviceID, requesterID, _hubConnection);
|
||||
await _appLauncher.RestartScreenCaster(viewerIDs, sessionId, accessKey, userConnectionId, requesterName, orgName, orgId, _hubConnection);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@ -8,7 +8,9 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web.Services.Description;
|
||||
|
||||
namespace Remotely.Agent.Services
|
||||
{
|
||||
@ -26,94 +28,6 @@ namespace Remotely.Agent.Services
|
||||
}
|
||||
|
||||
|
||||
public async Task<int> LaunchChatService(string orgName, string requesterID, 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",
|
||||
requesterID);
|
||||
}
|
||||
|
||||
|
||||
// Start Desktop app.
|
||||
await hubConnection.SendAsync("DisplayMessage", $"Starting chat service.", "Starting chat service.", "bg-success", requesterID);
|
||||
var args = $"{_rcBinaryPath} " +
|
||||
$"-mode Chat " +
|
||||
$"-requester \"{requesterID}\" " +
|
||||
$"-organization \"{orgName}\" " +
|
||||
$"-host \"{_connectionInfo.Host}\" " +
|
||||
$"-orgid \"{_connectionInfo.OrganizationID}\"";
|
||||
return StartLinuxDesktopApp(args);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
await hubConnection.SendAsync("DisplayMessage", "Chat service failed to start on target device.", "Failed to start chat service.", "bg-danger", requesterID);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public async Task LaunchRemoteControl(int targetSessionId, string requesterID, string serviceID, 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",
|
||||
requesterID);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Start Desktop app.
|
||||
await hubConnection.SendAsync("DisplayMessage", "Starting remote control.", "Starting remote control.", "bg-success", requesterID);
|
||||
var args = $"{_rcBinaryPath} " +
|
||||
$"-mode Unattended " +
|
||||
$"-requester \"{requesterID}\" " +
|
||||
$"-serviceid \"{serviceID}\" " +
|
||||
$"-deviceid {_connectionInfo.DeviceID} " +
|
||||
$"-host \"{_connectionInfo.Host}\" " +
|
||||
$"-orgid \"{_connectionInfo.OrganizationID}\"";
|
||||
StartLinuxDesktopApp(args);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
await hubConnection.SendAsync("DisplayMessage", "Remote control failed to start on target device.", "Failed to start remote control.", "bg-danger", requesterID);
|
||||
}
|
||||
}
|
||||
public async Task RestartScreenCaster(List<string> viewerIDs, string serviceID, string requesterID, HubConnection hubConnection, int targetSessionID = -1)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Start Desktop app.
|
||||
var args = $"{_rcBinaryPath} " +
|
||||
$"-mode Unattended " +
|
||||
$"-requester \"{requesterID}\" " +
|
||||
$"-serviceid \"{serviceID}\" " +
|
||||
$"-deviceid {_connectionInfo.DeviceID} " +
|
||||
$"-host \"{_connectionInfo.Host}\" " +
|
||||
$"-orgid \"{_connectionInfo.OrganizationID}\" " +
|
||||
$"-relaunch true " +
|
||||
$"-viewers {string.Join(",", viewerIDs)}";
|
||||
StartLinuxDesktopApp(args);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await hubConnection.SendAsync("SendConnectionFailedToViewers", viewerIDs);
|
||||
Logger.Write(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private int StartLinuxDesktopApp(string args)
|
||||
{
|
||||
var xauthority = GetXorgAuth();
|
||||
@ -180,5 +94,97 @@ namespace Remotely.Agent.Services
|
||||
catch { }
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public async Task<int> 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.Write(ex);
|
||||
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.Write(ex);
|
||||
await hubConnection.SendAsync("DisplayMessage", "Remote control failed to start on target device.", "Failed to start remote control.", "bg-danger", userConnectionId);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RestartScreenCaster(List<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.Write(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,20 +7,21 @@ namespace Remotely.Agent.Services
|
||||
{
|
||||
public class AppLauncherMac : IAppLauncher
|
||||
{
|
||||
public async Task<int> LaunchChatService(string orgName, string requesterID, HubConnection hubConnection)
|
||||
|
||||
public async Task<int> LaunchChatService(string pipeName, string userConnectionId, string requesterName, string orgName, string orgId, HubConnection hubConnection)
|
||||
{
|
||||
await hubConnection.SendAsync("DisplayMessage", "Feature under development.", "Feature is under development.", "bg-warning", requesterID);
|
||||
await hubConnection.SendAsync("DisplayMessage", "Feature under development.", "Feature is under development.", "bg-warning", userConnectionId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
public async Task LaunchRemoteControl(int targetSessionId, string requesterID, string serviceID, HubConnection hubConnection)
|
||||
public async Task LaunchRemoteControl(int targetSessionId, string sessionId, string accessKey, string userConnectionId, string requesterName, string orgName, string orgId, HubConnection hubConnection)
|
||||
{
|
||||
await hubConnection.SendAsync("DisplayMessage", "Feature under development.", "Feature is under development.", "bg-warning", requesterID);
|
||||
await hubConnection.SendAsync("DisplayMessage", "Feature under development.", "Feature is under development.", "bg-warning", userConnectionId);
|
||||
}
|
||||
|
||||
public async Task RestartScreenCaster(List<string> viewerIDs, string serviceID, string requesterID, HubConnection hubConnection, int targetSessionID = -1)
|
||||
public async Task RestartScreenCaster(List<string> viewerIDs, string sessionId, string accessKey, string userConnectionId, string requesterName, string orgName, string orgId, HubConnection hubConnection, int targetSessionID = -1)
|
||||
{
|
||||
await hubConnection.SendAsync("DisplayMessage", "Feature under development.", "Feature is under development.", "bg-warning", requesterID);
|
||||
await hubConnection.SendAsync("DisplayMessage", "Feature under development.", "Feature is under development.", "bg-warning", userConnectionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,43 +7,46 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Principal;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Remotely.Agent.Services
|
||||
{
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
public class AppLauncherWin : IAppLauncher
|
||||
{
|
||||
private readonly ConnectionInfo _connectionInfo;
|
||||
private readonly string _rcBinaryPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Desktop", EnvironmentHelper.DesktopExecutableFileName);
|
||||
|
||||
public AppLauncherWin(ConfigService configService)
|
||||
{
|
||||
ConnectionInfo = configService.GetConnectionInfo();
|
||||
_connectionInfo = configService.GetConnectionInfo();
|
||||
}
|
||||
|
||||
private ConnectionInfo ConnectionInfo { get; }
|
||||
|
||||
public async Task<int> LaunchChatService(string orgName, string requesterID, HubConnection hubConnection)
|
||||
public async Task<int> 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", requesterID);
|
||||
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", requesterID);
|
||||
await hubConnection.SendAsync("DisplayMessage", $"Starting chat service.", "Starting chat service.", "bg-success", userConnectionId);
|
||||
if (WindowsIdentity.GetCurrent().IsSystem)
|
||||
{
|
||||
var result = Win32Interop.OpenInteractiveProcess($"{_rcBinaryPath} " +
|
||||
$"-mode Chat " +
|
||||
$"-requester \"{requesterID}\" " +
|
||||
$"-organization \"{orgName}\" " +
|
||||
$"-host \"{ConnectionInfo.Host}\" " +
|
||||
$"-orgid \"{ConnectionInfo.OrganizationID}\"",
|
||||
var result = Win32Interop.OpenInteractiveProcess(
|
||||
_rcBinaryPath +
|
||||
$" --mode Chat" +
|
||||
$" --host \"{_connectionInfo.Host}\"" +
|
||||
$" --pipe-name {pipeName}" +
|
||||
$" --requester-name \"{requesterName}\"" +
|
||||
$" --org-name \"{orgName}\"" +
|
||||
$" --org-id \"{orgId}\"",
|
||||
targetSessionId: -1,
|
||||
forceConsoleSession: false,
|
||||
desktopName: "default",
|
||||
@ -55,7 +58,7 @@ namespace Remotely.Agent.Services
|
||||
"Chat service failed to start on target device.",
|
||||
"Failed to start chat service.",
|
||||
"bg-danger",
|
||||
requesterID);
|
||||
userConnectionId);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -64,12 +67,13 @@ namespace Remotely.Agent.Services
|
||||
}
|
||||
else
|
||||
{
|
||||
return Process.Start(_rcBinaryPath,
|
||||
$"-mode Chat " +
|
||||
$"-requester \"{requesterID}\" " +
|
||||
$"-organization \"{orgName}\" " +
|
||||
$"-host \"{ConnectionInfo.Host}\" " +
|
||||
$"-orgid \"{ConnectionInfo.OrganizationID}\"").Id;
|
||||
return Process.Start(_rcBinaryPath,
|
||||
$" --mode Chat" +
|
||||
$" --host \"{_connectionInfo.Host}\"" +
|
||||
$" --requester-name \"{userConnectionId}\"" +
|
||||
$" --org-name \"{orgName}\"" +
|
||||
$" --org-id \"{orgId}\"" +
|
||||
$" --pipe-name {pipeName}").Id;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -79,12 +83,12 @@ namespace Remotely.Agent.Services
|
||||
"Chat service failed to start on target device.",
|
||||
"Failed to start chat service.",
|
||||
"bg-danger",
|
||||
requesterID);
|
||||
userConnectionId);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public async Task LaunchRemoteControl(int targetSessionId, string requesterID, string serviceID, HubConnection hubConnection)
|
||||
public async Task LaunchRemoteControl(int targetSessionId, string sessionId, string accessKey, string userConnectionId, string requesterName, string orgName, string orgId, HubConnection hubConnection)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -94,7 +98,7 @@ namespace Remotely.Agent.Services
|
||||
"Remote control executable not found on target device.",
|
||||
"Executable not found on device.",
|
||||
"bg-danger",
|
||||
requesterID);
|
||||
userConnectionId);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -104,16 +108,18 @@ namespace Remotely.Agent.Services
|
||||
"Starting remote control.",
|
||||
"Starting remote control.",
|
||||
"bg-success",
|
||||
requesterID);
|
||||
userConnectionId);
|
||||
if (WindowsIdentity.GetCurrent().IsSystem)
|
||||
{
|
||||
var result = Win32Interop.OpenInteractiveProcess(_rcBinaryPath +
|
||||
$" -mode Unattended" +
|
||||
$" -requester \"{requesterID}\"" +
|
||||
$" -serviceid \"{serviceID}\"" +
|
||||
$" -deviceid {ConnectionInfo.DeviceID}" +
|
||||
$" -host {ConnectionInfo.Host}" +
|
||||
$" -orgid \"{ConnectionInfo.OrganizationID}\"",
|
||||
var result = Win32Interop.OpenInteractiveProcess(
|
||||
_rcBinaryPath +
|
||||
$" --mode Unattended" +
|
||||
$" --host {_connectionInfo.Host}" +
|
||||
$" --requester-name \"{requesterName}\"" +
|
||||
$" --org-name \"{orgName}\"" +
|
||||
$" --org-id \"{orgId}\"" +
|
||||
$" --session-id \"{sessionId}\"" +
|
||||
$" --access-key \"{accessKey}\"",
|
||||
targetSessionId: targetSessionId,
|
||||
forceConsoleSession: Shlwapi.IsOS(OsType.OS_ANYSERVER) && targetSessionId == -1,
|
||||
desktopName: "default",
|
||||
@ -125,19 +131,19 @@ namespace Remotely.Agent.Services
|
||||
"Remote control failed to start on target device.",
|
||||
"Failed to start remote control.",
|
||||
"bg-danger",
|
||||
requesterID);
|
||||
userConnectionId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// SignalR Connection IDs might start with a hyphen. We surround them
|
||||
// with quotes so the command line will be parsed correctly.
|
||||
Process.Start(_rcBinaryPath, $"-mode Unattended " +
|
||||
$"-requester \"{requesterID}\" " +
|
||||
$"-serviceid \"{serviceID}\" " +
|
||||
$"-deviceid {ConnectionInfo.DeviceID} " +
|
||||
$"-host {ConnectionInfo.Host} " +
|
||||
$"-orgid \"{ConnectionInfo.OrganizationID}\"");
|
||||
Process.Start(_rcBinaryPath,
|
||||
$" --mode Unattended" +
|
||||
$" --host {_connectionInfo.Host}" +
|
||||
$" --requester-name \"{requesterName}\"" +
|
||||
$" --org-name \"{orgName}\"" +
|
||||
$" --org-id \"{orgId}\"" +
|
||||
$" --session-id \"{sessionId}\"" +
|
||||
$" --access-key \"{accessKey}\"");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -147,10 +153,10 @@ namespace Remotely.Agent.Services
|
||||
"Remote control failed to start on target device.",
|
||||
"Failed to start remote control.",
|
||||
"bg-danger",
|
||||
requesterID);
|
||||
userConnectionId);
|
||||
}
|
||||
}
|
||||
public async Task RestartScreenCaster(List<string> viewerIDs, string serviceID, string requesterID, HubConnection hubConnection, int targetSessionID = -1)
|
||||
public async Task RestartScreenCaster(List<string> viewerIDs, string sessionId, string accessKey, string userConnectionId, string requesterName, string orgName, string orgId, HubConnection hubConnection, int targetSessionID = -1)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -162,14 +168,15 @@ namespace Remotely.Agent.Services
|
||||
await Task.Delay(1000);
|
||||
|
||||
var result = Win32Interop.OpenInteractiveProcess(_rcBinaryPath +
|
||||
$" -mode Unattended" +
|
||||
$" -requester \"{requesterID}\"" +
|
||||
$" -serviceid \"{serviceID}\"" +
|
||||
$" -deviceid {ConnectionInfo.DeviceID}" +
|
||||
$" -host {ConnectionInfo.Host}" +
|
||||
$" -orgid \"{ConnectionInfo.OrganizationID}\"" +
|
||||
$" -relaunch true" +
|
||||
$" -viewers {String.Join(",", viewerIDs)}",
|
||||
$" --mode Unattended" +
|
||||
$" --relaunch true" +
|
||||
$" --host {_connectionInfo.Host}" +
|
||||
$" --requester-name \"{requesterName}\"" +
|
||||
$" --org-name \"{orgName}\"" +
|
||||
$" --org-id \"{orgId}\"" +
|
||||
$" --session-id \"{sessionId}\"" +
|
||||
$" --access-key \"{accessKey}\"" +
|
||||
$" --viewers {string.Join(",", viewerIDs)}",
|
||||
|
||||
targetSessionId: targetSessionID,
|
||||
forceConsoleSession: Shlwapi.IsOS(OsType.OS_ANYSERVER) && targetSessionID == -1,
|
||||
@ -185,22 +192,21 @@ namespace Remotely.Agent.Services
|
||||
"Remote control failed to start on target device.",
|
||||
"Failed to start remote control.",
|
||||
"bg-danger",
|
||||
requesterID);
|
||||
userConnectionId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// SignalR Connection IDs might start with a hyphen. We surround them
|
||||
// with quotes so the command line will be parsed correctly.
|
||||
Process.Start(_rcBinaryPath,
|
||||
$"-mode Unattended " +
|
||||
$"-requester \"{requesterID}\" " +
|
||||
$"-serviceid \"{serviceID}\" " +
|
||||
$"-deviceid {ConnectionInfo.DeviceID} " +
|
||||
$"-host {ConnectionInfo.Host} " +
|
||||
$" -orgid \"{ConnectionInfo.OrganizationID}\"" +
|
||||
$"-relaunch true " +
|
||||
$"-viewers {String.Join(",", viewerIDs)}");
|
||||
Process.Start(_rcBinaryPath,
|
||||
$" --mode Unattended" +
|
||||
$" --relaunch true" +
|
||||
$" --host {_connectionInfo.Host}" +
|
||||
$" --requester-name \"{requesterName}\"" +
|
||||
$" --org-name \"{orgName}\"" +
|
||||
$" --org-id \"{orgId}\"" +
|
||||
$" --session-id \"{sessionId}\"" +
|
||||
$" --access-key \"{accessKey}\"" +
|
||||
$" --viewers {string.Join(",", viewerIDs)}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Immense.RemoteControl.Shared.Models;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Remotely.Agent.Interfaces;
|
||||
using Remotely.Agent.Models;
|
||||
using Remotely.Shared.Models;
|
||||
@ -16,13 +17,15 @@ namespace Remotely.Agent.Services
|
||||
{
|
||||
public class ChatClientService
|
||||
{
|
||||
private readonly IAppLauncher _appLauncher;
|
||||
private readonly MemoryCache _chatClients = new("ChatClients");
|
||||
private readonly SemaphoreSlim _messageLock = new(1,1);
|
||||
|
||||
public ChatClientService(IAppLauncher appLauncher)
|
||||
{
|
||||
AppLauncher = appLauncher;
|
||||
_appLauncher = appLauncher;
|
||||
}
|
||||
|
||||
private SemaphoreSlim MessageLock { get; } = new(1,1);
|
||||
private IAppLauncher AppLauncher { get; }
|
||||
private CacheItemPolicy CacheItemPolicy { get; } = new()
|
||||
{
|
||||
SlidingExpiration = TimeSpan.FromMinutes(10),
|
||||
@ -34,16 +37,17 @@ namespace Remotely.Agent.Services
|
||||
})
|
||||
};
|
||||
|
||||
private MemoryCache ChatClients { get; } = new("ChatClients");
|
||||
|
||||
public async Task SendMessage(string senderName,
|
||||
public async Task SendMessage(
|
||||
string senderName,
|
||||
string message,
|
||||
string orgName,
|
||||
string orgId,
|
||||
bool disconnected,
|
||||
string senderConnectionID,
|
||||
HubConnection hubConnection)
|
||||
{
|
||||
if (!await MessageLock.WaitAsync(30000))
|
||||
if (!await _messageLock.WaitAsync(30000))
|
||||
{
|
||||
Logger.Write("Timed out waiting for chat message lock.", Shared.Enums.EventType.Warning);
|
||||
return;
|
||||
@ -52,7 +56,7 @@ namespace Remotely.Agent.Services
|
||||
try
|
||||
{
|
||||
ChatSession chatSession;
|
||||
if (!ChatClients.Contains(senderConnectionID))
|
||||
if (!_chatClients.Contains(senderConnectionID))
|
||||
{
|
||||
if (disconnected)
|
||||
{
|
||||
@ -60,7 +64,8 @@ namespace Remotely.Agent.Services
|
||||
return;
|
||||
}
|
||||
|
||||
var procID = await AppLauncher.LaunchChatService(orgName, senderConnectionID, hubConnection);
|
||||
var pipeName = Guid.NewGuid().ToString();
|
||||
var procID = await _appLauncher.LaunchChatService(pipeName, senderConnectionID, senderName, orgName, orgId, hubConnection);
|
||||
|
||||
if (procID > 0)
|
||||
{
|
||||
@ -72,7 +77,7 @@ namespace Remotely.Agent.Services
|
||||
return;
|
||||
}
|
||||
|
||||
var clientPipe = new NamedPipeClientStream(".", "Remotely_Chat" + senderConnectionID, PipeDirection.InOut, PipeOptions.Asynchronous);
|
||||
var clientPipe = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
|
||||
clientPipe.Connect(15000);
|
||||
if (!clientPipe.IsConnected)
|
||||
{
|
||||
@ -81,14 +86,14 @@ namespace Remotely.Agent.Services
|
||||
}
|
||||
chatSession = new ChatSession() { PipeStream = clientPipe, ProcessID = procID };
|
||||
_ = Task.Run(async () => { await ReadFromStream(chatSession.PipeStream, senderConnectionID, hubConnection); });
|
||||
ChatClients.Add(senderConnectionID, chatSession, CacheItemPolicy);
|
||||
_chatClients.Add(senderConnectionID, chatSession, CacheItemPolicy);
|
||||
}
|
||||
|
||||
chatSession = (ChatSession)ChatClients.Get(senderConnectionID);
|
||||
chatSession = (ChatSession)_chatClients.Get(senderConnectionID);
|
||||
|
||||
if (!chatSession.PipeStream.IsConnected)
|
||||
{
|
||||
ChatClients.Remove(senderConnectionID);
|
||||
_chatClients.Remove(senderConnectionID);
|
||||
await hubConnection.SendAsync("DisplayMessage", "Chat disconnected. Please try again.", "Chat disconnected.", "bg-warning", senderConnectionID);
|
||||
return;
|
||||
}
|
||||
@ -104,7 +109,7 @@ namespace Remotely.Agent.Services
|
||||
}
|
||||
finally
|
||||
{
|
||||
MessageLock.Release();
|
||||
_messageLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,7 +126,7 @@ namespace Remotely.Agent.Services
|
||||
}
|
||||
}
|
||||
await hubConnection.SendAsync("Chat", string.Empty, true, senderConnectionID);
|
||||
ChatClients.Remove(senderConnectionID);
|
||||
_chatClients.Remove(senderConnectionID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Remotely.Shared;
|
||||
using Remotely.Shared.Enums;
|
||||
using Remotely.Shared.Models;
|
||||
using Remotely.Shared.Utilities;
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
using Remotely.Shared.Utilities;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.Versioning;
|
||||
using System.ServiceProcess;
|
||||
|
||||
namespace Remotely.Agent.Services
|
||||
{
|
||||
[SupportedOSPlatform("windows")]
|
||||
partial class WindowsService : ServiceBase
|
||||
{
|
||||
public WindowsService()
|
||||
|
||||
@ -47,8 +47,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.EventLog" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.MixedReality.WebRTC" Version="2.0.2" />
|
||||
<PackageReference Include="SkiaSharp" Version="2.88.0" />
|
||||
<PackageReference Include="SkiaSharp" Version="2.88.1" />
|
||||
<PackageReference Include="SkiaSharp.Views.Desktop.Common" Version="2.88.0" />
|
||||
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@ -19,7 +19,6 @@ namespace Remotely.Desktop.Core.Services
|
||||
Task Disconnect();
|
||||
Task DisconnectAllViewers();
|
||||
Task DisconnectViewer(Viewer viewer, bool notifyViewer);
|
||||
Task<IceServerModel[]> GetIceServers();
|
||||
Task<string> GetSessionID();
|
||||
Task NotifyRequesterUnattendedReady(string requesterID);
|
||||
Task NotifyViewersRelaunchedScreenCasterReady(string[] viewerIDs);
|
||||
@ -28,9 +27,7 @@ namespace Remotely.Desktop.Core.Services
|
||||
Task SendCtrlAltDelToAgent();
|
||||
Task SendDeviceInfo(string serviceID, string machineName, string deviceID);
|
||||
Task SendDtoToViewer<T>(T dto, string viewerId);
|
||||
Task SendIceCandidateToBrowser(string candidate, int sdpMlineIndex, string sdpMid, string viewerConnectionID);
|
||||
Task SendMessageToViewer(string viewerID, string message);
|
||||
Task SendRtcOfferToBrowser(string sdp, string viewerID, IceServerModel[] iceServers);
|
||||
Task SendViewerConnected(string viewerConnectionId);
|
||||
}
|
||||
|
||||
@ -69,7 +66,7 @@ namespace Remotely.Desktop.Core.Services
|
||||
catch { }
|
||||
}
|
||||
Connection = new HubConnectionBuilder()
|
||||
.WithUrl($"{host.Trim().TrimEnd('/')}/CasterHub")
|
||||
.WithUrl($"{host.Trim().TrimEnd('/')}/hubs/desktop")
|
||||
.AddMessagePackProtocol()
|
||||
.WithAutomaticReconnect()
|
||||
.Build();
|
||||
@ -115,11 +112,6 @@ namespace Remotely.Desktop.Core.Services
|
||||
return Connection.SendAsync("DisconnectViewer", viewer.ViewerConnectionID, notifyViewer);
|
||||
}
|
||||
|
||||
public async Task<IceServerModel[]> GetIceServers()
|
||||
{
|
||||
return await Connection.InvokeAsync<IceServerModel[]>("GetIceServers");
|
||||
}
|
||||
|
||||
public async Task<string> GetSessionID()
|
||||
{
|
||||
return await Connection.InvokeAsync<string>("GetSessionID");
|
||||
@ -164,15 +156,7 @@ namespace Remotely.Desktop.Core.Services
|
||||
var serializedDto = MessagePack.MessagePackSerializer.Serialize(dto);
|
||||
return Connection.SendAsync("SendDtoToBrowser", serializedDto, viewerId);
|
||||
}
|
||||
public Task SendIceCandidateToBrowser(string candidate, int sdpMlineIndex, string sdpMid, string viewerConnectionID)
|
||||
{
|
||||
return Connection.SendAsync("SendIceCandidateToBrowser", candidate, sdpMlineIndex, sdpMid, viewerConnectionID);
|
||||
}
|
||||
|
||||
public Task SendRtcOfferToBrowser(string sdp, string viewerID, IceServerModel[] iceServers)
|
||||
{
|
||||
return Connection.SendAsync("SendRtcOfferToBrowser", sdp, viewerID, iceServers);
|
||||
}
|
||||
public Task SendViewerConnected(string viewerConnectionId)
|
||||
{
|
||||
return Connection.SendAsync("ViewerConnected", viewerConnectionId);
|
||||
@ -200,7 +184,6 @@ namespace Remotely.Desktop.Core.Services
|
||||
string requesterName,
|
||||
bool notifyUser,
|
||||
bool enforceAttendedAccess,
|
||||
bool useWebRtc,
|
||||
string organizationName) =>
|
||||
{
|
||||
try
|
||||
@ -224,8 +207,7 @@ namespace Remotely.Desktop.Core.Services
|
||||
{
|
||||
NotifyUser = notifyUser,
|
||||
ViewerID = viewerID,
|
||||
RequesterName = requesterName,
|
||||
UseWebRtc = useWebRtc
|
||||
RequesterName = requesterName
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -235,45 +217,13 @@ namespace Remotely.Desktop.Core.Services
|
||||
});
|
||||
|
||||
|
||||
Connection.On("ReceiveIceCandidate", (string candidate, int sdpMlineIndex, string sdpMid, string viewerID) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (conductor.Viewers.TryGetValue(viewerID, out var viewer))
|
||||
{
|
||||
viewer.RtcSession.AddIceCandidate(sdpMid, sdpMlineIndex, candidate);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Connection.On("ReceiveRtcAnswer", async (string sdp, string viewerID) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (conductor.Viewers.TryGetValue(viewerID, out var viewer))
|
||||
{
|
||||
await viewer.RtcSession.SetRemoteDescription("answer", sdp);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
});
|
||||
|
||||
Connection.On("RequestScreenCast", (string viewerID, string requesterName, bool notifyUser, bool useWebRtc) =>
|
||||
Connection.On("RequestScreenCast", (string viewerID, string requesterName, bool notifyUser) =>
|
||||
{
|
||||
conductor.InvokeScreenCastRequested(new ScreenCastRequest()
|
||||
{
|
||||
NotifyUser = notifyUser,
|
||||
ViewerID = viewerID,
|
||||
RequesterName = requesterName,
|
||||
UseWebRtc = useWebRtc
|
||||
RequesterName = requesterName
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -18,15 +18,13 @@ namespace Remotely.Desktop.Core.Services
|
||||
{
|
||||
public interface IDeviceInitService
|
||||
{
|
||||
BrandingInfo BrandingInfo { get; }
|
||||
|
||||
Task GetInitParams();
|
||||
Task<BrandingInfo> GetBrandingInfo();
|
||||
void SetBrandingInfo(BrandingInfo branding);
|
||||
}
|
||||
|
||||
public class DeviceInitService : IDeviceInitService
|
||||
{
|
||||
private static BrandingInfo _brandingInfo;
|
||||
private static BrandingInfo _brandingInfo = new();
|
||||
|
||||
private readonly Conductor _conductor;
|
||||
private readonly IConfigService _configService;
|
||||
@ -36,14 +34,13 @@ namespace Remotely.Desktop.Core.Services
|
||||
_configService = configService;
|
||||
}
|
||||
|
||||
public BrandingInfo BrandingInfo => _brandingInfo;
|
||||
public async Task GetInitParams()
|
||||
public async Task<BrandingInfo> GetBrandingInfo()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_brandingInfo != null)
|
||||
if (_brandingInfo is not null)
|
||||
{
|
||||
return;
|
||||
return _brandingInfo;
|
||||
}
|
||||
|
||||
using var httpClient = new HttpClient();
|
||||
@ -81,7 +78,7 @@ namespace Remotely.Desktop.Core.Services
|
||||
|
||||
var brandingUrl = $"{config.Host.TrimEnd('/')}/api/branding/{config.OrganizationId}";
|
||||
_brandingInfo = await httpClient.GetFromJsonAsync<BrandingInfo>(brandingUrl).ConfigureAwait(false);
|
||||
return;
|
||||
return _brandingInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -100,6 +97,8 @@ namespace Remotely.Desktop.Core.Services
|
||||
{
|
||||
Logger.Write(ex, "Failed to resolve init params.", Shared.Enums.EventType.Warning);
|
||||
}
|
||||
|
||||
return _brandingInfo;
|
||||
}
|
||||
|
||||
public void SetBrandingInfo(BrandingInfo branding)
|
||||
|
||||
@ -97,9 +97,6 @@ namespace Remotely.Desktop.Core.Services
|
||||
case BaseDtoType.ToggleBlockInput:
|
||||
ToggleBlockInput(message);
|
||||
break;
|
||||
case BaseDtoType.ToggleWebRtcVideo:
|
||||
ToggleWebRtcVideo(message, viewer);
|
||||
break;
|
||||
case BaseDtoType.ClipboardTransfer:
|
||||
await ClipboardTransfer(message);
|
||||
break;
|
||||
@ -259,11 +256,5 @@ namespace Remotely.Desktop.Core.Services
|
||||
var dto = MessagePackSerializer.Deserialize<ToggleBlockInputDto>(message);
|
||||
KeyboardMouseInput.ToggleBlockInput(dto.ToggleOn);
|
||||
}
|
||||
|
||||
private void ToggleWebRtcVideo(byte[] message, Viewer viewer)
|
||||
{
|
||||
var dto = MessagePackSerializer.Deserialize<ToggleWebRtcVideoDto>(message);
|
||||
viewer.ToggleWebRtcVideo(dto.ToggleOn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,12 +104,6 @@ namespace Remotely.Desktop.Core.Services
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (EnvironmentHelper.IsWindows && screenCastRequest.UseWebRtc)
|
||||
{
|
||||
await viewer.InitializeWebRtc();
|
||||
}
|
||||
|
||||
// Wait until the first image is received.
|
||||
if (!TaskHelper.DelayUntil(() => !viewer.PendingSentFrames.Any(), TimeSpan.FromSeconds(30)))
|
||||
{
|
||||
@ -136,12 +130,6 @@ namespace Remotely.Desktop.Core.Services
|
||||
{
|
||||
try
|
||||
{
|
||||
if (viewer.IsUsingWebRtcVideo)
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (viewer.IsStalled)
|
||||
{
|
||||
// Viewer isn't responding. Abort sending.
|
||||
|
||||
@ -28,12 +28,10 @@ namespace Remotely.Desktop.Core.Services
|
||||
public Viewer(ICasterSocket casterSocket,
|
||||
IScreenCapturer screenCapturer,
|
||||
IClipboardService clipboardService,
|
||||
IWebRtcSessionFactory webRtcSessionFactory,
|
||||
IAudioCapturer audioCapturer)
|
||||
{
|
||||
Capturer = screenCapturer;
|
||||
CasterSocket = casterSocket;
|
||||
WebRtcSessionFactory = webRtcSessionFactory;
|
||||
ClipboardService = clipboardService;
|
||||
ClipboardService.ClipboardTextChanged += ClipboardService_ClipboardTextChanged;
|
||||
AudioCapturer = audioCapturer;
|
||||
@ -55,26 +53,9 @@ namespace Remotely.Desktop.Core.Services
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsUsingWebRtc
|
||||
{
|
||||
get
|
||||
{
|
||||
return RtcSession?.IsPeerConnected == true && RtcSession?.IsDataChannelOpen == true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsUsingWebRtcVideo
|
||||
{
|
||||
get
|
||||
{
|
||||
return RtcSession?.IsPeerConnected == true && RtcSession?.IsVideoTrackConnected == true;
|
||||
}
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
public ConcurrentQueue<SentFrame> PendingSentFrames { get; } = new();
|
||||
public TimeSpan RoundTripLatency { get; private set; }
|
||||
public WebRtcSession RtcSession { get; set; }
|
||||
|
||||
public string ViewerConnectionID { get; set; }
|
||||
private IAudioCapturer AudioCapturer { get; }
|
||||
@ -84,8 +65,6 @@ namespace Remotely.Desktop.Core.Services
|
||||
|
||||
private IClipboardService ClipboardService { get; }
|
||||
|
||||
private IWebRtcSessionFactory WebRtcSessionFactory { get; }
|
||||
|
||||
public void ApplyAutoQuality()
|
||||
{
|
||||
if (ImageQuality < DefaultQuality)
|
||||
@ -146,52 +125,20 @@ namespace Remotely.Desktop.Core.Services
|
||||
public void Dispose()
|
||||
{
|
||||
DisconnectRequested = true;
|
||||
Disposer.TryDisposeAll(RtcSession, Capturer);
|
||||
Disposer.TryDisposeAll(Capturer);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public async Task InitializeWebRtc()
|
||||
{
|
||||
try
|
||||
{
|
||||
var iceServers = await CasterSocket.GetIceServers();
|
||||
|
||||
RtcSession = WebRtcSessionFactory.GetNewSession(this);
|
||||
|
||||
if (RtcSession is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RtcSession.LocalSdpReady += async (sender, sdp) =>
|
||||
{
|
||||
await CasterSocket.SendRtcOfferToBrowser(sdp.Content, ViewerConnectionID, iceServers);
|
||||
};
|
||||
RtcSession.IceCandidateReady += async (sender, candidate) =>
|
||||
{
|
||||
await CasterSocket.SendIceCandidateToBrowser(candidate.Content, candidate.SdpMlineIndex, candidate.SdpMid, ViewerConnectionID);
|
||||
};
|
||||
|
||||
await RtcSession.Init(iceServers);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendAudioSample(byte[] audioSample)
|
||||
{
|
||||
var dto = new AudioSampleDto(audioSample);
|
||||
await SendToViewer(() => RtcSession.SendDto(dto),
|
||||
() => CasterSocket.SendDtoToViewer(dto, ViewerConnectionID));
|
||||
await TrySendToViewer(() => CasterSocket.SendDtoToViewer(dto, ViewerConnectionID));
|
||||
}
|
||||
|
||||
public async Task SendClipboardText(string clipboardText)
|
||||
{
|
||||
var dto = new ClipboardTextDto(clipboardText);
|
||||
await SendToViewer(() => RtcSession.SendDto(dto),
|
||||
() => CasterSocket.SendDtoToViewer(dto, ViewerConnectionID));
|
||||
await TrySendToViewer(() => CasterSocket.SendDtoToViewer(dto, ViewerConnectionID));
|
||||
}
|
||||
|
||||
public async Task SendCtrlAltDel()
|
||||
@ -207,8 +154,7 @@ namespace Remotely.Desktop.Core.Services
|
||||
}
|
||||
|
||||
var dto = new CursorChangeDto(cursorInfo.ImageBytes, cursorInfo.HotSpot.X, cursorInfo.HotSpot.Y, cursorInfo.CssOverride);
|
||||
await SendToViewer(() => RtcSession.SendDto(dto),
|
||||
() => CasterSocket.SendDtoToViewer(dto, ViewerConnectionID));
|
||||
await TrySendToViewer(() => CasterSocket.SendDtoToViewer(dto, ViewerConnectionID));
|
||||
}
|
||||
public async Task SendFile(FileUpload fileUpload, CancellationToken cancelToken, Action<double> progressUpdateCallback)
|
||||
{
|
||||
@ -223,8 +169,7 @@ namespace Remotely.Desktop.Core.Services
|
||||
StartOfFile = true
|
||||
};
|
||||
|
||||
await SendToViewer(async () => await RtcSession.SendDto(fileDto),
|
||||
async () => await CasterSocket.SendDtoToViewer(fileDto, ViewerConnectionID));
|
||||
await TrySendToViewer(async () => await CasterSocket.SendDtoToViewer(fileDto, ViewerConnectionID));
|
||||
|
||||
using var fs = File.OpenRead(fileUpload.FilePath);
|
||||
using var br = new BinaryReader(fs);
|
||||
@ -242,9 +187,7 @@ namespace Remotely.Desktop.Core.Services
|
||||
MessageId = messageId
|
||||
};
|
||||
|
||||
await SendToViewer(
|
||||
async () => await RtcSession.SendDto(fileDto),
|
||||
async () => await CasterSocket.SendDtoToViewer(fileDto, ViewerConnectionID));
|
||||
await TrySendToViewer(async () => await CasterSocket.SendDtoToViewer(fileDto, ViewerConnectionID));
|
||||
|
||||
progressUpdateCallback((double)fs.Position / fs.Length);
|
||||
}
|
||||
@ -257,8 +200,7 @@ namespace Remotely.Desktop.Core.Services
|
||||
StartOfFile = false
|
||||
};
|
||||
|
||||
await SendToViewer(async () => await RtcSession.SendDto(fileDto),
|
||||
async () => await CasterSocket.SendDtoToViewer(fileDto, ViewerConnectionID));
|
||||
await TrySendToViewer(async () => await CasterSocket.SendDtoToViewer(fileDto, ViewerConnectionID));
|
||||
|
||||
progressUpdateCallback(1);
|
||||
}
|
||||
@ -296,9 +238,7 @@ namespace Remotely.Desktop.Core.Services
|
||||
ImageBytes = chunk
|
||||
};
|
||||
|
||||
await SendToViewer(
|
||||
() => RtcSession.SendDto(dto),
|
||||
() => CasterSocket.SendDtoToViewer(dto, ViewerConnectionID));
|
||||
await TrySendToViewer(() => CasterSocket.SendDtoToViewer(dto, ViewerConnectionID));
|
||||
}
|
||||
}
|
||||
|
||||
@ -316,15 +256,13 @@ namespace Remotely.Desktop.Core.Services
|
||||
ScreenWidth = screenWidth,
|
||||
ScreenHeight = screenHeight
|
||||
};
|
||||
await SendToViewer(() => RtcSession.SendDto(dto),
|
||||
() => CasterSocket.SendDtoToViewer(dto, ViewerConnectionID));
|
||||
await TrySendToViewer(() => CasterSocket.SendDtoToViewer(dto, ViewerConnectionID));
|
||||
}
|
||||
|
||||
public async Task SendScreenSize(int width, int height)
|
||||
{
|
||||
var dto = new ScreenSizeDto(width, height);
|
||||
await SendToViewer(() => RtcSession.SendDto(dto),
|
||||
() => CasterSocket.SendDtoToViewer(dto, ViewerConnectionID));
|
||||
await TrySendToViewer(() => CasterSocket.SendDtoToViewer(dto, ViewerConnectionID));
|
||||
}
|
||||
|
||||
public async Task SendViewerConnected()
|
||||
@ -337,14 +275,9 @@ namespace Remotely.Desktop.Core.Services
|
||||
if (EnvironmentHelper.IsWindows)
|
||||
{
|
||||
var dto = new WindowsSessionsDto(Win32Interop.GetActiveSessions());
|
||||
await SendToViewer(() => RtcSession.SendDto(dto),
|
||||
() => CasterSocket.SendDtoToViewer(dto, ViewerConnectionID));
|
||||
await TrySendToViewer(() => CasterSocket.SendDtoToViewer(dto, ViewerConnectionID));
|
||||
}
|
||||
}
|
||||
public void ToggleWebRtcVideo(bool toggleOn)
|
||||
{
|
||||
RtcSession.ToggleWebRtcVideo(toggleOn);
|
||||
}
|
||||
|
||||
private async void AudioCapturer_AudioSampleReady(object sender, byte[] sample)
|
||||
{
|
||||
@ -356,18 +289,11 @@ namespace Remotely.Desktop.Core.Services
|
||||
await SendClipboardText(clipboardText);
|
||||
}
|
||||
|
||||
private Task SendToViewer(Func<Task> webRtcSend, Func<Task> websocketSend)
|
||||
private Task TrySendToViewer(Func<Task> websocketSend)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsUsingWebRtc)
|
||||
{
|
||||
return webRtcSend();
|
||||
}
|
||||
else
|
||||
{
|
||||
return websocketSend();
|
||||
}
|
||||
return websocketSend();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@ -1,234 +0,0 @@
|
||||
using MessagePack;
|
||||
using Microsoft.MixedReality.WebRTC;
|
||||
using Remotely.Shared.Utilities;
|
||||
using Remotely.Shared.Models;
|
||||
using Remotely.Shared.Models.RemoteControlDtos;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
|
||||
namespace Remotely.Desktop.Core.Services
|
||||
{
|
||||
public class WebRtcSession : IDisposable
|
||||
{
|
||||
public WebRtcSession(Services.Viewer viewer, IDtoMessageHandler rtcMessageHandler)
|
||||
{
|
||||
Viewer = viewer;
|
||||
RtcMessageHandler = rtcMessageHandler;
|
||||
}
|
||||
|
||||
public event EventHandler<IceCandidate> IceCandidateReady;
|
||||
|
||||
public event EventHandler<SdpMessage> LocalSdpReady;
|
||||
|
||||
public ulong CurrentBuffer { get; private set; }
|
||||
public bool IsDataChannelOpen => CaptureChannel?.State == DataChannel.ChannelState.Open;
|
||||
public bool IsPeerConnected => PeerSession?.IsConnected == true;
|
||||
public bool IsVideoTrackConnected
|
||||
{
|
||||
get
|
||||
{
|
||||
return Transceiver?.LocalVideoTrack?.Enabled == true;
|
||||
}
|
||||
}
|
||||
|
||||
private DataChannel CaptureChannel { get; set; }
|
||||
private IceServerModel[] IceServers { get; set; }
|
||||
private PeerConnection PeerSession { get; set; }
|
||||
private IDtoMessageHandler RtcMessageHandler { get; }
|
||||
private Transceiver Transceiver { get; set; }
|
||||
private ExternalVideoTrackSource VideoSource { get; set; }
|
||||
private Services.Viewer Viewer { get; }
|
||||
public void AddIceCandidate(string sdpMid, int sdpMlineIndex, string candidate)
|
||||
{
|
||||
PeerSession.AddIceCandidate(new IceCandidate()
|
||||
{
|
||||
Content = candidate,
|
||||
SdpMid = sdpMid,
|
||||
SdpMlineIndex = sdpMlineIndex
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Unable to exit process until DataChannel is removed/disposed,
|
||||
// and this throws internally (at least in 2.0 version).
|
||||
PeerSession?.RemoveDataChannel(CaptureChannel);
|
||||
}
|
||||
catch { }
|
||||
PeerSession.Transceivers.RemoveAll(x => true);
|
||||
Disposer.TryDisposeAll(Transceiver?.LocalVideoTrack, VideoSource, PeerSession);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public async Task Init(IceServerModel[] iceServers)
|
||||
{
|
||||
Logger.Write("Starting WebRTC connection.");
|
||||
|
||||
IceServers = iceServers;
|
||||
|
||||
PeerSession = new PeerConnection();
|
||||
|
||||
var iceList = IceServers.Select(x => new IceServer()
|
||||
{
|
||||
Urls = { x.Url },
|
||||
TurnPassword = x.TurnPassword ?? string.Empty,
|
||||
TurnUserName = x.TurnUsername ?? string.Empty
|
||||
}).ToList();
|
||||
|
||||
var config = new PeerConnectionConfiguration()
|
||||
{
|
||||
IceServers = iceList
|
||||
};
|
||||
|
||||
await PeerSession.InitializeAsync(config);
|
||||
|
||||
PeerSession.LocalSdpReadytoSend += PeerSession_LocalSdpReadytoSend;
|
||||
PeerSession.Connected += PeerConnection_Connected;
|
||||
PeerSession.IceStateChanged += PeerConnection_IceStateChanged;
|
||||
PeerSession.IceCandidateReadytoSend += PeerSession_IceCandidateReadytoSend;
|
||||
|
||||
CaptureChannel = await PeerSession.AddDataChannelAsync("ScreenCapture", true, true);
|
||||
CaptureChannel.BufferingChanged += DataChannel_BufferingChanged;
|
||||
CaptureChannel.MessageReceived += CaptureChannel_MessageReceived;
|
||||
CaptureChannel.StateChanged += CaptureChannel_StateChanged;
|
||||
|
||||
VideoSource = ExternalVideoTrackSource.CreateFromArgb32Callback(GetCaptureFrame);
|
||||
Transceiver = PeerSession.AddTransceiver(MediaKind.Video);
|
||||
|
||||
PeerSession.CreateOffer();
|
||||
}
|
||||
|
||||
public Task SendDto<T>(T dto) where T : BaseDto
|
||||
{
|
||||
CaptureChannel.SendMessage(MessagePackSerializer.Serialize(dto));
|
||||
TaskHelper.DelayUntil(() => CurrentBuffer < 128_000, TimeSpan.FromSeconds(5));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task SetRemoteDescription(string type, string sdp)
|
||||
{
|
||||
if (!Enum.TryParse<SdpMessageType>(type, true, out var sdpMessageType))
|
||||
{
|
||||
Logger.Write("Unable to parse remote WebRTC description type.");
|
||||
return;
|
||||
}
|
||||
|
||||
await PeerSession.SetRemoteDescriptionAsync(new SdpMessage()
|
||||
{
|
||||
Content = sdp,
|
||||
Type = sdpMessageType
|
||||
});
|
||||
|
||||
if (sdpMessageType == SdpMessageType.Offer)
|
||||
{
|
||||
PeerSession.CreateAnswer();
|
||||
}
|
||||
}
|
||||
|
||||
public void ToggleWebRtcVideo(bool toggleOn)
|
||||
{
|
||||
if (Transceiver?.LocalVideoTrack != null)
|
||||
{
|
||||
Transceiver.LocalVideoTrack.Dispose();
|
||||
Transceiver.LocalVideoTrack = null;
|
||||
}
|
||||
|
||||
if (toggleOn)
|
||||
{
|
||||
Transceiver.LocalVideoTrack = LocalVideoTrack.CreateFromSource(VideoSource, new LocalVideoTrackInitConfig()
|
||||
{
|
||||
trackName = "ScreenCapture"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async void CaptureChannel_MessageReceived(byte[] obj)
|
||||
{
|
||||
await RtcMessageHandler.ParseMessage(Viewer, obj);
|
||||
}
|
||||
private async void CaptureChannel_StateChanged()
|
||||
{
|
||||
// Clear the queue when WebRTC state changes.
|
||||
Viewer.PendingSentFrames.Clear();
|
||||
Logger.Write($"DataChannel state changed. New State: {CaptureChannel.State}");
|
||||
if (CaptureChannel.State == DataChannel.ChannelState.Closed)
|
||||
{
|
||||
await Init(IceServers);
|
||||
}
|
||||
}
|
||||
|
||||
private void DataChannel_BufferingChanged(ulong previous, ulong current, ulong limit)
|
||||
{
|
||||
CurrentBuffer = current;
|
||||
}
|
||||
|
||||
private void GetCaptureFrame(in FrameRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!IsVideoTrackConnected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var result = Viewer.Capturer.GetNextFrame();
|
||||
|
||||
if (!result.IsSuccess || result.Value is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var currentFrame = result.Value;
|
||||
|
||||
if (currentFrame == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var pixels = currentFrame.GetPixels();
|
||||
|
||||
var frame = new Argb32VideoFrame()
|
||||
{
|
||||
data = pixels,
|
||||
height = (uint)currentFrame.Height,
|
||||
width = (uint)currentFrame.Width,
|
||||
stride = currentFrame.RowBytes
|
||||
};
|
||||
request.CompleteRequest(in frame);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void PeerConnection_Connected()
|
||||
{
|
||||
Logger.Write("PeerConnection connected.");
|
||||
}
|
||||
|
||||
private void PeerConnection_IceStateChanged(IceConnectionState newState)
|
||||
{
|
||||
// Clear the queue when WebRTC state changes.
|
||||
Viewer.PendingSentFrames.Clear();
|
||||
Logger.Write($"Ice state changed to {newState}.");
|
||||
}
|
||||
|
||||
private void PeerSession_IceCandidateReadytoSend(IceCandidate candidate)
|
||||
{
|
||||
Logger.Write("Ice candidate ready to send.");
|
||||
IceCandidateReady?.Invoke(this, candidate);
|
||||
}
|
||||
private void PeerSession_LocalSdpReadytoSend(SdpMessage message)
|
||||
{
|
||||
Logger.Write($"Local SDP ready.");
|
||||
LocalSdpReady?.Invoke(this, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
namespace Remotely.Desktop.Core.Services
|
||||
{
|
||||
public interface IWebRtcSessionFactory
|
||||
{
|
||||
WebRtcSession GetNewSession(Services.Viewer viewer);
|
||||
}
|
||||
|
||||
public class WebRtcSessionFactory : IWebRtcSessionFactory
|
||||
{
|
||||
public WebRtcSessionFactory(IDtoMessageHandler messageHandler)
|
||||
{
|
||||
MessageHandler = messageHandler;
|
||||
}
|
||||
private IDtoMessageHandler MessageHandler { get; }
|
||||
|
||||
public WebRtcSession GetNewSession(Services.Viewer viewer)
|
||||
{
|
||||
return new WebRtcSession(viewer, MessageHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
@ -7,6 +7,9 @@
|
||||
<RootNamespace>Remotely.Desktop.XPlat</RootNamespace>
|
||||
<Platforms>AnyCPU;x64;x86</Platforms>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
@ -52,20 +55,14 @@
|
||||
<EmbeddedResource Include="Assets\Remotely_Icon.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="0.10.15" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="0.10.15" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.15" />
|
||||
<PackageReference Include="Avalonia" Version="0.10.18" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="0.10.18" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.18" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Desktop.Core\Desktop.Core.csproj" />
|
||||
<ProjectReference Include="..\Desktop.Shared\Desktop.Shared.csproj" />
|
||||
<ProjectReference Include="..\Shared\Shared.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="Views\HostNamePrompt.axaml.cs">
|
||||
<DependentUpon>HostNamePrompt.axaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Controls\MessageBox.axaml.cs">
|
||||
<DependentUpon>MessageBox.axaml</DependentUpon>
|
||||
</Compile>
|
||||
<ProjectReference Include="..\submodules\Immense.RemoteControl\Desktop.Linux\Desktop.Linux.csproj" />
|
||||
<ProjectReference Include="..\submodules\Immense.RemoteControl\Desktop.Shared\Desktop.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
66
Desktop.Linux/Program.cs
Normal file
@ -0,0 +1,66 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System;
|
||||
using Immense.RemoteControl.Desktop.Windows;
|
||||
using Remotely.Desktop.Shared.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Remotely.Shared.Services;
|
||||
using Immense.RemoteControl.Desktop.Shared.Services;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Immense.RemoteControl.Desktop.Shared.Enums;
|
||||
using Immense.RemoteControl.Desktop.UI.Services;
|
||||
using Remotely.Shared;
|
||||
|
||||
var provider = await Startup.UseRemoteControlClient(
|
||||
args,
|
||||
config =>
|
||||
{
|
||||
config.AddBrandingProvider<BrandingProvider>();
|
||||
},
|
||||
services =>
|
||||
{
|
||||
services.AddLogging(builder =>
|
||||
{
|
||||
#if DEBUG
|
||||
builder.SetMinimumLevel(LogLevel.Debug);
|
||||
#endif
|
||||
builder.AddProvider(new FileLoggerProvider());
|
||||
});
|
||||
|
||||
services.AddSingleton<IOrganizationIdProvider, OrganizationIdProvider>();
|
||||
},
|
||||
async services =>
|
||||
{
|
||||
var appState = services.GetRequiredService<IAppState>();
|
||||
if (appState.ArgDict.TryGetValue("org-id", out var orgId))
|
||||
{
|
||||
var orgIdProvider = services.GetRequiredService<IOrganizationIdProvider>();
|
||||
orgIdProvider.OrganizationId = orgId;
|
||||
}
|
||||
|
||||
var brandingProvider = services.GetRequiredService<IBrandingProvider>();
|
||||
if (brandingProvider is BrandingProvider branding)
|
||||
{
|
||||
await branding.TrySetFromApi();
|
||||
}
|
||||
|
||||
},
|
||||
AppConstants.ServerUrl);
|
||||
|
||||
|
||||
Console.WriteLine("Press Ctrl + C to exit.");
|
||||
|
||||
var shutdownService = provider.GetRequiredService<IShutdownService>();
|
||||
Console.CancelKeyPress += async (s, e) =>
|
||||
{
|
||||
await shutdownService.Shutdown();
|
||||
};
|
||||
|
||||
var dispatcher = provider.GetRequiredService<IAvaloniaDispatcher>();
|
||||
try
|
||||
{
|
||||
await Task.Delay(Timeout.InfiniteTimeSpan, dispatcher.AppCancellationToken);
|
||||
}
|
||||
catch (TaskCanceledException) { }
|
||||
8
Desktop.Linux/Properties/launchSettings.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Desktop.Linux": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "-m Attended -s some-session-id -a vERyLonGAndCOMpleXKeY -o Immense -r Han"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
Desktop.Shared/Assets/Remotely_Icon.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
Desktop.Shared/Assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 103 KiB |
27
Desktop.Shared/Desktop.Shared.csproj
Normal file
@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Assets\favicon.ico" />
|
||||
<None Remove="Assets\Remotely_Icon.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Assets\Remotely_Icon.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Shared\Shared.csproj" />
|
||||
<ProjectReference Include="..\submodules\Immense.RemoteControl\Desktop.Shared\Desktop.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Resource Include="Assets\favicon.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
119
Desktop.Shared/Services/BrandingProvider.cs
Normal file
@ -0,0 +1,119 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Immense.RemoteControl.Desktop.Shared.Services;
|
||||
using Immense.RemoteControl.Shared.Models;
|
||||
using Remotely.Shared;
|
||||
using Remotely.Shared.Enums;
|
||||
using Remotely.Shared.Models;
|
||||
using Remotely.Shared.Utilities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Remotely.Desktop.Shared.Services
|
||||
{
|
||||
public class BrandingProvider : IBrandingProvider
|
||||
{
|
||||
private readonly IAppState _appState;
|
||||
private readonly IOrganizationIdProvider _orgIdProvider;
|
||||
private BrandingInfoBase _brandingInfo = new()
|
||||
{
|
||||
Product = "Remote Control"
|
||||
};
|
||||
|
||||
public BrandingProvider(IAppState appState, IOrganizationIdProvider orgIdProvider)
|
||||
{
|
||||
_appState = appState;
|
||||
_orgIdProvider = orgIdProvider;
|
||||
|
||||
using var mrs = typeof(BrandingProvider).Assembly.GetManifestResourceStream("Desktop.Shared.Assets.Remotely_Icon.png");
|
||||
using var ms = new MemoryStream();
|
||||
mrs!.CopyTo(ms);
|
||||
|
||||
_brandingInfo.Icon = ms.ToArray();
|
||||
}
|
||||
|
||||
public Task<BrandingInfoBase> GetBrandingInfo()
|
||||
{
|
||||
return Task.FromResult(_brandingInfo);
|
||||
}
|
||||
|
||||
public void SetBrandingInfo(BrandingInfoBase brandingInfo)
|
||||
{
|
||||
_brandingInfo = brandingInfo;
|
||||
}
|
||||
|
||||
public async Task TrySetFromApi()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_appState.Host))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var host = _appState.Host;
|
||||
|
||||
using var httpClient = new HttpClient();
|
||||
|
||||
var fileName = Path.GetFileNameWithoutExtension(Process.GetCurrentProcess()?.MainModule?.FileName);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileName.Contains('[') &&
|
||||
fileName.Contains(']') &&
|
||||
!string.IsNullOrWhiteSpace(host))
|
||||
{
|
||||
var codeLength = AppConstants.RelayCodeLength + 2;
|
||||
|
||||
for (var i = 0; i < fileName.Length; i++)
|
||||
{
|
||||
var codeSection = string.Join("", fileName.Skip(i).Take(codeLength));
|
||||
if (codeSection.StartsWith("[") && codeSection.EndsWith("]"))
|
||||
{
|
||||
var relayCode = codeSection[1..5];
|
||||
|
||||
using var response = await httpClient.GetAsync($"{host.TrimEnd('/')}/api/Relay/{relayCode}").ConfigureAwait(false);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var organizationId = await response.Content.ReadAsStringAsync();
|
||||
_orgIdProvider.OrganizationId = organizationId;
|
||||
|
||||
var brandingUrl = $"{host.TrimEnd('/')}/api/branding/{organizationId}";
|
||||
var result = await httpClient.GetFromJsonAsync<BrandingInfo>(brandingUrl).ConfigureAwait(false);
|
||||
if (result is not null)
|
||||
{
|
||||
_brandingInfo = result;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(host) && !string.IsNullOrWhiteSpace(_orgIdProvider.OrganizationId))
|
||||
{
|
||||
var brandingUrl = $"{host.TrimEnd('/')}/api/branding/{_orgIdProvider.OrganizationId}";
|
||||
var result = await httpClient.GetFromJsonAsync<BrandingInfo>(brandingUrl).ConfigureAwait(false);
|
||||
if (result is not null)
|
||||
{
|
||||
_brandingInfo = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex, "Failed to resolve init params.", EventType.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
20
Desktop.Shared/Services/OrganizationIdProvider.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Immense.RemoteControl.Desktop.Shared.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Remotely.Desktop.Shared.Services
|
||||
{
|
||||
public interface IOrganizationIdProvider
|
||||
{
|
||||
string OrganizationId { get; set; }
|
||||
}
|
||||
public class OrganizationIdProvider : IOrganizationIdProvider
|
||||
{
|
||||
public string OrganizationId { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
<Application x:Class="Remotely.Desktop.Win.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:Remotely.Desktop.Win"
|
||||
DispatcherUnhandledException="Application_DispatcherUnhandledException"
|
||||
Startup="Application_Startup">
|
||||
<Application.Resources>
|
||||
<Style x:Key="TitlebarButton" TargetType="Button">
|
||||
<Setter Property="Background" Value="#FF464646"></Setter>
|
||||
<Setter Property="DockPanel.Dock" Value="Right"></Setter>
|
||||
<Setter Property="HorizontalAlignment" Value="Right"></Setter>
|
||||
<Setter Property="BorderBrush" Value="{x:Null}"></Setter>
|
||||
<Setter Property="Foreground" Value="White"></Setter>
|
||||
<Setter Property="FontWeight" Value="Bold"></Setter>
|
||||
<Setter Property="Height" Value="30"></Setter>
|
||||
<Setter Property="Width" Value="30"></Setter>
|
||||
<Setter Property="Cursor" Value="Hand"></Setter>
|
||||
<Setter Property="Margin" Value="5,0,5,0"></Setter>
|
||||
</Style>
|
||||
<Style x:Key="SectionHeader" TargetType="TextBlock">
|
||||
<Setter Property="FontWeight" Value="Bold"></Setter>
|
||||
<Setter Property="FontSize" Value="18"></Setter>
|
||||
</Style>
|
||||
<Style x:Key="NormalButton" TargetType="Button">
|
||||
<Setter Property="Background" Value="White"></Setter>
|
||||
<Setter Property="BorderThickness" Value="1"></Setter>
|
||||
<Setter Property="BorderBrush" Value="Black"></Setter>
|
||||
<Setter Property="Padding" Value="6,4"></Setter>
|
||||
</Style>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@ -1,260 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Win32;
|
||||
using Remotely.Desktop.Core;
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using Remotely.Desktop.Core.Services;
|
||||
using Remotely.Desktop.Win.Services;
|
||||
using Remotely.Desktop.Win.Views;
|
||||
using Remotely.Shared.Models;
|
||||
using Remotely.Shared.Utilities;
|
||||
using Remotely.Shared.Win32;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using System.Windows;
|
||||
using Form = System.Windows.Forms.Form;
|
||||
|
||||
namespace Remotely.Desktop.Win
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
public Form BackgroundForm { get; private set; }
|
||||
private ICasterSocket _casterSocket { get; set; }
|
||||
private Conductor _conductor { get; set; }
|
||||
private ICursorIconWatcher CursorIconWatcher { get; set; }
|
||||
private IServiceProvider Services => ServiceContainer.Instance;
|
||||
|
||||
public async void CursorIconWatcher_OnChange(object sender, CursorInfo cursor)
|
||||
{
|
||||
if (_conductor?.Viewers?.Count > 0)
|
||||
{
|
||||
foreach (var viewer in _conductor.Viewers.Values)
|
||||
{
|
||||
await viewer.SendCursorChange(cursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Application_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
|
||||
{
|
||||
Logger.Write(e.Exception);
|
||||
MessageBox.Show("There was an unhandled exception.", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void Application_Startup(object sender, StartupEventArgs e)
|
||||
{
|
||||
if (Environment.GetCommandLineArgs().Contains("-elevate"))
|
||||
{
|
||||
var commandLine = Win32Interop.GetCommandLine().Replace(" -elevate", "");
|
||||
|
||||
Logger.Write($"Elevating process {commandLine}.");
|
||||
var result = Win32Interop.OpenInteractiveProcess(
|
||||
commandLine,
|
||||
-1,
|
||||
false,
|
||||
"default",
|
||||
true,
|
||||
out var procInfo);
|
||||
Logger.Write($"Elevate result: {result}. Process ID: {procInfo.dwProcessId}.");
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
_ = Task.Run(Initialize);
|
||||
}
|
||||
|
||||
private void BuildServices()
|
||||
{
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddLogging(builder =>
|
||||
{
|
||||
builder.AddConsole().AddDebug().AddEventLog();
|
||||
});
|
||||
|
||||
serviceCollection.AddSingleton<ICursorIconWatcher, CursorIconWatcherWin>();
|
||||
serviceCollection.AddSingleton<IScreenCaster, ScreenCaster>();
|
||||
serviceCollection.AddSingleton<IKeyboardMouseInput, KeyboardMouseInputWin>();
|
||||
serviceCollection.AddSingleton<IClipboardService, ClipboardServiceWin>();
|
||||
serviceCollection.AddSingleton<IAudioCapturer, AudioCapturerWin>();
|
||||
serviceCollection.AddSingleton<ICasterSocket, CasterSocket>();
|
||||
serviceCollection.AddSingleton<IdleTimer>();
|
||||
serviceCollection.AddSingleton<Conductor>();
|
||||
serviceCollection.AddSingleton<IChatClientService, ChatHostService>();
|
||||
serviceCollection.AddSingleton<IChatUiService, ChatUiServiceWin>();
|
||||
serviceCollection.AddTransient<IScreenCapturer, ScreenCapturerWin>();
|
||||
serviceCollection.AddTransient<Viewer>();
|
||||
serviceCollection.AddScoped<IWebRtcSessionFactory, WebRtcSessionFactory>();
|
||||
serviceCollection.AddScoped<IFileTransferService, FileTransferServiceWin>();
|
||||
serviceCollection.AddSingleton<ISessionIndicator, SessionIndicatorWin>();
|
||||
serviceCollection.AddSingleton<IShutdownService, ShutdownServiceWin>();
|
||||
serviceCollection.AddScoped<IDtoMessageHandler, DtoMessageHandler>();
|
||||
serviceCollection.AddScoped<IRemoteControlAccessService, RemoteControlAccessServiceWin>();
|
||||
serviceCollection.AddScoped<IConfigService, ConfigServiceWin>();
|
||||
serviceCollection.AddScoped<IDeviceInitService, DeviceInitService>();
|
||||
serviceCollection.AddScoped<IClickOnceService, ClickOnceService>();
|
||||
|
||||
BackgroundForm = new Form()
|
||||
{
|
||||
Visible = false,
|
||||
Opacity = 0,
|
||||
ShowIcon = false,
|
||||
ShowInTaskbar = false,
|
||||
WindowState = System.Windows.Forms.FormWindowState.Minimized
|
||||
};
|
||||
serviceCollection.AddSingleton((serviceProvider) => BackgroundForm);
|
||||
|
||||
ServiceContainer.Instance = serviceCollection.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
Logger.Write((Exception)e.ExceptionObject);
|
||||
}
|
||||
|
||||
private async Task Initialize()
|
||||
{
|
||||
try
|
||||
{
|
||||
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
|
||||
|
||||
var args = Environment.GetCommandLineArgs()
|
||||
.SkipWhile(x => !x.StartsWith("-"))
|
||||
.ToArray();
|
||||
|
||||
BuildServices();
|
||||
|
||||
_conductor = Services.GetRequiredService<Conductor>();
|
||||
_casterSocket = Services.GetRequiredService<ICasterSocket>();
|
||||
_conductor.ProcessArgs(args);
|
||||
|
||||
SystemEvents.SessionEnding += async (s, e) =>
|
||||
{
|
||||
if (e.Reason == SessionEndReasons.SystemShutdown)
|
||||
{
|
||||
await _casterSocket.DisconnectAllViewers();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
await Services.GetRequiredService<IClickOnceService>()
|
||||
.TrySetBrandingFromActivationUri()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var deviceInitService = Services.GetRequiredService<IDeviceInitService>();
|
||||
await deviceInitService.GetInitParams().ConfigureAwait(false);
|
||||
|
||||
StartWinFormsThread();
|
||||
|
||||
if (_conductor.Mode == Core.Enums.AppMode.Chat)
|
||||
{
|
||||
var chatService = Services.GetRequiredService<IChatClientService>();
|
||||
await chatService.StartChat(_conductor.RequesterID, _conductor.OrganizationName).ConfigureAwait(false);
|
||||
}
|
||||
else if (_conductor.Mode == Core.Enums.AppMode.Unattended)
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
ShutdownMode = ShutdownMode.OnExplicitShutdown;
|
||||
});
|
||||
await StartScreenCasting().ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
MainWindow = new MainWindow();
|
||||
MainWindow.Show();
|
||||
});
|
||||
}
|
||||
|
||||
WaitForAppExit();
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendReadyNotificationToViewers()
|
||||
{
|
||||
|
||||
if (_conductor.ArgDict.ContainsKey("relaunch"))
|
||||
{
|
||||
Logger.Write($"Resuming after relaunch.");
|
||||
var viewersString = _conductor.ArgDict["viewers"];
|
||||
var viewerIDs = viewersString.Split(",".ToCharArray());
|
||||
await _casterSocket.NotifyViewersRelaunchedScreenCasterReady(viewerIDs);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _casterSocket.NotifyRequesterUnattendedReady(_conductor.RequesterID);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartScreenCasting()
|
||||
{
|
||||
|
||||
CursorIconWatcher = Services.GetRequiredService<ICursorIconWatcher>();
|
||||
|
||||
await _casterSocket.Connect(_conductor.Host);
|
||||
await _casterSocket.SendDeviceInfo(_conductor.ServiceID, Environment.MachineName, _conductor.DeviceID);
|
||||
|
||||
if (Win32Interop.GetCurrentDesktop(out var currentDesktopName))
|
||||
{
|
||||
Logger.Write($"Setting initial desktop to {currentDesktopName}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Write("Failed to get initial desktop name.");
|
||||
}
|
||||
|
||||
if (!Win32Interop.SwitchToInputDesktop())
|
||||
{
|
||||
Logger.Write("Failed to set initial desktop.");
|
||||
}
|
||||
|
||||
await SendReadyNotificationToViewers();
|
||||
Services.GetRequiredService<IdleTimer>().Start();
|
||||
CursorIconWatcher.OnChange += CursorIconWatcher_OnChange;
|
||||
Services.GetRequiredService<IClipboardService>().BeginWatching();
|
||||
Services.GetRequiredService<IKeyboardMouseInput>().Init();
|
||||
}
|
||||
|
||||
private void StartWinFormsThread()
|
||||
{
|
||||
var winformsThread = new Thread(() =>
|
||||
{
|
||||
System.Windows.Forms.Application.Run(BackgroundForm);
|
||||
})
|
||||
{
|
||||
IsBackground = true
|
||||
};
|
||||
winformsThread.TrySetApartmentState(ApartmentState.STA);
|
||||
winformsThread.Start();
|
||||
|
||||
Logger.Write("Background WinForms thread started.");
|
||||
}
|
||||
private void WaitForAppExit()
|
||||
{
|
||||
var appExitEvent = new ManualResetEventSlim();
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
Exit += (s, a) =>
|
||||
{
|
||||
appExitEvent.Set();
|
||||
};
|
||||
});
|
||||
appExitEvent.Wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
using System.Windows;
|
||||
|
||||
[assembly: ThemeInfo(
|
||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||
//(used if a resource is not found in the page,
|
||||
// or application resource dictionaries)
|
||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||
//(used if a resource is not found in the page,
|
||||
// app, or any theme specific resource dictionaries)
|
||||
)]
|
||||
@ -16,7 +16,7 @@
|
||||
<Product>Remotely Desktop</Product>
|
||||
<PackageProjectUrl>https://remotely.one</PackageProjectUrl>
|
||||
<Platforms>AnyCPU;x86;x64</Platforms>
|
||||
<StartupObject>Remotely.Desktop.Win.App</StartupObject>
|
||||
<StartupObject></StartupObject>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
@ -38,7 +38,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.MixedReality.WebRTC" Version="2.0.2" />
|
||||
<PackageReference Include="NAudio" Version="2.1.0" />
|
||||
<PackageReference Include="SharpDX" Version="4.2.0" />
|
||||
<PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" />
|
||||
@ -46,8 +45,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Desktop.Core\Desktop.Core.csproj" />
|
||||
<ProjectReference Include="..\Desktop.Shared\Desktop.Shared.csproj" />
|
||||
<ProjectReference Include="..\Shared\Shared.csproj" />
|
||||
<ProjectReference Include="..\submodules\Immense.RemoteControl\Desktop.Windows\Desktop.Windows.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -55,36 +55,6 @@
|
||||
<EmbeddedResource Include="Assets\Remotely_Icon.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ApplicationDefinition Update="App.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</ApplicationDefinition>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="App.xaml.cs">
|
||||
<SubType>Code</SubType>
|
||||
<DependentUpon>App.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Views\HostNamePrompt.xaml.cs">
|
||||
<DependentUpon>HostNamePrompt.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Views\MainWindow.xaml.cs">
|
||||
<SubType>Code</SubType>
|
||||
<DependentUpon>MainWindow.xaml</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Views\HostNamePrompt.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Update="Views\MainWindow.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
<Exec Command="if $(SolutionDir) == *Undefined* (
 exit 0
)
if $(ConfigurationName) == Debug (
 if $(PlatformName) == AnyCPU (
 md "$(SolutionDir)Agent\bin\Debug\net6.0\Desktop\"
 xcopy "$(TargetDir)*" "$(SolutionDir)Agent\bin\Debug\net6.0\Desktop\" /y /e /i
 )
 if $(PlatformName) == x64 (
 md "$(SolutionDir)Agent\bin\x64\Debug\net6.0\Desktop\"
 xcopy "$(TargetDir)*" "$(SolutionDir)Agent\bin\x64\Debug\net6.0\Desktop\" /y /e /i
 )
 if $(PlatformName) == x86 (
 md "$(SolutionDir)Agent\bin\x86\Debug\net6.0\Desktop\"
 xcopy "$(TargetDir)*" "$(SolutionDir)Agent\bin\x86\Debug\net6.0\Desktop\" /y /e /i
 )
)" />
|
||||
</Target>
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
using Remotely.Shared.Utilities;
|
||||
using SharpDX.Direct3D11;
|
||||
using SharpDX.DXGI;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
namespace Remotely.Desktop.Win.Models
|
||||
{
|
||||
public class DirectXOutput : IDisposable
|
||||
{
|
||||
public DirectXOutput(Adapter1 adapter,
|
||||
SharpDX.Direct3D11.Device device,
|
||||
OutputDuplication outputDuplication,
|
||||
Texture2D texture2D,
|
||||
DisplayModeRotation rotation)
|
||||
{
|
||||
Adapter = adapter;
|
||||
Device = device;
|
||||
OutputDuplication = outputDuplication;
|
||||
Texture2D = texture2D;
|
||||
Rotation = rotation;
|
||||
Bounds = new Rectangle(0, 0, texture2D.Description.Width, texture2D.Description.Height);
|
||||
}
|
||||
|
||||
public Adapter1 Adapter { get; }
|
||||
public Rectangle Bounds { get; set; }
|
||||
public SharpDX.Direct3D11.Device Device { get; }
|
||||
public OutputDuplication OutputDuplication { get; }
|
||||
public DisplayModeRotation Rotation { get; }
|
||||
public Texture2D Texture2D { get; }
|
||||
public void Dispose()
|
||||
{
|
||||
OutputDuplication?.ReleaseFrame();
|
||||
Disposer.TryDisposeAll(OutputDuplication, Texture2D, Adapter, Device);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
57
Desktop.Win/Program.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using Immense.RemoteControl.Desktop.Shared.Abstractions;
|
||||
using Immense.RemoteControl.Desktop.UI.WPF.Services;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System;
|
||||
using Immense.RemoteControl.Desktop.Windows;
|
||||
using Remotely.Desktop.Shared.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Remotely.Shared.Services;
|
||||
using Immense.RemoteControl.Desktop.Shared.Services;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Immense.RemoteControl.Desktop.Shared.Enums;
|
||||
using Remotely.Shared;
|
||||
|
||||
var provider = await Startup.UseRemoteControlClient(
|
||||
args,
|
||||
config =>
|
||||
{
|
||||
config.AddBrandingProvider<BrandingProvider>();
|
||||
},
|
||||
services =>
|
||||
{
|
||||
services.AddLogging(builder =>
|
||||
{
|
||||
#if DEBUG
|
||||
builder.SetMinimumLevel(LogLevel.Debug);
|
||||
#endif
|
||||
builder.AddProvider(new FileLoggerProvider());
|
||||
});
|
||||
|
||||
services.AddSingleton<IOrganizationIdProvider, OrganizationIdProvider>();
|
||||
},
|
||||
async services =>
|
||||
{
|
||||
var appState = services.GetRequiredService<IAppState>();
|
||||
if (appState.ArgDict.TryGetValue("org-id", out var orgId))
|
||||
{
|
||||
var orgIdProvider = services.GetRequiredService<IOrganizationIdProvider>();
|
||||
orgIdProvider.OrganizationId = orgId;
|
||||
}
|
||||
|
||||
var brandingProvider = services.GetRequiredService<IBrandingProvider>();
|
||||
if (brandingProvider is BrandingProvider branding)
|
||||
{
|
||||
await branding.TrySetFromApi();
|
||||
}
|
||||
|
||||
},
|
||||
AppConstants.ServerUrl);
|
||||
|
||||
var dispatcher = provider.GetRequiredService<IWindowsUiDispatcher>();
|
||||
try
|
||||
{
|
||||
await Task.Delay(Timeout.InfiniteTimeSpan, dispatcher.ApplicationExitingToken);
|
||||
}
|
||||
catch (TaskCanceledException) { }
|
||||
@ -1,50 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<ApplicationRevision>0</ApplicationRevision>
|
||||
<ApplicationVersion>1.0.0.0</ApplicationVersion>
|
||||
<BootstrapperEnabled>True</BootstrapperEnabled>
|
||||
<Configuration>Release</Configuration>
|
||||
<CreateDesktopShortcut>True</CreateDesktopShortcut>
|
||||
<CreateWebPageOnPublish>True</CreateWebPageOnPublish>
|
||||
<ErrorReportUrl>https://remotely.one/Contact</ErrorReportUrl>
|
||||
<ExcludeDeploymentUrl>False</ExcludeDeploymentUrl>
|
||||
<GenerateManifests>True</GenerateManifests>
|
||||
<Install>true</Install>
|
||||
<InstallFrom>Web</InstallFrom>
|
||||
<InstallUrl>http://127.0.0.1:5000/Content/Win-x64/ClickOnce/</InstallUrl>
|
||||
<IsRevisionIncremented>False</IsRevisionIncremented>
|
||||
<IsWebBootstrapper>True</IsWebBootstrapper>
|
||||
<MapFileExtensions>True</MapFileExtensions>
|
||||
<OpenBrowserOnPublish>false</OpenBrowserOnPublish>
|
||||
<Platform>x64</Platform>
|
||||
<ProductName>Remotely Desktop</ProductName>
|
||||
<PublishDir>..\Server\wwwroot\Content\Win-x64\ClickOnce\</PublishDir>
|
||||
<PublishUrl>..\Server\wwwroot\Content\Win-x64\ClickOnce\</PublishUrl>
|
||||
<PublisherName>Translucency Software</PublisherName>
|
||||
<PublishProtocol>ClickOnce</PublishProtocol>
|
||||
<PublishReadyToRun>False</PublishReadyToRun>
|
||||
<PublishSingleFile>False</PublishSingleFile>
|
||||
<RuntimeIdentifier>win10-x64</RuntimeIdentifier>
|
||||
<SelfContained>False</SelfContained>
|
||||
<SignatureAlgorithm>(none)</SignatureAlgorithm>
|
||||
<SignManifests>False</SignManifests>
|
||||
<SuiteName>Remotely</SuiteName>
|
||||
<SupportUrl>https://remotely.one/Contact</SupportUrl>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<TrustUrlParameters>True</TrustUrlParameters>
|
||||
<UpdateEnabled>True</UpdateEnabled>
|
||||
<UpdateMode>Foreground</UpdateMode>
|
||||
<UpdateRequired>False</UpdateRequired>
|
||||
<WebPageFileName>Publish.html</WebPageFileName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<BootstrapperPackage Include="Microsoft.NetCore.DesktopRuntime.6.0.x64">
|
||||
<Install>true</Install>
|
||||
<ProductName>.NET Desktop Runtime 6.0.0 (x64)</ProductName>
|
||||
</BootstrapperPackage>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@ -1,50 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
https://go.microsoft.com/fwlink/?LinkID=208121.
|
||||
-->
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<ApplicationRevision>0</ApplicationRevision>
|
||||
<ApplicationVersion>1.0.0.0</ApplicationVersion>
|
||||
<BootstrapperEnabled>True</BootstrapperEnabled>
|
||||
<Configuration>Release</Configuration>
|
||||
<CreateDesktopShortcut>True</CreateDesktopShortcut>
|
||||
<CreateWebPageOnPublish>True</CreateWebPageOnPublish>
|
||||
<ErrorReportUrl>https://remotely.one/Contact</ErrorReportUrl>
|
||||
<ExcludeDeploymentUrl>False</ExcludeDeploymentUrl>
|
||||
<GenerateManifests>True</GenerateManifests>
|
||||
<Install>true</Install>
|
||||
<InstallFrom>Web</InstallFrom>
|
||||
<InstallUrl>http://127.0.0.1:5000/Content/Win-x86/ClickOnce/</InstallUrl>
|
||||
<IsRevisionIncremented>False</IsRevisionIncremented>
|
||||
<IsWebBootstrapper>True</IsWebBootstrapper>
|
||||
<MapFileExtensions>True</MapFileExtensions>
|
||||
<OpenBrowserOnPublish>false</OpenBrowserOnPublish>
|
||||
<Platform>x86</Platform>
|
||||
<ProductName>Remotely Desktop</ProductName>
|
||||
<PublishDir>..\Server\wwwroot\Content\Win-x86\ClickOnce\</PublishDir>
|
||||
<PublishUrl>..\Server\wwwroot\Content\Win-x86\ClickOnce\</PublishUrl>
|
||||
<PublisherName>Translucency Software</PublisherName>
|
||||
<PublishProtocol>ClickOnce</PublishProtocol>
|
||||
<PublishReadyToRun>False</PublishReadyToRun>
|
||||
<PublishSingleFile>False</PublishSingleFile>
|
||||
<RuntimeIdentifier>win10-x86</RuntimeIdentifier>
|
||||
<SelfContained>False</SelfContained>
|
||||
<SignatureAlgorithm>(none)</SignatureAlgorithm>
|
||||
<SignManifests>False</SignManifests>
|
||||
<SuiteName>Remotely</SuiteName>
|
||||
<SupportUrl>https://remotely.one/Contact</SupportUrl>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<TrustUrlParameters>True</TrustUrlParameters>
|
||||
<UpdateEnabled>True</UpdateEnabled>
|
||||
<UpdateMode>Foreground</UpdateMode>
|
||||
<UpdateRequired>False</UpdateRequired>
|
||||
<WebPageFileName>Publish.html</WebPageFileName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<BootstrapperPackage Include="Microsoft.NetCore.DesktopRuntime.6.0.x86">
|
||||
<Install>true</Install>
|
||||
<ProductName>.NET Desktop Runtime 6.0.0 (x86)</ProductName>
|
||||
</BootstrapperPackage>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@ -1,7 +1,8 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Desktop.Win": {
|
||||
"commandName": "Project"
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "-m Attended -s some-session-id -a vERyLonGAndCOMpleXKeY -o Immense -r Han"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,119 +0,0 @@
|
||||
using NAudio.Wave;
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Remotely.Desktop.Win.Services
|
||||
{
|
||||
public class AudioCapturerWin : IAudioCapturer
|
||||
{
|
||||
private readonly List<byte> _tempBuffer = new();
|
||||
|
||||
private WasapiLoopbackCapture _capturer;
|
||||
private Stopwatch _sendTimer;
|
||||
private WaveFormat _targetFormat;
|
||||
|
||||
|
||||
public AudioCapturerWin()
|
||||
{
|
||||
_targetFormat = new WaveFormat(16000, 8, 1);
|
||||
}
|
||||
|
||||
public event EventHandler<byte[]> AudioSampleReady;
|
||||
public void ToggleAudio(bool toggleOn)
|
||||
{
|
||||
if (toggleOn)
|
||||
{
|
||||
Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private void Capturer_DataAvailable(object sender, WaveInEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (args.Buffer.All(x => x == 0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.BytesRecorded > 0)
|
||||
{
|
||||
lock (_tempBuffer)
|
||||
{
|
||||
if (!_sendTimer.IsRunning)
|
||||
{
|
||||
_sendTimer.Restart();
|
||||
}
|
||||
|
||||
_tempBuffer.AddRange(args.Buffer.Take(args.BytesRecorded));
|
||||
|
||||
if (_tempBuffer.Count > 50_000 ||
|
||||
_sendTimer.Elapsed.TotalMilliseconds > 1000)
|
||||
{
|
||||
_sendTimer.Reset();
|
||||
SendTempBuffer();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private void SendTempBuffer()
|
||||
{
|
||||
if (_tempBuffer.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var ms1 = new MemoryStream();
|
||||
using (var wfw = new WaveFileWriter(ms1, _capturer.WaveFormat))
|
||||
{
|
||||
wfw.Write(_tempBuffer.ToArray(), 0, _tempBuffer.Count);
|
||||
}
|
||||
_tempBuffer.Clear();
|
||||
|
||||
// Resample to 16-bit so Firefox will play it.
|
||||
using var ms2 = new MemoryStream(ms1.ToArray());
|
||||
using var wfr = new WaveFileReader(ms2);
|
||||
using var ms3 = new MemoryStream();
|
||||
using (var resampler = new MediaFoundationResampler(wfr, _targetFormat))
|
||||
{
|
||||
WaveFileWriter.WriteWavFileToStream(ms3, resampler);
|
||||
}
|
||||
AudioSampleReady?.Invoke(this, ms3.ToArray());
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
try
|
||||
{
|
||||
_capturer = new WasapiLoopbackCapture();
|
||||
_sendTimer = Stopwatch.StartNew();
|
||||
_capturer.DataAvailable += Capturer_DataAvailable;
|
||||
_capturer.StartRecording();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private void Stop()
|
||||
{
|
||||
if (_capturer is not null)
|
||||
{
|
||||
_capturer.DataAvailable -= Capturer_DataAvailable;
|
||||
}
|
||||
_capturer?.StopRecording();
|
||||
_capturer.Dispose();
|
||||
_sendTimer?.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,59 +0,0 @@
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using Remotely.Desktop.Win.ViewModels;
|
||||
using Remotely.Desktop.Win.Views;
|
||||
using Remotely.Shared.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace Remotely.Desktop.Win.Services
|
||||
{
|
||||
public class ChatUiServiceWin : IChatUiService
|
||||
{
|
||||
private ChatWindowViewModel ChatViewModel { get; set; }
|
||||
|
||||
public event EventHandler ChatWindowClosed;
|
||||
|
||||
public void ReceiveChat(ChatMessage chatMessage)
|
||||
{
|
||||
App.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
if (chatMessage.Disconnected)
|
||||
{
|
||||
MessageBox.Show("Your partner has disconnected.", "Partner Disconnected", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
App.Current.Shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ChatViewModel != null)
|
||||
{
|
||||
ChatViewModel.SenderName = chatMessage.SenderName;
|
||||
ChatViewModel.ChatMessages.Add(chatMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void ShowChatWindow(string organizationName, StreamWriter writer)
|
||||
{
|
||||
App.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
var chatWindow = new ChatWindow();
|
||||
chatWindow.Closing += ChatWindow_Closing;
|
||||
ChatViewModel = chatWindow.DataContext as ChatWindowViewModel;
|
||||
ChatViewModel.PipeStreamWriter = writer;
|
||||
ChatViewModel.OrganizationName = organizationName;
|
||||
chatWindow.Show();
|
||||
});
|
||||
}
|
||||
|
||||
private void ChatWindow_Closing(object sender, CancelEventArgs e)
|
||||
{
|
||||
ChatWindowClosed?.Invoke(this, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,106 +0,0 @@
|
||||
using Remotely.Desktop.Core;
|
||||
using Remotely.Desktop.Core.Services;
|
||||
using Remotely.Shared.Enums;
|
||||
using Remotely.Shared.Models;
|
||||
using Remotely.Shared.Utilities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using System.Xml;
|
||||
|
||||
namespace Remotely.Desktop.Win.Services
|
||||
{
|
||||
public interface IClickOnceService
|
||||
{
|
||||
string GetActivationUri();
|
||||
Task TrySetBrandingFromActivationUri();
|
||||
}
|
||||
public class ClickOnceService : IClickOnceService
|
||||
{
|
||||
private static string _activationUri;
|
||||
private readonly IDeviceInitService _deviceInitService;
|
||||
private readonly Conductor _conductor;
|
||||
|
||||
public ClickOnceService(Conductor conductor, IDeviceInitService deviceInitService)
|
||||
{
|
||||
_conductor = conductor;
|
||||
_deviceInitService = deviceInitService;
|
||||
}
|
||||
|
||||
public string GetActivationUri()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_activationUri))
|
||||
{
|
||||
return _activationUri;
|
||||
}
|
||||
var appRoot = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory).Parent;
|
||||
if (!Directory.Exists(Path.Combine(appRoot.FullName, "manifests")))
|
||||
{
|
||||
Logger.Write($"Manifests folder not found in root folder: {appRoot}", EventType.Debug);
|
||||
return _activationUri;
|
||||
}
|
||||
|
||||
var manifestFiles = appRoot.GetFiles("manifests\\*tion_*.manifest", SearchOption.AllDirectories);
|
||||
var manifestFile = manifestFiles.FirstOrDefault();
|
||||
if (manifestFile is null)
|
||||
{
|
||||
Logger.Write($"Manifest file not found.", EventType.Warning);
|
||||
return _activationUri;
|
||||
}
|
||||
|
||||
var manifest = new XmlDocument();
|
||||
manifest.Load(manifestFile.FullName);
|
||||
var node = manifest.GetElementsByTagName("deploymentProvider")[0];
|
||||
_activationUri = node.Attributes["codebase"].Value;
|
||||
|
||||
Logger.Write($"Found ActivationUri: {_activationUri}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
|
||||
return _activationUri;
|
||||
}
|
||||
|
||||
public async Task TrySetBrandingFromActivationUri()
|
||||
{
|
||||
var activationUri = GetActivationUri();
|
||||
|
||||
if (Uri.TryCreate(activationUri, UriKind.Absolute, out var result))
|
||||
{
|
||||
var host = $"{result.Scheme}://{result.Authority}";
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(host))
|
||||
{
|
||||
_conductor.UpdateHost(host);
|
||||
using var httpClient = new HttpClient();
|
||||
try
|
||||
{
|
||||
var url = $"{host.TrimEnd('/')}/api/branding";
|
||||
var query = HttpUtility.ParseQueryString(result.Query);
|
||||
if (query?.AllKeys?.Contains("organizationid") == true)
|
||||
{
|
||||
url += $"?organizationId={query["organizationid"]}";
|
||||
_conductor.UpdateOrganizationId(query["organizationid"]);
|
||||
}
|
||||
var branding = await httpClient.GetFromJsonAsync<BrandingInfo>(url).ConfigureAwait(false);
|
||||
if (branding != null)
|
||||
{
|
||||
_deviceInitService.SetBrandingInfo(branding);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,103 +0,0 @@
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using Remotely.Shared.Utilities;
|
||||
using Remotely.Shared.Win32;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Remotely.Desktop.Win.Services
|
||||
{
|
||||
public class ClipboardServiceWin : IClipboardService
|
||||
{
|
||||
private CancellationTokenSource _cancelTokenSource;
|
||||
|
||||
public event EventHandler<string> ClipboardTextChanged;
|
||||
|
||||
private string ClipboardText { get; set; }
|
||||
|
||||
public void BeginWatching()
|
||||
{
|
||||
App.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
App.Current.Exit -= App_Exit;
|
||||
App.Current.Exit += App_Exit;
|
||||
});
|
||||
|
||||
StopWatching();
|
||||
|
||||
_cancelTokenSource = new CancellationTokenSource();
|
||||
|
||||
|
||||
WatchClipboard(_cancelTokenSource.Token);
|
||||
}
|
||||
|
||||
public Task SetText(string clipboardText)
|
||||
{
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(clipboardText))
|
||||
{
|
||||
Clipboard.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
Clipboard.SetText(clipboardText);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
});
|
||||
thread.SetApartmentState(ApartmentState.STA);
|
||||
thread.IsBackground = true;
|
||||
thread.Start();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void StopWatching()
|
||||
{
|
||||
try
|
||||
{
|
||||
_cancelTokenSource?.Cancel();
|
||||
_cancelTokenSource?.Dispose();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private void App_Exit(object sender, System.Windows.ExitEventArgs e)
|
||||
{
|
||||
_cancelTokenSource?.Cancel();
|
||||
}
|
||||
private void WatchClipboard(CancellationToken cancelToken)
|
||||
{
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
|
||||
while (!cancelToken.IsCancellationRequested)
|
||||
{
|
||||
|
||||
try
|
||||
{
|
||||
Win32Interop.SwitchToInputDesktop();
|
||||
|
||||
if (Clipboard.ContainsText() && Clipboard.GetText() != ClipboardText)
|
||||
{
|
||||
ClipboardText = Clipboard.GetText();
|
||||
ClipboardTextChanged?.Invoke(this, ClipboardText);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
Thread.Sleep(500);
|
||||
}
|
||||
});
|
||||
thread.SetApartmentState(ApartmentState.STA);
|
||||
thread.IsBackground = true;
|
||||
thread.Start();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using Remotely.Shared.Models;
|
||||
using Remotely.Shared.Utilities;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Remotely.Desktop.Win.Services
|
||||
{
|
||||
|
||||
public class ConfigServiceWin : IConfigService
|
||||
{
|
||||
private static readonly string _configFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Remotely");
|
||||
private static readonly string _configFile = Path.Combine(_configFolder, "Config.json");
|
||||
|
||||
public DesktopAppConfig GetConfig()
|
||||
{
|
||||
var config = new DesktopAppConfig();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.Host) &&
|
||||
File.Exists(_configFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
config = JsonSerializer.Deserialize<DesktopAppConfig>(File.ReadAllText(_configFile));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
public void Save(DesktopAppConfig config)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(_configFolder);
|
||||
File.WriteAllText(_configFile, JsonSerializer.Serialize(config));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,109 +0,0 @@
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using Remotely.Shared.Models;
|
||||
using Remotely.Shared.Win32;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Timers;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Remotely.Desktop.Win.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// A class that can be used to watch for cursor icon changes.
|
||||
/// </summary>
|
||||
public class CursorIconWatcherWin : ICursorIconWatcher
|
||||
{
|
||||
public CursorIconWatcherWin()
|
||||
{
|
||||
ChangeTimer = new System.Timers.Timer(25);
|
||||
ChangeTimer.Elapsed += ChangeTimer_Elapsed;
|
||||
ChangeTimer.Start();
|
||||
}
|
||||
public event EventHandler<CursorInfo> OnChange;
|
||||
private System.Timers.Timer ChangeTimer { get; set; }
|
||||
private string PreviousCursorHandle { get; set; }
|
||||
|
||||
private User32.CursorInfo cursorInfo;
|
||||
|
||||
|
||||
public CursorInfo GetCurrentCursor()
|
||||
{
|
||||
try
|
||||
{
|
||||
var ci = new User32.CursorInfo();
|
||||
ci.cbSize = Marshal.SizeOf(ci);
|
||||
User32.GetCursorInfo(out ci);
|
||||
if (ci.flags == User32.CURSOR_SHOWING)
|
||||
{
|
||||
if (ci.hCursor.ToString() == Cursors.IBeam.Handle.ToString())
|
||||
{
|
||||
return new CursorInfo(Array.Empty<byte>(), Point.Empty, "text");
|
||||
}
|
||||
|
||||
using var icon = Icon.FromHandle(ci.hCursor);
|
||||
using var ms = new MemoryStream();
|
||||
using var cursor = new Cursor(ci.hCursor);
|
||||
var hotspot = cursor.HotSpot;
|
||||
icon.ToBitmap().Save(ms, ImageFormat.Png);
|
||||
return new CursorInfo(ms.ToArray(), hotspot);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new CursorInfo(Array.Empty<byte>(), Point.Empty, "default");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new CursorInfo(Array.Empty<byte>(), Point.Empty, "default");
|
||||
}
|
||||
}
|
||||
|
||||
private void ChangeTimer_Elapsed(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
if (OnChange == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
cursorInfo = new User32.CursorInfo();
|
||||
cursorInfo.cbSize = Marshal.SizeOf(cursorInfo);
|
||||
User32.GetCursorInfo(out cursorInfo);
|
||||
if (cursorInfo.flags == User32.CURSOR_SHOWING)
|
||||
{
|
||||
var currentCursor = cursorInfo.hCursor.ToString();
|
||||
if (currentCursor != PreviousCursorHandle)
|
||||
{
|
||||
if (currentCursor == Cursors.IBeam.Handle.ToString())
|
||||
{
|
||||
OnChange?.Invoke(this, new CursorInfo(Array.Empty<byte>(), Point.Empty, "text"));
|
||||
}
|
||||
else
|
||||
{
|
||||
using var icon = Icon.FromHandle(cursorInfo.hCursor);
|
||||
using var ms = new MemoryStream();
|
||||
using var cursor = new Cursor(cursorInfo.hCursor);
|
||||
var hotspot = cursor.HotSpot;
|
||||
icon.ToBitmap().Save(ms, ImageFormat.Png);
|
||||
OnChange?.Invoke(this, new CursorInfo(ms.ToArray(), hotspot));
|
||||
}
|
||||
PreviousCursorHandle = currentCursor;
|
||||
}
|
||||
}
|
||||
else if (PreviousCursorHandle != "0")
|
||||
{
|
||||
PreviousCursorHandle = "0";
|
||||
OnChange?.Invoke(this, new CursorInfo(Array.Empty<byte>(), Point.Empty, "default"));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
OnChange?.Invoke(this, new CursorInfo(Array.Empty<byte>(), Point.Empty, "default"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,37 +0,0 @@
|
||||
using System;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Remotely.Desktop.Win.Services
|
||||
{
|
||||
public class Executor : ICommand
|
||||
{
|
||||
public Executor(Action<object> executeAction, Predicate<object> isExecutable = null)
|
||||
{
|
||||
ExecuteAction = executeAction;
|
||||
IsExecutable = isExecutable;
|
||||
}
|
||||
|
||||
public event EventHandler CanExecuteChanged
|
||||
{
|
||||
add { CommandManager.RequerySuggested += value; }
|
||||
remove { CommandManager.RequerySuggested -= value; }
|
||||
}
|
||||
|
||||
private Action<object> ExecuteAction { get; set; }
|
||||
|
||||
private Predicate<object> IsExecutable { get; set; }
|
||||
public bool CanExecute(object parameter)
|
||||
{
|
||||
if (IsExecutable == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return IsExecutable.Invoke(parameter);
|
||||
}
|
||||
|
||||
public void Execute(object parameter)
|
||||
{
|
||||
ExecuteAction.Invoke(parameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,202 +0,0 @@
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using Remotely.Desktop.Core.Services;
|
||||
using Remotely.Desktop.Core.ViewModels;
|
||||
using Remotely.Desktop.Win.ViewModels;
|
||||
using Remotely.Desktop.Win.Views;
|
||||
using Remotely.Shared.Utilities;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Security.AccessControl;
|
||||
using System.Security.Principal;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace Remotely.Desktop.Win.Services
|
||||
{
|
||||
public class FileTransferServiceWin : IFileTransferService
|
||||
{
|
||||
private static readonly ConcurrentDictionary<string, FileStream> _partialTransfers =
|
||||
new();
|
||||
|
||||
private static readonly ConcurrentDictionary<string, FileTransferWindow> _fileTransferWindows =
|
||||
new();
|
||||
|
||||
private static readonly SemaphoreSlim _writeLock = new(1,1);
|
||||
private static MessageBoxResult? _result;
|
||||
|
||||
public string GetBaseDirectory()
|
||||
{
|
||||
var programDataPath = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
|
||||
return Directory.CreateDirectory(Path.Combine(programDataPath, "Remotely", "Shared")).FullName;
|
||||
}
|
||||
|
||||
public void OpenFileTransferWindow(Viewer viewer)
|
||||
{
|
||||
App.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
if (_fileTransferWindows.TryGetValue(viewer.ViewerConnectionID, out var window))
|
||||
{
|
||||
window.Activate();
|
||||
}
|
||||
else
|
||||
{
|
||||
window = new FileTransferWindow
|
||||
{
|
||||
DataContext = new FileTransferWindowViewModel(viewer, this)
|
||||
};
|
||||
window.Closed += (sender, arg) =>
|
||||
{
|
||||
_fileTransferWindows.Remove(viewer.ViewerConnectionID, out _);
|
||||
};
|
||||
_fileTransferWindows.AddOrUpdate(viewer.ViewerConnectionID, window, (k, v) => window);
|
||||
window.Show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task ReceiveFile(byte[] buffer, string fileName, string messageId, bool endOfFile, bool startOfFile)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _writeLock.WaitAsync();
|
||||
|
||||
var baseDir = GetBaseDirectory();
|
||||
|
||||
SetFileOrFolderPermissions(baseDir);
|
||||
|
||||
if (startOfFile)
|
||||
{
|
||||
var filePath = Path.Combine(baseDir, fileName);
|
||||
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
var count = 0;
|
||||
var ext = Path.GetExtension(fileName);
|
||||
var fileWithoutExt = Path.GetFileNameWithoutExtension(fileName);
|
||||
while (File.Exists(filePath))
|
||||
{
|
||||
filePath = Path.Combine(baseDir, $"{fileWithoutExt}-{count}{ext}");
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
File.Create(filePath).Close();
|
||||
SetFileOrFolderPermissions(filePath);
|
||||
var fs = new FileStream(filePath, FileMode.OpenOrCreate);
|
||||
_partialTransfers.AddOrUpdate(messageId, fs, (k, v) => fs);
|
||||
}
|
||||
|
||||
var fileStream = _partialTransfers[messageId];
|
||||
|
||||
if (buffer?.Length > 0)
|
||||
{
|
||||
await fileStream.WriteAsync(buffer, 0, buffer.Length);
|
||||
|
||||
}
|
||||
|
||||
if (endOfFile)
|
||||
{
|
||||
fileStream.Close();
|
||||
_partialTransfers.Remove(messageId, out _);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_writeLock.Release();
|
||||
if (endOfFile)
|
||||
{
|
||||
await Task.Run(ShowTransferComplete);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UploadFile(FileUpload fileUpload, Viewer viewer, CancellationToken cancelToken, Action<double> progressUpdateCallback)
|
||||
{
|
||||
try
|
||||
{
|
||||
await viewer.SendFile(fileUpload, cancelToken, progressUpdateCallback);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetFileOrFolderPermissions(string path)
|
||||
{
|
||||
FileSystemSecurity ds;
|
||||
|
||||
var aclSections = AccessControlSections.Access | AccessControlSections.Group | AccessControlSections.Owner;
|
||||
if (File.Exists(path))
|
||||
{
|
||||
ds = new FileSecurity(path, aclSections);
|
||||
}
|
||||
else if (Directory.Exists(path))
|
||||
{
|
||||
ds = new DirectorySecurity(path, aclSections);
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sid = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null);
|
||||
var account = (NTAccount)sid.Translate(typeof(NTAccount));
|
||||
|
||||
var accessAlreadySet = false;
|
||||
|
||||
foreach (FileSystemAccessRule rule in ds.GetAccessRules(true, true, typeof(NTAccount)))
|
||||
{
|
||||
if (rule.IdentityReference == account &&
|
||||
rule.FileSystemRights.HasFlag(FileSystemRights.Modify) &&
|
||||
rule.AccessControlType == AccessControlType.Allow)
|
||||
{
|
||||
accessAlreadySet = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!accessAlreadySet)
|
||||
{
|
||||
ds.AddAccessRule(new FileSystemAccessRule(account, FileSystemRights.Modify, AccessControlType.Allow));
|
||||
if (File.Exists(path))
|
||||
{
|
||||
new FileInfo(path).SetAccessControl((FileSecurity)ds);
|
||||
}
|
||||
else if (Directory.Exists(path))
|
||||
{
|
||||
new DirectoryInfo(path).SetAccessControl((DirectorySecurity)ds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowTransferComplete()
|
||||
{
|
||||
// Prevent multiple dialogs from popping up.
|
||||
if (_result is null)
|
||||
{
|
||||
_result = MessageBox.Show("File transfer complete. Show folder?",
|
||||
"Transfer Complete",
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Question,
|
||||
MessageBoxResult.Yes,
|
||||
MessageBoxOptions.ServiceNotification);
|
||||
|
||||
if (_result == MessageBoxResult.Yes)
|
||||
{
|
||||
Process.Start("explorer.exe", GetBaseDirectory());
|
||||
}
|
||||
|
||||
_result = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,410 +0,0 @@
|
||||
using Remotely.Desktop.Core.Enums;
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using Remotely.Desktop.Core.Services;
|
||||
using Remotely.Shared.Utilities;
|
||||
using Remotely.Shared.Win32;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using static Remotely.Shared.Win32.User32;
|
||||
|
||||
namespace Remotely.Desktop.Win.Services
|
||||
{
|
||||
public class KeyboardMouseInputWin : IKeyboardMouseInput
|
||||
{
|
||||
private readonly ConcurrentQueue<Action> _inputActions = new();
|
||||
private CancellationTokenSource _cancelTokenSource;
|
||||
private volatile bool _inputBlocked;
|
||||
private Thread _inputProcessingThread;
|
||||
|
||||
public Tuple<double, double> 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;
|
||||
return new Tuple<double, double>(absoluteX / capturer.GetVirtualScreenBounds().Width, absoluteY / capturer.GetVirtualScreenBounds().Height);
|
||||
}
|
||||
|
||||
public Tuple<double, double> 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;
|
||||
return new Tuple<double, double>(absoluteX, absoluteY);
|
||||
}
|
||||
|
||||
public void Init()
|
||||
{
|
||||
App.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
App.Current.Exit -= App_Exit;
|
||||
App.Current.Exit += App_Exit;
|
||||
});
|
||||
|
||||
StartInputProcessingThread();
|
||||
}
|
||||
|
||||
public void SendKeyDown(string key)
|
||||
{
|
||||
TryOnInputDesktop(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!ConvertJavaScriptKeyToVirtualKey(key, out var keyCode) || keyCode is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var union = new InputUnion()
|
||||
{
|
||||
ki = new KEYBDINPUT()
|
||||
{
|
||||
wVk = keyCode.Value,
|
||||
wScan = (ScanCodeShort)MapVirtualKeyEx((uint)keyCode.Value, VkMapType.MAPVK_VK_TO_VSC, GetKeyboardLayout()),
|
||||
time = 0,
|
||||
dwExtraInfo = GetMessageExtraInfo()
|
||||
}
|
||||
};
|
||||
var input = new INPUT() { type = InputType.KEYBOARD, U = union };
|
||||
SendInput(1, new INPUT[] { input }, INPUT.Size);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public void SendKeyUp(string key)
|
||||
{
|
||||
TryOnInputDesktop(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!ConvertJavaScriptKeyToVirtualKey(key, out var keyCode) || keyCode is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var union = new InputUnion()
|
||||
{
|
||||
ki = new KEYBDINPUT()
|
||||
{
|
||||
wVk = keyCode.Value,
|
||||
wScan = (ScanCodeShort)MapVirtualKeyEx((uint)keyCode.Value, VkMapType.MAPVK_VK_TO_VSC, GetKeyboardLayout()),
|
||||
time = 0,
|
||||
dwFlags = KEYEVENTF.KEYUP,
|
||||
dwExtraInfo = GetMessageExtraInfo()
|
||||
}
|
||||
};
|
||||
var input = new INPUT() { type = InputType.KEYBOARD, U = union };
|
||||
SendInput(1, new INPUT[] { input }, INPUT.Size);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public void SendMouseButtonAction(int button, ButtonAction buttonAction, double percentX, double percentY, Viewer viewer)
|
||||
{
|
||||
TryOnInputDesktop(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
MOUSEEVENTF mouseEvent;
|
||||
switch (button)
|
||||
{
|
||||
case 0:
|
||||
switch (buttonAction)
|
||||
{
|
||||
case ButtonAction.Down:
|
||||
mouseEvent = MOUSEEVENTF.LEFTDOWN;
|
||||
break;
|
||||
case ButtonAction.Up:
|
||||
mouseEvent = MOUSEEVENTF.LEFTUP;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
switch (buttonAction)
|
||||
{
|
||||
case ButtonAction.Down:
|
||||
mouseEvent = MOUSEEVENTF.MIDDLEDOWN;
|
||||
break;
|
||||
case ButtonAction.Up:
|
||||
mouseEvent = MOUSEEVENTF.MIDDLEUP;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
switch (buttonAction)
|
||||
{
|
||||
case ButtonAction.Down:
|
||||
mouseEvent = MOUSEEVENTF.RIGHTDOWN;
|
||||
break;
|
||||
case ButtonAction.Up:
|
||||
mouseEvent = MOUSEEVENTF.RIGHTUP;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
var xyPercent = GetAbsolutePercentFromRelativePercent(percentX, percentY, viewer.Capturer);
|
||||
// Coordinates must be normalized. The bottom-right coordinate is mapped to 65535.
|
||||
var normalizedX = xyPercent.Item1 * 65535D;
|
||||
var normalizedY = xyPercent.Item2 * 65535D;
|
||||
var union = new InputUnion() { mi = new MOUSEINPUT() { dwFlags = MOUSEEVENTF.ABSOLUTE | mouseEvent | MOUSEEVENTF.VIRTUALDESK, dx = (int)normalizedX, dy = (int)normalizedY, time = 0, mouseData = 0, dwExtraInfo = GetMessageExtraInfo() } };
|
||||
var input = new INPUT() { type = InputType.MOUSE, U = union };
|
||||
SendInput(1, new INPUT[] { input }, INPUT.Size);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void SendMouseMove(double percentX, double percentY, Viewer viewer)
|
||||
{
|
||||
TryOnInputDesktop(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var xyPercent = GetAbsolutePercentFromRelativePercent(percentX, percentY, viewer.Capturer);
|
||||
// Coordinates must be normalized. The bottom-right coordinate is mapped to 65535.
|
||||
var normalizedX = xyPercent.Item1 * 65535D;
|
||||
var normalizedY = xyPercent.Item2 * 65535D;
|
||||
var union = new InputUnion() { mi = new MOUSEINPUT() { dwFlags = MOUSEEVENTF.ABSOLUTE | MOUSEEVENTF.MOVE | MOUSEEVENTF.VIRTUALDESK, dx = (int)normalizedX, dy = (int)normalizedY, time = 0, mouseData = 0, dwExtraInfo = GetMessageExtraInfo() } };
|
||||
var input = new INPUT() { type = InputType.MOUSE, U = union };
|
||||
SendInput(1, new INPUT[] { input }, INPUT.Size);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void SendMouseWheel(int deltaY)
|
||||
{
|
||||
TryOnInputDesktop(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (deltaY < 0)
|
||||
{
|
||||
deltaY = -120;
|
||||
}
|
||||
else if (deltaY > 0)
|
||||
{
|
||||
deltaY = 120;
|
||||
}
|
||||
var union = new InputUnion() { mi = new MOUSEINPUT() { dwFlags = MOUSEEVENTF.WHEEL, dx = 0, dy = 0, time = 0, mouseData = deltaY, dwExtraInfo = GetMessageExtraInfo() } };
|
||||
var input = new INPUT() { type = InputType.MOUSE, U = union };
|
||||
SendInput(1, new INPUT[] { input }, INPUT.Size);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void SendText(string transferText)
|
||||
{
|
||||
TryOnInputDesktop(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
SendKeys.SendWait(transferText);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void SetKeyStatesUp()
|
||||
{
|
||||
TryOnInputDesktop(() =>
|
||||
{
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
foreach (VirtualKey key in Enum.GetValues(typeof(VirtualKey)))
|
||||
{
|
||||
try
|
||||
{
|
||||
var state = GetKeyState(key);
|
||||
if (state == -127)
|
||||
{
|
||||
var union = new InputUnion()
|
||||
{
|
||||
ki = new KEYBDINPUT()
|
||||
{
|
||||
wVk = key,
|
||||
wScan = 0,
|
||||
time = 0,
|
||||
dwFlags = KEYEVENTF.KEYUP,
|
||||
dwExtraInfo = GetMessageExtraInfo()
|
||||
}
|
||||
};
|
||||
var input = new INPUT() { type = InputType.KEYBOARD, U = union };
|
||||
SendInput(1, new INPUT[] { input }, INPUT.Size);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
});
|
||||
thread.SetApartmentState(ApartmentState.STA);
|
||||
thread.Start();
|
||||
});
|
||||
}
|
||||
|
||||
public void ToggleBlockInput(bool toggleOn)
|
||||
{
|
||||
_inputActions.Enqueue(() =>
|
||||
{
|
||||
_inputBlocked = toggleOn;
|
||||
var result = BlockInput(toggleOn);
|
||||
Logger.Write($"Result of ToggleBlockInput set to {toggleOn}: {result}");
|
||||
});
|
||||
}
|
||||
|
||||
private void App_Exit(object sender, System.Windows.ExitEventArgs e)
|
||||
{
|
||||
_cancelTokenSource?.Cancel();
|
||||
}
|
||||
private void CheckQueue(CancellationToken cancelToken)
|
||||
{
|
||||
while (!cancelToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_inputActions.TryDequeue(out var action))
|
||||
{
|
||||
action();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Thread.Sleep(1);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Write($"Stopping input processing on thread {Thread.CurrentThread.ManagedThreadId}.");
|
||||
}
|
||||
|
||||
private bool ConvertJavaScriptKeyToVirtualKey(string key, out VirtualKey? result)
|
||||
{
|
||||
result = key switch
|
||||
{
|
||||
"Down" or "ArrowDown" => VirtualKey.DOWN,
|
||||
"Up" or "ArrowUp" => VirtualKey.UP,
|
||||
"Left" or "ArrowLeft" => VirtualKey.LEFT,
|
||||
"Right" or "ArrowRight" => VirtualKey.RIGHT,
|
||||
"Enter" => VirtualKey.RETURN,
|
||||
"Esc" or "Escape" => VirtualKey.ESCAPE,
|
||||
"Alt" => VirtualKey.MENU,
|
||||
"Control" => VirtualKey.CONTROL,
|
||||
"Shift" => VirtualKey.SHIFT,
|
||||
"PAUSE" => VirtualKey.PAUSE,
|
||||
"BREAK" => VirtualKey.PAUSE,
|
||||
"Backspace" => VirtualKey.BACK,
|
||||
"Tab" => VirtualKey.TAB,
|
||||
"CapsLock" => VirtualKey.CAPITAL,
|
||||
"Delete" => VirtualKey.DELETE,
|
||||
"Home" => VirtualKey.HOME,
|
||||
"End" => VirtualKey.END,
|
||||
"PageUp" => VirtualKey.PRIOR,
|
||||
"PageDown" => VirtualKey.NEXT,
|
||||
"NumLock" => VirtualKey.NUMLOCK,
|
||||
"Insert" => VirtualKey.INSERT,
|
||||
"ScrollLock" => VirtualKey.SCROLL,
|
||||
"F1" => VirtualKey.F1,
|
||||
"F2" => VirtualKey.F2,
|
||||
"F3" => VirtualKey.F3,
|
||||
"F4" => VirtualKey.F4,
|
||||
"F5" => VirtualKey.F5,
|
||||
"F6" => VirtualKey.F6,
|
||||
"F7" => VirtualKey.F7,
|
||||
"F8" => VirtualKey.F8,
|
||||
"F9" => VirtualKey.F9,
|
||||
"F10" => VirtualKey.F10,
|
||||
"F11" => VirtualKey.F11,
|
||||
"F12" => VirtualKey.F12,
|
||||
"Meta" => VirtualKey.LWIN,
|
||||
"ContextMenu" => VirtualKey.MENU,
|
||||
_ => key.Length == 1 ?
|
||||
(VirtualKey)VkKeyScan(Convert.ToChar(key)) :
|
||||
null
|
||||
};
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
Logger.Write($"Unable to parse key input: {key}.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private void StartInputProcessingThread()
|
||||
{
|
||||
_cancelTokenSource?.Cancel();
|
||||
_cancelTokenSource?.Dispose();
|
||||
|
||||
|
||||
// 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.
|
||||
_inputProcessingThread = new Thread(() =>
|
||||
{
|
||||
Logger.Write($"New input processing thread started on thread {Thread.CurrentThread.ManagedThreadId}.");
|
||||
_cancelTokenSource = new CancellationTokenSource();
|
||||
|
||||
if (_inputBlocked)
|
||||
{
|
||||
ToggleBlockInput(true);
|
||||
}
|
||||
CheckQueue(_cancelTokenSource.Token);
|
||||
});
|
||||
|
||||
_inputProcessingThread.SetApartmentState(ApartmentState.STA);
|
||||
_inputProcessingThread.Start();
|
||||
}
|
||||
|
||||
private void TryOnInputDesktop(Action inputAction)
|
||||
{
|
||||
_inputActions.Enqueue(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
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;
|
||||
}
|
||||
inputAction();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using Remotely.Desktop.Win.ViewModels;
|
||||
using Remotely.Desktop.Win.Views;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Remotely.Desktop.Win.Services
|
||||
{
|
||||
public class RemoteControlAccessServiceWin : IRemoteControlAccessService
|
||||
{
|
||||
public Task<bool> PromptForAccess(string requesterName, string organizationName)
|
||||
{
|
||||
var result = App.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
var promptWindow = new PromptForAccessWindow();
|
||||
var viewModel = promptWindow.DataContext as PromptForAccessWindowViewModel;
|
||||
if (!string.IsNullOrWhiteSpace(requesterName))
|
||||
{
|
||||
viewModel.RequesterName = requesterName;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(organizationName))
|
||||
{
|
||||
viewModel.OrganizationName = organizationName;
|
||||
}
|
||||
promptWindow.ShowDialog();
|
||||
|
||||
return viewModel.PromptResult;
|
||||
});
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,446 +0,0 @@
|
||||
// The DirectX capture code is based off examples from the
|
||||
// SharpDX Samples at https://github.com/sharpdx/SharpDX.
|
||||
|
||||
// Copyright (c) 2010-2013 SharpDX - Alexandre Mutel
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
using Microsoft.Win32;
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using Remotely.Desktop.Core.Utilities;
|
||||
using Remotely.Desktop.Win.Models;
|
||||
using Remotely.Shared.Utilities;
|
||||
using Remotely.Shared.Win32;
|
||||
using SharpDX;
|
||||
using SharpDX.Direct3D11;
|
||||
using SharpDX.DXGI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
using Remotely.Shared;
|
||||
using Result = Remotely.Shared.Result;
|
||||
using SkiaSharp;
|
||||
using SkiaSharp.Views.Desktop;
|
||||
using Remotely.Desktop.Core.Extensions;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Remotely.Desktop.Win.Services
|
||||
{
|
||||
public class ScreenCapturerWin : IScreenCapturer
|
||||
{
|
||||
private readonly Dictionary<string, int> _bitBltScreens = new();
|
||||
private readonly Dictionary<string, DirectXOutput> _directxScreens = new();
|
||||
private readonly object _screenBoundsLock = new();
|
||||
private SKBitmap _currentFrame;
|
||||
private SKBitmap _previousFrame;
|
||||
|
||||
public ScreenCapturerWin()
|
||||
{
|
||||
Init();
|
||||
SystemEvents.DisplaySettingsChanged += SystemEvents_DisplaySettingsChanged;
|
||||
}
|
||||
|
||||
public event EventHandler<Rectangle> ScreenChanged;
|
||||
|
||||
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()
|
||||
{
|
||||
try
|
||||
{
|
||||
SystemEvents.DisplaySettingsChanged -= SystemEvents_DisplaySettingsChanged;
|
||||
ClearDirectXOutputs();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
public IEnumerable<string> GetDisplayNames()
|
||||
{
|
||||
return Screen.AllScreens.Select(x => x.DeviceName);
|
||||
}
|
||||
|
||||
public SKRect GetFrameDiffArea()
|
||||
{
|
||||
return ImageUtils.GetDiffArea(_currentFrame, _previousFrame, CaptureFullscreen);
|
||||
}
|
||||
|
||||
|
||||
public Result<SKBitmap> GetImageDiff()
|
||||
{
|
||||
return ImageUtils.GetImageDiff(_currentFrame, _previousFrame);
|
||||
}
|
||||
|
||||
public Result<SKBitmap> GetNextFrame()
|
||||
{
|
||||
lock (_screenBoundsLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Win32Interop.SwitchToInputDesktop())
|
||||
{
|
||||
// Something will occasionally prevent this from succeeding after active
|
||||
// desktop has changed to/from WinLogon (err code 170). I'm guessing a hook
|
||||
// is getting put in the desktop, which causes SetThreadDesktop to fail.
|
||||
// The caller can start a new thread, which seems to resolve it.
|
||||
var errCode = Marshal.GetLastWin32Error();
|
||||
var errMessage = $"Failed to switch to input desktop. Last Win32 error code: {errCode}";
|
||||
Logger.Write(errMessage);
|
||||
return Result.Fail<SKBitmap>(errMessage);
|
||||
}
|
||||
|
||||
if (NeedsInit)
|
||||
{
|
||||
Logger.Write("Init needed in GetNextFrame.");
|
||||
Init();
|
||||
}
|
||||
|
||||
SwapFrames();
|
||||
|
||||
var result = GetDirectXFrame();
|
||||
|
||||
if (!result.IsSuccess || result.Value is null || IsEmpty(result.Value))
|
||||
{
|
||||
result = GetBitBltFrame();
|
||||
if (!result.IsSuccess || result.Value is null)
|
||||
{
|
||||
var ex = result.Exception ?? new("Unknown error.");
|
||||
Logger.Write(ex);
|
||||
return Result.Fail<SKBitmap>(ex);
|
||||
}
|
||||
}
|
||||
|
||||
_currentFrame = result.Value;
|
||||
return result;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Write(e);
|
||||
NeedsInit = true;
|
||||
return Result.Fail<SKBitmap>(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int GetScreenCount()
|
||||
{
|
||||
return Screen.AllScreens.Length;
|
||||
}
|
||||
|
||||
public int GetSelectedScreenIndex()
|
||||
{
|
||||
if (_bitBltScreens.TryGetValue(SelectedScreen, out var index))
|
||||
{
|
||||
return index;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public Rectangle GetVirtualScreenBounds()
|
||||
{
|
||||
return SystemInformation.VirtualScreen;
|
||||
}
|
||||
|
||||
public void Init()
|
||||
{
|
||||
Win32Interop.SwitchToInputDesktop();
|
||||
|
||||
CaptureFullscreen = true;
|
||||
InitBitBlt();
|
||||
InitDirectX();
|
||||
|
||||
ScreenChanged?.Invoke(this, CurrentScreenBounds);
|
||||
|
||||
NeedsInit = false;
|
||||
}
|
||||
|
||||
public void SetSelectedScreen(string displayName)
|
||||
{
|
||||
lock (_screenBoundsLock)
|
||||
{
|
||||
if (displayName == SelectedScreen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_bitBltScreens.ContainsKey(displayName))
|
||||
{
|
||||
SelectedScreen = displayName;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedScreen = _bitBltScreens.Keys.First();
|
||||
}
|
||||
RefreshCurrentScreenBounds();
|
||||
}
|
||||
}
|
||||
|
||||
internal Result<SKBitmap> GetBitBltFrame()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var bitmap = new Bitmap(CurrentScreenBounds.Width, CurrentScreenBounds.Height, PixelFormat.Format32bppArgb);
|
||||
using (var graphic = Graphics.FromImage(bitmap))
|
||||
{
|
||||
graphic.CopyFromScreen(CurrentScreenBounds.Left, CurrentScreenBounds.Top, 0, 0, new Size(CurrentScreenBounds.Width, CurrentScreenBounds.Height));
|
||||
}
|
||||
return Result.Ok(bitmap.ToSKBitmap());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
Logger.Write("Capturer error in BitBltCapture.");
|
||||
NeedsInit = true;
|
||||
return Result.Fail<SKBitmap>("Error while capturing BitBlt frame.");
|
||||
}
|
||||
}
|
||||
|
||||
internal Result<SKBitmap> GetDirectXFrame()
|
||||
{
|
||||
if (!_directxScreens.TryGetValue(SelectedScreen, out var dxOutput))
|
||||
{
|
||||
return Result.Fail<SKBitmap>("DirectX output not found.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var outputDuplication = dxOutput.OutputDuplication;
|
||||
var device = dxOutput.Device;
|
||||
var texture2D = dxOutput.Texture2D;
|
||||
var bounds = dxOutput.Bounds;
|
||||
|
||||
var result = outputDuplication.TryAcquireNextFrame(50, out var duplicateFrameInfo, out var screenResource);
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
return Result.Fail<SKBitmap>("Next frame did not arrive.");
|
||||
}
|
||||
|
||||
if (duplicateFrameInfo.AccumulatedFrames == 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
outputDuplication.ReleaseFrame();
|
||||
}
|
||||
catch { }
|
||||
return Result.Fail<SKBitmap>("No frames were accumulated.");
|
||||
}
|
||||
|
||||
using Texture2D screenTexture2D = screenResource.QueryInterface<Texture2D>();
|
||||
device.ImmediateContext.CopyResource(screenTexture2D, texture2D);
|
||||
var dataBox = device.ImmediateContext.MapSubresource(texture2D, 0, MapMode.Read, SharpDX.Direct3D11.MapFlags.None);
|
||||
using var bitmap = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format32bppArgb);
|
||||
var bitmapData = bitmap.LockBits(bounds, ImageLockMode.WriteOnly, bitmap.PixelFormat);
|
||||
var dataBoxPointer = dataBox.DataPointer;
|
||||
var bitmapDataPointer = bitmapData.Scan0;
|
||||
for (var y = 0; y < bounds.Height; y++)
|
||||
{
|
||||
Utilities.CopyMemory(bitmapDataPointer, dataBoxPointer, bounds.Width * 4);
|
||||
dataBoxPointer = IntPtr.Add(dataBoxPointer, dataBox.RowPitch);
|
||||
bitmapDataPointer = IntPtr.Add(bitmapDataPointer, bitmapData.Stride);
|
||||
}
|
||||
bitmap.UnlockBits(bitmapData);
|
||||
device.ImmediateContext.UnmapSubresource(texture2D, 0);
|
||||
screenResource?.Dispose();
|
||||
|
||||
switch (dxOutput.Rotation)
|
||||
{
|
||||
case DisplayModeRotation.Unspecified:
|
||||
case DisplayModeRotation.Identity:
|
||||
break;
|
||||
case DisplayModeRotation.Rotate90:
|
||||
bitmap.RotateFlip(RotateFlipType.Rotate270FlipNone);
|
||||
break;
|
||||
case DisplayModeRotation.Rotate180:
|
||||
bitmap.RotateFlip(RotateFlipType.Rotate180FlipNone);
|
||||
break;
|
||||
case DisplayModeRotation.Rotate270:
|
||||
bitmap.RotateFlip(RotateFlipType.Rotate90FlipNone);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return Result.Ok(bitmap.ToSKBitmap());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex, "Error while getting DirectX frame.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
dxOutput.OutputDuplication.ReleaseFrame();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
return Result.Fail<SKBitmap>("Failed to get DirectX frame.");
|
||||
}
|
||||
|
||||
private void ClearDirectXOutputs()
|
||||
{
|
||||
foreach (var screen in _directxScreens.Values)
|
||||
{
|
||||
try
|
||||
{
|
||||
screen.Dispose();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
_directxScreens.Clear();
|
||||
}
|
||||
private void InitBitBlt()
|
||||
{
|
||||
_bitBltScreens.Clear();
|
||||
for (var i = 0; i < Screen.AllScreens.Length; i++)
|
||||
{
|
||||
_bitBltScreens.Add(Screen.AllScreens[i].DeviceName, i);
|
||||
}
|
||||
}
|
||||
|
||||
private void InitDirectX()
|
||||
{
|
||||
try
|
||||
{
|
||||
ClearDirectXOutputs();
|
||||
|
||||
using var factory = new Factory1();
|
||||
foreach (var adapter in factory.Adapters1.Where(x => (x.Outputs?.Length ?? 0) > 0))
|
||||
{
|
||||
foreach (var output in adapter.Outputs)
|
||||
{
|
||||
try
|
||||
{
|
||||
var device = new SharpDX.Direct3D11.Device(adapter);
|
||||
var output1 = output.QueryInterface<Output1>();
|
||||
|
||||
var bounds = output1.Description.DesktopBounds;
|
||||
var width = bounds.Right - bounds.Left;
|
||||
var height = bounds.Bottom - bounds.Top;
|
||||
|
||||
// Create Staging texture CPU-accessible
|
||||
var textureDesc = new Texture2DDescription
|
||||
{
|
||||
CpuAccessFlags = CpuAccessFlags.Read,
|
||||
BindFlags = BindFlags.None,
|
||||
Format = Format.B8G8R8A8_UNorm,
|
||||
Width = width,
|
||||
Height = height,
|
||||
OptionFlags = ResourceOptionFlags.None,
|
||||
MipLevels = 1,
|
||||
ArraySize = 1,
|
||||
SampleDescription = { Count = 1, Quality = 0 },
|
||||
Usage = ResourceUsage.Staging
|
||||
};
|
||||
|
||||
var texture2D = new Texture2D(device, textureDesc);
|
||||
|
||||
_directxScreens.Add(
|
||||
output1.Description.DeviceName,
|
||||
new DirectXOutput(adapter,
|
||||
device,
|
||||
output1.DuplicateOutput(device),
|
||||
texture2D,
|
||||
output1.Description.Rotation));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsEmpty(SKBitmap bitmap)
|
||||
{
|
||||
if (bitmap is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var height = bitmap.Height;
|
||||
var width = bitmap.Width;
|
||||
var bytesPerPixel = bitmap.BytesPerPixel;
|
||||
|
||||
try
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
byte* scan = (byte*)bitmap.GetPixels();
|
||||
|
||||
for (var row = 0; row < height; row++)
|
||||
{
|
||||
for (var column = 0; column < width; column++)
|
||||
{
|
||||
var index = (row * width * bytesPerPixel) + (column * bytesPerPixel);
|
||||
|
||||
byte* data = scan + index;
|
||||
|
||||
for (var i = 0; i < bytesPerPixel; i++)
|
||||
{
|
||||
if (data[i] != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshCurrentScreenBounds()
|
||||
{
|
||||
CurrentScreenBounds = Screen.AllScreens[_bitBltScreens[SelectedScreen]].Bounds;
|
||||
CaptureFullscreen = true;
|
||||
NeedsInit = true;
|
||||
ScreenChanged?.Invoke(this, CurrentScreenBounds);
|
||||
}
|
||||
|
||||
private void SwapFrames()
|
||||
{
|
||||
if (_currentFrame != null)
|
||||
{
|
||||
_previousFrame?.Dispose();
|
||||
_previousFrame = _currentFrame;
|
||||
}
|
||||
}
|
||||
private void SystemEvents_DisplaySettingsChanged(object sender, EventArgs e)
|
||||
{
|
||||
RefreshCurrentScreenBounds();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,98 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Remotely.Desktop.Core;
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using Remotely.Desktop.Core.Services;
|
||||
using Remotely.Desktop.Core.Utilities;
|
||||
using Remotely.Shared.Utilities;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace Remotely.Desktop.Win.Services
|
||||
{
|
||||
public class SessionIndicatorWin : ISessionIndicator
|
||||
{
|
||||
private readonly Form _backgroundForm;
|
||||
private readonly IDeviceInitService _deviceInitService;
|
||||
private Container _container;
|
||||
private ContextMenuStrip _contextMenuStrip;
|
||||
private NotifyIcon _notifyIcon;
|
||||
|
||||
public SessionIndicatorWin(Form backgroundForm, IDeviceInitService deviceInitService)
|
||||
{
|
||||
_backgroundForm = backgroundForm;
|
||||
_deviceInitService = deviceInitService;
|
||||
}
|
||||
public void Show()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_notifyIcon != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
App.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
App.Current.Exit += App_Exit;
|
||||
});
|
||||
|
||||
_backgroundForm.Invoke(new Action(() =>
|
||||
{
|
||||
_container = new Container();
|
||||
_contextMenuStrip = new ContextMenuStrip(_container);
|
||||
_contextMenuStrip.Items.Add("Exit", null, ExitMenuItem_Click);
|
||||
|
||||
Icon icon;
|
||||
|
||||
if (_deviceInitService.BrandingInfo?.Icon?.Any() == true)
|
||||
{
|
||||
using var ms = new MemoryStream(_deviceInitService.BrandingInfo.Icon);
|
||||
using var bitmap = new Bitmap(ms);
|
||||
icon = Icon.FromHandle(bitmap.GetHicon());
|
||||
}
|
||||
else
|
||||
{
|
||||
icon = Icon.ExtractAssociatedIcon(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, EnvironmentHelper.DesktopExecutableFileName));
|
||||
}
|
||||
|
||||
_notifyIcon = new NotifyIcon(_container)
|
||||
{
|
||||
Icon = icon,
|
||||
Text = "Remote Control Session",
|
||||
BalloonTipIcon = ToolTipIcon.Info,
|
||||
BalloonTipText = "A remote control session has started.",
|
||||
BalloonTipTitle = "Remote Control Started",
|
||||
ContextMenuStrip = _contextMenuStrip
|
||||
};
|
||||
|
||||
_notifyIcon.Visible = true;
|
||||
_notifyIcon.ShowBalloonTip(3000);
|
||||
}));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void App_Exit(object sender, EventArgs e)
|
||||
{
|
||||
if (_notifyIcon != null)
|
||||
{
|
||||
_notifyIcon.Visible = false;
|
||||
_notifyIcon?.Dispose();
|
||||
_notifyIcon?.Icon?.Dispose();
|
||||
}
|
||||
}
|
||||
private async void ExitMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
var casterSocket = ServiceContainer.Instance.GetRequiredService<ICasterSocket>();
|
||||
await casterSocket.DisconnectAllViewers();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Remotely.Desktop.Core;
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using Remotely.Desktop.Core.Services;
|
||||
using Remotely.Shared.Utilities;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace Remotely.Desktop.Win.Services
|
||||
{
|
||||
public class ShutdownServiceWin : IShutdownService
|
||||
{
|
||||
public async Task Shutdown()
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.Write($"Exiting process ID {Environment.ProcessId}.");
|
||||
var casterSocket = ServiceContainer.Instance.GetRequiredService<ICasterSocket>();
|
||||
await casterSocket.DisconnectAllViewers();
|
||||
await casterSocket.Disconnect();
|
||||
System.Windows.Forms.Application.Exit();
|
||||
App.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
App.Current.Shutdown();
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,97 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Remotely.Desktop.Core;
|
||||
using Remotely.Desktop.Core.Services;
|
||||
using Remotely.Desktop.Core.ViewModels;
|
||||
using Remotely.Shared.Models;
|
||||
using Remotely.Shared.Utilities;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace Remotely.Desktop.Win.ViewModels
|
||||
{
|
||||
public class BrandedViewModelBase : ViewModelBase
|
||||
{
|
||||
public BrandedViewModelBase()
|
||||
{
|
||||
DeviceInitService = ServiceContainer.Instance?.GetRequiredService<IDeviceInitService>();
|
||||
|
||||
ApplyBranding();
|
||||
}
|
||||
|
||||
public void ApplyBranding()
|
||||
{
|
||||
try
|
||||
{
|
||||
var brandingInfo = DeviceInitService?.BrandingInfo ?? new BrandingInfo();
|
||||
|
||||
ProductName = "Remotely";
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(brandingInfo?.Product))
|
||||
{
|
||||
ProductName = brandingInfo.Product;
|
||||
}
|
||||
|
||||
TitleBackgroundColor = new SolidColorBrush(Color.FromRgb(
|
||||
brandingInfo?.TitleBackgroundRed ?? 70,
|
||||
brandingInfo?.TitleBackgroundGreen ?? 70,
|
||||
brandingInfo?.TitleBackgroundBlue ?? 70));
|
||||
|
||||
TitleForegroundColor = new SolidColorBrush(Color.FromRgb(
|
||||
brandingInfo?.TitleForegroundRed ?? 29,
|
||||
brandingInfo?.TitleForegroundGreen ?? 144,
|
||||
brandingInfo?.TitleForegroundBlue ?? 241));
|
||||
|
||||
TitleButtonForegroundColor = new SolidColorBrush(Color.FromRgb(
|
||||
brandingInfo?.ButtonForegroundRed ?? 255,
|
||||
brandingInfo?.ButtonForegroundGreen ?? 255,
|
||||
brandingInfo?.ButtonForegroundBlue ?? 255));
|
||||
|
||||
Icon = GetBitmapImageIcon(brandingInfo);
|
||||
|
||||
FirePropertyChanged(nameof(ProductName));
|
||||
FirePropertyChanged(nameof(TitleBackgroundColor));
|
||||
FirePropertyChanged(nameof(TitleForegroundColor));
|
||||
FirePropertyChanged(nameof(TitleButtonForegroundColor));
|
||||
FirePropertyChanged(nameof(Icon));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex, "Error applying branding.");
|
||||
}
|
||||
}
|
||||
|
||||
public BitmapImage Icon { get; set; }
|
||||
public string ProductName { get; set; }
|
||||
public SolidColorBrush TitleBackgroundColor { get; set; }
|
||||
public SolidColorBrush TitleButtonForegroundColor { get; set; }
|
||||
public SolidColorBrush TitleForegroundColor { get; set; }
|
||||
protected IDeviceInitService DeviceInitService { get; }
|
||||
private BitmapImage GetBitmapImageIcon(BrandingInfo bi)
|
||||
{
|
||||
Stream imageStream;
|
||||
if (bi.Icon?.Any() == true)
|
||||
{
|
||||
imageStream = new MemoryStream(bi.Icon);
|
||||
}
|
||||
else
|
||||
{
|
||||
imageStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Remotely.Desktop.Win.Assets.Remotely_Icon.png");
|
||||
}
|
||||
|
||||
var bitmap = new BitmapImage();
|
||||
bitmap.BeginInit();
|
||||
bitmap.StreamSource = imageStream;
|
||||
bitmap.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bitmap.EndInit();
|
||||
bitmap.Freeze();
|
||||
imageStream.Close();
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,84 +0,0 @@
|
||||
using Remotely.Shared.Models;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Remotely.Desktop.Win.ViewModels
|
||||
{
|
||||
public class ChatWindowViewModel : BrandedViewModelBase
|
||||
{
|
||||
private string _inputText;
|
||||
private string _organizationName = "your IT provider";
|
||||
private string _senderName = "a technician";
|
||||
|
||||
public ObservableCollection<ChatMessage> ChatMessages { get; } = new ObservableCollection<ChatMessage>();
|
||||
|
||||
public string InputText
|
||||
{
|
||||
get
|
||||
{
|
||||
return _inputText;
|
||||
}
|
||||
set
|
||||
{
|
||||
_inputText = value;
|
||||
FirePropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string OrganizationName
|
||||
{
|
||||
get
|
||||
{
|
||||
return _organizationName;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value) ||
|
||||
value == _organizationName)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
_organizationName = value;
|
||||
FirePropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public StreamWriter PipeStreamWriter { get; set; }
|
||||
public string SenderName
|
||||
{
|
||||
get
|
||||
{
|
||||
return _senderName;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value == _senderName)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_senderName = value;
|
||||
FirePropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SendChatMessage()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(InputText))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var chatMessage = new ChatMessage(string.Empty, InputText);
|
||||
InputText = string.Empty;
|
||||
await PipeStreamWriter.WriteLineAsync(JsonSerializer.Serialize(chatMessage));
|
||||
await PipeStreamWriter.FlushAsync();
|
||||
chatMessage.SenderName = "You";
|
||||
ChatMessages.Add(chatMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,129 +0,0 @@
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using Remotely.Desktop.Core.Services;
|
||||
using Remotely.Desktop.Core.ViewModels;
|
||||
using Remotely.Desktop.Win.Services;
|
||||
using Remotely.Shared.Win32;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Remotely.Desktop.Win.ViewModels
|
||||
{
|
||||
public class FileTransferWindowViewModel : BrandedViewModelBase
|
||||
{
|
||||
private readonly IFileTransferService _fileTransferService;
|
||||
private readonly Viewer _viewer;
|
||||
private string _viewerConnectionId;
|
||||
private string _viewerName;
|
||||
|
||||
public FileTransferWindowViewModel() { }
|
||||
|
||||
public FileTransferWindowViewModel(
|
||||
Viewer viewer,
|
||||
IFileTransferService fileTransferService)
|
||||
{
|
||||
_fileTransferService = fileTransferService;
|
||||
_viewer = viewer;
|
||||
ViewerName = viewer.Name;
|
||||
ViewerConnectionId = viewer.ViewerConnectionID;
|
||||
}
|
||||
|
||||
public ObservableCollection<FileUpload> FileUploads { get; } = new ObservableCollection<FileUpload>();
|
||||
|
||||
public ICommand OpenFileUploadDialog => new Executor(async (param) =>
|
||||
{
|
||||
// Change initial directory so it doesn't open in %userprofile% path
|
||||
// for SYSTEM account.
|
||||
var rootDir = Path.GetPathRoot(Environment.SystemDirectory);
|
||||
var userDir = Path.Combine(rootDir,
|
||||
"Users",
|
||||
Win32Interop.GetUsernameFromSessionId((uint)Process.GetCurrentProcess().SessionId));
|
||||
|
||||
var ofd = new OpenFileDialog()
|
||||
{
|
||||
Title = "Upload File via Remotely",
|
||||
Multiselect = true,
|
||||
CheckFileExists = true,
|
||||
InitialDirectory = Directory.Exists(userDir) ? userDir : rootDir
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
// The OpenFileDialog throws an error if SYSTEM doesn't have a Desktop folder.
|
||||
var desktop = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Desktop");
|
||||
Directory.CreateDirectory(desktop);
|
||||
}
|
||||
catch { }
|
||||
|
||||
var result = ofd.ShowDialog();
|
||||
if (result == DialogResult.Cancel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (var file in ofd.FileNames)
|
||||
{
|
||||
if (File.Exists(file))
|
||||
{
|
||||
await UploadFile(file);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
public string ViewerConnectionId
|
||||
{
|
||||
get
|
||||
{
|
||||
return _viewerConnectionId;
|
||||
}
|
||||
set
|
||||
{
|
||||
_viewerConnectionId = value;
|
||||
FirePropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string ViewerName
|
||||
{
|
||||
get
|
||||
{
|
||||
return _viewerName;
|
||||
}
|
||||
set
|
||||
{
|
||||
_viewerName = value;
|
||||
FirePropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UploadFile(string filePath)
|
||||
{
|
||||
var fileUpload = new FileUpload()
|
||||
{
|
||||
FilePath = filePath
|
||||
};
|
||||
|
||||
App.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
FileUploads.Add(fileUpload);
|
||||
});
|
||||
|
||||
await _fileTransferService.UploadFile(fileUpload, _viewer, fileUpload.CancellationTokenSource.Token, (double progress) =>
|
||||
{
|
||||
App.Current.Dispatcher.Invoke(() => fileUpload.PercentProgress = progress);
|
||||
});
|
||||
}
|
||||
|
||||
public ICommand RemoveFileUpload => new Executor((param) =>
|
||||
{
|
||||
if (param is FileUpload fileUpload)
|
||||
{
|
||||
FileUploads.Remove(fileUpload);
|
||||
fileUpload.CancellationTokenSource.Cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
using Remotely.Desktop.Core.ViewModels;
|
||||
|
||||
namespace Remotely.Desktop.Win.ViewModels
|
||||
{
|
||||
public class HostNamePromptViewModel : BrandedViewModelBase
|
||||
{
|
||||
private string _host = "https://";
|
||||
|
||||
public string Host
|
||||
{
|
||||
get => _host;
|
||||
set
|
||||
{
|
||||
_host = value;
|
||||
FirePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,355 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Remotely.Desktop.Core;
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using Remotely.Desktop.Core.Services;
|
||||
using Remotely.Desktop.Win.Services;
|
||||
using Remotely.Desktop.Win.Views;
|
||||
using Remotely.Shared.Models;
|
||||
using Remotely.Shared.Utilities;
|
||||
using Remotely.Shared.Win32;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Security.Principal;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Remotely.Desktop.Win.ViewModels
|
||||
{
|
||||
public class MainWindowViewModel : BrandedViewModelBase
|
||||
{
|
||||
private readonly ICasterSocket _casterSocket;
|
||||
private readonly Conductor _conductor;
|
||||
private readonly IConfigService _configService;
|
||||
private readonly ICursorIconWatcher _cursorIconWatcher;
|
||||
private string _host;
|
||||
private string _sessionId;
|
||||
private string _statusMessage;
|
||||
|
||||
public MainWindowViewModel()
|
||||
{
|
||||
Current = this;
|
||||
|
||||
if (Services is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Application.Current.Exit += Application_Exit;
|
||||
|
||||
_configService = Services.GetRequiredService<IConfigService>();
|
||||
_cursorIconWatcher = Services.GetRequiredService<ICursorIconWatcher>();
|
||||
_cursorIconWatcher.OnChange += CursorIconWatcher_OnChange;
|
||||
_conductor = Services.GetRequiredService<Conductor>();
|
||||
_casterSocket = Services.GetRequiredService<ICasterSocket>();
|
||||
|
||||
Services.GetRequiredService<IClipboardService>().BeginWatching();
|
||||
Services.GetRequiredService<IKeyboardMouseInput>().Init();
|
||||
_conductor.ViewerRemoved += ViewerRemoved;
|
||||
_conductor.ViewerAdded += ViewerAdded;
|
||||
_conductor.ScreenCastRequested += ScreenCastRequested;
|
||||
}
|
||||
|
||||
public static MainWindowViewModel Current { get; private set; }
|
||||
|
||||
public static IServiceProvider Services => ServiceContainer.Instance;
|
||||
|
||||
public ICommand ChangeServerCommand
|
||||
{
|
||||
get
|
||||
{
|
||||
return new Executor(async (param) =>
|
||||
{
|
||||
PromptForHostName();
|
||||
await Init();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand ElevateToAdminCommand
|
||||
{
|
||||
get
|
||||
{
|
||||
return new Executor((param) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
//var filePath = Process.GetCurrentProcess().MainModule.FileName;
|
||||
var commandLine = Win32Interop.GetCommandLine().Replace(" -elevate", "");
|
||||
var sections = commandLine.Split('"', StringSplitOptions.RemoveEmptyEntries);
|
||||
var filePath = sections.First();
|
||||
var arguments = string.Join('"', sections.Skip(1));
|
||||
var psi = new ProcessStartInfo(filePath, arguments)
|
||||
{
|
||||
Verb = "RunAs",
|
||||
UseShellExecute = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden
|
||||
};
|
||||
Process.Start(psi);
|
||||
Environment.Exit(0);
|
||||
}
|
||||
// Exception can be thrown if UAC is dialog is cancelled.
|
||||
catch { }
|
||||
}, (param) =>
|
||||
{
|
||||
return !IsAdministrator;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand ElevateToServiceCommand
|
||||
{
|
||||
get
|
||||
{
|
||||
return new Executor((param) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo("cmd.exe")
|
||||
{
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
//var filePath = Process.GetCurrentProcess().MainModule.FileName;
|
||||
var commandLine = Win32Interop.GetCommandLine().Replace(" -elevate", "");
|
||||
var sections = commandLine.Split('"', StringSplitOptions.RemoveEmptyEntries);
|
||||
var filePath = sections.First();
|
||||
var arguments = string.Join('"', sections.Skip(1));
|
||||
Logger.Write($"Creating temporary service with file path {filePath} and arguments {arguments}.");
|
||||
psi.Arguments = $"/c sc create Remotely_Temp binPath=\"{filePath} {arguments} -elevate\"";
|
||||
Process.Start(psi).WaitForExit();
|
||||
psi.Arguments = "/c sc start Remotely_Temp";
|
||||
Process.Start(psi).WaitForExit();
|
||||
psi.Arguments = "/c sc delete Remotely_Temp";
|
||||
Process.Start(psi).WaitForExit();
|
||||
App.Current.Shutdown();
|
||||
}
|
||||
catch { }
|
||||
}, (param) =>
|
||||
{
|
||||
return IsAdministrator && !WindowsIdentity.GetCurrent().IsSystem;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public string Host
|
||||
{
|
||||
get => _host;
|
||||
set
|
||||
{
|
||||
_host = value;
|
||||
FirePropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsAdministrator => new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
|
||||
|
||||
public ICommand RemoveViewersCommand
|
||||
{
|
||||
get
|
||||
{
|
||||
return new Executor(async (param) =>
|
||||
{
|
||||
foreach (Viewer viewer in (param as IList<object>).ToArray())
|
||||
{
|
||||
ViewerRemoved(this, viewer.ViewerConnectionID);
|
||||
await _casterSocket.DisconnectViewer(viewer, true);
|
||||
}
|
||||
},
|
||||
(param) =>
|
||||
{
|
||||
return (param as IList<object>)?.Count > 0;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
public string StatusMessage
|
||||
{
|
||||
get => _statusMessage;
|
||||
set
|
||||
{
|
||||
_statusMessage = value;
|
||||
FirePropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<Viewer> Viewers { get; } = new ObservableCollection<Viewer>();
|
||||
|
||||
public void CopyLink()
|
||||
{
|
||||
Clipboard.SetText($"{Host}/RemoteControl?sessionID={StatusMessage?.Replace(" ", "")}");
|
||||
}
|
||||
|
||||
public async Task GetSessionID()
|
||||
{
|
||||
await _casterSocket.SendDeviceInfo(_conductor.ServiceID, Environment.MachineName, _conductor.DeviceID);
|
||||
var sessionId = await _casterSocket.GetSessionID();
|
||||
|
||||
var formattedSessionID = "";
|
||||
for (var i = 0; i < sessionId.Length; i += 3)
|
||||
{
|
||||
formattedSessionID += sessionId.Substring(i, 3) + " ";
|
||||
}
|
||||
|
||||
App.Current?.Dispatcher?.Invoke(() =>
|
||||
{
|
||||
_sessionId = formattedSessionID.Trim();
|
||||
StatusMessage = _sessionId;
|
||||
});
|
||||
}
|
||||
|
||||
public async Task Init()
|
||||
{
|
||||
StatusMessage = "Retrieving...";
|
||||
|
||||
Host = _configService.GetConfig().Host;
|
||||
|
||||
while (string.IsNullOrWhiteSpace(Host))
|
||||
{
|
||||
Host = "https://";
|
||||
PromptForHostName();
|
||||
}
|
||||
|
||||
_conductor.ProcessArgs(new string[] { "-mode", "Normal", "-host", Host });
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _casterSocket.Connect(_conductor.Host);
|
||||
|
||||
if (result)
|
||||
{
|
||||
_casterSocket.Connection.Closed += (ex) =>
|
||||
{
|
||||
App.Current?.Dispatcher?.Invoke(() =>
|
||||
{
|
||||
Viewers.Clear();
|
||||
StatusMessage = "Disconnected";
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
_casterSocket.Connection.Reconnecting += (ex) =>
|
||||
{
|
||||
App.Current?.Dispatcher?.Invoke(() =>
|
||||
{
|
||||
Viewers.Clear();
|
||||
StatusMessage = "Reconnecting";
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
_casterSocket.Connection.Reconnected += (id) =>
|
||||
{
|
||||
StatusMessage = _sessionId;
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
await DeviceInitService.GetInitParams();
|
||||
ApplyBranding();
|
||||
|
||||
await GetSessionID();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
|
||||
// If we got here, something went wrong.
|
||||
StatusMessage = "Failed";
|
||||
MessageBox.Show(Application.Current.MainWindow, "Failed to connect to server.", "Connection Failed", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
|
||||
public void PromptForHostName()
|
||||
{
|
||||
var prompt = new HostNamePrompt();
|
||||
if (!string.IsNullOrWhiteSpace(Host))
|
||||
{
|
||||
prompt.ViewModel.Host = Host;
|
||||
}
|
||||
|
||||
prompt.Owner = App.Current?.MainWindow;
|
||||
prompt.ShowDialog();
|
||||
var result = prompt.ViewModel.Host?.Trim()?.TrimEnd('/');
|
||||
|
||||
if (!Uri.TryCreate(result, UriKind.Absolute, out var serverUri) ||
|
||||
(serverUri.Scheme != Uri.UriSchemeHttp && serverUri.Scheme != Uri.UriSchemeHttps))
|
||||
{
|
||||
Logger.Write("Server URL is not valid.");
|
||||
MessageBox.Show("Server URL must be a valid Uri (e.g. https://app.remotely.one).", "Invalid Server URL", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
Host = result;
|
||||
var config = _configService.GetConfig();
|
||||
config.Host = Host;
|
||||
_configService.Save(config);
|
||||
}
|
||||
|
||||
public void ShutdownApp()
|
||||
{
|
||||
Services.GetRequiredService<IShutdownService>().Shutdown();
|
||||
}
|
||||
|
||||
private void Application_Exit(object sender, ExitEventArgs e)
|
||||
{
|
||||
App.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
Viewers.Clear();
|
||||
});
|
||||
}
|
||||
|
||||
private async void CursorIconWatcher_OnChange(object sender, CursorInfo cursor)
|
||||
{
|
||||
if (_conductor?.Viewers?.Count > 0)
|
||||
{
|
||||
foreach (var viewer in _conductor.Viewers.Values)
|
||||
{
|
||||
await viewer.SendCursorChange(cursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void ScreenCastRequested(object sender, ScreenCastRequest screenCastRequest)
|
||||
{
|
||||
await App.Current.Dispatcher.InvokeAsync(async () =>
|
||||
{
|
||||
App.Current.MainWindow.Activate();
|
||||
var result = MessageBox.Show(Application.Current.MainWindow, $"You've received a connection request from {screenCastRequest.RequesterName}. Accept?", "Connection Request", MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||
if (result == MessageBoxResult.Yes)
|
||||
{
|
||||
Services.GetRequiredService<IScreenCaster>().BeginScreenCasting(screenCastRequest);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _casterSocket.SendConnectionRequestDenied(screenCastRequest.ViewerID);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void ViewerAdded(object sender, Viewer viewer)
|
||||
{
|
||||
App.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
Viewers.Add(viewer);
|
||||
});
|
||||
}
|
||||
|
||||
private void ViewerRemoved(object sender, string viewerID)
|
||||
{
|
||||
App.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
var viewer = Viewers.FirstOrDefault(x => x.ViewerConnectionID == viewerID);
|
||||
if (viewer != null)
|
||||
{
|
||||
Viewers.Remove(viewer);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,55 +0,0 @@
|
||||
using Remotely.Desktop.Core.ViewModels;
|
||||
using Remotely.Desktop.Win.Services;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Remotely.Desktop.Win.ViewModels
|
||||
{
|
||||
public class PromptForAccessWindowViewModel : BrandedViewModelBase
|
||||
{
|
||||
private string _organizationName = "your IT provider";
|
||||
private string _requesterName = "a technician";
|
||||
public string OrganizationName
|
||||
{
|
||||
get => _organizationName;
|
||||
set
|
||||
{
|
||||
_organizationName = value;
|
||||
FirePropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool PromptResult { get; set; }
|
||||
|
||||
public string RequesterName
|
||||
{
|
||||
get => _requesterName;
|
||||
set
|
||||
{
|
||||
_requesterName = value;
|
||||
FirePropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand SetResultNo => new Executor(param =>
|
||||
{
|
||||
if (param is Window promptWindow)
|
||||
{
|
||||
PromptResult = false;
|
||||
promptWindow.Close();
|
||||
}
|
||||
});
|
||||
|
||||
public ICommand SetResultYes => new Executor(param =>
|
||||
{
|
||||
if (param is Window promptWindow)
|
||||
{
|
||||
PromptResult = true;
|
||||
promptWindow.Close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,103 +0,0 @@
|
||||
<Window x:Class="Remotely.Desktop.Win.Views.ChatWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:Remotely.Desktop.Win.Views"
|
||||
xmlns:ViewModels="clr-namespace:Remotely.Desktop.Win.ViewModels"
|
||||
xmlns:Models="clr-namespace:Remotely.Shared.Models;assembly=Remotely_Shared"
|
||||
mc:Ignorable="d"
|
||||
WindowStyle="None"
|
||||
ResizeMode="CanResizeWithGrip"
|
||||
MouseLeftButtonDown="Window_MouseLeftButtonDown"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
AllowsTransparency="True"
|
||||
BorderBrush="DimGray"
|
||||
BorderThickness="1"
|
||||
MinHeight="250"
|
||||
MinWidth="200"
|
||||
Topmost="True"
|
||||
Title="Chat Session" Height="450" Width="450"
|
||||
Icon="{Binding Icon}"
|
||||
Loaded="Window_Loaded"
|
||||
ContentRendered="Window_ContentRendered">
|
||||
|
||||
<Window.DataContext>
|
||||
<ViewModels:ChatWindowViewModel />
|
||||
</Window.DataContext>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Height="50" Background="{Binding TitleBackgroundColor}">
|
||||
<Grid Margin="10,0,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image Grid.Column="0" Height="50" Width="50" Margin="0,0,10,0" Source="{Binding Icon}"></Image>
|
||||
<TextBlock Grid.Column="1" Foreground="{Binding TitleForegroundColor}" FontWeight="Bold" FontSize="20" VerticalAlignment="Center">
|
||||
<Run Text="{Binding ProductName}"></Run>
|
||||
<Run Text="Chat"></Run>
|
||||
</TextBlock>
|
||||
<Button Grid.Column="2" Style="{StaticResource TitlebarButton}" Click="MinimizeButton_Click" Content="____" Foreground="{Binding TitleButtonForegroundColor}" Background="{Binding TitleBackgroundColor}"/>
|
||||
<Button Grid.Column="3" Style="{StaticResource TitlebarButton}" Click="CloseButton_Click" Content="X" Foreground="{Binding TitleButtonForegroundColor}" Background="{Binding TitleBackgroundColor}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<TextBlock Grid.Row="1" FontWeight="Bold" Foreground="DimGray" Margin="10,10,10,0" TextWrapping="Wrap">
|
||||
<Run>Chat session with</Run>
|
||||
<Run Text="{Binding OrganizationName}"></Run>
|
||||
</TextBlock>
|
||||
|
||||
<Grid Grid.Row="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="5" />
|
||||
<RowDefinition Height="50" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border BorderBrush="Gray" BorderThickness="1" Margin="5">
|
||||
<ScrollViewer x:Name="MessagesScrollViewer">
|
||||
<ItemsControl x:Name="MessagesItemsControl" ItemsSource="{Binding ChatMessages}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type Models:ChatMessage}">
|
||||
<TextBlock TextWrapping="Wrap" Margin="5" FontSize="14">
|
||||
<Run Text="{Binding SenderName}" FontWeight="Bold">
|
||||
<Run.Style>
|
||||
<Style TargetType="Run">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="Text" Value="You">
|
||||
<Setter Property="Foreground" Value="SteelBlue"></Setter>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Run.Style>
|
||||
</Run>
|
||||
<Run>: </Run>
|
||||
<Run Text="{Binding Message}"></Run>
|
||||
</TextBlock>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<GridSplitter Grid.Row="1" Height="5" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" />
|
||||
|
||||
<TextBox Grid.Row="2"
|
||||
FontSize="14"
|
||||
Margin="5"
|
||||
Text="{Binding InputText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
TextWrapping="Wrap"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
PreviewKeyUp="ChatInputBox_PreviewKeyUp" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
@ -1,59 +0,0 @@
|
||||
using Remotely.Desktop.Win.ViewModels;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Remotely.Desktop.Win.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for ChatWindow.xaml
|
||||
/// </summary>
|
||||
public partial class ChatWindow : Window
|
||||
{
|
||||
public ChatWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
|
||||
private ChatWindowViewModel ViewModel => DataContext as ChatWindowViewModel;
|
||||
|
||||
private async void ChatInputBox_PreviewKeyUp(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter)
|
||||
{
|
||||
await ViewModel.SendChatMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
private void ItemContainerGenerator_ItemsChanged(object sender, System.Windows.Controls.Primitives.ItemsChangedEventArgs e)
|
||||
{
|
||||
MessagesScrollViewer.ScrollToEnd();
|
||||
}
|
||||
|
||||
private void MinimizeButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
this.WindowState = WindowState.Minimized;
|
||||
}
|
||||
|
||||
private void Window_ContentRendered(object sender, EventArgs e)
|
||||
{
|
||||
Topmost = false;
|
||||
}
|
||||
|
||||
private void Window_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
MessagesItemsControl.ItemContainerGenerator.ItemsChanged += ItemContainerGenerator_ItemsChanged;
|
||||
}
|
||||
|
||||
private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
DragMove();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,68 +0,0 @@
|
||||
<Window x:Class="Remotely.Desktop.Win.Views.FileTransferWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:Remotely.Desktop.Win.Views"
|
||||
xmlns:vm="clr-namespace:Remotely.Desktop.Win.ViewModels"
|
||||
mc:Ignorable="d"
|
||||
Title="File Transfer"
|
||||
Height="300" Width="400"
|
||||
Topmost="True"
|
||||
ContentRendered="Window_ContentRendered"
|
||||
Icon="{Binding Icon}">
|
||||
|
||||
<Window.DataContext>
|
||||
<vm:FileTransferWindowViewModel />
|
||||
</Window.DataContext>
|
||||
|
||||
<Grid Margin="10">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock FontWeight="Bold" FontSize="16" Grid.Row="0">
|
||||
<Run>Upload files to</Run>
|
||||
<Run Text="{Binding ViewerName}"></Run>
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock Grid.Row="1" Margin="0,10,0,0">
|
||||
Current Uploads:
|
||||
</TextBlock>
|
||||
|
||||
<Border Grid.Row="2" Margin="0,5,0,0" BorderBrush="LightGray" BorderThickness="1">
|
||||
<ScrollViewer >
|
||||
<ItemsControl ItemsSource="{Binding FileUploads}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel HorizontalAlignment="Stretch" Margin="0,0,5,0">
|
||||
<TextBlock Text="{Binding DisplayName, StringFormat={}{0}:}" Margin="0,0,0,2"></TextBlock>
|
||||
<Grid Margin="0,0,0,10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ProgressBar Value="{Binding PercentProgress}" Maximum="1" LargeChange="0.01" Height="18"></ProgressBar>
|
||||
<Button Grid.Column="1"
|
||||
VerticalAlignment="Top"
|
||||
Command="{Binding DataContext.RemoveFileUpload, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:FileTransferWindow}}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="x"
|
||||
Style="{StaticResource NormalButton}"
|
||||
BorderThickness="0" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<Button Grid.Row="3" Margin="0,10,0,0" Height="60" Style="{StaticResource NormalButton}" Command="{Binding OpenFileUploadDialog}">
|
||||
Click here to upload files.
|
||||
</Button>
|
||||
</Grid>
|
||||
</Window>
|
||||
@ -1,25 +0,0 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Remotely.Desktop.Win.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for FileTransferWindow.xaml
|
||||
/// </summary>
|
||||
public partial class FileTransferWindow : Window
|
||||
{
|
||||
|
||||
public FileTransferWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
Left = Screen.PrimaryScreen.WorkingArea.Right - Width;
|
||||
Top = Screen.PrimaryScreen.WorkingArea.Bottom - Height;
|
||||
}
|
||||
|
||||
private void Window_ContentRendered(object sender, EventArgs e)
|
||||
{
|
||||
Topmost = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
<Window
|
||||
x:Class="Remotely.Desktop.Win.Views.HostNamePrompt"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:Remotely.Desktop.Win.Views"
|
||||
xmlns:ViewModels="clr-namespace:Remotely.Desktop.Win.ViewModels" x:Name="PromptWindow"
|
||||
mc:Ignorable="d"
|
||||
Title="Host Name" Height="150" Width="350" WindowStartupLocation="CenterOwner"
|
||||
Icon="{Binding Icon}">
|
||||
<Window.DataContext>
|
||||
<ViewModels:HostNamePromptViewModel/>
|
||||
</Window.DataContext>
|
||||
<StackPanel Margin="10">
|
||||
<TextBlock Margin="0,0,0,5" Style="{StaticResource SectionHeader}"><Run Text="Enter the server URL:"/></TextBlock>
|
||||
<TextBox x:Name="HostTextBox" Height="25" Text="{Binding Host, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
|
||||
<Button Style="{StaticResource NormalButton}" HorizontalAlignment="Right" Width="40" Height="30" Margin="0,5,0,0" IsDefault="True" Click="OKButton_Click" Content="OK"/>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
@ -1,23 +0,0 @@
|
||||
using Remotely.Desktop.Win.ViewModels;
|
||||
using System.Windows;
|
||||
|
||||
namespace Remotely.Desktop.Win.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for HostNamePrompt.xaml
|
||||
/// </summary>
|
||||
public partial class HostNamePrompt : Window
|
||||
{
|
||||
public HostNamePrompt()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public HostNamePromptViewModel ViewModel => DataContext as HostNamePromptViewModel;
|
||||
|
||||
private void OKButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
this.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,116 +0,0 @@
|
||||
<Window x:Class="Remotely.Desktop.Win.Views.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:Remotely.Desktop.Win.Views"
|
||||
xmlns:ViewModels="clr-namespace:Remotely.Desktop.Win.ViewModels"
|
||||
mc:Ignorable="d"
|
||||
Title="{Binding ProductName}" Height="250" Width="350"
|
||||
WindowStyle="None"
|
||||
ResizeMode="NoResize"
|
||||
MouseLeftButtonDown="Window_MouseLeftButtonDown"
|
||||
Closing="Window_Closing"
|
||||
Loaded="Window_Loaded"
|
||||
Icon="{Binding Icon}">
|
||||
<Window.Resources>
|
||||
<DrawingBrush x:Key="GearBrush">
|
||||
<DrawingBrush.Drawing>
|
||||
<DrawingGroup>
|
||||
<DrawingGroup.Children>
|
||||
<GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" />
|
||||
<GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M5.6385,-0.000199999999999534L5.3645,1.6368 4.0145,0.671800000000001 0.672499999999999,4.0128 1.6375,5.3648 -0.000500000000000611,5.6378 -0.000500000000000611,10.3628 1.6375,10.6358 0.6735,11.9858 4.0145,15.3268 5.3645,14.3628 5.6375,15.9998 10.3625,15.9998 10.6355,14.3618 11.9875,15.3278 15.3265,11.9858 14.3635,10.6358 16.0005,10.3628 16.0005,5.6378 14.3635,5.3648 15.3265,4.0138 11.9875,0.672800000000001 10.6355,1.6378 10.3625,-0.000199999999999534z" />
|
||||
<GeometryDrawing Brush="#FF424242" Geometry="F1M8.0005,6.4623C7.1525,6.4623 6.4625,7.1523 6.4625,8.0003 6.4625,8.8483 7.1525,9.5373 8.0005,9.5373 8.8485,9.5373 9.5385,8.8483 9.5385,8.0003 9.5385,7.1523 8.8485,6.4623 8.0005,6.4623 M8.0005,10.5633C6.5875,10.5633 5.4375,9.4133 5.4375,8.0003 5.4375,6.5873 6.5875,5.4373 8.0005,5.4373 9.4135,5.4373 10.5635,6.5873 10.5635,8.0003 10.5635,9.4133 9.4135,10.5633 8.0005,10.5633 M7.3535,13.9753L8.6475,13.9753 8.9555,12.1263 9.2565,12.0323C9.5005,11.9563,9.7375,11.8583,9.9635,11.7393L10.2425,11.5933 11.7675,12.6823 12.6825,11.7673 11.5915,10.2403 11.7385,9.9613C11.7675,9.9043 11.7975,9.8543 11.8265,9.8053 11.8655,9.7373 11.8995,9.6783 11.9225,9.6243 11.9445,9.5703 11.9615,9.5043 11.9815,9.4283 11.9965,9.3743 12.0115,9.3173 12.0305,9.2573L12.1235,8.9553 13.9755,8.6473 13.9755,7.3533 12.1265,7.0453 12.0325,6.7443C11.9565,6.5003,11.8585,6.2633,11.7395,6.0373L11.5935,5.7583 12.6825,4.2333 11.7675,3.3183 10.2405,4.4083 9.9615,4.2623C9.9045,4.2323 9.8545,4.2033 9.8055,4.1743 9.7375,4.1353 9.6785,4.1003 9.6245,4.0783L9.6245,4.0783C9.5705,4.0563 9.5045,4.0383 9.4285,4.0183 9.3745,4.0043 9.3175,3.9893 9.2575,3.9703L8.9565,3.8763 8.6475,2.0253 7.3535,2.0253 7.0455,3.8743 6.7445,3.9683C6.5005,4.0443,6.2635,4.1423,6.0375,4.2603L5.7585,4.4073 4.2335,3.3173 3.3185,4.2323 4.4085,5.7593 4.2625,6.0393C4.2325,6.0953 4.2035,6.1463 4.1745,6.1953 4.1355,6.2633 4.1015,6.3213 4.0785,6.3753 4.0565,6.4303 4.0385,6.4953 4.0185,6.5723 4.0045,6.6263 3.9895,6.6823 3.9705,6.7433L3.8775,7.0443 2.0255,7.3533 2.0255,8.6473 3.8745,8.9553 3.9685,9.2563C4.0445,9.5003,4.1425,9.7363,4.2615,9.9633L4.4075,10.2423 3.3175,11.7673 4.2325,12.6823 5.7605,11.5913 6.0395,11.7383C6.0955,11.7673 6.1465,11.7973 6.1955,11.8253 6.2635,11.8653 6.3215,11.8993 6.3755,11.9213 6.4295,11.9443 6.4955,11.9613 6.5715,11.9813 6.6265,11.9963 6.6825,12.0103 6.7435,12.0293L7.0445,12.1233 7.3535,13.9753z M9.5155,15.0003L6.4845,15.0003 6.1385,12.9243C6.0865,12.9083 6.0345,12.8903 5.9835,12.8693 5.9325,12.8473 5.8835,12.8243 5.8345,12.7983L4.1225,14.0213 1.9795,11.8783 3.2135,10.1503C3.1705,10.0563,3.1315,9.9613,3.0945,9.8643L1.0005,9.5153 1.0005,6.4843 3.0755,6.1383C3.0925,6.0863 3.1105,6.0343 3.1315,5.9833 3.1525,5.9323 3.1765,5.8823 3.2025,5.8343L1.9795,4.1213 4.1225,1.9783 5.8505,3.2123C5.9445,3.1703,6.0395,3.1313,6.1365,3.0943L6.4855,1.0003 9.5165,1.0003 9.8615,3.0753C9.9115,3.0913 9.9635,3.1093 10.0175,3.1313 10.0685,3.1523 10.1175,3.1763 10.1665,3.2013L11.8785,1.9793 14.0215,4.1223 12.7875,5.8493C12.8305,5.9433,12.8695,6.0393,12.9065,6.1353L15.0005,6.4853 15.0005,9.5153 12.9255,9.8613C12.9085,9.9143 12.8905,9.9663 12.8695,10.0163 12.8485,10.0673 12.8245,10.1173 12.7985,10.1663L14.0215,11.8783 11.8785,14.0213 10.1505,12.7873C10.0565,12.8293,9.9615,12.8693,9.8645,12.9063L9.5155,15.0003z" />
|
||||
</DrawingGroup.Children>
|
||||
</DrawingGroup>
|
||||
</DrawingBrush.Drawing>
|
||||
</DrawingBrush>
|
||||
</Window.Resources>
|
||||
|
||||
<Window.DataContext>
|
||||
<ViewModels:MainWindowViewModel />
|
||||
</Window.DataContext>
|
||||
|
||||
<Grid>
|
||||
<StackPanel>
|
||||
<Border Height="50" Background="{Binding TitleBackgroundColor}">
|
||||
<Grid Margin="10,0,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image Grid.Column="0" Height="45" Width="45" Margin="0,0,10,0" Source="{Binding Icon}"></Image>
|
||||
<TextBlock Grid.Column="1" Text="{Binding ProductName}" Foreground="{Binding TitleForegroundColor}" FontWeight="Bold" FontSize="20" VerticalAlignment="Center" />
|
||||
<Button Grid.Column="2" Style="{StaticResource TitlebarButton}" Click="MinimizeButton_Click" Content="____" Foreground="{Binding TitleButtonForegroundColor}" Background="{Binding TitleBackgroundColor}"/>
|
||||
<Button Grid.Column="3" Style="{StaticResource TitlebarButton}" Click="CloseButton_Click" Content="X" Foreground="{Binding TitleButtonForegroundColor}" Background="{Binding TitleBackgroundColor}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
<Grid Margin="10,15,10,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0" Margin="0,0,15,0">
|
||||
<TextBlock Style="{StaticResource SectionHeader}"><Run Text="Share Screen"/></TextBlock>
|
||||
<TextBlock FontSize="16" Margin="0,10,0,5"><Run Text="Your Session ID:"/></TextBlock>
|
||||
<TextBox FontSize="16" Text="{Binding StatusMessage}" IsReadOnly="True"/>
|
||||
<Grid Margin="0,10,0,0">
|
||||
<TextBlock VerticalAlignment="Center">
|
||||
Invite Link:
|
||||
</TextBlock>
|
||||
<Button HorizontalAlignment="Right" Style="{StaticResource NormalButton}" Click="CopyLinkButton_Click">
|
||||
Copy
|
||||
</Button>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Margin="15,0,0,0">
|
||||
<TextBlock Style="{StaticResource SectionHeader}"><Run Text="Viewers"/></TextBlock>
|
||||
<Grid Margin="0,5,0,0">
|
||||
<TextBlock FontSize="8" Margin="5,0,0,0">Name</TextBlock>
|
||||
<TextBlock FontSize="8" Margin="0,0,5,0" HorizontalAlignment="Right">Has Control</TextBlock>
|
||||
</Grid>
|
||||
<ListBox x:Name="ViewerListBox" Height="90" HorizontalContentAlignment="Stretch" ItemsSource="{Binding Viewers}" SelectionMode="Extended">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<DockPanel>
|
||||
<TextBlock Text="{Binding Name}"></TextBlock>
|
||||
<CheckBox IsChecked="{Binding HasControl}" HorizontalAlignment="Right"></CheckBox>
|
||||
</DockPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
<Button Style="{StaticResource NormalButton}"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{Binding RemoveViewersCommand}"
|
||||
CommandParameter="{Binding ElementName=ViewerListBox, Path=SelectedItems}"
|
||||
Margin="0,5,0,0"
|
||||
Content="Remove">
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<Button BorderThickness="0"
|
||||
Background="Transparent"
|
||||
VerticalAlignment="Bottom"
|
||||
HorizontalAlignment="Left"
|
||||
Cursor="Hand"
|
||||
Height="20"
|
||||
Width="20"
|
||||
VerticalContentAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Margin="10,5"
|
||||
Click="OptionsButton_Click">
|
||||
<Button.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="Change Server" Command="{Binding Path=ChangeServerCommand, Source={x:Static ViewModels:MainWindowViewModel.Current}}"></MenuItem>
|
||||
|
||||
<MenuItem Header="Elevate to Admin" Command="{Binding Path=ElevateToAdminCommand, Source={x:Static ViewModels:MainWindowViewModel.Current}}"></MenuItem>
|
||||
<MenuItem Header="Elevate to Service" Command="{Binding Path=ElevateToServiceCommand, Source={x:Static ViewModels:MainWindowViewModel.Current}}"></MenuItem>
|
||||
</ContextMenu>
|
||||
</Button.ContextMenu>
|
||||
<Rectangle Fill="{StaticResource GearBrush}"></Rectangle>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Window>
|
||||
@ -1,77 +0,0 @@
|
||||
using Remotely.Desktop.Win.ViewModels;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media.Animation;
|
||||
|
||||
namespace Remotely.Desktop.Win.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public MainWindowViewModel ViewModel => DataContext as MainWindowViewModel;
|
||||
private void CloseButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
private async void CopyLinkButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.CopyLink();
|
||||
var tooltip = new ToolTip
|
||||
{
|
||||
PlacementTarget = sender as Button,
|
||||
Placement = PlacementMode.Bottom,
|
||||
VerticalOffset = 5,
|
||||
Content = "Copied to clipboard!",
|
||||
HasDropShadow = true,
|
||||
StaysOpen = false,
|
||||
IsOpen = true
|
||||
};
|
||||
|
||||
await Task.Delay(750);
|
||||
var animation = new DoubleAnimation(0, TimeSpan.FromMilliseconds(750));
|
||||
tooltip.BeginAnimation(OpacityProperty, animation);
|
||||
}
|
||||
|
||||
private void MinimizeButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
WindowState = WindowState.Minimized;
|
||||
}
|
||||
|
||||
private void OptionsButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
(sender as Button).ContextMenu.IsOpen = true;
|
||||
}
|
||||
|
||||
private void Window_Closing(object sender, CancelEventArgs e)
|
||||
{
|
||||
ViewModel?.ShutdownApp();
|
||||
}
|
||||
|
||||
private async void Window_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!DesignerProperties.GetIsInDesignMode(this) &&
|
||||
ViewModel != null)
|
||||
{
|
||||
await ViewModel?.Init();
|
||||
}
|
||||
}
|
||||
|
||||
private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
DragMove();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,77 +0,0 @@
|
||||
<Window x:Class="Remotely.Desktop.Win.Views.PromptForAccessWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:Remotely.Desktop.Win.Views"
|
||||
mc:Ignorable="d"
|
||||
WindowStyle="None"
|
||||
xmlns:ViewModels="clr-namespace:Remotely.Desktop.Win.ViewModels"
|
||||
ResizeMode="CanResizeWithGrip"
|
||||
MouseLeftButtonDown="Window_MouseLeftButtonDown"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
AllowsTransparency="True"
|
||||
BorderBrush="DimGray"
|
||||
BorderThickness="1"
|
||||
Topmost="True"
|
||||
Title="Remote Control Request"
|
||||
MinHeight="200"
|
||||
MinWidth="250"
|
||||
Height="275"
|
||||
Width="450"
|
||||
Icon="{Binding Icon}"
|
||||
ContentRendered="Window_ContentRendered">
|
||||
|
||||
<Window.DataContext>
|
||||
<ViewModels:PromptForAccessWindowViewModel />
|
||||
</Window.DataContext>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Height="50" Background="{Binding TitleBackgroundColor}">
|
||||
<Grid Margin="10,0,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image Grid.Column="0" Height="50" Width="50" Margin="0,0,10,0" Source="{Binding Icon}"></Image>
|
||||
<TextBlock Grid.Column="1" Text="Remote Control Request" Foreground="{Binding TitleForegroundColor}" FontWeight="Bold" FontSize="20" VerticalAlignment="Center" />
|
||||
<Button Grid.Column="2" Style="{StaticResource TitlebarButton}" Click="MinimizeButton_Click" Content="____" Foreground="{Binding TitleButtonForegroundColor}" Background="{Binding TitleBackgroundColor}"/>
|
||||
<Button Grid.Column="3" Style="{StaticResource TitlebarButton}" Click="CloseButton_Click" Content="X" Foreground="{Binding TitleButtonForegroundColor}" Background="{Binding TitleBackgroundColor}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Row="1">
|
||||
<TextBlock Style="{StaticResource SectionHeader}" FontWeight="Bold" FontSize="18" Foreground="DimGray" Margin="10" TextWrapping="Wrap">
|
||||
A remote control session has been requested.
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock Style="{StaticResource SectionHeader}" FontWeight="Bold" FontSize="18" Foreground="DimGray" Margin="10" TextWrapping="Wrap">
|
||||
<Run>Would you like to allow </Run>
|
||||
<Run Text="{Binding RequesterName}"></Run>
|
||||
<Run> from </Run>
|
||||
<Run Text="{Binding OrganizationName}"></Run>
|
||||
<Run> to control your computer?</Run>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Top">
|
||||
<Button Width="60" Height="40" Content="Yes" Margin="10"
|
||||
Style="{StaticResource NormalButton}"
|
||||
Command="{Binding SetResultYes}"
|
||||
CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:PromptForAccessWindow}}}"></Button>
|
||||
<Button Width="60" Height="40" Content="No" Margin="10"
|
||||
Style="{StaticResource NormalButton}"
|
||||
Command="{Binding SetResultNo}"
|
||||
CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:PromptForAccessWindow}}}"></Button>
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
</Window>
|
||||
@ -1,45 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace Remotely.Desktop.Win.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for PromptForAccessWindow.xaml
|
||||
/// </summary>
|
||||
public partial class PromptForAccessWindow : Window
|
||||
{
|
||||
public PromptForAccessWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void CloseButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
private void MinimizeButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
this.WindowState = WindowState.Minimized;
|
||||
}
|
||||
|
||||
private void Window_ContentRendered(object sender, EventArgs e)
|
||||
{
|
||||
Topmost = false;
|
||||
}
|
||||
|
||||
private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
DragMove();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:Remotely.Desktop.XPlat;assembly=Remotely_Desktop"
|
||||
x:Class="Remotely.Desktop.XPlat.App">
|
||||
<Application.DataTemplates>
|
||||
<local:ViewLocator/>
|
||||
</Application.DataTemplates>
|
||||
|
||||
<Application.Styles>
|
||||
<StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/>
|
||||
<StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/>
|
||||
|
||||
<Style Selector="Button.TitlebarButton">
|
||||
<Setter Property="Background" Value="#FF464646"></Setter>
|
||||
<Setter Property="DockPanel.Dock" Value="Right"></Setter>
|
||||
<Setter Property="HorizontalAlignment" Value="Right"></Setter>
|
||||
<Setter Property="BorderBrush" Value="{x:Null}"></Setter>
|
||||
<Setter Property="Foreground" Value="White"></Setter>
|
||||
<Setter Property="FontWeight" Value="Bold"></Setter>
|
||||
<Setter Property="Height" Value="30"></Setter>
|
||||
<Setter Property="Width" Value="30"></Setter>
|
||||
<Setter Property="Cursor" Value="Hand"></Setter>
|
||||
<Setter Property="Margin" Value="5,0,5,0"></Setter>
|
||||
</Style>
|
||||
<Style Selector="TextBlock.SectionHeader">
|
||||
<Setter Property="FontWeight" Value="Bold"></Setter>
|
||||
<Setter Property="FontSize" Value="18"></Setter>
|
||||
</Style>
|
||||
<Style Selector="Button.NormalButton">
|
||||
<Setter Property="Background" Value="White"></Setter>
|
||||
<Setter Property="BorderThickness" Value="1"></Setter>
|
||||
<Setter Property="BorderBrush" Value="Black"></Setter>
|
||||
<Setter Property="Padding" Value="6,4"></Setter>
|
||||
</Style>
|
||||
<Style Selector="Button.HyperlinkButton">
|
||||
<Setter Property="Foreground" Value="DodgerBlue"></Setter>
|
||||
<Setter Property="BorderThickness" Value="0"></Setter>
|
||||
<Setter Property="BorderBrush" Value="Transparent"></Setter>
|
||||
<Setter Property="Cursor" Value="Hand"></Setter>
|
||||
<Setter Property="Background" Value="Transparent"></Setter>
|
||||
<Setter Property="Padding" Value="0"></Setter>
|
||||
</Style>
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
@ -1,130 +0,0 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Threading;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Remotely.Desktop.Core;
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using Remotely.Desktop.Core.Services;
|
||||
using Remotely.Desktop.XPlat.Services;
|
||||
using Remotely.Desktop.XPlat.Views;
|
||||
using Remotely.Shared.Utilities;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Remotely.Desktop.XPlat
|
||||
{
|
||||
public class App : Application
|
||||
{
|
||||
private static IServiceProvider Services => ServiceContainer.Instance;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
//if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
//{
|
||||
// desktop.MainWindow = new MainWindow
|
||||
// {
|
||||
// DataContext = new MainWindowViewModel(),
|
||||
// };
|
||||
//}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
|
||||
_ = Task.Run(Startup);
|
||||
}
|
||||
|
||||
private void BuildServices()
|
||||
{
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection.AddLogging(builder =>
|
||||
{
|
||||
builder.AddConsole().AddDebug();
|
||||
});
|
||||
|
||||
serviceCollection.AddSingleton<IScreenCaster, ScreenCaster>();
|
||||
serviceCollection.AddSingleton<ICasterSocket, CasterSocket>();
|
||||
serviceCollection.AddSingleton<IdleTimer>();
|
||||
serviceCollection.AddSingleton<Conductor>();
|
||||
serviceCollection.AddSingleton<IChatClientService, ChatHostService>();
|
||||
serviceCollection.AddTransient<Viewer>();
|
||||
serviceCollection.AddScoped<IWebRtcSessionFactory, WebRtcSessionFactory>();
|
||||
serviceCollection.AddScoped<IDtoMessageHandler, DtoMessageHandler>();
|
||||
serviceCollection.AddScoped<IDeviceInitService, DeviceInitService>();
|
||||
|
||||
switch (EnvironmentHelper.Platform)
|
||||
{
|
||||
case Shared.Enums.Platform.Linux:
|
||||
{
|
||||
serviceCollection.AddSingleton<IKeyboardMouseInput, KeyboardMouseInputLinux>();
|
||||
serviceCollection.AddSingleton<IClipboardService, ClipboardServiceLinux>();
|
||||
serviceCollection.AddSingleton<IAudioCapturer, AudioCapturerLinux>();
|
||||
serviceCollection.AddSingleton<IChatUiService, ChatUiServiceLinux>();
|
||||
serviceCollection.AddTransient<IScreenCapturer, ScreenCapturerLinux>();
|
||||
serviceCollection.AddScoped<IFileTransferService, FileTransferServiceLinux>();
|
||||
serviceCollection.AddSingleton<ICursorIconWatcher, CursorIconWatcherLinux>();
|
||||
serviceCollection.AddSingleton<ISessionIndicator, SessionIndicatorLinux>();
|
||||
serviceCollection.AddSingleton<IShutdownService, ShutdownServiceLinux>();
|
||||
serviceCollection.AddScoped<IRemoteControlAccessService, RemoteControlAccessServiceLinux>();
|
||||
serviceCollection.AddScoped<IConfigService, ConfigServiceLinux>();
|
||||
}
|
||||
break;
|
||||
case Shared.Enums.Platform.MacOS:
|
||||
{
|
||||
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new PlatformNotSupportedException();
|
||||
}
|
||||
|
||||
ServiceContainer.Instance = serviceCollection.BuildServiceProvider();
|
||||
}
|
||||
|
||||
|
||||
private async Task Startup()
|
||||
{
|
||||
|
||||
BuildServices();
|
||||
|
||||
var conductor = Services.GetRequiredService<Conductor>();
|
||||
|
||||
var args = Environment.GetCommandLineArgs().SkipWhile(x => !x.StartsWith("-"));
|
||||
Logger.Write("Processing Args: " + string.Join(", ", args));
|
||||
conductor.ProcessArgs(args.ToArray());
|
||||
|
||||
await Services.GetRequiredService<IDeviceInitService>().GetInitParams();
|
||||
|
||||
if (conductor.Mode == Core.Enums.AppMode.Chat)
|
||||
{
|
||||
await Services.GetRequiredService<IChatClientService>().StartChat(conductor.RequesterID, conductor.OrganizationName);
|
||||
}
|
||||
else if (conductor.Mode == Core.Enums.AppMode.Unattended)
|
||||
{
|
||||
var casterSocket = Services.GetRequiredService<ICasterSocket>();
|
||||
await casterSocket.Connect(conductor.Host).ConfigureAwait(false);
|
||||
await casterSocket.SendDeviceInfo(conductor.ServiceID, Environment.MachineName, conductor.DeviceID).ConfigureAwait(false);
|
||||
await casterSocket.NotifyRequesterUnattendedReady(conductor.RequesterID).ConfigureAwait(false);
|
||||
Services.GetRequiredService<IdleTimer>().Start();
|
||||
Services.GetRequiredService<IClipboardService>().BeginWatching();
|
||||
Services.GetRequiredService<IKeyboardMouseInput>().Init();
|
||||
}
|
||||
else
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(() => {
|
||||
this.RunWithMainWindow<MainWindow>();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 477 B |
@ -1,40 +0,0 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
xmlns:vm="clr-namespace:Remotely.Desktop.XPlat.ViewModels;assembly=Remotely_Desktop"
|
||||
x:Class="Remotely.Desktop.XPlat.Controls.MessageBox"
|
||||
Icon="{Binding WindowIcon}"
|
||||
Title="{Binding Caption}"
|
||||
SizeToContent="WidthAndHeight" MinWidth="300" MinHeight="100"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
<Window.DataContext>
|
||||
<vm:MessageBoxViewModel></vm:MessageBoxViewModel>
|
||||
</Window.DataContext>
|
||||
<StackPanel Margin="10, 20">
|
||||
<TextBlock Text="{Binding Message}"></TextBlock>
|
||||
|
||||
<Grid Margin="0,20,0,0">
|
||||
<Button Classes="NormalButton" HorizontalAlignment="Right" IsVisible="{Binding IsOkButtonVisible}"
|
||||
Command="{Binding OKCommand}"
|
||||
CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}"
|
||||
Content="OK">
|
||||
</Button>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Classes="NormalButton" HorizontalAlignment="Right" IsVisible="{Binding AreYesNoButtonsVisible}" Margin="5,0,5,0"
|
||||
Command="{Binding YesCommand}"
|
||||
CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}"
|
||||
Content="Yes">
|
||||
</Button>
|
||||
<Button Classes="NormalButton" HorizontalAlignment="Right" IsVisible="{Binding AreYesNoButtonsVisible}" Margin="5,0,5,0"
|
||||
Command="{Binding NoCommand}"
|
||||
CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}"
|
||||
Content="No">
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
|
||||
</StackPanel>
|
||||
</Window>
|
||||
@ -1,80 +0,0 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Remotely.Desktop.XPlat.ViewModels;
|
||||
using Remotely.Shared.Utilities;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Remotely.Desktop.XPlat.Controls
|
||||
{
|
||||
public class MessageBox : Window
|
||||
{
|
||||
public static async Task<MessageBoxResult> Show(string message, string caption, MessageBoxType type)
|
||||
{
|
||||
var messageBox = new MessageBox();
|
||||
var viewModel = messageBox.DataContext as MessageBoxViewModel;
|
||||
viewModel.Caption = caption;
|
||||
viewModel.Message = message;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case MessageBoxType.OK:
|
||||
viewModel.IsOkButtonVisible = true;
|
||||
break;
|
||||
case MessageBoxType.YesNo:
|
||||
viewModel.AreYesNoButtonsVisible = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (App.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop &&
|
||||
desktop.Windows.Count > 0)
|
||||
{
|
||||
await messageBox.ShowDialog(desktop.Windows.First());
|
||||
}
|
||||
else
|
||||
{
|
||||
var isClosed = false;
|
||||
messageBox.Closed += (sender, args) =>
|
||||
{
|
||||
isClosed = true;
|
||||
};
|
||||
messageBox.Show();
|
||||
await TaskHelper.DelayUntilAsync(() => isClosed, TimeSpan.MaxValue);
|
||||
}
|
||||
return viewModel.Result;
|
||||
|
||||
}
|
||||
public MessageBox()
|
||||
{
|
||||
// This doesn't appear to work when set in XAML.
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
|
||||
public enum MessageBoxType
|
||||
{
|
||||
OK,
|
||||
YesNo
|
||||
}
|
||||
|
||||
public enum MessageBoxResult
|
||||
{
|
||||
Cancel,
|
||||
OK,
|
||||
Yes,
|
||||
No
|
||||
}
|
||||
}
|
||||
@ -1,142 +0,0 @@
|
||||
/*
|
||||
|
||||
Copyright 1985, 1986, 1987, 1991, 1998 The Open Group
|
||||
|
||||
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.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
Except as contained in this notice, the name of The Open Group shall not be
|
||||
used in advertising or otherwise to promote the sale, use or other dealings
|
||||
in this Software without prior written authorization from The Open Group.
|
||||
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Remotely.Desktop.XPlat.Native.Linux
|
||||
{
|
||||
public static unsafe class LibX11
|
||||
{
|
||||
|
||||
[DllImport("libX11")]
|
||||
public static extern IntPtr XGetImage(IntPtr display, IntPtr drawable, int x, int y, int width, int height, long plane_mask, int format);
|
||||
|
||||
[DllImport("libX11")]
|
||||
public static extern IntPtr XDefaultVisual(IntPtr display, int screen_number);
|
||||
[DllImport("libX11")]
|
||||
public static extern int XScreenCount(IntPtr display);
|
||||
[DllImport("libX11")]
|
||||
public static extern int XDefaultScreen(IntPtr display);
|
||||
[DllImport("libX11")]
|
||||
public static extern IntPtr XOpenDisplay(string display_name);
|
||||
[DllImport("libX11")]
|
||||
public static extern void XCloseDisplay(IntPtr display);
|
||||
[DllImport("libX11")]
|
||||
public static extern IntPtr XRootWindow(IntPtr display, int screen_number);
|
||||
|
||||
[DllImport("libX11")]
|
||||
public static extern IntPtr XGetSubImage(IntPtr display, IntPtr drawable, int x, int y, uint width, uint height, ulong plane_mask, int format, IntPtr dest_image, int dest_x, int dest_y);
|
||||
[DllImport("libX11")]
|
||||
public static extern IntPtr XScreenOfDisplay(IntPtr display, int screen_number);
|
||||
[DllImport("libX11")]
|
||||
public static extern int XDisplayWidth(IntPtr display, int screen_number);
|
||||
[DllImport("libX11")]
|
||||
public static extern int XDisplayHeight(IntPtr display, int screen_number);
|
||||
[DllImport("libX11")]
|
||||
public static extern int XWidthOfScreen(IntPtr screen);
|
||||
[DllImport("libX11")]
|
||||
public static extern int XHeightOfScreen(IntPtr screen);
|
||||
[DllImport("libX11")]
|
||||
public static extern IntPtr XDefaultGC(IntPtr display, int screen_number);
|
||||
[DllImport("libX11")]
|
||||
public static extern IntPtr XDefaultRootWindow(IntPtr display);
|
||||
[DllImport("libX11")]
|
||||
public static extern void XGetInputFocus(IntPtr display, out IntPtr focus_return, out int revert_to_return);
|
||||
[DllImport("libX11")]
|
||||
public static extern IntPtr XStringToKeysym(string key);
|
||||
[DllImport("libX11")]
|
||||
public static extern uint XKeysymToKeycode(IntPtr display, IntPtr keysym);
|
||||
|
||||
[DllImport("libX11")]
|
||||
public static extern IntPtr XRootWindowOfScreen(IntPtr screen);
|
||||
[DllImport("libX11")]
|
||||
public static extern ulong XNextRequest(IntPtr display);
|
||||
[DllImport("libX11")]
|
||||
public static extern void XForceScreenSaver(IntPtr display, int mode);
|
||||
[DllImport("libX11")]
|
||||
public static extern void XSync(IntPtr display, bool discard);
|
||||
[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
|
||||
{
|
||||
public int width;
|
||||
public int height; /* size of image */
|
||||
public int xoffset; /* number of pixels offset in X direction */
|
||||
public int format; /* XYBitmap, XYPixmap, ZPixmap */
|
||||
//public char* data; /* pointer to image data */
|
||||
public IntPtr data; /* pointer to image data */
|
||||
public int byte_order; /* data byte order, LSBFirst, MSBFirst */
|
||||
public int bitmap_unit; /* quant. of scanline 8, 16, 32 */
|
||||
public int bitmap_bit_order; /* LSBFirst, MSBFirst */
|
||||
public int bitmap_pad; /* 8, 16, 32 either XY or ZPixmap */
|
||||
public int depth; /* depth of image */
|
||||
public int bytes_per_line; /* accelerator to next scanline */
|
||||
public int bits_per_pixel; /* bits per pixel (ZPixmap) */
|
||||
public ulong red_mask; /* bits in z arrangement */
|
||||
public ulong green_mask;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Remotely.Desktop.XPlat.Native.Linux
|
||||
{
|
||||
public class LibXtst
|
||||
{
|
||||
[DllImport("libXtst")]
|
||||
public static extern bool XTestQueryExtension(IntPtr display, out int event_base, out int error_base, out int major_version, out int minor_version);
|
||||
[DllImport("libXtst")]
|
||||
public static extern void XTestFakeKeyEvent(IntPtr display, uint keycode, bool is_press, ulong delay);
|
||||
[DllImport("libXtst")]
|
||||
public static extern void XTestFakeButtonEvent(IntPtr display, uint button, bool is_press, ulong delay);
|
||||
[DllImport("libXtst")]
|
||||
public static extern void XTestFakeMotionEvent(IntPtr display, int screen_number, int x, int y, ulong delay);
|
||||
}
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Remotely.Desktop.XPlat.Native.Linux
|
||||
{
|
||||
public class Libc
|
||||
{
|
||||
[DllImport("libc", SetLastError = true)]
|
||||
public static extern uint geteuid();
|
||||
}
|
||||
}
|
||||
@ -1,68 +0,0 @@
|
||||
/*
|
||||
* 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.XPlat.Native.Linux
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.ReactiveUI;
|
||||
using Remotely.Desktop.Core;
|
||||
using Remotely.Shared.Utilities;
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Remotely.Desktop.XPlat
|
||||
{
|
||||
class Program
|
||||
{
|
||||
|
||||
// Avalonia configuration, don't remove; also used by visual designer.
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.LogToTrace()
|
||||
.UseReactiveUI();
|
||||
|
||||
public static Conductor Conductor { get; private set; }
|
||||
public static IServiceProvider Services => ServiceContainer.Instance;
|
||||
|
||||
public static CancellationTokenSource AppCts { get; private set; }
|
||||
public static CancellationToken AppCancellationToken { get; private set; }
|
||||
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
|
||||
|
||||
BuildAvaloniaApp().Start(AppMain, args);
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppMain(Application app, string[] args)
|
||||
{
|
||||
AppCts = new CancellationTokenSource();
|
||||
AppCancellationToken = AppCts.Token;
|
||||
app.Run(AppCancellationToken);
|
||||
}
|
||||
|
||||
|
||||
private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
Logger.Write((Exception)e.ExceptionObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Desktop.Linux": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "-orgid \"e42c65b4-deb2-4dca-b4fb-ec8dcbe8f84c\" -host \"https://localhost:5001\""
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using System;
|
||||
|
||||
namespace Remotely.Desktop.XPlat.Services
|
||||
{
|
||||
public class AudioCapturerLinux : IAudioCapturer
|
||||
{
|
||||
#pragma warning disable
|
||||
public event EventHandler<byte[]> AudioSampleReady;
|
||||
#pragma warning restore
|
||||
|
||||
public void ToggleAudio(bool toggleOn)
|
||||
{
|
||||
// Not implemented.
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using Remotely.Desktop.XPlat.Controls;
|
||||
using Remotely.Desktop.XPlat.ViewModels;
|
||||
using Remotely.Desktop.XPlat.Views;
|
||||
using Remotely.Shared.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.IO.Pipes;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Remotely.Desktop.XPlat.Services
|
||||
{
|
||||
public class ChatUiServiceLinux : IChatUiService
|
||||
{
|
||||
private ChatWindowViewModel ChatViewModel { get; set; }
|
||||
|
||||
public event EventHandler ChatWindowClosed;
|
||||
|
||||
public void ReceiveChat(ChatMessage chatMessage)
|
||||
{
|
||||
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
if (chatMessage.Disconnected)
|
||||
{
|
||||
await MessageBox.Show("The partner has disconnected.", "Partner Disconnected", MessageBoxType.OK);
|
||||
Environment.Exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ChatViewModel != null)
|
||||
{
|
||||
ChatViewModel.SenderName = chatMessage.SenderName;
|
||||
ChatViewModel.ChatMessages.Add(chatMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void ShowChatWindow(string organizationName, StreamWriter writer)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var chatWindow = new ChatWindow();
|
||||
chatWindow.Closing += ChatWindow_Closing;
|
||||
ChatViewModel = chatWindow.DataContext as ChatWindowViewModel;
|
||||
ChatViewModel.PipeStreamWriter = writer;
|
||||
ChatViewModel.OrganizationName = organizationName;
|
||||
App.Current.Run(chatWindow);
|
||||
});
|
||||
}
|
||||
|
||||
private void ChatWindow_Closing(object sender, CancelEventArgs e)
|
||||
{
|
||||
ChatWindowClosed?.Invoke(this, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using Remotely.Shared.Utilities;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Remotely.Desktop.XPlat.Services
|
||||
{
|
||||
public class ClipboardServiceLinux : IClipboardService
|
||||
{
|
||||
private CancellationTokenSource cancelTokenSource;
|
||||
|
||||
public event EventHandler<string> ClipboardTextChanged;
|
||||
|
||||
private string ClipboardText { get; set; }
|
||||
|
||||
public void BeginWatching()
|
||||
{
|
||||
try
|
||||
{
|
||||
StopWatching();
|
||||
}
|
||||
finally
|
||||
{
|
||||
cancelTokenSource = new CancellationTokenSource();
|
||||
_ = Task.Run(async () => await WatchClipboard(cancelTokenSource.Token));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetText(string clipboardText)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(clipboardText))
|
||||
{
|
||||
await App.Current.Clipboard.ClearAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await App.Current.Clipboard.SetTextAsync(clipboardText);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void StopWatching()
|
||||
{
|
||||
cancelTokenSource?.Cancel();
|
||||
cancelTokenSource?.Dispose();
|
||||
}
|
||||
|
||||
private async Task WatchClipboard(CancellationToken cancelToken)
|
||||
{
|
||||
while (!cancelToken.IsCancellationRequested &&
|
||||
!Environment.HasShutdownStarted)
|
||||
{
|
||||
try
|
||||
{
|
||||
var currentText = await App.Current.Clipboard.GetTextAsync();
|
||||
if (!string.IsNullOrEmpty(currentText) && currentText != ClipboardText)
|
||||
{
|
||||
ClipboardText = currentText;
|
||||
ClipboardTextChanged?.Invoke(this, ClipboardText);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Thread.Sleep(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using Remotely.Shared.Models;
|
||||
using Remotely.Shared.Utilities;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Remotely.Desktop.XPlat.Services
|
||||
{
|
||||
public class ConfigServiceLinux : IConfigService
|
||||
{
|
||||
private static string ConfigFile => Path.Combine(ConfigFolder, "Config.json");
|
||||
private static string ConfigFolder => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "remotely.json");
|
||||
|
||||
public DesktopAppConfig GetConfig()
|
||||
{
|
||||
var config = new DesktopAppConfig();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.Host) &&
|
||||
File.Exists(ConfigFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
config = JsonSerializer.Deserialize<DesktopAppConfig>(File.ReadAllText(ConfigFile));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
public void Save(DesktopAppConfig config)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(ConfigFolder);
|
||||
File.WriteAllText(ConfigFile, JsonSerializer.Serialize(config));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using Remotely.Shared.Models;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
namespace Remotely.Desktop.XPlat.Services
|
||||
{
|
||||
public class CursorIconWatcherLinux : ICursorIconWatcher
|
||||
{
|
||||
#pragma warning disable
|
||||
public event EventHandler<CursorInfo> OnChange;
|
||||
#pragma warning restore
|
||||
|
||||
public CursorInfo GetCurrentCursor() => new(null, Point.Empty, "default");
|
||||
}
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
using System;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace Remotely.Desktop.XPlat.Services
|
||||
{
|
||||
public class Executor : ICommand
|
||||
{
|
||||
public Executor(Action<object> executeAction, Predicate<object> isExecutable = null)
|
||||
{
|
||||
ExecuteAction = executeAction;
|
||||
IsExecutable = isExecutable;
|
||||
}
|
||||
|
||||
#pragma warning disable
|
||||
public event EventHandler CanExecuteChanged;
|
||||
#pragma warning restore
|
||||
|
||||
private Action<object> ExecuteAction { get; set; }
|
||||
|
||||
private Predicate<object> IsExecutable { get; set; }
|
||||
public bool CanExecute(object parameter)
|
||||
{
|
||||
if (IsExecutable == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return IsExecutable.Invoke(parameter);
|
||||
}
|
||||
|
||||
public void Execute(object parameter)
|
||||
{
|
||||
ExecuteAction.Invoke(parameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,147 +0,0 @@
|
||||
using Avalonia.Threading;
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using Remotely.Desktop.Core.Services;
|
||||
using Remotely.Desktop.Core.ViewModels;
|
||||
using Remotely.Desktop.XPlat.Controls;
|
||||
using Remotely.Desktop.XPlat.ViewModels;
|
||||
using Remotely.Desktop.XPlat.Views;
|
||||
using Remotely.Shared.Utilities;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Remotely.Desktop.XPlat.Services
|
||||
{
|
||||
public class FileTransferServiceLinux : IFileTransferService
|
||||
{
|
||||
private static readonly SemaphoreSlim _writeLock = new(1,1);
|
||||
private static readonly ConcurrentDictionary<string, FileStream> _partialTransfers =
|
||||
new();
|
||||
private static readonly ConcurrentDictionary<string, FileTransferWindow> _fileTransferWindows =
|
||||
new();
|
||||
private static volatile bool _messageBoxPending;
|
||||
|
||||
public string GetBaseDirectory()
|
||||
{
|
||||
var desktopDir = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
|
||||
if (Directory.Exists(desktopDir))
|
||||
{
|
||||
return desktopDir;
|
||||
}
|
||||
|
||||
return Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "Remotely_Shared")).FullName;
|
||||
}
|
||||
|
||||
public void OpenFileTransferWindow(Viewer viewer)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
if (_fileTransferWindows.TryGetValue(viewer.ViewerConnectionID, out var window))
|
||||
{
|
||||
window.Activate();
|
||||
}
|
||||
else
|
||||
{
|
||||
window = new FileTransferWindow
|
||||
{
|
||||
DataContext = new FileTransferWindowViewModel(viewer, this)
|
||||
};
|
||||
window.Closed += (sender, arg) =>
|
||||
{
|
||||
_fileTransferWindows.Remove(viewer.ViewerConnectionID, out _);
|
||||
};
|
||||
_fileTransferWindows.AddOrUpdate(viewer.ViewerConnectionID, window, (k, v) => window);
|
||||
window.Show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task ReceiveFile(byte[] buffer, string fileName, string messageId, bool endOfFile, bool startOfFile)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _writeLock.WaitAsync();
|
||||
|
||||
var baseDir = GetBaseDirectory();
|
||||
|
||||
if (startOfFile)
|
||||
{
|
||||
var filePath = Path.Combine(baseDir, fileName);
|
||||
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
var count = 0;
|
||||
var ext = Path.GetExtension(fileName);
|
||||
var fileWithoutExt = Path.GetFileNameWithoutExtension(fileName);
|
||||
while (File.Exists(filePath))
|
||||
{
|
||||
filePath = Path.Combine(baseDir, $"{fileWithoutExt}-{count}{ext}");
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
File.Create(filePath).Close();
|
||||
|
||||
var fs = new FileStream(filePath, FileMode.OpenOrCreate);
|
||||
_partialTransfers.AddOrUpdate(messageId, fs, (k, v) => fs);
|
||||
}
|
||||
|
||||
var fileStream = _partialTransfers[messageId];
|
||||
|
||||
if (buffer?.Length > 0)
|
||||
{
|
||||
await fileStream.WriteAsync(buffer, 0, buffer.Length);
|
||||
|
||||
}
|
||||
|
||||
if (endOfFile)
|
||||
{
|
||||
fileStream.Close();
|
||||
_partialTransfers.Remove(messageId, out _);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_writeLock.Release();
|
||||
if (endOfFile)
|
||||
{
|
||||
await Task.Run(ShowTransferComplete);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UploadFile(FileUpload fileUpload, Viewer viewer, CancellationToken cancelToken, Action<double> progressUpdateCallback)
|
||||
{
|
||||
try
|
||||
{
|
||||
await viewer.SendFile(fileUpload, cancelToken, progressUpdateCallback);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowTransferComplete()
|
||||
{
|
||||
// Prevent multiple dialogs from popping up.
|
||||
if (!_messageBoxPending)
|
||||
{
|
||||
_messageBoxPending = true;
|
||||
|
||||
await MessageBox.Show($"File tranfer complete. Files saved to directory:\n\n{GetBaseDirectory()}",
|
||||
"Tranfer Complete",
|
||||
MessageBoxType.OK);
|
||||
|
||||
_messageBoxPending = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,255 +0,0 @@
|
||||
using Remotely.Desktop.Core.Enums;
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using Remotely.Desktop.Core.Services;
|
||||
using Remotely.Desktop.XPlat.Native.Linux;
|
||||
using Remotely.Shared.Utilities;
|
||||
using System;
|
||||
|
||||
namespace Remotely.Desktop.XPlat.Services
|
||||
{
|
||||
public class KeyboardMouseInputLinux : IKeyboardMouseInput
|
||||
{
|
||||
private IntPtr Display { get; set; }
|
||||
|
||||
public void Init()
|
||||
{
|
||||
// Nothing to do here. The Windows implementation needs to start
|
||||
// a processing queue to keep all input simulation on the same
|
||||
// thread. Linux doesn't.
|
||||
}
|
||||
|
||||
public void SendKeyDown(string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitDisplay();
|
||||
key = ConvertJavaScriptKeyToX11Key(key);
|
||||
var keySim = LibX11.XStringToKeysym(key);
|
||||
if (keySim == IntPtr.Zero)
|
||||
{
|
||||
Logger.Write($"Key not mapped: {key}");
|
||||
return;
|
||||
}
|
||||
|
||||
var keyCode = LibX11.XKeysymToKeycode(Display, keySim);
|
||||
LibXtst.XTestFakeKeyEvent(Display, keyCode, true, 0);
|
||||
LibX11.XSync(Display, false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void SendKeyUp(string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitDisplay();
|
||||
key = ConvertJavaScriptKeyToX11Key(key);
|
||||
var keySim = LibX11.XStringToKeysym(key);
|
||||
if (keySim == IntPtr.Zero)
|
||||
{
|
||||
Logger.Write($"Key not mapped: {key}");
|
||||
return;
|
||||
}
|
||||
|
||||
var keyCode = LibX11.XKeysymToKeycode(Display, keySim);
|
||||
LibXtst.XTestFakeKeyEvent(Display, keyCode, false, 0);
|
||||
LibX11.XSync(Display, false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public void SendMouseButtonAction(int button, ButtonAction buttonAction, double percentX, double percentY, Viewer viewer)
|
||||
{
|
||||
try
|
||||
{
|
||||
var isPressed = buttonAction == ButtonAction.Down;
|
||||
// Browser buttons start at 0. XTest starts at 1.
|
||||
var mouseButton = (uint)(button + 1);
|
||||
|
||||
InitDisplay();
|
||||
SendMouseMove(percentX, percentY, viewer);
|
||||
LibXtst.XTestFakeButtonEvent(Display, mouseButton, isPressed, 0);
|
||||
LibX11.XSync(Display, false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void SendMouseMove(double percentX, double percentY, Viewer viewer)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitDisplay();
|
||||
|
||||
var screenBounds = viewer.Capturer.CurrentScreenBounds;
|
||||
LibXtst.XTestFakeMotionEvent(Display,
|
||||
LibX11.XDefaultScreen(Display),
|
||||
screenBounds.X + (int)(screenBounds.Width * percentX),
|
||||
screenBounds.Y + (int)(screenBounds.Height * percentY),
|
||||
0);
|
||||
LibX11.XSync(Display, false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void SendMouseWheel(int deltaY)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitDisplay();
|
||||
if (deltaY > 0)
|
||||
{
|
||||
LibXtst.XTestFakeButtonEvent(Display, 4, true, 0);
|
||||
LibXtst.XTestFakeButtonEvent(Display, 4, false, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
LibXtst.XTestFakeButtonEvent(Display, 5, true, 0);
|
||||
LibXtst.XTestFakeButtonEvent(Display, 5, false, 0);
|
||||
}
|
||||
LibX11.XSync(Display, false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void SendRightMouseDown(double percentX, double percentY, Viewer viewer)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitDisplay();
|
||||
SendMouseMove(percentX, percentY, viewer);
|
||||
LibXtst.XTestFakeButtonEvent(Display, 3, true, 0);
|
||||
LibX11.XSync(Display, false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void SendRightMouseUp(double percentX, double percentY, Viewer viewer)
|
||||
{
|
||||
try
|
||||
{
|
||||
InitDisplay();
|
||||
SendMouseMove(percentX, percentY, viewer);
|
||||
LibXtst.XTestFakeButtonEvent(Display, 3, false, 0);
|
||||
LibX11.XSync(Display, false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
}
|
||||
public void SendText(string transferText)
|
||||
{
|
||||
foreach (var key in transferText)
|
||||
{
|
||||
SendKeyDown(key.ToString());
|
||||
SendKeyUp(key.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public void SetKeyStatesUp()
|
||||
{
|
||||
// Not implemented.
|
||||
}
|
||||
|
||||
public void ToggleBlockInput(bool toggleOn)
|
||||
{
|
||||
// Not implemented.
|
||||
}
|
||||
|
||||
private string ConvertJavaScriptKeyToX11Key(string key)
|
||||
{
|
||||
string keySym = key switch
|
||||
{
|
||||
"ArrowDown" => "Down",
|
||||
"ArrowUp" => "Up",
|
||||
"ArrowLeft" => "Left",
|
||||
"ArrowRight" => "Right",
|
||||
"Enter" => "Return",
|
||||
"Esc" => "Escape",
|
||||
"Alt" => "Alt_L",
|
||||
"Control" => "Control_L",
|
||||
"Shift" => "Shift_L",
|
||||
"PAUSE" => "Pause",
|
||||
"BREAK" => "Break",
|
||||
"Backspace" => "BackSpace",
|
||||
"Tab" => "Tab",
|
||||
"CapsLock" => "Caps_Lock",
|
||||
"Delete" => "Delete",
|
||||
"PageUp" => "Page_Up",
|
||||
"PageDown" => "Page_Down",
|
||||
"NumLock" => "Num_Lock",
|
||||
"ScrollLock" => "Scroll_Lock",
|
||||
"ContextMenu" => "Menu",
|
||||
" " => "space",
|
||||
"!" => "exclam",
|
||||
"\"" => "quotedbl",
|
||||
"#" => "numbersign",
|
||||
"$" => "dollar",
|
||||
"%" => "percent",
|
||||
"&" => "ampersand",
|
||||
"'" => "apostrophe",
|
||||
"(" => "parenleft",
|
||||
")" => "parenright",
|
||||
"*" => "asterisk",
|
||||
"+" => "plus",
|
||||
"," => "comma",
|
||||
"-" => "minus",
|
||||
"." => "period",
|
||||
"/" => "slash",
|
||||
":" => "colon",
|
||||
";" => "semicolon",
|
||||
"<" => "less",
|
||||
"=" => "equal",
|
||||
">" => "greater",
|
||||
"?" => "question",
|
||||
"@" => "at",
|
||||
"[" => "bracketleft",
|
||||
"\\" => "backslash",
|
||||
"]" => "bracketright",
|
||||
"_" => "underscore",
|
||||
"`" => "grave",
|
||||
"{" => "braceleft",
|
||||
"|" => "bar",
|
||||
"}" => "braceright",
|
||||
"~" => "asciitilde",
|
||||
_ => key,
|
||||
};
|
||||
return keySym;
|
||||
}
|
||||
private void InitDisplay()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Display == IntPtr.Zero)
|
||||
{
|
||||
Display = LibX11.XOpenDisplay(null);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,46 +0,0 @@
|
||||
using Avalonia.Threading;
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using Remotely.Desktop.XPlat.ViewModels;
|
||||
using Remotely.Desktop.XPlat.Views;
|
||||
using Remotely.Shared.Utilities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Remotely.Desktop.XPlat.Services
|
||||
{
|
||||
public class RemoteControlAccessServiceLinux : IRemoteControlAccessService
|
||||
{
|
||||
public async Task<bool> PromptForAccess(string requesterName, string organizationName)
|
||||
{
|
||||
return await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
var promptWindow = new PromptForAccessWindow();
|
||||
var viewModel = promptWindow.DataContext as PromptForAccessWindowViewModel;
|
||||
if (!string.IsNullOrWhiteSpace(requesterName))
|
||||
{
|
||||
viewModel.RequesterName = requesterName;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(organizationName))
|
||||
{
|
||||
viewModel.OrganizationName = organizationName;
|
||||
}
|
||||
|
||||
var isOpen = true;
|
||||
promptWindow.Closed += (sender, arg) =>
|
||||
{
|
||||
isOpen = false;
|
||||
};
|
||||
promptWindow.Show();
|
||||
while (isOpen)
|
||||
{
|
||||
await Task.Delay(100);
|
||||
}
|
||||
|
||||
return viewModel.PromptResult;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,231 +0,0 @@
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using Remotely.Desktop.Core.Utilities;
|
||||
using Remotely.Desktop.XPlat.Native.Linux;
|
||||
using Remotely.Shared;
|
||||
using Remotely.Shared.Utilities;
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Remotely.Desktop.XPlat.Services
|
||||
{
|
||||
public class ScreenCapturerLinux : IScreenCapturer
|
||||
{
|
||||
private readonly object _screenBoundsLock = new();
|
||||
private readonly Dictionary<string, LibXrandr.XRRMonitorInfo> _x11Screens = new();
|
||||
private SKBitmap _currentFrame;
|
||||
private SKBitmap _previousFrame;
|
||||
|
||||
public ScreenCapturerLinux()
|
||||
{
|
||||
Display = LibX11.XOpenDisplay(null);
|
||||
Init();
|
||||
}
|
||||
|
||||
public event EventHandler<Rectangle> ScreenChanged;
|
||||
|
||||
public bool CaptureFullscreen { get; set; } = true;
|
||||
public Rectangle CurrentScreenBounds { get; private set; }
|
||||
public IntPtr Display { get; private set; }
|
||||
public string SelectedScreen { get; private set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
LibX11.XCloseDisplay(Display);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
public IEnumerable<string> GetDisplayNames()
|
||||
{
|
||||
return _x11Screens.Keys.Select(x => x.ToString());
|
||||
}
|
||||
|
||||
public SKRect GetFrameDiffArea()
|
||||
{
|
||||
return ImageUtils.GetDiffArea(_currentFrame, _previousFrame, CaptureFullscreen);
|
||||
}
|
||||
|
||||
public Result<SKBitmap> GetImageDiff()
|
||||
{
|
||||
return ImageUtils.GetImageDiff(_currentFrame, _previousFrame);
|
||||
}
|
||||
|
||||
public Result<SKBitmap> GetNextFrame()
|
||||
{
|
||||
lock (_screenBoundsLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_currentFrame != null)
|
||||
{
|
||||
_previousFrame?.Dispose();
|
||||
try
|
||||
{
|
||||
_previousFrame = _currentFrame;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
}
|
||||
|
||||
_currentFrame = GetX11Capture();
|
||||
return Result.Ok(_currentFrame);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
Init();
|
||||
return Result.Fail<SKBitmap>(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
public int GetScreenCount()
|
||||
{
|
||||
return _x11Screens.Count;
|
||||
}
|
||||
|
||||
public int GetSelectedScreenIndex()
|
||||
{
|
||||
return int.Parse(SelectedScreen ?? "0");
|
||||
}
|
||||
|
||||
public Rectangle GetVirtualScreenBounds()
|
||||
{
|
||||
int width = 0;
|
||||
for (var i = 0; i < GetScreenCount(); i++)
|
||||
{
|
||||
width += LibX11.XWidthOfScreen(LibX11.XScreenOfDisplay(Display, i));
|
||||
}
|
||||
int height = 0;
|
||||
for (var i = 0; i < GetScreenCount(); i++)
|
||||
{
|
||||
height += LibX11.XHeightOfScreen(LibX11.XScreenOfDisplay(Display, i));
|
||||
}
|
||||
return new Rectangle(0, 0, width, height);
|
||||
}
|
||||
|
||||
public void Init()
|
||||
{
|
||||
try
|
||||
{
|
||||
CaptureFullscreen = true;
|
||||
_x11Screens.Clear();
|
||||
|
||||
var monitorsPtr = LibXrandr.XRRGetMonitors(Display, LibX11.XDefaultRootWindow(Display), true, out var monitorCount);
|
||||
|
||||
var monitorInfoSize = Marshal.SizeOf<LibXrandr.XRRMonitorInfo>();
|
||||
|
||||
for (var i = 0; i < monitorCount; i++)
|
||||
{
|
||||
var monitorPtr = new IntPtr(monitorsPtr.ToInt64() + i * monitorInfoSize);
|
||||
var monitorInfo = Marshal.PtrToStructure<LibXrandr.XRRMonitorInfo>(monitorPtr);
|
||||
|
||||
Logger.Write($"Found monitor: " +
|
||||
$"{monitorInfo.width}," +
|
||||
$"{monitorInfo.height}," +
|
||||
$"{monitorInfo.x}, " +
|
||||
$"{monitorInfo.y}");
|
||||
|
||||
_x11Screens.Add(i.ToString(), monitorInfo);
|
||||
}
|
||||
|
||||
LibXrandr.XRRFreeMonitors(monitorsPtr);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(SelectedScreen) ||
|
||||
!_x11Screens.ContainsKey(SelectedScreen))
|
||||
{
|
||||
SelectedScreen = _x11Screens.Keys.First();
|
||||
RefreshCurrentScreenBounds();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
}
|
||||
public void SetSelectedScreen(string displayName)
|
||||
{
|
||||
lock (_screenBoundsLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.Write($"Setting display to {displayName}.");
|
||||
if (displayName == SelectedScreen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_x11Screens.ContainsKey(displayName))
|
||||
{
|
||||
SelectedScreen = displayName;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedScreen = _x11Screens.Keys.First();
|
||||
}
|
||||
|
||||
RefreshCurrentScreenBounds();
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Write(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SKBitmap GetX11Capture()
|
||||
{
|
||||
var currentFrame = new SKBitmap(CurrentScreenBounds.Width, CurrentScreenBounds.Height);
|
||||
|
||||
var window = LibX11.XDefaultRootWindow(Display);
|
||||
|
||||
var imagePointer = LibX11.XGetImage(Display,
|
||||
window,
|
||||
CurrentScreenBounds.X,
|
||||
CurrentScreenBounds.Y,
|
||||
CurrentScreenBounds.Width,
|
||||
CurrentScreenBounds.Height,
|
||||
~0,
|
||||
2);
|
||||
|
||||
var image = Marshal.PtrToStructure<LibX11.XImage>(imagePointer);
|
||||
|
||||
var pixels = currentFrame.GetPixels();
|
||||
unsafe
|
||||
{
|
||||
byte* scan1 = (byte*)pixels.ToPointer();
|
||||
byte* scan2 = (byte*)image.data.ToPointer();
|
||||
var bytesPerPixel = currentFrame.BytesPerPixel;
|
||||
var totalSize = currentFrame.Height * currentFrame.Width * bytesPerPixel;
|
||||
for (int counter = 0; counter < totalSize - bytesPerPixel; counter++)
|
||||
{
|
||||
scan1[counter] = scan2[counter];
|
||||
}
|
||||
}
|
||||
|
||||
Marshal.DestroyStructure<LibX11.XImage>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using Remotely.Desktop.XPlat.Views;
|
||||
|
||||
namespace Remotely.Desktop.XPlat.Services
|
||||
{
|
||||
public class SessionIndicatorLinux : ISessionIndicator
|
||||
{
|
||||
public void Show()
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var indicatorWindow = new SessionIndicatorWindow();
|
||||
indicatorWindow.Show();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Remotely.Desktop.Core;
|
||||
using Remotely.Desktop.Core.Interfaces;
|
||||
using Remotely.Desktop.Core.Services;
|
||||
using Remotely.Shared.Utilities;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Remotely.Desktop.XPlat.Services
|
||||
{
|
||||
public class ShutdownServiceLinux : IShutdownService
|
||||
{
|
||||
public async Task Shutdown()
|
||||
{
|
||||
Logger.Debug($"Exiting process ID {Environment.ProcessId}.");
|
||||
var casterSocket = ServiceContainer.Instance.GetRequiredService<ICasterSocket>();
|
||||
await casterSocket.DisconnectAllViewers();
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Remotely.Desktop.XPlat.ViewModels;
|
||||
using System;
|
||||
|
||||
namespace Remotely.Desktop.XPlat
|
||||
{
|
||||
public class ViewLocator : IDataTemplate
|
||||
{
|
||||
public bool SupportsRecycling => false;
|
||||
|
||||
public IControl Build(object data)
|
||||
{
|
||||
var name = data.GetType().FullName.Replace("ViewModel", "View");
|
||||
var type = Type.GetType(name);
|
||||
|
||||
if (type != null)
|
||||
{
|
||||
return (Control)Activator.CreateInstance(type);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new TextBlock { Text = "Not Found: " + name };
|
||||
}
|
||||
}
|
||||
|
||||
public bool Match(object data)
|
||||
{
|
||||
return data is ReactiveViewModel;
|
||||
}
|
||||
}
|
||||
}
|
||||