Skip to content

Commit bf28152

Browse files
arihant2mathyouknowone
authored andcommitted
add os support modules
1 parent bae0ad3 commit bf28152

File tree

3 files changed

+318
-0
lines changed

3 files changed

+318
-0
lines changed

Lib/_android_support.py

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import io
2+
import sys
3+
from threading import RLock
4+
from time import sleep, time
5+
6+
# The maximum length of a log message in bytes, including the level marker and
7+
# tag, is defined as LOGGER_ENTRY_MAX_PAYLOAD at
8+
# https://cs.android.com/android/platform/superproject/+/android-14.0.0_r1:system/logging/liblog/include/log/log.h;l=71.
9+
# Messages longer than this will be truncated by logcat. This limit has already
10+
# been reduced at least once in the history of Android (from 4076 to 4068 between
11+
# API level 23 and 26), so leave some headroom.
12+
MAX_BYTES_PER_WRITE = 4000
13+
14+
# UTF-8 uses a maximum of 4 bytes per character, so limiting text writes to this
15+
# size ensures that we can always avoid exceeding MAX_BYTES_PER_WRITE.
16+
# However, if the actual number of bytes per character is smaller than that,
17+
# then we may still join multiple consecutive text writes into binary
18+
# writes containing a larger number of characters.
19+
MAX_CHARS_PER_WRITE = MAX_BYTES_PER_WRITE // 4
20+
21+
22+
# When embedded in an app on current versions of Android, there's no easy way to
23+
# monitor the C-level stdout and stderr. The testbed comes with a .c file to
24+
# redirect them to the system log using a pipe, but that wouldn't be convenient
25+
# or appropriate for all apps. So we redirect at the Python level instead.
26+
def init_streams(android_log_write, stdout_prio, stderr_prio):
27+
if sys.executable:
28+
return # Not embedded in an app.
29+
30+
global logcat
31+
logcat = Logcat(android_log_write)
32+
33+
sys.stdout = TextLogStream(
34+
stdout_prio, "python.stdout", sys.stdout.fileno())
35+
sys.stderr = TextLogStream(
36+
stderr_prio, "python.stderr", sys.stderr.fileno())
37+
38+
39+
class TextLogStream(io.TextIOWrapper):
40+
def __init__(self, prio, tag, fileno=None, **kwargs):
41+
# The default is surrogateescape for stdout and backslashreplace for
42+
# stderr, but in the context of an Android log, readability is more
43+
# important than reversibility.
44+
kwargs.setdefault("encoding", "UTF-8")
45+
kwargs.setdefault("errors", "backslashreplace")
46+
47+
super().__init__(BinaryLogStream(prio, tag, fileno), **kwargs)
48+
self._lock = RLock()
49+
self._pending_bytes = []
50+
self._pending_bytes_count = 0
51+
52+
def __repr__(self):
53+
return f"<TextLogStream {self.buffer.tag!r}>"
54+
55+
def write(self, s):
56+
if not isinstance(s, str):
57+
raise TypeError(
58+
f"write() argument must be str, not {type(s).__name__}")
59+
60+
# In case `s` is a str subclass that writes itself to stdout or stderr
61+
# when we call its methods, convert it to an actual str.
62+
s = str.__str__(s)
63+
64+
# We want to emit one log message per line wherever possible, so split
65+
# the string into lines first. Note that "".splitlines() == [], so
66+
# nothing will be logged for an empty string.
67+
with self._lock:
68+
for line in s.splitlines(keepends=True):
69+
while line:
70+
chunk = line[:MAX_CHARS_PER_WRITE]
71+
line = line[MAX_CHARS_PER_WRITE:]
72+
self._write_chunk(chunk)
73+
74+
return len(s)
75+
76+
# The size and behavior of TextIOWrapper's buffer is not part of its public
77+
# API, so we handle buffering ourselves to avoid truncation.
78+
def _write_chunk(self, s):
79+
b = s.encode(self.encoding, self.errors)
80+
if self._pending_bytes_count + len(b) > MAX_BYTES_PER_WRITE:
81+
self.flush()
82+
83+
self._pending_bytes.append(b)
84+
self._pending_bytes_count += len(b)
85+
if (
86+
self.write_through
87+
or b.endswith(b"\n")
88+
or self._pending_bytes_count > MAX_BYTES_PER_WRITE
89+
):
90+
self.flush()
91+
92+
def flush(self):
93+
with self._lock:
94+
self.buffer.write(b"".join(self._pending_bytes))
95+
self._pending_bytes.clear()
96+
self._pending_bytes_count = 0
97+
98+
# Since this is a line-based logging system, line buffering cannot be turned
99+
# off, i.e. a newline always causes a flush.
100+
@property
101+
def line_buffering(self):
102+
return True
103+
104+
105+
class BinaryLogStream(io.RawIOBase):
106+
def __init__(self, prio, tag, fileno=None):
107+
self.prio = prio
108+
self.tag = tag
109+
self._fileno = fileno
110+
111+
def __repr__(self):
112+
return f"<BinaryLogStream {self.tag!r}>"
113+
114+
def writable(self):
115+
return True
116+
117+
def write(self, b):
118+
if type(b) is not bytes:
119+
try:
120+
b = bytes(memoryview(b))
121+
except TypeError:
122+
raise TypeError(
123+
f"write() argument must be bytes-like, not {type(b).__name__}"
124+
) from None
125+
126+
# Writing an empty string to the stream should have no effect.
127+
if b:
128+
logcat.write(self.prio, self.tag, b)
129+
return len(b)
130+
131+
# This is needed by the test suite --timeout option, which uses faulthandler.
132+
def fileno(self):
133+
if self._fileno is None:
134+
raise io.UnsupportedOperation("fileno")
135+
return self._fileno
136+
137+
138+
# When a large volume of data is written to logcat at once, e.g. when a test
139+
# module fails in --verbose3 mode, there's a risk of overflowing logcat's own
140+
# buffer and losing messages. We avoid this by imposing a rate limit using the
141+
# token bucket algorithm, based on a conservative estimate of how fast `adb
142+
# logcat` can consume data.
143+
MAX_BYTES_PER_SECOND = 1024 * 1024
144+
145+
# The logcat buffer size of a device can be determined by running `logcat -g`.
146+
# We set the token bucket size to half of the buffer size of our current minimum
147+
# API level, because other things on the system will be producing messages as
148+
# well.
149+
BUCKET_SIZE = 128 * 1024
150+
151+
# https://cs.android.com/android/platform/superproject/+/android-14.0.0_r1:system/logging/liblog/include/log/log_read.h;l=39
152+
PER_MESSAGE_OVERHEAD = 28
153+
154+
155+
class Logcat:
156+
def __init__(self, android_log_write):
157+
self.android_log_write = android_log_write
158+
self._lock = RLock()
159+
self._bucket_level = 0
160+
self._prev_write_time = time()
161+
162+
def write(self, prio, tag, message):
163+
# Encode null bytes using "modified UTF-8" to avoid them truncating the
164+
# message.
165+
message = message.replace(b"\x00", b"\xc0\x80")
166+
167+
with self._lock:
168+
now = time()
169+
self._bucket_level += (
170+
(now - self._prev_write_time) * MAX_BYTES_PER_SECOND)
171+
172+
# If the bucket level is still below zero, the clock must have gone
173+
# backwards, so reset it to zero and continue.
174+
self._bucket_level = max(0, min(self._bucket_level, BUCKET_SIZE))
175+
self._prev_write_time = now
176+
177+
self._bucket_level -= PER_MESSAGE_OVERHEAD + len(tag) + len(message)
178+
if self._bucket_level < 0:
179+
sleep(-self._bucket_level / MAX_BYTES_PER_SECOND)
180+
181+
self.android_log_write(prio, tag, message)

