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 com.google.common.util.concurrent.ThreadFactoryBuilder;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.InputStreamReader;
24  import java.io.Reader;
25  import java.nio.charset.StandardCharsets;
26  import java.util.Queue;
27  import java.util.concurrent.BlockingQueue;
28  import java.util.concurrent.ExecutorService;
29  import java.util.concurrent.Executors;
30  import java.util.concurrent.LinkedBlockingQueue;
31  import java.util.concurrent.TimeUnit;
32  import java.util.concurrent.atomic.AtomicBoolean;
33  import org.apache.commons.logging.Log;
34  import org.apache.commons.logging.LogFactory;
35  import org.apache.hadoop.hbase.classification.InterfaceAudience;
36  import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
37  import org.apache.hadoop.hbase.util.Threads;
38  
39  /**
40   * This generates {@link KeyPress} objects from the given input stream and offers them to the
41   * given queue.
42   */
43  @InterfaceAudience.Private
44  public class KeyPressGenerator {
45  
46    private static final Log LOG = LogFactory.getLog(KeyPressGenerator.class);
47  
48    private enum ParseState {
49      START, ESCAPE, ESCAPE_SEQUENCE_PARAM1, ESCAPE_SEQUENCE_PARAM2
50    }
51  
52    private final Queue<KeyPress> keyPressQueue;
53    private final BlockingQueue<Character> inputCharacterQueue = new LinkedBlockingQueue<>();
54    private final Reader input;
55    private final InputStream inputStream;
56    private final AtomicBoolean stopThreads = new AtomicBoolean();
57    private final ExecutorService executorService;
58  
59    private ParseState parseState;
60    private int param1;
61    private int param2;
62  
63    public KeyPressGenerator(InputStream inputStream, Queue<KeyPress> keyPressQueue) {
64      this.inputStream = inputStream;
65      input = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
66      this.keyPressQueue = keyPressQueue;
67  
68      executorService = Executors.newFixedThreadPool(2, new ThreadFactoryBuilder()
69        .setNameFormat("KeyPressGenerator-%d").setDaemon(true)
70        .setUncaughtExceptionHandler(Threads.LOGGING_EXCEPTION_HANDLER).build());
71  
72      initState();
73    }
74  
75    public void start() {
76      executorService.execute(new Runnable() {
77        @Override
78        public void run() {
79          readerThread();
80        }
81      });
82      executorService.execute(new Runnable() {
83        @Override
84        public void run() {
85          generatorThread();
86        }
87      });
88    }
89  
90    private void initState() {
91      parseState = ParseState.START;
92      param1 = 0;
93      param2 = 0;
94    }
95  
96    private void readerThread() {
97      boolean done = false;
98      char[] readBuffer = new char[128];
99  
100     while (!done && !stopThreads.get()) {
101       try {
102         int n = inputStream.available();
103         if (n > 0) {
104           if (readBuffer.length < n) {
105             readBuffer = new char[readBuffer.length * 2];
106           }
107 
108           int rc = input.read(readBuffer, 0, readBuffer.length);
109           if (rc == -1) {
110             // EOF
111             done = true;
112           } else {
113             for (int i = 0; i < rc; i++) {
114               int ch = readBuffer[i];
115               inputCharacterQueue.offer((char) ch);
116             }
117           }
118         } else {
119           Thread.sleep(20);
120         }
121       } catch (InterruptedException ignored) {
122       } catch (IOException e) {
123         LOG.error("Caught an exception", e);
124         done = true;
125       }
126     }
127   }
128 
129   private void generatorThread() {
130     while (!stopThreads.get()) {
131       Character ch;
132       try {
133         ch = inputCharacterQueue.poll(100, TimeUnit.MILLISECONDS);
134       } catch (InterruptedException ignored) {
135         continue;
136       }
137 
138       if (ch == null) {
139         if (parseState == ParseState.ESCAPE) {
140           offer(new KeyPress(KeyPress.Type.Escape, null, false, false, false));
141           initState();
142         } else if (parseState != ParseState.START) {
143           offer(new KeyPress(KeyPress.Type.Unknown, null, false, false, false));
144           initState();
145         }
146         continue;
147       }
148 
149       if (parseState == ParseState.START) {
150         if (ch == 0x1B) {
151           parseState = ParseState.ESCAPE;
152           continue;
153         }
154 
155         switch (ch) {
156           case '\n':
157           case '\r':
158             offer(new KeyPress(KeyPress.Type.Enter, '\n', false, false, false));
159             continue;
160 
161           case 0x08:
162           case 0x7F:
163             offer(new KeyPress(KeyPress.Type.Backspace, '\b', false, false, false));
164             continue;
165 
166           case '\t':
167             offer(new KeyPress(KeyPress.Type.Tab, '\t', false, false, false));
168             continue;
169 
170           default:
171             // Do nothing
172             break;
173         }
174 
175         if (ch < 32) {
176           ctrlAndCharacter(ch);
177           continue;
178         }
179 
180         if (isPrintableChar(ch)) {
181           // Normal character
182           offer(new KeyPress(KeyPress.Type.Character, ch, false, false, false));
183           continue;
184         }
185 
186         offer(new KeyPress(KeyPress.Type.Unknown, null, false, false, false));
187         continue;
188       }
189 
190       if (parseState == ParseState.ESCAPE) {
191         if (ch == 0x1B) {
192           offer(new KeyPress(KeyPress.Type.Escape, null, false, false, false));
193           continue;
194         }
195 
196         if (ch < 32 && ch != 0x08) {
197           ctrlAltAndCharacter(ch);
198           initState();
199           continue;
200         } else if (ch == 0x7F || ch == 0x08) {
201           offer(new KeyPress(KeyPress.Type.Backspace, '\b', false, false, false));
202           initState();
203           continue;
204         }
205 
206         if (ch == '[' || ch == 'O') {
207           parseState = ParseState.ESCAPE_SEQUENCE_PARAM1;
208           continue;
209         }
210 
211         if (isPrintableChar(ch)) {
212           // Alt and character
213           offer(new KeyPress(KeyPress.Type.Character, ch, true, false, false));
214           initState();
215           continue;
216         }
217 
218         offer(new KeyPress(KeyPress.Type.Escape, null, false, false, false));
219         offer(new KeyPress(KeyPress.Type.Unknown, null, false, false, false));
220         initState();
221         continue;
222       }
223 
224       escapeSequenceCharacter(ch);
225     }
226   }
227 
228   private void ctrlAndCharacter(char ch) {
229     char ctrlCode;
230     switch (ch) {
231       case 0:
232         ctrlCode = ' ';
233         break;
234 
235       case 28:
236         ctrlCode = '\\';
237         break;
238 
239       case 29:
240         ctrlCode = ']';
241         break;
242 
243       case 30:
244         ctrlCode = '^';
245         break;
246 
247       case 31:
248         ctrlCode = '_';
249         break;
250 
251       default:
252         ctrlCode = (char) ('a' - 1 + ch);
253         break;
254     }
255     offer(new KeyPress(KeyPress.Type.Character, ctrlCode, false, true, false));
256   }
257 
258   private boolean isPrintableChar(char ch) {
259     if (Character.isISOControl(ch)) {
260       return false;
261     }
262     Character.UnicodeBlock block = Character.UnicodeBlock.of(ch);
263     return block != null && !block.equals(Character.UnicodeBlock.SPECIALS);
264   }
265 
266   private void ctrlAltAndCharacter(char ch) {
267     char ctrlCode;
268     switch (ch) {
269       case 0:
270         ctrlCode = ' ';
271         break;
272 
273       case 28:
274         ctrlCode = '\\';
275         break;
276 
277       case 29:
278         ctrlCode = ']';
279         break;
280 
281       case 30:
282         ctrlCode = '^';
283         break;
284 
285       case 31:
286         ctrlCode = '_';
287         break;
288 
289       default:
290         ctrlCode = (char) ('a' - 1 + ch);
291         break;
292     }
293     offer(new KeyPress(KeyPress.Type.Character, ctrlCode, true, true, false));
294   }
295 
296   private void escapeSequenceCharacter(char ch) {
297     switch (parseState) {
298       case ESCAPE_SEQUENCE_PARAM1:
299         if (ch == ';') {
300           parseState = ParseState.ESCAPE_SEQUENCE_PARAM2;
301         } else if (Character.isDigit(ch)) {
302           param1 = param1 * 10 + Character.digit(ch, 10);
303         } else {
304           doneEscapeSequenceCharacter(ch);
305         }
306         break;
307 
308       case ESCAPE_SEQUENCE_PARAM2:
309         if (Character.isDigit(ch)) {
310           param2 = param2 * 10 + Character.digit(ch, 10);
311         } else {
312           doneEscapeSequenceCharacter(ch);
313         }
314         break;
315 
316       default:
317         throw new AssertionError();
318     }
319   }
320 
321   private void doneEscapeSequenceCharacter(char last) {
322     boolean alt = false;
323     boolean ctrl = false;
324     boolean shift = false;
325     if (param2 != 0) {
326       alt = isAlt(param2);
327       ctrl = isCtrl(param2);
328       shift = isShift(param2);
329     }
330 
331     if (last != '~') {
332       switch (last) {
333         case 'A':
334           offer(new KeyPress(KeyPress.Type.ArrowUp, null, alt, ctrl, shift));
335           break;
336 
337         case 'B':
338           offer(new KeyPress(KeyPress.Type.ArrowDown, null, alt, ctrl, shift));
339           break;
340 
341         case 'C':
342           offer(new KeyPress(KeyPress.Type.ArrowRight, null, alt, ctrl, shift));
343           break;
344 
345         case 'D':
346           offer(new KeyPress(KeyPress.Type.ArrowLeft, null, alt, ctrl, shift));
347           break;
348 
349         case 'H':
350           offer(new KeyPress(KeyPress.Type.Home, null, alt, ctrl, shift));
351           break;
352 
353         case 'F':
354           offer(new KeyPress(KeyPress.Type.End, null, alt, ctrl, shift));
355           break;
356 
357         case 'P':
358           offer(new KeyPress(KeyPress.Type.F1, null, alt, ctrl, shift));
359           break;
360 
361         case 'Q':
362           offer(new KeyPress(KeyPress.Type.F2, null, alt, ctrl, shift));
363           break;
364 
365         case 'R':
366           offer(new KeyPress(KeyPress.Type.F3, null, alt, ctrl, shift));
367           break;
368 
369         case 'S':
370           offer(new KeyPress(KeyPress.Type.F4, null, alt, ctrl, shift));
371           break;
372 
373         case 'Z':
374           offer(new KeyPress(KeyPress.Type.ReverseTab, null, alt, ctrl, shift));
375           break;
376 
377         default:
378           offer(new KeyPress(KeyPress.Type.Unknown, null, alt, ctrl, shift));
379           break;
380       }
381       initState();
382       return;
383     }
384 
385     switch (param1) {
386       case 1:
387         offer(new KeyPress(KeyPress.Type.Home, null, alt, ctrl, shift));
388         break;
389 
390       case 2:
391         offer(new KeyPress(KeyPress.Type.Insert, null, alt, ctrl, shift));
392         break;
393 
394       case 3:
395         offer(new KeyPress(KeyPress.Type.Delete, null, alt, ctrl, shift));
396         break;
397 
398       case 4:
399         offer(new KeyPress(KeyPress.Type.End, null, alt, ctrl, shift));
400         break;
401 
402       case 5:
403         offer(new KeyPress(KeyPress.Type.PageUp, null, alt, ctrl, shift));
404         break;
405 
406       case 6:
407         offer(new KeyPress(KeyPress.Type.PageDown, null, alt, ctrl, shift));
408         break;
409 
410       case 11:
411         offer(new KeyPress(KeyPress.Type.F1, null, alt, ctrl, shift));
412         break;
413 
414       case 12:
415         offer(new KeyPress(KeyPress.Type.F2, null, alt, ctrl, shift));
416         break;
417 
418       case 13:
419         offer(new KeyPress(KeyPress.Type.F3, null, alt, ctrl, shift));
420         break;
421 
422       case 14:
423         offer(new KeyPress(KeyPress.Type.F4, null, alt, ctrl, shift));
424         break;
425 
426       case 15:
427         offer(new KeyPress(KeyPress.Type.F5, null, alt, ctrl, shift));
428         break;
429 
430       case 17:
431         offer(new KeyPress(KeyPress.Type.F6, null, alt, ctrl, shift));
432         break;
433 
434       case 18:
435         offer(new KeyPress(KeyPress.Type.F7, null, alt, ctrl, shift));
436         break;
437 
438       case 19:
439         offer(new KeyPress(KeyPress.Type.F8, null, alt, ctrl, shift));
440         break;
441 
442       case 20:
443         offer(new KeyPress(KeyPress.Type.F9, null, alt, ctrl, shift));
444         break;
445 
446       case 21:
447         offer(new KeyPress(KeyPress.Type.F10, null, alt, ctrl, shift));
448         break;
449 
450       case 23:
451         offer(new KeyPress(KeyPress.Type.F11, null, alt, ctrl, shift));
452         break;
453 
454       case 24:
455         offer(new KeyPress(KeyPress.Type.F12, null, alt, ctrl, shift));
456         break;
457 
458       default:
459         offer(new KeyPress(KeyPress.Type.Unknown, null, false, false, false));
460         break;
461     }
462 
463     initState();
464   }
465 
466   private boolean isShift(int param) {
467     return (param & 1) != 0;
468   }
469 
470   private boolean isAlt(int param) {
471     return (param & 2) != 0;
472   }
473 
474   private boolean isCtrl(int param) {
475     return (param & 4) != 0;
476   }
477 
478   private void offer(KeyPress keyPress) {
479     // Handle ctrl + c
480     if (keyPress.isCtrl() && keyPress.getType() == KeyPress.Type.Character &&
481       keyPress.getCharacter() == 'c') {
482       System.exit(0);
483     }
484 
485     keyPressQueue.offer(keyPress);
486   }
487 
488   public void stop() {
489     stopThreads.set(true);
490 
491     executorService.shutdown();
492     try {
493       while (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
494         LOG.warn("Waiting for thread-pool to terminate");
495       }
496     } catch (InterruptedException e) {
497       LOG.warn("Interrupted while waiting for thread-pool termination", e);
498     }
499   }
500 }