Skip to content

Commit

Permalink
Implement wake-on-lan.
Browse files Browse the repository at this point in the history
  • Loading branch information
bitbound committed Jun 22, 2023
1 parent e6cc771 commit 1794059
Show file tree
Hide file tree
Showing 14 changed files with 351 additions and 33 deletions.
8 changes: 8 additions & 0 deletions Agent/Services/AgentHubConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public class AgentHubConnection : IAgentHubConnection, IDisposable

private readonly IDeviceInformationService _deviceInfoService;
private readonly IHttpClientFactory _httpFactory;
private readonly IWakeOnLanService _wakeOnLanService;
private readonly ILogger<AgentHubConnection> _logger;
private readonly ILogger _fileLogger;
private readonly ScriptExecutor _scriptExecutor;
Expand All @@ -57,6 +58,7 @@ public AgentHubConnection(ConfigService configService,
IUpdater updater,
IDeviceInformationService deviceInfoService,
IHttpClientFactory httpFactory,
IWakeOnLanService wakeOnLanService,
IEnumerable<ILoggerProvider> loggerProviders,
ILogger<AgentHubConnection> logger)
{
Expand All @@ -68,6 +70,7 @@ public AgentHubConnection(ConfigService configService,
_updater = updater;
_deviceInfoService = deviceInfoService;
_httpFactory = httpFactory;
_wakeOnLanService = wakeOnLanService;
_logger = logger;
_fileLogger = loggerProviders
.OfType<FileLoggerProvider>()
Expand Down Expand Up @@ -489,6 +492,11 @@ private void RegisterMessageHandlers()
});

_hubConnection.On("TriggerHeartbeat", SendHeartbeat);

_hubConnection.On("WakeDevice", async (string macAddress) =>
{
await _wakeOnLanService.WakeDevice(macAddress);
});
}

private async Task<bool> VerifyServer()
Expand Down
44 changes: 44 additions & 0 deletions Agent/Services/DeviceInfoGeneratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
using Remotely.Shared.Utilities;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Runtime.InteropServices;

namespace Remotely.Agent.Services
Expand All @@ -31,6 +34,7 @@ protected Device GetDeviceBase(string deviceID, string orgID)
OSDescription = RuntimeInformation.OSDescription,
Is64Bit = Environment.Is64BitOperatingSystem,
IsOnline = true,
MacAddresses = GetMacAddresses().ToArray(),
OrganizationID = orgID,
AgentVersion = AppVersionHelper.GetAppVersion()
};
Expand Down Expand Up @@ -95,5 +99,45 @@ protected List<Drive> GetAllDrives()
return null;
}
}

private IEnumerable<string> GetMacAddresses()
{
var macAddress = new List<string>();

try
{
var nics = NetworkInterface.GetAllNetworkInterfaces();

if (!nics.Any())
{
return macAddress;
}

var onlineNics = nics
.Where(c =>
c.NetworkInterfaceType != NetworkInterfaceType.Loopback &&
c.OperationalStatus == OperationalStatus.Up);

foreach (var adapter in onlineNics)
{
var ipProperties = adapter.GetIPProperties();

var unicastAddresses = ipProperties.UnicastAddresses;
if (!unicastAddresses.Any(temp => temp.Address.AddressFamily == AddressFamily.InterNetwork))
{
continue;
}

var address = adapter.GetPhysicalAddress();
macAddress.Add(address.ToString());
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while getting MAC addresses.");
}

return macAddress;
}
}
}
39 changes: 39 additions & 0 deletions Agent/Services/WakeOnLanService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace Remotely.Agent.Services
{
public interface IWakeOnLanService
{
Task WakeDevice(string macAddress);
}

public class WakeOnLanService : IWakeOnLanService
{
public async Task WakeDevice(string macAddress)
{
var macBytes = Convert.FromHexString(macAddress);

using var client = new UdpClient();

var macData = Enumerable
.Repeat(macBytes, 16)
.SelectMany(x => x);

var packet = Enumerable
.Repeat((byte)0xFF, 6)
.Concat(macData)
.ToArray();

var broadcastAddress = System.Net.IPAddress.Parse("255.255.255.255");
// WOL usually uses port 9.
var endpoint = new IPEndPoint(broadcastAddress, 9);
await client.SendAsync(packet, packet.Length, endpoint);
}
}
}
8 changes: 0 additions & 8 deletions Server/API/AgentUpdateController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,6 @@ private async Task<bool> CheckForDeviceBan(string deviceIp)
var bannedDevices = _serviceSessionCache.GetAllDevices().Where(x => x.PublicIP == deviceIp);
var connectionIds = _serviceSessionCache.GetConnectionIdsByDeviceIds(bannedDevices.Select(x => x.ID));

