aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSteve Siani <alphonse.steve.siani.djissitchi@ibm.com>2019-11-19 22:44:58 -0500
committerSteve Siani <alphonse.steve.siani.djissitchi@ibm.com>2019-12-09 21:44:01 -0500
commit210cc0ab1bdff78fcefd80ad59e340635a21b063 (patch)
treea3bf62582a01890d33798c2090d6df6829ee0ef3
parenta3247099971acd800a753cdfee51f84ec7679a78 (diff)
Cli executor doesn't keep/support execution context
Issue-ID: CCSDK-1927 Signed-off-by: Steve Siani <alphonse.steve.siani.djissitchi@ibm.com> Change-Id: Ib417bfd62662676fe7520a5500df82ade716f66c
-rw-r--r--ms/blueprintsprocessor/functions/cli-executor/src/main/kotlin/internal/scripts/InternalSimpleCli.kt6
-rw-r--r--ms/blueprintsprocessor/modules/commons/ssh-lib/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BasicAuthSshClientService.kt133
-rw-r--r--ms/blueprintsprocessor/modules/commons/ssh-lib/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BlueprintSshClientService.kt10
-rw-r--r--ms/blueprintsprocessor/modules/commons/ssh-lib/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BlueprintSshClientServiceTest.kt92
-rw-r--r--ms/blueprintsprocessor/modules/commons/ssh-lib/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/echoShell/EchoShellFactory.kt102
5 files changed, 288 insertions, 55 deletions
diff --git a/ms/blueprintsprocessor/functions/cli-executor/src/main/kotlin/internal/scripts/InternalSimpleCli.kt b/ms/blueprintsprocessor/functions/cli-executor/src/main/kotlin/internal/scripts/InternalSimpleCli.kt
index b3708a092..a0dfb8b0b 100644
--- a/ms/blueprintsprocessor/functions/cli-executor/src/main/kotlin/internal/scripts/InternalSimpleCli.kt
+++ b/ms/blueprintsprocessor/functions/cli-executor/src/main/kotlin/internal/scripts/InternalSimpleCli.kt
@@ -1,6 +1,8 @@
/*
* Copyright © 2019 IBM.
*
+ * Modifications Copyright © 2019 IBM, Bell Canada
+ *
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
@@ -23,7 +25,7 @@ import org.onap.ccsdk.cds.blueprintsprocessor.functions.cli.executor.cliDeviceIn
import org.onap.ccsdk.cds.blueprintsprocessor.functions.cli.executor.getSshClientService
import org.onap.ccsdk.cds.blueprintsprocessor.services.execution.AbstractScriptComponentFunction
import org.onap.ccsdk.cds.blueprintsprocessor.services.execution.ComponentScriptExecutor
-import org.onap.ccsdk.cds.controllerblueprints.core.asJsonPrimitive
+import org.onap.ccsdk.cds.controllerblueprints.core.asJsonType
import org.slf4j.LoggerFactory
open class TestCliScriptFunction : AbstractScriptComponentFunction() {
@@ -70,7 +72,7 @@ open class Check : AbstractScriptComponentFunction() {
sshClientService.closeSessionNB()
// Set the Response Data
- setAttribute(ComponentScriptExecutor.ATTRIBUTE_RESPONSE_DATA, responseLog.asJsonPrimitive())
+ setAttribute(ComponentScriptExecutor.ATTRIBUTE_RESPONSE_DATA, responseLog.asJsonType())
log.info("Executing process")
}
diff --git a/ms/blueprintsprocessor/modules/commons/ssh-lib/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BasicAuthSshClientService.kt b/ms/blueprintsprocessor/modules/commons/ssh-lib/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BasicAuthSshClientService.kt
index 61baaa1ef..2885d6528 100644
--- a/ms/blueprintsprocessor/modules/commons/ssh-lib/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BasicAuthSshClientService.kt
+++ b/ms/blueprintsprocessor/modules/commons/ssh-lib/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BasicAuthSshClientService.kt
@@ -1,6 +1,8 @@
/*
* Copyright © 2019 IBM.
*
+ * Modifications Copyright © 2018-2019 IBM, Bell Canada
+ *
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
@@ -16,9 +18,9 @@
package org.onap.ccsdk.cds.blueprintsprocessor.ssh.service
+import org.apache.commons.io.output.TeeOutputStream
import org.apache.sshd.client.SshClient
-import org.apache.sshd.client.channel.ChannelExec
-import org.apache.sshd.client.channel.ClientChannel
+import org.apache.sshd.client.channel.ChannelShell
import org.apache.sshd.client.channel.ClientChannelEvent
import org.apache.sshd.client.keyverifier.AcceptAllServerKeyVerifier
import org.apache.sshd.client.session.ClientSession
@@ -26,75 +28,142 @@ import org.onap.ccsdk.cds.blueprintsprocessor.ssh.BasicAuthSshClientProperties
import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintProcessorException
import org.slf4j.LoggerFactory
import java.io.ByteArrayOutputStream
+import java.io.IOException
+import java.io.PipedInputStream
+import java.io.PipedOutputStream
import java.util.Collections
import java.util.EnumSet
+import java.util.Scanner
+import java.util.ArrayList
open class BasicAuthSshClientService(private val basicAuthSshClientProperties: BasicAuthSshClientProperties) :
- BlueprintSshClientService {
+ BlueprintSshClientService {
private val log = LoggerFactory.getLogger(BasicAuthSshClientService::class.java)!!
+ private val newLine = "\n".toByteArray()
+ private var channel: ChannelShell? = null
+ private var teeOutput: TeeOutputStream? = null
private lateinit var sshClient: SshClient
private lateinit var clientSession: ClientSession
- var channel: ChannelExec? = null
override suspend fun startSessionNB(): ClientSession {
sshClient = SshClient.setUpDefaultClient()
sshClient.serverKeyVerifier = AcceptAllServerKeyVerifier.INSTANCE
sshClient.start()
log.debug("SSH Client Service started successfully")
+
clientSession = sshClient.connect(
- basicAuthSshClientProperties.username, basicAuthSshClientProperties.host,
- basicAuthSshClientProperties.port
- )
- .verify(basicAuthSshClientProperties.connectionTimeOut)
- .session
+ basicAuthSshClientProperties.username, basicAuthSshClientProperties.host,
+ basicAuthSshClientProperties.port).verify(basicAuthSshClientProperties.connectionTimeOut).session
clientSession.addPasswordIdentity(basicAuthSshClientProperties.password)
clientSession.auth().verify(basicAuthSshClientProperties.connectionTimeOut)
+ startChannel()
+
log.info("SSH client session($clientSession) created")
return clientSession
}
- override suspend fun executeCommandsNB(commands: List<String>, timeOut: Long): String {
- val buffer = StringBuffer()
+ private fun startChannel() {
try {
- commands.forEach { command ->
- buffer.append("\nCommand : $command")
- buffer.append("\n" + executeCommandNB(command, timeOut))
+ channel = clientSession.createShellChannel()
+ val pipedIn = PipedOutputStream()
+ channel!!.setIn(PipedInputStream(pipedIn))
+ teeOutput = TeeOutputStream(ByteArrayOutputStream(), pipedIn)
+ channel!!.out = ByteArrayOutputStream()
+ channel!!.err = ByteArrayOutputStream()
+ channel!!.open()
+ } catch (e: Exception) {
+ throw BluePrintProcessorException("Failed to start Shell channel: ${e.message}")
+ }
+ }
+
+ override suspend fun executeCommandsNB(commands: List <String>, timeOut: Long): List<CommandResult> {
+ val response = ArrayList<CommandResult>()
+ try {
+ var stopLoop = false
+ val commandsIterator = commands.iterator()
+ while (commandsIterator.hasNext() && !stopLoop) {
+ val command = commandsIterator.next()
+ log.debug("Executing host command($command) \n")
+ val result = executeCommand(command, timeOut)
+ response.add(result)
+ // Once a command in the template has failed break out of the loop to stop executing further commands
+ if (!result.successful) {
+ log.debug("Template execution will stop because command ({}) has failed.", command)
+ stopLoop = true
+ }
}
} catch (e: Exception) {
- throw BluePrintProcessorException("Failed to execute commands, below the output : $buffer")
+ throw BluePrintProcessorException("Failed to execute commands, below the error message : ${e.message}")
}
- return buffer.toString()
+ return response
}
- override suspend fun executeCommandNB(command: String, timeOut: Long): String {
- log.debug("Executing host($clientSession) command($command)")
+ override suspend fun executeCommandNB(command: String, timeOut: Long): CommandResult {
+ val deviceOutput: String
+ var isSuccessful = true
+ try {
+ teeOutput!!.write(command.toByteArray())
+ teeOutput!!.write(newLine)
+ teeOutput!!.flush()
+ deviceOutput = waitForPrompt(timeOut)
+ } catch (e: IOException) {
+ throw BluePrintProcessorException("Exception during command execution: ${e.message}", e)
+ }
+
+ if (detectFailure(deviceOutput)) {
+ isSuccessful = false
+ }
- channel = clientSession.createExecChannel(command)
- checkNotNull(channel) { "failed to create Channel for the command : $command" }
+ val commandResult = CommandResult(command, deviceOutput, isSuccessful)
+ log.info("Command Response: ({}) $newLine", commandResult)
+ return commandResult
+ }
- // TODO("Convert to streaming ")
- val outputStream = ByteArrayOutputStream()
- channel!!.out = outputStream
- channel!!.err = outputStream
- channel!!.open().await()
- val waitMask = channel!!.waitFor(Collections.unmodifiableSet(EnumSet.of(ClientChannelEvent.CLOSED)), timeOut)
- if (waitMask.contains(ClientChannelEvent.TIMEOUT)) {
- throw BluePrintProcessorException("Failed to retrieve command result in time: $command")
+ private fun waitForPrompt(timeOut: Long): String {
+ val waitMask = channel!!.waitFor(
+ Collections.unmodifiableSet(EnumSet.of(ClientChannelEvent.CLOSED)), timeOut)
+ if (channel!!.out.toString().indexOfAny(arrayListOf("$", ">", "#")) <= 0 && waitMask.contains(ClientChannelEvent.TIMEOUT)) {
+ throw BluePrintProcessorException("Timeout: Failed to retrieve commands result in $timeOut ms")
}
- val exitStatus = channel!!.exitStatus
- ClientChannel.validateCommandExitStatusCode(command, exitStatus!!)
- return outputStream.toString()
+ val outputResult = channel!!.out.toString()
+ channel!!.out.flush()
+ return outputResult
}
override suspend fun closeSessionNB() {
- if (channel != null)
+ if (channel != null) {
channel!!.close()
+ }
+
+ if (clientSession.isOpen && !clientSession.isClosing) {
+ clientSession.close()
+ }
+
if (sshClient.isStarted) {
sshClient.stop()
}
log.debug("SSH Client Service stopped successfully")
}
+
+ // TODO filter output to check error message
+ private fun detectFailure(output: String): Boolean {
+ if (output.isNotBlank()) {
+ // Output can be multiline, need to check if any of the line starts with %
+ Scanner(output).use { scanner ->
+ while (scanner.hasNextLine()) {
+ val temp = scanner.nextLine()
+ if (temp.isNotBlank() && (temp.trim { it <= ' ' }.startsWith("%") ||
+ temp.trim { it <= ' ' }.startsWith("syntax error"))) {
+ return true
+ }
+ }
+ }
+ }
+ return false
+ }
}
+
+data class CommandResult(val command: String, val deviceOutput: String, val successful: Boolean)
diff --git a/ms/blueprintsprocessor/modules/commons/ssh-lib/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BlueprintSshClientService.kt b/ms/blueprintsprocessor/modules/commons/ssh-lib/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BlueprintSshClientService.kt
index 724c4277d..27ebf50bc 100644
--- a/ms/blueprintsprocessor/modules/commons/ssh-lib/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BlueprintSshClientService.kt
+++ b/ms/blueprintsprocessor/modules/commons/ssh-lib/src/main/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BlueprintSshClientService.kt
@@ -1,6 +1,8 @@
/*
* Copyright © 2019 IBM.
*
+ * Modifications Copyright © 2018-2019 IBM, Bell Canada
+ *
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
@@ -25,11 +27,11 @@ interface BlueprintSshClientService {
startSessionNB()
}
- fun executeCommands(commands: List<String>, timeOut: Long): String = runBlocking {
+ fun executeCommands(commands: List<String>, timeOut: Long): List<CommandResult> = runBlocking {
executeCommandsNB(commands, timeOut)
}
- fun executeCommand(command: String, timeOut: Long): String = runBlocking {
+ fun executeCommand(command: String, timeOut: Long): CommandResult = runBlocking {
executeCommandNB(command, timeOut)
}
@@ -39,9 +41,9 @@ interface BlueprintSshClientService {
suspend fun startSessionNB(): ClientSession
- suspend fun executeCommandsNB(commands: List<String>, timeOut: Long): String
+ suspend fun executeCommandsNB(commands: List<String>, timeOut: Long): List<CommandResult>
- suspend fun executeCommandNB(command: String, timeOut: Long): String
+ suspend fun executeCommandNB(command: String, timeOut: Long): CommandResult
suspend fun closeSessionNB()
}
diff --git a/ms/blueprintsprocessor/modules/commons/ssh-lib/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BlueprintSshClientServiceTest.kt b/ms/blueprintsprocessor/modules/commons/ssh-lib/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BlueprintSshClientServiceTest.kt
index 683816f7f..3785a21b2 100644
--- a/ms/blueprintsprocessor/modules/commons/ssh-lib/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BlueprintSshClientServiceTest.kt
+++ b/ms/blueprintsprocessor/modules/commons/ssh-lib/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/BlueprintSshClientServiceTest.kt
@@ -1,6 +1,8 @@
/*
* Copyright © 2019 IBM.
*
+ * Modifications Copyright © 2018-2019 IBM, Bell Canada
+ *
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
@@ -25,18 +27,22 @@ import org.apache.sshd.server.auth.pubkey.AcceptAllPublickeyAuthenticator
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider
import org.apache.sshd.server.session.ServerSession
import org.apache.sshd.server.shell.ProcessShellCommandFactory
-import org.junit.runner.RunWith
import org.onap.ccsdk.cds.blueprintsprocessor.core.BluePrintPropertiesService
import org.onap.ccsdk.cds.blueprintsprocessor.core.BluePrintPropertyConfiguration
import org.onap.ccsdk.cds.blueprintsprocessor.ssh.BluePrintSshLibConfiguration
+import org.onap.ccsdk.cds.blueprintsprocessor.ssh.service.echoShell.EchoShellFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.TestPropertySource
import org.springframework.test.context.junit4.SpringRunner
import java.nio.file.Paths
+import org.junit.runner.RunWith
+import kotlin.test.BeforeTest
+import kotlin.test.AfterTest
import kotlin.test.Test
-import kotlin.test.assertEquals
+import kotlin.test.assertTrue
import kotlin.test.assertNotNull
+import kotlin.test.assertEquals
@RunWith(SpringRunner::class)
@ContextConfiguration(
@@ -57,29 +63,76 @@ class BlueprintSshClientServiceTest {
@Autowired
lateinit var bluePrintSshLibPropertyService: BluePrintSshLibPropertyService
- @Test
- fun testBasicAuthSshClientService() {
+ lateinit var bluePrintSshLibPropertyServiceMock: BluePrintSshLibPropertyService
+
+ private lateinit var sshServer: SshServer
+
+ @BeforeTest
+ fun startShellServer() {
runBlocking {
- val sshServer = setupTestServer("localhost", 52815, "root", "dummyps")
+ println("Start local Shell server")
+ sshServer = setupTestServer("localhost", 52815, "root", "dummyps")
sshServer.start()
println(sshServer)
- val bluePrintSshLibPropertyService = bluePrintSshLibPropertyService.blueprintSshClientService("sample")
- val sshSession = bluePrintSshLibPropertyService.startSession()
- val response = bluePrintSshLibPropertyService.executeCommandsNB(arrayListOf("echo '1'", "echo '2'"), 2000)
- assertNotNull(response, "failed to get command response")
- bluePrintSshLibPropertyService.closeSession()
- sshServer.stop(true)
}
}
- private fun setupTestServer(host: String, port: Int, userName: String, password: String): SshServer {
+ @AfterTest
+ fun stopShellServer() {
+ println("End the Shell server")
+ sshServer.stop(true)
+ }
+
+ @Test
+ fun testStartSessionNB() {
+ val clientSession = getSshClientService().startSession()
+ assertNotNull(clientSession, "Failed to start ssh session with server")
+ }
+
+ @Test
+ fun testBasicAuthSshClientService() {
+ runBlocking {
+ val blueprintSshClientService = getSshClientService()
+ blueprintSshClientService.startSession()
+ // Preparing response
+ val commandResults = arrayListOf<CommandResult>()
+ commandResults.add(CommandResult("echo 1", "echo 1\n#", true))
+ commandResults.add(CommandResult("echo 2", "echo 1\n#echo 2\n#", true))
+ val response = blueprintSshClientService.executeCommands(arrayListOf("echo 1", "echo 2"), 2000)
+ blueprintSshClientService.closeSession()
+
+ assertEquals(response, commandResults, "failed to get command responses")
+ }
+ }
+
+ @Test
+ fun `testBasicAuthSshClientService single execution command`() {
+ runBlocking {
+ val blueprintSshClientService = getSshClientService()
+ blueprintSshClientService.startSession()
+ val response = blueprintSshClientService.executeCommand("echo 1", 2000)
+ blueprintSshClientService.closeSession()
+
+ assertEquals(response, CommandResult("echo 1", "echo 1\n#", true), "failed to get command response")
+ }
+ }
+
+ @Test
+ fun testCloseSessionNB() {
+ val bluePrintSshLibPropertyService = bluePrintSshLibPropertyService.blueprintSshClientService("sample")
+ val clientSession = bluePrintSshLibPropertyService.startSession()
+ bluePrintSshLibPropertyService.closeSession()
+ assertTrue(clientSession.isClosed, "Failed to close ssh session with server")
+ }
+
+ private fun setupTestServer(host: String, port: Int, username: String, password: String): SshServer {
val sshd = SshServer.setUpDefaultServer()
sshd.port = port
sshd.host = host
sshd.keyPairProvider = createTestHostKeyProvider()
- sshd.passwordAuthenticator = BogusPasswordAuthenticator(userName, password)
+ sshd.passwordAuthenticator = BogusPasswordAuthenticator(username, password)
sshd.publickeyAuthenticator = AcceptAllPublickeyAuthenticator.INSTANCE
- // sshd.shellFactory = EchoShellFactory()
+ sshd.shellFactory = EchoShellFactory.INSTANCE
sshd.commandFactory = ProcessShellCommandFactory.INSTANCE
return sshd
}
@@ -90,12 +143,17 @@ class BlueprintSshClientServiceTest {
keyProvider.algorithm = RSA_ALGORITHM
return keyProvider
}
+
+ private fun getSshClientService(): BlueprintSshClientService {
+ return bluePrintSshLibPropertyService.blueprintSshClientService("sample")
+ }
}
-class BogusPasswordAuthenticator(userName: String, password: String) : PasswordAuthenticator {
+class BogusPasswordAuthenticator(private val usr: String, private val pwd: String) : PasswordAuthenticator {
+
override fun authenticate(username: String, password: String, serverSession: ServerSession): Boolean {
- assertEquals(username, "root", "failed to match username")
- assertEquals(password, "dummyps", "failed to match password")
+ assertEquals(username, usr, "failed to match username")
+ assertEquals(password, pwd, "failed to match password")
return true
}
}
diff --git a/ms/blueprintsprocessor/modules/commons/ssh-lib/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/echoShell/EchoShellFactory.kt b/ms/blueprintsprocessor/modules/commons/ssh-lib/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/echoShell/EchoShellFactory.kt
new file mode 100644
index 000000000..9d308202f
--- /dev/null
+++ b/ms/blueprintsprocessor/modules/commons/ssh-lib/src/test/kotlin/org/onap/ccsdk/cds/blueprintsprocessor/ssh/service/echoShell/EchoShellFactory.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright © 2019 IBM. Bell Canada
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onap.ccsdk.cds.blueprintsprocessor.ssh.service.echoShell
+
+import org.apache.sshd.common.Factory
+import org.apache.sshd.server.Environment
+import org.apache.sshd.server.command.Command
+import org.apache.sshd.server.ExitCallback
+import java.io.InputStream
+import java.io.OutputStream
+import java.io.IOException
+import java.io.BufferedReader
+import java.io.InputStreamReader
+import java.io.InterruptedIOException
+
+class EchoShellFactory : Factory<Command> {
+
+ override fun create(): Command {
+ return EchoShell()
+ }
+
+ companion object {
+ val INSTANCE = EchoShellFactory()
+ }
+}
+
+class EchoShell : Command, Runnable {
+
+ var `in`: InputStream? = null
+ private set
+ var out: OutputStream? = null
+ private set
+ var err: OutputStream? = null
+ private set
+ private var callback: ExitCallback? = null
+ var environment: Environment? = null
+ private set
+ private var thread: Thread? = null
+
+ override fun setInputStream(`in`: InputStream) {
+ this.`in` = `in`
+ }
+
+ override fun setOutputStream(out: OutputStream) {
+ this.out = out
+ }
+
+ override fun setErrorStream(err: OutputStream) {
+ this.err = err
+ }
+
+ override fun setExitCallback(callback: ExitCallback) {
+ this.callback = callback
+ }
+
+ @Throws(IOException::class)
+ override fun start(env: Environment) {
+ environment = env
+ thread = Thread(this, "EchoShell")
+ thread!!.isDaemon = true
+ thread!!.start()
+ }
+
+ override fun destroy() {
+ thread!!.interrupt()
+ }
+
+ override fun run() {
+ val r = BufferedReader(InputStreamReader(`in`))
+ try {
+ while (true) {
+ val s = r.readLine() ?: return
+ out!!.write((s + "\n").toByteArray())
+ out!!.write("#".toByteArray())
+ out!!.flush()
+ if ("exit" == s) {
+ return
+ }
+ }
+ } catch (e: InterruptedIOException) {
+ // Ignore
+ } catch (e: Exception) {
+ e.printStackTrace()
+ } finally {
+ callback!!.onExit(0)
+ }
+ }
+}