/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #define LOG_TAG "Terminal" #include <utils/Log.h> #include <utils/Mutex.h> #include "android_runtime/AndroidRuntime.h" #include "jni.h" #include <nativehelper/JNIHelp.h> #include <nativehelper/ScopedLocalRef.h> #include <nativehelper/ScopedPrimitiveArray.h> #include <fcntl.h> #include <pty.h> #include <cstdio> #include <termios.h> #include <unistd.h> #include <vterm.h> #include <cstring> #define USE_TEST_SHELL 0 #define DEBUG_CALLBACKS 0 #define DEBUG_IO 0 #define DEBUG_SCROLLBACK 0 namespace android { /* * Callback class reference */ static jclass terminalCallbacksClass; /* * Callback methods */ static jmethodID damageMethod; static jmethodID moveRectMethod; static jmethodID moveCursorMethod; static jmethodID setTermPropBooleanMethod; static jmethodID setTermPropIntMethod; static jmethodID setTermPropStringMethod; static jmethodID setTermPropColorMethod; static jmethodID bellMethod; /* * CellRun class */ static jclass cellRunClass; static jfieldID cellRunDataField; static jfieldID cellRunDataSizeField; static jfieldID cellRunColSizeField; static jfieldID cellRunFgField; static jfieldID cellRunBgField; typedef short unsigned int dimen_t; class ScrollbackLine { public: inline ScrollbackLine(dimen_t _cols) : cols(_cols) { mCells = new VTermScreenCell[cols]; }; inline ~ScrollbackLine() { delete mCells; } inline dimen_t copyFrom(dimen_t cols, const VTermScreenCell* cells) { dimen_t n = this->cols > cols ? cols : this->cols; memcpy(mCells, cells, sizeof(VTermScreenCell) * n); return n; } inline dimen_t copyTo(dimen_t cols, VTermScreenCell* cells) { dimen_t n = cols > this->cols ? this->cols : cols; memcpy(cells, mCells, sizeof(VTermScreenCell) * n); return n; } inline void getCell(dimen_t col, VTermScreenCell* cell) { *cell = mCells[col]; } const dimen_t cols; private: VTermScreenCell* mCells; }; /* * Terminal session */ class Terminal { public: Terminal(jobject callbacks); ~Terminal(); status_t run(); size_t write(const char *bytes, size_t len); bool dispatchCharacter(int mod, int character); bool dispatchKey(int mod, int key); bool flushInput(); status_t resize(dimen_t rows, dimen_t cols, dimen_t scrollRows); status_t onPushline(dimen_t cols, const VTermScreenCell* cells); status_t onPopline(dimen_t cols, VTermScreenCell* cells); void getCellLocked(VTermPos pos, VTermScreenCell* cell); dimen_t getRows() const; dimen_t getCols() const; dimen_t getScrollRows() const; jobject getCallbacks() const; // Lock protecting mutations of internal libvterm state Mutex mLock; private: int mMasterFd; pid_t mChildPid; VTerm *mVt; VTermScreen *mVts; jobject mCallbacks; dimen_t mRows; dimen_t mCols; bool mKilled; ScrollbackLine **mScroll; dimen_t mScrollCur; dimen_t mScrollSize; }; /* * VTerm event handlers */ static int term_damage(VTermRect rect, void *user) { Terminal* term = reinterpret_cast<Terminal*>(user); #if DEBUG_CALLBACKS ALOGW("term_damage"); #endif JNIEnv* env = AndroidRuntime::getJNIEnv(); return env->CallIntMethod(term->getCallbacks(), damageMethod, rect.start_row, rect.end_row, rect.start_col, rect.end_col); } static int term_moverect(VTermRect dest, VTermRect src, void *user) { Terminal* term = reinterpret_cast<Terminal*>(user); #if DEBUG_CALLBACKS ALOGW("term_moverect"); #endif JNIEnv* env = AndroidRuntime::getJNIEnv(); return env->CallIntMethod(term->getCallbacks(), moveRectMethod, dest.start_row, dest.end_row, dest.start_col, dest.end_col, src.start_row, src.end_row, src.start_col, src.end_col); } static int term_movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user) { Terminal* term = reinterpret_cast<Terminal*>(user); #if DEBUG_CALLBACKS ALOGW("term_movecursor"); #endif JNIEnv* env = AndroidRuntime::getJNIEnv(); return env->CallIntMethod(term->getCallbacks(), moveCursorMethod, pos.row, pos.col, oldpos.row, oldpos.col, visible); } static int term_settermprop(VTermProp prop, VTermValue *val, void *user) { Terminal* term = reinterpret_cast<Terminal*>(user); #if DEBUG_CALLBACKS ALOGW("term_settermprop"); #endif JNIEnv* env = AndroidRuntime::getJNIEnv(); switch (vterm_get_prop_type(prop)) { case VTERM_VALUETYPE_BOOL: return env->CallIntMethod(term->getCallbacks(), setTermPropBooleanMethod, prop, val->boolean ? JNI_TRUE : JNI_FALSE); case VTERM_VALUETYPE_INT: return env->CallIntMethod(term->getCallbacks(), setTermPropIntMethod, prop, val->number); case VTERM_VALUETYPE_STRING: return env->CallIntMethod(term->getCallbacks(), setTermPropStringMethod, prop, env->NewStringUTF(val->string)); case VTERM_VALUETYPE_COLOR: return env->CallIntMethod(term->getCallbacks(), setTermPropIntMethod, prop, val->color.rgb.red, val->color.rgb.green, val->color.rgb.blue); default: ALOGE("unknown callback type"); return 0; } } static int term_bell(void *user) { Terminal* term = reinterpret_cast<Terminal*>(user); #if DEBUG_CALLBACKS ALOGW("term_bell"); #endif JNIEnv* env = AndroidRuntime::getJNIEnv(); return env->CallIntMethod(term->getCallbacks(), bellMethod); } static int term_sb_pushline(int cols, const VTermScreenCell *cells, void *user) { Terminal* term = reinterpret_cast<Terminal*>(user); #if DEBUG_CALLBACKS ALOGW("term_sb_pushline"); #endif return term->onPushline(cols, cells); } static int term_sb_popline(int cols, VTermScreenCell *cells, void *user) { Terminal* term = reinterpret_cast<Terminal*>(user); #if DEBUG_CALLBACKS ALOGW("term_sb_popline"); #endif return term->onPopline(cols, cells); } static VTermScreenCallbacks cb = { .damage = term_damage, .moverect = term_moverect, .movecursor = term_movecursor, .settermprop = term_settermprop, .bell = term_bell, // Resize requests are applied immediately, so callback is ignored .resize = NULL, .sb_pushline = term_sb_pushline, .sb_popline = term_sb_popline, }; Terminal::Terminal(jobject callbacks) : mCallbacks(callbacks), mRows(25), mCols(80), mKilled(false), mScrollCur(0), mScrollSize(100) { JNIEnv* env = AndroidRuntime::getJNIEnv(); mCallbacks = env->NewGlobalRef(callbacks); mScroll = new ScrollbackLine*[mScrollSize]; memset(mScroll, 0, sizeof(ScrollbackLine*) * mScrollSize); /* Create VTerm */ mVt = vterm_new(mRows, mCols); vterm_set_utf8(mVt, 1); /* Set up screen */ mVts = vterm_obtain_screen(mVt); vterm_screen_enable_altscreen(mVts, 1); vterm_screen_set_callbacks(mVts, &cb, this); vterm_screen_set_damage_merge(mVts, VTERM_DAMAGE_SCROLL); vterm_screen_reset(mVts, 1); } Terminal::~Terminal() { close(mMasterFd); ::kill(mChildPid, SIGHUP); vterm_free(mVt); delete mScroll; JNIEnv *env = AndroidRuntime::getJNIEnv(); env->DeleteGlobalRef(mCallbacks); } status_t Terminal::run() { struct termios termios; memset(&termios, 0, sizeof(termios)); termios.c_iflag = ICRNL|IXON|IUTF8; termios.c_oflag = OPOST|ONLCR|NL0|CR0|TAB0|BS0|VT0|FF0; termios.c_cflag = CS8|CREAD; termios.c_lflag = ISIG|ICANON|IEXTEN|ECHO|ECHOE|ECHOK; cfsetispeed(&termios, B38400); cfsetospeed(&termios, B38400); termios.c_cc[VINTR] = 0x1f & 'C'; termios.c_cc[VQUIT] = 0x1f & '\\'; termios.c_cc[VERASE] = 0x7f; termios.c_cc[VKILL] = 0x1f & 'U'; termios.c_cc[VEOF] = 0x1f & 'D'; termios.c_cc[VSTART] = 0x1f & 'Q'; termios.c_cc[VSTOP] = 0x1f & 'S'; termios.c_cc[VSUSP] = 0x1f & 'Z'; termios.c_cc[VREPRINT] = 0x1f & 'R'; termios.c_cc[VWERASE] = 0x1f & 'W'; termios.c_cc[VLNEXT] = 0x1f & 'V'; termios.c_cc[VMIN] = 1; termios.c_cc[VTIME] = 0; struct winsize size = { mRows, mCols, 0, 0 }; int stderr_save_fd = dup(2); if (stderr_save_fd < 0) { ALOGE("failed to dup stderr - %s", strerror(errno)); } mChildPid = forkpty(&mMasterFd, NULL, &termios, &size); if (mChildPid == 0) { /* Restore the ISIG signals back to defaults */ signal(SIGINT, SIG_DFL); signal(SIGQUIT, SIG_DFL); signal(SIGSTOP, SIG_DFL); signal(SIGCONT, SIG_DFL); FILE *stderr_save = fdopen(stderr_save_fd, "a"); if (!stderr_save) { ALOGE("failed to open stderr - %s", strerror(errno)); } // We know execvp(2) won't actually try to modify this. char *shell = const_cast<char*>("/system/bin/sh"); #if USE_TEST_SHELL char *arg1 = const_cast<char*>("-c"); char *arg2 = const_cast<char*>("x=1; c=0; while true; do echo -e \"stop \e[00;3${c}mechoing\e[00m yourself! ($x)\"; x=$(( $x + 1 )); c=$((($c+1)%7)); if [ $x -gt 110 ]; then sleep 0.5; fi; done"); char *args[4] = {shell, arg1, arg2, NULL}; #else char *args[2] = {shell, NULL}; #endif execvp(shell, args); fprintf(stderr_save, "Cannot exec(%s) - %s\n", shell, strerror(errno)); _exit(1); } ALOGD("entering read() loop"); while (1) { char buffer[4096]; ssize_t bytes = ::read(mMasterFd, buffer, sizeof buffer); #if DEBUG_IO ALOGD("read() returned %d bytes", bytes); #endif if (mKilled) { ALOGD("kill() requested"); break; } if (bytes == 0) { ALOGD("read() found EOF"); break; } if (bytes == -1) { ALOGE("read() failed: %s", strerror(errno)); return 1; } { Mutex::Autolock lock(mLock); vterm_input_write(mVt, buffer, bytes); vterm_screen_flush_damage(mVts); } } return 0; } size_t Terminal::write(const char *bytes, size_t len) { return ::write(mMasterFd, bytes, len); } bool Terminal::dispatchCharacter(int mod, int character) { Mutex::Autolock lock(mLock); vterm_keyboard_unichar(mVt, character, static_cast<VTermModifier>(mod)); return flushInput(); } bool Terminal::dispatchKey(int mod, int key) { Mutex::Autolock lock(mLock); vterm_keyboard_key(mVt, static_cast<VTermKey>(key), static_cast<VTermModifier>(mod)); return flushInput(); } bool Terminal::flushInput() { size_t len = vterm_output_get_buffer_current(mVt); if (len) { char buf[len]; len = vterm_output_read(mVt, buf, len); return len == write(buf, len); } return true; } status_t Terminal::resize(dimen_t rows, dimen_t cols, dimen_t scrollRows) { Mutex::Autolock lock(mLock); ALOGD("resize(%d, %d, %d)", rows, cols, scrollRows); mRows = rows; mCols = cols; // TODO: resize scrollback struct winsize size = { rows, cols, 0, 0 }; ioctl(mMasterFd, TIOCSWINSZ, &size); vterm_set_size(mVt, rows, cols); vterm_screen_flush_damage(mVts); return 0; } status_t Terminal::onPushline(dimen_t cols, const VTermScreenCell* cells) { ScrollbackLine* line = NULL; if (mScrollCur == mScrollSize) { /* Recycle old row if it's the right size */ if (mScroll[mScrollCur - 1]->cols == cols) { line = mScroll[mScrollCur - 1]; } else { delete mScroll[mScrollCur - 1]; } memmove(mScroll + 1, mScroll, sizeof(ScrollbackLine*) * (mScrollCur - 1)); } else if (mScrollCur > 0) { memmove(mScroll + 1, mScroll, sizeof(ScrollbackLine*) * mScrollCur); } if (line == NULL) { line = new ScrollbackLine(cols); } mScroll[0] = line; if (mScrollCur < mScrollSize) { mScrollCur++; } line->copyFrom(cols, cells); return 1; } status_t Terminal::onPopline(dimen_t cols, VTermScreenCell* cells) { if (mScrollCur == 0) { return 0; } ScrollbackLine* line = mScroll[0]; mScrollCur--; memmove(mScroll, mScroll + 1, sizeof(ScrollbackLine*) * mScrollCur); dimen_t n = line->copyTo(cols, cells); for (dimen_t col = n; col < cols; col++) { cells[col].chars[0] = 0; cells[col].width = 1; } delete line; return 1; } void Terminal::getCellLocked(VTermPos pos, VTermScreenCell* cell) { // The UI may be asking for cell data while the model is changing // underneath it, so we always fill with meaningful data. if (pos.row < 0) { size_t scrollRow = -pos.row; if (scrollRow > mScrollCur) { // Invalid region above current scrollback cell->width = 1; #if DEBUG_SCROLLBACK cell->bg.red = 255; #endif return; } ScrollbackLine* line = mScroll[scrollRow - 1]; if ((size_t) pos.col < line->cols) { // Valid scrollback cell line->getCell(pos.col, cell); cell->width = 1; #if DEBUG_SCROLLBACK cell->bg.blue = 255; #endif return; } else { // Extend last scrollback cell into invalid region line->getCell(line->cols - 1, cell); cell->width = 1; cell->chars[0] = ' '; #if DEBUG_SCROLLBACK cell->bg.green = 255; #endif return; } } if ((size_t) pos.row >= mRows) { // Invalid region below screen cell->width = 1; #if DEBUG_SCROLLBACK cell->bg.red = 128; #endif return; } // Valid screen cell vterm_screen_get_cell(mVts, pos, cell); } dimen_t Terminal::getRows() const { return mRows; } dimen_t Terminal::getCols() const { return mCols; } dimen_t Terminal::getScrollRows() const { return mScrollSize; } jobject Terminal::getCallbacks() const { return mCallbacks; } /* * JNI glue */ static jlong com_android_terminal_Terminal_nativeInit(JNIEnv* env, jclass clazz, jobject callbacks) { return reinterpret_cast<jlong>(new Terminal(callbacks)); } static jint com_android_terminal_Terminal_nativeDestroy(JNIEnv* env, jclass clazz, jlong ptr) { Terminal* term = reinterpret_cast<Terminal*>(ptr); delete term; return 0; } static jint com_android_terminal_Terminal_nativeRun(JNIEnv* env, jclass clazz, jlong ptr) { Terminal* term = reinterpret_cast<Terminal*>(ptr); return term->run(); } static jint com_android_terminal_Terminal_nativeResize(JNIEnv* env, jclass clazz, jlong ptr, jint rows, jint cols, jint scrollRows) { Terminal* term = reinterpret_cast<Terminal*>(ptr); return term->resize(rows, cols, scrollRows); } static inline int toArgb(const VTermColor& color) { return (0xff << 24 | color.rgb.red << 16 | color.rgb.green << 8 | color.rgb.blue); } static inline bool isCellStyleEqual(const VTermScreenCell& a, const VTermScreenCell& b) { if (toArgb(a.fg) != toArgb(b.fg)) return false; if (toArgb(a.bg) != toArgb(b.bg)) return false; if (a.attrs.bold != b.attrs.bold) return false; if (a.attrs.underline != b.attrs.underline) return false; if (a.attrs.italic != b.attrs.italic) return false; if (a.attrs.blink != b.attrs.blink) return false; if (a.attrs.reverse != b.attrs.reverse) return false; if (a.attrs.strike != b.attrs.strike) return false; if (a.attrs.font != b.attrs.font) return false; return true; } static jint com_android_terminal_Terminal_nativeGetCellRun(JNIEnv* env, jclass clazz, jlong ptr, jint row, jint col, jobject run) { Terminal* term = reinterpret_cast<Terminal*>(ptr); Mutex::Autolock lock(term->mLock); jcharArray dataArray = (jcharArray) env->GetObjectField(run, cellRunDataField); ScopedCharArrayRW data(env, dataArray); if (data.get() == NULL) { return -1; } VTermScreenCell firstCell, cell; VTermPos pos = { .row = row, .col = col, }; size_t dataSize = 0; size_t colSize = 0; while ((size_t) pos.col < term->getCols()) { memset(&cell, 0, sizeof(VTermScreenCell)); term->getCellLocked(pos, &cell); if (colSize == 0) { env->SetIntField(run, cellRunFgField, toArgb(cell.fg)); env->SetIntField(run, cellRunBgField, toArgb(cell.bg)); memcpy(&firstCell, &cell, sizeof(VTermScreenCell)); } else { if (!isCellStyleEqual(cell, firstCell)) { break; } } // Only include cell chars if they fit into run uint32_t rawCell = cell.chars[0]; size_t size = (rawCell < 0x10000) ? 1 : 2; if (dataSize + size <= data.size()) { if (rawCell < 0x10000) { data[dataSize++] = rawCell; } else { data[dataSize++] = (((rawCell - 0x10000) >> 10) & 0x3ff) + 0xd800; data[dataSize++] = ((rawCell - 0x10000) & 0x3ff) + 0xdc00; } for (int i = 1; i < cell.width; i++) { data[dataSize++] = ' '; } colSize += cell.width; pos.col += cell.width; } else { break; } } env->SetIntField(run, cellRunDataSizeField, dataSize); env->SetIntField(run, cellRunColSizeField, colSize); return 0; } static jint com_android_terminal_Terminal_nativeGetRows(JNIEnv* env, jclass clazz, jlong ptr) { Terminal* term = reinterpret_cast<Terminal*>(ptr); return term->getRows(); } static jint com_android_terminal_Terminal_nativeGetCols(JNIEnv* env, jclass clazz, jlong ptr) { Terminal* term = reinterpret_cast<Terminal*>(ptr); return term->getCols(); } static jint com_android_terminal_Terminal_nativeGetScrollRows(JNIEnv* env, jclass clazz, jlong ptr) { Terminal* term = reinterpret_cast<Terminal*>(ptr); return term->getScrollRows(); } static jboolean com_android_terminal_Terminal_nativeDispatchCharacter(JNIEnv *env, jclass clazz, jlong ptr, jint mod, jint c) { Terminal* term = reinterpret_cast<Terminal*>(ptr); return term->dispatchCharacter(mod, c); } static jboolean com_android_terminal_Terminal_nativeDispatchKey(JNIEnv *env, jclass clazz, jlong ptr, jint mod, jint c) { Terminal* term = reinterpret_cast<Terminal*>(ptr); return term->dispatchKey(mod, c); } static JNINativeMethod gMethods[] = { { "nativeInit", "(Lcom/android/terminal/TerminalCallbacks;)J", (void*)com_android_terminal_Terminal_nativeInit }, { "nativeDestroy", "(J)I", (void*)com_android_terminal_Terminal_nativeDestroy }, { "nativeRun", "(J)I", (void*)com_android_terminal_Terminal_nativeRun }, { "nativeResize", "(JIII)I", (void*)com_android_terminal_Terminal_nativeResize }, { "nativeGetCellRun", "(JIILcom/android/terminal/Terminal$CellRun;)I", (void*)com_android_terminal_Terminal_nativeGetCellRun }, { "nativeGetRows", "(J)I", (void*)com_android_terminal_Terminal_nativeGetRows }, { "nativeGetCols", "(J)I", (void*)com_android_terminal_Terminal_nativeGetCols }, { "nativeGetScrollRows", "(J)I", (void*)com_android_terminal_Terminal_nativeGetScrollRows }, { "nativeDispatchCharacter", "(JII)Z", (void*)com_android_terminal_Terminal_nativeDispatchCharacter}, { "nativeDispatchKey", "(JII)Z", (void*)com_android_terminal_Terminal_nativeDispatchKey }, }; int register_com_android_terminal_Terminal(JNIEnv* env) { ScopedLocalRef<jclass> localClass(env, env->FindClass("com/android/terminal/TerminalCallbacks")); android::terminalCallbacksClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass.get())); android::damageMethod = env->GetMethodID(terminalCallbacksClass, "damage", "(IIII)I"); android::moveRectMethod = env->GetMethodID(terminalCallbacksClass, "moveRect", "(IIIIIIII)I"); android::moveCursorMethod = env->GetMethodID(terminalCallbacksClass, "moveCursor", "(IIIII)I"); android::setTermPropBooleanMethod = env->GetMethodID(terminalCallbacksClass, "setTermPropBoolean", "(IZ)I"); android::setTermPropIntMethod = env->GetMethodID(terminalCallbacksClass, "setTermPropInt", "(II)I"); android::setTermPropStringMethod = env->GetMethodID(terminalCallbacksClass, "setTermPropString", "(ILjava/lang/String;)I"); android::setTermPropColorMethod = env->GetMethodID(terminalCallbacksClass, "setTermPropColor", "(IIII)I"); android::bellMethod = env->GetMethodID(terminalCallbacksClass, "bell", "()I"); ScopedLocalRef<jclass> cellRunLocal(env, env->FindClass("com/android/terminal/Terminal$CellRun")); cellRunClass = reinterpret_cast<jclass>(env->NewGlobalRef(cellRunLocal.get())); cellRunDataField = env->GetFieldID(cellRunClass, "data", "[C"); cellRunDataSizeField = env->GetFieldID(cellRunClass, "dataSize", "I"); cellRunColSizeField = env->GetFieldID(cellRunClass, "colSize", "I"); cellRunFgField = env->GetFieldID(cellRunClass, "fg", "I"); cellRunBgField = env->GetFieldID(cellRunClass, "bg", "I"); return jniRegisterNativeMethods(env, "com/android/terminal/Terminal", gMethods, NELEM(gMethods)); } } /* namespace android */