/* * Copyright (c) 2002-2016, 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 jline.internal; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Method; import java.text.MessageFormat; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import static jline.internal.Preconditions.checkNotNull; /** * Provides access to terminal line settings via <tt>stty</tt>. * * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a> * @author <a href="mailto:dwkemp@gmail.com">Dale Kemp</a> * @author <a href="mailto:jason@planet57.com">Jason Dillon</a> * @author <a href="mailto:jbonofre@apache.org">Jean-Baptiste Onofré</a> * @author <a href="mailto:gnodet@gmail.com">Guillaume Nodet</a> * @since 2.0 */ public final class TerminalLineSettings { public static final String JLINE_STTY = "jline.stty"; public static final String DEFAULT_STTY = "stty"; public static final String JLINE_SH = "jline.sh"; public static final String DEFAULT_SH = "sh"; private static final String UNDEFINED; public static final String DEFAULT_TTY = "/dev/tty"; private static final boolean SUPPORTS_REDIRECT; private static final Object REDIRECT_INHERIT; private static final Method REDIRECT_INPUT_METHOD; private static final Map<String, TerminalLineSettings> SETTINGS = new HashMap<String, TerminalLineSettings>(); static { if (Configuration.isHpux()) { UNDEFINED = "^-"; } else { UNDEFINED = "undef"; } boolean supportsRedirect; Object redirectInherit = null; Method redirectInputMethod = null; try { Class<?> redirect = Class.forName("java.lang.ProcessBuilder$Redirect"); redirectInherit = redirect.getField("INHERIT").get(null); redirectInputMethod = ProcessBuilder.class.getMethod("redirectInput", redirect); supportsRedirect = System.class.getMethod("console").invoke(null) != null; } catch (Throwable t) { supportsRedirect = false; } SUPPORTS_REDIRECT = supportsRedirect; REDIRECT_INHERIT = redirectInherit; REDIRECT_INPUT_METHOD = redirectInputMethod; } private String sttyCommand; private String shCommand; private String ttyDevice; private String config; private String initialConfig; private long configLastFetched; private boolean useRedirect; @Deprecated public TerminalLineSettings() throws IOException, InterruptedException { this(DEFAULT_TTY); } @Deprecated public TerminalLineSettings(String ttyDevice) throws IOException, InterruptedException { this(ttyDevice, false); } private TerminalLineSettings(String ttyDevice, boolean unused) throws IOException, InterruptedException { checkNotNull(ttyDevice); this.sttyCommand = Configuration.getString(JLINE_STTY, DEFAULT_STTY); this.shCommand = Configuration.getString(JLINE_SH, DEFAULT_SH); this.ttyDevice = ttyDevice; this.useRedirect = SUPPORTS_REDIRECT && DEFAULT_TTY.equals(ttyDevice); this.initialConfig = get("-g").trim(); this.config = get("-a"); this.configLastFetched = System.currentTimeMillis(); Log.debug("Config: ", config); // sanity check if (config.length() == 0) { throw new IOException(MessageFormat.format("Unrecognized stty code: {0}", config)); } } public static synchronized TerminalLineSettings getSettings(String device) throws IOException, InterruptedException { TerminalLineSettings settings = SETTINGS.get(device); if (settings == null) { settings = new TerminalLineSettings(device, false); SETTINGS.put(device, settings); } return settings; } public String getTtyDevice() { return ttyDevice; } public String getConfig() { return config; } public void restore() throws IOException, InterruptedException { set(initialConfig); } public String get(final String args) throws IOException, InterruptedException { checkNotNull(args); return stty(args); } public void set(final String args) throws IOException, InterruptedException { checkNotNull(args); stty(args.split(" ")); } public void set(final String... args) throws IOException, InterruptedException { checkNotNull(args); stty(args); } public void undef(final String name) throws IOException, InterruptedException { checkNotNull(name); stty(name, UNDEFINED); } /** * <p> * Get the value of a stty property, including the management of a cache. * </p> * * @param name the stty property. * @return the stty property value. */ public int getProperty(String name) { checkNotNull(name); if (!fetchConfig(name)) { return -1; } return getProperty(name, config); } public String getPropertyAsString(String name) { checkNotNull(name); if (!fetchConfig(name)) { return null; } return getPropertyAsString(name, config); } private boolean fetchConfig(String name) { long currentTime = System.currentTimeMillis(); try { // tty properties are cached so we don't have to worry too much about getting term width/height if (config == null || currentTime - configLastFetched > 1000) { config = get("-a"); } } catch (Exception e) { if (e instanceof InterruptedException) { Thread.currentThread().interrupt(); } Log.debug("Failed to query stty ", name, "\n", e); if (config == null) { return false; } } // always update the last fetched time and try to parse the output if (currentTime - configLastFetched > 1000) { configLastFetched = currentTime; } return true; } /** * <p> * Parses a stty output (provided by stty -a) and return the value of a given property. * </p> * * @param name property name. * @param stty string resulting of stty -a execution. * @return value of the given property. */ protected static String getPropertyAsString(String name, String stty) { // try the first kind of regex Pattern pattern = Pattern.compile(name + "\\s+=\\s+(.*?)[;\\n\\r]"); Matcher matcher = pattern.matcher(stty); if (!matcher.find()) { // try a second kind of regex pattern = Pattern.compile(name + "\\s+([^;]*)[;\\n\\r]"); matcher = pattern.matcher(stty); if (!matcher.find()) { // try a second try of regex pattern = Pattern.compile("(\\S*)\\s+" + name); matcher = pattern.matcher(stty); if (!matcher.find()) { return null; } } } return matcher.group(1); } protected static int getProperty(String name, String stty) { String str = getPropertyAsString(name, stty); return str != null ? parseControlChar(str) : -1; } private static int parseControlChar(String str) { // under if ("<undef>".equals(str)) { return -1; } // octal if (str.charAt(0) == '0') { return Integer.parseInt(str, 8); } // decimal if (str.charAt(0) >= '1' && str.charAt(0) <= '9') { return Integer.parseInt(str, 10); } // control char if (str.charAt(0) == '^') { if (str.charAt(1) == '?') { return 127; } else { return str.charAt(1) - 64; } } else if (str.charAt(0) == 'M' && str.charAt(1) == '-') { if (str.charAt(2) == '^') { if (str.charAt(3) == '?') { return 127 + 128; } else { return str.charAt(3) - 64 + 128; } } else { return str.charAt(2) + 128; } } else { return str.charAt(0); } } private String stty(final String... args) throws IOException, InterruptedException { String[] s = new String[args.length + 1]; s[0] = sttyCommand; System.arraycopy(args, 0, s, 1, args.length); return exec(s); } private String exec(final String... cmd) throws IOException, InterruptedException { checkNotNull(cmd); Log.trace("Running: ", cmd); Process p = null; if (useRedirect) { try { p = inheritInput(new ProcessBuilder(cmd)).start(); } catch (Throwable t) { useRedirect = false; } } if (p == null) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < cmd.length; i++) { if (i > 0) { sb.append(' '); } sb.append(cmd[i]); } sb.append(" < "); sb.append(ttyDevice); p = new ProcessBuilder(shCommand, "-c", sb.toString()).start(); } String result = waitAndCapture(p); Log.trace("Result: ", result); return result; } private static ProcessBuilder inheritInput(ProcessBuilder pb) throws Exception { REDIRECT_INPUT_METHOD.invoke(pb, REDIRECT_INHERIT); return pb; } public static String waitAndCapture(Process p) throws IOException, InterruptedException { ByteArrayOutputStream bout = new ByteArrayOutputStream(); InputStream in = null; InputStream err = null; OutputStream out = null; try { int c; in = p.getInputStream(); while ((c = in.read()) != -1) { bout.write(c); } err = p.getErrorStream(); while ((c = err.read()) != -1) { bout.write(c); } out = p.getOutputStream(); p.waitFor(); } finally { close(in, out, err); } return bout.toString(); } private static void close(final Closeable... closeables) { for (Closeable c : closeables) { if (c != null) { try { c.close(); } catch (Exception e) { // Ignore } } } } }