Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a1a56cd08c | |||
| d9af82d230 | |||
| fb1a293e88 | |||
| 803d6edd41 | |||
| 35b7bfe2e9 | |||
| c6db000d26 |
+7
-22
@@ -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)
|
||||
|
||||
@@ -1,3 +1,65 @@
|
||||
# racket-webview-qt
|
||||
|
||||
The Qt backend of racket-webview
|
||||
The Qt WebEngine backend of `racket-webview`.
|
||||
|
||||
## Version 0.2.3
|
||||
|
||||
This version replaces the shared-library/shared-memory transport with a
|
||||
standalone process interface.
|
||||
|
||||
## 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.
|
||||
|
||||
### Backend logging
|
||||
|
||||
Diagnostics written through `utils.h` use one-line structured stderr records:
|
||||
|
||||
```text
|
||||
json:{"topic":"webview-backend","level":"info","elapsed":0.125,"message":"..."}
|
||||
```
|
||||
|
||||
Stdout remains reserved for protocol messages. Unstructured stderr output from
|
||||
Qt or third-party libraries may still occur and should be treated as diagnostic
|
||||
text.
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# 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.
|
||||
|
||||
## Structured stderr logging
|
||||
|
||||
The backend writes diagnostic logging to stderr, never to stdout. Log entries
|
||||
created through `utils.h` are emitted as one line beginning with `json:` followed
|
||||
by a compact JSON object:
|
||||
|
||||
```text
|
||||
json:{"topic":"webview-backend","level":"debug","elapsed":1.234,"message":"..."}
|
||||
```
|
||||
|
||||
Newlines and other control characters in the message are JSON escaped, so one
|
||||
C++ log call always produces one physical stderr line. Consumers should also
|
||||
accept unstructured stderr lines because Qt, Qt WebEngine, or another library
|
||||
may write diagnostics directly.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,48 @@
|
||||
[
|
||||
{
|
||||
"name": "qtbase",
|
||||
"repositories": ["qtbase", "qtactiveqt", "qtimageformats"]
|
||||
},
|
||||
{
|
||||
"name": "qtdeclarative",
|
||||
"repositories": ["qtdeclarative"]
|
||||
},
|
||||
{
|
||||
"name": "qtmultimedia",
|
||||
"repositories": ["qtmultimedia"]
|
||||
},
|
||||
{
|
||||
"name": "qtconnectivity",
|
||||
"repositories": ["qtconnectivity"]
|
||||
},
|
||||
{
|
||||
"name": "qtlocation",
|
||||
"repositories": ["qtlocation"]
|
||||
},
|
||||
{
|
||||
"name": "qtwebsockets",
|
||||
"repositories": ["qtwebsockets"]
|
||||
},
|
||||
{
|
||||
"name": "qtserialport",
|
||||
"repositories": ["qtserialport"]
|
||||
},
|
||||
{
|
||||
"name": "qtwebengine",
|
||||
"repositories": ["qtwebengine"]
|
||||
},
|
||||
{
|
||||
"name": "designer",
|
||||
"modules": ["Designer"]
|
||||
},
|
||||
{
|
||||
"name": "linguist"
|
||||
},
|
||||
{
|
||||
"name": "assistant"
|
||||
},
|
||||
{
|
||||
"name": "qt_help",
|
||||
"modules": ["Help"]
|
||||
}
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,493 +1,461 @@
|
||||
#include <QThread>
|
||||
#include <QFile>
|
||||
#include <QApplication>
|
||||
|
||||
#include "rkt_protocol.h"
|
||||
#include "shm.h"
|
||||
#include "shmqueue.h"
|
||||
#include "command.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QThread>
|
||||
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include "rkt_protocol.h"
|
||||
#include "rktwebview_qt.h"
|
||||
#include "utils.h"
|
||||
|
||||
#ifdef DEBUG_RKT_WEBVIEW
|
||||
#define WAIT_TIME (300 * 1000)
|
||||
#else
|
||||
#define WAIT_TIME (10 * 1000)
|
||||
#endif
|
||||
namespace {
|
||||
|
||||
static void free_data(rkt_data_t *d)
|
||||
constexpr int STDIO_PROTOCOL_VERSION = 1;
|
||||
|
||||
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
|
||||
{
|
||||
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<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));
|
||||
}
|
||||
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<rkt_messagetype_t>(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<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
|
||||
#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
|
||||
|
||||
+6
-7
@@ -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);
|
||||
|
||||
+281
-73
@@ -1,93 +1,301 @@
|
||||
#include "rktwebview.h"
|
||||
#include <string>
|
||||
#include <QCoreApplication>
|
||||
#include <QElapsedTimer>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QProcess>
|
||||
#include <QStringList>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#include "utils.h"
|
||||
#include <cstdio>
|
||||
#include <utility>
|
||||
|
||||
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.10.2\\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");
|
||||
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"] = "<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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,36 @@
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <vector>
|
||||
|
||||
typedef struct {
|
||||
std::chrono::time_point<std::chrono::system_clock> start;
|
||||
} timer;
|
||||
|
||||
|
||||
|
||||
static int _log_level = LOG_INFO;
|
||||
static timer *_timer = nullptr;
|
||||
|
||||
static QString formattedMessage(const char *format, va_list args)
|
||||
{
|
||||
va_list countArgs;
|
||||
va_copy(countArgs, args);
|
||||
const int length = std::vsnprintf(nullptr, 0, format, countArgs);
|
||||
va_end(countArgs);
|
||||
|
||||
if (length < 0) {
|
||||
return QString::fromUtf8(format);
|
||||
}
|
||||
|
||||
std::vector<char> buffer(static_cast<size_t>(length) + 1);
|
||||
std::vsnprintf(buffer.data(), buffer.size(), format, args);
|
||||
return QString::fromUtf8(buffer.data(), length);
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
int logLevel()
|
||||
@@ -27,27 +46,48 @@ void setLogLevel(int l)
|
||||
const char *logIndicator(int l)
|
||||
{
|
||||
switch(l) {
|
||||
case LOG_ERROR: return "ERROR ";
|
||||
case LOG_INFO: return "INFO ";
|
||||
case LOG_DEBUG: return "DEBUG ";
|
||||
case LOG_WARNING: return "WARNING";
|
||||
case LOG_ERROR: return "error";
|
||||
case LOG_INFO: return "info";
|
||||
case LOG_DEBUG: return "debug";
|
||||
case LOG_WARNING: return "warning";
|
||||
}
|
||||
return "UNKNOWN";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
void logElapsed()
|
||||
double logElapsedSeconds()
|
||||
{
|
||||
if (_timer == nullptr) {
|
||||
_timer = new timer;
|
||||
_timer->start = std::chrono::system_clock::now();
|
||||
}
|
||||
|
||||
auto c = std::chrono::system_clock::now();
|
||||
auto duration = c - _timer->start;
|
||||
auto milliseconds
|
||||
const auto current = std::chrono::system_clock::now();
|
||||
const auto milliseconds
|
||||
= std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
duration)
|
||||
current - _timer->start)
|
||||
.count();
|
||||
fprintf(stderr, "%8.3lf: ", milliseconds/ 1000.0);
|
||||
return milliseconds / 1000.0;
|
||||
}
|
||||
|
||||
void logMessage(int level, const char *format, ...)
|
||||
{
|
||||
if (logLevel() < level) {
|
||||
return;
|
||||
}
|
||||
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
const QString message = formattedMessage(format, args);
|
||||
va_end(args);
|
||||
|
||||
QJsonObject object;
|
||||
object.insert(QStringLiteral("topic"), QStringLiteral("webview-backend"));
|
||||
object.insert(QStringLiteral("level"), QString::fromLatin1(logIndicator(level)));
|
||||
object.insert(QStringLiteral("elapsed"), logElapsedSeconds());
|
||||
object.insert(QStringLiteral("message"), message);
|
||||
|
||||
const QByteArray json = QJsonDocument(object).toJson(QJsonDocument::Compact);
|
||||
std::fprintf(stderr, "json:%s\n", json.constData());
|
||||
std::fflush(stderr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ inline std::string basedir(const std::string &path)
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
#ifdef RKTWEBVIEW_PRG_EXE
|
||||
#undef RKTWEBVIEW_EXPORT
|
||||
@@ -37,8 +38,9 @@ extern "C" {
|
||||
|
||||
RKTWEBVIEW_EXPORT int logLevel();
|
||||
RKTWEBVIEW_EXPORT void setLogLevel(int l);
|
||||
RKTWEBVIEW_EXPORT void logElapsed();
|
||||
RKTWEBVIEW_EXPORT double logElapsedSeconds();
|
||||
RKTWEBVIEW_EXPORT const char *logIndicator(int l);
|
||||
RKTWEBVIEW_EXPORT void logMessage(int level, const char *format, ...);
|
||||
}
|
||||
|
||||
#define LOG_ERROR 1
|
||||
@@ -46,28 +48,22 @@ RKTWEBVIEW_EXPORT const char *logIndicator(int l);
|
||||
#define LOG_INFO 3
|
||||
#define LOG_DEBUG 4
|
||||
|
||||
#define MKLOGSTMT(level, code) if (logLevel() >= level) { fprintf(stderr, "%s: ", logIndicator(level));logElapsed();code;fflush(stderr); }
|
||||
#define MKL0(level, msg) MKLOGSTMT(level, fprintf(stderr, msg))
|
||||
#define MKL1(level, msg, a) MKLOGSTMT(level, fprintf(stderr, msg, a))
|
||||
#define MKL2(level, msg, a, b) MKLOGSTMT(level, fprintf(stderr, msg, a, b))
|
||||
#define MKL3(level, msg, a, b, c) MKLOGSTMT(level, fprintf(stderr, msg, a, b, c))
|
||||
#define ERROR0(msg) logMessage(LOG_ERROR, msg)
|
||||
#define WARN0(msg) logMessage(LOG_WARNING, msg)
|
||||
#define INFO0(msg) logMessage(LOG_INFO, msg)
|
||||
#define DEBUG0(msg) logMessage(LOG_DEBUG, msg)
|
||||
|
||||
#define ERROR0(msg) MKL0(LOG_ERROR, msg)
|
||||
#define WARN0(msg) MKL0(LOG_WARNING, msg)
|
||||
#define INFO0(msg) MKL0(LOG_INFO, msg)
|
||||
#define DEBUG0(msg) MKL0(LOG_DEBUG, msg)
|
||||
#define ERROR1(msg, a) logMessage(LOG_ERROR, msg, a)
|
||||
#define WARN1(msg, a) logMessage(LOG_WARNING, msg, a)
|
||||
#define INFO1(msg, a) logMessage(LOG_INFO, msg, a)
|
||||
#define DEBUG1(msg, a) logMessage(LOG_DEBUG, msg, a)
|
||||
|
||||
#define ERROR1(msg, a) MKL1(LOG_ERROR, msg, a)
|
||||
#define WARN1(msg, a) MKL1(LOG_WARNING, msg, a)
|
||||
#define INFO1(msg, a) MKL1(LOG_INFO, msg, a)
|
||||
#define DEBUG1(msg, a) MKL1(LOG_DEBUG, msg, a)
|
||||
#define ERROR2(msg, a, b) logMessage(LOG_ERROR, msg, a, b)
|
||||
#define WARN2(msg, a, b) logMessage(LOG_WARNING, msg, a, b)
|
||||
#define INFO2(msg, a, b) logMessage(LOG_INFO, msg, a, b)
|
||||
#define DEBUG2(msg, a, b) logMessage(LOG_DEBUG, msg, a, b)
|
||||
|
||||
#define ERROR2(msg, a, b) MKL2(LOG_ERROR, msg, a, b)
|
||||
#define WARN2(msg, a, b) MKL2(LOG_WARNING, msg, a, b)
|
||||
#define INFO2(msg, a, b) MKL2(LOG_INFO, msg, a, b)
|
||||
#define DEBUG2(msg, a, b) MKL2(LOG_DEBUG, msg, a, b)
|
||||
|
||||
#define INFO3(msg, a, b, c) MKL3(LOG_INFO, msg, a, b, c)
|
||||
#define INFO3(msg, a, b, c) logMessage(LOG_INFO, msg, a, b, c)
|
||||
|
||||
inline void do_free_data(rkt_data_t *d)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user