diff --git a/Cargo.lock b/Cargo.lock index 800182c9f..5d5b9d946 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1314,7 +1314,7 @@ dependencies = [ [[package]] name = "deltachat-jsonrpc" -version = "0.1.0" +version = "1.86.0" dependencies = [ "anyhow", "async-channel", @@ -1349,6 +1349,7 @@ dependencies = [ "anyhow", "async-std", "deltachat", + "deltachat-jsonrpc", "human-panic", "libc", "num-traits", diff --git a/deltachat-ffi/Cargo.toml b/deltachat-ffi/Cargo.toml index 3f594976c..390a23745 100644 --- a/deltachat-ffi/Cargo.toml +++ b/deltachat-ffi/Cargo.toml @@ -16,6 +16,7 @@ crate-type = ["cdylib", "staticlib"] [dependencies] deltachat = { path = "../", default-features = false } +deltachat-jsonrpc = { path = "../deltachat-jsonrpc" } libc = "0.2" human-panic = "1" num-traits = "0.2" diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 13d2d56ee..e53e68df1 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -23,7 +23,7 @@ typedef struct _dc_provider dc_provider_t; typedef struct _dc_event dc_event_t; typedef struct _dc_event_emitter dc_event_emitter_t; typedef struct _dc_accounts_event_emitter dc_accounts_event_emitter_t; - +typedef struct _dc_json_api_instance dc_json_api_instance_t; /** * @mainpage Getting started @@ -5178,6 +5178,55 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot); */ + +/** + * @class dc_json_api_instance_t + * + * Opaque object for using the json rpc api from the cffi bindings. + */ + +/** + * Create the jsonrpc instance that is used to call the jsonrpc. + * + * @memberof dc_accounts_t + * @param account_manager The accounts object as created by dc_accounts_new(). + * @return Returns the jsonrpc instance, NULL on errors. + * Must be freed using dc_json_api_unref() after usage. + * + */ +dc_json_api_instance_t* dc_get_json_api(dc_accounts_t* account_manager); + +/** + * Free a jsonrpc instance. + * + * @memberof dc_json_api_instance_t + * @param json_api_instance jsonrpc instance as returned from dc_get_json_api(). + * If NULL is given, nothing is done and an error is logged. + */ +void dc_json_api_unref(dc_json_api_instance_t* json_api_instance); + +/** + * Makes an asynchronous jsonrpc request, + * returns immediately and once the result is ready it can be retrieved via dc_get_next_json_response() + * the jsonrpc specification defines an invocation id that can then be used to match request and response. + * + * @memberof dc_json_api_instance_t + * @param json_api_instance jsonrpc instance as returned from dc_get_json_api(). + * @param request JSON-RPC request as string + */ +void dc_json_request(dc_json_api_instance_t* json_api_instance, char* request); + +/** + * Get the next json_rpc response, blocks until there is a new event, so call this in a loop from a thread. + * + * @memberof dc_json_api_instance_t + * @param json_api_instance jsonrpc instance as returned from dc_get_json_api(). + * @return JSON-RPC response as string + * If NULL is returned, the accounts_t belonging to the jsonrpc instance is unref'd and no more events will come; + * in this case, free the jsonrpc instance using dc_json_api_unref(). + */ +char* dc_get_next_json_response(dc_json_api_instance_t* json_api_instance); + /** * @class dc_event_emitter_t * diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 5ca1877d5..c258a588f 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -18,6 +18,7 @@ use std::fmt::Write; use std::ops::Deref; use std::ptr; use std::str::FromStr; +use std::sync::Arc; use std::time::{Duration, SystemTime}; use anyhow::Context as _; @@ -4093,11 +4094,11 @@ pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) { /// Reader-writer lock wrapper for accounts manager to guarantee thread safety when using /// `dc_accounts_t` in multiple threads at once. pub struct AccountsWrapper { - inner: RwLock, + inner: Arc>, } impl Deref for AccountsWrapper { - type Target = RwLock; + type Target = Arc>; fn deref(&self) -> &Self::Target { &self.inner @@ -4106,7 +4107,7 @@ impl Deref for AccountsWrapper { impl AccountsWrapper { fn new(accounts: Accounts) -> Self { - let inner = RwLock::new(accounts); + let inner = Arc::new(RwLock::new(accounts)); Self { inner } } } @@ -4424,3 +4425,73 @@ pub unsafe extern "C" fn dc_accounts_get_next_event( .map(|ev| Box::into_raw(Box::new(ev))) .unwrap_or_else(ptr::null_mut) } + +use deltachat_jsonrpc::api::CommandApi; +use deltachat_jsonrpc::yerpc::{MessageHandle, RpcHandle}; + +pub struct dc_json_api_instance_t { + receiver: async_std::channel::Receiver, + handle: MessageHandle, +} + +#[no_mangle] +pub unsafe extern "C" fn dc_get_json_api( + account_manager: *mut dc_accounts_t, +) -> *mut dc_json_api_instance_t { + if account_manager.is_null() { + eprintln!("ignoring careless call to dc_get_json_api()"); + return ptr::null_mut(); + } + + let cmd_api = + deltachat_jsonrpc::api::CommandApi::new_from_cffi((*account_manager).inner.clone()); + + let (request_handle, receiver) = RpcHandle::new(); + let handle = MessageHandle::new(request_handle, cmd_api); + + let instance = dc_json_api_instance_t { receiver, handle }; + + Box::into_raw(Box::new(instance)) +} + +#[no_mangle] +pub unsafe extern "C" fn dc_json_api_unref(json_api_instance: *mut dc_json_api_instance_t) { + if json_api_instance.is_null() { + eprintln!("ignoring careless call to dc_json_api_unref()"); + return; + } + + Box::from_raw(json_api_instance); +} + +#[no_mangle] +pub unsafe extern "C" fn dc_json_request( + json_api_instance: *mut dc_json_api_instance_t, + request: *const libc::c_char, +) { + if json_api_instance.is_null() || request.is_null() { + eprintln!("ignoring careless call to dc_json_request()"); + return; + } + + let api = &*json_api_instance; + let handle = &api.handle; + let request = to_string_lossy(request); + async_std::task::spawn(async move { + handle.handle_message(&request).await; + }); +} + +#[no_mangle] +pub unsafe extern "C" fn dc_get_next_json_response( + json_api_instance: *mut dc_json_api_instance_t, +) -> *mut libc::c_char { + if json_api_instance.is_null() { + eprintln!("ignoring careless call to dc_get_next_json_response()"); + return ptr::null_mut(); + } + let api = &*json_api_instance; + async_std::task::block_on(api.receiver.recv()) + .map(|result| serde_json::to_string(&result).unwrap_or_default().strdup()) + .unwrap_or(ptr::null_mut()) +} diff --git a/deltachat-jsonrpc/Cargo.toml b/deltachat-jsonrpc/Cargo.toml index 9a1b8e12d..a7e337578 100644 --- a/deltachat-jsonrpc/Cargo.toml +++ b/deltachat-jsonrpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-jsonrpc" -version = "0.1.0" +version = "1.86.0" authors = ["Delta Chat Developers (ML) "] edition = "2021" default-run = "webserver" diff --git a/deltachat-jsonrpc/src/api/mod.rs b/deltachat-jsonrpc/src/api/mod.rs index c9f8489e6..cc80cb9b7 100644 --- a/deltachat-jsonrpc/src/api/mod.rs +++ b/deltachat-jsonrpc/src/api/mod.rs @@ -39,6 +39,10 @@ impl CommandApi { } } + pub fn new_from_cffi(accounts: Arc>) -> Self { + CommandApi { accounts } + } + async fn get_context(&self, id: u32) -> Result { let sc = self .accounts diff --git a/deltachat-jsonrpc/src/lib.rs b/deltachat-jsonrpc/src/lib.rs index 19a76cb0a..bd5d30694 100644 --- a/deltachat-jsonrpc/src/lib.rs +++ b/deltachat-jsonrpc/src/lib.rs @@ -1,6 +1,8 @@ pub mod api; pub use api::events; +pub use yerpc; + #[cfg(test)] mod tests { use super::api::{Accounts, CommandApi}; diff --git a/node/lib/deltachat.ts b/node/lib/deltachat.ts index 152e4baaf..6235c36ae 100644 --- a/node/lib/deltachat.ts +++ b/node/lib/deltachat.ts @@ -19,10 +19,11 @@ interface NativeAccount {} export class AccountManager extends EventEmitter { dcn_accounts: NativeAccount accountDir: string + json_rpc_started = false constructor(cwd: string, os = 'deltachat-node') { - debug('DeltaChat constructor') super() + debug('DeltaChat constructor') this.accountDir = cwd this.dcn_accounts = binding.dcn_accounts_new(os, this.accountDir) @@ -114,6 +115,31 @@ export class AccountManager extends EventEmitter { debug('Started event handler') } + startJSONRPCHandler(callback: ((response: string) => void) | null) { + if (this.dcn_accounts === null) { + throw new Error('dcn_account is null') + } + if (!callback) { + throw new Error('no callback set') + } + if (this.json_rpc_started) { + throw new Error('jsonrpc was started already') + } + + binding.dcn_accounts_start_jsonrpc(this.dcn_accounts, callback.bind(this)) + debug('Started jsonrpc handler') + this.json_rpc_started = true + } + + jsonRPCRequest(message: string) { + if (!this.json_rpc_started) { + throw new Error( + 'jsonrpc is not active, start it with startJSONRPCHandler first' + ) + } + binding.dcn_json_rpc_request(this.dcn_accounts, message) + } + startIO() { binding.dcn_accounts_start_io(this.dcn_accounts) } diff --git a/node/segfault.js b/node/segfault.js new file mode 100644 index 000000000..72945a83d --- /dev/null +++ b/node/segfault.js @@ -0,0 +1,11 @@ +const {default:dc} = require("./dist") + +const ac = new dc("testdtrdtrh") + +ac.startJSONRPCHandler(console.log) + +setTimeout(()=>{ +ac.close() // This segfaults -> TODO Findout why? + +console.log("still living") +}, 1000) diff --git a/node/src/module.c b/node/src/module.c index 2d2f4beee..0bfa7db0f 100644 --- a/node/src/module.c +++ b/node/src/module.c @@ -34,6 +34,9 @@ typedef struct dcn_accounts_t { dc_accounts_t* dc_accounts; napi_threadsafe_function threadsafe_event_handler; uv_thread_t event_handler_thread; + napi_threadsafe_function threadsafe_jsonrpc_handler; + uv_thread_t jsonrpc_thread; + dc_json_api_instance_t* jsonrpc_instance; int gc; } dcn_accounts_t; @@ -2932,6 +2935,10 @@ NAPI_METHOD(dcn_accounts_unref) { uv_thread_join(&dcn_accounts->event_handler_thread); dcn_accounts->event_handler_thread = 0; } + if (dcn_accounts->jsonrpc_instance) { + dc_json_api_unref(dcn_accounts->jsonrpc_instance); + dcn_accounts->jsonrpc_instance = NULL; + } dc_accounts_unref(dcn_accounts->dc_accounts); dcn_accounts->dc_accounts = NULL; @@ -3090,8 +3097,6 @@ static void accounts_event_handler_thread_func(void* arg) { dcn_accounts_t* dcn_accounts = (dcn_accounts_t*)arg; - - TRACE("event_handler_thread_func starting"); dc_accounts_event_emitter_t * dc_accounts_event_emitter = dc_accounts_get_event_emitter(dcn_accounts->dc_accounts); @@ -3242,6 +3247,129 @@ NAPI_METHOD(dcn_accounts_start_event_handler) { NAPI_RETURN_UNDEFINED(); } +// JSON RPC + +static void accounts_jsonrpc_thread_func(void* arg) +{ + dcn_accounts_t* dcn_accounts = (dcn_accounts_t*)arg; + TRACE("accounts_jsonrpc_thread_func starting"); + char* response; + while (true) { + if (dcn_accounts->jsonrpc_instance == NULL) { + TRACE("jsonrpc is null, bailing"); + break; + } + response = dc_get_next_json_response(dcn_accounts->jsonrpc_instance); + if (response == NULL) { + //TRACE("received NULL event, skipping"); + continue; + } + + if (!dcn_accounts->threadsafe_jsonrpc_handler) { + TRACE("threadsafe_jsonrpc_handler not set, bailing"); + break; + } + // Don't process events if we're being garbage collected! + if (dcn_accounts->gc == 1) { + TRACE("dc_accounts has been destroyed, bailing"); + break; + } + + napi_status status = napi_call_threadsafe_function(dcn_accounts->threadsafe_jsonrpc_handler, response, napi_tsfn_blocking); + + if (status == napi_closing) { + TRACE("JS function got released, bailing"); + break; + } + } + dc_json_api_unref(dcn_accounts->jsonrpc_instance); + dcn_accounts->jsonrpc_instance = NULL; + TRACE("accounts_jsonrpc_thread_func ended"); + napi_release_threadsafe_function(dcn_accounts->threadsafe_jsonrpc_handler, napi_tsfn_release); +} + +static void call_accounts_js_jsonrpc_handler(napi_env env, napi_value js_callback, void* _context, void* data) +{ + char* response = (char*)data; + napi_value global; + napi_status status = napi_get_global(env, &global); + if (status != napi_ok) { + napi_throw_error(env, NULL, "Unable to get global"); + } + + napi_value argv[1]; + if (response != 0) { + status = napi_create_string_utf8(env, response, NAPI_AUTO_LENGTH, &argv[0]); + } else { + status = napi_create_string_utf8(env, "", NAPI_AUTO_LENGTH, &argv[0]); + } + if (status != napi_ok) { + napi_throw_error(env, NULL, "Unable to create argv for js jsonrpc_handler arguments"); + } + free(response); + + TRACE("calling back into js"); + napi_value result; + status = napi_call_function( + env, + global, + js_callback, + 1, + argv, + &result); + if (status != napi_ok) { + TRACE("Unable to call jsonrpc_handler callback2"); + const napi_extended_error_info* error_result; + NAPI_STATUS_THROWS(napi_get_last_error_info(env, &error_result)); + } +} + +NAPI_METHOD(dcn_accounts_start_jsonrpc) { + NAPI_ARGV(2); + NAPI_DCN_ACCOUNTS(); + napi_value callback = argv[1]; + + TRACE("calling.."); + napi_value async_resource_name; + NAPI_STATUS_THROWS(napi_create_string_utf8(env, "dc_accounts_jsonrpc_callback", NAPI_AUTO_LENGTH, &async_resource_name)); + + TRACE("creating threadsafe function.."); + + NAPI_STATUS_THROWS(napi_create_threadsafe_function( + env, + callback, + 0, + async_resource_name, + 1, + 1, + NULL, + NULL, + dcn_accounts, + call_accounts_js_jsonrpc_handler, + &dcn_accounts->threadsafe_jsonrpc_handler)); + TRACE("done"); + + dcn_accounts->gc = 0; + dcn_accounts->jsonrpc_instance = dc_get_json_api(dcn_accounts->dc_accounts); + + TRACE("creating uv thread.."); + uv_thread_create(&dcn_accounts->jsonrpc_thread, accounts_jsonrpc_thread_func, dcn_accounts); + + NAPI_RETURN_UNDEFINED(); +} + +NAPI_METHOD(dcn_json_rpc_request) { + NAPI_ARGV(2); + NAPI_DCN_ACCOUNTS(); + if (!dcn_accounts->jsonrpc_instance) { + const char* msg = "dcn_accounts->jsonrpc_instance is null, have you called dcn_accounts_start_jsonrpc()?"; + NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg)); + } + NAPI_ARGV_UTF8_MALLOC(request, 1); + dc_json_request(dcn_accounts->jsonrpc_instance, request); + free(request); +} + NAPI_INIT() { /** @@ -3512,4 +3640,9 @@ NAPI_INIT() { NAPI_EXPORT_FUNCTION(dcn_send_webxdc_status_update); NAPI_EXPORT_FUNCTION(dcn_get_webxdc_status_updates); NAPI_EXPORT_FUNCTION(dcn_msg_get_webxdc_blob); + + + /** jsonrpc **/ + NAPI_EXPORT_FUNCTION(dcn_accounts_start_jsonrpc); + NAPI_EXPORT_FUNCTION(dcn_json_rpc_request); } diff --git a/node/src/napi-macros-extensions.h b/node/src/napi-macros-extensions.h index badb63969..968bc02ca 100644 --- a/node/src/napi-macros-extensions.h +++ b/node/src/napi-macros-extensions.h @@ -23,7 +23,7 @@ dcn_accounts_t* dcn_accounts; \ NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dcn_accounts)); \ if (!dcn_accounts) { \ - const char* msg = "Provided dnc_acounts is null"; \ + const char* msg = "Provided dcn_acounts is null"; \ NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg)); \ } \ if (!dcn_accounts->dc_accounts) { \ diff --git a/node/test/test.js b/node/test/test.js index 30393272c..5aad22382 100644 --- a/node/test/test.js +++ b/node/test/test.js @@ -2,7 +2,7 @@ import DeltaChat, { Message } from '../dist' import binding from '../binding' -import { strictEqual } from 'assert' +import { deepEqual, deepStrictEqual, strictEqual } from 'assert' import chai, { expect } from 'chai' import chaiAsPromised from 'chai-as-promised' import { EventId2EventName, C } from '../dist/constants' @@ -84,6 +84,96 @@ describe('static tests', function () { }) }) +describe.only('JSON RPC', function () { + it('smoketest', async function () { + const { dc } = DeltaChat.newTemporary() + let promise_resolve + const promise = new Promise((res, _rej) => { + promise_resolve = res + }) + dc.startJSONRPCHandler(promise_resolve) + dc.jsonRPCRequest( + JSON.stringify({ + jsonrpc: '2.0', + method: 'get_all_account_ids', + params: [], + id: 2, + }) + ) + deepStrictEqual( + { + jsonrpc: '2.0', + id: 2, + result: [1], + }, + JSON.parse(await promise) + ) + // TODO: fix that shutdown crashes! + dc.close() + }) + + it('basic test', async function () { + const { dc } = DeltaChat.newTemporary() + + const promises = {}; + dc.startJSONRPCHandler((msg) => { + const response = JSON.parse(msg) + promises[response.id](response) + delete promises[response.id] + }) + const call = (request) => { + dc.jsonRPCRequest(JSON.stringify(request)) + return new Promise((res, _rej)=> { + promises[request.id] = res + }) + } + + deepStrictEqual( + { + jsonrpc: '2.0', + id: 2, + result: [1], + }, + await call({ + jsonrpc: '2.0', + method: 'get_all_account_ids', + params: [], + id: 2, + }) + ) + + deepStrictEqual( + { + jsonrpc: '2.0', + id: 3, + result: 2, + }, + await call({ + jsonrpc: '2.0', + method: 'add_account', + params: [], + id: 3, + }) + ) + + deepStrictEqual( + { + jsonrpc: '2.0', + id: 4, + result: [1, 2], + }, + await call({ + jsonrpc: '2.0', + method: 'get_all_account_ids', + params: [], + id: 4, + }) + ) + + dc.close() + }) +}) + describe('Basic offline Tests', function () { it('opens a context', async function () { const { dc, context } = DeltaChat.newTemporary()