diff --git a/controllers/home.py b/controllers/home.py index da1f4d5875b5afb816f3391b36712a87e2f47e83..9191ce3ee74030ffb527455320b4ed7ccd78d315 100644 --- a/controllers/home.py +++ b/controllers/home.py @@ -7,7 +7,7 @@ import json import os -from flask import Blueprint,abort,render_template,render_template_string,url_for,redirect,make_response,send_from_directory +from flask import Blueprint,abort,render_template,render_template_string,url_for,redirect,make_response,send_from_directory,request from controllers.service import storage_service,rules_service from controllers.classes import getClasses,getClassInfo @@ -202,6 +202,9 @@ def merged_hide(merged_config): @home.route('/config/') def config_render(mode): # print(dict(app.config)) + UA = request.headers['User-Agent'] + ISTVB = 'okhttp/3' in UA + logger.info(UA) if mode == 1: jyw_ip = getHost(mode) logger.info(jyw_ip) @@ -229,7 +232,7 @@ def config_render(mode): rules = getRules('js',js_mode) rules = get_multi_rules(rules) # html = render_template('config.txt',rules=getRules('js'),host=host,mode=mode,jxs=jxs,base64Encode=base64Encode,config=new_conf) - html = render_template('config.txt',pys=pys,rules=rules,host=host,mode=mode,js_mode=js_mode,jxs=jxs,alists=alists,alists_str=alists_str,live_url=live_url,config=new_conf) + html = render_template('config.txt',UA=UA,ISTVB=ISTVB,pys=pys,rules=rules,host=host,mode=mode,js_mode=js_mode,jxs=jxs,alists=alists,alists_str=alists_str,live_url=live_url,config=new_conf) merged_config = custom_merge(parseText(html),customConfig) # print(merged_config['sites']) diff --git a/js/version.txt b/js/version.txt index 4764627f92506cb1851115abfa9b5635ee52b92a..03d70eaebc49e5ecbe8c536e9efbb212553d35ae 100644 --- a/js/version.txt +++ b/js/version.txt @@ -1 +1 @@ -3.9.2 \ No newline at end of file +3.9.3 \ No newline at end of file diff --git a/libs/drpy.ym.js b/libs/drpy.ym.js new file mode 100644 index 0000000000000000000000000000000000000000..3909a5c90c710887b362b486b54966edaf6459a3 --- /dev/null +++ b/libs/drpy.ym.js @@ -0,0 +1,4 @@ +import './util-ym.js' +import dr from './drpy.js' + +__JS_SPIDER__ = dr.DRPY() \ No newline at end of file diff --git "a/py/qkjs/pr\344\277\256\346\224\271/module.c" "b/py/qkjs/pr\344\277\256\346\224\271/module.c" new file mode 100644 index 0000000000000000000000000000000000000000..3f9a4495d233e9ddabd2ec6168a66801b63ceefd --- /dev/null +++ "b/py/qkjs/pr\344\277\256\346\224\271/module.c" @@ -0,0 +1,829 @@ +#include +#include + +#include "third-party/quickjs.h" + +// Node of Python callable that the context needs to keep available. +typedef struct PythonCallableNode PythonCallableNode; +struct PythonCallableNode { + PyObject *obj; + // Internal ID of the callable function. "magic" is QuickJS terminology. + int magic; + PythonCallableNode *next; +}; + +// Keeps track of the time if we are using a time limit. +typedef struct { + clock_t start; + clock_t limit; +} InterruptData; + +// The data of the type _quickjs.Context. +typedef struct { + PyObject_HEAD JSRuntime *runtime; + JSContext *context; + int has_time_limit; + clock_t time_limit; + // Used when releasing the GIL. + PyThreadState *thread_state; + InterruptData interrupt_data; + // NULL-terminated singly linked list of callable Python objects that we need to keep alive. + PythonCallableNode *python_callables; +} ContextData; + +// The data of the type _quickjs.Object. +typedef struct { + PyObject_HEAD; + ContextData *context; + JSValue object; +} ObjectData; + +// The exception raised by this module. +static PyObject *JSException = NULL; +static PyObject *StackOverflow = NULL; +// Converts a JSValue to a Python object. +// +// Takes ownership of the JSValue and will deallocate it (refcount reduced by 1). +static PyObject *quickjs_to_python(ContextData *context_obj, JSValue value); +// Whether converting item to QuickJS would be possible. +static int python_to_quickjs_possible(ContextData *context, PyObject *item); +// Converts item to QuickJS. +// +// If the Python object is not possible to convert to JS, undefined will be returned. This fallback +// will not be used if python_to_quickjs_possible returns 1. +static JSValueConst python_to_quickjs(ContextData *context, PyObject *item); + +static PyTypeObject Object; + +// Returns nonzero if we should stop due to a time limit. +static int js_interrupt_handler(JSRuntime *rt, void *opaque) { + InterruptData *data = opaque; + if (clock() - data->start >= data->limit) { + return 1; + } else { + return 0; + } +} + +// Sets up a context and an InterruptData struct if the context has a time limit. +static void setup_time_limit(ContextData *context, InterruptData *interrupt_data) { + if (context->has_time_limit) { + JS_SetInterruptHandler(context->runtime, js_interrupt_handler, interrupt_data); + interrupt_data->limit = context->time_limit; + interrupt_data->start = clock(); + } +} + +// Restores the context if the context has a time limit. +static void teardown_time_limit(ContextData *context) { + if (context->has_time_limit) { + JS_SetInterruptHandler(context->runtime, NULL, NULL); + } +} + +// This method is always called in a context before running JS code in QuickJS. It sets up time +// limites, releases the GIL etc. +static void prepare_call_js(ContextData *context) { + // We release the GIL in order to speed things up for certain use cases. + assert(!context->thread_state); + context->thread_state = PyEval_SaveThread(); + setup_time_limit(context, &context->interrupt_data); +} + +// This method is called right after returning from running JS code. Aquires the GIL etc. +static void end_call_js(ContextData *context) { + teardown_time_limit(context); + assert(context->thread_state); + PyEval_RestoreThread(context->thread_state); + context->thread_state = NULL; +} + +// Called when Python is called again from inside QuickJS. +static void prepare_call_python(ContextData *context) { + assert(context->thread_state); + PyEval_RestoreThread(context->thread_state); + context->thread_state = NULL; +} + +// Called when the operation started by prepare_call_python is done. +static void end_call_python(ContextData *context) { + assert(!context->thread_state); + context->thread_state = PyEval_SaveThread(); +} + +// GC traversal. +static int object_traverse(ObjectData *self, visitproc visit, void *arg) { + Py_VISIT(self->context); + return 0; +} + +// Creates an instance of the Object class. +static PyObject *object_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { + ObjectData *self = PyObject_GC_New(ObjectData, type); + if (self != NULL) { + self->context = NULL; + } + return (PyObject *)self; +} + +// Deallocates an instance of the Object class. +static void object_dealloc(ObjectData *self) { + if (self->context) { + PyObject_GC_UnTrack(self); + JS_FreeValue(self->context->context, self->object); + // We incremented the refcount of the context when we created this object, so we should + // decrease it now so we don't leak memory. + Py_DECREF(self->context); + } + PyObject_GC_Del(self); +} + +// _quickjs.Object.get +// +// Gets a Javascript property of the object. +static PyObject *object_get(ObjectData *self, PyObject *args) { + const char *name; + if (!PyArg_ParseTuple(args, "s", &name)) { + return NULL; + } + JSValue value = JS_GetPropertyStr(self->context->context, self->object, name); + return quickjs_to_python(self->context, value); +} + +static JSValue js_c_function( + JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic) { + ContextData *context = (ContextData *)JS_GetContextOpaque(ctx); + if (context->has_time_limit) { + return JS_ThrowInternalError(ctx, "Can not call into Python with a time limit set."); + } + PythonCallableNode *node = context->python_callables; + while (node && node->magic != magic) { + node = node->next; + } + if (!node) { + return JS_ThrowInternalError(ctx, "Internal error."); + } + prepare_call_python(context); + + PyObject *args = PyTuple_New(argc); + if (!args) { + end_call_python(context); + return JS_ThrowOutOfMemory(ctx); + } + int tuple_success = 1; + for (int i = 0; i < argc; ++i) { + PyObject *arg = quickjs_to_python(context, JS_DupValue(ctx, argv[i])); + if (!arg) { + tuple_success = 0; + break; + } + PyTuple_SET_ITEM(args, i, arg); + } + if (!tuple_success) { + Py_DECREF(args); + end_call_python(context); + return JS_ThrowInternalError(ctx, "Internal error: could not convert args."); + } + + PyObject *result = PyObject_CallObject(node->obj, args); + Py_DECREF(args); + if (!result) { + end_call_python(context); + return JS_ThrowInternalError(ctx, "Python call failed."); + } + JSValue js_result = JS_NULL; + if (python_to_quickjs_possible(context, result)) { + js_result = python_to_quickjs(context, result); + } else { + PyErr_Clear(); + js_result = JS_ThrowInternalError(ctx, "Can not convert Python result to JS."); + } + Py_DECREF(result); + + end_call_python(context); + return js_result; +} + +// _quickjs.Object.set +// +// Sets a Javascript property to the object. Callables are supported. +static PyObject *object_set(ObjectData *self, PyObject *args) { + const char *name; + PyObject *item; + if (!PyArg_ParseTuple(args, "sO", &name, &item)) { + return NULL; + } + int ret = 0; + if (PyCallable_Check(item) && (!PyObject_IsInstance(item, (PyObject *)&Object) || JS_IsFunction( + self->context->context, ((ObjectData *)item)->object))) { + PythonCallableNode *node = PyMem_Malloc(sizeof(PythonCallableNode)); + if (!node) { + return NULL; + } + Py_INCREF(item); + node->magic = 0; + if (self->context->python_callables) { + node->magic = self->context->python_callables->magic + 1; + } + node->obj = item; + node->next = self->context->python_callables; + self->context->python_callables = node; + + JSValue function = JS_NewCFunctionMagic( + self->context->context, + js_c_function, + name, + 0, // TODO: Should we allow setting the .length of the function to something other than 0? + JS_CFUNC_generic_magic, + node->magic); + // If this fails we don't notify the caller of this function. + ret = JS_SetPropertyStr(self->context->context, self->object, name, function); + if (ret != 1) { + PyErr_SetString(PyExc_TypeError, "Failed setting the variable as a callable."); + return NULL; + } else { + Py_RETURN_NONE; + } + } else { + if (python_to_quickjs_possible(self->context, item)) { + ret = JS_SetPropertyStr(self->context->context, self->object, name, + python_to_quickjs(self->context, item)); + if (ret != 1) { + PyErr_SetString(PyExc_TypeError, "Failed setting the variable."); + } + } + if (ret == 1) { + Py_RETURN_NONE; + } else { + return NULL; + } + } +} + +// _quickjs.Object.__call__ +static PyObject *object_call(ObjectData *self, PyObject *args, PyObject *kwds); + +// _quickjs.Object.json +// +// Returns the JSON representation of the object as a Python string. +static PyObject *object_json(ObjectData *self) { + JSContext *context = self->context->context; + JSValue json_string = JS_JSONStringify(context, self->object, JS_UNDEFINED, JS_UNDEFINED); + return quickjs_to_python(self->context, json_string); +} + +// All methods of the _quickjs.Object class. +static PyMethodDef object_methods[] = { + {"get", (PyCFunction)object_get, METH_VARARGS, "Gets a Javascript property of the object."}, + {"set", (PyCFunction)object_set, METH_VARARGS, "Sets a Javascript property to the object."}, + {"json", (PyCFunction)object_json, METH_NOARGS, "Converts to a JSON string."}, + {NULL} /* Sentinel */ +}; + +// Define the quickjs.Object type. +static PyTypeObject Object = {PyVarObject_HEAD_INIT(NULL, 0).tp_name = "_quickjs.Object", + .tp_doc = "Quickjs object", + .tp_basicsize = sizeof(ObjectData), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, + .tp_traverse = (traverseproc)object_traverse, + .tp_new = object_new, + .tp_dealloc = (destructor)object_dealloc, + .tp_call = (ternaryfunc)object_call, + .tp_methods = object_methods}; + +// Whether converting item to QuickJS would be possible. +static int python_to_quickjs_possible(ContextData *context, PyObject *item) { + if (PyBool_Check(item)) { + return 1; + } else if (PyLong_Check(item)) { + return 1; + } else if (PyFloat_Check(item)) { + return 1; + } else if (item == Py_None) { + return 1; + } else if (PyUnicode_Check(item)) { + return 1; + } else if (PyObject_IsInstance(item, (PyObject *)&Object)) { + ObjectData *object = (ObjectData *)item; + if (object->context != context) { + PyErr_Format(PyExc_ValueError, "Can not mix JS objects from different contexts."); + return 0; + } + return 1; + } else { + PyErr_Format(PyExc_TypeError, + "Unsupported type when converting a Python object to quickjs: %s.", + Py_TYPE(item)->tp_name); + return 0; + } +} + +// Converts item to QuickJS. +// +// If the Python object is not possible to convert to JS, undefined will be returned. This fallback +// will not be used if python_to_quickjs_possible returns 1. +static JSValueConst python_to_quickjs(ContextData *context, PyObject *item) { + if (PyBool_Check(item)) { + return JS_MKVAL(JS_TAG_BOOL, item == Py_True ? 1 : 0); + } else if (PyLong_Check(item)) { + int overflow; + long value = PyLong_AsLongAndOverflow(item, &overflow); + if (overflow) { + PyObject *float_value = PyNumber_Float(item); + double double_value = PyFloat_AsDouble(float_value); + Py_DECREF(float_value); + return JS_NewFloat64(context->context, double_value); + } else { + return JS_MKVAL(JS_TAG_INT, value); + } + } else if (PyFloat_Check(item)) { + return JS_NewFloat64(context->context, PyFloat_AsDouble(item)); + } else if (item == Py_None) { + return JS_NULL; + } else if (PyUnicode_Check(item)) { + return JS_NewString(context->context, PyUnicode_AsUTF8(item)); + } else if (PyObject_IsInstance(item, (PyObject *)&Object)) { + return JS_DupValue(context->context, ((ObjectData *)item)->object); + } else { + // Can not happen if python_to_quickjs_possible passes. + return JS_UNDEFINED; + } +} + +// _quickjs.Object.__call__ +static PyObject *object_call(ObjectData *self, PyObject *args, PyObject *kwds) { + if (self->context == NULL) { + // This object does not have a context and has not been created by this module. + Py_RETURN_NONE; + } + + // We first loop through all arguments and check that they are supported without doing anything. + // This makes the cleanup code simpler for the case where we have to raise an error. + const int nargs = PyTuple_Size(args); + for (int i = 0; i < nargs; ++i) { + PyObject *item = PyTuple_GetItem(args, i); + if (!python_to_quickjs_possible(self->context, item)) { + return NULL; + } + } + + // Now we know that all arguments are supported and we can convert them. + JSValueConst *jsargs = malloc(nargs * sizeof(JSValueConst)); + for (int i = 0; i < nargs; ++i) { + PyObject *item = PyTuple_GetItem(args, i); + jsargs[i] = python_to_quickjs(self->context, item); + } + + prepare_call_js(self->context); + JSValue value; + value = JS_Call(self->context->context, self->object, JS_NULL, nargs, jsargs); + for (int i = 0; i < nargs; ++i) { + JS_FreeValue(self->context->context, jsargs[i]); + } + free(jsargs); + end_call_js(self->context); + return quickjs_to_python(self->context, value); +} + +// Converts the current Javascript exception to a Python exception via a C string. +static void quickjs_exception_to_python(JSContext *context) { + JSValue exception = JS_GetException(context); + const char *cstring = JS_ToCString(context, exception); + const char *stack_cstring = NULL; + if (!JS_IsNull(exception) && !JS_IsUndefined(exception)) { + JSValue stack = JS_GetPropertyStr(context, exception, "stack"); + if (!JS_IsException(stack)) { + stack_cstring = JS_ToCString(context, stack); + JS_FreeValue(context, stack); + } + } + if (cstring != NULL) { + const char *safe_stack_cstring = stack_cstring ? stack_cstring : ""; + if (strstr(cstring, "stack overflow") != NULL) { + PyErr_Format(StackOverflow, "%s\n%s", cstring, safe_stack_cstring); + } else { + PyErr_Format(JSException, "%s\n%s", cstring, safe_stack_cstring); + } + } else { + // This has been observed to happen when different threads have used the same QuickJS + // runtime, but not at the same time. + // Could potentially be another problem though, since JS_ToCString may return NULL. + PyErr_Format(JSException, + "(Failed obtaining QuickJS error string. Concurrency issue?)"); + } + JS_FreeCString(context, cstring); + JS_FreeCString(context, stack_cstring); + JS_FreeValue(context, exception); +} + +// Converts a JSValue to a Python object. +// +// Takes ownership of the JSValue and will deallocate it (refcount reduced by 1). +static PyObject *quickjs_to_python(ContextData *context_obj, JSValue value) { + JSContext *context = context_obj->context; + int tag = JS_VALUE_GET_TAG(value); + // A return value of NULL means an exception. + PyObject *return_value = NULL; + + if (tag == JS_TAG_INT) { + return_value = Py_BuildValue("i", JS_VALUE_GET_INT(value)); + } else if (tag == JS_TAG_BIG_INT) { + const char *cstring = JS_ToCString(context, value); + return_value = PyLong_FromString(cstring, NULL, 10); + JS_FreeCString(context, cstring); + } else if (tag == JS_TAG_BOOL) { + return_value = Py_BuildValue("O", JS_VALUE_GET_BOOL(value) ? Py_True : Py_False); + } else if (tag == JS_TAG_NULL) { + return_value = Py_None; + } else if (tag == JS_TAG_UNDEFINED) { + return_value = Py_None; + } else if (tag == JS_TAG_EXCEPTION) { + quickjs_exception_to_python(context); + } else if (tag == JS_TAG_FLOAT64) { + return_value = Py_BuildValue("d", JS_VALUE_GET_FLOAT64(value)); + } else if (tag == JS_TAG_STRING) { + const char *cstring = JS_ToCString(context, value); + return_value = Py_BuildValue("s", cstring); + JS_FreeCString(context, cstring); + } else if (tag == JS_TAG_OBJECT || tag == JS_TAG_MODULE || tag == JS_TAG_SYMBOL) { + // This is a Javascript object or function. We wrap it in a _quickjs.Object. + return_value = PyObject_CallObject((PyObject *)&Object, NULL); + ObjectData *object = (ObjectData *)return_value; + // This is important. Otherwise, the context may be deallocated before the object, which + // will result in a segfault with high probability. + Py_INCREF(context_obj); + object->context = context_obj; + PyObject_GC_Track(object); + object->object = JS_DupValue(context, value); + } else { + PyErr_Format(PyExc_TypeError, "Unknown quickjs tag: %d", tag); + } + + JS_FreeValue(context, value); + if (return_value == Py_None) { + // Can not simply return PyNone for refcounting reasons. + Py_RETURN_NONE; + } + return return_value; +} + +static PyObject *test(PyObject *self, PyObject *args) { + return Py_BuildValue("i", 42); +} + +// Global state of the module. Currently none. +struct module_state {}; + +// GC traversal. +static int context_traverse(ContextData *self, visitproc visit, void *arg) { + PythonCallableNode *node = self->python_callables; + while (node) { + Py_VISIT(node->obj); + node = node->next; + } + return 0; +} + +// GC clearing. Object does not have a clearing method, therefore dependency cycles +// between Context and Object will always be cleared starting here. +static int context_clear(ContextData *self) { + PythonCallableNode *node = self->python_callables; + while (node) { + Py_CLEAR(node->obj); + node = node->next; + } + return 0; +} + +// Creates an instance of the _quickjs.Context class. +static PyObject *context_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { + ContextData *self = PyObject_GC_New(ContextData, type); + if (self != NULL) { + // We never have different contexts for the same runtime. This way, different + // _quickjs.Context can be used concurrently. + self->runtime = JS_NewRuntime(); + self->context = JS_NewContext(self->runtime); + self->has_time_limit = 0; + self->time_limit = 0; + self->thread_state = NULL; + self->python_callables = NULL; + JS_SetContextOpaque(self->context, self); + PyObject_GC_Track(self); + } + return (PyObject *)self; +} + +// Deallocates an instance of the _quickjs.Context class. +static void context_dealloc(ContextData *self) { + JS_FreeContext(self->context); + JS_FreeRuntime(self->runtime); + PyObject_GC_UnTrack(self); + PythonCallableNode *node = self->python_callables; + self->python_callables = NULL; + while (node) { + PythonCallableNode *this = node; + node = node->next; + // this->obj may already be NULL if GC'ed right before through context_clear. + Py_XDECREF(this->obj); + PyMem_Free(this); + } + PyObject_GC_Del(self); +} + +// Evaluates a Python string as JS and returns the result as a Python object. Will return +// _quickjs.Object for complex types (other than e.g. str, int). +static PyObject *context_eval_internal(ContextData *self, PyObject *args, int eval_type) { + const char *code; + if (!PyArg_ParseTuple(args, "s", &code)) { + return NULL; + } + prepare_call_js(self); + JSValue value; + value = JS_Eval(self->context, code, strlen(code), "", eval_type); + end_call_js(self); + return quickjs_to_python(self, value); +} + +// _quickjs.Context.eval +// +// Evaluates a Python string as JS and returns the result as a Python object. Will return +// _quickjs.Object for complex types (other than e.g. str, int). +static PyObject *context_eval(ContextData *self, PyObject *args) { + return context_eval_internal(self, args, JS_EVAL_TYPE_GLOBAL); +} + +// _quickjs.Context.module +// +// Evaluates a Python string as JS module. Otherwise identical to eval. +static PyObject *context_module(ContextData *self, PyObject *args) { + return context_eval_internal(self, args, JS_EVAL_TYPE_MODULE); +} + +// _quickjs.Context.execute_pending_job +// +// If there are pending jobs, executes one and returns True. Else returns False. +static PyObject *context_execute_pending_job(ContextData *self) { + prepare_call_js(self); + JSContext *ctx; + int ret = JS_ExecutePendingJob(self->runtime, &ctx); + end_call_js(self); + if (ret > 0) { + Py_RETURN_TRUE; + } else if (ret == 0) { + Py_RETURN_FALSE; + } else { + quickjs_exception_to_python(ctx); + return NULL; + } +} + +// _quickjs.Context.parse_json +// +// Evaluates a Python string as JSON and returns the result as a Python object. Will +// return _quickjs.Object for complex types (other than e.g. str, int). +static PyObject *context_parse_json(ContextData *self, PyObject *args) { + const char *data; + if (!PyArg_ParseTuple(args, "s", &data)) { + return NULL; + } + JSValue value; + Py_BEGIN_ALLOW_THREADS; + value = JS_ParseJSON(self->context, data, strlen(data), "context_parse_json.json"); + Py_END_ALLOW_THREADS; + return quickjs_to_python(self, value); +} + +// _quickjs.Context.get_global +// +// Retrieves the global object of the JS context. +static PyObject *context_get_global(ContextData *self) { + return quickjs_to_python(self, JS_GetGlobalObject(self->context)); +} + +// _quickjs.Context.get +// +// Retrieves a global variable from the JS context. +static PyObject *context_get(ContextData *self, PyObject *args) { + PyErr_WarnEx(PyExc_DeprecationWarning, + "Context.get is deprecated, use Context.get_global().get instead.", 1); + PyObject *global = context_get_global(self); + if (global == NULL) { + return NULL; + } + PyObject *ret = object_get((ObjectData *)global, args); + Py_DECREF(global); + return ret; +} + +// _quickjs.Context.set +// +// Sets a global variable to the JS context. +static PyObject *context_set(ContextData *self, PyObject *args) { + PyErr_WarnEx(PyExc_DeprecationWarning, + "Context.set is deprecated, use Context.get_global().set instead.", 1); + PyObject *global = context_get_global(self); + if (global == NULL) { + return NULL; + } + PyObject *ret = object_set((ObjectData *)global, args); + Py_DECREF(global); + return ret; +} + +// _quickjs.Context.set_memory_limit +// +// Sets the memory limit of the context. +static PyObject *context_set_memory_limit(ContextData *self, PyObject *args) { + Py_ssize_t limit; + if (!PyArg_ParseTuple(args, "n", &limit)) { + return NULL; + } + JS_SetMemoryLimit(self->runtime, limit); + Py_RETURN_NONE; +} + +// _quickjs.Context.set_time_limit +// +// Sets the CPU time limit of the context. This will be used in an interrupt handler. +static PyObject *context_set_time_limit(ContextData *self, PyObject *args) { + double limit; + if (!PyArg_ParseTuple(args, "d", &limit)) { + return NULL; + } + if (limit < 0) { + self->has_time_limit = 0; + } else { + self->has_time_limit = 1; + self->time_limit = (clock_t)(limit * CLOCKS_PER_SEC); + } + Py_RETURN_NONE; +} + +// _quickjs.Context.set_max_stack_size +// +// Sets the max stack size in bytes. +static PyObject *context_set_max_stack_size(ContextData *self, PyObject *args) { + Py_ssize_t limit; + if (!PyArg_ParseTuple(args, "n", &limit)) { + return NULL; + } + JS_SetMaxStackSize(self->runtime, limit); + Py_RETURN_NONE; +} + +// _quickjs.Context.memory +// +// Sets the CPU time limit of the context. This will be used in an interrupt handler. +static PyObject *context_memory(ContextData *self) { + PyObject *dict = PyDict_New(); + if (dict == NULL) { + return NULL; + } + JSMemoryUsage usage; + JS_ComputeMemoryUsage(self->runtime, &usage); +#define MEM_USAGE_ADD_TO_DICT(key) \ + { \ + PyObject *value = PyLong_FromLongLong(usage.key); \ + if (PyDict_SetItemString(dict, #key, value) != 0) { \ + return NULL; \ + } \ + Py_DECREF(value); \ + } + MEM_USAGE_ADD_TO_DICT(malloc_size); + MEM_USAGE_ADD_TO_DICT(malloc_limit); + MEM_USAGE_ADD_TO_DICT(memory_used_size); + MEM_USAGE_ADD_TO_DICT(malloc_count); + MEM_USAGE_ADD_TO_DICT(memory_used_count); + MEM_USAGE_ADD_TO_DICT(atom_count); + MEM_USAGE_ADD_TO_DICT(atom_size); + MEM_USAGE_ADD_TO_DICT(str_count); + MEM_USAGE_ADD_TO_DICT(str_size); + MEM_USAGE_ADD_TO_DICT(obj_count); + MEM_USAGE_ADD_TO_DICT(obj_size); + MEM_USAGE_ADD_TO_DICT(prop_count); + MEM_USAGE_ADD_TO_DICT(prop_size); + MEM_USAGE_ADD_TO_DICT(shape_count); + MEM_USAGE_ADD_TO_DICT(shape_size); + MEM_USAGE_ADD_TO_DICT(js_func_count); + MEM_USAGE_ADD_TO_DICT(js_func_size); + MEM_USAGE_ADD_TO_DICT(js_func_code_size); + MEM_USAGE_ADD_TO_DICT(js_func_pc2line_count); + MEM_USAGE_ADD_TO_DICT(js_func_pc2line_size); + MEM_USAGE_ADD_TO_DICT(c_func_count); + MEM_USAGE_ADD_TO_DICT(array_count); + MEM_USAGE_ADD_TO_DICT(fast_array_count); + MEM_USAGE_ADD_TO_DICT(fast_array_elements); + MEM_USAGE_ADD_TO_DICT(binary_object_count); + MEM_USAGE_ADD_TO_DICT(binary_object_size); + return dict; +} + +// _quickjs.Context.gc +// +// Runs garbage collection. +static PyObject *context_gc(ContextData *self) { + JS_RunGC(self->runtime); + Py_RETURN_NONE; +} + +static PyObject *context_add_callable(ContextData *self, PyObject *args) { + PyErr_WarnEx(PyExc_DeprecationWarning, + "Context.add_callable is deprecated, use Context.get_global().set instead.", 1); + PyObject *global = context_get_global(self); + if (global == NULL) { + return NULL; + } + PyObject *ret = object_set((ObjectData *)global, args); + Py_DECREF(global); + return ret; +} + +// All methods of the _quickjs.Context class. +static PyMethodDef context_methods[] = { + {"eval", (PyCFunction)context_eval, METH_VARARGS, "Evaluates a Javascript string."}, + {"module", + (PyCFunction)context_module, + METH_VARARGS, + "Evaluates a Javascript string as a module."}, + {"execute_pending_job", (PyCFunction)context_execute_pending_job, METH_NOARGS, "Executes a pending job."}, + {"parse_json", (PyCFunction)context_parse_json, METH_VARARGS, "Parses a JSON string."}, + {"get_global", (PyCFunction)context_get_global, METH_NOARGS, "Gets the Javascript global object."}, + {"get", (PyCFunction)context_get, METH_VARARGS, "Gets a Javascript global variable."}, + {"set", (PyCFunction)context_set, METH_VARARGS, "Sets a Javascript global variable."}, + {"set_memory_limit", + (PyCFunction)context_set_memory_limit, + METH_VARARGS, + "Sets the memory limit in bytes."}, + {"set_time_limit", + (PyCFunction)context_set_time_limit, + METH_VARARGS, + "Sets the CPU time limit in seconds (C function clock() is used)."}, + {"set_max_stack_size", + (PyCFunction)context_set_max_stack_size, + METH_VARARGS, + "Sets the maximum stack size in bytes. Default is 256kB."}, + {"memory", (PyCFunction)context_memory, METH_NOARGS, "Returns the memory usage as a dict."}, + {"gc", (PyCFunction)context_gc, METH_NOARGS, "Runs garbage collection."}, + {"add_callable", (PyCFunction)context_add_callable, METH_VARARGS, "Wraps a Python callable."}, + {NULL} /* Sentinel */ +}; + +// Define the _quickjs.Context type. +static PyTypeObject Context = {PyVarObject_HEAD_INIT(NULL, 0).tp_name = "_quickjs.Context", + .tp_doc = "Quickjs context", + .tp_basicsize = sizeof(ContextData), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, + .tp_traverse = (traverseproc)context_traverse, + .tp_clear = (inquiry)context_clear, + .tp_new = context_new, + .tp_dealloc = (destructor)context_dealloc, + .tp_methods = context_methods}; + +// All global methods in _quickjs. +static PyMethodDef myextension_methods[] = {{"test", (PyCFunction)test, METH_NOARGS, NULL}, + {NULL, NULL}}; + +// Define the _quickjs module. +static struct PyModuleDef moduledef = {PyModuleDef_HEAD_INIT, + "quickjs", + NULL, + sizeof(struct module_state), + myextension_methods, + NULL, + NULL, + NULL, + NULL}; + +// This function runs when the module is first imported. +PyMODINIT_FUNC PyInit__quickjs(void) { + if (PyType_Ready(&Context) < 0) { + return NULL; + } + if (PyType_Ready(&Object) < 0) { + return NULL; + } + + PyObject *module = PyModule_Create(&moduledef); + if (module == NULL) { + return NULL; + } + + JSException = PyErr_NewException("_quickjs.JSException", NULL, NULL); + if (JSException == NULL) { + return NULL; + } + StackOverflow = PyErr_NewException("_quickjs.StackOverflow", JSException, NULL); + if (StackOverflow == NULL) { + return NULL; + } + + Py_INCREF(&Context); + PyModule_AddObject(module, "Context", (PyObject *)&Context); + Py_INCREF(&Object); + PyModule_AddObject(module, "Object", (PyObject *)&Object); + PyModule_AddObject(module, "JSException", JSException); + PyModule_AddObject(module, "StackOverflow", StackOverflow); + return module; +} diff --git "a/py/qkjs/pr\344\277\256\346\224\271/test_quickjs.py" "b/py/qkjs/pr\344\277\256\346\224\271/test_quickjs.py" new file mode 100644 index 0000000000000000000000000000000000000000..f5adfc790fc3ea170ab454e6c90bb7c4e1945f04 --- /dev/null +++ "b/py/qkjs/pr\344\277\256\346\224\271/test_quickjs.py" @@ -0,0 +1,635 @@ +import concurrent.futures +import json +import unittest + +import quickjs + + +class LoadModule(unittest.TestCase): + def test_42(self): + self.assertEqual(quickjs.test(), 42) + + +class Context(unittest.TestCase): + def setUp(self): + self.context = quickjs.Context() + + def test_eval_int(self): + self.assertEqual(self.context.eval("40 + 2"), 42) + + def test_eval_float(self): + self.assertEqual(self.context.eval("40.0 + 2.0"), 42.0) + + def test_eval_str(self): + self.assertEqual(self.context.eval("'4' + '2'"), "42") + + def test_eval_bool(self): + self.assertEqual(self.context.eval("true || false"), True) + self.assertEqual(self.context.eval("true && false"), False) + + def test_eval_null(self): + self.assertIsNone(self.context.eval("null")) + + def test_eval_undefined(self): + self.assertIsNone(self.context.eval("undefined")) + + def test_wrong_type(self): + with self.assertRaises(TypeError): + self.assertEqual(self.context.eval(1), 42) + + def test_context_between_calls(self): + self.context.eval("x = 40; y = 2;") + self.assertEqual(self.context.eval("x + y"), 42) + + def test_function(self): + self.context.eval(""" + function special(x) { + return 40 + x; + } + """) + self.assertEqual(self.context.eval("special(2)"), 42) + + def test_get(self): + self.context.eval("x = 42; y = 'foo';") + self.assertEqual(self.context.get("x"), 42) + self.assertEqual(self.context.get("y"), "foo") + self.assertEqual(self.context.get("z"), None) + + def test_set(self): + self.context.eval("x = 'overriden'") + self.context.set("x", 42) + self.context.set("y", "foo") + self.assertTrue(self.context.eval("x == 42")) + self.assertTrue(self.context.eval("y == 'foo'")) + + def test_module(self): + self.context.module(""" + export function test() { + return 42; + } + """) + + def test_error(self): + with self.assertRaisesRegex(quickjs.JSException, "ReferenceError: 'missing' is not defined"): + self.context.eval("missing + missing") + + def test_lifetime(self): + def get_f(): + context = quickjs.Context() + f = context.eval(""" + a = function(x) { + return 40 + x; + } + """) + return f + + f = get_f() + self.assertTrue(f) + # The context has left the scope after f. f needs to keep the context alive for the + # its lifetime. Otherwise, we will get problems. + + def test_backtrace(self): + try: + self.context.eval(""" + function funcA(x) { + x.a.b = 1; + } + function funcB(x) { + funcA(x); + } + funcB({}); + """) + except Exception as e: + msg = str(e) + else: + self.fail("Expected exception.") + + self.assertIn("at funcA (:3)\n", msg) + self.assertIn("at funcB (:6)\n", msg) + + def test_memory_limit(self): + code = """ + (function() { + let arr = []; + for (let i = 0; i < 1000; ++i) { + arr.push(i); + } + })(); + """ + self.context.eval(code) + self.context.set_memory_limit(1000) + with self.assertRaisesRegex(quickjs.JSException, "null"): + self.context.eval(code) + self.context.set_memory_limit(1000000) + self.context.eval(code) + + def test_time_limit(self): + code = """ + (function() { + let arr = []; + for (let i = 0; i < 100000; ++i) { + arr.push(i); + } + return arr; + })(); + """ + self.context.eval(code) + self.context.set_time_limit(0) + with self.assertRaisesRegex(quickjs.JSException, "InternalError: interrupted"): + self.context.eval(code) + self.context.set_time_limit(-1) + self.context.eval(code) + + def test_memory_usage(self): + self.assertIn("memory_used_size", self.context.memory().keys()) + + def test_json_simple(self): + self.assertEqual(self.context.parse_json("42"), 42) + + def test_json_error(self): + with self.assertRaisesRegex(quickjs.JSException, "unexpected token"): + self.context.parse_json("a b c") + + def test_execute_pending_job(self): + self.context.eval("obj = {}") + self.assertEqual(self.context.execute_pending_job(), False) + self.context.eval("Promise.resolve().then(() => {obj.x = 1;})") + self.assertEqual(self.context.execute_pending_job(), True) + self.assertEqual(self.context.eval("obj.x"), 1) + self.assertEqual(self.context.execute_pending_job(), False) + + +class CallIntoPython(unittest.TestCase): + def setUp(self): + self.context = quickjs.Context() + + def test_make_function(self): + self.context.add_callable("f", lambda x: x + 2) + self.assertEqual(self.context.eval("f(40)"), 42) + + def test_make_two_functions(self): + for i in range(10): + self.context.add_callable("f", lambda x: i + x + 2) + self.context.add_callable("g", lambda x: i + x + 40) + f = self.context.get("f") + g = self.context.get("g") + self.assertEqual(f(40) - i, 42) + self.assertEqual(g(2) - i, 42) + self.assertEqual(self.context.eval("((f, a) => f(a))")(f, 40) - i, 42) + + def test_make_function_call_from_js(self): + self.context.add_callable("f", lambda x: x + 2) + g = self.context.eval("""( + function() { + return f(20) + 20; + } + )""") + self.assertEqual(g(), 42) + + def test_python_function_raises(self): + def error(a): + raise ValueError("A") + + self.context.add_callable("error", error) + with self.assertRaisesRegex(quickjs.JSException, "Python call failed"): + self.context.eval("error(0)") + + def test_make_function_two_args(self): + def concat(a, b): + return a + b + + self.context.add_callable("concat", concat) + result = self.context.eval("concat(40, 2)") + self.assertEqual(result, 42) + + concat = self.context.get("concat") + result = self.context.eval("((f, a, b) => 22 + f(a, b))")(concat, 10, 10) + self.assertEqual(result, 42) + + def test_make_function_two_string_args(self): + """Without the JS_DupValue in js_c_function, this test crashes.""" + def concat(a, b): + return a + "-" + b + + self.context.add_callable("concat", concat) + concat = self.context.get("concat") + result = concat("aaa", "bbb") + self.assertEqual(result, "aaa-bbb") + + def test_can_eval_in_same_context(self): + self.context.add_callable("f", lambda: 40 + self.context.eval("1 + 1")) + self.assertEqual(self.context.eval("f()"), 42) + + def test_can_call_in_same_context(self): + inner = self.context.eval("(function() { return 42; })") + self.context.add_callable("f", lambda: inner()) + self.assertEqual(self.context.eval("f()"), 42) + + def test_invalid_argument(self): + self.context.add_callable("p", lambda: 42) + self.assertEqual(self.context.eval("p()"), 42) + with self.assertRaisesRegex(quickjs.JSException, "Python call failed"): + self.context.eval("p(1)") + with self.assertRaisesRegex(quickjs.JSException, "Python call failed"): + self.context.eval("p({})") + + def test_time_limit_disallowed(self): + self.context.add_callable("f", lambda x: x + 2) + self.context.set_time_limit(1000) + with self.assertRaises(quickjs.JSException): + self.context.eval("f(40)") + + def test_conversion_failure_does_not_raise_system_error(self): + # https://github.com/PetterS/quickjs/issues/38 + + def test_list(): + return [1, 2, 3] + + self.context.add_callable("test_list", test_list) + with self.assertRaises(quickjs.JSException): + # With incorrect error handling, this (safely) made Python raise a SystemError + # instead of a JS exception. + self.context.eval("test_list()") + + +class Object(unittest.TestCase): + def setUp(self): + self.context = quickjs.Context() + + def test_function_is_object(self): + f = self.context.eval(""" + a = function(x) { + return 40 + x; + } + """) + self.assertIsInstance(f, quickjs.Object) + + def test_function_call_int(self): + f = self.context.eval(""" + f = function(x) { + return 40 + x; + } + """) + self.assertEqual(f(2), 42) + + def test_function_call_int_two_args(self): + f = self.context.eval(""" + f = function(x, y) { + return 40 + x + y; + } + """) + self.assertEqual(f(3, -1), 42) + + def test_function_call_many_times(self): + n = 1000 + f = self.context.eval(""" + f = function(x, y) { + return x + y; + } + """) + s = 0 + for i in range(n): + s += f(1, 1) + self.assertEqual(s, 2 * n) + + def test_function_call_str(self): + f = self.context.eval(""" + f = function(a) { + return a + " hej"; + } + """) + self.assertEqual(f("1"), "1 hej") + + def test_function_call_str_three_args(self): + f = self.context.eval(""" + f = function(a, b, c) { + return a + " hej " + b + " ho " + c; + } + """) + self.assertEqual(f("1", "2", "3"), "1 hej 2 ho 3") + + def test_function_call_object(self): + d = self.context.eval("d = {data: 42};") + f = self.context.eval(""" + f = function(d) { + return d.data; + } + """) + self.assertEqual(f(d), 42) + # Try again to make sure refcounting works. + self.assertEqual(f(d), 42) + self.assertEqual(f(d), 42) + + def test_function_call_unsupported_arg(self): + f = self.context.eval(""" + f = function(x) { + return 40 + x; + } + """) + with self.assertRaisesRegex(TypeError, "Unsupported type"): + self.assertEqual(f({}), 42) + + def test_json(self): + d = self.context.eval("d = {data: 42};") + self.assertEqual(json.loads(d.json()), {"data": 42}) + + def test_call_nonfunction(self): + d = self.context.eval("({data: 42})") + with self.assertRaisesRegex(quickjs.JSException, "TypeError: not a function"): + d(1) + + def test_wrong_context(self): + context1 = quickjs.Context() + context2 = quickjs.Context() + f = context1.eval("(function(x) { return x.a; })") + d = context2.eval("({a: 1})") + with self.assertRaisesRegex(ValueError, "Can not mix JS objects from different contexts."): + f(d) + + def test_get(self): + self.context.eval("a = {x: 42, y: 'foo'};") + a = self.context.get_global().get("a") + self.assertEqual(a.get("x"), 42) + self.assertEqual(a.get("y"), "foo") + self.assertEqual(a.get("z"), None) + + def test_set(self): + self.context.eval("a = {x: 'overridden'}") + a = self.context.get_global().get("a") + a.set("x", 42) + a.set("y", "foo") + self.assertTrue(self.context.eval("a.x == 42")) + self.assertTrue(self.context.eval("a.y == 'foo'")) + + def test_make_function(self): + print(11) + self.context.get_global().set("f", lambda x: x + 2) + self.assertEqual(self.context.eval("f(40)"), 42) + + +class FunctionTest(unittest.TestCase): + def test_adder(self): + f = quickjs.Function( + "adder", """ + function adder(x, y) { + return x + y; + } + """) + self.assertEqual(f(1, 1), 2) + self.assertEqual(f(100, 200), 300) + self.assertEqual(f("a", "b"), "ab") + + def test_identity(self): + identity = quickjs.Function( + "identity", """ + function identity(x) { + return x; + } + """) + for x in [True, [1], {"a": 2}, 1, 1.5, "hej", None]: + self.assertEqual(identity(x), x) + + def test_bool(self): + f = quickjs.Function( + "f", """ + function f(x) { + return [typeof x ,!x]; + } + """) + self.assertEqual(f(False), ["boolean", True]) + self.assertEqual(f(True), ["boolean", False]) + + def test_empty(self): + f = quickjs.Function("f", "function f() { }") + self.assertEqual(f(), None) + + def test_lists(self): + f = quickjs.Function( + "f", """ + function f(arr) { + const result = []; + arr.forEach(function(elem) { + result.push(elem + 42); + }); + return result; + }""") + self.assertEqual(f([0, 1, 2]), [42, 43, 44]) + + def test_dict(self): + f = quickjs.Function( + "f", """ + function f(obj) { + return obj.data; + }""") + self.assertEqual(f({"data": {"value": 42}}), {"value": 42}) + + def test_time_limit(self): + f = quickjs.Function( + "f", """ + function f() { + let arr = []; + for (let i = 0; i < 100000; ++i) { + arr.push(i); + } + return arr; + } + """) + f() + f.set_time_limit(0) + with self.assertRaisesRegex(quickjs.JSException, "InternalError: interrupted"): + f() + f.set_time_limit(-1) + f() + + def test_garbage_collection(self): + f = quickjs.Function( + "f", """ + function f() { + let a = {}; + let b = {}; + a.b = b; + b.a = a; + a.i = 42; + return a.i; + } + """) + initial_count = f.memory()["obj_count"] + for i in range(10): + prev_count = f.memory()["obj_count"] + self.assertEqual(f(run_gc=False), 42) + current_count = f.memory()["obj_count"] + self.assertGreater(current_count, prev_count) + + f.gc() + self.assertLessEqual(f.memory()["obj_count"], initial_count) + + def test_deep_recursion(self): + f = quickjs.Function( + "f", """ + function f(v) { + if (v <= 0) { + return 0; + } else { + return 1 + f(v - 1); + } + } + """) + + self.assertEqual(f(100), 100) + limit = 500 + with self.assertRaises(quickjs.StackOverflow): + f(limit) + f.set_max_stack_size(2000 * limit) + self.assertEqual(f(limit), limit) + + def test_add_callable(self): + f = quickjs.Function( + "f", """ + function f() { + return pfunc(); + } + """) + f.add_callable("pfunc", lambda: 42) + + self.assertEqual(f(), 42) + + def test_execute_pending_job(self): + f = quickjs.Function( + "f", """ + obj = {x: 0, y: 0}; + async function a() { + obj.x = await 1; + } + a(); + Promise.resolve().then(() => {obj.y = 1}); + function f() { + return obj.x + obj.y; + } + """) + self.assertEqual(f(), 0) + self.assertEqual(f.execute_pending_job(), True) + self.assertEqual(f(), 1) + self.assertEqual(f.execute_pending_job(), True) + self.assertEqual(f(), 2) + self.assertEqual(f.execute_pending_job(), False) + + +class JavascriptFeatures(unittest.TestCase): + def test_unicode_strings(self): + identity = quickjs.Function( + "identity", """ + function identity(x) { + return x; + } + """) + context = quickjs.Context() + for x in ["äpple", "≤≥", "☺"]: + self.assertEqual(identity(x), x) + self.assertEqual(context.eval('(function(){ return "' + x + '";})()'), x) + + def test_es2020_optional_chaining(self): + f = quickjs.Function( + "f", """ + function f(x) { + return x?.one?.two; + } + """) + self.assertIsNone(f({})) + self.assertIsNone(f({"one": 12})) + self.assertEqual(f({"one": {"two": 42}}), 42) + + def test_es2020_null_coalescing(self): + f = quickjs.Function( + "f", """ + function f(x) { + return x ?? 42; + } + """) + self.assertEqual(f(""), "") + self.assertEqual(f(0), 0) + self.assertEqual(f(11), 11) + self.assertEqual(f(None), 42) + + def test_symbol_conversion(self): + context = quickjs.Context() + context.eval("a = Symbol();") + context.set("b", context.eval("a")) + self.assertTrue(context.eval("a === b")) + + def test_large_python_integers_to_quickjs(self): + context = quickjs.Context() + # Without a careful implementation, this made Python raise a SystemError/OverflowError. + context.set("v", 10**25) + # There is precision loss occurring in JS due to + # the floating point implementation of numbers. + self.assertTrue(context.eval("v == 1e25")) + + def test_bigint(self): + context = quickjs.Context() + self.assertEqual(context.eval(f"BigInt('{10**100}')"), 10**100) + self.assertEqual(context.eval(f"BigInt('{-10**100}')"), -10**100) + +class Threads(unittest.TestCase): + def setUp(self): + self.context = quickjs.Context() + self.executor = concurrent.futures.ThreadPoolExecutor() + + def tearDown(self): + self.executor.shutdown() + + def test_concurrent(self): + """Demonstrates that the execution will crash unless the function executes on the same + thread every time. + + If the executor in Function is not present, this test will fail. + """ + data = list(range(1000)) + jssum = quickjs.Function( + "sum", """ + function sum(data) { + return data.reduce((a, b) => a + b, 0) + } + """) + + futures = [self.executor.submit(jssum, data) for _ in range(10)] + expected = sum(data) + for future in concurrent.futures.as_completed(futures): + self.assertEqual(future.result(), expected) + + def test_concurrent_own_executor(self): + data = list(range(1000)) + jssum1 = quickjs.Function("sum", + """ + function sum(data) { + return data.reduce((a, b) => a + b, 0) + } + """, + own_executor=True) + jssum2 = quickjs.Function("sum", + """ + function sum(data) { + return data.reduce((a, b) => a + b, 0) + } + """, + own_executor=True) + + futures = [self.executor.submit(f, data) for _ in range(10) for f in (jssum1, jssum2)] + expected = sum(data) + for future in concurrent.futures.as_completed(futures): + self.assertEqual(future.result(), expected) + + +class QJS(object): + def __init__(self): + self.interp = quickjs.Context() + self.interp.eval('var foo = "bar";') + + +class QuickJSContextInClass(unittest.TestCase): + def test_github_issue_7(self): + # This used to give stack overflow internal error, due to how QuickJS calculates stack + # frames. Passes with the 2021-03-27 release. + # + # TODO: Use the new JS_UpdateStackTop function in order to better handle stacks. + qjs = QJS() + self.assertEqual(qjs.interp.eval('2+2'), 4) diff --git "a/py/qkjs/quickjs_py\344\272\244\344\272\222.py" "b/py/qkjs/quickjs_py\344\272\244\344\272\222.py" new file mode 100644 index 0000000000000000000000000000000000000000..0874a3ceab87b41ca6cc1d358907058c4a684db3 --- /dev/null +++ "b/py/qkjs/quickjs_py\344\272\244\344\272\222.py" @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# File : quickjs_py交互.py +# Author: DaShenHan&道长-----先苦后甜,任凭晚风拂柳颜------ +# Date : 2022/10/12 + + +from quickjs import Context, Object as QuickJSObject +import json +from pprint import pp +from uuid import UUID +from datetime import date, datetime + +# https://github.com/PetterS/quickjs/pull/82 py交互扩展 +# print(QuickJSObject) +# QuickJSObject.set('a',1) +# print(Context.get_global()) +# exit() + +class JS: + interp = None + # Store global variables here. Reference from javascript by path + _globals = None + + # Used for generating unique ids in Context namespace + _incr = 0 + + # I cache the values passed from python to js. Otherwise, we create new representation + # objects each time a value is referenced. + _cache = None + + def __init__(self): + self.interp = Context() + self._globals = {} + self._cache = {} + + # Install js proxy logic + self.interp.add_callable("proxy_get", self.proxy_get) + self.interp.add_callable("proxy_set", self.proxy_set) + self.interp.eval(""" + var handler = { + get(target, property) { + rv = proxy_get(target.path, property) + if (typeof rv == 'string' && rv.substr(0, 5) == 'eval:') { + eval(rv.substr(5)); + return eval(rv.substr(5)); + } + return rv + }, + set(target, property, value) { + return proxy_set(target.path, property, value) + } + } + var mk_proxy = function(path) { + return new Proxy({path: path}, handler); + } + """) + + def set(self, **kwargs): + for (k, v) in kwargs.items(): + self.interp.set(k, v) + + def __call__(self, s): + return self.interp.eval(s) + + # ----------------------------------------------------------------- + def to_non_proxied(self, v): + # returns True/False and a value if the value can be represented + # by a Javascript type (not proxied) + if v in [None, True, False]: + return True, v + + if type(v) in [QuickJSObject, str, int, float]: + return True, v + + if type(v) in [UUID]: + return True, str(v) + + return False, None + + def to_eval_str(self, v, path=None): + # The value will be produced via eval if it is a string starting with eval: + + # Cache results + if id(v) and id(v) in self._cache: + return self._cache[id(v)] + + # If the value is a list, create a list of return values. Problem is + # that these have no path in the self._globals dict. They will have to + # be duplicated if they are objects. + + # BUG here - every reference to the list, create another copy - need to cache + if type(v) == list: + rv = [] + + for v1 in v: + can_non_proxy, non_proxied = self.to_non_proxied(v1) + if can_non_proxy: + self._incr += 1 + self.interp.set("_lv%s" % self._incr, v1) + rv.append("_lv%s" % self._incr) + else: + rv.append(self.to_eval_str(v1)) + rv = "[" + ",".join(rv) + "]" + self._cache[id(v)] = rv + return rv + + if type(v) == date: + rv = "new Date(%s, %s, %s)" % (v.year, v.month - 1, v.day) + self._cache[id(v)] = rv + return rv + + if type(v) == datetime: + rv = "new Date('%s')" % v.isoformat() + self._cache[id(v)] = rv + return rv + + # this creates a function, which can never be garbage collected + if callable(v): + self._incr += 1 + gname = "_fn%s" % self._incr + self.interp.add_callable(gname, v) + rv = "%s" % gname + self._cache[id(v)] = rv + return rv + + # Anonymous variables are created by values inside lists + if path is None: + self._incr += 1 + path = "_anon%s" % self._incr + self._globals[path] = v + + # I need to do this for objects and try getattr + if type(v) == dict: + rv = "mk_proxy('%s')" % path + self._cache[id(v)] = rv + return rv + + # Should be a user defined object to get here. Proxy it. + rv = "mk_proxy('%s')" % path + self._cache[id(v)] = rv + return rv + + # ----------------------------------------------------------------- + # Proxy Callback Points + def proxy_variable(self, **kwargs): + for (k, v) in kwargs.items(): + self._globals[k] = v + self.interp.set(k, None) + js("""%s = mk_proxy("%s");""" % (k, k)) + + def eval_path(self, path): + parts = path.split(".") + root = self._globals + for part in parts: + root = root[part] + return root + + def proxy_get(self, path, property): + # print(path, property) + root = self.eval_path(path) + try: + rv = root.get(property, None) + except: + # Object + rv = getattr(root, property) + + # print(path, property, rv) + + can_non_proxy, non_proxied = self.to_non_proxied(rv) + if can_non_proxy: + return rv + + new_path = path + "." + property + estr = self.to_eval_str(rv, path=new_path) + # print("eval:" + estr) + return "eval:" + estr + + def proxy_set(self, path, property, value): + # print(path, property, value) + root = self.eval_path(path) + root[property] = value + +if __name__ == '__main__': + # Example access class attributes + class example: + a = "I am a" + a1 = 111 + + def fn(self, a='not set'): + print("fn() called, a = ", a) + + + # Example access dict + l = { + "a": 1, + "fn": lambda: "XXXX", + "p1": None, + "p2": { + "p3": "PPP333" + }, + "p4": ["A", 4, None, example()], + "p5": example() + } + + js = JS() + + # Standard Variables + js.set(v1="Set via python") + print("v1 = ", js("v1")) + assert (js("v1") == "Set via python") + js.set(v2=None) + print("v2 = ", js("v2")) + assert (js("v2") is None) + + js.proxy_variable(l=l) + + # null + print("p1 = ", js("l.p1")) + assert (l['p1'] == js("l.p1")) + + # Access dict values + print("l.a = ", js("l.a")) + assert (l['a'] == js("l.a")) + js("l.b = 4") + print("l.b = ", js("l.b")) + assert (l['b'] == 4) + print("fn() = ", js("l.fn()")) + + # Undefined attribute + print("l.undef = ", js("l.undef")) + + # Nested dict + print("l.p2.p3 = ", js("l.p2.p3")) + assert (l['p2']['p3'] == js("l.p2.p3")) + + # Dict assigned from JS - Need to use .json() to unwrap in Python + js("l.c = {d: 4}") + print("l.c = ", js("l.c")) + print("l.c.d = ", js("l.c.d")) + print("l.c = ", l['c'].json()) + + # List + print("l.p4[1] =", js("l.p4[1]")) + assert (js("l.p4[1]") == l['p4'][1]) + print("calling l.p4[3].fn('called')") + js("l.p4[3].fn('called')") + + # THIS FAILS - p4 was copied and the original variable is never referenced. + js("l.p4.push('added')") + print("l.p4 = ", l['p4']) + + # Python Object accesss + print("l.p5 =", js("l.p5")) + print("l.p5.a1 =", js("l.p5.a1")) + assert (l['p5'].a1 == js("l.p5.a1")) + print("calling l.p5.fn(444)") + js("l.p5.fn(444)") + + # Print the global variables - will see anonymous variables + pp(js._globals) \ No newline at end of file diff --git a/readme.md b/readme.md index 1124d894f9ea5e1f8ca1c391aacb64458907fb7d..2fb53f7af94d4c07c3ca36b994086a7134bfc42b 100644 --- a/readme.md +++ b/readme.md @@ -46,7 +46,9 @@ [golang最好的js引擎-otto](https://github.com/robertkrimen/otto) [dockerfile教程](https://blog.csdn.net/qq_46158060/article/details/125718218) [获取本地设备信息](https://blog.csdn.net/cui_yonghua/article/details/125508991) -[获取本地设备信息](https://m.jb51.net/article/140716.htm) +[获取本地设备信息](https://m.jb51.net/article/140716.htm) +###### 2022/10/12 +- [X] js模式2动态配置链接支持多种壳子 ###### 2022/10/11 - [X] 增加自建解析加密示例 - [X] 发布3.9.0镜像,重大升级,优化js装载速度 diff --git a/templates/config.txt b/templates/config.txt index 83c814099dfc528fae0885cac9327c1eebfe7ec5..41d6ce1e575f38714e00d36cb8596d506172daba 100644 --- a/templates/config.txt +++ b/templates/config.txt @@ -19,8 +19,8 @@ "key":"dr_{{ rule.name }}", "name":"{{ rule.name }}(drpy)", "type":3, - "api":"{{ host }}/libs/drpy.min.js", - #"api":"{{ host }}/libs/drpy.js", + "api":{% if ISTVB %}"{{ host }}/libs/drpy.ym.js"{% else %}"{{ host }}/libs/drpy.min.js"{% endif %}, + #"api":"{{ host }}/libs/drpy.js", "searchable": {{ rule.searchable }}, "quickSearch": {{ rule.quickSearch }}, "filterable": {{ rule.filterable }},