├── .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 | 
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 |
--------------------------------------------------------------------------------