// TODO: Remove when devices have been removed.
var command = "sc delete Remotely_Service & taskkill /im Remotely_Agent.exe /f";
await _agentHubContext.Clients.Clients(connectionIds).SendAsync("ExecuteCommand",
"cmd",
command,
Guid.NewGuid().ToString(),
Guid.NewGuid().ToString());

await _agentHubContext.Clients.Clients(connectionIds).SendAsync("UninstallAgent");

return true;
Expand Down
6 changes: 6 additions & 0 deletions Server/Components/Devices/DeviceCard.razor
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@
<span class="ml-2">View Only</span>
</button>
</li>
<li>
<button class="dropdown-item" @onclick="() => WakeDevice()">
<i class="oi oi-eye" title="Wake Device"></i>
<span class="ml-2">Wake</span>
</button>
</li>
<li>
<FileInputButton ClassNames="dropdown-item btn btn-primary"
OnChanged="OnFileInputChanged">
Expand Down
28 changes: 20 additions & 8 deletions Server/Components/Devices/DeviceCard.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@ public partial class DeviceCard : AuthComponentBase, IDisposable
{
private readonly ConcurrentDictionary<string, double> _fileUploadProgressLookup = new();
private ElementReference _card;
private Theme _theme;
private Version _currentVersion = new();

private Theme _theme;
[Parameter]
public Device Device { get; set; }

Expand All @@ -42,12 +41,6 @@ public partial class DeviceCard : AuthComponentBase, IDisposable
[Inject]
private ICircuitConnection CircuitConnection { get; set; }

[Inject]
private IServiceHubSessionCache ServiceSessionCache { get; init; }

[Inject]
private IUpgradeService UpgradeService { get; init; }

[Inject]
private IDataService DataService { get; set; }

Expand All @@ -64,9 +57,15 @@ public partial class DeviceCard : AuthComponentBase, IDisposable

[Inject]
private IModalService ModalService { get; set; }

[Inject]
private IServiceHubSessionCache ServiceSessionCache { get; init; }

[Inject]
private IToastService ToastService { get; set; }

[Inject]
private IUpgradeService UpgradeService { get; init; }
public void Dispose()
{
AppState.PropertyChanged -= AppState_PropertyChanged;
Expand Down Expand Up @@ -323,5 +322,18 @@ private async Task UninstallAgent()
ParentFrame.Refresh();
}
}

private async Task WakeDevice()
{
var result = await CircuitConnection.WakeDevice(Device);
if (result.IsSuccess)
{
ToastService.ShowToast2("Wake command sent.", ToastType.Success);
}
else
{
ToastService.ShowToast2($"Wake command failed. Reason: {result.Reason}", ToastType.Error);
}
}
}
}
3 changes: 3 additions & 0 deletions Server/Components/Devices/DevicesFrame.razor
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
<option @key="group.ID" value="@group.ID">@group.Name</option>
}
</select>
<button class="btn btn-sm btn-secondary" title="Wake Devices" style="width: 40px; margin-left: 5px;" @onclick="WakeDevices">
<span class="oi oi-eye"></span>
</button>
</div>

<div>
Expand Down
32 changes: 32 additions & 0 deletions Server/Components/Devices/DevicesFrame.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,21 @@ private void FilterDevices()

}

private IEnumerable<Device> GetDevicesInSelectedGroup()
{
if (_selectedGroupId == _deviceGroupNone)
{
return _allDevices.Where(x => x.DeviceGroupID is null);
}

if (_selectedGroupId == _deviceGroupAll)
{
return _allDevices;
}

return _allDevices.Where(x => x.DeviceGroupID == _selectedGroupId);
}

private string GetDisplayName(PropertyInfo propInfo)
{
return propInfo.GetCustomAttribute<DisplayAttribute>()?.Name ?? propInfo.Name;
Expand Down Expand Up @@ -335,5 +350,22 @@ private void ToggleSortDirection()
_sortDirection = ListSortDirection.Ascending;
}
}

