@@ -52,6 +52,19 @@ extern int luaopen_pm3(lua_State *L);
5252extern 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
124151done :
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
133166typedef 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+ }
0 commit comments