From 4dc5e77bc86e800f73f46062a32fa5692d39da30 Mon Sep 17 00:00:00 2001 From: Dmitry Sapozhnikov <11535558+o-sdn-o@users.noreply.github.com> Date: Thu, 1 Jun 2023 15:21:23 +0500 Subject: [PATCH] #393 Application scripting (add script engine parallel launch) --- src/netxs/desktopio/console.hpp | 21 +- src/netxs/desktopio/directvt.hpp | 2 + src/netxs/desktopio/scripting.hpp | 133 +++++++++++++ src/netxs/desktopio/system.hpp | 318 ++++++++++++++++++++++++++++++ src/netxs/desktopio/terminal.hpp | 5 +- src/netxs/desktopio/xml.hpp | 44 +++-- src/vtm.xml | 21 ++ 7 files changed, 528 insertions(+), 16 deletions(-) create mode 100644 src/netxs/desktopio/scripting.hpp diff --git a/src/netxs/desktopio/console.hpp b/src/netxs/desktopio/console.hpp index 8ba2eba88..f60501249 100644 --- a/src/netxs/desktopio/console.hpp +++ b/src/netxs/desktopio/console.hpp @@ -5,6 +5,7 @@ #include "input.hpp" #include "system.hpp" +#include "scripting.hpp" namespace netxs::ui { @@ -3716,6 +3717,7 @@ namespace netxs::ui using tick = datetime::quartz, hint>; using list = std::vector; using gptr = sptr; + using repl = scripting::repl; //pro::keybd keybd{*this }; // host: Keyboard controller. pro::mouse mouse{*this }; // host: Mouse controller. @@ -3727,6 +3729,7 @@ namespace netxs::ui xmls config; // host: Running configuration. gptr client; // host: Standalone app. subs tokens; // host: Subscription tokens. + repl engine; // host:: Scripting engine. std::vector user_numbering; // host: . @@ -3739,10 +3742,26 @@ namespace netxs::ui host(sptr server, xmls config, pro::focus::mode m = pro::focus::mode::hub) : focus{*this, m, faux }, quartz{ bell::router(), e2::timer::tick.id }, - config{ config } + config{ config }, + engine{ *this } { using namespace std::chrono; auto& canal = *server; + + config.pushd("/config/scripting/"); + if (config.take("enabled", faux)) + { + auto lang = config.take("engine", ""s); + config.cd(lang); + auto path = config.take("cwd", ""s); + auto exec = config.take("cmd", ""s); + auto main = config.take("main", ""s); + engine.start(path, exec); + engine.write(main + "\n"); + //todo run integration script + } + config.popd(); + auto& g = skin::globals(); g.brighter = config.take("brighter" , cell{});//120); g.kb_focus = config.take("kb_focus" , cell{});//60 diff --git a/src/netxs/desktopio/directvt.hpp b/src/netxs/desktopio/directvt.hpp index 8f0a36399..339f7b1e5 100644 --- a/src/netxs/desktopio/directvt.hpp +++ b/src/netxs/desktopio/directvt.hpp @@ -41,9 +41,11 @@ namespace netxs::prompt X(pipe) /* */ \ X(pool) /* */ \ X(rail) /* */ \ + X(repl) /* */ \ X(s11n) /* */ \ X(sock) /* */ \ X(term) /* */ \ + X(task) /* */ \ X(text) /* */ \ X(tile) /* */ \ X(user) /* */ \ diff --git a/src/netxs/desktopio/scripting.hpp b/src/netxs/desktopio/scripting.hpp new file mode 100644 index 000000000..4748d7fb3 --- /dev/null +++ b/src/netxs/desktopio/scripting.hpp @@ -0,0 +1,133 @@ +// Copyright (c) NetXS Group. +// Licensed under the MIT license. + +#pragma once + +#include "application.hpp" + +namespace netxs::scripting +{ + using namespace ui; + + template + class repl + { + using s11n = directvt::binary::s11n; + using pidt = os::pidt; + using task = os::task; + + Host& owner; + + // repl: Event handler. + class xlat + : public s11n + { + Host& owner; // xlat: Boss object reference. + subs token; // xlat: Subscription tokens. + + public: + void disable() + { + token.clear(); + } + + void handle(s11n::xs::fullscreen lock) + { + //... + } + void handle(s11n::xs::focus_cut lock) + { + //... + } + void handle(s11n::xs::focus_set lock) + { + //... + } + void handle(s11n::xs::keybd_event lock) + { + //... + }; + + xlat(Host& owner) + : s11n{ *this }, + owner{ owner } + { + owner.LISTEN(tier::anycast, e2::form::prop::ui::header, utf8, token) + { + //s11n::form_header.send(owner, 0, utf8); + }; + owner.LISTEN(tier::anycast, e2::form::prop::ui::footer, utf8, token) + { + //s11n::form_footer.send(owner, 0, utf8); + }; + owner.LISTEN(tier::release, hids::events::device::mouse::any, gear, token) + { + //... + }; + owner.LISTEN(tier::general, hids::events::die, gear, token) + { + //... + }; + } + }; + + xlat stream; // repl: Event tracker. + text curdir; // repl: Current working directory. + text cmdarg; // repl: Startup command line arguments. + flag active; // repl: Scripting engine lifetime. + pidt procid; // repl: PTY child process id. + task engine; // repl: Scripting engine instance. + + // repl: Proceed DirectVT input. + void ondata(view data) + { + if (active) + { + log(prompt::repl, ansi::hi(utf::debase(data))); + //stream.s11n::sync(data); + } + } + // repl: Shutdown callback handler. + void onexit(si32 code) + { + //netxs::events::enqueue(owner.This(), [&, code](auto& boss) mutable + //{ + // if (code) log(ansi::bgc(reddk).fgc(whitelt).add('\n', prompt::repl, "Exit code ", utf::to_hex_0x(code), ' ').nil()); + // else log(prompt::repl, "Exit code 0"); + // //backup.reset(); // Call repl::dtor. + //}); + } + + public: + // repl: Write client data. + void write(view data) + { + log(prompt::repl, "exec: ", ansi::hi(utf::debase(data))); + engine.write(data); + } + // repl: Start a new process. + void start(text cwd, text cmd) + { + curdir = cwd; + cmdarg = cmd; + if (!engine) + { + procid = engine.start(curdir, cmdarg, [&](auto utf8_shadow) { ondata(utf8_shadow); }, + [&](auto exit_reason) { onexit(exit_reason); }); + } + } + void shut() + { + active = faux; + if (engine) engine.shut(); + } + + repl(Host& owner) + : owner{ owner }, + stream{owner }, + active{ true } + { + + } + }; +} \ No newline at end of file diff --git a/src/netxs/desktopio/system.hpp b/src/netxs/desktopio/system.hpp index 3f5e74542..d4a30906f 100644 --- a/src/netxs/desktopio/system.hpp +++ b/src/netxs/desktopio/system.hpp @@ -3111,6 +3111,7 @@ namespace netxs::os log(prompt::dtvt, "Reading thread started", ' ', utf::to_hex_0x(stdinput.get_id())); directvt::binary::stream::reading_loop(termlink, receiver); preclose(0); + //todo revise if (termlink), see os::task::read_socket_thread() auto exit_code = wait_child(); shutdown(exit_code); log(prompt::dtvt, "Reading thread ended", ' ', utf::to_hex_0x(stdinput.get_id())); @@ -3140,6 +3141,323 @@ namespace netxs::os }; } + struct task + { + ipc::stdcon termlink; + std::thread stdinput; + std::thread stdwrite; + std::thread waitexit; + pidt proc_pid; + fd_t prochndl; + text writebuf; + std::mutex writemtx; + std::condition_variable writesyn; + std::function receiver; + std::function shutdown; + + task() + : prochndl{ os::invalid_fd }, + proc_pid{ } + { } + ~task() + { + log(prompt::task, "Destructor started"); + stop(); + log(prompt::task, "Destructor complete"); + } + + operator bool () { return termlink; } + + // task: Cleaning in order to be able to restart. + void cleanup() + { + if (stdwrite.joinable()) + { + writesyn.notify_one(); + log(prompt::task, "Writing thread joining", ' ', utf::to_hex_0x(stdwrite.get_id())); + stdwrite.join(); + } + if (stdinput.joinable()) + { + log(prompt::task, "Reading thread joining", ' ', utf::to_hex_0x(stdinput.get_id())); + stdinput.join(); + } + if (waitexit.joinable()) + { + log(prompt::task, "Child process waiter thread joining", ' ', utf::to_hex_0x(waitexit.get_id())); + waitexit.join(); + } + auto guard = std::lock_guard{ writemtx }; + termlink = {}; + writebuf = {}; + } + void shut() + { + if (termlink) + { + termlink.shut(); + } + } + auto wait_child() + { + auto guard = std::lock_guard{ writemtx }; + auto exit_code = si32{}; + log(prompt::task, "Wait child process ", proc_pid); + termlink.stop(); + if (proc_pid != 0) + { + #if defined(_WIN32) + + auto code = DWORD{ 0 }; + if (!::GetExitCodeProcess(prochndl, &code)) + { + log(prompt::task, "::GetExitCodeProcess() return code: ", ::GetLastError()); + } + else if (code == STILL_ACTIVE) + { + log(prompt::task, "Child process still running"); + auto result = WAIT_OBJECT_0 == ::WaitForSingleObject(prochndl, app_wait_timeout /*10 seconds*/); + if (!result || !::GetExitCodeProcess(prochndl, &code)) + { + ::TerminateProcess(prochndl, 0); + code = 0; + } + } + else log(prompt::task, "Child process exit code", ' ', utf::to_hex_0x(code), " (", code, ")"); + exit_code = code; + io::close(prochndl); + + #else + + auto status = int{}; + ok(::kill(proc_pid, SIGKILL), "::kill(pid, SIGKILL)", os::unexpected_msg); + ok(::waitpid(proc_pid, &status, 0), "::waitpid(pid)", os::unexpected_msg); // Wait for the child to avoid zombies. + if (WIFEXITED(status)) + { + exit_code = WEXITSTATUS(status); + log(prompt::task, "Child process exit code", ' ', exit_code); + } + else + { + exit_code = 0; + log(prompt::task, "Child process exit code not detected"); + } + + #endif + } + log(prompt::task, "Child waiting complete"); + return exit_code; + } + auto start(text cwd, text cmdline, std::function input_hndl, + std::function shutdown_hndl) + { + receiver = input_hndl; + shutdown = shutdown_hndl; + utf::change(cmdline, "\\\"", "'"); + log(prompt::task, "New child process: '", utf::debase(cmdline), "' at the ", cwd.empty() ? "current working directory"s + : "'" + cwd + "'"); + #if defined(_WIN32) + + auto s_pipe_r = os::invalid_fd; + auto s_pipe_w = os::invalid_fd; + auto m_pipe_r = os::invalid_fd; + auto m_pipe_w = os::invalid_fd; + auto startinf = STARTUPINFOEXW{ sizeof(STARTUPINFOEXW) }; + auto procsinf = PROCESS_INFORMATION{}; + auto attrbuff = std::vector{}; + auto attrsize = SIZE_T{ 0 }; + auto stdhndls = std::array{}; + + auto tunnel = [&] + { + auto sa = SECURITY_ATTRIBUTES{}; + sa.nLength = sizeof(SECURITY_ATTRIBUTES); + sa.lpSecurityDescriptor = NULL; + sa.bInheritHandle = TRUE; + if (::CreatePipe(&s_pipe_r, &m_pipe_w, &sa, 0) + && ::CreatePipe(&m_pipe_r, &s_pipe_w, &sa, 0)) + { + startinf.StartupInfo.dwFlags = STARTF_USESTDHANDLES; + startinf.StartupInfo.hStdInput = s_pipe_r; + startinf.StartupInfo.hStdOutput = s_pipe_w; + startinf.StartupInfo.hStdError = s_pipe_w; + return true; + } + else + { + io::close(m_pipe_w); + io::close(m_pipe_r); + io::close(s_pipe_w); + io::close(s_pipe_r); + return faux; + } + }; + auto fillup = [&] + { + stdhndls[0] = s_pipe_r; + stdhndls[1] = s_pipe_w; + ::InitializeProcThreadAttributeList(nullptr, 1, 0, &attrsize); + attrbuff.resize(attrsize); + startinf.lpAttributeList = reinterpret_cast(attrbuff.data()); + + if (::InitializeProcThreadAttributeList(startinf.lpAttributeList, 1, 0, &attrsize) + && ::UpdateProcThreadAttribute(startinf.lpAttributeList, + 0, + PROC_THREAD_ATTRIBUTE_HANDLE_LIST, + &stdhndls, + sizeof(stdhndls), + nullptr, + nullptr)) + { + return true; + } + else return faux; + }; + auto create = [&] + { + auto wide_cmdline = utf::to_utf(cmdline); + return ::CreateProcessW(nullptr, // lpApplicationName + wide_cmdline.data(), // lpCommandLine + nullptr, // lpProcessAttributes + nullptr, // lpThreadAttributes + TRUE, // bInheritHandles + DETACHED_PROCESS | // create without attached console, dwCreationFlags + EXTENDED_STARTUPINFO_PRESENT, // override startupInfo type + nullptr, // lpEnvironment + cwd.size() ? utf::to_utf(cwd).c_str()// lpCurrentDirectory + : nullptr, + &startinf.StartupInfo, // lpStartupInfo (ptr to STARTUPINFO) + &procsinf); // lpProcessInformation + }; + + if (tunnel() + && fillup() + && create()) + { + io::close( procsinf.hThread ); + prochndl = procsinf.hProcess; + proc_pid = procsinf.dwProcessId; + termlink = { m_pipe_r, m_pipe_w }; + } + else os::fail(prompt::task, "Child process creation error"); + + io::close(s_pipe_w); // Close inheritable handles to avoid deadlocking at process exit. + io::close(s_pipe_r); // Only when all write handles to the pipe are closed, the ReadFile function returns zero. + + #else + + fd_t to_server[2] = { os::invalid_fd, os::invalid_fd }; + fd_t to_client[2] = { os::invalid_fd, os::invalid_fd }; + ok(::pipe(to_server), "::pipe(to_server)", os::unexpected_msg); + ok(::pipe(to_client), "::pipe(to_client)", os::unexpected_msg); + + termlink = { to_server[0], to_client[1] }; + + proc_pid = ::fork(); + if (proc_pid == 0) // Child branch. + { + io::close(to_client[1]); + io::close(to_server[0]); + + ::signal(SIGINT, SIG_DFL); // Reset control signals to default values. + ::signal(SIGQUIT, SIG_DFL); // + ::signal(SIGTSTP, SIG_DFL); // + ::signal(SIGTTIN, SIG_DFL); // + ::signal(SIGTTOU, SIG_DFL); // + ::signal(SIGCHLD, SIG_DFL); // + + ::dup2(to_client[0], os::stdin_fd ); // Assign stdio lines atomically + ::dup2(to_server[1], os::stdout_fd); // = close(new); fcntl(old, F_DUPFD, new). + ::dup2(to_server[1], os::stderr_fd); // + os::fdcleanup(); + + if (cwd.size()) + { + auto err = std::error_code{}; + fs::current_path(cwd, err); + //todo use dtvt to log + //if (err) os::fail(prompt::dtvt, "Failed to change current working directory to '", cwd, "', error code: ", err.value()); + //else log(prompt::dtvt, "Change current working directory to '", cwd, "'"); + } + + os::process::execvp(cmdline); + auto errcode = errno; + //todo use dtvt to log + //os::fail(prompt::dtvt, "Exec error"); + ::close(os::stderr_fd); + ::close(os::stdout_fd); + ::close(os::stdin_fd ); + os::process::exit(errcode); + } + + // Parent branch. + io::close(to_client[0]); + io::close(to_server[1]); + + #endif + + stdinput = std::thread([&] { read_socket_thread(); }); + stdwrite = std::thread([&] { send_socket_thread(); }); + + if (termlink) log(prompt::task, "Console created for pid ", proc_pid); + + return proc_pid; + } + void stop() + { + if (termlink) + { + wait_child(); + } + cleanup(); + } + void read_socket_thread() + { + log(prompt::task, "Reading thread started", ' ', utf::to_hex_0x(stdinput.get_id())); + auto flow = text{}; + while (termlink) + { + auto shot = termlink.recv(); + if (shot && termlink) + { + flow += shot; + auto crop = view{ flow }; + utf::purify(crop); + receiver(crop); + flow.erase(0, crop.size()); // Delete processed data. + } + else break; + } + if (termlink) // Skip if stop was called via dtor. + { + auto exit_code = wait_child(); + shutdown(exit_code); + } + log(prompt::task, "Reading thread ended", ' ', utf::to_hex_0x(stdinput.get_id())); + } + void send_socket_thread() + { + log(prompt::task, "Writing thread started", ' ', utf::to_hex_0x(stdwrite.get_id())); + auto guard = std::unique_lock{ writemtx }; + auto cache = text{}; + while ((void)writesyn.wait(guard, [&]{ return writebuf.size() || !termlink; }), termlink) + { + std::swap(cache, writebuf); + guard.unlock(); + if (termlink.send(cache)) cache.clear(); + else break; + guard.lock(); + } + log(prompt::task, "Writing thread ended", ' ', utf::to_hex_0x(stdwrite.get_id())); + } + void write(view data) + { + auto guard = std::lock_guard{ writemtx }; + writebuf += data; + if (termlink) writesyn.notify_one(); + } + }; + namespace tty { auto& globals() diff --git a/src/netxs/desktopio/terminal.hpp b/src/netxs/desktopio/terminal.hpp index ca2ad0fd9..cff00dc73 100644 --- a/src/netxs/desktopio/terminal.hpp +++ b/src/netxs/desktopio/terminal.hpp @@ -7258,7 +7258,6 @@ namespace netxs::ui class dtvt : public ui::form { - using sync = std::condition_variable; using s11n = directvt::binary::s11n; // dtvt: Event handler. @@ -7312,7 +7311,7 @@ namespace netxs::ui for (auto& jgc : lock.thing) { cell::gc_set_data(jgc.token, jgc.cluster); - if constexpr (debugmode) log(prompt::term, "New gc token: ", jgc.token, " cluster size ", jgc.cluster.size(), " data: ", jgc.cluster); + if constexpr (debugmode) log(prompt::dtvt, "New gc token: ", jgc.token, " cluster size ", jgc.cluster.size(), " data: ", jgc.cluster); } netxs::events::enqueue(owner.This(), [&](auto& boss) mutable { @@ -7688,7 +7687,7 @@ namespace netxs::ui { netxs::events::enqueue(This(), [&, code](auto& boss) mutable { - if (code) log(ansi::bgc(reddk).fgc(whitelt).add('\n', prompt::term, "Exit code ", utf::to_hex_0x(code), ' ').nil()); + if (code) log(ansi::bgc(reddk).fgc(whitelt).add('\n', prompt::dtvt, "Exit code ", utf::to_hex_0x(code), ' ').nil()); else log(prompt::dtvt, "Exit code 0"); backup.reset(); // Call dtvt::dtor. }); diff --git a/src/netxs/desktopio/xml.hpp b/src/netxs/desktopio/xml.hpp index f3f2e8fc5..a87b40ac0 100644 --- a/src/netxs/desktopio/xml.hpp +++ b/src/netxs/desktopio/xml.hpp @@ -228,7 +228,7 @@ namespace netxs::xml empty_tag, // '/>' ex: ... /> equal, // '=' ex: name=value defaults, // '*' ex: name* - compact, // '/[^>]' ex: compact syntax: + //compact, // '/[^>]' ex: compact syntax: include, // ':' ex: localpath, // ex: filepath, // ex: @@ -344,7 +344,7 @@ namespace netxs::xml case eof: fgc = redlt; break; case top_token: fgc = top_token_fg; break; case end_token: fgc = end_token_fg; break; - case compact: fgc = end_token_fg; break; + //case compact: fgc = end_token_fg; break; case token: fgc = token_fg; break; case raw_text: fgc = yellowdk; break; case quoted_text: fgc = yellowdk; break; @@ -719,7 +719,7 @@ namespace netxs::xml case type::eof: return view{ "{EOF}" } ; case type::token: return view{ "{token}" } ; case type::raw_text: return view{ "{raw text}" }; - case type::compact: return view{ "{compact}" } ; + //case type::compact: return view{ "{compact}" } ; case type::quoted_text: return view_quoted_text ; case type::begin_tag: return view_begin_tag ; case type::close_tag: return view_close_tag ; @@ -749,11 +749,12 @@ namespace netxs::xml else if (data.starts_with(view_close_tag )) what = type::close_tag; else if (data.starts_with(view_begin_tag )) what = type::begin_tag; else if (data.starts_with(view_empty_tag )) what = type::empty_tag; - else if (data.starts_with(view_slash )) - { - if (last == type::token) what = type::compact; - else what = type::unknown; - } + else if (data.starts_with(view_slash )) what = type::unknown; + //else if (data.starts_with(view_slash )) + //{ + // if (last == type::token) what = type::compact; + // else what = type::unknown; + //} else if (data.starts_with(view_close_inline )) what = type::close_inline; else if (data.starts_with(view_quoted_text )) what = type::quoted_text; else if (data.starts_with(view_equal )) what = type::equal; @@ -821,7 +822,7 @@ namespace netxs::xml case type::tag_value: body(data, type::raw_text); break; case type::spaces: utf::trim_front(data, whitespaces); break; case type::na: utf::get_tail(data, find_start); break; - case type::compact: + //case type::compact: case type::unknown: if (data.size()) data.remove_prefix(1); break; default: break; } @@ -844,7 +845,6 @@ namespace netxs::xml { //todo //include external blocks if name contains ':'s. - // if name contains '/'s ... item->name = page.append(kind, name(data)); auto temp = data; utf::trim_front(temp, whitespaces); @@ -1132,12 +1132,14 @@ namespace netxs::xml { using vect = xml::document::vect; using sptr = netxs::sptr; + using hist = std::list>; sptr document; // settings: XML document. vect tempbuff; // settings: Temp buffer. vect homelist; // settings: Current directory item list. text homepath; // settings: Current working directory. text backpath; // settings: Fallback path. + hist cwdstack; // settings: Stack for saving current cwd. settings() = default; settings(settings const&) = default; @@ -1168,10 +1170,28 @@ namespace netxs::xml auto test = !!homelist.size(); if (!test) { - log(prompt::xml, ansi::err(" xml path not found: ") + homepath); + log(prompt::xml, ansi::err("xml path not found: ") + homepath); } return test; } + void popd() + { + if (cwdstack.empty()) + { + log(prompt::xml, "CWD stack is empty"); + } + else + { + auto& [gotopath, fallback] = cwdstack.back(); + cd(gotopath, fallback); + cwdstack.pop_back(); + } + } + void pushd(text gotopath, view fallback = {}) + { + cwdstack.push_back({ homepath, backpath }); + cd(gotopath, fallback); + } template auto take(text frompath, T defval = {}) { @@ -1194,7 +1214,7 @@ namespace netxs::xml else frompath = homepath + "/" + frompath; } if (tempbuff.size()) crop = tempbuff.back()->value(); - else if constexpr (!Quiet) log(prompt::xml, ansi::fgc(redlt) + " xml path not found: " + ansi::nil() + frompath); + else if constexpr (!Quiet) log(prompt::xml, ansi::fgc(redlt) + "xml path not found: " + ansi::nil() + frompath); tempbuff.clear(); if (auto result = xml::take(crop)) return result.value(); if (crop.size()) return take("/config/set/" + crop, defval); diff --git a/src/vtm.xml b/src/vtm.xml index 0aea68065..ac3ff3864 100644 --- a/src/vtm.xml +++ b/src/vtm.xml @@ -1,5 +1,26 @@ R"==( + + + + + + +
+ public class HelloWorld + { + public void printHelloWorld() + { + System.out.println("Hello World!"); + } + public static void main(String[] args) + { + printHelloWorld(); + } + } +
+
+