22using Microsoft . Extensions . Logging ;
33using Remotely . Shared . Enums ;
44using Remotely . Shared . Models ;
5- using Remotely . Shared . Utilities ;
65using System ;
76using System . Collections . Concurrent ;
8- using System . Collections . Generic ;
97using System . Diagnostics ;
108using System . Linq ;
11- using System . Text ;
129using System . Threading ;
1310using System . Threading . Tasks ;
1411
1512namespace Remotely . Agent . Services
1613{
1714 public interface IExternalScriptingShell
1815 {
19- ScriptResult WriteInput ( string input , TimeSpan timeout ) ;
16+ Process ShellProcess { get ; }
17+ Task Init ( ScriptingShell shell , string shellProcessName , string lineEnding , string connectionId ) ;
18+ Task < ScriptResult > WriteInput ( string input , TimeSpan timeout ) ;
2019 }
2120
2221 public class ExternalScriptingShell : IExternalScriptingShell
2322 {
24- private static readonly ConcurrentDictionary < string , ExternalScriptingShell > _sessions = new ( ) ;
23+ private static readonly ConcurrentDictionary < string , IExternalScriptingShell > _sessions = new ( ) ;
2524 private readonly IConfigService _configService ;
2625 private readonly ILogger < ExternalScriptingShell > _logger ;
26+ private readonly ManualResetEvent _outputDone = new ( false ) ;
27+ private readonly SemaphoreSlim _writeLock = new ( 1 , 1 ) ;
28+ private string _errorOut = string . Empty ;
29+ private string _lastInputID = string . Empty ;
2730 private string _lineEnding ;
31+ private System . Timers . Timer _processIdleTimeout = new ( TimeSpan . FromMinutes ( 10 ) )
32+ {
33+ AutoReset = false
34+ } ;
35+
36+ private string _senderConnectionId ;
2837 private ScriptingShell _shell ;
38+ private string _standardOut = string . Empty ;
2939
3040 public ExternalScriptingShell (
3141 IConfigService configService ,
@@ -34,47 +44,31 @@ public ExternalScriptingShell(
3444 _configService = configService ;
3545 _logger = logger ;
3646 }
47+ public Process ShellProcess { get ; set ; }
3748
38- private string ErrorOut { get ; set ; }
39-
40- private string LastInputID { get ; set ; }
41-
42- private ManualResetEvent OutputDone { get ; } = new ( false ) ;
43-
44- private System . Timers . Timer ProcessIdleTimeout { get ; set ; }
45-
46- private string SenderConnectionId { get ; set ; }
47-
48- private Process ShellProcess { get ; set ; }
49-
50- private string StandardOut { get ; set ; }
51-
52- private Stopwatch Stopwatch { get ; set ; }
5349
5450 // TODO: Turn into cache and factory.
55- public static ExternalScriptingShell GetCurrent ( ScriptingShell shell , string senderConnectionId )
51+ public static async Task < IExternalScriptingShell > GetCurrent ( ScriptingShell shell , string senderConnectionId )
5652 {
5753 if ( _sessions . TryGetValue ( $ "{ shell } -{ senderConnectionId } ", out var session ) &&
5854 session . ShellProcess ? . HasExited != true )
5955 {
60- session . ProcessIdleTimeout . Stop ( ) ;
61- session . ProcessIdleTimeout . Start ( ) ;
6256 return session ;
6357 }
6458 else
6559 {
66- session = Program . Services . GetRequiredService < ExternalScriptingShell > ( ) ;
60+ session = Program . Services . GetRequiredService < IExternalScriptingShell > ( ) ;
6761
6862 switch ( shell )
6963 {
7064 case ScriptingShell . WinPS :
71- session . Init ( shell , "powershell.exe" , "\r \n " , senderConnectionId ) ;
65+ await session . Init ( shell , "powershell.exe" , "\r \n " , senderConnectionId ) ;
7266 break ;
7367 case ScriptingShell . Bash :
74- session . Init ( shell , "bash" , "\n " , senderConnectionId ) ;
68+ await session . Init ( shell , "bash" , "\n " , senderConnectionId ) ;
7569 break ;
7670 case ScriptingShell . CMD :
77- session . Init ( shell , "cmd.exe" , "\r \n " , senderConnectionId ) ;
71+ await session . Init ( shell , "cmd.exe" , "\r \n " , senderConnectionId ) ;
7872 break ;
7973 default :
8074 throw new ArgumentException ( $ "Unknown external scripting shell type: { shell } ") ;
@@ -84,135 +78,143 @@ public static ExternalScriptingShell GetCurrent(ScriptingShell shell, string sen
8478 }
8579 }
8680
87- public ScriptResult WriteInput ( string input , TimeSpan timeout )
81+ public async Task Init ( ScriptingShell shell , string shellProcessName , string lineEnding , string connectionId )
82+ {
83+ _shell = shell ;
84+ _lineEnding = lineEnding ;
85+ _senderConnectionId = connectionId ;
86+
87+ var psi = new ProcessStartInfo ( shellProcessName )
88+ {
89+ WindowStyle = ProcessWindowStyle . Hidden ,
90+ Verb = "RunAs" ,
91+ UseShellExecute = false ,
92+ RedirectStandardError = true ,
93+ RedirectStandardInput = true ,
94+ RedirectStandardOutput = true
95+ } ;
96+
97+ var connectionInfo = _configService . GetConnectionInfo ( ) ;
98+ psi . Environment . Add ( "DeviceId" , connectionInfo . DeviceID ) ;
99+ psi . Environment . Add ( "ServerUrl" , connectionInfo . Host ) ;
100+
101+ ShellProcess = new Process
102+ {
103+ StartInfo = psi
104+ } ;
105+ ShellProcess . ErrorDataReceived += ShellProcess_ErrorDataReceived ;
106+ ShellProcess . OutputDataReceived += ShellProcess_OutputDataReceived ;
107+
108+ ShellProcess . Start ( ) ;
109+
110+ ShellProcess . BeginErrorReadLine ( ) ;
111+ ShellProcess . BeginOutputReadLine ( ) ;
112+
113+ _processIdleTimeout = new System . Timers . Timer ( TimeSpan . FromMinutes ( 10 ) )
114+ {
115+ AutoReset = false
116+ } ;
117+ _processIdleTimeout . Elapsed += ProcessIdleTimeout_Elapsed ;
118+ _processIdleTimeout . Start ( ) ;
119+
120+ if ( shell == ScriptingShell . WinPS )
121+ {
122+ await WriteInput ( "$VerbosePreference = \" Continue\" ;" , TimeSpan . FromSeconds ( 5 ) ) ;
123+ await WriteInput ( "$DebugPreference = \" Continue\" ;" , TimeSpan . FromSeconds ( 5 ) ) ;
124+ await WriteInput ( "$InformationPreference = \" Continue\" ;" , TimeSpan . FromSeconds ( 5 ) ) ;
125+ await WriteInput ( "$WarningPreference = \" Continue\" ;" , TimeSpan . FromSeconds ( 5 ) ) ;
126+ }
127+ }
128+
129+ public async Task < ScriptResult > WriteInput ( string input , TimeSpan timeout )
88130 {
131+ await _writeLock . WaitAsync ( ) ;
132+ var sw = Stopwatch . StartNew ( ) ;
133+
89134 try
90135 {
91- StandardOut = "" ;
92- ErrorOut = "" ;
93- Stopwatch = Stopwatch . StartNew ( ) ;
94- lock ( ShellProcess )
95- {
96- LastInputID = Guid . NewGuid ( ) . ToString ( ) ;
97- OutputDone . Reset ( ) ;
98- ShellProcess . StandardInput . Write ( input + _lineEnding ) ;
99- ShellProcess . StandardInput . Write ( "echo " + LastInputID + _lineEnding ) ;
100-
101- var result = Task . WhenAny (
102- Task . Run ( ( ) =>
103- {
104- return ShellProcess . WaitForExit ( ( int ) timeout . TotalMilliseconds ) ;
105- } ) ,
106- Task . Run ( ( ) =>
107- {
108- return OutputDone . WaitOne ( ) ;
109-
110- } ) ) . ConfigureAwait ( false ) . GetAwaiter ( ) . GetResult ( ) ;
111-
112- if ( ! result . Result )
136+ _processIdleTimeout . Stop ( ) ;
137+ _processIdleTimeout . Start ( ) ;
138+ _outputDone . Reset ( ) ;
139+
140+ _standardOut = "" ;
141+ _errorOut = "" ;
142+ _lastInputID = Guid . NewGuid ( ) . ToString ( ) ;
143+
144+ ShellProcess . StandardInput . Write ( input + _lineEnding ) ;
145+ ShellProcess . StandardInput . Write ( "echo " + _lastInputID + _lineEnding ) ;
146+
147+ var result = await Task . WhenAny (
148+ Task . Run ( ( ) =>
149+ {
150+ return ShellProcess . WaitForExit ( ( int ) timeout . TotalMilliseconds ) ;
151+ } ) ,
152+ Task . Run ( ( ) =>
113153 {
114- return GeneratePartialResult ( input ) ;
115- }
154+ return _outputDone . WaitOne ( ) ;
155+
156+ } ) ) . ConfigureAwait ( false ) . GetAwaiter ( ) . GetResult ( ) ;
157+
158+ if ( ! result )
159+ {
160+ return GeneratePartialResult ( input , sw . Elapsed ) ;
116161 }
117- return GenerateCompletedResult ( input ) ;
162+
163+ return GenerateCompletedResult ( input , sw . Elapsed ) ;
118164 }
119165 catch ( Exception ex )
120166 {
121167 _logger . LogError ( ex , "Error while writing input to scripting shell." ) ;
122- ErrorOut += Environment . NewLine + ex . Message ;
168+ _errorOut += Environment . NewLine + ex . Message ;
123169
124170 // Something's wrong. Let the next command start a new session.
125171 RemoveSession ( ) ;
126172 }
173+ finally
174+ {
175+ _writeLock . Release ( ) ;
176+ }
127177
128- return GeneratePartialResult ( input ) ;
178+ return GeneratePartialResult ( input , sw . Elapsed ) ;
129179 }
130180
131- private ScriptResult GenerateCompletedResult ( string input )
181+ private ScriptResult GenerateCompletedResult ( string input , TimeSpan runtime )
132182 {
133183 return new ScriptResult ( )
134184 {
135185 Shell = _shell ,
136- RunTime = Stopwatch . Elapsed ,
186+ RunTime = runtime ,
137187 ScriptInput = input ,
138- SenderConnectionID = SenderConnectionId ,
188+ SenderConnectionID = _senderConnectionId ,
139189 DeviceID = _configService . GetConnectionInfo ( ) . DeviceID ,
140- StandardOutput = StandardOut . Split ( Environment . NewLine ) ,
141- ErrorOutput = ErrorOut . Split ( Environment . NewLine ) ,
142- HadErrors = ! string . IsNullOrWhiteSpace ( ErrorOut ) ||
190+ StandardOutput = _standardOut . Split ( Environment . NewLine ) ,
191+ ErrorOutput = _errorOut . Split ( Environment . NewLine ) ,
192+ HadErrors = ! string . IsNullOrWhiteSpace ( _errorOut ) ||
143193 ( ShellProcess . HasExited && ShellProcess . ExitCode != 0 )
144194 } ;
145195 }
146196
147- private ScriptResult GeneratePartialResult ( string input )
197+ private ScriptResult GeneratePartialResult ( string input , TimeSpan runtime )
148198 {
149199 var partialResult = new ScriptResult ( )
150200 {
151201 Shell = _shell ,
152- RunTime = Stopwatch . Elapsed ,
202+ RunTime = runtime ,
153203 ScriptInput = input ,
154- SenderConnectionID = SenderConnectionId ,
204+ SenderConnectionID = _senderConnectionId ,
155205 DeviceID = _configService . GetConnectionInfo ( ) . DeviceID ,
156- StandardOutput = StandardOut . Split ( Environment . NewLine ) ,
206+ StandardOutput = _standardOut . Split ( Environment . NewLine ) ,
157207 ErrorOutput = ( new [ ] { "WARNING: The command execution timed out and was forced to return before finishing. " +
158208 "The results may be partial, and the terminal process has been reset. " +
159209 "Please note that interactive commands aren't supported." } )
160- . Concat ( ErrorOut . Split ( Environment . NewLine ) )
210+ . Concat ( _errorOut . Split ( Environment . NewLine ) )
161211 . ToArray ( ) ,
162- HadErrors = ! string . IsNullOrWhiteSpace ( ErrorOut ) ||
212+ HadErrors = ! string . IsNullOrWhiteSpace ( _errorOut ) ||
163213 ( ShellProcess . HasExited && ShellProcess . ExitCode != 0 )
164214 } ;
165215 ProcessIdleTimeout_Elapsed ( this , null ) ;
166216 return partialResult ;
167217 }
168-
169- private void Init ( ScriptingShell shell , string shellProcessName , string lineEnding , string connectionId )
170- {
171- _shell = shell ;
172- _lineEnding = lineEnding ;
173- SenderConnectionId = connectionId ;
174-
175- var psi = new ProcessStartInfo ( shellProcessName )
176- {
177- WindowStyle = ProcessWindowStyle . Hidden ,
178- Verb = "RunAs" ,
179- UseShellExecute = false ,
180- RedirectStandardError = true ,
181- RedirectStandardInput = true ,
182- RedirectStandardOutput = true
183- } ;
184-
185- var connectionInfo = _configService . GetConnectionInfo ( ) ;
186- psi . Environment . Add ( "DeviceId" , connectionInfo . DeviceID ) ;
187- psi . Environment . Add ( "ServerUrl" , connectionInfo . Host ) ;
188-
189- ShellProcess = new Process
190- {
191- StartInfo = psi
192- } ;
193- ShellProcess . ErrorDataReceived += ShellProcess_ErrorDataReceived ;
194- ShellProcess . OutputDataReceived += ShellProcess_OutputDataReceived ;
195-
196- ShellProcess . Start ( ) ;
197-
198- ShellProcess . BeginErrorReadLine ( ) ;
199- ShellProcess . BeginOutputReadLine ( ) ;
200-
201- ProcessIdleTimeout = new System . Timers . Timer ( TimeSpan . FromMinutes ( 10 ) . TotalMilliseconds )
202- {
203- AutoReset = false
204- } ;
205- ProcessIdleTimeout . Elapsed += ProcessIdleTimeout_Elapsed ;
206- ProcessIdleTimeout . Start ( ) ;
207-
208- if ( shell == ScriptingShell . WinPS )
209- {
210- WriteInput ( "$VerbosePreference = \" Continue\" ;" , TimeSpan . FromSeconds ( 5 ) ) ;
211- WriteInput ( "$DebugPreference = \" Continue\" ;" , TimeSpan . FromSeconds ( 5 ) ) ;
212- WriteInput ( "$InformationPreference = \" Continue\" ;" , TimeSpan . FromSeconds ( 5 ) ) ;
213- WriteInput ( "$WarningPreference = \" Continue\" ;" , TimeSpan . FromSeconds ( 5 ) ) ;
214- }
215- }
216218 private void ProcessIdleTimeout_Elapsed ( object sender , System . Timers . ElapsedEventArgs e )
217219 {
218220 RemoveSession ( ) ;
@@ -221,26 +223,26 @@ private void ProcessIdleTimeout_Elapsed(object sender, System.Timers.ElapsedEven
221223 private void RemoveSession ( )
222224 {
223225 ShellProcess ? . Kill ( ) ;
224- _sessions . TryRemove ( SenderConnectionId , out _ ) ;
226+ _sessions . TryRemove ( _senderConnectionId , out _ ) ;
225227 }
226228
227229 private void ShellProcess_ErrorDataReceived ( object sender , DataReceivedEventArgs e )
228230 {
229231 if ( e ? . Data != null )
230232 {
231- ErrorOut += e . Data + Environment . NewLine ;
233+ _errorOut += e . Data + Environment . NewLine ;
232234 }
233235 }
234236
235237 private void ShellProcess_OutputDataReceived ( object sender , DataReceivedEventArgs e )
236238 {
237- if ( e ? . Data ? . Contains ( LastInputID ) == true )
239+ if ( e ? . Data ? . Contains ( _lastInputID ) == true )
238240 {
239- OutputDone . Set ( ) ;
241+ _outputDone . Set ( ) ;
240242 }
241243 else
242244 {
243- StandardOut += e . Data + Environment . NewLine ;
245+ _standardOut += e . Data + Environment . NewLine ;
244246 }
245247
246248 }
0 commit comments