问题
I'm writing a game that uses python and webkit, and a webpage is the front-end/GUI. The PC is connected to an Arduino that controls a coin hopper and other i/o. When the Arduino sends 'coinin' over serial, I capture this in a serial-watching thread, then run some javascript on the webpage to 'add' a coin to the game.
For simplicity in troubleshooting, I set up an example that runs a test thread instead of reading serial, but the problem is the same. The thread tries to add a coin every second by running 'addcoin()' on the webpage. If you uncomment the run_javascript() line, the program core dumps.
I came up with a keyboard hack workaround. The test thread, instead of trying to run_javascript() directly, does an os.system call to xdotool to type the letters 'conn' to the program window. That window has a key-event listener, and when it gets the letters 'conn' in keybuffer[], it then runs the desired run_javascript() call to the webpage. If you copy the two files to a folder, and run the python program, you'll see the coin text count up every second (Hit BackSpace to end the program). If you try to run the javascript from the thread instead, you'll see the program core dump.
The question is, is there a better way to do this, without having to use the keyboard hack to run the javascript? Although the hack gets around the problem, it introduces a weakness in the game. You can cheat a coin in by typing 'conn' on the keyboard. I'd like to find some other way to trigger the event, without having to use the keyboard event.
Sample webpage index.htm
<html>
<script language="JavaScript" type="text/javascript">
var mycoins=0;
document.onkeydown = function(evt) {
evt = evt || window.event;
cancelKeypress = (evt.ctrlKey && evt.keyCode == 84);
return false;
};
function addcoin()
{
mycoins+=1;
id('mycoins').innerHTML="You Have "+mycoins.toString()+" coins"
}
function id(myID){return document.getElementById(myID)}
</script>
<html>
<body>
<div id=mycoins>You Have 0 Coins</div>
</body>
</html>
sample python
#!/usr/bin/python
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
gi.require_version('WebKit2', '4.0')
from gi.repository import WebKit2
import os,time,sys,threading,serial
defaultpath = os.path.dirname(os.path.realpath(__file__))
killthread=False
keybuffer=[]
buffkeys=['c','o','n','h','p','e']
myname=os.path.basename(__file__)
serial_ports=['/dev/ttyUSB0','/dev/ttyUSB1','/dev/ttyACM0','/dev/ttyACM1']
checkserial=True;
class BrowserView:
def __init__(self):
global checkserial
window = Gtk.Window()
window.connect("key-press-event", self._key_pressed, window)
self.view = WebKit2.WebView()
self.view.load_uri('file:///'+defaultpath+'/index.htm')
self.view.connect("notify::title", self.window_title_change)
window.add(self.view)
window.fullscreen()
window.show_all()
'''
######not used for this example#######################################
serial_port=""
for x in serial_ports:
#print 'trying ',x
if os.popen('ls '+x+' >/dev/null 2>&1 ; echo $?').read().strip()=='0':
serial_port=x
break;
baud=9600
if len(serial_port)>1:
self.ser = serial.Serial(serial_port, baud, timeout=0)
else:
self.view.load_uri('file:///'+defaultpath+'/signDOWN.htm?Serial%20Port%20Error|Keno%20will%20auto%20close')
checkserial=False;
if checkserial:
thread = threading.Thread(target=self.read_from_port)
thread.start()
#######################################################################
'''
#####thread test#############
thread = threading.Thread(target=self.testthread)
thread.start()
def testthread(self):
while True:
os.system('xdotool search --name '+myname+' type conn')
#self.view.run_javascript('addcoin()') #causes core dump
if killthread==True:
break;
time.sleep(1)
def read_from_port(self):
while True:
if self.ser.inWaiting()>0:
response=self.ser.readline()
print(response)
if 'coinin' in response:
os.system('xdotool search --name '+myname+' type conn')
#self.view.run_javascript('addcoin()') #causes core dump
if killthread==True:
break;
time.sleep(1)
def checkbuffer(self):
global keybuffer
if 'conn' in ''.join(str(x) for x in keybuffer):
self.view.run_javascript('addcoin()')
keybuffer=[]
def window_title_change(self, widget, param):
if not self.view.get_title():
return
os.chdir(defaultpath)
if self.view.get_title().startswith("pythondiag:::"):
message = self.view.get_title().split(":::",1)[1]
os.system('zenity --notification --text='+message+' --timeout=2')
def _key_pressed(self, widget, event, window):
global keybuffer
mykey=Gdk.keyval_name(event.keyval)
isakey=False
for x in buffkeys:
if mykey==x:
isakey=True;
if isakey:
keybuffer.append(Gdk.keyval_name(event.keyval))
else:
keybuffer=[]
self.checkbuffer()
if mykey == 'BackSpace':
self.myquit()
def myquit(self):
global killthread
killthread=True
try:
self.ser.write('clear\n')
except:
pass
Gtk.main_quit()
if __name__ == "__main__":
BrowserView()
Gtk.main()
回答1:
Update: This answer is updated for general case, original answer below.
Although GIL allows only one python thread being run at given time, we know nothing about other thread state at the moment of context switch (it's just like executing multithreaded programm on a single-core machine.) That's why you should call any not MT-safe methods from the thread they "belong" to (that includes GTK calls, which "belong" to main event loop).
If you want to call such a function, you should schedule it's execution in the main loop. Probably the easiest approach is to use idle_add. Also note, that idle_add
'ed function should
return True
or False
whether it should be called again later or not, respectively.
Your code shoul look like this:
from gi.repository import GLib
...
class ThreadedWork:
def function(self, arg):
''' function to be called in mainloop'''
if arg:
return GLib.SOURCE_REMOVE
return GLib.SOURCE_CONTINUE
def scheduler(self, function, arg):
''' scheduler (purely for readability issues) '''
GLib.idle_add(function, arg)
def thread_func(self):
''' long long thread function '''
while True:
# Do some long work
# After it is done, schedule execution of mainloop functions.
self.scheduler(self.function, True)
time.sleep(1)
Original answer: Looks like it's due to run_javascript being not MT-safe (unlike this method, for example).
from gi.repository import GLib
...
class BrowserView:
def javascript_runner(self, script_name):
GLib.idle_add(self.view.run_javascript, script_name)
def testthread(self):
while True:
os.system('xdotool search --name '+myname+' type conn')
# After long work is done, schedule execution of mainloop functions.
self.javascript_runner('addcoin()')
if killthread: # btw, there is no need to check ==True explicitly
break
time.sleep(1)
回答2:
I wanted to post the complete test code for anyone looking for a way to run webkit while also running a thread to get information back from the serial port (or any thread), and then do something useful with it. I searched for a solution to this for about week before asking the question, but couldn't find anything that specifically worked with webkit.
If you want to use the serial part, uncomment that section, and comment the testthread section. If you have any questions on how to use this, please ask and I'll do my best to answer them.
run_javascript('your_js_function()') is how python directs the webpage to do something.
The def window_title_change(self, widget, param): function is how you communicate from the webpage back to python. You also have to have the 'self.view.connect("notify::title", self.window_title_change)' line in the BrowserView Class, as shown in the sample code, so python will detect the change, and act on it.
For example, on your webpage, include this function:
function python(x)
{
document.title=""
document.title=x
}
Then to call python from your webpage to do something for you, simply call python like this:
python('pythondiag:::'hello python');
On the python side, you can write any function you need, to do anything you need to do to interact with the system. Webkit is a great solution to use HTML and javascript as a front-end/GUI, and then interact with your PC through python.
Thanks to Alexander Dmitriev's excellent solution to the original problem, here is the complete code with NO CORE DUMP...WOOHOO! I hope this can help others that have had this problem.
Sample webpage index.htm
<html>
<script language="JavaScript" type="text/javascript">
var mycoins=0;
document.onkeydown = function(evt) {
evt = evt || window.event;
cancelKeypress = (evt.ctrlKey && evt.keyCode == 84);
return false;
};
function addcoin()
{
mycoins+=1;
id('mycoins').innerHTML="You Have "+mycoins.toString()+" coins"
}
function id(myID){return document.getElementById(myID)}
</script>
<html>
<body>
<div id=mycoins>You Have 0 Coins</div>
</body>
</html>
sample python
#!/usr/bin/python
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GLib
gi.require_version('WebKit2', '4.0')
from gi.repository import WebKit2
import os,time,sys,threading,serial
defaultpath = os.path.dirname(os.path.realpath(__file__))
killthread=False
myname=os.path.basename(__file__)
serial_ports=['/dev/ttyUSB0','/dev/ttyUSB1','/dev/ttyACM0','/dev/ttyACM1']
checkserial=True;
class BrowserView:
def __init__(self):
global checkserial
window = Gtk.Window()
window.connect("key-press-event", self._key_pressed, window)
self.view = WebKit2.WebView()
self.view.load_uri('file:///'+defaultpath+'/index.htm')
self.view.connect("notify::title", self.window_title_change)
window.add(self.view)
window.fullscreen()
window.show_all()
'''
######Uncomment this to use the serial port watcher#####################
serial_port=""
for x in serial_ports:
#print 'trying ',x
if os.popen('ls '+x+' >/dev/null 2>&1 ; echo $?').read().strip()=='0':
serial_port=x
break;
baud=9600
if len(serial_port)>1:
self.ser = serial.Serial(serial_port, baud, timeout=0)
else:
self.view.load_uri('file:///'+defaultpath+'/signDOWN.htm?Serial%20Port%20Error|Keno%20will%20auto%20close')
checkserial=False;
if checkserial:
thread = threading.Thread(target=self.read_from_port)
thread.start()
#########################################################################
'''
#####thread test--comment out to use the serial port watcher#############
thread = threading.Thread(target=self.testthread)
thread.start()
#########################################################################
def javascript_runner(self, script_name):
GLib.idle_add(self.view.run_javascript, script_name)
def testthread(self):
while True:
self.javascript_runner('addcoin()')
if killthread:
break
time.sleep(1)
def read_from_port(self):
while True:
if self.ser.inWaiting()>0:
response=self.ser.readline()
print(response)
if 'coinin' in response:
self.javascript_runner('addcoin()')
if killthread:
break;
time.sleep(1)
def window_title_change(self, widget, param):
if not self.view.get_title():
return
os.chdir(defaultpath)
if self.view.get_title().startswith("pythondiag:::"):
message = self.view.get_title().split(":::",1)[1]
os.system('zenity --notification --text='+message+' --timeout=2')
def _key_pressed(self, widget, event, window):
mykey=Gdk.keyval_name(event.keyval)
print mykey
if mykey == 'BackSpace':
self.myquit()
def myquit(self):
global killthread
killthread=True
try:
self.ser.write('clear\n')
except:
pass
Gtk.main_quit()
if __name__ == "__main__":
BrowserView()
Gtk.main()
来源:https://stackoverflow.com/questions/59311107/python-and-webkit-watching-serial-port-thread-how-to-avoid-core-dump-running-j