/*
* 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
}
}
}
}
}