View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.apache.hadoop.hbase.hbtop.terminal.impl;
19  
20  import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.clearAll;
21  import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.cursor;
22  import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.moveCursor;
23  import static org.apache.hadoop.hbase.hbtop.terminal.impl.EscapeSequences.normal;
24  
25  import edu.umd.cs.findbugs.annotations.Nullable;
26  import java.io.BufferedReader;
27  import java.io.IOException;
28  import java.io.InputStreamReader;
29  import java.io.OutputStreamWriter;
30  import java.io.PrintWriter;
31  import java.nio.charset.StandardCharsets;
32  import java.util.Queue;
33  import java.util.StringTokenizer;
34  import java.util.concurrent.ConcurrentLinkedQueue;
35  
36  import org.apache.commons.logging.Log;
37  import org.apache.commons.logging.LogFactory;
38  import org.apache.hadoop.hbase.classification.InterfaceAudience;
39  import org.apache.hadoop.hbase.hbtop.terminal.CursorPosition;
40  import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
41  import org.apache.hadoop.hbase.hbtop.terminal.Terminal;
42  import org.apache.hadoop.hbase.hbtop.terminal.TerminalPrinter;
43  import org.apache.hadoop.hbase.hbtop.terminal.TerminalSize;
44  
45  /**
46   * An implementation of the {@link Terminal} interface for normal display mode.
47   *
48   * This implementation produces output intended for human viewing. In particular, it only displays
49   * one screenful of data. The output contains some escape sequences for formatting.
50   */
51  @InterfaceAudience.Private
52  public class TerminalImpl implements Terminal {
53  
54    private static final Log LOG = LogFactory.getLog(TerminalImpl.class);
55  
56    private TerminalSize cachedTerminalSize;
57  
58    private final PrintWriter output;
59  
60    private final ScreenBuffer screenBuffer;
61  
62    private final Queue<KeyPress> keyPressQueue;
63    private final KeyPressGenerator keyPressGenerator;
64  
65    public TerminalImpl() {
66      this(null);
67    }
68  
69    public TerminalImpl(@Nullable String title) {
70      output = new PrintWriter(new OutputStreamWriter(System.out, StandardCharsets.UTF_8));
71      sttyRaw();
72  
73      if (title != null) {
74        setTitle(title);
75      }
76  
77      screenBuffer = new ScreenBuffer();
78  
79      cachedTerminalSize = queryTerminalSize();
80      updateTerminalSize(cachedTerminalSize.getColumns(), cachedTerminalSize.getRows());
81  
82      keyPressQueue = new ConcurrentLinkedQueue<>();
83      keyPressGenerator = new KeyPressGenerator(System.in, keyPressQueue);
84      keyPressGenerator.start();
85  
86      Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
87        @Override
88        public void run() {
89          output.printf("%s%s%s%s", moveCursor(0, 0), cursor(true), normal(), clearAll());
90          output.flush();
91          sttyCooked();
92        }
93      }));
94  
95      // Clear the terminal
96      output.write(clearAll());
97      output.flush();
98    }
99  
100   private void setTitle(String title) {
101     output.write(EscapeSequences.setTitle(title));
102     output.flush();
103   }
104 
105   private void updateTerminalSize(int columns, int rows) {
106     screenBuffer.reallocate(columns, rows);
107   }
108 
109   @Override
110   public void clear() {
111     screenBuffer.clear();
112   }
113 
114   @Override
115   public void refresh() {
116     screenBuffer.flush(output);
117   }
118 
119   @Override
120   public TerminalSize getSize() {
121     return cachedTerminalSize;
122   }
123 
124   @Nullable
125   @Override
126   public TerminalSize doResizeIfNecessary() {
127     TerminalSize currentTerminalSize = queryTerminalSize();
128     if (!currentTerminalSize.equals(cachedTerminalSize)) {
129       cachedTerminalSize = currentTerminalSize;
130       updateTerminalSize(cachedTerminalSize.getColumns(), cachedTerminalSize.getRows());
131       return cachedTerminalSize;
132     }
133     return null;
134   }
135 
136   @Nullable
137   @Override
138   public KeyPress pollKeyPress() {
139     return keyPressQueue.poll();
140   }
141 
142   @Override
143   public CursorPosition getCursorPosition() {
144     return screenBuffer.getCursorPosition();
145   }
146 
147   @Override
148   public void setCursorPosition(int column, int row) {
149     screenBuffer.setCursorPosition(column, row);
150   }
151 
152   @Override
153   public void hideCursor() {
154     screenBuffer.hideCursor();
155   }
156 
157   @Override
158   public TerminalPrinter getTerminalPrinter(int startRow) {
159     return new TerminalPrinterImpl(screenBuffer, startRow);
160   }
161 
162   @Override
163   public void close() {
164     keyPressGenerator.stop();
165   }
166 
167   private TerminalSize queryTerminalSize() {
168     String sizeString = doStty("size");
169 
170     int rows = 0;
171     int columns = 0;
172 
173     StringTokenizer tokenizer = new StringTokenizer(sizeString);
174     int rc = Integer.parseInt(tokenizer.nextToken());
175     if (rc > 0) {
176       rows = rc;
177     }
178 
179     rc = Integer.parseInt(tokenizer.nextToken());
180     if (rc > 0) {
181       columns = rc;
182     }
183     return new TerminalSize(columns, rows);
184   }
185 
186   private void sttyRaw() {
187     doStty("-ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost " +
188       "-echo -echonl -icanon -isig -iexten -parenb cs8 min 1");
189   }
190 
191   private void sttyCooked() {
192     doStty("sane cooked");
193   }
194 
195   private String doStty(String sttyOptionsString) {
196     String [] cmd = {"/bin/sh", "-c", "stty " + sttyOptionsString + " < /dev/tty"};
197 
198     try {
199       Process process = Runtime.getRuntime().exec(cmd);
200 
201       String ret;
202 
203       // stdout
204       try (BufferedReader stdout = new BufferedReader(new InputStreamReader(
205         process.getInputStream(), StandardCharsets.UTF_8))) {
206         ret = stdout.readLine();
207       }
208 
209       // stderr
210       try (BufferedReader stderr = new BufferedReader(new InputStreamReader(
211         process.getErrorStream(), StandardCharsets.UTF_8))) {
212         String line = stderr.readLine();
213         if ((line != null) && (line.length() > 0)) {
214           LOG.error("Error output from stty: " + line);
215         }
216       }
217 
218       try {
219         process.waitFor();
220       } catch (InterruptedException ignored) {
221       }
222 
223       int exitValue = process.exitValue();
224       if (exitValue != 0) {
225         LOG.error("stty returned error code: " + exitValue);
226       }
227       return ret;
228     } catch (IOException e) {
229       throw new RuntimeException(e);
230     }
231   }
232 }