318 lines
11 KiB
Kotlin
318 lines
11 KiB
Kotlin
/*
|
|
* Copyright 2020 dorkbox, llc
|
|
|
|
* 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 dorkbox.executor
|
|
|
|
import dorkbox.executor.exceptions.InvalidExitValueException
|
|
import dorkbox.executor.processResults.SyncProcessResult
|
|
import kotlinx.coroutines.runBlocking
|
|
import org.slf4j.Logger
|
|
import java.io.File
|
|
import java.io.IOException
|
|
import java.util.concurrent.TimeUnit
|
|
import java.util.concurrent.TimeoutException
|
|
|
|
/**
|
|
* see https://github.com/hierynomus/sshj
|
|
*
|
|
* NOTE: JSCH is no longer maintained.
|
|
* The fork from https://github.com/mwiede/jsch fixes many issues, but STILL does not connect to an ubuntu 18.04 instance
|
|
*
|
|
* The SSHJ implementation works and is well documented. It is also used by Intellij 2019.2+, so it is also well tested and used
|
|
*/
|
|
class SshExecOptions(val executor: Executor) {
|
|
companion object {
|
|
init {
|
|
/*
|
|
* see https://github.com/hierynomus/sshj
|
|
*
|
|
* NOTE: JSCH is no longer maintained.
|
|
* The fork from https://github.com/mwiede/jsch fixes many issues, but STILL does not connect to an ubuntu 18.04 instance
|
|
*
|
|
* The SSHJ implementation works and is well documented. It is also used by Intellij 2019.2+, so it is also well tested and used
|
|
*/
|
|
try {
|
|
Class.forName("net.schmizz.sshj.SSHClient")
|
|
} catch (e: Exception) {
|
|
throw RuntimeException("Unable to execute SSH commands. The SSHJ library is not available. \n\n" +
|
|
"For example, implementation(\"com.hierynomus:sshj:0.31.0\") as a dependency will add this library. \n" +
|
|
"You might need to use a more recent version, as 0.31.0 might not be the latest available.", e)
|
|
}
|
|
}
|
|
}
|
|
private var host: String? = null
|
|
private var port: Int = 22
|
|
private var userName: String? = null
|
|
private var password: String? = null
|
|
|
|
private var privateKeyFile: String? = null
|
|
|
|
private var verifier: net.schmizz.sshj.transport.verification.HostKeyVerifier? = null
|
|
private var strictHostCheck = true
|
|
private var knownHostsFile: String? = null
|
|
|
|
|
|
private lateinit var ssh: net.schmizz.sshj.SSHClient
|
|
|
|
internal fun startProcess(timeout: Long, timeoutUnit: TimeUnit, logger: Logger?): SshProcess {
|
|
// have to fixup several SSHJ loggers!
|
|
LogHelper.fixSshLogger(logger)
|
|
|
|
// have to setup the SSH client loggers BEFORE creating it!
|
|
val factory = LogHelper.getLogFactory(logger)
|
|
val config = object : net.schmizz.sshj.DefaultConfig() {
|
|
override fun setLoggerFactory(loggerFactory: net.schmizz.sshj.common.LoggerFactory) {
|
|
super.setLoggerFactory(factory)
|
|
}
|
|
override fun getLoggerFactory(): net.schmizz.sshj.common.LoggerFactory {
|
|
return factory
|
|
}
|
|
}
|
|
config.loggerFactory = factory
|
|
|
|
ssh = net.schmizz.sshj.SSHClient(config)
|
|
|
|
|
|
if (strictHostCheck) {
|
|
if (knownHostsFile != null) {
|
|
ssh.loadKnownHosts(File(knownHostsFile!!))
|
|
} else {
|
|
ssh.addHostKeyVerifier(verifier)
|
|
}
|
|
} else {
|
|
ssh.addHostKeyVerifier(net.schmizz.sshj.transport.verification.PromiscuousVerifier())
|
|
}
|
|
|
|
|
|
if (timeout > 0L) {
|
|
ssh.connectTimeout = timeoutUnit.toMillis(timeout).toInt()
|
|
}
|
|
|
|
ssh.connect(host, port)
|
|
|
|
if (privateKeyFile != null) {
|
|
ssh.authPublickey(userName, privateKeyFile)
|
|
} else {
|
|
ssh.authPassword(userName, password)
|
|
}
|
|
|
|
return try {
|
|
val session = ssh.startSession()
|
|
|
|
val execCommand = executor.builder.command().joinToString(separator = " ")
|
|
val command = session.exec(execCommand)
|
|
|
|
SshProcess(ssh, session, command)
|
|
} catch (e: Exception) {
|
|
throw IOException(e.message, e)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return the underlying SSH Client in case more specific configurations are necessary
|
|
*/
|
|
fun ssh(): net.schmizz.sshj.SSHClient {
|
|
return ssh
|
|
}
|
|
|
|
fun userName(userName: String): SshExecOptions {
|
|
this.userName = userName
|
|
return this
|
|
}
|
|
|
|
fun host(host: String): SshExecOptions {
|
|
this.host = host
|
|
return this
|
|
}
|
|
|
|
fun port(port: Int): SshExecOptions {
|
|
this.port = port
|
|
return this
|
|
}
|
|
|
|
fun privateKeyFile(privateKeyFile: String): SshExecOptions {
|
|
this.privateKeyFile = privateKeyFile
|
|
return this
|
|
}
|
|
|
|
fun password(password: String): SshExecOptions {
|
|
this.password = password
|
|
return this
|
|
}
|
|
|
|
fun disableStrictHostChecking(): SshExecOptions {
|
|
strictHostCheck = false
|
|
return this
|
|
}
|
|
|
|
fun setHostVerifier(verifier: net.schmizz.sshj.transport.verification.HostKeyVerifier): SshExecOptions {
|
|
this.verifier = verifier
|
|
return this
|
|
}
|
|
|
|
fun setKnownHostsFile(knownHostsFile: String): SshExecOptions {
|
|
this.knownHostsFile = knownHostsFile
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Sets the program and its arguments which are being executed.
|
|
*
|
|
* @param command A string array containing the program and its arguments.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun command(vararg command: String): SshExecOptions {
|
|
executor.command(Executor.fixArguments(listOf(*command)))
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Sets the program and its arguments which are being executed.
|
|
*
|
|
* @param command The iterable containing the program and its arguments.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun command(command: Iterable<String>): SshExecOptions {
|
|
executor.command(command)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Splits string by spaces and passes it to [Executor.command]<br></br>
|
|
*
|
|
* NB: this method do not handle whitespace escaping,
|
|
* `"mkdir new\ folder"` would be interpreted as
|
|
* `{"mkdir", "new\", "folder"}` command.
|
|
*
|
|
* @param commandWithArgs A string array containing the program and its arguments.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun commandSplit(commandWithArgs: String): SshExecOptions {
|
|
executor.commandSplit(commandWithArgs)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Add arguments to an existing command, which will be executed.
|
|
*
|
|
* This does not replace commands, it adds to them
|
|
*
|
|
* @param arguments A string array containing the program and/or its arguments.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun addArg(vararg arguments: String): SshExecOptions {
|
|
executor.addArg(*arguments)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Add arguments to an existing command, which will be executed.
|
|
*
|
|
* This does not replace commands, it adds to them
|
|
*
|
|
* @param arguments A string array containing the program and/or its arguments.
|
|
*
|
|
* @return This process executor.
|
|
*/
|
|
fun addArg(arguments: Iterable<String>): SshExecOptions {
|
|
executor.addArg(arguments)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* @return information regarding the username + host + port this connect is with
|
|
*/
|
|
fun info(): String {
|
|
return "$userName@$host:$port"
|
|
}
|
|
|
|
/**
|
|
* Executes the JAVA sub process.
|
|
*
|
|
* This method waits until the process exits, a timeout occurs or the caller thread gets interrupted.
|
|
*
|
|
* In the latter cases the process gets destroyed as well.
|
|
*
|
|
* @param timeout If specified (non-zero), then if the process is running longer than this
|
|
* specified interval, a [TimeoutException] is thrown and the process is destroyed.
|
|
*
|
|
* @return exit code of the finished process.
|
|
*
|
|
* @throws IOException an error occurred when process was started or stopped.
|
|
* @throws InterruptedException this thread was interrupted.
|
|
* @throws TimeoutException timeout set by [.timeout] was reached.
|
|
* @throws InvalidExitValueException if invalid exit value was returned (@see [.exitValues]).
|
|
*/
|
|
@JvmOverloads
|
|
suspend fun start(timeout: Long = 0, timeoutUnit: TimeUnit = TimeUnit.SECONDS): SyncProcessResult {
|
|
return executor.start(timeout, timeoutUnit)
|
|
}
|
|
|
|
/**
|
|
* Start the sub process in a new coroutine. The calling thread will continue execution. This method does not wait until the process exits.
|
|
*
|
|
* Calling [SyncProcessResult.output] will result in a blocking read of process output.
|
|
*
|
|
* The value passed to [.timeout] is ignored. Use [DeferredProcessResult.await] to wait for the process to finish.
|
|
*
|
|
* Invoke [DeferredProcessResult.cancel] to destroy the process.
|
|
*
|
|
* @param timeout If specified (non-zero), then if the process is running longer than this
|
|
* specified interval, a [TimeoutException] is thrown and the process is destroyed.
|
|
*
|
|
* @return [DeferredProcessResult] representing the process results (value/completed output-streams/etc) of the finished process.
|
|
*
|
|
* @throws IOException an error occurred when process was started.
|
|
*/
|
|
@JvmOverloads
|
|
@Throws(IOException::class)
|
|
fun startAsync(timeout: Long = 0, timeoutUnit: TimeUnit = TimeUnit.SECONDS): DeferredProcessResult {
|
|
return executor.startAsync(timeout, timeoutUnit)
|
|
}
|
|
|
|
/**
|
|
* The calling thread will immediately execute the sub process. When trying to close the input streams, the colling thread may block.
|
|
*
|
|
* Waits until:
|
|
* - the process stops
|
|
* - a timeout occurs and the caller thread gets interrupted. (In this case the process gets destroyed as well.)
|
|
*
|
|
* Calling [SyncProcessResult.output] will result in a non-blocking read of process output.
|
|
*
|
|
* @param timeout If specified (non-zero), then if the process is running longer than this
|
|
* specified interval, a [TimeoutException] is thrown and the process is destroyed.
|
|
*
|
|
* @return results of the finished process (exit code and output, if any)
|
|
*
|
|
* @throws IOException an error occurred when process was started or stopped.
|
|
* @throws InterruptedException this thread was interrupted.
|
|
* @throws TimeoutException timeout set by [.timeout] was reached.
|
|
* @throws InvalidExitValueException if invalid exit value was returned (@see [.exitValues]).
|
|
*/
|
|
@JvmOverloads
|
|
@Throws(IOException::class, InterruptedException::class, TimeoutException::class, InvalidExitValueException::class)
|
|
fun startBlocking(timeout: Long = 0, timeoutUnit: TimeUnit = TimeUnit.SECONDS): SyncProcessResult {
|
|
return runBlocking {
|
|
start(timeout, timeoutUnit)
|
|
}
|
|
}
|
|
}
|
|
|