Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ public AnsiOutput (AppModel appModel = AppModel.FullScreen)
return;
}

// The duplicate only proves the fd is usable; writes go through UnixIOHelper.TryWriteStdout, which
// resolves TerminalDevice.OutputFd itself. Close it so each AnsiOutput does not leak a descriptor.
UnixIOHelper.close (fdCopy);

_platform = AnsiPlatform.UnixRaw;
}

Expand Down
5 changes: 4 additions & 1 deletion Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,10 @@ public void Suspend ()

if (!SuspendHelper.Suspend ())
{
return;
// Do NOT return here. The alternate buffer has already been left and the cursor shown, so returning
// would leave the terminal that way while the app keeps drawing into the scrollback buffer.
Logging.Warning ("NetOutput.Suspend: SuspendHelper.Suspend () returned false; the process was never "
+ "stopped. Re-entering the alternate buffer anyway.");
}

//Enable alternative screen buffer.
Expand Down
127 changes: 127 additions & 0 deletions Terminal.Gui/Drivers/PlatformDetection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,133 @@ namespace Terminal.Gui.Drivers;
/// </summary>
public static class PlatformDetection
{
/// <summary>
/// The .NET OS platform names Terminal.Gui carries platform-specific data for, in probe order.
/// </summary>
/// <remarks>
/// <para>
/// These are the names <see cref="OperatingSystem.IsOSPlatform"/> matches against. Note that
/// <c>ANDROID</c> is distinct from <c>LINUX</c>: <see cref="OperatingSystem.IsLinux"/> returns
/// <see langword="false"/> on Android even though Android runs a Linux kernel.
/// </para>
/// <para>
/// Order matters. A runtime normally reports exactly one of these names, but Mac Catalyst matches both
/// <c>MACCATALYST</c> and <c>IOS</c> — .NET aliases <c>IOS</c> on that target. <c>MACCATALYST</c> is
/// therefore listed first so the more specific name wins.
/// </para>
/// </remarks>
internal static readonly string [] KnownPlatformNames =
[
"WINDOWS",
"LINUX",
"ANDROID",
"OSX",
"MACCATALYST",
"IOS",
"TVOS",
"FREEBSD",
"NETBSD",
"OPENBSD",
"SOLARIS",
"ILLUMOS",
"HAIKU"
];

/// <summary>
/// Determines whether a platform name identifies an Apple (Darwin) platform.
/// </summary>
/// <param name="platformName">A platform name, as returned by <see cref="GetPlatformName"/>.</param>
/// <returns><see langword="true"/> if <paramref name="platformName"/> is an Apple platform.</returns>
/// <remarks>
/// Apple platforms share Darwin's kernel interfaces and, on ARM64, Apple's variadic calling convention, so
/// they must be treated as a group rather than testing for macOS alone. watchOS is absent because .NET has no
/// <c>WATCHOS</c> platform name — it never reports one, so there is nothing to match.
/// </remarks>
internal static bool IsApplePlatformName (string platformName) => platformName is "OSX" or "MACCATALYST" or "IOS" or "TVOS";

/// <summary>
/// Gets the .NET OS platform name for the current platform (for example <c>LINUX</c>, <c>OSX</c>, or
/// <c>ANDROID</c>).
/// </summary>
/// <returns>
/// The matching entry from <see cref="KnownPlatformNames"/>, or <see cref="string.Empty"/> when the current
/// platform is not one Terminal.Gui has platform-specific data for (for example <c>BROWSER</c>).
/// </returns>
internal static string GetPlatformName ()
{
foreach (string name in KnownPlatformNames)
{
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Create (name)))
{
continue;
}

return name;
}

return string.Empty;
}

/// <summary>
/// Determines whether the current operating system is Linux.
/// </summary>
/// <remarks>
/// This method returns <see langword="true"/> only when running on a Linux distribution. Other Unix-like
/// platforms such as macOS and FreeBSD return <see langword="false"/>, as does Android.
/// </remarks>
/// <returns><see langword="true"/> if the operating system is Linux; otherwise, <see langword="false"/>.</returns>
[Obsolete ("Use OperatingSystem.IsLinux () instead. This shim exists for binary compatibility with v2.4.17 and earlier.")]
public static bool IsLinux () => OperatingSystem.IsLinux ();

/// <summary>
/// Determines whether the current operating system is macOS.
/// </summary>
/// <returns><see langword="true"/> if the current operating system is macOS; otherwise, <see langword="false"/>.</returns>
[Obsolete ("Use OperatingSystem.IsMacOS () instead. This shim exists for binary compatibility with v2.4.17 and earlier.")]
public static bool IsMac () => OperatingSystem.IsMacOS ();

