diff --git a/src/app/commands/cmd_run_script.cpp b/src/app/commands/cmd_run_script.cpp index 15540cc4f..f30c6b10d 100644 --- a/src/app/commands/cmd_run_script.cpp +++ b/src/app/commands/cmd_run_script.cpp @@ -73,7 +73,7 @@ void RunScriptCommand::onExecute(Context* context) return; } - App::instance()->scriptEngine()->evalUserFile(m_filename, m_params); + App::instance()->scriptEngine()->evalUserFileInTask(m_filename, m_params); if (context->isUIAvailable()) ui::Manager::getDefault()->invalidate(); diff --git a/src/app/script/dialog_class.cpp b/src/app/script/dialog_class.cpp index d91e2eb52..d03b59a9a 100644 --- a/src/app/script/dialog_class.cpp +++ b/src/app/script/dialog_class.cpp @@ -32,6 +32,7 @@ #include "base/fs.h" #include "base/paths.h" #include "base/remove_from_container.h" +#include "ui/await.h" #include "ui/box.h" #include "ui/button.h" #include "ui/combobox.h" @@ -125,6 +126,7 @@ struct Dialog { // Reference used to keep the dialog alive (so it's not garbage // collected) when it's visible. int showRef = LUA_REFNIL; + int dlgStateRef = LUA_REFNIL; lua_State* L = nullptr; Dialog(const ui::Window::Type windowType, const std::string& title) @@ -152,6 +154,11 @@ struct Dialog { this->L = L; lua_pushvalue(L, 1); showRef = luaL_ref(L, LUA_REGISTRYINDEX); + // Keep a reference of this thread state while the dialog is still alive + // because it will be reused when calling the event handlers to create + // new lua threads. + lua_pushthread(L); + dlgStateRef = luaL_ref(L, LUA_REGISTRYINDEX); } } @@ -163,8 +170,14 @@ struct Dialog { void unrefShow() { if (showRef != LUA_REFNIL) { + int status = lua_status(this->L); + TRACE("status: %d %d\n", showRef, status); + + // this->L luaL_unref(this->L, LUA_REGISTRYINDEX, showRef); + luaL_unref(this->L, LUA_REGISTRYINDEX, dlgStateRef); showRef = LUA_REFNIL; + dlgStateRef = LUA_REFNIL; L = nullptr; } } @@ -336,18 +349,7 @@ void Dialog_connect_signal(lua_State* L, callback(L, std::forward(args)...); if (lua_isfunction(L, -2)) { - try { - if (lua_pcall(L, 1, 0, 0)) { - if (const char* s = lua_tostring(L, -1)) - App::instance()->scriptEngine()->consolePrint(s); - } - } - catch (const std::exception& ex) { - // This is used to catch unhandled exception or for - // example, std::runtime_error exceptions when a Tx() is - // created without an active sprite. - App::instance()->scriptEngine()->handleException(ex); - } + App::instance()->scriptEngine()->callInTask(L, 1); } else { lua_pop(L, 1); // Pop the value which should have been a function @@ -358,63 +360,65 @@ void Dialog_connect_signal(lua_State* L, int Dialog_new(lua_State* L) { - // If we don't have UI, just return nil - if (!App::instance()->isGui()) - return 0; + return ui::await([L]() -> int { + // If we don't have UI, just return nil + if (!App::instance()->isGui()) + return 0; - // Get the title and the type of window (with or without title bar) - ui::Window::Type windowType = ui::Window::WithTitleBar; - std::string title = "Script"; - if (lua_isstring(L, 1)) { - title = lua_tostring(L, 1); - } - else if (lua_istable(L, 1)) { - int type = lua_getfield(L, 1, "title"); - if (type != LUA_TNIL) - title = lua_tostring(L, -1); - lua_pop(L, 1); - - type = lua_getfield(L, 1, "notitlebar"); - if (type != LUA_TNIL && lua_toboolean(L, -1)) - windowType = ui::Window::WithoutTitleBar; - lua_pop(L, 1); - } - - auto dlg = push_new(L, windowType, title); - - // The uservalue of the dialog userdata will contain a table that - // stores all the callbacks to handle events. As these callbacks can - // reference the dialog itself, it's important to store callbacks in - // this table that depends on the dialog lifetime itself - // (i.e. uservalue) and in the global registry, because in that case - // we could create a cyclic reference that would be not GC'd. - lua_newtable(L); - lua_setuservalue(L, -2); - - if (lua_istable(L, 1)) { - int type = lua_getfield(L, 1, "parent"); - if (type != LUA_TNIL) { - if (auto parentDlg = may_get_obj(L, -1)) - dlg->window.setParentDisplay(parentDlg->window.display()); + // Get the title and the type of window (with or without title bar) + ui::Window::Type windowType = ui::Window::WithTitleBar; + std::string title = "Script"; + if (lua_isstring(L, 1)) { + title = lua_tostring(L, 1); } - lua_pop(L, 1); + else if (lua_istable(L, 1)) { + int type = lua_getfield(L, 1, "title"); + if (type != LUA_TNIL) + title = lua_tostring(L, -1); + lua_pop(L, 1); - type = lua_getfield(L, 1, "onclose"); - if (type == LUA_TFUNCTION) { - Dialog_connect_signal(L, -2, dlg->window.Close, [](lua_State*, CloseEvent&) { - // Do nothing - }); + type = lua_getfield(L, 1, "notitlebar"); + if (type != LUA_TNIL && lua_toboolean(L, -1)) + windowType = ui::Window::WithoutTitleBar; + lua_pop(L, 1); } - lua_pop(L, 1); - } - // The showRef must be the last reference to the dialog to be - // unreferenced after the window is closed (that's why this is the - // last connection to ui::Window::Close) - dlg->unrefShowOnClose(); + auto dlg = push_new(L, windowType, title); - TRACE_DIALOG("Dialog_new", dlg); - return 1; + // The uservalue of the dialog userdata will contain a table that + // stores all the callbacks to handle events. As these callbacks can + // reference the dialog itself, it's important to store callbacks in + // this table that depends on the dialog lifetime itself + // (i.e. uservalue) and in the global registry, because in that case + // we could create a cyclic reference that would be not GC'd. + lua_newtable(L); + lua_setuservalue(L, -2); + + if (lua_istable(L, 1)) { + int type = lua_getfield(L, 1, "parent"); + if (type != LUA_TNIL) { + if (auto parentDlg = may_get_obj(L, -1)) + dlg->window.setParentDisplay(parentDlg->window.display()); + } + lua_pop(L, 1); + + type = lua_getfield(L, 1, "onclose"); + if (type == LUA_TFUNCTION) { + Dialog_connect_signal(L, -2, dlg->window.Close, [](lua_State*, CloseEvent&) { + // Do nothing + }); + } + lua_pop(L, 1); + } + + // The showRef must be the last reference to the dialog to be + // unreferenced after the window is closed (that's why this is the + // last connection to ui::Window::Close) + dlg->unrefShowOnClose(); + + TRACE_DIALOG("Dialog_new", dlg); + return 1; + }); } int Dialog_gc(lua_State* L) @@ -1879,31 +1883,34 @@ int Dialog_set_bounds(lua_State* L) return 0; } +#define wrap(func) \ + [](lua_State* L) -> int { return ui::await([L]() -> int { return func(L); }); } + const luaL_Reg Dialog_methods[] = { - { "__gc", Dialog_gc }, - { "show", Dialog_show }, - { "showMenu", Dialog_showMenu }, - { "close", Dialog_close }, - { "newrow", Dialog_newrow }, - { "separator", Dialog_separator }, - { "label", Dialog_label }, - { "button", Dialog_button }, - { "check", Dialog_check }, - { "radio", Dialog_radio }, - { "menuItem", Dialog_menuItem }, - { "entry", Dialog_entry }, - { "number", Dialog_number }, - { "slider", Dialog_slider }, - { "combobox", Dialog_combobox }, - { "color", Dialog_color }, - { "shades", Dialog_shades }, - { "file", Dialog_file }, - { "canvas", Dialog_canvas }, - { "tab", Dialog_tab }, - { "endtabs", Dialog_endtabs }, - { "modify", Dialog_modify }, - { "repaint", Dialog_repaint }, - { nullptr, nullptr } + { "__gc", wrap(Dialog_gc) }, + { "show", wrap(Dialog_show) }, + { "showMenu", Dialog_showMenu }, + { "close", Dialog_close }, + { "newrow", Dialog_newrow }, + { "separator", Dialog_separator }, + { "label", Dialog_label }, + { "button", wrap(Dialog_button) }, + { "check", Dialog_check }, + { "radio", Dialog_radio }, + { "menuItem", Dialog_menuItem }, + { "entry", Dialog_entry }, + { "number", Dialog_number }, + { "slider", Dialog_slider }, + { "combobox", Dialog_combobox }, + { "color", Dialog_color }, + { "shades", Dialog_shades }, + { "file", Dialog_file }, + { "canvas", Dialog_canvas }, + { "tab", Dialog_tab }, + { "endtabs", Dialog_endtabs }, + { "modify", Dialog_modify }, + { "repaint", Dialog_repaint }, + { nullptr, nullptr } }; const Property Dialog_properties[] = { diff --git a/src/app/script/engine.cpp b/src/app/script/engine.cpp index 40bf2b344..f7f35ec56 100644 --- a/src/app/script/engine.cpp +++ b/src/app/script/engine.cpp @@ -35,6 +35,8 @@ #include "fmt/format.h" #include "ui/base.h" #include "ui/cursor_type.h" +#include "ui/manager.h" +#include "ui/message_loop.h" #include "ui/mouse_button.h" #include @@ -213,7 +215,80 @@ void register_websocket_class(lua_State* L); void set_app_params(lua_State* L, const Params& params); -Engine::Engine() : L(luaL_newstate()), m_delegate(nullptr), m_printLastResult(false) +#define RUNNING_TASKS "RunningTasks" + +RunScriptTask::RunScriptTask(lua_State* L, int nelems, Func&& func) +{ + m_mainL = L; + m_L = lua_newthread(m_mainL); + // Save a reference to the lua thread in the registry to avoid garbage collection + // before it gets executed. + m_LRef = luaL_ref(m_mainL, LUA_REGISTRYINDEX); + + // Move nelems stack elements from main thread to the child lua thread. + lua_xmove(m_mainL, m_L, nelems); + + // We use a global table to have access to the state of all the currently + // running scripts + int type = lua_getglobal(m_L, RUNNING_TASKS); + if (type == LUA_TNIL) { + lua_newtable(m_L); + lua_pushvalue(m_L, -1); + lua_setglobal(m_L, RUNNING_TASKS); + } + + lua_pushlightuserdata(m_L, m_L); + lua_pushlightuserdata(m_L, this); + lua_settable(m_L, -3); + lua_pop(m_L, 1); + + lua_sethook( + m_L, + [](lua_State* L, lua_Debug* ar) { + if (ar->event == LUA_HOOKCOUNT) { + int type = lua_getglobal(L, RUNNING_TASKS); + if (type == LUA_TTABLE) { + lua_pushlightuserdata(L, L); + lua_gettable(L, -2); + RunScriptTask* task = (RunScriptTask*)lua_topointer(L, -1); + lua_pop(L, 2); + if (task->wantsToStop()) { + lua_pushliteral(L, "Script stopped"); + lua_error(L); + } + } + } + }, + LUA_MASKCOUNT, + 10); + + // TODO: use the token for allowing the script to report progress somehow. + m_task.on_execute([this, func](base::task_token& token) { func(m_L); }); +} + +RunScriptTask::~RunScriptTask() +{ + luaL_unref(m_mainL, LUA_REGISTRYINDEX, m_LRef); + + // Collect script garbage. + lua_gc(m_L, LUA_GCCOLLECT); +} + +void RunScriptTask::execute(base::thread_pool& pool) +{ + m_task.start(pool); +} + +void RunScriptTask::stop() +{ + m_wantsToStop = true; +} + +Engine::Engine() + : L(luaL_newstate()) + , m_delegate(nullptr) + , m_threadPool(3) + , m_printLastResult(false) { #if _DEBUG int top = lua_gettop(L); @@ -525,6 +600,12 @@ Engine::~Engine() void Engine::destroy() { + // Stop all running tasks. + for (auto& task : m_tasks) { + task->stop(); + } + m_threadPool.wait_all(); + close_all_dialogs(); lua_close(L); L = nullptr; @@ -594,6 +675,11 @@ bool Engine::evalCode(const std::string& code, const std::string& filename) } void Engine::handleException(const std::exception& ex) +{ + handleException(L, ex); +} + +void Engine::handleException(lua_State* L, const std::exception& ex) { luaL_where(L, 1); const char* where = lua_tostring(L, -1); @@ -631,6 +717,110 @@ bool Engine::evalFile(const std::string& filename, const Params& params) return result; } +void Engine::callInTask(lua_State* parentL, int nargs) +{ + executeTask(parentL, nargs + 1, [this, nargs](lua_State* L) { + try { + if (lua_pcall(L, nargs, 0, 0)) { + if (const char* s = lua_tostring(L, -1)) + consolePrint(s); + } + } + catch (const std::exception& ex) { + // This is used to catch unhandled exception or for + // example, std::runtime_error exceptions when a Tx() is + // created without an active sprite. + handleException(L, ex); + } + }); +} + +void Engine::executeTask(lua_State* parentL, int nelems, RunScriptTask::Func&& func) +{ + auto task = std::make_unique(parentL, nelems, std::move(func)); + auto* taskPtr = task.get(); + task->onDone([this, taskPtr](base::task_token&) { onTaskDone(taskPtr); }); + m_tasks.push_back(std::move(task)); + taskPtr->execute(m_threadPool); +} + +void Engine::onTaskDone(const RunScriptTask* task) +{ + for (auto it = m_tasks.begin(); it != m_tasks.end(); ++it) { + if ((*it).get() == task) { + m_tasks.erase(it); + break; + } + } +} + +bool Engine::evalUserFileInTask(const std::string& filename, const Params& params) +{ + executeTask(L, 0, [filename, params, this](lua_State* L) { + std::string absFilename = base::get_absolute_path(filename); + // Set the _SCRIPT_PATH global so require() can find .lua files from + // the script path. + std::string path = base::get_file_path(absFilename); + SetScriptForRequire setScript(L, path.c_str()); + + bool ok = true; + std::stringstream buf; + { + std::ifstream s(FSTREAM_PATH(filename)); + // Returns false if we cannot open the file + if (!s) { + ok = false; + return; + } + buf << s.rdbuf(); + } + + AddScriptFilename addScript(absFilename); + set_app_params(L, params); + + const std::string& code = buf.str(); + const std::string& atFilename = "@" + absFilename; + + int returnCode; + try { + if (luaL_loadbuffer(L, code.c_str(), code.size(), atFilename.c_str()) || + lua_pcall(L, 0, 1, 0)) { + const char* s = lua_tostring(L, -1); + if (s) + TRACE("Error: %s\n", s); + // onConsoleError(s); + ok = false; + returnCode = -1; + } + else { + // Return code + if (lua_isinteger(L, -1)) + returnCode = lua_tointeger(L, -1); + else + returnCode = 0; + + // Code was executed correctly + if (m_printLastResult) { + if (!lua_isnone(L, -1)) { + const char* result = lua_tostring(L, -1); + if (result) + TRACE("Result: %s\n", result); + // onConsolePrint(result); + } + } + } + lua_pop(L, 1); + } + catch (const std::exception& ex) { + // handleException(ex); + TRACE("Exception: %s\n", ex.what()); + ok = false; + returnCode = -1; + } + }); + return true; +} + bool Engine::evalUserFile(const std::string& filename, const Params& params) { // Set the _SCRIPT_PATH global so require() can find .lua files from @@ -660,6 +850,12 @@ void Engine::stopDebugger() lua_sethook(L, nullptr, 0, 0); } +void Engine::stopScript() +{ + lua_pushliteral(L, "Script stopped"); + lua_error(L); +} + void Engine::onConsoleError(const char* text) { if (text && m_delegate) diff --git a/src/app/script/engine.h b/src/app/script/engine.h index dd087a74f..17e5b2260 100644 --- a/src/app/script/engine.h +++ b/src/app/script/engine.h @@ -16,6 +16,8 @@ #include "app/color.h" #include "app/commands/params.h" #include "app/extensions.h" +#include "base/task.h" +#include "base/thread_pool.h" #include "base/uuid.h" #include "doc/brush.h" #include "doc/frame.h" @@ -84,8 +86,32 @@ public: virtual void endFile(const std::string& file) = 0; }; +class RunScriptTask { +public: + typedef std::function Func; + + RunScriptTask(lua_State* L, int nelems, Func&& func); + ~RunScriptTask(); + + void onDone(base::task::func_t&& funcDone) { m_task.on_done(std::move(funcDone)); } + void execute(base::thread_pool& pool); + void stop(); + bool wantsToStop() const { return m_wantsToStop; } + +private: + // Lua's main thread state. + lua_State* m_mainL; + // Lua's thread state based on m_mainL. + lua_State* m_L; + int m_LRef = LUA_REFNIL; + base::task m_task; + bool m_wantsToStop = false; +}; + class Engine { public: + typedef std::vector> Tasks; + Engine(); ~Engine(); @@ -101,8 +127,13 @@ public: bool evalCode(const std::string& code, const std::string& filename = std::string()); bool evalFile(const std::string& filename, const Params& params = Params()); bool evalUserFile(const std::string& filename, const Params& params = Params()); + bool evalUserFileInTask(const std::string& filename, const Params& params = Params()); + // Calls the function in the stack with the number of arguments specified by nargs in + // a new RunScriptTask. + void callInTask(lua_State* L, int nargs); void handleException(const std::exception& ex); + void handleException(lua_State* L, const std::exception& ex); void consolePrint(const char* text) { onConsolePrint(text); } @@ -112,13 +143,22 @@ public: void startDebugger(DebuggerDelegate* debuggerDelegate); void stopDebugger(); + // Stops the currently running lua chunk + void stopScript(); private: void onConsoleError(const char* text); void onConsolePrint(const char* text); + // Creates a new RunScriptTask based on parentL and moving nelems from parentL's stack + // to the child lua thread stack + void executeTask(lua_State* parentL, int nelems, RunScriptTask::Func&& func); + void onTaskDone(const RunScriptTask* task); + static void checkProgress(lua_State* L, lua_Debug* ar); lua_State* L; EngineDelegate* m_delegate; + base::thread_pool m_threadPool; + Tasks m_tasks; bool m_printLastResult; int m_returnCode; }; diff --git a/src/ui/await.h b/src/ui/await.h new file mode 100644 index 000000000..2ec00e333 --- /dev/null +++ b/src/ui/await.h @@ -0,0 +1,57 @@ +// Aseprite UI Library +// Copyright (C) 2025 Igara Studio S.A. +// Copyright (C) 2001-2018 David Capello +// +// This file is released under the terms of the MIT license. +// Read LICENSE.txt for more information. + +#ifndef UI_AWAIT_H_INCLUDED +#define UI_AWAIT_H_INCLUDED +#include +#pragma once + +#include "os/event.h" +#include "os/event_queue.h" +#include "ui/system.h" + +#include +#include + +namespace ui { + +// Awaits for the future result of the function passed. +// It is meant to be used from background threads to ask something to the main +// thread and wait for its result. +// If await is called from the main thread it just executes the function +// and returns the result. +template +T await(std::function&& func) +{ + if (ui::is_ui_thread()) + return func(); + + std::promise promise; + std::future future = promise.get_future(); + + // Queue the event + os::Event ev; + ev.setType(os::Event::Callback); + ev.setCallback([func, &promise]() { + try { + if constexpr (std::is_void()) + func(); + else + promise.set_value(func()); + } + catch (...) { + promise.set_exception(std::current_exception()); + } + }); + os::queue_event(ev); + // Wait for the future + return future.get(); +} + +} // namespace ui + +#endif