/// <inheritdoc/> public override Task <IPtyConnection> StartTerminalAsync(PtyOptions options, TraceSource trace, CancellationToken cancellationToken) { var winSize = new WinSize((ushort)options.Rows, (ushort)options.Cols); string?[] terminalArgs = GetExecvpArgs(options); var controlCharacters = new Dictionary <TermSpecialControlCharacter, sbyte> { { TermSpecialControlCharacter.VEOF, 4 }, { TermSpecialControlCharacter.VEOL, -1 }, { TermSpecialControlCharacter.VEOL2, -1 }, { TermSpecialControlCharacter.VERASE, 0x7f }, { TermSpecialControlCharacter.VWERASE, 23 }, { TermSpecialControlCharacter.VKILL, 21 }, { TermSpecialControlCharacter.VREPRINT, 18 }, { TermSpecialControlCharacter.VINTR, 3 }, { TermSpecialControlCharacter.VQUIT, 0x1c }, { TermSpecialControlCharacter.VSUSP, 26 }, { TermSpecialControlCharacter.VSTART, 17 }, { TermSpecialControlCharacter.VSTOP, 19 }, { TermSpecialControlCharacter.VLNEXT, 22 }, { TermSpecialControlCharacter.VDISCARD, 15 }, { TermSpecialControlCharacter.VMIN, 1 }, { TermSpecialControlCharacter.VTIME, 0 }, { TermSpecialControlCharacter.VDSUSP, 25 }, { TermSpecialControlCharacter.VSTATUS, 20 }, }; var term = new Termios( inputFlag: TermInputFlag.ICRNL | TermInputFlag.IXON | TermInputFlag.IXANY | TermInputFlag.IMAXBEL | TermInputFlag.BRKINT | TermInputFlag.IUTF8, outputFlag: TermOuptutFlag.OPOST | TermOuptutFlag.ONLCR, controlFlag: TermConrolFlag.CREAD | TermConrolFlag.CS8 | TermConrolFlag.HUPCL, localFlag: TermLocalFlag.ICANON | TermLocalFlag.ISIG | TermLocalFlag.IEXTEN | TermLocalFlag.ECHO | TermLocalFlag.ECHOE | TermLocalFlag.ECHOK | TermLocalFlag.ECHOKE | TermLocalFlag.ECHOCTL, speed: TermSpeed.B38400, controlCharacters: controlCharacters); int controller = 0; int pid = forkpty(ref controller, null, ref term, ref winSize); if (pid == -1) { throw new InvalidOperationException($"forkpty(4) failed with error {Marshal.GetLastWin32Error()}"); } if (pid == 0) { // We are in a forked process! See http://man7.org/linux/man-pages/man2/fork.2.html for details. // Only our thread is running. We inherited open file descriptors and get a copy of the parent process memory. Environment.CurrentDirectory = options.Cwd; execvpe(options.App, terminalArgs, options.Environment); // Unreachable code after execvpe() } // We have forked the terminal return(Task.FromResult <IPtyConnection>(new PtyConnection(controller, pid))); }
/// <inheritdoc/> public Task <IPtyConnection> StartTerminalAsync( PtyOptions options, TraceSource trace, CancellationToken cancellationToken) { if (NativeMethods.IsPseudoConsoleSupported && !options.ForceWinPty) { return(this.StartPseudoConsoleAsync(options, trace, cancellationToken)); } else { return(this.StartWinPtyTerminalAsync(options, trace, cancellationToken)); } }
/// <summary> /// Gets the arguments to pass to execvp. /// </summary> /// <param name="options">The options for spawning the pty.</param> /// <returns>An array of arguments to pass to execvp.</returns> protected static string?[] GetExecvpArgs(PtyOptions options) { // execvp(2) args array must end with null. The first arg is the app itself. if (options.CommandLine.Length == 0) { return(new[] { options.App, null }); } var result = new string?[options.CommandLine.Length + 2]; Array.Copy(options.CommandLine, 0, result, 1, options.CommandLine.Length); result[0] = options.App; return(result); }
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)); }
private Task <IPtyConnection> StartPseudoConsoleAsync( PtyOptions options, TraceSource trace, CancellationToken cancellationToken) { // Create the in/out pipes if (!CreatePipe(out SafePipeHandle inPipePseudoConsoleSide, out SafePipeHandle inPipeOurSide, null, 0)) { throw new InvalidOperationException("Could not create an anonymous pipe", new Win32Exception()); } if (!CreatePipe(out SafePipeHandle outPipeOurSide, out SafePipeHandle outPipePseudoConsoleSide, null, 0)) { throw new InvalidOperationException("Could not create an anonymous pipe", new Win32Exception()); } var coord = new Coord(options.Cols, options.Rows); var pseudoConsoleHandle = new SafePseudoConsoleHandle(); int hr; RuntimeHelpers.PrepareConstrainedRegions(); try { // Run CreatePseudoConsole* in a CER to make sure we don't leak handles. // MSDN suggest to put all CER code in a finally block // See http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.runtimehelpers.prepareconstrainedregions(v=vs.110).aspx } finally { // Create the Pseudo Console, using the pipes hr = CreatePseudoConsole(coord, inPipePseudoConsoleSide.Handle, outPipePseudoConsoleSide.Handle, 0, out IntPtr hPC); // Remember the handle inside the CER to prevent leakage if (hPC != IntPtr.Zero && hPC != INVALID_HANDLE_VALUE) { pseudoConsoleHandle.InitialSetHandle(hPC); } } if (hr != S_OK) { Marshal.ThrowExceptionForHR(hr); } // Prepare the StartupInfoEx structure attached to the ConPTY. var startupInfo = default(STARTUPINFOEX); startupInfo.InitAttributeListAttachedToConPTY(pseudoConsoleHandle); IntPtr lpEnvironment = Marshal.StringToHGlobalUni(GetEnvironmentString(options.Environment)); try { string app = GetAppOnPath(options.App, options.Cwd, options.Environment); string arguments = options.VerbatimCommandLine ? WindowsArguments.FormatVerbatim(options.CommandLine) : WindowsArguments.Format(options.CommandLine); var commandLine = new StringBuilder(app.Length + arguments.Length + 4); bool quoteApp = app.Contains(" ") && !app.StartsWith("\"") && !app.EndsWith("\""); if (quoteApp) { commandLine.Append('"').Append(app).Append('"'); } else { commandLine.Append(app); } if (!string.IsNullOrWhiteSpace(arguments)) { commandLine.Append(' '); commandLine.Append(arguments); } bool success; int errorCode = 0; var processInfo = default(PROCESS_INFORMATION); var processHandle = new SafeProcessHandle(); var mainThreadHandle = new SafeThreadHandle(); RuntimeHelpers.PrepareConstrainedRegions(); try { // Run CreateProcess* in a CER to make sure we don't leak handles. } finally { success = CreateProcess( null, // lpApplicationName commandLine.ToString(), null, // lpProcessAttributes null, // lpThreadAttributes false, // bInheritHandles VERY IMPORTANT that this is false EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, // dwCreationFlags lpEnvironment, options.Cwd, ref startupInfo, out processInfo); if (!success) { errorCode = Marshal.GetLastWin32Error(); } // Remember the handles inside the CER to prevent leakage if (processInfo.hProcess != IntPtr.Zero && processInfo.hProcess != INVALID_HANDLE_VALUE) { processHandle.InitialSetHandle(processInfo.hProcess); } if (processInfo.hThread != IntPtr.Zero && processInfo.hThread != INVALID_HANDLE_VALUE) { mainThreadHandle.InitialSetHandle(processInfo.hThread); } } if (!success) { var exception = new Win32Exception(errorCode); throw new InvalidOperationException($"Could not start terminal process {commandLine.ToString()}: {exception.Message}", exception); } var connectionOptions = new PseudoConsoleConnection.PseudoConsoleConnectionHandles( inPipePseudoConsoleSide, outPipePseudoConsoleSide, inPipeOurSide, outPipeOurSide, pseudoConsoleHandle, processHandle, processInfo.dwProcessId, mainThreadHandle); var result = new PseudoConsoleConnection(connectionOptions); return(Task.FromResult <IPtyConnection>(result)); } finally { startupInfo.FreeAttributeList(); if (lpEnvironment != IntPtr.Zero) { Marshal.FreeHGlobal(lpEnvironment); } } }
private async Task <IPtyConnection> StartWinPtyTerminalAsync( PtyOptions options, TraceSource trace, CancellationToken cancellationToken) { IntPtr error; IntPtr config = winpty_config_new(WINPTY_FLAG_COLOR_ESCAPES, out error); ThrowIfErrorOrNull("Error creating WinPTY config", error, config); winpty_config_set_initial_size(config, options.Cols, options.Rows); IntPtr handle = winpty_open(config, out error); winpty_config_free(config); ThrowIfErrorOrNull("Error launching WinPTY agent", error, handle); string commandLine = options.VerbatimCommandLine ? WindowsArguments.FormatVerbatim(options.CommandLine) : WindowsArguments.Format(options.CommandLine); string env = GetEnvironmentString(options.Environment); string app = GetAppOnPath(options.App, options.Cwd, options.Environment); trace.TraceInformation($"Starting terminal process '{app}' with command line {commandLine}"); IntPtr spawnConfig = winpty_spawn_config_new( WINPTY_SPAWN_FLAG_AUTO_SHUTDOWN, app, commandLine, options.Cwd, env, out error); ThrowIfErrorOrNull("Error creating WinPTY spawn config", error, spawnConfig); bool spawnSuccess = winpty_spawn(handle, spawnConfig, out SafeProcessHandle hProcess, out IntPtr thread, out int procError, out error); winpty_spawn_config_free(spawnConfig); if (!spawnSuccess) { if (procError != 0) { if (error != IntPtr.Zero) { winpty_error_free(error); } throw new InvalidOperationException($"Unable to start WinPTY terminal '{app}': {new Win32Exception(procError).Message} ({procError})"); } ThrowIfError("Unable to start WinPTY terminal process", error, alwaysThrow: true); } Stream?writeToStream = null; Stream?readFromStream = null; try { writeToStream = await CreatePipeAsync(winpty_conin_name(handle), PipeDirection.Out, cancellationToken); readFromStream = await CreatePipeAsync(winpty_conout_name(handle), PipeDirection.In, cancellationToken); } catch { writeToStream?.Dispose(); hProcess.Close(); winpty_free(handle); throw; } return(new WinPtyConnection(readFromStream, writeToStream, handle, hProcess)); }
public async Task <IPtyConnection> CreateSteamCMD(string strparams) { //if (activeSteamCMD != null && !activeSteamCMD.HasExited) if (activeSteamCMD != null && activeSteamCMD.ExitCode == default(int)) { throw new Exception("Can't start a SteamCMD when one is already open!"); } //string app = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Path.Combine(Environment.SystemDirectory, "cmd.exe") : "sh"; string app = SteamCMDExe.FullName; string cwd = SteamCMDFolder.FullName; var options = new PtyOptions() { Name = "SteamCMD terminal", App = app, CommandLine = strparams.Split(" "), VerbatimCommandLine = true, Cwd = cwd, Cols = 100, Rows = 80, Environment = new Dictionary <string, string>() }; IPtyConnection terminal = await PtyProvider.SpawnAsync(options, new System.Threading.CancellationToken()); terminal.ProcessExited += (sender, e) => { activeSteamCMD = null; }; activeSteamCMD = terminal; /* * * var steamCMDProc = new Process(); * steamCMDProc.StartInfo.FileName = SteamCMDExe.FullName; * steamCMDProc.StartInfo.WorkingDirectory = SteamCMDFolder.FullName; * steamCMDProc.StartInfo.Arguments = strparams; * * steamCMDProc.StartInfo.RedirectStandardInput = true; * steamCMDProc.StartInfo.RedirectStandardError = true; * steamCMDProc.StartInfo.RedirectStandardOutput = true; * //steamCMDProc.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; * steamCMDProc.StartInfo.CreateNoWindow = true; * steamCMDProc.StartInfo.UseShellExecute = false; * steamCMDProc.EnableRaisingEvents = true; // Investigate? * //activeSteamCMD.EnableRaisingEvents = false; * // activeSteamCMD.OutputDataReceived += (sender, eventArgs) => outputStringBuilder.AppendLine(eventArgs.Data); * // activeSteamCMD.ErrorDataReceived += (sender, eventArgs) => outputStringBuilder.AppendLine(eventArgs.Data); * * steamCMDProc.Exited += (sender, e) => * { * //activeSteamCMD.Close(); * activeSteamCMD = null; * }; * * activeSteamCMD = steamCMDProc; */ return(activeSteamCMD); }
/// <inheritdoc/> public abstract Task <IPtyConnection> StartTerminalAsync(PtyOptions options, TraceSource trace, CancellationToken cancellationToken);