/// <summary>
/// Determines if the current platform is Windows.
/// </summary>
/// <returns><see langword="true"/> if the operating system is Windows; otherwise, <see langword="false"/>.</returns>
[Obsolete ("Use OperatingSystem.IsWindows () instead. This shim exists for binary compatibility with v2.4.17 and earlier.")]
public static bool IsWindows () => OperatingSystem.IsWindows ();

/// <summary>
/// Determines whether the current operating system is a Unix-like platform.
/// </summary>
/// <remarks>
/// Returns <see langword="true"/> for Linux, macOS (Darwin), and FreeBSD only. Other Unix platforms .NET can
/// report — NetBSD, OpenBSD, Solaris, illumos, Haiku — return <see langword="false"/>, which is why driver
/// code should test <c>!OperatingSystem.IsWindows ()</c> rather than call this.
/// </remarks>
/// <returns>
/// <see langword="true"/> if the operating system is Linux, macOS, or FreeBSD; otherwise,
/// <see langword="false"/>.
/// </returns>
[Obsolete ("Test !OperatingSystem.IsWindows () instead; this method excludes NetBSD, OpenBSD, Solaris, illumos and Haiku. "
+ "This shim exists for binary compatibility with v2.4.17 and earlier.")]
public static bool IsUnixLike () => OperatingSystem.IsLinux () || OperatingSystem.IsMacOS () || OperatingSystem.IsFreeBSD ();

/// <summary>Returns the <see cref="TuiPlatform"/> for the current operating system.</summary>
/// <remarks>Any platform that is neither Windows nor macOS reports <see cref="TuiPlatform.Linux"/>.</remarks>
[Obsolete ("Use OperatingSystem.IsWindows ()/IsMacOS ()/IsLinux () instead. This shim exists for binary compatibility "
+ "with v2.4.17 and earlier.")]
public static TuiPlatform GetCurrentPlatform ()
{
if (OperatingSystem.IsWindows ())
{
return TuiPlatform.Windows;
}

if (OperatingSystem.IsMacOS ())
{
return TuiPlatform.Macos;
}

return TuiPlatform.Linux;
}

/// <summary>
/// Determines if the current platform is WSL (Windows Subsystem for Linux).
/// </summary>
Expand Down
101 changes: 68 additions & 33 deletions Terminal.Gui/Drivers/UnixHelpers/SuspendHelper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System;
using System.Runtime.InteropServices;

namespace Terminal.Gui.Drivers;
Expand All @@ -7,12 +6,27 @@ internal static class SuspendHelper
{
private static int _suspendSignal;

/// <summary>Suspends the process by sending SIGTSTP to the process group.</summary>
/// <returns>True if the suspension was successful.</returns>
public static bool Suspend ()
{
int signal = GetSuspendSignal ();
/// <summary>Suspends the process by sending <c>SIGTSTP</c> to the process group.</summary>
/// <returns>
/// <see langword="true"/> if the process group was stopped and has since resumed. <see langword="false"/> if
/// the platform has no suspend signal, or if <c>killpg</c> failed.
/// </returns>
/// <remarks>
/// A <see langword="false"/> return means the process was never stopped. Callers that tore down terminal state
/// before calling this — leaving the alternate buffer, returning to cooked mode — must restore it either way,
/// because there is nothing to resume from and the torn-down state would otherwise persist.
/// </remarks>
public static bool Suspend () => Suspend (GetSuspendSignal (), killpg);

/// <summary>Testable core of <see cref="Suspend()"/>, with the syscall supplied by the caller.</summary>
/// <param name="signal">The signal to send, or -1 when the platform has no suspend signal.</param>
/// <param name="killProcessGroup">
/// The <c>killpg</c> implementation, taking the process group and signal and returning 0 on success. Blocks
/// until the process group resumes when it really does stop the process.
/// </param>
/// <returns><see langword="true"/> only if <paramref name="killProcessGroup"/> reported success.</returns>
internal static bool Suspend (int signal, Func<int, int, int> killProcessGroup)
{
Logging.Information ($"SuspendHelper.Suspend: signal={signal}");

if (signal == -1)
Expand All @@ -23,9 +37,17 @@ public static bool Suspend ()
}

Logging.Information ($"SuspendHelper.Suspend: Calling killpg(0, {signal}) [SIGTSTP]...");
int result = killpg (0, signal);
int errno = Marshal.GetLastWin32Error ();
Logging.Information ($"SuspendHelper.Suspend: killpg returned {result}, errno={errno}");
int result = killProcessGroup (0, signal);

if (result != 0)
{
// errno is only meaningful for the real P/Invoke, which sets SetLastError.
Logging.Warning ($"SuspendHelper.Suspend: killpg returned {result} (errno={Marshal.GetLastWin32Error ()}). Process was not stopped.");

return false;
}

Logging.Information ("SuspendHelper.Suspend: killpg succeeded; process group has resumed.");

return true;
}
Expand All @@ -37,34 +59,47 @@ private static int GetSuspendSignal ()
return _suspendSignal;
}

