Skip to content

Commit 72c393d

Browse files
committed
Hotfix for resolving scripting shells through DI.
1 parent b3af151 commit 72c393d

File tree

5 files changed

+160
-156
lines changed

5 files changed

+160
-156
lines changed

Agent/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ private static void RegisterServices(IServiceCollection services)
9595
services.AddSingleton<IWakeOnLanService, WakeOnLanService>();
9696
services.AddHostedService(services => services.GetRequiredService<ICpuUtilizationSampler>());
9797
services.AddScoped<IChatClientService, ChatClientService>();
98-
services.AddTransient<IPSCore, PSCore>();
98+
services.AddTransient<IPsCoreShell, PsCoreShell>();
9999
services.AddTransient<IExternalScriptingShell, ExternalScriptingShell>();
100100
services.AddScoped<IConfigService, ConfigService>();
101101
services.AddScoped<IUninstaller, Uninstaller>();

Agent/Services/AgentHubConnection.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ private void RegisterMessageHandlers()
356356
{
357357
try
358358
{
359-
var session = PSCore.GetCurrent(senderConnectionId);
359+
var session = PsCoreShell.GetCurrent(senderConnectionId);
360360
var completion = session.GetCompletions(inputText, currentIndex, forward);
361361
var completionModel = completion.ToPwshCompletion();
362362
await _hubConnection.InvokeAsync("ReturnPowerShellCompletions", completionModel, intent, senderConnectionId).ConfigureAwait(false);

Agent/Services/ExternalScriptingShell.cs

Lines changed: 122 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,40 @@
22
using Microsoft.Extensions.Logging;
33
using Remotely.Shared.Enums;
44
using Remotely.Shared.Models;
5-
using Remotely.Shared.Utilities;
65
using System;
76
using System.Collections.Concurrent;
8-
using System.Collections.Generic;
97
using System.Diagnostics;
108
using System.Linq;
11-
using System.Text;
129
using System.Threading;
1310
using System.Threading.Tasks;
1411

1512
namespace 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

Comments
 (0)