Lib/_apple_support.py

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import io
2+
import sys
3+
4+
5+
def init_streams(log_write, stdout_level, stderr_level):
6+
# Redirect stdout and stderr to the Apple system log. This method is
7+
# invoked by init_apple_streams() (initconfig.c) if config->use_system_logger
8+
# is enabled.
9+
sys.stdout = SystemLog(log_write, stdout_level, errors=sys.stderr.errors)
10+
sys.stderr = SystemLog(log_write, stderr_level, errors=sys.stderr.errors)
11+
12+
13+
class SystemLog(io.TextIOWrapper):
14+
def __init__(self, log_write, level, **kwargs):
15+
kwargs.setdefault("encoding", "UTF-8")
16+
kwargs.setdefault("line_buffering", True)
17+
super().__init__(LogStream(log_write, level), **kwargs)
18+
19+
def __repr__(self):
20+
return f"<SystemLog (level {self.buffer.level})>"
21+
22+
def write(self, s):
23+
if not isinstance(s, str):
24+
raise TypeError(
25+
f"write() argument must be str, not {type(s).__name__}")
26+
27+
# In case `s` is a str subclass that writes itself to stdout or stderr
28+
# when we call its methods, convert it to an actual str.
29+
s = str.__str__(s)
30+
31+
# We want to emit one log message per line, so split
32+
# the string before sending it to the superclass.
33+
for line in s.splitlines(keepends=True):
34+
super().write(line)
35+
36+
return len(s)
37+
38+
39+
class LogStream(io.RawIOBase):
40+
def __init__(self, log_write, level):
41+
self.log_write = log_write
42+
self.level = level
43+
44+
def __repr__(self):
45+
return f"<LogStream (level {self.level!r})>"
46+
47+
def writable(self):
48+
return True
49+
50+
def write(self, b):
51+
if type(b) is not bytes:
52+
try:
53+
b = bytes(memoryview(b))
54+
except TypeError:
55+
raise TypeError(
56+
f"write() argument must be bytes-like, not {type(b).__name__}"
57+
) from None
58+
59+
# Writing an empty string to the stream should have no effect.
60+
if b:
61+
# Encode null bytes using "modified UTF-8" to avoid truncating the
62+
# message. This should not affect the return value, as the caller
63+
# may be expecting it to match the length of the input.
64+
self.log_write(self.level, b.replace(b"\x00", b"\xc0\x80"))
65+
66+
return len(b)

