001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.hbtop.terminal.impl;
019
020import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.clearAll;
021import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.cursor;
022import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.moveCursor;
023import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.normal;
024
025import edu.umd.cs.findbugs.annotations.Nullable;
026import java.io.BufferedReader;
027import java.io.IOException;
028import java.io.InputStreamReader;
029import java.io.OutputStreamWriter;
030import java.io.PrintWriter;
031import java.io.UncheckedIOException;
032import java.nio.charset.StandardCharsets;
033import java.util.Queue;
034import java.util.StringTokenizer;
035import java.util.concurrent.ConcurrentLinkedQueue;
036import org.apache.hadoop.hbase.hbtop.terminal.CursorPosition;
037import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
038import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
039import org.apache.hadoop.hbase.hbtop.terminal.TerminalPrinter;
040import org.apache.hadoop.hbase.hbtop.terminal.TerminalSize;
041import org.apache.yetus.audience.InterfaceAudience;
042import org.slf4j.Logger;
043import org.slf4j.LoggerFactory;
044
045
046/**
047 * The implementation of the {@link Terminal} interface.
048 */
049@InterfaceAudience.Private
050public class TerminalImpl implements Terminal {
051
052  private static final Logger LOGGER = LoggerFactory.getLogger(TerminalImpl.class);
053
054  private TerminalSize cachedTerminalSize;
055
056  private final PrintWriter output;
057
058  private final ScreenBuffer screenBuffer;
059
060  private final Queue<KeyPress> keyPressQueue;
061  private final KeyPressGenerator keyPressGenerator;
062
063  public TerminalImpl() {
064    this(null);
065  }
066
067  public TerminalImpl(@Nullable String title) {
068    output = new PrintWriter(new OutputStreamWriter(System.out, StandardCharsets.UTF_8));
069    sttyRaw();
070
071    if (title != null) {
072      setTitle(title);
073    }
074
075    screenBuffer = new ScreenBuffer();
076
077    cachedTerminalSize = queryTerminalSize();
078    updateTerminalSize(cachedTerminalSize.getColumns(), cachedTerminalSize.getRows());
079
080    keyPressQueue = new ConcurrentLinkedQueue<>();
081    keyPressGenerator = new KeyPressGenerator(System.in, keyPressQueue);
082    keyPressGenerator.start();
083
084    Runtime.getRuntime().addShutdownHook(new Thread(() -> {
085      output.printf("%s%s%s%s", moveCursor(0, 0), cursor(true), normal(), clearAll());
086      output.flush();
087      sttyCooked();
088    }));
089
090    // Clear the terminal
091    output.write(clearAll());
092    output.flush();
093  }
094
095  private void setTitle(String title) {
096    output.write(EscapeSequences.setTitle(title));
097    output.flush();
098  }
099
100  private void updateTerminalSize(int columns, int rows) {
101    screenBuffer.reallocate(columns, rows);
102  }
103
104  @Override
105  public void clear() {
106    screenBuffer.clear();
107  }
108
109  @Override
110  public void refresh() {
111    screenBuffer.flush(output);
112  }
113
114  @Override
115  public TerminalSize getSize() {
116    return cachedTerminalSize;
117  }
118
119  @Nullable
120  @Override
121  public TerminalSize doResizeIfNecessary() {
122    TerminalSize currentTerminalSize = queryTerminalSize();
123    if (!currentTerminalSize.equals(cachedTerminalSize)) {
124      cachedTerminalSize = currentTerminalSize;
125      updateTerminalSize(cachedTerminalSize.getColumns(), cachedTerminalSize.getRows());
126      return cachedTerminalSize;
127    }
128    return null;
129  }
130
131  @Nullable
132  @Override
133  public KeyPress pollKeyPress() {
134    return keyPressQueue.poll();
135  }
136
137  @Override
138  public CursorPosition getCursorPosition() {
139    return screenBuffer.getCursorPosition();
140  }
141
142  @Override
143  public void setCursorPosition(int column, int row) {
144    screenBuffer.setCursorPosition(column, row);
145  }
146
147  @Override
148  public void hideCursor() {
149    screenBuffer.hideCursor();
150  }
151
152  @Override
153  public TerminalPrinter getTerminalPrinter(int startRow) {
154    return new TerminalPrinterImpl(screenBuffer, startRow);
155  }
156
157  @Override
158  public void close() {
159    keyPressGenerator.stop();
160  }
161
162  private TerminalSize queryTerminalSize() {
163    String sizeString = doStty("size");
164
165    int rows = 0;
166    int columns = 0;
167
168    StringTokenizer tokenizer = new StringTokenizer(sizeString);
169    int rc = Integer.parseInt(tokenizer.nextToken());
170    if (rc > 0) {
171      rows = rc;
172    }
173
174    rc = Integer.parseInt(tokenizer.nextToken());
175    if (rc > 0) {
176      columns = rc;
177    }
178    return new TerminalSize(columns, rows);
179  }
180
181  private void sttyRaw() {
182    doStty("-ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost " +
183      "-echo -echonl -icanon -isig -iexten -parenb cs8 min 1");
184  }
185
186  private void sttyCooked() {
187    doStty("sane cooked");
188  }
189
190  private String doStty(String sttyOptionsString) {
191    String [] cmd = {"/bin/sh", "-c", "stty " + sttyOptionsString + " < /dev/tty"};
192
193    try {
194      Process process = Runtime.getRuntime().exec(cmd);
195
196      String ret;
197
198      // stdout
199      try (BufferedReader stdout = new BufferedReader(new InputStreamReader(
200        process.getInputStream(), StandardCharsets.UTF_8))) {
201        ret = stdout.readLine();
202      }
203
204      // stderr
205      try (BufferedReader stderr = new BufferedReader(new InputStreamReader(
206        process.getErrorStream(), StandardCharsets.UTF_8))) {
207        String line = stderr.readLine();
208        if ((line != null) && (line.length() > 0)) {
209          LOGGER.error("Error output from stty: " + line);
210        }
211      }
212
213      try {
214        process.waitFor();
215      } catch (InterruptedException ignored) {
216      }
217
218      int exitValue = process.exitValue();
219      if (exitValue != 0) {
220        LOGGER.error("stty returned error code: " + exitValue);
221      }
222      return ret;
223    } catch (IOException e) {
224      throw new UncheckedIOException(e);
225    }
226  }
227}