1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
|
#include <android/log.h>
#include <errno.h>
#include <jni.h>
#include <pthread.h>
#include <Python.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
static void throw_runtime_exception(JNIEnv *env, const char *message) {
(*env)->ThrowNew(
env,
(*env)->FindClass(env, "java/lang/RuntimeException"),
message);
}
static void throw_errno(JNIEnv *env, const char *error_prefix) {
char error_message[1024];
snprintf(error_message, sizeof(error_message),
"%s: %s", error_prefix, strerror(errno));
throw_runtime_exception(env, error_message);
}
// --- Stdio redirection ------------------------------------------------------
// Most apps won't need this, because the Python-level sys.stdout and sys.stderr
// are redirected to the Android logcat by Python itself. However, in the
// testbed it's useful to redirect the native streams as well, to debug problems
// in the Python startup or redirection process.
//
// Based on
// https://github.com/beeware/briefcase-android-gradle-template/blob/v0.3.11/%7B%7B%20cookiecutter.safe_formal_name%20%7D%7D/app/src/main/cpp/native-lib.cpp
typedef struct {
FILE *file;
int fd;
android_LogPriority priority;
char *tag;
int pipe[2];
} StreamInfo;
// The FILE member can't be initialized here because stdout and stderr are not
// compile-time constants. Instead, it's initialized immediately before the
// redirection.
static StreamInfo STREAMS[] = {
{NULL, STDOUT_FILENO, ANDROID_LOG_INFO, "native.stdout", {-1, -1}},
{NULL, STDERR_FILENO, ANDROID_LOG_WARN, "native.stderr", {-1, -1}},
{NULL, -1, ANDROID_LOG_UNKNOWN, NULL, {-1, -1}},
};
// The maximum length of a log message in bytes, including the level marker and
// tag, is defined as LOGGER_ENTRY_MAX_PAYLOAD in
// platform/system/logging/liblog/include/log/log.h. As of API level 30, messages
// longer than this will be be truncated by logcat. This limit has already been
// reduced at least once in the history of Android (from 4076 to 4068 between API
// level 23 and 26), so leave some headroom.
static const int MAX_BYTES_PER_WRITE = 4000;
static void *redirection_thread(void *arg) {
StreamInfo *si = (StreamInfo*)arg;
ssize_t read_size;
char buf[MAX_BYTES_PER_WRITE];
while ((read_size = read(si->pipe[0], buf, sizeof buf - 1)) > 0) {
buf[read_size] = '\0'; /* add null-terminator */
__android_log_write(si->priority, si->tag, buf);
}
return 0;
}
static char *redirect_stream(StreamInfo *si) {
/* make the FILE unbuffered, to ensure messages are never lost */
if (setvbuf(si->file, 0, _IONBF, 0)) {
return "setvbuf";
}
/* create the pipe and redirect the file descriptor */
if (pipe(si->pipe)) {
return "pipe";
}
if (dup2(si->pipe[1], si->fd) == -1) {
return "dup2";
}
/* start the logging thread */
pthread_t thr;
if ((errno = pthread_create(&thr, 0, redirection_thread, si))) {
return "pthread_create";
}
if ((errno = pthread_detach(thr))) {
return "pthread_detach";
}
return 0;
}
JNIEXPORT void JNICALL Java_org_python_testbed_PythonTestRunner_redirectStdioToLogcat(
JNIEnv *env, jobject obj
) {
STREAMS[0].file = stdout;
STREAMS[1].file = stderr;
for (StreamInfo *si = STREAMS; si->file; si++) {
char *error_prefix;
if ((error_prefix = redirect_stream(si))) {
throw_errno(env, error_prefix);
return;
}
}
}
// --- Python initialization ---------------------------------------------------
static char *init_signals() {
// Some tests use SIGUSR1, but that's blocked by default in an Android app in
// order to make it available to `sigwait` in the Signal Catcher thread.
// (https://cs.android.com/android/platform/superproject/+/android14-qpr3-release:art/runtime/signal_catcher.cc).
// That thread's functionality is only useful for debugging the JVM, so disabling
// it should not weaken the tests.
//
// There's no safe way of stopping the thread completely (#123982), but simply
// unblocking SIGUSR1 is enough to fix most tests.
//
// However, in tests that generate multiple different signals in quick
// succession, it's possible for SIGUSR1 to arrive while the main thread is busy
// running the C-level handler for a different signal. In that case, the SIGUSR1
// may be sent to the Signal Catcher thread instead, which will generate a log
// message containing the text "reacting to signal".
//
// Such tests may need to be changed in one of the following ways:
// * Use a signal other than SIGUSR1 (e.g. test_stress_delivery_simultaneous in
// test_signal.py).
// * Send the signal to a specific thread rather than the whole process (e.g.
// test_signals in test_threadsignals.py.
sigset_t set;
if (sigemptyset(&set)) {
return "sigemptyset";
}
if (sigaddset(&set, SIGUSR1)) {
return "sigaddset";
}
if ((errno = pthread_sigmask(SIG_UNBLOCK, &set, NULL))) {
return "pthread_sigmask";
}
return NULL;
}
static void throw_status(JNIEnv *env, PyStatus status) {
throw_runtime_exception(env, status.err_msg ? status.err_msg : "");
}
JNIEXPORT int JNICALL Java_org_python_testbed_PythonTestRunner_runPython(
JNIEnv *env, jobject obj, jstring home, jarray args
) {
const char *home_utf8 = (*env)->GetStringUTFChars(env, home, NULL);
char cwd[PATH_MAX];
snprintf(cwd, sizeof(cwd), "%s/%s", home_utf8, "cwd");
if (chdir(cwd)) {
throw_errno(env, "chdir");
return 1;
}
char *error_prefix;
if ((error_prefix = init_signals())) {
throw_errno(env, error_prefix);
return 1;
}
PyConfig config;
PyStatus status;
PyConfig_InitPythonConfig(&config);
jsize argc = (*env)->GetArrayLength(env, args);
const char *argv[argc + 1];
for (int i = 0; i < argc; i++) {
jobject arg = (*env)->GetObjectArrayElement(env, args, i);
argv[i] = (*env)->GetStringUTFChars(env, arg, NULL);
}
argv[argc] = NULL;
// PyConfig_SetBytesArgv "must be called before other methods, since the
// preinitialization configuration depends on command line arguments"
if (PyStatus_Exception(status = PyConfig_SetBytesArgv(&config, argc, (char**)argv))) {
throw_status(env, status);
return 1;
}
status = PyConfig_SetBytesString(&config, &config.home, home_utf8);
if (PyStatus_Exception(status)) {
throw_status(env, status);
return 1;
}
status = Py_InitializeFromConfig(&config);
if (PyStatus_Exception(status)) {
throw_status(env, status);
return 1;
}
return Py_RunMain();
}
|