-
Notifications
You must be signed in to change notification settings - Fork 9
JPERF-716 background process results #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
49573c2
2a99d22
3cdc0d2
1ab56f8
4469ee6
cdc5f53
fc9107f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| name: CI | ||
|
|
||
| on: | ||
| push: | ||
| pull_request: | ||
| workflow_dispatch: | ||
| inputs: | ||
| release: | ||
| description: 'Release? yes/no' | ||
| default: 'no' | ||
|
|
||
| jobs: | ||
| build: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v2 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Cache Gradle | ||
| uses: actions/cache@v2 | ||
| with: | ||
| path: ~/.gradle | ||
| key: ${{ runner.os }}-${{ hashFiles('gradle') }} | ||
| - name: Build | ||
| run: ./gradlew build | ||
| - name: Upload test reports | ||
| if: always() | ||
| uses: actions/upload-artifact@v2 | ||
| with: | ||
| name: test-reports | ||
| path: build/reports/tests | ||
| - name: Release | ||
| if: github.event.inputs.release == 'yes' | ||
| env: | ||
| atlassian_private_username: ${{ secrets.ARTIFACTORY_USERNAME }} | ||
| atlassian_private_password: ${{ secrets.ARTIFACTORY_API_KEY }} | ||
| run: | | ||
| ./gradlew release -Prelease.customUsername=${{ secrets.REPOSITORY_ACCESS_TOKEN }} | ||
| ./gradlew publish | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| package com.atlassian.performance.tools.ssh | ||
|
|
||
| import com.atlassian.performance.tools.ssh.api.BackgroundProcess | ||
| import com.atlassian.performance.tools.ssh.api.SshConnection | ||
| import net.schmizz.sshj.connection.channel.direct.Session | ||
| import org.apache.logging.log4j.Level | ||
| import org.apache.logging.log4j.LogManager | ||
| import java.time.Duration | ||
| import java.util.concurrent.atomic.AtomicBoolean | ||
|
|
||
| internal class SshjBackgroundProcess( | ||
| private val session: Session, | ||
| private val command: Session.Command | ||
| ) : BackgroundProcess { | ||
|
|
||
| private var closed = AtomicBoolean(false) | ||
|
|
||
| override fun stop(timeout: Duration): SshConnection.SshResult { | ||
| tryToInterrupt() | ||
| val result = WaitingCommand(command, timeout, Level.DEBUG, Level.DEBUG).waitForResult() | ||
| close() | ||
| return result | ||
| } | ||
|
|
||
| private fun tryToInterrupt() { | ||
| try { | ||
| sendSigint() | ||
| } catch (e: Exception) { | ||
| LOG.debug("cannot interrupt, if the command doesn't run anymore, then the write connection is closed", e) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * [Session.Command.signal] doesn't work, so send the CTRL-C character rather than SSH-level SIGINT signal. | ||
| * [OpenSSH server was not supporting this standard](https://bugzilla.mindrot.org/show_bug.cgi?id=1424). | ||
| * It's supported since 7.9p1 (late 2018), but our test Ubuntu still runs on 7.6p1. | ||
| */ | ||
| private fun sendSigint() { | ||
| val ctrlC = 3 | ||
| command.outputStream.write(ctrlC); | ||
| command.outputStream.flush(); | ||
| } | ||
|
|
||
| override fun close() { | ||
| if (!closed.getAndSet(true)) { | ||
| command.use {} | ||
| session.use {} | ||
| } | ||
| } | ||
|
|
||
| private companion object { | ||
| private val LOG = LogManager.getLogger(this::class.java) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| package com.atlassian.performance.tools.ssh | ||
|
|
||
| import com.atlassian.performance.tools.ssh.api.SshConnection | ||
| import net.schmizz.sshj.connection.channel.direct.Session | ||
| import org.apache.logging.log4j.Level | ||
| import org.apache.logging.log4j.LogManager | ||
| import org.apache.logging.log4j.Logger | ||
| import java.io.InputStream | ||
| import java.time.Duration | ||
| import java.time.Instant | ||
| import java.util.concurrent.TimeUnit | ||
|
|
||
| internal class WaitingCommand( | ||
| private val command: Session.Command, | ||
| private val timeout: Duration, | ||
| private val stdout: Level, | ||
| private val stderr: Level | ||
| ) { | ||
|
|
||
| fun waitForResult(): SshConnection.SshResult { | ||
| command.waitForCompletion(timeout) | ||
| return SshConnection.SshResult( | ||
| exitStatus = command.exitStatus, | ||
| output = command.inputStream.readAndLog(stdout), | ||
| errorOutput = command.errorStream.readAndLog(stderr) | ||
| ) | ||
| } | ||
|
|
||
| private fun Session.Command.waitForCompletion( | ||
| timeout: Duration | ||
| ) { | ||
| val expectedEnd = Instant.now().plus(timeout) | ||
| val extendedTime = timeout.multipliedBy(5).dividedBy(4) | ||
| try { | ||
| this.join(extendedTime.toMillis(), TimeUnit.MILLISECONDS) | ||
| } catch (e: Exception) { | ||
| val output = readOutput() | ||
| throw Exception("SSH command failed to finish in extended time ($extendedTime): $output", e) | ||
| } | ||
| val actualEnd = Instant.now() | ||
| if (actualEnd.isAfter(expectedEnd)) { | ||
| val overtime = Duration.between(expectedEnd, actualEnd) | ||
| throw Exception("SSH command exceeded timeout $timeout by $overtime") | ||
| } | ||
| } | ||
|
|
||
| private fun Session.Command.readOutput(): SshjExecutedCommand { | ||
| return try { | ||
| this.close() | ||
| SshjExecutedCommand( | ||
| stdout = this.inputStream.reader().use { it.readText() }, | ||
| stderr = this.errorStream.reader().use { it.readText() } | ||
| ) | ||
| } catch (e: Exception) { | ||
| LOG.error("Failed do close ssh channel. Can't get command output", e) | ||
| SshjExecutedCommand( | ||
| stdout = "<couldn't get command stdout>", | ||
| stderr = "<couldn't get command stderr>" | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| private fun InputStream.readAndLog(level: Level): String { | ||
| val output = this.reader().use { it.readText() } | ||
| if (output.isNotBlank()) { | ||
| LOG.log(level, output) | ||
| } | ||
| return output | ||
| } | ||
|
|
||
| private data class SshjExecutedCommand( | ||
| val stdout: String, | ||
| val stderr: String | ||
| ) | ||
|
|
||
| private companion object { | ||
| private val LOG: Logger = LogManager.getLogger(this::class.java) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package com.atlassian.performance.tools.ssh.api | ||
|
|
||
| import java.time.Duration | ||
|
|
||
| /** | ||
| * Runs in the background. Is independent of `SshConnection`s being closed. | ||
| * Can be used for commands, which will not stop on their own, e.g. `tail -f`, `ping`, `top`, etc. | ||
| * @since 2.4.0 | ||
| */ | ||
| interface BackgroundProcess : AutoCloseable { | ||
|
|
||
| /** | ||
| * Interrupts the process, then waits up to [timeout] for its completion. | ||
| * Skips the interrupt if the process is already finished. | ||
| * Throws if getting the [SshConnection.SshResult] fails. | ||
| * Closes the open resources. | ||
| * | ||
| * @return the result of the stopped process, could have a non-zero exit code | ||
| */ | ||
| fun stop(timeout: Duration): SshConnection.SshResult | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ import java.util.concurrent.TimeUnit | |
| * | ||
| * @see [SshConnection.stopProcess] | ||
| */ | ||
| @Deprecated(message = "Use BackgroundProcess instead") | ||
| class DetachedProcess private constructor( | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried to do it in But for now, I had to resign from the requirement of starting/stopping on different |
||
| private val cmd: String, | ||
| private val uuid: UUID | ||
|
|
@@ -26,6 +27,7 @@ class DetachedProcess private constructor( | |
| logger.debug("Starting process $uuid $cmd") | ||
| session.exec("screen -dm bash -c '${savePID(uuid)} && $cmd'") | ||
| .use { command -> command.join(15, TimeUnit.SECONDS) } | ||
| @Suppress("DEPRECATION") // used transitively by public API | ||
| return DetachedProcess(cmd, uuid) | ||
| } | ||
|
|
||
|
|
@@ -37,4 +39,4 @@ class DetachedProcess private constructor( | |
| session.exec("kill -3 `cat $dir/$uuid`") | ||
| .use { command -> command.join(15, TimeUnit.SECONDS) } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,4 +14,4 @@ class SshConnectionTest { | |
| Assert.assertEquals(sshResult.output, "test\n") | ||
| } | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.