commit 9c7f95e72913c686d29d286b6cf69873e9edbb52
parent 764e417aeeeaa1b2f214d6d870147f7f5316d6fe
Author: Florian Dold <dold@taler.net>
Date: Sun, 9 Aug 2026 21:19:17 +0200
wallet: make runtime shutdown deterministic
Diffstat:
5 files changed, 261 insertions(+), 18 deletions(-)
diff --git a/qtart.c b/qtart.c
@@ -565,6 +565,7 @@ int main(int argc, char **argv)
return 0;
fail:
js_std_free_handlers(rt);
+ js_curl_http_client_destroy(http_impl);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
return 1;
diff --git a/quickjs/quickjs-libc.c b/quickjs/quickjs-libc.c
@@ -210,6 +210,8 @@ typedef struct JSThreadState {
// used to provided fairer scheduling of event reactions
unsigned int poll_iteration_count;
+ BOOL stop_requested;
+
struct list_head http_requests;
#ifndef NO_HTTP
@@ -3312,6 +3314,13 @@ static int js_os_poll(JSContext *ctx)
int http_poll_fd_index;
#endif
+ pthread_mutex_lock(&ts->host_pipe->mutex);
+ BOOL stop_requested = ts->stop_requested;
+ pthread_mutex_unlock(&ts->host_pipe->mutex);
+ if (stop_requested) {
+ return -1;
+ }
+
/* only check signals in the main thread */
if (!ts->is_worker_thread &&
unlikely(os_pending_signals != 0)) {
@@ -4837,6 +4846,28 @@ js_os_post_message_from_host(JSContext *ctx, const char *msg_str)
return -1;
}
+int
+js_std_request_stop(JSContext *ctx)
+{
+ JSThreadState *ts = JS_GetRuntimeOpaque(JS_GetRuntime(ctx));
+ JSHostMessagePipe *hp;
+ uint8_t ch = '\0';
+
+ if (!ts || !ts->host_pipe) {
+ return -1;
+ }
+ hp = ts->host_pipe;
+ pthread_mutex_lock(&hp->mutex);
+ if (!ts->stop_requested) {
+ ts->stop_requested = TRUE;
+ while (write(hp->write_fd, &ch, 1) < 0 && errno == EINTR) {
+ /* retry */
+ }
+ }
+ pthread_mutex_unlock(&hp->mutex);
+ return 0;
+}
+
static JSValue js_os_simulateHostMessage(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
{
diff --git a/quickjs/quickjs-libc.h b/quickjs/quickjs-libc.h
@@ -66,6 +66,7 @@ void js_std_promise_rejection_tracker(JSContext *ctx, JSValueConst promise,
void js_std_set_worker_new_context_func(JSContext *(*func)(JSRuntime *rt));
void js_os_set_host_message_handler(JSContext *ctx, JSHostMessageHandlerFn f, void *cls);
int js_os_post_message_from_host(JSContext *ctx, const char *msg_str);
+int js_std_request_stop(JSContext *ctx);
#ifndef NO_HTTP
void js_os_set_http_impl(JSRuntime *rt, struct JSHttpClientImplementation *impl);
diff --git a/taler_wallet_core_lib.c b/taler_wallet_core_lib.c
@@ -32,6 +32,16 @@ extern const uint8_t qjsc_wallet_core[];
extern const uint32_t qjsc_wallet_core_size;
static JSClassID js_wallet_instance_handle_id;
+static pthread_once_t wallet_class_id_once = PTHREAD_ONCE_INIT;
+
+static JSClassDef js_wallet_instance_handle_class = {
+ .class_name = "TalerWalletInstanceHandle",
+};
+
+static void init_wallet_class_id(void)
+{
+ JS_NewClassID(&js_wallet_instance_handle_id);
+}
struct HostMessage {
struct list_head link;
@@ -42,11 +52,22 @@ struct TALER_WALLET_Instance
{
// Mutex that is locked while initializing the handle
pthread_mutex_t handle_mutex;
+ pthread_cond_t state_changed;
JSRuntime *rt;
JSContext *ctx;
pthread_t wallet_thread;
+ int wallet_thread_valid;
+
+ enum {
+ WALLET_CREATED,
+ WALLET_STARTING,
+ WALLET_RUNNING,
+ WALLET_STOPPING,
+ WALLET_STOPPED,
+ WALLET_FAILED,
+ } state;
TALER_WALLET_MessageHandlerFn handler_f;
void *handler_cls;
@@ -55,6 +76,7 @@ struct TALER_WALLET_Instance
void *log_handler_cls;
struct JSHttpClientImplementation *http_impl;
+ int owns_http_impl;
};
@@ -116,6 +138,13 @@ TALER_WALLET_send_request (struct TALER_WALLET_Instance *twi,
{
int ret;
pthread_mutex_lock(&twi->handle_mutex);
+ while (twi->state == WALLET_STARTING) {
+ pthread_cond_wait(&twi->state_changed, &twi->handle_mutex);
+ }
+ if (twi->state != WALLET_RUNNING || !twi->ctx) {
+ pthread_mutex_unlock(&twi->handle_mutex);
+ return -1;
+ }
ret = js_os_post_message_from_host(twi->ctx, msg);
pthread_mutex_unlock(&twi->handle_mutex);
return ret;
@@ -137,11 +166,20 @@ struct TALER_WALLET_Instance *
TALER_WALLET_create(void)
{
struct TALER_WALLET_Instance *wh;
- wh = malloc(sizeof (*wh));
- memset(wh, 0, sizeof *wh);
+ wh = calloc(1, sizeof (*wh));
+ if (!wh) {
+ return NULL;
+ }
if (0 != pthread_mutex_init(&wh->handle_mutex, NULL)) {
- abort();
+ free(wh);
+ return NULL;
+ }
+ if (0 != pthread_cond_init(&wh->state_changed, NULL)) {
+ pthread_mutex_destroy(&wh->handle_mutex);
+ free(wh);
+ return NULL;
}
+ wh->state = WALLET_CREATED;
return wh;
}
@@ -156,6 +194,9 @@ static JSValue js_native_log(JSContext *ctx,
const char *msg = NULL;
uint32_t level = 0;
wh = JS_GetOpaque(func_data[0], js_wallet_instance_handle_id);
+ if (!wh) {
+ return JS_ThrowInternalError(ctx, "invalid wallet log context");
+ }
if (NULL != wh->log_handler_f) {
JS_ToUint32(ctx, &level, argv[0]);
tag = JS_ToCString(ctx, argv[1]);
@@ -175,10 +216,15 @@ static void *
run(void *cls)
{
struct TALER_WALLET_Instance *wh = cls;
+ int handlers_initialized = 0;
wh->rt = JS_NewRuntime();
+ if (!wh->rt) {
+ goto fail;
+ }
js_std_init_handlers(wh->rt);
+ handlers_initialized = 1;
if (wh->http_impl) {
js_os_set_http_impl(wh->rt, wh->http_impl);
@@ -190,11 +236,14 @@ run(void *cls)
if (!wh->ctx) {
fprintf(stderr, "qjs: cannot allocate JS context\n");
- pthread_mutex_unlock(&wh->handle_mutex);
- return NULL;
+ goto fail;
}
- JS_NewClassID(&js_wallet_instance_handle_id);
+ if (0 != pthread_once(&wallet_class_id_once, init_wallet_class_id) ||
+ 0 != JS_NewClass(wh->rt, js_wallet_instance_handle_id,
+ &js_wallet_instance_handle_class)) {
+ goto fail;
+ }
JS_SetHostPromiseRejectionTracker(wh->rt, js_std_promise_rejection_tracker,
@@ -208,11 +257,16 @@ run(void *cls)
JSValue data;
data = JS_NewObjectClass(wh->ctx, js_wallet_instance_handle_id);
+ if (JS_IsException(data)) {
+ goto fail;
+ }
JS_SetOpaque(data, wh);
global_obj = JS_GetGlobalObject(wh->ctx);
// We could also try to add this to "os" or "tart", but we are lazy.
JS_SetPropertyStr(wh->ctx, global_obj, "__nativeLog",
JS_NewCFunctionData(wh->ctx, js_native_log, 3, 0, 1, &data));
+ JS_FreeValue(wh->ctx, global_obj);
+ JS_FreeValue(wh->ctx, data);
}
// fprintf(stderr, "qtart: loading JS code\n");
@@ -223,12 +277,50 @@ run(void *cls)
js_os_set_host_message_handler(wh->ctx, wallet_host_message_handler, wh);
- pthread_mutex_unlock(&wh->handle_mutex);
+ if (0 != eval_buf(wh->ctx, "installNativeWalletListener()",
+ "<talerwalletcore>")) {
+ goto fail;
+ }
- eval_buf(wh->ctx, "installNativeWalletListener()", "<talerwalletcore>");
+ pthread_mutex_lock(&wh->handle_mutex);
+ wh->state = WALLET_RUNNING;
+ pthread_cond_broadcast(&wh->state_changed);
+ pthread_mutex_unlock(&wh->handle_mutex);
js_std_loop(wh->ctx);
+ pthread_mutex_lock(&wh->handle_mutex);
+ wh->state = WALLET_STOPPING;
+ pthread_cond_broadcast(&wh->state_changed);
+ pthread_mutex_unlock(&wh->handle_mutex);
+ js_std_free_handlers(wh->rt);
+ handlers_initialized = 0;
+ JS_FreeContext(wh->ctx);
+ JS_FreeRuntime(wh->rt);
+ pthread_mutex_lock(&wh->handle_mutex);
+ wh->ctx = NULL;
+ wh->rt = NULL;
+ wh->state = WALLET_STOPPED;
+ pthread_cond_broadcast(&wh->state_changed);
+ pthread_mutex_unlock(&wh->handle_mutex);
+ return NULL;
+
+fail:
+ if (handlers_initialized) {
+ js_std_free_handlers(wh->rt);
+ }
+ if (wh->ctx) {
+ JS_FreeContext(wh->ctx);
+ }
+ if (wh->rt) {
+ JS_FreeRuntime(wh->rt);
+ }
+ pthread_mutex_lock(&wh->handle_mutex);
+ wh->ctx = NULL;
+ wh->rt = NULL;
+ wh->state = WALLET_FAILED;
+ pthread_cond_broadcast(&wh->state_changed);
+ pthread_mutex_unlock(&wh->handle_mutex);
return NULL;
}
@@ -240,42 +332,103 @@ TALER_WALLET_run (struct TALER_WALLET_Instance *wh)
pthread_t wallet_thread;
pthread_attr_t tattr;
+ if (!wh) {
+ return -1;
+ }
pthread_mutex_lock(&wh->handle_mutex);
+ if (wh->state != WALLET_CREATED) {
+ pthread_mutex_unlock(&wh->handle_mutex);
+ return -1;
+ }
+ wh->state = WALLET_STARTING;
if (0 != pthread_attr_init(&tattr)) {
+ wh->state = WALLET_CREATED;
pthread_mutex_unlock(&wh->handle_mutex);
fprintf(stderr, "could not initialize pthread attr\n");
return -1;
}
if (0 != pthread_attr_setstacksize(&tattr, WALLET_THREAD_STACK_SIZE)) {
+ pthread_attr_destroy(&tattr);
+ wh->state = WALLET_CREATED;
pthread_mutex_unlock(&wh->handle_mutex);
fprintf(stderr, "could not set stack size\n");
return -1;
}
if (0 != pthread_create(&wallet_thread, &tattr, run, wh)) {
+ pthread_attr_destroy(&tattr);
+ wh->state = WALLET_CREATED;
pthread_mutex_unlock(&wh->handle_mutex);
fprintf(stderr, "could not create wallet thread\n");
return -1;
}
wh->wallet_thread = wallet_thread;
+ wh->wallet_thread_valid = 1;
+ pthread_attr_destroy(&tattr);
+ pthread_mutex_unlock(&wh->handle_mutex);
return 0;
}
void TALER_WALLET_join(struct TALER_WALLET_Instance *wh)
{
- pthread_join(wh->wallet_thread, NULL);
+ pthread_t thread;
+
+ if (!wh) {
+ return;
+ }
+ pthread_mutex_lock(&wh->handle_mutex);
+ if (!wh->wallet_thread_valid) {
+ pthread_mutex_unlock(&wh->handle_mutex);
+ return;
+ }
+ thread = wh->wallet_thread;
+ pthread_mutex_unlock(&wh->handle_mutex);
+ if (!pthread_equal(pthread_self(), thread)) {
+ pthread_join(thread, NULL);
+ pthread_mutex_lock(&wh->handle_mutex);
+ wh->wallet_thread_valid = 0;
+ pthread_mutex_unlock(&wh->handle_mutex);
+ }
}
void
TALER_WALLET_destroy(struct TALER_WALLET_Instance *twi)
{
- pthread_kill(twi->wallet_thread, SIGKILL);
+ pthread_t thread;
+ int have_thread;
+
+ if (!twi) {
+ return;
+ }
+ pthread_mutex_lock(&twi->handle_mutex);
+ while (twi->state == WALLET_STARTING) {
+ pthread_cond_wait(&twi->state_changed, &twi->handle_mutex);
+ }
+ have_thread = twi->wallet_thread_valid;
+ thread = twi->wallet_thread;
+ if (twi->state == WALLET_RUNNING && twi->ctx) {
+ js_std_request_stop(twi->ctx);
+ }
+ pthread_mutex_unlock(&twi->handle_mutex);
+
+ if (have_thread && pthread_equal(pthread_self(), thread)) {
+ return;
+ }
+ if (have_thread) {
+ pthread_join(thread, NULL);
+ }
+ if (twi->owns_http_impl) {
+ js_curl_http_client_destroy(twi->http_impl);
+ }
+ pthread_cond_destroy(&twi->state_changed);
+ pthread_mutex_destroy(&twi->handle_mutex);
+ free(twi);
}
@@ -289,9 +442,11 @@ static struct LogRedirectContext redir_ctx;
static int pfd[2];
static pthread_t log_thr;
-static void *thread_func(void *) {
+static void *thread_func(void *cls) {
ssize_t rdsz;
char buf[1024];
+
+ (void) cls;
while ((rdsz = read(pfd[0], buf, sizeof buf - 1)) > 0) {
if (buf[rdsz - 1] == '\n') --rdsz;
buf[rdsz] = 0; /* add null-terminator */
@@ -304,6 +459,12 @@ static void *thread_func(void *) {
int
TALER_start_redirect_std(TALER_LogFn logfn, void *cls)
{
+ int saved_stdout;
+ int saved_stderr;
+
+ if (!logfn) {
+ return -1;
+ }
if (redir_ctx.active) {
return -2;
}
@@ -312,18 +473,48 @@ TALER_start_redirect_std(TALER_LogFn logfn, void *cls)
setvbuf(stderr, 0, _IONBF, 0);
/* create the pipe and redirect stdout and stderr */
- pipe(pfd);
- dup2(pfd[1], 1);
- dup2(pfd[1], 2);
-
+ if (pipe(pfd) != 0) {
+ return -1;
+ }
+ saved_stdout = dup(STDOUT_FILENO);
+ saved_stderr = dup(STDERR_FILENO);
+ if (saved_stdout < 0 || saved_stderr < 0 ||
+ dup2(pfd[1], STDOUT_FILENO) < 0 ||
+ dup2(pfd[1], STDERR_FILENO) < 0) {
+ if (saved_stdout >= 0) {
+ (void) dup2(saved_stdout, STDOUT_FILENO);
+ }
+ if (saved_stderr >= 0) {
+ (void) dup2(saved_stderr, STDERR_FILENO);
+ }
+ if (saved_stdout >= 0) {
+ close(saved_stdout);
+ }
+ if (saved_stderr >= 0) {
+ close(saved_stderr);
+ }
+ close(pfd[0]);
+ close(pfd[1]);
+ return -1;
+ }
redir_ctx.cls = cls;
redir_ctx.logfn = logfn;
redir_ctx.active = 1;
/* spawn the logging thread */
- if (pthread_create(&log_thr, 0, thread_func, 0) == -1) {
+ if (pthread_create(&log_thr, 0, thread_func, 0) != 0) {
+ (void) dup2(saved_stdout, STDOUT_FILENO);
+ (void) dup2(saved_stderr, STDERR_FILENO);
+ close(saved_stdout);
+ close(saved_stderr);
+ close(pfd[0]);
+ close(pfd[1]);
+ redir_ctx.active = 0;
return -1;
}
+ close(saved_stdout);
+ close(saved_stderr);
+ close(pfd[1]);
pthread_detach(log_thr);
return 0;
}
@@ -332,7 +523,17 @@ void
TALER_set_http_client_implementation(struct TALER_WALLET_Instance *twi,
struct JSHttpClientImplementation *impl)
{
+ pthread_mutex_lock(&twi->handle_mutex);
+ if (twi->state != WALLET_CREATED) {
+ pthread_mutex_unlock(&twi->handle_mutex);
+ return;
+ }
+ if (twi->owns_http_impl) {
+ js_curl_http_client_destroy(twi->http_impl);
+ }
twi->http_impl = impl;
+ twi->owns_http_impl = 0;
+ pthread_mutex_unlock(&twi->handle_mutex);
}
#ifndef NO_CURL
@@ -341,6 +542,13 @@ TALER_set_curl_http_client(struct TALER_WALLET_Instance *twi)
{
struct JSHttpClientImplementation *client = js_curl_http_client_create();
TALER_set_http_client_implementation(twi, client);
+ pthread_mutex_lock(&twi->handle_mutex);
+ if (twi->http_impl == client) {
+ twi->owns_http_impl = client != NULL;
+ } else {
+ js_curl_http_client_destroy(client);
+ }
+ pthread_mutex_unlock(&twi->handle_mutex);
}
#endif
diff --git a/taler_wallet_core_lib.h b/taler_wallet_core_lib.h
@@ -131,9 +131,10 @@ TALER_WALLET_join(struct TALER_WALLET_Instance *twi);
* an appropriate shutdown message should be sent first,
* and destroy() should only be called after the wallet has
* sent a response to the shutdown message.
+ * Must not be called from a wallet message or log callback.
*/
-//void
-//TALER_WALLET_destroy(struct TALER_WALLET_Instance *twi);
+void
+TALER_WALLET_destroy(struct TALER_WALLET_Instance *twi);
/**
* Handler for messages that should be logged.
@@ -155,6 +156,7 @@ TALER_start_redirect_std(TALER_LogFn logfn, void *handler_p);
/**
* Set the HTTP client implementation to be used by the wallet.
+ * Must be called before #TALER_WALLET_run.
*
* @param twi wallet-core instance
* @param impl HTTP client implementation