if (OperatingSystem.IsMacOS () ||
OperatingSystem.IsFreeBSD () ||
RuntimeInformation.IsOSPlatform (OSPlatform.Create ("NETBSD")) ||
RuntimeInformation.IsOSPlatform (OSPlatform.Create ("OPENBSD")))
{
_suspendSignal = 18;
}
else if (OperatingSystem.IsLinux ())
{
_suspendSignal = 20;
}
else if (RuntimeInformation.IsOSPlatform (OSPlatform.Create ("SOLARIS")) ||
RuntimeInformation.IsOSPlatform (OSPlatform.Create ("ILLUMOS")))
{
_suspendSignal = 24;
}
else if (RuntimeInformation.IsOSPlatform (OSPlatform.Create ("HAIKU")))
{
_suspendSignal = 21;
}
else
{
_suspendSignal = -1;
}
_suspendSignal = MapSuspendSignal (PlatformDetection.GetPlatformName ());

return _suspendSignal;
}

/// <summary>
/// Maps a .NET OS platform name to that platform's <c>SIGTSTP</c> signal number.
/// </summary>
/// <param name="platformName">
/// A platform name from <see cref="PlatformDetection.KnownPlatformNames"/>, as returned by
/// <see cref="PlatformDetection.GetPlatformName"/>.
/// </param>
/// <returns>The <c>SIGTSTP</c> number, or -1 when the platform has no known suspend signal.</returns>
/// <remarks>
/// Values come from each platform's <c>signal.h</c>. Two entries are easy to get wrong:
/// <list type="bullet">
/// <item>
/// Haiku's <c>SIGTSTP</c> is 13. 21 is <c>SIGKILLTHR</c> there, so sending 21 would kill threads
/// instead of suspending.
/// </item>
/// <item>
/// Android needs its own entry because <see cref="OperatingSystem.IsLinux"/> returns
/// <see langword="false"/> on Android.
/// </item>
/// </list>
/// </remarks>
internal static int MapSuspendSignal (string platformName) =>
platformName switch
{
// Darwin (all Apple platforms) and the BSDs share the historical BSD signal numbering.
"OSX" or "MACCATALYST" or "IOS" or "TVOS" or "FREEBSD" or "NETBSD" or "OPENBSD" => 18,

// Linux, on every architecture .NET targets — including ppc64le, which follows the generic Linux signal
// numbering even though its ioctl numbering differs. (MIPS uses 24, SPARC and Alpha 18; none is a .NET
// target, so unlike TIOCGWINSZ this needs no architecture.)
"LINUX" or "ANDROID" => 20,
"SOLARIS" or "ILLUMOS" => 24,
"HAIKU" => 13,
_ => -1
};

[DllImport ("libc", SetLastError = true)]
private static extern int killpg (int pgrp, int sig);
}
86 changes: 81 additions & 5 deletions Terminal.Gui/Drivers/UnixHelpers/UnixIOHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ public enum Condition : short
[DllImport ("libc", SetLastError = true)]
public static extern int dup (int fd);

/// <summary>
/// Close a file descriptor.
/// </summary>
/// <param name="fd">File descriptor to close.</param>
/// <returns>0 on success, -1 on error.</returns>
[DllImport ("libc", SetLastError = true)]
public static extern int close (int fd);

/// <summary>
/// Wait until all output written to the file descriptor has been transmitted.
/// </summary>
Expand Down Expand Up @@ -178,10 +186,79 @@ public struct WinSize
}

/// <summary>
/// Get window/terminal size using ioctl.
/// Platform-specific constant (different on Darwin/BSD vs Linux).
/// Get window/terminal size using ioctl. Platform-specific constant; see <see cref="MapTiocgwinsz"/>.
/// </summary>
public static readonly uint TIOCGWINSZ = OperatingSystem.IsLinux () ? 0x5413u : 0x40087468u;
public static readonly uint TIOCGWINSZ = MapTiocgwinsz (RuntimeInformation.ProcessArchitecture, PlatformDetection.GetPlatformName ());

