Files
biliup/ykdl/util/jsengine.py
T
2019-06-15 15:23:35 +08:00

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