diff --git a/README.md b/README.md index a816cd0..f86d20e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,21 @@ Pty.Net is a cross platform, .NET library providing idiomatic bindings for `fork Pty.Net supports Linux, macOS, and Windows. On versions of windows older than 1809 the [winpty](https://github.com/rprichard/winpty) is used. For windows 1809+ this library ships a side-by-side copy of conhost. -# Contributing +pty bindings for .NET languages. This allows you to spawn processes connected to a pseudoterminal. + +This is useful for: + +- Writing a terminal emulator. +- Convincing programs that you are a terminal in order to get control sequences from their output. + +## Supported Platforms + +Pty.Net supports Windows, MacOS, and Linux and can run on the following .NET runtimes: + +- .NET 4.7.2 +- .NET Standard 2.0 + +## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us diff --git a/azure-pipelines/dotnet.yml b/azure-pipelines/dotnet.yml index c9aa4c0..6007d0e 100644 --- a/azure-pipelines/dotnet.yml +++ b/azure-pipelines/dotnet.yml @@ -9,4 +9,4 @@ steps: command: test arguments: --no-build -c $(BuildConfiguration) -v n /p:CollectCoverage=true testRunTitle: netcoreapp2.2-$(Agent.JobName) - workingDirectory: src + workingDirectory: src \ No newline at end of file diff --git a/src/Pty.Net.Tests/ExitTests.cs b/src/Pty.Net.Tests/ExitTests.cs new file mode 100644 index 0000000..a762e5e --- /dev/null +++ b/src/Pty.Net.Tests/ExitTests.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Pty.Net.Tests +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using Xunit; + + public class ExitTests + { + [Fact] + public async Task SuccessfulExitTest() + { + var completionSource = new TaskCompletionSource(); + + using IPtyConnection terminal = await Utilities.CreateConnectionAsync(Utilities.TimeoutToken); + terminal.ProcessExited += (sender, e) => + { + completionSource.SetResult(e.ExitCode); + }; + + await terminal.RunCommand("exit", Utilities.TimeoutToken); + + var exitCode = await completionSource.Task.WithCancellation(Utilities.TimeoutToken); + + Assert.Equal(0, exitCode); + Assert.Equal(0, terminal.ExitCode); + } + + [Fact] + public async Task UnsuccessfulExitTest() + { + var completionSource = new TaskCompletionSource(); + + using IPtyConnection terminal = await Utilities.CreateConnectionAsync(Utilities.TimeoutToken); + terminal.ProcessExited += (sender, e) => + { + completionSource.SetResult(e.ExitCode); + }; + + await terminal.RunCommand("exit 1", Utilities.TimeoutToken); + + var exitCode = await completionSource.Task.WithCancellation(Utilities.TimeoutToken); + + Assert.Equal(1, exitCode); + Assert.Equal(1, terminal.ExitCode); + } + + [Fact] + public async Task ForceKillTest() + { + var completionSource = new TaskCompletionSource(); + + using IPtyConnection terminal = await Utilities.CreateConnectionAsync(Utilities.TimeoutToken); + terminal.ProcessExited += (sender, e) => + { + completionSource.SetResult(e.ExitCode); + }; + + terminal.Kill(); + + await completionSource.Task.WithCancellation(Utilities.TimeoutToken); + } + } +} diff --git a/src/Pty.Net.Tests/Properties/AssemblyInfo.cs b/src/Pty.Net.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..aeeb572 --- /dev/null +++ b/src/Pty.Net.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/src/Pty.Net.Tests/PtyTests.cs b/src/Pty.Net.Tests/PtyTests.cs index 693d23c..f4d8334 100644 --- a/src/Pty.Net.Tests/PtyTests.cs +++ b/src/Pty.Net.Tests/PtyTests.cs @@ -16,126 +16,28 @@ namespace Pty.Net.Tests public class PtyTests { - private static readonly int TestTimeoutMs = Debugger.IsAttached ? 300_000 : 5_000; - - private CancellationToken TimeoutToken { get; } = new CancellationTokenSource(TestTimeoutMs).Token; - [Fact] 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() - { - { "FOO", "bar" }, - { "Bazz", string.Empty }, - }, - }; - - IPtyConnection terminal = await PtyProvider.SpawnAsync(options, this.TimeoutToken); - - var processExitedTcs = new TaskCompletionSource(); - 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(TaskCreationOptions.RunContinuationsAsynchronously); - var firstDataFound = new TaskCompletionSource(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); + using var terminal = await Utilities.CreateConnectionAsync(Utilities.TimeoutToken); - 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; - } - } - } + await terminal.RunCommand($"echo {Data}", Utilities.TimeoutToken); - 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); - } + Assert.True(await Utilities.FindOutput(terminal.ReaderStream, Data)); + } - terminal.Resize(40, 10); + [Fact] + public async Task OldConnectToTerminal() + { + const string Data = "abc✓ЖЖЖ①Ⅻㄨㄩ 啊阿鼾齄丂丄狚狛狜狝﨨﨩ˊˋ˙– ⿻〇㐀㐁䶴䶵"; - terminal.Dispose(); + using var terminal = await Utilities.CreateConnectionAsync(Utilities.TimeoutToken); - 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. - } + var output = await Utilities.RunAndFind(terminal, $"echo {Data}", Data, Utilities.TimeoutToken); - Assert.True(terminal.WaitForExit(TestTimeoutMs)); + Assert.NotNull(output); } } } diff --git a/src/Pty.Net.Tests/ResizeTests.cs b/src/Pty.Net.Tests/ResizeTests.cs new file mode 100644 index 0000000..1c61b85 --- /dev/null +++ b/src/Pty.Net.Tests/ResizeTests.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Pty.Net.Tests +{ + using System; + using System.IO; + using System.Runtime.InteropServices; + using System.Text.RegularExpressions; + using System.Threading.Tasks; + using Xunit; + + public class ResizeTests + { + [Fact] + public async Task ZeroSizeTest() + { + using var terminal = await Utilities.CreateConnectionAsync(Utilities.TimeoutToken); + + terminal.Resize(0, 0); + } + + [Fact] + public async Task NegativeSizeTest() + { + using var terminal = await Utilities.CreateConnectionAsync(Utilities.TimeoutToken); + + Assert.Throws(() => terminal.Resize(80, -25)); + Assert.Throws(() => terminal.Resize(-80, 25)); + Assert.Throws(() => terminal.Resize(-80, -25)); + } + + [Fact] + public async Task LargeInvalidSizeTest() + { + using var terminal = await Utilities.CreateConnectionAsync(Utilities.TimeoutToken); + + var size = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? short.MaxValue + 1 + : ushort.MaxValue + 1; + + Assert.Throws(() => terminal.Resize(size, 25)); + Assert.Throws(() => terminal.Resize(80, size)); + Assert.Throws(() => terminal.Resize(size, size)); + } + + [Fact] + public async Task LargeValidSizeTest() + { + using var terminal = await Utilities.CreateConnectionAsync(Utilities.TimeoutToken); + + var size = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? (int)short.MaxValue + : (int)ushort.MaxValue; + + terminal.Resize(size, 25); + terminal.Resize(80, size); + terminal.Resize(size, size); + } + + [Fact] + public async Task ActuallyResizesTest() + { + using var terminal = await Utilities.CreateConnectionAsync(Utilities.TimeoutToken); + terminal.Resize(72, 13); + + var command = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? "mode" + : "echo Lines: && tput lines && echo Columns: && tput cols"; + + var output = await Utilities.RunAndFind(terminal, command, "Lines:\\D*(?\\d+).*Columns:\\D*(?\\d+)"); + + var metches = Regex.Match(output, "Lines:\\D*(?\\d+).*Columns:\\D*(?\\d+)"); + + Assert.Equal("13", metches.Groups["rows"].Value); + Assert.Equal("72", metches.Groups["cols"].Value); + } + } +} diff --git a/src/Pty.Net.Tests/Utilities.cs b/src/Pty.Net.Tests/Utilities.cs new file mode 100644 index 0000000..213d95b --- /dev/null +++ b/src/Pty.Net.Tests/Utilities.cs @@ -0,0 +1,248 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Pty.Net.Tests +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Runtime.InteropServices; + using System.Text; + using System.Text.RegularExpressions; + using System.Threading; + using System.Threading.Tasks; + + internal static class Utilities + { + public static readonly int TestTimeoutMs = Debugger.IsAttached ? 300_000 : 10_000; + + public static CancellationToken TimeoutToken => new CancellationTokenSource(TestTimeoutMs).Token; + + public static async Task CreateConnectionAsync(CancellationToken token = default) + { + string app = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Path.Combine(Environment.SystemDirectory, "cmd.exe") : "sh"; + var options = new PtyOptions + { + Name = "Custom terminal", + Cols = 80, + Rows = 25, + Cwd = Environment.CurrentDirectory, + App = app, + Environment = new Dictionary() + { + { "FOO", "bar" }, + { "Bazz", string.Empty }, + }, + }; + + return await PtyProvider.SpawnAsync(options, token); + } + + public static async Task RunCommand(this IPtyConnection terminal, string command, CancellationToken token = default) + { + var processExitedTcs = new TaskCompletionSource(); + 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(TaskCreationOptions.RunContinuationsAsynchronously); + var firstDataFound = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + + var checkForFirstOutput = 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=><~]))"); + var output = string.Empty; + + while (!token.IsCancellationRequested) + { + int count = await terminal.ReaderStream.ReadAsync(buffer, 0, buffer.Length).WithCancellation(token); + 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(command); + if (index >= 0) + { + firstDataFound.TrySetResult(null); + return; + } + } + + firstOutput.TrySetCanceled(); + firstDataFound.TrySetCanceled(); + return; + }); + + byte[] commandBuffer = encoding.GetBytes(command); + + try + { + await firstOutput.Task; + Console.WriteLine("first output found"); + } + catch (OperationCanceledException exception) + { + throw new InvalidOperationException( + $"Could not get any output from terminal{GetTerminalExitCode()}", + exception); + } + + await terminal.WriterStream.WriteAsync(commandBuffer, 0, commandBuffer.Length, token); + await terminal.WriterStream.FlushAsync(); + + await firstDataFound.Task.WithCancellation(token); + + await terminal.WriterStream.WriteAsync(new byte[] { 0x0D }, 0, 1, token); + await terminal.WriterStream.FlushAsync(); + + await checkForFirstOutput; + } + + public static async Task FindOutput(Stream terminalReadStream, string search, CancellationToken token = default) + { + var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + 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=><~]))"); + var searchRegex = new Regex(search); + var output = string.Empty; + + while (!token.IsCancellationRequested) + { + int count = await terminalReadStream.ReadAsync(buffer, 0, buffer.Length).WithCancellation(token); + if (count == 0) + { + break; + } + + output += encoding.GetString(buffer, 0, count); + output = output.Replace("\r", string.Empty).Replace("\n", string.Empty); + output = ansiRegex.Replace(output, string.Empty); + if (searchRegex.IsMatch(output)) + { + return true; + } + } + + return false; + } + + public static async Task RunAndFind(IPtyConnection terminal, string command, string search, CancellationToken token = default) + { + var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + var processExitedTcs = new TaskCompletionSource(); + 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(TaskCreationOptions.RunContinuationsAsynchronously); + var firstDataFound = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var output = string.Empty; + var regexOffset = 0; + + 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=><~]))"); + var searchRegex = new Regex(search); + while (!token.IsCancellationRequested && !processExitedTcs.Task.IsCompleted) + { + int count = await terminal.ReaderStream.ReadAsync(buffer, 0, buffer.Length).WithCancellation(token); + 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); + + Console.WriteLine($"output: {output}"); + var index = output.IndexOf(command); + if (index >= 0) + { + regexOffset = index + command.Length; + firstDataFound.TrySetResult(null); + if (index <= output.Length - (2 * search.Length) + && output.IndexOf(search, index + search.Length) >= 0) + { + return search; + } + else if (searchRegex.IsMatch(output, regexOffset)) + { + return searchRegex.Match(output, regexOffset).ToString(); + } + } + } + + firstOutput.TrySetCanceled(); + firstDataFound.TrySetCanceled(); + return null; + }); + + try + { + await firstOutput.Task; + } + catch (OperationCanceledException exception) + { + throw new InvalidOperationException( + $"Could not get any output from terminal{GetTerminalExitCode()}", + exception); + } + + try + { + byte[] commandBuffer = encoding.GetBytes(command); + await terminal.WriterStream.WriteAsync(commandBuffer, 0, commandBuffer.Length, token); + await terminal.WriterStream.FlushAsync(); + + await firstDataFound.Task; + + await terminal.WriterStream.WriteAsync(new byte[] { 0x0D }, 0, 1, token); // Enter + await terminal.WriterStream.FlushAsync(); + + return await checkTerminalOutputAsync; + } + catch (Exception exception) + { + throw new InvalidOperationException( + $"Could not get expected data from terminal.{GetTerminalExitCode()} Actual terminal output:\n{output}", + exception); + } + } + + public static async Task WithCancellation(this Task task, CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource(); + using (cancellationToken.Register(s => ((TaskCompletionSource)s).TrySetResult(true), tcs)) + { + if (task != await Task.WhenAny(task, tcs.Task).ConfigureAwait(false)) + { + cancellationToken.ThrowIfCancellationRequested(); + } + } + + // Rethrow any fault/cancellation exception, even if we awaited above. + // But if we skipped the above if branch, this will actually yield + // on an incompleted task. + return await task.ConfigureAwait(false); + } + } +} diff --git a/src/Pty.Net/Linux/NativeMethods.cs b/src/Pty.Net/Linux/NativeMethods.cs index 086f4f9..525d96d 100644 --- a/src/Pty.Net/Linux/NativeMethods.cs +++ b/src/Pty.Net/Linux/NativeMethods.cs @@ -83,6 +83,17 @@ public enum TermSpecialControlCharacter VTIME = 5, } + public enum FcntlOperation + { + F_GETFL = 3, + F_SETFL = 4, + } + + public enum FcntlFlags + { + O_NONBLOCK = 0x0004, + } + // int cfsetispeed(struct termios *, speed_t); [DllImport(LibSystem)] internal static extern int cfsetispeed(ref Termios termios, IntPtr speed); @@ -138,6 +149,9 @@ internal static void execvpe(string file, string?[] args, IDictionary protected override bool Kill(int master) { - return kill(this.Pid, SIGHUP) != -1; + var status = kill(this.Pid, SIGHUP) != -1; + + if (!status) + { + Console.WriteLine($"failed to kill process {this.Pid}"); + } + + return status; } /// protected override bool Resize(int fd, int cols, int rows) { - var size = new WinSize((ushort)rows, (ushort)cols); + var size = new WinSize(rows, cols); return ioctl(fd, TIOCSWINSZ, ref size) != -1; } diff --git a/src/Pty.Net/Mac/NativeMethods.cs b/src/Pty.Net/Mac/NativeMethods.cs index a3353e0..b7115ce 100644 --- a/src/Pty.Net/Mac/NativeMethods.cs +++ b/src/Pty.Net/Mac/NativeMethods.cs @@ -253,10 +253,10 @@ public struct WinSize public ushort XPixel; public ushort YPixel; - public WinSize(ushort rows, ushort cols) + public WinSize(int rows, int cols) { - this.Rows = rows; - this.Cols = cols; + this.Rows = checked((ushort)rows); + this.Cols = checked((ushort)cols); this.XPixel = 0; this.YPixel = 0; } diff --git a/src/Pty.Net/Mac/PtyConnection.cs b/src/Pty.Net/Mac/PtyConnection.cs index ded1601..22ee188 100644 --- a/src/Pty.Net/Mac/PtyConnection.cs +++ b/src/Pty.Net/Mac/PtyConnection.cs @@ -30,7 +30,7 @@ protected override bool Kill(int fd) /// protected override bool Resize(int fd, int cols, int rows) { - var size = new WinSize((ushort)rows, (ushort)cols); + var size = new WinSize(rows, cols); return ioctl(fd, TIOCSWINSZ, ref size) != -1; } diff --git a/src/Pty.Net/Unix/PtyConnection.cs b/src/Pty.Net/Unix/PtyConnection.cs index 6e1b8c1..cfa3700 100644 --- a/src/Pty.Net/Unix/PtyConnection.cs +++ b/src/Pty.Net/Unix/PtyConnection.cs @@ -65,7 +65,14 @@ public void Dispose() { this.ReaderStream?.Dispose(); this.WriterStream?.Dispose(); - this.Kill(); + + try + { + this.Kill(); + } + catch + { + } } /// @@ -80,6 +87,16 @@ public void Kill() /// public void Resize(int cols, int rows) { + if (cols < 0) + { + throw new ArgumentOutOfRangeException(nameof(cols)); + } + + if (rows < 0) + { + throw new ArgumentOutOfRangeException(nameof(rows)); + } + if (!this.Resize(this.master, cols, rows)) { throw new InvalidOperationException($"Resizing terminal failed with error {Marshal.GetLastWin32Error()}"); @@ -144,10 +161,10 @@ private void ChildWatcherThreadProc() return; } - Console.WriteLine($"Wait succeeded"); this.exitSignal = status & SignalMask; this.exitCode = this.exitSignal == 0 ? (status >> 8) & ExitCodeMask : 0; this.terminalProcessTerminatedEvent.Set(); + Console.WriteLine($"Wait on {this.pid} succeeded with signal {this.exitSignal}, code: {this.exitCode}"); this.ProcessExited?.Invoke(this, new PtyExitedEventArgs(this.exitCode)); } } diff --git a/src/Pty.Net/Unix/PtyStream.cs b/src/Pty.Net/Unix/PtyStream.cs index 3a0d6f2..ffbfb44 100644 --- a/src/Pty.Net/Unix/PtyStream.cs +++ b/src/Pty.Net/Unix/PtyStream.cs @@ -18,7 +18,7 @@ internal sealed class PtyStream : FileStream /// The fd to connect the stream to. /// The access permissions to set on the fd. public PtyStream(int fd, FileAccess fileAccess) - : base(new SafeFileHandle((IntPtr)fd, ownsHandle: false), fileAccess, bufferSize: 1024, isAsync: false) + : base(new SafeFileHandle((IntPtr)fd, ownsHandle: false), fileAccess, bufferSize: 1024, isAsync: true) { } diff --git a/src/Pty.Net/Windows/NativeMethods.cs b/src/Pty.Net/Windows/NativeMethods.cs index f6109eb..e4fbdde 100644 --- a/src/Pty.Net/Windows/NativeMethods.cs +++ b/src/Pty.Net/Windows/NativeMethods.cs @@ -175,13 +175,13 @@ internal struct STARTUPINFO [StructLayout(LayoutKind.Sequential)] internal struct Coord { - public ushort X; - public ushort Y; + public short X; + public short Y; public Coord(int x, int y) { - this.X = (ushort)x; - this.Y = (ushort)y; + this.X = checked((short)x); + this.Y = checked((short)y); } } diff --git a/src/Pty.Net/Windows/PseudoConsoleConnection.cs b/src/Pty.Net/Windows/PseudoConsoleConnection.cs index c16fbc7..a3f3753 100644 --- a/src/Pty.Net/Windows/PseudoConsoleConnection.cs +++ b/src/Pty.Net/Windows/PseudoConsoleConnection.cs @@ -75,6 +75,16 @@ public void Kill() /// public void Resize(int cols, int rows) { + if (cols < 0) + { + throw new ArgumentOutOfRangeException(nameof(cols)); + } + + if (rows < 0) + { + throw new ArgumentOutOfRangeException(nameof(rows)); + } + int hr = ResizePseudoConsole(this.handles.PseudoConsoleHandle, new Coord(cols, rows)); if (hr != S_OK) { diff --git a/src/Pty.Net/Windows/PtyProvider.cs b/src/Pty.Net/Windows/PtyProvider.cs index 63ffd63..f077eca 100644 --- a/src/Pty.Net/Windows/PtyProvider.cs +++ b/src/Pty.Net/Windows/PtyProvider.cs @@ -322,7 +322,7 @@ private Task StartPseudoConsoleAsync( throw new InvalidOperationException("Could not create an anonymous pipe", new Win32Exception()); } - var coord = new Coord(options.Cols, options.Rows); + var coord = new Coord(checked((short)options.Cols), checked((short)options.Rows)); var pseudoConsoleHandle = new SafePseudoConsoleHandle(); int hr; RuntimeHelpers.PrepareConstrainedRegions();