src/jdk.internal.le/share/classes/jdk/internal/org/jline/terminal/impl/LineDisciplineTerminal.java
author jlahoda
Mon, 04 Nov 2019 09:40:35 +0100
changeset 58903 eeb1c0da2126
parent 52938 5ff7480c9e28
permissions -rw-r--r--
8229815: Upgrade Jline to 3.12.1 Reviewed-by: rfield

/*
 * Copyright (c) 2002-2018, 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.
 *
 * https://opensource.org/licenses/BSD-3-Clause
 */
package jdk.internal.org.jline.terminal.impl;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.util.Objects;

import jdk.internal.org.jline.terminal.Attributes;
import jdk.internal.org.jline.terminal.Attributes.ControlChar;
import jdk.internal.org.jline.terminal.Attributes.InputFlag;
import jdk.internal.org.jline.terminal.Attributes.LocalFlag;
import jdk.internal.org.jline.terminal.Attributes.OutputFlag;
import jdk.internal.org.jline.terminal.Size;
import jdk.internal.org.jline.terminal.Terminal;
import jdk.internal.org.jline.utils.NonBlocking;
import jdk.internal.org.jline.utils.NonBlockingPumpInputStream;
import jdk.internal.org.jline.utils.NonBlockingReader;

/**
 * Abstract terminal with support for line discipline.
 * The {@link Terminal} interface represents the slave
 * side of a PTY, but implementations derived from this class
 * will handle both the slave and master side of things.
 *
 * In order to correctly handle line discipline, the terminal
 * needs to read the input in advance in order to raise the
 * signals as fast as possible.
 * For example, when the user hits Ctrl+C, we can't wait until
 * the application consumes all the read events.
 * The same applies to echoing, when enabled, as the echoing
 * has to happen as soon as the user hit the keyboard, and not
 * only when the application running in the terminal processes
 * the input.
 */
public class LineDisciplineTerminal extends AbstractTerminal {

    private static final String DEFAULT_TERMINAL_ATTRIBUTES =
                    "speed 9600 baud; 24 rows; 80 columns;\n" +
                    "lflags: icanon isig iexten echo echoe -echok echoke -echonl echoctl\n" +
                    "\t-echoprt -altwerase -noflsh -tostop -flusho pendin -nokerninfo\n" +
                    "\t-extproc\n" +
                    "iflags: -istrip icrnl -inlcr -igncr ixon -ixoff ixany imaxbel iutf8\n" +
                    "\t-ignbrk brkint -inpck -ignpar -parmrk\n" +
                    "oflags: opost onlcr -oxtabs -onocr -onlret\n" +
                    "cflags: cread cs8 -parenb -parodd hupcl -clocal -cstopb -crtscts -dsrflow\n" +
                    "\t-dtrflow -mdmbuf\n" +
                    "cchars: discard = ^O; dsusp = ^Y; eof = ^D; eol = <undef>;\n" +
                    "\teol2 = <undef>; erase = ^?; intr = ^C; kill = ^U; lnext = ^V;\n" +
                    "\tmin = 1; quit = ^\\; reprint = ^R; start = ^Q; status = ^T;\n" +
                    "\tstop = ^S; susp = ^Z; time = 0; werase = ^W;\n";

    private static final int PIPE_SIZE = 1024;

    /*
     * Master output stream
     */
    protected final OutputStream masterOutput;

    /*
     * Slave input pipe write side
     */
    protected final OutputStream slaveInputPipe;

    /*
     * Slave streams
     */
    protected final NonBlockingPumpInputStream slaveInput;
    protected final NonBlockingReader slaveReader;
    protected final PrintWriter slaveWriter;
    protected final OutputStream slaveOutput;

    /**
     * Console data
     */
    protected final Attributes attributes;
    protected final Size size;

    public LineDisciplineTerminal(String name,
                                  String type,
                                  OutputStream masterOutput,
                                  Charset encoding) throws IOException {
        this(name, type, masterOutput, encoding, SignalHandler.SIG_DFL);
    }

    public LineDisciplineTerminal(String name,
                                  String type,
                                  OutputStream masterOutput,
                                  Charset encoding,
                                  SignalHandler signalHandler) throws IOException {
        super(name, type, encoding, signalHandler);
        NonBlockingPumpInputStream input = NonBlocking.nonBlockingPumpInputStream(PIPE_SIZE);
        this.slaveInputPipe = input.getOutputStream();
        this.slaveInput = input;
        this.slaveReader = NonBlocking.nonBlocking(getName(), slaveInput, encoding());
        this.slaveOutput = new FilteringOutputStream();
        this.slaveWriter = new PrintWriter(new OutputStreamWriter(slaveOutput, encoding()));
        this.masterOutput = masterOutput;
        this.attributes = ExecPty.doGetAttr(DEFAULT_TERMINAL_ATTRIBUTES);
        this.size = new Size(160, 50);
        parseInfoCmp();
    }

