From 2f2a37760289021a5e814eae1fc2ea740071e469 Mon Sep 17 00:00:00 2001 From: Stephen Davidson Date: Wed, 18 Oct 2017 18:05:03 -0500 Subject: [PATCH 1/7] Fixes for Hangs and Timeouts on startup. Fixes the following: https://github.com/marc0der/gradle-spawn-plugin/issues/30 https://www.securecoding.cert.org/confluence/display/java/FIO07-J.+Do+not+let+external+processes+block+on+IO+buffers Adds timeout for TaskSpawning. --- .gitignore | 4 + build.gradle | 2 +- .../gradle/spawn/DefaultSpawnTask.groovy | 5 ++ .../gradle/spawn/SpawnProcessTask.groovy | 78 ++++++++++++++----- .../gradle/spawn/SpawnProcessTaskSpec.groovy | 27 +++++++ 5 files changed, 96 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 328397e..bf3abdd 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ .pid.lock build gradle-spawn-plugin.iml +/bin/ +/.classpath +/.project +/.settings/ diff --git a/build.gradle b/build.gradle index b273578..3345c7f 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ dependencies { group = 'com.wiredforcode' archivesBaseName = 'gradle-spawn-plugin' -version = '0.8.0' +version = '0.8.2' apply plugin: 'idea' apply plugin: 'groovy' diff --git a/src/main/groovy/com/wiredforcode/gradle/spawn/DefaultSpawnTask.groovy b/src/main/groovy/com/wiredforcode/gradle/spawn/DefaultSpawnTask.groovy index 1779061..f8b1158 100644 --- a/src/main/groovy/com/wiredforcode/gradle/spawn/DefaultSpawnTask.groovy +++ b/src/main/groovy/com/wiredforcode/gradle/spawn/DefaultSpawnTask.groovy @@ -5,6 +5,11 @@ import org.gradle.api.DefaultTask class DefaultSpawnTask extends DefaultTask { String pidLockFileName = '.pid.lock' String directory = '.' + /** + * Time to wait for process to start/finish in seconds. + */ + int timeout + File getPidFile() { return new File(directory, pidLockFileName) diff --git a/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy b/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy index effcb03..b282be4 100644 --- a/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy +++ b/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy @@ -48,36 +48,41 @@ class SpawnProcessTask extends DefaultSpawnTask { private void checkForAbnormalExit(Process process) { try { - process.waitFor() def exitValue = process.exitValue() if (exitValue) { throw new GradleException("The process terminated unexpectedly - status code ${exitValue}") } } catch (IllegalThreadStateException ignored) { + throw new GradleException("Process failed to finish starting before timeout") } } - private boolean waitUntilIsReadyOrEnd(Process process) { - def line - def reader = new BufferedReader(new InputStreamReader(process.getInputStream())) - boolean isReady = false - while (!isReady && (line = reader.readLine()) != null) { - logger.quiet line - runOutputActions(line) - if (line.contains(ready)) { - logger.quiet "$command is ready." - isReady = true - } - } - isReady - } - - def runOutputActions(String line) { - outputActions.each { Closure outputAction -> - outputAction.call(line) + private boolean waitUntilIsReadyOrEnd(final Process process) { + def currentThread = Thread.currentThread() + final ReaderWorker reader = new ReaderWorker(); + Thread worker = new Thread(new Runnable(){ + public void run(){ + reader.waitUntilIsReadyOrEnd(process, currentThread) + } + }); + worker.start(); + long endTime = timeout <= 0 ? Long.MAX_VALUE : System.currentTimeMillis() + timeout; + long startTime = System.currentTimeMillis() + try { + currentThread.sleep(timeout <= 0 ? Long.MAX_VALUE : timeout * 1000) + logger.warn "Timed out after: " + timeout + " seconds" + } catch (InterruptedException e) { + //Should just be the reader thread waking up because it found the success message. + logger.quiet "Finished after: " + (System.currentTimeMillis() - startTime) + "ms" } + if (reader.e != null){ + throw e; + }//else + worker.interrupt(); + reader.isReady } + private Process buildProcess(String directory, String command) { def builder = new ProcessBuilder(command.split(' ')) builder.redirectErrorStream(true) @@ -96,4 +101,39 @@ class SpawnProcessTask extends DefaultSpawnTask { return pidField.getInt(process) } + + class ReaderWorker { + boolean isReady = false + Exception e; + void waitUntilIsReadyOrEnd(Process process, Thread waiter){ + def line + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())) + try { + while (!isReady && (line = reader.readLine()) != null) { + logger.quiet line + runOutputActions(line) + if (line.contains(ready)) { + logger.quiet "$command is ready." + isReady = true + } + } + } catch (Exception e){ + this.e = e; + e.printStackTrace(); + } finally { + try { + reader.close() + } catch (IOException e){ + logger.info("Exception closing process inputstream: ${e.message}", e) + }//end catch + }//end finally + waiter.interrupt() + }//end waitUntilIsReadyOrEnd + + def runOutputActions(String line) { + outputActions.each { Closure outputAction -> + outputAction.call(line) + } + } + } } diff --git a/src/test/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTaskSpec.groovy b/src/test/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTaskSpec.groovy index 22e96b1..26e5f02 100644 --- a/src/test/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTaskSpec.groovy +++ b/src/test/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTaskSpec.groovy @@ -1,5 +1,7 @@ package com.wiredforcode.gradle.spawn +import java.nio.channels.InterruptedByTimeoutException + import org.gradle.api.GradleException import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder @@ -191,6 +193,31 @@ class SpawnProcessTaskSpec extends Specification { outputBuilder.toString() == "Starting...\nIt is done...\n" task.getPidFile().name == pidLockFileName } + + void "will timeout on missing ready"(){ + given: + def command = './process.sh' + def ready = 'It is not done...' + + and: + setExecutableProcess("process.sh") + + and: + task.command = command + task.ready = ready + task.directory = directory.toString() + task.timeout = 7 + + when: + task.spawn() + + then: + def e = thrown(GradleException) + e.message == "Process failed to finish starting before timeout" + + and: + !task.getPidFile().exists() + } private void setExecutableProcess(String processFile) { def processSource = new File("src/test/resources/$processFile") From c7c822affc9be6766b51a17598b5af02935e232b Mon Sep 17 00:00:00 2001 From: Stephen Davidson Date: Wed, 18 Oct 2017 22:10:00 -0500 Subject: [PATCH 2/7] SpawnTask timeout Added timeout support for KillProcessTask. Added hardKill for stuck processes. Fixes for Thread hangs due to lack of input on SpawnTask. --- .../gradle/spawn/KillProcessTask.groovy | 19 ++++- .../gradle/spawn/SpawnProcessTask.groovy | 37 +++++++--- .../gradle/spawn/KillProcessTaskSpec.groovy | 72 +++++++++++++++++++ .../gradle/spawn/SpawnProcessTaskSpec.groovy | 5 +- 4 files changed, 121 insertions(+), 12 deletions(-) diff --git a/src/main/groovy/com/wiredforcode/gradle/spawn/KillProcessTask.groovy b/src/main/groovy/com/wiredforcode/gradle/spawn/KillProcessTask.groovy index 15c60b1..5e02eac 100644 --- a/src/main/groovy/com/wiredforcode/gradle/spawn/KillProcessTask.groovy +++ b/src/main/groovy/com/wiredforcode/gradle/spawn/KillProcessTask.groovy @@ -1,5 +1,7 @@ package com.wiredforcode.gradle.spawn +import java.util.concurrent.TimeUnit + import org.gradle.api.tasks.TaskAction class KillProcessTask extends DefaultSpawnTask { @@ -15,9 +17,24 @@ class KillProcessTask extends DefaultSpawnTask { def process = "kill $pid".execute() try { - process.waitFor() + if (timeout <= 0){ + process.waitFor() + } else { + killWithTimeOut(process, pid) + } } finally { pidFile.delete() } } + + void killWithTimeOut(Process process, String pid){ + boolean success = process.waitFor(timeout, TimeUnit.SECONDS) + if (!success){ + logger.info "Soft stop timed out, executing 'kill -s 9 ${pid}" + def hardKillProcess = "kill -s 9 $pid".execute() + hardKillProcess.waitFor() + } + } + + } diff --git a/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy b/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy index b282be4..b57c644 100644 --- a/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy +++ b/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy @@ -53,33 +53,43 @@ class SpawnProcessTask extends DefaultSpawnTask { throw new GradleException("The process terminated unexpectedly - status code ${exitValue}") } } catch (IllegalThreadStateException ignored) { - throw new GradleException("Process failed to finish starting before timeout") + throw new GradleException("Process failed to finish starting before timeout ${timeout}sec") } } private boolean waitUntilIsReadyOrEnd(final Process process) { - def currentThread = Thread.currentThread() + final def currentThread = Thread.currentThread() final ReaderWorker reader = new ReaderWorker(); + reader.waiter = currentThread Thread worker = new Thread(new Runnable(){ public void run(){ - reader.waitUntilIsReadyOrEnd(process, currentThread) + reader.waitUntilIsReadyOrEnd(process) } }); worker.start(); - long endTime = timeout <= 0 ? Long.MAX_VALUE : System.currentTimeMillis() + timeout; long startTime = System.currentTimeMillis() + def started = false try { - currentThread.sleep(timeout <= 0 ? Long.MAX_VALUE : timeout * 1000) + Thread.sleep(timeout <= 0 ? Long.MAX_VALUE : timeout * 1000) logger.warn "Timed out after: " + timeout + " seconds" } catch (InterruptedException e) { //Should just be the reader thread waking up because it found the success message. logger.quiet "Finished after: " + (System.currentTimeMillis() - startTime) + "ms" + //Clear interrupt status + Thread.interrupted() + started = true } if (reader.e != null){ throw e; }//else worker.interrupt(); - reader.isReady + //Timeout in 10sec, or timeout. + if (worker.isAlive()){ + //Timeout failed. Clear thread + reader.waiter = null; + } + logger.debug "Spawn Post Processing. Ready = ${reader.isReady}" + started && reader.isReady } @@ -104,12 +114,16 @@ class SpawnProcessTask extends DefaultSpawnTask { class ReaderWorker { boolean isReady = false + Thread waiter Exception e; - void waitUntilIsReadyOrEnd(Process process, Thread waiter){ + + void waitUntilIsReadyOrEnd(Process process){ def line BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())) + def currentThread = Thread.currentThread() try { - while (!isReady && (line = reader.readLine()) != null) { + while (!currentThread.isInterrupted() && !isReady + && (line = reader.readLine()) != null) { logger.quiet line runOutputActions(line) if (line.contains(ready)) { @@ -119,7 +133,7 @@ class SpawnProcessTask extends DefaultSpawnTask { } } catch (Exception e){ this.e = e; - e.printStackTrace(); + logger.warn("Exception starting process", e) } finally { try { reader.close() @@ -127,7 +141,10 @@ class SpawnProcessTask extends DefaultSpawnTask { logger.info("Exception closing process inputstream: ${e.message}", e) }//end catch }//end finally - waiter.interrupt() + logger.debug "Wake listeners. Ready = ${isReady}" + if (waiter != null && !currentThread.isInterrupted()) { + waiter.interrupt() + } //else, waiter has abandoned listening }//end waitUntilIsReadyOrEnd def runOutputActions(String line) { diff --git a/src/test/groovy/com/wiredforcode/gradle/spawn/KillProcessTaskSpec.groovy b/src/test/groovy/com/wiredforcode/gradle/spawn/KillProcessTaskSpec.groovy index 5dee6b6..eb0d1c1 100644 --- a/src/test/groovy/com/wiredforcode/gradle/spawn/KillProcessTaskSpec.groovy +++ b/src/test/groovy/com/wiredforcode/gradle/spawn/KillProcessTaskSpec.groovy @@ -105,4 +105,76 @@ class KillProcessTaskSpec extends Specification { then: killTask.pidLockFileName == '.new.pid.lock' } + + void "should kill a process with timeout set to long"() { + println "should kill a process with timeout set to long" + given: + def directoryPath = directory.toString() + def processSource = new File("src/test/resources/process.sh") + def process = new File(directory, "process.sh") + process << processSource.text + process.setExecutable(true) + + and: + spawnTask.command = "./process.sh" + spawnTask.ready = "It is done..." + spawnTask.directory = directoryPath + + and: + killTask.directory = directoryPath + killTask.timeout = 30 + + when: + spawnTask.spawn() + def lockFile = spawnTask.pidFile + + then: + lockFile.exists() + + when: + killTask.kill() + + then: + !lockFile.exists() + + cleanup: + assert directory.deleteDir() + killTask.timeout = 0 + } + + void "should kill a process with timeout set to short"() { + println "should kill a process with timeout set to short" + given: + def directoryPath = directory.toString() + def processSource = new File("src/test/resources/process.sh") + def process = new File(directory, "process.sh") + process << processSource.text + process.setExecutable(true) + + and: + spawnTask.command = "./process.sh" + spawnTask.ready = "It is done..." + spawnTask.directory = directoryPath + + and: + killTask.directory = directoryPath + killTask.timeout = 1 + + when: + spawnTask.spawn() + def lockFile = spawnTask.pidFile + + then: + lockFile.exists() + + when: + killTask.kill() + + then: + !lockFile.exists() + + cleanup: + assert directory.deleteDir() + killTask.timeout = 0 + } } diff --git a/src/test/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTaskSpec.groovy b/src/test/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTaskSpec.groovy index 26e5f02..995e20c 100644 --- a/src/test/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTaskSpec.groovy +++ b/src/test/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTaskSpec.groovy @@ -213,10 +213,13 @@ class SpawnProcessTaskSpec extends Specification { then: def e = thrown(GradleException) - e.message == "Process failed to finish starting before timeout" + e.message == "Process failed to finish starting before timeout 7sec" and: !task.getPidFile().exists() + + cleanup: + task.timeout = 0 } private void setExecutableProcess(String processFile) { From 0a4a882295e94f25577ef9e65b0ff3a99d227494 Mon Sep 17 00:00:00 2001 From: Stephen Davidson Date: Thu, 19 Oct 2017 00:37:01 -0500 Subject: [PATCH 3/7] Siphon capability. Some languages (such as Golang) interpret the closing of STDOUT as "Shutdown". This patch allows for that behaviour to be supported. --- build.gradle | 2 +- .../gradle/spawn/SpawnProcessTask.groovy | 36 +++++++++++++++---- .../gradle/spawn/SpawnProcessTaskSpec.groovy | 1 + 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index 3345c7f..a03760a 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ dependencies { group = 'com.wiredforcode' archivesBaseName = 'gradle-spawn-plugin' -version = '0.8.2' +version = '0.8.3' apply plugin: 'idea' apply plugin: 'groovy' diff --git a/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy b/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy index b57c644..e4ba1df 100644 --- a/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy +++ b/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy @@ -114,6 +114,12 @@ class SpawnProcessTask extends DefaultSpawnTask { class ReaderWorker { boolean isReady = false + /** + * If set, do NOT abort reading and close the pipe from the process stdout. There + * are applications that will pick this up as a "Shutdown" signal. Instead, continue + * "siphoning" or reading from the InputStream, but just "chuck" the data. + */ + boolean siphon Thread waiter Exception e; @@ -121,14 +127,27 @@ class SpawnProcessTask extends DefaultSpawnTask { def line BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())) def currentThread = Thread.currentThread() + boolean interruptSent = false try { - while (!currentThread.isInterrupted() && !isReady - && (line = reader.readLine()) != null) { + while ((!currentThread.isInterrupted() && !isReady && (line = reader.readLine()) != null) + //If there is no data, nothing to siphon + || (line != null && siphon)) { + //provision siphoning for process that dump when stdout is closed + //This applies to golang based code specifically + if (siphon && isReady){ + //done processing + continue; + } logger.quiet line runOutputActions(line) if (line.contains(ready)) { logger.quiet "$command is ready." isReady = true + logger.debug "Wake listeners. Ready = ${isReady}" + if (waiter != null && !currentThread.isInterrupted()) { + waiter.interrupt() + interruptSent = true + } //else, waiter has abandoned listening } } } catch (Exception e){ @@ -136,15 +155,18 @@ class SpawnProcessTask extends DefaultSpawnTask { logger.warn("Exception starting process", e) } finally { try { - reader.close() + if (!siphon){ + //If siphoning, don't close. Otherwise the launched process will shutdown. + reader.close() + } } catch (IOException e){ logger.info("Exception closing process inputstream: ${e.message}", e) }//end catch + if (waiter != null && !interruptSent) { + waiter.interrupt() + } //else, waiter has abandoned listening }//end finally - logger.debug "Wake listeners. Ready = ${isReady}" - if (waiter != null && !currentThread.isInterrupted()) { - waiter.interrupt() - } //else, waiter has abandoned listening + logger.trace "Finished reading stdout" }//end waitUntilIsReadyOrEnd def runOutputActions(String line) { diff --git a/src/test/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTaskSpec.groovy b/src/test/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTaskSpec.groovy index 995e20c..d6289fc 100644 --- a/src/test/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTaskSpec.groovy +++ b/src/test/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTaskSpec.groovy @@ -145,6 +145,7 @@ class SpawnProcessTaskSpec extends Specification { task.directory == directory.toString() } + //KABOOM! void "should not write the pid lock file if the process exits abnormally"() { given: def command = './exitAbnormally.sh' From f490f9600190cfeb0d89610d6086778db6c2c8e5 Mon Sep 17 00:00:00 2001 From: Stephen Davidson Date: Thu, 19 Oct 2017 00:55:38 -0500 Subject: [PATCH 4/7] Make siphon property generally accessible. --- .../gradle/spawn/SpawnProcessTask.groovy | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy b/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy index e4ba1df..a76472d 100644 --- a/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy +++ b/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy @@ -7,6 +7,12 @@ import org.gradle.api.tasks.TaskAction class SpawnProcessTask extends DefaultSpawnTask { String command String ready + /** + * If set, do NOT abort reading and close the pipe from the process stdout. There + * are applications that will pick this up as a "Shutdown" signal. Instead, continue + * "siphoning" or reading from the InputStream, but just "chuck" the data. + */ + boolean siphon List outputActions = new ArrayList() @Input @@ -114,12 +120,6 @@ class SpawnProcessTask extends DefaultSpawnTask { class ReaderWorker { boolean isReady = false - /** - * If set, do NOT abort reading and close the pipe from the process stdout. There - * are applications that will pick this up as a "Shutdown" signal. Instead, continue - * "siphoning" or reading from the InputStream, but just "chuck" the data. - */ - boolean siphon Thread waiter Exception e; From cb0f3be4ccf2c7b1122372a5177021ac9e620d0e Mon Sep 17 00:00:00 2001 From: Stephen Davidson Date: Thu, 19 Oct 2017 14:14:08 -0500 Subject: [PATCH 5/7] Fixes for SpinLock and unexpected ThreadExits. --- build.gradle | 2 +- .../gradle/spawn/SpawnProcessTask.groovy | 26 ++++++++++++------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/build.gradle b/build.gradle index a03760a..ee4e6ed 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ dependencies { group = 'com.wiredforcode' archivesBaseName = 'gradle-spawn-plugin' -version = '0.8.3' +version = '0.8.4' apply plugin: 'idea' apply plugin: 'groovy' diff --git a/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy b/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy index a76472d..0366b35 100644 --- a/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy +++ b/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy @@ -72,6 +72,8 @@ class SpawnProcessTask extends DefaultSpawnTask { reader.waitUntilIsReadyOrEnd(process) } }); + worker.setDaemon(true) + worker.setName(name + "-worker") worker.start(); long startTime = System.currentTimeMillis() def started = false @@ -88,12 +90,10 @@ class SpawnProcessTask extends DefaultSpawnTask { if (reader.e != null){ throw e; }//else + //Signal worker that it is done. worker.interrupt(); - //Timeout in 10sec, or timeout. - if (worker.isAlive()){ - //Timeout failed. Clear thread - reader.waiter = null; - } + //Clear listening. + reader.waiter = null; logger.debug "Spawn Post Processing. Ready = ${reader.isReady}" started && reader.isReady } @@ -129,13 +129,17 @@ class SpawnProcessTask extends DefaultSpawnTask { def currentThread = Thread.currentThread() boolean interruptSent = false try { - while ((!currentThread.isInterrupted() && !isReady && (line = reader.readLine()) != null) - //If there is no data, nothing to siphon - || (line != null && siphon)) { + //Data to read, no EOF + while (((line = reader.readLine()) != null) + //Can run and not yet ready + && (isRunnable(currentThread) && !isReady + //If there is no data, nothing to siphon + || (line != null && siphon))) { //provision siphoning for process that dump when stdout is closed //This applies to golang based code specifically if (siphon && isReady){ //done processing + logger.quiet currentThread.name + ':' + line continue; } logger.quiet line @@ -144,7 +148,7 @@ class SpawnProcessTask extends DefaultSpawnTask { logger.quiet "$command is ready." isReady = true logger.debug "Wake listeners. Ready = ${isReady}" - if (waiter != null && !currentThread.isInterrupted()) { + if (waiter != null && !currentThread.isInterrupted() && !interruptSent) { waiter.interrupt() interruptSent = true } //else, waiter has abandoned listening @@ -169,6 +173,10 @@ class SpawnProcessTask extends DefaultSpawnTask { logger.trace "Finished reading stdout" }//end waitUntilIsReadyOrEnd + boolean isRunnable(Thread currentThread){ + return !currentThread.isInterrupted() && waiter != null & waiter.isAlive() + } + def runOutputActions(String line) { outputActions.each { Closure outputAction -> outputAction.call(line) From 956ef299e964a5d82fca513f6fe9e71da83d5d23 Mon Sep 17 00:00:00 2001 From: Stephen Davidson Date: Sun, 29 Oct 2017 11:27:50 -0500 Subject: [PATCH 6/7] Don't need to continue writing output after task has launched. --- .../groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy b/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy index 0366b35..664e396 100644 --- a/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy +++ b/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy @@ -139,7 +139,6 @@ class SpawnProcessTask extends DefaultSpawnTask { //This applies to golang based code specifically if (siphon && isReady){ //done processing - logger.quiet currentThread.name + ':' + line continue; } logger.quiet line From 6c9f1fcd512f886c52ab92a1ae3e5b5ef97b0542 Mon Sep 17 00:00:00 2001 From: Stephen Davidson Date: Tue, 21 Nov 2017 17:07:52 -0600 Subject: [PATCH 7/7] 0.8.5 Release. Handling for parsing/reading StdErr. --- build.gradle | 4 +- .../gradle/spawn/SpawnProcessTask.groovy | 75 +++++++++++++++---- 2 files changed, 64 insertions(+), 15 deletions(-) diff --git a/build.gradle b/build.gradle index ee4e6ed..dd1935b 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ dependencies { group = 'com.wiredforcode' archivesBaseName = 'gradle-spawn-plugin' -version = '0.8.4' +version = '0.8.5' apply plugin: 'idea' apply plugin: 'groovy' @@ -34,7 +34,7 @@ distributions { ext { bintrayBaseUrl = 'https://api.bintray.com/maven' - bintrayUsername = 'vermeulen-mp' + bintrayUsername = 'gorky' bintrayRepository = 'gradle-plugins' bintrayPackage = 'gradle-spawn-plugin' } diff --git a/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy b/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy index 664e396..e9ca385 100644 --- a/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy +++ b/src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy @@ -32,6 +32,7 @@ class SpawnProcessTask extends DefaultSpawnTask { @TaskAction void spawn() { + logger.quiet "Spawning ${command}" if (!(command && ready)) { throw new GradleException("Ensure that mandatory fields command and ready are set.") } @@ -59,6 +60,7 @@ class SpawnProcessTask extends DefaultSpawnTask { throw new GradleException("The process terminated unexpectedly - status code ${exitValue}") } } catch (IllegalThreadStateException ignored) { + logger.debug(ignored.getMessage(), ignored) throw new GradleException("Process failed to finish starting before timeout ${timeout}sec") } } @@ -94,7 +96,7 @@ class SpawnProcessTask extends DefaultSpawnTask { worker.interrupt(); //Clear listening. reader.waiter = null; - logger.debug "Spawn Post Processing. Ready = ${reader.isReady}" + logger.quiet "Spawn Post Processing. Ready = ${reader.isReady}" started && reader.isReady } @@ -120,12 +122,54 @@ class SpawnProcessTask extends DefaultSpawnTask { class ReaderWorker { boolean isReady = false - Thread waiter + volatile Thread waiter Exception e; - void waitUntilIsReadyOrEnd(Process process){ + void waitUntilIsReadyOrEnd(final Process process){ + //Read StdOut + final Thread outThread = new Thread(new Runnable(){ + public void run(){ + waitUntilIsReadyOrEnd(process, process.getInputStream(), "StdOut") + } + }) + outThread.start(); + //Read StdErr + final Thread errThread = new Thread(new Runnable(){ + public void run(){ + waitUntilIsReadyOrEnd(process, process.getErrorStream(), "StdErr") + } + }) + errThread.start(); + final Thread currentThread = Thread.currentThread() + logger.quiet "Waiting on startup Readers" + + if (!isRunnable(currentThread)){ + logger.quiet "Process finished fast finished startup" + outThread.interrupt(); + errThread.interrupt(); + } else { + waitOn(outThread, errThread, "StdOut") + waitOn(errThread, outThread, "StdErr") + logger.quiet "Joins complete" + } + } + + void waitOn(final Thread toWaitFor, final Thread other, String currentReader){ + try { + toWaitFor.join(); + } catch (InterruptedException e){ + logger.quiet "$currentReader Reader interrupted..." + other.interrupt() + final Thread waiter = this.waiter + if (waiter != null){ + waiter.interrupt() + } //else, stopped and deleted by another thread. + } + } + + void waitUntilIsReadyOrEnd(Process process, InputStream stream, String streamName){ def line - BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())) + BufferedReader reader = new BufferedReader(new InputStreamReader(stream)) def currentThread = Thread.currentThread() boolean interruptSent = false try { @@ -135,7 +179,7 @@ class SpawnProcessTask extends DefaultSpawnTask { && (isRunnable(currentThread) && !isReady //If there is no data, nothing to siphon || (line != null && siphon))) { - //provision siphoning for process that dump when stdout is closed + //provision siphoning for process that dumps when stdout is closed //This applies to golang based code specifically if (siphon && isReady){ //done processing @@ -144,9 +188,9 @@ class SpawnProcessTask extends DefaultSpawnTask { logger.quiet line runOutputActions(line) if (line.contains(ready)) { - logger.quiet "$command is ready." + logger.quiet "$streamName: $command is ready." isReady = true - logger.debug "Wake listeners. Ready = ${isReady}" + logger.debug "$streamName: Wake listeners. Ready = ${isReady}" if (waiter != null && !currentThread.isInterrupted() && !interruptSent) { waiter.interrupt() interruptSent = true @@ -155,7 +199,7 @@ class SpawnProcessTask extends DefaultSpawnTask { } } catch (Exception e){ this.e = e; - logger.warn("Exception starting process", e) + logger.warn("$streamName: Exception starting process", e) } finally { try { if (!siphon){ @@ -163,17 +207,22 @@ class SpawnProcessTask extends DefaultSpawnTask { reader.close() } } catch (IOException e){ - logger.info("Exception closing process inputstream: ${e.message}", e) + logger.info("Exception closing process $streamName: ${e.message}", e) }//end catch - if (waiter != null && !interruptSent) { + //Interrupt If waiter is listening + if (waiter != null && !interruptSent + //Or Ready or not StdErr + && (this.isReady || !streamName.equals("StdErr"))) { waiter.interrupt() - } //else, waiter has abandoned listening + logger.quiet "$streamName sent Interrupt" + } //else, waiter has abandoned listening or StdErr with a closed stream. }//end finally - logger.trace "Finished reading stdout" + logger.quiet "Finished reading $streamName" }//end waitUntilIsReadyOrEnd boolean isRunnable(Thread currentThread){ - return !currentThread.isInterrupted() && waiter != null & waiter.isAlive() + final Thread waiter = this.waiter; + return !currentThread.isInterrupted() && waiter != null && waiter.isAlive() } def runOutputActions(String line) {