commit 4d90ca8d61974ff8cda5c829a9f0a5d25a98bbeb
parent 807eb3fa7eb4a0d555646865a370f5f66cbaa4fc
Author: Antoine A <>
Date: Wed, 6 Mar 2024 13:12:18 +0100
Supports graceful shutdown using coroutine cancelation
Diffstat:
7 files changed, 249 insertions(+), 249 deletions(-)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -45,7 +45,6 @@ import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.utils.io.*
-import kotlinx.coroutines.runBlocking
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import org.postgresql.util.PSQLState
@@ -262,23 +261,21 @@ class BankDbInit : CliktCommand("Initialize the libeufin-bank database", name =
val cfg = config.loadDbConfig()
val ctx = config.loadBankConfig()
Database(cfg.dbConnStr, ctx.regionalCurrency, ctx.fiatCurrency).use { db ->
- runBlocking {
- db.conn { conn ->
- if (requestReset) {
- resetDatabaseTables(conn, cfg, sqlFilePrefix = "libeufin-bank")
- }
- initializeDatabaseTables(conn, cfg, sqlFilePrefix = "libeufin-bank")
- }
- // Create admin account if missing
- val res = createAdminAccount(db, ctx) // logs provided by the helper
- when (res) {
- AccountCreationResult.BonusBalanceInsufficient -> {}
- AccountCreationResult.LoginReuse -> {}
- AccountCreationResult.PayToReuse ->
- throw Exception("Failed to create admin's account")
- AccountCreationResult.Success ->
- logger.info("Admin's account created")
+ db.conn { conn ->
+ if (requestReset) {
+ resetDatabaseTables(conn, cfg, sqlFilePrefix = "libeufin-bank")
}
+ initializeDatabaseTables(conn, cfg, sqlFilePrefix = "libeufin-bank")
+ }
+ // Create admin account if missing
+ val res = createAdminAccount(db, ctx) // logs provided by the helper
+ when (res) {
+ AccountCreationResult.BonusBalanceInsufficient -> {}
+ AccountCreationResult.LoginReuse -> {}
+ AccountCreationResult.PayToReuse ->
+ throw Exception("Failed to create admin's account")
+ AccountCreationResult.Success ->
+ logger.info("Admin's account created")
}
}
}
@@ -293,30 +290,28 @@ class ServeBank : CliktCommand("Run libeufin-bank HTTP server", name = "serve")
val dbCfg = cfg.loadDbConfig()
val serverCfg = cfg.loadServerConfig()
Database(dbCfg.dbConnStr, ctx.regionalCurrency, ctx.fiatCurrency).use { db ->
- runBlocking {
- if (ctx.allowConversion) {
- logger.info("Ensure exchange account exists")
- val info = db.account.bankInfo("exchange", ctx.payto)
- if (info == null) {
- throw Exception("Exchange account missing: an exchange account named 'exchange' is required for conversion to be enabled")
- } else if (!info.isTalerExchange) {
- throw Exception("Account is not an exchange: an exchange account named 'exchange' is required for conversion to be enabled")
- }
- logger.info("Ensure conversion is enabled")
- val sqlProcedures = Path("${dbCfg.sqlDir}/libeufin-conversion-setup.sql")
- if (!sqlProcedures.exists()) {
- throw Exception("Missing libeufin-conversion-setup.sql file")
- }
- db.conn { it.execSQLUpdate(sqlProcedures.readText()) }
- } else {
- logger.info("Ensure conversion is disabled")
- val sqlProcedures = Path("${dbCfg.sqlDir}/libeufin-conversion-drop.sql")
- if (!sqlProcedures.exists()) {
- throw Exception("Missing libeufin-conversion-drop.sql file")
- }
- db.conn { it.execSQLUpdate(sqlProcedures.readText()) }
- // Remove conversion info from the database ?
+ if (ctx.allowConversion) {
+ logger.info("Ensure exchange account exists")
+ val info = db.account.bankInfo("exchange", ctx.payto)
+ if (info == null) {
+ throw Exception("Exchange account missing: an exchange account named 'exchange' is required for conversion to be enabled")
+ } else if (!info.isTalerExchange) {
+ throw Exception("Account is not an exchange: an exchange account named 'exchange' is required for conversion to be enabled")
+ }
+ logger.info("Ensure conversion is enabled")
+ val sqlProcedures = Path("${dbCfg.sqlDir}/libeufin-conversion-setup.sql")
+ if (!sqlProcedures.exists()) {
+ throw Exception("Missing libeufin-conversion-setup.sql file")
}
+ db.conn { it.execSQLUpdate(sqlProcedures.readText()) }
+ } else {
+ logger.info("Ensure conversion is disabled")
+ val sqlProcedures = Path("${dbCfg.sqlDir}/libeufin-conversion-drop.sql")
+ if (!sqlProcedures.exists()) {
+ throw Exception("Missing libeufin-conversion-drop.sql file")
+ }
+ db.conn { it.execSQLUpdate(sqlProcedures.readText()) }
+ // Remove conversion info from the database ?
}
val env = applicationEngineEnvironment {
@@ -354,16 +349,14 @@ class ChangePw : CliktCommand("Change account password", name = "passwd") {
val ctx = cfg.loadBankConfig()
val dbCfg = cfg.loadDbConfig()
Database(dbCfg.dbConnStr, ctx.regionalCurrency, ctx.fiatCurrency).use { db ->
- runBlocking {
- val res = db.account.reconfigPassword(username, password, null, true)
- when (res) {
- AccountPatchAuthResult.UnknownAccount ->
- throw Exception("Password change for '$username' account failed: unknown account")
- AccountPatchAuthResult.OldPasswordMismatch,
- AccountPatchAuthResult.TanRequired -> { /* Can never happen */ }
- AccountPatchAuthResult.Success ->
- logger.info("Password change for '$username' account succeeded")
- }
+ val res = db.account.reconfigPassword(username, password, null, true)
+ when (res) {
+ AccountPatchAuthResult.UnknownAccount ->
+ throw Exception("Password change for '$username' account failed: unknown account")
+ AccountPatchAuthResult.OldPasswordMismatch,
+ AccountPatchAuthResult.TanRequired -> { /* Can never happen */ }
+ AccountPatchAuthResult.Success ->
+ logger.info("Password change for '$username' account succeeded")
}
}
}
@@ -400,33 +393,31 @@ class EditAccount : CliktCommand(
val ctx = cfg.loadBankConfig()
val dbCfg = cfg.loadDbConfig()
Database(dbCfg.dbConnStr, ctx.regionalCurrency, ctx.fiatCurrency).use { db ->
- runBlocking {
- val req = AccountReconfiguration(
- name = name,
- is_taler_exchange = exchange,
- is_public = is_public,
- contact_data = ChallengeContactData(
- // PATCH semantic, if not given do not change, if empty remove
- email = if (email == null) Option.None else Option.Some(if (email != "") email else null),
- phone = if (phone == null) Option.None else Option.Some(if (phone != "") phone else null),
- ),
- cashout_payto_uri = Option.Some(cashout_payto_uri),
- debit_threshold = debit_threshold
- )
- when (patchAccount(db, ctx, req, username, true, true)) {
- AccountPatchResult.Success ->
- logger.info("Account '$username' edited")
- AccountPatchResult.UnknownAccount ->
- throw Exception("Account '$username' not found")
- AccountPatchResult.MissingTanInfo ->
- throw Exception("missing info for tan channel ${req.tan_channel.get()}")
- AccountPatchResult.NonAdminName,
- AccountPatchResult.NonAdminCashout,
- AccountPatchResult.NonAdminDebtLimit,
- is AccountPatchResult.TanRequired -> {
- // Unreachable as we edit account as admin
- }
- }
+ val req = AccountReconfiguration(
+ name = name,
+ is_taler_exchange = exchange,
+ is_public = is_public,
+ contact_data = ChallengeContactData(
+ // PATCH semantic, if not given do not change, if empty remove
+ email = if (email == null) Option.None else Option.Some(if (email != "") email else null),
+ phone = if (phone == null) Option.None else Option.Some(if (phone != "") phone else null),
+ ),
+ cashout_payto_uri = Option.Some(cashout_payto_uri),
+ debit_threshold = debit_threshold
+ )
+ when (patchAccount(db, ctx, req, username, true, true)) {
+ AccountPatchResult.Success ->
+ logger.info("Account '$username' edited")
+ AccountPatchResult.UnknownAccount ->
+ throw Exception("Account '$username' not found")
+ AccountPatchResult.MissingTanInfo ->
+ throw Exception("missing info for tan channel ${req.tan_channel.get()}")
+ AccountPatchResult.NonAdminName,
+ AccountPatchResult.NonAdminCashout,
+ AccountPatchResult.NonAdminDebtLimit,
+ is AccountPatchResult.TanRequired -> {
+ // Unreachable as we edit account as admin
+ }
}
}
}
@@ -479,37 +470,35 @@ class CreateAccount : CliktCommand(
val dbCfg = cfg.loadDbConfig()
Database(dbCfg.dbConnStr, ctx.regionalCurrency, ctx.fiatCurrency).use { db ->
- runBlocking {
- val req = json ?: options?.run {
- RegisterAccountRequest(
- username = username,
- password = password,
- name = name,
- is_public = is_public,
- is_taler_exchange = exchange,
- contact_data = ChallengeContactData(
- email = Option.Some(email),
- phone = Option.Some(phone),
- ),
- cashout_payto_uri = cashout_payto_uri,
- payto_uri = payto_uri,
- debit_threshold = debit_threshold
- )
- }
- req?.let {
- val (result, internalPayto) = createAccount(db, ctx, req, true)
- when (result) {
- AccountCreationResult.BonusBalanceInsufficient ->
- throw Exception("Insufficient admin funds to grant bonus")
- AccountCreationResult.LoginReuse ->
- throw Exception("Account username reuse '${req.username}'")
- AccountCreationResult.PayToReuse ->
- throw Exception("Bank internalPayToUri reuse '$internalPayto'")
- AccountCreationResult.Success ->
- logger.info("Account '${req.username}' created")
- }
- println(internalPayto)
+ val req = json ?: options?.run {
+ RegisterAccountRequest(
+ username = username,
+ password = password,
+ name = name,
+ is_public = is_public,
+ is_taler_exchange = exchange,
+ contact_data = ChallengeContactData(
+ email = Option.Some(email),
+ phone = Option.Some(phone),
+ ),
+ cashout_payto_uri = cashout_payto_uri,
+ payto_uri = payto_uri,
+ debit_threshold = debit_threshold
+ )
+ }
+ req?.let {
+ val (result, internalPayto) = createAccount(db, ctx, req, true)
+ when (result) {
+ AccountCreationResult.BonusBalanceInsufficient ->
+ throw Exception("Insufficient admin funds to grant bonus")
+ AccountCreationResult.LoginReuse ->
+ throw Exception("Account username reuse '${req.username}'")
+ AccountCreationResult.PayToReuse ->
+ throw Exception("Bank internalPayToUri reuse '$internalPayto'")
+ AccountCreationResult.Success ->
+ logger.info("Account '${req.username}' created")
}
+ println(internalPayto)
}
}
}
diff --git a/common/src/main/kotlin/Cli.kt b/common/src/main/kotlin/Cli.kt
@@ -33,6 +33,7 @@ import com.github.ajalt.clikt.parameters.types.path
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.event.Level
+import kotlinx.coroutines.*
private val logger: Logger = LoggerFactory.getLogger("libeufin-config")
@@ -48,13 +49,22 @@ fun Throwable.fmtLog(logger: Logger) {
logger.debug("{}", this)
}
-fun cliCmd(logger: Logger, level: Level, lambda: () -> Unit) {
+fun cliCmd(logger: Logger, level: Level, lambda: suspend () -> Unit) {
// Set root log level
val root = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) as ch.qos.logback.classic.Logger
root.level = ch.qos.logback.classic.Level.convertAnSLF4JLevel(level)
// Run cli command catching all errors
try {
- lambda()
+ runBlocking {
+ val job = launch {
+ lambda()
+ }
+ Runtime.getRuntime().addShutdownHook(object : Thread() {
+ override fun run() = runBlocking{
+ job.cancelAndJoin()
+ }
+ })
+ }
} catch (e: Throwable) {
e.fmtLog(logger)
throw ProgramResult(1)
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
@@ -444,10 +444,8 @@ class EbicsFetch: CliktCommand("Fetches EBICS files") {
LocalDate.parse(pinnedStartVal).atStartOfDay(ZoneId.of("UTC")).toInstant()
} else null
ctx.pinnedStart = pinnedStartArg
- runBlocking {
- if (!fetchDocuments(db, ctx, docs)) {
- throw Exception("Failed to fetch documents")
- }
+ if (!fetchDocuments(db, ctx, docs)) {
+ throw Exception("Failed to fetch documents")
}
} else {
val configValue = cfg.config.requireString("nexus-fetch", "frequency")
@@ -460,12 +458,10 @@ class EbicsFetch: CliktCommand("Fetches EBICS files") {
} else {
cfgFrequency
}
- runBlocking {
- do {
- fetchDocuments(db, ctx, docs)
- delay(((frequency?.inSeconds ?: 0) * 1000).toLong())
- } while (frequency != null)
- }
+ do {
+ fetchDocuments(db, ctx, docs)
+ delay(((frequency?.inSeconds ?: 0) * 1000).toLong())
+ } while (frequency != null)
}
}
}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt
@@ -23,7 +23,6 @@ import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.groups.*
import com.github.ajalt.clikt.parameters.options.*
import io.ktor.client.*
-import kotlinx.coroutines.runBlocking
import tech.libeufin.common.*
import tech.libeufin.common.crypto.*
import tech.libeufin.ebics.*
@@ -234,29 +233,26 @@ class EbicsSetup: CliktCommand("Set up the EBICS subscriber") {
val clientKeys = loadOrGenerateClientKeys(cfg.clientPrivateKeysFilename)
val httpClient = HttpClient()
// Privs exist. Upload their pubs
- runBlocking {
- val keysNotSub = !clientKeys.submitted_ini
- if ((!clientKeys.submitted_ini) || forceKeysResubmission)
- doKeysRequestAndUpdateState(cfg, clientKeys, httpClient, KeysOrderType.INI)
- // Eject PDF if the keys were submitted for the first time, or the user asked.
- if (keysNotSub || generateRegistrationPdf) makePdf(clientKeys, cfg)
- if ((!clientKeys.submitted_hia) || forceKeysResubmission)
- doKeysRequestAndUpdateState(cfg, clientKeys, httpClient, KeysOrderType.HIA)
- }
+ val keysNotSub = !clientKeys.submitted_ini
+ if ((!clientKeys.submitted_ini) || forceKeysResubmission)
+ doKeysRequestAndUpdateState(cfg, clientKeys, httpClient, KeysOrderType.INI)
+ // Eject PDF if the keys were submitted for the first time, or the user asked.
+ if (keysNotSub || generateRegistrationPdf) makePdf(clientKeys, cfg)
+ if ((!clientKeys.submitted_hia) || forceKeysResubmission)
+ doKeysRequestAndUpdateState(cfg, clientKeys, httpClient, KeysOrderType.HIA)
+
// Checking if the bank keys exist on disk.
var bankKeys = loadBankKeys(cfg.bankPublicKeysFilename)
if (bankKeys == null) {
- runBlocking {
- try {
- doKeysRequestAndUpdateState(
- cfg,
- clientKeys,
- httpClient,
- KeysOrderType.HPB
- )
- } catch (e: Exception) {
- throw Exception("Could not download bank keys. Send client keys (and/or related PDF document with --generate-registration-pdf) to the bank", e)
- }
+ try {
+ doKeysRequestAndUpdateState(
+ cfg,
+ clientKeys,
+ httpClient,
+ KeysOrderType.HPB
+ )
+ } catch (e: Exception) {
+ throw Exception("Could not download bank keys. Send client keys (and/or related PDF document with --generate-registration-pdf) to the bank", e)
}
logger.info("Bank keys stored at ${cfg.bankPublicKeysFilename}")
bankKeys = loadBankKeys(cfg.bankPublicKeysFilename)!!
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt
@@ -242,14 +242,12 @@ class EbicsSubmit : CliktCommand("Submits any initiated payment found in the dat
frequency
}
}
- runBlocking {
- do {
- // TODO error handling
- submitBatch(ctx, db)
- // TODO take submitBatch taken time in the delay
- delay(((frequency?.inSeconds ?: 0) * 1000).toLong())
- } while (frequency != null)
- }
+ do {
+ // TODO error handling
+ submitBatch(ctx, db)
+ // TODO take submitBatch taken time in the delay
+ delay(((frequency?.inSeconds ?: 0) * 1000).toLong())
+ } while (frequency != null)
}
}
}
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
@@ -251,18 +251,16 @@ class InitiatePayment: CliktCommand("Initiate an outgoing payment") {
}
Database(dbCfg.dbConnStr).use { db ->
- runBlocking {
- db.initiatedPaymentCreate(
- InitiatedPayment(
- id = -1,
- amount = amount,
- wireTransferSubject = subject,
- creditPaytoUri = payto.toString(),
- initiationTime = Instant.now(),
- requestUid = requestUid
- )
+ db.initiatedPaymentCreate(
+ InitiatedPayment(
+ id = -1,
+ amount = amount,
+ wireTransferSubject = subject,
+ creditPaytoUri = payto.toString(),
+ initiationTime = Instant.now(),
+ requestUid = requestUid
)
- }
+ )
}
}
}
@@ -299,17 +297,15 @@ class FakeIncoming: CliktCommand("Genere a fake incoming payment") {
}
Database(dbCfg.dbConnStr).use { db ->
- runBlocking {
- ingestIncomingPayment(db,
- IncomingPayment(
- amount = amount,
- debitPaytoUri = payto.toString(),
- wireTransferSubject = subject,
- executionTime = Instant.now(),
- bankId = bankId
- )
+ ingestIncomingPayment(db,
+ IncomingPayment(
+ amount = amount,
+ debitPaytoUri = payto.toString(),
+ wireTransferSubject = subject,
+ executionTime = Instant.now(),
+ bankId = bankId
)
- }
+ )
}
}
}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt
@@ -55,6 +55,7 @@ import java.security.interfaces.RSAPrivateCrtKey
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.*
+import kotlinx.coroutines.*
/**
* Available EBICS versions.
@@ -285,89 +286,103 @@ suspend fun ebicsDownload(
reqXml: ByteArray,
isEbics3: Boolean,
processing: (InputStream) -> Unit
-) {
- val initResp = postEbics(client, cfg, bankKeys, reqXml, isEbics3)
- logger.debug("Download init phase done. EBICS- and bank-technical codes are: ${initResp.technicalReturnCode}, ${initResp.bankReturnCode}")
- if (initResp.technicalReturnCode != EbicsReturnCode.EBICS_OK) {
- throw Exception("Download init phase has EBICS-technical error: ${initResp.technicalReturnCode}")
- }
- if (initResp.bankReturnCode == EbicsReturnCode.EBICS_NO_DOWNLOAD_DATA_AVAILABLE) {
- logger.debug("Download content is empty")
- return
- }
- if (initResp.bankReturnCode != EbicsReturnCode.EBICS_OK) {
- throw Exception("Download init phase has bank-technical error: ${initResp.bankReturnCode}")
- }
- val tId = initResp.transactionID
- ?: throw EbicsSideException(
- "EBICS download init phase did not return a transaction ID, cannot do the transfer phase.",
- sideEc = EbicsSideError.EBICS_UPLOAD_TRANSACTION_ID_MISSING
- )
- logger.debug("EBICS download transaction passed the init phase, got ID: $tId")
- val howManySegments = initResp.numSegments
- if (howManySegments == null) {
- throw Exception("Init response lacks the quantity of segments, failing.")
- }
- val ebicsChunks = mutableListOf<String>()
- // Getting the chunk(s)
- val firstDataChunk = initResp.orderDataEncChunk
- ?: throw EbicsSideException(
- "OrderData element not found, despite non empty payload, failing.",
- sideEc = EbicsSideError.ORDER_DATA_ELEMENT_NOT_FOUND
- )
- val dataEncryptionInfo = initResp.dataEncryptionInfo ?: run {
- throw EbicsSideException(
- "EncryptionInfo element not found, despite non empty payload, failing.",
- sideEc = EbicsSideError.ENCRYPTION_INFO_ELEMENT_NOT_FOUND
- )
- }
- ebicsChunks.add(firstDataChunk)
- // proceed with the transfer phase.
- for (x in 2 .. howManySegments) {
- // request segment number x.
- val transReq = if (isEbics3)
- createEbics3DownloadTransferPhase(cfg, clientKeys, x, howManySegments, tId)
- else createEbics25DownloadTransferPhase(cfg, clientKeys, x, howManySegments, tId)
-
- val transResp = postEbics(client, cfg, bankKeys, transReq, isEbics3)
- if (!areCodesOk(transResp)) {
+) = coroutineScope {
+ val scope = this
+ // We need to run the logic in a non-cancelable context because we need to send
+ // a receipt for each open download transaction, otherwise we'll be stuck in an
+ // error loop until the pending transaction timeout.
+ // TODO find a way to cancel the pending transaction ?
+ withContext(NonCancellable) {
+ val initResp = postEbics(client, cfg, bankKeys, reqXml, isEbics3)
+ logger.debug("Download init phase done. EBICS- and bank-technical codes are: ${initResp.technicalReturnCode}, ${initResp.bankReturnCode}")
+ if (initResp.technicalReturnCode != EbicsReturnCode.EBICS_OK) {
+ throw Exception("Download init phase has EBICS-technical error: ${initResp.technicalReturnCode}")
+ }
+ if (initResp.bankReturnCode == EbicsReturnCode.EBICS_NO_DOWNLOAD_DATA_AVAILABLE) {
+ logger.debug("Download content is empty")
+ return@withContext
+ } else if (initResp.bankReturnCode != EbicsReturnCode.EBICS_OK) {
+ throw Exception("Download init phase has bank-technical error: ${initResp.bankReturnCode}")
+ }
+ val tId = initResp.transactionID
+ ?: throw EbicsSideException(
+ "EBICS download init phase did not return a transaction ID, cannot do the transfer phase.",
+ sideEc = EbicsSideError.EBICS_UPLOAD_TRANSACTION_ID_MISSING
+ )
+ logger.debug("EBICS download transaction passed the init phase, got ID: $tId")
+ val howManySegments = initResp.numSegments
+ if (howManySegments == null) {
+ throw Exception("Init response lacks the quantity of segments, failing.")
+ }
+ val ebicsChunks = mutableListOf<String>()
+ // Getting the chunk(s)
+ val firstDataChunk = initResp.orderDataEncChunk
+ ?: throw EbicsSideException(
+ "OrderData element not found, despite non empty payload, failing.",
+ sideEc = EbicsSideError.ORDER_DATA_ELEMENT_NOT_FOUND
+ )
+ val dataEncryptionInfo = initResp.dataEncryptionInfo ?: run {
throw EbicsSideException(
- "EBICS transfer segment #$x failed.",
- sideEc = EbicsSideError.TRANSFER_SEGMENT_FAILED
+ "EncryptionInfo element not found, despite non empty payload, failing.",
+ sideEc = EbicsSideError.ENCRYPTION_INFO_ELEMENT_NOT_FOUND
)
}
- val chunk = transResp.orderDataEncChunk
- if (chunk == null) {
- throw Exception("EBICS transfer phase lacks chunk #$x, failing.")
+ ebicsChunks.add(firstDataChunk)
+ // proceed with the transfer phase.
+ for (x in 2 .. howManySegments) {
+ if (!scope.isActive) break
+ // request segment number x.
+ val transReq = if (isEbics3)
+ createEbics3DownloadTransferPhase(cfg, clientKeys, x, howManySegments, tId)
+ else createEbics25DownloadTransferPhase(cfg, clientKeys, x, howManySegments, tId)
+
+ val transResp = postEbics(client, cfg, bankKeys, transReq, isEbics3)
+ if (!areCodesOk(transResp)) {
+ throw EbicsSideException(
+ "EBICS transfer segment #$x failed.",
+ sideEc = EbicsSideError.TRANSFER_SEGMENT_FAILED
+ )
+ }
+ val chunk = transResp.orderDataEncChunk
+ if (chunk == null) {
+ throw Exception("EBICS transfer phase lacks chunk #$x, failing.")
+ }
+ ebicsChunks.add(chunk)
+ }
+ suspend fun receipt(success: Boolean) {
+ val receiptXml = if (isEbics3)
+ createEbics3DownloadReceiptPhase(cfg, clientKeys, tId, success)
+ else createEbics25DownloadReceiptPhase(cfg, clientKeys, tId, success)
+
+ // Sending the receipt to the bank.
+ postEbics(
+ client,
+ cfg,
+ bankKeys,
+ receiptXml,
+ isEbics3
+ )
+ }
+ if (scope.isActive) {
+ // all chunks gotten, shaping a meaningful response now.
+ val payloadBytes = decryptAndDecompressPayload(
+ clientKeys.encryption_private_key,
+ dataEncryptionInfo,
+ ebicsChunks
+ )
+ // Process payload
+ val res = runCatching {
+ processing(payloadBytes)
+ }
+ receipt(res.isSuccess)
+
+ res.getOrThrow()
+ } else {
+ receipt(false)
+ throw CancellationException()
}
- ebicsChunks.add(chunk)
- }
- // all chunks gotten, shaping a meaningful response now.
- val payloadBytes = decryptAndDecompressPayload(
- clientKeys.encryption_private_key,
- dataEncryptionInfo,
- ebicsChunks
- )
- // Process payload
- val res = runCatching {
- processing(payloadBytes)
}
- // payload reconstructed, receipt to the bank.
- val success = res.isSuccess
- val receiptXml = if (isEbics3)
- createEbics3DownloadReceiptPhase(cfg, clientKeys, tId, success)
- else createEbics25DownloadReceiptPhase(cfg, clientKeys, tId, success)
-
- // Sending the receipt to the bank.
- postEbics(
- client,
- cfg,
- bankKeys,
- receiptXml,
- isEbics3
- )
- // Receipt didn't throw, can now return the payload.
- return res.getOrThrow()
+ Unit
}
/**
@@ -509,7 +524,7 @@ suspend fun doEbicsUpload(
bankKeys: BankPublicKeysFile,
orderService: Ebics3Request.OrderDetails.Service,
payload: ByteArray,
-): EbicsResponseContent {
+): EbicsResponseContent = withContext(NonCancellable) {
// TODO use a lambda and pass the order detail there for atomicity ?
val preparedPayload = prepareUploadPayload(cfg, clientKeys, bankKeys, payload, isEbics3 = true)
val initXml = createEbics3RequestForUploadInitialization(
@@ -558,5 +573,5 @@ suspend fun doEbicsUpload(
bankErrorCode = initResp.bankReturnCode
)
// EBICS- and bank-technical codes were both EBICS_OK, success!
- return transferResp
+ transferResp
}
\ No newline at end of file