I am trying to make a connection with an FTP server (vsftpd with SSL/TLS configured including implicit passive mode) and list all files in the "." directory. I use Android Studio with Kotlin.
Whenever I invoke the enqueueTask(workManager)
and wait 10 seconds (initial delay of the worker), the FTP client connects to the vsftpd server succesfully but upon giving wrong user and pass credentials (which leads to the code throwing ConnectException
due to loggedIn
being false) the exception is not re-thrown immediately and only thrown after some minutes of waiting.
Why is this behaviour occuring and how to resolve it such that the exception is thrown as soon as it occurs so that it can be caught by the withContext(Dispatchers.IO)
for retry?
build.gradle
:
buildscript { ext.kotlin_version = "1.3.72" repositories { google() jcenter() } dependencies { classpath "com.android.tools.build:gradle:4.0.1" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" }}allprojects { repositories { google() jcenter() }}apply plugin: 'com.android.application'apply plugin: 'kotlin-android'apply plugin: 'kotlin-android-extensions'android { compileSdkVersion 28 buildToolsVersion '29.0.3' kotlinOptions { jvmTarget = "1.8" } defaultConfig { applicationId "com.app.backupapp" minSdkVersion 27 targetSdkVersion 28 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } }}dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'androidx.core:core-ktx:1.3.1' implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'commons-net:commons-net:3.6' // for ftp implementation 'androidx.work:work-runtime-ktx:2.5.0' // for workmanager testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'}
PeriodicWorker
class:
class PeriodicWorker( appContext: Context, private val workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) { companion object { private const val TAG: String = "PeriodicWorker" private const val TIME: Long = 3 private val UNIT_TIME: TimeUnit = TimeUnit.HOURS fun enqueueTask(workManager: WorkManager) { val periodicWork = PeriodicWorkRequestBuilder<PeriodicWorker>( TIME, UNIT_TIME ) .setInitialDelay(10, TimeUnit.SECONDS) // for testing purposes .setBackoffCriteria( BackoffPolicy.LINEAR, PeriodicWorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.SECONDS ) .build() workManager.enqueueUniquePeriodicWork("unique", ExistingPeriodicWorkPolicy.KEEP, periodicWork) } } private val ftpHandler: FtpHandler = FtpHandler() override suspend fun doWork(): Result { return withContext(Dispatchers.IO) { try { // Note: throw ConnectException --> when throw exception here, the exception is caught immediately by catch block val filesInFtpServer: Array<String>? = ftpHandler.listFileNamesFtpServer() // Note 2: Should throw exception but doesn't immediately do that Result.success() } catch (e: Exception) { Log.e(TAG, "doWork: ${e.message}, ${e.stackTrace.joinToString { ", " }}") Result.retry() } } }}
ftpHandler
class:
class FtpHandler { companion object { private const val DATA_TIMEOUT_MS = 1000 * 5 private const val CONNECTION_TIMEOUT_MS = 1000 * 5 private const val TAG = "FtpHandler" } private val client: FTPSClient = FTPSClient("TLS", true) private val ftpHost = "10.0.2.2" private val ftpPort = 21 private val ftpUsername = "wrong_user" private val ftpPassword = "wrong_pass" init { client.setDataTimeout(DATA_TIMEOUT_MS) client.connectTimeout = CONNECTION_TIMEOUT_MS } private fun login(): Boolean { // Establish a connection with the FTP Server client.connect(ftpHost, ftpPort) client.execPBSZ(0) client.execPROT("P") client.enterLocalPassiveMode() // Login with given username and pass return client.login(ftpUsername, ftpPassword) } fun listFileNamesFtpServer(directory: String = "."): Array<String>? { var res: Array<String>? = null try { val loggedIn: Boolean = login() if (loggedIn) { if (FTPReply.isPositiveCompletion(client.replyCode)) { client.changeWorkingDirectory(directory) res = client.listNames() } else { throw ConnectException() } } else { throw ConnectException() } } catch (e: Exception) { throw e } finally { try { client.logout() client.disconnect() } catch (e: IOException) { throw e } } return res }}
I suspect it has to do with the timeouts of the FTP socket that might be interleaved at some points of the execution of login
function. I tried using another test function that does dummy http request and waits 3000 milliseconds until it throws an Exception and even that immediately triggered exception and caught in the context Dispatchers.IO.