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);
+}
+```