StopDetectingInputStream.java revision 3827:44bdefe64114
1/* 2 * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26package jdk.internal.jshell.tool; 27 28import java.io.IOException; 29import java.io.InputStream; 30import java.util.function.Consumer; 31 32public final class StopDetectingInputStream extends InputStream { 33 public static final int INITIAL_SIZE = 128; 34 35 private final Runnable stop; 36 private final Consumer<Exception> errorHandler; 37 38 private boolean initialized; 39 private int[] buffer = new int[INITIAL_SIZE]; 40 private int start; 41 private int end; 42 private State state = State.WAIT; 43 44 public StopDetectingInputStream(Runnable stop, Consumer<Exception> errorHandler) { 45 this.stop = stop; 46 this.errorHandler = errorHandler; 47 } 48 49 public synchronized InputStream setInputStream(InputStream input) { 50 if (initialized) 51 throw new IllegalStateException("Already initialized."); 52 initialized = true; 53 54 Thread reader = new Thread(() -> { 55 try { 56 int read; 57 while (true) { 58 //to support external terminal editors, the "cmdin.read" must not run when 59 //an external editor is running. At the same time, it needs to run while the 60 //user's code is running (so Ctrl-C is detected). Hence waiting here until 61 //there is a confirmed need for input. 62 State currentState = waitInputNeeded(); 63 if (currentState == State.CLOSED) { 64 break; 65 } 66 if ((read = input.read()) == (-1)) { 67 break; 68 } 69 if (read == 3 && currentState == State.BUFFER) { 70 stop.run(); 71 } else { 72 write(read); 73 } 74 } 75 } catch (IOException ex) { 76 errorHandler.accept(ex); 77 } finally { 78 shutdown(); 79 } 80 }); 81 reader.setDaemon(true); 82 reader.start(); 83 84 return this; 85 } 86 87 @Override 88 public synchronized int read() { 89 while (start == end) { 90 if (state == State.CLOSED) { 91 return -1; 92 } 93 if (state == State.WAIT) { 94 state = State.READ; 95 } 96 notifyAll(); 97 try { 98 wait(); 99 } catch (InterruptedException ex) { 100 //ignore 101 } 102 } 103 try { 104 return buffer[start]; 105 } finally { 106 start = (start + 1) % buffer.length; 107 } 108 } 109 110 public synchronized void shutdown() { 111 state = State.CLOSED; 112 notifyAll(); 113 } 114 115 public synchronized void write(int b) { 116 if (state != State.BUFFER) { 117 state = State.WAIT; 118 } 119 int newEnd = (end + 1) % buffer.length; 120 if (newEnd == start) { 121 //overflow: 122 int[] newBuffer = new int[buffer.length * 2]; 123 int rightPart = (end > start ? end : buffer.length) - start; 124 int leftPart = end > start ? 0 : start - 1; 125 System.arraycopy(buffer, start, newBuffer, 0, rightPart); 126 System.arraycopy(buffer, 0, newBuffer, rightPart, leftPart); 127 buffer = newBuffer; 128 start = 0; 129 end = rightPart + leftPart; 130 newEnd = end + 1; 131 } 132 buffer[end] = b; 133 end = newEnd; 134 notifyAll(); 135 } 136 137 public synchronized void setState(State state) { 138 if (this.state != State.CLOSED) { 139 this.state = state; 140 notifyAll(); 141 } 142 } 143 144 private synchronized State waitInputNeeded() { 145 while (state == State.WAIT) { 146 try { 147 wait(); 148 } catch (InterruptedException ex) { 149 //ignore 150 } 151 } 152 153 return state; 154 } 155 156 public enum State { 157 /* No reading from the input should happen. This is the default state. The StopDetectingInput 158 * must be in this state when an external editor is being run, so that the external editor 159 * can read from the input. 160 */ 161 WAIT, 162 /* A single input character should be read. Reading from the StopDetectingInput will move it 163 * into this state, and the state will be automatically changed back to WAIT when a single 164 * input character is obtained. This state is typically used while the user is editing the 165 * input line. 166 */ 167 READ, 168 /* Continuously read from the input. Forward Ctrl-C ((int) 3) to the "stop" Runnable, buffer 169 * all other input. This state is typically used while the user's code is running, to provide 170 * the ability to detect Ctrl-C in order to stop the execution. 171 */ 172 BUFFER, 173 /* The input is closed. 174 */ 175 CLOSED 176 } 177 178} 179