diff --git a/.github/workflows/gh-page.yaml b/.github/workflows/gh-page.yaml new file mode 100644 index 0000000..4c7c0a2 --- /dev/null +++ b/.github/workflows/gh-page.yaml @@ -0,0 +1,44 @@ +# Your GitHub workflow file under .github/workflows/ +# Trigger the action on push to main +on: + push: + branches: + - main + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + actions: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + publish-docs: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Dotnet Setup + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 8.x + + - run: dotnet tool update -g docfx + - run: docfx Docs/docfx.json + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + # Upload entire repository + path: 'Docs/_site' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 659ee6f..15bfdbb 100644 --- a/.gitignore +++ b/.gitignore @@ -421,3 +421,6 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk + +*/_site +*/api* \ No newline at end of file diff --git a/Docs/docfx.json b/Docs/docfx.json new file mode 100644 index 0000000..ba2b8e3 --- /dev/null +++ b/Docs/docfx.json @@ -0,0 +1,59 @@ +{ + "metadata": [ + { + "src": [ + { + "src": "../System", + "files": [ + "**/*.csproj" + ] + } + ], + "dest": "api.System" + }, + { + "src": [ + { + "src": "../Web", + "files": [ + "**/*.csproj" + ] + } + + ], + "dest": "api.Web" + } + ], + "build": { + "content": [ + { + "files": [ + "**/*.{md,yml}" + ], + "exclude": [ + "_site/**" + ] + } + ], + "resource": [ + { + "files": [ + "images/**" + ] + } + ], + "output": "_site", + "template": [ + "default", + "modern", + "templates/material" + ], + "globalMetadata": { + "_appName": "Tools", + "_appTitle": "Tools", + "_appLogoPath": "images/logo.png", + "_enableSearch": true, + "pdf": false + } + } +} \ No newline at end of file diff --git a/Docs/docs/Awake.md b/Docs/docs/Awake.md new file mode 100644 index 0000000..bd58b47 --- /dev/null +++ b/Docs/docs/Awake.md @@ -0,0 +1,92 @@ +# Awake + +Module is exported from Microsoft Powertoys under license MIT. + +>[!NOTE] +>Works only for Windows host + + +## V1 + +Simple usage + +```C# +using FrApps42.System.Computer.Awake.v1; +... + +// Keep Screen on +Awake..SetIndefiniteKeepAwake(true); +// Keep Screen off +Awake..SetIndefiniteKeepAwake(false); + +// Disable Keep Awake +Awake.SetNoKeepAwake(); + +... + +Awake.CompleteExit(0, false, "AppName"); + +``` + +If you want to log Awake error + +```C# +using FrApps42.System.Computer.Awake.v1; +... + +private static void LogUnexpectedOrCancelledKeepAwakeThreadCompletion(){ + Console.WriteLine("The keep-awake thread was terminated early."); +} + +private static void LogCompletedKeepAwakeThread(bool result) +{ + Console.WriteLine($"Exited keep-awake thread successfully: {result}"); +} + +// Keep Screen on +Awake..SetIndefiniteKeepAwake(LogCompletedKeepAwakeThread, LogUnexpectedOrCancelledKeepAwakeThreadCompletion,true); +// Keep Screen off +Awake..SetIndefiniteKeepAwake(LogCompletedKeepAwakeThread, LogUnexpectedOrCancelledKeepAwakeThreadCompletion,false); + +// Disable Keep Awake +Awake.SetNoKeepAwake(); + +... + +Awake.CompleteExit(0, false, "AppName"); + +``` + +## V2 + +Updated version of Power Awake + +```C# +using FrApps42.System.Computer.Awake.v1; +... + +private static void LogUnexpectedOrCancelledKeepAwakeThreadCompletion(){ + Console.WriteLine("The keep-awake thread was terminated early."); +} + +private static void LogCompletedKeepAwakeThread(bool result) +{ + Console.WriteLine($"Exited keep-awake thread successfully: {result}"); +} + +// Keep Screen on +Awake..SetIndefiniteKeepAwake(true); +// Keep Screen off +Awake..SetIndefiniteKeepAwake(false); + +// Keep Awake for a specified seconds with screen on +Awake.SetTimedKeepAwake(3600, true); +// Keep Awake for a specified seconds with screen off +Awake.SetTimedKeepAwake(3600, false); + +// Disable Keep Awake +Awake.SetNoKeepAwake(); + +``` + +In V2, be sure to disable KeepAwake before app closing. \ No newline at end of file diff --git a/Docs/docs/Net.md b/Docs/docs/Net.md new file mode 100644 index 0000000..00dbbc7 --- /dev/null +++ b/Docs/docs/Net.md @@ -0,0 +1,16 @@ +# NET + +>[!NOTE] +> Tested on Windows, should works on Linux and MacOS + +## IsOnline + +Simple class to test if computer is Online. + +```C# +using FrApps42.System.Net;` + + +bool result = (new IsOnline("8.8.8.8")).Check(); + +``` \ No newline at end of file diff --git a/Docs/docs/Shutdown.md b/Docs/docs/Shutdown.md new file mode 100644 index 0000000..07e4833 --- /dev/null +++ b/Docs/docs/Shutdown.md @@ -0,0 +1,6 @@ +# Shutdown + +>[!NOTE] +>Works only for Windows host + +Simple lib to shutdown local or remote Windows computer \ No newline at end of file diff --git a/Docs/docs/introduction.md b/Docs/docs/introduction.md new file mode 100644 index 0000000..d4b8201 --- /dev/null +++ b/Docs/docs/introduction.md @@ -0,0 +1,5 @@ +# Introduction + +Available Namespace : +- System +- Web \ No newline at end of file diff --git a/Docs/docs/toc.yml b/Docs/docs/toc.yml new file mode 100644 index 0000000..e46d956 --- /dev/null +++ b/Docs/docs/toc.yml @@ -0,0 +1,8 @@ +- name: Introduction + href: introduction.md +- name: Shutdown + href: Shutdown.md +- name: Awake + href: Awake.md +- name: Net + href: Net.md \ No newline at end of file diff --git a/Docs/images/Logo Inversed.png b/Docs/images/Logo Inversed.png new file mode 100644 index 0000000..07d52e1 Binary files /dev/null and b/Docs/images/Logo Inversed.png differ diff --git a/Docs/images/logo.github.png b/Docs/images/logo.github.png new file mode 100644 index 0000000..55cfd4c Binary files /dev/null and b/Docs/images/logo.github.png differ diff --git a/Docs/images/logo.png b/Docs/images/logo.png new file mode 100644 index 0000000..bfd5dc2 Binary files /dev/null and b/Docs/images/logo.png differ diff --git a/Docs/images/logo.svg b/Docs/images/logo.svg new file mode 100644 index 0000000..dd972cb --- /dev/null +++ b/Docs/images/logo.svg @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/Docs/index.md b/Docs/index.md new file mode 100644 index 0000000..bdb580c --- /dev/null +++ b/Docs/index.md @@ -0,0 +1,5 @@ +--- +_layout: landing +--- + + \ No newline at end of file diff --git a/Docs/templates/material/public/main.css b/Docs/templates/material/public/main.css new file mode 100644 index 0000000..fa38586 --- /dev/null +++ b/Docs/templates/material/public/main.css @@ -0,0 +1,183 @@ +@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@100;400;700&display=swap'); + +:root { + --bs-font-sans-serif: 'Roboto'; + --bs-border-radius: 10px; + + --border-radius-button: 40px; + --card-box-shadow: 0 1px 2px 0 #3d41440f, 0 1px 3px 1px #3d414429; + + --material-yellow-light: #e6dfbf; + --material-yellow-dark: #5a5338; + + --material-blue-light: #c4d9f1; + --material-blue-dark: #383e5a; + + --material-red-light: #f1c4c4; + --material-red-dark: #5a3838; + + --material-warning-header: #f57f171a; + --material-warning-background: #f6e8bd; + --material-warning-background-dark: #57502c; + + --material-info-header: #1976d21a; + --material-info-background: #e3f2fd; + --material-info-background-dark: #2c4557; + + --material-danger-header: #d32f2f1a; + --material-danger-background: #ffebee; + --material-danger-background-dark: #572c2c; +} + +/* HEADINGS */ + +h1 { + font-weight: 600; + font-size: 32px; +} + +h2 { + font-weight: 600; + font-size: 24px; + line-height: 1.8; +} + +h3 { + font-weight: 600; + font-size: 20px; + line-height: 1.8; +} + +h5 { + font-size: 14px; + padding: 10px 0px; +} + +article h2, +article h3, +article h4 { + margin-top: 15px; + margin-bottom: 15px; +} + +article h4 { + padding-bottom: 8px; + border-bottom: 2px solid #ddd; +} + +/** IMAGES **/ +img { + border-radius: var(--bs-border-radius); + box-shadow: var(--card-box-shadow); +} + +/** NAVBAR **/ +.navbar-brand > img { + box-shadow: none; + color: var(--bs-nav-link-color); + margin-right: 15px; +} + +[data-bs-theme='light'] nav.navbar { + background-color: var(--bs-primary-bg-subtle); +} + +[data-bs-theme='dark'] nav.navbar { + background-color: var(--bs-tertiary-bg); +} + +.navbar-nav > li > a { + border-radius: var(--border-radius-button); + transition: 200ms; +} + +.navbar-nav a.nav-link:focus, +.navbar-nav a.nav-link:hover { + background-color: var(--bs-primary-border-subtle); +} + +.navbar-nav .nav-link.active, +.navbar-nav .nav-link.show { + color: var(--bs-link-hover-color); +} + +/** SEARCH AND FILTER **/ +input.form-control { + border-radius: var(--border-radius-button); +} + +form.filter { + margin: 0.3rem; +} + +/** ALERTS **/ +.alert { + padding: 0; + border: none; + box-shadow: var(--card-box-shadow); +} + +.alert > p { + padding: 0.2rem 0.7rem 0.7rem 1rem; +} + +.alert > ul { + margin-bottom: 0; + padding: 5px 40px; +} + +.alert > h5 { + padding: 0.5rem 0.7rem 0.7rem 1rem; + border-radius: var(--bs-border-radius) var(--bs-border-radius) 0 0; + font-weight: bold; + text-transform: capitalize; +} + +.alert-info { + color: var(--material-blue-dark); + background-color: var(--material-info-background); +} + +[data-bs-theme='dark'] .alert-info { + color: var(--material-blue-light); + background-color: var(--material-info-background-dark); +} + +.alert-info > h5 { + background-color: var(--material-info-header); +} + +.alert-warning { + color: var(--material-yellow-dark); + background-color: var(--material-warning-background); +} + +[data-bs-theme='dark'] .alert-warning { + color: var(--material-yellow-light); + background-color: var(--material-warning-background-dark); +} + +.alert-warning > h5 { + background-color: var(--material-warning-header); +} + +.alert-danger { + color: var(--material-red-dark); + background-color: var(--material-danger-background); +} + +[data-bs-theme='dark'] .alert-danger { + color: var(--material-red-light); + background-color: var(--material-danger-background-dark); +} + +.alert-danger > h5 { + background-color: var(--material-danger-header); +} + +/* CODE HIGHLIGHT */ +code { + border-radius: var(--bs-border-radius); + margin: 4px 2px; + box-shadow: var(--card-box-shadow); +} diff --git a/Docs/toc.yml b/Docs/toc.yml new file mode 100644 index 0000000..1ffadad --- /dev/null +++ b/Docs/toc.yml @@ -0,0 +1,6 @@ +- name: Docs + href: docs/ +- name: System + href: api.System/ +- name: Web + href: api.Web/ \ No newline at end of file diff --git a/README.md b/README.md index 2fe592c..73752b9 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,17 @@ This repos contains tool classes in C# for multiple types of usage. -## Available tools +[Documentation](https://frapp42.github.io/website/docs/tools/) -* Request - * Makes API requests - * Namespace: `FrApps42.Web.API` +## Available packages -* IsOnline - * Checks if a device is online - * Namespace: `FrApps42.System.Net` +* `FrApp42.System` - [Link](https://www.nuget.org/packages/FrApp42.System) + * IsOnline: Ping a device with it's hostname or ip address to check if it's online + * Shutdown: Shutdown a device through SMB with it's hostname or ip address. It only work for Windows devices. + * WakeOnLan: Power on a device with it mac address. +* `FrApp42.Web` - [Link](https://www.nuget.org/packages/FrApp42.Web) + * Request: Make API request for any of your C# projects. ⚠️ *SOAP requests are not supported.* ⚠️ -* Shutdown - * Shutdowns a device with SMB - * Namespace: `FrApps42.System.Computer.Shutdown` +## Licence -* WakeOnLan - * Waked up a device from it's MAC address - * Namespace: `FrApps42.System.Net` +This project is under GPLv3 licence diff --git a/System.Test/IsOnlineTest.cs b/System.Test/IsOnlineTest.cs new file mode 100644 index 0000000..34d3906 --- /dev/null +++ b/System.Test/IsOnlineTest.cs @@ -0,0 +1,64 @@ +using FrApp42.System.Net; +using System.Net; + +namespace System.Test +{ + [TestClass] + public class IsOnlineTest + { + private readonly string _googleHostname = "google.com"; + private readonly IPAddress _googleIpAddress = IPAddress.Parse("8.8.8.8"); + + [TestMethod] + public void ConstructorWithHostnameAndDefaultTimeout() + { + IsOnline isOnline = new(_googleHostname); + Assert.AreEqual(5, isOnline.TimeOut, "Default timeout should be 5."); + } + + [TestMethod] + public void ConstructorWithHostnameAndCustomTimeout() + { + int customTimeout = 10; + + IsOnline isOnline = new(_googleHostname, customTimeout); + Assert.AreEqual(customTimeout, isOnline.TimeOut, $"Timeout should be {customTimeout}."); + } + + [TestMethod] + public void ConstructorWithIpAddressAndDefaultTimeout() + { + IsOnline isOnline = new(_googleIpAddress); + Assert.AreEqual(5, isOnline.TimeOut, "Default timeout should be 5."); + } + + [TestMethod] + public void ConstructorWithIpAddressAndCustomTimeout() + { + int customTimeout = 10; + + IsOnline isOnline = new(_googleIpAddress, customTimeout); + Assert.AreEqual(customTimeout, isOnline.TimeOut, $"Timeout should be {customTimeout}."); + } + + /*[TestMethod] + public void CheckWithHostname() + { + IsOnline isOnline = new(_googleHostname); + + bool result = isOnline.Check(); + + Assert.IsTrue(result, "Google hostname should be reachable."); + } + + [TestMethod] + public void CheckWithIpAddress() + { + IsOnline isOnline = new(_googleIpAddress); + + bool result = isOnline.Check(); + + Assert.IsTrue(result, "Google IP address should be reachable."); + }*/ + } +} diff --git a/System.Test/ShutdownTest.cs b/System.Test/ShutdownTest.cs new file mode 100644 index 0000000..313dc33 --- /dev/null +++ b/System.Test/ShutdownTest.cs @@ -0,0 +1,244 @@ +using FrApp42.System.Computer.Shutdown; + +namespace System.Test +{ + [TestClass] + public class ShutdownTest + { + private readonly string _hostname = "machine.local"; + + [TestMethod] + public void ClassicShutdown() + { + Shutdown shutdown = new(); + shutdown + .SetMachine(_hostname) + .ShutdownComputer(); + + string command = shutdown.GetCommand(); + string expectedCommand = $" /m {_hostname} /s"; + + Assert.AreEqual(expectedCommand, command, "The generated command does not match the expected command."); + } + + [TestMethod] + public void LogOffUser() + { + Shutdown shutdown = new(); + shutdown + .LogoutUser(); + + string command = shutdown.GetCommand(); + string expectedCommand = $" /l"; + + Assert.AreEqual(expectedCommand, command, "The generated command does not match the expected command."); + } + + [TestMethod] + public void ShutdownAndSignOnAuto() + { + Shutdown shutdown = new(); + shutdown + .ShutdownAndSignOn(); + + string command = shutdown.GetCommand(); + string expectedCommand = $" /sg"; + + Assert.AreEqual(expectedCommand, command, "The generated command does not match the expected command."); + } + + [TestMethod] + public void TimeOutAndComment() + { + Shutdown shutdown = new(); + shutdown + .SetMachine(_hostname) + .SetTimeOut(60) + .SetComment("Scheduled maintenance") + .ShutdownComputer(); + + string command = shutdown.GetCommand(); + string expectedCommand = $" /m {_hostname} /t 60 /c \"Scheduled maintenance\" /s"; + + Assert.AreEqual(expectedCommand, command, "The generated command does not match the expected command."); + } + + [TestMethod] + public void RebootComputer() + { + Shutdown shutdown = new(); + shutdown + .SetMachine(_hostname) + .Reboot(); + + string command = shutdown.GetCommand(); + string expectedCommand = $" /m {_hostname} /r"; + + Assert.AreEqual(expectedCommand, command, "The generated command does not match the expected command."); + } + + [TestMethod] + public void HibernateComputer() + { + Shutdown shutdown = new(); + shutdown + .SetMachine(_hostname) + .Hibernate(); + + string command = shutdown.GetCommand(); + string expectedCommand = $" /m {_hostname} /h"; + + Assert.AreEqual(expectedCommand, command, "The generated command does not match the expected command."); + } + + [TestMethod] + public void SoftShutdown() + { + Shutdown shutdown = new(); + shutdown + .SetMachine(_hostname) + .ShutdownComputer() + .Soft(); + + string command = shutdown.GetCommand(); + string expectedCommand = $" /m {_hostname} /s /soft"; + + Assert.AreEqual(expectedCommand, command, "The generated command does not match the expected command."); + } + + [TestMethod] + public void OpenBootOptionsTest() + { + Shutdown shutdown = new(); + shutdown + .SetMachine(_hostname) + .Reboot() + .OpenBootOptions(); + + string command = shutdown.GetCommand(); + string expectedCommand = $" /m {_hostname} /r /o"; + + Assert.AreEqual(expectedCommand, command, "The generated command does not match the expected command."); + } + + [TestMethod] + public void ForceShutdownComputer() + { + Shutdown shutdown = new(); + shutdown + .SetMachine(_hostname) + .ShutdownComputer() + .ForceShutdown(); + + string command = shutdown.GetCommand(); + string expectedCommand = $" /m {_hostname} /s /f"; + + Assert.AreEqual(expectedCommand, command, "The generated command does not match the expected command."); + } + + [TestMethod] + public void ShutdownWithComment() + { + Shutdown shutdown = new(); + shutdown + .SetMachine(_hostname) + .ShutdownComputer() + .SetComment("End of day shutdown"); + + string command = shutdown.GetCommand(); + string expectedCommand = $" /m {_hostname} /s /c \"End of day shutdown\""; + + Assert.AreEqual(expectedCommand, command, "The generated command does not match the expected command."); + } + + [TestMethod] + public void LogoutNotWithMachine() + { + Shutdown shutdown = new(); + shutdown + .SetMachine(_hostname) + .LogoutUser(); + + string command = shutdown.GetCommand(); + string expectedCommand = $" /m {_hostname}"; + + Assert.AreEqual(expectedCommand, command, "The command should not contain /l when /m is present."); + } + + [TestMethod] + public void LogoutNotWithTimeout() + { + Shutdown shutdown = new(); + shutdown + .SetTimeOut(60) + .LogoutUser(); + + string command = shutdown.GetCommand(); + string expectedCommand = $" /t 60"; + + Assert.AreEqual(expectedCommand, command, "The command should not contain /l when /t is present."); + } + + [TestMethod] + public void MachineNotWithLogout() + { + Shutdown shutdown = new(); + shutdown + .LogoutUser() + .SetMachine(_hostname); + + string command = shutdown.GetCommand(); + string expectedCommand = $" /l"; + + Assert.AreEqual(expectedCommand, command, "The command should not contain /m when /l is present."); + } + + [TestMethod] + public void TimeoutNotWithLogout() + { + Shutdown shutdown = new(); + shutdown + .LogoutUser() + .SetTimeOut(60); + + string command = shutdown.GetCommand(); + string expectedCommand = $" /l"; + + Assert.AreEqual(expectedCommand, command, "The command should not contain /t when /l is present."); + } + + [TestMethod] + public void NoDuplicateArguments() + { + Shutdown shutdown = new(); + shutdown + .SetMachine(_hostname) + .SetMachine(_hostname); + + string command = shutdown.GetCommand(); + string expectedCommand = $" /m {_hostname}"; + + Assert.AreEqual(expectedCommand, command, "The command should not contain duplicated /m arguments."); + + shutdown = new Shutdown(); + shutdown + .SetTimeOut(60) + .SetTimeOut(60); + + command = shutdown.GetCommand(); + expectedCommand = $" /t 60"; + + Assert.AreEqual(expectedCommand, command, "The command should not contain duplicated /t arguments."); + + shutdown = new Shutdown(); + shutdown + .ShutdownComputer() + .ShutdownComputer(); + + command = shutdown.GetCommand(); + expectedCommand = $" /s"; + + Assert.AreEqual(expectedCommand, command, "The command should not contain duplicated /s arguments."); + } + } +} diff --git a/System.Test/System.Test.csproj b/System.Test/System.Test.csproj new file mode 100644 index 0000000..3f37dea --- /dev/null +++ b/System.Test/System.Test.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/System.Test/WakeOnLanTest.cs b/System.Test/WakeOnLanTest.cs new file mode 100644 index 0000000..950aabd --- /dev/null +++ b/System.Test/WakeOnLanTest.cs @@ -0,0 +1,57 @@ +using FrApp42.System.Net; + +namespace System.Test +{ + [TestClass] + public class WakeOnLanTest + { + private readonly string _macAddressColon = "FF:FF:FF:FF:FF:FF"; + private readonly string _macAddressDash = "FF-FF-FF-FF-FF-FF"; + private readonly string _expectedFormattedMacAddress = "FFFFFFFFFFFF"; + + [TestMethod] + public void BuildMagicPacketWithColon() + { + BuildMagicPacketTest(_macAddressColon); + } + + [TestMethod] + public void BuildMagicPacketWithDash() + { + BuildMagicPacketTest(_macAddressDash); + } + + [TestMethod] + public void MacFormatterWithColon() + { + string actualFormattedMac = WakeOnLan.MacFormatter().Replace(_macAddressColon, ""); + + Assert.AreEqual(_expectedFormattedMacAddress, actualFormattedMac, "The MAC address was not formatted correctly."); + } + + [TestMethod] + public void MacFormatterWithDash() + { + string actualFormattedMac = WakeOnLan.MacFormatter().Replace(_macAddressDash, ""); + + Assert.AreEqual(_expectedFormattedMacAddress, actualFormattedMac, "The MAC address was not formatted correctly."); + } + + private void BuildMagicPacketTest(string macAddress) + { + byte[] expectedMagicPacket = new byte[102]; + + for (int i = 0; i < 6; i++) + expectedMagicPacket[i] = 0xFF; + + byte[] macBytes = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; + + for (int i = 0; i < 16; i++) + Array.Copy(macBytes, 0, expectedMagicPacket, 6 + i * 6, 6); + + byte[] actualMagicPacket = WakeOnLan.BuildMagicPacket(macAddress); + + CollectionAssert.AreEqual(expectedMagicPacket, actualMagicPacket, "The magic packet is not built correctly."); + } + } +} diff --git a/System/Computer/Awake/Awake.v1.cs b/System/Computer/Awake/Awake.v1.cs new file mode 100644 index 0000000..b16565a --- /dev/null +++ b/System/Computer/Awake/Awake.v1.cs @@ -0,0 +1,161 @@ +// Source Microsoft Powertoys +// License MIT +// https://github.com/microsoft/PowerToys + +using FrApp42.System.Computer.Awake.Models; +using FrApp42.System.Computer.Awake.Natives; +using NLog; + +namespace FrApp42.System.Computer.Awake.v1 +{ + public class Awake + { + + private static readonly Logger _log; + private static CancellationTokenSource _cts; + private static CancellationToken _ct; + + private static Task? _runnerThread; + + static Awake() { + _log = LogManager.GetCurrentClassLogger(); + _cts = new CancellationTokenSource(); + } + + #region Public Functions + + /// + /// Set Indefinite Keep Awake + /// + /// + public static void SetIndefiniteKeepAwake(bool keepDisplayOn = false) + { + SetIndefiniteKeepAwake(null, null, keepDisplayOn); + } + + /// + /// Set Indefinite Keep Awake + /// + /// + /// + /// + public static void SetIndefiniteKeepAwake(Action? callback, Action? failureCallback, bool keepDisplayOn = false) + { + _cts.Cancel(); + + try + { + if (_runnerThread != null && !_runnerThread.IsCanceled) + { + _runnerThread.Wait(_ct); + } + } + catch (OperationCanceledException) + { + _log.Info("Confirmed background thread cancellation when setting indefinite keep awake."); + } + + _cts = new CancellationTokenSource(); + _ct = _cts.Token; + + try + { + _runnerThread = Task.Run(() => RunIndefiniteLoop(keepDisplayOn), _ct) + .ContinueWith((result) => callback(result.Result), TaskContinuationOptions.OnlyOnRanToCompletion) + .ContinueWith((result) => failureCallback, TaskContinuationOptions.NotOnRanToCompletion); + } + catch (Exception ex) + { + _log.Error(ex.Message); + } + } + + /// + /// Disable Awake + /// + public static void SetNoKeepAwake() + { + _cts.Cancel(); + + try + { + if (_runnerThread != null && !_runnerThread.IsCanceled) + { + _runnerThread.Wait(_ct); + } + } + catch (OperationCanceledException) + { + _log.Info("Confirmed background thread cancellation when disabling explicit keep awake."); + } + } + + #endregion + + #region Private Functions + + private static ExecutionState ComputeAwakeState(bool keepDisplayOn) + { + return keepDisplayOn + ? ExecutionState.ES_SYSTEM_REQUIRED | ExecutionState.ES_DISPLAY_REQUIRED | ExecutionState.ES_CONTINUOUS + : ExecutionState.ES_SYSTEM_REQUIRED | ExecutionState.ES_CONTINUOUS; + } + + /// + /// Sets the computer awake state using the native Win32 SetThreadExecutionState API. This + /// function is just a nice-to-have wrapper that helps avoid tracking the success or failure of + /// the call. + /// + /// Single or multiple EXECUTION_STATE entries. + /// true if successful, false if failed + private static bool SetAwakeState(ExecutionState state) + { + try + { + var stateResult = Bridge.SetThreadExecutionState(state); + return stateResult != 0; + } + catch + { + return false; + } + } + + + private static bool RunIndefiniteLoop(bool keepDisplayOn = false) + { + bool success; + if (keepDisplayOn) + { + success = SetAwakeState(ExecutionState.ES_SYSTEM_REQUIRED | ExecutionState.ES_DISPLAY_REQUIRED | ExecutionState.ES_CONTINUOUS); + } + else + { + success = SetAwakeState(ExecutionState.ES_SYSTEM_REQUIRED | ExecutionState.ES_CONTINUOUS); + } + + try + { + if (success) + { + _log.Info($"Initiated indefinite keep awake in background thread: {Bridge.GetCurrentThreadId()}. Screen on: {keepDisplayOn}"); + + WaitHandle.WaitAny(new[] { _ct.WaitHandle }); + } + else + { + _log.Info("Could not successfully set up indefinite keep awake."); + } + } + catch (OperationCanceledException ex) + { + // Task was clearly cancelled. + _log.Info($"Background thread termination: {Bridge.GetCurrentThreadId()}. Message: {ex.Message}"); + } + return success; + } + + #endregion + + } +} diff --git a/System/Computer/Awake/Awake.v2.cs b/System/Computer/Awake/Awake.v2.cs new file mode 100644 index 0000000..76c4744 --- /dev/null +++ b/System/Computer/Awake/Awake.v2.cs @@ -0,0 +1,205 @@ +// Source Microsoft Powertoys +// License MIT +// https://github.com/microsoft/PowerToys + +using FrApp42.System.Computer.Awake.Models; +using FrApp42.System.Computer.Awake.Natives; +using FrApp42.System.Computer.Awake.Statics; +using NLog; +using System.Collections.Concurrent; +using System.Reactive.Linq; + +namespace FrApp42.System.Computer.Awake.v2 +{ + public class Awake + { + + private static readonly Logger _log; + private static CancellationTokenSource _cts; + private static CancellationToken _ct; + + private static readonly BlockingCollection _stateQueue; + + static Awake() + { + _log = LogManager.GetCurrentClassLogger(); + _cts = new CancellationTokenSource(); + StartMonitor(); + } + + #region Public Functions + + /// + /// Set Indefinite Keep Awake + /// + /// + public static void SetIndefiniteKeepAwake(bool keepDisplayOn = false) + { + CancelExistingThread(); + + _stateQueue.Add(ComputeAwakeState(keepDisplayOn)); + } + + /// + /// Set Keep Awake until specified DateTimeOffset + /// + /// + /// + public static void SetExpirableKeepAwake(DateTimeOffset expireAt, bool keepDisplayOn = true) + { + _log.Info($"Expirable keep-awake. Expected expiration date/time: {expireAt} with display on setting set to {keepDisplayOn}."); + + CancelExistingThread(); + + if (expireAt > DateTimeOffset.Now) + { + _log.Info($"Starting expirable log for {expireAt}"); + _stateQueue.Add(ComputeAwakeState(keepDisplayOn)); + + Observable.Timer(expireAt - DateTimeOffset.Now).Subscribe( + _ => + { + _log.Info($"Completed expirable keep-awake."); + CancelExistingThread(); + + _log.Info("Exiting after expirable keep awake."); + CompleteExit(Environment.ExitCode); + }, + _cts.Token); + } + else + { + _log.Info("The specified target date and time is not in the future."); + _log.Info($"Current time: {DateTimeOffset.Now}\tTarget time: {expireAt}"); + } + } + + /// + /// Set Keep Awake for specified seconds + /// + /// + /// + /// + public static void SetTimedKeepAwake(uint seconds, bool keepDisplayOn = true, bool logElapsedSeconds = false) + { + _log.Info($"Timed keep-awake. Expected runtime: {seconds} seconds with display on setting set to {keepDisplayOn}."); + CancelExistingThread(); + + _log.Info($"Timed keep awake started for {seconds} seconds."); + _stateQueue.Add(ComputeAwakeState(keepDisplayOn)); + + IObservable timerObservable = Observable.Timer(TimeSpan.FromSeconds(seconds)); + IObservable intervalObservable = Observable.Interval(TimeSpan.FromSeconds(1)).TakeUntil(timerObservable); + + IObservable combinedObservable = Observable.CombineLatest(intervalObservable, timerObservable.StartWith(0), (elapsedSeconds, _) => elapsedSeconds + 1); + + combinedObservable.Subscribe( + elapsedSeconds => + { + if(logElapsedSeconds) + { + uint timeRemaining = seconds - (uint)elapsedSeconds; + if (timeRemaining >= 0) + { + _log.Info($"[Awake]\n{TimeSpan.FromSeconds(timeRemaining).ToHumanReadableString()}"); + + } + } + }, + () => + { + Console.WriteLine("Completed timed thread."); + CancelExistingThread(); + + _log.Info("Exiting after timed keep-awake."); + CompleteExit(Environment.ExitCode); + }, + _cts.Token); + } + + /// + /// Disable Kepp Awake + /// + public static void SetNoKeepAwake() + { + CancelExistingThread(); + } + + #endregion + + #region Private Functions + + private static void StartMonitor() + { + Thread monitorThread = new(() => + { + Thread.CurrentThread.IsBackground = true; + while (true) + { + ExecutionState state = _stateQueue.Take(); + + _log.Info($"Setting state to {state}"); + + SetAwakeState(state); + } + }); + monitorThread.Start(); + } + + /// + /// Sets the computer awake state using the native Win32 SetThreadExecutionState API. This + /// function is just a nice-to-have wrapper that helps avoid tracking the success or failure of + /// the call. + /// + /// Single or multiple EXECUTION_STATE entries. + /// true if successful, false if failed + private static bool SetAwakeState(ExecutionState state) + { + try + { + var stateResult = Bridge.SetThreadExecutionState(state); + return stateResult != 0; + } + catch + { + return false; + } + } + + private static ExecutionState ComputeAwakeState(bool keepDisplayOn) + { + return keepDisplayOn + ? ExecutionState.ES_SYSTEM_REQUIRED | ExecutionState.ES_DISPLAY_REQUIRED | ExecutionState.ES_CONTINUOUS + : ExecutionState.ES_SYSTEM_REQUIRED | ExecutionState.ES_CONTINUOUS; + } + + private static void CancelExistingThread() + { + _log.Info($"Attempting to ensure that the thread is properly cleaned up..."); + + // Resetting the thread state. + _stateQueue.Add(ExecutionState.ES_CONTINUOUS); + + // Next, make sure that any existing background threads are terminated. + _cts.Cancel(); + _cts.Dispose(); + + _cts = new CancellationTokenSource(); + _log.Info("Instantiating of new token source and thread token completed."); + } + + /// + /// Performs a clean exit from Awake. + /// + /// Exit code to exit with. + private static void CompleteExit(int exitCode) + { + CancelExistingThread(); + + Bridge.PostQuitMessage(exitCode); + Environment.Exit(exitCode); + } + + #endregion + } +} diff --git a/System/Computer/Awake/Models/BatteryReportingScale.cs b/System/Computer/Awake/Models/BatteryReportingScale.cs new file mode 100644 index 0000000..6f21355 --- /dev/null +++ b/System/Computer/Awake/Models/BatteryReportingScale.cs @@ -0,0 +1,12 @@ +// Source Microsoft Powertoys +// License MIT +// https://github.com/microsoft/PowerToys + +namespace FrApp42.System.Computer.Awake.Models +{ + internal struct BatteryReportingScale + { + public uint Granularity; + public uint Capacity; + } +} diff --git a/System/Computer/Awake/Models/ControlType.cs b/System/Computer/Awake/Models/ControlType.cs new file mode 100644 index 0000000..d98850e --- /dev/null +++ b/System/Computer/Awake/Models/ControlType.cs @@ -0,0 +1,21 @@ +// Source Microsoft Powertoys +// License MIT +// https://github.com/microsoft/PowerToys + +namespace FrApp42.System.Computer.Awake.Models +{ + /// + /// The type of control signal received by the handler. + /// + /// + /// See HandlerRoutine callback function. + /// + internal enum ControlType + { + CTRL_C_EVENT = 0, + CTRL_BREAK_EVENT = 1, + CTRL_CLOSE_EVENT = 2, + CTRL_LOGOFF_EVENT = 5, + CTRL_SHUTDOWN_EVENT = 6, + } +} diff --git a/System/Computer/Awake/Models/ExecutionState.cs b/System/Computer/Awake/Models/ExecutionState.cs new file mode 100644 index 0000000..7063f46 --- /dev/null +++ b/System/Computer/Awake/Models/ExecutionState.cs @@ -0,0 +1,15 @@ +// Source Microsoft Powertoys +// License MIT +// https://github.com/microsoft/PowerToys + +namespace FrApp42.System.Computer.Awake.Models +{ + [Flags] + internal enum ExecutionState : uint + { + ES_AWAYMODE_REQUIRED = 0x00000040, + ES_CONTINUOUS = 0x80000000, + ES_DISPLAY_REQUIRED = 0x00000002, + ES_SYSTEM_REQUIRED = 0x00000001, + } +} diff --git a/System/Computer/Awake/Models/MenuInfo.cs b/System/Computer/Awake/Models/MenuInfo.cs new file mode 100644 index 0000000..c5bbf2c --- /dev/null +++ b/System/Computer/Awake/Models/MenuInfo.cs @@ -0,0 +1,20 @@ +// Source Microsoft Powertoys +// License MIT +// https://github.com/microsoft/PowerToys + +using System.Runtime.InteropServices; + +namespace FrApp42.System.Computer.Awake.Models +{ + [StructLayout(LayoutKind.Sequential)] + internal struct MenuInfo + { + public uint CbSize; // Size of the structure, in bytes + public uint FMask; // Specifies which members of the structure are valid + public uint DwStyle; // Style of the menu + public uint CyMax; // Maximum height of the menu, in pixels + public IntPtr HbrBack; // Handle to the brush used for the menu's background + public uint DwContextHelpID; // Context help ID + public IntPtr DwMenuData; // Pointer to the menu's user data + } +} diff --git a/System/Computer/Awake/Models/Msg.cs b/System/Computer/Awake/Models/Msg.cs new file mode 100644 index 0000000..304143b --- /dev/null +++ b/System/Computer/Awake/Models/Msg.cs @@ -0,0 +1,18 @@ +// Source Microsoft Powertoys +// License MIT +// https://github.com/microsoft/PowerToys + +using System.Drawing; + +namespace FrApp42.System.Computer.Awake.Models +{ + internal struct Msg + { + public IntPtr HWnd; + public uint Message; + public IntPtr WParam; + public IntPtr LParam; + public uint Time; + public Point Pt; + } +} diff --git a/System/Computer/Awake/Models/SingleThreadSynchronizationContext.cs b/System/Computer/Awake/Models/SingleThreadSynchronizationContext.cs new file mode 100644 index 0000000..8456aa8 --- /dev/null +++ b/System/Computer/Awake/Models/SingleThreadSynchronizationContext.cs @@ -0,0 +1,58 @@ +// Source Microsoft Powertoys +// License MIT +// https://github.com/microsoft/PowerToys + +namespace FrApp42.System.Computer.Awake.Models +{ + internal sealed class SingleThreadSynchronizationContext : SynchronizationContext + { + private readonly Queue> queue = + new(); + +#pragma warning disable CS8765 // Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes). + public override void Post(SendOrPostCallback d, object state) +#pragma warning restore CS8765 // Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes). + { + lock (queue) + { + queue.Enqueue(Tuple.Create(d, state)); + Monitor.Pulse(queue); + } + } + + public void BeginMessageLoop() + { + while (true) + { + Tuple work; + lock (queue) + { + while (queue.Count == 0) + { + Monitor.Wait(queue); + } + + work = queue.Dequeue(); + } + + if (work == null) + { + break; + } + + work.Item1(work.Item2); + } + } + + public void EndMessageLoop() + { + lock (queue) + { +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + queue.Enqueue(null); // Signal the end of the message loop +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. + Monitor.Pulse(queue); + } + } + } +} diff --git a/System/Computer/Awake/Models/SystemPowerCapabilities.cs b/System/Computer/Awake/Models/SystemPowerCapabilities.cs new file mode 100644 index 0000000..107ba14 --- /dev/null +++ b/System/Computer/Awake/Models/SystemPowerCapabilities.cs @@ -0,0 +1,70 @@ +// Source Microsoft Powertoys +// License MIT +// https://github.com/microsoft/PowerToys + +using System.Runtime.InteropServices; + +namespace FrApp42.System.Computer.Awake.Models +{ + internal struct SystemPowerCapabilities + { + [MarshalAs(UnmanagedType.U1)] + public bool PowerButtonPresent; + [MarshalAs(UnmanagedType.U1)] + public bool SleepButtonPresent; + [MarshalAs(UnmanagedType.U1)] + public bool LidPresent; + [MarshalAs(UnmanagedType.U1)] + public bool SystemS1; + [MarshalAs(UnmanagedType.U1)] + public bool SystemS2; + [MarshalAs(UnmanagedType.U1)] + public bool SystemS3; + [MarshalAs(UnmanagedType.U1)] + public bool SystemS4; + [MarshalAs(UnmanagedType.U1)] + public bool SystemS5; + [MarshalAs(UnmanagedType.U1)] + public bool HiberFilePresent; + [MarshalAs(UnmanagedType.U1)] + public bool FullWake; + [MarshalAs(UnmanagedType.U1)] + public bool VideoDimPresent; + [MarshalAs(UnmanagedType.U1)] + public bool ApmPresent; + [MarshalAs(UnmanagedType.U1)] + public bool UpsPresent; + [MarshalAs(UnmanagedType.U1)] + public bool ThermalControl; + [MarshalAs(UnmanagedType.U1)] + public bool ProcessorThrottle; + public byte ProcessorMinThrottle; + public byte ProcessorMaxThrottle; + [MarshalAs(UnmanagedType.U1)] + public bool FastSystemS4; + [MarshalAs(UnmanagedType.U1)] + public bool Hiberboot; + [MarshalAs(UnmanagedType.U1)] + public bool WakeAlarmPresent; + [MarshalAs(UnmanagedType.U1)] + public bool AoAc; + [MarshalAs(UnmanagedType.U1)] + public bool DiskSpinDown; + public byte HiberFileType; + [MarshalAs(UnmanagedType.U1)] + public bool AoAcConnectivitySupported; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] + private readonly byte[] spare3; + [MarshalAs(UnmanagedType.U1)] + public bool SystemBatteriesPresent; + [MarshalAs(UnmanagedType.U1)] + public bool BatteriesAreShortTerm; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] + public BatteryReportingScale[] BatteryScale; + public SystemPowerState AcOnLineWake; + public SystemPowerState SoftLidWake; + public SystemPowerState RtcWake; + public SystemPowerState MinDeviceWakeState; + public SystemPowerState DefaultLowLatencyWake; + } +} diff --git a/System/Computer/Awake/Models/SystemPowerState.cs b/System/Computer/Awake/Models/SystemPowerState.cs new file mode 100644 index 0000000..8578abd --- /dev/null +++ b/System/Computer/Awake/Models/SystemPowerState.cs @@ -0,0 +1,24 @@ +// Source Microsoft Powertoys +// License MIT +// https://github.com/microsoft/PowerToys + +namespace FrApp42.System.Computer.Awake.Models +{ + /// + /// Represents the system power state. + /// + /// + /// See System power states. + /// + internal enum SystemPowerState + { + PowerSystemUnspecified = 0, + PowerSystemWorking = 1, + PowerSystemSleeping1 = 2, + PowerSystemSleeping2 = 3, + PowerSystemSleeping3 = 4, + PowerSystemHibernate = 5, + PowerSystemShutdown = 6, + PowerSystemMaximum = 7, + } +} diff --git a/System/Computer/Awake/Models/WndClassEx.cs b/System/Computer/Awake/Models/WndClassEx.cs new file mode 100644 index 0000000..d0a071d --- /dev/null +++ b/System/Computer/Awake/Models/WndClassEx.cs @@ -0,0 +1,25 @@ +// Source Microsoft Powertoys +// License MIT +// https://github.com/microsoft/PowerToys + +using System.Runtime.InteropServices; + +namespace FrApp42.System.Computer.Awake.Models +{ + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct WndClassEx + { + public uint CbSize; + public uint Style; + public IntPtr LpfnWndProc; + public int CbClsExtra; + public int CbWndExtra; + public IntPtr HInstance; + public IntPtr HIcon; + public IntPtr HCursor; + public IntPtr HbrBackground; + public string LpszMenuName; + public string LpszClassName; + public IntPtr HIconSm; + } +} diff --git a/System/Computer/Awake/Natives/Bridge.cs b/System/Computer/Awake/Natives/Bridge.cs new file mode 100644 index 0000000..3487663 --- /dev/null +++ b/System/Computer/Awake/Natives/Bridge.cs @@ -0,0 +1,105 @@ +// Source Microsoft Powertoys +// License MIT +// https://github.com/microsoft/PowerToys + +using FrApp42.System.Computer.Awake.Models; +using System.Drawing; +using System.Runtime.InteropServices; + +namespace FrApp42.System.Computer.Awake.Natives +{ + internal sealed class Bridge + { + [UnmanagedFunctionPointer(CallingConvention.Winapi, SetLastError = true)] + internal delegate int WndProcDelegate(IntPtr hWnd, uint message, IntPtr wParam, IntPtr lParam); + + [DllImport("Powrprof.dll", SetLastError = true)] + internal static extern bool GetPwrCapabilities(out SystemPowerCapabilities lpSystemPowerCapabilities); + + //[DllImport("kernel32.dll", SetLastError = true)] + //internal static extern bool SetConsoleCtrlHandler(ConsoleEventHandler handler, bool add); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + internal static extern ExecutionState SetThreadExecutionState(ExecutionState esFlags); + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern uint GetCurrentThreadId(); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool AllocConsole(); + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern bool SetStdHandle(int nStdHandle, IntPtr hHandle); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + internal static extern IntPtr CreateFile( + [MarshalAs(UnmanagedType.LPWStr)] string filename, + [MarshalAs(UnmanagedType.U4)] uint access, + [MarshalAs(UnmanagedType.U4)] FileShare share, + IntPtr securityAttributes, + [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition, + [MarshalAs(UnmanagedType.U4)] FileAttributes flagsAndAttributes, + IntPtr templateFile); + + [DllImport("user32.dll", SetLastError = true)] + internal static extern IntPtr CreatePopupMenu(); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern bool InsertMenu(IntPtr hMenu, uint uPosition, uint uFlags, uint uIDNewItem, string lpNewItem); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool TrackPopupMenuEx(IntPtr hMenu, uint uFlags, int x, int y, IntPtr hWnd, IntPtr lptpm); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern IntPtr SendMessage(IntPtr hWnd, uint msg, nuint wParam, string lParam); + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool DestroyMenu(IntPtr hMenu); + + [DllImport("user32.dll")] + internal static extern bool DestroyWindow(IntPtr hWnd); + + [DllImport("user32.dll")] + internal static extern void PostQuitMessage(int nExitCode); + + [DllImport("user32.dll", SetLastError = true)] + internal static extern bool TranslateMessage(ref Msg lpMsg); + + [DllImport("user32.dll", SetLastError = true)] + internal static extern IntPtr DispatchMessage(ref Msg lpMsg); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern IntPtr RegisterClassEx(ref WndClassEx lpwcx); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern IntPtr CreateWindowEx(uint dwExStyle, string lpClassName, string lpWindowName, uint dwStyle, int x, int y, int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam); + + [DllImport("user32.dll", SetLastError = true)] + internal static extern int DefWindowProc(IntPtr hWnd, uint message, IntPtr wParam, IntPtr lParam); + + [DllImport("user32.dll", SetLastError = true)] + internal static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool GetCursorPos(out Point lpPoint); + + [DllImport("user32.dll")] + internal static extern bool ScreenToClient(IntPtr hWnd, ref Point lpPoint); + + [DllImport("user32.dll")] + internal static extern bool GetMessage(out Msg lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax); + + [DllImport("user32.dll", SetLastError = true)] + internal static extern bool UpdateWindow(IntPtr hWnd); + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool SetMenuInfo(IntPtr hMenu, ref MenuInfo lpcmi); + + [DllImport("user32.dll")] + internal static extern bool SetForegroundWindow(IntPtr hWnd); + } +} diff --git a/System/Computer/Awake/Natives/Constants.cs b/System/Computer/Awake/Natives/Constants.cs new file mode 100644 index 0000000..c43df12 --- /dev/null +++ b/System/Computer/Awake/Natives/Constants.cs @@ -0,0 +1,52 @@ +// Source Microsoft Powertoys +// License MIT +// https://github.com/microsoft/PowerToys + +namespace FrApp42.System.Computer.Awake.Natives +{ + [global::System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Win32 API convention.")] + internal sealed class Constants + { + // Window Messages + internal const uint WM_COMMAND = 0x0111; + internal const uint WM_USER = 0x0400U; + internal const uint WM_CLOSE = 0x0010; + internal const int WM_DESTROY = 0x0002; + internal const int WM_LBUTTONDOWN = 0x0201; + internal const int WM_RBUTTONDOWN = 0x0204; + + // Menu Flags + internal const uint MF_BYPOSITION = 1024; + internal const uint MF_STRING = 0; + internal const uint MF_SEPARATOR = 0x00000800; + internal const uint MF_POPUP = 0x00000010; + internal const uint MF_UNCHECKED = 0x00000000; + internal const uint MF_CHECKED = 0x00000008; + internal const uint MF_ENABLED = 0x00000000; + internal const uint MF_DISABLED = 0x00000002; + + // Standard Handles + internal const int STD_OUTPUT_HANDLE = -11; + + // Generic Access Rights + internal const uint GENERIC_WRITE = 0x40000000; + internal const uint GENERIC_READ = 0x80000000; + + // Notification Icons + internal const int NIF_ICON = 0x00000002; + internal const int NIF_MESSAGE = 0x00000001; + internal const int NIF_TIP = 0x00000004; + internal const int NIM_ADD = 0x00000000; + internal const int NIM_DELETE = 0x00000002; + internal const int NIM_MODIFY = 0x00000001; + + // Track Popup Menu Flags + internal const uint TPM_LEFT_ALIGN = 0x0000; + internal const uint TPM_BOTTOMALIGN = 0x0020; + internal const uint TPM_LEFT_BUTTON = 0x0000; + + // Menu Item Info Flags + internal const uint MNS_AUTO_DISMISS = 0x10000000; + internal const uint MIM_STYLE = 0x00000010; + } +} diff --git a/System/Computer/Awake/README.md b/System/Computer/Awake/README.md new file mode 100644 index 0000000..e4605c1 --- /dev/null +++ b/System/Computer/Awake/README.md @@ -0,0 +1,3 @@ +### Powertoys + +Code is extracte from PowerToys \ No newline at end of file diff --git a/System/Computer/Awake/Statics/Constants.cs b/System/Computer/Awake/Statics/Constants.cs new file mode 100644 index 0000000..31e241c --- /dev/null +++ b/System/Computer/Awake/Statics/Constants.cs @@ -0,0 +1,22 @@ +// Source Microsoft Powertoys +// License MIT +// https://github.com/microsoft/PowerToys + +namespace FrApp42.System.Computer.Awake.Statics +{ + internal static class Constants + { + internal const string AppName = "Awake"; + internal const string FullAppName = "PowerToys " + AppName; + internal const string TrayWindowId = "Awake.MessageWindow"; + internal const string BuildRegistryLocation = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion"; + + // PowerToys Awake build code name. Used for exact logging + // that does not map to PowerToys broad version schema to pinpoint + // internal issues easier. + // Format of the build ID is: CODENAME_MMDDYYYY, where MMDDYYYY + // is representative of the date when the last change was made before + // the pull request is issued. + internal const string BuildId = "DAISY023_04102024"; + } +} diff --git a/System/Computer/Awake/Statics/ExtensionMethods.cs b/System/Computer/Awake/Statics/ExtensionMethods.cs new file mode 100644 index 0000000..68b0945 --- /dev/null +++ b/System/Computer/Awake/Statics/ExtensionMethods.cs @@ -0,0 +1,33 @@ +// Source Microsoft Powertoys +// License MIT +// https://github.com/microsoft/PowerToys + + +namespace FrApp42.System.Computer.Awake.Statics +{ + internal static class ExtensionMethods + { + public static void AddRange(this ICollection target, IEnumerable source) + { + ArgumentNullException.ThrowIfNull(target); + ArgumentNullException.ThrowIfNull(source); + + foreach (var element in source) + { + target.Add(element); + } + } + + public static string ToHumanReadableString(this TimeSpan timeSpan) + { + // Get days, hours, minutes, and seconds from the TimeSpan + int days = timeSpan.Days; + int hours = timeSpan.Hours; + int minutes = timeSpan.Minutes; + int seconds = timeSpan.Seconds; + + // Format the string based on the presence of days, hours, minutes, and seconds + return $"{days:D2} {hours:D2} {minutes:D2} {seconds:D2}"; + } + } +} diff --git a/System/Net/WakeOnLan.cs b/System/Net/WakeOnLan.cs index af1ed4b..b8fa4cf 100644 --- a/System/Net/WakeOnLan.cs +++ b/System/Net/WakeOnLan.cs @@ -90,6 +90,6 @@ public static async Task SendWakeOnLan(IPAddress localIpAddress, IPAddress multi /// /// A Regex object for formatting MAC addresses. [GeneratedRegex("[: -]")] - private static partial Regex MacFormatter(); + public static partial Regex MacFormatter(); } } diff --git a/System/System.csproj b/System/System.csproj index 3694c6c..8047a78 100644 --- a/System/System.csproj +++ b/System/System.csproj @@ -21,6 +21,8 @@ FrenchyApps42 Logo.png 1.0.1 + 1.0.1.1 + 1.0.1.1 @@ -34,6 +36,11 @@ + + + + + True diff --git a/Tools.sln b/Tools.sln index d82f748..afaa27a 100644 --- a/Tools.sln +++ b/Tools.sln @@ -7,6 +7,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System", "System\System.csp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Web", "Web\Web.csproj", "{3872357E-B751-4C1B-AFD7-E4883E2FBB4C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Test", "System.Test\System.Test.csproj", "{1501A81F-3308-4471-BCDF-113AC9BF13EA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Web.Test", "Web.Test\Web.Test.csproj", "{69DD9B31-30B7-43FC-A43B-462FDB56A4FF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +25,14 @@ Global {3872357E-B751-4C1B-AFD7-E4883E2FBB4C}.Debug|Any CPU.Build.0 = Debug|Any CPU {3872357E-B751-4C1B-AFD7-E4883E2FBB4C}.Release|Any CPU.ActiveCfg = Release|Any CPU {3872357E-B751-4C1B-AFD7-E4883E2FBB4C}.Release|Any CPU.Build.0 = Release|Any CPU + {1501A81F-3308-4471-BCDF-113AC9BF13EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1501A81F-3308-4471-BCDF-113AC9BF13EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1501A81F-3308-4471-BCDF-113AC9BF13EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1501A81F-3308-4471-BCDF-113AC9BF13EA}.Release|Any CPU.Build.0 = Release|Any CPU + {69DD9B31-30B7-43FC-A43B-462FDB56A4FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {69DD9B31-30B7-43FC-A43B-462FDB56A4FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69DD9B31-30B7-43FC-A43B-462FDB56A4FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {69DD9B31-30B7-43FC-A43B-462FDB56A4FF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Web.Test/Models/HttpBinDeleteResponse.cs b/Web.Test/Models/HttpBinDeleteResponse.cs new file mode 100644 index 0000000..1fcd3bc --- /dev/null +++ b/Web.Test/Models/HttpBinDeleteResponse.cs @@ -0,0 +1,6 @@ +namespace Web.Test.Models +{ + public class HttpBinDeleteResponse : HttpBinResponseBase + { + } +} diff --git a/Web.Test/Models/HttpBinGetResponse.cs b/Web.Test/Models/HttpBinGetResponse.cs new file mode 100644 index 0000000..dede37f --- /dev/null +++ b/Web.Test/Models/HttpBinGetResponse.cs @@ -0,0 +1,6 @@ +namespace Web.Test.Models +{ + public class HttpBinGetResponse : HttpBinResponseBase + { + } +} diff --git a/Web.Test/Models/HttpBinPatchResponse.cs b/Web.Test/Models/HttpBinPatchResponse.cs new file mode 100644 index 0000000..4e3f542 --- /dev/null +++ b/Web.Test/Models/HttpBinPatchResponse.cs @@ -0,0 +1,6 @@ +namespace Web.Test.Models +{ + public class HttpBinPatchResponse : HttpBinPostResponse + { + } +} diff --git a/Web.Test/Models/HttpBinPostFileResponse.cs b/Web.Test/Models/HttpBinPostFileResponse.cs new file mode 100644 index 0000000..35c1dbb --- /dev/null +++ b/Web.Test/Models/HttpBinPostFileResponse.cs @@ -0,0 +1,6 @@ +namespace Web.Test.Models +{ + public class HttpBinPostFileResponse : HttpBinPostResponse + { + } +} diff --git a/Web.Test/Models/HttpBinPostResponse.cs b/Web.Test/Models/HttpBinPostResponse.cs new file mode 100644 index 0000000..fd0ece0 --- /dev/null +++ b/Web.Test/Models/HttpBinPostResponse.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +namespace Web.Test.Models +{ + public class HttpBinPostResponse : HttpBinResponseBase + { + [JsonPropertyName("data")] + public string Data { get; set; } + + [JsonPropertyName("files")] + public Dictionary Files { get; set; } + + [JsonPropertyName("form")] + public Dictionary Form { get; set; } + + [JsonPropertyName("json")] + public object Json { get; set; } + } +} diff --git a/Web.Test/Models/HttpBinPutResponse.cs b/Web.Test/Models/HttpBinPutResponse.cs new file mode 100644 index 0000000..3a3f889 --- /dev/null +++ b/Web.Test/Models/HttpBinPutResponse.cs @@ -0,0 +1,6 @@ +namespace Web.Test.Models +{ + public class HttpBinPutResponse : HttpBinPostResponse + { + } +} diff --git a/Web.Test/Models/HttpBinResponseBase.cs b/Web.Test/Models/HttpBinResponseBase.cs new file mode 100644 index 0000000..2f930af --- /dev/null +++ b/Web.Test/Models/HttpBinResponseBase.cs @@ -0,0 +1,40 @@ +using System.Text.Json.Serialization; + +namespace Web.Test.Models +{ + public class HttpBinResponseBase + { + [JsonPropertyName("args")] + public Dictionary Args { get; set; } + + [JsonPropertyName("headers")] + public HttpBinResponseHeaders Headers { get; set; } + + [JsonPropertyName("origin")] + public string Origin { get; set; } + + [JsonPropertyName("url")] + public string Url { get; set; } + } + + public class HttpBinResponseHeaders + { + [JsonPropertyName("Accept")] + public string Accept { get; set; } + + [JsonPropertyName("Accept-Encoding")] + public string AcceptEncoding { get; set; } + + [JsonPropertyName("Accept-Language")] + public string AcceptLanguage { get; set; } + + [JsonPropertyName("Host")] + public string Host { get; set; } + + [JsonPropertyName("User-Agent")] + public string UserAgent { get; set; } + + [JsonPropertyName("X-Amzn-Trace-Id")] + public string AmazonTraceId { get; set; } + } +} diff --git a/Web.Test/RequestTest.cs b/Web.Test/RequestTest.cs new file mode 100644 index 0000000..ae6877b --- /dev/null +++ b/Web.Test/RequestTest.cs @@ -0,0 +1,139 @@ +using FrApp42.Web.API; +using System.Text; +using Web.Test.Models; + +namespace Web.Test +{ + [TestClass] + public class RequestTest + { + private const string TestGetUrl = "https://httpbin.org/get"; + private const string TestDeleteUrl = "https://httpbin.org/delete"; + private const string TestPatchUrl = "https://httpbin.org/patch"; + private const string TestPostUrl = "https://httpbin.org/post"; + private const string TestPutUrl = "https://httpbin.org/put"; + + private const string TestGetJpegImageUrl = "https://httpbin.org/image/jpeg"; + private const string TestGetPngImageUrl = "https://httpbin.org/image/png"; + private const string TestGetSvgImageUrl = "https://httpbin.org/image/svg"; + private const string TestGetWebpImageUrl = "https://httpbin.org/image/webp"; + + [TestMethod] + public async Task SendGetRequest() + { + Request request = new(TestGetUrl, HttpMethod.Get); + Result result = await request.Run(); + + AssertResponse(result, TestGetUrl); + } + + [TestMethod] + public async Task SendDeleteRequest() + { + Request request = new(TestDeleteUrl, HttpMethod.Delete); + Result result = await request.Run(); + + AssertResponse(result, TestDeleteUrl); + } + + [TestMethod] + public async Task SendPatchRequest() + { + Request request = new(TestPatchUrl, HttpMethod.Patch); + Result result = await request.Run(); + + AssertResponse(result, TestPatchUrl); + } + + [TestMethod] + public async Task SendPostRequest() + { + Request request = new(TestPostUrl, HttpMethod.Post); + Result result = await request.Run(); + + AssertResponse(result, TestPostUrl); + } + + [TestMethod] + public async Task SendPutRequest() + { + Request request = new(TestPutUrl, HttpMethod.Put); + Result result = await request.Run(); + + AssertResponse(result, TestPutUrl); + } + + [TestMethod] + public async Task SendPostRequestWithFile() + { + byte[] fileBytes = Encoding.UTF8.GetBytes("Hello world"); + string fileName = "test.txt"; + + Request request = new(TestPostUrl, HttpMethod.Post); + request + .SetContentType("text/plain") + .AddDocumentBody(fileBytes, fileName); + + Result result = await request.RunDocument(); + + AssertResponse(result, TestPostUrl); + StringAssert.Contains(result.Value.Data, "Hello world", "File content should contain 'Hello World'"); + } + + [TestMethod] + public async Task SendJpegImageRequest() + { + await SendImageRequest("image/jpeg", "jpeg"); + } + + [TestMethod] + public async Task SendPngImageRequest() + { + await SendImageRequest("image/png", "png"); + } + + [TestMethod] + public async Task SendSvgImageRequest() + { + await SendImageRequest("image/svg+xml", "svg"); + } + + [TestMethod] + public async Task SendWebpImageRequest() + { + await SendImageRequest("image/webp", "webp"); + } + + private async Task SendImageRequest(string acceptType, string imageType) + { + Request request = new(TestGetWebpImageUrl, HttpMethod.Get); + request + .AddHeader("Accept", acceptType); + + Result result = await request.RunGetBytes(); + + AssertImageResponse(result, imageType); + } + + private void AssertResponse(Result result, string expectedUrl) where T : class + { + Assert.AreEqual(200, result.StatusCode, "Status code should be 200"); + Assert.IsNotNull(result.Value, "Response value should not be null"); + + dynamic response = result.Value; + Assert.AreEqual(expectedUrl, response.Url, "The response URL should match the request URL"); + + dynamic headers = response.Headers; + Assert.IsNotNull(headers, "Headers should not be null"); + Assert.AreEqual("httpbin.org", headers.Host, "Host header should be 'httpbin.org'"); + } + + private void AssertImageResponse(Result result, string imgType) + { + Assert.AreEqual(200, result.StatusCode, "Status code should be 200"); + Assert.IsNotNull(result.Value, "Response value should not be null"); + Assert.IsTrue(result.Value.Length > 0, "Image data should not be empty"); + File.WriteAllBytes($"test_image.{imgType}", result.Value); + } + } +} diff --git a/Web.Test/Web.Test.csproj b/Web.Test/Web.Test.csproj new file mode 100644 index 0000000..81a7461 --- /dev/null +++ b/Web.Test/Web.Test.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/Web/API/Request.cs b/Web/API/Request.cs index 6d57eb3..f66fcb5 100644 --- a/Web/API/Request.cs +++ b/Web/API/Request.cs @@ -251,12 +251,12 @@ public async Task> Run() return result; } - /// - /// Executes the HTTP request with the binary document content included. - /// - /// The type of the response expected from the request. - /// A Result object containing the response. - public async Task> RunDocument() + /// + /// Executes the HTTP request and returns the response content as a byte array. + /// + /// The type of the response expected from the request. + /// A Result object containing the response. + public async Task> RunDocument() { HttpRequestMessage request = BuildBaseRequest(); @@ -288,6 +288,42 @@ public async Task> RunDocument() return result; } + /// + /// Executes the HTTP request that will return a byte array. + /// + /// + /// A object containing the response content as a byte array, + /// the status code of the response, and any error message if the request fails. + /// + public async Task> RunGetBytes() + { + HttpRequestMessage request = BuildBaseRequest(); + Result result = new(); + + try + { + HttpResponseMessage response = await _httpClient.SendAsync(request); + + result.StatusCode = (int)response.StatusCode; + + if (response.IsSuccessStatusCode) + { + result.Value = await response.Content.ReadAsByteArrayAsync(); + } + else + { + result.Error = await response.Content.ReadAsStringAsync(); + } + } + catch (Exception ex) + { + result.StatusCode = 500; + result.Error = ex.Message; + } + + return result; + } + #endregion #region Private function diff --git a/Web/README.md b/Web/README.md index 6edff7d..f452943 100644 --- a/Web/README.md +++ b/Web/README.md @@ -22,7 +22,7 @@ string url = "your-url"; Request request = new(url); request .AddHeader("key", "value") - .AcceptJson() + .AcceptJson(); Result result = await request.Run(); @@ -41,3 +41,21 @@ class MyModel public string Description { get; set; } } ``` + +### Get an image +```csharp +using FrApp42.Web.API; + +string url = "your-url"; + +Request request = new(url); +request + .AddHeader("Accept", "image/png"); + +Result result = await request.RunGetBytes(); + +if (result.StatusCode == 200 && result.Value != null) +{ + File.WriteAllBytes($"image.png", result.Value); +} +```