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}