In the tutorial, they teach how to support real-time p2p command-line messaging using websockets by implementing both client and server. I'm trying to finish an exercise where I have the client input messages via a javafx gui and receive messages inside the gui in the form of a chat log (basically a chat room)
I'm having trouble simply starting up the gui and the websocket together. I tried GlobalScope.launch in hopes that both would get run, but only the GUI gets launched. If I use runBlocking instead, only the websocket is active.
Here's what I have so far.
Other issues:
Don't know how to reference the javafx label variable inside the outputMessages function, so that we can update the chatlog. I try placing the label variable in the global scope, but it only results in a compile error, so I put it back inside SAKApplication.
How to update the label field to move to the next line (tried adding "/n" but it literally added "\n")
import java.util.Queue
import java.util.LinkedList
//var a = Label("s")
val messagesToSend: Queue<String> = LinkedList<String>()
class SAKApplication : Application() {
val l = Label("no text")
override fun start(primaryStage: Stage) {
val btn = Button()
btn.text = "Say 'Hello World'"
btn.onAction = EventHandler<ActionEvent> { println("Hello World!") }
val root = StackPane()
root.children.add(btn)
val textField = TextField()
// a = l
// action event
val event: EventHandler<ActionEvent> =
EventHandler {
l.text += "/n" + textField.getText()
messagesToSend.add(textField.getText())
}
// when enter is pressed
textField.setOnAction(event)
// add textfield
root.children.add(textField)
root.children.add(l)
val scene = Scene(root, 300.0, 250.0)
if (primaryStage != null) {
primaryStage.title = "Hello World!"
primaryStage.scene = scene
primaryStage.show()
}
val client = HttpClient {
install(WebSockets)
}
GlobalScope.launch {
client.webSocket(method = HttpMethod.Get, host = "127.0.0.1", port = 8080, path = "/chat") {
while(true) {
val messageOutputRoutine = launch { outputMessages() }
val userInputRoutine = launch { inputMessages() }
userInputRoutine.join() // Wait for completion; either "exit" or error
messageOutputRoutine.cancelAndJoin()
}
}
}
client.close()
}
}
fun main(args: Array<String>) {
Application.launch(SAKApplication::class.java, *args)
}
suspend fun DefaultClientWebSocketSession.outputMessages() {
try {
for (message in incoming) {
message as? Frame.Text ?: continue
// a.text += "/n" + message.readText()
println(message.readText())
}
} catch (e: Exception) {
println("Error while receiving: " + e.localizedMessage)
}
}
suspend fun DefaultClientWebSocketSession.inputMessages() {
val name = readLine() ?: ""
send(name)
while (true) {
sleep(1)
if (messagesToSend.isEmpty()) { continue }
val message = messagesToSend.remove()
if (message.equals("exit", true)) return
try {
send(message)
} catch (e: Exception) {
println("Error while sending: " + e.localizedMessage)
return
}
}
}
Related
In the App, I click "Start" button to display new information every 100ms, then click "Stop" button to stop display new information. it's work as my expection.
But If I click "Start" button for two times, then I click "Stop" button to hope to stop display new information, but the new information keep displaying, why? It seems that myJob.cancel() doesn't work.
class HandleMeter: ViewModel() {
var myInfo = mutableStateOf("Hello")
private lateinit var myJob: Job
private fun soundDbFlow(period: Long = 100) = flow {
while (true) {
emit(soundDb())
delay(period)
}
}
fun calCurrentAsynNew() {
myJob = viewModelScope.launch(Dispatchers.IO) {
soundDbFlow().collect { myInfo.value = it.toString() + " OK Asyn " + a++.toString() }
}
}
fun cancelJob(){
myJob.cancel()
}
...
}
#Composable
fun Greeting(handleMeter: HandleMeter) {
var info = handleMeter.myInfo
Column(
modifier = Modifier.fillMaxSize()
) {
Text(text = "Hello ${info.value}")
Button(
onClick = { handleMeter.calCurrentAsynNew() }
) {
Text("Start")
}
Button(
onClick = { handleMeter.cancelJob() }
) {
Text("Stop")
}
}
}
When you call handleMeter.calCurrentAsynNew() it starts a new coroutine and assigns the returned Job to myJob. When you click Start second time, it again starts a new coroutine and updates the value of myJob to this new Job instance but this doesn't cancel the previous coroutine, it's still running.
If you want to cancel previous coroutine when calCurrentAsynNew() is called, you will have to manually cancel it using myJob.cancel()
fun calCurrentAsynNew() {
if(::myJob.isInitialized)
myJob.cancel()
myJob = viewModelScope.launch {
soundDbFlow().collect { myInfo.value = it.toString() + " OK Asyn " + a++.toString() }
}
}
Instead of using lateinit var myJob: Job you could have also used var myJob: Job? = null and then to cancel it, use myJob?.cancel()
Dispatchers.IO is also not required in this case.
I am trying to create a polling mechanism with kotlin coroutines using sharedFlow and want to stop when there are no subscribers and active when there is at least one subscriber. My question is, is sharedFlow the right choice in this scenario or should I use channel. I tried using channelFlow but I am unaware how to close the channel (not cancel the job) outside the block body. Can someone help? Here's the snippet.
fun poll(id: String) = channelFlow {
while (!isClosedForSend) {
try {
send(repository.getDetails(id))
delay(MIN_REFRESH_TIME_MS)
} catch (throwable: Throwable) {
Timber.e("error -> ${throwable.message}")
}
invokeOnClose { Timber.e("channel flow closed.") }
}
}
You can use SharedFlow which emits values in a broadcast fashion (won't emit new value until the previous one is consumed by all the collectors).
val sharedFlow = MutableSharedFlow<String>()
val scope = CoroutineScope(Job() + Dispatchers.IO)
var producer: Job()
scope.launch {
val producer = launch() {
sharedFlow.emit(...)
}
sharedFlow.subscriptionCount
.map {count -> count > 0}
.distinctUntilChanged()
.collect { isActive -> if (isActive) stopProducing() else startProducing()
}
fun CoroutineScope.startProducing() {
producer = launch() {
sharedFlow.emit(...)
}
}
fun stopProducing() {
producer.cancel()
}
First of all, when you call channelFlow(block), there is no need to close the channel manually. The channel will be closed automatically after the execution of block is done.
I think the "produce" coroutine builder function may be what you need. But unfortunately, it's still an experimental api.
fun poll(id: String) = someScope.produce {
invokeOnClose { Timber.e("channel flow closed.") }
while (true) {
try {
send(repository.getDetails(id))
// delay(MIN_REFRESH_TIME_MS) //no need
} catch (throwable: Throwable) {
Timber.e("error -> ${throwable.message}")
}
}
}
fun main() = runBlocking {
val channel = poll("hello")
channel.receive()
channel.cancel()
}
The produce function will suspended when you don't call the returned channel's receive() method, so there is no need to delay.
UPDATE: Use broadcast for sharing values across multiple ReceiveChannel.
fun poll(id: String) = someScope.broadcast {
invokeOnClose { Timber.e("channel flow closed.") }
while (true) {
try {
send(repository.getDetails(id))
// delay(MIN_REFRESH_TIME_MS) //no need
} catch (throwable: Throwable) {
Timber.e("error -> ${throwable.message}")
}
}
}
fun main() = runBlocking {
val broadcast = poll("hello")
val channel1 = broadcast.openSubscription()
val channel2 = broadcast.openSubscription()
channel1.receive()
channel2.receive()
broadcast.cancel()
}
I am trying to make a ProgressDialog show up while the application is looking for an IP Address in the network. In my present codes, even though the initialization of the ProgressDialog is at the beginning, it shows after what I am waiting for finishes.
Here is my code:
val clickListener = View.OnClickListener { view ->
when(view.id) {
R.id.button_upload -> {
progressDialog = ProgressDialog(activity)
progressDialog!!.setMessage("Looking for the server. Please wait...")
progressDialog!!.setCancelable(false)
progressDialog!!.show()
if(findServer()) {
Log.i("TAG", "FOUND")
} else {
Log.i("TAG", "NOT FOUND")
}
}
}
}
private fun findServer(): Boolean {
if(canPingServer()) {
Toast.makeText(context, "We are connected to the server server", Toast.LENGTH_LONG).show()
gView.button_upload.setText("Upload")
gView.button_upload.isEnabled = true
progressDialog!!.dismiss()
return true
} else {
Toast.makeText(context, "We cannot connect to the server.", Toast.LENGTH_LONG).show()
gView.button_upload.setText("Server not found")
gView.button_upload.isEnabled = false
progressDialog!!.dismiss()
return false
}
}
private fun canPingServer(): Boolean {
val runtime = Runtime.getRuntime()
try {
val mIpAddrProcess = runtime.exec("/system/bin/ping -c 1 192.168.1.4")
val mExitValue = mIpAddrProcess.waitFor()
Log.i("TAG","mExitValue $mExitValue")
return mExitValue == 0
} catch (ignore: InterruptedException) {
ignore.printStackTrace()
Log.i("TAG"," Exception:$ignore")
} catch (e: IOException) {
e.printStackTrace()
Log.i("TAG"," Exception:$e")
}
return false
}
I believe that I have to create the AsyncTask<Void, Void, String> for this, but the thing is, this fragment have inherited from another class already like so
class UploadFragment : BaseFragment() {.....}
It's showing because you findServer() function needs to execute on a different thread.
val clickListener = View.OnClickListener { view ->
when(view.id) {
R.id.button_upload -> {
progressDialog = ProgressDialog(activity)
progressDialog!!.setMessage("Looking for the server. Please wait...")
progressDialog!!.setCancelable(false)
progressDialog!!.show()
Thread(Runnable {
if(findServer()) {
Log.i("TAG", "FOUND")
} else {
Log.i("TAG", "NOT FOUND")
}
}).start()
}
}
}
AsyncTask<Void, Void, String> is another way to multi thread in java but I believe the way I showed above would suit your needs better. You need to be careful though because anything that has to run on the main thread I.e. your toasts or where you are setting the text of your elements still needs to happen on the main thread. You can accomplish this by surrounding anything that requires being run on the main thread with
activity.runOnUiThread(java.lang.Runnable {
//put code here that needs to be run on the ui thread
})
In you case an example would be
private fun findServer(): Boolean {
if(canPingServer()) {
activity.runOnUiThread(java.lang.Runnable {
Toast.makeText(context, "We are connected to the server server", Toast.LENGTH_LONG).show()
gView.button_upload.setText("Upload")
gView.button_upload.isEnabled = true
progressDialog!!.dismiss()
})
return true
} else {
activity.runOnUiThread(java.lang.Runnable {
Toast.makeText(context, "We cannot connect to the server.", Toast.LENGTH_LONG).show()
gView.button_upload.setText("Server not found")
gView.button_upload.isEnabled = false
progressDialog!!.dismiss()
})
return false
}
}
Will the following code leak resources when the Kotlin coroutine is canceled?
General: The code is nested inside a ViewModel!
The method retrievePDFDocument will get triggered in the onStart Event of a Fragment.
fun retrievePDFDocument() {
job = viewModelScope.launch {
withContext(Dispatchers.IO) {
downloadFile(assetPath.value!!)
}
}
}
And here the suspend function:
private fun downloadFile(strPdfUrl: String): File? {
var inputStream: InputStream? = null
val lenghtOfFile: Int //lenghtOfFile is used for calculating download progress
//this is where the file will be seen after the download
var fileOut: FileOutputStream? = null
var localPDFFile: File? = null
if(strPdfUrl.isBlank())
return localPDFFile
try {
val pdfUrl = URL(strPdfUrl)
val urlConnection = pdfUrl.openConnection() as HttpURLConnection
if (urlConnection.responseCode == 200) {
//file input is from the url
inputStream = BufferedInputStream(urlConnection.inputStream)
lenghtOfFile = urlConnection.contentLength
localPDFFile = File(localPdfDirectory, pdfFileName.value!!)
fileOut = FileOutputStream(localPDFFile)
//here’s the download code
val buffer = ByteArray(1024)
var total: Long = 0
while (true) {
// If coroutine is cancelled
// a CancellationException will be thrown here
// Do not more work then necesarry
coroutineContext.ensureActive()
val length = inputStream.read(buffer)
if (length <= 0) break
total += length.toLong()
_currProgress.postValue( (total * 100 / lenghtOfFile).toInt() )
fileOut.write(buffer, 0, length)
}
}
} catch (e: IOException) {
Log.e("PdfViewerViewModel - downloadFile Error: ${e.message}")
localPdfFile?.delete() // remove partially downloaded file
localPDFFile = null
} catch (e1: CancellationException) {
Log.e("PdfViewerViewModel - downloadFile CancellationException: ${e1.message}")
localPdfFile?.delete() // remove partially downloaded file
localPdfFile = null
finally {
try {
inputStream?.close()
fileOut?.apply {
flush()
close()
}
} catch (e1: IOException) { //do nothing here }
}
return localPDFFile
}
Kindly regards
Frank
#Update 10.04.2020
Implementation with coroutineContext.ensureActive() and catching the exception
The traditional approach in Java to execute an external process is to start a new Process, start two threads to consume its inputStream and errorStream and then call its blocking Process.waitFor() to wait till the external command has exited.
How can this be done in a (almost) non-blocking style with Kotlin coroutines?
I tried it this way. Do you have any suggestions to improve it?
(How to asynchronously read the streams, also call ProcessBuilder.start() in withContext(Dispatchers.IO), are there too many calls to Dispatchers.IO, ...?)
suspend fun executeCommand(commandArgs: List<String>): ExecuteCommandResult {
try {
val process = ProcessBuilder(commandArgs).start()
val outputStream = GlobalScope.async(Dispatchers.IO) { readStream(process.inputStream) }
val errorStream = GlobalScope.async(Dispatchers.IO) { readStream(process.errorStream) }
val exitCode = withContext(Dispatchers.IO) {
process.waitFor()
}
return ExecuteCommandResult(exitCode, outputStream.await(), errorStream.await())
} catch (e: Exception) {
return ExecuteCommandResult(-1, "", e.localizedMessage)
}
}
private suspend fun readStream(inputStream: InputStream): String {
val readLines = mutableListOf<String>()
withContext(Dispatchers.IO) {
try {
inputStream.bufferedReader().use { reader ->
var line: String?
do {
line = reader.readLine()
if (line != null) {
readLines.add(line)
}
} while (line != null)
}
} catch (e: Exception) {
// ..
}
}
return readLines.joinToString(System.lineSeparator())
}