using NUnit.Framework; using NUnit.Framework.Internal; using System; using Vanara.Extensions; using Vanara.InteropServices; using static Vanara.PInvoke.Kernel32; using static Vanara.PInvoke.WsmSvc; namespace Vanara.PInvoke.Tests { public class ShellClient : IDisposable { private SafeWSMAN_API_HANDLE m_apiHandle; private WSMAN_SHELL_ASYNC m_async; private bool m_bExecute; private bool m_bSetup; private WSMAN_COMMAND_HANDLE m_command; private Win32Error m_errorCode; private SafeEventHandle m_event; private WSMAN_SHELL_ASYNC m_ReceiveAsync; private Win32Error m_ReceiveErrorCode; private SafeEventHandle m_ReceiveEvent; private SafeWSMAN_SESSION_HANDLE m_session; private WSMAN_SHELL_HANDLE m_shell; // Constructor. public ShellClient() { } // Clean up the used resources public void Dispose() { if (!m_command.IsNull) { WSManCloseCommand(m_command, 0, m_async); WaitForSingleObject(m_event, INFINITE); if (Win32Error.NO_ERROR != m_errorCode) { wprintf("WSManCloseCommand failed: {0}\n", m_errorCode); } else { m_command = default; } } if (!m_shell.IsNull) { WSManCloseShell(m_shell, 0, m_async); WaitForSingleObject(m_event, INFINITE); if (Win32Error.NO_ERROR != m_errorCode) { wprintf("WSManCloseShell failed: {0}\n", m_errorCode); } else { m_shell = default; } } // Frees memory of session and closes all related operations before returning m_session?.Dispose(); // deinitializes the Winrm client stack; all operations will finish before this API will return m_apiHandle?.Dispose(); m_event?.Dispose(); m_ReceiveEvent?.Dispose(); m_bSetup = false; m_bExecute = false; } // Execute shell-related operations public bool Execute(string resourceUri, string commandLine, byte[] sendData, uint count) { if (!m_bSetup) { wprintf("Setup() needs to be called first"); return false; } if (m_bExecute) { wprintf("Execute() can only be called once"); return false; } m_bExecute = true; // WSManCreateShell WSManCreateShell(m_session, 0, resourceUri, default, default, default, m_async, out m_shell); WaitForSingleObject(m_event, INFINITE); if (Win32Error.NO_ERROR != m_errorCode) { wprintf("WSManCreateShell failed: {0}\n", m_errorCode); return false; } // WSManRunShellCommand WSManRunShellCommand(m_shell, 0, commandLine, default, default, m_async, out m_command); WaitForSingleObject(m_event, INFINITE); if (Win32Error.NO_ERROR != m_errorCode) { wprintf("WSManRunShellCommand failed: {0}\n", m_errorCode); return false; } // WSManReceiveShellOutput WSManReceiveShellOutput(m_shell, m_command, 0, default, m_ReceiveAsync, out SafeWSMAN_OPERATION_HANDLE receiveOperation); // Send operation can be executed many times to send large data if (count >= 1) { for (uint i = 1; i <= count; i++) { // last send operation should indicate end of stream if (!Send(sendData, (i == count))) { wprintf("Send {0} failed.\n", i); } } } // Receive operation is finished WaitForSingleObject(m_ReceiveEvent, INFINITE); if (Win32Error.NO_ERROR != m_ReceiveErrorCode) { wprintf("WSManReceiveShellOutput failed: {0}\n", m_ReceiveErrorCode); return false; } receiveOperation.Dispose(); return true; } // Initialize session for subsequent operations public bool Setup(string connection, WSManAuthenticationFlags authenticationMechanism, string username, string password) { if (m_bSetup) return true; // initialize the WinRM client m_errorCode = WSManInitialize(0, out m_apiHandle); if (Win32Error.NO_ERROR != m_errorCode) { wprintf("WSManInitialize failed: {0}\n", m_errorCode); return false; } // Create a session which can be used to perform subsequent operations var serverAuthenticationCredentials = new SafeCoTaskMemStruct(new WSMAN_AUTHENTICATION_CREDENTIALS { authenticationMechanism = authenticationMechanism, userAccount = new WSMAN_USERNAME_PASSWORD_CREDS { username = username, password = password } }); m_errorCode = WSManCreateSession(m_apiHandle, connection, 0, serverAuthenticationCredentials, default, out m_session); if (Win32Error.NO_ERROR != m_errorCode) { wprintf("WSManCreateSession failed: {0}\n", m_errorCode); return false; } // Repeat the call to set any desired session options WSManSessionOption option = WSManSessionOption.WSMAN_OPTION_DEFAULT_OPERATION_TIMEOUTMS; var data = new WSMAN_DATA { type = WSManDataType.WSMAN_DATA_TYPE_DWORD, union = new WSMAN_DATA.WSMAN_DATA_UNION { number = 60000 } }; m_errorCode = WSManSetSessionOption(m_session, option, data); if (Win32Error.NO_ERROR != m_errorCode) { wprintf("WSManSetSessionOption failed: {0}\n", m_errorCode); return false; } // Prepare async call m_event = CreateEvent(default, false, false, default); if (m_event.IsInvalid) { m_errorCode = Win32Error.GetLastError(); wprintf("CreateEvent failed: {0}\n", m_errorCode); return false; } m_async = new WSMAN_SHELL_ASYNC { operationContext = default, completionFunction = m_WSManShellCompletionFunction }; m_ReceiveEvent = CreateEvent(default, false, false, default); if (m_ReceiveEvent.IsInvalid) { m_errorCode = Win32Error.GetLastError(); wprintf("CreateEvent failed: {0}\n", m_errorCode); return false; } m_ReceiveAsync = new WSMAN_SHELL_ASYNC { operationContext = default, completionFunction = m_ReceiveCallback }; m_bSetup = true; return true; } // Receive async callback private void m_ReceiveCallback(IntPtr operationContext, WSManCallbackFlags flags, in WSMAN_ERROR error, WSMAN_SHELL_HANDLE shell, WSMAN_COMMAND_HANDLE command, WSMAN_OPERATION_HANDLE operationHandle, IntPtr pdata) { if (0 != error.code) { m_ReceiveErrorCode = error.code; // NOTE: if the errorDetail needs to be used outside of the callback, then need to allocate memory, copy the content to that // memory as error.errorDetail itself is owned by WSMan client stack and will be deallocated and invalid when the callback exits wprintf(error.errorDetail); } // Output the received data to the console WSMAN_RECEIVE_DATA_RESULT data = pdata.ToStructure(); if (pdata != default) { if (data.streamData.type == WSManDataType.WSMAN_DATA_TYPE_BINARY && data.streamData.union.binaryData.dataLength != 0) { HFILE hFile = ((0 == string.Compare(data.streamId, WSMAN_STREAM_ID_STDERR)) ? GetStdHandle(StdHandleType.STD_ERROR_HANDLE) : GetStdHandle(StdHandleType.STD_OUTPUT_HANDLE)); WriteFile(hFile, data.streamData.union.binaryData.data, data.streamData.union.binaryData.dataLength, out _); } } // for WSManReceiveShellOutput, needs to wait for state to be done before signalliing the end of the operation if ((0 != error.code) || (pdata != default && !data.commandState.IsNull && string.Compare(data.commandState, WSMAN_COMMAND_STATE_DONE) == 0)) { SetEvent(m_ReceiveEvent); } } // async callback private void m_WSManShellCompletionFunction(IntPtr operationContext, WSManCallbackFlags flags, in WSMAN_ERROR error, WSMAN_SHELL_HANDLE shell, WSMAN_COMMAND_HANDLE command, WSMAN_OPERATION_HANDLE operationHandle, IntPtr data) { if (0 != error.code) { m_errorCode = error.code; // NOTE: if the errorDetail needs to be used outside of the callback, then need to allocate memory, copy the content to that // memory as error->errorDetail itself is owned by WSMan client stack and will be deallocated and invalid when the callback exits wprintf(error.errorDetail); } // for non-receieve operation, the callback simply signals the async operation is finished SetEvent(m_event); } private bool Send(byte[] sendData, bool endOfStream) { // WSManSendShellInput var streamData = new WSMAN_DATA { type = WSManDataType.WSMAN_DATA_TYPE_BINARY }; using var pSendData = new PinnedObject(sendData); if (sendData != null && sendData.Length > 0) { streamData.union.binaryData.dataLength = (uint)sendData.Length; streamData.union.binaryData.data = pSendData; } WSManSendShellInput(m_shell, m_command, 0, WSMAN_STREAM_ID_STDIN, streamData, endOfStream, m_async, out SafeWSMAN_OPERATION_HANDLE sendOperation); WaitForSingleObject(m_event, INFINITE); if (Win32Error.NO_ERROR != m_errorCode) { wprintf("WSManSendShellInput failed: {0}\n", m_errorCode); return false; } sendOperation.Dispose(); return true; } private void wprintf(string fmt, params object[] p) => TestContext.Write(fmt, p); } [TestFixture] public class WsmSvcTests { [OneTimeSetUp] public void _Setup() { } [OneTimeTearDown] public void _TearDown() { } [Test] public void Test() { using var cli = new ShellClient(); cli.Setup("http://fqdn.domain.com", WSManAuthenticationFlags.WSMAN_FLAG_NO_AUTHENTICATION, null, null); cli.Execute("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd", "dir", null, 1); } } }