From 1114fa041d4460021b801deae0bd3f7587a66010 Mon Sep 17 00:00:00 2001 From: christian <6939810+chkr1011@users.noreply.github.com> Date: Mon, 20 Oct 2025 20:52:31 +0200 Subject: [PATCH] Add new PowerShell project --- MQTTnet.sln | 6 ++ .../Cmdlets/ConnectMqttSessionCmdlet.cs | 47 ++++++++++++ .../Cmdlets/DisconnectMqttSessionCmdlet.cs | 31 ++++++++ .../Cmdlets/NewMqttSessionCmdlet.cs | 13 ++++ .../Cmdlets/PublishMqttMessageCmdlet.cs | 40 ++++++++++ .../Cmdlets/ReceiveMqttMessageCmdlet.cs | 74 +++++++++++++++++++ .../RegisterMqttMessageHandlerCmdlet.cs | 26 +++++++ .../Cmdlets/RemoveMqttSessionCmdlet.cs | 16 ++++ .../Cmdlets/SubscribeMqttTopicCmdlet.cs | 30 ++++++++ .../Cmdlets/UnsubscribeMqttTopicCmdlet.cs | 21 ++++++ .../MQTTnet.PowerShell.csproj | 19 +++++ Source/MQTTnet.PowerShell/MqttMessage.cs | 19 +++++ Source/MQTTnet.PowerShell/MqttSession.cs | 72 ++++++++++++++++++ Source/MQTTnet.PowerShell/Test.ps1 | 15 ++++ Source/MQTTnet.Tests/Server/Load_Tests.cs | 5 +- 15 files changed, 432 insertions(+), 2 deletions(-) create mode 100644 Source/MQTTnet.PowerShell/Cmdlets/ConnectMqttSessionCmdlet.cs create mode 100644 Source/MQTTnet.PowerShell/Cmdlets/DisconnectMqttSessionCmdlet.cs create mode 100644 Source/MQTTnet.PowerShell/Cmdlets/NewMqttSessionCmdlet.cs create mode 100644 Source/MQTTnet.PowerShell/Cmdlets/PublishMqttMessageCmdlet.cs create mode 100644 Source/MQTTnet.PowerShell/Cmdlets/ReceiveMqttMessageCmdlet.cs create mode 100644 Source/MQTTnet.PowerShell/Cmdlets/RegisterMqttMessageHandlerCmdlet.cs create mode 100644 Source/MQTTnet.PowerShell/Cmdlets/RemoveMqttSessionCmdlet.cs create mode 100644 Source/MQTTnet.PowerShell/Cmdlets/SubscribeMqttTopicCmdlet.cs create mode 100644 Source/MQTTnet.PowerShell/Cmdlets/UnsubscribeMqttTopicCmdlet.cs create mode 100644 Source/MQTTnet.PowerShell/MQTTnet.PowerShell.csproj create mode 100644 Source/MQTTnet.PowerShell/MqttMessage.cs create mode 100644 Source/MQTTnet.PowerShell/MqttSession.cs create mode 100644 Source/MQTTnet.PowerShell/Test.ps1 diff --git a/MQTTnet.sln b/MQTTnet.sln index 2c1fcd71..5886b974 100644 --- a/MQTTnet.sln +++ b/MQTTnet.sln @@ -33,6 +33,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.Extensions.TopicTem EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.Server", "Source\MQTTnet.Server\MQTTnet.Server.csproj", "{C876EFFD-C5AD-4E26-BD9F-4C48252C02BC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MQTTnet.PowerShell", "Source\MQTTnet.PowerShell\MQTTnet.PowerShell.csproj", "{37B129C5-0231-4EB5-AF13-77959068911C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -79,6 +81,10 @@ Global {C876EFFD-C5AD-4E26-BD9F-4C48252C02BC}.Debug|Any CPU.Build.0 = Debug|Any CPU {C876EFFD-C5AD-4E26-BD9F-4C48252C02BC}.Release|Any CPU.ActiveCfg = Release|Any CPU {C876EFFD-C5AD-4E26-BD9F-4C48252C02BC}.Release|Any CPU.Build.0 = Release|Any CPU + {37B129C5-0231-4EB5-AF13-77959068911C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {37B129C5-0231-4EB5-AF13-77959068911C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37B129C5-0231-4EB5-AF13-77959068911C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {37B129C5-0231-4EB5-AF13-77959068911C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Source/MQTTnet.PowerShell/Cmdlets/ConnectMqttSessionCmdlet.cs b/Source/MQTTnet.PowerShell/Cmdlets/ConnectMqttSessionCmdlet.cs new file mode 100644 index 00000000..7bf780b9 --- /dev/null +++ b/Source/MQTTnet.PowerShell/Cmdlets/ConnectMqttSessionCmdlet.cs @@ -0,0 +1,47 @@ +using System.Management.Automation; + +namespace MQTTnet.PowerShell.Cmdlets; + +[Cmdlet(VerbsCommunications.Connect, "MqttSession")] +[OutputType(typeof(MqttClientConnectResult))] +public class ConnectMqttSessionCmdlet : PSCmdlet +{ + [Parameter] + public SwitchParameter CleanSession { get; set; } = true; + + [Parameter] + public string? ClientId { get; set; } = Guid.NewGuid().ToString(); + + [Parameter(Mandatory = true)] + public required new string Host { get; set; } + + [Parameter] + public string? Password { get; set; } + + [Parameter] + public int Port { get; set; } = 1883; + + [Parameter(Mandatory = true, ValueFromPipeline = true)] + public required MqttSession Session { get; set; } + + [Parameter] + public string? Username { get; set; } + + [Parameter] + public SwitchParameter UseTls { get; set; } + + protected override void ProcessRecord() + { + var clientOptionsBuilder = new MqttClientOptionsBuilder(); + clientOptionsBuilder.WithTcpServer(Host, Port); + + if (Username != null) + { + clientOptionsBuilder.WithCredentials(Username, Password); + } + + var response = Session.Client.ConnectAsync(clientOptionsBuilder.Build()).GetAwaiter().GetResult(); + + WriteObject(response); + } +} \ No newline at end of file diff --git a/Source/MQTTnet.PowerShell/Cmdlets/DisconnectMqttSessionCmdlet.cs b/Source/MQTTnet.PowerShell/Cmdlets/DisconnectMqttSessionCmdlet.cs new file mode 100644 index 00000000..b9e509bc --- /dev/null +++ b/Source/MQTTnet.PowerShell/Cmdlets/DisconnectMqttSessionCmdlet.cs @@ -0,0 +1,31 @@ +using System.Management.Automation; + +namespace MQTTnet.PowerShell.Cmdlets; + +[Cmdlet(VerbsCommunications.Disconnect, "MqttSession")] +public class DisconnectMqttSessionCmdlet : PSCmdlet +{ + [Parameter] + public MqttClientDisconnectOptionsReason Reason { get; set; } = MqttClientDisconnectOptionsReason.NormalDisconnection; + + [Parameter] + public string? ReasonString { get; set; } + + [Parameter(Mandatory = true, ValueFromPipeline = true)] + public required MqttSession Session { get; set; } + + [Parameter] + public uint SessionExpiryInterval { get; set; } + + protected override void ProcessRecord() + { + if (Session.Client.IsConnected) + { + var options = new MqttClientDisconnectOptionsBuilder().WithSessionExpiryInterval(SessionExpiryInterval).WithReason(Reason).WithReasonString(ReasonString).Build(); + + Session.Client.DisconnectAsync(options).GetAwaiter().GetResult(); + } + + WriteObject("Disconnected."); + } +} \ No newline at end of file diff --git a/Source/MQTTnet.PowerShell/Cmdlets/NewMqttSessionCmdlet.cs b/Source/MQTTnet.PowerShell/Cmdlets/NewMqttSessionCmdlet.cs new file mode 100644 index 00000000..1eeef896 --- /dev/null +++ b/Source/MQTTnet.PowerShell/Cmdlets/NewMqttSessionCmdlet.cs @@ -0,0 +1,13 @@ +using System.Management.Automation; + +namespace MQTTnet.PowerShell.Cmdlets; + +[Cmdlet(VerbsCommon.New, "MqttSession")] +[OutputType(typeof(MqttSession))] +public class NewMqttSessionCmdlet : PSCmdlet +{ + protected override void ProcessRecord() + { + WriteObject(new MqttSession()); + } +} \ No newline at end of file diff --git a/Source/MQTTnet.PowerShell/Cmdlets/PublishMqttMessageCmdlet.cs b/Source/MQTTnet.PowerShell/Cmdlets/PublishMqttMessageCmdlet.cs new file mode 100644 index 00000000..0b2ec372 --- /dev/null +++ b/Source/MQTTnet.PowerShell/Cmdlets/PublishMqttMessageCmdlet.cs @@ -0,0 +1,40 @@ +using System.Management.Automation; +using System.Text; +using MQTTnet.Protocol; + +namespace MQTTnet.PowerShell.Cmdlets; + +[Cmdlet(VerbsData.Publish, "MqttMessage")] +public class PublishMqttMessageCmdlet : PSCmdlet +{ + [Parameter(Mandatory = true)] + public string? Payload { get; set; } + + [Parameter] + public int QoS { get; set; } = 0; + + [Parameter] + public SwitchParameter Retain { get; set; } + + [Parameter(Mandatory = true, ValueFromPipeline = true)] + public required MqttSession Session { get; set; } + + [Parameter(Mandatory = true)] + public required string Topic { get; set; } + + protected override void ProcessRecord() + { + // if (Session == null || !Session.IsConnected) + // throw new InvalidOperationException("Session not connected."); + + var msg = new MqttApplicationMessageBuilder().WithTopic(Topic) + .WithPayload(Encoding.UTF8.GetBytes(Payload ?? string.Empty)) + .WithQualityOfServiceLevel((MqttQualityOfServiceLevel)QoS) + .WithRetainFlag(Retain) + .Build(); + + var response = Session.Client.PublishAsync(msg).GetAwaiter().GetResult(); + + WriteObject(response); + } +} \ No newline at end of file diff --git a/Source/MQTTnet.PowerShell/Cmdlets/ReceiveMqttMessageCmdlet.cs b/Source/MQTTnet.PowerShell/Cmdlets/ReceiveMqttMessageCmdlet.cs new file mode 100644 index 00000000..857fc680 --- /dev/null +++ b/Source/MQTTnet.PowerShell/Cmdlets/ReceiveMqttMessageCmdlet.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Management.Automation; + +namespace MQTTnet.PowerShell.Cmdlets; + +[Cmdlet(VerbsCommunications.Receive, "MqttMessage")] +[OutputType(typeof(MqttMessage))] +public class ReceiveMqttMessageCmdlet : PSCmdlet +{ + [Parameter(Mandatory = true, ValueFromPipeline = true)] + public required MqttSession Session { get; set; } + + [Parameter] + [ValidateRange(0, int.MaxValue)] + public int TimeoutSeconds { get; set; } = 0; + + protected override void ProcessRecord() + { + var tcs = new TaskCompletionSource(); + EventHandler? handler = null; + CancellationTokenSource? cts = null; + + try + { + handler = (s, e) => + { + if (!tcs.Task.IsCompleted) + { + tcs.TrySetResult(e); + } + }; + + Session.MessageReceived += handler; + + // Optionaler Timeout + if (TimeoutSeconds > 0) + { + cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromSeconds(TimeoutSeconds)); + cts.Token.Register(() => + { + if (!tcs.Task.IsCompleted) + { + tcs.TrySetCanceled(); + } + }); + } + + WriteVerbose("Waiting for next MQTT message..."); + + try + { + var message = tcs.Task.GetAwaiter().GetResult(); + WriteObject(message); + } + catch (TaskCanceledException) + { + WriteWarning($"Timeout after {TimeoutSeconds} seconds — no message received."); + } + } + finally + { + if (handler != null) + { + Session.MessageReceived -= handler; + } + + cts?.Dispose(); + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.PowerShell/Cmdlets/RegisterMqttMessageHandlerCmdlet.cs b/Source/MQTTnet.PowerShell/Cmdlets/RegisterMqttMessageHandlerCmdlet.cs new file mode 100644 index 00000000..62852bfd --- /dev/null +++ b/Source/MQTTnet.PowerShell/Cmdlets/RegisterMqttMessageHandlerCmdlet.cs @@ -0,0 +1,26 @@ +using System.Management.Automation; +using System.Management.Automation.Runspaces; + +namespace MQTTnet.PowerShell.Cmdlets; + +[Cmdlet(VerbsLifecycle.Register, "MqttMessageHandler")] +public class RegisterMqttMessageHandlerCmdlet : PSCmdlet +{ + [Parameter(Mandatory = true, ValueFromPipeline = true)] + public required MqttSession Session { get; set; } + + [Parameter(Mandatory = true)] + public ScriptBlock? Action { get; set; } + + protected override void ProcessRecord() + { + EventHandler handler = (s, e) => + { + //throw new NotImplementedException(); + //InvokeCommand.InvokeScript(Action, false, PipelineResultTypes.Output, null, new object[] { e.Topic, e.Payload }); + }; + + Session.MessageReceived += handler; + WriteObject($"Handler registered for MQTT messages on session."); + } +} \ No newline at end of file diff --git a/Source/MQTTnet.PowerShell/Cmdlets/RemoveMqttSessionCmdlet.cs b/Source/MQTTnet.PowerShell/Cmdlets/RemoveMqttSessionCmdlet.cs new file mode 100644 index 00000000..fe5da4c6 --- /dev/null +++ b/Source/MQTTnet.PowerShell/Cmdlets/RemoveMqttSessionCmdlet.cs @@ -0,0 +1,16 @@ +using System.Management.Automation; + +namespace MQTTnet.PowerShell.Cmdlets; + +[Cmdlet(VerbsCommon.Remove, "MqttSession")] +public class RemoveMqttSessionCmdlet : PSCmdlet +{ + [Parameter(Mandatory = true, ValueFromPipeline = true)] + public required MqttSession Session { get; set; } + + protected override void ProcessRecord() + { + Session.Dispose(); + WriteObject("Session removed."); + } +} \ No newline at end of file diff --git a/Source/MQTTnet.PowerShell/Cmdlets/SubscribeMqttTopicCmdlet.cs b/Source/MQTTnet.PowerShell/Cmdlets/SubscribeMqttTopicCmdlet.cs new file mode 100644 index 00000000..8ad774d2 --- /dev/null +++ b/Source/MQTTnet.PowerShell/Cmdlets/SubscribeMqttTopicCmdlet.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Management.Automation; + +namespace MQTTnet.PowerShell.Cmdlets; + +[Cmdlet("Subscribe", "MqttTopic")] +[OutputType(typeof(MqttClientSubscribeResult))] +public class SubscribeMqttTopicCmdlet : PSCmdlet +{ + [Parameter] + public int QoS { get; set; } = 0; + + [Parameter(Mandatory = true, ValueFromPipeline = true)] + public required MqttSession Session { get; set; } + + [Parameter(Mandatory = true)] + public required string Topic { get; set; } + + protected override void ProcessRecord() + { + var topicFilter = new MqttTopicFilterBuilder().WithTopic(Topic).Build(); + + var response = Session.Client.SubscribeAsync(topicFilter).GetAwaiter().GetResult(); + + WriteObject(response); + } +} \ No newline at end of file diff --git a/Source/MQTTnet.PowerShell/Cmdlets/UnsubscribeMqttTopicCmdlet.cs b/Source/MQTTnet.PowerShell/Cmdlets/UnsubscribeMqttTopicCmdlet.cs new file mode 100644 index 00000000..84f69cbe --- /dev/null +++ b/Source/MQTTnet.PowerShell/Cmdlets/UnsubscribeMqttTopicCmdlet.cs @@ -0,0 +1,21 @@ +using System.Management.Automation; + +namespace MQTTnet.PowerShell.Cmdlets; + +[Cmdlet("Unsubscribe", "MqttTopic")] +[OutputType(typeof(MqttClientUnsubscribeResult))] +public class UnsubscribeMqttTopicCmdlet : PSCmdlet +{ + [Parameter(Mandatory = true, ValueFromPipeline = true)] + public required MqttSession Session { get; set; } + + [Parameter(Mandatory = true)] + public required string Topic { get; set; } + + protected override void ProcessRecord() + { + var response = Session.Client.UnsubscribeAsync(Topic).GetAwaiter().GetResult(); + + WriteObject(response); + } +} \ No newline at end of file diff --git a/Source/MQTTnet.PowerShell/MQTTnet.PowerShell.csproj b/Source/MQTTnet.PowerShell/MQTTnet.PowerShell.csproj new file mode 100644 index 00000000..cb5d0a08 --- /dev/null +++ b/Source/MQTTnet.PowerShell/MQTTnet.PowerShell.csproj @@ -0,0 +1,19 @@ + + + + MQTTnet.PowerShell + default + enable + + + + + All + + + + + + + + diff --git a/Source/MQTTnet.PowerShell/MqttMessage.cs b/Source/MQTTnet.PowerShell/MqttMessage.cs new file mode 100644 index 00000000..8a28d36a --- /dev/null +++ b/Source/MQTTnet.PowerShell/MqttMessage.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using MQTTnet.Packets; + +namespace MQTTnet.PowerShell; + +public class MqttMessage +{ + public required string? ContentType { get; init; } + public required string Payload { get; init; } + public int QoS { get; init; } + public required byte[] RawPayload { get; init; } + public string? ResponseTopic { get; init; } + public bool Retain { get; init; } + public required string Topic { get; init; } + public required List? UserProperties { get; init; } +} \ No newline at end of file diff --git a/Source/MQTTnet.PowerShell/MqttSession.cs b/Source/MQTTnet.PowerShell/MqttSession.cs new file mode 100644 index 00000000..014dba0a --- /dev/null +++ b/Source/MQTTnet.PowerShell/MqttSession.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Buffers; +using System.Text; +using MQTTnet.Protocol; + +namespace MQTTnet.PowerShell; + +public sealed class MqttSession : IDisposable +{ + public MqttSession() + { + var factory = new MqttClientFactory(); + Client = factory.CreateMqttClient(); + + Client.ApplicationMessageReceivedAsync += e => + { + MessageReceived?.Invoke(this, new MqttMessage + { + Topic = e.ApplicationMessage.Topic, + Payload = Encoding.UTF8.GetString(e.ApplicationMessage.Payload), + RawPayload = e.ApplicationMessage.Payload.ToArray(), + UserProperties = e.ApplicationMessage.UserProperties, + QoS = (int)e.ApplicationMessage.QualityOfServiceLevel, + ContentType = e.ApplicationMessage.ContentType, + Retain = e.ApplicationMessage.Retain, + ResponseTopic = e.ApplicationMessage.ResponseTopic + + }); + return Task.CompletedTask; + }; + + // Client.ConnectedAsync += e => + // { + // //Connected?.Invoke(this, e); + // return Task.CompletedTask; + // }; + // + // Client.DisconnectedAsync += e => + // { + // //Disconnected?.Invoke(this, e); + // return Task.CompletedTask; + // }; + } + + // public event EventHandler Connected; + // public event EventHandler Disconnected; + public event EventHandler? MessageReceived; + + public IMqttClient Client { get; } + + public void Dispose() + { + try + { + if (Client != null) + { + if (Client.IsConnected) + { + Client.DisconnectAsync().Wait(); + } + + Client.Dispose(); + } + } + catch + { + } + } +} \ No newline at end of file diff --git a/Source/MQTTnet.PowerShell/Test.ps1 b/Source/MQTTnet.PowerShell/Test.ps1 new file mode 100644 index 00000000..985330ea --- /dev/null +++ b/Source/MQTTnet.PowerShell/Test.ps1 @@ -0,0 +1,15 @@ +Import-Module ./bin/Debug/net8.0/MQTTnet.PowerShell.dll + +$session = New-MqttSession + +Connect-MqttSession -Session $session -Host "192.168.1.16" + +Publish-MqttMessage -Session $session -Topic "Test" -Payload "Hello from PowerShell" + +Subscribe-MqttTopic -Session $session -Topic "test/demo" + +Receive-MqttMessage -Session $session -TimeoutSeconds 200 + +Disconnect-MqttSession -Session $session +Remove-MqttSession -Session $session + diff --git a/Source/MQTTnet.Tests/Server/Load_Tests.cs b/Source/MQTTnet.Tests/Server/Load_Tests.cs index 2c8276e6..a302d414 100644 --- a/Source/MQTTnet.Tests/Server/Load_Tests.cs +++ b/Source/MQTTnet.Tests/Server/Load_Tests.cs @@ -1,6 +1,7 @@ #if DEBUG using System; +using System.Globalization; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -56,7 +57,7 @@ public sealed class Load_Tests : BaseTestClass for (var j = 0; j < 1000; j++) { - publishPacket.Topic = j.ToString(); + publishPacket.Topic = j.ToString(CultureInfo.InvariantCulture); await client.SendAsync(publishPacket, CancellationToken.None); } @@ -143,7 +144,7 @@ public sealed class Load_Tests : BaseTestClass for (var j = 0; j < 1000; j++) { - var message = applicationMessageBuilder.WithTopic(j.ToString()).Build(); + var message = applicationMessageBuilder.WithTopic(j.ToString(CultureInfo.InvariantCulture)).Build(); await client.PublishAsync(message); }