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 * An implementation of the {@link Terminal} interface for normal display mode. This implementation
047 * produces output intended for human viewing. In particular, it only displays one screenful of
048 * data. The output contains some escape sequences for formatting.
049 */
050@InterfaceAudience.Private
051public class TerminalImpl implements Terminal {
052
053  private static final Logger LOGGER = LoggerFactory.getLogger(TerminalImpl.class);
054
055  private TerminalSize cachedTerminalSize;
056
057  private final PrintWriter output;
058
059  private final ScreenBuffer screenBuffer;
060
061  private final Queue<KeyPress> keyPressQueue;
062  private final KeyPressGenerator keyPressGenerator;
063
064  public TerminalImpl() {
065    this(null);
066  }
067
068  public TerminalImpl(@Nullable String title) {
069    output = new PrintWriter(new OutputStreamWriter(System.out, StandardCharsets.UTF_8));
070    sttyRaw();
071
072    if (title != null) {
073      setTitle(title);
074    }
075
076    screenBuffer = new ScreenBuffer();
077
078    cachedTerminalSize = queryTerminalSize();
079    updateTerminalSize(cachedTerminalSize.getColumns(), cachedTerminalSize.getRows());
080
081    keyPressQueue = new ConcurrentLinkedQueue<>();
082    keyPressGenerator = new KeyPressGenerator(System.in, keyPressQueue);
083    keyPressGenerator.start();
084
085    Runtime.getRuntime().addShutdownHook(new Thread(() -> {
086      output.printf("%s%s%s%s", moveCursor(0, 0), cursor(true), normal(), clearAll());
087      output.flush();
088      sttyCooked();
089    }));
090
091    // Clear the terminal
092    output.write(clearAll());
093    output.flush();
094  }
095
096  private void setTitle(String title) {
097    output.write(EscapeSequences.setTitle(title));
098    output.flush();
099  }
100
101  private void updateTerminalSize(int columns, int rows) {
102    screenBuffer.reallocate(columns, rows);
103  }
104
105  @Override
106  public void clear() {
107    screenBuffer.clear();
108  }
109
110  @Override
111  public void refresh() {
112    screenBuffer.flush(output);
113  }
114
115  @Override
116  public TerminalSize getSize() {
117    return cachedTerminalSize;
118  }
119
120  @Nullable
121  @Override
122  public TerminalSize doResizeIfNecessary() {
123    TerminalSize currentTerminalSize = queryTerminalSize();
124    if (!currentTerminalSize.equals(cachedTerminalSize)) {
125      cachedTerminalSize = currentTerminalSize;
126      updateTerminalSize(cachedTerminalSize.getColumns(), cachedTerminalSize.getRows());
127      return cachedTerminalSize;
128    }
129    return null;
130  }
131
132  @Nullable
133  @Override
134  public KeyPress pollKeyPress() {
135    return keyPressQueue.poll();
136  }
137
138  @Override
139  public CursorPosition getCursorPosition() {
140    return screenBuffer.getCursorPosition();
141  }
142
143  @Override
144  public void setCursorPosition(int column, int row) {
145    screenBuffer.setCursorPosition(column, row);
146  }
147
148  @Override
149  public void hideCursor() {
150    screenBuffer.hideCursor();
151  }
152
153  @Override
154  public TerminalPrinter getTerminalPrinter(int startRow) {
155    return new TerminalPrinterImpl(screenBuffer, startRow);
156  }
157
158  @Override
159  public void close() {
160    keyPressGenerator.stop();
161  }
162
163  private TerminalSize queryTerminalSize() {
164    String sizeString = doStty("size");
165
166    int rows = 0;
167    int columns = 0;
168
169    StringTokenizer tokenizer = new StringTokenizer(sizeString);
170    int rc = Integer.parseInt(tokenizer.nextToken());
171    if (rc > 0) {
172      rows = rc;
173    }
174
175    rc = Integer.parseInt(tokenizer.nextToken());
176    if (rc > 0) {
177      columns = rc;
178    }
179    return new TerminalSize(columns, rows);
180  }
181
182  private void sttyRaw() {
183    doStty("-ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost "
184      + "-echo -echonl -icanon -isig -iexten -parenb cs8 min 1");
185  }
186
187  private void sttyCooked() {
188    doStty("sane cooked");
189  }
190
191  private String doStty(String sttyOptionsString) {
192    String[] cmd = { "/bin/sh", "-c", "stty " + sttyOptionsString + " < /dev/tty" };
193
194    try {
195      Process process = Runtime.getRuntime().exec(cmd);
196
197      String ret;
198
199      // stdout
200      try (BufferedReader stdout = new BufferedReader(
201        new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
202        ret = stdout.readLine();
203      }
204
205      // stderr
206      try (BufferedReader stderr = new BufferedReader(
207        new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) {
208        String line = stderr.readLine();
209        if ((line != null) && (line.length() > 0)) {
210          LOGGER.error("Error output from stty: " + line);
211        }
212      }
213
214      try {
215        process.waitFor();
216      } catch (InterruptedException e) {
217        // Restore interrupt status
218        Thread.currentThread().interrupt();
219      }
220
221      int exitValue = process.exitValue();
222      if (exitValue != 0) {
223        LOGGER.error("stty returned error code: " + exitValue);
224      }
225      return ret;
226    } catch (IOException e) {
227      throw new UncheckedIOException(e);
228    }
229  }
230}