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