xantares / nfc-bindings

scripting language bindings for libnfc
BSD 3-Clause "New" or "Revised" License
23 stars 9 forks source link

quick_start_example cannot be interrupted, initiator_select_passive_target() blocks hard? #8

Closed elonen closed 6 years ago

elonen commented 9 years ago

While testing quick_start_example.py (on a Raspberry PI with SCL3711 USB NFC reader), I found the example script cannot be interrupted with Ctrl-C. It only reacts after I actually show a tag.

Additionally, in my own application which is using run_in_executor (multithreading) on asyncio, initiator_select_passive_target(...) seems to somehow block the entire program (main thread) in a similar way, not only the thread it's running in.

Is this an intended behavior and/or am I perhaps using it incorrectly?

xantares commented 9 years ago

I don't remember it being blocked on my machine, a fork used this: https://github.com/akito45/nfc-bindings/commit/496095bbf43d629fd18fb7015442e0e00f5f6916

elonen commented 9 years ago

Thanks for the pointer! Tried the akito45 -threads swig option, but ctrl-c still simply prints '^C', and only shows the KeyboardInterrupt stack trace after I show a tag to the reader:

$ sudo venv/bin/python quick_start_example.py 
Version:  debian/1.7.1-3-1-g0599c08
NFC reader: SCM Micro / SCL3711-NFC&RW opened
^C

( The execution blocks here. Showing a card to the reader, it continues with: )

Traceback (most recent call last):
  File "quick_start_example.py", line 29, in <module>
    ret = nfc.initiator_select_passive_target(pnd, nmMifare, 0, 0, nt)
  File "/home/pi/nfc/venv/lib/python3.4/site-packages/nfc.py", line 205, in initiator_select_passive_target
    return _nfc.initiator_select_passive_target(*args)
KeyboardInterrupt

The nfc-poll example program (written in C) interrupts just fine, so it doesn't seem to be a libnfc or driver issue. I've never written a Python extension, but from what I've understood, this sounds like a GIL problem. I'm using Python 3.4.

I thought maybe the -threads addition didn't actually do anything, so I grepped for it. The last line contains: swig2.0 -python -threads -ignoremissing:

$ grep -r threads *
CMakeFiles/CMakeOutput.log:Configured with: ../src/configure -v --with-pkgversion='Raspbian 4.9.2-10' --with-bugurl=file:///usr/share/doc/gcc-4.9/README.Bugs --enable-languages=c,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-4.9 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --with-gxx-include-dir=/usr/include/c++/4.9 --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --enable-gnu-unique-object --disable-libitm --disable-libquadmath --enable-plugin --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-4.9-armhf/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-4.9-armhf --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-4.9-armhf --with-arch-directory=arm --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --enable-objc-gc --enable-multiarch --disable-sjlj-exceptions --with-arch=armv6 --with-fpu=vfp --with-float=hard --enable-checking=release --build=arm-linux-gnueabihf --host=arm-linux-gnueabihf --target=arm-linux-gnueabihf
CMakeFiles/CMakeOutput.log:  ignore line: [Configured with: ../src/configure -v --with-pkgversion='Raspbian 4.9.2-10' --with-bugurl=file:///usr/share/doc/gcc-4.9/README.Bugs --enable-languages=c,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-4.9 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --with-gxx-include-dir=/usr/include/c++/4.9 --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --enable-gnu-unique-object --disable-libitm --disable-libquadmath --enable-plugin --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-4.9-armhf/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-4.9-armhf --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-4.9-armhf --with-arch-directory=arm --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --enable-objc-gc --enable-multiarch --disable-sjlj-exceptions --with-arch=armv6 --with-fpu=vfp --with-float=hard --enable-checking=release --build=arm-linux-gnueabihf --host=arm-linux-gnueabihf --target=arm-linux-gnueabihf]
python/CMakeLists.txt:set (CMAKE_SWIG_FLAGS "-threads -ignoremissing")
python/CMakeFiles/_nfc.dir/build.make:  cd /home/pi/nfc/nfc-bindings/python && /usr/bin/swig2.0 -python -threads -ignoremissing -outdir /home/pi/nfc/nfc-bindings/python -I/usr/include -I/home/pi/nfc/nfc-bindings/python -I/usr/include/python3.4m -o /home/pi/nfc/nfc-bindings/python/nfcPYTHON_wrap.c /home/pi/nfc/nfc-bindings/nfc.i
python/nfcPYTHON_wrap.c:#  if defined(SWIG_PYTHON_USE_GIL) /* Use PyGILState threads calls */

Reading through the web, I next tried (without really understanding what's going on behind the scenes) adding...

%exception nfc_initiator_select_passive_target {
   Py_BEGIN_ALLOW_THREADS
   $action
   Py_END_ALLOW_THREADS
}
int nfc_initiator_select_passive_target(nfc_device *pnd, const nfc_modulation nm, const uint8_t *pbtInitData, const size_t szInitData, nfc_target *pnt);

...but that resulted in immediate termination:

Version:  debian/1.7.1-3-1-g0599c08
NFC reader: SCM Micro / SCL3711-NFC&RW opened
Fatal Python error: PyEval_SaveThread: NULL tstate
xantares commented 9 years ago

could you try to put this somewhere at the beginning of the generated nfc.py ? (without -threads)

        # Reset the default Crtl-C behavior
        import signal
        try:
            signal.signal(signal.SIGINT, signal.SIG_DFL)
        except ValueError:
            pass
elonen commented 9 years ago

Tried, no effect. Also, as I wrote earlier, the call seems to halt the whole program, even if called from a separate thread.

BTW: nfc.initiator_target_is_present() doesn't seem to block.

xantares commented 9 years ago

are you sure you're not getting the same behavior from C, ie if you translate quick_start_example.py to c does it interrupt ?

elonen commented 9 years ago

I can't write a complete conversion right now, but at editing nfc-poll.c so that...

  if ((res = nfc_initiator_poll_target(pnd, nmModulations, szModulations, uiPollNr, uiPeriod, &nt))  < 0) {
    nfc_perror(pnd, "nfc_initiator_poll_target");
    nfc_close(pnd);
    nfc_exit(context);
    exit(EXIT_FAILURE);
  }

...changes into:

res = nfc_initiator_select_passive_target(pnd, *nmModulations, 0, 0, &nt);

...interrupts just fine, so at least the function itself doesn't seem to be broken.

elonen commented 9 years ago

Here's the page that led me to try the Py_BEGIN_ALLOW_THREADS directive:

http://stackoverflow.com/questions/2510696/allowing-threads-from-python-after-calling-a-blocking-i-o-code-in-a-python-exten

ghost commented 7 years ago

Has anyone managed to resolve this? I've got the same issue on a Raspberry Pi, latest version of Arch Linux ARM, libnfc 1.7.1 and Python 3.6.0.

xantares commented 6 years ago

fixed in d2582a4, feedback welcome