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}