This is a package to allow handling of interrupts inside of Pyodide. Pyodide does not have preemptive multitasking. This package enables handling keyboard interrupts in Pyodide.
This defines one context handler check_interrupts(callback, interval) which causes callback to be called every interval instructions.
>>>defcallback(): print("check")
... withcheck_interrupts(callback, 10):
... foriinrange(50):
... print(i, end=",")
0,1,check2,3,4,5,6,check7,8,9,10,11,check12,13,14,15,16,check17,18,19,20,21,check22,23,24,25,26,check27,28,29,30,31,check32,33,34,35,36,check37,38,39,40,41,check42,43,44,45,46,check47,48,49,checkIn real usage, I use the following callback:
defcheck_for_interrupt(interrupt_buffer):
defhelper():
ifinterrupt_buffer() ==0:
returnraiseKeyboardInterrupt()
returnhelperinterrupt_buffer is a javascript wrapper around a SharedArrayBuffer. On the main thread:
letuuid=uuid();letinterrupt_buffer=newInt32Array(newSharedArrayBuffer(4));pyodide_worker.postMessage({"cmd" : "execute_python", code, interrupt_buffer, uuid});letresult=awaitresponsePromise(uuid);// If user cancels, write a nonzero value into our SAB, this will signal pyodide to quit execution of code.onUserCancel(()=>{interrupt_buffer[0]=2;});On the pyodide worker thread:
self.messages={};functionhandleExecutePython(msg){// Wrap interrupt buffer in a function that gets its value// Pyodide Python <==> Javascript bindings don't understand how to get values out of the SAB directly.msg.interrupt_buffer=function(){returnmsg.interrupt_buffer[0];};messages[msg.uuid]=msg;self.pyodide.globals["handle_message"](uuid);}and then the pyodide code:
fromjsimportmessages, postMessagedefhandle_message(uuid):
msg=dict(messages[uuid])
delmessages[uuid]
# Here would use msg["cmd"] to look up handling in a dispatch.interrupt_buffer=msg["interrupt_buffer"]
# check_for_interrupt will raise a KeyboardInterrupt if "onUserCancel" handler is executed on main thread.withcheck_interrupts(check_for_interrupt(interrupt_buffer), 10_000):
result=run_code(code)
postMessage({"cmd" : "execute_pyodide_result", "result" : result, "uuid" : uuid })
defrun_code(code):
# Parse code into ast, handle errors, get result out, etc hereI quote from the MDN docs for SharedArrayBuffer:
As a baseline requirement, your document needs to be in a secure context.
For top-level documents, two headers will need to be set to cross-origin isolate your site:
Cross-Origin-Opener-Policy with same-origin as value (protects your origin from attackers) Cross-Origin-Embedder-Policy with require-corp as value (protects victims from your origin)
Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp
To check if cross origin isolation has been successful, you can test against the crossOriginIsolated property available to window and worker contexts
To build a copy for local use, I recommend creating a virtual environment and then using pip install . in that virtual environment.
To upload to pypi, we must build the package for a manylinux ABI to insure that the binaries will be compatible with most systems.
The manylinux repository provides docker images with the appropriate old versions of CentOS for us to use to build these. To build, run sudo ./docker_build_wheels.sh. Warning: This will download a ~300mb docker image the first time you do it. Note that you will need to have docker installed for this to work.
The resulting wheels will end up in the dist directory and will be suitable for upload to pypi.