Omzetting naar stdio.
This commit is contained in:
+7
-22
@@ -12,20 +12,6 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|||||||
find_package(QT NAMES Qt6 REQUIRED COMPONENTS Widgets WebEngineWidgets)
|
find_package(QT NAMES Qt6 REQUIRED COMPONENTS Widgets WebEngineWidgets)
|
||||||
find_package(Qt${QT_VERSION_MAJOR} 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
|
add_executable(rktwebview_prg
|
||||||
main.cpp
|
main.cpp
|
||||||
|
|
||||||
@@ -37,8 +23,6 @@ add_executable(rktwebview_prg
|
|||||||
|
|
||||||
rktutils.h rktutils.cpp
|
rktutils.h rktutils.cpp
|
||||||
command.h command.cpp
|
command.h command.cpp
|
||||||
shm.h shm.cpp
|
|
||||||
shmqueue.h shmqueue.cpp
|
|
||||||
|
|
||||||
rkt_protocol.h
|
rkt_protocol.h
|
||||||
rktwebview_types.h
|
rktwebview_types.h
|
||||||
@@ -48,15 +32,16 @@ add_executable(rktwebview_prg
|
|||||||
build_menu_from_json.h
|
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}::Widgets)
|
||||||
target_link_libraries(rktwebview_prg PRIVATE Qt${QT_VERSION_MAJOR}::WebEngineWidgets)
|
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_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)
|
||||||
|
|||||||
@@ -1,11 +1,53 @@
|
|||||||
# racket-webview-qt
|
# 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.
|
This version replaces the shared-library/shared-memory transport with a
|
||||||
Symptom: native HTML <select> popup keeps growing/repainting while open;
|
standalone process interface.
|
||||||
QtWebEngine process busy, Racket process idle.
|
|
||||||
Resolved by upgrading to Qt 6.11.1.
|
|
||||||
|
|
||||||
|
## Transport
|
||||||
|
|
||||||
|
`rktwebview_prg` receives line-delimited JSON commands on stdin, writes
|
||||||
|
line-delimited JSON results and events to stdout, and writes all diagnostics to
|
||||||
|
stderr. The existing command numbers and command payloads in `rkt_protocol.h`
|
||||||
|
are unchanged.
|
||||||
|
|
||||||
|
See `STDIO-PROTOCOL.md` for the transport envelope and shutdown rules.
|
||||||
|
|
||||||
|
The former shared library, shared-memory queue, and callback sources remain in
|
||||||
|
the repository for comparison, but they are no longer part of the CMake target.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
The CMake target requires Qt Widgets and Qt WebEngineWidgets and builds only:
|
||||||
|
|
||||||
|
```text
|
||||||
|
rktwebview_prg
|
||||||
|
```
|
||||||
|
|
||||||
|
## Previous Qt update
|
||||||
|
|
||||||
|
QtWebEngine select popup rendering issues were observed with older Qt 6.x. The
|
||||||
|
native HTML `<select>` 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.
|
||||||
|
|||||||
@@ -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.
|
||||||
Binary file not shown.
Binary file not shown.
@@ -1,494 +1,461 @@
|
|||||||
#include <QThread>
|
|
||||||
#include <QFile>
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
|
||||||
#include "rkt_protocol.h"
|
|
||||||
#include "shm.h"
|
|
||||||
#include "shmqueue.h"
|
|
||||||
#include "command.h"
|
|
||||||
#include "utils.h"
|
|
||||||
|
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QJsonParseError>
|
||||||
|
#include <QThread>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "rkt_protocol.h"
|
||||||
#include "rktwebview_qt.h"
|
#include "rktwebview_qt.h"
|
||||||
|
#include "utils.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
#ifdef DEBUG_RKT_WEBVIEW
|
constexpr int STDIO_PROTOCOL_VERSION = 1;
|
||||||
#define WAIT_TIME (300 * 1000)
|
|
||||||
#else
|
|
||||||
#define WAIT_TIME (10 * 1000)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
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<std::mutex> 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
|
class Handler : public QThread
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Shm *shm;
|
Rktwebview_qt *webviewHandler = nullptr;
|
||||||
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;
|
|
||||||
|
|
||||||
protected:
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int wv = data->data.event.wv;
|
QJsonObject message;
|
||||||
char *evt = data->data.event.event;
|
message["type"] = "event";
|
||||||
|
message["wv"] = data->data.event.wv;
|
||||||
|
message["data"] = QString::fromUtf8(data->data.event.event);
|
||||||
|
writeMessage(message);
|
||||||
|
|
||||||
std::string evt_d(evt);
|
freeData(data);
|
||||||
_handler->event_queue->enqueue(wv, evt_d);
|
|
||||||
|
|
||||||
free_data(data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
const char *me = argv[0];
|
INFO0("Starting stdio backend\n");
|
||||||
|
|
||||||
{
|
Handler commandHandler;
|
||||||
time_t my_time = time(NULL);
|
commandHandler.webviewHandler = new Rktwebview_qt(argc, argv);
|
||||||
INFO1("Starting at %s", ctime(&my_time));
|
commandHandler.webviewHandler->initApp();
|
||||||
}
|
commandHandler.start();
|
||||||
|
|
||||||
if (argc < 8) {
|
sendReady();
|
||||||
ERROR1("%s: wrong number of arguments\n", me);
|
commandHandler.webviewHandler->execApp();
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const char *shm_name = argv[1];
|
INFO0("Qt event loop stopped; waiting for stdin handler\n");
|
||||||
const char *shm_size_str = argv[2];
|
commandHandler.wait();
|
||||||
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];
|
|
||||||
|
|
||||||
size_t shm_size = atoi(shm_size_str);
|
delete commandHandler.webviewHandler;
|
||||||
int cmd_slot = atoi(cmd_slot_str);
|
commandHandler.webviewHandler = nullptr;
|
||||||
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<int>(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));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
INFO0("Exiting stdio backend\n");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Handler::run()
|
void Handler::run()
|
||||||
{
|
{
|
||||||
int wait_ms = WAIT_TIME; // 10 seconds.
|
std::string line;
|
||||||
while (!quit) {
|
bool quit = false;
|
||||||
int cmd;
|
|
||||||
std::string data;
|
while (!quit && std::getline(std::cin, line)) {
|
||||||
bool something_came = command_queue->dequeue(cmd, data, wait_ms);
|
if (line.empty()) {
|
||||||
if (!something_came) {
|
continue;
|
||||||
DEBUG1("No command received last %d seconds\n", wait_ms / 1000);
|
|
||||||
cmd = CMD_NOOP;
|
|
||||||
}
|
}
|
||||||
QJsonObject data_obj = QJsonDocument::fromJson(data.c_str()).object();
|
|
||||||
|
|
||||||
switch(cmd) {
|
QJsonParseError parseError;
|
||||||
case CMD_NOOP: {
|
const QJsonDocument document =
|
||||||
if (quit) {
|
QJsonDocument::fromJson(QByteArray::fromStdString(line), &parseError);
|
||||||
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();
|
|
||||||
|
|
||||||
int wv = webview_handler->rktWebViewCreate(context, parent, event_cb);
|
if (parseError.error != QJsonParseError::NoError || !document.isObject()) {
|
||||||
result_queue->enqueue(wv);
|
const QString message = QString("Invalid protocol JSON: %1")
|
||||||
}
|
.arg(parseError.errorString());
|
||||||
break;
|
ERROR1("%s\n", message.toUtf8().constData());
|
||||||
case CMD_CLOSE_WV: {
|
sendProtocolError(message);
|
||||||
int wv = data_obj["wv"].toInt();
|
continue;
|
||||||
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<rkt_messagetype_t>(type)
|
|
||||||
);
|
|
||||||
result_queue->enqueue(r);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default: {
|
|
||||||
ERROR1("Unknown command: %d\n", cmd);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
DEBUG0("Exiting handler thread\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
void Alive::run()
|
const QJsonObject request = document.object();
|
||||||
{
|
const int id = request["id"].toInt(-1);
|
||||||
int wait_ms = WAIT_TIME;
|
|
||||||
int ping_no;
|
if (request["type"].toString() != "command") {
|
||||||
std::string data;
|
sendProtocolError("Expected a command message", id);
|
||||||
bool go_on = true;
|
continue;
|
||||||
while (go_on) {
|
}
|
||||||
bool something_came = alive_queue->dequeue(ping_no, data, wait_ms);
|
|
||||||
if (!something_came) {
|
const int command = request["command"].toInt(CMD_NOOP);
|
||||||
ERROR0("No alive message received, stopping alive loop and quitting rktwebview_prg\n");
|
const QJsonObject data = request["data"].toObject();
|
||||||
handler->quit = true;
|
|
||||||
handler->command_queue->enqueue(CMD_NOOP);
|
if (id < 0) {
|
||||||
alive_timeout = true;
|
sendProtocolError("Command has no valid request id");
|
||||||
go_on = false;
|
continue;
|
||||||
} else {
|
}
|
||||||
if (ping_no == CMD_ALIVE_QUIT) {
|
|
||||||
DEBUG0("Got Quit ping for alive thread\n");
|
switch (command) {
|
||||||
go_on = false;
|
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 {
|
} else {
|
||||||
DEBUG1("Got alive ping: %d\n", ping_no);
|
sendResult(id,
|
||||||
alive_ack_queue->enqueue(ping_no);
|
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<rkt_messagetype_t>(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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
#ifndef RKT_PROTOCOL_H
|
#ifndef RKT_PROTOCOL_H
|
||||||
#define 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_HANDLE_IS_VALID 1
|
||||||
#define CMD_QUIT 2
|
#define CMD_QUIT 2
|
||||||
#define CMD_CONTEXT_NEW 3 // arguments: boilerplate_js: string, has_cert: bool, cert_pem: string -> context: int
|
#define CMD_CONTEXT_NEW 3 // arguments: boilerplate_js: string, has_cert: bool, cert_pem: string -> context: int
|
||||||
|
|||||||
+6
-7
@@ -180,15 +180,14 @@ void Rktwebview_qt::processCommand(Command *cmd)
|
|||||||
case COMMAND_CLOSE: {
|
case COMMAND_CLOSE: {
|
||||||
int wv = cmd->args[0].toInt();
|
int wv = cmd->args[0].toInt();
|
||||||
if (_views.contains(wv)) {
|
if (_views.contains(wv)) {
|
||||||
WebviewWindow *w= _views[wv];
|
WebviewWindow *w = _views[wv];
|
||||||
_views.remove(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();
|
w->closeView();
|
||||||
cmd->result = true;
|
cmd->result = true;
|
||||||
while(w->isVisible()) {
|
|
||||||
_app->processEvents();
|
|
||||||
}
|
|
||||||
_view_js_callbacks.remove(wv);
|
|
||||||
delete w;
|
|
||||||
} else if (_trays.contains(wv)) {
|
} else if (_trays.contains(wv)) {
|
||||||
QSystemTrayIcon *tray = _trays[wv];
|
QSystemTrayIcon *tray = _trays[wv];
|
||||||
_trays.remove(wv);
|
_trays.remove(wv);
|
||||||
|
|||||||
+281
-74
@@ -1,94 +1,301 @@
|
|||||||
#include "rktwebview.h"
|
#include <QCoreApplication>
|
||||||
#include <string>
|
#include <QElapsedTimer>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QProcess>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
#ifdef _WIN32
|
#include <cstdio>
|
||||||
#include <windows.h>
|
#include <utility>
|
||||||
#else
|
|
||||||
#include <unistd.h>
|
|
||||||
#endif
|
|
||||||
#include "utils.h"
|
|
||||||
|
|
||||||
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[])
|
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);
|
if (!QFileInfo::exists(program)) {
|
||||||
#ifdef _WIN32
|
std::fprintf(stderr,
|
||||||
std::string prg = loc + "\\rktwebview_prg.exe";
|
"FAIL: backend executable not found: %s\n",
|
||||||
SetDllDirectoryA("C:\\Qt\\6.11.1\\msvc2022_64\\bin");
|
program.toUtf8().constData());
|
||||||
#else
|
return 1;
|
||||||
std::string prg = loc + "/rktwebview_prg";
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef _WIN32
|
|
||||||
{
|
|
||||||
std::string e = std::string("RKT_WEBVIEW_PRG=") + prg;
|
|
||||||
_putenv(e.c_str());
|
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
setenv("RKT_WEBVIEW_PRG", prg.c_str(), true);
|
|
||||||
setenv("LD_LIBRARY_PATH", loc.c_str(), true);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
rkt_webview_init(__FUNCTION__);
|
ProtocolClient client(program);
|
||||||
|
if (!client.start()) {
|
||||||
int context = rkt_webview_new_context("", nullptr);
|
client.abort();
|
||||||
rkt_webview_set_loglevel(rkt_webview_loglevel_t::log_debug);
|
return 1;
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
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);
|
const QJsonObject initialInfo = client.command(CMD_INFO);
|
||||||
rkt_webview_set_title(wv, "Hi there, this is a title!");
|
ok = expectResult(initialInfo, 0, "INFO reports no open windows") && ok;
|
||||||
rkt_webview_set_icon(wv, "../../rktplayer.png");
|
|
||||||
|
|
||||||
rkt_data_t *d = rkt_webview_info();
|
if (!protocolOnly && ok) {
|
||||||
rkt_webview_free_data(d);
|
QJsonObject contextData;
|
||||||
|
contextData["boilerplate_js"] = "";
|
||||||
|
contextData["has_pem_cert"] = false;
|
||||||
|
contextData["pem_cert"] = "";
|
||||||
|
|
||||||
rkt_webview_move(wv, 100, 200);
|
const QJsonObject contextResult = client.command(CMD_CONTEXT_NEW, contextData);
|
||||||
rkt_webview_resize(wv, 800, 600);
|
const int context = contextResult["result"].toInt(-1);
|
||||||
//rkt_webview_set_url(wv, "https://wikipedia.org");
|
if (context < 0) {
|
||||||
rkt_webview_set_html(wv, "<html><head><title>Hi!</title></head><body><h1>Oke test</h1><select id=\"sel-lang\"><option value=\"nl\" selected>Nederlands</option><option value=\"en\">English</option></select><p>Ja</p></body></html>");
|
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();
|
QJsonObject createData;
|
||||||
fprintf(stderr, "%s\n", d->data.metrics.log_file);
|
createData["context"] = context;
|
||||||
rkt_webview_free_data(d);
|
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) {
|
if (ok) {
|
||||||
rkt_data_t *d = rkt_webview_get_event();
|
QJsonObject titleData;
|
||||||
rkt_webview_free_data(d);
|
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"] = "<html><head><title>stdio test</title></head>"
|
||||||
|
"<body><h1>rktwebview stdio test</h1></body></html>";
|
||||||
|
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<br><b>Hopefully</b> it does display.");
|
|
||||||
Sleep(15000);
|
|
||||||
#else
|
|
||||||
sleep(30
|
|
||||||
);
|
|
||||||
#endif
|
|
||||||
d = rkt_webview_info();
|
|
||||||
rkt_webview_free_data(d);
|
|
||||||
|
|
||||||
rkt_webview_close(wv);
|
const bool stopped = client.stop();
|
||||||
|
return ok && stopped ? 0 : 1;
|
||||||
d = rkt_webview_info();
|
|
||||||
rkt_webview_free_data(d);
|
|
||||||
|
|
||||||
rkt_webview_close(tray_wv);
|
|
||||||
|
|
||||||
rkt_webview_cleanup();
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user