diff --git a/CMakeLists.txt b/CMakeLists.txt index a012870..bb8fe3b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,20 +12,6 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(QT NAMES Qt6 REQUIRED COMPONENTS Widgets WebEngineWidgets) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets WebEngineWidgets) -add_library(rktwebview SHARED - rktwebview_global.h - rktwebview.h - rktwebview.cpp - shm.h shm.cpp - shmqueue.h shmqueue.cpp - rkt_protocol.h - rktwebview_types.h - json.cpp json.h - utils.h - utils.cpp - memqueue.h memqueue.cpp -) - add_executable(rktwebview_prg main.cpp @@ -37,8 +23,6 @@ add_executable(rktwebview_prg rktutils.h rktutils.cpp command.h command.cpp - shm.h shm.cpp - shmqueue.h shmqueue.cpp rkt_protocol.h rktwebview_types.h @@ -48,15 +32,16 @@ add_executable(rktwebview_prg build_menu_from_json.h ) -add_executable(rktwebview_test - rktwebview_test.cpp -) - target_link_libraries(rktwebview_prg PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) target_link_libraries(rktwebview_prg PRIVATE Qt${QT_VERSION_MAJOR}::WebEngineWidgets) -target_compile_definitions(rktwebview PRIVATE RKTWEBVIEW_LIBRARY) target_compile_definitions(rktwebview_prg PRIVATE RKTWEBVIEW_PRG_EXE) -target_link_Libraries(rktwebview_test PRIVATE rktwebview) +add_executable(rktwebview_test + rktwebview_test.cpp + rkt_protocol.h +) +target_link_libraries(rktwebview_test PRIVATE Qt${QT_VERSION_MAJOR}::Core) + +add_dependencies(rktwebview_test rktwebview_prg) diff --git a/README.md b/README.md index 07785c7..211fc40 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,53 @@ # racket-webview-qt -The Qt backend of racket-webview +The Qt WebEngine backend of `racket-webview`. -# Version 0.2.2 +## Version 0.2.3 -QtWebEngine select popup rendering issue observed with older Qt 6.x. -Symptom: native HTML ` popup could keep growing or repainting while open while +the QtWebEngine process remained busy. Upgrading to Qt 6.11.1 resolved this. + +## Integration test + +`rktwebview_test` starts the built `rktwebview_prg` with `QProcess` and tests +the stdio handshake, request/result matching, basic webview commands, and clean +shutdown. Run it from the build directory: + +```sh +./rktwebview_test +``` + +For a protocol-only test that does not create a window: + +```sh +./rktwebview_test --protocol-only +``` + +Set `RKT_WEBVIEW_PRG` to test a backend executable outside the test program's +directory. diff --git a/STDIO-PROTOCOL.md b/STDIO-PROTOCOL.md new file mode 100644 index 0000000..75f03ab --- /dev/null +++ b/STDIO-PROTOCOL.md @@ -0,0 +1,71 @@ +# racket-webview stdio protocol + +The Qt backend is a child process of the Racket process. Communication uses +UTF-8 JSON records, one compact JSON object per line. + +- stdin of `rktwebview_prg`: commands from Racket +- stdout of `rktwebview_prg`: handshake, results, and events +- stderr of `rktwebview_prg`: diagnostics only + +No diagnostic text may be written to stdout. + +## Compatibility + +The existing numeric command identifiers in `rkt_protocol.h` and their JSON +payload objects are retained. The stdio transport adds only an envelope and a +request identifier. The request identifier allows replies to be matched even +when several Racket threads issue commands concurrently. + +## Handshake + +The backend writes this record after Qt has been initialized and the command +reader has started: + +```json +{"type":"ready","protocol":1} +``` + +Racket must not send application commands until this record has been received. + +## Command + +```json +{"type":"command","id":17,"command":6,"data":{"wv":1,"url":"https://example.test"}} +``` + +Fields: + +- `id`: positive request identifier chosen by Racket +- `command`: existing numeric command from `rkt_protocol.h` +- `data`: existing command payload object + +## Result + +```json +{"type":"result","id":17,"result":0,"data":null} +``` + +`result` has the same meaning as before. Depending on the command it can be a +`result_t`, a handle, a context identifier, a window-state value, or a metric. +For `CMD_CALL_JS`, `data` contains the JavaScript result string. + +Every command now receives a result record, including commands that previously +returned `void`, such as close and setting the OU token. Their public Racket +return values remain unchanged. + +## Event + +```json +{"type":"event","wv":1,"data":"{\"event\":\"page-loaded\",...}"} +``` + +The `data` field is the original event JSON string. It is deliberately not +restructured by the transport, so existing event parsing remains unchanged. +Events are independent of command results. + +## Shutdown + +A normal shutdown sends `CMD_QUIT`, waits for its result, closes the child's +stdin, and waits for the child process. If the Racket process terminates +unexpectedly, the operating system closes the pipe; EOF on stdin makes the Qt +backend close all windows and quit. diff --git a/lib/windows-x86_64/rktwebview.exp b/lib/windows-x86_64/rktwebview.exp deleted file mode 100644 index 5fe64a7..0000000 Binary files a/lib/windows-x86_64/rktwebview.exp and /dev/null differ diff --git a/lib/windows-x86_64/rktwebview.lib b/lib/windows-x86_64/rktwebview.lib deleted file mode 100644 index d741d18..0000000 Binary files a/lib/windows-x86_64/rktwebview.lib and /dev/null differ diff --git a/main.cpp b/main.cpp index fbfe984..26f7eb0 100644 --- a/main.cpp +++ b/main.cpp @@ -1,494 +1,461 @@ -#include -#include #include - -#include "rkt_protocol.h" -#include "shm.h" -#include "shmqueue.h" -#include "command.h" -#include "utils.h" - #include #include +#include +#include +#include +#include +#include + +#include "rkt_protocol.h" #include "rktwebview_qt.h" +#include "utils.h" +namespace { -#ifdef DEBUG_RKT_WEBVIEW -#define WAIT_TIME (300 * 1000) -#else -#define WAIT_TIME (10 * 1000) -#endif +constexpr int STDIO_PROTOCOL_VERSION = 1; -static void free_data(rkt_data_t *d) +std::mutex output_mutex; + +void writeMessage(const QJsonObject &message) { - do_free_data(d); + const QByteArray json = QJsonDocument(message).toJson(QJsonDocument::Compact); + std::lock_guard lock(output_mutex); + std::cout.write(json.constData(), json.size()); + std::cout.put('\n'); + std::cout.flush(); +} + +void sendReady() +{ + QJsonObject message; + message["type"] = "ready"; + message["protocol"] = STDIO_PROTOCOL_VERSION; + writeMessage(message); +} + +void sendResult(int id, + int result, + const QJsonValue &data = QJsonValue(QJsonValue::Null)) +{ + QJsonObject message; + message["type"] = "result"; + message["id"] = id; + message["result"] = result; + message["data"] = data; + writeMessage(message); +} + +void sendProtocolError(const QString &messageText, int id = -1) +{ + QJsonObject message; + message["type"] = "protocol-error"; + if (id >= 0) { + message["id"] = id; + } + message["message"] = messageText; + writeMessage(message); +} + +void freeData(rkt_data_t *data) +{ + do_free_data(data); } class Handler : public QThread { public: - Shm *shm; - ShmQueue *command_queue; - ShmQueue *result_queue; - ShmQueue *event_queue; - Rktwebview_qt *webview_handler; - bool quit; - - // QThread interface -protected: - void run(); -}; - -class Alive : public QThread -{ -public: - Handler *handler; - ShmQueue *alive_queue; - ShmQueue *alive_ack_queue; - bool alive_timeout; + Rktwebview_qt *webviewHandler = nullptr; protected: - void run(); + void run() override; }; -static Handler *_handler; -static Alive *_alive; -static void event_cb(rkt_data_t *data) +void eventCallback(rkt_data_t *data) { - if (data->kind != rkt_data_kind_t::event) { + if (data == nullptr || data->kind != rkt_data_kind_t::event) { + freeData(data); return; } - int wv = data->data.event.wv; - char *evt = data->data.event.event; + QJsonObject message; + message["type"] = "event"; + message["wv"] = data->data.event.wv; + message["data"] = QString::fromUtf8(data->data.event.event); + writeMessage(message); - std::string evt_d(evt); - _handler->event_queue->enqueue(wv, evt_d); - - free_data(data); + freeData(data); } +} // namespace int main(int argc, char *argv[]) { - const char *me = argv[0]; + INFO0("Starting stdio backend\n"); - { - time_t my_time = time(NULL); - INFO1("Starting at %s", ctime(&my_time)); - } + Handler commandHandler; + commandHandler.webviewHandler = new Rktwebview_qt(argc, argv); + commandHandler.webviewHandler->initApp(); + commandHandler.start(); - if (argc < 8) { - ERROR1("%s: wrong number of arguments\n", me); - exit(1); - } + sendReady(); + commandHandler.webviewHandler->execApp(); - const char *shm_name = argv[1]; - const char *shm_size_str = argv[2]; - const char *cmd_slot_str = argv[3]; - const char *res_slot_str = argv[4]; - const char *evt_slot_str = argv[5]; - const char *alive_slot_str = argv[6]; - const char *alive_ack_slot_str = argv[7]; + INFO0("Qt event loop stopped; waiting for stdin handler\n"); + commandHandler.wait(); - size_t shm_size = atoi(shm_size_str); - int cmd_slot = atoi(cmd_slot_str); - int res_slot = atoi(res_slot_str); - int evt_slot = atoi(evt_slot_str); - int alive_slot = atoi(alive_slot_str); - int alive_ack_slot = atoi(alive_ack_slot_str); - - MKLOGSTMT(LOG_INFO, fprintf(stderr, "%s %s %s %s %s %s %s %s\n", me, shm_name, shm_size_str, - cmd_slot_str, res_slot_str, evt_slot_str, alive_slot_str, alive_ack_slot_str)); - MKLOGSTMT(LOG_INFO, fprintf(stderr, "%s %s %d %d %d %d %d %d\n", me, shm_name, static_cast(shm_size), - cmd_slot, res_slot, evt_slot, alive_slot, alive_ack_slot)); - - if (!(shm_size > 0 && cmd_slot > 0 && res_slot > 0 && evt_slot > 0 && alive_slot > 0 && alive_ack_slot > 0)) { - ERROR1("%s: Invalid shm size or slots\n", me); - exit(2); - } - - Handler *handler = new Handler(); - _handler = handler; - handler->shm = new Shm(shm_name, shm_size, false); - handler->command_queue = new ShmQueue(handler->shm, cmd_slot, false); - handler->result_queue = new ShmQueue(handler->shm, res_slot, false); - handler->event_queue = new ShmQueue(handler->shm, evt_slot, false); - handler->webview_handler = new Rktwebview_qt(argc, argv); - handler->start(); - - Alive *alive = new Alive(); - _alive = alive; - alive->handler = handler; - alive->alive_timeout = false; - alive->alive_queue = new ShmQueue(handler->shm, alive_slot, false); - alive->alive_ack_queue = new ShmQueue(handler->shm, alive_ack_slot, false); - alive->start(); - - handler->webview_handler->initApp(); - handler->webview_handler->execApp(); - - INFO0("waiting for thread to end\n"); - handler->wait(); - INFO0("Handler thread stopped\n"); - alive->wait(); - INFO0("Alive thread stopped\n"); - - if (alive->alive_timeout) { - // take ownership over all queues and SHM, because we didn't get an alive message - // and probably the racket side has crashed because of that. - ERROR0("Taking ownership of shared memory, shared queues and shared semaphores\n"); - handler->command_queue->takeOwnership(); - handler->result_queue->takeOwnership(); - handler->event_queue->takeOwnership(); - handler->shm->takeOwnership(); - alive->alive_queue->takeOwnership(); - alive->alive_ack_queue->takeOwnership(); - } - - INFO0("cleaning up shm\n"); - - delete alive->alive_queue; - delete alive; - - delete handler->webview_handler; - delete handler->command_queue; - delete handler->result_queue; - delete handler->event_queue; - delete handler->shm; - delete handler; - - { - time_t my_time = time(NULL); - INFO1("Exiting at %s", ctime(&my_time)); - } + delete commandHandler.webviewHandler; + commandHandler.webviewHandler = nullptr; + INFO0("Exiting stdio backend\n"); return 0; } void Handler::run() { - int wait_ms = WAIT_TIME; // 10 seconds. - while (!quit) { - int cmd; - std::string data; - bool something_came = command_queue->dequeue(cmd, data, wait_ms); - if (!something_came) { - DEBUG1("No command received last %d seconds\n", wait_ms / 1000); - cmd = CMD_NOOP; + std::string line; + bool quit = false; + + while (!quit && std::getline(std::cin, line)) { + if (line.empty()) { + continue; } - QJsonObject data_obj = QJsonDocument::fromJson(data.c_str()).object(); - switch(cmd) { - case CMD_NOOP: { - if (quit) { - DEBUG1("Alive timeout, quit = %d\n", quit); - webview_handler->closeAllWindows(); - DEBUG0("Closed all windows\n"); - webview_handler->rktQuit(); - DEBUG0("Quit application\n"); - } - } - break; - case CMD_QUIT: { - INFO0("Got quit message\n"); - webview_handler->rktQuit(); - INFO0("Enqueing RESULT_QUIT to result queue\n"); - result_queue->enqueue(RESULT_QUIT); - quit = true; - } - break; - case CMD_INFO: { - int open_windows = webview_handler->openWindows(); - result_queue->enqueue(open_windows); - } - break; - case CMD_HANDLE_IS_VALID: { - int wv = data_obj["wv"].toInt(); - bool oke = webview_handler->rktValid(wv); - result_queue->enqueue(oke); - } - break; - case CMD_SET_LOGLEVEL: { - int ll = data_obj["wv"].toInt(); - setLogLevel(ll); - bool oke = true; - result_queue->enqueue(oke); - } - break; - case CMD_CONTEXT_NEW: { - QString boilerplate_js = data_obj["boilerplate_js"].toString(); - bool has_pem_cert = data_obj["has_pem_cert"].toBool(); - QString pem_cert = data_obj["pem_cert"].toString(); - int context = webview_handler->newContext(boilerplate_js, has_pem_cert, pem_cert); - result_queue->enqueue(context); - } - break; - case CMD_CREATE_WV: { - int context = data_obj["context"].toInt(); - int parent = data_obj["parent"].toInt(); + QJsonParseError parseError; + const QJsonDocument document = + QJsonDocument::fromJson(QByteArray::fromStdString(line), &parseError); - int wv = webview_handler->rktWebViewCreate(context, parent, event_cb); - result_queue->enqueue(wv); - } - break; - case CMD_CLOSE_WV: { - int wv = data_obj["wv"].toInt(); - webview_handler->rktWebViewClose(wv); - } - break; - case CMD_SET_URL: { - int wv = data_obj["wv"].toInt(); - QString url = data_obj["url"].toString(); - result_t r = webview_handler->rktSetUrl(wv, url.toUtf8().constData()); - result_queue->enqueue(r); - } - break; - case CMD_SET_HTML: { - int wv = data_obj["wv"].toInt(); - QString html = data_obj["html"].toString(); - result_t r = webview_handler->rktSetHtml(wv, html.toUtf8().constData()); - result_queue->enqueue(r); - } - break; - case CMD_RUN_JS: { - int wv = data_obj["wv"].toInt(); - QString js = data_obj["js"].toString(); - result_t r = webview_handler->rktRunJs(wv, js); - result_queue->enqueue(r); - } - break; - case CMD_CALL_JS: { - int wv = data_obj["wv"].toInt(); - QString js = data_obj["js"].toString(); - rkt_data_t *res = webview_handler->rktCallJs(wv, js.toUtf8().constData()); - result_queue->enqueue(res->data.js_result.result, res->data.js_result.value); - free_data(res); - } - break; - case CMD_OPEN_DEVTOOLS: { - int wv = data_obj["wv"].toInt(); - result_t r = webview_handler->rktOpenDevtools(wv); - result_queue->enqueue(r); - } - break; - case CMD_MOVE: { - int wv = data_obj["wv"].toInt(); - int x = data_obj["x"].toInt(); - int y = data_obj["y"].toInt(); - result_t r = webview_handler->rktMove(wv, x, y); - result_queue->enqueue(r); - } - break; - case CMD_RESIZE: { - int wv = data_obj["wv"].toInt(); - int w = data_obj["w"].toInt(); - int h = data_obj["h"].toInt(); - result_t r = webview_handler->rktResize(wv, w, h); - result_queue->enqueue(r); - } - break; - case CMD_HIDE: { - int wv = data_obj["wv"].toInt(); - result_t r = webview_handler->rktHideWindow(wv); - result_queue->enqueue(r); - } - break; - case CMD_SHOW: { - int wv = data_obj["wv"].toInt(); - result_t r = webview_handler->rktShowWindow(wv); - result_queue->enqueue(r); - } - break; - case CMD_PRESENT: { - int wv = data_obj["wv"].toInt(); - result_t r = webview_handler->rktPresentWindow(wv); - result_queue->enqueue(r); - } - break; - case CMD_MAXIMIZE: { - int wv = data_obj["wv"].toInt(); - result_t r = webview_handler->rktMaximizeWindow(wv); - result_queue->enqueue(r); - } - break; - case CMD_MINIMIZE: { - int wv = data_obj["wv"].toInt(); - result_t r = webview_handler->rktMinimizeWindow(wv); - result_queue->enqueue(r); - } - break; - case CMD_SHOW_NORMAL: { - int wv = data_obj["wv"].toInt(); - result_t r = webview_handler->rktShowNormalWindow(wv); - result_queue->enqueue(r); - } - break; - case CMD_WINDOW_STATE: { - int wv = data_obj["wv"].toInt(); - window_state_t r = webview_handler->rktWindowState(wv); - result_queue->enqueue(r); - } - break; - case CMD_SET_TITLE: { - int wv = data_obj["wv"].toInt(); - QString title = data_obj["title"].toString(); - result_t r = webview_handler->rktWindowSetTitle(wv, title.toUtf8().constData()); - result_queue->enqueue(r); - } - break; - case CMD_SET_ICON: { - int wv = data_obj["wv"].toInt(); - QString icon_file = data_obj["icon"].toString(); - result_t r = webview_handler->rktWindowSetIcon(wv, icon_file.toUtf8().constData()); - result_queue->enqueue(r); - } - break; - case CMD_CREATE_TRAY: { - QString icon_file = data_obj["icon"].toString(); - QString tooltip = data_obj["tooltip"].toString(); - - int tray = webview_handler->rktTrayCreate(icon_file.toUtf8().constData(), - tooltip.toUtf8().constData(), - event_cb); - result_queue->enqueue(tray); - } - break; - case CMD_TRAY_SET_ICON: { - int tray = data_obj["wv"].toInt(); - QString icon_file = data_obj["icon"].toString(); - - result_t r = webview_handler->rktTraySetIcon(tray, - icon_file.toUtf8().constData()); - result_queue->enqueue(r); - } - break; - case CMD_TRAY_SET_TOOLTIP: { - int tray = data_obj["wv"].toInt(); - QString tooltip = data_obj["tooltip"].toString(); - - result_t r = webview_handler->rktTraySetTooltip(tray, - tooltip.toUtf8().constData()); - result_queue->enqueue(r); - } - break; - case CMD_TRAY_SHOW_MESSAGE: { - int tray = data_obj["wv"].toInt(); - QString title = data_obj["title"].toString(); - QString message = data_obj["message"].toString(); - - result_t r = webview_handler->rktTrayShowMessage(tray, - title.toUtf8().constData(), - message.toUtf8().constData()); - result_queue->enqueue(r); - } - break; - case CMD_TRAY_CLEAR_MENU: { - int tray = data_obj["wv"].toInt(); - result_t r = webview_handler->rktTraySetMenu(tray, nullptr); - result_queue->enqueue(r); - } - break; - case CMD_TRAY_SET_MENU: { - int tray = data_obj["wv"].toInt(); - QString menu_json = data_obj["menu_json"].toString(); - - result_t r = webview_handler->rktTraySetMenu(tray, - menu_json.toUtf8().constData()); - result_queue->enqueue(r); - } - break; - case CMD_CHOOSE_DIR: { - int wv = data_obj["wv"].toInt(); - QString title = data_obj["title"].toString(); - QString base_dir = data_obj["base_dir"].toString(); - result_t r = webview_handler->rktChooseDir(wv, - title.toUtf8().constData(), - base_dir.toUtf8().constData() - ); - result_queue->enqueue(r); - } - break; - case CMD_FILE_OPEN: { - int wv = data_obj["wv"].toInt(); - QString title = data_obj["title"].toString(); - QString base_dir = data_obj["base_dir"].toString(); - QString permitted_exts = data_obj["permitted_exts"].toString(); - result_t r = webview_handler->rktFileOpen(wv, - title.toUtf8().constData(), - base_dir.toUtf8().constData(), - permitted_exts.toUtf8().constData() - ); - result_queue->enqueue(r); - } - break; - case CMD_FILE_SAVE: { - int wv = data_obj["wv"].toInt(); - QString title = data_obj["title"].toString(); - QString base_dir = data_obj["base_dir"].toString(); - QString permitted_exts = data_obj["permitted_exts"].toString(); - result_t r = webview_handler->rktFileSave(wv, - title.toUtf8().constData(), - base_dir.toUtf8().constData(), - permitted_exts.toUtf8().constData() - ); - result_queue->enqueue(r); - } - break; - case CMD_SET_OU_TOKEN: { - int wv = data_obj["wv"].toInt(); - QString token = data_obj["token"].toString(); - webview_handler->rktSetOUToken(wv, token.toUtf8().constData()); - } - break; - case CMD_MSG_BOX: { - int wv = data_obj["wv"].toInt(); - QString title = data_obj["title"].toString(); - QString message = data_obj["message"].toString(); - QString submsg = data_obj["submessage"].toString(); - int type = data_obj["type"].toInt(); - result_t r = webview_handler->rktMessageBox(wv, - title.toUtf8().constData(), - message.toUtf8().constData(), - submsg.toUtf8().constData(), - static_cast(type) - ); - result_queue->enqueue(r); - } - break; - default: { - ERROR1("Unknown command: %d\n", cmd); - } + if (parseError.error != QJsonParseError::NoError || !document.isObject()) { + const QString message = QString("Invalid protocol JSON: %1") + .arg(parseError.errorString()); + ERROR1("%s\n", message.toUtf8().constData()); + sendProtocolError(message); + continue; } - } - DEBUG0("Exiting handler thread\n"); -} -void Alive::run() -{ - int wait_ms = WAIT_TIME; - int ping_no; - std::string data; - bool go_on = true; - while (go_on) { - bool something_came = alive_queue->dequeue(ping_no, data, wait_ms); - if (!something_came) { - ERROR0("No alive message received, stopping alive loop and quitting rktwebview_prg\n"); - handler->quit = true; - handler->command_queue->enqueue(CMD_NOOP); - alive_timeout = true; - go_on = false; - } else { - if (ping_no == CMD_ALIVE_QUIT) { - DEBUG0("Got Quit ping for alive thread\n"); - go_on = false; + const QJsonObject request = document.object(); + const int id = request["id"].toInt(-1); + + if (request["type"].toString() != "command") { + sendProtocolError("Expected a command message", id); + continue; + } + + const int command = request["command"].toInt(CMD_NOOP); + const QJsonObject data = request["data"].toObject(); + + if (id < 0) { + sendProtocolError("Command has no valid request id"); + continue; + } + + switch (command) { + case CMD_NOOP: + sendResult(id, result_t::oke); + break; + + case CMD_QUIT: + INFO0("Got quit command\n"); + webviewHandler->rktQuit(); + sendResult(id, RESULT_QUIT); + quit = true; + break; + + case CMD_INFO: + sendResult(id, webviewHandler->openWindows()); + break; + + case CMD_HANDLE_IS_VALID: { + const int wv = data["wv"].toInt(); + sendResult(id, webviewHandler->rktValid(wv) ? 1 : 0); + break; + } + + case CMD_SET_LOGLEVEL: { + const int level = data["wv"].toInt(); + setLogLevel(level); + sendResult(id, result_t::oke); + break; + } + + case CMD_CONTEXT_NEW: { + const QString boilerplateJs = data["boilerplate_js"].toString(); + const bool hasPemCertificate = data["has_pem_cert"].toBool(); + const QString pemCertificate = data["pem_cert"].toString(); + const int context = webviewHandler->newContext(boilerplateJs, + hasPemCertificate, + pemCertificate); + sendResult(id, context); + break; + } + + case CMD_CREATE_WV: { + const int context = data["context"].toInt(); + const int parent = data["parent"].toInt(); + const int wv = webviewHandler->rktWebViewCreate(context, + parent, + eventCallback); + sendResult(id, wv); + break; + } + + case CMD_CLOSE_WV: { + const int wv = data["wv"].toInt(); + webviewHandler->rktWebViewClose(wv); + sendResult(id, result_t::oke); + break; + } + + case CMD_SET_URL: { + const int wv = data["wv"].toInt(); + const QString url = data["url"].toString(); + sendResult(id, webviewHandler->rktSetUrl(wv, url.toUtf8().constData())); + break; + } + + case CMD_SET_HTML: { + const int wv = data["wv"].toInt(); + const QString html = data["html"].toString(); + sendResult(id, webviewHandler->rktSetHtml(wv, html.toUtf8().constData())); + break; + } + + case CMD_RUN_JS: { + const int wv = data["wv"].toInt(); + const QString js = data["js"].toString(); + sendResult(id, webviewHandler->rktRunJs(wv, js)); + break; + } + + case CMD_CALL_JS: { + const int wv = data["wv"].toInt(); + const QString js = data["js"].toString(); + rkt_data_t *response = webviewHandler->rktCallJs(wv, js.toUtf8().constData()); + if (response == nullptr || response->kind != rkt_data_kind_t::js_result) { + sendResult(id, result_t::failed, "Missing JavaScript result"); } else { - DEBUG1("Got alive ping: %d\n", ping_no); - alive_ack_queue->enqueue(ping_no); + sendResult(id, + response->data.js_result.result, + QString::fromUtf8(response->data.js_result.value)); } + freeData(response); + break; + } + + case CMD_OPEN_DEVTOOLS: { + const int wv = data["wv"].toInt(); + sendResult(id, webviewHandler->rktOpenDevtools(wv)); + break; + } + + case CMD_MOVE: { + const int wv = data["wv"].toInt(); + sendResult(id, + webviewHandler->rktMove(wv, + data["x"].toInt(), + data["y"].toInt())); + break; + } + + case CMD_RESIZE: { + const int wv = data["wv"].toInt(); + sendResult(id, + webviewHandler->rktResize(wv, + data["w"].toInt(), + data["h"].toInt())); + break; + } + + case CMD_HIDE: { + const int wv = data["wv"].toInt(); + sendResult(id, webviewHandler->rktHideWindow(wv)); + break; + } + + case CMD_SHOW: { + const int wv = data["wv"].toInt(); + sendResult(id, webviewHandler->rktShowWindow(wv)); + break; + } + + case CMD_PRESENT: { + const int wv = data["wv"].toInt(); + sendResult(id, webviewHandler->rktPresentWindow(wv)); + break; + } + + case CMD_MAXIMIZE: { + const int wv = data["wv"].toInt(); + sendResult(id, webviewHandler->rktMaximizeWindow(wv)); + break; + } + + case CMD_MINIMIZE: { + const int wv = data["wv"].toInt(); + sendResult(id, webviewHandler->rktMinimizeWindow(wv)); + break; + } + + case CMD_SHOW_NORMAL: { + const int wv = data["wv"].toInt(); + sendResult(id, webviewHandler->rktShowNormalWindow(wv)); + break; + } + + case CMD_WINDOW_STATE: { + const int wv = data["wv"].toInt(); + sendResult(id, webviewHandler->rktWindowState(wv)); + break; + } + + case CMD_SET_TITLE: { + const int wv = data["wv"].toInt(); + const QString title = data["title"].toString(); + sendResult(id, + webviewHandler->rktWindowSetTitle(wv, + title.toUtf8().constData())); + break; + } + + case CMD_SET_ICON: { + const int wv = data["wv"].toInt(); + const QString icon = data["icon"].toString(); + sendResult(id, + webviewHandler->rktWindowSetIcon(wv, + icon.toUtf8().constData())); + break; + } + + case CMD_CREATE_TRAY: { + const QString icon = data["icon"].toString(); + const QString tooltip = data["tooltip"].toString(); + const int tray = webviewHandler->rktTrayCreate(icon.toUtf8().constData(), + tooltip.toUtf8().constData(), + eventCallback); + sendResult(id, tray); + break; + } + + case CMD_TRAY_SET_ICON: { + const int tray = data["wv"].toInt(); + const QString icon = data["icon"].toString(); + sendResult(id, + webviewHandler->rktTraySetIcon(tray, + icon.toUtf8().constData())); + break; + } + + case CMD_TRAY_SET_TOOLTIP: { + const int tray = data["wv"].toInt(); + const QString tooltip = data["tooltip"].toString(); + sendResult(id, + webviewHandler->rktTraySetTooltip(tray, + tooltip.toUtf8().constData())); + break; + } + + case CMD_TRAY_SHOW_MESSAGE: { + const int tray = data["wv"].toInt(); + const QString title = data["title"].toString(); + const QString message = data["message"].toString(); + sendResult(id, + webviewHandler->rktTrayShowMessage(tray, + title.toUtf8().constData(), + message.toUtf8().constData())); + break; + } + + case CMD_TRAY_CLEAR_MENU: { + const int tray = data["wv"].toInt(); + sendResult(id, webviewHandler->rktTraySetMenu(tray, nullptr)); + break; + } + + case CMD_TRAY_SET_MENU: { + const int tray = data["wv"].toInt(); + const QString menuJson = data["menu_json"].toString(); + sendResult(id, + webviewHandler->rktTraySetMenu(tray, + menuJson.toUtf8().constData())); + break; + } + + case CMD_CHOOSE_DIR: { + const int wv = data["wv"].toInt(); + const QString title = data["title"].toString(); + const QString baseDir = data["base_dir"].toString(); + sendResult(id, + webviewHandler->rktChooseDir(wv, + title.toUtf8().constData(), + baseDir.toUtf8().constData())); + break; + } + + case CMD_FILE_OPEN: { + const int wv = data["wv"].toInt(); + const QString title = data["title"].toString(); + const QString baseDir = data["base_dir"].toString(); + const QString permittedExtensions = data["permitted_exts"].toString(); + sendResult(id, + webviewHandler->rktFileOpen(wv, + title.toUtf8().constData(), + baseDir.toUtf8().constData(), + permittedExtensions.toUtf8().constData())); + break; + } + + case CMD_FILE_SAVE: { + const int wv = data["wv"].toInt(); + const QString title = data["title"].toString(); + const QString baseDir = data["base_dir"].toString(); + const QString permittedExtensions = data["permitted_exts"].toString(); + sendResult(id, + webviewHandler->rktFileSave(wv, + title.toUtf8().constData(), + baseDir.toUtf8().constData(), + permittedExtensions.toUtf8().constData())); + break; + } + + case CMD_SET_OU_TOKEN: { + const int wv = data["wv"].toInt(); + const QString token = data["token"].toString(); + webviewHandler->rktSetOUToken(wv, token.toUtf8().constData()); + sendResult(id, result_t::oke); + break; + } + + case CMD_MSG_BOX: { + const int wv = data["wv"].toInt(); + const QString title = data["title"].toString(); + const QString message = data["message"].toString(); + const QString submessage = data["submessage"].toString(); + const auto type = static_cast(data["type"].toInt()); + sendResult(id, + webviewHandler->rktMessageBox(wv, + title.toUtf8().constData(), + message.toUtf8().constData(), + submessage.toUtf8().constData(), + type)); + break; + } + + default: { + const QString message = QString("Unknown command: %1").arg(command); + ERROR1("%s\n", message.toUtf8().constData()); + sendResult(id, result_t::failed, message); + break; + } } } - DEBUG0("Exiting alive thread\n"); + + if (!quit) { + INFO0("stdin closed; stopping Qt backend\n"); + webviewHandler->closeAllWindows(); + webviewHandler->rktQuit(); + } + + DEBUG0("Exiting stdin handler\n"); } diff --git a/rkt_protocol.h b/rkt_protocol.h index d8f636e..5b66e09 100644 --- a/rkt_protocol.h +++ b/rkt_protocol.h @@ -1,6 +1,10 @@ #ifndef RKT_PROTOCOL_H #define RKT_PROTOCOL_H +// The numeric command identifiers and payload objects are shared by both +// transports. The active transport wraps each command in one JSON line; see +// STDIO-PROTOCOL.md. + #define CMD_HANDLE_IS_VALID 1 #define CMD_QUIT 2 #define CMD_CONTEXT_NEW 3 // arguments: boilerplate_js: string, has_cert: bool, cert_pem: string -> context: int diff --git a/rktwebview_qt.cpp b/rktwebview_qt.cpp index b7a4a98..5216eb1 100644 --- a/rktwebview_qt.cpp +++ b/rktwebview_qt.cpp @@ -180,15 +180,14 @@ void Rktwebview_qt::processCommand(Command *cmd) case COMMAND_CLOSE: { int wv = cmd->args[0].toInt(); if (_views.contains(wv)) { - WebviewWindow *w= _views[wv]; - _views.remove(wv); + WebviewWindow *w = _views[wv]; + + // WebviewWindow::closeEvent removes the view administration and + // schedules the window and web view with deleteLater(). Do not + // inspect or delete w after closeView(): processing the close + // event can already have scheduled or performed its destruction. w->closeView(); cmd->result = true; - while(w->isVisible()) { - _app->processEvents(); - } - _view_js_callbacks.remove(wv); - delete w; } else if (_trays.contains(wv)) { QSystemTrayIcon *tray = _trays[wv]; _trays.remove(wv); diff --git a/rktwebview_test.cpp b/rktwebview_test.cpp index ce1b985..3fda309 100644 --- a/rktwebview_test.cpp +++ b/rktwebview_test.cpp @@ -1,94 +1,301 @@ -#include "rktwebview.h" -#include +#include +#include +#include +#include +#include +#include +#include -#ifdef _WIN32 -#include -#else -#include -#endif -#include "utils.h" +#include +#include -void evt_cb(int n) +#include "rkt_protocol.h" + +namespace { + +constexpr int protocolVersion = 1; +constexpr int timeoutMs = 10000; + +class ProtocolClient { - fprintf(stderr, "events waiting: %d - %d\n", n, rkt_webview_events_waiting()); +public: + explicit ProtocolClient(QString program) + : program_(std::move(program)) + { + process_.setProcessChannelMode(QProcess::SeparateChannels); + } + + bool start() + { + process_.start(program_, {}); + if (!process_.waitForStarted(timeoutMs)) { + fail(QString("Could not start %1: %2") + .arg(program_, process_.errorString())); + return false; + } + + const QJsonObject ready = readMessage("ready", -1); + if (ready.isEmpty()) { + return false; + } + if (ready["protocol"].toInt(-1) != protocolVersion) { + fail(QString("Unexpected protocol version: %1") + .arg(ready["protocol"].toInt(-1))); + return false; + } + + std::fprintf(stderr, "PASS: backend ready (protocol %d)\n", protocolVersion); + return true; + } + + QJsonObject command(int command, const QJsonObject &data = {}) + { + const int id = nextId_++; + + QJsonObject request; + request["type"] = "command"; + request["id"] = id; + request["command"] = command; + request["data"] = data; + + const QByteArray json = QJsonDocument(request).toJson(QJsonDocument::Compact) + '\n'; + if (process_.write(json) != json.size() || !process_.waitForBytesWritten(timeoutMs)) { + fail(QString("Could not write command %1: %2") + .arg(command) + .arg(process_.errorString())); + return {}; + } + + return readMessage("result", id); + } + + bool stop() + { + const QJsonObject result = command(CMD_QUIT); + if (result.isEmpty() || result["result"].toInt() != RESULT_QUIT) { + fail("QUIT did not return RESULT_QUIT"); + process_.kill(); + process_.waitForFinished(timeoutMs); + return false; + } + + process_.closeWriteChannel(); + if (!process_.waitForFinished(timeoutMs)) { + fail("Backend did not stop after QUIT"); + process_.kill(); + process_.waitForFinished(timeoutMs); + return false; + } + + drainStderr(); + std::fprintf(stderr, "PASS: backend stopped cleanly\n"); + return process_.exitStatus() == QProcess::NormalExit && process_.exitCode() == 0; + } + + void abort() + { + if (process_.state() != QProcess::NotRunning) { + process_.kill(); + process_.waitForFinished(timeoutMs); + } + drainStderr(); + } + +private: + QJsonObject readMessage(const QString &expectedType, int expectedId) + { + QElapsedTimer timer; + timer.start(); + + while (timer.elapsed() < timeoutMs) { + while (process_.canReadLine()) { + const QByteArray line = process_.readLine().trimmed(); + if (line.isEmpty()) { + continue; + } + + QJsonParseError error; + const QJsonDocument document = QJsonDocument::fromJson(line, &error); + if (error.error != QJsonParseError::NoError || !document.isObject()) { + fail(QString("Invalid JSON from backend: %1").arg(QString::fromUtf8(line))); + return {}; + } + + const QJsonObject message = document.object(); + const QString type = message["type"].toString(); + + if (type == "event") { + std::fprintf(stderr, + "EVENT: webview=%d %s\n", + message["wv"].toInt(), + message["data"].toString().toUtf8().constData()); + continue; + } + + if (type == "protocol-error") { + fail(QString("Protocol error: %1").arg(message["message"].toString())); + return {}; + } + + if (type != expectedType) { + fail(QString("Expected message type %1, got %2") + .arg(expectedType, type)); + return {}; + } + + if (expectedId >= 0 && message["id"].toInt(-1) != expectedId) { + fail(QString("Expected result id %1, got %2") + .arg(expectedId) + .arg(message["id"].toInt(-1))); + return {}; + } + + return message; + } + + drainStderr(); + if (process_.state() == QProcess::NotRunning) { + fail(QString("Backend stopped unexpectedly with exit code %1") + .arg(process_.exitCode())); + return {}; + } + + process_.waitForReadyRead(100); + } + + fail(QString("Timed out waiting for %1").arg(expectedType)); + return {}; + } + + void drainStderr() + { + const QByteArray diagnostics = process_.readAllStandardError(); + if (!diagnostics.isEmpty()) { + std::fwrite(diagnostics.constData(), 1, diagnostics.size(), stderr); + } + } + + static void fail(const QString &message) + { + std::fprintf(stderr, "FAIL: %s\n", message.toUtf8().constData()); + } + + QString program_; + QProcess process_; + int nextId_ = 1; +}; + +bool expectResult(const QJsonObject &message, int expected, const char *description) +{ + if (message.isEmpty()) { + return false; + } + + const int actual = message["result"].toInt(); + if (actual != expected) { + std::fprintf(stderr, + "FAIL: %s returned %d, expected %d\n", + description, + actual, + expected); + return false; + } + + std::fprintf(stderr, "PASS: %s\n", description); + return true; } +QString backendProgram(const QCoreApplication &app) +{ + const QString environmentProgram = qEnvironmentVariable("RKT_WEBVIEW_PRG"); + if (!environmentProgram.isEmpty()) { + return environmentProgram; + } + +#ifdef Q_OS_WIN + return app.applicationDirPath() + "/rktwebview_prg.exe"; +#else + return app.applicationDirPath() + "/rktwebview_prg"; +#endif +} + +} // namespace + int main(int argc, char *argv[]) { - std::string me = argv[0]; + QCoreApplication app(argc, argv); + const bool protocolOnly = app.arguments().contains("--protocol-only"); + const QString program = backendProgram(app); - std::string loc = basedir(me); -#ifdef _WIN32 - std::string prg = loc + "\\rktwebview_prg.exe"; - SetDllDirectoryA("C:\\Qt\\6.11.1\\msvc2022_64\\bin"); -#else - std::string prg = loc + "/rktwebview_prg"; -#endif - -#ifdef _WIN32 - { - std::string e = std::string("RKT_WEBVIEW_PRG=") + prg; - _putenv(e.c_str()); + if (!QFileInfo::exists(program)) { + std::fprintf(stderr, + "FAIL: backend executable not found: %s\n", + program.toUtf8().constData()); + return 1; } -#else - setenv("RKT_WEBVIEW_PRG", prg.c_str(), true); - setenv("LD_LIBRARY_PATH", loc.c_str(), true); -#endif - rkt_webview_init(__FUNCTION__); - - int context = rkt_webview_new_context("", nullptr); - rkt_webview_set_loglevel(rkt_webview_loglevel_t::log_debug); - - const char *icon_file = "../../resource/rktplayer.png"; - FILE *f = fopen(icon_file, "rb"); - if (f == nullptr) { - WARN1("Cannot find icon file %s\n", icon_file); - } else { - fclose(f); + ProtocolClient client(program); + if (!client.start()) { + client.abort(); + return 1; } - int tray_wv = rkt_webview_tray_create(icon_file, "This is a test tray icon"); - INFO1("tray_wv = %d\n", tray_wv); - rkt_webview_register_evt_callback(evt_cb); + bool ok = true; + ok = expectResult(client.command(CMD_NOOP), 0, "NOOP") && ok; - int wv = rkt_webview_create(context, 0); - rkt_webview_set_title(wv, "Hi there, this is a title!"); - rkt_webview_set_icon(wv, "../../rktplayer.png"); + const QJsonObject initialInfo = client.command(CMD_INFO); + ok = expectResult(initialInfo, 0, "INFO reports no open windows") && ok; - rkt_data_t *d = rkt_webview_info(); - rkt_webview_free_data(d); + if (!protocolOnly && ok) { + QJsonObject contextData; + contextData["boilerplate_js"] = ""; + contextData["has_pem_cert"] = false; + contextData["pem_cert"] = ""; - rkt_webview_move(wv, 100, 200); - rkt_webview_resize(wv, 800, 600); - //rkt_webview_set_url(wv, "https://wikipedia.org"); - rkt_webview_set_html(wv, "Hi!

Oke test

Ja

"); + const QJsonObject contextResult = client.command(CMD_CONTEXT_NEW, contextData); + const int context = contextResult["result"].toInt(-1); + if (context < 0) { + std::fprintf(stderr, "FAIL: CONTEXT_NEW returned %d\n", context); + ok = false; + } else { + std::fprintf(stderr, "PASS: context created (%d)\n", context); + } - d = rkt_webview_info(); - fprintf(stderr, "%s\n", d->data.metrics.log_file); - rkt_webview_free_data(d); + QJsonObject createData; + createData["context"] = context; + createData["parent"] = 0; + const QJsonObject createResult = client.command(CMD_CREATE_WV, createData); + const int webview = createResult["result"].toInt(-1); + if (webview <= 0) { + std::fprintf(stderr, "FAIL: CREATE_WV returned %d\n", webview); + ok = false; + } else { + std::fprintf(stderr, "PASS: webview created (%d)\n", webview); + } - while(rkt_webview_events_waiting() > 0) { - rkt_data_t *d = rkt_webview_get_event(); - rkt_webview_free_data(d); + if (ok) { + QJsonObject titleData; + titleData["wv"] = webview; + titleData["title"] = "rktwebview stdio integration test"; + ok = expectResult(client.command(CMD_SET_TITLE, titleData), 0, "SET_TITLE") && ok; + + QJsonObject htmlData; + htmlData["wv"] = webview; + htmlData["html"] = "stdio test" + "

rktwebview stdio test

"; + ok = expectResult(client.command(CMD_SET_HTML, htmlData), 0, "SET_HTML") && ok; + + QJsonObject validData; + validData["wv"] = webview; + ok = expectResult(client.command(CMD_HANDLE_IS_VALID, validData), 1, + "HANDLE_IS_VALID before close") && ok; + + QJsonObject closeData; + closeData["wv"] = webview; + ok = expectResult(client.command(CMD_CLOSE_WV, closeData), 0, "CLOSE_WV") && ok; + } } -#ifdef _WIN32 - Sleep(15000); - rkt_webview_tray_show_message(tray_wv, "This is a title", "This is a message to display
Hopefully it does display."); - Sleep(15000); -#else - sleep(30 - ); -#endif - d = rkt_webview_info(); - rkt_webview_free_data(d); - rkt_webview_close(wv); - - d = rkt_webview_info(); - rkt_webview_free_data(d); - - rkt_webview_close(tray_wv); - - rkt_webview_cleanup(); + const bool stopped = client.stop(); + return ok && stopped ? 0 : 1; }