public async Task ConnectToTerminal() { const uint CtrlCExitCode = 0xC000013A; var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); const string Data = "abc✓ЖЖЖ①Ⅻㄨㄩ 啊阿鼾齄丂丄狚狛狜狝﨨﨩ˊˋ˙– ⿻〇㐀㐁䶴䶵"; string app = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Path.Combine(Environment.SystemDirectory, "cmd.exe") : "sh"; var options = new PtyOptions { Name = "Custom terminal", Cols = Data.Length + Environment.CurrentDirectory.Length + 50, Rows = 25, Cwd = Environment.CurrentDirectory, App = app, Environment = new Dictionary <string, string>() { { "FOO", "bar" }, { "Bazz", string.Empty }, }, }; IPtyConnection terminal = await PtyProvider.SpawnAsync(options, this.TimeoutToken); var processExitedTcs = new TaskCompletionSource <uint>(); terminal.ProcessExited += (sender, e) => processExitedTcs.TrySetResult((uint)terminal.ExitCode); string GetTerminalExitCode() => processExitedTcs.Task.IsCompleted ? $". Terminal process has exited with exit code {processExitedTcs.Task.GetAwaiter().GetResult()}." : string.Empty; var firstOutput = new TaskCompletionSource <object?>(TaskCreationOptions.RunContinuationsAsynchronously); var firstDataFound = new TaskCompletionSource <object?>(TaskCreationOptions.RunContinuationsAsynchronously); var output = string.Empty; var checkTerminalOutputAsync = Task.Run(async() => { var buffer = new byte[4096]; var ansiRegex = new Regex( @"[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PRZcf-ntqry=><~]))"); while (!this.TimeoutToken.IsCancellationRequested && !processExitedTcs.Task.IsCompleted) { int count = await terminal.ReaderStream.ReadAsync(buffer, 0, buffer.Length, this.TimeoutToken); if (count == 0) { break; } firstOutput.TrySetResult(null); output += encoding.GetString(buffer, 0, count); output = output.Replace("\r", string.Empty).Replace("\n", string.Empty); output = ansiRegex.Replace(output, string.Empty); var index = output.IndexOf(Data); if (index >= 0) { firstDataFound.TrySetResult(null); if (index <= output.Length - (2 * Data.Length) && output.IndexOf(Data, index + Data.Length) >= 0) { return(true); } } } firstOutput.TrySetCanceled(); firstDataFound.TrySetCanceled(); return(false); }); try { await firstOutput.Task; } catch (OperationCanceledException exception) { throw new InvalidOperationException( $"Could not get any output from terminal{GetTerminalExitCode()}", exception); } try { byte[] commandBuffer = encoding.GetBytes("echo " + Data); await terminal.WriterStream.WriteAsync(commandBuffer, 0, commandBuffer.Length, this.TimeoutToken); await terminal.WriterStream.FlushAsync(); await firstDataFound.Task; await terminal.WriterStream.WriteAsync(new byte[] { 0x0D }, 0, 1, this.TimeoutToken); // Enter await terminal.WriterStream.FlushAsync(); Assert.True(await checkTerminalOutputAsync); } catch (Exception exception) { throw new InvalidOperationException( $"Could not get expected data from terminal.{GetTerminalExitCode()} Actual terminal output:\n{output}", exception); } terminal.Resize(40, 10); terminal.Dispose(); using (this.TimeoutToken.Register(() => processExitedTcs.TrySetCanceled(this.TimeoutToken))) { uint exitCode = await processExitedTcs.Task; Assert.True( exitCode == CtrlCExitCode || // WinPty terminal exit code. exitCode == 1 || // Pseudo Console exit code on Win 10. exitCode == 0); // pty exit code on *nix. } Assert.True(terminal.WaitForExit(TestTimeoutMs)); }
public async Task <bool> UpdateServer(Server server) { var serverPath = new DirectoryInfo(server.Path); if (!serverPath.Exists) { serverPath.Create(); } using (var scope = _serviceScopeFactory.CreateScope()) using (var db = scope.ServiceProvider.GetRequiredService <MonitorDBContext>()) { var scopedServices = scope.ServiceProvider; StringBuilder outputStringBuilder = new StringBuilder(); // Params stuff List <string[]> parameterBuilder = new List <string[]> { new string[] { "+login", "anonymous" }, // TODO: Allow user logins new string[] { "+force_install_dir", $"\"{serverPath.FullName}\"" } }; // F*****g dynamic shit param builder what the f**k List <string> updateText = new List <string> { server.Game.SteamID?.ToString(CultureInfo.InvariantCulture) }; if (!String.IsNullOrEmpty(server.Branch) && server.Branch != "public") { updateText.Add("-beta"); updateText.Add(server.Branch); if (!String.IsNullOrEmpty(server.BranchPassword)) { updateText.Add("-betapassword"); updateText.Add(server.BranchPassword); } // TODO: Should add 'validate' param? } // app_update param builder StringBuilder updateParams = new StringBuilder(); if (updateText.Count > 1) { updateParams.Append("\""); } updateParams.Append(String.Join(" ", updateText)); if (updateText.Count > 1) { updateParams.Append("\""); } parameterBuilder.Add(new string[] { "+app_update", updateParams.ToString() }); parameterBuilder.Add(new string[] { "+quit" }); string steamCMDArguments = String.Join(" ", parameterBuilder.Select(param => String.Join(" ", param)).ToArray()); _logger.LogInformation("Starting SteamCMD with arguments: {0}", steamCMDArguments); // Init proccess listeners IPtyConnection steamCMDProc = null; var downloadProgressRegex = @"Update state \((.+?)\) downloading, progress: ([\d.]+) \((\d*) \/ (\d*)\)"; CultureInfo ci = (CultureInfo)CultureInfo.CurrentCulture.Clone(); ci.NumberFormat.CurrencyDecimalSeparator = "."; //DataReceivedEventHandler outputEventHandler = (sender, e) => EventHandler outputEventHandler = (sender, e2) => { var e = (CustomDataReceivedEventArgs)e2; _logger.LogDebug("SteamCMD Log {0} - {1}: {2}", server.Id, steamCMDProc.Pid, e.Data); UpdateMessage?.Invoke(server, new ConsoleEventArgs() { NewLine = e.Data }); if (!string.IsNullOrEmpty(e.Data)) { Match match = Regex.Match(e.Data, downloadProgressRegex, RegexOptions.IgnoreCase); if (match.Success) { UpdateProgress?.Invoke(server, new ServerUpdateProgressEventArgs() { Progress = float.Parse(match.Groups[2].Value, NumberStyles.Any, ci) }); } } }; //DataReceivedEventHandler errorEventHandler = (sender, e) => EventHandler errorEventHandler = (sender, e2) => { var e = (CustomDataReceivedEventArgs)e2; _logger.LogWarning("SteamCMD emitted {0} - {1}: {2}", server.Id, steamCMDProc.Pid, e.Data); UpdateMessage?.Invoke(server, new ConsoleEventArgs() { NewLine = e.Data, IsError = true }); }; // Start process var hasErrored = false; var processExitedCTS = new CancellationTokenSource(); var processExitedCToken = processExitedCTS.Token; try { steamCMDProc = await _steamCMDService.CreateSteamCMD(steamCMDArguments); //steamCMDProc.OutputDataReceived += outputEventHandler; //steamCMDProc.ErrorDataReceived += errorEventHandler; //steamCMDProc.Start(); //steamCMDProc.BeginOutputReadLine(); //steamCMDProc.BeginErrorReadLine(); // Exit event var processExitedTcs = new TaskCompletionSource <uint>(); steamCMDProc.ProcessExited += (sender, e) => { processExitedTcs.TrySetResult((uint)steamCMDProc.ExitCode); processExitedCTS.Cancel(); //steamCMDProc.ReaderStream.Close(); //steamCMDProc.ReaderStream.Dispose(); }; server.UpdatePID = steamCMDProc.Pid; db.Update(server); await db.SaveChangesAsync(); ServerUpdateStart?.Invoke(server, new EventArgs()); // Event // Reading console var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); var decoder = encoding.GetDecoder(); var sb = new StringBuilder(); var byteBuffer = new byte[1024]; var maxCharsPerBuffer = encoding.GetMaxCharCount(1024); var charBuffer = new char[maxCharsPerBuffer]; int currentLinePos = 0; bool bLastCarriageReturn = false; while (!processExitedTcs.Task.IsCompleted) { try { var bytesRead = await steamCMDProc.ReaderStream.ReadAsync(byteBuffer, 0, byteBuffer.Length).WithCancellation(processExitedCToken); if (bytesRead == 0) { break; } int charLen = decoder.GetChars(byteBuffer, 0, bytesRead, charBuffer, 0); sb !.Append(charBuffer, 0, charLen); MonitorUtils.MoveLinesFromStringBuilderToMessageQueue(ref currentLinePos, ref bLastCarriageReturn, sb, (line) => outputEventHandler?.Invoke(steamCMDProc, new CustomDataReceivedEventArgs(line))); } catch (OperationCanceledException) { break; } } if (!processExitedTcs.Task.IsCompleted) { await processExitedTcs.Task; } /* * var readStdOut = Task.Run(async delegate * { * string? line = null; * //while ((line = await steamCMDProc.StandardOutput.ReadLineAsync()) != null) * while (steamCMDProc.StandardOutput.Peek() > -1) * { * line = steamCMDProc.StandardOutput.ReadLine(); * _logger.LogWarning("Read line! {0}", line); * outputEventHandler.Invoke(steamCMDProc, new CustomDataReceivedEventArgs(line)); * } * }); * var readErrOut = Task.Run(async delegate * { * string? line = null; * //while ((line = await steamCMDProc.StandardError.ReadLineAsync()) != null) * while (steamCMDProc.StandardError.Peek() > -1) * { * line = steamCMDProc.StandardError.ReadLine(); * errorEventHandler.Invoke(steamCMDProc, new CustomDataReceivedEventArgs(line)); * } * }); * * await Task.WhenAll(readStdOut, readErrOut); */ //await steamCMDProc.WaitForExitAsync(); //steamCMDProc.WaitForExit(); //steamCMDProc.CancelOutputRead(); //steamCMDProc.CancelErrorRead(); try { if (steamCMDProc.ExitCode != 0) { hasErrored = true; throw new Exception(); } } catch (InvalidOperationException err) { // Do nothing, for some f*****g reason it seems it can't get the exit code } } catch (Exception err) { _logger.LogError(err, "There has been an error while updating the server via SteamCMD"); return(false); } finally { _logger.LogInformation("Update finished for server {0}!", server.Id); server.UpdatePID = null; db.Update(server); await db.SaveChangesAsync(); ServerUpdated?.Invoke(server, new ServerUpdateEventArgs() { Error = hasErrored }); if (steamCMDProc != null) { //steamCMDProc.OutputDataReceived -= outputEventHandler; //steamCMDProc.ErrorDataReceived -= errorEventHandler; steamCMDProc.Dispose(); } processExitedCTS.Dispose(); } } return(true); }