/******************************************************************************* * Copyright (c) 2012 Pivotal Software, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Pivotal Software, Inc. - initial API and implementation *******************************************************************************/ package org.grails.ide.eclipse.longrunning.client; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.concurrent.TimeoutException; import org.grails.ide.eclipse.runtime.shared.longrunning.GrailsProcessConstants; /** * Provides a readline method on some input stream. This is our own custom implementation of readline because it * needs to meet some specific requirements that aren't provided by BufferedReader's implementation. Specifically, * we need to be able to guarantee that readline won't get stuck indefinitely if no input is forthcoming. * * @author Kris De Volder * @since 2.6 */ public class LineReader { private final boolean DEBUG_PROTOCOL; private BufferedReader in; private GrailsClient grailsClient; public LineReader(InputStream in) { this.DEBUG_PROTOCOL = false; this.in = new BufferedReader(new InputStreamReader(in)); } /** * @param inputStream * @param debugProtocol */ public LineReader(InputStream in, boolean debugProtocol) { this.DEBUG_PROTOCOL = debugProtocol; this.in = new BufferedReader(new InputStreamReader(in)); } public LineReader(GrailsClient grailsClient, InputStream inputStream, boolean debugProtocol) { this(inputStream, debugProtocol); this.grailsClient = grailsClient; } public String readLine(long timeOut) throws IOException, TimeoutException { String line = readLineHelp(timeOut); while (line!=null && line.startsWith("%debug")) { line = readLineHelp(timeOut); } return line; } public void close() throws IOException { if (in!=null) { try { in.close(); } finally { in = null; } } } /** * To avoid getting stuck when the process only sends a partial line without line terminator... * readline may return early with only partial line data. In that case, it is important that next * time it reads... it should treat the input that comes as preceded with the same line header * as the partial line that was aborted early. * <p> * Thus, this field will contain a line header like {@link GrailsProcess}.CONSOLE_OUT or * {@link GrailsProcess}.CONSOLE_ERR if the last line read was a partial line. It will * contain null if the last line read was a complete, terminated line. */ private String partialLineHeader = null; private String readLineHelp(long timeOut) throws IOException, TimeoutException { // We have to implement our own readLine, otherwise it isn't possible to guarantee // that the readLine will not hang indefinitely when no input is forthcoming from // a stuck Grails process. long maxTime = System.currentTimeMillis() + timeOut; boolean eol = false; boolean readSomething = false; StringBuffer line = partialLineHeader==null? new StringBuffer() : new StringBuffer(partialLineHeader); partialLineHeader = null; int waiting = 0; while (!eol) { if (System.currentTimeMillis() > maxTime) throw new TimeoutException("Timeout: Grails process did not produce output for over "+timeOut+" milliseconds"); if (ready()) { waiting = 0; maxTime = System.currentTimeMillis() + timeOut; int charOrEof = read(); if (charOrEof==-1) { //Reached end of file eol = true; if (!readSomething) { return null; } } else { char c = (char) charOrEof; if (c=='\n') { eol = true; } else if (c!='\r') { readSomething = true; line.append(c); } } } else { try { waiting++; if (waiting>1 && line.length()>GrailsProcessConstants.PROTOCOL_HEADER_LEN) { String header = line.substring(0, GrailsProcessConstants.PROTOCOL_HEADER_LEN); if (header.equals(GrailsProcessConstants.CONSOLE_OUT) || header.equals(GrailsProcessConstants.CONSOLE_ERR)) { //If we are blocked for more than one polling interval, and we have partial line data meant for the console... //then pretend we had a newline. This is to avoid getting blocked when we are sent a question without a line terminator. partialLineHeader = header; //next time we read... we'll need to pretend we see this //header since we won't see the header again when reading the remainder of a partial line. eol = true; } } if (waiting>1 && grailsClient!=null && !grailsClient.isRunning()) { //If the data we are reading comes from a Grails process, don't keep waiting // after process is already dead. throw new GrailsProcessDiedException("Grails process died"); } Thread.sleep(GrailsClient.POLLING_INTERVAL); } catch (InterruptedException e) { //Ignore } } } if (DEBUG_PROTOCOL) { GrailsClient.debug_protocol("recv<<< "+line); } return line.toString(); } private int read() throws IOException { if (in==null) { return -1; // pretend we have an eof ready } else { return in.read(); } } private boolean ready() throws IOException { if (in==null) { // pretend we have an EOF ready return true; } else { return in.ready(); } } }