/// <summary>
/// Maps a .NET OS platform name to that platform's <c>TIOCGWINSZ</c> ioctl request code.
/// </summary>
/// <param name="processArchitecture">
/// The architecture of the running process — <see cref="RuntimeInformation.ProcessArchitecture"/>. Only
/// consulted for Linux, whose ioctl numbering varies by architecture.
/// </param>
/// <param name="platformName">
/// A platform name from <see cref="PlatformDetection.KnownPlatformNames"/>, as returned by
/// <see cref="PlatformDetection.GetPlatformName"/>.
/// </param>
/// <returns>The <c>TIOCGWINSZ</c> request code for <paramref name="platformName"/>.</returns>
/// <remarks>
/// <para>
/// Android needs its own entry because <see cref="OperatingSystem.IsLinux"/> returns
/// <see langword="false"/> on Android even though it uses the same asm-generic ioctl numbering as Linux.
/// </para>
/// <para>
/// On Linux the value is architecture-dependent, not purely OS-dependent, which is why this takes an
/// architecture. Every architecture .NET targets uses asm-generic numbering (0x5413) except ppc64le, whose
/// UAPI defines <c>TIOCGWINSZ</c> as <c>_IOR ('t', 104, struct winsize)</c> — the BSD encoding, 0x40087468.
/// Linux on MIPS, SPARC and Alpha does the same, but .NET has no targets for those.
/// </para>
/// </remarks>
internal static uint MapTiocgwinsz (Architecture processArchitecture, string platformName) =>
platformName switch
{
// asm-generic ioctl numbering, except on ppc64le which uses the BSD encoding.
"LINUX" or "ANDROID" => processArchitecture == Architecture.Ppc64le ? 0x40087468u : 0x5413u,

// Solaris/illumos: TIOC|104, where TIOC is ('T' << 8).
"SOLARIS" or "ILLUMOS" => 0x5468u,

// Haiku uses its own sequential numbering: TIOCGWINSZ is (TCGETA + 12), where TCGETA is 0x8000.
"HAIKU" => 0x800Cu,

// BSD _IOR ('t', 104, struct winsize): Darwin (all Apple platforms), FreeBSD, NetBSD, OpenBSD, and the
// best guess for any other Unix, since BSD-style ioctl encoding is the most common.
_ => 0x40087468u
};

/// <summary>
/// Determines whether <see cref="ioctl_arm64"/> must be used in place of <see cref="ioctl"/>.
/// </summary>
/// <param name="processArchitecture">
/// The architecture of the running process — <see cref="RuntimeInformation.ProcessArchitecture"/>, never
/// <see cref="RuntimeInformation.OSArchitecture"/>. See the remarks.
/// </param>
/// <param name="platformName">A platform name, as returned by <see cref="PlatformDetection.GetPlatformName"/>.</param>
/// <returns><see langword="true"/> when the Apple ARM64 variadic calling convention applies.</returns>
/// <remarks>
/// <para>
/// Apple's ARM64 ABI passes every variadic argument on the stack, unlike standard AAPCS64 where they go in
/// registers. <c>ioctl</c> is variadic, so on Apple ARM64 the <c>winsize*</c> must be pushed past the eight
/// register slots — which is what <see cref="ioctl_arm64"/>'s placeholder parameters accomplish. Every other
/// ARM64 platform (Linux, Android, FreeBSD) follows standard AAPCS64 and must use the plain
/// <see cref="ioctl"/>. See https://github.com/dotnet/runtime/issues/48796#issuecomment-3695794860.
/// </para>
/// <para>
/// This must key off the <em>process</em> architecture, not the OS architecture. .NET deliberately reports
/// <see cref="RuntimeInformation.OSArchitecture"/> as <see cref="Architecture.Arm64"/> for an x64 process
/// translated by Rosetta (it checks <c>sysctl.proc_translated</c>), but such a process executes x64 code and
/// follows the x64 ABI, where variadic arguments go in registers. Keying off
/// <see cref="RuntimeInformation.OSArchitecture"/> therefore selects the stack-passing path for a process
/// that needs the register path, and terminal sizing fails.
/// </para>
/// </remarks>
internal static bool UseArm64VariadicIoctl (Architecture processArchitecture, string platformName) =>
processArchitecture == Architecture.Arm64 && PlatformDetection.IsApplePlatformName (platformName);

/// <summary>
/// I/O control operations on file descriptors.
Expand Down Expand Up @@ -432,8 +509,7 @@ public static bool TryGetTerminalSize (out Size size)
var ioctlResult = 0;
WinSize ws;

if (RuntimeInformation.OSArchitecture == Architecture.Arm64
&& OperatingSystem.IsMacOS ())
if (UseArm64VariadicIoctl (RuntimeInformation.ProcessArchitecture, PlatformDetection.GetPlatformName ()))
{
ioctlResult = ioctl_arm64 (fd,
TIOCGWINSZ,
Expand Down
Loading
Loading