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}