private async Task WakeDevices()
{
var devices = GetDevicesInSelectedGroup();
var result = await CircuitConnection.WakeDevices(devices.ToArray());

if (result.IsSuccess)
{
ToastService.ShowToast2("Wake commands sent.", ToastType.Success);
}
else
{
ToastService.ShowToast2(
$"Failed to send wake commands. Reason: {result.Reason}",
ToastType.Error);
}
}
}
}
55 changes: 47 additions & 8 deletions Server/Data/AppDb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder options)

protected override void OnModelCreating(ModelBuilder builder)
{
var jsonOptions = JsonSerializerOptions.Default;

base.OnModelCreating(builder);

Expand Down Expand Up @@ -102,8 +103,8 @@ protected override void OnModelCreating(ModelBuilder builder)
builder.Entity<RemotelyUser>()
.Property(x => x.UserOptions)
.HasConversion(
x => JsonSerializer.Serialize(x, (JsonSerializerOptions)null),
x => JsonSerializer.Deserialize<RemotelyUserOptions>(x, (JsonSerializerOptions)null));
x => JsonSerializer.Serialize(x, jsonOptions),
x => JsonSerializer.Deserialize<RemotelyUserOptions>(x, jsonOptions));
builder.Entity<RemotelyUser>()
.HasMany(x => x.SavedScripts)
.WithOne(x => x.Creator);
Expand All @@ -117,8 +118,8 @@ protected override void OnModelCreating(ModelBuilder builder)
builder.Entity<Device>()
.Property(x => x.Drives)
.HasConversion(
x => JsonSerializer.Serialize(x, (JsonSerializerOptions)null),
x => JsonSerializer.Deserialize<List<Drive>>(x, (JsonSerializerOptions)null));
x => JsonSerializer.Serialize(x, jsonOptions),
x => TryDeserializeProperty<List<Drive>>(x, jsonOptions));
builder.Entity<Device>()
.Property(x => x.Drives)
.Metadata.SetValueComparer(new ValueComparer<List<Drive>>(true));
Expand All @@ -136,6 +137,11 @@ protected override void OnModelCreating(ModelBuilder builder)
builder.Entity<Device>()
.HasMany(x => x.ScriptSchedules)
.WithMany(x => x.Devices);
builder.Entity<Device>()
.Property(x => x.MacAddresses)
.HasConversion(
x => JsonSerializer.Serialize(x, jsonOptions),
x => DeserializeStringArray(x, jsonOptions));

builder.Entity<DeviceGroup>()
.HasMany(x => x.Devices);
Expand All @@ -153,16 +159,16 @@ protected override void OnModelCreating(ModelBuilder builder)
builder.Entity<ScriptResult>()
.Property(x => x.ErrorOutput)
.HasConversion(
x => JsonSerializer.Serialize(x, (JsonSerializerOptions)null),
x => JsonSerializer.Deserialize<string[]>(x, (JsonSerializerOptions)null))
x => JsonSerializer.Serialize(x, jsonOptions),
x => DeserializeStringArray(x, jsonOptions))
.Metadata
.SetValueComparer(_stringArrayComparer);

builder.Entity<ScriptResult>()
.Property(x => x.StandardOutput)
.HasConversion(
x => JsonSerializer.Serialize(x, (JsonSerializerOptions)null),
x => JsonSerializer.Deserialize<string[]>(x, (JsonSerializerOptions)null))
x => JsonSerializer.Serialize(x, jsonOptions),
x => DeserializeStringArray(x, jsonOptions))
.Metadata
.SetValueComparer(_stringArrayComparer);

Expand Down Expand Up @@ -198,5 +204,38 @@ protected override void OnModelCreating(ModelBuilder builder)
}

}

private static string[] DeserializeStringArray(string value, JsonSerializerOptions jsonOptions)
{
try
{
if (string.IsNullOrEmpty(value))
{
return Array.Empty<string>();
}
return JsonSerializer.Deserialize<string[]>(value, jsonOptions) ?? Array.Empty<string>();
}
catch
{
return Array.Empty<string>();
}
}

private static T TryDeserializeProperty<T>(string value, JsonSerializerOptions jsonOptions)
where T: new()
{
try
{
if (string.IsNullOrEmpty(value))
{
return new();
}
return JsonSerializer.Deserialize<T>(value, jsonOptions) ?? new();
}
catch
{
return new();
}
}
}
}
12 changes: 12 additions & 0 deletions Server/Enums/ToastType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Remotely.Server.Enums
{
public enum ToastType
{
Primary,
Secondary,
Success,
Info,
Warning,
Error
}
}
Loading

0 comments on commit 1794059

Please sign in to comment.