/* * Copyright (C) 2016-2019 Robin Gareus * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * 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 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 Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include "pbd/error.h" #include "pbd/file_utils.h" #include "pbd/compose.h" #include "ardour/directory_names.h" #include "ardour/filesystem_paths.h" #include "ardour/luascripting.h" #include "ardour/lua_script_params.h" #include "ardour/search_paths.h" #include "ardour/utils.h" #include "lua/luastate.h" #include "LuaBridge/LuaBridge.h" #include "pbd/i18n.h" #include "sha1.c" using namespace ARDOUR; using namespace PBD; using namespace std; LuaScripting* LuaScripting::_instance = 0; LuaScripting& LuaScripting::instance () { if (!_instance) { _instance = new LuaScripting; } return *_instance; } LuaScripting::LuaScripting () : _sl_dsp (0) , _sl_session (0) , _sl_hook (0) , _sl_action (0) , _sl_snippet (0) , _sl_setup (0) , _sl_tracks (0) { ; } LuaScripting::~LuaScripting () { if (getenv ("ARDOUR_RUNNING_UNDER_VALGRIND")) { // don't bother, just exit quickly. delete _sl_dsp; delete _sl_session; delete _sl_hook; delete _sl_action; delete _sl_snippet; delete _sl_setup; delete _sl_tracks; } } void LuaScripting::refresh (bool run_scan) { Glib::Threads::Mutex::Lock lm (_lock); delete _sl_dsp; delete _sl_session; delete _sl_hook; delete _sl_action; delete _sl_snippet; delete _sl_setup; delete _sl_tracks; _sl_dsp = 0; _sl_session = 0; _sl_hook = 0; _sl_action = 0; _sl_snippet = 0; _sl_setup = 0; _sl_tracks = 0; if (run_scan) { lm.release (); scan (); } } bool LuaScripting::Sorter::operator() (LuaScriptInfoPtr const a, LuaScriptInfoPtr const b) const { return ARDOUR::cmp_nocase_utf8 (a->name, b->name) < 0; } LuaScriptInfoPtr LuaScripting::script_info (const std::string &script) { return scan_script ("", script); } void LuaScripting::scan () { Glib::Threads::Mutex::Lock lm (_lock); #define CLEAR_OR_NEW(LIST) \ if (LIST) { LIST->clear (); } else { LIST = new LuaScriptList (); } CLEAR_OR_NEW (_sl_dsp) CLEAR_OR_NEW (_sl_session) CLEAR_OR_NEW (_sl_hook) CLEAR_OR_NEW (_sl_action) CLEAR_OR_NEW (_sl_snippet) CLEAR_OR_NEW (_sl_setup) CLEAR_OR_NEW (_sl_tracks) #undef CLEAR_OR_NEW vector luascripts; find_files_matching_pattern (luascripts, lua_search_path (), "*.lua"); for (vector::iterator i = luascripts.begin(); i != luascripts.end (); ++i) { LuaScriptInfoPtr lsi = scan_script (*i); if (!lsi) { PBD::info << string_compose (_("Script '%1' has no valid descriptor."), *i) << endmsg; continue; } switch (lsi->type) { case LuaScriptInfo::DSP: _sl_dsp->push_back(lsi); break; case LuaScriptInfo::Session: _sl_session->push_back(lsi); break; case LuaScriptInfo::EditorHook: _sl_hook->push_back(lsi); break; case LuaScriptInfo::EditorAction: _sl_action->push_back(lsi); break; case LuaScriptInfo::Snippet: _sl_snippet->push_back(lsi); break; case LuaScriptInfo::SessionInit: _sl_setup->push_back(lsi); break; default: break; } } std::sort (_sl_dsp->begin(), _sl_dsp->end(), Sorter()); std::sort (_sl_session->begin(), _sl_session->end(), Sorter()); std::sort (_sl_hook->begin(), _sl_hook->end(), Sorter()); std::sort (_sl_action->begin(), _sl_action->end(), Sorter()); std::sort (_sl_snippet->begin(), _sl_snippet->end(), Sorter()); std::sort (_sl_setup->begin(), _sl_setup->end(), Sorter()); std::sort (_sl_tracks->begin(), _sl_tracks->end(), Sorter()); scripts_changed (); /* EMIT SIGNAL */ } void LuaScripting::lua_print (std::string s) { PBD::info << "Lua: " << s << "\n"; } LuaScriptInfoPtr LuaScripting::scan_script (const std::string &fn, const std::string &sc) { LuaState lua; if (!(fn.empty() ^ sc.empty())){ // give either file OR script assert (0); return LuaScriptInfoPtr(); } lua_State* L = lua.getState(); lua.Print.connect (&LuaScripting::lua_print); lua.sandbox (true); lua.do_command ( "ardourluainfo = {}" "function ardour (entry)" " ardourluainfo['type'] = assert(entry['type'])" " ardourluainfo['name'] = assert(entry['name'])" " ardourluainfo['category'] = entry['category'] or 'Unknown'" " ardourluainfo['author'] = entry['author'] or 'Unknown'" " ardourluainfo['license'] = entry['license'] or ''" " ardourluainfo['description'] = entry['description'] or ''" " end" ); try { int err; if (fn.empty()) { err = lua.do_command (sc); } else { err = lua.do_file (fn); } if (err) { #ifndef NDEBUG cerr << "failed to load lua script fn: '"<< fn << "'\n"; #endif return LuaScriptInfoPtr(); } } catch (luabridge::LuaException const& e) { #ifndef NDEBUG cerr << "Exception: Failed to parse lua script fn: '"<< fn << "' " << e.what () << "\n"; #endif PBD::warning << "Exception: Failed to parse lua script fn: '"<< fn << "' " << e.what () << "\n"; return LuaScriptInfoPtr(); } catch (...) { #ifndef NDEBUG cerr << "Exception: Failed to parse lua script fn: '"<< fn << "'\n"; #endif PBD::warning << "Exception: Failed to parse lua script fn: '"<< fn << "'\n"; return LuaScriptInfoPtr(); } luabridge::LuaRef nfo = luabridge::getGlobal (L, "ardourluainfo"); if (nfo.type() != LUA_TTABLE) { #ifndef NDEBUG cerr << "failed to get ardour{} table from script\n"; #endif return LuaScriptInfoPtr(); } if (nfo["name"].type() != LUA_TSTRING || nfo["type"].type() != LUA_TSTRING) { #ifndef NDEBUG cerr << "script-type or script-name is not a string\n"; #endif return LuaScriptInfoPtr(); } std::string name = nfo["name"].cast(); LuaScriptInfo::ScriptType type = LuaScriptInfo::str2type (nfo["type"].cast()); if (name.empty() || type == LuaScriptInfo::Invalid) { #ifndef NDEBUG cerr << "invalid script-type or missing script name\n"; #endif return LuaScriptInfoPtr(); } char hash[41]; Sha1Digest s; sha1_init (&s); if (fn.empty()) { sha1_write (&s, (const uint8_t *) sc.c_str(), sc.size ()); } else { try { std::string script = Glib::file_get_contents (fn); sha1_write (&s, (const uint8_t *) script.c_str(), script.size ()); } catch (Glib::FileError const& err) { return LuaScriptInfoPtr(); } } sha1_result_hash (&s, hash); LuaScriptInfoPtr lsi (new LuaScriptInfo (type, name, fn, hash)); for (luabridge::Iterator i(nfo); !i.isNil (); ++i) { if (!i.key().isString() || !i.value().isString()) { return LuaScriptInfoPtr(); } std::string key = i.key().tostring(); std::string val = i.value().tostring(); if (key == "author") { lsi->author = val; } if (key == "license") { lsi->license = val; } if (key == "description") { lsi->description = val; } if (key == "category") { lsi->category = val; } } if (type == LuaScriptInfo::EditorAction) { luabridge::LuaRef lua_rs = luabridge::getGlobal (L, "route_setup"); if (lua_rs.isFunction ()) { lsi->subtype |= LuaScriptInfo::RouteSetup; } luabridge::LuaRef lua_ss = luabridge::getGlobal (L, "session_setup"); if (lua_ss.isFunction ()) { try { if (lua_ss () == true) { lsi->subtype |= LuaScriptInfo::SessionSetup; } } catch (...) { } } } return lsi; } LuaScriptList & LuaScripting::scripts (LuaScriptInfo::ScriptType type) { if (!_sl_dsp || !_sl_session || !_sl_hook || !_sl_action || !_sl_snippet || ! _sl_setup || ! _sl_tracks) { scan (); } switch (type) { case LuaScriptInfo::DSP: return *_sl_dsp; break; case LuaScriptInfo::Session: return *_sl_session; break; case LuaScriptInfo::EditorHook: return *_sl_hook; break; case LuaScriptInfo::EditorAction: return *_sl_action; break; case LuaScriptInfo::Snippet: return *_sl_snippet; break; case LuaScriptInfo::SessionInit: return *_sl_setup; break; default: break; } return _empty_script_info; // make some compilers happy } LuaScriptInfoPtr LuaScripting::by_name (const std::string& name, LuaScriptInfo::ScriptType type) { LuaScriptList lsl (scripts (type)); for (LuaScriptList::const_iterator s = lsl.begin(); s != lsl.end(); ++s) { if ((*s)->name == name) { return (*s); } } return LuaScriptInfoPtr(); } std::string LuaScriptInfo::type2str (const ScriptType t) { switch (t) { case LuaScriptInfo::DSP: return "DSP"; case LuaScriptInfo::Session: return "Session"; case LuaScriptInfo::EditorHook: return "EditorHook"; case LuaScriptInfo::EditorAction: return "EditorAction"; case LuaScriptInfo::Snippet: return "Snippet"; case LuaScriptInfo::SessionInit: return "SessionInit"; default: return "Invalid"; } } LuaScriptInfo::ScriptType LuaScriptInfo::str2type (const std::string& str) { const char* type = str.c_str(); if (!strcasecmp (type, "DSP")) {return LuaScriptInfo::DSP;} if (!strcasecmp (type, "Session")) {return LuaScriptInfo::Session;} if (!strcasecmp (type, "EditorHook")) {return LuaScriptInfo::EditorHook;} if (!strcasecmp (type, "EditorAction")) {return LuaScriptInfo::EditorAction;} if (!strcasecmp (type, "Snippet")) {return LuaScriptInfo::Snippet;} if (!strcasecmp (type, "SessionInit")) {return LuaScriptInfo::SessionInit;} return LuaScriptInfo::Invalid; } LuaScriptParamList LuaScriptParams::script_params (const LuaScriptInfoPtr& lsi, const std::string &pname) { assert (lsi); return LuaScriptParams::script_params (lsi->path, pname); } LuaScriptParamList LuaScriptParams::script_params (const std::string& s, const std::string &pname, bool file) { LuaState lua; return LuaScriptParams::script_params (lua, s, pname, file); } LuaScriptParamList LuaScriptParams::script_params (LuaState& lua, const std::string& s, const std::string &pname, bool file) { LuaScriptParamList rv; lua_State* L = lua.getState(); lua.sandbox (true); lua.do_command ("function ardour () end"); try { if (file) { lua.do_file (s); } else { lua.do_command (s); } } catch (...) { return rv; } luabridge::LuaRef lua_params = luabridge::getGlobal (L, pname.c_str()); if (lua_params.isFunction ()) { luabridge::LuaRef params = lua_params (); if (params.isTable ()) { for (luabridge::Iterator i (params); !i.isNil (); ++i) { if (!i.key ().isString ()) { continue; } if (!i.value ().isTable ()) { continue; } if (!i.value ()["title"].isString ()) { continue; } std::string name = i.key ().cast (); std::string title = i.value ()["title"].cast (); std::string dflt; bool optional = false; bool preseeded = false; if (i.value ()["default"].isString ()) { dflt = i.value ()["default"].cast (); } if (i.value ()["optional"].isBoolean ()) { optional = i.value ()["optional"].cast (); } if (i.value ()["preseeded"].isBoolean ()) { preseeded = i.value ()["preseeded"].cast (); } LuaScriptParamPtr lsspp (new LuaScriptParam(name, title, dflt, optional, preseeded)); rv.push_back (lsspp); } } } return rv; } void LuaScriptParams::params_to_ref (luabridge::LuaRef *tbl_args, const LuaScriptParamList& args) { assert (tbl_args && (*tbl_args).isTable ()); for (LuaScriptParamList::const_iterator i = args.begin(); i != args.end(); ++i) { if ((*i)->optional && !(*i)->is_set) { continue; } (*tbl_args)[(*i)->name] = (*i)->value; } } void LuaScriptParams::ref_to_params (LuaScriptParamList& args, luabridge::LuaRef *tbl_ref) { assert (tbl_ref && (*tbl_ref).isTable ()); for (luabridge::Iterator i (*tbl_ref); !i.isNil (); ++i) { if (!i.key ().isString ()) { assert(0); continue; } std::string name = i.key ().cast (); std::string value = i.value ().cast (); for (LuaScriptParamList::const_iterator ii = args.begin(); ii != args.end(); ++ii) { if ((*ii)->name == name) { (*ii)->value = value; break; } } } } bool LuaScripting::try_compile (const std::string& script, const LuaScriptParamList& args) { const std::string& bytecode = get_factory_bytecode (script); if (bytecode.empty()) { return false; } LuaState l; l.Print.connect (&LuaScripting::lua_print); l.sandbox (true); lua_State* L = l.getState(); l.do_command ("" " function checkfactory (b, a)" " assert(type(b) == 'string', 'ByteCode must be string')" " load(b)()" // assigns f " assert(type(f) == 'string', 'Assigned ByteCode must be string')" " local factory = load(f)" " assert(type(factory) == 'function', 'Factory is a not a function')" " local env = _ENV; env.f = nil env.os = nil env.io = nil" " load (string.dump(factory, true), nil, nil, env)(a)" " end" ); try { luabridge::LuaRef lua_test = luabridge::getGlobal (L, "checkfactory"); l.do_command ("checkfactory = nil"); // hide it. l.do_command ("collectgarbage()"); luabridge::LuaRef tbl_arg (luabridge::newTable(L)); LuaScriptParams::params_to_ref (&tbl_arg, args); lua_test (bytecode, tbl_arg); return true; // OK } catch (luabridge::LuaException const& e) { #ifndef NDEBUG cerr << e.what() << "\n"; #endif lua_print (e.what()); } catch (...) { } return false; } std::string LuaScripting::get_factory_bytecode (const std::string& script, const std::string& ffn, const std::string& fp) { LuaState l; l.Print.connect (&LuaScripting::lua_print); l.sandbox (true); lua_State* L = l.getState(); l.do_command ( " function ardour () end" "" " function dump_function (f)" " assert(type(f) == 'function', 'Factory is a not a function')" " return string.format(\"" + fp + " = %q\", string.dump(f, true))" " end" ); try { luabridge::LuaRef lua_dump = luabridge::getGlobal (L, "dump_function"); l.do_command ("dump_function = nil"); // hide it l.do_command (script); // register "factory" luabridge::LuaRef lua_factory = luabridge::getGlobal (L, ffn.c_str()); if (lua_factory.isFunction()) { return (lua_dump(lua_factory)).cast (); } } catch (...) { } return ""; } std::string LuaScripting::user_script_dir () { std::string dir = Glib::build_filename (user_config_directory(), lua_dir_name); g_mkdir_with_parents (dir.c_str(), 0744); return dir; }