diff --git a/laf b/laf index 65829107c..23c11583b 160000 --- a/laf +++ b/laf @@ -1 +1 @@ -Subproject commit 65829107c838817987f3cf6374cc68c583e5d538 +Subproject commit 23c11583b626b36b125dbe74a2d1cde880780623 diff --git a/src/app/commands/cmd_grid.cpp b/src/app/commands/cmd_grid.cpp index 686ddc58f..688d4e91c 100644 --- a/src/app/commands/cmd_grid.cpp +++ b/src/app/commands/cmd_grid.cpp @@ -52,6 +52,7 @@ protected: docPref.grid.snap(newValue); StatusBar::instance()->showSnapToGridWarning(newValue); + StatusBar::instance()->showRunningScriptsWindow(newValue); } }; 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..391bbb5a8 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; } } @@ -301,6 +314,7 @@ struct Dialog { template void Dialog_connect_signal(lua_State* L, int dlgIdx, + std::string signalName, obs::signal& signal, Callback callback) { @@ -336,18 +350,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, signalName); } else { lua_pop(L, 1); // Pop the value which should have been a function @@ -358,63 +361,73 @@ 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()) + try { + 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()); + } + lua_pop(L, 1); + + type = lua_getfield(L, 1, "onclose"); + if (type == LUA_TFUNCTION) { + Dialog_connect_signal(L, -2, "onclose", 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; + }, + 500); + } + catch (AwaitTimeoutException& ex) { + luaL_error(L, "Could not create Dialog"); 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()); - } - 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) @@ -764,7 +777,7 @@ int Dialog_button_base(lua_State* L, T** outputWidget = nullptr) type = lua_getfield(L, 2, "onclick"); if (type == LUA_TFUNCTION) { - Dialog_connect_signal(L, 1, widget->Click, [](lua_State*) { + Dialog_connect_signal(L, 1, "onclick", widget->Click, [](lua_State*) { // Do nothing }); closeWindowByDefault = false; @@ -838,7 +851,7 @@ int Dialog_entry(lua_State* L) if (lua_istable(L, 2)) { int type = lua_getfield(L, 2, "onchange"); if (type == LUA_TFUNCTION) { - Dialog_connect_signal(L, 1, widget->Change, [](lua_State* L) { + Dialog_connect_signal(L, 1, "onchange", widget->Change, [](lua_State* L) { // Do nothing }); } @@ -868,7 +881,7 @@ int Dialog_number(lua_State* L) type = lua_getfield(L, 2, "onchange"); if (type == LUA_TFUNCTION) { - Dialog_connect_signal(L, 1, widget->Change, [](lua_State* L) { + Dialog_connect_signal(L, 1, "onchange", widget->Change, [](lua_State* L) { // Do nothing }); } @@ -909,7 +922,7 @@ int Dialog_slider(lua_State* L) if (lua_istable(L, 2)) { int type = lua_getfield(L, 2, "onchange"); if (type == LUA_TFUNCTION) { - Dialog_connect_signal(L, 1, widget->Change, [](lua_State* L) { + Dialog_connect_signal(L, 1, "onchange", widget->Change, [](lua_State* L) { // Do nothing }); } @@ -917,7 +930,7 @@ int Dialog_slider(lua_State* L) type = lua_getfield(L, 2, "onrelease"); if (type == LUA_TFUNCTION) { - Dialog_connect_signal(L, 1, widget->SliderReleased, [](lua_State* L) { + Dialog_connect_signal(L, 1, "onrelease", widget->SliderReleased, [](lua_State* L) { // Do nothing }); } @@ -955,7 +968,7 @@ int Dialog_combobox(lua_State* L) type = lua_getfield(L, 2, "onchange"); if (type == LUA_TFUNCTION) { - Dialog_connect_signal(L, 1, widget->Change, [](lua_State* L) { + Dialog_connect_signal(L, 1, "onchange", widget->Change, [](lua_State* L) { // Do nothing }); } @@ -979,10 +992,14 @@ int Dialog_color(lua_State* L) if (lua_istable(L, 2)) { int type = lua_getfield(L, 2, "onchange"); if (type == LUA_TFUNCTION) { - Dialog_connect_signal(L, 1, widget->Change, [](lua_State* L, const app::Color& color) { - push_obj(L, color); - lua_setfield(L, -2, "color"); - }); + Dialog_connect_signal(L, + 1, + "onchange", + widget->Change, + [](lua_State* L, const app::Color& color) { + push_obj(L, color); + lua_setfield(L, -2, "color"); + }); } } @@ -1026,6 +1043,7 @@ int Dialog_shades(lua_State* L) if (type == LUA_TFUNCTION) { Dialog_connect_signal(L, 1, + "onclick", widget->Click, [widget](lua_State* L, ColorShades::ClickEvent& ev) { lua_pushinteger(L, (int)ev.button()); @@ -1101,7 +1119,7 @@ int Dialog_file(lua_State* L) if (lua_istable(L, 2)) { int type = lua_getfield(L, 2, "onchange"); if (type == LUA_TFUNCTION) { - Dialog_connect_signal(L, 1, widget->Change, [](lua_State* L) { + Dialog_connect_signal(L, 1, "onchange", widget->Change, [](lua_State* L) { // Do nothing }); } @@ -1276,7 +1294,7 @@ int Dialog_canvas(lua_State* L) if (lua_istable(L, 2)) { int type = lua_getfield(L, 2, "onpaint"); if (type == LUA_TFUNCTION) { - Dialog_connect_signal(L, 1, widget->Paint, [](lua_State* L, GraphicsContext& gc) { + Dialog_connect_signal(L, 1, "onpaint", widget->Paint, [](lua_State* L, GraphicsContext& gc) { push_new(L, std::move(gc)); lua_setfield(L, -2, "context"); }); @@ -1285,51 +1303,55 @@ int Dialog_canvas(lua_State* L) type = lua_getfield(L, 2, "onkeydown"); if (type == LUA_TFUNCTION) { - Dialog_connect_signal(L, 1, widget->KeyDown, fill_keymessage_values); + Dialog_connect_signal(L, 1, "onkeydown", widget->KeyDown, fill_keymessage_values); handleKeyEvents = true; } lua_pop(L, 1); type = lua_getfield(L, 2, "onkeyup"); if (type == LUA_TFUNCTION) { - Dialog_connect_signal(L, 1, widget->KeyUp, fill_keymessage_values); + Dialog_connect_signal(L, 1, "onkeyup", widget->KeyUp, fill_keymessage_values); handleKeyEvents = true; } lua_pop(L, 1); type = lua_getfield(L, 2, "onmousemove"); if (type == LUA_TFUNCTION) { - Dialog_connect_signal(L, 1, widget->MouseMove, fill_mousemessage_values); + Dialog_connect_signal(L, 1, "onmousemove", widget->MouseMove, fill_mousemessage_values); } lua_pop(L, 1); type = lua_getfield(L, 2, "onmousedown"); if (type == LUA_TFUNCTION) { - Dialog_connect_signal(L, 1, widget->MouseDown, fill_mousemessage_values); + Dialog_connect_signal(L, 1, "onmousedown", widget->MouseDown, fill_mousemessage_values); } lua_pop(L, 1); type = lua_getfield(L, 2, "onmouseup"); if (type == LUA_TFUNCTION) { - Dialog_connect_signal(L, 1, widget->MouseUp, fill_mousemessage_values); + Dialog_connect_signal(L, 1, "onmouseup", widget->MouseUp, fill_mousemessage_values); } lua_pop(L, 1); type = lua_getfield(L, 2, "ondblclick"); if (type == LUA_TFUNCTION) { - Dialog_connect_signal(L, 1, widget->DoubleClick, fill_mousemessage_values); + Dialog_connect_signal(L, 1, "ondblclick", widget->DoubleClick, fill_mousemessage_values); } lua_pop(L, 1); type = lua_getfield(L, 2, "onwheel"); if (type == LUA_TFUNCTION) { - Dialog_connect_signal(L, 1, widget->Wheel, fill_mousemessage_values); + Dialog_connect_signal(L, 1, "onwheel", widget->Wheel, fill_mousemessage_values); } lua_pop(L, 1); type = lua_getfield(L, 2, "ontouchmagnify"); if (type == LUA_TFUNCTION) { - Dialog_connect_signal(L, 1, widget->TouchMagnify, fill_touchmessage_values); + Dialog_connect_signal(L, + 1, + "ontouchmagnify", + widget->TouchMagnify, + fill_touchmessage_values); } lua_pop(L, 1); } @@ -1382,7 +1404,7 @@ int Dialog_tab(lua_State* L) if (lua_istable(L, 2)) { int type = lua_getfield(L, 2, "onclick"); if (type == LUA_TFUNCTION) { - Dialog_connect_signal(L, 1, tab->Click, [id](lua_State* L) { + Dialog_connect_signal(L, 1, "onclick", tab->Click, [id](lua_State* L) { lua_pushstring(L, id.c_str()); lua_setfield(L, -2, "tab"); }); @@ -1436,7 +1458,7 @@ int Dialog_endtabs(lua_State* L) type = lua_getfield(L, 2, "onchange"); if (type == LUA_TFUNCTION) { auto tab = dlg->wipTab; - Dialog_connect_signal(L, 1, dlg->wipTab->TabChanged, [tab](lua_State* L) { + Dialog_connect_signal(L, 1, "onchange", dlg->wipTab->TabChanged, [tab](lua_State* L) { lua_pushstring(L, tab->tabId(tab->selectedTab()).c_str()); lua_setfield(L, -2, "tab"); }); @@ -1879,31 +1901,42 @@ int Dialog_set_bounds(lua_State* L) return 0; } +#define wrap(func) \ + [](lua_State* L) -> int { \ + try { \ + return ui::await([L]() -> int { return func(L); }, 500); \ + } \ + catch (AwaitTimeoutException & ex) { \ + luaL_error(L, "Could not execute %s", #func); \ + return 0; \ + } \ + } + 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..bc88f63b8 100644 --- a/src/app/script/engine.cpp +++ b/src/app/script/engine.cpp @@ -35,7 +35,10 @@ #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 "ui/system.h" #include #include @@ -213,7 +216,100 @@ 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" + +std::mutex runningTasksMutex; + +RunScriptTask::RunScriptTask(lua_State* L, int nelems, const std::string& description, Func&& func) + : m_LRef(LUA_REFNIL) + , m_description(description) +{ + m_L = lua_newthread(L); + // Move the thread object in the parent thread stack to the new thread stack. + lua_xmove(L, m_L, 1); + // Save a reference to the lua thread in the registry to avoid garbage collection + // before it gets executed. + m_LRef = luaL_ref(m_L, LUA_REGISTRYINDEX); + + // Move nelems stack elements from main thread to the child lua thread. + lua_xmove(L, m_L, nelems); + + // We use a global table to have access to the state of all the currently + // running scripts + { + std::lock_guard lock(runningTasksMutex); + 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, this); + lua_rawsetp(m_L, -2, m_L); + lua_pop(m_L, 1); + } + lua_sethook( + m_L, + [](lua_State* L, lua_Debug* ar) { + if (ar->event == LUA_HOOKCOUNT) { + std::lock_guard lock(runningTasksMutex); + int type = lua_getglobal(L, RUNNING_TASKS); + if (type == LUA_TTABLE) { + lua_rawgetp(L, -1, L); + RunScriptTask* task = (RunScriptTask*)lua_topointer(L, -1); + lua_pop(L, 2); + if (task->wantsToStop()) { + luaL_where(L, 0); + const char* where = lua_tostring(L, -1); + luaL_traceback(L, L, "Script stopped", 0); + const char* traceback = lua_tostring(L, -1); + std::string msg(fmt::format("{}{}", where, traceback)); + lua_pop(L, 2); + lua_pushstring(L, msg.c_str()); + 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() +{ + std::lock_guard lock(runningTasksMutex); + int type = lua_getglobal(m_L, RUNNING_TASKS); + if (type == LUA_TTABLE) { + lua_pushlightuserdata(m_L, nullptr); + lua_rawsetp(m_L, -2, m_L); + } + lua_pop(m_L, 1); + + luaL_unref(m_L, 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 +621,15 @@ Engine::~Engine() void Engine::destroy() { + // Stop all running tasks. + { + std::lock_guard lock(m_mutex); + for (auto& task : m_tasks) { + task->stop(); + } + } + m_threadPool.wait_all(); + close_all_dialogs(); lua_close(L); L = nullptr; @@ -594,6 +699,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 +741,122 @@ bool Engine::evalFile(const std::string& filename, const Params& params) return result; } +void Engine::callInTask(lua_State* parentL, int nargs, const std::string& description) +{ + executeTask(parentL, nargs + 1, description, [this, nargs](lua_State* L) { + try { + if (lua_pcall(L, nargs, 0, 0)) { + if (const char* s = lua_tostring(L, -1)) { + std::string error = std::string(s); + ui::execute_from_ui_thread([this, error]() { consolePrint(error.c_str()); }); + } + } + } + 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, + const std::string& description, + RunScriptTask::Func&& func) +{ + auto task = std::make_unique(parentL, nelems, description, std::move(func)); + auto* taskPtr = task.get(); + task->onDone([this, taskPtr](base::task_token&) { onTaskDone(taskPtr); }); + { + std::lock_guard lock(m_mutex); + m_tasks.push_back(std::move(task)); + } + TaskStart(taskPtr); + taskPtr->execute(m_threadPool); +} + +void Engine::onTaskDone(const RunScriptTask* task) +{ + std::lock_guard lock(m_mutex); + TaskDone(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, [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) { + std::string error = std::string(s); + ui::execute_from_ui_thread([this, error]() { onConsoleError(error.c_str()); }); + } + 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 +886,16 @@ void Engine::stopDebugger() lua_sethook(L, nullptr, 0, 0); } +void Engine::stopTask(const RunScriptTask* task) +{ + std::lock_guard lock(m_mutex); + for (const auto& t : m_tasks) { + if (t.get() == task) { + t->stop(); + } + } +} + 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..c928bb7cb 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,36 @@ public: virtual void endFile(const std::string& file) = 0; }; +class RunScriptTask { +public: + typedef std::function Func; + + RunScriptTask(lua_State* L, int nelems, const std::string& description, 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; } + + int ref() const { return m_LRef; } + const std::string& description() const { return m_description; } + bool isRunning() const { return m_task.running(); } + bool isEnqueued() const { return m_task.enqueued(); } + +private: + // Lua's thread state. + lua_State* m_L; + int m_LRef; + base::task m_task; + bool m_wantsToStop = false; + std::string m_description; +}; + class Engine { public: + typedef std::vector> Tasks; + Engine(); ~Engine(); @@ -101,8 +131,22 @@ 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, const std::string& description); + std::vector tasks() const + { + std::lock_guard lock(m_mutex); + std::vector tasks; + tasks.reserve(m_tasks.size()); + for (const auto& task : m_tasks) + tasks.push_back(task.get()); + return tasks; + } void handleException(const std::exception& ex); + void handleException(lua_State* L, const std::exception& ex); void consolePrint(const char* text) { onConsolePrint(text); } @@ -112,13 +156,29 @@ public: void startDebugger(DebuggerDelegate* debuggerDelegate); void stopDebugger(); + // Stops the specified task + void stopTask(const RunScriptTask* task); + + obs::signal TaskStart; + obs::signal TaskDone; 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, + const std::string& description, + 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; + mutable std::mutex m_mutex; + Tasks m_tasks; bool m_printLastResult; int m_returnCode; }; diff --git a/src/app/ui/status_bar.cpp b/src/app/ui/status_bar.cpp index 5c52392c9..86157eb32 100644 --- a/src/app/ui/status_bar.cpp +++ b/src/app/ui/status_bar.cpp @@ -22,6 +22,7 @@ #include "app/modules/gui.h" #include "app/modules/palettes.h" #include "app/pref/preferences.h" +#include "app/script/engine.h" #include "app/tools/active_tool.h" #include "app/tools/tool.h" #include "app/ui/button_set.h" @@ -44,8 +45,16 @@ #include "fmt/format.h" #include "gfx/size.h" #include "os/surface.h" +#include "os/system.h" #include "text/font.h" +#include "ui/box.h" +#include "ui/button.h" +#include "ui/label.h" +#include "ui/listbox.h" +#include "ui/listitem.h" +#include "ui/popup_window.h" #include "ui/ui.h" +#include "ui/view.h" #include "ver/info.h" #include @@ -607,6 +616,70 @@ private: ui::Button m_button; }; +class StatusBar::RunningScriptsWindow : public ui::Window { +public: + // TODO: Replace the title by a string + RunningScriptsWindow() : ui::Window(Type::WithTitleBar, "Running scripts") + { + setMinSize({ 500, 200 }); + m_runningScripts.setExpansive(true); + m_view.setExpansive(true); + m_view.attachToView(&m_runningScripts); + addChild(&m_view); + + initTheme(); + + refreshList(); + + App::instance()->scriptEngine()->TaskStart.connect([this](const script::RunScriptTask*) { + ui::execute_from_ui_thread([this] { refreshList(); }); + }); + App::instance()->scriptEngine()->TaskDone.connect([this](const script::RunScriptTask*) { + ui::execute_from_ui_thread([this] { refreshList(); }); + }); + } + + void refreshList() + { + m_runningScripts.removeAllChildren(); + for (const auto* task : App::instance()->scriptEngine()->tasks()) { + auto* item = new TaskItem(task); + m_runningScripts.addChild(item); + } + m_runningScripts.layout(); + flushRedraw(); + } + +private: + class TaskItem : public ListItem { + public: + TaskItem(const script::RunScriptTask* task) : m_label(""), m_stop("Stop") + { + m_task = task; + m_label.setText( + fmt::format("{} ({})", task->description(), (task->isEnqueued() ? "enqueued" : "running"))); + + m_label.setExpansive(true); + m_row.setExpansive(true); + m_row.addChild(&m_label); + if (!task->isEnqueued()) + m_row.addChild(&m_stop); + addChild(&m_row); + + m_stop.Click.connect([this]() { App::instance()->scriptEngine()->stopTask(m_task); }); + } + + private: + const script::RunScriptTask* m_task; + ui::HBox m_row; + ui::Label m_label; + ui::Button m_stop; + }; + + ui::View m_view; + ui::ListBox m_runningScripts; +}; + // This widget is used to show the current frame. class GotoFrameEntry : public Entry { public: @@ -886,6 +959,25 @@ void StatusBar::showSnapToGridWarning(bool state) } } +void StatusBar::showRunningScriptsWindow(bool state) +{ + if (state) { + if (!m_runningScriptsWindow) + m_runningScriptsWindow = new RunningScriptsWindow; + + m_runningScriptsWindow->setDisplay(display(), false); + + if (!m_runningScriptsWindow->isVisible()) { + m_runningScriptsWindow->openWindow(); + m_runningScriptsWindow->remapWindow(); + // updateRunningScriptsWindowPosition(); + } + } + else if (m_runningScriptsWindow) { + m_runningScriptsWindow->closeWindow(nullptr); + } +} + ////////////////////////////////////////////////////////////////////// // StatusBar message handler diff --git a/src/app/ui/status_bar.h b/src/app/ui/status_bar.h index 90075fe72..38a652fac 100644 --- a/src/app/ui/status_bar.h +++ b/src/app/ui/status_bar.h @@ -66,6 +66,7 @@ public: void showTile(int msecs, doc::tile_t tile, const std::string& text = std::string()); void showTool(int msecs, tools::Tool* tool); void showSnapToGridWarning(bool state); + void showRunningScriptsWindow(bool state); // Used by AppEditor to update the zoom level in the status bar. void updateFromEditor(Editor* editor); @@ -119,6 +120,9 @@ private: // Snap to grid window class SnapToGridWindow; SnapToGridWindow* m_snapToGridWindow; + + class RunningScriptsWindow; + RunningScriptsWindow* m_runningScriptsWindow; }; } // namespace app diff --git a/src/ui/await.h b/src/ui/await.h new file mode 100644 index 000000000..17a72b641 --- /dev/null +++ b/src/ui/await.h @@ -0,0 +1,68 @@ +// 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 +#pragma once + +#include "os/event.h" +#include "os/event_queue.h" +#include "ui/system.h" + +#include +#include + +namespace ui { + +class AwaitTimeoutException : std::runtime_error { +public: + AwaitTimeoutException() : std::runtime_error("Await timed out") {} +}; + +// 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, int timeout = 0) +{ + 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 + if (timeout > 0) { + auto status = future.wait_for(std::chrono::milliseconds(timeout)); + if (status != std::future_status::ready) { + throw AwaitTimeoutException(); + } + } + return future.get(); +} + +} // namespace ui + +#endif