    public NonBlockingReader reader() {
        return slaveReader;
    }

    public PrintWriter writer() {
        return slaveWriter;
    }

    @Override
    public InputStream input() {
        return slaveInput;
    }

    @Override
    public OutputStream output() {
        return slaveOutput;
    }

    public Attributes getAttributes() {
        Attributes attr = new Attributes();
        attr.copy(attributes);
        return attr;
    }

    public void setAttributes(Attributes attr) {
        attributes.copy(attr);
    }

    public Size getSize() {
        Size sz = new Size();
        sz.copy(size);
        return sz;
    }

    public void setSize(Size sz) {
        size.copy(sz);
    }

   @Override
    public void raise(Signal signal) {
       Objects.requireNonNull(signal);
        // Do not call clear() atm as this can cause
        // deadlock between reading / writing threads
        // TODO: any way to fix that ?
        /*
        if (!attributes.getLocalFlag(LocalFlag.NOFLSH)) {
            try {
                slaveReader.clear();
            } catch (IOException e) {
                // Ignore
            }
        }
        */
        echoSignal(signal);
        super.raise(signal);
    }

    /**
     * Master input processing.
     * All data coming to the terminal should be provided
     * using this method.
     *
     * @param c the input byte
     * @throws IOException if anything wrong happens
     */
    public void processInputByte(int c) throws IOException {
        boolean flushOut = doProcessInputByte(c);
        slaveInputPipe.flush();
        if (flushOut) {
            masterOutput.flush();
        }
    }

    public void processInputBytes(byte[] input) throws IOException {
        processInputBytes(input, 0, input.length);
    }

    public void processInputBytes(byte[] input, int offset, int length) throws IOException {
        boolean flushOut = false;
        for (int i = 0; i < length; i++) {
            flushOut |= doProcessInputByte(input[offset + i]);
        }
        slaveInputPipe.flush();
        if (flushOut) {
            masterOutput.flush();
        }
    }

    protected boolean doProcessInputByte(int c) throws IOException {
        if (attributes.getLocalFlag(LocalFlag.ISIG)) {
            if (c == attributes.getControlChar(ControlChar.VINTR)) {
                raise(Signal.INT);
                return false;
            } else if (c == attributes.getControlChar(ControlChar.VQUIT)) {
                raise(Signal.QUIT);
                return false;
            } else if (c == attributes.getControlChar(ControlChar.VSUSP)) {
                raise(Signal.TSTP);
                return false;
            } else if (c == attributes.getControlChar(ControlChar.VSTATUS)) {
                raise(Signal.INFO);
            }
        }
        if (c == '\r') {
            if (attributes.getInputFlag(InputFlag.IGNCR)) {
                return false;
            }
            if (attributes.getInputFlag(InputFlag.ICRNL)) {
                c = '\n';
            }
        } else if (c == '\n' && attributes.getInputFlag(InputFlag.INLCR)) {
            c = '\r';
        }
        boolean flushOut = false;
        if (attributes.getLocalFlag(LocalFlag.ECHO)) {
            processOutputByte(c);
            flushOut = true;
        }
        slaveInputPipe.write(c);
        return flushOut;
    }

    /**
     * Master output processing.
     * All data going to the master should be provided by this method.
     *
     * @param c the output byte
     * @throws IOException if anything wrong happens
     */
    protected void processOutputByte(int c) throws IOException {
        if (attributes.getOutputFlag(OutputFlag.OPOST)) {
            if (c == '\n') {
                if (attributes.getOutputFlag(OutputFlag.ONLCR)) {
                    masterOutput.write('\r');
                    masterOutput.write('\n');
                    return;
                }
            }
        }
        masterOutput.write(c);
    }

    protected void processIOException(IOException ioException) {
        this.slaveInput.setIoException(ioException);
    }

    public void close() throws IOException {
        super.close();
        try {
            slaveReader.close();
        } finally {
            try {
                slaveInputPipe.close();
            } finally {
                try {
                } finally {
                    slaveWriter.close();
                }
            }
        }
    }

    private class FilteringOutputStream extends OutputStream {
        @Override
        public void write(int b) throws IOException {
            processOutputByte(b);
            flush();
        }

        @Override
        public void write(byte[] b, int off, int len) throws IOException {
            if (b == null) {
                throw new NullPointerException();
            } else if ((off < 0) || (off > b.length) || (len < 0) ||
                    ((off + len) > b.length) || ((off + len) < 0)) {
                throw new IndexOutOfBoundsException();
            } else if (len == 0) {
                return;
            }
            for (int i = 0 ; i < len ; i++) {
                processOutputByte(b[off + i]);
            }
            flush();
        }

        @Override
        public void flush() throws IOException {
            masterOutput.flush();
        }

        @Override
        public void close() throws IOException {
            masterOutput.close();
        }
    }
}