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}