Files
racket-webview/rktwebview_qt/rktwebview.cpp
2026-03-26 14:35:10 +01:00

557 lines
15 KiB
C++

#include <malloc.h>
#include <chrono>
#include <stdint.h>
#include <string.h>
#ifdef _WIN32
#include <windows.h>
#include <filesystem>
#else
#include <unistd.h>
#include <spawn.h>
#endif
#include <fcntl.h>
#include "rktwebview.h"
#include "rkt_protocol.h"
#include "shmqueue.h"
#include "json.h"
#include "utils.h"
#define SHM_SIZE (10 * 1024 * 1024) // 10MB
#define COMMAND_SLOT 1
#define COMMAND_RESULT_SLOT 2
#define EVENT_SLOT 3
//#define DEBUG
/////////////////////////////////////////////////////////////////////
// Utility functions
/////////////////////////////////////////////////////////////////////
typedef struct {
std::string name;
size_t shm_size;
Shm *shm;
ShmQueue *command_queue;
ShmQueue *command_result_queue;
ShmQueue *event_queue;
#ifdef _WIN32
HANDLE rkt_webview_prg_pid;
#else
pid_t rkt_webview_prg_pid;
#endif
bool rkt_webview_prg_started;
bool valid;
} Handle_t;
Handle_t *handler = nullptr;
static bool fileExists(const char *filename) {
#ifdef _WIN32
DWORD dwAttrib = GetFileAttributes(filename);
return (dwAttrib != INVALID_FILE_ATTRIBUTES &&
!(dwAttrib & FILE_ATTRIBUTE_DIRECTORY));
#else
return access(filename, X_OK) != -1;
#endif
}
uint64_t current_ms() {
using namespace std::chrono;
return duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
}
bool runRktWebview(Handle_t *handler)
{
char *rkt_webview_prg_path = getenv("RKT_WEBVIEW_PRG");
if (rkt_webview_prg_path == nullptr) {
ERROR0("RKT_WEBVIEW_PRG environment variable not set, cannot start webview program\n");
return false;
}
if (!fileExists(rkt_webview_prg_path)) {
ERROR1("%s does not exist or is not executable\n", rkt_webview_prg_path);
return false;
}
std::string path = basedir(rkt_webview_prg_path);
char shm_size_str[30];
char command_slot[10];
char command_result_slot[10];
char event_slot[10];
sprintf(shm_size_str, "%d", static_cast<int>(handler->shm_size));
sprintf(command_slot, "%d", COMMAND_SLOT);
sprintf(command_result_slot, "%d", COMMAND_RESULT_SLOT);
sprintf(event_slot, "%d", EVENT_SLOT);
// run rktwebview_prg using the environment variable RKT_WEBVIEW_PRG
#ifdef _WIN32
const char *td = getenv("TEMP");
if (td == nullptr) { td = "C:\\"; }
std::string tmpdir = td;
std::string logfile = tmpdir + "\\" + "rktwebview.log"; //handler->name + ".log";
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;
HANDLE h = CreateFile(logfile.c_str(),
FILE_APPEND_DATA,
FILE_SHARE_WRITE | FILE_SHARE_READ,
&sa,
OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory( &si, sizeof(si) );
ZeroMemory( &pi, sizeof(pi) );
si.cb = sizeof(si);
si.dwFlags |= STARTF_USESTDHANDLES;
si.hStdInput= NULL;
si.hStdOutput = h;
si.hStdError = h;
DWORD flags = CREATE_NO_WINDOW | NORMAL_PRIORITY_CLASS;
std::string cmdargs = std::string("") + handler->name + " " + shm_size_str + " " + command_slot + " " + command_result_slot + " " + event_slot;
std::string exe = std::string(rkt_webview_prg_path);
std::string dir = basedir(exe);
std::string cmdline = exe + " " + cmdargs;
char *cmdline_str = const_cast<char *>(cmdline.c_str());
bool r = CreateProcessA(NULL, cmdline_str, NULL, NULL, TRUE, flags, NULL, path.c_str(), &si, &pi);
if (!r) {
ERROR2("Cannot create process '%s' (error = %ld)\n", cmdline_str, GetLastError());
}
return r;
#else
char *argv[] = { rkt_webview_prg_path, const_cast<char *>(handler->name.c_str()), shm_size_str, command_slot, command_result_slot, event_slot, nullptr };
int r = posix_spawn(&handler->rkt_webview_prg_pid, rkt_webview_prg_path, nullptr, nullptr, argv, environ);
return (r == 0);
#endif
}
/////////////////////////////////////////////////////////////////////
// Main C Interface
/////////////////////////////////////////////////////////////////////
void rkt_webview_cleanup()
{
if (handler != nullptr) {
// Does nothing.
// See QTBUG-145033
// QtWebEngine cannot be started as part of QApplication more than once in an application run.
// So we would need to cleanup at exit of racket/drracket.
// Cleaning up when exiting the current custodian is not enough.
/*
QApplication *app = handler->app();
if (app != nullptr) {
// testing with #:custodian (current-custodian)
QTimer t;
QObject::connect(&t, &QTimer::timeout, app, [app]() {
fprintf(stderr, "Quitting\n");
app->quit();
});
t.setSingleShot(true);
t.start(250);
fprintf(stderr, "Executing app\n");
app->exec();
fprintf(stderr, "App exited\n");
delete handler;
handler = nullptr;
}
*/
if (handler->valid) {
INFO0("Sending quit message\n");
handler->command_queue->enqueue(CMD_QUIT);
INFO0("Message sent\n");
bool stopped = false;
while(!stopped) {
int cmd;
std::string s;
INFO0("Getting result of quit message\n");
handler->command_result_queue->dequeue(cmd, s, true);
INFO1("got %d\n", cmd);
if (cmd == RESULT_QUIT) {
stopped = true;
}
}
}
delete handler->event_queue;
delete handler->command_result_queue;
delete handler->command_queue;
delete handler->shm;
delete handler;
handler = nullptr;
}
}
void rkt_webview_init()
{
if (handler == nullptr) {
// Create shared memory and communication queues
char buf[1024];
#ifdef DEBUG
sprintf(buf, "rktwebview-dbg");
#else
#ifdef _WIN32
DWORD p = GetCurrentProcessId();
sprintf(buf, "rktwebview-%ld", p);
#else
pid_t p = getpid();
sprintf(buf, "rktwebview-%x", p);
#endif
#endif
handler = new Handle_t;
handler->valid = true;
handler->name = buf;
handler->shm_size = SHM_SIZE;
handler->shm = new Shm(handler->name.data(), handler->shm_size, true);
if (!handler->shm->isValid()) { handler->valid = false; }
handler->command_queue = new ShmQueue(handler->shm, COMMAND_SLOT, true);
handler->command_result_queue = new ShmQueue(handler->shm, COMMAND_RESULT_SLOT, true);
handler->event_queue = new ShmQueue(handler->shm, EVENT_SLOT, true);
// Start rktwebview_prg application with the right information
#ifndef DEBUG
handler->rkt_webview_prg_started = runRktWebview(handler);
if (!handler->rkt_webview_prg_started) { handler->valid = false; }
#endif
}
}
bool rkt_webview_valid(rktwebview_t wv)
{
rkt_webview_init();
if (handler != nullptr && handler->valid) {
handler->command_queue->enqueue(CMD_HANDLE_IS_VALID);
int result;
std::string json_result;
handler->command_result_queue->dequeue(result, json_result, true);
return result == 0;
} else {
return false;
}
}
static inline bool validHandle()
{
return (handler != nullptr) && handler->valid;
}
#define FAIL_HANDLE if (!validHandle()) { return result_t::invalid_handle; }
#define FAIL_CONTEXT if (!validHandle()) { return 0; }
#define FAIL_WV if (!validHandle()) { return 0; }
#define NOOP_HANDLE if (!validHandle()) { return; }
#define FAIL_CALL_JS if (!validHandle()) { return nullptr; }
rkt_wv_context_t rkt_webview_new_context(const char *boilerplate_js, const char *optional_server_cert_pem)
{
rkt_webview_init();
FAIL_CONTEXT
JSON j;
std::string bpj(boilerplate_js);
bool has_pem_cert = optional_server_cert_pem != nullptr;
std::string osc(has_pem_cert ? optional_server_cert_pem : "");
j["boilerplate_js"] = bpj;
j["has_pem_cert"] = has_pem_cert;
j["pem_cert"] = osc;
handler->command_queue->enqueue(CMD_CONTEXT_NEW, j.dump());
int result;
std::string json_result;
handler->command_result_queue->dequeue(result, json_result, true);
return result;
}
int rkt_webview_create(rkt_wv_context_t context, rktwebview_t parent)
{
rkt_webview_init();
FAIL_WV
JSON j;
j["context"] = context;
j["parent"] = parent;
handler->command_queue->enqueue(CMD_CREATE_WV, j.dump());
int result;
std::string json_result;
handler->command_result_queue->dequeue(result, json_result, true);
return result;
}
void rkt_webview_close(rktwebview_t wv)
{
rkt_webview_init();
NOOP_HANDLE
JSON j;
j["wv"] = wv;
handler->command_queue->enqueue(CMD_CLOSE_WV, j.dump());
}
#define CMDRES4(cmd, wv, key, val, key2, val2, key3, val3, key4, val4) \
rkt_webview_init(); \
FAIL_HANDLE \
JSON j; \
j["wv"] = wv; \
j[key] = val; \
j[key2] = val2; \
j[key3] = val3; \
j[key4] = val4; \
handler->command_queue->enqueue(cmd, j.dump()); \
int result; \
std::string json_result; \
handler->command_result_queue->dequeue(result, json_result, true); \
result_t r = static_cast<result_t>(result); \
return r;
#define CMDRES3(cmd, wv, key, val, key2, val2, key3, val3) \
CMDRES4(cmd, wv, key, val, key2, val2, key3, val3, "nil3", "none")
#define CMDRES2(cmd, wv, key, val, key2, val2) \
CMDRES3(cmd, wv, key, val, key2, val2, "nil2", "none")
#define CMDRES(cmd, wv, key, val) \
CMDRES2(cmd, wv, key, val, "nil1", "none")
#define CMDRES0(cmd, wv) \
CMDRES(cmd, wv, "nil0", "none")
result_t rkt_webview_set_url(rktwebview_t wv, const char *url)
{
CMDRES(CMD_SET_URL, wv, "url", url)
}
result_t rkt_webview_set_html(rktwebview_t wv, const char *html)
{
CMDRES(CMD_SET_HTML, wv, "html", html)
}
result_t rkt_webview_run_js(rktwebview_t wv, const char *js)
{
CMDRES(CMD_RUN_JS, wv, "js", js)
}
rkt_data_t *rkt_webview_call_js(rktwebview_t wv, const char *js)
{
rkt_webview_init();
FAIL_CALL_JS
JSON j;
j["wv"] = wv;
j["js"] = std::string(js);
handler->command_queue->enqueue(CMD_CALL_JS, j.dump());
int result;
std::string json_result;
handler->command_result_queue->dequeue(result, json_result, true);
rkt_data_t *r = new rkt_data_t();
r->kind = rkt_data_kind_t::js_result;
r->data.js_result.result = static_cast<result_t>(result);
r->data.js_result.value = strdup(json_result.c_str());
return r;
}
result_t rkt_webview_open_devtools(rktwebview_t wv)
{
CMDRES(CMD_OPEN_DEVTOOLS, wv, "nil", "none")
}
/*
void rkt_webview_process_events(int for_ms)
{
rkt_webview_init();
uint64_t start_ms = current_ms();
uint64_t end_ms = start_ms + for_ms;
while (current_ms() < end_ms) {
QThread::usleep(500); // sleep 0.5 ms
handler->doEvents();
}
}
*/
result_t rkt_webview_move(rktwebview_t wv, int x, int y)
{
CMDRES2(CMD_MOVE, wv, "x", x, "y", y)
}
result_t rkt_webview_resize(rktwebview_t wv, int width, int height)
{
CMDRES2(CMD_RESIZE, wv, "w", width, "h", height)
}
result_t rkt_webview_hide(rktwebview_t w)
{
CMDRES0(CMD_HIDE, w)
}
result_t rkt_webview_show(rktwebview_t w)
{
CMDRES0(CMD_SHOW, w)
}
result_t rkt_webview_present(rktwebview_t w)
{
CMDRES0(CMD_PRESENT, w)
}
result_t rkt_webview_maximize(rktwebview_t w)
{
CMDRES0(CMD_MAXIMIZE, w)
}
result_t rkt_webview_minimize(rktwebview_t w)
{
CMDRES0(CMD_MINIMIZE, w)
}
result_t rkt_webview_show_normal(rktwebview_t w)
{
CMDRES0(CMD_SHOW_NORMAL, w)
}
window_state_t rkt_webview_window_state(rktwebview_t w)
{
auto f = [w]() {
CMDRES0(CMD_WINDOW_STATE, w)
};
int r = f();
return static_cast<window_state_t>(r);
}
result_t rkt_webview_set_title(rktwebview_t wv, const char *title)
{
CMDRES(CMD_SET_TITLE, wv, "title", title)
}
result_t rkt_webview_choose_dir(rktwebview_t w, const char *title, const char *base_dir)
{
CMDRES2(CMD_CHOOSE_DIR, w, "title", title, "base_dir", base_dir)
}
result_t rkt_webview_file_open(rktwebview_t w, const char *title, const char *base_dir, const char *permitted_exts)
{
CMDRES3(CMD_FILE_OPEN, w, "title", title, "base_dir", base_dir, "permitted_exts", permitted_exts)
}
result_t rkt_webview_file_save(rktwebview_t w, const char *title, const char *base_dir, const char *permitted_exts)
{
CMDRES3(CMD_FILE_SAVE, w, "title", title, "base_dir", base_dir, "permitted_exts", permitted_exts)
}
void rkt_webview_set_ou_token(rktwebview_t wv, const char *token)
{
rkt_webview_init();
NOOP_HANDLE
JSON j;
j["wv"] = wv;
j["token"] = std::string(token);
handler->command_queue->enqueue(CMD_SET_OU_TOKEN, j.dump());
}
void rkt_webview_free_data(rkt_data_t *d)
{
if (d == nullptr) { return; }
if (d->kind == version) {
free(d);
} else if (d->kind == event) {
free(d->data.event.event);
free(d);
} else if (d->kind == js_result) {
free(d->data.js_result.value);
free(d);
} else {
ERROR1("UNEXPECTED: data kind %d cannot be freed\n", d->kind);
}
}
rkt_data_t *rkt_webview_version()
{
rkt_data_t *d = static_cast<rkt_data_t *>(malloc(sizeof(rkt_data_t)));
d->kind = version;
d->data.version.api_major = RKT_WEBVIEW_API_MAJOR;
d->data.version.api_minor = RKT_WEBVIEW_API_MINOR;
d->data.version.api_patch = RKT_WEBVIEW_API_PATCH;
return d;
}
result_t rkt_webview_message_box(rktwebview_t w, const char *title, const char *message, const char *submessage, rkt_messagetype_t type)
{
CMDRES4(CMD_MSG_BOX, w, "title", title, "message", message, "submessage", submessage, "type", static_cast<int>(type))
}
int rkt_webview_events_waiting()
{
return handler->event_queue->depth();
}
rkt_data_t *rkt_webview_get_event()
{
//fprintf(stderr, "rkt_webview_get_event\n");
int wv;
std::string data;
if (handler->event_queue->dequeue(wv, data, false)) {
//fprintf(stderr, "got event %d %s\n", wv, data.c_str());
rkt_data_t *d = reinterpret_cast<rkt_data_t *>(malloc(sizeof(rkt_data_t)));
d->kind = rkt_data_kind_t::event;
size_t ds = data.size();
d->data.event.event = reinterpret_cast<char *>(malloc(ds + 1));
memcpy(d->data.event.event, data.c_str(), ds);
d->data.event.event[ds] = '\0';
d->data.event.wv = wv;
//fprintf(stderr, "event: %d - '%s'\n", d->data.event.wv, d->data.event.event);fflush(stderr);
return d;
} else {
return nullptr;
}
}
void rkt_webview_env(const char *env_cmds[])
{
int i;
for(i = 0; env_cmds[i] != nullptr && strcmp(env_cmds[i], "") != 0; i++) {
#ifdef WIN32
_putenv(env_cmds[i]);
#else
putenv(env_cmds[i]);
#endif
}
}