├── .gitignore ├── CODEOWNERS ├── .editorconfig ├── settings.gradle.kts ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── dependency-locks │ ├── kapt.lockfile │ ├── archives.lockfile │ ├── kaptTest.lockfile │ ├── signatures.lockfile │ ├── compileOnly.lockfile │ ├── testCompileOnly.lockfile │ ├── annotationProcessor.lockfile │ ├── testAnnotationProcessor.lockfile │ ├── compileOnlyDependenciesMetadata.lockfile │ ├── runtimeOnlyDependenciesMetadata.lockfile │ ├── testCompileOnlyDependenciesMetadata.lockfile │ ├── testRuntimeOnlyDependenciesMetadata.lockfile │ ├── kotlinCompilerPluginClasspath.lockfile │ ├── kotlinCompilerClasspath.lockfile │ ├── compile.lockfile │ ├── default.lockfile │ ├── runtime.lockfile │ ├── compileClasspath.lockfile │ ├── runtimeClasspath.lockfile │ ├── apiDependenciesMetadata.lockfile │ ├── implementationDependenciesMetadata.lockfile │ ├── testCompileClasspath.lockfile │ ├── testCompile.lockfile │ ├── testRuntime.lockfile │ ├── testRuntimeClasspath.lockfile │ ├── testApiDependenciesMetadata.lockfile │ └── testImplementationDependenciesMetadata.lockfile ├── .gitattributes ├── src ├── test │ ├── kotlin │ │ └── com │ │ │ └── atlassian │ │ │ └── performance │ │ │ └── tools │ │ │ └── ssh │ │ │ ├── api │ │ │ ├── SshConnectionTest.kt │ │ │ ├── SshHostTest.kt │ │ │ ├── SshContainer.kt │ │ │ └── SshTest.kt │ │ │ └── port │ │ │ ├── LocalPortTest.kt │ │ │ └── RemotePortTest.kt │ └── resources │ │ └── log4j2.xml └── main │ ├── kotlin │ └── com │ │ └── atlassian │ │ └── performance │ │ └── tools │ │ └── ssh │ │ ├── api │ │ ├── BackgroundProcess.kt │ │ ├── auth │ │ │ ├── PasswordAuthentication.kt │ │ │ ├── SshAuthentication.kt │ │ │ └── PublicKeyAuthentication.kt │ │ ├── DetachedProcess.kt │ │ ├── SshHost.kt │ │ ├── Ssh.kt │ │ └── SshConnection.kt │ │ ├── PerformanceDefaultConfig.kt │ │ ├── port │ │ ├── RemotePort.kt │ │ └── LocalPort.kt │ │ ├── SshjBackgroundProcess.kt │ │ ├── WaitingCommand.kt │ │ └── SshjConnection.kt │ └── java │ └── net │ └── schmizz │ └── sshj │ └── connection │ └── channel │ └── SocketStreamCopyMonitor.java ├── LICENSE.txt ├── README.md ├── .github └── workflows │ └── ci.yml ├── CONTRIBUTING.md ├── gradlew.bat ├── CODE_OF_CONDUCT.md ├── CHANGELOG.md └── gradlew /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @atlassian/jpt 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{kt,kts}] 2 | indent_size = 4 -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "ssh" 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atlassian/ssh/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Declare files that will always have LF line endings on checkout. 2 | *.kt text eol=lf 3 | *.csv text eol=lf 4 | *.jpt text eol=lf -------------------------------------------------------------------------------- /gradle/dependency-locks/kapt.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | -------------------------------------------------------------------------------- /gradle/dependency-locks/archives.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | -------------------------------------------------------------------------------- /gradle/dependency-locks/kaptTest.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | -------------------------------------------------------------------------------- /gradle/dependency-locks/signatures.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | -------------------------------------------------------------------------------- /gradle/dependency-locks/compileOnly.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | -------------------------------------------------------------------------------- /gradle/dependency-locks/testCompileOnly.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | -------------------------------------------------------------------------------- /gradle/dependency-locks/annotationProcessor.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | -------------------------------------------------------------------------------- /gradle/dependency-locks/testAnnotationProcessor.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | -------------------------------------------------------------------------------- /gradle/dependency-locks/compileOnlyDependenciesMetadata.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | -------------------------------------------------------------------------------- /gradle/dependency-locks/runtimeOnlyDependenciesMetadata.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | -------------------------------------------------------------------------------- /gradle/dependency-locks/testCompileOnlyDependenciesMetadata.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | -------------------------------------------------------------------------------- /gradle/dependency-locks/testRuntimeOnlyDependenciesMetadata.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradle/dependency-locks/kotlinCompilerPluginClasspath.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.2.70 5 | -------------------------------------------------------------------------------- /gradle/dependency-locks/kotlinCompilerClasspath.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | org.jetbrains.kotlin:kotlin-compiler-embeddable:1.2.70 5 | org.jetbrains.kotlin:kotlin-reflect:1.2.70 6 | org.jetbrains.kotlin:kotlin-script-runtime:1.2.70 7 | org.jetbrains.kotlin:kotlin-stdlib-common:1.2.70 8 | org.jetbrains.kotlin:kotlin-stdlib:1.2.70 9 | org.jetbrains:annotations:15.0 10 | -------------------------------------------------------------------------------- /src/test/kotlin/com/atlassian/performance/tools/ssh/api/SshConnectionTest.kt: -------------------------------------------------------------------------------- 1 | package com.atlassian.performance.tools.ssh.api 2 | 3 | import org.junit.Assert 4 | import org.junit.Test 5 | 6 | class SshConnectionTest { 7 | 8 | @Test 9 | fun shouldRunCommandOverSsh() { 10 | SshContainer().useConnection { ssh: SshConnection -> 11 | val sshResult = ssh.safeExecute("echo test") 12 | 13 | Assert.assertTrue(sshResult.isSuccessful()) 14 | Assert.assertEquals(sshResult.output, "test\n") 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright @ 2018 Atlassian Pty Ltd 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /gradle/dependency-locks/compile.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | com.atlassian.performance.tools:io:1.2.0 5 | com.atlassian.performance.tools:jvm-tasks:1.2.4 6 | com.hierynomus:sshj:0.23.0 7 | com.jcraft:jzlib:1.1.3 8 | net.i2p.crypto:eddsa:0.2.0 9 | org.apache.logging.log4j:log4j-api:2.20.0 10 | org.bouncycastle:bcpkix-jdk15on:1.56 11 | org.bouncycastle:bcprov-jdk15on:1.56 12 | org.glassfish:javax.json:1.1 13 | org.jetbrains.kotlin:kotlin-stdlib-common:1.2.70 14 | org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.2.70 15 | org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.70 16 | org.jetbrains.kotlin:kotlin-stdlib:1.2.70 17 | org.jetbrains:annotations:15.0 18 | org.slf4j:slf4j-api:1.7.25 19 | -------------------------------------------------------------------------------- /gradle/dependency-locks/default.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | com.atlassian.performance.tools:io:1.2.0 5 | com.atlassian.performance.tools:jvm-tasks:1.2.4 6 | com.hierynomus:sshj:0.23.0 7 | com.jcraft:jzlib:1.1.3 8 | net.i2p.crypto:eddsa:0.2.0 9 | org.apache.logging.log4j:log4j-api:2.20.0 10 | org.bouncycastle:bcpkix-jdk15on:1.56 11 | org.bouncycastle:bcprov-jdk15on:1.56 12 | org.glassfish:javax.json:1.1 13 | org.jetbrains.kotlin:kotlin-stdlib-common:1.2.70 14 | org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.2.70 15 | org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.70 16 | org.jetbrains.kotlin:kotlin-stdlib:1.2.70 17 | org.jetbrains:annotations:15.0 18 | org.slf4j:slf4j-api:1.7.25 19 | -------------------------------------------------------------------------------- /gradle/dependency-locks/runtime.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | com.atlassian.performance.tools:io:1.2.0 5 | com.atlassian.performance.tools:jvm-tasks:1.2.4 6 | com.hierynomus:sshj:0.23.0 7 | com.jcraft:jzlib:1.1.3 8 | net.i2p.crypto:eddsa:0.2.0 9 | org.apache.logging.log4j:log4j-api:2.20.0 10 | org.bouncycastle:bcpkix-jdk15on:1.56 11 | org.bouncycastle:bcprov-jdk15on:1.56 12 | org.glassfish:javax.json:1.1 13 | org.jetbrains.kotlin:kotlin-stdlib-common:1.2.70 14 | org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.2.70 15 | org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.70 16 | org.jetbrains.kotlin:kotlin-stdlib:1.2.70 17 | org.jetbrains:annotations:15.0 18 | org.slf4j:slf4j-api:1.7.25 19 | -------------------------------------------------------------------------------- /gradle/dependency-locks/compileClasspath.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | com.atlassian.performance.tools:io:1.2.0 5 | com.atlassian.performance.tools:jvm-tasks:1.2.4 6 | com.hierynomus:sshj:0.23.0 7 | com.jcraft:jzlib:1.1.3 8 | net.i2p.crypto:eddsa:0.2.0 9 | org.apache.logging.log4j:log4j-api:2.20.0 10 | org.bouncycastle:bcpkix-jdk15on:1.56 11 | org.bouncycastle:bcprov-jdk15on:1.56 12 | org.glassfish:javax.json:1.1 13 | org.jetbrains.kotlin:kotlin-stdlib-common:1.2.70 14 | org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.2.70 15 | org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.70 16 | org.jetbrains.kotlin:kotlin-stdlib:1.2.70 17 | org.jetbrains:annotations:15.0 18 | org.slf4j:slf4j-api:1.7.25 19 | -------------------------------------------------------------------------------- /gradle/dependency-locks/runtimeClasspath.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | com.atlassian.performance.tools:io:1.2.0 5 | com.atlassian.performance.tools:jvm-tasks:1.2.4 6 | com.hierynomus:sshj:0.23.0 7 | com.jcraft:jzlib:1.1.3 8 | net.i2p.crypto:eddsa:0.2.0 9 | org.apache.logging.log4j:log4j-api:2.20.0 10 | org.bouncycastle:bcpkix-jdk15on:1.56 11 | org.bouncycastle:bcprov-jdk15on:1.56 12 | org.glassfish:javax.json:1.1 13 | org.jetbrains.kotlin:kotlin-stdlib-common:1.2.70 14 | org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.2.70 15 | org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.70 16 | org.jetbrains.kotlin:kotlin-stdlib:1.2.70 17 | org.jetbrains:annotations:15.0 18 | org.slf4j:slf4j-api:1.7.25 19 | -------------------------------------------------------------------------------- /gradle/dependency-locks/apiDependenciesMetadata.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | com.atlassian.performance.tools:io:1.2.0 5 | com.atlassian.performance.tools:jvm-tasks:1.2.4 6 | com.hierynomus:sshj:0.23.0 7 | com.jcraft:jzlib:1.1.3 8 | net.i2p.crypto:eddsa:0.2.0 9 | org.apache.logging.log4j:log4j-api:2.20.0 10 | org.bouncycastle:bcpkix-jdk15on:1.56 11 | org.bouncycastle:bcprov-jdk15on:1.56 12 | org.glassfish:javax.json:1.1 13 | org.jetbrains.kotlin:kotlin-stdlib-common:1.2.70 14 | org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.2.70 15 | org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.70 16 | org.jetbrains.kotlin:kotlin-stdlib:1.2.70 17 | org.jetbrains:annotations:15.0 18 | org.slf4j:slf4j-api:1.7.25 19 | -------------------------------------------------------------------------------- /gradle/dependency-locks/implementationDependenciesMetadata.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | com.atlassian.performance.tools:io:1.2.0 5 | com.atlassian.performance.tools:jvm-tasks:1.2.4 6 | com.hierynomus:sshj:0.23.0 7 | com.jcraft:jzlib:1.1.3 8 | net.i2p.crypto:eddsa:0.2.0 9 | org.apache.logging.log4j:log4j-api:2.20.0 10 | org.bouncycastle:bcpkix-jdk15on:1.56 11 | org.bouncycastle:bcprov-jdk15on:1.56 12 | org.glassfish:javax.json:1.1 13 | org.jetbrains.kotlin:kotlin-stdlib-common:1.2.70 14 | org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.2.70 15 | org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.70 16 | org.jetbrains.kotlin:kotlin-stdlib:1.2.70 17 | org.jetbrains:annotations:15.0 18 | org.slf4j:slf4j-api:1.7.25 19 | -------------------------------------------------------------------------------- /src/main/kotlin/com/atlassian/performance/tools/ssh/api/BackgroundProcess.kt: -------------------------------------------------------------------------------- 1 | package com.atlassian.performance.tools.ssh.api 2 | 3 | import java.time.Duration 4 | 5 | /** 6 | * Runs in the background. Is independent of `SshConnection`s being closed. 7 | * Can be used for commands, which will not stop on their own, e.g. `tail -f`, `ping`, `top`, etc. 8 | * @since 2.4.0 9 | */ 10 | interface BackgroundProcess : AutoCloseable { 11 | 12 | /** 13 | * Interrupts the process, then waits up to [timeout] for its completion. 14 | * Skips the interrupt if the process is already finished. 15 | * Throws if getting the [SshConnection.SshResult] fails. 16 | * Closes the open resources. 17 | * 18 | * @return the result of the stopped process, could have a non-zero exit code 19 | */ 20 | fun stop(timeout: Duration): SshConnection.SshResult 21 | } 22 | -------------------------------------------------------------------------------- /src/main/kotlin/com/atlassian/performance/tools/ssh/api/auth/PasswordAuthentication.kt: -------------------------------------------------------------------------------- 1 | package com.atlassian.performance.tools.ssh.api.auth 2 | 3 | import net.schmizz.sshj.SSHClient 4 | import javax.json.Json 5 | import javax.json.JsonObject 6 | 7 | data class PasswordAuthentication(private val password: String) : SshAuthentication() { 8 | internal companion object { 9 | const val TYPE = "password" 10 | } 11 | 12 | internal constructor(json: JsonObject) : 13 | this(json.getString("value")) 14 | 15 | 16 | override fun toJson(): JsonObject { 17 | return Json.createObjectBuilder() 18 | .add("type", TYPE) 19 | .add("value", password) 20 | .build() 21 | } 22 | 23 | override fun authenticate(userName: String, sshClient: SSHClient) { 24 | sshClient.authPassword(userName, password) 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/atlassian/performance/tools/ssh/api/auth/SshAuthentication.kt: -------------------------------------------------------------------------------- 1 | package com.atlassian.performance.tools.ssh.api.auth 2 | 3 | import net.schmizz.sshj.SSHClient 4 | import javax.json.JsonObject 5 | 6 | abstract class SshAuthentication internal constructor() { 7 | internal abstract fun authenticate(userName: String, sshClient: SSHClient) 8 | internal abstract fun toJson(): JsonObject 9 | 10 | internal companion object { 11 | fun fromJson( 12 | json: JsonObject 13 | ): SshAuthentication { 14 | return when (json.getString("type")) { 15 | PasswordAuthentication.TYPE -> PasswordAuthentication(json) 16 | PublicKeyAuthentication.TYPE -> PublicKeyAuthentication(json) 17 | else -> { 18 | throw IllegalStateException("Unknown authentication type ${json}") 19 | } 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/atlassian/performance/tools/ssh/api/auth/PublicKeyAuthentication.kt: -------------------------------------------------------------------------------- 1 | package com.atlassian.performance.tools.ssh.api.auth 2 | 3 | import net.schmizz.sshj.SSHClient 4 | import java.nio.file.Path 5 | import java.nio.file.Paths 6 | import javax.json.Json 7 | import javax.json.JsonObject 8 | 9 | data class PublicKeyAuthentication(internal val key: Path) : SshAuthentication() { 10 | companion object { 11 | const val TYPE = "public-key" 12 | } 13 | 14 | internal constructor(json: JsonObject) : 15 | this(Paths.get(json.getString("value"))) 16 | 17 | override fun toJson(): JsonObject { 18 | return Json.createObjectBuilder() 19 | .add("type", "public-key") 20 | .add("value", key.toAbsolutePath().toString()) 21 | .build() 22 | } 23 | 24 | override fun authenticate(userName: String, sshClient: SSHClient) { 25 | sshClient.authPublickey(userName, key.toString()) 26 | } 27 | } -------------------------------------------------------------------------------- /gradle/dependency-locks/testCompileClasspath.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | com.atlassian.performance.tools:io:1.2.0 5 | com.atlassian.performance.tools:jvm-tasks:1.2.4 6 | com.atlassian.performance.tools:ssh-ubuntu:0.1.0 7 | com.hierynomus:sshj:0.23.0 8 | com.jcraft:jzlib:1.1.3 9 | junit:junit:4.12 10 | net.i2p.crypto:eddsa:0.2.0 11 | org.apache.logging.log4j:log4j-api:2.20.0 12 | org.apache.logging.log4j:log4j-core:2.20.0 13 | org.bouncycastle:bcpkix-jdk15on:1.56 14 | org.bouncycastle:bcprov-jdk15on:1.56 15 | org.glassfish:javax.json:1.1 16 | org.hamcrest:hamcrest-core:1.3 17 | org.jetbrains.kotlin:kotlin-stdlib-common:1.2.70 18 | org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.2.70 19 | org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.70 20 | org.jetbrains.kotlin:kotlin-stdlib:1.2.70 21 | org.jetbrains:annotations:15.0 22 | org.slf4j:slf4j-api:1.7.25 23 | -------------------------------------------------------------------------------- /src/test/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/main/kotlin/com/atlassian/performance/tools/ssh/PerformanceDefaultConfig.kt: -------------------------------------------------------------------------------- 1 | package com.atlassian.performance.tools.ssh 2 | 3 | import net.schmizz.sshj.DefaultConfig 4 | import net.schmizz.sshj.transport.random.BouncyCastleRandom 5 | import net.schmizz.sshj.transport.random.JCERandom 6 | import net.schmizz.sshj.transport.random.Random 7 | import net.schmizz.sshj.transport.random.SingletonRandomFactory 8 | import net.schmizz.sshj.common.Factory 9 | 10 | internal class PerformanceDefaultConfig : DefaultConfig() { 11 | companion object { 12 | val bcFactory = MemoizingFactory(BouncyCastleRandom.Factory()) 13 | val jceFactory = MemoizingFactory(JCERandom.Factory()) 14 | } 15 | override fun initRandomFactory(bouncyCastleRegistered: Boolean) { 16 | randomFactory = SingletonRandomFactory(if (bouncyCastleRegistered) bcFactory else jceFactory) 17 | } 18 | 19 | class MemoizingFactory(private val factory: Factory) : Factory { 20 | val random : Random by lazy { factory.create() } 21 | override fun create(): Random { 22 | return random 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/atlassian/performance/tools/ssh/port/RemotePort.kt: -------------------------------------------------------------------------------- 1 | package com.atlassian.performance.tools.ssh.port 2 | 3 | import net.schmizz.sshj.SSHClient 4 | import net.schmizz.sshj.connection.channel.forwarded.RemotePortForwarder 5 | import net.schmizz.sshj.connection.channel.forwarded.SocketForwardingConnectListener 6 | import java.net.InetSocketAddress 7 | 8 | internal class RemotePort( 9 | private val remotePort: Int 10 | ) { 11 | 12 | internal fun forward(sshClient: SSHClient, localPort: Int): AutoCloseable { 13 | val remotePortForwarder = sshClient.remotePortForwarder 14 | val forward = remotePortForwarder.bind( 15 | RemotePortForwarder.Forward(remotePort), 16 | SocketForwardingConnectListener(InetSocketAddress("localhost", localPort)) 17 | ) 18 | return AutoCloseable { 19 | sshClient.remotePortForwarder.cancel(forward) 20 | sshClient.disconnect() 21 | } 22 | } 23 | 24 | internal companion object { 25 | internal fun create(port: Int): RemotePort { 26 | return RemotePort(port) 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/atlassian/performance/tools/ssh/api/SshHostTest.kt: -------------------------------------------------------------------------------- 1 | package com.atlassian.performance.tools.ssh.api 2 | 3 | import com.atlassian.performance.tools.ssh.api.auth.PasswordAuthentication 4 | import com.atlassian.performance.tools.ssh.api.auth.PublicKeyAuthentication 5 | import org.junit.Assert 6 | import org.junit.Test 7 | import java.nio.file.Paths 8 | 9 | class SshHostTest { 10 | 11 | @Test 12 | fun shouldSerializeToJsonWithPassword() { 13 | val sshHost = SshHost( 14 | "127.0.0.1", 15 | "name", 16 | PasswordAuthentication("password"), 17 | 22 18 | ) 19 | 20 | val sshHostFromJson = SshHost(sshHost.toJson()) 21 | 22 | Assert.assertEquals(sshHost, sshHostFromJson) 23 | } 24 | 25 | @Test 26 | fun shouldSerializeToJsonWithKey() { 27 | val sshHost = SshHost( 28 | "127.0.0.1", 29 | "name", 30 | PublicKeyAuthentication(Paths.get("/public/key")), 31 | 22 32 | ) 33 | 34 | val sshHostFromJson = SshHost(sshHost.toJson()) 35 | 36 | Assert.assertEquals(sshHost, sshHostFromJson) 37 | } 38 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/atlassian/performance/tools/ssh/api/SshContainer.kt: -------------------------------------------------------------------------------- 1 | package com.atlassian.performance.tools.ssh.api 2 | 3 | 4 | import com.atlassian.performance.tools.ssh.api.auth.PublicKeyAuthentication 5 | import com.atlassian.performance.tools.sshubuntu.api.SshUbuntuContainer 6 | 7 | internal class SshContainer { 8 | internal fun useConnection(action: (sshConnection: SshConnection) -> Unit) { 9 | SshUbuntuContainer().start().use { sshUbuntu -> 10 | return@use Ssh( 11 | sshUbuntu.ssh.toSshHost() 12 | ).newConnection() 13 | .use { action(it) } 14 | } 15 | } 16 | 17 | internal fun useSsh(action: (ssh: Ssh) -> Unit) { 18 | SshUbuntuContainer().start().use { sshUbuntu -> 19 | action(Ssh(sshUbuntu.ssh.toSshHost())) 20 | } 21 | } 22 | 23 | 24 | private fun com.atlassian.performance.tools.sshubuntu.api.SshHost.toSshHost(): SshHost { 25 | return SshHost( 26 | ipAddress = this.ipAddress, 27 | userName = this.userName, 28 | authentication = PublicKeyAuthentication(key = this.privateKey), 29 | port = this.port 30 | ) 31 | } 32 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![CI](https://github.com/atlassian/ssh/workflows/CI/badge.svg) 2 | 3 | ## SSH 4 | You can use SSH lib to connect with an SSH server and execute remote commands. 5 | The lib extends [sshj](https://github.com/hierynomus/sshj/releases/tag/v0.23.0) and gives a higher level API. 6 | 7 | ## Features 8 | - connect to remote SSH servers even if they're in progress of starting up 9 | - execute remote ssh commands 10 | - print error and output messages to logs 11 | - fetch output of the command 12 | - create and work with detached processes 13 | - download files from the remote server 14 | 15 | ## Requirements 16 | - JRE 8 - 11 17 | - running SSH server running on port 22 18 | - private key, username, and IP that can be used to connect to the remote server 19 | 20 | ## Reporting issues 21 | 22 | We track all the changes in a [public issue tracker](https://ecosystem.atlassian.net/secure/RapidBoard.jspa?rapidView=457&projectKey=JPERF). 23 | All the suggestions and bug reports are welcome. 24 | 25 | ## Contributing 26 | 27 | See [CONTRIBUTING.md](CONTRIBUTING.md). 28 | 29 | ## License 30 | Copyright (c) 2018 Atlassian and others. 31 | Apache 2.0 licensed, see [LICENSE.txt](LICENSE.txt) file. 32 | -------------------------------------------------------------------------------- /src/test/kotlin/com/atlassian/performance/tools/ssh/port/LocalPortTest.kt: -------------------------------------------------------------------------------- 1 | package com.atlassian.performance.tools.ssh.port 2 | 3 | import com.atlassian.performance.tools.ssh.api.Ssh 4 | import com.atlassian.performance.tools.ssh.api.SshContainer 5 | import com.atlassian.performance.tools.ssh.api.SshHost 6 | import com.atlassian.performance.tools.ssh.api.auth.PublicKeyAuthentication 7 | import org.junit.Assert 8 | import org.junit.Test 9 | 10 | class LocalPortTest { 11 | 12 | @Test 13 | fun shouldForwardLocalPorts() { 14 | SshContainer().useSsh { ssh -> 15 | val localPort = 8022 16 | ssh.forwardLocalPort( 17 | localPort = localPort, 18 | remotePort = 22 19 | ).use { 20 | val result = Ssh( 21 | SshHost( 22 | ipAddress = "127.0.0.1", 23 | userName = ssh.host.userName, 24 | authentication = PublicKeyAuthentication(ssh.host.key), 25 | port = localPort 26 | ) 27 | ).newConnection() 28 | .use { it.execute("echo test") } 29 | 30 | Assert.assertEquals(true, result.isSuccessful()) 31 | } 32 | } 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/atlassian/performance/tools/ssh/port/LocalPort.kt: -------------------------------------------------------------------------------- 1 | package com.atlassian.performance.tools.ssh.port 2 | 3 | import net.schmizz.sshj.SSHClient 4 | import net.schmizz.sshj.connection.channel.direct.LocalPortForwarder 5 | import java.net.InetSocketAddress 6 | import java.net.ServerSocket 7 | import kotlin.concurrent.thread 8 | 9 | internal class LocalPort( 10 | private val localPort: Int 11 | ) { 12 | 13 | internal fun forward(sshClient: SSHClient, remotePort: Int): AutoCloseable { 14 | val params = LocalPortForwarder.Parameters("localhost", localPort, sshClient.remoteHostname, remotePort) 15 | val serverSocket = ServerSocket() 16 | serverSocket.reuseAddress = true 17 | serverSocket.bind(InetSocketAddress(params.localHost, params.localPort)) 18 | val localPortForwarder = sshClient.newLocalPortForwarder(params, serverSocket) 19 | thread( 20 | isDaemon = true, 21 | name = "forwarding-local-localPort-$localPort-to-remote-localPort-$remotePort" 22 | ) { 23 | localPortForwarder.listen() 24 | } 25 | return AutoCloseable { 26 | serverSocket.close() 27 | localPortForwarder.close() 28 | sshClient.disconnect() 29 | } 30 | } 31 | 32 | internal companion object { 33 | internal fun create(port: Int): LocalPort { 34 | return LocalPort(port) 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /gradle/dependency-locks/testCompile.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | com.atlassian.performance.tools:io:1.2.0 5 | com.atlassian.performance.tools:jvm-tasks:1.2.4 6 | com.atlassian.performance.tools:ssh-ubuntu:0.1.0 7 | com.hierynomus:sshj:0.23.0 8 | com.jcraft:jzlib:1.1.3 9 | com.kohlschutter.junixsocket:junixsocket-common:2.0.4 10 | com.kohlschutter.junixsocket:junixsocket-native-common:2.0.4 11 | javax.activation:javax.activation-api:1.2.0 12 | javax.annotation:javax.annotation-api:1.3.2 13 | javax.xml.bind:jaxb-api:2.3.1 14 | junit:junit:4.12 15 | net.i2p.crypto:eddsa:0.2.0 16 | net.java.dev.jna:jna-platform:5.2.0 17 | net.java.dev.jna:jna:5.2.0 18 | org.apache.commons:commons-compress:1.18 19 | org.apache.logging.log4j:log4j-api:2.20.0 20 | org.apache.logging.log4j:log4j-core:2.20.0 21 | org.apache.logging.log4j:log4j-slf4j-impl:2.20.0 22 | org.bouncycastle:bcpkix-jdk15on:1.56 23 | org.bouncycastle:bcprov-jdk15on:1.56 24 | org.glassfish:javax.json:1.1 25 | org.hamcrest:hamcrest-core:1.3 26 | org.jetbrains.kotlin:kotlin-stdlib-common:1.2.70 27 | org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.2.70 28 | org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.70 29 | org.jetbrains.kotlin:kotlin-stdlib:1.2.70 30 | org.jetbrains:annotations:15.0 31 | org.rnorth.duct-tape:duct-tape:1.0.7 32 | org.rnorth.visible-assertions:visible-assertions:2.1.2 33 | org.rnorth:tcp-unix-socket-proxy:1.0.2 34 | org.scijava:native-lib-loader:2.0.2 35 | org.slf4j:slf4j-api:1.7.25 36 | org.testcontainers:testcontainers:1.10.5 37 | -------------------------------------------------------------------------------- /gradle/dependency-locks/testRuntime.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | com.atlassian.performance.tools:io:1.2.0 5 | com.atlassian.performance.tools:jvm-tasks:1.2.4 6 | com.atlassian.performance.tools:ssh-ubuntu:0.1.0 7 | com.hierynomus:sshj:0.23.0 8 | com.jcraft:jzlib:1.1.3 9 | com.kohlschutter.junixsocket:junixsocket-common:2.0.4 10 | com.kohlschutter.junixsocket:junixsocket-native-common:2.0.4 11 | javax.activation:javax.activation-api:1.2.0 12 | javax.annotation:javax.annotation-api:1.3.2 13 | javax.xml.bind:jaxb-api:2.3.1 14 | junit:junit:4.12 15 | net.i2p.crypto:eddsa:0.2.0 16 | net.java.dev.jna:jna-platform:5.2.0 17 | net.java.dev.jna:jna:5.2.0 18 | org.apache.commons:commons-compress:1.18 19 | org.apache.logging.log4j:log4j-api:2.20.0 20 | org.apache.logging.log4j:log4j-core:2.20.0 21 | org.apache.logging.log4j:log4j-slf4j-impl:2.20.0 22 | org.bouncycastle:bcpkix-jdk15on:1.56 23 | org.bouncycastle:bcprov-jdk15on:1.56 24 | org.glassfish:javax.json:1.1 25 | org.hamcrest:hamcrest-core:1.3 26 | org.jetbrains.kotlin:kotlin-stdlib-common:1.2.70 27 | org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.2.70 28 | org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.70 29 | org.jetbrains.kotlin:kotlin-stdlib:1.2.70 30 | org.jetbrains:annotations:15.0 31 | org.rnorth.duct-tape:duct-tape:1.0.7 32 | org.rnorth.visible-assertions:visible-assertions:2.1.2 33 | org.rnorth:tcp-unix-socket-proxy:1.0.2 34 | org.scijava:native-lib-loader:2.0.2 35 | org.slf4j:slf4j-api:1.7.25 36 | org.testcontainers:testcontainers:1.10.5 37 | -------------------------------------------------------------------------------- /gradle/dependency-locks/testRuntimeClasspath.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | com.atlassian.performance.tools:io:1.2.0 5 | com.atlassian.performance.tools:jvm-tasks:1.2.4 6 | com.atlassian.performance.tools:ssh-ubuntu:0.1.0 7 | com.hierynomus:sshj:0.23.0 8 | com.jcraft:jzlib:1.1.3 9 | com.kohlschutter.junixsocket:junixsocket-common:2.0.4 10 | com.kohlschutter.junixsocket:junixsocket-native-common:2.0.4 11 | javax.activation:javax.activation-api:1.2.0 12 | javax.annotation:javax.annotation-api:1.3.2 13 | javax.xml.bind:jaxb-api:2.3.1 14 | junit:junit:4.12 15 | net.i2p.crypto:eddsa:0.2.0 16 | net.java.dev.jna:jna-platform:5.2.0 17 | net.java.dev.jna:jna:5.2.0 18 | org.apache.commons:commons-compress:1.18 19 | org.apache.logging.log4j:log4j-api:2.20.0 20 | org.apache.logging.log4j:log4j-core:2.20.0 21 | org.apache.logging.log4j:log4j-slf4j-impl:2.20.0 22 | org.bouncycastle:bcpkix-jdk15on:1.56 23 | org.bouncycastle:bcprov-jdk15on:1.56 24 | org.glassfish:javax.json:1.1 25 | org.hamcrest:hamcrest-core:1.3 26 | org.jetbrains.kotlin:kotlin-stdlib-common:1.2.70 27 | org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.2.70 28 | org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.70 29 | org.jetbrains.kotlin:kotlin-stdlib:1.2.70 30 | org.jetbrains:annotations:15.0 31 | org.rnorth.duct-tape:duct-tape:1.0.7 32 | org.rnorth.visible-assertions:visible-assertions:2.1.2 33 | org.rnorth:tcp-unix-socket-proxy:1.0.2 34 | org.scijava:native-lib-loader:2.0.2 35 | org.slf4j:slf4j-api:1.7.25 36 | org.testcontainers:testcontainers:1.10.5 37 | -------------------------------------------------------------------------------- /gradle/dependency-locks/testApiDependenciesMetadata.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | com.atlassian.performance.tools:io:1.2.0 5 | com.atlassian.performance.tools:jvm-tasks:1.2.4 6 | com.atlassian.performance.tools:ssh-ubuntu:0.1.0 7 | com.hierynomus:sshj:0.23.0 8 | com.jcraft:jzlib:1.1.3 9 | com.kohlschutter.junixsocket:junixsocket-common:2.0.4 10 | com.kohlschutter.junixsocket:junixsocket-native-common:2.0.4 11 | javax.activation:javax.activation-api:1.2.0 12 | javax.annotation:javax.annotation-api:1.3.2 13 | javax.xml.bind:jaxb-api:2.3.1 14 | junit:junit:4.12 15 | net.i2p.crypto:eddsa:0.2.0 16 | net.java.dev.jna:jna-platform:5.2.0 17 | net.java.dev.jna:jna:5.2.0 18 | org.apache.commons:commons-compress:1.18 19 | org.apache.logging.log4j:log4j-api:2.20.0 20 | org.apache.logging.log4j:log4j-core:2.20.0 21 | org.apache.logging.log4j:log4j-slf4j-impl:2.20.0 22 | org.bouncycastle:bcpkix-jdk15on:1.56 23 | org.bouncycastle:bcprov-jdk15on:1.56 24 | org.glassfish:javax.json:1.1 25 | org.hamcrest:hamcrest-core:1.3 26 | org.jetbrains.kotlin:kotlin-stdlib-common:1.2.70 27 | org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.2.70 28 | org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.70 29 | org.jetbrains.kotlin:kotlin-stdlib:1.2.70 30 | org.jetbrains:annotations:15.0 31 | org.rnorth.duct-tape:duct-tape:1.0.7 32 | org.rnorth.visible-assertions:visible-assertions:2.1.2 33 | org.rnorth:tcp-unix-socket-proxy:1.0.2 34 | org.scijava:native-lib-loader:2.0.2 35 | org.slf4j:slf4j-api:1.7.25 36 | org.testcontainers:testcontainers:1.10.5 37 | -------------------------------------------------------------------------------- /gradle/dependency-locks/testImplementationDependenciesMetadata.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | com.atlassian.performance.tools:io:1.2.0 5 | com.atlassian.performance.tools:jvm-tasks:1.2.4 6 | com.atlassian.performance.tools:ssh-ubuntu:0.1.0 7 | com.hierynomus:sshj:0.23.0 8 | com.jcraft:jzlib:1.1.3 9 | com.kohlschutter.junixsocket:junixsocket-common:2.0.4 10 | com.kohlschutter.junixsocket:junixsocket-native-common:2.0.4 11 | javax.activation:javax.activation-api:1.2.0 12 | javax.annotation:javax.annotation-api:1.3.2 13 | javax.xml.bind:jaxb-api:2.3.1 14 | junit:junit:4.12 15 | net.i2p.crypto:eddsa:0.2.0 16 | net.java.dev.jna:jna-platform:5.2.0 17 | net.java.dev.jna:jna:5.2.0 18 | org.apache.commons:commons-compress:1.18 19 | org.apache.logging.log4j:log4j-api:2.20.0 20 | org.apache.logging.log4j:log4j-core:2.20.0 21 | org.apache.logging.log4j:log4j-slf4j-impl:2.20.0 22 | org.bouncycastle:bcpkix-jdk15on:1.56 23 | org.bouncycastle:bcprov-jdk15on:1.56 24 | org.glassfish:javax.json:1.1 25 | org.hamcrest:hamcrest-core:1.3 26 | org.jetbrains.kotlin:kotlin-stdlib-common:1.2.70 27 | org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.2.70 28 | org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.70 29 | org.jetbrains.kotlin:kotlin-stdlib:1.2.70 30 | org.jetbrains:annotations:15.0 31 | org.rnorth.duct-tape:duct-tape:1.0.7 32 | org.rnorth.visible-assertions:visible-assertions:2.1.2 33 | org.rnorth:tcp-unix-socket-proxy:1.0.2 34 | org.scijava:native-lib-loader:2.0.2 35 | org.slf4j:slf4j-api:1.7.25 36 | org.testcontainers:testcontainers:1.10.5 37 | -------------------------------------------------------------------------------- /src/main/kotlin/com/atlassian/performance/tools/ssh/api/DetachedProcess.kt: -------------------------------------------------------------------------------- 1 | package com.atlassian.performance.tools.ssh.api 2 | 3 | import net.schmizz.sshj.connection.channel.direct.Session 4 | import org.apache.logging.log4j.LogManager 5 | import org.apache.logging.log4j.Logger 6 | import java.util.UUID 7 | import java.util.concurrent.TimeUnit 8 | 9 | /** 10 | * Works in the background on a remote system. 11 | * 12 | * @see [SshConnection.stopProcess] 13 | */ 14 | @Deprecated(message = "Use BackgroundProcess instead") 15 | class DetachedProcess private constructor( 16 | private val cmd: String, 17 | private val uuid: UUID 18 | ) { 19 | private val logger: Logger = LogManager.getLogger(this::class.java) 20 | 21 | internal companion object { 22 | private val logger: Logger = LogManager.getLogger(this::class.java) 23 | private val dir = "~/.jpt-processes" 24 | 25 | fun start(cmd: String, session: Session): DetachedProcess { 26 | val uuid = UUID.randomUUID() 27 | logger.debug("Starting process $uuid $cmd") 28 | session.exec("screen -dm bash -c '${savePID(uuid)} && $cmd'") 29 | .use { command -> command.join(15, TimeUnit.SECONDS) } 30 | @Suppress("DEPRECATION") // used transitively by public API 31 | return DetachedProcess(cmd, uuid) 32 | } 33 | 34 | private fun savePID(uuid: UUID): String = "mkdir -p $dir && echo $$ > $dir/$uuid" 35 | } 36 | 37 | internal fun stop(session: Session) { 38 | logger.debug("Stopping process $uuid $cmd") 39 | session.exec("kill -3 `cat $dir/$uuid`") 40 | .use { command -> command.join(15, TimeUnit.SECONDS) } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | inputs: 8 | release: 9 | description: 'Release? yes/no' 10 | default: 'no' 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 0 20 | - name: Cache Gradle 21 | uses: actions/cache@v2 22 | with: 23 | path: ~/.gradle 24 | key: ${{ runner.os }}-${{ hashFiles('gradle') }} 25 | - name: Build 26 | run: ./gradlew build 27 | - name: Upload test reports 28 | if: always() 29 | uses: actions/upload-artifact@v2 30 | with: 31 | name: test-reports 32 | path: build/reports/tests 33 | release: 34 | runs-on: ubuntu-latest 35 | permissions: 36 | contents: write 37 | id-token: write 38 | needs: build 39 | if: github.event.inputs.release == 'yes' 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v2 43 | with: 44 | fetch-depth: 0 45 | - name: Cache Gradle 46 | uses: actions/cache@v2 47 | with: 48 | path: ~/.gradle 49 | key: ${{ runner.os }}-${{ hashFiles('gradle') }} 50 | - name: Get publish token 51 | id: publish-token 52 | uses: atlassian-labs/artifact-publish-token@v1.0.1 53 | - name: Release 54 | env: 55 | atlassian_private_username: ${{ steps.publish-token.outputs.artifactoryUsername }} 56 | atlassian_private_password: ${{ steps.publish-token.outputs.artifactoryApiKey }} 57 | run: | 58 | ./gradlew release \ 59 | -Prelease.customUsername=${{ github.actor }} \ 60 | -Prelease.customPassword=${{ github.token }} 61 | ./gradlew publish 62 | -------------------------------------------------------------------------------- /src/test/kotlin/com/atlassian/performance/tools/ssh/port/RemotePortTest.kt: -------------------------------------------------------------------------------- 1 | package com.atlassian.performance.tools.ssh.port 2 | 3 | import com.sun.net.httpserver.HttpHandler 4 | import com.sun.net.httpserver.HttpServer 5 | import org.junit.Assert 6 | import org.junit.Test 7 | import java.net.InetSocketAddress 8 | import java.util.concurrent.Executor 9 | import kotlin.concurrent.thread 10 | import com.atlassian.performance.tools.ssh.api.SshContainer 11 | 12 | class RemotePortTest { 13 | 14 | @Test 15 | fun shouldForwardRemotePorts() { 16 | val localPort = 8866 17 | val remotePort = 8877 18 | val message = "hello" 19 | val server = HttpServer.create(InetSocketAddress(localPort), 0) 20 | server.executor = Executor { runnable -> 21 | thread(isDaemon = true) { 22 | runnable.run() 23 | } 24 | } 25 | server.createContext("/").handler = HttpHandler { exchange -> 26 | exchange.sendResponseHeaders(200, message.toByteArray().size.toLong()) 27 | val outputStream = exchange.responseBody 28 | outputStream.write(message.toByteArray()) 29 | outputStream.close() 30 | } 31 | server.start() 32 | 33 | SshContainer().useSsh { ssh -> 34 | ssh.forwardRemotePort( 35 | localPort = localPort, 36 | remotePort = remotePort 37 | ).use { 38 | val result = ssh 39 | .newConnection() 40 | .use { 41 | it.execute("""wget -q -O - localhost:$remotePort/""") 42 | } 43 | 44 | Assert.assertEquals(true, result.isSuccessful()) 45 | Assert.assertEquals(message, result.output) 46 | } 47 | 48 | } 49 | server.stop(0) 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/atlassian/performance/tools/ssh/SshjBackgroundProcess.kt: -------------------------------------------------------------------------------- 1 | package com.atlassian.performance.tools.ssh 2 | 3 | import com.atlassian.performance.tools.ssh.api.BackgroundProcess 4 | import com.atlassian.performance.tools.ssh.api.SshConnection 5 | import net.schmizz.sshj.connection.channel.direct.Session 6 | import org.apache.logging.log4j.Level 7 | import org.apache.logging.log4j.LogManager 8 | import java.time.Duration 9 | import java.util.concurrent.atomic.AtomicBoolean 10 | 11 | internal class SshjBackgroundProcess( 12 | private val session: Session, 13 | private val command: Session.Command 14 | ) : BackgroundProcess { 15 | 16 | private var closed = AtomicBoolean(false) 17 | 18 | override fun stop(timeout: Duration): SshConnection.SshResult { 19 | tryToInterrupt() 20 | val result = WaitingCommand(command, timeout, Level.DEBUG, Level.DEBUG).waitForResult() 21 | close() 22 | return result 23 | } 24 | 25 | private fun tryToInterrupt() { 26 | try { 27 | sendSigint() 28 | } catch (e: Exception) { 29 | LOG.debug("cannot interrupt, if the command doesn't run anymore, then the write connection is closed", e) 30 | } 31 | } 32 | 33 | /** 34 | * [Session.Command.signal] doesn't work, so send the CTRL-C character rather than SSH-level SIGINT signal. 35 | * [OpenSSH server was not supporting this standard](https://bugzilla.mindrot.org/show_bug.cgi?id=1424). 36 | * It's supported since 7.9p1 (late 2018), but our test Ubuntu still runs on 7.6p1. 37 | */ 38 | private fun sendSigint() { 39 | val ctrlC = 3 40 | command.outputStream.write(ctrlC); 41 | command.outputStream.flush(); 42 | } 43 | 44 | override fun close() { 45 | if (!closed.getAndSet(true)) { 46 | command.use {} 47 | session.use {} 48 | } 49 | } 50 | 51 | private companion object { 52 | private val LOG = LogManager.getLogger(this::class.java) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for taking the time to contribute! 4 | 5 | The following is a set of guidelines for contributing to [Ssh](README.md). 6 | All the changes are welcome. Please help us to improve code, examples and documentation. 7 | 8 | ## Developer’s environment 9 | 10 | You can build and run JPT on MacOS, Windows or Linux. You'll need JDK 8-11 and docker 1.6+ to build and test the project. 11 | 12 | ## Submitting changes 13 | 14 | Pull requests, issues and comments are welcome. For pull requests: 15 | 16 | - Add tests for new features and bug fixes 17 | - Follow the existing style 18 | - Separate unrelated changes into multiple pull requests 19 | 20 | See the [existing issues](https://ecosystem.atlassian.net/projects/JPERF/issues/?filter=allissues) for things to start contributing. 21 | 22 | For bigger changes, make sure you start a discussion first by creating 23 | an issue and explaining the intended change. 24 | 25 | All the pull requests and other changes will be accepted and merged by Atlassians. 26 | 27 | Atlassian requires contributors to sign a Contributor License Agreement, 28 | known as a CLA. This serves as a record stating that the contributor is 29 | entitled to contribute the code/documentation/translation to the project 30 | and is willing to have it used in distributions and derivative works 31 | (or is willing to transfer ownership). 32 | 33 | Prior to accepting your contributions we ask that you please follow the appropriate 34 | link below to digitally sign the CLA. The Corporate CLA is for those who are 35 | contributing as a member of an organization and the individual CLA is for 36 | those contributing as an individual. 37 | 38 | * [CLA for corporate contributors](https://opensource.atlassian.com/corporate) 39 | * [CLA for individuals](https://opensource.atlassian.com/individual) 40 | 41 | ## Style Guide / Coding conventions 42 | 43 | [Git commit messages](https://chris.beams.io/posts/git-commit/) 44 | 45 | ## Releasing 46 | 47 | Versioning, releasing and distribution are managed by the [gradle-release] plugin. 48 | 49 | [gradle-release]: https://bitbucket.org/atlassian/gradle-release/src/release-0.5.0/README.md -------------------------------------------------------------------------------- /src/test/kotlin/com/atlassian/performance/tools/ssh/api/SshTest.kt: -------------------------------------------------------------------------------- 1 | package com.atlassian.performance.tools.ssh.api 2 | 3 | import org.junit.Assert 4 | import org.junit.Test 5 | import java.time.Duration 6 | import kotlin.system.measureTimeMillis 7 | 8 | class SshTest { 9 | 10 | @Test 11 | fun shouldDetachProcess() { 12 | SshContainer().useSsh { sshHost -> 13 | installPing(sshHost) 14 | 15 | val ping = sshHost.newConnection().use { ssh -> 16 | @Suppress("DEPRECATION") // tests public API 17 | ssh.startProcess("ping localhost") 18 | } 19 | sshHost.newConnection().use { ssh -> 20 | @Suppress("DEPRECATION") // tests public API 21 | ssh.stopProcess(ping) 22 | } 23 | } 24 | } 25 | 26 | @Test 27 | fun shouldNotWaitForBackground() { 28 | SshContainer().useSsh { sshHost -> 29 | val runMillis = measureTimeMillis { 30 | sshHost.runInBackground("sleep 8") 31 | } 32 | 33 | Assert.assertTrue(runMillis < 1000) 34 | } 35 | } 36 | 37 | @Test 38 | fun shouldGetBackgroundResults() { 39 | SshContainer().useSsh { sshHost -> 40 | installPing(sshHost) 41 | 42 | val ping = sshHost.runInBackground("ping localhost") 43 | Thread.sleep(2000) 44 | // meanwhile we can create and kill connections 45 | sshHost.newConnection().use { it.safeExecute("ls") } 46 | Thread.sleep(2000) 47 | val pingResult = ping.stop(Duration.ofMillis(20)) 48 | 49 | Assert.assertTrue(pingResult.isSuccessful()) 50 | Assert.assertTrue(pingResult.output.contains("localhost ping statistics")) 51 | } 52 | } 53 | 54 | @Test 55 | fun shouldTolerateEarlyFinish() { 56 | SshContainer().useSsh { sshHost -> 57 | installPing(sshHost) 58 | 59 | val fail = sshHost.runInBackground("nonexistent-command") 60 | val failResult = fail.stop(Duration.ofMillis(20)) 61 | 62 | Assert.assertEquals(127, failResult.exitStatus) 63 | } 64 | } 65 | 66 | private fun installPing(sshHost: Ssh) { 67 | sshHost.newConnection().use { it.execute("apt-get update -qq && apt-get install iputils-ping -y") } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/kotlin/com/atlassian/performance/tools/ssh/api/SshHost.kt: -------------------------------------------------------------------------------- 1 | package com.atlassian.performance.tools.ssh.api 2 | 3 | import com.atlassian.performance.tools.ssh.api.auth.PublicKeyAuthentication 4 | import com.atlassian.performance.tools.ssh.api.auth.SshAuthentication 5 | import java.nio.file.Path 6 | import javax.json.Json 7 | import javax.json.JsonObject 8 | 9 | /** 10 | * Holds SSH coordinates. 11 | * 12 | * @param ipAddress IP of the remote system. 13 | * @param userName User allowed to connect to the remote server. 14 | * @param authentication Private SSH authentication method for the user. 15 | * @param port Port of the remote system. 16 | */ 17 | data class SshHost( 18 | val ipAddress: String, 19 | val userName: String, 20 | val authentication: SshAuthentication, 21 | val port: Int 22 | ) { 23 | constructor(json: JsonObject) : this( 24 | ipAddress = json.getString("ipAddress"), 25 | userName = json.getString("userName"), 26 | authentication = SshAuthentication.fromJson(json.getJsonObject("authentication")), 27 | port = json.getInt("port") 28 | ) 29 | 30 | @Deprecated( 31 | message = "Use the primary constructor" 32 | ) 33 | constructor( 34 | ipAddress: String, 35 | userName: String, 36 | key: Path, 37 | port: Int 38 | ) : this( 39 | ipAddress = ipAddress, 40 | userName = userName, 41 | authentication = PublicKeyAuthentication(key), 42 | port = port 43 | ) 44 | 45 | constructor( 46 | ipAddress: String, 47 | userName: String, 48 | key: Path 49 | ) : this( 50 | ipAddress = ipAddress, 51 | userName = userName, 52 | authentication = PublicKeyAuthentication(key), 53 | port = 22 54 | ) 55 | 56 | val key: Path 57 | get() { 58 | if (authentication is PublicKeyAuthentication) { 59 | return authentication.key 60 | } else { 61 | throw Exception("The authentication used by this host does not use public key auth.") 62 | } 63 | } 64 | 65 | fun toJson(): JsonObject { 66 | return Json.createObjectBuilder() 67 | .add("ipAddress", ipAddress) 68 | .add("userName", userName) 69 | .add("port", port) 70 | .add("authentication", authentication.toJson()) 71 | .build() 72 | } 73 | } -------------------------------------------------------------------------------- /src/main/java/net/schmizz/sshj/connection/channel/SocketStreamCopyMonitor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C)2009 - SSHJ Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package net.schmizz.sshj.connection.channel; 17 | 18 | import net.schmizz.concurrent.Event; 19 | import net.schmizz.sshj.common.IOUtils; 20 | 21 | import java.io.IOException; 22 | import java.net.Socket; 23 | import java.util.concurrent.TimeUnit; 24 | 25 | import static com.hierynomus.sshj.backport.Sockets.asCloseable; 26 | 27 | /** 28 | * Hack for https://github.com/hierynomus/sshj/issues/317. 29 | * The PR (https://github.com/hierynomus/sshj/pull/491) is already accepted so there's a hope it's only 30 | * a temporary workaround. 31 | */ 32 | public class SocketStreamCopyMonitor 33 | extends Thread { 34 | 35 | private SocketStreamCopyMonitor(Runnable r) { 36 | super(r); 37 | setName("sockmon"); 38 | setDaemon(true); 39 | } 40 | 41 | public static void monitor(final int frequency, final TimeUnit unit, 42 | final Event x, final Event y, 43 | final Channel channel, final Socket socket) { 44 | new SocketStreamCopyMonitor(new Runnable() { 45 | public void run() { 46 | try { 47 | await(x); 48 | await(y); 49 | } catch (IOException ignored) { 50 | } finally { 51 | IOUtils.closeQuietly(channel, asCloseable(socket)); 52 | } 53 | } 54 | 55 | private void await(final Event event) throws IOException { 56 | while(true){ 57 | if(event.tryAwait(frequency, unit)){ 58 | break; 59 | } 60 | } 61 | } 62 | }).start(); 63 | } 64 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. 6 | 7 | Examples of unacceptable behavior by participants include: 8 | 9 | * The use of sexualized language or imagery 10 | * Personal attacks 11 | * Trolling or insulting/derogatory comments 12 | * Public or private harassment 13 | * Publishing other's private information, such as physical or electronic addresses, without explicit permission 14 | * Submitting contributions or comments that you know to violate the intellectual property or privacy rights of others 15 | * Other unethical or unprofessional conduct 16 | 17 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 18 | By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. 19 | 20 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. 21 | 22 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a project maintainer. Complaints will result in a response and be reviewed and investigated in a way that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident. 23 | 24 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.3.0, available at [http://contributor-covenant.org/version/1/3/0/][version] 25 | 26 | [homepage]: http://contributor-covenant.org 27 | [version]: http://contributor-covenant.org/version/1/3/0/ 28 | -------------------------------------------------------------------------------- /src/main/kotlin/com/atlassian/performance/tools/ssh/WaitingCommand.kt: -------------------------------------------------------------------------------- 1 | package com.atlassian.performance.tools.ssh 2 | 3 | import com.atlassian.performance.tools.ssh.api.SshConnection 4 | import net.schmizz.sshj.connection.channel.direct.Session 5 | import org.apache.logging.log4j.Level 6 | import org.apache.logging.log4j.LogManager 7 | import org.apache.logging.log4j.Logger 8 | import java.io.InputStream 9 | import java.time.Duration 10 | import java.time.Instant 11 | import java.util.concurrent.TimeUnit 12 | 13 | internal class WaitingCommand( 14 | private val command: Session.Command, 15 | private val timeout: Duration, 16 | private val stdout: Level, 17 | private val stderr: Level 18 | ) { 19 | 20 | fun waitForResult(): SshConnection.SshResult { 21 | command.waitForCompletion(timeout) 22 | return SshConnection.SshResult( 23 | exitStatus = command.exitStatus, 24 | output = command.inputStream.readAndLog(stdout), 25 | errorOutput = command.errorStream.readAndLog(stderr) 26 | ) 27 | } 28 | 29 | private fun Session.Command.waitForCompletion( 30 | timeout: Duration 31 | ) { 32 | val expectedEnd = Instant.now().plus(timeout) 33 | val extendedTime = timeout.multipliedBy(5).dividedBy(4) 34 | try { 35 | this.join(extendedTime.toMillis(), TimeUnit.MILLISECONDS) 36 | } catch (e: Exception) { 37 | val output = readOutput() 38 | throw Exception("SSH command failed to finish in extended time ($extendedTime): $output", e) 39 | } 40 | val actualEnd = Instant.now() 41 | if (actualEnd.isAfter(expectedEnd)) { 42 | val overtime = Duration.between(expectedEnd, actualEnd) 43 | throw Exception("SSH command exceeded timeout $timeout by $overtime") 44 | } 45 | } 46 | 47 | private fun Session.Command.readOutput(): SshjExecutedCommand { 48 | return try { 49 | this.close() 50 | SshjExecutedCommand( 51 | stdout = this.inputStream.reader().use { it.readText() }, 52 | stderr = this.errorStream.reader().use { it.readText() } 53 | ) 54 | } catch (e: Exception) { 55 | LOG.error("Failed do close ssh channel. Can't get command output", e) 56 | SshjExecutedCommand( 57 | stdout = "", 58 | stderr = "" 59 | ) 60 | } 61 | } 62 | 63 | private fun InputStream.readAndLog(level: Level): String { 64 | val output = this.reader().use { it.readText() } 65 | if (output.isNotBlank()) { 66 | LOG.log(level, output) 67 | } 68 | return output 69 | } 70 | 71 | private data class SshjExecutedCommand( 72 | val stdout: String, 73 | val stderr: String 74 | ) 75 | 76 | private companion object { 77 | private val LOG: Logger = LogManager.getLogger(this::class.java) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/kotlin/com/atlassian/performance/tools/ssh/SshjConnection.kt: -------------------------------------------------------------------------------- 1 | package com.atlassian.performance.tools.ssh 2 | 3 | import com.atlassian.performance.tools.io.api.ensureDirectory 4 | import com.atlassian.performance.tools.ssh.api.DetachedProcess 5 | import com.atlassian.performance.tools.ssh.api.SshConnection 6 | import com.atlassian.performance.tools.ssh.api.SshConnection.SshResult 7 | import com.atlassian.performance.tools.ssh.api.SshHost 8 | import net.schmizz.sshj.SSHClient 9 | import net.schmizz.sshj.connection.channel.direct.Session 10 | import org.apache.logging.log4j.Level 11 | import org.apache.logging.log4j.LogManager 12 | import org.apache.logging.log4j.Logger 13 | import java.io.File 14 | import java.nio.file.Path 15 | import java.time.Duration 16 | 17 | /** 18 | * An [SshConnection] based on the [SSHJ library](https://github.com/hierynomus/sshj). 19 | */ 20 | internal class SshjConnection internal constructor( 21 | private val ssh: SSHClient, 22 | private val sshHost: SshHost 23 | ) : SshConnection { 24 | 25 | private val logger: Logger = LogManager.getLogger(this::class.java) 26 | 27 | override fun execute( 28 | cmd: String, 29 | timeout: Duration, 30 | stdout: Level, 31 | stderr: Level 32 | ): SshResult { 33 | val sshResult = safeExecute( 34 | cmd = cmd, 35 | timeout = timeout, 36 | stdout = stdout, 37 | stderr = stderr 38 | ) 39 | if (!sshResult.isSuccessful()) { 40 | throw Exception("Error while executing $cmd. Exit status code $sshResult") 41 | } 42 | return sshResult 43 | } 44 | 45 | override fun safeExecute( 46 | cmd: String, 47 | timeout: Duration, 48 | stdout: Level, 49 | stderr: Level 50 | ): SshResult = ssh 51 | .startSession() 52 | .use { safeExecute(it, cmd, timeout, stdout, stderr) } 53 | 54 | private fun safeExecute( 55 | session: Session, 56 | cmd: String, 57 | timeout: Duration, 58 | stdout: Level, 59 | stderr: Level 60 | ): SshResult { 61 | logger.debug("${sshHost.userName}@${sshHost.ipAddress}$ $cmd") 62 | return session.exec(cmd).use { command -> 63 | WaitingCommand(command, timeout, stdout, stderr).waitForResult() 64 | } 65 | } 66 | 67 | @Suppress("DEPRECATION", "OverridingDeprecatedMember") // used in public API, can only remove in a MAJOR release 68 | override fun startProcess(cmd: String): DetachedProcess { 69 | return ssh.startSession().use { DetachedProcess.start(cmd, it) } 70 | } 71 | 72 | @Suppress("DEPRECATION", "OverridingDeprecatedMember") // used in public API, can only remove in a MAJOR release 73 | override fun stopProcess(process: DetachedProcess) { 74 | ssh.startSession().use { process.stop(it) } 75 | } 76 | 77 | override fun download(remoteSource: String, localDestination: Path) { 78 | localDestination.toFile().parentFile.ensureDirectory() 79 | val scpFileTransfer = ssh.newSCPFileTransfer() 80 | scpFileTransfer.download(remoteSource, localDestination.toString()) 81 | } 82 | 83 | override fun upload(localSource: File, remoteDestination: String) { 84 | val scpFileTransfer = ssh.newSCPFileTransfer() 85 | scpFileTransfer.upload(localSource.absolutePath, remoteDestination) 86 | } 87 | 88 | override fun getHost(): SshHost = sshHost 89 | 90 | override fun close() { 91 | ssh.close() 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/kotlin/com/atlassian/performance/tools/ssh/api/Ssh.kt: -------------------------------------------------------------------------------- 1 | package com.atlassian.performance.tools.ssh.api 2 | 3 | import com.atlassian.performance.tools.jvmtasks.api.ExponentialBackoff 4 | import com.atlassian.performance.tools.jvmtasks.api.IdempotentAction 5 | import com.atlassian.performance.tools.ssh.PerformanceDefaultConfig 6 | import com.atlassian.performance.tools.ssh.SshjBackgroundProcess 7 | import com.atlassian.performance.tools.ssh.SshjConnection 8 | import com.atlassian.performance.tools.ssh.port.LocalPort 9 | import com.atlassian.performance.tools.ssh.port.RemotePort 10 | import net.schmizz.sshj.SSHClient 11 | import java.time.Duration 12 | 13 | /** 14 | * Connects to [host] via SSH. 15 | * 16 | * @param host remote SSH server we're connecting to. 17 | * @param connectivityPatience how many times we're going to try to connect to the server. 18 | */ 19 | data class Ssh( 20 | val host: SshHost, 21 | private val connectivityPatience: Int 22 | ) { 23 | 24 | /** 25 | * Connects to [host] via SSH with up to 4 retries. 26 | * 27 | * @param host remote SSH server we're connecting to. 28 | */ 29 | constructor( 30 | host: SshHost 31 | ) : this( 32 | host = host, 33 | connectivityPatience = 4 34 | ) 35 | 36 | /** 37 | * Connects to [host]. 38 | * 39 | * @return A new [SshConnection]. 40 | */ 41 | fun newConnection(): SshConnection { 42 | return SshjConnection( 43 | prepareClient(), 44 | host 45 | ) 46 | } 47 | 48 | /** 49 | * Runs [cmd] in the background, without waiting for its completion. The returned process can be stopped later. 50 | * 51 | * @since 2.4.0 52 | */ 53 | fun runInBackground(cmd: String): BackgroundProcess { 54 | val session = prepareClient().startSession() 55 | session.allocateDefaultPTY() 56 | val command = session.exec(cmd) 57 | return SshjBackgroundProcess(session, command) 58 | } 59 | 60 | /** 61 | * Creates an encrypted connection between a local machine and a remote machine through which you can relay traffic. 62 | * 63 | * See https://www.ssh.com/ssh/tunneling/example#sec-What-Is-SSH-Port-Forwarding-aka-SSH-Tunneling. 64 | * 65 | * Listen for connections on local machine and [localPort]. 66 | * Forwards all the traffic to a remote machine and [remotePort]. 67 | * 68 | * @param localPort port on the local host. 69 | * @param remotePort localPort on a remote machine. 70 | * @since 2.2.0 71 | */ 72 | fun forwardLocalPort(localPort: Int, remotePort: Int): AutoCloseable { 73 | return LocalPort.create(localPort).forward(prepareClient(), remotePort) 74 | } 75 | 76 | /** 77 | * Creates an encrypted connection between a local machine and a remote machine through which you can relay traffic. 78 | * 79 | * See https://www.ssh.com/ssh/tunneling/example#sec-What-Is-SSH-Port-Forwarding-aka-SSH-Tunneling. 80 | * 81 | * Listen for connections on remote machine and [remotePort]. 82 | * Forwards all the traffic to a local machine and [localPort]. 83 | * 84 | * @param localPort port on the local host. 85 | * @param remotePort port on a remote machine. 86 | * @since 2.2.0 87 | */ 88 | fun forwardRemotePort(localPort: Int, remotePort: Int): AutoCloseable { 89 | return RemotePort.create(remotePort).forward(prepareClient(), localPort) 90 | } 91 | 92 | private fun prepareClient(): SSHClient { 93 | val ssh = SSHClient(PerformanceDefaultConfig()) 94 | ssh.connection.keepAlive.keepAliveInterval = 60 95 | ssh.addHostKeyVerifier { _, _, _ -> true } 96 | waitForConnectivity(ssh) 97 | host.authentication.authenticate(host.userName, ssh) 98 | return ssh 99 | } 100 | 101 | private fun waitForConnectivity( 102 | ssh: SSHClient 103 | ) { 104 | val address = host.ipAddress 105 | val port = host.port 106 | IdempotentAction("connect to $address on port $port") { 107 | ssh.connect( 108 | address, 109 | port 110 | ) 111 | }.retry( 112 | maxAttempts = connectivityPatience, 113 | backoff = ExponentialBackoff( 114 | baseBackoff = Duration.ofSeconds(1) 115 | ) 116 | ) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## API 8 | The API consists of all public Kotlin types from `com.atlassian.performance.tools.ssh.api` and its subpackages: 9 | 10 | * [source compatibility] 11 | * [binary compatibility] 12 | * [behavioral compatibility] with behavioral contracts expressed via Javadoc 13 | 14 | [source compatibility]: http://cr.openjdk.java.net/~darcy/OpenJdkDevGuide/OpenJdkDevelopersGuide.v0.777.html#source_compatibility 15 | [binary compatibility]: http://cr.openjdk.java.net/~darcy/OpenJdkDevGuide/OpenJdkDevelopersGuide.v0.777.html#binary_compatibility 16 | [behavioral compatibility]: http://cr.openjdk.java.net/~darcy/OpenJdkDevGuide/OpenJdkDevelopersGuide.v0.777.html#behavioral_compatibility 17 | 18 | ## [Unreleased] 19 | [Unreleased]: https://github.com/atlassian/ssh/compare/release-2.4.3...master 20 | 21 | ### Fixed 22 | - Drop `log4j-core` and `slf4j-impl` from POM. Fix [JPERF-570]. 23 | - Relax `log4j-api` to a SemVer range. 24 | 25 | [JPERF-570]: https://ecosystem.atlassian.net/browse/JPERF-570 26 | 27 | ## [2.4.3] - 2022-06-23 28 | [2.4.3]: https://github.com/atlassian/ssh/compare/release-2.4.2...release-2.4.3 29 | 30 | Empty release to test changes in release process. 31 | 32 | ## [2.4.2] - 2022-01-13 33 | [2.4.2]: https://github.com/atlassian/ssh/compare/release-2.4.1...release-2.4.2 34 | 35 | ### Fixed 36 | - Bump log4j dependency to `2.17.1`. Address [JPERF-766]. 37 | 38 | [JPERF-766]: https://ecosystem.atlassian.net/browse/JPERF-766 39 | 40 | ## [2.4.1] - 2022-01-13 41 | [2.4.1]: https://github.com/atlassian/ssh/compare/release-2.4.0...release-2.4.1 42 | 43 | ### Added 44 | - Add logging ip address of target machine where command is run 45 | 46 | ## [2.4.0] - 2021-01-07 47 | [2.4.0]: https://github.com/atlassian/ssh/compare/release-2.3.1...release-2.4.0 48 | 49 | ### Added 50 | - Add `Ssh.runInBackground`, which yields `SshResult`s unlike the old `SshConnection.startProcess`. Resolve [JPERF-716]. 51 | - Tolerate lack of interrupt if `BackgroundProcess` is already finished. 52 | 53 | ### Deprecated 54 | - Deprecate `SshConnection.startProcess`, `stopProcess` and `DetachedProcess` in favor of new `BackgroundProcess` APIs. 55 | 56 | [JPERF-716]: https://ecosystem.atlassian.net/browse/JPERF-716 57 | 58 | ## [2.3.1] - 2020-04-06 59 | [2.3.1]: https://github.com/atlassian/ssh/compare/release-2.3.0...release-2.3.1 60 | 61 | ### Fixed 62 | - Random number generator is now reused between SSH sessions [JPERF-617]. 63 | 64 | [JPERF-617]: https://ecosystem.atlassian.net/browse/JPERF-617 65 | 66 | ## [2.3.0] - 2019-05-07 67 | [2.3.0]: https://github.com/atlassian/ssh/compare/release-2.2.0...release-2.3.0 68 | 69 | ### Added 70 | - Expose the SSH host via SSH connection. Unblock [JPERF-478]. 71 | 72 | [JPERF-478]: https://ecosystem.atlassian.net/browse/JPERF-478 73 | 74 | ## [2.2.0] - 2019-02-27 75 | [2.2.0]: https://github.com/atlassian/ssh/compare/release-2.1.0...release-2.2.0 76 | 77 | ### Added 78 | - Support local port forwarding. 79 | - Support remote port forwarding. 80 | 81 | ## [2.1.0] - 2018-10-26 82 | [2.1.0]: https://github.com/atlassian/ssh/compare/release-2.0.0...release-2.1.0 83 | 84 | ### Added 85 | - Support password authentication which resolves JPERF-237 86 | 87 | [JPERF-237]: https://ecosystem.atlassian.net/browse/JPERF-237 88 | 89 | ## [2.0.0] - 2018-10-26 90 | [2.0.0]: https://github.com/atlassian/ssh/compare/release-1.2.0...release-2.0.0 91 | 92 | ### Removed 93 | - Remove Kotlin default args from the API for: 94 | - `SshConnection.execute` 95 | - `SshConnection.safeExecute` 96 | Changing `SshConnection` into an interface broke binary compatibility by moving the synthetic `$default` bridge 97 | methods from `SshConnection` to `SshConnection.DefaultImpls`, which was caused by the exposed default args. 98 | As a consequence, break Kotlin source compatibility due to infeasibility of providing individual overloads for 99 | just `stdout` or just `stderr`. 100 | - Remove default args from the `Ssh` constructor. 101 | 102 | ### Added 103 | - Enable abstraction of `SshConnection` and all of its public methods, enabling mocking. Resolve [JPERF-218]. 104 | 105 | [JPERF-218]: https://ecosystem.atlassian.net/browse/JPERF-218 106 | 107 | ## [1.2.0] - 2018-10-24 108 | [1.2.0]: https://github.com/atlassian/ssh/compare/release-1.1.0...release-1.2.0 109 | 110 | ### Added 111 | - Support custom ssh ports which resolves [JPERF-233]. 112 | 113 | [JPERF-233]: https://ecosystem.atlassian.net/browse/JPERF-233 114 | 115 | ## [1.1.0] - 2018-09-21 116 | [1.1.0]: https://github.com/atlassian/ssh/compare/release-1.0.0...release-1.1.0 117 | 118 | ### Added 119 | - Support uploading via SSH. 120 | 121 | ## [1.0.0] - 2018-08-30 122 | [1.0.0]: https://github.com/atlassian/ssh/compare/release-0.1.0...release-1.0.0 123 | 124 | ### Changed 125 | - Define the public API. 126 | 127 | ### Added 128 | - License. 129 | 130 | ## [0.1.0] - 2018-08-02 131 | [0.1.0]: https://github.com/atlassian/ssh/compare/initial-commit...release-0.1.0 132 | 133 | ### Added 134 | - Migrate SSH from [JPT submodule]. 135 | - Add [README.md](README.md). 136 | - Configure Bitbucket Pipelines. 137 | 138 | [JPT submodule]: https://stash.atlassian.com/projects/JIRASERVER/repos/jira-performance-tests/browse/ssh?at=cb909508d9c504d7126d68af9c72087f5822ff2b 139 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /src/main/kotlin/com/atlassian/performance/tools/ssh/api/SshConnection.kt: -------------------------------------------------------------------------------- 1 | package com.atlassian.performance.tools.ssh.api 2 | 3 | import org.apache.logging.log4j.Level 4 | import java.io.Closeable 5 | import java.io.File 6 | import java.nio.file.Path 7 | import java.time.Duration 8 | 9 | /** 10 | * A secure shell connected with a remote server, which can execute commands and transfer files. 11 | * 12 | * @see [Ssh.newConnection] 13 | */ 14 | interface SshConnection : Closeable { 15 | 16 | /** 17 | * Executes [cmd]. Fails if the exit status is non-zero. 18 | * Times out after 30 seconds. 19 | * Logs standard output at the DEBUG level. 20 | * Logs standard errors at the WARN level. 21 | * 22 | * @param [cmd] Runs within the secure shell on the remote system. For example: `pwd`. 23 | */ 24 | @JvmDefault 25 | fun execute( 26 | cmd: String 27 | ): SshResult = execute( 28 | cmd = cmd, 29 | timeout = Duration.ofSeconds(30), 30 | stdout = Level.DEBUG, 31 | stderr = Level.WARN 32 | ) 33 | 34 | /** 35 | * Executes [cmd]. Fails if the exit status is non-zero. 36 | * Logs standard output at the DEBUG level. 37 | * Logs standard errors at the WARN level. 38 | * 39 | * @param [cmd] Runs within the secure shell on the remote system. For example: `pwd`. 40 | * @param [timeout] Limits the amount of time spent on waiting for [cmd] to finish. 41 | */ 42 | @JvmDefault 43 | fun execute( 44 | cmd: String, 45 | timeout: Duration 46 | ): SshResult = execute( 47 | cmd = cmd, 48 | timeout = timeout, 49 | stdout = Level.DEBUG, 50 | stderr = Level.WARN 51 | ) 52 | 53 | /** 54 | * Executes [cmd]. Fails if the exit status is non-zero. 55 | * 56 | * @param [cmd] Runs within the secure shell on the remote system. For example: `pwd`. 57 | * @param [timeout] Limits the amount of time spent on waiting for [cmd] to finish. 58 | * @param [stdout] Controls the log level of [cmd]'s standard output stream. 59 | * @param [stderr] Controls the log level of [cmd]'s standard error stream. 60 | */ 61 | fun execute( 62 | cmd: String, 63 | timeout: Duration, 64 | stdout: Level, 65 | stderr: Level 66 | ): SshResult 67 | 68 | /** 69 | * Executes [cmd]. Returns the result regardless of the exit status. 70 | * Times out after 30 seconds. 71 | * Logs standard output at the TRACE level. 72 | * Logs standard errors at the DEBUG level. 73 | * 74 | * @param [cmd] Runs within the secure shell on the remote system. For example: `pwd`. 75 | */ 76 | @JvmDefault 77 | fun safeExecute( 78 | cmd: String 79 | ): SshResult = safeExecute( 80 | cmd = cmd, 81 | timeout = Duration.ofSeconds(30), 82 | stdout = Level.TRACE, 83 | stderr = Level.DEBUG 84 | ) 85 | 86 | /** 87 | * Executes [cmd]. Returns the result regardless of the exit status. 88 | * Logs standard output at the TRACE level. 89 | * Logs standard errors at the DEBUG level. 90 | * 91 | * @param [cmd] Runs within the secure shell on the remote system. For example: `pwd`. 92 | * @param [timeout] Limits the amount of time spent on waiting for [cmd] to finish. 93 | */ 94 | @JvmDefault 95 | fun safeExecute( 96 | cmd: String, 97 | timeout: Duration 98 | ): SshResult = safeExecute( 99 | cmd = cmd, 100 | timeout = timeout, 101 | stdout = Level.TRACE, 102 | stderr = Level.DEBUG 103 | ) 104 | 105 | /** 106 | * Executes [cmd]. Returns the result regardless of the exit status. 107 | * 108 | * @param [cmd] Runs within the secure shell on the remote system. For example: `pwd`. 109 | * @param [timeout] Limits the amount of time spent on waiting for [cmd] to finish. 110 | * @param [stdout] Controls the log level of [cmd]'s standard output stream. 111 | * @param [stderr] Controls the log level of [cmd]'s standard error stream. 112 | */ 113 | fun safeExecute( 114 | cmd: String, 115 | timeout: Duration, 116 | stdout: Level, 117 | stderr: Level 118 | ): SshResult 119 | 120 | /** 121 | * Starts a [DetachedProcess]. You can use [stopProcess] to stop it later. 122 | */ 123 | @Deprecated(message = "Use Ssh.runInBackground instead") 124 | fun startProcess( 125 | cmd: String 126 | ): DetachedProcess 127 | 128 | /** 129 | * Stops a [DetachedProcess]. 130 | */ 131 | @Deprecated(message = "Use BackgroundProcess.stop instead") 132 | fun stopProcess( 133 | process: DetachedProcess 134 | ) 135 | 136 | /** 137 | * Downloads files from a remote system. 138 | * 139 | * @param remoteSource Points to the file on the remote machine. 140 | * @param localDestination Points to a destination on a local system. 141 | */ 142 | fun download( 143 | remoteSource: String, 144 | localDestination: Path 145 | ) 146 | 147 | /** 148 | * Uploads files to a remote system. 149 | * 150 | * @param localSource Points to the file on the local machine. 151 | * @param remoteDestination Points to a destination on a remote machine. 152 | */ 153 | fun upload( 154 | localSource: File, 155 | remoteDestination: String 156 | ) 157 | 158 | /** 159 | * @since 2.3.0 160 | */ 161 | @JvmDefault 162 | fun getHost(): SshHost = throw Exception("Not implemented") 163 | 164 | /** 165 | * Holds results of a SSH command. 166 | * 167 | * @param exitStatus Holds exit code from a remotely executed SSH command. 168 | * @param output Holds standard output produced by a SSH command. 169 | * @param errorOutput Holds standard error produced by a SSH command. 170 | */ 171 | data class SshResult( 172 | val exitStatus: Int, 173 | val output: String, 174 | val errorOutput: String 175 | ) { 176 | fun isSuccessful(): Boolean { 177 | return exitStatus == 0 178 | } 179 | } 180 | } 181 | --------------------------------------------------------------------------------