lib: add method to cancel in-flight requests

This commit is contained in:
Oscar Mira 2023-02-26 13:20:38 +01:00
parent 47efd85fd6
commit 56ecb91657
7 changed files with 65 additions and 29 deletions

View File

@ -6,4 +6,5 @@ import im.molly.monero.RemoteNode;
interface IRemoteNodeClient {
// RemoteNode getRemoteNode();
HttpResponse makeRequest(String method, String path, String header, in byte[] body);
oneway void cancelAll();
}

View File

@ -7,6 +7,7 @@ ScopedJvmGlobalRef<jclass> OwnedTxOut;
jmethodID HttpResponse_getBody;
jmethodID HttpResponse_getCode;
jmethodID HttpResponse_getContentType;
jmethodID IRemoteNodeClient_cancelAll;
jmethodID IRemoteNodeClient_makeRequest;
jmethodID Logger_logFromNative;
jmethodID OwnedTxOut_ctor;
@ -29,6 +30,8 @@ void initializeJniCache(JNIEnv* env) {
.getMethodId(env, "getCode", "()I");
HttpResponse_getContentType = httpResponse
.getMethodId(env, "getContentType", "()Ljava/lang/String;");
IRemoteNodeClient_cancelAll = iRemoteNodeClient
.getMethodId(env, "cancelAll", "()V");
IRemoteNodeClient_makeRequest = iRemoteNodeClient
.getMethodId(env,
"makeRequest",

View File

@ -13,6 +13,7 @@ extern ScopedJvmGlobalRef<jclass> OwnedTxOut;
extern jmethodID HttpResponse_getBody;
extern jmethodID HttpResponse_getCode;
extern jmethodID HttpResponse_getContentType;
extern jmethodID IRemoteNodeClient_cancelAll;
extern jmethodID IRemoteNodeClient_makeRequest;
extern jmethodID Logger_logFromNative;
extern jmethodID OwnedTxOut_ctor;

View File

@ -24,12 +24,13 @@ static_assert(CRYPTONOTE_MAX_BLOCK_NUMBER == 500000000,
Wallet::Wallet(
JNIEnv* env,
int network_id,
std::unique_ptr<HttpClientFactory> http_client_factory,
const JvmRef<jobject>& remote_node_client,
const JvmRef<jobject>& callback)
: m_wallet(static_cast<cryptonote::network_type>(network_id),
0, /* kdf_rounds */
true, /* unattended */
std::move(http_client_factory)),
std::make_unique<RemoteNodeClientFactory>(env, remote_node_client)),
m_remote_node_client(env, remote_node_client),
m_callback(env, callback),
m_account_ready(false),
m_blockchain_height(1),
@ -212,11 +213,9 @@ Java_im_molly_monero_WalletNative_nativeCreate(
jobject thiz,
jint network_id,
jobject p_remote_node_client) {
auto wallet = new Wallet(
env, network_id,
std::make_unique<RemoteNodeClientFactory>(
env, JvmParamRef<jobject>(p_remote_node_client)),
JvmParamRef<jobject>(thiz));
auto wallet = new Wallet(env, network_id,
JvmParamRef<jobject>(p_remote_node_client),
JvmParamRef<jobject>(thiz));
return nativeToJvmPointer(wallet);
}

View File

@ -24,7 +24,7 @@ class Wallet : tools::i_wallet2_callback {
Wallet(JNIEnv* env,
int network_id,
std::unique_ptr<HttpClientFactory> http_client_factory,
const JvmRef<jobject>& remote_node_client,
const JvmRef<jobject>& callback);
void restoreAccount(const std::vector<char>& secret_scalar, uint64_t account_timestamp);

View File

@ -2,23 +2,23 @@ package im.molly.monero
import android.net.Uri
import android.os.ParcelFileDescriptor
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import okhttp3.*
import java.io.FileOutputStream
import java.io.IOException
import kotlin.coroutines.resumeWithException
internal class RemoteNodeClient(
private val nodeSelector: RemoteNodeSelector,
private val httpClient: OkHttpClient,
private val scope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher,
ioDispatcher: CoroutineDispatcher,
) : IRemoteNodeClient.Stub() {
private val logger = loggerFor<RemoteNodeClient>()
/** Disable connecting to the Monero network */
private val requestsScope = CoroutineScope(ioDispatcher + SupervisorJob())
// /** Disable connecting to the Monero network */
// var offline = false
private fun selectedNode() = nodeSelector.select()
@ -47,6 +47,11 @@ internal class RemoteNodeClient(
}
}
@CalledByNative("http_client.cc")
override fun cancelAll() {
requestsScope.coroutineContext.cancelChildren()
}
private fun execute(
method: String?,
uri: Uri,
@ -56,10 +61,12 @@ internal class RemoteNodeClient(
password: String?,
): HttpResponse {
logger.d("HTTP: $method $uri, header_len=${header?.length}, body_size=${body?.size}")
val headers = header?.parseHttpHeader()
val contentType = headers?.get("Content-Type")?.let { value ->
MediaType.get(value)
}
val request = with(Request.Builder()) {
when (method) {
"GET" -> {}
@ -71,16 +78,28 @@ internal class RemoteNodeClient(
url(uri.toString())
build()
}
val response = httpClient.newCall(request).execute()
return if (response.isSuccessful) {
val response = runBlocking(requestsScope.coroutineContext) {
val call = httpClient.newCall(request)
try {
call.await()
} catch (ioe: IOException) {
if (!call.isCanceled) {
throw ioe
}
null
}
}
return if (response == null) {
HttpResponse(code = 499)
} else if (response.isSuccessful) {
val responseBody = requireNotNull(response.body())
val pipe = ParcelFileDescriptor.createPipe()
scope.launch(ioDispatcher) {
pipe[1].use { readFd ->
FileOutputStream(readFd.fileDescriptor).use { outputStream ->
responseBody.use {
it.byteStream().copyTo(outputStream)
}
requestsScope.launch {
pipe[1].use { writeSide ->
FileOutputStream(writeSide.fileDescriptor).use { outputStream ->
responseBody.byteStream().copyTo(outputStream)
}
}
}
@ -90,13 +109,28 @@ internal class RemoteNodeClient(
body = pipe[0],
)
} else {
HttpResponse(code = response.code()).also {
response.close()
}
HttpResponse(code = response.code())
}
}
private fun String.parseHttpHeader(): Headers =
@OptIn(ExperimentalCoroutinesApi::class)
private suspend fun Call.await() = suspendCancellableCoroutine { continuation ->
enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
continuation.resume(response) {
response.close()
}
}
override fun onFailure(call: Call, e: IOException) {
continuation.resumeWithException(e)
}
})
continuation.invokeOnCancellation { cancel() }
}
fun String.parseHttpHeader(): Headers =
with(Headers.Builder()) {
splitToSequence("\r\n")
.filter { line -> line.isNotEmpty() }

View File

@ -39,13 +39,11 @@ class WalletClient private constructor(
network: MoneroNetwork,
nodeSelector: RemoteNodeSelector? = null,
httpClient: OkHttpClient? = null,
coroutineContext: CoroutineContext = Dispatchers.Default + SupervisorJob(),
ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
): WalletClient {
val scope = CoroutineScope(coroutineContext)
val remoteNodeClient = nodeSelector?.let {
requireNotNull(httpClient)
RemoteNodeClient(it, httpClient, scope, ioDispatcher)
RemoteNodeClient(it, httpClient, ioDispatcher)
}
val (serviceConnection, service) = bindService(context)
return WalletClient(context, service, serviceConnection, network, remoteNodeClient)