From 32a36e5ecc1b8c7d105d55b6de5c86cdba465803 Mon Sep 17 00:00:00 2001 From: Vladyslav Nikonov Date: Fri, 13 Dec 2024 16:02:25 +0200 Subject: [PATCH 1/4] feat(dotnet-client): implement C# NOW-proto client --- NowProto.sln | 63 +++- nugets/Devolutions.NowClient/AExecParams.cs | 60 ++++ .../Devolutions.NowClient.csproj | 14 + .../Devolutions.NowClient/ExecBatchParams.cs | 34 ++ .../ExecProcessParams.cs | 50 +++ .../Devolutions.NowClient/ExecPwshParams.cs | 140 ++++++++ nugets/Devolutions.NowClient/ExecRunParams.cs | 17 + nugets/Devolutions.NowClient/ExecSession.cs | 196 +++++++++++ .../Devolutions.NowClient/ExecShellParams.cs | 50 +++ .../Devolutions.NowClient/ExecWinPsParams.cs | 141 ++++++++ .../IExecSessionHandler.cs | 12 + .../IMessageBoxRspHandler.cs | 9 + nugets/Devolutions.NowClient/INowTransport.cs | 11 + .../Devolutions.NowClient/MessageBoxParams.cs | 69 ++++ .../MessageBoxResponse.cs | 53 +++ .../NowChannelTransport.cs | 37 ++ nugets/Devolutions.NowClient/NowClient.cs | 331 ++++++++++++++++++ .../NowClientException.cs | 13 + .../NowSessionException.cs | 27 ++ nugets/Devolutions.NowClient/README.md | 17 + .../SystemShutdownParams.cs | 50 +++ .../Worker/CommandChannelClose.cs | 15 + .../Worker/CommandExecAbort.cs | 18 + .../Worker/CommandExecBatch.cs | 16 + .../Worker/CommandExecCancel.cs | 14 + .../Worker/CommandExecData.cs | 27 ++ .../Worker/CommandExecProcess.cs | 16 + .../Worker/CommandExecPwsh.cs | 16 + .../Worker/CommandExecRun.cs | 13 + .../Worker/CommandExecShell.cs | 16 + .../Worker/CommandExecWinPs.cs | 16 + .../Worker/CommandSessionLock.cs | 12 + .../Worker/CommandSessionLogoff.cs | 12 + .../Worker/CommandSessionMsgBox.cs | 18 + .../Worker/CommandSystemShutdown.cs | 14 + .../Worker/IClientCommand.cs | 7 + .../Devolutions.NowClient/Worker/WorkerCtx.cs | 239 +++++++++++++ .../Devolutions.NowProto.Tests.csproj | 2 +- .../Devolutions.NowProto.Tests/MsgChannel.cs | 2 +- .../MsgExecGeneral.cs | 8 +- .../Capabilities/NowCapabilityExec.cs | 2 + .../Capabilities/NowCapabilitySession.cs | 2 + .../Capabilities/NowCapabilitySystem.cs | 2 + .../ChannelMessageKind.cs | 9 + .../Devolutions.NowProto.csproj | 1 + .../Devolutions.NowProto/ExecMessageKind.cs | 18 + .../Messages/NowMsgChannelCapset.cs | 4 +- .../Messages/NowMsgChannelClose.cs | 4 +- .../Messages/NowMsgChannelHeartbeat.cs | 4 +- .../Messages/NowMsgExecAbort.cs | 4 +- .../Messages/NowMsgExecBatch.cs | 4 +- .../Messages/NowMsgExecCancelReq.cs | 4 +- .../Messages/NowMsgExecCancelRsp.cs | 24 +- .../Messages/NowMsgExecData.cs | 10 +- .../Messages/NowMsgExecProcess.cs | 4 +- .../Messages/NowMsgExecPwsh.cs | 4 +- .../Messages/NowMsgExecResult.cs | 22 +- .../Messages/NowMsgExecRun.cs | 4 +- .../Messages/NowMsgExecShell.cs | 4 +- .../Messages/NowMsgExecStarted.cs | 4 +- .../Messages/NowMsgExecWinPs.cs | 4 +- .../Messages/NowMsgSessionLock.cs | 4 +- .../Messages/NowMsgSessionLogoff.cs | 4 +- .../Messages/NowMsgSessionMessageBoxRsp.cs | 6 +- .../Messages/NowMsgSessionMsgBoxReq.cs | 4 +- .../Messages/NowMsgSystemShutdown.cs | 4 +- nugets/Devolutions.NowProto/NowMessage.cs | 8 +- nugets/Devolutions.NowProto/NowWriteCursor.cs | 2 + .../SessionMessageKind.cs | 10 + .../Devolutions.NowProto/SystemMessageKind.cs | 7 + 70 files changed, 1975 insertions(+), 87 deletions(-) create mode 100644 nugets/Devolutions.NowClient/AExecParams.cs create mode 100644 nugets/Devolutions.NowClient/Devolutions.NowClient.csproj create mode 100644 nugets/Devolutions.NowClient/ExecBatchParams.cs create mode 100644 nugets/Devolutions.NowClient/ExecProcessParams.cs create mode 100644 nugets/Devolutions.NowClient/ExecPwshParams.cs create mode 100644 nugets/Devolutions.NowClient/ExecRunParams.cs create mode 100644 nugets/Devolutions.NowClient/ExecSession.cs create mode 100644 nugets/Devolutions.NowClient/ExecShellParams.cs create mode 100644 nugets/Devolutions.NowClient/ExecWinPsParams.cs create mode 100644 nugets/Devolutions.NowClient/IExecSessionHandler.cs create mode 100644 nugets/Devolutions.NowClient/IMessageBoxRspHandler.cs create mode 100644 nugets/Devolutions.NowClient/INowTransport.cs create mode 100644 nugets/Devolutions.NowClient/MessageBoxParams.cs create mode 100644 nugets/Devolutions.NowClient/MessageBoxResponse.cs create mode 100644 nugets/Devolutions.NowClient/NowChannelTransport.cs create mode 100644 nugets/Devolutions.NowClient/NowClient.cs create mode 100644 nugets/Devolutions.NowClient/NowClientException.cs create mode 100644 nugets/Devolutions.NowClient/NowSessionException.cs create mode 100644 nugets/Devolutions.NowClient/README.md create mode 100644 nugets/Devolutions.NowClient/SystemShutdownParams.cs create mode 100644 nugets/Devolutions.NowClient/Worker/CommandChannelClose.cs create mode 100644 nugets/Devolutions.NowClient/Worker/CommandExecAbort.cs create mode 100644 nugets/Devolutions.NowClient/Worker/CommandExecBatch.cs create mode 100644 nugets/Devolutions.NowClient/Worker/CommandExecCancel.cs create mode 100644 nugets/Devolutions.NowClient/Worker/CommandExecData.cs create mode 100644 nugets/Devolutions.NowClient/Worker/CommandExecProcess.cs create mode 100644 nugets/Devolutions.NowClient/Worker/CommandExecPwsh.cs create mode 100644 nugets/Devolutions.NowClient/Worker/CommandExecRun.cs create mode 100644 nugets/Devolutions.NowClient/Worker/CommandExecShell.cs create mode 100644 nugets/Devolutions.NowClient/Worker/CommandExecWinPs.cs create mode 100644 nugets/Devolutions.NowClient/Worker/CommandSessionLock.cs create mode 100644 nugets/Devolutions.NowClient/Worker/CommandSessionLogoff.cs create mode 100644 nugets/Devolutions.NowClient/Worker/CommandSessionMsgBox.cs create mode 100644 nugets/Devolutions.NowClient/Worker/CommandSystemShutdown.cs create mode 100644 nugets/Devolutions.NowClient/Worker/IClientCommand.cs create mode 100644 nugets/Devolutions.NowClient/Worker/WorkerCtx.cs create mode 100644 nugets/Devolutions.NowProto/ChannelMessageKind.cs create mode 100644 nugets/Devolutions.NowProto/ExecMessageKind.cs create mode 100644 nugets/Devolutions.NowProto/SessionMessageKind.cs create mode 100644 nugets/Devolutions.NowProto/SystemMessageKind.cs diff --git a/NowProto.sln b/NowProto.sln index 61f7c3d..5bf1e9d 100644 --- a/NowProto.sln +++ b/NowProto.sln @@ -5,30 +5,65 @@ VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "nugets", "nugets", "{BD6154A5-8825-4023-A6AE-CE9FCC4B073B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Devolutions.NowProto", "nugets\Devolutions.NowProto\Devolutions.NowProto.csproj", "{2EB02FFB-DC42-4217-8520-93F06E17BFC4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Devolutions.NowProto", "nugets\Devolutions.NowProto\Devolutions.NowProto.csproj", "{2EB02FFB-DC42-4217-8520-93F06E17BFC4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Devolutions.NowProto.Tests", "nugets\Devolutions.NowProto.Tests\Devolutions.NowProto.Tests.csproj", "{34912761-E8CB-4D88-A4B4-365F6DB88985}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Devolutions.NowProto.Tests", "nugets\Devolutions.NowProto.Tests\Devolutions.NowProto.Tests.csproj", "{34912761-E8CB-4D88-A4B4-365F6DB88985}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Devolutions.NowClient", "nugets\Devolutions.NowClient\Devolutions.NowClient.csproj", "{4692658E-C138-4A40-A10C-7E55D38D250B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU + Debug|ARM64 = Debug|ARM64 + Debug|Win32 = Debug|Win32 + Debug|x64 = Debug|x64 + Release|ARM64 = Release|ARM64 + Release|Win32 = Release|Win32 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Debug|ARM64.Build.0 = Debug|ARM64 + {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Debug|Win32.ActiveCfg = Debug|Any CPU + {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Debug|Win32.Build.0 = Debug|Any CPU + {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Debug|x64.ActiveCfg = Debug|x64 + {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Debug|x64.Build.0 = Debug|x64 + {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Release|ARM64.ActiveCfg = Release|ARM64 + {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Release|ARM64.Build.0 = Release|ARM64 + {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Release|Win32.ActiveCfg = Release|Any CPU + {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Release|Win32.Build.0 = Release|Any CPU + {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Release|x64.ActiveCfg = Release|x64 + {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Release|x64.Build.0 = Release|x64 + {34912761-E8CB-4D88-A4B4-365F6DB88985}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {34912761-E8CB-4D88-A4B4-365F6DB88985}.Debug|ARM64.Build.0 = Debug|ARM64 + {34912761-E8CB-4D88-A4B4-365F6DB88985}.Debug|Win32.ActiveCfg = Debug|Any CPU + {34912761-E8CB-4D88-A4B4-365F6DB88985}.Debug|Win32.Build.0 = Debug|Any CPU + {34912761-E8CB-4D88-A4B4-365F6DB88985}.Debug|x64.ActiveCfg = Debug|x64 + {34912761-E8CB-4D88-A4B4-365F6DB88985}.Debug|x64.Build.0 = Debug|x64 + {34912761-E8CB-4D88-A4B4-365F6DB88985}.Release|ARM64.ActiveCfg = Release|ARM64 + {34912761-E8CB-4D88-A4B4-365F6DB88985}.Release|ARM64.Build.0 = Release|ARM64 + {34912761-E8CB-4D88-A4B4-365F6DB88985}.Release|Win32.ActiveCfg = Release|Any CPU + {34912761-E8CB-4D88-A4B4-365F6DB88985}.Release|Win32.Build.0 = Release|Any CPU + {34912761-E8CB-4D88-A4B4-365F6DB88985}.Release|x64.ActiveCfg = Release|x64 + {34912761-E8CB-4D88-A4B4-365F6DB88985}.Release|x64.Build.0 = Release|x64 + {4692658E-C138-4A40-A10C-7E55D38D250B}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {4692658E-C138-4A40-A10C-7E55D38D250B}.Debug|ARM64.Build.0 = Debug|ARM64 + {4692658E-C138-4A40-A10C-7E55D38D250B}.Debug|Win32.ActiveCfg = Debug|Any CPU + {4692658E-C138-4A40-A10C-7E55D38D250B}.Debug|Win32.Build.0 = Debug|Any CPU + {4692658E-C138-4A40-A10C-7E55D38D250B}.Debug|x64.ActiveCfg = Debug|x64 + {4692658E-C138-4A40-A10C-7E55D38D250B}.Debug|x64.Build.0 = Debug|x64 + {4692658E-C138-4A40-A10C-7E55D38D250B}.Release|ARM64.ActiveCfg = Release|ARM64 + {4692658E-C138-4A40-A10C-7E55D38D250B}.Release|ARM64.Build.0 = Release|ARM64 + {4692658E-C138-4A40-A10C-7E55D38D250B}.Release|Win32.ActiveCfg = Release|Any CPU + {4692658E-C138-4A40-A10C-7E55D38D250B}.Release|Win32.Build.0 = Release|Any CPU + {4692658E-C138-4A40-A10C-7E55D38D250B}.Release|x64.ActiveCfg = Release|x64 + {4692658E-C138-4A40-A10C-7E55D38D250B}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Release|Any CPU.Build.0 = Release|Any CPU - {34912761-E8CB-4D88-A4B4-365F6DB88985}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {34912761-E8CB-4D88-A4B4-365F6DB88985}.Debug|Any CPU.Build.0 = Debug|Any CPU - {34912761-E8CB-4D88-A4B4-365F6DB88985}.Release|Any CPU.ActiveCfg = Release|Any CPU - {34912761-E8CB-4D88-A4B4-365F6DB88985}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection GlobalSection(NestedProjects) = preSolution {2EB02FFB-DC42-4217-8520-93F06E17BFC4} = {BD6154A5-8825-4023-A6AE-CE9FCC4B073B} {34912761-E8CB-4D88-A4B4-365F6DB88985} = {BD6154A5-8825-4023-A6AE-CE9FCC4B073B} + {4692658E-C138-4A40-A10C-7E55D38D250B} = {BD6154A5-8825-4023-A6AE-CE9FCC4B073B} EndGlobalSection EndGlobal diff --git a/nugets/Devolutions.NowClient/AExecParams.cs b/nugets/Devolutions.NowClient/AExecParams.cs new file mode 100644 index 0000000..8127622 --- /dev/null +++ b/nugets/Devolutions.NowClient/AExecParams.cs @@ -0,0 +1,60 @@ +using System.Threading.Channels; + +using Devolutions.NowClient.Worker; + +namespace Devolutions.NowClient +{ + /// + /// Common remote execution parameters for all execution styles. + /// + public abstract class AExecParams + { + /// + /// Callback for stdout data collection. + /// + public StdoutHandler OnStdout + { + set + { + _stdoutHandler = value; + } + } + + /// + /// Callback for stderr data collection. + /// + public StderrHandler OnStderr + { + set + { + _stderrHandler = value; + } + } + + /// + /// Callback to be called when the execution session starts on the remote host. + /// + public StartedHandler OnStarted + { + set + { + _startedHandler = value; + } + } + + internal ExecSession ToExecSession(uint sessionId, ChannelWriter commandWriter) + { + return new ExecSession( + sessionId, + commandWriter, + _stdoutHandler, + _stderrHandler, + _startedHandler + ); + } + + private StdoutHandler? _stdoutHandler; + private StderrHandler? _stderrHandler; + private StartedHandler? _startedHandler; + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/Devolutions.NowClient.csproj b/nugets/Devolutions.NowClient/Devolutions.NowClient.csproj new file mode 100644 index 0000000..905049a --- /dev/null +++ b/nugets/Devolutions.NowClient/Devolutions.NowClient.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + Win32;x64;ARM64 + + + + + + + diff --git a/nugets/Devolutions.NowClient/ExecBatchParams.cs b/nugets/Devolutions.NowClient/ExecBatchParams.cs new file mode 100644 index 0000000..73c0f28 --- /dev/null +++ b/nugets/Devolutions.NowClient/ExecBatchParams.cs @@ -0,0 +1,34 @@ +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient +{ + /// + /// Batch(Windows CMD) remote execution session parameters. + /// + /// Command/script to execute. + public class ExecBatchParams(string command) : AExecParams + { + /// + /// Set the working directory for the command/script. + /// + public ExecBatchParams Directory(string directory) + { + _directory = directory; + return this; + } + + internal NowMsgExecBatch ToNowMessage(uint sessionId) + { + var builder = new NowMsgExecBatch.Builder(sessionId, command); + + if (_directory != null) + { + builder.Directory(_directory); + } + + return builder.Build(); + } + + private string? _directory = null; + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/ExecProcessParams.cs b/nugets/Devolutions.NowClient/ExecProcessParams.cs new file mode 100644 index 0000000..77783bd --- /dev/null +++ b/nugets/Devolutions.NowClient/ExecProcessParams.cs @@ -0,0 +1,50 @@ +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient +{ + /// + /// Plain process execution session parameters (e.g. CreateProcessW on Windows hosts). + /// + /// + public class ExecProcessParams(string filename) : AExecParams + { + /// + /// Set the command line parameters for the process. + /// + public ExecProcessParams Parameters(string parameters) + { + _parameters = parameters; + return this; + } + + /// + /// Set the working directory for the process. + /// + public ExecProcessParams Directory(string directory) + { + _directory = directory; + return this; + } + + internal NowMsgExecProcess ToNowMessage(uint sessionId) + { + var builder = new NowMsgExecProcess.Builder(sessionId, filename); + + if (_parameters != null) + { + builder.Parameters(_parameters); + } + + if (_directory != null) + { + builder.Directory(_directory); + } + + return builder.Build(); + } + + + private string? _parameters = null; + private string? _directory = null; + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/ExecPwshParams.cs b/nugets/Devolutions.NowClient/ExecPwshParams.cs new file mode 100644 index 0000000..9bdcc08 --- /dev/null +++ b/nugets/Devolutions.NowClient/ExecPwshParams.cs @@ -0,0 +1,140 @@ +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient +{ + /// + /// Powershell 7(pwsh) remote execution session parameters. + /// + /// PowerShell command/script to execute. + public class ExecPwshParams(string command) : AExecParams + { + /// + /// Set the working directory for the command/script. + /// + public ExecPwshParams Directory(string directory) + { + _directory = directory; + return this; + } + + /// + /// Set the configuration name for the PowerShell session. (-ConfigurationName) + /// + public ExecPwshParams ConfigurationName(string configurationName) + { + _configurationName = configurationName; + return this; + } + + /// + /// Set the execution policy for the PowerShell session. (-ExecutionPolicy) + /// + public ExecPwshParams ExecutionPolicy(string executionPolicy) + { + _executionPolicy = executionPolicy; + return this; + } + + /// + /// Set the apartment state for the PowerShell session. (-Sta/-Mta) + /// + public ExecPwshParams ApartmentState(NowMsgExecPwsh.ApartmentStateKind apartmentState) + { + _apartmentState = apartmentState; + return this; + } + + /// + /// Disable the PowerShell logo display. (-NoLogo) + /// + public ExecPwshParams NoLogo() + { + _noLogo = true; + return this; + } + + /// + /// Do not close the PowerShell session after the command/script execution. (-NoExit) + /// + public ExecPwshParams NoExit() + { + _noExit = true; + return this; + } + + /// + /// Do not load the PowerShell profile. (-NoProfile) + /// + public ExecPwshParams NoProfile() + { + _noProfile = true; + return this; + } + + /// + /// Run the PowerShell session in non-interactive mode. (-NonInteractive) + /// + public ExecPwshParams NonInteractive() + { + _nonInteractive = true; + return this; + } + + internal NowMsgExecPwsh ToNowMessage(uint sessionId) + { + var builder = new NowMsgExecPwsh.Builder(sessionId, command); + + if (_directory != null) + { + builder.Directory(_directory); + } + + if (_configurationName != null) + { + builder.ConfigurationName(_configurationName); + } + + if (_executionPolicy != null) + { + builder.ExecutionPolicy(_executionPolicy); + } + + if (_apartmentState != null) + { + builder.ApartmentState(_apartmentState.Value); + } + + if (_noLogo) + { + builder.SetNoLogo(); + } + + if (_noExit) + { + builder.SetNoExit(); + } + + if (_noProfile) + { + builder.SetNoProfile(); + } + + if (_nonInteractive) + { + builder.SetNonInteractive(); + } + + return builder.Build(); + } + + + private string? _configurationName = null; + private string? _executionPolicy = null; + private string? _directory = null; + private NowMsgExecPwsh.ApartmentStateKind? _apartmentState = null; + private bool _noLogo = false; + private bool _noExit = false; + private bool _noProfile = false; + private bool _nonInteractive = false; + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/ExecRunParams.cs b/nugets/Devolutions.NowClient/ExecRunParams.cs new file mode 100644 index 0000000..dfd138f --- /dev/null +++ b/nugets/Devolutions.NowClient/ExecRunParams.cs @@ -0,0 +1,17 @@ +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient +{ + /// + /// OS-specific simple "fire-and-forget" command execution parameters. + /// Note that session state/result is not available for this type of execution. + /// + /// Command to execute. + public class ExecRunParams(string command) : AExecParams + { + internal NowMsgExecRun ToNowMessage(uint sessionId) + { + return new NowMsgExecRun(sessionId, command); + } + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/ExecSession.cs b/nugets/Devolutions.NowClient/ExecSession.cs new file mode 100644 index 0000000..ed550cf --- /dev/null +++ b/nugets/Devolutions.NowClient/ExecSession.cs @@ -0,0 +1,196 @@ +using System.Diagnostics; +using System.Threading.Channels; + +using Devolutions.NowClient.Worker; +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient +{ + public delegate void StdoutHandler(uint sessionId, ArraySegment data, bool last); + + public delegate void StderrHandler(uint sessionId, ArraySegment data, bool last); + + public delegate void StartedHandler(uint sessionId); + + /// + /// Active execution session handler. + /// + public class ExecSession : IExecSessionHandler + { + void IExecSessionHandler.HandleOutput(NowMsgExecData msg) + { + switch (msg.Stream) + { + case NowMsgExecData.StreamKind.Stdout when _onStdout != null: + _onStdout(SessionId, msg.Data, msg.Last); + return; + case NowMsgExecData.StreamKind.Stderr when _onStderr != null: + _onStderr(SessionId, msg.Data, msg.Last); + return; + case NowMsgExecData.StreamKind.Stdin: + default: + Debug.WriteLine("Received unexpected output stream"); + break; + } + } + + void IExecSessionHandler.HandleStarted() + { + _onStarted?.Invoke(SessionId); + } + + void IExecSessionHandler.HandleCancelRsp(NowMsgExecCancelRsp msg) + { + if (!_cancelPending) + { + Debug.WriteLine("Received unexpected cancel response"); + return; + } + + _cancelResponse = msg; + _cancelReceived.Release(); + _responseReceivedEvent.Release(); + } + + void IExecSessionHandler.HandleResult(NowMsgExecResult msg) + { + _result = msg; + _responseReceivedEvent.Release(); + _cancelReceived.Release(); + } + + internal ExecSession( + uint sessionId, + ChannelWriter commandWriter, + StdoutHandler? onStdout, + StderrHandler? onStderr, + StartedHandler? onStarted + ) + { + SessionId = sessionId; + _onStdout = onStdout; + _onStderr = onStderr; + _onStarted = onStarted; + _commandWriter = commandWriter; + } + + /// + /// Send an abort signal to the remote host with given exit code (if supported by OS). + /// Successfully sent abort signal will mark the session as terminated both on + /// server and client sides. + /// + public async Task Abort(uint exitCode) + { + ThrowIfExited(); + + await _commandWriter.WriteAsync(new CommandExecAbort(SessionId, exitCode)); + _canceled = true; + _cancelReceived.Release(1); + _responseReceivedEvent.Release(1); + } + + /// + /// Send execution session cancel request to the remote host. This method will wait + /// server response and throw an exception if the request has failed. Session is only + /// considered cancelled if the server returned success response. + /// + public async Task Cancel() + { + ThrowIfExited(); + + if (_cancelResponse != null) + { + _cancelResponse.ThrowIfError(); + return; + } + + if (!_cancelPending) + { + _cancelPending = true; + await _commandWriter.WriteAsync(new CommandExecCancel(SessionId)); + } + + await _cancelReceived.WaitAsync(); + + if (_cancelResponse == null) + { + // Graceful session exit or abort could have happened in the meantime. + throw new NowSessionException(SessionId, NowSessionException.NowSessionExceptionKind.ExitedSessionInteraction); + } + + _cancelResponse.ThrowIfError(); + + // mark as completed only if cancel was successful + _canceled = true; + } + + /// + /// Send stdin data to the remote host. + /// + public async Task SendStdin(ArraySegment data, bool last) + { + ThrowIfExited(); + + if (_lastStdinSent) + { + throw new NowSessionException(SessionId, NowSessionException.NowSessionExceptionKind.StdinClosed); + } + + if (last) + { + _lastStdinSent = true; + } + + await _commandWriter.WriteAsync(new CommandExecData(SessionId, data, last)); + } + + /// + /// Wait execution session to complete and return the exit code. Method will throw + /// an exception if execution session has encountered an error. (non-zero exit + /// codes are still considered as successful execution result) + /// + public async Task GetResult() + { + + if (_result != null) + { + return _result.GetExitCodeOrThrow(); + } + + await _responseReceivedEvent.WaitAsync(); + + return _canceled + ? throw new NowSessionException(SessionId, NowSessionException.NowSessionExceptionKind.Terminated) + : _result?.GetExitCodeOrThrow() + ?? throw new NowClientException("No result received"); + } + + /// + /// Current session id. + /// + public uint SessionId { get; } + + private void ThrowIfExited() + { + if (_result != null || _canceled) + { + throw new NowSessionException(SessionId, NowSessionException.NowSessionExceptionKind.ExitedSessionInteraction); + } + } + + private bool _lastStdinSent; + private bool _canceled; + private bool _cancelPending; + private NowMsgExecCancelRsp? _cancelResponse; + private NowMsgExecResult? _result; + + private readonly SemaphoreSlim _cancelReceived = new(0, 1); + private readonly SemaphoreSlim _responseReceivedEvent = new(0, 1); + + private readonly StdoutHandler? _onStdout; + private readonly StderrHandler? _onStderr; + private readonly StartedHandler? _onStarted; + + private readonly ChannelWriter _commandWriter; + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/ExecShellParams.cs b/nugets/Devolutions.NowClient/ExecShellParams.cs new file mode 100644 index 0000000..9aa8b1c --- /dev/null +++ b/nugets/Devolutions.NowClient/ExecShellParams.cs @@ -0,0 +1,50 @@ +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient +{ + /// + /// Shell(e.g. sh, bash, etc.) remote execution session parameters. + /// + /// Command/script to execute. + public class ExecShellParams(string command) : AExecParams + { + /// + /// Set the shell to use for the command/script execution. + /// Uses default system shell if not set. + /// + public ExecShellParams Shell(string shell) + { + _shell = shell; + return this; + } + + /// + /// Set the working directory for the command/script. + /// + public ExecShellParams Directory(string directory) + { + _directory = directory; + return this; + } + + internal NowMsgExecShell ToNowMessage(uint sessionId) + { + var builder = new NowMsgExecShell.Builder(sessionId, command); + + if (_shell != null) + { + builder.Shell(_shell); + } + + if (_directory != null) + { + builder.Directory(_directory); + } + + return builder.Build(); + } + + private string? _shell = null; + private string? _directory = null; + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/ExecWinPsParams.cs b/nugets/Devolutions.NowClient/ExecWinPsParams.cs new file mode 100644 index 0000000..bf61786 --- /dev/null +++ b/nugets/Devolutions.NowClient/ExecWinPsParams.cs @@ -0,0 +1,141 @@ +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient +{ + /// + /// Powershell 5(PowerShell.exe) remote execution session parameters. + /// + /// PowerShell command/script to execute. + public class ExecWinPsParams(string filename) : AExecParams + { + /// + /// Set the working directory for the command/script. + /// + public ExecWinPsParams Directory(string directory) + { + _directory = directory; + return this; + } + + /// + /// Set the configuration name for the PowerShell session. (-ConfigurationName) + /// + public ExecWinPsParams ConfigurationName(string configurationName) + { + _configurationName = configurationName; + return this; + } + + /// + /// Set the execution policy for the PowerShell session. (-ExecutionPolicy) + /// + public ExecWinPsParams ExecutionPolicy(string executionPolicy) + { + _executionPolicy = executionPolicy; + return this; + } + + /// + /// Set the apartment state for the PowerShell session. (-Sta/-Mta) + /// + public ExecWinPsParams ApartmentState(NowMsgExecWinPs.ApartmentStateKind apartmentState) + { + _apartmentState = apartmentState; + return this; + } + + /// + /// Disable the PowerShell logo display. (-NoLogo) + /// + public ExecWinPsParams NoLogo() + { + _noLogo = true; + return this; + } + + + /// + /// Do not close the PowerShell session after the command/script execution. (-NoExit) + /// + public ExecWinPsParams NoExit() + { + _noExit = true; + return this; + } + + /// + /// Do not load the PowerShell profile. (-NoProfile) + /// + public ExecWinPsParams NoProfile() + { + _noProfile = true; + return this; + } + + /// + /// Run the PowerShell session in non-interactive mode. (-NonInteractive) + /// + public ExecWinPsParams NonInteractive() + { + _nonInteractive = true; + return this; + } + + internal NowMsgExecWinPs ToNowMessage(uint sessionId) + { + var builder = new NowMsgExecWinPs.Builder(sessionId, filename); + + if (_directory != null) + { + builder.Directory(_directory); + } + + if (_configurationName != null) + { + builder.ConfigurationName(_configurationName); + } + + if (_executionPolicy != null) + { + builder.ExecutionPolicy(_executionPolicy); + } + + if (_apartmentState != null) + { + builder.ApartmentState(_apartmentState.Value); + } + + if (_noLogo) + { + builder.SetNoLogo(); + } + + if (_noExit) + { + builder.SetNoExit(); + } + + if (_noProfile) + { + builder.SetNoProfile(); + } + + if (_nonInteractive) + { + builder.SetNonInteractive(); + } + + return builder.Build(); + } + + + private string? _configurationName = null; + private string? _executionPolicy = null; + private string? _directory = null; + private NowMsgExecWinPs.ApartmentStateKind? _apartmentState = null; + private bool _noLogo = false; + private bool _noExit = false; + private bool _noProfile = false; + private bool _nonInteractive = false; + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/IExecSessionHandler.cs b/nugets/Devolutions.NowClient/IExecSessionHandler.cs new file mode 100644 index 0000000..7c69534 --- /dev/null +++ b/nugets/Devolutions.NowClient/IExecSessionHandler.cs @@ -0,0 +1,12 @@ +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient +{ + internal interface IExecSessionHandler + { + void HandleOutput(NowMsgExecData msg); + void HandleStarted(); + void HandleCancelRsp(NowMsgExecCancelRsp msg); + void HandleResult(NowMsgExecResult msg); + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/IMessageBoxRspHandler.cs b/nugets/Devolutions.NowClient/IMessageBoxRspHandler.cs new file mode 100644 index 0000000..7f50e24 --- /dev/null +++ b/nugets/Devolutions.NowClient/IMessageBoxRspHandler.cs @@ -0,0 +1,9 @@ +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient +{ + internal interface IMessageBoxRspHandler + { + void HandleMessageBoxRsp(NowMsgSessionMessageBoxRsp response); + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/INowTransport.cs b/nugets/Devolutions.NowClient/INowTransport.cs new file mode 100644 index 0000000..011ddeb --- /dev/null +++ b/nugets/Devolutions.NowClient/INowTransport.cs @@ -0,0 +1,11 @@ +namespace Devolutions.NowClient +{ + /// + /// Interface for the NOW-proto transport layer (e.g. RDP DVC channel). + /// + public interface INowTransport + { + Task Write(byte[] data); + Task Read(); + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/MessageBoxParams.cs b/nugets/Devolutions.NowClient/MessageBoxParams.cs new file mode 100644 index 0000000..6390fb6 --- /dev/null +++ b/nugets/Devolutions.NowClient/MessageBoxParams.cs @@ -0,0 +1,69 @@ +using Devolutions.NowProto.Messages; + +using static Devolutions.NowProto.Messages.NowMsgSessionMessageBoxReq; + +namespace Devolutions.NowClient +{ + /// + /// Message box parameters. + /// + public class MessageBoxParams(string message) + { + /// + /// Sets the style of the message box. + /// + public MessageBoxParams Style(MessageBoxStyle style) + { + _style = style; + return this; + } + + /// + /// Sets the timeout of the message box. + /// + public MessageBoxParams Timeout(TimeSpan timeout) + { + _timeout = timeout; + return this; + } + + /// + /// Sets the title of the message box. + /// + public MessageBoxParams Title(string title) + { + _title = title; + return this; + } + + internal NowMsgSessionMessageBoxReq ToNowMessage(uint requestId, bool responseRequired) + { + var builder = new Builder(requestId, message); + if (_style != null) + { + builder.Style(_style.Value); + } + + if (_timeout != null) + { + builder.Timeout(_timeout.Value); + } + + if (_title != null) + { + builder.Title(_title); + } + + if (responseRequired) + { + builder.WithResponse(); + } + + return builder.Build(); + } + + private MessageBoxStyle? _style = null; + private TimeSpan? _timeout = null; + private string? _title = null; + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/MessageBoxResponse.cs b/nugets/Devolutions.NowClient/MessageBoxResponse.cs new file mode 100644 index 0000000..63f6c87 --- /dev/null +++ b/nugets/Devolutions.NowClient/MessageBoxResponse.cs @@ -0,0 +1,53 @@ +using Devolutions.NowProto.Messages; + +using MessageBoxResponseKind = Devolutions.NowProto.Messages.NowMsgSessionMessageBoxRsp.MessageBoxResponse; + +namespace Devolutions.NowClient +{ + /// + /// Message box response handler. + /// + public class MessageBoxResponse : IMessageBoxRspHandler + { + void IMessageBoxRspHandler.HandleMessageBoxRsp(NowMsgSessionMessageBoxRsp response) + { + if (_response != null) + { + throw new NowClientException("Invalid use of IMessageBoxRspHandler"); + } + + _response = response; + _responseReceivedEvent.Release(); + } + + /// + /// Returns the response to the message box. + /// + public async Task GetResponse() + { + // Already received response prior to this call. + if (_response != null) + { + return _response.GetResponseOrThrow(); + } + + await _responseReceivedEvent.WaitAsync(); + + return _response?.GetResponseOrThrow() + ?? throw new NowClientException("No message box response has been received."); + } + + internal MessageBoxResponse(uint requestId) + { + RequestId = requestId; + } + + public uint RequestId { get; } + + private NowMsgSessionMessageBoxRsp? _response; + + // SemaphoreSlim is explicitly supports async operation via WaitAsync and don't + // require semaphore.Release() to be called in the same thread as semaphore.Wait() + private readonly SemaphoreSlim _responseReceivedEvent = new(0, 1); + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/NowChannelTransport.cs b/nugets/Devolutions.NowClient/NowChannelTransport.cs new file mode 100644 index 0000000..9ce8ec7 --- /dev/null +++ b/nugets/Devolutions.NowClient/NowChannelTransport.cs @@ -0,0 +1,37 @@ +using Devolutions.NowProto; + +namespace Devolutions.NowClient +{ + /// + /// Transport wrapper with NOW-proto message + /// serialization/deserialization capabilities. + /// + internal class NowChannelTransport(INowTransport transport) + { + public async Task WriteMessage(INowSerialize message) + { + var cursor = new NowWriteCursor(_writeBuffer); + message.Serialize(cursor); + + var bytesFilled = checked((int)cursor.BytesFilled); + await transport.Write(_writeBuffer[0..bytesFilled]); + } + + public async Task ReadMessage() where T : INowDeserialize + { + var message = await ReadMessageAny(); + return message.Deserialize(); + } + + public async Task ReadMessageAny() + { + var frame = await transport.Read(); + var cursor = new NowReadCursor(frame); + + return NowMessage.Read(cursor); + } + + private const int DefaultBufferSize = 1024 * 64; // 64KB Buffer + private readonly byte[] _writeBuffer = new byte[DefaultBufferSize]; + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/NowClient.cs b/nugets/Devolutions.NowClient/NowClient.cs new file mode 100644 index 0000000..f0dd744 --- /dev/null +++ b/nugets/Devolutions.NowClient/NowClient.cs @@ -0,0 +1,331 @@ +using System.Diagnostics; +using System.Threading.Channels; + +using Devolutions.NowClient.Worker; +using Devolutions.NowProto.Capabilities; +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient +{ + /// + /// NOW-proto remote execution channel client. + /// + public class NowClient + { + /// + /// Performs connection and negotiation sequence to NOW-proto server + /// over the provided transport (e.g. DVC-based channel) and returns + /// a client object ready to accept commands. + /// + public static async Task Connect(INowTransport transportImpl) + { + var channel = new NowChannelTransport(transportImpl); + + // Support all capabilities by default on client side. + var clientCapabilities = new NowMsgChannelCapset.Builder() + .HeartbeatInterval(TimeSpan.FromSeconds(60)) + .SystemCapset(NowCapabilitySystem.All) + .SessionCapset(NowCapabilitySession.All) + .ExecCapset(NowCapabilityExec.All) + .Build(); + + await channel.WriteMessage(clientCapabilities); + + // Wait for downgraded capabilities from server or throw timeout error. + var capabilities = await channel.ReadMessage() + .WaitAsync(TimeSpan.FromSeconds(TimeoutConnectSeconds)); + + // Negotiation successful + Debug.WriteLine($"NOW channel negotiation complete"); + + // client -> worker communication channel. + // Reverse communication (e.g) worker -> client is done through passed handler objects. + var clientChannel = Channel.CreateBounded(IoChannelCapacity); + + var ctx = new WorkerCtx + { + NowChannel = channel, + Capabilities = capabilities, + LastHeartbeat = DateTime.Now, + Commands = clientChannel.Reader, + HeartbeatInterval = capabilities.HeartbeatInterval, + }; + + var workerTask = Task.Run(() => WorkerCtx.Run(ctx)); + + return new NowClient(capabilities, workerTask, clientChannel.Writer); + } + + /// + /// Send system shutdown command to the NOW-proto server. + /// + public async Task SystemShutdown(SystemShutdownParams shutdownParams) + { + ThrowIfWorkerTerminated(); + + if (!Capabilities.SystemCapset.HasFlag(NowCapabilitySystem.Shutdown)) + { + ThrowCapabilitiesError("Shutdown"); + } + + await _commandWriter.WriteAsync(new CommandSystemShutdown(shutdownParams.NowMessage)); + } + + /// + /// Send session lock command to the NOW-proto server. + /// + public async Task SessionLock() + { + ThrowIfWorkerTerminated(); + + if (!Capabilities.SessionCapset.HasFlag(NowCapabilitySession.Lock)) + { + ThrowCapabilitiesError("Session lock"); + } + + await _commandWriter.WriteAsync(new CommandSessionLock()); + } + + /// + /// Send session logoff command to the NOW-proto server. + /// + public async Task SessionLogoff() + { + ThrowIfWorkerTerminated(); + + if (!Capabilities.SessionCapset.HasFlag(NowCapabilitySession.Logoff)) + { + ThrowCapabilitiesError("Session logoff"); + } + + await _commandWriter.WriteAsync(new CommandSessionLogoff()); + } + + /// + /// Send message box request to the NOW-proto server. + /// This request will return response handler which could be + /// used to wait for the response result. + /// + public async Task SessionMessageBox(MessageBoxParams msgBoxParams) + { + ThrowIfWorkerTerminated(); + + if (!Capabilities.SessionCapset.HasFlag(NowCapabilitySession.Msgbox)) + { + ThrowCapabilitiesError("Message box"); + } + + var requestId = _nextMessageBoxId++; + var message = msgBoxParams.ToNowMessage(requestId, true); + var handler = new MessageBoxResponse(requestId); + var command = new CommandSessionMsgBox(message, handler); + + await _commandWriter.WriteAsync(command); + + return handler; + } + + /// + /// Send message box request to the NOW-proto server. + /// This request is fire-and-forget and does not wait for the response. + /// + public async Task SessionMessageBoxNoResponse(MessageBoxParams msgBoxParams) + { + ThrowIfWorkerTerminated(); + + if (!Capabilities.SessionCapset.HasFlag(NowCapabilitySession.Msgbox)) + { + ThrowCapabilitiesError("Message box"); + } + + var requestId = _nextMessageBoxId++; + var message = msgBoxParams.ToNowMessage(requestId, false); + var command = new CommandSessionMsgBox(message, null); + + await _commandWriter.WriteAsync(command); + } + + /// + /// Start a new simple remote execution session. + /// (see for more details). + /// + public async Task ExecRun(ExecRunParams execParams) + { + ThrowIfWorkerTerminated(); + + if (!Capabilities.ExecCapset.HasFlag(NowCapabilityExec.Run)) + { + ThrowCapabilitiesError("Run execution style"); + } + + var sessionId = _nextExecSessionId++; + var message = execParams.ToNowMessage(sessionId); + var command = new CommandExecRun(message); + + await _commandWriter.WriteAsync(command); + } + + /// + /// Start a new process remote execution session. + /// See for more details. + /// + public async Task ExecProcess(ExecProcessParams execParams) + { + ThrowIfWorkerTerminated(); + + if (!Capabilities.ExecCapset.HasFlag(NowCapabilityExec.Process)) + { + ThrowCapabilitiesError("Process execution style"); + } + + var sessionId = _nextExecSessionId++; + var message = execParams.ToNowMessage(sessionId); + var execSession = execParams.ToExecSession(sessionId, _commandWriter); + var command = new CommandExecProcess(message, execSession); + + await _commandWriter.WriteAsync(command); + + return execSession; + } + + /// + /// Start a new shell remote execution session. + /// See for more details. + /// + public async Task ExecShell(ExecShellParams execParams) + { + ThrowIfWorkerTerminated(); + + if (!Capabilities.ExecCapset.HasFlag(NowCapabilityExec.Shell)) + { + ThrowCapabilitiesError("Shell execution style"); + } + + var sessionId = _nextExecSessionId++; + var message = execParams.ToNowMessage(sessionId); + var execSession = execParams.ToExecSession(sessionId, _commandWriter); + var command = new CommandExecShell(message, execSession); + + await _commandWriter.WriteAsync(command); + + return execSession; + } + + /// + /// Start a new batch remote execution session. + /// See for more details. + /// + public async Task ExecBatch(ExecBatchParams execParams) + { + ThrowIfWorkerTerminated(); + + if (!Capabilities.ExecCapset.HasFlag(NowCapabilityExec.Batch)) + { + ThrowCapabilitiesError("Batch execution style"); + } + + var sessionId = _nextExecSessionId++; + var message = execParams.ToNowMessage(sessionId); + var execSession = execParams.ToExecSession(sessionId, _commandWriter); + var command = new CommandExecBatch(message, execSession); + + await _commandWriter.WriteAsync(command); + + return execSession; + } + + /// + /// Start a new PowerShell remote execution session. + /// See for more details. + /// + public async Task ExecWinPs(ExecWinPsParams execParams) + { + ThrowIfWorkerTerminated(); + + if (!Capabilities.ExecCapset.HasFlag(NowCapabilityExec.WinPs)) + { + ThrowCapabilitiesError("Windows PowerShell execution style"); + } + + var sessionId = _nextExecSessionId++; + var message = execParams.ToNowMessage(sessionId); + var execSession = execParams.ToExecSession(sessionId, _commandWriter); + var command = new CommandExecWinPs(message, execSession); + + await _commandWriter.WriteAsync(command); + + return execSession; + } + + /// + /// Start a new PowerShell Core remote execution session. + /// See for more details. + /// + public async Task ExecPwsh(ExecPwshParams execParams) + { + ThrowIfWorkerTerminated(); + + if (!Capabilities.ExecCapset.HasFlag(NowCapabilityExec.Pwsh)) + { + ThrowCapabilitiesError("Pwsh execution style"); + } + + var sessionId = _nextExecSessionId++; + var message = execParams.ToNowMessage(sessionId); + var execSession = execParams.ToExecSession(sessionId, _commandWriter); + var command = new CommandExecPwsh(message, execSession); + + await _commandWriter.WriteAsync(command); + + return execSession; + } + + /// + /// Gracefully terminate and close the NOW-proto communication channel. + /// + public async Task ForceTermiate() + { + await _commandWriter.WriteAsync(new CommandChannelClose()); + } + + private static void ThrowCapabilitiesError(string capability) + { + throw new NowClientException($"{capability} is not supported by server."); + } + + private void ThrowIfWorkerTerminated() + { + if (_runnerTask.IsCompleted) + { + throw new NowClientException("NOW-proto worker has been terminated."); + } + } + + private NowClient( + NowMsgChannelCapset capabilities, + Task runnerTask, + ChannelWriter commandWriter + ) + { + this.Capabilities = capabilities; + this._runnerTask = runnerTask; + this._commandWriter = commandWriter; + } + + private const int TimeoutConnectSeconds = 10; + private const int IoChannelCapacity = 1024; + + /// + /// Check if the NOW-proto channel has been terminated. + /// + public bool IsTerminated => _runnerTask.IsCompleted; + + public NowMsgChannelCapset Capabilities { get; } + + private uint _nextMessageBoxId = 0; + private uint _nextExecSessionId = 0; + + private readonly Task _runnerTask; + private readonly ChannelWriter _commandWriter; + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/NowClientException.cs b/nugets/Devolutions.NowClient/NowClientException.cs new file mode 100644 index 0000000..8511d09 --- /dev/null +++ b/nugets/Devolutions.NowClient/NowClientException.cs @@ -0,0 +1,13 @@ +namespace Devolutions.NowClient +{ + /// + /// Base class for all exceptions thrown by the NowClient library. + /// + public class NowClientException : Exception + { + internal NowClientException(string displayMessage) + : base(displayMessage) + { + } + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/NowSessionException.cs b/nugets/Devolutions.NowClient/NowSessionException.cs new file mode 100644 index 0000000..2abbc91 --- /dev/null +++ b/nugets/Devolutions.NowClient/NowSessionException.cs @@ -0,0 +1,27 @@ +namespace Devolutions.NowClient +{ + internal class NowSessionException(uint sessionId, NowSessionException.NowSessionExceptionKind kind) + : NowClientException(GetDisplayMessage(kind)) + { + public enum NowSessionExceptionKind + { + ExitedSessionInteraction = 1, + Terminated = 2, + StdinClosed = 3, + } + + private static string GetDisplayMessage(NowSessionExceptionKind kind) + { + return kind switch + { + NowSessionExceptionKind.ExitedSessionInteraction => "Can't interact with already exited session.", + NowSessionExceptionKind.Terminated => "Session has been cancelled.", + NowSessionExceptionKind.StdinClosed => "Can't send data to already closed stdin stream.", + _ => $"Unknown session exception." + }; + } + + public NowSessionExceptionKind Kind => kind; + public uint SessionId => sessionId; + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/README.md b/nugets/Devolutions.NowClient/README.md new file mode 100644 index 0000000..c06a4f4 --- /dev/null +++ b/nugets/Devolutions.NowClient/README.md @@ -0,0 +1,17 @@ +NOW-proto client (C# library) +===================================== + +This library provides implements high level async client for `NowProto` protocol. + +### NowClient Architecture overview +- Provides async client implementation for NOW-proto remote execution channel. +- Transport layer is detached from the actual underlying transport implementation details via + `INowTransport` interface. +- All long-running operations (e.g. remote execution session & message box responses) return + proxy objects which could be used to wait for the operation to complete or send additional + commands on demand (e.g. cancel execution or send stdin data). +- Client uses background worker task to handle incoming messages both from user code calling client + and messages coming from the server. +- `NowClient` is thread safe and could be used from multiple + threads simultaneously (only mpsc `Channel` and a few atomic + variables are used) diff --git a/nugets/Devolutions.NowClient/SystemShutdownParams.cs b/nugets/Devolutions.NowClient/SystemShutdownParams.cs new file mode 100644 index 0000000..fcc8bfe --- /dev/null +++ b/nugets/Devolutions.NowClient/SystemShutdownParams.cs @@ -0,0 +1,50 @@ +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient +{ + /// + /// System shutdown parameters. + /// + public class SystemShutdownParams + { + /// + /// Enable forced shutdown mode. + /// + public SystemShutdownParams Force() + { + _builder = _builder.Force(true); + return this; + } + + /// + /// Reboot the system instead of shutting it down. + /// + public SystemShutdownParams Reboot() + { + _builder = _builder.Reboot(true); + return this; + } + + /// + /// Set timeout before system shutdown. + /// + public SystemShutdownParams Timeout(TimeSpan timeout) + { + _builder = _builder.Timeout(timeout); + return this; + } + + /// + /// Set optional shutdown message. + /// + public SystemShutdownParams Message(string message) + { + _builder = _builder.Message(message); + return this; + } + + internal NowMsgSystemShutdown NowMessage => _builder.Build(); + + private NowMsgSystemShutdown.Builder _builder = new(); + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/Worker/CommandChannelClose.cs b/nugets/Devolutions.NowClient/Worker/CommandChannelClose.cs new file mode 100644 index 0000000..7206ab1 --- /dev/null +++ b/nugets/Devolutions.NowClient/Worker/CommandChannelClose.cs @@ -0,0 +1,15 @@ +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient.Worker +{ + internal class CommandChannelClose : IClientCommand + { + async Task IClientCommand.Execute(WorkerCtx ctx) + { + await ctx.NowChannel.WriteMessage(NowMsgChannelClose.Graceful()); + + // Exit worker and block any further command execution. + ctx.ExitRequested = true; + } + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/Worker/CommandExecAbort.cs b/nugets/Devolutions.NowClient/Worker/CommandExecAbort.cs new file mode 100644 index 0000000..58fb72c --- /dev/null +++ b/nugets/Devolutions.NowClient/Worker/CommandExecAbort.cs @@ -0,0 +1,18 @@ +using System.Text; + +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient.Worker +{ + internal class CommandExecAbort(uint sessionId, uint exitCode) : IClientCommand + { + async Task IClientCommand.Execute(WorkerCtx ctx) + { + await ctx.NowChannel.WriteMessage(new NowMsgExecAbort(sessionId, exitCode)); + + // Abort unregisters exec session unconditionally if the message + // was successfully sent. + ctx.ExecSessionHandlers.Remove(sessionId); + } + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/Worker/CommandExecBatch.cs b/nugets/Devolutions.NowClient/Worker/CommandExecBatch.cs new file mode 100644 index 0000000..ab95da0 --- /dev/null +++ b/nugets/Devolutions.NowClient/Worker/CommandExecBatch.cs @@ -0,0 +1,16 @@ +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient.Worker +{ + internal class CommandExecBatch(NowMsgExecBatch message, IExecSessionHandler handler) : IClientCommand + { + async Task IClientCommand.Execute(WorkerCtx ctx) + { + await ctx.NowChannel.WriteMessage(message); + + // Register the handler for the session to receive + // the result and task output. + ctx.ExecSessionHandlers[message.SessionId] = handler; + } + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/Worker/CommandExecCancel.cs b/nugets/Devolutions.NowClient/Worker/CommandExecCancel.cs new file mode 100644 index 0000000..b320ca3 --- /dev/null +++ b/nugets/Devolutions.NowClient/Worker/CommandExecCancel.cs @@ -0,0 +1,14 @@ +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient.Worker +{ + internal class CommandExecCancel(uint sessionId) : IClientCommand + { + Task IClientCommand.Execute(WorkerCtx ctx) + { + // This only sends the request to cancel the session, the session state + // will be only changed when the response is received. + return ctx.NowChannel.WriteMessage(new NowMsgExecCancelReq(sessionId)); + } + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/Worker/CommandExecData.cs b/nugets/Devolutions.NowClient/Worker/CommandExecData.cs new file mode 100644 index 0000000..6ca3a6a --- /dev/null +++ b/nugets/Devolutions.NowClient/Worker/CommandExecData.cs @@ -0,0 +1,27 @@ +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient.Worker +{ + internal class CommandExecData( + uint sessionId, + ArraySegment data, + bool last + ) : IClientCommand + { + private const int DataChunkSize = 1024 * 32; // 32K chunks + + async Task IClientCommand.Execute(WorkerCtx ctx) + { + int offset = 0; + // Send data in chunks if provided input data is too large. + while (offset < data.Count) + { + int length = Math.Min(DataChunkSize, data.Count - offset); + + await ctx.NowChannel.WriteMessage(new NowMsgExecData(sessionId, NowMsgExecData.StreamKind.Stdin, last, + data[offset..(offset + length)])); + offset += length; + } + } + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/Worker/CommandExecProcess.cs b/nugets/Devolutions.NowClient/Worker/CommandExecProcess.cs new file mode 100644 index 0000000..20d2e50 --- /dev/null +++ b/nugets/Devolutions.NowClient/Worker/CommandExecProcess.cs @@ -0,0 +1,16 @@ +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient.Worker +{ + internal class CommandExecProcess(NowMsgExecProcess message, IExecSessionHandler handler) : IClientCommand + { + async Task IClientCommand.Execute(WorkerCtx ctx) + { + await ctx.NowChannel.WriteMessage(message); + + // Register the handler for the session to receive + // the result and task output. + ctx.ExecSessionHandlers[message.SessionId] = handler; + } + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/Worker/CommandExecPwsh.cs b/nugets/Devolutions.NowClient/Worker/CommandExecPwsh.cs new file mode 100644 index 0000000..9506543 --- /dev/null +++ b/nugets/Devolutions.NowClient/Worker/CommandExecPwsh.cs @@ -0,0 +1,16 @@ +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient.Worker +{ + internal class CommandExecPwsh(NowMsgExecPwsh message, IExecSessionHandler handler) : IClientCommand + { + async Task IClientCommand.Execute(WorkerCtx ctx) + { + await ctx.NowChannel.WriteMessage(message); + + // Register the handler for the session to receive + // the result and task output. + ctx.ExecSessionHandlers[message.SessionId] = handler; + } + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/Worker/CommandExecRun.cs b/nugets/Devolutions.NowClient/Worker/CommandExecRun.cs new file mode 100644 index 0000000..c726567 --- /dev/null +++ b/nugets/Devolutions.NowClient/Worker/CommandExecRun.cs @@ -0,0 +1,13 @@ +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient.Worker +{ + internal class CommandExecRun(NowMsgExecRun message) : IClientCommand + { + async Task IClientCommand.Execute(WorkerCtx ctx) + { + await ctx.NowChannel.WriteMessage(message); + // No handler is required for fire-and-forget execution. + } + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/Worker/CommandExecShell.cs b/nugets/Devolutions.NowClient/Worker/CommandExecShell.cs new file mode 100644 index 0000000..ecd3bf9 --- /dev/null +++ b/nugets/Devolutions.NowClient/Worker/CommandExecShell.cs @@ -0,0 +1,16 @@ +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient.Worker +{ + internal class CommandExecShell(NowMsgExecShell message, IExecSessionHandler handler) : IClientCommand + { + async Task IClientCommand.Execute(WorkerCtx ctx) + { + await ctx.NowChannel.WriteMessage(message); + + // Register the handler for the session to receive + // the result and task output. + ctx.ExecSessionHandlers[message.SessionId] = handler; + } + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/Worker/CommandExecWinPs.cs b/nugets/Devolutions.NowClient/Worker/CommandExecWinPs.cs new file mode 100644 index 0000000..f45e84b --- /dev/null +++ b/nugets/Devolutions.NowClient/Worker/CommandExecWinPs.cs @@ -0,0 +1,16 @@ +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient.Worker +{ + internal class CommandExecWinPs(NowMsgExecWinPs message, IExecSessionHandler handler) : IClientCommand + { + async Task IClientCommand.Execute(WorkerCtx ctx) + { + await ctx.NowChannel.WriteMessage(message); + + // Register the handler for the session to receive + // the result and task output. + ctx.ExecSessionHandlers[message.SessionId] = handler; + } + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/Worker/CommandSessionLock.cs b/nugets/Devolutions.NowClient/Worker/CommandSessionLock.cs new file mode 100644 index 0000000..208643f --- /dev/null +++ b/nugets/Devolutions.NowClient/Worker/CommandSessionLock.cs @@ -0,0 +1,12 @@ +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient.Worker +{ + internal class CommandSessionLock : IClientCommand + { + public async Task Execute(WorkerCtx ctx) + { + await ctx.NowChannel.WriteMessage(new NowMsgSessionLock()); + } + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/Worker/CommandSessionLogoff.cs b/nugets/Devolutions.NowClient/Worker/CommandSessionLogoff.cs new file mode 100644 index 0000000..6b6a4a1 --- /dev/null +++ b/nugets/Devolutions.NowClient/Worker/CommandSessionLogoff.cs @@ -0,0 +1,12 @@ +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient.Worker +{ + internal class CommandSessionLogoff : IClientCommand + { + public async Task Execute(WorkerCtx ctx) + { + await ctx.NowChannel.WriteMessage(new NowMsgSessionLogoff()); + } + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/Worker/CommandSessionMsgBox.cs b/nugets/Devolutions.NowClient/Worker/CommandSessionMsgBox.cs new file mode 100644 index 0000000..49c1676 --- /dev/null +++ b/nugets/Devolutions.NowClient/Worker/CommandSessionMsgBox.cs @@ -0,0 +1,18 @@ +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient.Worker +{ + internal class CommandSessionMsgBox(NowMsgSessionMessageBoxReq request, IMessageBoxRspHandler? handler) + : IClientCommand + { + public async Task Execute(WorkerCtx ctx) + { + await ctx.NowChannel.WriteMessage(request); + + if (handler != null) + { + ctx.MessageBoxHandlers[request.RequestId] = handler; + } + } + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/Worker/CommandSystemShutdown.cs b/nugets/Devolutions.NowClient/Worker/CommandSystemShutdown.cs new file mode 100644 index 0000000..fc9466f --- /dev/null +++ b/nugets/Devolutions.NowClient/Worker/CommandSystemShutdown.cs @@ -0,0 +1,14 @@ +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient.Worker +{ + internal class CommandSystemShutdown(NowMsgSystemShutdown request) : IClientCommand + { + public async Task Execute(WorkerCtx ctx) + { + await ctx.NowChannel.WriteMessage(_request); + } + + private readonly NowMsgSystemShutdown _request = request; + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/Worker/IClientCommand.cs b/nugets/Devolutions.NowClient/Worker/IClientCommand.cs new file mode 100644 index 0000000..dad682f --- /dev/null +++ b/nugets/Devolutions.NowClient/Worker/IClientCommand.cs @@ -0,0 +1,7 @@ +namespace Devolutions.NowClient.Worker +{ + internal interface IClientCommand + { + Task Execute(WorkerCtx ctx); + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowClient/Worker/WorkerCtx.cs b/nugets/Devolutions.NowClient/Worker/WorkerCtx.cs new file mode 100644 index 0000000..ef74226 --- /dev/null +++ b/nugets/Devolutions.NowClient/Worker/WorkerCtx.cs @@ -0,0 +1,239 @@ +using System.Diagnostics; +using System.Linq.Expressions; +using System.Threading.Channels; + +using Devolutions.NowProto; +using Devolutions.NowProto.Exceptions; +using Devolutions.NowProto.Messages; + +namespace Devolutions.NowClient.Worker +{ + /// + /// Background worker logic for NowClient. + /// + internal class WorkerCtx + { + public static async Task Run(WorkerCtx ctx) + { + Task? clientReadTask = null; + Task? serverReadTask = null; + Task? heartbeatCheckTask = null; + + // Main async IO loop. (akin to tokio's `select!`) + while (!ctx.ExitRequested) + { + var tasks = new List(); + + // Check if task was completed on the previous loop iteration. + // and re-add it to the list of tasks to be awaited. + if (clientReadTask == null) + { + clientReadTask = ctx.Commands.ReadAsync().AsTask(); + tasks.Add(clientReadTask); + } + + if (serverReadTask == null) + { + serverReadTask = ctx.NowChannel.ReadMessageAny(); + tasks.Add(serverReadTask); + } + + // Skip task of heartbeat interval was not negotiated. + if (heartbeatCheckTask == null && ctx.HeartbeatInterval != null) + { + heartbeatCheckTask = Task.Delay(ctx.HeartbeatInterval.Value * 2); + tasks.Add(heartbeatCheckTask); + } + + var completedTask = await Task.WhenAny(tasks); + + if (completedTask == clientReadTask) + { + var command = await clientReadTask; + clientReadTask = null; + // See concrete commands implementations in + // Devolutions.NowClient.Worker namespace. + await command.Execute(ctx); + } + else if (completedTask == serverReadTask) + { + NowMessage.NowMessageView message = await serverReadTask; + + switch (message.MessageClass) + { + case NowMessage.ClassChannel: + HandleChannelMessage(message, ctx); + break; + case NowMessage.ClassSystem: + // System messages are not expected to have responses(yet). + Debug.WriteLine($"Unhandled system message kind={message.MessageKind}"); + break; + case NowMessage.ClassSession: + HandleSessionMessage(message, ctx); + break; + case NowMessage.ClassExec: + HandleExecMessage(message, ctx); + break; + default: + Debug.WriteLine($"Unhandled message class={message.MessageClass}"); + break; + } + } + else if (completedTask == heartbeatCheckTask) + { + var heartbeatLeeway = TimeSpan.FromSeconds(5); + + heartbeatCheckTask = null; + if ((DateTime.Now - ctx.LastHeartbeat) > (ctx.HeartbeatInterval + heartbeatLeeway)) + { + // Channel is considered dead; No attempt to send any messages should be made. + ctx.ExitRequested = true; + } + } + } + } + + private static void HandleChannelMessage(NowMessage.NowMessageView message, WorkerCtx ctx) + { + var kind = (ChannelMessageKind)message.MessageKind; + + switch (kind) + { + case ChannelMessageKind.Heartbeat: + ctx.LastHeartbeat = DateTime.Now; + break; + case ChannelMessageKind.Close: + var decoded = message.Deserialize(); + ctx.ExitRequested = true; + try + { + decoded.ThrowIfError(); + } + catch (NowException e) + { + Debug.WriteLine($"Channel close error: {e.Message}"); + } + + break; + default: + Debug.WriteLine($"Unhandled channel message kind={message.MessageKind}"); + break; + } + } + + private static void HandleSessionMessage(NowMessage.NowMessageView message, WorkerCtx ctx) + { + var kind = (SessionMessageKind)message.MessageKind; + + switch (kind) + { + case SessionMessageKind.MsgBoxRsp: + var decoded = message.Deserialize(); + + if (ctx.MessageBoxHandlers.TryGetValue(decoded.RequestId, out IMessageBoxRspHandler? handler)) + { + handler.HandleMessageBoxRsp(decoded); + ctx.MessageBoxHandlers.Remove(decoded.RequestId); + } + else + { + Debug.WriteLine($"Received unexpected message box response with requestId={decoded.RequestId}"); + } + + break; + default: + Debug.WriteLine($"Unhandled session message kind: {message.MessageKind}"); + break; + } + } + + private static void HandleExecMessage(NowMessage.NowMessageView message, WorkerCtx ctx) + { + var kind = (ExecMessageKind)message.MessageKind; + switch (kind) + { + case ExecMessageKind.CancelRsp: + { + var decoded = message.Deserialize(); + + if (ctx.ExecSessionHandlers.TryGetValue(decoded.SessionId, out IExecSessionHandler? handler)) + { + handler.HandleCancelRsp(decoded); + // Unregister session if the cancel was successful. + if (decoded.IsSuccess) + { + ctx.ExecSessionHandlers.Remove(decoded.SessionId); + } + } + else + { + Debug.WriteLine( + $"Received unexpected exec cancel response with sessionId={decoded.SessionId}"); + } + + break; + } + case ExecMessageKind.Data: + { + var decoded = message.Deserialize(); + if (ctx.ExecSessionHandlers.TryGetValue(decoded.SessionId, out IExecSessionHandler? handler)) + { + handler.HandleOutput(decoded); + } + else + { + Debug.WriteLine($"Received unexpected exec data with sessionId={decoded.SessionId}"); + } + + break; + } + case ExecMessageKind.Started: + { + var decoded = message.Deserialize(); + if (ctx.ExecSessionHandlers.TryGetValue(decoded.SessionId, out IExecSessionHandler? handler)) + { + handler.HandleStarted(); + } + else + { + Debug.WriteLine($"Received unexpected exec started with sessionId={decoded.SessionId}"); + } + + break; + } + case ExecMessageKind.Result: + { + var decoded = message.Deserialize(); + if (ctx.ExecSessionHandlers.TryGetValue(decoded.SessionId, out IExecSessionHandler? handler)) + { + handler.HandleResult(decoded); + // Unregister session after receiving the result. + ctx.ExecSessionHandlers.Remove(decoded.SessionId); + } + else + { + Debug.WriteLine($"Received unexpected exec result with sessionId={decoded.SessionId}"); + } + + break; + } + default: + Debug.WriteLine($"Unhandled exec message kind: {message.MessageKind}"); + break; + } + } + + // -- IO -- + public required NowChannelTransport NowChannel; + public required ChannelReader Commands; + + // -- State -- + public required TimeSpan? HeartbeatInterval; + public required DateTime LastHeartbeat; + public required NowMsgChannelCapset Capabilities; + public bool ExitRequested = false; + + public Dictionary MessageBoxHandlers = []; + public Dictionary ExecSessionHandlers = []; + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowProto.Tests/Devolutions.NowProto.Tests.csproj b/nugets/Devolutions.NowProto.Tests/Devolutions.NowProto.Tests.csproj index 7314cf4..855bb16 100644 --- a/nugets/Devolutions.NowProto.Tests/Devolutions.NowProto.Tests.csproj +++ b/nugets/Devolutions.NowProto.Tests/Devolutions.NowProto.Tests.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - + Win32;x64;ARM64 false true diff --git a/nugets/Devolutions.NowProto.Tests/MsgChannel.cs b/nugets/Devolutions.NowProto.Tests/MsgChannel.cs index ab59363..c098314 100644 --- a/nugets/Devolutions.NowProto.Tests/MsgChannel.cs +++ b/nugets/Devolutions.NowProto.Tests/MsgChannel.cs @@ -1,4 +1,4 @@ -using Devolutions.NowProto.Capabilities; +using Devolutions.NowProto.Capabilities; namespace Devolutions.NowProto.Tests { diff --git a/nugets/Devolutions.NowProto.Tests/MsgExecGeneral.cs b/nugets/Devolutions.NowProto.Tests/MsgExecGeneral.cs index 2dae0d6..68be812 100644 --- a/nugets/Devolutions.NowProto.Tests/MsgExecGeneral.cs +++ b/nugets/Devolutions.NowProto.Tests/MsgExecGeneral.cs @@ -48,7 +48,7 @@ public void CancelRsp() var decoded = NowTest.MessageRoundtrip(msg, encoded); - Assert.Equal(msg.RequestId, decoded.RequestId); + Assert.Equal(msg.SessionId, decoded.SessionId); Assert.True(msg.IsSuccess); msg.ThrowIfError(); } @@ -66,7 +66,7 @@ public void CancelRspError() var decoded = NowTest.MessageRoundtrip(msg, encoded); - Assert.Equal(msg.RequestId, decoded.RequestId); + Assert.Equal(msg.SessionId, decoded.SessionId); Assert.False(msg.IsSuccess); Assert.Throws(() => msg.ThrowIfError()); } @@ -85,7 +85,7 @@ public void Result() var decoded = NowTest.MessageRoundtrip(msg, encoded); - Assert.Equal(msg.RequestId, decoded.RequestId); + Assert.Equal(msg.SessionId, decoded.SessionId); Assert.True(msg.IsSuccess); Assert.Equal((uint)42, msg.GetExitCodeOrThrow()); } @@ -107,7 +107,7 @@ public void ResultError() var decoded = NowTest.MessageRoundtrip(msg, encoded); - Assert.Equal(msg.RequestId, decoded.RequestId); + Assert.Equal(msg.SessionId, decoded.SessionId); Assert.False(msg.IsSuccess); Assert.Throws(() => msg.GetExitCodeOrThrow()); } diff --git a/nugets/Devolutions.NowProto/Capabilities/NowCapabilityExec.cs b/nugets/Devolutions.NowProto/Capabilities/NowCapabilityExec.cs index 47f5328..a5042d7 100644 --- a/nugets/Devolutions.NowProto/Capabilities/NowCapabilityExec.cs +++ b/nugets/Devolutions.NowProto/Capabilities/NowCapabilityExec.cs @@ -46,5 +46,7 @@ public enum NowCapabilityExec : ushort /// NOW-PROTO: NOW_CAP_EXEC_STYLE_PWSH /// Pwsh = 0x0020, + + All = Run | Process | Shell | Batch | WinPs | Pwsh, } } \ No newline at end of file diff --git a/nugets/Devolutions.NowProto/Capabilities/NowCapabilitySession.cs b/nugets/Devolutions.NowProto/Capabilities/NowCapabilitySession.cs index 497ffe7..2d2d63f 100644 --- a/nugets/Devolutions.NowProto/Capabilities/NowCapabilitySession.cs +++ b/nugets/Devolutions.NowProto/Capabilities/NowCapabilitySession.cs @@ -28,5 +28,7 @@ public enum NowCapabilitySession : ushort /// NOW-PROTO: NOW_CAP_SESSION_MSGBOX /// Msgbox = 0x0004, + + All = Lock | Logoff | Msgbox, } } \ No newline at end of file diff --git a/nugets/Devolutions.NowProto/Capabilities/NowCapabilitySystem.cs b/nugets/Devolutions.NowProto/Capabilities/NowCapabilitySystem.cs index c04e23c..c19aac8 100644 --- a/nugets/Devolutions.NowProto/Capabilities/NowCapabilitySystem.cs +++ b/nugets/Devolutions.NowProto/Capabilities/NowCapabilitySystem.cs @@ -16,5 +16,7 @@ public enum NowCapabilitySystem : ushort /// NOW-PROTO: NOW_CAP_SYSTEM_SHUTDOWN /// Shutdown = 0x0001, + + All = Shutdown, } } \ No newline at end of file diff --git a/nugets/Devolutions.NowProto/ChannelMessageKind.cs b/nugets/Devolutions.NowProto/ChannelMessageKind.cs new file mode 100644 index 0000000..30f3d5e --- /dev/null +++ b/nugets/Devolutions.NowProto/ChannelMessageKind.cs @@ -0,0 +1,9 @@ +namespace Devolutions.NowProto +{ + public enum ChannelMessageKind : byte + { + Capset = 0x01, + Heartbeat = 0x02, + Close = 0x03, + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowProto/Devolutions.NowProto.csproj b/nugets/Devolutions.NowProto/Devolutions.NowProto.csproj index fa71b7a..650734e 100644 --- a/nugets/Devolutions.NowProto/Devolutions.NowProto.csproj +++ b/nugets/Devolutions.NowProto/Devolutions.NowProto.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + Win32;x64;ARM64 diff --git a/nugets/Devolutions.NowProto/ExecMessageKind.cs b/nugets/Devolutions.NowProto/ExecMessageKind.cs new file mode 100644 index 0000000..c9dad06 --- /dev/null +++ b/nugets/Devolutions.NowProto/ExecMessageKind.cs @@ -0,0 +1,18 @@ +namespace Devolutions.NowProto +{ + public enum ExecMessageKind : byte + { + Abort = 0x01, + CancelReq = 0x02, + CancelRsp = 0x03, + Result = 0x04, + Data = 0x05, + Started = 0x06, + Run = 0x10, + Process = 0x11, + Shell = 0x12, + Batch = 0x13, + Winps = 0x14, + Pwsh = 0x15, + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowProto/Messages/NowMsgChannelCapset.cs b/nugets/Devolutions.NowProto/Messages/NowMsgChannelCapset.cs index 0799580..12ea8c9 100644 --- a/nugets/Devolutions.NowProto/Messages/NowMsgChannelCapset.cs +++ b/nugets/Devolutions.NowProto/Messages/NowMsgChannelCapset.cs @@ -18,8 +18,8 @@ public class NowMsgChannelCapset : INowSerialize, INowDeserialize NowMessage.ClassChannel; - static byte INowMessage.TypeMessageKind => 0x01; // NOW-PROTO: NOW_EXEC_CAPSET_MSG_ID + public static byte TypeMessageClass => NowMessage.ClassChannel; + public static byte TypeMessageKind => 0x01; // NOW-PROTO: NOW_EXEC_CAPSET_MSG_ID byte INowMessage.MessageClass => NowMessage.ClassChannel; byte INowMessage.MessageKind => 0x01; diff --git a/nugets/Devolutions.NowProto/Messages/NowMsgChannelClose.cs b/nugets/Devolutions.NowProto/Messages/NowMsgChannelClose.cs index 4c1e483..289938c 100644 --- a/nugets/Devolutions.NowProto/Messages/NowMsgChannelClose.cs +++ b/nugets/Devolutions.NowProto/Messages/NowMsgChannelClose.cs @@ -13,8 +13,8 @@ public class NowMsgChannelClose : INowSerialize, INowDeserialize NowMessage.ClassChannel; - static byte INowMessage.TypeMessageKind => 0x03; // NOW-PROTO: NOW_CHANNEL_CLOSE_MSG_ID + public static byte TypeMessageClass => NowMessage.ClassChannel; + public static byte TypeMessageKind => 0x03; // NOW-PROTO: NOW_CHANNEL_CLOSE_MSG_ID byte INowMessage.MessageClass => NowMessage.ClassChannel; byte INowMessage.MessageKind => 0x03; diff --git a/nugets/Devolutions.NowProto/Messages/NowMsgChannelHeartbeat.cs b/nugets/Devolutions.NowProto/Messages/NowMsgChannelHeartbeat.cs index ff3221f..568c49e 100644 --- a/nugets/Devolutions.NowProto/Messages/NowMsgChannelHeartbeat.cs +++ b/nugets/Devolutions.NowProto/Messages/NowMsgChannelHeartbeat.cs @@ -11,8 +11,8 @@ public class NowMsgChannelHeartbeat : INowSerialize, INowDeserialize NowMessage.ClassChannel; - static byte INowMessage.TypeMessageKind => 0x02; // NOW-PROTO: NOW_CHANNEL_HEARTBEAT_MSG_ID + public static byte TypeMessageClass => NowMessage.ClassChannel; + public static byte TypeMessageKind => 0x02; // NOW-PROTO: NOW_CHANNEL_HEARTBEAT_MSG_ID byte INowMessage.MessageClass => NowMessage.ClassChannel; byte INowMessage.MessageKind => 0x02; diff --git a/nugets/Devolutions.NowProto/Messages/NowMsgExecAbort.cs b/nugets/Devolutions.NowProto/Messages/NowMsgExecAbort.cs index a9605d8..66aa422 100644 --- a/nugets/Devolutions.NowProto/Messages/NowMsgExecAbort.cs +++ b/nugets/Devolutions.NowProto/Messages/NowMsgExecAbort.cs @@ -11,8 +11,8 @@ public class NowMsgExecAbort(uint sessionId, uint exitCode) : INowSerialize, INo { // -- INowMessage -- - static byte INowMessage.TypeMessageClass => NowMessage.ClassExec; - static byte INowMessage.TypeMessageKind => 0x01; // NOW-PROTO: NOW_EXEC_ABORT_MSG_ID + public static byte TypeMessageClass => NowMessage.ClassExec; + public static byte TypeMessageKind => 0x01; // NOW-PROTO: NOW_EXEC_ABORT_MSG_ID byte INowMessage.MessageClass => NowMessage.ClassExec; byte INowMessage.MessageKind => 0x01; diff --git a/nugets/Devolutions.NowProto/Messages/NowMsgExecBatch.cs b/nugets/Devolutions.NowProto/Messages/NowMsgExecBatch.cs index 8a91954..3a6a39c 100644 --- a/nugets/Devolutions.NowProto/Messages/NowMsgExecBatch.cs +++ b/nugets/Devolutions.NowProto/Messages/NowMsgExecBatch.cs @@ -11,8 +11,8 @@ public class NowMsgExecBatch : INowSerialize, INowDeserialize { // -- INowMessage -- - static byte INowMessage.TypeMessageClass => NowMessage.ClassExec; - static byte INowMessage.TypeMessageKind => 0x13; // NOW-PROTO: NOW_EXEC_BATCH_MSG_ID + public static byte TypeMessageClass => NowMessage.ClassExec; + public static byte TypeMessageKind => 0x13; // NOW-PROTO: NOW_EXEC_BATCH_MSG_ID byte INowMessage.MessageClass => NowMessage.ClassExec; byte INowMessage.MessageKind => 0x13; diff --git a/nugets/Devolutions.NowProto/Messages/NowMsgExecCancelReq.cs b/nugets/Devolutions.NowProto/Messages/NowMsgExecCancelReq.cs index e538ab4..1cc44bd 100644 --- a/nugets/Devolutions.NowProto/Messages/NowMsgExecCancelReq.cs +++ b/nugets/Devolutions.NowProto/Messages/NowMsgExecCancelReq.cs @@ -9,8 +9,8 @@ public class NowMsgExecCancelReq(uint sessionId) : INowSerialize, INowDeserializ { // -- INowMessage -- - static byte INowMessage.TypeMessageClass => NowMessage.ClassExec; - static byte INowMessage.TypeMessageKind => 0x02; // NOW-PROTO: NOW_EXEC_CANCEL_REQ_MSG_ID + public static byte TypeMessageClass => NowMessage.ClassExec; + public static byte TypeMessageKind => 0x02; // NOW-PROTO: NOW_EXEC_CANCEL_REQ_MSG_ID byte INowMessage.MessageClass => NowMessage.ClassExec; byte INowMessage.MessageKind => 0x02; diff --git a/nugets/Devolutions.NowProto/Messages/NowMsgExecCancelRsp.cs b/nugets/Devolutions.NowProto/Messages/NowMsgExecCancelRsp.cs index 98fde7d..86f7444 100644 --- a/nugets/Devolutions.NowProto/Messages/NowMsgExecCancelRsp.cs +++ b/nugets/Devolutions.NowProto/Messages/NowMsgExecCancelRsp.cs @@ -12,10 +12,8 @@ public class NowMsgExecCancelRsp : INowSerialize, INowDeserialize NowMessage.ClassExec; - static byte INowMessage.TypeMessageKind => 0x03; // NOW-PROTO: NOW_EXEC_RESULT_MSG_ID + public static byte TypeMessageClass => NowMessage.ClassExec; + public static byte TypeMessageKind => 0x03; // NOW-PROTO: NOW_EXEC_RESULT_MSG_ID byte INowMessage.MessageClass => NowMessage.ClassExec; byte INowMessage.MessageKind => 0x03; @@ -27,7 +25,7 @@ public class NowMsgExecCancelRsp : INowSerialize, INowDeserialize public static byte TypeMessageClass => NowMessage.ClassExec; public static byte TypeMessageKind => 0x05; // NOW-PROTO: NOW_EXEC_DATA_MSG_ID - public byte MessageClass => NowMessage.ClassExec; - public byte MessageKind => 0x05; + byte INowMessage.MessageClass => NowMessage.ClassExec; + byte INowMessage.MessageKind => 0x05; // -- INowDeserialize -- - public static NowMsgExecData Deserialize(ushort flags, NowReadCursor cursor) + static NowMsgExecData INowDeserialize.Deserialize(ushort flags, NowReadCursor cursor) { cursor.EnsureEnoughBytes(FixedPartSize); var msgFlags = (MsgFlags)flags; diff --git a/nugets/Devolutions.NowProto/Messages/NowMsgExecProcess.cs b/nugets/Devolutions.NowProto/Messages/NowMsgExecProcess.cs index 024ec77..7c579c9 100644 --- a/nugets/Devolutions.NowProto/Messages/NowMsgExecProcess.cs +++ b/nugets/Devolutions.NowProto/Messages/NowMsgExecProcess.cs @@ -11,8 +11,8 @@ public class NowMsgExecProcess : INowSerialize, INowDeserialize NowMessage.ClassExec; - static byte INowMessage.TypeMessageKind => 0x11; // NOW-PROTO: NOW_EXEC_PROCESS_MSG_ID + public static byte TypeMessageClass => NowMessage.ClassExec; + public static byte TypeMessageKind => 0x11; // NOW-PROTO: NOW_EXEC_PROCESS_MSG_ID byte INowMessage.MessageClass => NowMessage.ClassExec; byte INowMessage.MessageKind => 0x11; diff --git a/nugets/Devolutions.NowProto/Messages/NowMsgExecPwsh.cs b/nugets/Devolutions.NowProto/Messages/NowMsgExecPwsh.cs index 4269615..97358e6 100644 --- a/nugets/Devolutions.NowProto/Messages/NowMsgExecPwsh.cs +++ b/nugets/Devolutions.NowProto/Messages/NowMsgExecPwsh.cs @@ -12,8 +12,8 @@ public class NowMsgExecPwsh : INowSerialize, INowDeserialize { // -- INowMessage -- - static byte INowMessage.TypeMessageClass => NowMessage.ClassExec; - static byte INowMessage.TypeMessageKind => 0x15; // NOW-PROTO: NOW_EXEC_PWSH_MSG_ID + public static byte TypeMessageClass => NowMessage.ClassExec; + public static byte TypeMessageKind => 0x15; // NOW-PROTO: NOW_EXEC_PWSH_MSG_ID byte INowMessage.MessageClass => NowMessage.ClassExec; byte INowMessage.MessageKind => 0x15; diff --git a/nugets/Devolutions.NowProto/Messages/NowMsgExecResult.cs b/nugets/Devolutions.NowProto/Messages/NowMsgExecResult.cs index 5180d13..d939df2 100644 --- a/nugets/Devolutions.NowProto/Messages/NowMsgExecResult.cs +++ b/nugets/Devolutions.NowProto/Messages/NowMsgExecResult.cs @@ -13,8 +13,8 @@ public class NowMsgExecResult : INowSerialize, INowDeserialize { // -- INowMessage -- - static byte INowMessage.TypeMessageClass => NowMessage.ClassExec; - static byte INowMessage.TypeMessageKind => 0x04; // NOW-PROTO: NOW_EXEC_RESULT_MSG_ID + public static byte TypeMessageClass => NowMessage.ClassExec; + public static byte TypeMessageKind => 0x04; // NOW-PROTO: NOW_EXEC_RESULT_MSG_ID byte INowMessage.MessageClass => NowMessage.ClassExec; byte INowMessage.MessageKind => 0x04; @@ -26,7 +26,7 @@ public class NowMsgExecResult : INowSerialize, INowDeserialize void INowSerialize.SerializeBody(NowWriteCursor cursor) { cursor.EnsureEnoughBytes(FixedPartSize); - cursor.WriteUint32Le(RequestId); + cursor.WriteUint32Le(SessionId); cursor.WriteUint32Le(_exitCode); _status.Serialize(cursor); } @@ -48,24 +48,24 @@ NowReadCursor cursor // -- impl -- - private const uint FixedPartSize = 8; // u32 requestId, u32 exitCode + private const uint FixedPartSize = 8; // u32 sessionId, u32 exitCode - internal NowMsgExecResult(uint requestId, uint exitCode, NowStatus status) + internal NowMsgExecResult(uint sessionId, uint exitCode, NowStatus status) { - RequestId = requestId; + SessionId = sessionId; _exitCode = exitCode; _status = status; } - public static NowMsgExecResult Success(uint requestId, uint exitCode) + public static NowMsgExecResult Success(uint sessionId, uint exitCode) { - return new NowMsgExecResult(requestId, exitCode, NowStatus.Success()); + return new NowMsgExecResult(sessionId, exitCode, NowStatus.Success()); } - public static NowMsgExecResult Error(uint requestId, NowStatusException exception) + public static NowMsgExecResult Error(uint sessionId, NowStatusException exception) { return new NowMsgExecResult( - requestId, + sessionId, 0, NowStatus.Error(exception) ); @@ -79,7 +79,7 @@ public uint GetExitCodeOrThrow() return _exitCode; } - public uint RequestId { get; private init; } + public uint SessionId { get; private init; } private readonly NowStatus _status; private readonly uint _exitCode; diff --git a/nugets/Devolutions.NowProto/Messages/NowMsgExecRun.cs b/nugets/Devolutions.NowProto/Messages/NowMsgExecRun.cs index fd4af3b..e62a1d3 100644 --- a/nugets/Devolutions.NowProto/Messages/NowMsgExecRun.cs +++ b/nugets/Devolutions.NowProto/Messages/NowMsgExecRun.cs @@ -14,8 +14,8 @@ public class NowMsgExecRun(uint sessionId, string command) : INowSerialize, INow { // -- INowMessage -- - static byte INowMessage.TypeMessageClass => NowMessage.ClassExec; - static byte INowMessage.TypeMessageKind => 0x10; // NOW-PROTO: NOW_EXEC_RUN_MSG_ID + public static byte TypeMessageClass => NowMessage.ClassExec; + public static byte TypeMessageKind => 0x10; // NOW-PROTO: NOW_EXEC_RUN_MSG_ID byte INowMessage.MessageClass => NowMessage.ClassExec; byte INowMessage.MessageKind => 0x10; diff --git a/nugets/Devolutions.NowProto/Messages/NowMsgExecShell.cs b/nugets/Devolutions.NowProto/Messages/NowMsgExecShell.cs index af7824d..fcba7a3 100644 --- a/nugets/Devolutions.NowProto/Messages/NowMsgExecShell.cs +++ b/nugets/Devolutions.NowProto/Messages/NowMsgExecShell.cs @@ -11,8 +11,8 @@ public class NowMsgExecShell : INowSerialize, INowDeserialize { // -- INowMessage -- - static byte INowMessage.TypeMessageClass => NowMessage.ClassExec; - static byte INowMessage.TypeMessageKind => 0x12; // NOW-PROTO: NOW_EXEC_SHELL_MSG_ID + public static byte TypeMessageClass => NowMessage.ClassExec; + public static byte TypeMessageKind => 0x12; // NOW-PROTO: NOW_EXEC_SHELL_MSG_ID byte INowMessage.MessageClass => NowMessage.ClassExec; byte INowMessage.MessageKind => 0x12; diff --git a/nugets/Devolutions.NowProto/Messages/NowMsgExecStarted.cs b/nugets/Devolutions.NowProto/Messages/NowMsgExecStarted.cs index 44cc136..118becb 100644 --- a/nugets/Devolutions.NowProto/Messages/NowMsgExecStarted.cs +++ b/nugets/Devolutions.NowProto/Messages/NowMsgExecStarted.cs @@ -10,8 +10,8 @@ public class NowMsgExecStarted(uint sessionId) : INowSerialize, INowDeserialize< { // -- INowMessage -- - static byte INowMessage.TypeMessageClass => NowMessage.ClassExec; - static byte INowMessage.TypeMessageKind => 0x06; // NOW-PROTO: NOW_EXEC_STARTED_MSG_ID + public static byte TypeMessageClass => NowMessage.ClassExec; + public static byte TypeMessageKind => 0x06; // NOW-PROTO: NOW_EXEC_STARTED_MSG_ID byte INowMessage.MessageClass => NowMessage.ClassExec; byte INowMessage.MessageKind => 0x06; diff --git a/nugets/Devolutions.NowProto/Messages/NowMsgExecWinPs.cs b/nugets/Devolutions.NowProto/Messages/NowMsgExecWinPs.cs index 4f9da85..71df9fe 100644 --- a/nugets/Devolutions.NowProto/Messages/NowMsgExecWinPs.cs +++ b/nugets/Devolutions.NowProto/Messages/NowMsgExecWinPs.cs @@ -12,8 +12,8 @@ public class NowMsgExecWinPs : INowSerialize, INowDeserialize { // -- INowMessage -- - static byte INowMessage.TypeMessageClass => NowMessage.ClassExec; - static byte INowMessage.TypeMessageKind => 0x14; // NOW-PROTO: NOW_EXEC_WIN_PS_MSG_ID + public static byte TypeMessageClass => NowMessage.ClassExec; + public static byte TypeMessageKind => 0x14; // NOW-PROTO: NOW_EXEC_WIN_PS_MSG_ID byte INowMessage.MessageClass => NowMessage.ClassExec; byte INowMessage.MessageKind => 0x14; diff --git a/nugets/Devolutions.NowProto/Messages/NowMsgSessionLock.cs b/nugets/Devolutions.NowProto/Messages/NowMsgSessionLock.cs index 0c892d3..ed10224 100644 --- a/nugets/Devolutions.NowProto/Messages/NowMsgSessionLock.cs +++ b/nugets/Devolutions.NowProto/Messages/NowMsgSessionLock.cs @@ -9,8 +9,8 @@ public class NowMsgSessionLock : INowSerialize, INowDeserialize NowMessage.ClassSession; - static byte INowMessage.TypeMessageKind => 0x01; // NOW-PROTO: NOW_SESSION_LOCK_MSG_ID + public static byte TypeMessageClass => NowMessage.ClassSession; + public static byte TypeMessageKind => 0x01; // NOW-PROTO: NOW_SESSION_LOCK_MSG_ID byte INowMessage.MessageClass => NowMessage.ClassSession; byte INowMessage.MessageKind => 0x01; diff --git a/nugets/Devolutions.NowProto/Messages/NowMsgSessionLogoff.cs b/nugets/Devolutions.NowProto/Messages/NowMsgSessionLogoff.cs index d790974..fe0d4ef 100644 --- a/nugets/Devolutions.NowProto/Messages/NowMsgSessionLogoff.cs +++ b/nugets/Devolutions.NowProto/Messages/NowMsgSessionLogoff.cs @@ -9,8 +9,8 @@ public class NowMsgSessionLogoff : INowSerialize, INowDeserialize NowMessage.ClassSession; - static byte INowMessage.TypeMessageKind => 0x02; // NOW-PROTO: NOW_SESSION_LOGOFF_MSG_ID + public static byte TypeMessageClass => NowMessage.ClassSession; + public static byte TypeMessageKind => 0x02; // NOW-PROTO: NOW_SESSION_LOGOFF_MSG_ID byte INowMessage.MessageClass => NowMessage.ClassSession; byte INowMessage.MessageKind => 0x02; diff --git a/nugets/Devolutions.NowProto/Messages/NowMsgSessionMessageBoxRsp.cs b/nugets/Devolutions.NowProto/Messages/NowMsgSessionMessageBoxRsp.cs index b51325d..194f477 100644 --- a/nugets/Devolutions.NowProto/Messages/NowMsgSessionMessageBoxRsp.cs +++ b/nugets/Devolutions.NowProto/Messages/NowMsgSessionMessageBoxRsp.cs @@ -16,8 +16,8 @@ public class NowMsgSessionMessageBoxRsp : INowSerialize, INowDeserialize NowMessage.ClassSession; public static byte TypeMessageKind => 0x04; // NOW-PROTO: NOW_SESSION_MSGBOX_RSP_MSG_ID - public byte MessageClass => NowMessage.ClassSession; - public byte MessageKind => 0x04; + byte INowMessage.MessageClass => NowMessage.ClassSession; + byte INowMessage.MessageKind => 0x04; // -- INowSerialize -- ushort INowSerialize.Flags => 0; @@ -33,7 +33,7 @@ void INowSerialize.SerializeBody(NowWriteCursor cursor) // -- INowDeserialize -- - public static NowMsgSessionMessageBoxRsp Deserialize(ushort flags, NowReadCursor cursor) + static NowMsgSessionMessageBoxRsp INowDeserialize.Deserialize(ushort flags, NowReadCursor cursor) { cursor.EnsureEnoughBytes(FixedPartSize); var requestId = cursor.ReadUInt32Le(); diff --git a/nugets/Devolutions.NowProto/Messages/NowMsgSessionMsgBoxReq.cs b/nugets/Devolutions.NowProto/Messages/NowMsgSessionMsgBoxReq.cs index 959e978..217638e 100644 --- a/nugets/Devolutions.NowProto/Messages/NowMsgSessionMsgBoxReq.cs +++ b/nugets/Devolutions.NowProto/Messages/NowMsgSessionMsgBoxReq.cs @@ -13,8 +13,8 @@ public class NowMsgSessionMessageBoxReq : INowSerialize, INowDeserialize NowMessage.ClassSession; - static byte INowMessage.TypeMessageKind => 0x03; // NOW-PROTO: NOW_SESSION_MSGBOX_REQ_MSG_ID + public static byte TypeMessageClass => NowMessage.ClassSession; + public static byte TypeMessageKind => 0x03; // NOW-PROTO: NOW_SESSION_MSGBOX_REQ_MSG_ID byte INowMessage.MessageClass => NowMessage.ClassSession; byte INowMessage.MessageKind => 0x03; diff --git a/nugets/Devolutions.NowProto/Messages/NowMsgSystemShutdown.cs b/nugets/Devolutions.NowProto/Messages/NowMsgSystemShutdown.cs index 817e451..5b91215 100644 --- a/nugets/Devolutions.NowProto/Messages/NowMsgSystemShutdown.cs +++ b/nugets/Devolutions.NowProto/Messages/NowMsgSystemShutdown.cs @@ -11,8 +11,8 @@ public class NowMsgSystemShutdown : INowSerialize, INowDeserialize NowMessage.ClassSystem; - static byte INowMessage.TypeMessageKind => 0x03; // NOW-PROTO: NOW_SYSTEM_SHUTDOWN_ID + public static byte TypeMessageClass => NowMessage.ClassSystem; + public static byte TypeMessageKind => 0x03; // NOW-PROTO: NOW_SYSTEM_SHUTDOWN_ID byte INowMessage.MessageClass => NowMessage.ClassSystem; byte INowMessage.MessageKind => 0x03; diff --git a/nugets/Devolutions.NowProto/NowMessage.cs b/nugets/Devolutions.NowProto/NowMessage.cs index d5244ea..35ed331 100644 --- a/nugets/Devolutions.NowProto/NowMessage.cs +++ b/nugets/Devolutions.NowProto/NowMessage.cs @@ -10,16 +10,16 @@ namespace Devolutions.NowProto public class NowMessage { // NOW-PROTO: NOW_CHANNEL_MSG_CLASS_ID - internal const byte ClassChannel = 0x10; + public const byte ClassChannel = 0x10; // NOW-PROTO: NOW_SYSTEM_MSG_CLASS_ID - internal const byte ClassSystem = 0x11; + public const byte ClassSystem = 0x11; // NOW-PROTO: NOW_SESSION_MSG_CLASS_ID - internal const byte ClassSession = 0x12; + public const byte ClassSession = 0x12; // NOW-PROTO: NOW_EXEC_MSG_CLASS_ID - internal const byte ClassExec = 0x13; + public const byte ClassExec = 0x13; /// /// Immutable message view. Allows to read message header and body without deserialization diff --git a/nugets/Devolutions.NowProto/NowWriteCursor.cs b/nugets/Devolutions.NowProto/NowWriteCursor.cs index 89dadae..876c511 100644 --- a/nugets/Devolutions.NowProto/NowWriteCursor.cs +++ b/nugets/Devolutions.NowProto/NowWriteCursor.cs @@ -23,6 +23,7 @@ public void EnsureEnoughBytes(uint length) public void Advance(uint length) { _buffer = _buffer[(int)length..]; + BytesFilled += length; } public void WriteBytes(ReadOnlySpan data) @@ -87,5 +88,6 @@ public void WriteUint32Le(uint value) } private ArraySegment _buffer = buffer; + public uint BytesFilled = 0; } } \ No newline at end of file diff --git a/nugets/Devolutions.NowProto/SessionMessageKind.cs b/nugets/Devolutions.NowProto/SessionMessageKind.cs new file mode 100644 index 0000000..9ba0a28 --- /dev/null +++ b/nugets/Devolutions.NowProto/SessionMessageKind.cs @@ -0,0 +1,10 @@ +namespace Devolutions.NowProto +{ + public enum SessionMessageKind : byte + { + Lock = 0x01, + Logoff = 0x02, + MsgBoxReq = 0x03, + MsgBoxRsp = 0x04, + } +} \ No newline at end of file diff --git a/nugets/Devolutions.NowProto/SystemMessageKind.cs b/nugets/Devolutions.NowProto/SystemMessageKind.cs new file mode 100644 index 0000000..30c00a5 --- /dev/null +++ b/nugets/Devolutions.NowProto/SystemMessageKind.cs @@ -0,0 +1,7 @@ +namespace Devolutions.NowProto +{ + public enum SystemMessageKind : byte + { + Shutdown = 0x03, + } +} \ No newline at end of file From 2bf06ffb5c51f964fbfb54e3353b0b1b89e1019e Mon Sep 17 00:00:00 2001 From: Vladyslav Nikonov Date: Fri, 7 Feb 2025 01:36:28 +0200 Subject: [PATCH 2/4] refactor: fixed bugs and minor refactoring --- NowProto.sln | 42 +++++++------------ .../Devolutions.NowClient.csproj | 2 +- .../Devolutions.NowClient/ExecBatchParams.cs | 2 +- .../Devolutions.NowClient/ExecPwshParams.cs | 2 +- nugets/Devolutions.NowClient/ExecSession.cs | 2 +- .../Devolutions.NowClient/ExecShellParams.cs | 2 +- .../Devolutions.NowClient/ExecWinPsParams.cs | 2 +- nugets/Devolutions.NowClient/NowClient.cs | 13 +++++- .../Devolutions.NowClient/Worker/WorkerCtx.cs | 26 ++++++++---- .../Devolutions.NowProto.Tests.csproj | 2 +- .../Devolutions.NowProto.csproj | 2 +- 11 files changed, 52 insertions(+), 45 deletions(-) diff --git a/NowProto.sln b/NowProto.sln index 5bf1e9d..175b9bf 100644 --- a/NowProto.sln +++ b/NowProto.sln @@ -13,50 +13,36 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Devolutions.NowClient", "nu EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|ARM64 = Debug|ARM64 - Debug|Win32 = Debug|Win32 Debug|x64 = Debug|x64 - Release|ARM64 = Release|ARM64 - Release|Win32 = Release|Win32 + Debug|x86 = Debug|x86 Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Debug|ARM64.Build.0 = Debug|ARM64 - {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Debug|Win32.ActiveCfg = Debug|Any CPU - {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Debug|Win32.Build.0 = Debug|Any CPU {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Debug|x64.ActiveCfg = Debug|x64 {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Debug|x64.Build.0 = Debug|x64 - {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Release|ARM64.ActiveCfg = Release|ARM64 - {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Release|ARM64.Build.0 = Release|ARM64 - {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Release|Win32.ActiveCfg = Release|Any CPU - {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Release|Win32.Build.0 = Release|Any CPU + {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Debug|x86.ActiveCfg = Debug|x86 + {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Debug|x86.Build.0 = Debug|x86 {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Release|x64.ActiveCfg = Release|x64 {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Release|x64.Build.0 = Release|x64 - {34912761-E8CB-4D88-A4B4-365F6DB88985}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {34912761-E8CB-4D88-A4B4-365F6DB88985}.Debug|ARM64.Build.0 = Debug|ARM64 - {34912761-E8CB-4D88-A4B4-365F6DB88985}.Debug|Win32.ActiveCfg = Debug|Any CPU - {34912761-E8CB-4D88-A4B4-365F6DB88985}.Debug|Win32.Build.0 = Debug|Any CPU + {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Release|x86.ActiveCfg = Release|x86 + {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Release|x86.Build.0 = Release|x86 {34912761-E8CB-4D88-A4B4-365F6DB88985}.Debug|x64.ActiveCfg = Debug|x64 {34912761-E8CB-4D88-A4B4-365F6DB88985}.Debug|x64.Build.0 = Debug|x64 - {34912761-E8CB-4D88-A4B4-365F6DB88985}.Release|ARM64.ActiveCfg = Release|ARM64 - {34912761-E8CB-4D88-A4B4-365F6DB88985}.Release|ARM64.Build.0 = Release|ARM64 - {34912761-E8CB-4D88-A4B4-365F6DB88985}.Release|Win32.ActiveCfg = Release|Any CPU - {34912761-E8CB-4D88-A4B4-365F6DB88985}.Release|Win32.Build.0 = Release|Any CPU + {34912761-E8CB-4D88-A4B4-365F6DB88985}.Debug|x86.ActiveCfg = Debug|x86 + {34912761-E8CB-4D88-A4B4-365F6DB88985}.Debug|x86.Build.0 = Debug|x86 {34912761-E8CB-4D88-A4B4-365F6DB88985}.Release|x64.ActiveCfg = Release|x64 {34912761-E8CB-4D88-A4B4-365F6DB88985}.Release|x64.Build.0 = Release|x64 - {4692658E-C138-4A40-A10C-7E55D38D250B}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {4692658E-C138-4A40-A10C-7E55D38D250B}.Debug|ARM64.Build.0 = Debug|ARM64 - {4692658E-C138-4A40-A10C-7E55D38D250B}.Debug|Win32.ActiveCfg = Debug|Any CPU - {4692658E-C138-4A40-A10C-7E55D38D250B}.Debug|Win32.Build.0 = Debug|Any CPU + {34912761-E8CB-4D88-A4B4-365F6DB88985}.Release|x86.ActiveCfg = Release|x86 + {34912761-E8CB-4D88-A4B4-365F6DB88985}.Release|x86.Build.0 = Release|x86 {4692658E-C138-4A40-A10C-7E55D38D250B}.Debug|x64.ActiveCfg = Debug|x64 {4692658E-C138-4A40-A10C-7E55D38D250B}.Debug|x64.Build.0 = Debug|x64 - {4692658E-C138-4A40-A10C-7E55D38D250B}.Release|ARM64.ActiveCfg = Release|ARM64 - {4692658E-C138-4A40-A10C-7E55D38D250B}.Release|ARM64.Build.0 = Release|ARM64 - {4692658E-C138-4A40-A10C-7E55D38D250B}.Release|Win32.ActiveCfg = Release|Any CPU - {4692658E-C138-4A40-A10C-7E55D38D250B}.Release|Win32.Build.0 = Release|Any CPU + {4692658E-C138-4A40-A10C-7E55D38D250B}.Debug|x86.ActiveCfg = Debug|x86 + {4692658E-C138-4A40-A10C-7E55D38D250B}.Debug|x86.Build.0 = Debug|x86 {4692658E-C138-4A40-A10C-7E55D38D250B}.Release|x64.ActiveCfg = Release|x64 {4692658E-C138-4A40-A10C-7E55D38D250B}.Release|x64.Build.0 = Release|x64 + {4692658E-C138-4A40-A10C-7E55D38D250B}.Release|x86.ActiveCfg = Release|x86 + {4692658E-C138-4A40-A10C-7E55D38D250B}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/nugets/Devolutions.NowClient/Devolutions.NowClient.csproj b/nugets/Devolutions.NowClient/Devolutions.NowClient.csproj index 905049a..0e1fa76 100644 --- a/nugets/Devolutions.NowClient/Devolutions.NowClient.csproj +++ b/nugets/Devolutions.NowClient/Devolutions.NowClient.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - Win32;x64;ARM64 + x86;x64;ARM64 diff --git a/nugets/Devolutions.NowClient/ExecBatchParams.cs b/nugets/Devolutions.NowClient/ExecBatchParams.cs index 73c0f28..b4968d1 100644 --- a/nugets/Devolutions.NowClient/ExecBatchParams.cs +++ b/nugets/Devolutions.NowClient/ExecBatchParams.cs @@ -3,7 +3,7 @@ namespace Devolutions.NowClient { /// - /// Batch(Windows CMD) remote execution session parameters. + /// Batch (Windows CMD) remote execution session parameters. /// /// Command/script to execute. public class ExecBatchParams(string command) : AExecParams diff --git a/nugets/Devolutions.NowClient/ExecPwshParams.cs b/nugets/Devolutions.NowClient/ExecPwshParams.cs index 9bdcc08..49df250 100644 --- a/nugets/Devolutions.NowClient/ExecPwshParams.cs +++ b/nugets/Devolutions.NowClient/ExecPwshParams.cs @@ -3,7 +3,7 @@ namespace Devolutions.NowClient { /// - /// Powershell 7(pwsh) remote execution session parameters. + /// Powershell 7 and higher (pwsh) remote execution session parameters. /// /// PowerShell command/script to execute. public class ExecPwshParams(string command) : AExecParams diff --git a/nugets/Devolutions.NowClient/ExecSession.cs b/nugets/Devolutions.NowClient/ExecSession.cs index ed550cf..bb70078 100644 --- a/nugets/Devolutions.NowClient/ExecSession.cs +++ b/nugets/Devolutions.NowClient/ExecSession.cs @@ -115,7 +115,7 @@ public async Task Cancel() if (_cancelResponse == null) { // Graceful session exit or abort could have happened in the meantime. - throw new NowSessionException(SessionId, NowSessionException.NowSessionExceptionKind.ExitedSessionInteraction); + return; } _cancelResponse.ThrowIfError(); diff --git a/nugets/Devolutions.NowClient/ExecShellParams.cs b/nugets/Devolutions.NowClient/ExecShellParams.cs index 9aa8b1c..7727ed1 100644 --- a/nugets/Devolutions.NowClient/ExecShellParams.cs +++ b/nugets/Devolutions.NowClient/ExecShellParams.cs @@ -3,7 +3,7 @@ namespace Devolutions.NowClient { /// - /// Shell(e.g. sh, bash, etc.) remote execution session parameters. + /// Shell (e.g. sh, bash, etc.) remote execution session parameters. /// /// Command/script to execute. public class ExecShellParams(string command) : AExecParams diff --git a/nugets/Devolutions.NowClient/ExecWinPsParams.cs b/nugets/Devolutions.NowClient/ExecWinPsParams.cs index bf61786..b43d321 100644 --- a/nugets/Devolutions.NowClient/ExecWinPsParams.cs +++ b/nugets/Devolutions.NowClient/ExecWinPsParams.cs @@ -3,7 +3,7 @@ namespace Devolutions.NowClient { /// - /// Powershell 5(PowerShell.exe) remote execution session parameters. + /// Powershell 5 (PowerShell.exe) remote execution session parameters. /// /// PowerShell command/script to execute. public class ExecWinPsParams(string filename) : AExecParams diff --git a/nugets/Devolutions.NowClient/NowClient.cs b/nugets/Devolutions.NowClient/NowClient.cs index f0dd744..ae29d65 100644 --- a/nugets/Devolutions.NowClient/NowClient.cs +++ b/nugets/Devolutions.NowClient/NowClient.cs @@ -51,7 +51,18 @@ public static async Task Connect(INowTransport transportImpl) HeartbeatInterval = capabilities.HeartbeatInterval, }; - var workerTask = Task.Run(() => WorkerCtx.Run(ctx)); + var workerTask = Task.Run(() => + { + try + { + return WorkerCtx.Run(ctx); + } + catch (Exception e) + { + Debug.WriteLine($"NowClient worker exception: {e}"); + throw; + } + }); return new NowClient(capabilities, workerTask, clientChannel.Writer); } diff --git a/nugets/Devolutions.NowClient/Worker/WorkerCtx.cs b/nugets/Devolutions.NowClient/Worker/WorkerCtx.cs index ef74226..096900d 100644 --- a/nugets/Devolutions.NowClient/Worker/WorkerCtx.cs +++ b/nugets/Devolutions.NowClient/Worker/WorkerCtx.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using System.Linq.Expressions; using System.Threading.Channels; using Devolutions.NowProto; @@ -19,23 +18,23 @@ public static async Task Run(WorkerCtx ctx) Task? serverReadTask = null; Task? heartbeatCheckTask = null; + var tasks = new List(); + // Main async IO loop. (akin to tokio's `select!`) while (!ctx.ExitRequested) { - var tasks = new List(); + tasks.Clear(); // Check if task was completed on the previous loop iteration. // and re-add it to the list of tasks to be awaited. if (clientReadTask == null) { clientReadTask = ctx.Commands.ReadAsync().AsTask(); - tasks.Add(clientReadTask); } if (serverReadTask == null) { serverReadTask = ctx.NowChannel.ReadMessageAny(); - tasks.Add(serverReadTask); } // Skip task of heartbeat interval was not negotiated. @@ -45,6 +44,9 @@ public static async Task Run(WorkerCtx ctx) tasks.Add(heartbeatCheckTask); } + tasks.Add(clientReadTask); + tasks.Add(serverReadTask); + var completedTask = await Task.WhenAny(tasks); if (completedTask == clientReadTask) @@ -57,7 +59,8 @@ public static async Task Run(WorkerCtx ctx) } else if (completedTask == serverReadTask) { - NowMessage.NowMessageView message = await serverReadTask; + var message = await serverReadTask; + serverReadTask = null; switch (message.MessageClass) { @@ -84,11 +87,15 @@ public static async Task Run(WorkerCtx ctx) var heartbeatLeeway = TimeSpan.FromSeconds(5); heartbeatCheckTask = null; - if ((DateTime.Now - ctx.LastHeartbeat) > (ctx.HeartbeatInterval + heartbeatLeeway)) + if (!((DateTime.Now - ctx.LastHeartbeat) > (ctx.HeartbeatInterval + heartbeatLeeway))) { - // Channel is considered dead; No attempt to send any messages should be made. - ctx.ExitRequested = true; + continue; } + + Debug.WriteLine("Heartbeat timeout triggered"); + + // Channel is considered dead; No attempt to send any messages should be made. + ctx.ExitRequested = true; } } } @@ -125,6 +132,8 @@ private static void HandleSessionMessage(NowMessage.NowMessageView message, Work { var kind = (SessionMessageKind)message.MessageKind; + Debug.WriteLine($"Received session message"); + switch (kind) { case SessionMessageKind.MsgBoxRsp: @@ -150,6 +159,7 @@ private static void HandleSessionMessage(NowMessage.NowMessageView message, Work private static void HandleExecMessage(NowMessage.NowMessageView message, WorkerCtx ctx) { var kind = (ExecMessageKind)message.MessageKind; + switch (kind) { case ExecMessageKind.CancelRsp: diff --git a/nugets/Devolutions.NowProto.Tests/Devolutions.NowProto.Tests.csproj b/nugets/Devolutions.NowProto.Tests/Devolutions.NowProto.Tests.csproj index 855bb16..6f38842 100644 --- a/nugets/Devolutions.NowProto.Tests/Devolutions.NowProto.Tests.csproj +++ b/nugets/Devolutions.NowProto.Tests/Devolutions.NowProto.Tests.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - Win32;x64;ARM64 + x86;x64 false true diff --git a/nugets/Devolutions.NowProto/Devolutions.NowProto.csproj b/nugets/Devolutions.NowProto/Devolutions.NowProto.csproj index 650734e..24bb675 100644 --- a/nugets/Devolutions.NowProto/Devolutions.NowProto.csproj +++ b/nugets/Devolutions.NowProto/Devolutions.NowProto.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - Win32;x64;ARM64 + x86;x64;ARM64 From 6dfbb8a2e3e122480b936cbc0682e320dd78d65c Mon Sep 17 00:00:00 2001 From: Vladyslav Nikonov Date: Sat, 8 Feb 2025 01:37:53 +0200 Subject: [PATCH 3/4] refacor: session cancellation bugfix --- nugets/Devolutions.NowClient/ExecSession.cs | 21 +++++++++---------- .../Devolutions.NowClient/Worker/WorkerCtx.cs | 5 ----- .../Messages/NowMsgExecPwsh.cs | 6 ++---- .../Messages/NowMsgExecWinPs.cs | 6 ++---- 4 files changed, 14 insertions(+), 24 deletions(-) diff --git a/nugets/Devolutions.NowClient/ExecSession.cs b/nugets/Devolutions.NowClient/ExecSession.cs index bb70078..97bd492 100644 --- a/nugets/Devolutions.NowClient/ExecSession.cs +++ b/nugets/Devolutions.NowClient/ExecSession.cs @@ -49,14 +49,16 @@ void IExecSessionHandler.HandleCancelRsp(NowMsgExecCancelRsp msg) _cancelResponse = msg; _cancelReceived.Release(); - _responseReceivedEvent.Release(); } void IExecSessionHandler.HandleResult(NowMsgExecResult msg) { _result = msg; _responseReceivedEvent.Release(); - _cancelReceived.Release(); + if (_cancelPending) + { + _cancelReceived.Release(); + } } internal ExecSession( @@ -84,9 +86,9 @@ public async Task Abort(uint exitCode) ThrowIfExited(); await _commandWriter.WriteAsync(new CommandExecAbort(SessionId, exitCode)); - _canceled = true; - _cancelReceived.Release(1); - _responseReceivedEvent.Release(1); + _aborted = true; + _cancelReceived.Release(); + _responseReceivedEvent.Release(); } /// @@ -119,9 +121,6 @@ public async Task Cancel() } _cancelResponse.ThrowIfError(); - - // mark as completed only if cancel was successful - _canceled = true; } /// @@ -159,7 +158,7 @@ public async Task GetResult() await _responseReceivedEvent.WaitAsync(); - return _canceled + return _aborted ? throw new NowSessionException(SessionId, NowSessionException.NowSessionExceptionKind.Terminated) : _result?.GetExitCodeOrThrow() ?? throw new NowClientException("No result received"); @@ -172,14 +171,14 @@ public async Task GetResult() private void ThrowIfExited() { - if (_result != null || _canceled) + if (_result != null || _aborted) { throw new NowSessionException(SessionId, NowSessionException.NowSessionExceptionKind.ExitedSessionInteraction); } } private bool _lastStdinSent; - private bool _canceled; + private bool _aborted; private bool _cancelPending; private NowMsgExecCancelRsp? _cancelResponse; private NowMsgExecResult? _result; diff --git a/nugets/Devolutions.NowClient/Worker/WorkerCtx.cs b/nugets/Devolutions.NowClient/Worker/WorkerCtx.cs index 096900d..73b20a3 100644 --- a/nugets/Devolutions.NowClient/Worker/WorkerCtx.cs +++ b/nugets/Devolutions.NowClient/Worker/WorkerCtx.cs @@ -169,11 +169,6 @@ private static void HandleExecMessage(NowMessage.NowMessageView message, WorkerC if (ctx.ExecSessionHandlers.TryGetValue(decoded.SessionId, out IExecSessionHandler? handler)) { handler.HandleCancelRsp(decoded); - // Unregister session if the cancel was successful. - if (decoded.IsSuccess) - { - ctx.ExecSessionHandlers.Remove(decoded.SessionId); - } } else { diff --git a/nugets/Devolutions.NowProto/Messages/NowMsgExecPwsh.cs b/nugets/Devolutions.NowProto/Messages/NowMsgExecPwsh.cs index 97358e6..74bfd41 100644 --- a/nugets/Devolutions.NowProto/Messages/NowMsgExecPwsh.cs +++ b/nugets/Devolutions.NowProto/Messages/NowMsgExecPwsh.cs @@ -161,8 +161,8 @@ public NowMsgExecPwsh Build() { return new NowMsgExecPwsh { - SessionId = _sessionId, - Command = _command, + SessionId = sessionId, + Command = command, _flags = _flags, _directory = _directory, _executionPolicy = _executionPolicy, @@ -170,8 +170,6 @@ public NowMsgExecPwsh Build() }; } - private readonly uint _sessionId = sessionId; - private readonly string _command = command; private MsgFlags _flags = MsgFlags.None; private string _directory = ""; private string _executionPolicy = ""; diff --git a/nugets/Devolutions.NowProto/Messages/NowMsgExecWinPs.cs b/nugets/Devolutions.NowProto/Messages/NowMsgExecWinPs.cs index 71df9fe..989e6f4 100644 --- a/nugets/Devolutions.NowProto/Messages/NowMsgExecWinPs.cs +++ b/nugets/Devolutions.NowProto/Messages/NowMsgExecWinPs.cs @@ -161,8 +161,8 @@ public NowMsgExecWinPs Build() { return new NowMsgExecWinPs { - SessionId = _sessionId, - Command = _command, + SessionId = sessionId, + Command = command, _flags = _flags, _directory = _directory, _executionPolicy = _executionPolicy, @@ -170,8 +170,6 @@ public NowMsgExecWinPs Build() }; } - private readonly uint _sessionId = sessionId; - private readonly string _command = command; private MsgFlags _flags = MsgFlags.None; private string _directory = ""; private string _executionPolicy = ""; From e4819fadbc9a6dc246dd45ae887462fe6733da89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Mon, 10 Feb 2025 13:38:16 -0500 Subject: [PATCH 4/4] fix dotnet target arch handling in xtask --- NowProto.sln | 14 +++++++ .../Devolutions.NowProto.Tests.csproj | 2 +- xtask/src/dotnet.rs | 40 +++++++++++++++++-- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/NowProto.sln b/NowProto.sln index 175b9bf..e1882e7 100644 --- a/NowProto.sln +++ b/NowProto.sln @@ -13,32 +13,46 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Devolutions.NowClient", "nu EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|ARM64 = Debug|ARM64 Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 + Release|ARM64 = Release|ARM64 Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Debug|ARM64.Build.0 = Debug|ARM64 {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Debug|x64.ActiveCfg = Debug|x64 {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Debug|x64.Build.0 = Debug|x64 {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Debug|x86.ActiveCfg = Debug|x86 {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Debug|x86.Build.0 = Debug|x86 + {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Release|ARM64.ActiveCfg = Release|ARM64 + {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Release|ARM64.Build.0 = Release|ARM64 {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Release|x64.ActiveCfg = Release|x64 {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Release|x64.Build.0 = Release|x64 {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Release|x86.ActiveCfg = Release|x86 {2EB02FFB-DC42-4217-8520-93F06E17BFC4}.Release|x86.Build.0 = Release|x86 + {34912761-E8CB-4D88-A4B4-365F6DB88985}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {34912761-E8CB-4D88-A4B4-365F6DB88985}.Debug|ARM64.Build.0 = Debug|ARM64 {34912761-E8CB-4D88-A4B4-365F6DB88985}.Debug|x64.ActiveCfg = Debug|x64 {34912761-E8CB-4D88-A4B4-365F6DB88985}.Debug|x64.Build.0 = Debug|x64 {34912761-E8CB-4D88-A4B4-365F6DB88985}.Debug|x86.ActiveCfg = Debug|x86 {34912761-E8CB-4D88-A4B4-365F6DB88985}.Debug|x86.Build.0 = Debug|x86 + {34912761-E8CB-4D88-A4B4-365F6DB88985}.Release|ARM64.ActiveCfg = Release|ARM64 + {34912761-E8CB-4D88-A4B4-365F6DB88985}.Release|ARM64.Build.0 = Release|ARM64 {34912761-E8CB-4D88-A4B4-365F6DB88985}.Release|x64.ActiveCfg = Release|x64 {34912761-E8CB-4D88-A4B4-365F6DB88985}.Release|x64.Build.0 = Release|x64 {34912761-E8CB-4D88-A4B4-365F6DB88985}.Release|x86.ActiveCfg = Release|x86 {34912761-E8CB-4D88-A4B4-365F6DB88985}.Release|x86.Build.0 = Release|x86 + {4692658E-C138-4A40-A10C-7E55D38D250B}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {4692658E-C138-4A40-A10C-7E55D38D250B}.Debug|ARM64.Build.0 = Debug|ARM64 {4692658E-C138-4A40-A10C-7E55D38D250B}.Debug|x64.ActiveCfg = Debug|x64 {4692658E-C138-4A40-A10C-7E55D38D250B}.Debug|x64.Build.0 = Debug|x64 {4692658E-C138-4A40-A10C-7E55D38D250B}.Debug|x86.ActiveCfg = Debug|x86 {4692658E-C138-4A40-A10C-7E55D38D250B}.Debug|x86.Build.0 = Debug|x86 + {4692658E-C138-4A40-A10C-7E55D38D250B}.Release|ARM64.ActiveCfg = Release|ARM64 + {4692658E-C138-4A40-A10C-7E55D38D250B}.Release|ARM64.Build.0 = Release|ARM64 {4692658E-C138-4A40-A10C-7E55D38D250B}.Release|x64.ActiveCfg = Release|x64 {4692658E-C138-4A40-A10C-7E55D38D250B}.Release|x64.Build.0 = Release|x64 {4692658E-C138-4A40-A10C-7E55D38D250B}.Release|x86.ActiveCfg = Release|x86 diff --git a/nugets/Devolutions.NowProto.Tests/Devolutions.NowProto.Tests.csproj b/nugets/Devolutions.NowProto.Tests/Devolutions.NowProto.Tests.csproj index 6f38842..544246c 100644 --- a/nugets/Devolutions.NowProto.Tests/Devolutions.NowProto.Tests.csproj +++ b/nugets/Devolutions.NowProto.Tests/Devolutions.NowProto.Tests.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - x86;x64 + x86;x64;ARM64 false true diff --git a/xtask/src/dotnet.rs b/xtask/src/dotnet.rs index 1404ba4..5670dfb 100644 --- a/xtask/src/dotnet.rs +++ b/xtask/src/dotnet.rs @@ -1,4 +1,6 @@ use crate::prelude::*; +use std::env; +use std::path::{Path, PathBuf}; pub fn fmt(sh: &Shell) -> anyhow::Result<()> { let _s = Section::new("DOTNET-FORMATTING"); @@ -14,14 +16,42 @@ pub fn fmt(sh: &Shell) -> anyhow::Result<()> { Ok(()) } +pub fn get_target_arch() -> anyhow::Result<&'static str> { + match env::consts::ARCH { + "x86_64" => Ok("x64"), + "aarch64" => Ok("ARM64"), + _ => anyhow::bail!("Unsupported architecture: {}", env::consts::ARCH), + } +} + +pub fn get_dotnet_output_path() -> anyhow::Result { + let arch_folder = get_target_arch()?; + let build_config = "Debug"; + let target_framework = "net8.0"; + + let output_path = Path::new("nugets") + .join("Devolutions.NowProto") + .join("bin") + .join(arch_folder) + .join(build_config) + .join(target_framework); + + Ok(output_path) +} + pub fn build(sh: &Shell) -> anyhow::Result<()> { let _s = Section::new("DOTNET-BUILD"); - cmd!(sh, "dotnet build").run()?; + let platform = get_target_arch()?; + cmd!(sh, "dotnet build -p:Platform={platform}").run()?; if is_verbose() { - list_files(sh, "./nugets/Devolutions.NowProto/bin/Debug/net8.0") - .context("failed to list artifacts generated by dotnet build")?; + let build_path = get_dotnet_output_path()?; + if !build_path.exists() { + anyhow::bail!("Expected build output directory does not exist: {:?}", build_path); + } + + list_files(sh, build_path.to_str().unwrap()).context("failed to list artifacts generated by dotnet build")?; } println!("All good!"); @@ -32,7 +62,9 @@ pub fn build(sh: &Shell) -> anyhow::Result<()> { pub fn tests_run(sh: &Shell) -> anyhow::Result<()> { let _s = Section::new("DOTNET-TESTS-RUN"); - cmd!(sh, "dotnet test").run()?; + let platform = get_target_arch()?; + + cmd!(sh, "dotnet test -p:Platform={platform}").run()?; println!("All good!");