Giovanni Botta Giovanni Botta - 6 months ago 56
Bash Question

Gradle: execute Groovy interactive shell with project classpath

I have a Gradle project composed of several sub projects. I just created a new one to add support for an interactive Groovy shell that I would like to run with:

gradle console


or

gradle console:run


So my new
console
module's build.gradle file is the following:

apply plugin: 'groovy'
apply plugin:'application'

mainClassName = 'org.codehaus.groovy.tools.shell.Main'

dependencies {
compile 'org.codehaus.groovy:groovy-all:2.2.2'
compile 'org.fusesource.jansi:jansi:1.11'
compile 'commons-cli:commons-cli:1.2'
compile 'jline:jline:2.11'
compile project(':my-module')
}

task(console, dependsOn: 'classes', type: JavaExec) {
main = 'org.codehaus.groovy.tools.shell.Main'
classpath = sourceSets.main.runtimeClasspath
}


However, when I run
gradle :console:run
or
gradle console
I get something like:

:console:run
Groovy Shell (2.2.2, JVM: 1.6.0_45)
Type 'help' or '\h' for help.
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
groovy:000>
BUILD SUCCESSFUL

Total time: 4.529 secs
giovanni@mylaptop:~/Projects/my-project$


So the interactive shell seems to start but it exits right away.

Am I doing something wrong?

EDIT: Added the following to the build.gradle file:

run.standardInput = System.in


Now the standard input gets read from the input stream (thanks to the comments).

However, Gradle seems to get stuck on this:

Groovy Shell (2.2.2, JVM: 1.6.0_45)
Type 'help' or '\h' for help.
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
groovy:000>
> Building 88% > :console:run


And no input gets accepted. Even this leads to the same thing:

gradle --no-daemon console:run

Answer

This works for JDK 7+ (for JDK 6, look at the next figure):

configurations {
    console
}

dependencies {
    // ... compile dependencies, runtime dependencies, etc.
    console 'commons-cli:commons-cli:1.2'
    console('jline:jline:2.11') {
        exclude(group: 'junit', module: 'junit')
    }
    console 'org.codehaus.groovy:groovy-groovysh:2.2.+'
}

task(console, dependsOn: 'classes') << {
    def classpath = sourceSets.main.runtimeClasspath + configurations.console

    def command = [
        'java',
        '-cp', classpath.collect().join(System.getProperty('path.separator')),
        'org.codehaus.groovy.tools.shell.Main',
        '--color'
    ]

    def proc = new ProcessBuilder(command)
        .redirectOutput(ProcessBuilder.Redirect.INHERIT)
        .redirectInput(ProcessBuilder.Redirect.INHERIT)
        .redirectError(ProcessBuilder.Redirect.INHERIT)
        .start()

    proc.waitFor()

    if (0 != proc.exitValue()) {
        throw new RuntimeException("console exited with status: ${proc.exitValue()}")
    }
}

To make this work for JDK 6, I modified the solution from http://stackoverflow.com/a/4274535/206543. My solution is tailored to a standard Linux terminal, so if you are running a shell that uses a char sequence other than '\n' for newlines or that encodes backspaces as a value other other 127, you may need to modify it some. I didn't determine how to make colors print correctly, so its output is rather monotone, but it will get the job done:

configurations {
    console
}

dependencies {
    // ... compile dependencies, runtime dependencies, etc.
    console 'commons-cli:commons-cli:1.2'
    console('jline:jline:2.11') {
        exclude(group: 'junit', module: 'junit')
    }
    console 'org.codehaus.groovy:groovy-groovysh:2.2.+'
}

class StreamCopier implements Runnable {
    def istream
    def ostream
    StreamCopier(istream, ostream) {
        this.istream = istream
        this.ostream = ostream
    }
    void run() {
        int n
        def buffer = new byte[4096]
        while ((n = istream.read(buffer)) != -1) {
            ostream.write(buffer, 0, n)
            ostream.flush()
        }
    }
}

class InputCopier implements Runnable {
    def istream
    def ostream
    def stdout
    InputCopier(istream, ostream, stdout) {
        this.istream = istream
        this.ostream = ostream
        this.stdout = stdout
    }
    void run() {
        try {
            int n
            def buffer = java.nio.ByteBuffer.allocate(4096)
            while ((n = istream.read(buffer)) != -1) {
                ostream.write(buffer.array(), 0, n)
                ostream.flush()
                buffer.clear()
                if (127 == buffer.get(0)) {
                    stdout.print("\b \b")
                    stdout.flush()
                }
            }
        }
        catch (final java.nio.channels.AsynchronousCloseException exception) {
            // Ctrl+D pressed
        }
        finally {
            ostream.close()
        }
    }
}

def getChannel(istream) {
    def f = java.io.FilterInputStream.class.getDeclaredField("in")
    f.setAccessible(true)
    while (istream instanceof java.io.FilterInputStream) {
        istream = f.get(istream)
    }
    istream.getChannel()
}

task(console, dependsOn: 'classes') << {
    def classpath = sourceSets.main.runtimeClasspath + configurations.console

    def command = [
        'java',
        '-cp', classpath.collect().join(System.getProperty('path.separator')),
        'org.codehaus.groovy.tools.shell.Main'
    ]

    def proc = new ProcessBuilder(command).start()

    def stdout = new Thread(new StreamCopier(proc.getInputStream(), System.out))
    stdout.start()

    def stderr = new Thread(new StreamCopier(proc.getErrorStream(), System.err))
    stderr.start()

    def stdin  = new Thread(new InputCopier(
        getChannel(System.in),
        proc.getOutputStream(),
        System.out))
    stdin.start()

    proc.waitFor()
    System.in.close()
    stdout.join()
    stderr.join()
    stdin.join()

    if (0 != proc.exitValue()) {
        throw new RuntimeException("console exited with status: ${proc.exitValue()}")
    }
}

Then, execute it via:

gradle console

or, if you get a lot of noise from gradle:

gradle console -q