Skip to content

Commit 0e4b0af

Browse files
authored
Implement rudimentary version of module termios (#1879)
* Implement termios mock * termios on Linux * termios baud rates * Implement termios.error
1 parent 49e18cb commit 0e4b0af

File tree

2 files changed

+495
-0
lines changed

2 files changed

+495
-0
lines changed
Lines changed: 383 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,383 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
#nullable enable
6+
7+
using System;
8+
using System.Collections;
9+
using System.Numerics;
10+
using System.Runtime.CompilerServices;
11+
using System.Runtime.InteropServices;
12+
using System.Runtime.Versioning;
13+
14+
using Microsoft.Scripting.Runtime;
15+
16+
using IronPython.Runtime;
17+
using IronPython.Runtime.Operations;
18+
19+
using static IronPython.Modules.PythonIOModule;
20+
21+
22+
[assembly: PythonModule("termios", typeof(IronPython.Modules.PythonTermios), PlatformsAttribute.PlatformFamily.Unix)]
23+
namespace IronPython.Modules;
24+
25+
[SupportedOSPlatform("linux")]
26+
[SupportedOSPlatform("macos")]
27+
public static class PythonTermios {
28+
29+
public const string __doc__ = "Stub of termios, just enough to support module tty.";
30+
// and also prompt_toolkit.terminal.vt100_input
31+
32+
#pragma warning disable IPY01 // Parameter which is marked not nullable does not have the NotNullAttribute
33+
[SpecialName]
34+
public static void PerformModuleReload(PythonContext context, PythonDictionary dict)
35+
=> context.EnsureModuleException("termioserror", dict, "error", "termios");
36+
#pragma warning restore IPY01 // Parameter which is marked not nullable does not have the NotNullAttribute
37+
38+
39+
#region termios IO Control Codes (TIOC*)
40+
41+
public static int TIOCGWINSZ => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x40087468 : 0x5413;
42+
43+
44+
#endregion
45+
46+
47+
#region Other Public Constants
48+
// Linux: glibc/bits/termios.h (/usr/include/{x86_64,aarch64}-linux-gnu/)
49+
// macOS: usr/include/sys/termios.h (/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk)
50+
51+
// iflag
52+
public const int IGNBRK = 0x0001; // ignore break condition
53+
public const int BRKINT = 0x0002; // signal interrupt on break
54+
public const int IGNPAR = 0x0004; // ignore characters with parity errors
55+
public const int PARMRK = 0x0008; // mark parity and framing errors
56+
public const int INPCK = 0x0010; // enable input parity check
57+
public const int ISTRIP = 0x0020; // mask off 8th bit
58+
public const int INLCR = 0x0040; // map NL into CR on input
59+
public const int IGNCR = 0x0080; // ignore CR
60+
public const int ICRNL = 0x0100; // map CR to NL on input
61+
public static int IXON => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ?
62+
0x0200 : 0x0400; // enable start output control
63+
public static int IXOFF => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ?
64+
0x0400 : 0x1000; // enable stop output control
65+
public const int IXANY = 0x0800; // any char will restart after stop
66+
public const int IMAXBEL = 0x2000; // ring bell on input queue full
67+
public const int IUTF8 = 0x4000; // maintain state for UTF-8 VERASE
68+
69+
70+
// oflag
71+
public const int OPOST = 0x0001; // enable output processing
72+
public static int ONLCR => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ?
73+
0x0002 : 0x0004; // map NL to CR-NL
74+
75+
76+
// cflag
77+
public static int CSIZE => CS5 | CS6 | CS7 | CS8; // number of bits per character
78+
public static int CS5 => 0x0000; // 5 bits per character
79+
public static int CS6 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0100 : 0x0010; // 6 bits per character
80+
public static int CS7 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0200 : 0x0020; // 7 bits per character
81+
public static int CS8 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0300 : 0x0030; // 8 bits per character
82+
public static int CREAD => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0800 : 0x0080; // enable receiver
83+
public static int PARENB => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x1000 : 0x0100; // parity enable
84+
public static int HUPCL => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x4000 : 0x0400; // hang up on last close
85+
86+
[PythonHidden(PlatformID.MacOSX)]
87+
public static int CBAUD => 0x100f; // mask for baud rate
88+
[PythonHidden(PlatformID.MacOSX)]
89+
public static int CBAUDEX => 0x1000; // extra baud speed mask
90+
91+
92+
// lflag
93+
public static uint ECHOKE => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0001u : 0x0800u; // visual erase for line kill
94+
public static uint ECHOE => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0002u : 0x0010u; // visually erase chars
95+
public static uint ECHOK => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0004u : 0x0020u; // echo NL after line kill
96+
public static uint ECHO => 0x0008u; // enable echoing of input characters
97+
public static uint ECHONL => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0010u : 0x0040u; // echo NL even if ECHO is off
98+
public static uint ECHOPRT => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0020u : 0x0400u; // visual erase mode for hardcopy
99+
public static uint ECHOCTL => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0040u : 0x0200u; // echo control characters as ^(Char)
100+
public static uint ISIG => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0080u : 0x0001u; // enable signals
101+
public static uint ICANON => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0100u : 0x0002u; // canonical mode
102+
public static uint IEXTEN => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0400u : 0x8000u; // enable extended input character processing
103+
public static uint TOSTOP => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0040_0000u : 0x0100u; // stop background jobs from output
104+
public static uint FLUSHO => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x0080_0000u : 0x1000u; // output being flushed
105+
public static uint PENDIN => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x2000_0000u : 0x4000u; // retype pending input
106+
public static uint NOFLSH => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0x8000_0000u : 0x0080u; // don't flush after interrupt
107+
108+
109+
// when the changes take effect:
110+
public const int TCSANOW = 0; // change immediately
111+
public const int TCSADRAIN = 1; // flush output, then change
112+
public const int TCSAFLUSH = 2; // discard input, flush output, then change
113+
114+
115+
// control characters
116+
public static int VEOF => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 0 : 4;
117+
public static int VEOL => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 1 : 11;
118+
public static int VEOL2 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 2 : 16;
119+
public static int VERASE => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 3 : 2;
120+
public static int VWERASE => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 4 : 14;
121+
public static int VKILL => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 5 : 3;
122+
public static int VREPRINT => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 6 : 12;
123+
public static int VINTR => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 8 : 0;
124+
public static int VQUIT => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 9 : 1;
125+
public static int VSUSP => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 10 : 10;
126+
public static int VSTART => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 12 : 8;
127+
public static int VSTOP => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 13 : 9;
128+
public static int VLNEXT => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 14 : 15;
129+
public static int VDISCARD => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 15 : 13;
130+
public static int VMIN => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 16 : 6;
131+
public static int VTIME => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 17 : 5;
132+
[PythonHidden(PlatformID.MacOSX)]
133+
public static int VSWTC => 7;
134+
public static int NCCS => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 20 : 32;
135+
136+
137+
// tcflush() uses these
138+
public static int TCIFLUSH => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 1 : 0; // flush input
139+
public static int TCOFLUSH => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 2 : 1; // flush output
140+
public static int TCIOFLUSH => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 3 : 2; // flush both
141+
142+
143+
// tcflow() uses these
144+
public static int TCOOFF => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 1 : 0; // suspend output
145+
public static int TCOON => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 2 : 1; // restart output
146+
public static int TCIOFF => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 3 : 2; // transmit STOP character
147+
public static int TCION => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 4 : 3; // transmit START character
148+
149+
// baud rates
150+
public static int B0 => 0;
151+
public static int B50 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 50 : 1;
152+
public static int B75 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 75 : 2;
153+
public static int B110 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 110 : 3;
154+
public static int B134 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 134 : 4;
155+
public static int B150 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 150 : 5;
156+
public static int B200 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 200 : 6;
157+
public static int B300 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 300 : 7;
158+
public static int B600 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 600 : 8;
159+
public static int B1200 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 1200 : 9;
160+
public static int B1800 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 1800 : 10;
161+
public static int B2400 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 2400 : 11;
162+
public static int B4800 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 4800 : 12;
163+
public static int B9600 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 9600 : 13;
164+
public static int B19200 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 19200 : 14;
165+
public static int B38400 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 38400 : 15;
166+
public static int B57600 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 57600 : 0x1001;
167+
public static int B115200 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 115200 : 0x1002;
168+
public static int B230400 => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 230400 : 0x1003;
169+
// higher baud rates are not defined on macOS
170+
171+
public static object tcgetattr(CodeContext context, int fd) {
172+
if (fd < 0) throw PythonOps.ValueError("file descriptor cannot be a negative integer ({0})", fd);
173+
if (fd > 0) throw new NotImplementedException("termios support only for stdin");
174+
175+
if (context.LanguageContext.SystemStandardIn is not TextIOWrapper stdin) {
176+
throw new NotImplementedException("termios support only for stdin");
177+
}
178+
if (stdin.closed || !stdin.isatty(context) || stdin.fileno(context) != 0 || Console.IsInputRedirected) {
179+
throw new NotImplementedException("termios support only for stdin connected to tty");
180+
}
181+
182+
var cc = new PythonList(NCCS);
183+
var specialChars = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? macos__specialChars : linux__specialChars;
184+
for (int i = 0; i < NCCS; i++) {
185+
byte c = i < specialChars.Length ? specialChars[i] : (byte)0;
186+
cc.Add(Bytes.FromByte(c));
187+
}
188+
return PythonList.FromArrayNoCopy([
189+
_iflag,
190+
_oflag,
191+
_cflag,
192+
_lflag,
193+
_ispeed,
194+
_ospeed,
195+
cc
196+
]);
197+
}
198+
199+
public static object tcgetattr(CodeContext context, object? file) {
200+
if (!ReferenceEquals(file, context.LanguageContext.SystemStandardIn)) {
201+
throw new NotImplementedException("termios support only for stdin");
202+
}
203+
return tcgetattr(context, 0);
204+
}
205+
206+
207+
public static void tcsetattr(CodeContext context, int fd, int when, object? attributes) {
208+
if (fd != 0) throw new NotImplementedException();
209+
210+
if (context.LanguageContext.SystemStandardIn is not TextIOWrapper stdin) {
211+
throw new NotImplementedException("termios support only for stdin");
212+
}
213+
if (stdin.closed || !stdin.isatty(context) || stdin.fileno(context) != 0 || Console.IsInputRedirected) {
214+
throw new NotImplementedException("termios support only for stdin connected to tty");
215+
}
216+
217+
if (attributes is not IList attrs || attrs.Count != 7) {
218+
throw PythonOps.TypeError("tcsetattr, arg 3: must be 7 element list");
219+
}
220+
221+
uint newLflag = attrs[LFlagIdx] switch {
222+
int i => (uint)i,
223+
uint ui => ui,
224+
long l => (uint)l,
225+
BigInteger bi => (uint)bi,
226+
Extensible<BigInteger> ebi => (uint)ebi.Value,
227+
_ => throw PythonOps.TypeErrorForBadInstance("tcsetattr: an integer is required (got type {0})", attrs[LFlagIdx])
228+
};
229+
230+
if (attrs[SpecialCharsIdx] is not IList chars || chars.Count != NCCS) {
231+
throw PythonOps.TypeError("tcsetattr, atributes[{0}] must be {1} element list", SpecialCharsIdx, NCCS);
232+
}
233+
234+
var specialChars = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? macos__specialChars : linux__specialChars;
235+
for (int i = 0; i < chars.Count; i++) {
236+
object? o = chars[i];
237+
int newVal;
238+
if (o is Bytes b && b.Count == 1) {
239+
newVal = b[0];
240+
} else if (!Converter.TryConvertToInt32(o, out newVal)) {
241+
throw PythonOps.TypeError("tcsetattr: elements of attributes must be characters or integers");
242+
}
243+
int expected = i < specialChars.Length ? specialChars[i] : 0;
244+
if (newVal != expected) {
245+
throw new NotImplementedException("tcsetattr: setting special characters is not supported");
246+
}
247+
}
248+
249+
if (when != TCSANOW) {
250+
stdin.flush(context);
251+
}
252+
253+
if ((newLflag & (ECHO | ICANON | IEXTEN | ISIG)) == 0) {
254+
setraw(context, stdin);
255+
} else {
256+
setcbreak(context, stdin);
257+
}
258+
}
259+
260+
261+
public static void tcsetattr(CodeContext context, object? file, int when, [NotNone] object attributes) {
262+
if (!ReferenceEquals(file, context.LanguageContext.SystemStandardIn)) {
263+
throw new NotImplementedException("termios support only for stdin");
264+
}
265+
tcsetattr(context, 0, when, attributes);
266+
}
267+
268+
#endregion
269+
270+
271+
private const int IFlagIdx = 0;
272+
private const int OFlagIdx = 1;
273+
private const int CFlagIdx = 2;
274+
private const int LFlagIdx = 3;
275+
private const int ISpeedIdx = 4;
276+
private const int OSpeedIdx = 5;
277+
private const int SpecialCharsIdx = 6;
278+
279+
private static int _iflag => BRKINT | ICRNL | IXON | IXANY | IMAXBEL | IUTF8;
280+
private static int _oflag => OPOST | ONLCR;
281+
private static int _cflag => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ?
282+
CS8 | CREAD | HUPCL
283+
: CS8 | CREAD | HUPCL | (CBAUD & ~CBAUDEX);
284+
private static uint _lflag => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ?
285+
ECHOKE | ECHOE | ECHOK | ECHO | ECHOCTL | ISIG | ICANON | IEXTEN | PENDIN
286+
: ECHOKE | ECHOE | ECHOK | ECHO | ECHOCTL | ISIG | ICANON | IEXTEN;
287+
private static int _ispeed => B38400;
288+
private static int _ospeed => B38400;
289+
290+
private static readonly byte[] macos__specialChars = [
291+
(byte)0x04, // VEOF ^D
292+
(byte)0xff, // VEOL
293+
(byte)0xff, // VEOL2
294+
(byte)0x7f, // VERASE DEL
295+
(byte)0x17, // VWERASE ^W
296+
(byte)0x15, // VKILL ^U
297+
(byte)0x12, // VREPRINT ^R
298+
(byte)0x00, // reserved
299+
(byte)0x03, // VINTR ^C
300+
(byte)0x1c, // VQUIT ^\
301+
(byte)0x1a, // VSUSP ^Z
302+
(byte)0x19, // VDSUSP ^Y
303+
(byte)0x11, // VSTART ^Q
304+
(byte)0x13, // VSTOP ^S
305+
(byte)0x16, // VLNEXT ^V
306+
(byte)0x0f, // VDISCARD ^O
307+
(byte)0x01, // VMIN
308+
(byte)0x00, // VTIME
309+
(byte)0x14, // VSTATUS ^T
310+
(byte)0x00, // reserved
311+
];
312+
313+
private static readonly byte[] linux__specialChars = [
314+
(byte)0x03, // VINTR ^C
315+
(byte)0x1c, // VQUIT ^\
316+
(byte)0x7f, // VERASE DEL
317+
(byte)0x15, // VKILL ^U
318+
(byte)0x04, // VEOF ^D
319+
(byte)0x00, // VTIME
320+
(byte)0x01, // VMIN
321+
(byte)0x00, // VSWTC
322+
(byte)0x11, // VSTART ^Q
323+
(byte)0x13, // VSTOP ^S
324+
(byte)0x1a, // VSUSP ^Z
325+
(byte)0xff, // VEOL
326+
(byte)0x12, // VREPRINT ^R
327+
(byte)0x0f, // VDISCARD ^O
328+
(byte)0x17, // VWERASE ^W
329+
(byte)0x16, // VLNEXT ^V
330+
(byte)0xff, // VEOL2
331+
// rest are reserved
332+
];
333+
334+
335+
private static object? _savedRawStdin;
336+
337+
private static void setraw(CodeContext context, TextIOWrapper stdin) {
338+
if (_savedRawStdin is null && stdin.buffer is BufferedReader reader) {
339+
_savedRawStdin = reader.raw;
340+
reader.raw = new RawConsole(context);
341+
}
342+
}
343+
344+
private static void setcbreak(CodeContext context, TextIOWrapper stdin) {
345+
if (_savedRawStdin is not null
346+
&& stdin.buffer is BufferedReader reader
347+
&& reader.raw is RawConsole) {
348+
349+
reader.raw = _savedRawStdin;
350+
_savedRawStdin = null;
351+
}
352+
}
353+
354+
private class RawConsole : _RawIOBase {
355+
public RawConsole(CodeContext context) : base(context) {
356+
}
357+
358+
public override object? read(CodeContext context, object? size=null) {
359+
int intSize = size switch {
360+
null => -1,
361+
int i => i,
362+
BigInteger bi => (int)bi,
363+
Extensible<BigInteger> ebi => (int)ebi.Value,
364+
_ => throw PythonOps.TypeErrorForBadInstance("integer argument expected, got '{0}'", size)
365+
};
366+
if (intSize == 0) return null;
367+
368+
ConsoleKeyInfo info = Console.ReadKey(intercept: true);
369+
return Bytes.FromByte(unchecked((byte)info.KeyChar));
370+
}
371+
372+
public override int fileno(CodeContext context) => 0;
373+
public override bool isatty(CodeContext context) => true;
374+
}
375+
376+
private static int ToInt(this object? o)
377+
=> o switch {
378+
int i => i,
379+
BigInteger bi => (int)bi,
380+
Extensible<BigInteger> ebi => (int)ebi.Value,
381+
_ => throw PythonOps.TypeErrorForBadInstance("an integer is required (got type {0})", o)
382+
};
383+
}

0 commit comments

Comments
 (0)