233 lines
6.7 KiB
Python
233 lines
6.7 KiB
Python
#!/usr/bin/env python
|
|
|
|
'''
|
|
Simple Javascript engines' wrapper
|
|
|
|
Description:
|
|
This library wraps the system's built-in Javascript interpreter to python.
|
|
|
|
Platform:
|
|
macOS: Use JavascriptCore
|
|
Linux: Use gjs on Gnome, cjs on Cinnamon or NodeJS if installed
|
|
Windows: Use Win10's built-in Chakra, if not available use NodeJS
|
|
|
|
Usage:
|
|
|
|
from jsengine import JSEngine, javascript_is_supported
|
|
|
|
if not javascript_is_supported: # always check this first!
|
|
......
|
|
|
|
ctx = JSEngine()
|
|
ctx.eval('1 + 1') # => 2
|
|
|
|
ctx2 = JSEngine("""
|
|
function add(x, y) {
|
|
return x + y;
|
|
}
|
|
""")
|
|
ctx2.call("add", 1, 2) # => 3
|
|
'''
|
|
|
|
from __future__ import print_function
|
|
from distutils.spawn import find_executable
|
|
from subprocess import Popen, PIPE
|
|
import io
|
|
import json
|
|
import os
|
|
import platform
|
|
import re
|
|
import sys
|
|
import tempfile
|
|
|
|
### Before using this library, check this variable first!!!
|
|
javascript_is_supported = True
|
|
|
|
# Exceptions
|
|
class ProgramError(Exception):
|
|
pass
|
|
|
|
class RuntimeError(Exception):
|
|
pass
|
|
|
|
|
|
use_chakra = False
|
|
|
|
# Choose javascript interpreter
|
|
# macOS: use built-in JavaScriptCore
|
|
if platform.system() == 'Darwin':
|
|
interpreter = ['/System/Library/Frameworks/JavaScriptCore.framework/Versions/A/Resources/jsc']
|
|
|
|
# Windows: Try Chakra, if fails, load Node.js
|
|
elif platform.system() == 'Windows':
|
|
from .jsengine_chakra import ChakraHandle, chakra_available
|
|
if chakra_available:
|
|
use_chakra = True
|
|
elif find_executable('node') is not None:
|
|
interpreter = ['node']
|
|
else:
|
|
print('Please install Node.js!', file=sys.stderr)
|
|
javascript_is_supported = False
|
|
|
|
# Linux: use gjs on Gnome, cjs on Cinnamon or JavaScriptCore/NodeJS if installed
|
|
elif platform.system() == 'Linux':
|
|
if find_executable('gjs') is not None:
|
|
interpreter = ['gjs']
|
|
elif find_executable('cjs') is not None:
|
|
interpreter = ['cjs']
|
|
elif find_executable('jsc') is not None:
|
|
interpreter = ['jsc']
|
|
elif find_executable('nodejs') is not None:
|
|
interpreter = ['nodejs']
|
|
elif find_executable('node') is not None:
|
|
interpreter = ['node']
|
|
else:
|
|
print('Please install at least one of the following Javascript interpreter: gjs, cjs, nodejs', file=sys.stderr)
|
|
javascript_is_supported = False
|
|
else:
|
|
print('Sorry, the Javascript engine is currently not supported on your system', file=sys.stderr)
|
|
javascript_is_supported = False
|
|
|
|
|
|
|
|
# Inject to the script to let it return jsonlized value to python
|
|
injected_script = r'''
|
|
var exports = undefined;
|
|
(function(program, execJS) { execJS(program) })(
|
|
function() {
|
|
return eval(#{encoded_source});
|
|
},
|
|
function(program) {
|
|
var print = (this.print === undefined) ? console.log : this.print;
|
|
try {
|
|
result = program();
|
|
print("");
|
|
if (typeof result == 'undefined' && result !== null) {
|
|
print('["ok"]');
|
|
}
|
|
else {
|
|
try {
|
|
print(JSON.stringify(['ok', result]));
|
|
}
|
|
catch (err) {
|
|
print('["err", "Script returns a value with an unknown type"]');
|
|
}
|
|
}
|
|
}
|
|
catch (err) {
|
|
print(JSON.stringify(['err', '' + err]));
|
|
}
|
|
});
|
|
'''
|
|
|
|
|
|
|
|
class AbstractJSEngine:
|
|
def __init__(self, source=''):
|
|
self._source = source
|
|
|
|
def call(self, identifier, *args):
|
|
args = json.dumps(args)
|
|
code = '{identifier}.apply(this,{args})'.format(identifier=identifier, args=args)
|
|
return self._eval(code)
|
|
|
|
def eval(self, code=''):
|
|
return self._eval(code)
|
|
|
|
|
|
class ChakraJSEngine(AbstractJSEngine):
|
|
def __init__(self, source=''):
|
|
AbstractJSEngine.__init__(self, source)
|
|
self.chakra = ChakraHandle()
|
|
if source:
|
|
self.chakra.eval_js(source)
|
|
|
|
def _eval(self, code):
|
|
if not code.strip():
|
|
return None
|
|
data = json.dumps(code, ensure_ascii=True)
|
|
code = 'JSON.stringify([eval({data})]);'.format(data=data);
|
|
ok, result = self.chakra.eval_js(code)
|
|
if ok:
|
|
return json.loads(result)[0]
|
|
else:
|
|
raise ProgramError(str(result))
|
|
|
|
|
|
class ExternalJSEngine(AbstractJSEngine):
|
|
def __init__(self, source=''):
|
|
AbstractJSEngine.__init__(self, source)
|
|
self._last_code = ''
|
|
|
|
def _eval(self, code):
|
|
# TODO: may need a thread lock, if running multithreading
|
|
if not code.strip():
|
|
return None
|
|
if self._last_code:
|
|
self._source += '\n' + self._last_code
|
|
self._last_code = code
|
|
data = json.dumps(code, ensure_ascii=True)
|
|
code = 'return eval({data});'.format(data=data)
|
|
return self._exec(code)
|
|
|
|
def _exec(self, code):
|
|
if self._source:
|
|
code = self._source + '\n' + code
|
|
code = self._inject_script(code)
|
|
output = self._run_interpreter_with_tempfile(code)
|
|
output = output.replace('\r\n', '\n').replace('\r', '\n')
|
|
last_line = output.split('\n')[-2]
|
|
ret = json.loads(last_line)
|
|
if len(ret) == 1:
|
|
return None
|
|
status, value = ret
|
|
if status == 'ok':
|
|
return value
|
|
else:
|
|
raise ProgramError(value)
|
|
|
|
def _run_interpreter_with_tempfile(self, code):
|
|
(fd, filename) = tempfile.mkstemp(prefix='execjs', suffix='.js')
|
|
os.close(fd)
|
|
try:
|
|
# decoding in python2
|
|
if hasattr(code, 'decode'):
|
|
code = code.decode('utf8')
|
|
with io.open(filename, 'w', encoding='utf8') as fp:
|
|
fp.write(code)
|
|
|
|
cmd = interpreter + [filename]
|
|
p = None
|
|
try:
|
|
p = Popen(cmd, stdout=PIPE, universal_newlines=True)
|
|
stdoutdata, stderrdata = p.communicate()
|
|
ret = p.wait()
|
|
finally:
|
|
del p
|
|
if ret != 0:
|
|
raise RuntimeError('Javascript interpreter returns non-zero value!')
|
|
return stdoutdata
|
|
finally:
|
|
os.remove(filename)
|
|
|
|
def _inject_script(self, source):
|
|
encoded_source = \
|
|
'(function(){ ' + \
|
|
self._encode_unicode_codepoints(source) + \
|
|
' })()'
|
|
return injected_script.replace('#{encoded_source}', json.dumps(encoded_source))
|
|
|
|
def _encode_unicode_codepoints(self, str):
|
|
codepoint_format = '\\u{0:04x}'.format
|
|
def codepoint(m):
|
|
return codepoint_format(ord(m.group(0)))
|
|
return re.sub('[^\x00-\x7f]', codepoint, str)
|
|
|
|
|
|
if use_chakra:
|
|
class JSEngine(ChakraJSEngine):
|
|
pass
|
|
else:
|
|
class JSEngine(ExternalJSEngine):
|
|
pass
|