From 24efc75f7b152ec5a24cf5aa00d9c0fa2cac8e13 Mon Sep 17 00:00:00 2001 From: nathan Date: Wed, 15 Oct 2014 02:19:42 +0200 Subject: [PATCH] WIP getting simple version of jline working/trimmed --- Dorkbox-Util/.classpath | 4 +- Dorkbox-Util/src/dorkbox/util/FileUtil.java | 62 +++- .../src/dorkbox/util/crypto/Crypto.java | 3 + .../util/{ => input}/InputConsole.java | 327 ++++++++++++----- .../dorkbox/util/input/InputStreamReader.java | 342 ++++++++++++++++++ .../src/dorkbox/util/input/Terminal.java | 66 ++++ .../src/dorkbox/util/input/TerminalType.java | 14 + .../input/posix/PosixTerminalControl.java | 68 ++++ .../util/input/posix/TermiosStruct.java | 44 +++ .../util/input/posix/UnixTerminal.java | 167 +++++++++ .../unsupported/UnsupportedTerminal.java | 30 ++ .../util/input/windows/ConsoleMode.java | 71 ++++ .../util/input/windows/WindowsTerminal.java | 131 +++++++ .../util/process/ShellProcessBuilder.java | 5 +- 14 files changed, 1238 insertions(+), 96 deletions(-) rename Dorkbox-Util/src/dorkbox/util/{ => input}/InputConsole.java (53%) create mode 100644 Dorkbox-Util/src/dorkbox/util/input/InputStreamReader.java create mode 100644 Dorkbox-Util/src/dorkbox/util/input/Terminal.java create mode 100644 Dorkbox-Util/src/dorkbox/util/input/TerminalType.java create mode 100644 Dorkbox-Util/src/dorkbox/util/input/posix/PosixTerminalControl.java create mode 100644 Dorkbox-Util/src/dorkbox/util/input/posix/TermiosStruct.java create mode 100644 Dorkbox-Util/src/dorkbox/util/input/posix/UnixTerminal.java create mode 100644 Dorkbox-Util/src/dorkbox/util/input/unsupported/UnsupportedTerminal.java create mode 100644 Dorkbox-Util/src/dorkbox/util/input/windows/ConsoleMode.java create mode 100644 Dorkbox-Util/src/dorkbox/util/input/windows/WindowsTerminal.java diff --git a/Dorkbox-Util/.classpath b/Dorkbox-Util/.classpath index 1c7ad61..e7d4eb3 100644 --- a/Dorkbox-Util/.classpath +++ b/Dorkbox-Util/.classpath @@ -12,8 +12,6 @@ - - @@ -25,5 +23,7 @@ + + diff --git a/Dorkbox-Util/src/dorkbox/util/FileUtil.java b/Dorkbox-Util/src/dorkbox/util/FileUtil.java index 3bb08bc..0c37d8f 100644 --- a/Dorkbox-Util/src/dorkbox/util/FileUtil.java +++ b/Dorkbox-Util/src/dorkbox/util/FileUtil.java @@ -4,6 +4,7 @@ import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -532,6 +533,28 @@ public class FileUtil { unzipJar(zipFile, outputDir, true); } + /** + * Unzips a ZIP file. Will close the input stream. + * + * @return The path to the output directory. + */ + public static void unzip(ZipInputStream inputStream, String outputDir) throws IOException { + if (outputDir == null) { + throw new IllegalArgumentException("outputDir cannot be null."); + } + + unzip(inputStream, new File(outputDir)); + } + + /** + * Unzips a ZIP file. Will close the input stream. + * + * @return The path to the output directory. + */ + public static void unzip(ZipInputStream inputStream, File outputDir) throws IOException { + unzipJar(inputStream, outputDir, true); + } + /** * Unzips a ZIP file * @@ -564,7 +587,21 @@ public class FileUtil { unjarzip0(zipFile, outputDir, extractManifest); } + /** + * Unzips a ZIP file. Will close the input stream. + * + * @return The path to the output directory. + */ + public static void unzipJar(ZipInputStream inputStream, File outputDir, boolean extractManifest) throws IOException { + if (inputStream == null) { + throw new IllegalArgumentException("inputStream cannot be null."); + } + if (outputDir == null) { + throw new IllegalArgumentException("outputDir cannot be null."); + } + unjarzip1(inputStream, outputDir, extractManifest); + } /** * Unzips a ZIP or JAR file (and handles the manifest if requested) @@ -582,15 +619,20 @@ public class FileUtil { throw new RuntimeException("Source filesize is too large!"); } + ZipInputStream inputStream = new ZipInputStream(new FileInputStream(zipFile)); - ZipInputStream inputStrem = new ZipInputStream(new FileInputStream(zipFile)); + unjarzip1(inputStream, outputDir, extractManifest); + } + + /** + * Unzips a ZIP file + * + * @return The path to the output directory. + */ + private static void unjarzip1(ZipInputStream inputStream, File outputDir, boolean extractManifest) throws FileNotFoundException, IOException { try { - while (true) { - ZipEntry entry = inputStrem.getNextEntry(); - if (entry == null) { - break; - } - + ZipEntry entry = null; + while ((entry = inputStream.getNextEntry()) != null) { String name = entry.getName(); if (!extractManifest && name.startsWith("META-INF/")) { @@ -607,13 +649,13 @@ public class FileUtil { FileOutputStream output = new FileOutputStream(file); try { - Sys.copyStream(inputStrem, output); + Sys.copyStream(inputStream, output); } finally { - output.close(); + Sys.close(output); } } } finally { - inputStrem.close(); + Sys.close(inputStream); } } diff --git a/Dorkbox-Util/src/dorkbox/util/crypto/Crypto.java b/Dorkbox-Util/src/dorkbox/util/crypto/Crypto.java index 458edbd..90246f9 100644 --- a/Dorkbox-Util/src/dorkbox/util/crypto/Crypto.java +++ b/Dorkbox-Util/src/dorkbox/util/crypto/Crypto.java @@ -89,6 +89,9 @@ import dorkbox.urlHandler.Box; * 1) Necessary * 2) Compatible with GWT * + * + * To determine if we have hardware acclerated AES + * java -XX:+PrintFlagsFinal -version | grep UseAES */ public class Crypto { private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(Crypto.class); diff --git a/Dorkbox-Util/src/dorkbox/util/InputConsole.java b/Dorkbox-Util/src/dorkbox/util/input/InputConsole.java similarity index 53% rename from Dorkbox-Util/src/dorkbox/util/InputConsole.java rename to Dorkbox-Util/src/dorkbox/util/input/InputConsole.java index 74b019a..e664cdc 100644 --- a/Dorkbox-Util/src/dorkbox/util/InputConsole.java +++ b/Dorkbox-Util/src/dorkbox/util/input/InputConsole.java @@ -1,37 +1,54 @@ -package dorkbox.util; +package dorkbox.util.input; +import java.io.BufferedReader; +import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.io.UnsupportedEncodingException; +import java.io.Reader; +import java.net.URL; +import java.nio.charset.Charset; +import java.security.CodeSource; +import java.security.ProtectionDomain; import java.util.concurrent.atomic.AtomicBoolean; -import jline.IDE_Terminal; -import jline.Terminal; -import jline.console.ConsoleReader; - import org.fusesource.jansi.Ansi; import org.fusesource.jansi.AnsiConsole; import org.slf4j.Logger; +import dorkbox.util.OS; +import dorkbox.util.input.posix.UnixTerminal; +import dorkbox.util.input.unsupported.UnsupportedTerminal; +import dorkbox.util.input.windows.WindowsTerminal; + public class InputConsole { private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(InputConsole.class); private static final InputConsole consoleProxyReader = new InputConsole(); private static final char[] emptyLine = new char[0]; - static { - // setup (if necessary) the JLINE console logger. - // System.setProperty("jline.internal.Log.trace", "TRUE"); - // System.setProperty("jline.internal.Log.debug", "TRUE"); - } - /** * empty method to allow code to initialize the input console. */ public static void init() { } - public static final void destroy() { + // this is run by our init... + { + AnsiConsole.systemInstall(); + + // don't forget we have to shut down the ansi console as well + // alternatively, shut everything down when the JVM closes. + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + AnsiConsole.systemUninstall(); + InputConsole.destroy(); + } + }); + } + + // called by our shutdown thread + private static final void destroy() { consoleProxyReader.destroy0(); } @@ -74,8 +91,6 @@ public class InputConsole { } - private final ConsoleReader jlineReader; - private final Object inputLockSingle = new Object(); private final Object inputLockLine = new Object(); @@ -84,42 +99,87 @@ public class InputConsole { private volatile char[] readLine = null; private volatile int readChar = -1; - private final boolean isIDE; + private final boolean unsupported; + + private final Terminal terminal; + private Reader reader; + private final String encoding; + + - // the streams are ALREADY buffered! - // private InputConsole() { - boolean isIDECheck = false; - Terminal terminal = null; - ConsoleReader console = null; - try { - console = new ConsoleReader(); + Logger logger = InputConsole.logger; + boolean unsupported = false; - terminal = console.getTerminal(); - terminal.setEchoEnabled(true); - isIDECheck = terminal instanceof IDE_Terminal; - } catch (UnsupportedEncodingException ignored) { - } catch (IOException ignored) { + String type = System.getProperty(TerminalType.TYPE, TerminalType.AUTO).toLowerCase(); + if ("dumb".equals(System.getenv("TERM"))) { + type = "none"; + logger.debug("$TERM=dumb; setting type={}", type); } - this.isIDE = isIDECheck; - this.jlineReader = console; + logger.debug("Creating terminal; type={}", type); - Logger logger2 = logger; - if (logger2.isDebugEnabled()) { - if (isIDECheck) { - logger2.debug("Terminal is in IDE (best guess). Unable to support single key input. Only line input available."); + Terminal t; + try { + if (type.equals(TerminalType.UNIX)) { + t = new UnixTerminal(); + } + else if (type.equals(TerminalType.WIN) || type.equals(TerminalType.WINDOWS)) { + t = new WindowsTerminal(); + } + else if (type.equals(TerminalType.NONE) || type.equals(TerminalType.OFF) || type.equals(TerminalType.FALSE)) { + t = new UnsupportedTerminal(); + unsupported = true; } else { - String terminalType; - if (terminal != null) { - terminalType = terminal.getClass().getSimpleName(); + if (isIDEAutoDetect()) { + logger.debug("Terminal is in UNSUPPORTED (best guess). Unable to support single key input. Only line input available."); + t = new UnsupportedTerminal(); + unsupported = true; } else { - terminalType = "NULL"; + if (OS.isWindows()) { + t = new WindowsTerminal(); + } else { + t = new UnixTerminal(); + } } - - logger2.debug("Terminal Type: {}", terminalType); } } + catch (Exception e) { + logger.error("Failed to construct terminal, falling back to unsupported"); + t = new UnsupportedTerminal(); + unsupported = true; + } + + InputStream in; + + try { + t.init(); + in = t.wrapInIfNeeded(System.in); + } + catch (Throwable e) { + logger.error("Terminal initialization failed, falling back to unsupported"); + t = new UnsupportedTerminal(); + unsupported = true; + in = System.in; + + try { + t.init(); + } catch (IOException e1) { + // UnsupportedTerminal can't do this + } + } + + this.encoding = this.encoding != null ? this.encoding : getEncoding(); + this.reader = new InputStreamReader(in, this.encoding); + + if (unsupported) { + this.reader = new BufferedReader(this.reader); + } + + this.unsupported = unsupported; + this.terminal = t; + + logger.debug("Created Terminal: {}", this.terminal); } /** @@ -145,6 +205,7 @@ public class InputConsole { private void destroy0() { // Don't change this, because we don't want to enable reading, etc from this once it's destroyed. + // so we pretend that it's still running // isRunning.set(false); if (this.isInShutdown.compareAndSet(true, true)) { @@ -159,34 +220,22 @@ public class InputConsole { this.inputLockLine.notifyAll(); } - // we want to make sure this happens in a new thread, since this can BLOCK our main event dispatch thread - Thread thread = new Thread(new Runnable() { - @Override - public void run() { - InputConsole.this.jlineReader.shutdown(); - }}); - thread.setDaemon(true); - thread.setName("Console Input Shutdown"); - thread.start(); + try { + InputConsole inputConsole = InputConsole.this; + + inputConsole.terminal.restore(); + inputConsole.reader.close(); + } catch (IOException ignored) { + ignored.printStackTrace(); + } } private void echo0(boolean enableEcho) { - if (this.jlineReader != null) { - Terminal terminal = this.jlineReader.getTerminal(); - if (terminal != null) { - terminal.setEchoEnabled(enableEcho); - } - } + this.terminal.setEchoEnabled(enableEcho); } private boolean echo0() { - if (this.jlineReader != null) { - Terminal terminal = this.jlineReader.getTerminal(); - if (terminal != null) { - return terminal.isEchoEnabled(); - } - } - return false; + return this.terminal.isEchoEnabled(); } @@ -225,7 +274,7 @@ public class InputConsole { // the chars of the line! // so, readChar is REALLY the index at which we return letters (until the whole string is returned - if (this.isIDE) { + if (this.unsupported) { int integer = this.indexOfStringForReadChar.get(); if (integer == -1) { @@ -284,15 +333,15 @@ public class InputConsole { private final void run() { Logger logger2 = logger; - if (this.jlineReader == null) { - logger2.error("Unable to start Console Reader"); - return; - } - // if we are eclipse, we MUST do this per line! (per character DOESN'T work.) - if (this.isIDE) { + // if we are eclipse/etc, we MUST do this per line! (per character DOESN'T work.) + // char readers will get looped for the WHOLE string, so reading by char will work, + // it just waits until \n until it triggers + if (this.unsupported) { + BufferedReader reader = (BufferedReader) this.reader; + try { - while ((this.readLine = this.jlineReader.readLine()) != null) { + while ((this.readLine = reader.readLine().toCharArray()) != null) { // notify everyone waiting for a line of text. synchronized (this.inputLockSingle) { if (this.readLine.length > 0) { @@ -309,8 +358,8 @@ public class InputConsole { } catch (Exception ignored) { ignored.printStackTrace(); } - } else { - + } + else { try { final boolean ansiEnabled = Ansi.isEnabled(); Ansi ansi = Ansi.ansi(); @@ -320,7 +369,7 @@ public class InputConsole { // don't type ; in a bash shell, it quits everything // \n is replaced by \r in unix terminal? - while ((typedChar = this.jlineReader.readCharacter()) != -1) { + while ((typedChar = this.reader.read()) != -1) { char asChar = (char) typedChar; if (logger2.isTraceEnabled()) { @@ -339,26 +388,30 @@ public class InputConsole { // clear ourself + one extra. if (ansiEnabled) { - int amtToBackspace = 2; // ConsoleReader.getPrintableCharacters(typedChar).length(); + // size of the buffer BEFORE our backspace was typed int length = buf.length(); + int amtToOverwrite = 2; // backspace is always 2 chars (^?) + if (length > 1) { char charAt = buf.charAt(length-1); - amtToBackspace += ConsoleReader.getPrintableCharacters(charAt).length(); - buf.delete(length-1, length); + amtToOverwrite += getPrintableCharacters(charAt); - length--; + // delete last item in our buffer + buf.setLength(--length); - // now figure out where the cursor is at. + // now figure out where the cursor is really at. + // this is more memory friendly than buf.toString.length for (int i=0;i[language[_territory][.codeset][@modifier]] + * + * @param ctype The ctype to parse, may be null + * @return The encoding, if one was present, otherwise null + */ + static String extractEncodingFromCtype(String ctype) { + if (ctype != null && ctype.indexOf('.') > 0) { + String encodingAndModifier = ctype.substring(ctype.indexOf('.') + 1); + if (encodingAndModifier.indexOf('@') > 0) { + return encodingAndModifier.substring(0, encodingAndModifier.indexOf('@')); + } else { + return encodingAndModifier; + } + } + return null; + } + + /** + * try to guess if we are running inside an IDE + */ + private boolean isIDEAutoDetect() { + try { + // Get the location of this class + ProtectionDomain pDomain = getClass().getProtectionDomain(); + CodeSource cSource = pDomain.getCodeSource(); + URL loc = cSource.getLocation(); // file:/X:/workspace/xxxx/classes/ when it's in eclipse + + // if we are in eclipse, this won't be a jar -- it will be a class directory. + File locFile = new File(loc.getFile()); + return locFile.isDirectory(); + + } catch (Exception e) { + } + + // fall-back to unsupported + return true; + } + + + /** + * Return the number of characters that will be printed when the specified + * character is echoed to the screen + * + * Adapted from cat by Torbjorn Granlund, as repeated in stty by David MacKenzie. + */ + public static int getPrintableCharacters(final int ch) { +// StringBuilder sbuff = new StringBuilder(); + + if (ch >= 32) { + if (ch < 127) { +// sbuff.append((char) ch); + return 1; + } + else if (ch == 127) { +// sbuff.append('^'); +// sbuff.append('?'); + return 2; + } + else { +// sbuff.append('M'); +// sbuff.append('-'); + int count = 2; + + if (ch >= 128 + 32) { + if (ch < 128 + 127) { +// sbuff.append((char) (ch - 128)); + count++; + } + else { +// sbuff.append('^'); +// sbuff.append('?'); + count += 2; + } + } + else { +// sbuff.append('^'); +// sbuff.append((char) (ch - 128 + 64)); + count += 2; + } + return count; + } + } + else { +// sbuff.append('^'); +// sbuff.append((char) (ch + 64)); + return 2; + } + +// return sbuff; + } } \ No newline at end of file diff --git a/Dorkbox-Util/src/dorkbox/util/input/InputStreamReader.java b/Dorkbox-Util/src/dorkbox/util/input/InputStreamReader.java new file mode 100644 index 0000000..09c35be --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/input/InputStreamReader.java @@ -0,0 +1,342 @@ +/* + * Copyright (c) 2002-2012, the original author or authors. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * http://www.opensource.org/licenses/bsd-license.php + */ +package dorkbox.util.input; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.MalformedInputException; +import java.nio.charset.UnmappableCharacterException; + + +/** + * + * NOTE for JLine: the default InputStreamReader that comes from the JRE + * usually read more bytes than needed from the input stream, which + * is not usable in a character per character model used in the console. + * We thus use the harmony code which only reads the minimal number of bytes, + * with a modification to ensure we can read larger characters (UTF-16 has + * up to 4 bytes, and UTF-32, rare as it is, may have up to 8). + */ +/** + * A class for turning a byte stream into a character stream. Data read from the + * source input stream is converted into characters by either a default or a + * provided character converter. The default encoding is taken from the + * "file.encoding" system property. {@code InputStreamReader} contains a buffer + * of bytes read from the source stream and converts these into characters as + * needed. The buffer size is 8K. + * + * @see OutputStreamWriter + */ +public class InputStreamReader extends Reader { + private InputStream in; + + private static final int BUFFER_SIZE = 8192; + + private boolean endOfInput = false; + + String encoding; + + CharsetDecoder decoder; + + ByteBuffer bytes = ByteBuffer.allocate(BUFFER_SIZE); + + /** + * Constructs a new {@code InputStreamReader} on the {@link InputStream} + * {@code in}. This constructor sets the character converter to the encoding + * specified in the "file.encoding" property and falls back to ISO 8859_1 + * (ISO-Latin-1) if the property doesn't exist. + * + * @param in + * the input stream from which to read characters. + */ + public InputStreamReader(InputStream in) { + super(in); + this.in = in; + // FIXME: This should probably use Configuration.getFileEncoding() + this.encoding = System.getProperty("file.encoding", "ISO8859_1"); //$NON-NLS-1$//$NON-NLS-2$ + this.decoder = Charset.forName(this.encoding).newDecoder().onMalformedInput( + CodingErrorAction.REPLACE).onUnmappableCharacter( + CodingErrorAction.REPLACE); + this.bytes.limit(0); + } + + /** + * Constructs a new InputStreamReader on the InputStream {@code in}. The + * character converter that is used to decode bytes into characters is + * identified by name by {@code enc}. If the encoding cannot be found, an + * UnsupportedEncodingException error is thrown. + * + * @param in + * the InputStream from which to read characters. + * @param enc + * identifies the character converter to use. + * @throws NullPointerException + * if {@code enc} is {@code null}. + * @throws UnsupportedEncodingException + * if the encoding specified by {@code enc} cannot be found. + */ + public InputStreamReader(InputStream in, final String enc) +// throws UnsupportedEncodingException + { + super(in); + + if (enc == null) { + throw new NullPointerException(); + } + this.in = in; + + try { + this.decoder = Charset.forName(enc).newDecoder().onMalformedInput( + CodingErrorAction.REPLACE).onUnmappableCharacter( + CodingErrorAction.REPLACE); + } catch (IllegalArgumentException e) { + e.printStackTrace(); +// throw (UnsupportedEncodingException) +// new UnsupportedEncodingException(enc).initCause(e); + } + this.bytes.limit(0); + } + + /** + * Constructs a new InputStreamReader on the InputStream {@code in} and + * CharsetDecoder {@code dec}. + * + * @param in + * the source InputStream from which to read characters. + * @param dec + * the CharsetDecoder used by the character conversion. + */ + public InputStreamReader(InputStream in, CharsetDecoder dec) { + super(in); + dec.averageCharsPerByte(); + this.in = in; + this.decoder = dec; + this.bytes.limit(0); + } + + /** + * Constructs a new InputStreamReader on the InputStream {@code in} and + * Charset {@code charset}. + * + * @param in + * the source InputStream from which to read characters. + * @param charset + * the Charset that defines the character converter + */ + public InputStreamReader(InputStream in, Charset charset) { + super(in); + this.in = in; + this.decoder = charset.newDecoder().onMalformedInput( + CodingErrorAction.REPLACE).onUnmappableCharacter( + CodingErrorAction.REPLACE); + this.bytes.limit(0); + } + + /** + * Closes this reader. This implementation closes the source InputStream and + * releases all local storage. + * + * @throws IOException + * if an error occurs attempting to close this reader. + */ + @Override + public void close() throws IOException { + synchronized (this.lock) { + this.decoder = null; + if (this.in != null) { + this.in.close(); + this.in = null; + } + } + } + + /** + * Returns the name of the encoding used to convert bytes into characters. + * The value {@code null} is returned if this reader has been closed. + * + * @return the name of the character converter or {@code null} if this + * reader is closed. + */ + public String getEncoding() { + if (!isOpen()) { + return null; + } + return this.encoding; + } + + /** + * Reads a single character from this reader and returns it as an integer + * with the two higher-order bytes set to 0. Returns -1 if the end of the + * reader has been reached. The byte value is either obtained from + * converting bytes in this reader's buffer or by first filling the buffer + * from the source InputStream and then reading from the buffer. + * + * @return the character read or -1 if the end of the reader has been + * reached. + * @throws IOException + * if this reader is closed or some other I/O error occurs. + */ + @Override + public int read() throws IOException { + synchronized (this.lock) { + if (!isOpen()) { + throw new IOException("InputStreamReader is closed."); + } + + char buf[] = new char[4]; + return read(buf, 0, 4) != -1 ? Character.codePointAt(buf, 0) : -1; + } + } + + /** + * Reads at most {@code length} characters from this reader and stores them + * at position {@code offset} in the character array {@code buf}. Returns + * the number of characters actually read or -1 if the end of the reader has + * been reached. The bytes are either obtained from converting bytes in this + * reader's buffer or by first filling the buffer from the source + * InputStream and then reading from the buffer. + * + * @param buf + * the array to store the characters read. + * @param offset + * the initial position in {@code buf} to store the characters + * read from this reader. + * @param length + * the maximum number of characters to read. + * @return the number of characters read or -1 if the end of the reader has + * been reached. + * @throws IndexOutOfBoundsException + * if {@code offset < 0} or {@code length < 0}, or if + * {@code offset + length} is greater than the length of + * {@code buf}. + * @throws IOException + * if this reader is closed or some other I/O error occurs. + */ + @Override + public int read(char[] buf, int offset, int length) throws IOException { + synchronized (this.lock) { + if (!isOpen()) { + throw new IOException("InputStreamReader is closed."); + } + if (offset < 0 || offset > buf.length - length || length < 0) { + throw new IndexOutOfBoundsException(); + } + if (length == 0) { + return 0; + } + + CharBuffer out = CharBuffer.wrap(buf, offset, length); + CoderResult result = CoderResult.UNDERFLOW; + + // bytes.remaining() indicates number of bytes in buffer + // when 1-st time entered, it'll be equal to zero + boolean needInput = !this.bytes.hasRemaining(); + + while (out.hasRemaining()) { + // fill the buffer if needed + if (needInput) { + try { + if (this.in.available() == 0 + && out.position() > offset) { + // we could return the result without blocking read + break; + } + } catch (IOException e) { + // available didn't work so just try the read + } + + int to_read = this.bytes.capacity() - this.bytes.limit(); + int off = this.bytes.arrayOffset() + this.bytes.limit(); + int was_red = this.in.read(this.bytes.array(), off, to_read); + + if (was_red == -1) { + this.endOfInput = true; + break; + } else if (was_red == 0) { + break; + } + this.bytes.limit(this.bytes.limit() + was_red); + needInput = false; + } + + // decode bytes + result = this.decoder.decode(this.bytes, out, false); + + if (result.isUnderflow()) { + // compact the buffer if no space left + if (this.bytes.limit() == this.bytes.capacity()) { + this.bytes.compact(); + this.bytes.limit(this.bytes.position()); + this.bytes.position(0); + } + needInput = true; + } else { + break; + } + } + + if (result == CoderResult.UNDERFLOW && this.endOfInput) { + result = this.decoder.decode(this.bytes, out, true); + this.decoder.flush(out); + this.decoder.reset(); + } + if (result.isMalformed()) { + throw new MalformedInputException(result.length()); + } else if (result.isUnmappable()) { + throw new UnmappableCharacterException(result.length()); + } + + return out.position() - offset == 0 ? -1 : out.position() - offset; + } + } + + /* + * Answer a boolean indicating whether or not this InputStreamReader is + * open. + */ + private boolean isOpen() { + return this.in != null; + } + + /** + * Indicates whether this reader is ready to be read without blocking. If + * the result is {@code true}, the next {@code read()} will not block. If + * the result is {@code false} then this reader may or may not block when + * {@code read()} is called. This implementation returns {@code true} if + * there are bytes available in the buffer or the source stream has bytes + * available. + * + * @return {@code true} if the receiver will not block when {@code read()} + * is called, {@code false} if unknown or blocking will occur. + * @throws IOException + * if this reader is closed or some other I/O error occurs. + */ + @Override + public boolean ready() throws IOException { + synchronized (this.lock) { + if (this.in == null) { + throw new IOException("InputStreamReader is closed."); + } + try { + return this.bytes.hasRemaining() || this.in.available() > 0; + } catch (IOException e) { + return false; + } + } + } +} \ No newline at end of file diff --git a/Dorkbox-Util/src/dorkbox/util/input/Terminal.java b/Dorkbox-Util/src/dorkbox/util/input/Terminal.java new file mode 100644 index 0000000..3d01685 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/input/Terminal.java @@ -0,0 +1,66 @@ +package dorkbox.util.input; + +import java.io.IOException; +import java.io.InputStream; + +public abstract class Terminal { + protected final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(getClass()); + + + public static final int DEFAULT_WIDTH = 80; + public static final int DEFAULT_HEIGHT = 24; + + private volatile boolean echoEnabled; + private volatile Thread shutdown; + + public Terminal() { + if (this.shutdown != null) { + try { + Runtime.getRuntime().removeShutdownHook(this.shutdown); + } + catch (IllegalStateException e) { + // The VM is shutting down, ignore + } + } + + // Register a task to restore the terminal on shutdown + Runnable runnable = new Runnable() { + @Override + public void run() { + try { + restore(); + } catch (IOException e) { + Terminal.this.logger.error("Unable to restore the terminal", e); + } + } + }; + + this.shutdown = new Thread(runnable, "Terminal"); + + try { + Runtime.getRuntime().addShutdownHook(this.shutdown); + } + catch (IllegalStateException e) { + // The VM is shutting down, ignore + } + } + + + public abstract void init() throws IOException; + public abstract void restore() throws IOException; + + public void setEchoEnabled(boolean enabled) { + this.echoEnabled = enabled; + } + + public boolean isEchoEnabled() { + return this.echoEnabled; + } + + public abstract int getWidth(); + public abstract int getHeight(); + + public InputStream wrapInIfNeeded(InputStream in) throws IOException { + return in; + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/input/TerminalType.java b/Dorkbox-Util/src/dorkbox/util/input/TerminalType.java new file mode 100644 index 0000000..0be770c --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/input/TerminalType.java @@ -0,0 +1,14 @@ +package dorkbox.util.input; + +public class TerminalType { + public static final String TYPE = "input.terminal"; + + public static final String AUTO = "auto"; + public static final String UNIX = "unix"; + public static final String WIN = "win"; + public static final String WINDOWS = "windows"; + + public static final String NONE = "none"; + public static final String OFF = "off"; + public static final String FALSE = "false"; +} diff --git a/Dorkbox-Util/src/dorkbox/util/input/posix/PosixTerminalControl.java b/Dorkbox-Util/src/dorkbox/util/input/posix/PosixTerminalControl.java new file mode 100644 index 0000000..1de32eb --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/input/posix/PosixTerminalControl.java @@ -0,0 +1,68 @@ +package dorkbox.util.input.posix; + +import java.nio.ByteBuffer; + +import com.sun.jna.Library; + +public interface PosixTerminalControl extends Library { + public static final int TCSANOW = 0; + public static final int TBUFLEN = 124; + + // Definitions at: http://linux.die.net/man/3/termios + // also: http://code.metager.de/source/xref/DragonFly-BSD/sys/sys/termios.h + public static final int IGNBRK = 0x00000001; /* ignore BREAK condition */ + public static final int BRKINT = 0x00000002; /* map BREAK to SIGINTR */ + + + public static final int ISIG = 0000001; + public static final int ICANON = 0000002; + public static final int ECHO = 0000010; + public static final int IXON = 0002000; + + + public static final int VINTR = 0; + public static final int VQUIT = 1; + public static final int VERASE = 2; + public static final int VKILL = 3; + public static final int VEOF = 4; + public static final int VTIME = 5; + public static final int VMIN = 6; + public static final int VSWTC = 7; + public static final int VSTART = 8; + public static final int VSTOP = 9; + public static final int VSUSP = 10; + public static final int VEOL = 11; + public static final int VREPRINT = 12; + public static final int VDISCARD = 13; + public static final int VWERASE = 14; + public static final int VLNEXT = 15; + public static final int VEOL2 = 16; + + // MAGIC! + public static final int TIOCGWINSZ = System.getProperty("os.name").equalsIgnoreCase("linux") ? 0x5413 : 1074295912; + + public int open(String path, int flags); + + public int close(int fd); + + /** + * Original signature : int ioctl(int, int, char*)
+ */ + public int ioctl(int d, int request, ByteBuffer data); + + /** + * Put the state of FD into *TERMIOS_P.
+ * + * Original signature : int tcgetattr(int, char*)
+ */ + public int tcgetattr(int fd, TermiosStruct termios_p); + + /** + * Set the state of FD to *TERMIOS_P.
+ * + * Values for OPTIONAL_ACTIONS (TCSA*) are in .
+ * + * Original signature : int tcsetattr(int, int, char*)
+ */ + public int tcsetattr(int fd, int optional_actions, TermiosStruct termios_p); +} diff --git a/Dorkbox-Util/src/dorkbox/util/input/posix/TermiosStruct.java b/Dorkbox-Util/src/dorkbox/util/input/posix/TermiosStruct.java new file mode 100644 index 0000000..2ee9364 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/input/posix/TermiosStruct.java @@ -0,0 +1,44 @@ +package dorkbox.util.input.posix; + +import java.util.Arrays; +import java.util.List; + +import com.sun.jna.Structure; + +public class TermiosStruct extends Structure { + /** input mode flags */ + public int c_iflag; + /** output mode flags */ + public int c_oflag; + /** control mode flags */ + public int c_cflag; + /** local mode flags */ + public int c_lflag; + /** line discipline */ + public byte c_line; + + /** control characters */ + public byte[] c_cc = new byte[32]; + + /** input speed */ + public int c_ispeed; + /** output speed */ + public int c_ospeed; + + public TermiosStruct() { + } + + @Override + protected List getFieldOrder() { + return Arrays.asList( + "c_iflag", + "c_oflag", + "c_cflag", + "c_lflag", + "c_line", + "c_cc", + "c_ispeed", + "c_ospeed" + ); + } +} \ No newline at end of file diff --git a/Dorkbox-Util/src/dorkbox/util/input/posix/UnixTerminal.java b/Dorkbox-Util/src/dorkbox/util/input/posix/UnixTerminal.java new file mode 100644 index 0000000..6d738b5 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/input/posix/UnixTerminal.java @@ -0,0 +1,167 @@ +package dorkbox.util.input.posix; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import com.sun.jna.Native; + +import dorkbox.util.input.Terminal; + +/** + * Terminal that is used for unix platforms. Terminal initialization + * is handled via JNA and ioctl/tcgetattr/tcsetattr/cfmakeraw. + * + * This implementation should work for an reasonable POSIX system. + */ +public class UnixTerminal extends Terminal { + + private volatile TermiosStruct termInfoDefault = new TermiosStruct(); + private volatile TermiosStruct termInfo = new TermiosStruct(); + + private PosixTerminalControl term; + private ByteBuffer windowSizeBuffer = ByteBuffer.allocate(8); + + public UnixTerminal() throws Exception { + this.term = (PosixTerminalControl) Native.loadLibrary("c", PosixTerminalControl.class); + + // save off the defaults + if (this.term.tcgetattr(0, this.termInfoDefault) !=0) { + throw new IOException("Failed to get terminal info"); + } + } + + @Override + public void init() throws IOException { + + // COMPARABLE TO (from upstream) + //settings.set("-icanon min 1 -ixon"); + //settings.set("dsusp undef"); + + /* + * NOT done in constructor, since our unit test DOES NOT use this! + * + * Set the console to be character-buffered instead of line-buffered. + * Allow ctrl-s and ctrl-q keypress to be used (as forward search) + */ + +// raw mode +// t->c_iflag &= ~(IMAXBEL|IXOFF|INPCK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL|IXON|IGNPAR); +// t->c_iflag |= IGNBRK; +// t->c_oflag &= ~OPOST; +// t->c_lflag &= ~(ECHO|ECHOE|ECHOK|ECHONL|ICANON|ISIG|IEXTEN|NOFLSH|TOSTOP|PENDIN); +// t->c_cflag &= ~(CSIZE|PARENB); +// t->c_cflag |= CS8|CREAD; +// t->c_cc[VMIN] = 1; +// t->c_cc[VTIME] = 0; + + + if (this.term.tcgetattr(0, this.termInfo) !=0) { + throw new IOException("Failed to get terminal info"); + } + + this.termInfo.c_iflag &= ~PosixTerminalControl.IXON; // DISABLE - flow control mediated by ^S and ^Q +// struct.c_iflag |= PosixTerminalControl.IUTF8; // DISABLE - flow control mediated by ^S and ^Q + + this.termInfo.c_lflag &= ~PosixTerminalControl.ICANON; // DISABLE - canonical mode (pass chars straight through to terminal) +// struct.c_lflag &= ~PosixTerminalControl.ISIG; // DISABLE - When any of the characters INTR, QUIT, SUSP, or DSUSP are received, generate the corresponding signal. + + + // If MIN > 0 and TIME = 0, MIN sets the number of characters to receive before the read is satisfied. As TIME is zero, the timer is not used. + this.termInfo.c_cc[PosixTerminalControl.VMIN] = 1; // Minimum number of characters for noncanonical read (MIN). + this.termInfo.c_cc[PosixTerminalControl.VTIME] = 0; // Timeout in deciseconds for noncanonical read (TIME). + + this.termInfo.c_cc[PosixTerminalControl.VSUSP] = 0; // suspend disabled + this.termInfo.c_cc[PosixTerminalControl.VEOF] = 0; // eof disabled + this.termInfo.c_cc[PosixTerminalControl.VEOL] = 0; // eol disabled + + + if (this.term.tcsetattr(0, PosixTerminalControl.TCSANOW, this.termInfo) != 0) { + throw new IOException("Can not set terminal flags"); + } + } + + /** + * Restore the original terminal configuration, which can be used when + * shutting down the console reader. The ConsoleReader cannot be + * used after calling this method. + */ + @Override + public void restore() throws IOException { + if (this.term.tcsetattr(0, PosixTerminalControl.TCSANOW, this.termInfoDefault) != 0) { + throw new IOException("Can not reset terminal to defaults"); + } + } + + /** + * Returns number of columns in the terminal. + */ + @Override + public int getWidth() { + if (this.term.ioctl(0, PosixTerminalControl.TIOCGWINSZ, this.windowSizeBuffer) != 0) { + return DEFAULT_WIDTH; + } + + short columns = (short)(0x000000FF &this.windowSizeBuffer.get(2) + (0x000000FF & this.windowSizeBuffer.get(3)) * 256); + return columns; + } + + /** + * Returns number of rows in the terminal. + */ + @Override + public int getHeight() { + if (this.term.ioctl(0, PosixTerminalControl.TIOCGWINSZ, this.windowSizeBuffer) != 0) { + return DEFAULT_HEIGHT; + } + + short rows = (short)(0x000000FF &this.windowSizeBuffer.get(0) + (0x000000FF & this.windowSizeBuffer.get(1)) * 256); + return rows; + } + + @Override + public synchronized void setEchoEnabled(final boolean enabled) { + // have to reget them, since flags change everything + if (this.term.tcgetattr(0, this.termInfo) !=0) { + this.logger.error("Failed to get terminal info"); + } + + if (enabled) { + this.termInfo.c_lflag |= PosixTerminalControl.ECHO; // ENABLE Echo input characters. + } + else { + this.termInfo.c_lflag &= ~PosixTerminalControl.ECHO; // DISABLE Echo input characters. + } + + if (this.term.tcsetattr(0, PosixTerminalControl.TCSANOW, this.termInfo) != 0) { + this.logger.error("Can not set terminal flags"); + } + + super.setEchoEnabled(enabled); + } + + public void disableInterruptCharacter() { + // have to re-get them, since flags change everything + if (this.term.tcgetattr(0, this.termInfo) !=0) { + this.logger.error("Failed to get terminal info"); + } + + this.termInfo.c_cc[PosixTerminalControl.VINTR] = 0; // interrupt disabled + + if (this.term.tcsetattr(0, PosixTerminalControl.TCSANOW, this.termInfo) != 0) { + this.logger.error("Can not set terminal flags"); + } + } + + public void enableInterruptCharacter() { + // have to re-get them, since flags change everything + if (this.term.tcgetattr(0, this.termInfo) !=0) { + this.logger.error("Failed to get terminal info"); + } + + this.termInfo.c_cc[PosixTerminalControl.VINTR] = 3; // interrupt is ctrl-c + + if (this.term.tcsetattr(0, PosixTerminalControl.TCSANOW, this.termInfo) != 0) { + this.logger.error("Can not set terminal flags"); + } + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/input/unsupported/UnsupportedTerminal.java b/Dorkbox-Util/src/dorkbox/util/input/unsupported/UnsupportedTerminal.java new file mode 100644 index 0000000..54affae --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/input/unsupported/UnsupportedTerminal.java @@ -0,0 +1,30 @@ +package dorkbox.util.input.unsupported; + +import java.io.IOException; + +import dorkbox.util.input.Terminal; + +public class UnsupportedTerminal extends Terminal { + public UnsupportedTerminal() { +// setAnsiSupported(false); + setEchoEnabled(true); + } + + @Override + public void init() throws IOException { + } + + @Override + public void restore() { + } + + @Override + public int getWidth() { + return 0; + } + + @Override + public int getHeight() { + return 0; + } +} \ No newline at end of file diff --git a/Dorkbox-Util/src/dorkbox/util/input/windows/ConsoleMode.java b/Dorkbox-Util/src/dorkbox/util/input/windows/ConsoleMode.java new file mode 100644 index 0000000..428ef17 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/input/windows/ConsoleMode.java @@ -0,0 +1,71 @@ +package dorkbox.util.input.windows; + +/** + * Console mode + *

+ * Constants copied wincon.h. + */ +public enum ConsoleMode { + /** + * The ReadFile or ReadConsole function returns only when a carriage return + * character is read. If this mode is disable, the functions return when one + * or more characters are available. + */ + ENABLE_LINE_INPUT(2), + + /** + * Characters read by the ReadFile or ReadConsole function are written to + * the active screen buffer as they are read. This mode can be used only if + * the ENABLE_LINE_INPUT mode is also enabled. + */ + ENABLE_ECHO_INPUT(4), + + /** + * CTRL+C is processed by the system and is not placed in the input buffer. + * If the input buffer is being read by ReadFile or ReadConsole, other + * control keys are processed by the system and are not returned in the + * ReadFile or ReadConsole buffer. If the ENABLE_LINE_INPUT mode is also + * enabled, backspace, carriage return, and linefeed characters are handled + * by the system. + */ + ENABLE_PROCESSED_INPUT(1), + + /** + * User interactions that change the size of the console screen buffer are + * reported in the console's input buffee. Information about these events + * can be read from the input buffer by applications using + * theReadConsoleInput function, but not by those using ReadFile + * orReadConsole. + */ + ENABLE_WINDOW_INPUT(8), + + /** + * If the mouse pointer is within the borders of the console window and the + * window has the keyboard focus, mouse events generated by mouse movement + * and button presses are placed in the input buffer. These events are + * discarded by ReadFile or ReadConsole, even when this mode is enabled. + */ + ENABLE_MOUSE_INPUT(16), + + /** + * When enabled, text entered in a console window will be inserted at the + * current cursor location and all text following that location will not be + * overwritten. When disabled, all following text will be overwritten. An OR + * operation must be performed with this flag and the ENABLE_EXTENDED_FLAGS + * flag to enable this functionality. + */ + ENABLE_PROCESSED_OUTPUT(1), + + /** + * This flag enables the user to use the mouse to select and edit text. To + * enable this option, use the OR to combine this flag with + * ENABLE_EXTENDED_FLAGS. + */ + ENABLE_WRAP_AT_EOL_OUTPUT(2),; + + public final int code; + + ConsoleMode(final int code) { + this.code = code; + } +} \ No newline at end of file diff --git a/Dorkbox-Util/src/dorkbox/util/input/windows/WindowsTerminal.java b/Dorkbox-Util/src/dorkbox/util/input/windows/WindowsTerminal.java new file mode 100644 index 0000000..25cc950 --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/input/windows/WindowsTerminal.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2002-2012, the original author or authors. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * http://www.opensource.org/licenses/bsd-license.php + */ +package dorkbox.util.input.windows; + +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.fusesource.jansi.internal.WindowsSupport; + +import dorkbox.util.input.Terminal; + +/** + * Terminal implementation for Microsoft Windows. Terminal initialization in + * {@link #init} is accomplished by calling the Win32 APIs SetConsoleMode and + * GetConsoleMode to + * disable character echoing. + *

+ *

+ * By default, the {@link #wrapInIfNeeded(java.io.InputStream)} method will attempt + * to test to see if the specified {@link InputStream} is {@link System#in} or a wrapper + * around {@link FileDescriptor#in}, and if so, will bypass the character reading to + * directly invoke the readc() method in the JNI library. This is so the class + * can read special keys (like arrow keys) which are otherwise inaccessible via + * the {@link System#in} stream. Using JNI reading can be bypassed by setting + * the jline.WindowsTerminal.directConsole system property + * to false. + *

+ * + * @author Marc Prud'hommeaux + * @author Jason Dillon + * @since 2.0 + */ +public class WindowsTerminal extends Terminal +{ + public static final String DIRECT_CONSOLE = WindowsTerminal.class.getName() + ".directConsole"; + + private int originalMode; + + public WindowsTerminal() { + } + + @Override + public void init() throws IOException { + this.originalMode = WindowsSupport.getConsoleMode(); + WindowsSupport.setConsoleMode(this.originalMode & ~ConsoleMode.ENABLE_ECHO_INPUT.code); + setEchoEnabled(false); + } + + /** + * Restore the original terminal configuration, which can be used when + * shutting down the console reader. The ConsoleReader cannot be + * used after calling this method. + */ + @Override + public void restore() throws IOException { + // restore the old console mode + WindowsSupport.setConsoleMode(this.originalMode); + } + + @Override + public int getWidth() { + int w = WindowsSupport.getWindowsTerminalWidth(); + return w < 1 ? DEFAULT_WIDTH : w; + } + + @Override + public int getHeight() { + int h = WindowsSupport.getWindowsTerminalHeight(); + return h < 1 ? DEFAULT_HEIGHT : h; + } + + @Override + public void setEchoEnabled(final boolean enabled) { + // Must set these four modes at the same time to make it work fine. + if (enabled) { + WindowsSupport.setConsoleMode(WindowsSupport.getConsoleMode() | + ConsoleMode.ENABLE_ECHO_INPUT.code | + ConsoleMode.ENABLE_LINE_INPUT.code | + ConsoleMode.ENABLE_PROCESSED_INPUT.code | + ConsoleMode.ENABLE_WINDOW_INPUT.code); + } + else { + WindowsSupport.setConsoleMode(WindowsSupport.getConsoleMode() & + ~(ConsoleMode.ENABLE_LINE_INPUT.code | + ConsoleMode.ENABLE_ECHO_INPUT.code | + ConsoleMode.ENABLE_PROCESSED_INPUT.code | + ConsoleMode.ENABLE_WINDOW_INPUT.code)); + } + super.setEchoEnabled(enabled); + } + + + @Override + public InputStream wrapInIfNeeded(InputStream in) throws IOException { + if (isSystemIn(in)) { + return new InputStream() { + @Override + public int read() throws IOException { + return WindowsSupport.readByte(); + } + }; + } else { + return in; + } + } + + private boolean isSystemIn(final InputStream in) throws IOException { + if (in == null) { + return false; + } + else if (in == System.in) { + return true; + } + else if (in instanceof FileInputStream && ((FileInputStream) in).getFD() == FileDescriptor.in) { + return true; + } + + return false; + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/process/ShellProcessBuilder.java b/Dorkbox-Util/src/dorkbox/util/process/ShellProcessBuilder.java index 89adc68..5ac78c2 100644 --- a/Dorkbox-Util/src/dorkbox/util/process/ShellProcessBuilder.java +++ b/Dorkbox-Util/src/dorkbox/util/process/ShellProcessBuilder.java @@ -115,7 +115,10 @@ public class ShellProcessBuilder { } else { // *nix executableName = "/bin/bash"; - // executableName = "/bin/sh"; + File file = new File(executableName); + if (!file.canExecute()) { + executableName = "/bin/sh"; + } arguments.add(0, "-c"); } } else if (workingDirectory != null) {