├── CreateSchema.sql ├── WoL ├── Startup.cs ├── wwwroot │ ├── favicon.ico │ └── css │ │ ├── open-iconic │ │ ├── font │ │ │ ├── fonts │ │ │ │ ├── open-iconic.eot │ │ │ │ ├── open-iconic.otf │ │ │ │ ├── open-iconic.ttf │ │ │ │ ├── open-iconic.woff │ │ │ │ └── open-iconic.svg │ │ │ └── css │ │ │ │ └── open-iconic-bootstrap.min.css │ │ ├── ICON-LICENSE │ │ ├── README.md │ │ └── FONT-LICENSE │ │ └── site.css ├── Services │ ├── IWakeService.cs │ ├── IAddressLookupService.cs │ ├── WakeService.cs │ ├── IPingService.cs │ ├── AddressLookupService.cs │ ├── IcmpPingService.cs │ ├── RdpPortPingService.cs │ ├── CompositePingService.cs │ └── DnsPingServiceBase.cs ├── Data │ ├── SqliteDbContext.cs │ ├── SqlServerDbContext.cs │ ├── CovariantDbContextFactory.cs │ ├── IHostService.cs │ ├── ApplicationDbContext.cs │ └── HostService.cs ├── _Imports.razor ├── App.razor ├── Shared │ ├── MainLayout.razor │ └── NavMenu.razor ├── appsettings.Development.json ├── GlobalSuppressions.cs ├── Extensions │ ├── HostExtensions.cs │ ├── PingServiceExtensions.cs │ ├── StringExtensions.cs │ ├── TaskExtensions.cs │ └── LoggingExtensions.cs ├── appsettings.json ├── Pages │ ├── Error.razor │ ├── _Host.cshtml │ ├── DeleteHost.razor │ ├── Hosts.razor │ ├── Wake.razor │ └── AddHost.razor ├── Program.cs ├── Properties │ └── launchSettings.json ├── Models │ ├── Host.cs │ └── ViewModels │ │ ├── HostViewModel.cs │ │ └── AddHostViewModel.cs ├── Components │ └── HostStatus.razor ├── WoL.csproj └── Migrations │ ├── Sqlite │ ├── 20220614141028_CreateInitialSchema.cs │ ├── SqliteDbContextModelSnapshot.cs │ └── 20220614141028_CreateInitialSchema.Designer.cs │ └── SqlServer │ ├── 20191211184319_CreateInitialSchema.cs │ ├── SqlServerDbContextModelSnapshot.cs │ └── 20191211184319_CreateInitialSchema.Designer.cs ├── screenshot-01-index.png ├── version.json ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── standalone-build.yml │ ├── cd.yml │ ├── cron-docker-rebuild.yml │ └── docker-build-and-push.yml ├── .dockerignore ├── Directory.Build.targets ├── Dockerfile ├── Directory.Packages.props ├── LICENSE.txt ├── Directory.Build.props ├── WoL.sln ├── .gitattributes ├── azure-pipelines.yml ├── README.md ├── .gitignore └── .editorconfig /CreateSchema.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georg-jung/BlazorWoL/HEAD/CreateSchema.sql -------------------------------------------------------------------------------- /WoL/Startup.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georg-jung/BlazorWoL/HEAD/WoL/Startup.cs -------------------------------------------------------------------------------- /WoL/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georg-jung/BlazorWoL/HEAD/WoL/wwwroot/favicon.ico -------------------------------------------------------------------------------- /screenshot-01-index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georg-jung/BlazorWoL/HEAD/screenshot-01-index.png -------------------------------------------------------------------------------- /WoL/wwwroot/css/open-iconic/font/fonts/open-iconic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georg-jung/BlazorWoL/HEAD/WoL/wwwroot/css/open-iconic/font/fonts/open-iconic.eot -------------------------------------------------------------------------------- /WoL/wwwroot/css/open-iconic/font/fonts/open-iconic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georg-jung/BlazorWoL/HEAD/WoL/wwwroot/css/open-iconic/font/fonts/open-iconic.otf -------------------------------------------------------------------------------- /WoL/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georg-jung/BlazorWoL/HEAD/WoL/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf -------------------------------------------------------------------------------- /WoL/wwwroot/css/open-iconic/font/fonts/open-iconic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/georg-jung/BlazorWoL/HEAD/WoL/wwwroot/css/open-iconic/font/fonts/open-iconic.woff -------------------------------------------------------------------------------- /WoL/Services/IWakeService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace WoL.Services 4 | { 5 | public interface IWakeService 6 | { 7 | Task Wake(byte[] mac); 8 | } 9 | } -------------------------------------------------------------------------------- /WoL/Data/SqliteDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace WoL.Data 4 | { 5 | public class SqliteDbContext : ApplicationDbContext 6 | { 7 | public SqliteDbContext(DbContextOptions options) : base(options) 8 | { 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /WoL/Data/SqlServerDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace WoL.Data 4 | { 5 | public class SqlServerDbContext : ApplicationDbContext 6 | { 7 | public SqlServerDbContext(DbContextOptions options) : base(options) 8 | { 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /WoL/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using Microsoft.AspNetCore.Authorization 3 | @using Microsoft.AspNetCore.Components.Authorization 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using Microsoft.JSInterop 8 | @using WoL 9 | @using WoL.Shared 10 | -------------------------------------------------------------------------------- /WoL/Services/IAddressLookupService.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.NetworkInformation; 3 | using System.Threading.Tasks; 4 | 5 | namespace WoL.Services 6 | { 7 | public interface IAddressLookupService 8 | { 9 | Task<(IPAddress, string)> GetIpAndName(string hostname); 10 | Task GetMac(IPAddress ip); 11 | } 12 | } -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", 3 | "version": "1.4-alpha", 4 | "publicReleaseRefSpec": [ 5 | "^refs/heads/release/v\\d+(?:\\.\\d+)?$", 6 | "^refs/tags/v\\d+\\.\\d+" 7 | ], 8 | "release": { 9 | "branchName": "release/v{version}" 10 | } 11 | } -------------------------------------------------------------------------------- /WoL/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Sorry, there's nothing at this address.

8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: nuget 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | labels: 9 | - dependencies 10 | - package-ecosystem: "docker" 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | - package-ecosystem: "github-actions" 15 | directory: "/" 16 | schedule: 17 | interval: daily 18 | -------------------------------------------------------------------------------- /WoL/Services/WakeService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Threading.Tasks; 6 | 7 | namespace WoL.Services 8 | { 9 | public class WakeService : IWakeService 10 | { 11 | public Task Wake(byte[] mac) 12 | { 13 | return IPAddress.Broadcast.SendWolAsync(mac, 40000); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /WoL/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 | 6 | 7 |
8 |
9 | 10 | Created by 11 | Georg Jung 12 | 13 |
14 | 15 |
16 | @Body 17 |
18 |
19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - 'release/v**' 7 | paths-ignore: 8 | - '**.md' 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | docker-build: 15 | uses: ./.github/workflows/docker-build-and-push.yml 16 | with: 17 | dockerfile: ./Dockerfile 18 | push: false 19 | standalone-build: 20 | uses: ./.github/workflows/standalone-build.yml 21 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | # **/.git # we currently want .git for nbgv 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md 26 | -------------------------------------------------------------------------------- /WoL/Services/IPingService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading.Tasks; 4 | 5 | namespace WoL.Services 6 | { 7 | public interface IPingService 8 | { 9 | Task IsReachable(IPAddress ip, TimeSpan timeout); 10 | Task IsReachable(string hostname, TimeSpan timeout); 11 | 12 | public enum PingResult 13 | { 14 | Unreachable, 15 | HostNotFound, 16 | Success 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /WoL/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | // uncomment to override the production sqlite setting and use tsql localdb instead 3 | /* 4 | "ConnectionStrings": { 5 | "TsqlConnection": "Server=(localdb)\\mssqllocaldb;Database=blazor-wol;Trusted_Connection=True;MultipleActiveResultSets=true", 6 | "SqliteConnection": "" 7 | },*/ 8 | "DetailedErrors": true, 9 | "Logging": { 10 | "LogLevel": { 11 | "Default": "Information", 12 | "Microsoft": "Warning", 13 | "Microsoft.Hosting.Lifetime": "Information" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /WoL/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Literale nicht als lokalisierte Parameter übergeben", Justification = "Internal service's messages, e.g. logging and exceptions, don't need to be localized.", Scope = "namespaceanddescendants", Target = "WoL.Services")] 7 | -------------------------------------------------------------------------------- /WoL/Extensions/HostExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using WoL.Models; 7 | 8 | namespace WoL.Extensions 9 | { 10 | public static class HostExtensions 11 | { 12 | public static string GetMacString(this Host value) 13 | { 14 | var adrBytes = value?.MacAddress ?? throw new ArgumentNullException(nameof(value)); 15 | return string.Join(":", from z in adrBytes select z.ToString("X2", CultureInfo.InvariantCulture)); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | 6 | 7 | cobertura 8 | [xunit.*]* 9 | 10 | $(OutputPath)/ 11 | 12 | 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base 4 | WORKDIR /app 5 | EXPOSE 80 6 | EXPOSE 443 7 | 8 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build 9 | WORKDIR /src 10 | COPY ["WoL.sln", "Directory.Build.props", "Directory.Packages.props", "./"] 11 | COPY ["WoL/WoL.csproj", "./WoL/"] 12 | RUN dotnet restore "WoL.sln" 13 | COPY . . 14 | RUN dotnet build "WoL.sln" -c Release -o /app/build 15 | 16 | FROM build AS publish 17 | RUN dotnet publish "WoL/WoL.csproj" -c Release -o /app/publish 18 | 19 | FROM base AS final 20 | WORKDIR /app 21 | COPY --from=publish /app/publish . 22 | ENTRYPOINT ["dotnet", "WoL.dll"] 23 | -------------------------------------------------------------------------------- /WoL/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | // set either TsqlConnection or SqliteConnection depending on which backend you want to use 4 | /* "TsqlConnection": "Server=(localdb)\\mssqllocaldb;Database=blazor-wol;Trusted_Connection=True;MultipleActiveResultSets=true", */ 5 | "SqliteConnection": "Data Source=wol-db.sqlite" 6 | }, 7 | "Logging": { 8 | "LogLevel": { 9 | "Default": "Information", 10 | "Microsoft": "Warning", 11 | "Microsoft.Hosting.Lifetime": "Information" 12 | } 13 | }, 14 | "AllowedHosts": "*", 15 | "AutoUpdateDatabase": true, 16 | "RequireHttps": false, 17 | "ApplicationInsights": { 18 | "InstrumentationKey": null 19 | }, 20 | "ApplicationInsightsStorageFolder": null 21 | } 22 | -------------------------------------------------------------------------------- /WoL/Pages/Error.razor: -------------------------------------------------------------------------------- 1 | @page "/error" 2 | 3 | 4 |

Error.

5 |

An error occurred while processing your request.

6 | 7 |

Development Mode

8 |

9 | Swapping to Development environment will display more detailed information about the error that occurred. 10 |

11 |

12 | The Development environment shouldn't be enabled for deployed applications. 13 | It can result in displaying sensitive information from exceptions to end users. 14 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 15 | and restarting the app. 16 |

-------------------------------------------------------------------------------- /WoL/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Hosting; 10 | using Microsoft.Extensions.Logging; 11 | 12 | namespace WoL 13 | { 14 | public static class Program 15 | { 16 | public static void Main(string[] args) 17 | { 18 | CreateHostBuilder(args).Build().Run(); 19 | } 20 | 21 | public static IHostBuilder CreateHostBuilder(string[] args) => 22 | Host.CreateDefaultBuilder(args) 23 | .ConfigureWebHostDefaults(webBuilder => 24 | { 25 | webBuilder.UseStartup(); 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /WoL/Services/AddressLookupService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.NetworkInformation; 6 | using System.Net.Sockets; 7 | using System.Threading.Tasks; 8 | 9 | namespace WoL.Services 10 | { 11 | public class AddressLookupService : IAddressLookupService 12 | { 13 | public async Task<(IPAddress, string)> GetIpAndName(string hostname) 14 | { 15 | var res = await Dns.GetHostEntryAsync(hostname).ConfigureAwait(false); 16 | return (res.AddressList.First(ip => ip.AddressFamily == AddressFamily.InterNetwork), res.HostName); 17 | } 18 | 19 | public async Task GetMac(IPAddress ip) 20 | { 21 | return await ArpLookup.Arp.LookupAsync(ip).ConfigureAwait(false); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /WoL/Extensions/PingServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using WoL.Models.ViewModels; 6 | using WoL.Services; 7 | 8 | namespace WoL.Extensions 9 | { 10 | public static class PingServiceExtensions 11 | { 12 | public static HostViewModel.HostStatus ToHostStatus(this IPingService.PingResult result) 13 | { 14 | return result switch 15 | { 16 | IPingService.PingResult.Success => HostViewModel.HostStatus.Online, 17 | IPingService.PingResult.Unreachable => HostViewModel.HostStatus.Unreachable, 18 | IPingService.PingResult.HostNotFound => HostViewModel.HostStatus.HostnameInvalid, 19 | _ => throw new NotImplementedException() 20 | }; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /WoL/Services/IcmpPingService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Net.NetworkInformation; 7 | using System.Threading.Tasks; 8 | using static WoL.Services.IPingService; 9 | 10 | namespace WoL.Services 11 | { 12 | public class IcmpPingService : DnsPingServiceBase 13 | { 14 | public IcmpPingService(IAddressLookupService addressLookupService, ILoggerFactory loggerFactory) : base(addressLookupService, loggerFactory.CreateLogger()) 15 | { 16 | } 17 | 18 | public override async Task IsReachable(IPAddress ip, TimeSpan timeout) 19 | { 20 | using var ping = new Ping(); 21 | var reply = await ping.SendPingAsync(ip, (int)timeout.TotalMilliseconds).ConfigureAwait(false); 22 | return reply.Status == IPStatus.Success; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /WoL/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:39536", 7 | "sslPort": 44378 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "WoL": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 25 | }, 26 | "Docker": { 27 | "commandName": "Docker", 28 | "launchBrowser": true, 29 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", 30 | "publishAllPorts": true, 31 | "useSSL": true 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /.github/workflows/standalone-build.yml: -------------------------------------------------------------------------------- 1 | name: Standalone Build 2 | 3 | on: 4 | workflow_call 5 | 6 | jobs: 7 | build-and-push: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | include: 12 | - args: '' 13 | name: fdd 14 | - args: '-r win-x86 --self-contained /p:PublishSingleFile=true' 15 | name: win-x86 16 | - args: '-r win-x86 --self-contained' 17 | name: win-x86-iis 18 | - args: '-r linux-x64 --self-contained /p:PublishSingleFile=true' 19 | name: linux-x64 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 # avoid shallow clone so nbgv can do its work. 24 | - name: Publish 25 | run: dotnet publish -c Release ${{ matrix.args }} /p:ContinuousIntegrationBuild=true --output bin/${{ matrix.name }} 26 | - name: Upload Artifacts 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: ${{ matrix.name }} 30 | path: bin/${{ matrix.name }}/ 31 | -------------------------------------------------------------------------------- /WoL/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace WoL.Extensions 8 | { 9 | public static class StringExtensions 10 | { 11 | public static byte[] ParseMacAddress(this string mac) 12 | { 13 | var macString = mac?.Replace(":", "-", StringComparison.Ordinal)?.ToUpper(CultureInfo.InvariantCulture) ?? throw new ArgumentNullException(nameof(mac)); 14 | return System.Net.NetworkInformation.PhysicalAddress.Parse(macString).GetAddressBytes(); 15 | } 16 | 17 | public static bool TryParseMacAddress(this string mac, out byte[] bytes) 18 | { 19 | bytes = null; 20 | try 21 | { 22 | bytes = ParseMacAddress(mac); 23 | return true; 24 | } 25 | catch (FormatException) 26 | { 27 | return false; 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /WoL/Data/CovariantDbContextFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace WoL.Data 6 | { 7 | // see https://github.com/dotnet/efcore/issues/26630#issuecomment-1014837147 8 | internal class CovariantDbContextFactory 9 | : IDbContextFactory 10 | where TContextOut : DbContext 11 | where TContextIn : TContextOut 12 | { 13 | private readonly IDbContextFactory _contextInFactory; 14 | 15 | public CovariantDbContextFactory(IDbContextFactory contextInFactory) 16 | { 17 | _contextInFactory = contextInFactory; 18 | } 19 | 20 | public TContextOut CreateDbContext() 21 | => _contextInFactory.CreateDbContext(); 22 | 23 | public async Task CreateDbContextAsync(CancellationToken cancellationToken = default) 24 | => await _contextInFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /WoL/Models/Host.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using WoL.Extensions; 7 | 8 | namespace WoL.Models 9 | { 10 | public class Host 11 | { 12 | public int Id { get; set; } 13 | 14 | [StringLength(255)] 15 | public string Hostname { get; set; } 16 | 17 | [StringLength(255)] 18 | [Required] 19 | public string Caption { get; set; } 20 | 21 | [MaxLength(6)] 22 | [Required] 23 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Eigenschaften dürfen keine Arrays zurückgeben", Justification = "A mac address intrinsically is a byte[]")] 24 | public byte[] MacAddress { get; set; } 25 | 26 | public override string ToString() 27 | { 28 | return $"Host(Id = {Id}; Hostname = {Hostname}; MacAddress = {(MacAddress == null ? "null" : this.GetMacString())}; Caption = {Caption})"; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /WoL/Components/HostStatus.razor: -------------------------------------------------------------------------------- 1 | @using WoL.Models.ViewModels 2 | 3 | @switch (Status) 4 | { 5 | case HostViewModel.HostStatus.Loading: 6 | if (SpinnerIfLoading) 7 | { 8 | 9 | } 10 | else 11 | { 12 | @:  13 | } 14 | break; 15 | case HostViewModel.HostStatus.Online: 16 | Online 17 | break; 18 | case HostViewModel.HostStatus.Unreachable: 19 | Offline 20 | break; 21 | case HostViewModel.HostStatus.NoHostname: 22 | n/a 23 | break; 24 | case HostViewModel.HostStatus.HostnameInvalid: 25 | Invalid hostname 26 | break; 27 | } 28 | 29 | @code { 30 | [Parameter] 31 | public HostViewModel.HostStatus Status { get; set; } 32 | 33 | [Parameter] 34 | public bool SpinnerIfLoading { get; set; } = true; 35 | } 36 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | true 5 | $(NoWarn);NU1507 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /WoL/WoL.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | Linux 5 | ..\Dockerfile 6 | 7d2f101a-8b2f-42f0-9f8f-f7d55045184e 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /WoL/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 21 |
22 | 23 | @code { 24 | private bool collapseNavMenu = true; 25 | 26 | private string NavMenuCssClass => collapseNavMenu ? "collapse" : null; 27 | 28 | private void ToggleNavMenu() 29 | { 30 | collapseNavMenu = !collapseNavMenu; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Georg Jung 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | latest 6 | false 7 | latest 8 | 9 | Georg Jung 10 | Georg Jung 11 | © Georg Jung. All rights reserved. 12 | true 13 | true 14 | true 15 | snupkg 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /WoL/wwwroot/css/open-iconic/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /WoL.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29519.181 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WoL", "WoL\WoL.csproj", "{4DC0498D-11AD-4868-AE24-AD135C45C91F}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {4DC0498D-11AD-4868-AE24-AD135C45C91F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {4DC0498D-11AD-4868-AE24-AD135C45C91F}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {4DC0498D-11AD-4868-AE24-AD135C45C91F}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {4DC0498D-11AD-4868-AE24-AD135C45C91F}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {3B2DDC1A-B71A-4B1B-8CA2-1F1356689DEE} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /WoL/Pages/_Host.cshtml: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @namespace WoL.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | @{ 5 | Layout = null; 6 | } 7 | 8 | 9 | 10 | 11 | 12 | 13 | Wake-on-LAN 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | An error has occurred. This application may no longer respond until reloaded. 26 | 27 | 28 | An unhandled exception has occurred. See browser dev tools for details. 29 | 30 | Reload 31 | 🗙 32 |
33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /WoL/Models/ViewModels/HostViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using WoL.Extensions; 6 | 7 | namespace WoL.Models.ViewModels 8 | { 9 | public class HostViewModel 10 | { 11 | public HostViewModel() 12 | { 13 | } 14 | 15 | public HostViewModel(Host host) 16 | { 17 | if (host == null) 18 | throw new ArgumentNullException(nameof(host)); 19 | 20 | Id = host.Id; 21 | Hostname = host.Hostname; 22 | Caption = host.Caption; 23 | MacAddress = host.GetMacString(); 24 | Status = string.IsNullOrEmpty(Hostname) ? HostStatus.NoHostname : HostStatus.Loading; 25 | } 26 | 27 | public int Id { get; set; } 28 | 29 | public string Hostname { get; set; } 30 | 31 | public string Caption { get; set; } 32 | 33 | public string MacAddress { get; set; } 34 | 35 | public HostStatus Status { get; set; } 36 | 37 | public enum HostStatus 38 | { 39 | Loading, 40 | Online, 41 | Unreachable, 42 | HostnameInvalid, 43 | NoHostname 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /WoL/Services/RdpPortPingService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Net.Sockets; 7 | using System.Threading.Tasks; 8 | using WoL.Extensions; 9 | 10 | namespace WoL.Services 11 | { 12 | public class RdpPortPingService : DnsPingServiceBase 13 | { 14 | public RdpPortPingService(IAddressLookupService addressLookupService, ILoggerFactory loggerFactory) : base(addressLookupService, loggerFactory.CreateLogger()) 15 | { 16 | } 17 | 18 | private static async Task IsPortOpen(IPAddress ip, int port, TimeSpan timeout) 19 | { 20 | try 21 | { 22 | using var client = new TcpClient(); 23 | await client.ConnectAsync(ip, port).TimeoutAfter(timeout).ConfigureAwait(false); 24 | return true; 25 | } 26 | catch (Exception ex) 27 | when (ex is SocketException || ex is OperationCanceledException) 28 | { 29 | return false; 30 | } 31 | } 32 | 33 | public override Task IsReachable(IPAddress ip, TimeSpan timeout) 34 | { 35 | return IsPortOpen(ip, 3389, timeout); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'release/v**' 7 | paths-ignore: 8 | - '**.md' 9 | 10 | permissions: 11 | contents: write 12 | packages: write 13 | 14 | jobs: 15 | docker-build: 16 | uses: ./.github/workflows/docker-build-and-push.yml 17 | with: 18 | dockerfile: ./Dockerfile 19 | push: true 20 | standalone-build: 21 | uses: ./.github/workflows/standalone-build.yml 22 | create-github-release: 23 | runs-on: ubuntu-latest 24 | needs: [docker-build, standalone-build] 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 # avoid shallow clone so nbgv can do its work. 29 | - uses: dotnet/nbgv@v0.4 30 | id: nbgv 31 | - uses: actions/download-artifact@v4 32 | with: 33 | path: artifacts 34 | - name: ZIP artifacts 35 | run: | 36 | 7z a artifacts/fdd.zip ./artifacts/fdd/* 37 | 7z a artifacts/win-x86.zip ./artifacts/win-x86/* 38 | 7z a artifacts/win-x86-iis.zip ./artifacts/win-x86-iis/* 39 | 7z a artifacts/linux-x64.zip ./artifacts/linux-x64/* 40 | - uses: softprops/action-gh-release@v1 41 | with: 42 | tag_name: v${{ steps.nbgv.outputs.SemVer2 }} 43 | draft: true 44 | prerelease: false 45 | files: artifacts/*.zip 46 | target_commitish: ${{ github.sha }} 47 | -------------------------------------------------------------------------------- /WoL/Models/ViewModels/AddHostViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using WoL.Extensions; 7 | 8 | namespace WoL.Models.ViewModels 9 | { 10 | public class AddHostViewModel : IValidatableObject 11 | { 12 | public int Id { get; set; } 13 | 14 | [StringLength(255)] 15 | [Display(Name = "Hostname")] 16 | public string Hostname { get; set; } 17 | 18 | [StringLength(255)] 19 | [Required] 20 | [Display(Name = "Title")] 21 | public string Caption { get; set; } 22 | 23 | [StringLength(17, MinimumLength = 17)] 24 | [Display(Name = "Mac address")] 25 | public string MacAddress { get; set; } 26 | 27 | public IEnumerable Validate(ValidationContext validationContext) 28 | { 29 | if (string.IsNullOrWhiteSpace(MacAddress) && string.IsNullOrWhiteSpace(Hostname)) 30 | yield return new ValidationResult("You must enter either a hostname or a mac address.", new[] { nameof(Hostname) }); 31 | if (!string.IsNullOrWhiteSpace(MacAddress) && !string.IsNullOrWhiteSpace(Hostname)) 32 | yield return new ValidationResult("You must enter either a hostname or a mac address, but not both.", new[] { nameof(Hostname) }); 33 | if (!string.IsNullOrEmpty(MacAddress) && !MacAddress.TryParseMacAddress(out _)) 34 | yield return new ValidationResult("The entered mac address is not valid.", new[] { nameof(MacAddress) }); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /WoL/Services/CompositePingService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Threading.Tasks; 7 | 8 | namespace WoL.Services 9 | { 10 | public class CompositePingService : DnsPingServiceBase 11 | { 12 | private readonly RdpPortPingService rdpPing; 13 | private readonly IcmpPingService icmpPing; 14 | 15 | public CompositePingService(IAddressLookupService addressLookupService, RdpPortPingService rdpPing, IcmpPingService icmpPing, ILoggerFactory loggerFactory) : base(addressLookupService, loggerFactory.CreateLogger()) 16 | { 17 | this.rdpPing = rdpPing; 18 | this.icmpPing = icmpPing; 19 | } 20 | 21 | public override async Task IsReachable(IPAddress ip, TimeSpan timeout) 22 | { 23 | // start pings in parallel 24 | var rdp = rdpPing.IsReachable(ip, timeout); 25 | var icmp = icmpPing.IsReachable(ip, timeout); 26 | 27 | var finished = await Task.WhenAny(rdp, icmp).ConfigureAwait(false); 28 | // if one finishes early and can connect, return early 29 | if (await finished.ConfigureAwait(false)) 30 | return true; 31 | 32 | // otherwise wait for the second task and check if it is true 33 | if (finished == icmp && await rdp.ConfigureAwait(false)) 34 | return true; 35 | if (finished == rdp && await icmp.ConfigureAwait(false)) 36 | return true; 37 | 38 | return false; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /WoL/Migrations/Sqlite/20220614141028_CreateInitialSchema.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace WoL.Migrations.Sqlite 7 | { 8 | public partial class CreateInitialSchema : Migration 9 | { 10 | protected override void Up(MigrationBuilder migrationBuilder) 11 | { 12 | migrationBuilder.CreateTable( 13 | name: "Host", 14 | columns: table => new 15 | { 16 | Id = table.Column(type: "INTEGER", nullable: false) 17 | .Annotation("Sqlite:Autoincrement", true), 18 | Hostname = table.Column(type: "TEXT", maxLength: 255, nullable: true), 19 | Caption = table.Column(type: "TEXT", maxLength: 255, nullable: false), 20 | MacAddress = table.Column(type: "BLOB", maxLength: 6, nullable: false) 21 | }, 22 | constraints: table => 23 | { 24 | table.PrimaryKey("PK_Host", x => x.Id); 25 | }); 26 | 27 | migrationBuilder.CreateIndex( 28 | name: "IX_Host_Hostname", 29 | table: "Host", 30 | column: "Hostname", 31 | unique: true); 32 | 33 | migrationBuilder.CreateIndex( 34 | name: "IX_Host_MacAddress", 35 | table: "Host", 36 | column: "MacAddress", 37 | unique: true); 38 | } 39 | 40 | protected override void Down(MigrationBuilder migrationBuilder) 41 | { 42 | migrationBuilder.DropTable( 43 | name: "Host"); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /WoL/Data/IHostService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using WoL.Models; 5 | 6 | namespace WoL.Data 7 | { 8 | public interface IHostService 9 | { 10 | Task Add(Host host); 11 | Task Delete(int id); 12 | Task Find(int id); 13 | Task> GetAll(); 14 | 15 | public class DuplicateEntryException : ArgumentException 16 | { 17 | public string Field { get; } 18 | public string Value { get; } 19 | 20 | public DuplicateEntryException() 21 | { 22 | } 23 | 24 | public DuplicateEntryException(string message) : base(message) 25 | { 26 | } 27 | 28 | public DuplicateEntryException(string message, Exception innerException) : base(message, innerException) 29 | { 30 | } 31 | 32 | private static string CreateMessage(string field, string value) => 33 | $"A host entry with {field} '{value}' does already exist."; 34 | 35 | private static string CreateMessage(string field) => 36 | $"A host entry with the same {field} does already exist."; 37 | 38 | public DuplicateEntryException(string field, string value, string paramName, Exception innerException) : base(CreateMessage(field, value), paramName, innerException) 39 | { 40 | Field = field; 41 | Value = value; 42 | } 43 | 44 | public DuplicateEntryException(string field, string paramName, Exception innerException) : base(CreateMessage(field), paramName, innerException) 45 | { 46 | Field = field; 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /WoL/Migrations/Sqlite/SqliteDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using WoL.Data; 7 | 8 | #nullable disable 9 | 10 | namespace WoL.Migrations.Sqlite 11 | { 12 | [DbContext(typeof(SqliteDbContext))] 13 | partial class SqliteDbContextModelSnapshot : ModelSnapshot 14 | { 15 | protected override void BuildModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder.HasAnnotation("ProductVersion", "6.0.5"); 19 | 20 | modelBuilder.Entity("WoL.Models.Host", b => 21 | { 22 | b.Property("Id") 23 | .ValueGeneratedOnAdd() 24 | .HasColumnType("INTEGER"); 25 | 26 | b.Property("Caption") 27 | .IsRequired() 28 | .HasMaxLength(255) 29 | .HasColumnType("TEXT"); 30 | 31 | b.Property("Hostname") 32 | .HasMaxLength(255) 33 | .HasColumnType("TEXT"); 34 | 35 | b.Property("MacAddress") 36 | .IsRequired() 37 | .HasMaxLength(6) 38 | .HasColumnType("BLOB"); 39 | 40 | b.HasKey("Id"); 41 | 42 | b.HasIndex("Hostname") 43 | .IsUnique(); 44 | 45 | b.HasIndex("MacAddress") 46 | .IsUnique(); 47 | 48 | b.ToTable("Host"); 49 | }); 50 | #pragma warning restore 612, 618 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/cron-docker-rebuild.yml: -------------------------------------------------------------------------------- 1 | name: Regular base image update check 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 1 * * *' 7 | 8 | env: 9 | REGISTRY: ghcr.io 10 | IMAGE_NAME: ghcr.io/georg-jung/blazorwol:latest 11 | BASE_IMAGE: mcr.microsoft.com/dotnet/aspnet:7.0 12 | 13 | jobs: 14 | check_base_image_updated: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: read 18 | packages: read 19 | outputs: 20 | image-needs-updating: ${{ steps.check.outputs.needs-updating }} 21 | build-ref: ${{ steps.latestrelease.outputs.releasetag }} 22 | steps: 23 | - name: Docker Image Update Checker 24 | id: check 25 | uses: georg-jung/docker-image-update-checker@main 26 | with: 27 | base-image: ${{ env.BASE_IMAGE }} 28 | image: ${{ env.IMAGE_NAME }} 29 | github_token: ${{ secrets.GITHUB_TOKEN }} 30 | verbose: true 31 | - name: get latest release with tag 32 | id: latestrelease 33 | run: | 34 | echo "releasetag=$(curl --header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' -s ${{ github.api_url }}/repos/${{ github.repository }}/releases/latest | jq '.tag_name' | sed 's/\"//g')" \ 35 | >> $GITHUB_OUTPUT 36 | if: steps.check.outputs.needs-updating == 'true' 37 | - name: echo release tag 38 | run: | 39 | echo ${{ steps.latestrelease.outputs.releasetag }} 40 | if: steps.check.outputs.needs-updating == 'true' 41 | call_rebuild: 42 | needs: check_base_image_updated 43 | if: ${{ needs.check_base_image_updated.outputs.image-needs-updating == 'true' }} 44 | uses: ./.github/workflows/docker-build-and-push.yml 45 | with: 46 | dockerfile: ./Dockerfile 47 | push: true 48 | build-ref: ${{ needs.check_base_image_updated.outputs.build-ref }} 49 | -------------------------------------------------------------------------------- /WoL/Data/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using WoL.Models; 8 | 9 | namespace WoL.Data 10 | { 11 | public class ApplicationDbContext : DbContext 12 | { 13 | public DbSet Hosts { get; set; } 14 | 15 | public ApplicationDbContext(DbContextOptions options) 16 | : base(options) 17 | { 18 | } 19 | 20 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Argumente von öffentlichen Methoden validieren", Justification = "Called by the framework, won't be called with null argument")] 21 | protected override void OnModelCreating(ModelBuilder builder) 22 | { 23 | //This will singularize all table names 24 | foreach (IMutableEntityType entityType in builder.Model.GetEntityTypes()) 25 | { 26 | // this works different since ef core 3 27 | // https://docs.microsoft.com/de-de/ef/core/what-is-new/ef-core-3.0/breaking-changes#provider-specific-metadata-api-changes 28 | entityType.SetTableName(entityType.ClrType.Name); 29 | }; 30 | 31 | // disable cascading deletes globally 32 | /*var cascadeFKs = builder.Model.GetEntityTypes() 33 | .SelectMany(t => t.GetForeignKeys()) 34 | .Where(fk => !fk.IsOwnership && fk.DeleteBehavior == DeleteBehavior.Cascade); 35 | 36 | foreach (var fk in cascadeFKs) 37 | fk.DeleteBehavior = DeleteBehavior.Restrict;*/ 38 | 39 | builder.Entity().HasIndex(e => e.Hostname).IsUnique(); 40 | builder.Entity().HasIndex(e => e.MacAddress).IsUnique(); 41 | 42 | base.OnModelCreating(builder); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /WoL/Migrations/Sqlite/20220614141028_CreateInitialSchema.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using WoL.Data; 8 | 9 | #nullable disable 10 | 11 | namespace WoL.Migrations.Sqlite 12 | { 13 | [DbContext(typeof(SqliteDbContext))] 14 | [Migration("20220614141028_CreateInitialSchema")] 15 | partial class CreateInitialSchema 16 | { 17 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 18 | { 19 | #pragma warning disable 612, 618 20 | modelBuilder.HasAnnotation("ProductVersion", "6.0.5"); 21 | 22 | modelBuilder.Entity("WoL.Models.Host", b => 23 | { 24 | b.Property("Id") 25 | .ValueGeneratedOnAdd() 26 | .HasColumnType("INTEGER"); 27 | 28 | b.Property("Caption") 29 | .IsRequired() 30 | .HasMaxLength(255) 31 | .HasColumnType("TEXT"); 32 | 33 | b.Property("Hostname") 34 | .HasMaxLength(255) 35 | .HasColumnType("TEXT"); 36 | 37 | b.Property("MacAddress") 38 | .IsRequired() 39 | .HasMaxLength(6) 40 | .HasColumnType("BLOB"); 41 | 42 | b.HasKey("Id"); 43 | 44 | b.HasIndex("Hostname") 45 | .IsUnique(); 46 | 47 | b.HasIndex("MacAddress") 48 | .IsUnique(); 49 | 50 | b.ToTable("Host"); 51 | }); 52 | #pragma warning restore 612, 618 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /WoL/Services/DnsPingServiceBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Threading.Tasks; 7 | using static WoL.Services.IPingService; 8 | 9 | namespace WoL.Services 10 | { 11 | public abstract class DnsPingServiceBase : IPingService 12 | { 13 | private readonly IAddressLookupService addressLookupService; 14 | private readonly ILogger logger; 15 | 16 | protected DnsPingServiceBase(IAddressLookupService addressLookupService, ILogger logger) 17 | { 18 | this.addressLookupService = addressLookupService; 19 | this.logger = logger; 20 | } 21 | 22 | public async Task IsReachable(string hostname, TimeSpan timeout) 23 | { 24 | IPAddress ip; 25 | try 26 | { 27 | (ip, _) = await addressLookupService.GetIpAndName(hostname).ConfigureAwait(false); 28 | } 29 | // could throw at least SocketException, ArgumentException, ArgumentOutOfRangeException, InvalidOperationException, maybe NRE, which would be of interest 30 | // we want to catch them all, basically anything that can make the resolution fail 31 | #pragma warning disable CA1031 // Keine allgemeinen Ausnahmetypen abfangen 32 | catch (Exception ex) 33 | #pragma warning restore CA1031 // Keine allgemeinen Ausnahmetypen abfangen 34 | { 35 | logger.LogDebug(ex, $"Exception during {nameof(IAddressLookupService)}.{nameof(IAddressLookupService.GetIpAndName)}"); 36 | return PingResult.HostNotFound; 37 | } 38 | return await IsReachable(ip, timeout).ConfigureAwait(false) ? PingResult.Success : PingResult.Unreachable; 39 | } 40 | 41 | public abstract Task IsReachable(IPAddress ip, TimeSpan timeout); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /WoL/Migrations/SqlServer/20191211184319_CreateInitialSchema.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace WoL.Migrations 7 | { 8 | public partial class CreateInitialSchema : Migration 9 | { 10 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Argumente von öffentlichen Methoden validieren", Justification = "Auto-generated and typically called by the framework, won't be called with null")] 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.CreateTable( 14 | name: "Host", 15 | columns: table => new 16 | { 17 | Id = table.Column(nullable: false) 18 | .Annotation("SqlServer:Identity", "1, 1"), 19 | Hostname = table.Column(maxLength: 255, nullable: true), 20 | Caption = table.Column(maxLength: 255, nullable: false), 21 | MacAddress = table.Column(maxLength: 6, nullable: false) 22 | }, 23 | constraints: table => 24 | { 25 | table.PrimaryKey("PK_Host", x => x.Id); 26 | }); 27 | 28 | migrationBuilder.CreateIndex( 29 | name: "IX_Host_Hostname", 30 | table: "Host", 31 | column: "Hostname", 32 | unique: true, 33 | filter: "[Hostname] IS NOT NULL"); 34 | 35 | migrationBuilder.CreateIndex( 36 | name: "IX_Host_MacAddress", 37 | table: "Host", 38 | column: "MacAddress", 39 | unique: true); 40 | } 41 | 42 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Argumente von öffentlichen Methoden validieren", Justification = "Auto-generated and typically called by the framework, won't be called with null")] 43 | protected override void Down(MigrationBuilder migrationBuilder) 44 | { 45 | migrationBuilder.DropTable( 46 | name: "Host"); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /WoL/Migrations/SqlServer/SqlServerDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using WoL.Data; 8 | 9 | #nullable disable 10 | 11 | namespace WoL.Migrations.SqlServer 12 | { 13 | [DbContext(typeof(SqlServerDbContext))] 14 | partial class SqlServerDbContextModelSnapshot : ModelSnapshot 15 | { 16 | protected override void BuildModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "6.0.5") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128); 22 | 23 | SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); 24 | 25 | modelBuilder.Entity("WoL.Models.Host", b => 26 | { 27 | b.Property("Id") 28 | .ValueGeneratedOnAdd() 29 | .HasColumnType("int"); 30 | 31 | SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); 32 | 33 | b.Property("Caption") 34 | .IsRequired() 35 | .HasMaxLength(255) 36 | .HasColumnType("nvarchar(255)"); 37 | 38 | b.Property("Hostname") 39 | .HasMaxLength(255) 40 | .HasColumnType("nvarchar(255)"); 41 | 42 | b.Property("MacAddress") 43 | .IsRequired() 44 | .HasMaxLength(6) 45 | .HasColumnType("varbinary(6)"); 46 | 47 | b.HasKey("Id"); 48 | 49 | b.HasIndex("Hostname") 50 | .IsUnique() 51 | .HasFilter("[Hostname] IS NOT NULL"); 52 | 53 | b.HasIndex("MacAddress") 54 | .IsUnique(); 55 | 56 | b.ToTable("Host"); 57 | }); 58 | #pragma warning restore 612, 618 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /WoL/Migrations/SqlServer/20191211184319_CreateInitialSchema.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | using WoL.Data; 9 | 10 | namespace WoL.Migrations 11 | { 12 | [DbContext(typeof(ApplicationDbContext))] 13 | [Migration("20191211184319_CreateInitialSchema")] 14 | partial class CreateInitialSchema 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "3.1.0") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("WoL.Models.Host", b => 25 | { 26 | b.Property("Id") 27 | .ValueGeneratedOnAdd() 28 | .HasColumnType("int") 29 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 30 | 31 | b.Property("Caption") 32 | .IsRequired() 33 | .HasColumnType("nvarchar(255)") 34 | .HasMaxLength(255); 35 | 36 | b.Property("Hostname") 37 | .HasColumnType("nvarchar(255)") 38 | .HasMaxLength(255); 39 | 40 | b.Property("MacAddress") 41 | .IsRequired() 42 | .HasColumnType("varbinary(6)") 43 | .HasMaxLength(6); 44 | 45 | b.HasKey("Id"); 46 | 47 | b.HasIndex("Hostname") 48 | .IsUnique() 49 | .HasFilter("[Hostname] IS NOT NULL"); 50 | 51 | b.HasIndex("MacAddress") 52 | .IsUnique(); 53 | 54 | b.ToTable("Host"); 55 | }); 56 | #pragma warning restore 612, 618 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /WoL/Pages/DeleteHost.razor: -------------------------------------------------------------------------------- 1 | @page "/DeleteHost/{Id:int}" 2 | 3 | @using WoL.Data 4 | @using WoL.Models 5 | @using WoL.Services 6 | @using WoL.Extensions 7 | @using Microsoft.Extensions.Logging 8 | @inject IHostService HostService 9 | @inject IAddressLookupService AddressService 10 | @inject NavigationManager NavigationManager 11 | @inject ILogger L 12 | 13 |
14 |

Delete Host

15 | 16 |
17 | 18 |

19 | Are you sure you want to delete this host? 20 |

21 | 22 | @if (model != null) 23 | { 24 |
25 |
26 |
27 | Title 28 |
29 |
30 | @model.Caption 31 |
32 |
33 | Mac Address 34 |
35 |
36 | @model.GetMacString() 37 |
38 |
39 | Hostname 40 |
41 |
42 | @model.Hostname 43 |
44 |
45 | 46 |
47 | 48 | @if (Deleting) 49 | { 50 | 54 | } 55 | else 56 | { 57 | 58 | } | 59 | 60 | Back to List 61 | 62 |
63 |
64 | } 65 |
66 | 67 | @code { 68 | [Parameter] 69 | public int Id { get; set; } 70 | 71 | Host model; 72 | bool Deleting = false; 73 | 74 | protected override async Task OnParametersSetAsync() 75 | { 76 | model = await HostService.Find(Id); 77 | } 78 | 79 | private async Task Delete() 80 | { 81 | Deleting = true; 82 | await HostService.Delete(Id); 83 | L.HostDeleted(model); 84 | NavigationManager.NavigateTo("/"); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /.github/workflows/docker-build-and-push.yml: -------------------------------------------------------------------------------- 1 | name: Docker Build and Push 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | dockerfile: 7 | required: true 8 | type: string 9 | build-args: 10 | required: false 11 | type: string 12 | push: 13 | required: true 14 | type: boolean 15 | build-ref: 16 | required: false 17 | type: string 18 | 19 | env: 20 | REGISTRY: ghcr.io 21 | IMAGE_NAME: georg-jung/blazorwol # ${{ github.repository }} would be georg-jung/BlazorWoL 22 | 23 | jobs: 24 | build-and-push: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | with: 29 | fetch-depth: 0 # avoid shallow clone so nbgv can do its work. 30 | ref: ${{ inputs.build-ref }} 31 | - uses: dotnet/nbgv@v0.4 32 | id: nbgv 33 | - name: Set up Docker Buildx 34 | uses: docker/setup-buildx-action@v3 35 | - name: Log in to the Container registry 36 | uses: docker/login-action@v3 37 | with: 38 | registry: ${{ env.REGISTRY }} 39 | username: ${{ github.actor }} 40 | password: ${{ secrets.GITHUB_TOKEN }} 41 | - name: Extract metadata (tags, labels) for docker image 42 | id: meta 43 | uses: docker/metadata-action@v5 44 | with: 45 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 46 | # type=semver extracts git tag information 47 | # see https://github.com/marketplace/actions/docker-metadata-action#typesemver 48 | tags: | 49 | type=ref,event=branch 50 | type=semver,pattern={{version}},value=${{ steps.nbgv.outputs.SemVer2 }} 51 | type=semver,pattern={{major}}.{{minor}},value=${{ steps.nbgv.outputs.SemVer2 }} 52 | type=sha 53 | type=semver,pattern={{version}},value=${{ inputs.build-ref }} 54 | type=semver,pattern={{major}}.{{minor}},value=${{ inputs.build-ref }} 55 | type=raw,value=latest 56 | - name: Build and push container image 57 | uses: docker/build-push-action@v6 58 | with: 59 | context: . 60 | file: ${{ inputs.dockerfile }} 61 | build-args: ${{ inputs.build-args }} 62 | builder: ${{ steps.buildx.outputs.name }} 63 | push: ${{ inputs.push }} 64 | tags: ${{ steps.meta.outputs.tags }} 65 | labels: ${{ steps.meta.outputs.labels }} 66 | cache-from: type=local,src=/tmp/.buildx-cache 67 | cache-to: type=local,dest=/tmp/.buildx-cache 68 | -------------------------------------------------------------------------------- /WoL/Extensions/TaskExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace WoL.Extensions 8 | { 9 | public static class TaskExtensions 10 | { 11 | // see https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md#always-dispose-cancellationtokensources-used-for-timeouts 12 | public static async Task TimeoutAfter(this Task task, TimeSpan timeout) 13 | { 14 | if (task == null) 15 | throw new ArgumentNullException(nameof(task)); 16 | using var cts = new CancellationTokenSource(); 17 | var delayTask = Task.Delay(timeout, cts.Token); 18 | 19 | var resultTask = await Task.WhenAny(task, delayTask).ConfigureAwait(false); 20 | if (resultTask == delayTask) 21 | { 22 | // Operation cancelled 23 | throw new OperationCanceledException(); 24 | } 25 | else 26 | { 27 | // Cancel the timer task so that it does not fire 28 | cts.Cancel(); 29 | } 30 | 31 | await task.ConfigureAwait(false); 32 | } 33 | 34 | // see https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md#always-dispose-cancellationtokensources-used-for-timeouts 35 | public static async Task WithCancellation(this Task task, CancellationToken cancellationToken) 36 | { 37 | if (task == null) 38 | throw new ArgumentNullException(nameof(task)); 39 | var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); 40 | 41 | // This disposes the registration as soon as one of the tasks trigger 42 | await using (cancellationToken.Register(state => 43 | { 44 | ((TaskCompletionSource)state)!.TrySetResult(null); 45 | }, 46 | tcs)) 47 | { 48 | var resultTask = await Task.WhenAny(task, tcs.Task).ConfigureAwait(false); 49 | if (resultTask == tcs.Task) 50 | { 51 | // Operation cancelled 52 | throw new OperationCanceledException(cancellationToken); 53 | } 54 | 55 | return await task.ConfigureAwait(false); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | # Ensure shell scripts use LF line endings (linux only accepts LF) 7 | *.sh eol=lf 8 | *.ps1 eol=lf 9 | 10 | ############################################################################### 11 | # Set default behavior for command prompt diff. 12 | # 13 | # This is need for earlier builds of msysgit that does not have it on by 14 | # default for csharp files. 15 | # Note: This is only used by command line 16 | ############################################################################### 17 | #*.cs diff=csharp 18 | 19 | ############################################################################### 20 | # Set the merge driver for project and solution files 21 | # 22 | # Merging from the command prompt will add diff markers to the files if there 23 | # are conflicts (Merging from VS is not affected by the settings below, in VS 24 | # the diff markers are never inserted). Diff markers may cause the following 25 | # file extensions to fail to load in VS. An alternative would be to treat 26 | # these files as binary and thus will always conflict and require user 27 | # intervention with every merge. To do so, just uncomment the entries below 28 | ############################################################################### 29 | #*.sln merge=binary 30 | #*.csproj merge=binary 31 | #*.vbproj merge=binary 32 | #*.vcxproj merge=binary 33 | #*.vcproj merge=binary 34 | #*.dbproj merge=binary 35 | #*.fsproj merge=binary 36 | #*.lsproj merge=binary 37 | #*.wixproj merge=binary 38 | #*.modelproj merge=binary 39 | #*.sqlproj merge=binary 40 | #*.wwaproj merge=binary 41 | 42 | ############################################################################### 43 | # behavior for image files 44 | # 45 | # image files are treated as binary by default. 46 | ############################################################################### 47 | #*.jpg binary 48 | #*.png binary 49 | #*.gif binary 50 | 51 | ############################################################################### 52 | # diff behavior for common document formats 53 | # 54 | # Convert binary document formats to text before diffing them. This feature 55 | # is only available from the command line. Turn it on by uncommenting the 56 | # entries below. 57 | ############################################################################### 58 | #*.doc diff=astextplain 59 | #*.DOC diff=astextplain 60 | #*.docx diff=astextplain 61 | #*.DOCX diff=astextplain 62 | #*.dot diff=astextplain 63 | #*.DOT diff=astextplain 64 | #*.pdf diff=astextplain 65 | #*.PDF diff=astextplain 66 | #*.rtf diff=astextplain 67 | #*.RTF diff=astextplain 68 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # ASP.NET 2 | # Build and test ASP.NET projects. 3 | # Add steps that publish symbols, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/apps/aspnet/build-aspnet-4 5 | 6 | trigger: 7 | - master 8 | 9 | pool: 10 | vmImage: 'windows-latest' 11 | 12 | variables: 13 | solution: './WoL.sln' 14 | 15 | steps: 16 | - task: DotNetCoreCLI@2 17 | inputs: 18 | command: custom 19 | custom: tool 20 | arguments: install --tool-path . nbgv 21 | displayName: Install NBGV tool 22 | 23 | - script: nbgv cloud -c -a 24 | displayName: Set Version / NBGV 25 | 26 | - script: dotnet restore "$(solution)" 27 | displayName: dotnet restore 28 | 29 | - task: DotNetCoreCLI@2 30 | inputs: 31 | command: 'test' 32 | arguments: '-c Release --no-restore' 33 | 34 | # FDD 35 | 36 | - task: DotNetCoreCLI@2 37 | inputs: 38 | command: 'publish' 39 | modifyOutputPath: false 40 | publishWebProjects: true 41 | arguments: '-c Release --no-restore --output $(Build.ArtifactStagingDirectory)/fdd' 42 | 43 | - task: PublishPipelineArtifact@1 44 | inputs: 45 | targetPath: '$(Build.ArtifactStagingDirectory)/fdd' 46 | artifact: 'fdd' 47 | 48 | # Win x86 (x64 just uses more space and ram) 49 | 50 | - task: DotNetCoreCLI@2 51 | inputs: 52 | command: 'publish' 53 | modifyOutputPath: false 54 | publishWebProjects: true 55 | arguments: '-c Release -r win-x86 --self-contained /p:PublishSingleFile=true --output $(Build.ArtifactStagingDirectory)/win-x86' 56 | 57 | - task: PublishPipelineArtifact@1 58 | inputs: 59 | targetPath: '$(Build.ArtifactStagingDirectory)/win-x86' 60 | artifact: 'win-x86' 61 | 62 | # Win x86 non-single-file for hosting in IIS 63 | 64 | - task: DotNetCoreCLI@2 65 | inputs: 66 | command: 'publish' 67 | modifyOutputPath: false 68 | publishWebProjects: true 69 | arguments: '-c Release -r win-x86 --self-contained --output $(Build.ArtifactStagingDirectory)/win-x86-iis' 70 | 71 | - task: PublishPipelineArtifact@1 72 | inputs: 73 | targetPath: '$(Build.ArtifactStagingDirectory)/win-x86-iis' 74 | artifact: 'win-x86-iis' 75 | 76 | # Win ARM (IoT Core / Raspberry PI) 77 | 78 | - task: DotNetCoreCLI@2 79 | inputs: 80 | command: 'publish' 81 | modifyOutputPath: false 82 | publishWebProjects: true 83 | arguments: '-c Release -r win-arm --self-contained /p:PublishSingleFile=true --output $(Build.ArtifactStagingDirectory)/win-arm' 84 | 85 | - task: PublishPipelineArtifact@1 86 | inputs: 87 | targetPath: '$(Build.ArtifactStagingDirectory)/win-arm' 88 | artifact: 'win-arm' 89 | 90 | # Linux x64 91 | 92 | - task: DotNetCoreCLI@2 93 | inputs: 94 | command: 'publish' 95 | modifyOutputPath: false 96 | publishWebProjects: true 97 | arguments: '-c Release -r linux-x64 --self-contained /p:PublishSingleFile=true --output $(Build.ArtifactStagingDirectory)/linux-x64' 98 | 99 | - task: PublishPipelineArtifact@1 100 | inputs: 101 | targetPath: '$(Build.ArtifactStagingDirectory)/linux-x64' 102 | artifact: 'linux-x64' 103 | -------------------------------------------------------------------------------- /WoL/Pages/Hosts.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 | @using WoL.Data 4 | @using WoL.Models.ViewModels 5 | @using WoL.Services 6 | @using WoL.Extensions 7 | @using WoL.Components 8 | @inject IHostService HostService 9 | @inject IPingService PingService 10 | 11 |
12 |

Wake-on-LAN

13 | 14 |
15 | 16 | @if (hosts == null) 17 | { 18 |

Lade Hosts...

19 | } 20 | else 21 | { 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | @foreach (var host in hosts) 38 | { 39 | 40 | 41 | 46 | 47 | 48 | 52 | 53 | } 54 | 55 |
Title 27 | 28 | Status 29 | 30 | MACHostname
@host.Caption 42 | 43 | 44 | 45 | @host.MacAddress@host.Hostname 49 | Wake | 50 | Delete 51 |
56 | @if (hosts.Count == 0) 57 | { 58 |

59 | Currently, there are no saved hosts. Click here to add one. 60 |

61 | } 62 | } 63 |
64 | 65 | @code { 66 | private List hosts; 67 | 68 | protected override async Task OnInitializedAsync() 69 | { 70 | hosts = (await HostService.GetAll()).Select(h => new HostViewModel(h)).ToList(); 71 | 72 | var pingable = hosts.Where(CanPing); 73 | 74 | var _ = Task.WhenAll(pingable.Select(h => PingAndSetStatus(h))); 75 | } 76 | 77 | private bool CanPing(HostViewModel host) => !string.IsNullOrEmpty(host.Hostname); 78 | 79 | private async Task RePingAll() 80 | { 81 | var pingable = hosts.Where(CanPing); 82 | 83 | var tasks = new List(); 84 | foreach (var host in pingable) 85 | { 86 | host.Status = HostViewModel.HostStatus.Loading; 87 | tasks.Add(PingAndSetStatus(host)); 88 | } 89 | base.StateHasChanged(); 90 | await Task.WhenAll(tasks); 91 | } 92 | 93 | private async Task RePing(HostViewModel h) 94 | { 95 | if (!CanPing(h)) 96 | return; 97 | h.Status = HostViewModel.HostStatus.Loading; 98 | base.StateHasChanged(); 99 | await PingAndSetStatus(h); 100 | } 101 | 102 | private async Task PingAndSetStatus(HostViewModel h) 103 | { 104 | var timeout = 2500; 105 | var res = await PingService.IsReachable(h.Hostname, TimeSpan.FromMilliseconds(timeout)); 106 | h.Status = res.ToHostStatus(); 107 | base.StateHasChanged(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /WoL/wwwroot/css/open-iconic/README.md: -------------------------------------------------------------------------------- 1 | [Open Iconic v1.1.1](http://useiconic.com/open) 2 | =========== 3 | 4 | ### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons) 5 | 6 | 7 | 8 | ## What's in Open Iconic? 9 | 10 | * 223 icons designed to be legible down to 8 pixels 11 | * Super-light SVG files - 61.8 for the entire set 12 | * SVG sprite—the modern replacement for icon fonts 13 | * Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats 14 | * Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats 15 | * PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. 16 | 17 | 18 | ## Getting Started 19 | 20 | #### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections. 21 | 22 | ### General Usage 23 | 24 | #### Using Open Iconic's SVGs 25 | 26 | We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). 27 | 28 | ``` 29 | icon name 30 | ``` 31 | 32 | #### Using Open Iconic's SVG Sprite 33 | 34 | Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. 35 | 36 | Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* 37 | 38 | ``` 39 | 40 | 41 | 42 | ``` 43 | 44 | Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. 45 | 46 | ``` 47 | .icon { 48 | width: 16px; 49 | height: 16px; 50 | } 51 | ``` 52 | 53 | Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. 54 | 55 | ``` 56 | .icon-account-login { 57 | fill: #f00; 58 | } 59 | ``` 60 | 61 | To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). 62 | 63 | #### Using Open Iconic's Icon Font... 64 | 65 | 66 | ##### …with Bootstrap 67 | 68 | You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` 69 | 70 | 71 | ``` 72 | 73 | ``` 74 | 75 | 76 | ``` 77 | 78 | ``` 79 | 80 | ##### …with Foundation 81 | 82 | You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` 83 | 84 | ``` 85 | 86 | ``` 87 | 88 | 89 | ``` 90 | 91 | ``` 92 | 93 | ##### …on its own 94 | 95 | You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` 96 | 97 | ``` 98 | 99 | ``` 100 | 101 | ``` 102 | 103 | ``` 104 | 105 | 106 | ## License 107 | 108 | ### Icons 109 | 110 | All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). 111 | 112 | ### Fonts 113 | 114 | All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). 115 | -------------------------------------------------------------------------------- /WoL/Data/HostService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Data.SqlClient; 2 | using Microsoft.EntityFrameworkCore; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text.RegularExpressions; 7 | using System.Threading.Tasks; 8 | using Microsoft.Data.Sqlite; 9 | using WoL.Models; 10 | 11 | namespace WoL.Data 12 | { 13 | public class HostService : IHostService 14 | { 15 | private static readonly Regex tsqlParseDuplicateValue = new Regex(@"The duplicate key value is \(([^)]+)\)"); 16 | private static readonly Regex tsqlParseIndexName = new Regex(@"with unique index '([^']+)'\."); 17 | // this makes assumptions about the names of the indices which is kind of bad 18 | // on the other hand it sticks to ef's index naming conventions 19 | private static readonly Regex tsqlParseIdxField = new Regex(@"_([^_]+)$"); 20 | 21 | private static readonly Regex sqliteParseParseIdxField = new Regex(@"UNIQUE constraint failed: ([^']+)"); 22 | 23 | private readonly IDbContextFactory _contextFactory; 24 | 25 | public HostService(IDbContextFactory contextFactory) 26 | { 27 | _contextFactory = contextFactory; 28 | } 29 | 30 | public async Task> GetAll() 31 | { 32 | using var ctx = _contextFactory.CreateDbContext(); 33 | return await ctx.Hosts.AsNoTracking().ToListAsync().ConfigureAwait(false); 34 | } 35 | 36 | public async Task Add(Host host) 37 | { 38 | using var ctx = _contextFactory.CreateDbContext(); 39 | try 40 | { 41 | await ctx.Hosts.AddAsync(host).ConfigureAwait(false); 42 | await ctx.SaveChangesAsync().ConfigureAwait(false); 43 | } 44 | catch (DbUpdateException dbue) 45 | // handle tsql 46 | // see https://docs.microsoft.com/en-us/sql/relational-databases/errors-events/database-engine-events-and-errors 47 | // i.e. "Cannot insert duplicate key row in object 'dbo.Host' with unique index 'IX_Host_Hostname'. The duplicate key value is (georg-nuc). The statement has been terminated." 48 | when (dbue.InnerException is SqlException sqlEx && 49 | (sqlEx.Number == 2601 || sqlEx.Number == 2627)) 50 | { 51 | var msg = sqlEx.Message; 52 | var duplVal = tsqlParseDuplicateValue.Match(msg).Groups[1].Value; 53 | var idxName = tsqlParseIndexName.Match(msg).Groups[1].Value; 54 | var fieldName = tsqlParseIdxField.Match(idxName).Groups[1].Value; 55 | throw new IHostService.DuplicateEntryException(fieldName, duplVal, nameof(host), dbue); 56 | } 57 | catch (DbUpdateException dbue) 58 | // handle sqlite 59 | // see https://sqlite.org/rescode.html 60 | // i.e. "SQLite Error 19: 'UNIQUE constraint failed: Host.MacAddress'." 61 | when (dbue.InnerException is SqliteException sqlEx && 62 | (sqlEx.SqliteErrorCode == 19 || sqlEx.SqliteExtendedErrorCode == 2067) && 63 | sqliteParseParseIdxField.Match(sqlEx.Message) is Match m && 64 | m.Success) 65 | { 66 | var fieldName = m.Groups[1].Value; 67 | if (fieldName.StartsWith("host.", StringComparison.OrdinalIgnoreCase)) 68 | fieldName = fieldName.Substring(5); 69 | throw new IHostService.DuplicateEntryException(fieldName, nameof(host), dbue); 70 | } 71 | } 72 | 73 | public async Task Delete(int id) 74 | { 75 | using var ctx = _contextFactory.CreateDbContext(); 76 | var host = await ctx.Hosts.FindAsync(id).ConfigureAwait(false); 77 | ctx.Hosts.Remove(host); 78 | await ctx.SaveChangesAsync().ConfigureAwait(false); 79 | } 80 | 81 | public async Task Find(int id) 82 | { 83 | using var ctx = _contextFactory.CreateDbContext(); 84 | return await ctx.Hosts.AsNoTracking().FirstAsync(h => h.Id == id).ConfigureAwait(false); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /WoL/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); 2 | 3 | html, body { 4 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 5 | } 6 | 7 | a, .btn-link { 8 | color: #0366d6; 9 | } 10 | 11 | .btn-primary { 12 | color: #fff; 13 | background-color: #1b6ec2; 14 | border-color: #1861ac; 15 | } 16 | 17 | app { 18 | position: relative; 19 | display: flex; 20 | flex-direction: column; 21 | } 22 | 23 | .top-row { 24 | height: 3.5rem; 25 | display: flex; 26 | align-items: center; 27 | } 28 | 29 | .main { 30 | flex: 1; 31 | } 32 | 33 | .main .top-row { 34 | background-color: #f7f7f7; 35 | border-bottom: 1px solid #d6d5d5; 36 | justify-content: flex-end; 37 | } 38 | 39 | .main .top-row > a, .main .top-row .btn-link { 40 | white-space: nowrap; 41 | } 42 | 43 | .main .top-row a:first-child { 44 | overflow: hidden; 45 | text-overflow: ellipsis; 46 | } 47 | 48 | .sidebar { 49 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 50 | } 51 | 52 | .sidebar .top-row { 53 | background-color: rgba(0,0,0,0.4); 54 | } 55 | 56 | .sidebar .navbar-brand { 57 | font-size: 1.1rem; 58 | } 59 | 60 | .sidebar .oi { 61 | width: 2rem; 62 | font-size: 1.1rem; 63 | vertical-align: text-top; 64 | top: -2px; 65 | } 66 | 67 | .sidebar .nav-item { 68 | font-size: 0.9rem; 69 | padding-bottom: 0.5rem; 70 | } 71 | 72 | .sidebar .nav-item:first-of-type { 73 | padding-top: 1rem; 74 | } 75 | 76 | .sidebar .nav-item:last-of-type { 77 | padding-bottom: 1rem; 78 | } 79 | 80 | .sidebar .nav-item a { 81 | color: #d7d7d7; 82 | border-radius: 4px; 83 | height: 3rem; 84 | display: flex; 85 | align-items: center; 86 | line-height: 3rem; 87 | } 88 | 89 | .sidebar .nav-item a.active { 90 | background-color: rgba(255,255,255,0.25); 91 | color: white; 92 | } 93 | 94 | .sidebar .nav-item a:hover { 95 | background-color: rgba(255,255,255,0.1); 96 | color: white; 97 | } 98 | 99 | .content { 100 | padding-top: 1.1rem; 101 | } 102 | 103 | .navbar-toggler { 104 | background-color: rgba(255, 255, 255, 0.1); 105 | } 106 | 107 | .valid.modified:not([type=checkbox]) { 108 | outline: 1px solid #26b050; 109 | } 110 | 111 | .invalid { 112 | outline: 1px solid red; 113 | } 114 | 115 | .validation-message { 116 | color: red; 117 | } 118 | 119 | #blazor-error-ui { 120 | background: lightyellow; 121 | bottom: 0; 122 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 123 | display: none; 124 | left: 0; 125 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 126 | position: fixed; 127 | width: 100%; 128 | z-index: 1000; 129 | } 130 | 131 | #blazor-error-ui .dismiss { 132 | cursor: pointer; 133 | position: absolute; 134 | right: 0.75rem; 135 | top: 0.5rem; 136 | } 137 | 138 | @media (max-width: 767.98px) { 139 | .main .top-row:not(.auth) { 140 | display: none; 141 | } 142 | 143 | .main .top-row.auth { 144 | justify-content: space-between; 145 | } 146 | 147 | .main .top-row a, .main .top-row .btn-link { 148 | margin-left: 0; 149 | } 150 | } 151 | 152 | @media (min-width: 768px) { 153 | app { 154 | flex-direction: row; 155 | } 156 | 157 | .sidebar { 158 | width: 250px; 159 | height: 100vh; 160 | position: sticky; 161 | top: 0; 162 | } 163 | 164 | .main .top-row { 165 | position: sticky; 166 | top: 0; 167 | } 168 | 169 | .main > div { 170 | padding-left: 2rem !important; 171 | padding-right: 1.5rem !important; 172 | } 173 | 174 | .navbar-toggler { 175 | display: none; 176 | } 177 | 178 | .sidebar .collapse { 179 | /* Never collapse the sidebar for wide screens */ 180 | display: block; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /WoL/wwwroot/css/open-iconic/FONT-LICENSE: -------------------------------------------------------------------------------- 1 | SIL OPEN FONT LICENSE Version 1.1 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | PREAMBLE 6 | The goals of the Open Font License (OFL) are to stimulate worldwide 7 | development of collaborative font projects, to support the font creation 8 | efforts of academic and linguistic communities, and to provide a free and 9 | open framework in which fonts may be shared and improved in partnership 10 | with others. 11 | 12 | The OFL allows the licensed fonts to be used, studied, modified and 13 | redistributed freely as long as they are not sold by themselves. The 14 | fonts, including any derivative works, can be bundled, embedded, 15 | redistributed and/or sold with any software provided that any reserved 16 | names are not used by derivative works. The fonts and derivatives, 17 | however, cannot be released under any other type of license. The 18 | requirement for fonts to remain under this license does not apply 19 | to any document created using the fonts or their derivatives. 20 | 21 | DEFINITIONS 22 | "Font Software" refers to the set of files released by the Copyright 23 | Holder(s) under this license and clearly marked as such. This may 24 | include source files, build scripts and documentation. 25 | 26 | "Reserved Font Name" refers to any names specified as such after the 27 | copyright statement(s). 28 | 29 | "Original Version" refers to the collection of Font Software components as 30 | distributed by the Copyright Holder(s). 31 | 32 | "Modified Version" refers to any derivative made by adding to, deleting, 33 | or substituting -- in part or in whole -- any of the components of the 34 | Original Version, by changing formats or by porting the Font Software to a 35 | new environment. 36 | 37 | "Author" refers to any designer, engineer, programmer, technical 38 | writer or other person who contributed to the Font Software. 39 | 40 | PERMISSION & CONDITIONS 41 | Permission is hereby granted, free of charge, to any person obtaining 42 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 43 | redistribute, and sell modified and unmodified copies of the Font 44 | Software, subject to the following conditions: 45 | 46 | 1) Neither the Font Software nor any of its individual components, 47 | in Original or Modified Versions, may be sold by itself. 48 | 49 | 2) Original or Modified Versions of the Font Software may be bundled, 50 | redistributed and/or sold with any software, provided that each copy 51 | contains the above copyright notice and this license. These can be 52 | included either as stand-alone text files, human-readable headers or 53 | in the appropriate machine-readable metadata fields within text or 54 | binary files as long as those fields can be easily viewed by the user. 55 | 56 | 3) No Modified Version of the Font Software may use the Reserved Font 57 | Name(s) unless explicit written permission is granted by the corresponding 58 | Copyright Holder. This restriction only applies to the primary font name as 59 | presented to the users. 60 | 61 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 62 | Software shall not be used to promote, endorse or advertise any 63 | Modified Version, except to acknowledge the contribution(s) of the 64 | Copyright Holder(s) and the Author(s) or with their explicit written 65 | permission. 66 | 67 | 5) The Font Software, modified or unmodified, in part or in whole, 68 | must be distributed entirely under this license, and must not be 69 | distributed under any other license. The requirement for fonts to 70 | remain under this license does not apply to any document created 71 | using the Font Software. 72 | 73 | TERMINATION 74 | This license becomes null and void if any of the above conditions are 75 | not met. 76 | 77 | DISCLAIMER 78 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 79 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 80 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 81 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 82 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 83 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 84 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 85 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 86 | OTHER DEALINGS IN THE FONT SOFTWARE. 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blazor Wake-on-LAN 2 | 3 | [![CI](https://github.com/georg-jung/BlazorWoL/actions/workflows/ci.yml/badge.svg)](https://github.com/georg-jung/BlazorWoL/actions/workflows/ci.yml) 4 | 5 | This is a [Wake-on-LAN](https://en.wikipedia.org/wiki/Wake-on-LAN) app for your network, written in server-side blazor. I developed it for internal use at my workplace and because I wanted to build a small, limited-scope but fully-working and done-right blazor app. [Getting started](#getting-started) is as easy as downloading and running. Feel free to improve/fork/PR this if you think I could have done anything better. 6 | 7 | ## Getting Started 8 | 9 | ### With Docker 10 | 11 | You can start a BlazorWoL docker container like this: 12 | 13 | ```bash 14 | docker run -e "ConnectionStrings__SqliteConnection=Data Source=/blazorwol/db.sqlite" -v /var/blazorwol:/blazorwol --network host --name blazorwol ghcr.io/georg-jung/blazorwol:latest 15 | ``` 16 | 17 | Please note that this does not work on Windows, even with WSL, as docker host networking is required. 18 | 19 | ### By downloading binaries 20 | 21 | 1. [Download](#download) and unzip the [latest release](https://github.com/georg-jung/BlazorWoL/releases/latest/). 22 | 2. Run the application by starting `WoL.exe` 23 | 3. Open the shown location in a browser (probably `localhost:5000`) and add your first host. 24 | 25 | You can add the application to IIS for more serious hosting. You can set up T-SQL as a backend for backup etc.. See the [`appsettings.json` file](WoL/appsettings.json) for details. If you host this in IIS you might want to [configure it to be always running](https://serverfault.com/a/823531), otherwise the first request after some idle time will take seconds. 26 | 27 | ## Download 28 | 29 | The platform-specific releases have no prerequisits. Just unpack and double-click. The framework dependend release is smaller and portable but requires [.Net 7 Runtime to be installed](https://dotnet.microsoft.com/download/dotnet-core). 30 | 31 | * [Windows x86](https://github.com/georg-jung/BlazorWoL/releases/latest/download/win-x86.zip) 32 | * This app does not take advantage of 64bit, so I chose to deploy x86 due to the smaller footprint. 33 | * [Windows x86 IIS](https://github.com/georg-jung/BlazorWoL/releases/latest/download/win-x86-iis.zip) 34 | * All files listed here except from the *Runtime Dependent* one are self contained. Thus, you don't need to have the .Net runtime installed. They are published as single file executables too (you get one ~85mb exe file instead of hundreds of smaller files). To be able to host a .Net application in IIS it must not be published as a single file. So, if you want to host BlazorWoL in IIS, you may choose this download. 35 | * [Windows ARM](https://github.com/georg-jung/BlazorWoL/releases/latest/download/win-arm.zip) 36 | * [Linux x64](https://github.com/georg-jung/BlazorWoL/releases/latest/download/linux-x64.zip) 37 | * [Runtime Dependent (.Net 7.0)](https://github.com/georg-jung/BlazorWoL/releases/latest/download/fdd.zip) *previously known as framework dependent* 38 | 39 | ## Features 40 | 41 | ![Screenshot](screenshot-01-index.png) 42 | 43 | * Wake arbitrary hosts on the network of the server where this is hosted via [Magic Packet](https://superuser.com/a/1066637) 44 | * Add new hosts via the webinterface using either their hostname or their mac address. 45 | * Detect the online status of saved hosts. To determine, they are at the same time [ping](https://en.wikipedia.org/wiki/Ping_(networking_utility))ed and we try to establish a TCP connection on port 3389. This port [is used](https://serverfault.com/a/12006) by the Microsoft Remote Desktop Protocol. This way, we can work with hosts that don't answer normal pings. 46 | * Delete existing hosts from the list. 47 | * When waking a host, the application repeatedly tries to reach the host and updates you about the status. You see immediately when the host finished booting so that you can connect via ssh/RDP/etc.. 48 | 49 | ## Showcase 50 | 51 | This application uses the following techniques and might be suitable as a simple but full-fledged example of how they work: 52 | 53 | * [Blazor](https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor) 54 | * Components and Pages in Razor 55 | * UI-Server-interaction which would typically require AJAX/writing JavaScript. See the `Wake` page and the *Status* column of the index page. 56 | * .Net 7 57 | * [Entity Framework Core](https://docs.microsoft.com/en-us/ef/core/) Code First 58 | * Automatic Migrations 59 | * Supports T-SQL and SQLite, selection by connection string in `appsettings.json` 60 | * Dependency Injection using [`Microsoft.Extensions.DependencyInjection`](https://www.nuget.org/packages/Microsoft.Extensions.DependencyInjection/) 61 | * Continuous Integration and Continuous Deployment using GitHub Actions. 62 | * Docker image creation and push 63 | * Zipped executables in GitHub Release 64 | * Dependency updates are partly automated using [Dependabot](https://dependabot.com/). 65 | * Usage of Microsoft.Extensions.Logging with event ids and custom parameters. 66 | * Usage of Application Insights to keep track of logged application events in production. 67 | * Git height based versioning using [NBGV](https://github.com/dotnet/nerdbank.gitversioning). 68 | 69 | ## Known Limitations 70 | 71 | * Adding hosts by hostname is currently impossible on platforms except Windows and Linux, as there is no appropriate ARP API. 72 | -------------------------------------------------------------------------------- /WoL/Pages/Wake.razor: -------------------------------------------------------------------------------- 1 | @page "/Wake/{Id:int}" 2 | 3 | @using WoL.Data 4 | @using WoL.Models 5 | @using WoL.Models.ViewModels 6 | @using WoL.Services 7 | @using WoL.Extensions 8 | @using WoL.Components 9 | @using Microsoft.Extensions.Logging 10 | @inject IHostService HostService 11 | @inject IWakeService WakeService 12 | @inject IPingService PingService 13 | @inject ILogger L 14 | 15 |
16 |

Wake

17 | 18 |
19 | 20 | @if (!string.IsNullOrEmpty(Alert)) 21 | { 22 |
23 | @if (AlertSpinner) 24 | { 25 | 26 | } 27 | @Alert 28 |
29 | } 30 | 31 | 32 | @if (model != null) 33 | { 34 |
35 |
36 |
37 |
38 | Title 39 |
40 |
41 | @model.Caption 42 |
43 |
44 | Mac Address 45 |
46 |
47 | @model.MacAddress 48 |
49 |
50 | Hostname 51 |
52 |
53 | @model.Hostname 54 |
55 |
56 |
57 |
58 | 59 | 60 |

61 | Host Status 62 |

63 | @if (IsCheckingStatus) 64 | { 65 | 66 | } 67 |
68 |
69 |
70 |
71 | Status 72 |
73 |
74 | 75 |
76 |
77 | Last Checked 78 |
79 |
80 | @(LastStatusCheck?.ToString("HH:mm:ss") ?? "-") 81 |
82 |
83 | Tries Count 84 |
85 |
86 | @PingTries 87 |
88 |
89 |
90 |
91 | } 92 | 93 | 94 | Back to List 95 | 96 |
97 | 98 | @code { 99 | [Parameter] 100 | public int Id { get; set; } 101 | 102 | Host host; 103 | HostViewModel model; 104 | 105 | const int MaxPingTries = 400; 106 | 107 | string Alert { get; set; } = null; 108 | string AlertClass { get; set; } = null; 109 | bool AlertSpinner { get; set; } = false; 110 | bool IsCheckingStatus { get; set; } = false; 111 | int PingTries { get; set; } = 0; 112 | DateTime? LastStatusCheck { get; set; } = null; 113 | HostViewModel.HostStatus HostStatus { get; set; } 114 | 115 | private string Info 116 | { 117 | set 118 | { 119 | AlertClass = "alert-info"; 120 | Alert = value; 121 | } 122 | } 123 | 124 | private string Success 125 | { 126 | set 127 | { 128 | AlertClass = "alert-success"; 129 | Alert = value; 130 | } 131 | } 132 | 133 | private string Danger 134 | { 135 | set 136 | { 137 | AlertClass = "alert-danger"; 138 | Alert = value; 139 | } 140 | } 141 | 142 | protected override async Task OnParametersSetAsync() 143 | { 144 | await StartBackgroundWork(); 145 | } 146 | 147 | private async Task StartBackgroundWork() 148 | { 149 | host = await HostService.Find(Id); 150 | if (host == null) 151 | { 152 | Danger = "This host does not exist."; 153 | return; 154 | } 155 | L.WakeHost(host); 156 | model = new HostViewModel(host); 157 | AlertSpinner = true; 158 | Info = "Sending wake-up packet..."; 159 | base.StateHasChanged(); 160 | await WakeService.Wake(host.MacAddress); 161 | AlertSpinner = false; 162 | Success = "Wake-up packet sent."; 163 | var _ = LoopPinging(); 164 | } 165 | 166 | private async Task LoopPinging() 167 | { 168 | const int minInterval = 1500; 169 | IsCheckingStatus = true; 170 | while (PingTries < MaxPingTries && (model.Status == HostViewModel.HostStatus.Unreachable || model.Status == HostViewModel.HostStatus.Loading)) 171 | { 172 | var msecSince = Convert.ToInt32((DateTime.Now - LastStatusCheck)?.TotalMilliseconds ?? minInterval); 173 | if (msecSince < minInterval) 174 | await Task.Delay(minInterval - msecSince); 175 | await PingAndSetStatus(); 176 | } 177 | L.WakeHostFinalStatus(host, PingTries, model.Status); 178 | IsCheckingStatus = false; 179 | base.StateHasChanged(); 180 | } 181 | 182 | private async Task PingAndSetStatus() 183 | { 184 | const int timeout = 2500; 185 | LastStatusCheck = DateTime.Now; 186 | var res = await PingService.IsReachable(host.Hostname, TimeSpan.FromMilliseconds(timeout)); 187 | model.Status = res.ToHostStatus(); 188 | PingTries++; 189 | base.StateHasChanged(); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /WoL/Extensions/LoggingExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Hosting; 6 | using Microsoft.Extensions.Logging; 7 | using WoL.Data; 8 | using WoL.Models.ViewModels; 9 | 10 | namespace WoL.Extensions 11 | { 12 | // to be accessible from razor views this needs to be public 13 | public static class LoggingExtensions 14 | { 15 | // this class is build like this one from the framework: 16 | // https://github.com/dotnet/aspnetcore/blob/f2e6e6ff334176540ef0b3291122e359c2106d1a/src/Security/Authentication/Core/src/LoggingExtensions.cs 17 | 18 | private static readonly Action addHostUnknownException; 19 | private static readonly Action addHostDuplicateEntryException; 20 | private static readonly Action getHostPlatformNotSupportedException; 21 | private static readonly Action getHostUnknownException; 22 | private static readonly Action getHostHostNotFound; 23 | private static readonly Action hostAdded; 24 | private static readonly Action wakeHost; 25 | private static readonly Action wakeHostFinalStatus; 26 | private static readonly Action hostDeleted; 27 | 28 | static LoggingExtensions() 29 | { 30 | addHostUnknownException = LoggerMessage.Define( 31 | LogLevel.Error, 32 | new EventId(1, "AddHostUnknownException"), 33 | "An unknown exception was thrown when adding a host to the HostService." 34 | ); 35 | 36 | addHostDuplicateEntryException = LoggerMessage.Define( 37 | LogLevel.Information, 38 | new EventId(2, "AddHostDuplicateEntryException"), 39 | "The host {Host} could not be added as one of it's entries that should be unique weren't." 40 | ); 41 | 42 | getHostPlatformNotSupportedException = LoggerMessage.Define( 43 | LogLevel.Warning, 44 | new EventId(3, "GetHostPlatformNotSupportedException"), 45 | "A PlatformNotSupported exception was thrown when trying to get a host for the user's input." 46 | ); 47 | 48 | getHostUnknownException = LoggerMessage.Define( 49 | LogLevel.Warning, 50 | new EventId(4, "GetHostUnknownException"), 51 | "An unknown exception was thrown while resolving the host '{CaptionInput}' {HostNameInput} / {MacAddressInput}." 52 | ); 53 | 54 | getHostHostNotFound = LoggerMessage.Define( 55 | LogLevel.Information, 56 | new EventId(5, "GetHostUnknownException"), 57 | "While resolving '{CaptionInput}' {HostNameInput} / {MacAddressInput} no host could be found." 58 | ); 59 | 60 | hostAdded = LoggerMessage.Define( 61 | LogLevel.Information, 62 | new EventId(10, "HostAdded"), 63 | "Host {Host} was added successfully." 64 | ); 65 | 66 | wakeHost = LoggerMessage.Define( 67 | LogLevel.Information, 68 | new EventId(20, "WakeHost"), 69 | "Waking {Host}..." 70 | ); 71 | 72 | wakeHostFinalStatus = LoggerMessage.Define( 73 | LogLevel.Information, 74 | new EventId(21, "WakeHostFinalStatus"), 75 | "Final HostStatus of {Host} after {PingTries} ping tries is {HostStatus}." 76 | ); 77 | 78 | hostDeleted = LoggerMessage.Define( 79 | LogLevel.Information, 80 | new EventId(30, "HostDeleted"), 81 | "Host {Host} deleted." 82 | ); 83 | } 84 | 85 | public static void AddHostUnknownException(this ILogger logger, Exception exc) 86 | { 87 | addHostUnknownException(logger, exc); 88 | } 89 | 90 | public static void AddHostDuplicateEntryException(this ILogger logger, Models.Host value, IHostService.DuplicateEntryException exc) 91 | { 92 | addHostDuplicateEntryException(logger, value, exc); 93 | } 94 | 95 | public static void GetHostPlatformNotSupportedException(this ILogger logger, PlatformNotSupportedException exc) 96 | { 97 | getHostPlatformNotSupportedException(logger, exc); 98 | } 99 | 100 | public static void GetHostUnknownException(this ILogger logger, string captionInput, string hostNameInput, string macAddressInput, Exception exc) 101 | { 102 | getHostUnknownException(logger, captionInput, hostNameInput, macAddressInput, exc); 103 | } 104 | 105 | public static void GetHostHostNotFound(this ILogger logger, string captionInput, string hostNameInput, string macAddressInput, Exception exc) 106 | { 107 | getHostHostNotFound(logger, captionInput, hostNameInput, macAddressInput, exc); 108 | } 109 | 110 | public static void HostAdded(this ILogger logger, Models.Host host) 111 | { 112 | hostAdded(logger, host, null); 113 | } 114 | 115 | public static void WakeHost(this ILogger logger, Models.Host host) 116 | { 117 | wakeHost(logger, host, null); 118 | } 119 | 120 | public static void WakeHostFinalStatus(this ILogger logger, Models.Host host, int pingTries, HostViewModel.HostStatus finalStatus) 121 | { 122 | wakeHostFinalStatus(logger, host, pingTries, finalStatus, null); 123 | } 124 | 125 | public static void HostDeleted(this ILogger logger, Models.Host host) 126 | { 127 | hostDeleted(logger, host, null); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /WoL/Pages/AddHost.razor: -------------------------------------------------------------------------------- 1 | @page "/AddHost" 2 | 3 | @using WoL.Data 4 | @using WoL.Models 5 | @using WoL.Models.ViewModels 6 | @using WoL.Services 7 | @using WoL.Extensions 8 | @using Microsoft.Extensions.Logging 9 | @using System.Net.Sockets 10 | @inject IHostService HostService 11 | @inject IAddressLookupService AddressService 12 | @inject NavigationManager NavigationManager 13 | @inject ILogger L 14 | 15 |
16 |

Add Host

17 | 18 |
19 | 20 |

21 | Add a new host by entering a caption and either a hostname/IP address or a mac address (but not both). If you enter a hostname/IP address the mac address will be determined automatically and the entered value will be replaced by the one found using DNS. Please note that adding hosts by hostname might only work if they are currently powered on, as the mac address can not be determined automatically otherwise (via ARP). 22 |

23 |
24 |
25 | 26 | @if (!string.IsNullOrEmpty(Alert)) 27 | { 28 |
@Alert
29 | } 30 | 31 | 32 | 33 |
34 |
35 | 36 | 37 | 38 |
39 |
40 |
41 | 42 | 43 | 44 |
45 |

46 | - or - 47 |

48 |
49 | 50 | 51 | 52 |
53 |
54 | @if (Creating) 55 | { 56 | 60 | } 61 | else 62 | { 63 | 64 | } 65 |
66 |
67 |
68 |
69 | 70 | 75 |
76 | 77 | @code { 78 | AddHostViewModel model = new AddHostViewModel(); 79 | string Alert = null; 80 | bool Creating = false; 81 | 82 | private Host GetMacHost() 83 | { 84 | var mac = model.MacAddress.ParseMacAddress(); 85 | return new Host() 86 | { 87 | Caption = model.Caption, 88 | MacAddress = mac 89 | }; 90 | } 91 | 92 | private async Task GetHostByName() 93 | { 94 | (var ip, var name) = await AddressService.GetIpAndName(model.Hostname); 95 | var mac = (await AddressService.GetMac(ip)).GetAddressBytes(); 96 | return new Host() 97 | { 98 | Caption = model.Caption, 99 | Hostname = name, 100 | MacAddress = mac 101 | }; 102 | } 103 | 104 | private async Task GetHost() 105 | { 106 | if (!string.IsNullOrEmpty(model.MacAddress)) 107 | return GetMacHost(); 108 | return await GetHostByName(); 109 | } 110 | 111 | private async Task HandleValidSubmit(EditContext context) 112 | { 113 | Creating = true; 114 | Host host; 115 | try 116 | { 117 | host = await GetHost(); 118 | } 119 | catch (PlatformNotSupportedException ex) 120 | { 121 | L.GetHostPlatformNotSupportedException(ex); 122 | Alert = "This operation is not supported on the operating system this application is running on. Adding hosts by hostname is currently not supported on Mac OS X due to the lack of an appropriate ARP API."; 123 | Creating = false; 124 | return; 125 | } 126 | catch (SocketException sockEx) when (sockEx.SocketErrorCode == SocketError.HostNotFound) 127 | { 128 | // https://blog.jetbrains.com/dotnet/2020/04/27/socket-error-codes-depend-runtime-operating-system/ 129 | // For cross platform socket error handling 130 | 131 | // https://docs.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2 132 | /* WSAHOST_NOT_FOUND 133 | Host not found. 134 | No such host is known. The name is not an official host name or alias, or it cannot be found in the database(s) being queried. This error may also be returned for protocol and service queries, and means that the specified name could not be found in the relevant database. 135 | */ 136 | L.GetHostHostNotFound(model.Caption, model.Hostname, model.MacAddress, sockEx); 137 | Alert = "This host could not be found."; 138 | Creating = false; 139 | return; 140 | } 141 | catch (Exception ex) 142 | { 143 | L.GetHostUnknownException(model.Caption, model.Hostname, model.MacAddress, ex); 144 | Alert = "This host or it's mac address could not be found."; 145 | Creating = false; 146 | return; 147 | } 148 | try 149 | { 150 | await HostService.Add(host); 151 | L.HostAdded(host); 152 | } 153 | catch (IHostService.DuplicateEntryException duplEx) 154 | { 155 | L.AddHostDuplicateEntryException(host, duplEx); 156 | Alert = string.IsNullOrEmpty(duplEx.Value) 157 | ? $"Creation failed as an entry with this {duplEx.Field.ToLower()} does already exist." 158 | : $"Creation failed as an entry with {duplEx.Field.ToLower()} '{duplEx.Value}' does already exist."; 159 | Creating = false; 160 | return; 161 | } 162 | catch (Exception ex) 163 | { 164 | L.AddHostUnknownException(ex); 165 | Alert = "This entry could not be created. Maybe a similar entry which shares some of the entered values does already exist. Please contact the administrator."; 166 | Creating = false; 167 | return; 168 | } 169 | NavigationManager.NavigateTo("/"); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/visualstudio 3 | # Edit at https://www.gitignore.io/?templates=visualstudio 4 | 5 | ### VisualStudio ### 6 | ## Ignore Visual Studio temporary files, build results, and 7 | ## files generated by popular Visual Studio add-ons. 8 | ## 9 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 10 | 11 | # User-specific files 12 | *.rsuser 13 | *.suo 14 | *.user 15 | *.userosscache 16 | *.sln.docstates 17 | 18 | # User-specific files (MonoDevelop/Xamarin Studio) 19 | *.userprefs 20 | 21 | # Mono auto generated files 22 | mono_crash.* 23 | 24 | # Build results 25 | [Dd]ebug/ 26 | [Dd]ebugPublic/ 27 | [Rr]elease/ 28 | [Rr]eleases/ 29 | x64/ 30 | x86/ 31 | [Aa][Rr][Mm]/ 32 | [Aa][Rr][Mm]64/ 33 | bld/ 34 | [Bb]in/ 35 | [Oo]bj/ 36 | [Ll]og/ 37 | 38 | # Visual Studio 2015/2017 cache/options directory 39 | .vs/ 40 | # Uncomment if you have tasks that create the project's static files in wwwroot 41 | #wwwroot/ 42 | 43 | # Visual Studio 2017 auto generated files 44 | Generated\ Files/ 45 | 46 | # MSTest test Results 47 | [Tt]est[Rr]esult*/ 48 | [Bb]uild[Ll]og.* 49 | 50 | # NUnit 51 | *.VisualState.xml 52 | TestResult.xml 53 | nunit-*.xml 54 | 55 | # Build Results of an ATL Project 56 | [Dd]ebugPS/ 57 | [Rr]eleasePS/ 58 | dlldata.c 59 | 60 | # Benchmark Results 61 | BenchmarkDotNet.Artifacts/ 62 | 63 | # .NET Core 64 | project.lock.json 65 | project.fragment.lock.json 66 | artifacts/ 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.vspscc 94 | *.vssscc 95 | .builds 96 | *.pidb 97 | *.svclog 98 | *.scc 99 | 100 | # Chutzpah Test files 101 | _Chutzpah* 102 | 103 | # Visual C++ cache files 104 | ipch/ 105 | *.aps 106 | *.ncb 107 | *.opendb 108 | *.opensdf 109 | *.sdf 110 | *.cachefile 111 | *.VC.db 112 | *.VC.VC.opendb 113 | 114 | # Visual Studio profiler 115 | *.psess 116 | *.vsp 117 | *.vspx 118 | *.sap 119 | 120 | # Visual Studio Trace Files 121 | *.e2e 122 | 123 | # TFS 2012 Local Workspace 124 | $tf/ 125 | 126 | # Guidance Automation Toolkit 127 | *.gpState 128 | 129 | # ReSharper is a .NET coding add-in 130 | _ReSharper*/ 131 | *.[Rr]e[Ss]harper 132 | *.DotSettings.user 133 | 134 | # JustCode is a .NET coding add-in 135 | .JustCode 136 | 137 | # TeamCity is a build add-in 138 | _TeamCity* 139 | 140 | # DotCover is a Code Coverage Tool 141 | *.dotCover 142 | 143 | # AxoCover is a Code Coverage Tool 144 | .axoCover/* 145 | !.axoCover/settings.json 146 | 147 | # Visual Studio code coverage results 148 | *.coverage 149 | *.coveragexml 150 | 151 | # NCrunch 152 | _NCrunch_* 153 | .*crunch*.local.xml 154 | nCrunchTemp_* 155 | 156 | # MightyMoose 157 | *.mm.* 158 | AutoTest.Net/ 159 | 160 | # Web workbench (sass) 161 | .sass-cache/ 162 | 163 | # Installshield output folder 164 | [Ee]xpress/ 165 | 166 | # DocProject is a documentation generator add-in 167 | DocProject/buildhelp/ 168 | DocProject/Help/*.HxT 169 | DocProject/Help/*.HxC 170 | DocProject/Help/*.hhc 171 | DocProject/Help/*.hhk 172 | DocProject/Help/*.hhp 173 | DocProject/Help/Html2 174 | DocProject/Help/html 175 | 176 | # Click-Once directory 177 | publish/ 178 | 179 | # Publish Web Output 180 | *.[Pp]ublish.xml 181 | *.azurePubxml 182 | # Note: Comment the next line if you want to checkin your web deploy settings, 183 | # but database connection strings (with potential passwords) will be unencrypted 184 | *.pubxml 185 | *.publishproj 186 | 187 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 188 | # checkin your Azure Web App publish settings, but sensitive information contained 189 | # in these scripts will be unencrypted 190 | PublishScripts/ 191 | 192 | # NuGet Packages 193 | *.nupkg 194 | # NuGet Symbol Packages 195 | *.snupkg 196 | # The packages folder can be ignored because of Package Restore 197 | **/[Pp]ackages/* 198 | # except build/, which is used as an MSBuild target. 199 | !**/[Pp]ackages/build/ 200 | # Uncomment if necessary however generally it will be regenerated when needed 201 | #!**/[Pp]ackages/repositories.config 202 | # NuGet v3's project.json files produces more ignorable files 203 | *.nuget.props 204 | *.nuget.targets 205 | 206 | # Microsoft Azure Build Output 207 | csx/ 208 | *.build.csdef 209 | 210 | # Microsoft Azure Emulator 211 | ecf/ 212 | rcf/ 213 | 214 | # Windows Store app package directories and files 215 | AppPackages/ 216 | BundleArtifacts/ 217 | Package.StoreAssociation.xml 218 | _pkginfo.txt 219 | *.appx 220 | *.appxbundle 221 | *.appxupload 222 | 223 | # Visual Studio cache files 224 | # files ending in .cache can be ignored 225 | *.[Cc]ache 226 | # but keep track of directories ending in .cache 227 | !?*.[Cc]ache/ 228 | 229 | # Others 230 | ClientBin/ 231 | ~$* 232 | *~ 233 | *.dbmdl 234 | *.dbproj.schemaview 235 | *.jfm 236 | *.pfx 237 | *.publishsettings 238 | orleans.codegen.cs 239 | 240 | # Including strong name files can present a security risk 241 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 242 | #*.snk 243 | 244 | # Since there are multiple workflows, uncomment next line to ignore bower_components 245 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 246 | #bower_components/ 247 | 248 | # RIA/Silverlight projects 249 | Generated_Code/ 250 | 251 | # Backup & report files from converting an old project file 252 | # to a newer Visual Studio version. Backup files are not needed, 253 | # because we have git ;-) 254 | _UpgradeReport_Files/ 255 | Backup*/ 256 | UpgradeLog*.XML 257 | UpgradeLog*.htm 258 | ServiceFabricBackup/ 259 | *.rptproj.bak 260 | 261 | # SQL Server files 262 | *.mdf 263 | *.ldf 264 | *.ndf 265 | 266 | # Business Intelligence projects 267 | *.rdl.data 268 | *.bim.layout 269 | *.bim_*.settings 270 | *.rptproj.rsuser 271 | *- [Bb]ackup.rdl 272 | *- [Bb]ackup ([0-9]).rdl 273 | *- [Bb]ackup ([0-9][0-9]).rdl 274 | 275 | # Microsoft Fakes 276 | FakesAssemblies/ 277 | 278 | # GhostDoc plugin setting file 279 | *.GhostDoc.xml 280 | 281 | # Node.js Tools for Visual Studio 282 | .ntvs_analysis.dat 283 | node_modules/ 284 | 285 | # Visual Studio 6 build log 286 | *.plg 287 | 288 | # Visual Studio 6 workspace options file 289 | *.opt 290 | 291 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 292 | *.vbw 293 | 294 | # Visual Studio LightSwitch build output 295 | **/*.HTMLClient/GeneratedArtifacts 296 | **/*.DesktopClient/GeneratedArtifacts 297 | **/*.DesktopClient/ModelManifest.xml 298 | **/*.Server/GeneratedArtifacts 299 | **/*.Server/ModelManifest.xml 300 | _Pvt_Extensions 301 | 302 | # Paket dependency manager 303 | .paket/paket.exe 304 | paket-files/ 305 | 306 | # FAKE - F# Make 307 | .fake/ 308 | 309 | # CodeRush personal settings 310 | .cr/personal 311 | 312 | # Python Tools for Visual Studio (PTVS) 313 | __pycache__/ 314 | *.pyc 315 | 316 | # Cake - Uncomment if you are using it 317 | # tools/** 318 | # !tools/packages.config 319 | 320 | # Tabs Studio 321 | *.tss 322 | 323 | # Telerik's JustMock configuration file 324 | *.jmconfig 325 | 326 | # BizTalk build output 327 | *.btp.cs 328 | *.btm.cs 329 | *.odx.cs 330 | *.xsd.cs 331 | 332 | # OpenCover UI analysis results 333 | OpenCover/ 334 | 335 | # Azure Stream Analytics local run output 336 | ASALocalRun/ 337 | 338 | # MSBuild Binary and Structured Log 339 | *.binlog 340 | 341 | # NVidia Nsight GPU debugger configuration file 342 | *.nvuser 343 | 344 | # MFractors (Xamarin productivity tool) working folder 345 | .mfractor/ 346 | 347 | # Local History for Visual Studio 348 | .localhistory/ 349 | 350 | # BeatPulse healthcheck temp database 351 | healthchecksdb 352 | 353 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 354 | MigrationBackup/ 355 | 356 | # End of https://www.gitignore.io/api/visualstudio 357 | -------------------------------------------------------------------------------- /WoL/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:Icons;src:url(../fonts/open-iconic.eot);src:url(../fonts/open-iconic.eot?#iconic-sm) format('embedded-opentype'),url(../fonts/open-iconic.woff) format('woff'),url(../fonts/open-iconic.ttf) format('truetype'),url(../fonts/open-iconic.otf) format('opentype'),url(../fonts/open-iconic.svg#iconic-sm) format('svg');font-weight:400;font-style:normal}.oi{position:relative;top:1px;display:inline-block;speak:none;font-family:Icons;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.oi:empty:before{width:1em;text-align:center;box-sizing:content-box}.oi.oi-align-center:before{text-align:center}.oi.oi-align-left:before{text-align:left}.oi.oi-align-right:before{text-align:right}.oi.oi-flip-horizontal:before{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.oi.oi-flip-vertical:before{-webkit-transform:scale(1,-1);-ms-transform:scale(-1,1);transform:scale(1,-1)}.oi.oi-flip-horizontal-vertical:before{-webkit-transform:scale(-1,-1);-ms-transform:scale(-1,1);transform:scale(-1,-1)}.oi-account-login:before{content:'\e000'}.oi-account-logout:before{content:'\e001'}.oi-action-redo:before{content:'\e002'}.oi-action-undo:before{content:'\e003'}.oi-align-center:before{content:'\e004'}.oi-align-left:before{content:'\e005'}.oi-align-right:before{content:'\e006'}.oi-aperture:before{content:'\e007'}.oi-arrow-bottom:before{content:'\e008'}.oi-arrow-circle-bottom:before{content:'\e009'}.oi-arrow-circle-left:before{content:'\e00a'}.oi-arrow-circle-right:before{content:'\e00b'}.oi-arrow-circle-top:before{content:'\e00c'}.oi-arrow-left:before{content:'\e00d'}.oi-arrow-right:before{content:'\e00e'}.oi-arrow-thick-bottom:before{content:'\e00f'}.oi-arrow-thick-left:before{content:'\e010'}.oi-arrow-thick-right:before{content:'\e011'}.oi-arrow-thick-top:before{content:'\e012'}.oi-arrow-top:before{content:'\e013'}.oi-audio-spectrum:before{content:'\e014'}.oi-audio:before{content:'\e015'}.oi-badge:before{content:'\e016'}.oi-ban:before{content:'\e017'}.oi-bar-chart:before{content:'\e018'}.oi-basket:before{content:'\e019'}.oi-battery-empty:before{content:'\e01a'}.oi-battery-full:before{content:'\e01b'}.oi-beaker:before{content:'\e01c'}.oi-bell:before{content:'\e01d'}.oi-bluetooth:before{content:'\e01e'}.oi-bold:before{content:'\e01f'}.oi-bolt:before{content:'\e020'}.oi-book:before{content:'\e021'}.oi-bookmark:before{content:'\e022'}.oi-box:before{content:'\e023'}.oi-briefcase:before{content:'\e024'}.oi-british-pound:before{content:'\e025'}.oi-browser:before{content:'\e026'}.oi-brush:before{content:'\e027'}.oi-bug:before{content:'\e028'}.oi-bullhorn:before{content:'\e029'}.oi-calculator:before{content:'\e02a'}.oi-calendar:before{content:'\e02b'}.oi-camera-slr:before{content:'\e02c'}.oi-caret-bottom:before{content:'\e02d'}.oi-caret-left:before{content:'\e02e'}.oi-caret-right:before{content:'\e02f'}.oi-caret-top:before{content:'\e030'}.oi-cart:before{content:'\e031'}.oi-chat:before{content:'\e032'}.oi-check:before{content:'\e033'}.oi-chevron-bottom:before{content:'\e034'}.oi-chevron-left:before{content:'\e035'}.oi-chevron-right:before{content:'\e036'}.oi-chevron-top:before{content:'\e037'}.oi-circle-check:before{content:'\e038'}.oi-circle-x:before{content:'\e039'}.oi-clipboard:before{content:'\e03a'}.oi-clock:before{content:'\e03b'}.oi-cloud-download:before{content:'\e03c'}.oi-cloud-upload:before{content:'\e03d'}.oi-cloud:before{content:'\e03e'}.oi-cloudy:before{content:'\e03f'}.oi-code:before{content:'\e040'}.oi-cog:before{content:'\e041'}.oi-collapse-down:before{content:'\e042'}.oi-collapse-left:before{content:'\e043'}.oi-collapse-right:before{content:'\e044'}.oi-collapse-up:before{content:'\e045'}.oi-command:before{content:'\e046'}.oi-comment-square:before{content:'\e047'}.oi-compass:before{content:'\e048'}.oi-contrast:before{content:'\e049'}.oi-copywriting:before{content:'\e04a'}.oi-credit-card:before{content:'\e04b'}.oi-crop:before{content:'\e04c'}.oi-dashboard:before{content:'\e04d'}.oi-data-transfer-download:before{content:'\e04e'}.oi-data-transfer-upload:before{content:'\e04f'}.oi-delete:before{content:'\e050'}.oi-dial:before{content:'\e051'}.oi-document:before{content:'\e052'}.oi-dollar:before{content:'\e053'}.oi-double-quote-sans-left:before{content:'\e054'}.oi-double-quote-sans-right:before{content:'\e055'}.oi-double-quote-serif-left:before{content:'\e056'}.oi-double-quote-serif-right:before{content:'\e057'}.oi-droplet:before{content:'\e058'}.oi-eject:before{content:'\e059'}.oi-elevator:before{content:'\e05a'}.oi-ellipses:before{content:'\e05b'}.oi-envelope-closed:before{content:'\e05c'}.oi-envelope-open:before{content:'\e05d'}.oi-euro:before{content:'\e05e'}.oi-excerpt:before{content:'\e05f'}.oi-expand-down:before{content:'\e060'}.oi-expand-left:before{content:'\e061'}.oi-expand-right:before{content:'\e062'}.oi-expand-up:before{content:'\e063'}.oi-external-link:before{content:'\e064'}.oi-eye:before{content:'\e065'}.oi-eyedropper:before{content:'\e066'}.oi-file:before{content:'\e067'}.oi-fire:before{content:'\e068'}.oi-flag:before{content:'\e069'}.oi-flash:before{content:'\e06a'}.oi-folder:before{content:'\e06b'}.oi-fork:before{content:'\e06c'}.oi-fullscreen-enter:before{content:'\e06d'}.oi-fullscreen-exit:before{content:'\e06e'}.oi-globe:before{content:'\e06f'}.oi-graph:before{content:'\e070'}.oi-grid-four-up:before{content:'\e071'}.oi-grid-three-up:before{content:'\e072'}.oi-grid-two-up:before{content:'\e073'}.oi-hard-drive:before{content:'\e074'}.oi-header:before{content:'\e075'}.oi-headphones:before{content:'\e076'}.oi-heart:before{content:'\e077'}.oi-home:before{content:'\e078'}.oi-image:before{content:'\e079'}.oi-inbox:before{content:'\e07a'}.oi-infinity:before{content:'\e07b'}.oi-info:before{content:'\e07c'}.oi-italic:before{content:'\e07d'}.oi-justify-center:before{content:'\e07e'}.oi-justify-left:before{content:'\e07f'}.oi-justify-right:before{content:'\e080'}.oi-key:before{content:'\e081'}.oi-laptop:before{content:'\e082'}.oi-layers:before{content:'\e083'}.oi-lightbulb:before{content:'\e084'}.oi-link-broken:before{content:'\e085'}.oi-link-intact:before{content:'\e086'}.oi-list-rich:before{content:'\e087'}.oi-list:before{content:'\e088'}.oi-location:before{content:'\e089'}.oi-lock-locked:before{content:'\e08a'}.oi-lock-unlocked:before{content:'\e08b'}.oi-loop-circular:before{content:'\e08c'}.oi-loop-square:before{content:'\e08d'}.oi-loop:before{content:'\e08e'}.oi-magnifying-glass:before{content:'\e08f'}.oi-map-marker:before{content:'\e090'}.oi-map:before{content:'\e091'}.oi-media-pause:before{content:'\e092'}.oi-media-play:before{content:'\e093'}.oi-media-record:before{content:'\e094'}.oi-media-skip-backward:before{content:'\e095'}.oi-media-skip-forward:before{content:'\e096'}.oi-media-step-backward:before{content:'\e097'}.oi-media-step-forward:before{content:'\e098'}.oi-media-stop:before{content:'\e099'}.oi-medical-cross:before{content:'\e09a'}.oi-menu:before{content:'\e09b'}.oi-microphone:before{content:'\e09c'}.oi-minus:before{content:'\e09d'}.oi-monitor:before{content:'\e09e'}.oi-moon:before{content:'\e09f'}.oi-move:before{content:'\e0a0'}.oi-musical-note:before{content:'\e0a1'}.oi-paperclip:before{content:'\e0a2'}.oi-pencil:before{content:'\e0a3'}.oi-people:before{content:'\e0a4'}.oi-person:before{content:'\e0a5'}.oi-phone:before{content:'\e0a6'}.oi-pie-chart:before{content:'\e0a7'}.oi-pin:before{content:'\e0a8'}.oi-play-circle:before{content:'\e0a9'}.oi-plus:before{content:'\e0aa'}.oi-power-standby:before{content:'\e0ab'}.oi-print:before{content:'\e0ac'}.oi-project:before{content:'\e0ad'}.oi-pulse:before{content:'\e0ae'}.oi-puzzle-piece:before{content:'\e0af'}.oi-question-mark:before{content:'\e0b0'}.oi-rain:before{content:'\e0b1'}.oi-random:before{content:'\e0b2'}.oi-reload:before{content:'\e0b3'}.oi-resize-both:before{content:'\e0b4'}.oi-resize-height:before{content:'\e0b5'}.oi-resize-width:before{content:'\e0b6'}.oi-rss-alt:before{content:'\e0b7'}.oi-rss:before{content:'\e0b8'}.oi-script:before{content:'\e0b9'}.oi-share-boxed:before{content:'\e0ba'}.oi-share:before{content:'\e0bb'}.oi-shield:before{content:'\e0bc'}.oi-signal:before{content:'\e0bd'}.oi-signpost:before{content:'\e0be'}.oi-sort-ascending:before{content:'\e0bf'}.oi-sort-descending:before{content:'\e0c0'}.oi-spreadsheet:before{content:'\e0c1'}.oi-star:before{content:'\e0c2'}.oi-sun:before{content:'\e0c3'}.oi-tablet:before{content:'\e0c4'}.oi-tag:before{content:'\e0c5'}.oi-tags:before{content:'\e0c6'}.oi-target:before{content:'\e0c7'}.oi-task:before{content:'\e0c8'}.oi-terminal:before{content:'\e0c9'}.oi-text:before{content:'\e0ca'}.oi-thumb-down:before{content:'\e0cb'}.oi-thumb-up:before{content:'\e0cc'}.oi-timer:before{content:'\e0cd'}.oi-transfer:before{content:'\e0ce'}.oi-trash:before{content:'\e0cf'}.oi-underline:before{content:'\e0d0'}.oi-vertical-align-bottom:before{content:'\e0d1'}.oi-vertical-align-center:before{content:'\e0d2'}.oi-vertical-align-top:before{content:'\e0d3'}.oi-video:before{content:'\e0d4'}.oi-volume-high:before{content:'\e0d5'}.oi-volume-low:before{content:'\e0d6'}.oi-volume-off:before{content:'\e0d7'}.oi-warning:before{content:'\e0d8'}.oi-wifi:before{content:'\e0d9'}.oi-wrench:before{content:'\e0da'}.oi-x:before{content:'\e0db'}.oi-yen:before{content:'\e0dc'}.oi-zoom-in:before{content:'\e0dd'}.oi-zoom-out:before{content:'\e0de'} -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Schema: http://EditorConfig.org 2 | # Docs: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference 3 | # based on: https://github.com/dotnet/efcore/blob/a17cfadb272309c7597685231616228700297206/.editorconfig 4 | 5 | # top-most EditorConfig file 6 | root = true 7 | 8 | # Don't use tabs for indentation. 9 | [*] 10 | indent_style = space 11 | trim_trailing_whitespace = true 12 | guidelines = 140 13 | max_line_length = 140 14 | end_of_line = lf 15 | insert_final_newline = true 16 | 17 | # Code files 18 | [*.{cs,csx,vb,vbx}] 19 | indent_size = 4 20 | insert_final_newline = true 21 | 22 | # Xml project files 23 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] 24 | indent_size = 2 25 | 26 | # Xml config files 27 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct,xml,stylecop}] 28 | indent_size = 2 29 | 30 | # JSON files 31 | [*.json] 32 | indent_size = 2 33 | 34 | # Powershell files 35 | [*.ps1] 36 | indent_size = 2 37 | 38 | # Shell scripts 39 | [*.sh] 40 | end_of_line = lf 41 | indent_size = 2 42 | 43 | [*.{cmd,bat}] 44 | end_of_line = crlf 45 | indent_size = 2 46 | 47 | ## Language conventions 48 | # Dotnet code style settings: 49 | [*.{cs,vb}] 50 | # "This." and "Me." qualifiers 51 | dotnet_style_qualification_for_field = false:suggestion 52 | dotnet_style_qualification_for_property = false:suggestion 53 | dotnet_style_qualification_for_method = false:suggestion 54 | dotnet_style_qualification_for_event = false:suggestion 55 | 56 | # Language keywords instead of framework type names for type references 57 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 58 | dotnet_style_predefined_type_for_member_access = true:suggestion 59 | 60 | # Modifier preferences 61 | dotnet_style_require_accessibility_modifiers = always:suggestion 62 | dotnet_style_readonly_field = true:warning 63 | 64 | # Parentheses preferences 65 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 66 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 67 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 68 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 69 | 70 | # Expression-level preferences 71 | dotnet_style_object_initializer = true:suggestion 72 | dotnet_style_collection_initializer = true:suggestion 73 | dotnet_style_explicit_tuple_names = true:suggestion 74 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 75 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 76 | dotnet_style_prefer_auto_properties = true:silent 77 | dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion 78 | dotnet_style_prefer_conditional_expression_over_return = true:suggestion 79 | 80 | # Null-checking preferences 81 | dotnet_style_coalesce_expression = true:suggestion 82 | dotnet_style_null_propagation = true:suggestion 83 | 84 | # CSharp code style settings: 85 | [*.cs] 86 | # Modifier preferences 87 | csharp_preferred_modifier_order = public,private,protected,internal,const,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion 88 | 89 | # Implicit and explicit types 90 | csharp_style_var_for_built_in_types = true:suggestion 91 | csharp_style_var_when_type_is_apparent = true:suggestion 92 | csharp_style_var_elsewhere = true:suggestion 93 | 94 | # Expression-bodied members 95 | #csharp_style_expression_bodied_methods = false:silent 96 | #csharp_style_expression_bodied_constructors = false:silent 97 | csharp_style_expression_bodied_operators = false:silent 98 | csharp_style_expression_bodied_properties = true:suggestion 99 | csharp_style_expression_bodied_indexers = true:suggestion 100 | csharp_style_expression_bodied_accessors = true:suggestion 101 | 102 | # Pattern matching 103 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 104 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 105 | 106 | # Inlined variable declarations 107 | csharp_style_inlined_variable_declaration = true:suggestion 108 | 109 | # Expression-level preferences 110 | csharp_prefer_simple_default_expression = true:suggestion 111 | csharp_style_deconstructed_variable_declaration = true:suggestion 112 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 113 | 114 | # Null-checking preference 115 | csharp_style_throw_expression = true:suggestion 116 | csharp_style_conditional_delegate_call = true:suggestion 117 | 118 | # Code block preferences 119 | csharp_prefer_braces = true:suggestion 120 | 121 | ## Formatting conventions 122 | # Dotnet formatting settings: 123 | [*.{cs,vb}] 124 | # Organize usings 125 | dotnet_sort_system_directives_first = true 126 | dotnet_separate_import_directive_groups = false 127 | 128 | # CSharp formatting settings: 129 | [*.cs] 130 | # Newline options 131 | csharp_new_line_before_open_brace = all 132 | csharp_new_line_before_else = true 133 | csharp_new_line_before_catch = true 134 | csharp_new_line_before_finally = true 135 | csharp_new_line_before_members_in_object_initializers = true 136 | csharp_new_line_before_members_in_anonymous_types = true 137 | csharp_new_line_between_query_expression_clauses = true 138 | 139 | # Identation options 140 | csharp_indent_block_contents = true 141 | csharp_indent_braces = false 142 | csharp_indent_case_contents_when_block = false 143 | csharp_indent_switch_labels = true 144 | csharp_indent_case_contents = true 145 | csharp_indent_labels = no_change 146 | 147 | # Spacing options 148 | csharp_space_after_cast = false 149 | csharp_space_after_keywords_in_control_flow_statements = true 150 | csharp_space_between_method_declaration_parameter_list_parentheses = false 151 | csharp_space_between_method_call_parameter_list_parentheses = false 152 | csharp_space_between_parentheses = false 153 | csharp_space_before_colon_in_inheritance_clause = true 154 | csharp_space_after_colon_in_inheritance_clause = true 155 | csharp_space_around_binary_operators = before_and_after 156 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 157 | csharp_space_between_method_call_name_and_opening_parenthesis = false 158 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 159 | csharp_space_after_comma = true 160 | csharp_space_after_dot = false 161 | csharp_space_after_semicolon_in_for_statement = true 162 | csharp_space_around_declaration_statements = do_not_ignore 163 | csharp_space_before_comma = false 164 | csharp_space_before_dot = false 165 | csharp_space_before_open_square_brackets = false 166 | csharp_space_before_semicolon_in_for_statement = false 167 | csharp_space_between_empty_square_brackets = false 168 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 169 | csharp_space_between_square_brackets = false 170 | 171 | # Wrap options 172 | csharp_preserve_single_line_statements = true 173 | csharp_preserve_single_line_blocks = true 174 | 175 | ## Naming conventions 176 | 177 | # CS1591: Fehledes XML-Kommentar für öffentlich sichtbaren Typ oder Element 178 | dotnet_diagnostic.CS1591.severity = silent 179 | 180 | # SA1600: Elements should be documented 181 | dotnet_diagnostic.SA1600.severity = silent 182 | 183 | # SA1309: A field name in C# begins with an underscore. 184 | dotnet_diagnostic.SA1309.severity = silent 185 | 186 | # SA1101: A call to an instance member of the local class or a base class is not prefixed with 'this.', within a C# code file. 187 | dotnet_diagnostic.SA1101.severity = silent 188 | 189 | # SA1602: Enumeration items should be documented 190 | dotnet_diagnostic.SA1602.severity = silent 191 | 192 | # SA1516: Adjacent C# elements are not separated by a blank line. 193 | # this does not work with new C# top level statements 194 | dotnet_diagnostic.SA1516.severity = silent 195 | 196 | [*.{cs,vb}] 197 | 198 | ## Naming styles 199 | 200 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 201 | dotnet_naming_style.camel_case_style.capitalization = camel_case 202 | 203 | # PascalCase with I prefix 204 | dotnet_naming_style.interface_style.capitalization = pascal_case 205 | dotnet_naming_style.interface_style.required_prefix = I 206 | 207 | # PascalCase with T prefix 208 | dotnet_naming_style.type_parameter_style.capitalization = pascal_case 209 | dotnet_naming_style.type_parameter_style.required_prefix = T 210 | 211 | # camelCase with _ prefix 212 | dotnet_naming_style._camelCase.capitalization = camel_case 213 | dotnet_naming_style._camelCase.required_prefix = _ 214 | 215 | ## Rules 216 | # Interfaces 217 | dotnet_naming_symbols.interface_symbol.applicable_kinds = interface 218 | dotnet_naming_symbols.interface_symbol.applicable_accessibilities = * 219 | dotnet_naming_rule.interface_naming.symbols = interface_symbol 220 | dotnet_naming_rule.interface_naming.style = interface_style 221 | dotnet_naming_rule.interface_naming.severity = suggestion 222 | 223 | # Classes, Structs, Enums, Properties, Methods, Local Functions, Events, Namespaces 224 | dotnet_naming_symbols.class_symbol.applicable_kinds = class, struct, enum, property, method, local_function, event, namespace, delegate 225 | dotnet_naming_symbols.class_symbol.applicable_accessibilities = * 226 | 227 | dotnet_naming_rule.class_naming.symbols = class_symbol 228 | dotnet_naming_rule.class_naming.style = pascal_case_style 229 | dotnet_naming_rule.class_naming.severity = suggestion 230 | 231 | # Type Parameters 232 | dotnet_naming_symbols.type_parameter_symbol.applicable_kinds = type_parameter 233 | dotnet_naming_symbols.type_parameter_symbol.applicable_accessibilities = * 234 | 235 | dotnet_naming_rule.type_parameter_naming.symbols = type_parameter_symbol 236 | dotnet_naming_rule.type_parameter_naming.style = type_parameter_style 237 | dotnet_naming_rule.type_parameter_naming.severity = suggestion 238 | 239 | # Visible Fields 240 | dotnet_naming_symbols.public_field_symbol.applicable_kinds = field 241 | dotnet_naming_symbols.public_field_symbol.applicable_accessibilities = public, internal, protected, protected_internal, private_protected 242 | 243 | dotnet_naming_rule.public_field_naming.symbols = public_field_symbol 244 | dotnet_naming_rule.public_field_naming.style = pascal_case_style 245 | dotnet_naming_rule.public_field_naming.severity = suggestion 246 | 247 | # Private constant Fields 248 | dotnet_naming_symbols.const_field_symbol.applicable_kinds = field 249 | dotnet_naming_symbols.const_field_symbol.applicable_accessibilities = private 250 | dotnet_naming_symbols.const_field_symbol.required_modifiers = const 251 | 252 | dotnet_naming_rule.const_field_naming.symbols = const_field_symbol 253 | dotnet_naming_rule.const_field_naming.style = pascal_case_style 254 | dotnet_naming_rule.const_field_naming.severity = suggestion 255 | 256 | # Private Fields 257 | dotnet_naming_symbols.private_field_symbol.applicable_kinds = field 258 | dotnet_naming_symbols.private_field_symbol.applicable_accessibilities = private 259 | 260 | dotnet_naming_rule.private_field_naming.symbols = private_field_symbol 261 | dotnet_naming_rule.private_field_naming.style = _camelCase 262 | dotnet_naming_rule.private_field_naming.severity = suggestion 263 | 264 | # Parameters 265 | dotnet_naming_symbols.parameter_symbol.applicable_kinds = parameter 266 | dotnet_naming_symbols.parameter_symbol.applicable_accessibilities = * 267 | 268 | dotnet_naming_rule.parameter_naming.symbols = parameter_symbol 269 | dotnet_naming_rule.parameter_naming.style = camel_case_style 270 | dotnet_naming_rule.parameter_naming.severity = suggestion 271 | 272 | # Everything Local 273 | dotnet_naming_symbols.everything_else.applicable_kinds = local 274 | dotnet_naming_symbols.everything_else.applicable_accessibilities = * 275 | 276 | dotnet_naming_rule.everything_else_naming.symbols = everything_else 277 | dotnet_naming_rule.everything_else_naming.style = camel_case_style 278 | dotnet_naming_rule.everything_else_naming.severity = suggestion 279 | 280 | # SA1633 File should have header 281 | dotnet_diagnostic.SA1633.severity=silent 282 | -------------------------------------------------------------------------------- /WoL/wwwroot/css/open-iconic/font/fonts/open-iconic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Created by FontForge 20120731 at Tue Jul 1 20:39:22 2014 9 | By P.J. Onori 10 | Created by P.J. Onori with FontForge 2.0 (http://fontforge.sf.net) 11 | 12 | 13 | 14 | 27 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 45 | 47 | 49 | 51 | 53 | 55 | 57 | 59 | 61 | 63 | 65 | 67 | 69 | 71 | 74 | 76 | 79 | 81 | 84 | 86 | 88 | 91 | 93 | 95 | 98 | 100 | 102 | 104 | 106 | 109 | 112 | 115 | 117 | 121 | 123 | 125 | 127 | 130 | 132 | 134 | 136 | 138 | 141 | 143 | 145 | 147 | 149 | 151 | 153 | 155 | 157 | 159 | 162 | 165 | 167 | 169 | 172 | 174 | 177 | 179 | 181 | 183 | 185 | 189 | 191 | 194 | 196 | 198 | 200 | 202 | 205 | 207 | 209 | 211 | 213 | 215 | 218 | 220 | 222 | 224 | 226 | 228 | 230 | 232 | 234 | 236 | 238 | 241 | 243 | 245 | 247 | 249 | 251 | 253 | 256 | 259 | 261 | 263 | 265 | 267 | 269 | 272 | 274 | 276 | 280 | 282 | 285 | 287 | 289 | 292 | 295 | 298 | 300 | 302 | 304 | 306 | 309 | 312 | 314 | 316 | 318 | 320 | 322 | 324 | 326 | 330 | 334 | 338 | 340 | 343 | 345 | 347 | 349 | 351 | 353 | 355 | 358 | 360 | 363 | 365 | 367 | 369 | 371 | 373 | 375 | 377 | 379 | 381 | 383 | 386 | 388 | 390 | 392 | 394 | 396 | 399 | 401 | 404 | 406 | 408 | 410 | 412 | 414 | 416 | 419 | 421 | 423 | 425 | 428 | 431 | 435 | 438 | 440 | 442 | 444 | 446 | 448 | 451 | 453 | 455 | 457 | 460 | 462 | 464 | 466 | 468 | 471 | 473 | 477 | 479 | 481 | 483 | 486 | 488 | 490 | 492 | 494 | 496 | 499 | 501 | 504 | 506 | 509 | 512 | 515 | 517 | 520 | 522 | 524 | 526 | 529 | 532 | 534 | 536 | 539 | 542 | 543 | 544 | --------------------------------------------------------------------------------