From 6b21fbdeced1dfee361b791857521dfd32286088 Mon Sep 17 00:00:00 2001 From: Pavel Koneski Date: Thu, 16 Jan 2025 13:28:44 -0800 Subject: [PATCH 1/4] Implement termios mock --- src/core/IronPython.Modules/termios.cs | 323 ++++++++++++++++++ .../suite/modules/io_related/test_termios.py | 103 ++++++ 2 files changed, 426 insertions(+) create mode 100644 src/core/IronPython.Modules/termios.cs create mode 100644 tests/suite/modules/io_related/test_termios.py diff --git a/src/core/IronPython.Modules/termios.cs b/src/core/IronPython.Modules/termios.cs new file mode 100644 index 000000000..43a0e5382 --- /dev/null +++ b/src/core/IronPython.Modules/termios.cs @@ -0,0 +1,323 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +#nullable enable + +using System; +using System.Collections; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +using Microsoft.Scripting.Runtime; + +using IronPython.Runtime; +using IronPython.Runtime.Operations; + +using static IronPython.Modules.PythonIOModule; + +[assembly: PythonModule("termios", typeof(IronPython.Modules.PythonTermios), PlatformsAttribute.PlatformFamily.Unix)] +namespace IronPython.Modules; + +[SupportedOSPlatform("linux")] +[SupportedOSPlatform("macos")] +public static class PythonTermios { + + public const string __doc__ = "Stub of termios, just enough to support module tty."; + // and also prompt_toolkit.terminal.vt100_input + + #region termios IO Control Codes (TIOC*) + + public static int TIOCGWINSZ => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 1074295912 : 21523; + + + #endregion + + #region Other Public Constants + // Linux: glibc/bits/termios.h (/usr/include/{x86_64,aarch64}-linux-gnu/) + // macOS: usr/include/sys/termios.h (/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk) + + // iflag + public const int IGNBRK = 0x0001; // ignore break condition + public const int BRKINT = 0x0002; // signal interrupt on break + public const int IGNPAR = 0x0004; // ignore characters with parity errors + public const int PARMRK = 0x0008; // mark parity and framing errors + public const int INPCK = 0x0010; // enable input parity check + public const int ISTRIP = 0x0020; // mask off 8th bit + public const int INLCR = 0x0040; // map NL into CR on input + public const int IGNCR = 0x0080; // ignore CR + public const int ICRNL = 0x0100; // map CR to NL on input + public static int IXON => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? + 0x0200 : 0x0400; // enable start output control + public static int IXOFF => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? + 0x0400 : 0x1000; // enable stop output control + public const int IXANY = 0x0800; // any char will restart after stop + public const int IMAXBEL = 0x2000; // ring bell on input queue full + public const int IUTF8 = 0x4000; // maintain state for UTF-8 VERASE + + + // oflag + public const int OPOST = 0x0001; // enable output processing + public static int ONLCR => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? + 0x0002 : 0x0004; // map NL to CR-NL + + + // cflag + public static int CSIZE => CS5 | CS6 | CS7 | CS8; // number of bits per character + public static int CS5 => 0x0000; // 5 bits per character + public static int CS6 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0100 : 0x0010; // 6 bits per character + public static int CS7 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0200 : 0x0020; // 7 bits per character + public static int CS8 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0300 : 0x0030; // 8 bits per character + public static int CREAD => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0800 : 0x0080; // enable receiver + public static int PARENB => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x1000 : 0x0100; // parity enable + public static int HUPCL => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x4000 : 0x0400; // hang up on last close + + + // lflag + public static uint ECHOKE => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0001u : 0x0800u; // visual erase for line kill + public static uint ECHOE => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0002u : 0x0010u; // visually erase chars + public static uint ECHOK => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0004u : 0x0020u; // echo NL after line kill + public static uint ECHO => 0x0008u; // enable echoing of input characters + public static uint ECHONL => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0010u : 0x0040u; // echo NL even if ECHO is off + public static uint ECHOPRT => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0020u : 0x0400u; // visual erase mode for hardcopy + public static uint ECHOCTL => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0040u : 0x0200u; // echo control characters as ^(Char) + public static uint ISIG => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0080u : 0x0001u; // enable signals + public static uint ICANON => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0100u : 0x0002u; // canonical mode + public static uint IEXTEN => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0400u : 0x8000u; // enable extended input character processing + public static uint TOSTOP => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0040_0000u : 0x0100u; // stop background jobs from output + public static uint FLUSHO => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0080_0000u : 0x1000u; // output being flushed + public static uint PENDIN => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x2000_0000u : 0x2000u; // XXX retype pending input + public static uint NOFLSH => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x8000_0000u : 0x0080u; // don't flush after interrupt + + + // when the changes take effect: + public const int TCSANOW = 0; // change immediately + public const int TCSADRAIN = 1; // flush output, then change + public const int TCSAFLUSH = 2; // discard input, flush output, then change + + + // control characters - enabled by: + public const int VEOF = 0; // ICANON + public const int VEOL = 1; // ICANON + public const int VEOL2 = 2; // ICANON + IEXTEN + public const int VERASE = 3; // ICANON + public const int VWERASE = 4; // ICANON + IEXTEN + public const int VKILL = 5; // ICANON + public const int VREPRINT = 6; // ICANON + IEXTEN + // 7: spare 1 + public const int VINTR = 8; // ISIG + public const int VQUIT = 9; // ISIG + public const int VSUSP = 10; // ISIG + public const int VDSUSP = 11; // ISIG + IEXTEN + public const int VSTART = 12; // IXON, IXOFF + public const int VSTOP = 13; // IXON, IXOFF + public const int VLNEXT = 14; // IEXTEN + public const int VDISCARD = 15; // IEXTEN + public const int VMIN = 16; // !ICANON + public const int VTIME = 17; // !ICANON + public const int VSTATUS = 18; // ICANON + IEXTEN + // 19: spare 2 + public const int NCCS = 20; // size of the character array (lowest common value, Linux has more) + + + // tcflush() uses these + public static int TCIFLUSH => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 1 : 0; // flush input + public static int TCOFLUSH => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 2 : 1; // flush output + public static int TCIOFLUSH => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 3 : 2; // flush both + + + // tcflow() uses these + public static int TCOOFF => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 1 : 0; // suspend output + public static int TCOON => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 2 : 1; // restart output + public static int TCIOFF => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 3 : 2; // transmit STOP character + public static int TCION => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 4 : 3; // transmit START character + + #endregion + + + #region Public API + + public static object tcgetattr(CodeContext context, int fd) { + if (fd < 0) throw PythonOps.ValueError("file descriptor cannot be a negative integer ({0})", fd); + if (fd > 0) throw new NotImplementedException("termios support only for stdin"); + + if (context.LanguageContext.SystemStandardIn is not TextIOWrapper stdin) { + throw new NotImplementedException("termios support only for stdin"); + } + if (stdin.closed || !stdin.isatty(context) || stdin.fileno(context) != 0 || Console.IsInputRedirected) { + throw new NotImplementedException("termios support only for stdin connected to tty"); + } + + var cc = new PythonList(_specialChars.Length); + for (int i = 0; i < _specialChars.Length; i++) { + cc.Add(Bytes.FromByte(_specialChars[i])); + } + return PythonList.FromArrayNoCopy(new object[] { + _iflag, + _oflag, + _cflag, + _lflag, + _ispeed, + _ospeed, + cc + }); + } + + public static object tcgetattr(CodeContext context, object? file) { + if (!ReferenceEquals(file, context.LanguageContext.SystemStandardIn)) { + throw new NotImplementedException("termios support only for stdin"); + } + return tcgetattr(context, 0); + } + + + public static void tcsetattr(CodeContext context, int fd, int when, object? attributes) { + if (fd != 0) throw new NotImplementedException(); + + if (context.LanguageContext.SystemStandardIn is not TextIOWrapper stdin) { + throw new NotImplementedException("termios support only for stdin"); + } + if (stdin.closed || !stdin.isatty(context) || stdin.fileno(context) != 0 || Console.IsInputRedirected) { + throw new NotImplementedException("termios support only for stdin connected to tty"); + } + + if (attributes is not IList attrs || attrs.Count != 7) { + throw PythonOps.TypeError("tcsetattr, arg 3: must be 7 element list"); + } + + uint newLflag = attrs[LFlagIdx] switch { + int i => (uint)i, + uint ui => ui, + long l => (uint)l, + BigInteger bi => (uint)bi, + Extensible ebi => (uint)ebi.Value, + _ => throw PythonOps.TypeErrorForBadInstance("tcsetattr: an integer is required (got type {0})", attrs[LFlagIdx]) + }; + + if (attrs[SpecialCharsIdx] is not IList chars || chars.Count != _specialChars.Length) { + throw PythonOps.TypeError("tcsetattr, atributes[{0}] must be {1} element list", SpecialCharsIdx, _specialChars.Length); + } + + for (int i = 0; i < chars.Count; i++) { + object? o = chars[i]; + int newVal; + if (o is Bytes b && b.Count == 1) { + newVal = b[0]; + } else if (!Converter.TryConvertToInt32(o, out newVal)) { + throw PythonOps.TypeError("tcsetattr: elements of attributes must be characters or integers"); + } + if (newVal != _specialChars[i]) { + throw new NotImplementedException("tcsetattr: setting special characters is not supported"); + } + } + + if (when != TCSANOW) { + stdin.flush(context); + } + + if ((newLflag & (ECHO | ICANON | IEXTEN | ISIG)) == 0) { + setraw(context, stdin); + } else { + setcbreak(context, stdin); + } + } + + + public static void tcsetattr(CodeContext context, object? file, int when, [NotNone] object attributes) { + if (!ReferenceEquals(file, context.LanguageContext.SystemStandardIn)) { + throw new NotImplementedException("termios support only for stdin"); + } + tcsetattr(context, 0, when, attributes); + } + + #endregion + + + private const int IFlagIdx = 0; + private const int OFlagIdx = 1; + private const int CFlagIdx = 2; + private const int LFlagIdx = 3; + private const int ISpeedIdx = 4; + private const int OSpeedIdx = 5; + private const int SpecialCharsIdx = 6; + + private static int _iflag => BRKINT | ICRNL | IXON | IXANY | IMAXBEL | IUTF8; + private static int _oflag => OPOST | ONLCR; + private static int _cflag => CS8 | CREAD | HUPCL; + private static uint _lflag => ECHOKE | ECHOE | ECHOK | ECHO | ECHOCTL | ISIG | ICANON | IEXTEN | PENDIN; + private static int _ispeed = 38400; + private static int _ospeed = 38400; + + private static byte[] _specialChars = new byte[NCCS] { + (byte)0x04, // VEOF ^D + (byte)0xff, // VEOL + (byte)0xff, // VEOL2 + (byte)0x7f, // VERASE DEL + (byte)0x17, // VWERASE ^W + (byte)0x15, // VKILL ^U + (byte)0x12, // VREPRINT ^R + (byte)0x00, // reserved + (byte)0x03, // VINTR ^C + (byte)0x1c, // VQUIT ^\ + (byte)0x1a, // VSUSP ^Z + (byte)0x19, // VDSUSP ^Y + (byte)0x11, // VSTART ^Q + (byte)0x13, // VSTOP ^S + (byte)0x16, // VLNEXT ^V + (byte)0x0f, // VDISCARD ^O + (byte)0x01, // VMIN + (byte)0x00, // VTIME + (byte)0x14, // VSTATUS ^T + (byte)0x00, // reserved + }; + + private static object? _savedRawStdin; + + private static void setraw(CodeContext context, TextIOWrapper stdin) { + if (_savedRawStdin is null && stdin.buffer is BufferedReader reader) { + _savedRawStdin = reader.raw; + reader.raw = new RawConsole(context); + } + } + + private static void setcbreak(CodeContext context, TextIOWrapper stdin) { + if (_savedRawStdin is not null + && stdin.buffer is BufferedReader reader + && reader.raw is RawConsole) { + + reader.raw = _savedRawStdin; + _savedRawStdin = null; + } + } + + private class RawConsole : _RawIOBase { + public RawConsole(CodeContext context) : base(context) { + } + + public override object? read(CodeContext context, object? size=null) { + int intSize = size switch { + null => -1, + int i => i, + BigInteger bi => (int)bi, + Extensible ebi => (int)ebi.Value, + _ => throw PythonOps.TypeErrorForBadInstance("integer argument expected, got '{0}'", size) + }; + if (intSize == 0) return null; + + ConsoleKeyInfo info = Console.ReadKey(intercept: true); + return Bytes.FromByte(unchecked((byte)info.KeyChar)); + } + + public override int fileno(CodeContext context) => 0; + public override bool isatty(CodeContext context) => true; + } + + private static int ToInt(this object? o) + => o switch { + int i => i, + BigInteger bi => (int)bi, + Extensible ebi => (int)ebi.Value, + _ => throw PythonOps.TypeErrorForBadInstance("an integer is required (got type {0})", o) + }; +} diff --git a/tests/suite/modules/io_related/test_termios.py b/tests/suite/modules/io_related/test_termios.py new file mode 100644 index 000000000..53ce86f99 --- /dev/null +++ b/tests/suite/modules/io_related/test_termios.py @@ -0,0 +1,103 @@ +# Licensed to the .NET Foundation under one or more agreements. +# The .NET Foundation licenses this file to you under the Apache 2.0 License. +# See the LICENSE file in the project root for more information. + +# tests for module 'termios' (Posix only) + +import unittest + +from iptest import is_posix, is_linux, is_osx + +if is_posix: + import termios +else: + try: + import termios + except ImportError: + pass + else: + raise AssertionError("There should be no module termios on Windows") + + +@unittest.skipUnless(is_posix, "Posix-specific test") +class TermiosTest(unittest.TestCase): + + def verify_flags(self, names, values, typ=int): + for i, flag in enumerate(names): + with self.subTest(flag=flag): + self.assertTrue(hasattr(termios, flag)) + self.assertEqual(getattr(termios, flag), values[i]) + + + def test_iflags(self): + iflags = ["IGNBRK", "BRKINT", "IGNPAR", "PARMRK", "INPCK", "ISTRIP", "INLCR", "IGNCR", "ICRNL", "IXON", "IXOFF", "IXANY"] + if is_osx: + iflag_values = [0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80, 0x100, 0x200, 0x400, 0x800] + elif is_linux: + iflag_values = [0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80, 0x100, 0x400, 0x1000, 0x800] + self.verify_flags(iflags, iflag_values) + + + def test_oflags(self): + oflags = ["OPOST", "ONLCR"] + if is_osx: + oflag_values = [0x1, 0x2] + elif is_linux: + oflag_values = [0x1, 0x4] + self.verify_flags(oflags, oflag_values) + + + def test_cflags(self): + cflags = ["CSIZE", "CS5", "CS6", "CS7", "CS8", "CREAD", "PARENB", "HUPCL"] + if is_osx: + cflag_values = [0x300, 0x0, 0x100, 0x200, 0x300, 0X800, 0x1000, 0x4000] + elif is_linux: + cflag_values = [0x30, 0x0, 0x10, 0x20, 0x30, 0x80, 0x0100, 0x400] + self.verify_flags(cflags, cflag_values) + + + def test_lflags(self): + lflags = ["ECHOKE", "ECHOE", "ECHOK", "ECHO", "ECHONL", "ECHOPRT", "ECHOCTL", "ISIG", "ICANON", "IEXTEN", "TOSTOP", "FLUSHO", "PENDIN", "NOFLSH"] + if is_osx: + lflag_values = [0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80, 0x100, 0x400, 0x400000, 0x800000, 0x20000000, 0x80000000] + elif is_linux: + lflag_values = [0x800, 0x10, 0x20, 0x8, 0x40, 0x400, 0x200, 0x1, 0x2, 0x8000, 0x100, 0x1000, 0x2000, 0x80] + self.verify_flags(lflags, lflag_values) + + + def test_when_enum(self): + when = ["TCSANOW", "TCSADRAIN", "TCSAFLUSH"] + when_values = [0, 1, 2] + self.verify_flags(when, when_values) + + + def test_tcflush_queue(self): + queue = ["TCIFLUSH", "TCOFLUSH", "TCIOFLUSH"] + if is_osx: + queue_values = [1, 2, 3] + elif is_linux: + queue_values = [0, 1, 2] + self.verify_flags(queue, queue_values) + + + def test_tcflow_action(self): + action = ["TCOOFF", "TCOON", "TCIOFF", "TCION"] + if is_osx: + action_values = [1, 2, 3, 4] + elif is_linux: + action_values = [0, 1, 2, 3] + self.verify_flags(action, action_values) + + + def test_cc(self): + if is_osx: + cc = ["VEOF", "VEOL", "VEOL2", "VERASE", "VWERASE", "VKILL", "VREPRINT", "VINTR", "VQUIT", "VSUSP", "VSTART", "VSTOP", "VLNEXT", "VDISCARD", "VMIN", "VTIME", "NCCS"] + cc_values = [0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 13, 14, 15, 16, 17, 20] + elif is_linux: + cc = ["VEOF", "VEOL", "VEOL2", "VERASE", "VWERASE", "VKILL", "VREPRINT", "VINTR", "VQUIT", "VSUSP", "VDSUSP", "VSTART", "VSTOP", "VLNEXT", "VDISCARD", "VMIN", "VTIME", "NCCS"] + cc_values = [0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 20] + self.verify_flags(cc, cc_values) + + +if __name__ == "__main__": + unittest.main(verbosity=2) From f5337353e02e53a516e37a537f04a7e915498b0e Mon Sep 17 00:00:00 2001 From: Pavel Koneski Date: Mon, 20 Jan 2025 05:35:30 +0000 Subject: [PATCH 2/4] termios on Linux --- src/core/IronPython.Modules/termios.cs | 110 ++++++++++++------ .../suite/modules/io_related/test_termios.py | 9 +- 2 files changed, 77 insertions(+), 42 deletions(-) diff --git a/src/core/IronPython.Modules/termios.cs b/src/core/IronPython.Modules/termios.cs index 43a0e5382..4aa911025 100644 --- a/src/core/IronPython.Modules/termios.cs +++ b/src/core/IronPython.Modules/termios.cs @@ -17,6 +17,7 @@ using static IronPython.Modules.PythonIOModule; + [assembly: PythonModule("termios", typeof(IronPython.Modules.PythonTermios), PlatformsAttribute.PlatformFamily.Unix)] namespace IronPython.Modules; @@ -73,6 +74,11 @@ public static class PythonTermios { public static int PARENB => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x1000 : 0x0100; // parity enable public static int HUPCL => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x4000 : 0x0400; // hang up on last close + [PythonHidden(PlatformID.MacOSX)] + public static int CBAUD => 0x100f; // mask for baud rate + [PythonHidden(PlatformID.MacOSX)] + public static int CBAUDEX => 0x1000; // extra baud speed mask + // lflag public static uint ECHOKE => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0001u : 0x0800u; // visual erase for line kill @@ -87,7 +93,7 @@ public static class PythonTermios { public static uint IEXTEN => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0400u : 0x8000u; // enable extended input character processing public static uint TOSTOP => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0040_0000u : 0x0100u; // stop background jobs from output public static uint FLUSHO => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0080_0000u : 0x1000u; // output being flushed - public static uint PENDIN => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x2000_0000u : 0x2000u; // XXX retype pending input + public static uint PENDIN => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x2000_0000u : 0x4000u; // retype pending input public static uint NOFLSH => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x8000_0000u : 0x0080u; // don't flush after interrupt @@ -97,28 +103,26 @@ public static class PythonTermios { public const int TCSAFLUSH = 2; // discard input, flush output, then change - // control characters - enabled by: - public const int VEOF = 0; // ICANON - public const int VEOL = 1; // ICANON - public const int VEOL2 = 2; // ICANON + IEXTEN - public const int VERASE = 3; // ICANON - public const int VWERASE = 4; // ICANON + IEXTEN - public const int VKILL = 5; // ICANON - public const int VREPRINT = 6; // ICANON + IEXTEN - // 7: spare 1 - public const int VINTR = 8; // ISIG - public const int VQUIT = 9; // ISIG - public const int VSUSP = 10; // ISIG - public const int VDSUSP = 11; // ISIG + IEXTEN - public const int VSTART = 12; // IXON, IXOFF - public const int VSTOP = 13; // IXON, IXOFF - public const int VLNEXT = 14; // IEXTEN - public const int VDISCARD = 15; // IEXTEN - public const int VMIN = 16; // !ICANON - public const int VTIME = 17; // !ICANON - public const int VSTATUS = 18; // ICANON + IEXTEN - // 19: spare 2 - public const int NCCS = 20; // size of the character array (lowest common value, Linux has more) + // control characters + public static int VEOF => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0 : 4; + public static int VEOL => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 1 : 11; + public static int VEOL2 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 2 : 16; + public static int VERASE => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 3 : 2; + public static int VWERASE => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 4 : 14; + public static int VKILL => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 5 : 3; + public static int VREPRINT => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 6 : 12; + public static int VINTR => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 8 : 0; + public static int VQUIT => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 9 : 1; + public static int VSUSP => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 10 : 10; + public static int VSTART => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 12 : 8; + public static int VSTOP => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 13 : 9; + public static int VLNEXT => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 14 : 15; + public static int VDISCARD => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 15 : 13; + public static int VMIN => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 16 : 6; + public static int VTIME => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 17 : 5; + [PythonHidden(PlatformID.MacOSX)] + public static int VSWTC => 7; + public static int NCCS => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 20 : 32; // tcflush() uses these @@ -149,11 +153,13 @@ public static object tcgetattr(CodeContext context, int fd) { throw new NotImplementedException("termios support only for stdin connected to tty"); } - var cc = new PythonList(_specialChars.Length); - for (int i = 0; i < _specialChars.Length; i++) { - cc.Add(Bytes.FromByte(_specialChars[i])); + var cc = new PythonList(NCCS); + var specialChars = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? macos__specialChars : linux__specialChars; + for (int i = 0; i < NCCS; i++) { + byte c = i < specialChars.Length ? specialChars[i] : (byte)0; + cc.Add(Bytes.FromByte(c)); } - return PythonList.FromArrayNoCopy(new object[] { + return PythonList.FromArrayNoCopy([ _iflag, _oflag, _cflag, @@ -161,7 +167,7 @@ public static object tcgetattr(CodeContext context, int fd) { _ispeed, _ospeed, cc - }); + ]); } public static object tcgetattr(CodeContext context, object? file) { @@ -195,10 +201,11 @@ public static void tcsetattr(CodeContext context, int fd, int when, object? attr _ => throw PythonOps.TypeErrorForBadInstance("tcsetattr: an integer is required (got type {0})", attrs[LFlagIdx]) }; - if (attrs[SpecialCharsIdx] is not IList chars || chars.Count != _specialChars.Length) { - throw PythonOps.TypeError("tcsetattr, atributes[{0}] must be {1} element list", SpecialCharsIdx, _specialChars.Length); + if (attrs[SpecialCharsIdx] is not IList chars || chars.Count != NCCS) { + throw PythonOps.TypeError("tcsetattr, atributes[{0}] must be {1} element list", SpecialCharsIdx, NCCS); } + var specialChars = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? macos__specialChars : linux__specialChars; for (int i = 0; i < chars.Count; i++) { object? o = chars[i]; int newVal; @@ -207,7 +214,8 @@ public static void tcsetattr(CodeContext context, int fd, int when, object? attr } else if (!Converter.TryConvertToInt32(o, out newVal)) { throw PythonOps.TypeError("tcsetattr: elements of attributes must be characters or integers"); } - if (newVal != _specialChars[i]) { + int expected = i < specialChars.Length ? specialChars[i] : 0; + if (newVal != expected) { throw new NotImplementedException("tcsetattr: setting special characters is not supported"); } } @@ -244,12 +252,16 @@ public static void tcsetattr(CodeContext context, object? file, int when, [NotNo private static int _iflag => BRKINT | ICRNL | IXON | IXANY | IMAXBEL | IUTF8; private static int _oflag => OPOST | ONLCR; - private static int _cflag => CS8 | CREAD | HUPCL; - private static uint _lflag => ECHOKE | ECHOE | ECHOK | ECHO | ECHOCTL | ISIG | ICANON | IEXTEN | PENDIN; - private static int _ispeed = 38400; - private static int _ospeed = 38400; - - private static byte[] _specialChars = new byte[NCCS] { + private static int _cflag => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? + CS8 | CREAD | HUPCL + : CS8 | CREAD | HUPCL | (CBAUD & ~CBAUDEX); + private static uint _lflag => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? + ECHOKE | ECHOE | ECHOK | ECHO | ECHOCTL | ISIG | ICANON | IEXTEN | PENDIN + : ECHOKE | ECHOE | ECHOK | ECHO | ECHOCTL | ISIG | ICANON | IEXTEN; + private static int _ispeed => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 38400 : /* B38400 */ 15; + private static int _ospeed => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 38400 : /* B38400 */ 15; + + private static readonly byte[] macos__specialChars = [ (byte)0x04, // VEOF ^D (byte)0xff, // VEOL (byte)0xff, // VEOL2 @@ -270,7 +282,29 @@ public static void tcsetattr(CodeContext context, object? file, int when, [NotNo (byte)0x00, // VTIME (byte)0x14, // VSTATUS ^T (byte)0x00, // reserved - }; + ]; + + private static readonly byte[] linux__specialChars = [ + (byte)0x03, // VINTR ^C + (byte)0x1c, // VQUIT ^\ + (byte)0x7f, // VERASE DEL + (byte)0x15, // VKILL ^U + (byte)0x04, // VEOF ^D + (byte)0x00, // VTIME + (byte)0x01, // VMIN + (byte)0x00, // VSWTC + (byte)0x11, // VSTART ^Q + (byte)0x13, // VSTOP ^S + (byte)0x1a, // VSUSP ^Z + (byte)0xff, // VEOL + (byte)0x12, // VREPRINT ^R + (byte)0x0f, // VDISCARD ^O + (byte)0x17, // VWERASE ^W + (byte)0x16, // VLNEXT ^V + (byte)0xff, // VEOL2 + // rest are reserved + ]; + private static object? _savedRawStdin; diff --git a/tests/suite/modules/io_related/test_termios.py b/tests/suite/modules/io_related/test_termios.py index 53ce86f99..0504fea66 100644 --- a/tests/suite/modules/io_related/test_termios.py +++ b/tests/suite/modules/io_related/test_termios.py @@ -52,7 +52,8 @@ def test_cflags(self): if is_osx: cflag_values = [0x300, 0x0, 0x100, 0x200, 0x300, 0X800, 0x1000, 0x4000] elif is_linux: - cflag_values = [0x30, 0x0, 0x10, 0x20, 0x30, 0x80, 0x0100, 0x400] + cflags += ["CBAUD", "CBAUDEX"] + cflag_values = [0x30, 0x0, 0x10, 0x20, 0x30, 0x80, 0x0100, 0x400, 0x100f, 0x1000] self.verify_flags(cflags, cflag_values) @@ -61,7 +62,7 @@ def test_lflags(self): if is_osx: lflag_values = [0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80, 0x100, 0x400, 0x400000, 0x800000, 0x20000000, 0x80000000] elif is_linux: - lflag_values = [0x800, 0x10, 0x20, 0x8, 0x40, 0x400, 0x200, 0x1, 0x2, 0x8000, 0x100, 0x1000, 0x2000, 0x80] + lflag_values = [0x800, 0x10, 0x20, 0x8, 0x40, 0x400, 0x200, 0x1, 0x2, 0x8000, 0x100, 0x1000, 0x4000, 0x80] self.verify_flags(lflags, lflag_values) @@ -94,8 +95,8 @@ def test_cc(self): cc = ["VEOF", "VEOL", "VEOL2", "VERASE", "VWERASE", "VKILL", "VREPRINT", "VINTR", "VQUIT", "VSUSP", "VSTART", "VSTOP", "VLNEXT", "VDISCARD", "VMIN", "VTIME", "NCCS"] cc_values = [0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 13, 14, 15, 16, 17, 20] elif is_linux: - cc = ["VEOF", "VEOL", "VEOL2", "VERASE", "VWERASE", "VKILL", "VREPRINT", "VINTR", "VQUIT", "VSUSP", "VDSUSP", "VSTART", "VSTOP", "VLNEXT", "VDISCARD", "VMIN", "VTIME", "NCCS"] - cc_values = [0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 20] + cc = ["VEOF", "VEOL", "VEOL2", "VERASE", "VWERASE", "VKILL", "VREPRINT", "VINTR", "VQUIT", "VSUSP", "VSTART", "VSTOP", "VLNEXT", "VDISCARD", "VMIN", "VTIME", "VSWTC", "NCCS"] + cc_values = [4, 11, 16, 2, 14, 3, 12, 0, 1, 10, 8, 9, 15, 13, 6, 5, 7, 32] self.verify_flags(cc, cc_values) From bc1f309f035bb8bd4f1be9d951edbfe16eb7e8ad Mon Sep 17 00:00:00 2001 From: Pavel Koneski Date: Mon, 20 Jan 2025 11:00:21 -0800 Subject: [PATCH 3/4] termios baud rates --- src/core/IronPython.Modules/termios.cs | 31 ++++++++++++++----- .../suite/modules/io_related/test_termios.py | 8 +++++ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/core/IronPython.Modules/termios.cs b/src/core/IronPython.Modules/termios.cs index 4aa911025..ec34059cb 100644 --- a/src/core/IronPython.Modules/termios.cs +++ b/src/core/IronPython.Modules/termios.cs @@ -30,7 +30,7 @@ public static class PythonTermios { #region termios IO Control Codes (TIOC*) - public static int TIOCGWINSZ => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 1074295912 : 21523; + public static int TIOCGWINSZ => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x40087468 : 0x5413; #endregion @@ -137,10 +137,27 @@ public static class PythonTermios { public static int TCIOFF => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 3 : 2; // transmit STOP character public static int TCION => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 4 : 3; // transmit START character - #endregion - - - #region Public API + // baud rates + public static int B0 => 0; + public static int B50 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 50 : 1; + public static int B75 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 75 : 2; + public static int B110 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 110 : 3; + public static int B134 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 134 : 4; + public static int B150 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 150 : 5; + public static int B200 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 200 : 6; + public static int B300 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 300 : 7; + public static int B600 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 600 : 8; + public static int B1200 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 1200 : 9; + public static int B1800 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 1800 : 10; + public static int B2400 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 2400 : 11; + public static int B4800 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 4800 : 12; + public static int B9600 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 9600 : 13; + public static int B19200 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 19200 : 14; + public static int B38400 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 38400 : 15; + public static int B57600 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 57600 : 0x1001; + public static int B115200 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 115200 : 0x1002; + public static int B230400 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 230400 : 0x1003; + // higher baud rates are not defined on macOS public static object tcgetattr(CodeContext context, int fd) { if (fd < 0) throw PythonOps.ValueError("file descriptor cannot be a negative integer ({0})", fd); @@ -258,8 +275,8 @@ public static void tcsetattr(CodeContext context, object? file, int when, [NotNo private static uint _lflag => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? ECHOKE | ECHOE | ECHOK | ECHO | ECHOCTL | ISIG | ICANON | IEXTEN | PENDIN : ECHOKE | ECHOE | ECHOK | ECHO | ECHOCTL | ISIG | ICANON | IEXTEN; - private static int _ispeed => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 38400 : /* B38400 */ 15; - private static int _ospeed => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 38400 : /* B38400 */ 15; + private static int _ispeed => B38400; + private static int _ospeed => B38400; private static readonly byte[] macos__specialChars = [ (byte)0x04, // VEOF ^D diff --git a/tests/suite/modules/io_related/test_termios.py b/tests/suite/modules/io_related/test_termios.py index 0504fea66..247b87bd6 100644 --- a/tests/suite/modules/io_related/test_termios.py +++ b/tests/suite/modules/io_related/test_termios.py @@ -99,6 +99,14 @@ def test_cc(self): cc_values = [4, 11, 16, 2, 14, 3, 12, 0, 1, 10, 8, 9, 15, 13, 6, 5, 7, 32] self.verify_flags(cc, cc_values) + def test_baud_rates(self): + rates = ["B0", "B50", "B75", "B110", "B134", "B150", "B200", "B300", "B600", "B1200", "B1800", "B2400", "B4800", "B9600", "B19200", "B38400", "B57600", "B115200", "B230400"] + if is_osx: + rates_values = [0, 50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400] + elif is_linux: + rates_values = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 4097, 4098, 4099] + self.verify_flags(rates, rates_values) + if __name__ == "__main__": unittest.main(verbosity=2) From eacfcd9fca74f2d63a60421e0ce90b4bbaeda6e0 Mon Sep 17 00:00:00 2001 From: Pavel Koneski Date: Mon, 20 Jan 2025 20:39:08 -0800 Subject: [PATCH 4/4] Implement termios.error --- src/core/IronPython.Modules/termios.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/core/IronPython.Modules/termios.cs b/src/core/IronPython.Modules/termios.cs index ec34059cb..5a4e9b913 100644 --- a/src/core/IronPython.Modules/termios.cs +++ b/src/core/IronPython.Modules/termios.cs @@ -7,6 +7,7 @@ using System; using System.Collections; using System.Numerics; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; @@ -28,6 +29,13 @@ public static class PythonTermios { public const string __doc__ = "Stub of termios, just enough to support module tty."; // and also prompt_toolkit.terminal.vt100_input +#pragma warning disable IPY01 // Parameter which is marked not nullable does not have the NotNullAttribute + [SpecialName] + public static void PerformModuleReload(PythonContext context, PythonDictionary dict) + => context.EnsureModuleException("termioserror", dict, "error", "termios"); +#pragma warning restore IPY01 // Parameter which is marked not nullable does not have the NotNullAttribute + + #region termios IO Control Codes (TIOC*) public static int TIOCGWINSZ => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x40087468 : 0x5413; @@ -35,6 +43,7 @@ public static class PythonTermios { #endregion + #region Other Public Constants // Linux: glibc/bits/termios.h (/usr/include/{x86_64,aarch64}-linux-gnu/) // macOS: usr/include/sys/termios.h (/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk)