Skip to content

Commit 2bfed9f

Browse files
committed
Merge branch 'fix/1.4.x'
2 parents 15688f7 + 9e58022 commit 2bfed9f

File tree

7 files changed

+72
-24
lines changed

7 files changed

+72
-24
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
*.log
2+
/Releases
3+
/archived
4+
15
### Gradle ###
26
.gradle
37
build/

README.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ moment.
2222

2323
- **macOS** - Stable since v1.2.0
2424
- **Windows** - Stable since v1.4.1
25-
- **Linux** - Beta. Known issue: horizontal scrolling is not working
25+
- **Linux** - Stable since v1.4.3
2626

2727
## Features
2828

@@ -152,7 +152,6 @@ express your desired priorities in the issue tracker.
152152
- Example-level variables
153153
- Checking for version updates
154154
- Establish release builds (minified and without debug logs and symbols)
155-
- Streamlining building executables for multiple OS
156155

157156
## Development
158157

src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/manager/NetworkClientManager.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ class NetworkClientManager : CallDataStore {
210210
val oldCallId = requestExampleToCallMapping.put(requestExampleId, callData.id)
211211
if (oldCallId != null) {
212212
CoroutineScope(Dispatchers.IO).launch {
213-
callDataMap[oldCallId]?.cancel?.invoke()
213+
callDataMap[oldCallId]?.cancel?.invoke(null)
214214
callDataMap.remove(oldCallId)
215215
}
216216
}
@@ -231,7 +231,7 @@ class NetworkClientManager : CallDataStore {
231231
}
232232

233233
fun cancel(selectedRequestExampleId: String) {
234-
getCallDataByRequestExampleId(selectedRequestExampleId)?.let { it.cancel() }
234+
getCallDataByRequestExampleId(selectedRequestExampleId)?.let { it.cancel(null) }
235235
}
236236

237237
private fun <T> emptySharedFlow() = emptyFlow<T>().shareIn(CoroutineScope(Dispatchers.IO), SharingStarted.Eagerly)

src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/AbstractTransportClient.kt

+17-11
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import com.sunnychung.application.multiplatform.hellohttp.network.util.TrustAllS
1111
import com.sunnychung.application.multiplatform.hellohttp.util.log
1212
import com.sunnychung.application.multiplatform.hellohttp.util.uuidString
1313
import com.sunnychung.lib.multiplatform.kdatetime.KInstant
14+
import kotlinx.coroutines.CoroutineExceptionHandler
15+
import kotlinx.coroutines.CoroutineName
1416
import kotlinx.coroutines.CoroutineScope
1517
import kotlinx.coroutines.Dispatchers
1618
import kotlinx.coroutines.delay
@@ -68,6 +70,10 @@ abstract class AbstractTransportClient internal constructor(callDataStore: CallD
6870
}
6971
}
7072

73+
protected fun coroutineExceptionHandler() = CoroutineExceptionHandler { context, ex ->
74+
log.w(ex) { "Uncaught exception in coroutine ${context[CoroutineName.Key] ?: "-"}" }
75+
}
76+
7177
internal fun createSslContext(sslConfig: SslConfig): CustomSsl {
7278
return SSLContext.getInstance("TLS")
7379
.run {
@@ -97,17 +103,17 @@ abstract class AbstractTransportClient internal constructor(callDataStore: CallD
97103
val cert = CertificateFactory.getInstance("X.509").generateCertificate(it.certificate.content.inputStream())
98104
val key = KeyFactory.getInstance("RSA").generatePrivate(PKCS8EncodedKeySpec(it.privateKey.content))
99105

100-
val password = uuidString()
101-
val keyStore = KeyStore.getInstance("JKS")
102-
keyStore.load(null)
103-
keyStore.setCertificateEntry("cert", cert)
104-
keyStore.setKeyEntry("key", key, password.toCharArray(), arrayOf(cert))
105-
val keyManagers = KeyManagerFactory.getInstance("SunX509")
106-
.apply { init(keyStore, password.toCharArray()) }
107-
.keyManagers
108-
keyManagers.first()
109-
}
110-
init(keyManager?.let { arrayOf(it) }, trustManager?.let { arrayOf(it) }, SecureRandom())
106+
val password = uuidString()
107+
val keyStore = KeyStore.getInstance("JKS")
108+
keyStore.load(null)
109+
keyStore.setCertificateEntry("cert", cert)
110+
keyStore.setKeyEntry("key", key, password.toCharArray(), arrayOf(cert))
111+
val keyManagers = KeyManagerFactory.getInstance("SunX509")
112+
.apply { init(keyStore, password.toCharArray()) }
113+
.keyManagers
114+
keyManagers.first()
115+
}
116+
init(keyManager?.let { arrayOf(it) }, trustManager?.let { arrayOf(it) }, SecureRandom())
111117
CustomSsl(sslContext = this, keyManager = keyManager, trustManager = trustManager)
112118
}
113119
}

src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/ApacheHttpTransportClient.kt

+41-2
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,19 @@ import com.sunnychung.application.multiplatform.hellohttp.network.apache.Http2Fr
1313
import com.sunnychung.application.multiplatform.hellohttp.network.util.CallDataUserResponseUtil
1414
import com.sunnychung.application.multiplatform.hellohttp.util.log
1515
import com.sunnychung.lib.multiplatform.kdatetime.KInstant
16+
import com.sunnychung.lib.multiplatform.kdatetime.extension.seconds
17+
import kotlinx.coroutines.CancellationException
1618
import kotlinx.coroutines.CoroutineScope
1719
import kotlinx.coroutines.Dispatchers
1820
import kotlinx.coroutines.ExperimentalCoroutinesApi
21+
import kotlinx.coroutines.cancel
22+
import kotlinx.coroutines.delay
1923
import kotlinx.coroutines.flow.MutableSharedFlow
2024
import kotlinx.coroutines.launch
2125
import kotlinx.coroutines.runBlocking
2226
import kotlinx.coroutines.suspendCancellableCoroutine
27+
import kotlinx.coroutines.sync.Mutex
28+
import kotlinx.coroutines.sync.withLock
2329
import org.apache.hc.client5.http.SystemDefaultDnsResolver
2430
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse
2531
import org.apache.hc.client5.http.async.methods.SimpleResponseConsumer
@@ -44,6 +50,7 @@ import org.apache.hc.core5.http.nio.RequestChannel
4450
import org.apache.hc.core5.http.protocol.HttpContext
4551
import org.apache.hc.core5.http2.HttpVersionPolicy
4652
import org.apache.hc.core5.http2.config.H2Config
53+
import org.apache.hc.core5.http2.frame.FrameFlag
4754
import org.apache.hc.core5.http2.frame.FrameType
4855
import org.apache.hc.core5.http2.frame.RawFrame
4956
import org.apache.hc.core5.http2.hpack.HPackInspectHeader
@@ -54,8 +61,11 @@ import java.net.InetAddress
5461
import java.nio.ByteBuffer
5562
import java.security.Principal
5663
import java.security.cert.Certificate
64+
import java.util.Collections
5765
import java.util.concurrent.ConcurrentHashMap
66+
import java.util.concurrent.atomic.AtomicBoolean
5867
import java.util.concurrent.atomic.AtomicInteger
68+
import kotlin.coroutines.CoroutineContext
5969

6070
class ApacheHttpTransportClient(networkClientManager: NetworkClientManager) : AbstractTransportClient(networkClientManager) {
6171

@@ -172,6 +182,9 @@ class ApacheHttpTransportClient(networkClientManager: NetworkClientManager) : Ab
172182
},
173183
object : H2InspectListener {
174184
val suspendedHeaderFrames = ConcurrentHashMap<Int, H2HeaderFrame>()
185+
val openedStreamIds = Collections.newSetFromMap(ConcurrentHashMap<Int, Boolean>())
186+
val lock = Mutex()
187+
val isCancelled = AtomicBoolean(false)
175188

176189
override fun onHeaderInputDecoded(connection: HttpConnection, streamId: Int?, headers: MutableList<HPackInspectHeader>) {
177190
val serialized = http2FrameSerializer.serializeHeaders(headers)
@@ -211,6 +224,14 @@ class ApacheHttpTransportClient(networkClientManager: NetworkClientManager) : Ab
211224
val type = FrameType.valueOf(frame.type)
212225
log.d { "processFrame $streamId $type" }
213226
if (type == FrameType.HEADERS) {
227+
if (frame.flags and FrameFlag.END_STREAM.value == 0) {
228+
openedStreamIds += streamId
229+
} else {
230+
openedStreamIds -= streamId
231+
if (openedStreamIds.isEmpty()) {
232+
checkForHangConnectionLater()
233+
}
234+
}
214235
val frame = suspendedHeaderFrames.getOrPut(streamId) { H2HeaderFrame(streamId = streamId) }
215236
frame.frameHeader = serialized
216237
if (frame.isComplete()) {
@@ -232,6 +253,20 @@ class ApacheHttpTransportClient(networkClientManager: NetworkClientManager) : Ab
232253
}
233254
}
234255

256+
fun checkForHangConnectionLater() {
257+
CoroutineScope(Dispatchers.IO).launch {
258+
delay(3.seconds().toMilliseconds())
259+
lock.withLock {
260+
if (openedStreamIds.isEmpty() && !isCancelled.get()) {
261+
val message = "The connection has no active stream for some seconds, it appears to be hanging. Cancelling the connection."
262+
emitEvent(callId, message)
263+
callData.cancel(Exception(message))
264+
isCancelled.set(true)
265+
}
266+
}
267+
}
268+
}
269+
235270
}
236271
)
237272

@@ -304,7 +339,7 @@ class ApacheHttpTransportClient(networkClientManager: NetworkClientManager) : Ab
304339
}
305340
}
306341

307-
CoroutineScope(Dispatchers.IO).launch {
342+
CoroutineScope(Dispatchers.IO).launch(coroutineExceptionHandler()) {
308343
val callData = callData[callId]!!
309344
callData.waitForPreparation()
310345
log.d { "Call $callId is prepared" }
@@ -407,11 +442,15 @@ class ApacheHttpTransportClient(networkClientManager: NetworkClientManager) : Ab
407442

408443
})
409444

410-
data.cancel = {
445+
data.cancel = { error ->
411446
log.d { "Request to cancel the call" }
412447
val cancelResult = call.cancel() // no use at all
413448
log.d { "Cancel result = $cancelResult" }
414449
httpClient.close(CloseMode.IMMEDIATE)
450+
451+
// httpClient.close is buggy. Do not rely on it
452+
data.status = ConnectionStatus.DISCONNECTED
453+
this.cancel(error?.let { CancellationException(it.message, it) })
415454
}
416455
}
417456

src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/GrpcTransportClient.kt

+6-6
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,7 @@ class GrpcTransportClient(networkClientManager: NetworkClientManager) : Abstract
547547

548548
var (responseFlow, responseObserver) = flowAndStreamObserver<DynamicMessage>()
549549
try {
550-
val cancel = {
550+
val cancel = { _: Throwable? ->
551551
if (call.status.isConnectionActive()) {
552552
try {
553553
responseObserver.onCompleted()
@@ -584,7 +584,7 @@ class GrpcTransportClient(networkClientManager: NetworkClientManager) : Abstract
584584
log.d { "Response = ${responseJsonData.decodeToString()}" }
585585
} catch (e: Throwable) {
586586
setStreamError(e)
587-
call.cancel()
587+
call.cancel(e)
588588
}
589589

590590
if (postFlightAction != null) {
@@ -604,7 +604,7 @@ class GrpcTransportClient(networkClientManager: NetworkClientManager) : Abstract
604604
requestObserver.onNext(request)
605605
} catch (e: Throwable) {
606606
setStreamError(e)
607-
call.cancel()
607+
call.cancel(e)
608608
}
609609
}
610610
}
@@ -618,7 +618,7 @@ class GrpcTransportClient(networkClientManager: NetworkClientManager) : Abstract
618618
if (!hasInvokedCancelDueToError) {
619619
hasInvokedCancelDueToError = true
620620
setStreamError(e)
621-
call.cancel()
621+
call.cancel(e)
622622
}
623623
}
624624
}
@@ -627,12 +627,12 @@ class GrpcTransportClient(networkClientManager: NetworkClientManager) : Abstract
627627
fun initiateClientStreamableCall(requestObserver: StreamObserver<DynamicMessage>) {
628628
call.sendPayload = buildSendPayloadFunction(requestObserver)
629629
call.sendEndOfStream = buildSendEndOfStream(requestObserver)
630-
call.cancel = {
630+
call.cancel = { e ->
631631
if (call.status.isConnectionActive()) {
632632
try {
633633
call.sendEndOfStream()
634634
} catch (_: Throwable) {}
635-
cancel()
635+
cancel(e)
636636
}
637637
}
638638
// actually, at this stage it could be not yet connected

src/jvmMain/kotlin/com/sunnychung/application/multiplatform/hellohttp/network/TransportClient.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class CallData(
4343
val optionalResponseSize: AtomicInteger,
4444
val response: UserResponse,
4545

46-
var cancel: () -> Unit,
46+
var cancel: (Throwable?) -> Unit,
4747
var sendPayload: (String) -> Unit = {},
4848
var sendEndOfStream: () -> Unit = {},
4949
)

0 commit comments

Comments
 (0)