Skip to content
This repository was archived by the owner on Apr 23, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@
.pid.lock
build
gradle-spawn-plugin.iml
/bin/
/.classpath
/.project
/.settings/
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ dependencies {

group = 'com.wiredforcode'
archivesBaseName = 'gradle-spawn-plugin'
version = '0.8.0'
version = '0.8.5'

apply plugin: 'idea'
apply plugin: 'groovy'
Expand All @@ -34,7 +34,7 @@ distributions {

ext {
bintrayBaseUrl = 'https://api.bintray.com/maven'
bintrayUsername = 'vermeulen-mp'
bintrayUsername = 'gorky'
bintrayRepository = 'gradle-plugins'
bintrayPackage = 'gradle-spawn-plugin'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.wiredforcode.gradle.spawn

import java.util.concurrent.TimeUnit

import org.gradle.api.tasks.TaskAction

class KillProcessTask extends DefaultSpawnTask {
Expand All @@ -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()
}
}


}
173 changes: 154 additions & 19 deletions src/main/groovy/com/wiredforcode/gradle/spawn/SpawnProcessTask.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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<Closure> outputActions = new ArrayList<Closure>()

@Input
Expand All @@ -26,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.")
}
Expand All @@ -48,36 +55,52 @@ 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) {
logger.debug(ignored.getMessage(), ignored)
throw new GradleException("Process failed to finish starting before timeout ${timeout}sec")
}
}

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<String> outputAction ->
outputAction.call(line)
private boolean waitUntilIsReadyOrEnd(final Process process) {
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)
}
});
worker.setDaemon(true)
worker.setName(name + "-worker")
worker.start();
long startTime = System.currentTimeMillis()
def started = false
try {
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
//Signal worker that it is done.
worker.interrupt();
//Clear listening.
reader.waiter = null;
logger.quiet "Spawn Post Processing. Ready = ${reader.isReady}"
started && reader.isReady
}


private Process buildProcess(String directory, String command) {
def builder = new ProcessBuilder(command.split(' '))
builder.redirectErrorStream(true)
Expand All @@ -96,4 +119,116 @@ class SpawnProcessTask extends DefaultSpawnTask {

return pidField.getInt(process)
}

class ReaderWorker {
boolean isReady = false
volatile Thread waiter
Exception e;

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(stream))
def currentThread = Thread.currentThread()
boolean interruptSent = false
try {
//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 dumps 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 "$streamName: $command is ready."
isReady = true
logger.debug "$streamName: Wake listeners. Ready = ${isReady}"
if (waiter != null && !currentThread.isInterrupted() && !interruptSent) {
waiter.interrupt()
interruptSent = true
} //else, waiter has abandoned listening
}
}
} catch (Exception e){
this.e = e;
logger.warn("$streamName: Exception starting process", e)
} finally {
try {
if (!siphon){
//If siphoning, don't close. Otherwise the launched process will shutdown.
reader.close()
}
} catch (IOException e){
logger.info("Exception closing process $streamName: ${e.message}", e)
}//end catch
//Interrupt If waiter is listening
if (waiter != null && !interruptSent
//Or Ready or not StdErr
&& (this.isReady || !streamName.equals("StdErr"))) {
waiter.interrupt()
logger.quiet "$streamName sent Interrupt"
} //else, waiter has abandoned listening or StdErr with a closed stream.
}//end finally
logger.quiet "Finished reading $streamName"
}//end waitUntilIsReadyOrEnd

boolean isRunnable(Thread currentThread){
final Thread waiter = this.waiter;
return !currentThread.isInterrupted() && waiter != null && waiter.isAlive()
}

def runOutputActions(String line) {
outputActions.each { Closure<String> outputAction ->
outputAction.call(line)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Loading