mysqlshdk/scripting/polyglot/shell_javascript.cc (401 lines of code) (raw):
/*
* Copyright (c) 2024, Oracle and/or its affiliates.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2.0,
* as published by the Free Software Foundation.
*
* This program is designed to work with certain software (including
* but not limited to OpenSSL) that is licensed under separate terms,
* as designated in a particular file or component or in included license
* documentation. The authors of MySQL hereby grant you an additional
* permission to link the program and your derivative works with the
* separately licensed software that they have either included with
* the program or referenced in the documentation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
* the GNU General Public License, version 2.0, for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "mysqlshdk/scripting/polyglot/shell_javascript.h"
#include <cerrno>
#include <list>
#include <stack>
#include "my_config.h"
#include "mysqlshdk/include/scripting/module_registry.h"
#include "mysqlshdk/include/scripting/obj_date.h"
#include "mysqlshdk/include/scripting/object_factory.h"
#include "mysqlshdk/include/scripting/object_registry.h"
#include "mysqlshdk/include/shellcore/base_shell.h" // FIXME
#include "mysqlshdk/include/shellcore/console.h"
#include "mysqlshdk/include/shellcore/scoped_contexts.h"
#include "mysqlshdk/libs/utils/logger.h"
#include "mysqlshdk/libs/utils/utils_file.h"
#include "mysqlshdk/libs/utils/utils_general.h"
#include "mysqlshdk/libs/utils/utils_path.h"
#include "mysqlshdk/libs/utils/utils_string.h"
#include "mysqlshdk/scripting/polyglot/languages/polyglot_language.h"
#include "mysqlshdk/scripting/polyglot/native_wrappers/polyglot_collectable.h"
#include "mysqlshdk/scripting/polyglot/native_wrappers/polyglot_object_wrapper.h"
#include "mysqlshdk/scripting/polyglot/polyglot_type_conversion.h"
#include "mysqlshdk/scripting/polyglot/polyglot_wrappers/types_polyglot.h"
#include "mysqlshdk/scripting/polyglot/utils/polyglot_error.h"
#include "mysqlshdk/scripting/polyglot/utils/polyglot_utils.h"
#include "scripting/jscript_core_definitions.h"
#ifdef HAVE_JS
#include "mysqlshdk/scripting/polyglot/languages/polyglot_javascript.h"
#endif
namespace shcore {
namespace polyglot {
namespace {
struct Source {
static constexpr const char *name = "source";
static constexpr std::size_t argc = 1;
static constexpr auto callback = &Shell_javascript::f_source;
};
struct Load_native_module {
static constexpr const char *name = "loadNativeModule";
static constexpr std::size_t argc = 1;
static constexpr auto callback = &Shell_javascript::f_load_native_module;
};
struct Load_module {
static constexpr const char *name = "loadModule";
static constexpr std::size_t argc = 2;
static constexpr auto callback = &Shell_javascript::f_load_module;
};
struct Current_module_folder {
static constexpr const char *name = "currentModuleFolder";
static constexpr auto callback = &Shell_javascript::f_current_module_folder;
};
struct List_native_modules {
static constexpr const char *name = "listNativeModules";
static constexpr auto callback = &Shell_javascript::f_list_native_modules;
};
struct Repr {
static constexpr const char *name = "repr";
static constexpr std::size_t argc = 1;
static constexpr auto callback = &Shell_javascript::f_repr;
};
struct Unrepr {
static constexpr const char *name = "unrepr";
static constexpr std::size_t argc = 1;
static constexpr auto callback = &Shell_javascript::f_unrepr;
};
struct Type {
static constexpr const char *name = "type";
static constexpr std::size_t argc = 1;
static constexpr auto callback = &Shell_javascript::f_type;
};
struct Print {
static constexpr auto callback = &Shell_javascript::f_print;
};
struct Println {
static constexpr auto callback = &Shell_javascript::f_println;
};
} // namespace
#ifdef HAVE_JS
/*
* load_core_module loads the content of the given module file
* and inserts the definitions on the JS globals.
*/
void Shell_javascript::load_core_module() const {
shcore::Scoped_naming_style style(naming_style());
std::string script = "(function (){" + shcore::js_core_module + "})();";
if (const auto rc = eval("core.js", script, nullptr); rc != poly_ok) {
throw Polyglot_error(thread(), rc);
}
}
void Shell_javascript::unload_core_module() const {
shcore::Scoped_naming_style style(naming_style());
std::string script = R"((function () {
for (var name in require.__mh.__cache) {
require.__mh.__cache[name] = null;
if (name in this) {
this[name] = null;
}
}
})();
)";
if (const auto rc = eval("core.js", script, nullptr); rc != poly_ok) {
throw Polyglot_error(thread(), rc);
}
}
void Shell_javascript::initialize(const std::shared_ptr<IFile_system> &fs) {
Java_script_interface::initialize(fs);
set_global_function("repr",
&native_handler_fixed_args<Shell_javascript, Repr>);
set_global_function("unrepr",
&native_handler_fixed_args<Shell_javascript, Unrepr>);
set_global_function("type",
&polyglot_handler_fixed_args<Shell_javascript, Type>);
set_global_function("source",
&native_handler_fixed_args<Shell_javascript, Source>);
set_global_function("print",
&native_handler_variable_args<Shell_javascript, Print>);
set_global_function("println",
&native_handler_variable_args<Shell_javascript, Println>);
set_global_function(
"__list_native_modules",
&polyglot_handler_no_args<Shell_javascript, List_native_modules>);
set_global_function(
"__load_native_module",
&native_handler_fixed_args<Shell_javascript, Load_native_module>);
set_global_function(
"__load_module",
&polyglot_handler_fixed_args<Shell_javascript, Load_module>);
set_global_function(
"__current_module_folder",
&native_handler_no_args<Shell_javascript, Current_module_folder>);
load_core_module();
}
void Shell_javascript::finalize() {
unload_core_module();
for (const auto global : {
"__current_module_folder",
"__load_module",
"__load_native_module",
"__list_native_modules",
"println",
"print",
"source",
"type",
"unrepr",
"repr",
}) {
set_global(global, nullptr);
}
Java_script_interface::finalize();
}
void Shell_javascript::init_context_builder() {
Java_script_interface::init_context_builder();
// NOTE: This is required to enable the proper loading of JavaScript
// Modules
throw_if_error(poly_context_builder_allow_io, thread(), m_context_builder,
true);
// The current working directory should be specified in the context
// builder so it is able to track relative paths. When executing a
// module (i.e. provided through the -f option), the CWD would be the
// module path so relative paths within the module are properly
// resolved.
std::string cwd = shcore::path::getcwd();
if (auto run_file = mysqlsh::current_shell_options()->get().run_file;
!run_file.empty()) {
if (!shcore::path::is_absolute(run_file)) {
run_file = shcore::path::join_path({cwd, run_file});
}
run_file = shcore::path::normalize(run_file);
cwd = shcore::path::dirname(run_file);
}
throw_if_error(poly_context_builder_set_current_working_directory, thread(),
m_context_builder, cwd.c_str());
}
void Shell_javascript::output_handler(const char *bytes, size_t length) {
mysqlsh::current_console()->print(std::string(bytes, length));
}
void Shell_javascript::error_handler(const char *bytes, size_t length) {
mysqlsh::current_console()->print_diag(std::string(bytes, length));
}
poly_value Shell_javascript::from_native_object(
const Object_bridge_ref &object) const {
poly_value result = nullptr;
if (object && object->class_name() == "Date") {
std::shared_ptr<Date> date = std::static_pointer_cast<Date>(object);
// 0 date values can come from MySQL but they're not supported by the JS
// Date object, so we convert them to null
if (date->has_date() && date->get_year() == 0 && date->get_month() == 0 &&
date->get_day() == 0) {
throw_if_error(poly_create_null, thread(), context(), &result);
} else if (!date->has_date()) {
// there's no Time object in JS and we can't use Date to represent time
// only
std::string t;
result = poly_string(date->append_descr(t));
} else {
auto source = shcore::str_format(
"new Date(%d, %d, %d, %d, %d, %d, %d)", date->get_year(),
date->get_month() - 1, date->get_day(), date->get_hour(),
date->get_min(), date->get_sec(), date->get_usec() / 1000);
if (const auto rc = eval("<shell>", source, &result); rc != poly_ok) {
throw Polyglot_error(thread(), rc);
}
}
}
return result;
}
double Shell_javascript::call_num_method(poly_value object,
const char *method) const {
poly_value member;
double ret_val{0};
if (get_member(object, method, &member)) {
bool executable{false};
throw_if_error(poly_value_can_execute, thread(), member, &executable);
if (!executable) {
throw std::runtime_error("Non executable operation!");
}
poly_value result;
throw_if_error(poly_value_execute, thread(), member, nullptr, 0, &result);
throw_if_error(poly_value_as_double, thread(), result, &ret_val);
}
return ret_val;
}
shcore::Value Shell_javascript::native_array(poly_value object) {
int64_t array_size{0};
throw_if_error(poly_value_get_array_size, thread(), object, &array_size);
auto narray = shcore::make_array();
narray->resize(array_size);
for (int32_t c = array_size, i = 0; i < c; i++) {
poly_value item;
throw_if_error(poly_value_get_array_element, thread(), object, i, &item);
(*narray)[i] = convert(item);
}
return Value(std::move(narray));
}
shcore::Value Shell_javascript::native_object(poly_value object) {
auto keys = get_member_keys(thread(), context(), object);
auto dict = shcore::make_dict();
for (const auto &key : keys) {
poly_value value;
throw_if_error(poly_value_get_member, thread(), object, key.c_str(),
&value);
dict->set(key, convert(value));
}
return Value(std::move(dict));
}
shcore::Value Shell_javascript::native_function(poly_value object) {
return Value(std::make_shared<polyglot::Polyglot_function>(shared_from_this(),
object));
}
shcore::Value Shell_javascript::to_native_object(
poly_value object, const std::string &class_name) {
if (class_name == "Date") {
int year, month, day, hour, min, msec;
float sec;
year = static_cast<int>(call_num_method(object, "getFullYear"));
month = static_cast<int>(call_num_method(object, "getMonth"));
day = static_cast<int>(call_num_method(object, "getDate"));
hour = static_cast<int>(call_num_method(object, "getHours"));
min = static_cast<int>(call_num_method(object, "getMinutes"));
sec = static_cast<float>(call_num_method(object, "getSeconds"));
msec = static_cast<int>(call_num_method(object, "getMilliseconds"));
return Value(std::make_shared<Date>(year, month + 1, day, hour, min, sec,
msec * 1000));
} else if (class_name == "Array") {
return native_array(object);
} else if (class_name == "Object") {
return native_object(object);
} else if (class_name == "Function") {
return native_function(object);
} else if (class_name == "Error") {
poly_value poly_cause;
throw_if_error(poly_value_get_member, thread(), object, "cause",
&poly_cause);
shcore::Value cause = convert(poly_cause);
if (cause.get_type() != shcore::Value_type::Null &&
cause.get_type() != shcore::Value_type::Map) {
poly_value poly_message;
throw_if_error(poly_value_get_member, thread(), object, "message",
&poly_message);
cause = convert(poly_message);
}
return cause;
}
return Java_script_interface::to_native_object(object, class_name);
}
void Shell_javascript::load_module(const std::string &path, poly_value module) {
const auto script_scope = enter_script(path);
shcore::Scoped_naming_style style(naming_style());
// load the file
std::string source;
if (!load_text_file(path, source)) {
throw std::runtime_error(
shcore::str_format("Failed to open '%s'", path.c_str()));
}
std::string code =
"(function(m){(function(exports,module,__filename,__dirname){" + source +
"})(m.exports,m,m.__filename,m.__dirname)});";
auto new_context = copy_global_context();
poly_value executable;
if (const auto rc =
poly_context_parse(thread(), new_context, get_language_id(),
path.c_str(), code.c_str(), &executable);
rc != poly_ok) {
try {
auto issue = Polyglot_error(thread(), rc);
throw std::runtime_error(shcore::str_format(
"Failed to parse '%s': %s", path.c_str(), issue.format().c_str()));
} catch (const Polyglot_generic_error &) {
// No additional details...
throw std::runtime_error(
shcore::str_format("Failed to parse '%s'", path.c_str()));
}
}
poly_value result;
throw_if_error(poly_value_execute, thread(), executable, nullptr, 0, &result);
bool is_function{false};
if (poly_ok != poly_value_can_execute(thread(), result, &is_function) ||
!is_function) {
// this shouldn't happen, as it's our code which creates this function
throw std::runtime_error(
shcore::str_format("Failed to execute '%s'.", path.c_str()));
}
// // execute the function (and user code), passing the module object
poly_value args[] = {module};
poly_value maybe_result;
throw_if_error(poly_value_execute, thread(), result, args, 1, &maybe_result);
m_storage->store(new_context);
}
Value Shell_javascript::f_source(const Argument_list &args) {
Scoped_naming_style style(naming_style());
const auto str = args[0].as_string();
// Loads the source content
std::string source;
if (load_text_file(str, source)) {
execute(source, str);
} else {
throw std::runtime_error("Error loading script");
}
return {};
}
Value Shell_javascript::f_load_native_module(const Argument_list &args) {
Scoped_naming_style style(naming_style());
const auto str = args[0].as_string();
const auto core_modules = Object_factory::package_contents("__modules__");
if (std::find(core_modules.begin(), core_modules.end(), str) !=
core_modules.end()) {
const auto module = Object_factory::call_constructor(
"__modules__", str, shcore::Argument_list());
return Value(module);
}
return {};
}
poly_value Shell_javascript::f_load_module(
const std::vector<poly_value> &args) {
Scoped_naming_style style(naming_style());
const auto module = to_string(args[0]);
load_module(module, args[1]);
return nullptr;
}
Value Shell_javascript::f_current_module_folder() {
return Value(current_script_folder());
}
poly_value Shell_javascript::f_list_native_modules() {
const auto core_modules = Object_factory::package_contents("__modules__");
std::vector<poly_value> module_array;
module_array.reserve(core_modules.size());
for (const auto &module : core_modules) {
module_array.push_back(poly_string(module));
}
return poly_array(thread(), context(), module_array);
}
Value Shell_javascript::f_repr(const Argument_list &args) {
return Value(args[0].repr());
}
Value Shell_javascript::f_unrepr(const Argument_list &args) {
return Value::parse(args[0].as_string());
}
poly_value Shell_javascript::f_type(const std::vector<poly_value> &args) {
return type_info(args[0]);
}
std::string Shell_javascript::get_print_values(const Argument_list &args) {
std::string text;
for (size_t i = 0; i < args.size(); i++) {
if (i > 0) text.push_back(' ');
text += args[i].descr(true);
}
return text;
}
Value Shell_javascript::f_print(const Argument_list &args) {
mysqlsh::current_console()->print(get_print_values(args));
return {};
}
Value Shell_javascript::f_println(const Argument_list &args) {
mysqlsh::current_console()->println(get_print_values(args));
return {};
}
bool Shell_javascript::load_plugin(const std::string &file_name) {
bool ret_val = true;
std::string load_code = "require.__mh.__load_module(" +
shcore::quote_string(file_name, '"') + ")";
poly_value executable;
if (const auto rc =
poly_context_parse(thread(), context(), get_language_id(),
"(load_plugin)", load_code.c_str(), &executable);
rc != poly_ok) {
ret_val = false;
log_error("Error loading JavaScript file '%s':\n\t%s", file_name.c_str(),
Polyglot_error(thread(), rc).format().c_str());
} else {
if (poly_ok !=
poly_value_execute(thread(), executable, nullptr, 0, nullptr)) {
ret_val = false;
log_error("Error loading JavaScript file '%s':\n\tExecution failed: %s",
file_name.c_str(),
Polyglot_error(thread(), rc).format().c_str());
}
}
return ret_val;
}
#endif
} // namespace polyglot
} // namespace shcore