Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 148e5ed

Browse files
committed
python fixes
now, one can do "script run mything.py" multiple times without crashing, and exit codes are correct. Pm3PyRun_SimpleFileNoExit: * use XDECREF instead of DECREF (handles possible nullptr). * don't double-free "er". * print exit status correctly. CmdScriptRun: * allow invoking scripts multiple times. * fix memleak of argtable contents if help path not triggered. * configure_c_stdio should be 0 or 1, not < 0. * py_conf: don't explicitly set options to their default values, it's confusing. * Call Py_Finalize only when exiting pm3. main_loop: * cleanup cmdscript from main_loop. Important to call Py_Finalize from here in case Qt is used (in which case main_loop is on Qt thread). ProxGuiQT::MainLoop: * reduce timer delay to 0 - there isn't a need to wait here.
1 parent 65de25c commit 148e5ed

4 files changed

Lines changed: 164 additions & 63 deletions

File tree

client/src/cmdscript.c

Lines changed: 158 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,19 @@ extern int luaopen_pm3(lua_State *L);
5252
extern PyObject *PyInit__pm3(void);
5353
#endif // HAVE_PYTHON_SWIG
5454

55+
static void Pm3Py_FlushStream(const char *stream_name) {
56+
PyObject *flush_stream = PySys_GetObject(stream_name);
57+
if (!flush_stream) {
58+
return;
59+
}
60+
PyObject *fr = PyObject_CallMethod(flush_stream, "flush", NULL);
61+
if (fr) {
62+
Py_DECREF(fr);
63+
} else {
64+
PyErr_Clear();
65+
}
66+
}
67+
5568
// Partly ripped from PyRun_SimpleFileExFlags
5669
// but does not terminate client on sys.exit
5770
// and print exit code only if != 0
@@ -95,20 +108,34 @@ static int Pm3PyRun_SimpleFileNoExit(FILE *fp, const char *filename) {
95108
Py_CLEAR(m);
96109

97110
if (PyErr_ExceptionMatches(PyExc_SystemExit)) {
98-
// PyErr_Print() exists if SystemExit so we've to handle it ourselves
99-
PyObject *ty = 0, *er = 0, *tr = 0;
111+
// PyErr_Print() exits on SystemExit, so we handle it ourselves.
112+
// Normalize first so `er` is a proper SystemExit instance with .code.
113+
// CPython exit-code semantics: None -> 0, int -> int, other -> 1.
114+
PyObject *ty = NULL, *er = NULL, *tr = NULL;
100115
PyErr_Fetch(&ty, &er, &tr);
101-
102-
long err = PyLong_AsLong(er);
116+
PyErr_NormalizeException(&ty, &er, &tr);
117+
118+
long err = 0;
119+
if (er != NULL) {
120+
PyObject *code = PyObject_GetAttrString(er, "code");
121+
if (code != NULL && code != Py_None) {
122+
if (PyLong_Check(code)) {
123+
err = PyLong_AsLong(code);
124+
} else {
125+
err = 1;
126+
}
127+
}
128+
Py_XDECREF(code);
129+
}
103130
if (err) {
104131
PrintAndLogEx(WARNING, "\nScript terminated by " _YELLOW_("SystemExit %li"), err);
105132
} else {
106133
ret = 0;
107134
}
108135

109-
Py_DECREF(ty);
110-
Py_DECREF(er);
111-
Py_DECREF(er);
136+
Py_XDECREF(ty);
137+
Py_XDECREF(er);
138+
Py_XDECREF(tr);
112139
PyErr_Clear();
113140
goto done;
114141

@@ -122,12 +149,18 @@ static int Pm3PyRun_SimpleFileNoExit(FILE *fp, const char *filename) {
122149
ret = 0;
123150

124151
done:
152+
// Flush sys.stdout / sys.stderr explicitly. Since we run with buffering,
153+
// they would not otherwise be flushed until Py_Finalize.
154+
Pm3Py_FlushStream("stdout");
155+
Pm3Py_FlushStream("stderr");
156+
125157
if (set_file_name && PyDict_DelItemString(d, "__file__")) {
126158
PyErr_Clear();
127159
}
128160
Py_XDECREF(m);
129161
return ret;
130162
}
163+
131164
#endif // HAVE_PYTHON
132165

133166
typedef enum {
@@ -295,6 +328,8 @@ static int CmdScriptRun(const char *Cmd) {
295328
arg_strx0(NULL, NULL, "<params>", "script parameters"),
296329
arg_param_end
297330
};
331+
ctx->argtable = argtable;
332+
ctx->argtableLen = arg_getsize(argtable);
298333

299334
int fnlen = 0;
300335
char filename[FILE_PATH_SIZE] = {0};
@@ -310,8 +345,6 @@ static int CmdScriptRun(const char *Cmd) {
310345
if ((strlen(filename) == 0) ||
311346
(strcmp(filename, "-h") == 0) ||
312347
(strcmp(filename, "--help") == 0)) {
313-
ctx->argtable = argtable;
314-
ctx->argtableLen = arg_getsize(argtable);
315348
CLIParserPrintHelp(ctx);
316349
CLIParserFree(ctx);
317350
return PM3_ESOFT;
@@ -439,26 +472,14 @@ static int CmdScriptRun(const char *Cmd) {
439472
PrintAndLogEx(SUCCESS, "executing python " _YELLOW_("%s"), script_path);
440473
PrintAndLogEx(SUCCESS, "args " _YELLOW_("'%s'"), arguments);
441474

442-
#ifdef HAVE_PYTHON_SWIG
443-
// hook Proxmark3 API
444-
PyImport_AppendInittab("_pm3", PyInit__pm3);
445-
#endif
446-
#if PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION < 10
447-
Py_Initialize();
448-
#else
449-
PyConfig py_conf;
450-
PyStatus status;
451-
// We need to use Python mode instead of isolated to avoid breaking stuff.
452-
PyConfig_InitPythonConfig(&py_conf);
453-
// Let's still make things bit safer by being as close as possible to isolated mode.
454-
py_conf.configure_c_stdio = -1;
455-
py_conf.faulthandler = 0;
456-
py_conf.use_hash_seed = 0;
457-
py_conf.install_signal_handlers = 0;
458-
py_conf.parse_argv = 0;
459-
py_conf.user_site_directory = 1;
460-
py_conf.use_environment = 0;
461-
#endif
475+
// Python interpreter must be initialized ONCE per client session.
476+
// Cycling Py_Initialize / Py_Finalize across script runs is documented
477+
// as fragile in CPython because C extension modules (our SWIG-built
478+
// _pm3 included) retain static state across the cycle and crash on
479+
// re-init. So we init lazily on the first script and leave Python
480+
// alive; between runs we just refresh sys.argv and clear __main__'s
481+
// globals so each script starts from a clean namespace.
482+
static bool s_py_initialized = false;
462483

463484
//int argc, char ** argv
464485
char *argv[FILE_PATH_SIZE];
@@ -469,58 +490,129 @@ static int CmdScriptRun(const char *Cmd) {
469490
free(script_path);
470491
return PM3_ESOFT;
471492
}
493+
472494
#if PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION < 10
473495
wchar_t *py_args[argc + 1];
474496
for (int i = 0; i <= argc; i++) {
475497
py_args[i] = Py_DecodeLocale(argv[i], NULL);
476498
}
499+
#endif
477500

478-
PySys_SetArgv(argc + 1, py_args);
501+
#if PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION >= 10
502+
// Declared at this scope so the `pyexception` label below can reach
503+
// them after a goto from inside the init block.
504+
PyConfig py_conf;
505+
PyStatus status;
506+
#endif
507+
if (!s_py_initialized) {
508+
#ifdef HAVE_PYTHON_SWIG
509+
// hook Proxmark3 API
510+
PyImport_AppendInittab("_pm3", PyInit__pm3);
511+
#endif
512+
#if PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION < 10
513+
Py_Initialize();
514+
PySys_SetArgv(argc + 1, py_args);
479515
#else
480-
// The following line will implicitly pre-initialize Python
481-
status = PyConfig_SetBytesArgv(&py_conf, argc + 1, argv);
482-
if (PyStatus_Exception(status)) {
483-
goto pyexception;
484-
}
485-
// We disallowed in py_conf environment variables interfering with python interpreter's behavior.
486-
// Let's manually enable the ones we truly need.
487-
const char *virtual_env = getenv("VIRTUAL_ENV");
488-
if (virtual_env != NULL) {
489-
size_t length = strlen(virtual_env) + strlen("/bin/python3") + 1;
490-
char python_executable_path[length];
491-
snprintf(python_executable_path, length, "%s/bin/python3", virtual_env);
492-
status = PyConfig_SetBytesString(&py_conf, &py_conf.executable, python_executable_path);
516+
// We need to use Python mode instead of isolated to avoid breaking stuff.
517+
PyConfig_InitPythonConfig(&py_conf);
518+
// Let's still make things bit safer by being as close as possible to isolated mode.
519+
py_conf.faulthandler = 0;
520+
py_conf.use_hash_seed = 0;
521+
py_conf.install_signal_handlers = 0;
522+
py_conf.parse_argv = 0;
523+
py_conf.use_environment = 0;
524+
525+
// The following line will implicitly pre-initialize Python
526+
status = PyConfig_SetBytesArgv(&py_conf, argc + 1, argv);
493527
if (PyStatus_Exception(status)) {
494528
goto pyexception;
495529
}
496-
} else {
497-
// This is required by Proxspace to work with an isolated Python configuration
498-
status = PyConfig_SetBytesString(&py_conf, &py_conf.home, getenv("PYTHONHOME"));
530+
// We disallowed in py_conf environment variables interfering with python interpreter's behavior.
531+
// Let's manually enable the ones we truly need.
532+
const char *virtual_env = getenv("VIRTUAL_ENV");
533+
if (virtual_env != NULL) {
534+
size_t length = strlen(virtual_env) + strlen("/bin/python3") + 1;
535+
char python_executable_path[length];
536+
snprintf(python_executable_path, length, "%s/bin/python3", virtual_env);
537+
status = PyConfig_SetBytesString(&py_conf, &py_conf.executable, python_executable_path);
538+
if (PyStatus_Exception(status)) {
539+
goto pyexception;
540+
}
541+
} else {
542+
// This is required by Proxspace to work with an isolated Python configuration
543+
status = PyConfig_SetBytesString(&py_conf, &py_conf.home, getenv("PYTHONHOME"));
544+
if (PyStatus_Exception(status)) {
545+
goto pyexception;
546+
}
547+
}
548+
// This is required for allowing `import pm3` in python scripts
549+
status = PyConfig_SetBytesString(&py_conf, &py_conf.pythonpath_env, getenv("PYTHONPATH"));
499550
if (PyStatus_Exception(status)) {
500551
goto pyexception;
501552
}
502-
}
503-
// This is required for allowing `import pm3` in python scripts
504-
status = PyConfig_SetBytesString(&py_conf, &py_conf.pythonpath_env, getenv("PYTHONPATH"));
505-
if (PyStatus_Exception(status)) {
506-
goto pyexception;
507-
}
508553

509-
status = Py_InitializeFromConfig(&py_conf);
510-
if (PyStatus_Exception(status)) {
511-
goto pyexception;
512-
}
554+
status = Py_InitializeFromConfig(&py_conf);
555+
if (PyStatus_Exception(status)) {
556+
goto pyexception;
557+
}
513558

514-
// clean up
515-
PyConfig_Clear(&py_conf);
559+
// clean up
560+
PyConfig_Clear(&py_conf);
516561
#endif
562+
// setup search paths (only once - PySys_GetObject("path") is
563+
// prepended each call, so repeating would grow sys.path).
564+
set_python_paths();
565+
s_py_initialized = true;
566+
} else {
567+
// Already initialized. Refresh sys.argv for this run and wipe
568+
// __main__ globals so the new script doesn't see leftovers.
569+
PyObject *argv_list = PyList_New(argc + 1);
570+
for (int i = 0; i <= argc; i++) {
571+
PyList_SET_ITEM(argv_list, i, PyUnicode_DecodeFSDefault(argv[i]));
572+
}
573+
PySys_SetObject("argv", argv_list);
574+
Py_DECREF(argv_list);
575+
576+
PyObject *main_module = PyImport_AddModule("__main__");
577+
if (main_module != NULL) {
578+
PyObject *main_dict = PyModule_GetDict(main_module);
579+
// Wipe leftover user globals from the previous run but preserve
580+
// the module attributes Python itself put there at init
581+
// (without these, e.g. `if __name__ == '__main__':` raises
582+
// NameError and the script silently aborts).
583+
// __file__/__cached__ are re-set per-run in Pm3PyRun_SimpleFileNoExit.
584+
static const char *const KEEP_KEYS[] = {
585+
"__name__", "__doc__", "__package__",
586+
"__loader__", "__spec__", "__builtins__",
587+
NULL,
588+
};
589+
PyObject *keys = PyDict_Keys(main_dict);
590+
if (keys != NULL) {
591+
Py_ssize_t n = PyList_GET_SIZE(keys);
592+
for (Py_ssize_t i = 0; i < n; i++) {
593+
PyObject *key = PyList_GET_ITEM(keys, i);
594+
const char *k = PyUnicode_AsUTF8(key);
595+
if (k == NULL) {
596+
PyErr_Clear();
597+
continue;
598+
}
599+
bool keep = false;
600+
for (const char *const *kk = KEEP_KEYS; *kk != NULL; kk++) {
601+
if (strcmp(k, *kk) == 0) { keep = true; break; }
602+
}
603+
if (!keep) {
604+
PyDict_DelItem(main_dict, key);
605+
}
606+
}
607+
Py_DECREF(keys);
608+
}
609+
}
610+
}
611+
517612
for (int i = 0; i < argc; ++i) {
518613
free(argv[i + 1]);
519614
}
520615

521-
// setup search paths.
522-
set_python_paths();
523-
524616
FILE *f = fopen(script_path, "r");
525617
if (f == NULL) {
526618
PrintAndLogEx(ERR, "Could open file " _YELLOW_("%s"), script_path);
@@ -534,7 +626,6 @@ static int CmdScriptRun(const char *Cmd) {
534626
PyMem_RawFree(py_args[i]);
535627
}
536628
#endif
537-
Py_Finalize();
538629
free(script_path);
539630
if (ret) {
540631
PrintAndLogEx(WARNING, "\nfinished " _YELLOW_("%s") " with exception", filename);
@@ -611,3 +702,8 @@ int CmdScript(const char *Cmd) {
611702
return CmdsParse(CommandTable, Cmd);
612703
}
613704

705+
void CmdScriptCleanup(void) {
706+
#ifdef HAVE_PYTHON
707+
Py_Finalize();
708+
#endif
709+
}

client/src/cmdscript.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@
2222
#include "common.h"
2323

2424
int CmdScript(const char *Cmd);
25+
void CmdScriptCleanup(void);
2526

2627
#endif

client/src/proxguiqt.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ void ProxGuiQT::MainLoop() {
256256
connect(this, SIGNAL(HidePictureWindowSignal()), this, SLOT(_HidePictureWindow()));
257257

258258
//start proxmark thread after starting event loop
259-
QTimer::singleShot(200, this, SLOT(_StartProxmarkThread()));
259+
QTimer::singleShot(0, this, SLOT(_StartProxmarkThread()));
260260

261261
#if defined(__MACH__) && defined(__APPLE__)
262262
//Prevent the terminal from loosing focus during launch by making the client unfocusable

client/src/proxmark3.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
#include "flash.h"
3737
#include "preferences.h"
3838
#include "commonutil.h"
39+
#include "cmdscript.h"
3940

4041
#ifndef _WIN32
4142
#include <locale.h>
@@ -512,6 +513,7 @@ main_loop(const char *script_cmds_file, char *script_cmd, bool stayInCommandLoop
512513
char prompt_filtered[PROXPROMPT_MAX_SIZE] = {0};
513514
memcpy_filter_ansi(prompt_filtered, prompt, sizeof(prompt_filtered), !g_session.supports_colors);
514515
g_pendingPrompt = true;
516+
// TODO this should be free'd via pm3line_free
515517
script_cmd = pm3line_read(prompt_filtered);
516518
#if defined(_WIN32)
517519
//Check if color support needs to be enabled again in case the window buffer did change
@@ -614,6 +616,8 @@ main_loop(const char *script_cmds_file, char *script_cmd, bool stayInCommandLoop
614616
free(cmd);
615617
cmd = NULL;
616618
}
619+
620+
CmdScriptCleanup();
617621
}
618622

619623
#ifndef LIBPM3

0 commit comments

Comments
 (0)