Lib/_ios_support.py

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import sys
2+
try:
3+
from ctypes import cdll, c_void_p, c_char_p, util
4+
except ImportError:
5+
# ctypes is an optional module. If it's not present, we're limited in what
6+
# we can tell about the system, but we don't want to prevent the module
7+
# from working.
8+
print("ctypes isn't available; iOS system calls will not be available", file=sys.stderr)
9+
objc = None
10+
else:
11+
# ctypes is available. Load the ObjC library, and wrap the objc_getClass,
12+
# sel_registerName methods
13+
lib = util.find_library("objc")
14+
if lib is None:
15+
# Failed to load the objc library
16+
raise ImportError("ObjC runtime library couldn't be loaded")
17+
18+
objc = cdll.LoadLibrary(lib)
19+
objc.objc_getClass.restype = c_void_p
20+
objc.objc_getClass.argtypes = [c_char_p]
21+
objc.sel_registerName.restype = c_void_p
22+
objc.sel_registerName.argtypes = [c_char_p]
23+
24+
25+
def get_platform_ios():
26+
# Determine if this is a simulator using the multiarch value
27+
is_simulator = sys.implementation._multiarch.endswith("simulator")
28+
29+
# We can't use ctypes; abort
30+
if not objc:
31+
return None
32+
33+
# Most of the methods return ObjC objects
34+
objc.objc_msgSend.restype = c_void_p
35+
# All the methods used have no arguments.
36+
objc.objc_msgSend.argtypes = [c_void_p, c_void_p]
37+
38+
# Equivalent of:
39+
# device = [UIDevice currentDevice]
40+
UIDevice = objc.objc_getClass(b"UIDevice")
41+
SEL_currentDevice = objc.sel_registerName(b"currentDevice")
42+
device = objc.objc_msgSend(UIDevice, SEL_currentDevice)
43+
44+
# Equivalent of:
45+
# device_systemVersion = [device systemVersion]
46+
SEL_systemVersion = objc.sel_registerName(b"systemVersion")
47+
device_systemVersion = objc.objc_msgSend(device, SEL_systemVersion)
48+
49+
# Equivalent of:
50+
# device_systemName = [device systemName]
51+
SEL_systemName = objc.sel_registerName(b"systemName")
52+
device_systemName = objc.objc_msgSend(device, SEL_systemName)
53+
54+
# Equivalent of:
55+
# device_model = [device model]
56+
SEL_model = objc.sel_registerName(b"model")
57+
device_model = objc.objc_msgSend(device, SEL_model)
58+
59+
# UTF8String returns a const char*;
60+
SEL_UTF8String = objc.sel_registerName(b"UTF8String")
61+
objc.objc_msgSend.restype = c_char_p
62+
63+
# Equivalent of:
64+
# system = [device_systemName UTF8String]
65+
# release = [device_systemVersion UTF8String]
66+
# model = [device_model UTF8String]
67+
system = objc.objc_msgSend(device_systemName, SEL_UTF8String).decode()
68+
release = objc.objc_msgSend(device_systemVersion, SEL_UTF8String).decode()
69+
model = objc.objc_msgSend(device_model, SEL_UTF8String).decode()
70+
71+
return system, release, model, is_simulator

0 commit comments

Comments
 (0)