diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.FileDescriptors.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.FileDescriptors.cs deleted file mode 100644 index 614c77c5827502..00000000000000 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.FileDescriptors.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using Microsoft.Win32.SafeHandles; - -internal static partial class Interop -{ - internal static partial class Sys - { - internal static class FileDescriptors - { - internal static readonly SafeFileHandle STDIN_FILENO = CreateFileHandle(0); - internal static readonly SafeFileHandle STDOUT_FILENO = CreateFileHandle(1); - internal static readonly SafeFileHandle STDERR_FILENO = CreateFileHandle(2); - - private static SafeFileHandle CreateFileHandle(int fileNumber) - { - return new SafeFileHandle((IntPtr)fileNumber, ownsHandle: false); - } - } - } -} diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.IsATty.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.IsATty.cs index da47ef8eb1b9da..beb79d77d5165b 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.IsATty.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.IsATty.cs @@ -3,14 +3,13 @@ using System; using System.Runtime.InteropServices; -using Microsoft.Win32.SafeHandles; internal static partial class Interop { internal static partial class Sys { - [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_IsATty", SetLastError = true)] + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_IsATty")] [return: MarshalAs(UnmanagedType.Bool)] - internal static partial bool IsATty(SafeFileHandle fd); + internal static partial bool IsATty(IntPtr fd); } } diff --git a/src/libraries/System.Console/ref/System.Console.cs b/src/libraries/System.Console/ref/System.Console.cs index 6d814278c1ae20..ed6a329434b08b 100644 --- a/src/libraries/System.Console/ref/System.Console.cs +++ b/src/libraries/System.Console/ref/System.Console.cs @@ -101,6 +101,10 @@ public static void MoveBufferArea(int sourceLeft, int sourceTop, int sourceWidth public static System.IO.Stream OpenStandardError() { throw null; } public static System.IO.Stream OpenStandardError(int bufferSize) { throw null; } [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("android")] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")] + public static Microsoft.Win32.SafeHandles.SafeFileHandle OpenStandardErrorHandle() { throw null; } + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("android")] [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")] [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")] @@ -108,9 +112,18 @@ public static void MoveBufferArea(int sourceLeft, int sourceTop, int sourceWidth [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("android")] [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] public static System.IO.Stream OpenStandardInput(int bufferSize) { throw null; } + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("android")] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")] + public static Microsoft.Win32.SafeHandles.SafeFileHandle OpenStandardInputHandle() { throw null; } public static System.IO.Stream OpenStandardOutput() { throw null; } public static System.IO.Stream OpenStandardOutput(int bufferSize) { throw null; } [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("android")] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("ios")] + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("tvos")] + public static Microsoft.Win32.SafeHandles.SafeFileHandle OpenStandardOutputHandle() { throw null; } + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("android")] [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] public static int Read() { throw null; } [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("android")] diff --git a/src/libraries/System.Console/src/System.Console.csproj b/src/libraries/System.Console/src/System.Console.csproj index 0b24fa4c0cb1ff..802f7e0432dd4c 100644 --- a/src/libraries/System.Console/src/System.Console.csproj +++ b/src/libraries/System.Console/src/System.Console.csproj @@ -58,17 +58,13 @@ - - + - @@ -99,8 +95,6 @@ Link="Common\Interop\Unix\Interop.Libraries.cs" /> - @@ -227,10 +221,6 @@ Link="Common\Interop\Unix\Interop.IOErrors.cs" /> - - + /// Gets the standard input handle. + /// + /// A representing the standard input handle. + /// + /// The returned handle does not own the underlying resource, so disposing it will not close the standard input handle. + /// + [UnsupportedOSPlatform("android")] + [UnsupportedOSPlatform("browser")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static SafeFileHandle OpenStandardInputHandle() + { + return ConsolePal.OpenStandardInputHandle(); + } + + /// + /// Gets the standard output handle. + /// + /// A representing the standard output handle. + /// + /// The returned handle does not own the underlying resource, so disposing it will not close the standard output handle. + /// + [UnsupportedOSPlatform("android")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static SafeFileHandle OpenStandardOutputHandle() + { + return ConsolePal.OpenStandardOutputHandle(); + } + + /// + /// Gets the standard error handle. + /// + /// A representing the standard error handle. + /// + /// The returned handle does not own the underlying resource, so disposing it will not close the standard error handle. + /// + [UnsupportedOSPlatform("android")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public static SafeFileHandle OpenStandardErrorHandle() + { + return ConsolePal.OpenStandardErrorHandle(); + } + [UnsupportedOSPlatform("android")] [UnsupportedOSPlatform("browser")] [UnsupportedOSPlatform("ios")] diff --git a/src/libraries/System.Console/src/System/ConsolePal.Android.cs b/src/libraries/System.Console/src/System/ConsolePal.Android.cs index 2be7b49d595e9d..58e29f3ab758bb 100644 --- a/src/libraries/System.Console/src/System/ConsolePal.Android.cs +++ b/src/libraries/System.Console/src/System/ConsolePal.Android.cs @@ -4,6 +4,7 @@ using System.IO; using System.Runtime.InteropServices; using System.Text; +using Microsoft.Win32.SafeHandles; #pragma warning disable IDE0060 @@ -30,6 +31,12 @@ internal static void EnsureConsoleInitialized() { } public static Stream OpenStandardError() => new LogcatStream(OutputEncoding); + public static SafeFileHandle OpenStandardInputHandle() => throw new PlatformNotSupportedException(); + + public static SafeFileHandle OpenStandardOutputHandle() => throw new PlatformNotSupportedException(); + + public static SafeFileHandle OpenStandardErrorHandle() => throw new PlatformNotSupportedException(); + public static Encoding InputEncoding => throw new PlatformNotSupportedException(); public static void SetConsoleInputEncoding(Encoding enc) => throw new PlatformNotSupportedException(); diff --git a/src/libraries/System.Console/src/System/ConsolePal.WebAssembly.cs b/src/libraries/System.Console/src/System/ConsolePal.Browser.cs similarity index 90% rename from src/libraries/System.Console/src/System/ConsolePal.WebAssembly.cs rename to src/libraries/System.Console/src/System/ConsolePal.Browser.cs index e02991d6baf31c..40d2cd6d8bfca9 100644 --- a/src/libraries/System.Console/src/System/ConsolePal.WebAssembly.cs +++ b/src/libraries/System.Console/src/System/ConsolePal.Browser.cs @@ -8,11 +8,11 @@ namespace System { - internal sealed class WasmConsoleStream : ConsoleStream + internal sealed class BrowserConsoleStream : ConsoleStream { private readonly SafeFileHandle _handle; - internal WasmConsoleStream(SafeFileHandle handle, FileAccess access) + internal BrowserConsoleStream(SafeFileHandle handle, FileAccess access) : base(access) { _handle = handle; @@ -83,14 +83,22 @@ internal static void EnsureConsoleInitialized() { } public static Stream OpenStandardOutput() { - return new WasmConsoleStream(Interop.Sys.Dup(Interop.Sys.FileDescriptors.STDOUT_FILENO), FileAccess.Write); + return new BrowserConsoleStream(OpenStandardOutputHandle(), FileAccess.Write); } public static Stream OpenStandardError() { - return new WasmConsoleStream(Interop.Sys.Dup(Interop.Sys.FileDescriptors.STDERR_FILENO), FileAccess.Write); + return new BrowserConsoleStream(OpenStandardErrorHandle(), FileAccess.Write); } + public static SafeFileHandle OpenStandardInputHandle() => throw new PlatformNotSupportedException(); + + public static SafeFileHandle OpenStandardOutputHandle() => OpenStandardHandle(1); + + public static SafeFileHandle OpenStandardErrorHandle() => OpenStandardHandle(2); + + private static SafeFileHandle OpenStandardHandle(IntPtr fd) => new SafeFileHandle(fd, ownsHandle: false); + public static Encoding InputEncoding => throw new PlatformNotSupportedException(); public static void SetConsoleInputEncoding(Encoding enc) => throw new PlatformNotSupportedException(); diff --git a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs index 98aca9d7ef698b..57d4ef11fd62e3 100644 --- a/src/libraries/System.Console/src/System/ConsolePal.Unix.cs +++ b/src/libraries/System.Console/src/System/ConsolePal.Unix.cs @@ -45,20 +45,28 @@ internal static partial class ConsolePal public static Stream OpenStandardInput() { - return new UnixConsoleStream(Interop.CheckIo(Interop.Sys.Dup(Interop.Sys.FileDescriptors.STDIN_FILENO)), FileAccess.Read, + return new UnixConsoleStream(OpenStandardInputHandle(), FileAccess.Read, useReadLine: !Console.IsInputRedirected); } public static Stream OpenStandardOutput() { - return new UnixConsoleStream(Interop.CheckIo(Interop.Sys.Dup(Interop.Sys.FileDescriptors.STDOUT_FILENO)), FileAccess.Write); + return new UnixConsoleStream(OpenStandardOutputHandle(), FileAccess.Write); } public static Stream OpenStandardError() { - return new UnixConsoleStream(Interop.CheckIo(Interop.Sys.Dup(Interop.Sys.FileDescriptors.STDERR_FILENO)), FileAccess.Write); + return new UnixConsoleStream(OpenStandardErrorHandle(), FileAccess.Write); } + public static SafeFileHandle OpenStandardInputHandle() => OpenStandardHandle(0); + + public static SafeFileHandle OpenStandardOutputHandle() => OpenStandardHandle(1); + + public static SafeFileHandle OpenStandardErrorHandle() => OpenStandardHandle(2); + + private static SafeFileHandle OpenStandardHandle(IntPtr fd) => new SafeFileHandle(fd, ownsHandle: false); + public static Encoding InputEncoding { get { return GetConsoleEncoding(); } @@ -660,7 +668,7 @@ static void TransferBytes(ReadOnlySpan src, StdInReader dst) /// Gets whether the specified file descriptor was redirected. /// It's considered redirected if it doesn't refer to a terminal. /// - private static bool IsHandleRedirected(SafeFileHandle fd) + private static bool IsHandleRedirected(IntPtr fd) { return !Interop.Sys.IsATty(fd); } @@ -671,7 +679,7 @@ private static bool IsHandleRedirected(SafeFileHandle fd) /// public static bool IsInputRedirectedCore() { - return IsHandleRedirected(Interop.Sys.FileDescriptors.STDIN_FILENO); + return IsHandleRedirected(0); } /// Gets whether Console.Out is redirected. @@ -679,7 +687,7 @@ public static bool IsInputRedirectedCore() /// public static bool IsOutputRedirectedCore() { - return IsHandleRedirected(Interop.Sys.FileDescriptors.STDOUT_FILENO); + return IsHandleRedirected(1); } /// Gets whether Console.Error is redirected. @@ -687,7 +695,7 @@ public static bool IsOutputRedirectedCore() /// public static bool IsErrorRedirectedCore() { - return IsHandleRedirected(Interop.Sys.FileDescriptors.STDERR_FILENO); + return IsHandleRedirected(2); } /// Creates an encoding from the current environment. @@ -889,8 +897,8 @@ private static unsafe void EnsureInitializedCore() // This also resets it for termination due to an unhandled exception. AppDomain.CurrentDomain.UnhandledException += (_, _) => { Interop.Sys.UninitializeTerminal(); }; - s_terminalHandle = !Console.IsOutputRedirected ? Interop.Sys.FileDescriptors.STDOUT_FILENO : - !Console.IsInputRedirected ? Interop.Sys.FileDescriptors.STDIN_FILENO : + s_terminalHandle = !Console.IsOutputRedirected ? OpenStandardOutputHandle() : + !Console.IsInputRedirected ? OpenStandardInputHandle() : null; // Provide the native lib with the correct code from the terminfo to transition us into @@ -1108,7 +1116,7 @@ private static void InvalidateTerminalSettings() // DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION is set. // In both cases, they are written to stdout. internal static void WriteTerminalAnsiColorString(string? value) - => WriteTerminalAnsiString(value, Interop.Sys.FileDescriptors.STDOUT_FILENO, mayChangeCursorPosition: false); + => WriteTerminalAnsiString(value, OpenStandardOutputHandle(), mayChangeCursorPosition: false); /// Writes a terminfo-based ANSI escape string to stdout. /// The string to write. diff --git a/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs b/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs index 4022de1a439fe6..70aec828947c03 100644 --- a/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs +++ b/src/libraries/System.Console/src/System/ConsolePal.Wasi.cs @@ -20,20 +20,28 @@ internal static partial class ConsolePal { public static Stream OpenStandardInput() { - return new UnixConsoleStream(Interop.Sys.FileDescriptors.STDIN_FILENO, FileAccess.Read, + return new UnixConsoleStream(OpenStandardInputHandle(), FileAccess.Read, useReadLine: !Console.IsInputRedirected); } public static Stream OpenStandardOutput() { - return new UnixConsoleStream(Interop.Sys.FileDescriptors.STDOUT_FILENO, FileAccess.Write); + return new UnixConsoleStream(OpenStandardOutputHandle(), FileAccess.Write); } public static Stream OpenStandardError() { - return new UnixConsoleStream(Interop.Sys.FileDescriptors.STDERR_FILENO, FileAccess.Write); + return new UnixConsoleStream(OpenStandardErrorHandle(), FileAccess.Write); } + public static SafeFileHandle OpenStandardInputHandle() => OpenStandardHandle(0); + + public static SafeFileHandle OpenStandardOutputHandle() => OpenStandardHandle(1); + + public static SafeFileHandle OpenStandardErrorHandle() => OpenStandardHandle(2); + + private static SafeFileHandle OpenStandardHandle(IntPtr fd) => new SafeFileHandle(fd, ownsHandle: false); + public static Encoding InputEncoding { get { return GetConsoleEncoding(); } diff --git a/src/libraries/System.Console/src/System/ConsolePal.Windows.cs b/src/libraries/System.Console/src/System/ConsolePal.Windows.cs index 76573cb18eccbc..64b1acc8e83966 100644 --- a/src/libraries/System.Console/src/System/ConsolePal.Windows.cs +++ b/src/libraries/System.Console/src/System/ConsolePal.Windows.cs @@ -5,6 +5,7 @@ using System.IO; using System.Runtime.InteropServices; using System.Text; +using Microsoft.Win32.SafeHandles; namespace System { @@ -90,6 +91,18 @@ private static unsafe bool ConsoleHandleIsWritable(IntPtr outErrHandle) return r != 0; // In Win32 apps w/ no console, bResult should be 0 for failure. } + public static SafeFileHandle OpenStandardInputHandle() => OpenStandardHandle(Interop.Kernel32.HandleTypes.STD_INPUT_HANDLE); + + public static SafeFileHandle OpenStandardOutputHandle() => OpenStandardHandle(Interop.Kernel32.HandleTypes.STD_OUTPUT_HANDLE); + + public static SafeFileHandle OpenStandardErrorHandle() => OpenStandardHandle(Interop.Kernel32.HandleTypes.STD_ERROR_HANDLE); + + private static SafeFileHandle OpenStandardHandle(int handleType) + { + IntPtr handle = Interop.Kernel32.GetStdHandle(handleType); + return new SafeFileHandle(handle, ownsHandle: false); + } + public static Encoding InputEncoding { get { return EncodingHelper.GetSupportedConsoleEncoding((int)Interop.Kernel32.GetConsoleCP()); } diff --git a/src/libraries/System.Console/src/System/ConsolePal.iOS.cs b/src/libraries/System.Console/src/System/ConsolePal.iOS.cs index 5360134bdc108d..223c91d2eba732 100644 --- a/src/libraries/System.Console/src/System/ConsolePal.iOS.cs +++ b/src/libraries/System.Console/src/System/ConsolePal.iOS.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.IO; using System.Text; +using Microsoft.Win32.SafeHandles; #pragma warning disable IDE0060 @@ -34,6 +35,12 @@ internal static void EnsureConsoleInitialized() public static Stream OpenStandardError() => new NSLogStream(OutputEncoding); + public static SafeFileHandle OpenStandardInputHandle() => throw new PlatformNotSupportedException(); + + public static SafeFileHandle OpenStandardOutputHandle() => throw new PlatformNotSupportedException(); + + public static SafeFileHandle OpenStandardErrorHandle() => throw new PlatformNotSupportedException(); + public static Encoding InputEncoding => throw new PlatformNotSupportedException(); public static void SetConsoleInputEncoding(Encoding enc) => throw new PlatformNotSupportedException(); diff --git a/src/libraries/System.Console/tests/ConsoleHandles.cs b/src/libraries/System.Console/tests/ConsoleHandles.cs new file mode 100644 index 00000000000000..b09c94b71497c8 --- /dev/null +++ b/src/libraries/System.Console/tests/ConsoleHandles.cs @@ -0,0 +1,110 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.IO; +using Microsoft.DotNet.RemoteExecutor; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.Tests +{ + public partial class ConsoleTests + { + [Fact] + [PlatformSpecific(TestPlatforms.Any & ~TestPlatforms.Browser & ~TestPlatforms.iOS & ~TestPlatforms.tvOS & ~TestPlatforms.Android)] + public void OpenStandardInputHandle_ReturnsValidHandle() + { + using SafeFileHandle inputHandle = Console.OpenStandardInputHandle(); + Assert.NotNull(inputHandle); + Assert.False(inputHandle.IsInvalid); + Assert.False(inputHandle.IsClosed); + } + + [Fact] + [PlatformSpecific(TestPlatforms.Any & ~TestPlatforms.iOS & ~TestPlatforms.tvOS & ~TestPlatforms.Android)] + public void OpenStandardOutputHandle_ReturnsValidHandle() + { + using SafeFileHandle outputHandle = Console.OpenStandardOutputHandle(); + Assert.NotNull(outputHandle); + Assert.False(outputHandle.IsInvalid); + Assert.False(outputHandle.IsClosed); + } + + [Fact] + [PlatformSpecific(TestPlatforms.Any & ~TestPlatforms.iOS & ~TestPlatforms.tvOS & ~TestPlatforms.Android)] + public void OpenStandardErrorHandle_ReturnsValidHandle() + { + using SafeFileHandle errorHandle = Console.OpenStandardErrorHandle(); + Assert.NotNull(errorHandle); + Assert.False(errorHandle.IsInvalid); + Assert.False(errorHandle.IsClosed); + } + + [Fact] + [PlatformSpecific(TestPlatforms.Any & ~TestPlatforms.Browser & ~TestPlatforms.iOS & ~TestPlatforms.tvOS & ~TestPlatforms.Android)] + public void OpenStandardHandles_DoNotOwnHandle() + { + SafeFileHandle inputHandle = Console.OpenStandardInputHandle(); + SafeFileHandle outputHandle = Console.OpenStandardOutputHandle(); + SafeFileHandle errorHandle = Console.OpenStandardErrorHandle(); + + // Disposing should not close the underlying handle since ownsHandle is false + inputHandle.Dispose(); + outputHandle.Dispose(); + errorHandle.Dispose(); + + // Should still be able to get new handles + using SafeFileHandle inputHandle2 = Console.OpenStandardInputHandle(); + using SafeFileHandle outputHandle2 = Console.OpenStandardOutputHandle(); + using SafeFileHandle errorHandle2 = Console.OpenStandardErrorHandle(); + + Assert.NotNull(inputHandle2); + Assert.NotNull(outputHandle2); + Assert.NotNull(errorHandle2); + Assert.False(inputHandle2.IsInvalid); + Assert.False(outputHandle2.IsInvalid); + Assert.False(errorHandle2.IsInvalid); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [PlatformSpecific(TestPlatforms.Any & ~TestPlatforms.Browser & ~TestPlatforms.iOS & ~TestPlatforms.tvOS & ~TestPlatforms.Android)] + public void OpenStandardHandles_CanBeUsedWithStream() + { + using RemoteInvokeHandle child = RemoteExecutor.Invoke(() => + { + using SafeFileHandle outputHandle = Console.OpenStandardOutputHandle(); + using FileStream fs = new FileStream(outputHandle, FileAccess.Write); + using StreamWriter writer = new StreamWriter(fs); + writer.WriteLine("Test output"); + }, new RemoteInvokeOptions { StartInfo = new ProcessStartInfo() { RedirectStandardOutput = true } }); + + // Verify the output was written + string output = child.Process.StandardOutput.ReadLine(); + Assert.Equal("Test output", output); + + child.Process.WaitForExit(); + } + + [Fact] + [PlatformSpecific(TestPlatforms.Android | TestPlatforms.iOS | TestPlatforms.tvOS | TestPlatforms.Browser)] + public void OpenStandardInputHandle_ThrowsOnUnsupportedPlatforms() + { + Assert.Throws(() => Console.OpenStandardInputHandle()); + } + + [Fact] + [PlatformSpecific(TestPlatforms.Android | TestPlatforms.iOS | TestPlatforms.tvOS)] + public void OpenStandardOutputHandle_ThrowsOnUnsupportedPlatforms() + { + Assert.Throws(() => Console.OpenStandardOutputHandle()); + } + + [Fact] + [PlatformSpecific(TestPlatforms.Android | TestPlatforms.iOS | TestPlatforms.tvOS)] + public void OpenStandardErrorHandle_ThrowsOnUnsupportedPlatforms() + { + Assert.Throws(() => Console.OpenStandardErrorHandle()); + } + } +}