add json api to cffi and expose it in dc node

This commit is contained in:
Simon Laux
2022-06-12 20:33:48 +02:00
parent 346fab7f26
commit e12aeb7bd8
11 changed files with 397 additions and 10 deletions

View File

@@ -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"

View File

@@ -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
*

View File

@@ -19,6 +19,7 @@ use std::future::Future;
use std::ops::Deref;
use std::ptr;
use std::str::FromStr;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use anyhow::Context as _;
@@ -4108,11 +4109,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<Accounts>,
inner: Arc<RwLock<Accounts>>,
}
impl Deref for AccountsWrapper {
type Target = RwLock<Accounts>;
type Target = Arc<RwLock<Accounts>>;
fn deref(&self) -> &Self::Target {
&self.inner
@@ -4121,7 +4122,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 }
}
}
@@ -4437,3 +4438,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<deltachat_jsonrpc::yerpc::Message>,
handle: MessageHandle<CommandApi>,
}
#[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())
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "0.1.0"
version = "1.86.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
default-run = "webserver"

View File

@@ -39,6 +39,10 @@ impl CommandApi {
}
}
pub fn new_from_cffi(accounts: Arc<RwLock<Accounts>>) -> Self {
CommandApi { accounts }
}
async fn get_context(&self, id: u32) -> Result<deltachat::context::Context> {
let sc = self
.accounts

View File

@@ -1,6 +1,8 @@
pub mod api;
pub use api::events;
pub use yerpc;
#[cfg(test)]
mod tests {
use super::api::{Accounts, CommandApi};

View File

@@ -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)
}

11
node/segfault.js Normal file
View File

@@ -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)

View File

@@ -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;
@@ -2924,6 +2927,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;
@@ -3082,8 +3089,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);
@@ -3234,6 +3239,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() {
/**
@@ -3504,4 +3632,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);
}

View File

@@ -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) { \

View File

@@ -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()