1 /*
2  * Copyright (c) 2002-2018, the original author or authors.
3  *
4  * This software is distributable under the BSD license. See the terms of the
5  * BSD license in the documentation provided with this software.
6  *
7  * http://www.opensource.org/licenses/bsd-license.php
8  */
9 package jdk.internal.org.jline.terminal;
10 
11 import java.io.FileDescriptor;
12 import java.io.FileInputStream;
13 import java.io.FileOutputStream;
14 import java.io.IOException;
15 import java.io.InputStream;
16 import java.io.OutputStream;
17 import java.lang.reflect.Method;
18 import java.nio.charset.Charset;
19 import java.nio.charset.UnsupportedCharsetException;
20 import java.util.Optional;
21 import java.util.ServiceLoader;
22 import java.util.function.Function;
23 
24 import jdk.internal.org.jline.terminal.impl.AbstractPosixTerminal;
25 import jdk.internal.org.jline.terminal.impl.DumbTerminal;
26 import jdk.internal.org.jline.terminal.impl.ExecPty;
27 import jdk.internal.org.jline.terminal.impl.ExternalTerminal;
28 import jdk.internal.org.jline.terminal.impl.PosixPtyTerminal;
29 import jdk.internal.org.jline.terminal.impl.PosixSysTerminal;
30 import jdk.internal.org.jline.terminal.spi.JansiSupport;
31 import jdk.internal.org.jline.terminal.spi.JnaSupport;
32 import jdk.internal.org.jline.terminal.spi.Pty;
33 import jdk.internal.org.jline.utils.Log;
34 import jdk.internal.org.jline.utils.OSUtils;
35 
36 import static jdk.internal.org.jline.terminal.impl.AbstractWindowsTerminal.TYPE_WINDOWS;
37 import static jdk.internal.org.jline.terminal.impl.AbstractWindowsTerminal.TYPE_WINDOWS_256_COLOR;
38 
39 /**
40  * Builder class to create terminals.
41  */
42 public final class TerminalBuilder {
43 
44     //
45     // System properties
46     //
47 
48     public static final String PROP_ENCODING = "org.jline.terminal.encoding";
49     public static final String PROP_CODEPAGE = "org.jline.terminal.codepage";
50     public static final String PROP_TYPE = "org.jline.terminal.type";
51     public static final String PROP_JNA = "org.jline.terminal.jna";
52     public static final String PROP_JANSI = "org.jline.terminal.jansi";
53     public static final String PROP_EXEC = "org.jline.terminal.exec";
54     public static final String PROP_DUMB = "org.jline.terminal.dumb";
55     public static final String PROP_DUMB_COLOR = "org.jline.terminal.dumb.color";
56 
57     //
58     // Other system properties controlling various jline parts
59     //
60 
61     public static final String PROP_NON_BLOCKING_READS = "org.jline.terminal.pty.nonBlockingReads";
62     public static final String PROP_COLOR_DISTANCE = "org.jline.utils.colorDistance";
63     public static final String PROP_DISABLE_ALTERNATE_CHARSET = "org.jline.utils.disableAlternateCharset";
64 
65     /**
66      * Returns the default system terminal.
67      * Terminals should be closed properly using the {@link Terminal#close()}
68      * method in order to restore the original terminal state.
69      *
70      * <p>
71      * This call is equivalent to:
72      * <code>builder().build()</code>
73      * </p>
74      *
75      * @return the default system terminal
76      * @throws IOException if an error occurs
77      */
terminal()78     public static Terminal terminal() throws IOException {
79         return builder().build();
80     }
81 
82     /**
83      * Creates a new terminal builder instance.
84      *
85      * @return a builder
86      */
builder()87     public static TerminalBuilder builder() {
88         return new TerminalBuilder();
89     }
90 
91     private String name;
92     private InputStream in;
93     private OutputStream out;
94     private String type;
95     private Charset encoding;
96     private int codepage;
97     private Boolean system;
98     private Boolean jna;
99     private Boolean jansi;
100     private Boolean exec;
101     private Boolean dumb;
102     private Attributes attributes;
103     private Size size;
104     private boolean nativeSignals = false;
105     private Terminal.SignalHandler signalHandler = Terminal.SignalHandler.SIG_DFL;
106     private boolean paused = false;
107     private Function<InputStream, InputStream> inputStreamWrapper = in -> in;
108 
TerminalBuilder()109     private TerminalBuilder() {
110     }
111 
name(String name)112     public TerminalBuilder name(String name) {
113         this.name = name;
114         return this;
115     }
116 
streams(InputStream in, OutputStream out)117     public TerminalBuilder streams(InputStream in, OutputStream out) {
118         this.in = in;
119         this.out = out;
120         return this;
121     }
122 
system(boolean system)123     public TerminalBuilder system(boolean system) {
124         this.system = system;
125         return this;
126     }
127 
jna(boolean jna)128     public TerminalBuilder jna(boolean jna) {
129         this.jna = jna;
130         return this;
131     }
132 
jansi(boolean jansi)133     public TerminalBuilder jansi(boolean jansi) {
134         this.jansi = jansi;
135         return this;
136     }
137 
exec(boolean exec)138     public TerminalBuilder exec(boolean exec) {
139         this.exec = exec;
140         return this;
141     }
142 
dumb(boolean dumb)143     public TerminalBuilder dumb(boolean dumb) {
144         this.dumb = dumb;
145         return this;
146     }
147 
type(String type)148     public TerminalBuilder type(String type) {
149         this.type = type;
150         return this;
151     }
152 
153     /**
154      * Set the encoding to use for reading/writing from the console.
155      * If {@code null} (the default value), JLine will automatically select
156      * a {@link Charset}, usually the default system encoding. However,
157      * on some platforms (e.g. Windows) it may use a different one depending
158      * on the {@link Terminal} implementation.
159      *
160      * <p>Use {@link Terminal#encoding()} to get the {@link Charset} that
161      * should be used for a {@link Terminal}.</p>
162      *
163      * @param encoding The encoding to use or null to automatically select one
164      * @return The builder
165      * @throws UnsupportedCharsetException If the given encoding is not supported
166      * @see Terminal#encoding()
167      */
encoding(String encoding)168     public TerminalBuilder encoding(String encoding) throws UnsupportedCharsetException {
169         return encoding(encoding != null ? Charset.forName(encoding) : null);
170     }
171 
172     /**
173      * Set the {@link Charset} to use for reading/writing from the console.
174      * If {@code null} (the default value), JLine will automatically select
175      * a {@link Charset}, usually the default system encoding. However,
176      * on some platforms (e.g. Windows) it may use a different one depending
177      * on the {@link Terminal} implementation.
178      *
179      * <p>Use {@link Terminal#encoding()} to get the {@link Charset} that
180      * should be used to read/write from a {@link Terminal}.</p>
181      *
182      * @param encoding The encoding to use or null to automatically select one
183      * @return The builder
184      * @see Terminal#encoding()
185      */
encoding(Charset encoding)186     public TerminalBuilder encoding(Charset encoding) {
187         this.encoding = encoding;
188         return this;
189     }
190 
191     /**
192      * @param codepage the codepage
193      * @return The builder
194      * @deprecated JLine now writes Unicode output independently from the selected
195      *   code page. Using this option will only make it emulate the selected code
196      *   page for {@link Terminal#input()} and {@link Terminal#output()}.
197      */
198     @Deprecated
codepage(int codepage)199     public TerminalBuilder codepage(int codepage) {
200         this.codepage = codepage;
201         return this;
202     }
203 
204     /**
205      * Attributes to use when creating a non system terminal,
206      * i.e. when the builder has been given the input and
207      * outut streams using the {@link #streams(InputStream, OutputStream)} method
208      * or when {@link #system(boolean)} has been explicitely called with
209      * <code>false</code>.
210      *
211      * @param attributes the attributes to use
212      * @return The builder
213      * @see #size(Size)
214      * @see #system(boolean)
215      */
attributes(Attributes attributes)216     public TerminalBuilder attributes(Attributes attributes) {
217         this.attributes = attributes;
218         return this;
219     }
220 
221     /**
222      * Initial size to use when creating a non system terminal,
223      * i.e. when the builder has been given the input and
224      * outut streams using the {@link #streams(InputStream, OutputStream)} method
225      * or when {@link #system(boolean)} has been explicitely called with
226      * <code>false</code>.
227      *
228      * @param size the initial size
229      * @return The builder
230      * @see #attributes(Attributes)
231      * @see #system(boolean)
232      */
size(Size size)233     public TerminalBuilder size(Size size) {
234         this.size = size;
235         return this;
236     }
237 
nativeSignals(boolean nativeSignals)238     public TerminalBuilder nativeSignals(boolean nativeSignals) {
239         this.nativeSignals = nativeSignals;
240         return this;
241     }
242 
signalHandler(Terminal.SignalHandler signalHandler)243     public TerminalBuilder signalHandler(Terminal.SignalHandler signalHandler) {
244         this.signalHandler = signalHandler;
245         return this;
246     }
247 
248     /**
249      * Initial paused state of the terminal (defaults to false).
250      * By default, the terminal is started, but in some cases,
251      * one might want to make sure the input stream is not consumed
252      * before needed, in which case the terminal needs to be created
253      * in a paused state.
254      * @param paused the initial paused state
255      * @return The builder
256      * @see Terminal#pause()
257      */
paused(boolean paused)258     public TerminalBuilder paused(boolean paused) {
259         this.paused = paused;
260         return this;
261     }
262 
inputStreamWrapper(Function<InputStream, InputStream> wrapper)263     public TerminalBuilder inputStreamWrapper(Function<InputStream, InputStream> wrapper) {
264         this.inputStreamWrapper = wrapper;
265         return this;
266     }
267 
build()268     public Terminal build() throws IOException {
269         Terminal terminal = doBuild();
270         Log.debug(() -> "Using terminal " + terminal.getClass().getSimpleName());
271         if (terminal instanceof AbstractPosixTerminal) {
272             Log.debug(() -> "Using pty " + ((AbstractPosixTerminal) terminal).getPty().getClass().getSimpleName());
273         }
274         return terminal;
275     }
276 
doBuild()277     private Terminal doBuild() throws IOException {
278         String name = this.name;
279         if (name == null) {
280             name = "JLine terminal";
281         }
282         Charset encoding = this.encoding;
283         if (encoding == null) {
284             String charsetName = System.getProperty(PROP_ENCODING);
285             if (charsetName != null && Charset.isSupported(charsetName)) {
286                 encoding = Charset.forName(charsetName);
287             }
288         }
289         int codepage = this.codepage;
290         if (codepage <= 0) {
291             String str = System.getProperty(PROP_CODEPAGE);
292             if (str != null) {
293                 codepage = Integer.parseInt(str);
294             }
295         }
296         String type = this.type;
297         if (type == null) {
298             type = System.getProperty(PROP_TYPE);
299         }
300         if (type == null) {
301             type = System.getenv("TERM");
302         }
303         Boolean jna = this.jna;
304         if (jna == null) {
305             jna = getBoolean(PROP_JNA, true);
306         }
307         Boolean jansi = this.jansi;
308         if (jansi == null) {
309             jansi = getBoolean(PROP_JANSI, true);
310         }
311         Boolean exec = this.exec;
312         if (exec == null) {
313             exec = getBoolean(PROP_EXEC, true);
314         }
315         Boolean dumb = this.dumb;
316         if (dumb == null) {
317             dumb = getBoolean(PROP_DUMB, null);
318         }
319         if ((system != null && system) || (system == null && in == null && out == null)) {
320             if (attributes != null || size != null) {
321                 Log.warn("Attributes and size fields are ignored when creating a system terminal");
322             }
323             IllegalStateException exception = new IllegalStateException("Unable to create a system terminal");
324             if (OSUtils.IS_WINDOWS) {
325                 boolean cygwinTerm = "cygwin".equals(System.getenv("TERM"));
326                 boolean ansiPassThrough = OSUtils.IS_CONEMU;
327                 //
328                 // Cygwin support
329                 //
330                 if ((OSUtils.IS_CYGWIN || OSUtils.IS_MSYSTEM) && exec && !cygwinTerm) {
331                     try {
332                         Pty pty = ExecPty.current();
333                         // Cygwin defaults to XTERM, but actually supports 256 colors,
334                         // so if the value comes from the environment, change it to xterm-256color
335                         if ("xterm".equals(type) && this.type == null && System.getProperty(PROP_TYPE) == null) {
336                             type = "xterm-256color";
337                         }
338                         return new PosixSysTerminal(name, type, pty, inputStreamWrapper.apply(pty.getSlaveInput()), pty.getSlaveOutput(), encoding, nativeSignals, signalHandler);
339                     } catch (IOException e) {
340                         // Ignore if not a tty
341                         Log.debug("Error creating EXEC based terminal: ", e.getMessage(), e);
342                         exception.addSuppressed(e);
343                     }
344                 }
345                 if (jna) {
346                     try {
347                         return load(JnaSupport.class).winSysTerminal(name, type, ansiPassThrough, encoding, codepage, nativeSignals, signalHandler, paused, inputStreamWrapper);
348                     } catch (Throwable t) {
349                         Log.debug("Error creating JNA based terminal: ", t.getMessage(), t);
350                         exception.addSuppressed(t);
351                     }
352                 }
353                 if (jansi) {
354                     try {
355                         return load(JansiSupport.class).winSysTerminal(name, type, ansiPassThrough, encoding, codepage, nativeSignals, signalHandler, paused);
356                     } catch (Throwable t) {
357                         Log.debug("Error creating JANSI based terminal: ", t.getMessage(), t);
358                         exception.addSuppressed(t);
359                     }
360                 }
361             } else {
362                 if (jna) {
363                     try {
364                         Pty pty = load(JnaSupport.class).current();
365                         return new PosixSysTerminal(name, type, pty, inputStreamWrapper.apply(pty.getSlaveInput()), pty.getSlaveOutput(), encoding, nativeSignals, signalHandler);
366                     } catch (Throwable t) {
367                         // ignore
368                         Log.debug("Error creating JNA based terminal: ", t.getMessage(), t);
369                         exception.addSuppressed(t);
370                     }
371                 }
372                 if (jansi) {
373                     try {
374                         Pty pty = load(JansiSupport.class).current();
375                         return new PosixSysTerminal(name, type, pty, inputStreamWrapper.apply(pty.getSlaveInput()), pty.getSlaveOutput(), encoding, nativeSignals, signalHandler);
376                     } catch (Throwable t) {
377                         Log.debug("Error creating JANSI based terminal: ", t.getMessage(), t);
378                         exception.addSuppressed(t);
379                     }
380                 }
381                 if (exec) {
382                     try {
383                         Pty pty = ExecPty.current();
384                         return new PosixSysTerminal(name, type, pty, inputStreamWrapper.apply(pty.getSlaveInput()), pty.getSlaveOutput(), encoding, nativeSignals, signalHandler);
385                     } catch (Throwable t) {
386                         // Ignore if not a tty
387                         Log.debug("Error creating EXEC based terminal: ", t.getMessage(), t);
388                         exception.addSuppressed(t);
389                     }
390                 }
391             }
392             if (dumb == null || dumb) {
393                 // forced colored dumb terminal
394                 boolean color = getBoolean(PROP_DUMB_COLOR, false);
395                 // detect emacs using the env variable
396                 if (!color) {
397                     color = System.getenv("INSIDE_EMACS") != null;
398                 }
399                 // detect Intellij Idea
400                 if (!color) {
401                     String command = getParentProcessCommand();
402                     color = command != null && command.contains("idea");
403                 }
404                 if (!color && dumb == null) {
405                     if (Log.isDebugEnabled()) {
406                         Log.warn("Creating a dumb terminal", exception);
407                     } else {
408                         Log.warn("Unable to create a system terminal, creating a dumb terminal (enable debug logging for more information)");
409                     }
410                 }
411                 return new DumbTerminal(name, color ? Terminal.TYPE_DUMB_COLOR : Terminal.TYPE_DUMB,
412                         new FileInputStream(FileDescriptor.in),
413                         new FileOutputStream(FileDescriptor.out),
414                         encoding, signalHandler);
415             } else {
416                 throw exception;
417             }
418         } else {
419             if (jna) {
420                 try {
421                     Pty pty = load(JnaSupport.class).open(attributes, size);
422                     return new PosixPtyTerminal(name, type, pty, in, out, encoding, signalHandler, paused);
423                 } catch (Throwable t) {
424                     Log.debug("Error creating JNA based terminal: ", t.getMessage(), t);
425                 }
426             }
427             if (jansi) {
428                 try {
429                     Pty pty = load(JansiSupport.class).open(attributes, size);
430                     return new PosixPtyTerminal(name, type, pty, in, out, encoding, signalHandler, paused);
431                 } catch (Throwable t) {
432                     Log.debug("Error creating JANSI based terminal: ", t.getMessage(), t);
433                 }
434             }
435             Terminal terminal = new ExternalTerminal(name, type, in, out, encoding, signalHandler, paused);
436             if (attributes != null) {
437                 terminal.setAttributes(attributes);
438             }
439             if (size != null) {
440                 terminal.setSize(size);
441             }
442             return terminal;
443         }
444     }
445 
getParentProcessCommand()446     private static String getParentProcessCommand() {
447         try {
448             Class<?> phClass = Class.forName("java.lang.ProcessHandle");
449             Object current = phClass.getMethod("current").invoke(null);
450             Object parent = ((Optional<?>) phClass.getMethod("parent").invoke(current)).orElse(null);
451             Method infoMethod = phClass.getMethod("info");
452             Object info = infoMethod.invoke(parent);
453             Object command = ((Optional<?>) infoMethod.getReturnType().getMethod("command").invoke(info)).orElse(null);
454             return (String) command;
455         } catch (Throwable t) {
456             return null;
457         }
458     }
459 
getBoolean(String name, Boolean def)460     private static Boolean getBoolean(String name, Boolean def) {
461         try {
462             String str = System.getProperty(name);
463             if (str != null) {
464                 return Boolean.parseBoolean(str);
465             }
466         } catch (IllegalArgumentException | NullPointerException e) {
467         }
468         return def;
469     }
470 
load(Class<S> clazz)471     private <S> S load(Class<S> clazz) {
472         return ServiceLoader.load(clazz, clazz.getClassLoader()).iterator().next();
473     }
474 }
475