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 java.io.IOException;
021import java.io.InputStream;
022import java.io.InputStreamReader;
023import java.io.Reader;
024import java.nio.charset.StandardCharsets;
025import java.util.Queue;
026import java.util.concurrent.BlockingQueue;
027import java.util.concurrent.ExecutorService;
028import java.util.concurrent.Executors;
029import java.util.concurrent.LinkedBlockingQueue;
030import java.util.concurrent.TimeUnit;
031import java.util.concurrent.atomic.AtomicBoolean;
032import org.apache.hadoop.hbase.hbtop.terminal.KeyPress;
033import org.apache.hadoop.hbase.util.Threads;
034import org.apache.yetus.audience.InterfaceAudience;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038import org.apache.hbase.thirdparty.com.google.common.util.concurrent.ThreadFactoryBuilder;
039
040
041/**
042 * This generates {@link KeyPress} objects from the given input stream and offers them to the
043 * given queue.
044 */
045@InterfaceAudience.Private
046public class KeyPressGenerator {
047
048  private static final Logger LOGGER = LoggerFactory.getLogger(KeyPressGenerator.class);
049
050  private enum ParseState {
051    START, ESCAPE, ESCAPE_SEQUENCE_PARAM1, ESCAPE_SEQUENCE_PARAM2
052  }
053
054  private final Queue<KeyPress> keyPressQueue;
055  private final BlockingQueue<Character> inputCharacterQueue = new LinkedBlockingQueue<>();
056  private final Reader input;
057  private final InputStream inputStream;
058  private final AtomicBoolean stopThreads = new AtomicBoolean();
059  private final ExecutorService executorService;
060
061  private ParseState parseState;
062  private int param1;
063  private int param2;
064
065  public KeyPressGenerator(InputStream inputStream, Queue<KeyPress> keyPressQueue) {
066    this.inputStream = inputStream;
067    input = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
068    this.keyPressQueue = keyPressQueue;
069
070    executorService = Executors.newFixedThreadPool(2, new ThreadFactoryBuilder()
071      .setNameFormat("KeyPressGenerator-%d").setDaemon(true)
072      .setUncaughtExceptionHandler(Threads.LOGGING_EXCEPTION_HANDLER).build());
073
074    initState();
075  }
076
077  public void start() {
078    executorService.execute(this::readerThread);
079    executorService.execute(this::generatorThread);
080  }
081
082  private void initState() {
083    parseState = ParseState.START;
084    param1 = 0;
085    param2 = 0;
086  }
087
088  private void readerThread() {
089    boolean done = false;
090    char[] readBuffer = new char[128];
091
092    while (!done && !stopThreads.get()) {
093      try {
094        int n = inputStream.available();
095        if (n > 0) {
096          if (readBuffer.length < n) {
097            readBuffer = new char[readBuffer.length * 2];
098          }
099
100          int rc = input.read(readBuffer, 0, readBuffer.length);
101          if (rc == -1) {
102            // EOF
103            done = true;
104          } else {
105            for (int i = 0; i < rc; i++) {
106              int ch = readBuffer[i];
107              inputCharacterQueue.offer((char) ch);
108            }
109          }
110        } else {
111          Thread.sleep(20);
112        }
113      } catch (InterruptedException ignored) {
114      } catch (IOException e) {
115        LOGGER.error("Caught an exception", e);
116        done = true;
117      }
118    }
119  }
120
121  private void generatorThread() {
122    while (!stopThreads.get()) {
123      Character ch;
124      try {
125        ch = inputCharacterQueue.poll(100, TimeUnit.MILLISECONDS);
126      } catch (InterruptedException ignored) {
127        continue;
128      }
129
130      if (ch == null) {
131        if (parseState == ParseState.ESCAPE) {
132          offer(new KeyPress(KeyPress.Type.Escape, null, false, false, false));
133          initState();
134        } else if (parseState != ParseState.START) {
135          offer(new KeyPress(KeyPress.Type.Unknown, null, false, false, false));
136          initState();
137        }
138        continue;
139      }
140
141      if (parseState == ParseState.START) {
142        if (ch == 0x1B) {
143          parseState = ParseState.ESCAPE;
144          continue;
145        }
146
147        switch (ch) {
148          case '\n':
149          case '\r':
150            offer(new KeyPress(KeyPress.Type.Enter, '\n', false, false, false));
151            continue;
152
153          case 0x08:
154          case 0x7F:
155            offer(new KeyPress(KeyPress.Type.Backspace, '\b', false, false, false));
156            continue;
157
158          case '\t':
159            offer(new KeyPress(KeyPress.Type.Tab, '\t', false, false, false));
160            continue;
161
162          default:
163            // Do nothing
164            break;
165        }
166
167        if (ch < 32) {
168          ctrlAndCharacter(ch);
169          continue;
170        }
171
172        if (isPrintableChar(ch)) {
173          // Normal character
174          offer(new KeyPress(KeyPress.Type.Character, ch, false, false, false));
175          continue;
176        }
177
178        offer(new KeyPress(KeyPress.Type.Unknown, null, false, false, false));
179        continue;
180      }
181
182      if (parseState == ParseState.ESCAPE) {
183        if (ch == 0x1B) {
184          offer(new KeyPress(KeyPress.Type.Escape, null, false, false, false));
185          continue;
186        }
187
188        if (ch < 32 && ch != 0x08) {
189          ctrlAltAndCharacter(ch);
190          initState();
191          continue;
192        } else if (ch == 0x7F || ch == 0x08) {
193          offer(new KeyPress(KeyPress.Type.Backspace, '\b', false, false, false));
194          initState();
195          continue;
196        }
197
198        if (ch == '[' || ch == 'O') {
199          parseState = ParseState.ESCAPE_SEQUENCE_PARAM1;
200          continue;
201        }
202
203        if (isPrintableChar(ch)) {
204          // Alt and character
205          offer(new KeyPress(KeyPress.Type.Character, ch, true, false, false));
206          initState();
207          continue;
208        }
209
210        offer(new KeyPress(KeyPress.Type.Escape, null, false, false, false));
211        offer(new KeyPress(KeyPress.Type.Unknown, null, false, false, false));
212        initState();
213        continue;
214      }
215
216      escapeSequenceCharacter(ch);
217    }
218  }
219
220  private void ctrlAndCharacter(char ch) {
221    char ctrlCode;
222    switch (ch) {
223      case 0:
224        ctrlCode = ' ';
225        break;
226
227      case 28:
228        ctrlCode = '\\';
229        break;
230
231      case 29:
232        ctrlCode = ']';
233        break;
234
235      case 30:
236        ctrlCode = '^';
237        break;
238
239      case 31:
240        ctrlCode = '_';
241        break;
242
243      default:
244        ctrlCode = (char) ('a' - 1 + ch);
245        break;
246    }
247    offer(new KeyPress(KeyPress.Type.Character, ctrlCode, false, true, false));
248  }
249
250  private boolean isPrintableChar(char ch) {
251    if (Character.isISOControl(ch)) {
252      return false;
253    }
254    Character.UnicodeBlock block = Character.UnicodeBlock.of(ch);
255    return block != null && !block.equals(Character.UnicodeBlock.SPECIALS);
256  }
257
258  private void ctrlAltAndCharacter(char ch) {
259    char ctrlCode;
260    switch (ch) {
261      case 0:
262        ctrlCode = ' ';
263        break;
264
265      case 28:
266        ctrlCode = '\\';
267        break;
268
269      case 29:
270        ctrlCode = ']';
271        break;
272
273      case 30:
274        ctrlCode = '^';
275        break;
276
277      case 31:
278        ctrlCode = '_';
279        break;
280
281      default:
282        ctrlCode = (char) ('a' - 1 + ch);
283        break;
284    }
285    offer(new KeyPress(KeyPress.Type.Character, ctrlCode, true, true, false));
286  }
287
288  private void escapeSequenceCharacter(char ch) {
289    switch (parseState) {
290      case ESCAPE_SEQUENCE_PARAM1:
291        if (ch == ';') {
292          parseState = ParseState.ESCAPE_SEQUENCE_PARAM2;
293        } else if (Character.isDigit(ch)) {
294          param1 = param1 * 10 + Character.digit(ch, 10);
295        } else {
296          doneEscapeSequenceCharacter(ch);
297        }
298        break;
299
300      case ESCAPE_SEQUENCE_PARAM2:
301        if (Character.isDigit(ch)) {
302          param2 = param2 * 10 + Character.digit(ch, 10);
303        } else {
304          doneEscapeSequenceCharacter(ch);
305        }
306        break;
307
308      default:
309        throw new AssertionError();
310    }
311  }
312
313  private void doneEscapeSequenceCharacter(char last) {
314    boolean alt = false;
315    boolean ctrl = false;
316    boolean shift = false;
317    if (param2 != 0) {
318      alt = isAlt(param2);
319      ctrl = isCtrl(param2);
320      shift = isShift(param2);
321    }
322
323    if (last != '~') {
324      switch (last) {
325        case 'A':
326          offer(new KeyPress(KeyPress.Type.ArrowUp, null, alt, ctrl, shift));
327          break;
328
329        case 'B':
330          offer(new KeyPress(KeyPress.Type.ArrowDown, null, alt, ctrl, shift));
331          break;
332
333        case 'C':
334          offer(new KeyPress(KeyPress.Type.ArrowRight, null, alt, ctrl, shift));
335          break;
336
337        case 'D':
338          offer(new KeyPress(KeyPress.Type.ArrowLeft, null, alt, ctrl, shift));
339          break;
340
341        case 'H':
342          offer(new KeyPress(KeyPress.Type.Home, null, alt, ctrl, shift));
343          break;
344
345        case 'F':
346          offer(new KeyPress(KeyPress.Type.End, null, alt, ctrl, shift));
347          break;
348
349        case 'P':
350          offer(new KeyPress(KeyPress.Type.F1, null, alt, ctrl, shift));
351          break;
352
353        case 'Q':
354          offer(new KeyPress(KeyPress.Type.F2, null, alt, ctrl, shift));
355          break;
356
357        case 'R':
358          offer(new KeyPress(KeyPress.Type.F3, null, alt, ctrl, shift));
359          break;
360
361        case 'S':
362          offer(new KeyPress(KeyPress.Type.F4, null, alt, ctrl, shift));
363          break;
364
365        case 'Z':
366          offer(new KeyPress(KeyPress.Type.ReverseTab, null, alt, ctrl, shift));
367          break;
368
369        default:
370          offer(new KeyPress(KeyPress.Type.Unknown, null, alt, ctrl, shift));
371          break;
372      }
373      initState();
374      return;
375    }
376
377    switch (param1) {
378      case 1:
379        offer(new KeyPress(KeyPress.Type.Home, null, alt, ctrl, shift));
380        break;
381
382      case 2:
383        offer(new KeyPress(KeyPress.Type.Insert, null, alt, ctrl, shift));
384        break;
385
386      case 3:
387        offer(new KeyPress(KeyPress.Type.Delete, null, alt, ctrl, shift));
388        break;
389
390      case 4:
391        offer(new KeyPress(KeyPress.Type.End, null, alt, ctrl, shift));
392        break;
393
394      case 5:
395        offer(new KeyPress(KeyPress.Type.PageUp, null, alt, ctrl, shift));
396        break;
397
398      case 6:
399        offer(new KeyPress(KeyPress.Type.PageDown, null, alt, ctrl, shift));
400        break;
401
402      case 11:
403        offer(new KeyPress(KeyPress.Type.F1, null, alt, ctrl, shift));
404        break;
405
406      case 12:
407        offer(new KeyPress(KeyPress.Type.F2, null, alt, ctrl, shift));
408        break;
409
410      case 13:
411        offer(new KeyPress(KeyPress.Type.F3, null, alt, ctrl, shift));
412        break;
413
414      case 14:
415        offer(new KeyPress(KeyPress.Type.F4, null, alt, ctrl, shift));
416        break;
417
418      case 15:
419        offer(new KeyPress(KeyPress.Type.F5, null, alt, ctrl, shift));
420        break;
421
422      case 17:
423        offer(new KeyPress(KeyPress.Type.F6, null, alt, ctrl, shift));
424        break;
425
426      case 18:
427        offer(new KeyPress(KeyPress.Type.F7, null, alt, ctrl, shift));
428        break;
429
430      case 19:
431        offer(new KeyPress(KeyPress.Type.F8, null, alt, ctrl, shift));
432        break;
433
434      case 20:
435        offer(new KeyPress(KeyPress.Type.F9, null, alt, ctrl, shift));
436        break;
437
438      case 21:
439        offer(new KeyPress(KeyPress.Type.F10, null, alt, ctrl, shift));
440        break;
441
442      case 23:
443        offer(new KeyPress(KeyPress.Type.F11, null, alt, ctrl, shift));
444        break;
445
446      case 24:
447        offer(new KeyPress(KeyPress.Type.F12, null, alt, ctrl, shift));
448        break;
449
450      default:
451        offer(new KeyPress(KeyPress.Type.Unknown, null, false, false, false));
452        break;
453    }
454
455    initState();
456  }
457
458  private boolean isShift(int param) {
459    return (param & 1) != 0;
460  }
461
462  private boolean isAlt(int param) {
463    return (param & 2) != 0;
464  }
465
466  private boolean isCtrl(int param) {
467    return (param & 4) != 0;
468  }
469
470  private void offer(KeyPress keyPress) {
471    // Handle ctrl + c
472    if (keyPress.isCtrl() && keyPress.getType() == KeyPress.Type.Character &&
473      keyPress.getCharacter() == 'c') {
474      System.exit(0);
475    }
476
477    keyPressQueue.offer(keyPress);
478  }
479
480  public void stop() {
481    stopThreads.set(true);
482
483    executorService.shutdown();
484    try {
485      while (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
486        LOGGER.warn("Waiting for thread-pool to terminate");
487      }
488    } catch (InterruptedException e) {
489      LOGGER.warn("Interrupted while waiting for thread-pool termination", e);
490    }
491  }
492}