com_android_terminal_Terminal.cpp revision de15e79aadde33fd8c880c19bd4fc6caca0bf795
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17#define LOG_TAG "Terminal"
18
19#include <utils/Log.h>
20#include "forkpty.h"
21#include "jni.h"
22#include "JNIHelp.h"
23#include "ScopedLocalRef.h"
24#include "ScopedPrimitiveArray.h"
25
26#include <fcntl.h>
27#include <stdio.h>
28#include <termios.h>
29#include <unistd.h>
30#include <util.h>
31#include <utmp.h>
32
33#include <vterm.h>
34
35#include <string.h>
36
37#define USE_TEST_SHELL 1
38#define DEBUG_CALLBACKS 0
39#define DEBUG_IO 0
40
41namespace android {
42
43/*
44 * JavaVM reference
45 */
46static JavaVM* gJavaVM;
47
48/*
49 * Callback class reference
50 */
51static jclass terminalCallbacksClass;
52
53/*
54 * Callback methods
55 */
56static jmethodID damageMethod;
57static jmethodID prescrollMethod;
58static jmethodID moveRectMethod;
59static jmethodID moveCursorMethod;
60static jmethodID setTermPropBooleanMethod;
61static jmethodID setTermPropIntMethod;
62static jmethodID setTermPropStringMethod;
63static jmethodID setTermPropColorMethod;
64static jmethodID bellMethod;
65static jmethodID resizeMethod;
66
67/*
68 * CellRun class
69 */
70static jclass cellRunClass;
71static jfieldID cellRunDataField;
72static jfieldID cellRunDataSizeField;
73static jfieldID cellRunColSizeField;
74static jfieldID cellRunFgField;
75static jfieldID cellRunBgField;
76
77/*
78 * Terminal session
79 */
80class Terminal {
81public:
82    Terminal(jobject callbacks, int rows, int cols);
83    ~Terminal();
84
85    int run();
86    int stop();
87
88    size_t write(const char *bytes, size_t len);
89
90    int flushDamage();
91    int resize(short unsigned int rows, short unsigned int cols);
92
93    int getCell(VTermPos pos, VTermScreenCell* cell);
94
95    int getRows() const;
96    int getCols() const;
97
98    jobject getCallbacks() const;
99
100private:
101    int mMasterFd;
102    VTerm *mVt;
103    VTermScreen *mVts;
104
105    jobject mCallbacks;
106    short unsigned int mRows;
107    short unsigned int mCols;
108    bool mStopped;
109};
110
111static JNIEnv* getEnv() {
112    JNIEnv* env;
113
114    if (gJavaVM->AttachCurrentThread(&env, NULL) < 0) {
115        return NULL;
116    }
117
118    return env;
119}
120
121/*
122 * VTerm event handlers
123 */
124
125static int term_damage(VTermRect rect, void *user) {
126    Terminal* term = reinterpret_cast<Terminal*>(user);
127#if DEBUG_CALLBACKS
128    ALOGW("term_damage");
129#endif
130
131    JNIEnv* env = getEnv();
132    if (env == NULL) {
133        ALOGE("term_damage: couldn't get JNIEnv");
134        return 0;
135    }
136
137    return env->CallIntMethod(term->getCallbacks(), damageMethod, rect.start_row, rect.end_row,
138            rect.start_col, rect.end_col);
139}
140
141static int term_prescroll(VTermRect rect, void *user) {
142    Terminal* term = reinterpret_cast<Terminal*>(user);
143#if DEBUG_CALLBACKS
144    ALOGW("term_prescroll");
145#endif
146
147    JNIEnv* env = getEnv();
148    if (env == NULL) {
149        ALOGE("term_prescroll: couldn't get JNIEnv");
150        return 0;
151    }
152
153    return env->CallIntMethod(term->getCallbacks(), prescrollMethod, rect.start_row, rect.end_row,
154            rect.start_col, rect.end_col);
155}
156
157static int term_moverect(VTermRect dest, VTermRect src, void *user) {
158    Terminal* term = reinterpret_cast<Terminal*>(user);
159#if DEBUG_CALLBACKS
160    ALOGW("term_moverect");
161#endif
162
163    JNIEnv* env = getEnv();
164    if (env == NULL) {
165        ALOGE("term_moverect: couldn't get JNIEnv");
166        return 0;
167    }
168
169    return env->CallIntMethod(term->getCallbacks(), moveRectMethod,
170            dest.start_row, dest.end_row, dest.start_col, dest.end_col,
171            src.start_row, src.end_row, src.start_col, src.end_col);
172}
173
174static int term_movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user) {
175    Terminal* term = reinterpret_cast<Terminal*>(user);
176#if DEBUG_CALLBACKS
177    ALOGW("term_movecursor");
178#endif
179
180    JNIEnv* env = getEnv();
181    if (env == NULL) {
182        ALOGE("term_movecursor: couldn't get JNIEnv");
183        return 0;
184    }
185
186    return env->CallIntMethod(term->getCallbacks(), moveCursorMethod, pos.row,
187            pos.col, oldpos.row, oldpos.col, visible);
188}
189
190static int term_settermprop(VTermProp prop, VTermValue *val, void *user) {
191    Terminal* term = reinterpret_cast<Terminal*>(user);
192#if DEBUG_CALLBACKS
193    ALOGW("term_settermprop");
194#endif
195
196    JNIEnv* env = getEnv();
197    if (env == NULL) {
198        ALOGE("term_settermprop: couldn't get JNIEnv");
199        return 0;
200    }
201
202    switch (vterm_get_prop_type(prop)) {
203    case VTERM_VALUETYPE_BOOL:
204        return env->CallIntMethod(term->getCallbacks(), setTermPropBooleanMethod,
205                static_cast<jboolean>(val->boolean));
206    case VTERM_VALUETYPE_INT:
207        return env->CallIntMethod(term->getCallbacks(), setTermPropIntMethod, prop, val->number);
208    case VTERM_VALUETYPE_STRING:
209        return env->CallIntMethod(term->getCallbacks(), setTermPropStringMethod, prop,
210                env->NewStringUTF(val->string));
211    case VTERM_VALUETYPE_COLOR:
212        return env->CallIntMethod(term->getCallbacks(), setTermPropIntMethod, prop, val->color.red,
213                val->color.green, val->color.blue);
214    default:
215        ALOGE("unknown callback type");
216        return 0;
217    }
218}
219
220static int term_setmousefunc(VTermMouseFunc func, void *data, void *user) {
221    Terminal* term = reinterpret_cast<Terminal*>(user);
222#if DEBUG_CALLBACKS
223    ALOGW("term_setmousefunc");
224#endif
225    return 1;
226}
227
228static int term_bell(void *user) {
229    Terminal* term = reinterpret_cast<Terminal*>(user);
230#if DEBUG_CALLBACKS
231    ALOGW("term_bell");
232#endif
233
234    JNIEnv* env = getEnv();
235    if (env == NULL) {
236        ALOGE("term_bell: couldn't get JNIEnv");
237        return 0;
238    }
239
240    return env->CallIntMethod(term->getCallbacks(), bellMethod);
241}
242
243static int term_resize(int rows, int cols, void *user) {
244    Terminal* term = reinterpret_cast<Terminal*>(user);
245#if DEBUG_CALLBACKS
246    ALOGW("term_resize");
247#endif
248
249    JNIEnv* env = getEnv();
250    if (env == NULL) {
251        ALOGE("term_bell: couldn't get JNIEnv");
252        return 0;
253    }
254
255    return env->CallIntMethod(term->getCallbacks(), resizeMethod, rows, cols);
256}
257
258static VTermScreenCallbacks cb = {
259    .damage = term_damage,
260    .prescroll = term_prescroll,
261    .moverect = term_moverect,
262    .movecursor = term_movecursor,
263    .settermprop = term_settermprop,
264    .setmousefunc = term_setmousefunc,
265    .bell = term_bell,
266    .resize = term_resize,
267};
268
269Terminal::Terminal(jobject callbacks, int rows, int cols) :
270        mCallbacks(callbacks), mRows(rows), mCols(cols), mStopped(false) {
271    /* Create VTerm */
272    mVt = vterm_new(rows, cols);
273    vterm_parser_set_utf8(mVt, 1);
274
275    /* Set up screen */
276    mVts = vterm_obtain_screen(mVt);
277    vterm_screen_enable_altscreen(mVts, 1);
278    vterm_screen_set_callbacks(mVts, &cb, this);
279    vterm_screen_set_damage_merge(mVts, VTERM_DAMAGE_SCROLL);
280    vterm_screen_reset(mVts, 1);
281}
282
283Terminal::~Terminal() {
284    close(mMasterFd);
285    vterm_free(mVt);
286}
287
288int Terminal::run() {
289    /* None of the docs about termios explain how to construct a new one of
290     * these, so this is largely a guess */
291    struct termios termios = {
292        .c_iflag = ICRNL|IXON|IUTF8,
293        .c_oflag = OPOST|ONLCR|NL0|CR0|TAB0|BS0|VT0|FF0,
294        .c_cflag = CS8|CREAD,
295        .c_lflag = ISIG|ICANON|IEXTEN|ECHO|ECHOE|ECHOK,
296        /* c_cc later */
297    };
298
299    cfsetispeed(&termios, B38400);
300    cfsetospeed(&termios, B38400);
301
302    termios.c_cc[VINTR]    = 0x1f & 'C';
303    termios.c_cc[VQUIT]    = 0x1f & '\\';
304    termios.c_cc[VERASE]   = 0x7f;
305    termios.c_cc[VKILL]    = 0x1f & 'U';
306    termios.c_cc[VEOF]     = 0x1f & 'D';
307    termios.c_cc[VSTART]   = 0x1f & 'Q';
308    termios.c_cc[VSTOP]    = 0x1f & 'S';
309    termios.c_cc[VSUSP]    = 0x1f & 'Z';
310    termios.c_cc[VREPRINT] = 0x1f & 'R';
311    termios.c_cc[VWERASE]  = 0x1f & 'W';
312    termios.c_cc[VLNEXT]   = 0x1f & 'V';
313    termios.c_cc[VMIN]     = 1;
314    termios.c_cc[VTIME]    = 0;
315
316    struct winsize size = { mRows, mCols, 0, 0 };
317
318    int stderr_save_fd = dup(2);
319    if (stderr_save_fd < 0) {
320        ALOGE("failed to dup stderr - %s", strerror(errno));
321    }
322
323    pid_t kid = forkpty(&mMasterFd, NULL, &termios, &size);
324    if (kid == 0) {
325        /* Restore the ISIG signals back to defaults */
326        signal(SIGINT, SIG_DFL);
327        signal(SIGQUIT, SIG_DFL);
328        signal(SIGSTOP, SIG_DFL);
329        signal(SIGCONT, SIG_DFL);
330
331        FILE *stderr_save = fdopen(stderr_save_fd, "a");
332
333        if (!stderr_save) {
334            ALOGE("failed to open stderr - %s", strerror(errno));
335        }
336
337        char *shell = "/system/bin/sh"; //getenv("SHELL");
338#if USE_TEST_SHELL
339        char *args[4] = {shell, "-c", "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)); sleep 0.5; done", NULL};
340#else
341        char *args[2] = {shell, NULL};
342#endif
343
344        execvp(shell, args);
345        fprintf(stderr_save, "Cannot exec(%s) - %s\n", shell, strerror(errno));
346        _exit(1);
347    }
348
349    ALOGD("entering read() loop");
350    while (1) {
351        char buffer[4096];
352        ssize_t bytes = ::read(mMasterFd, buffer, sizeof buffer);
353#if DEBUG_IO
354        ALOGD("read() returned %d bytes", bytes);
355#endif
356
357        if (mStopped) {
358            ALOGD("stop() requested");
359            break;
360        }
361        if (bytes == 0) {
362            ALOGD("read() found EOF");
363            break;
364        }
365        if (bytes == -1) {
366            ALOGE("read() failed: %s", strerror(errno));
367            return 1;
368        }
369
370        vterm_push_bytes(mVt, buffer, bytes);
371
372        vterm_screen_flush_damage(mVts);
373    }
374
375    return 0;
376}
377
378int Terminal::stop() {
379    // TODO: explicitly kill forked child process
380    mStopped = true;
381    return 0;
382}
383
384size_t Terminal::write(const char *bytes, size_t len) {
385    return ::write(mMasterFd, bytes, len);
386}
387
388int Terminal::flushDamage() {
389    vterm_screen_flush_damage(mVts);
390    return 0;
391}
392
393int Terminal::resize(short unsigned int rows, short unsigned int cols) {
394    ALOGD("resize(%d, %d)", rows, cols);
395
396    mRows = rows;
397    mCols = cols;
398
399    struct winsize size = { rows, cols, 0, 0 };
400    ioctl(mMasterFd, TIOCSWINSZ, &size);
401
402    vterm_set_size(mVt, rows, cols);
403    vterm_screen_flush_damage(mVts);
404
405    return 0;
406}
407
408int Terminal::getCell(VTermPos pos, VTermScreenCell* cell) {
409    return vterm_screen_get_cell(mVts, pos, cell);
410}
411
412int Terminal::getRows() const {
413    return mRows;
414}
415
416int Terminal::getCols() const {
417    return mCols;
418}
419
420jobject Terminal::getCallbacks() const {
421    return mCallbacks;
422}
423
424/*
425 * JNI glue
426 */
427
428static jint com_android_terminal_Terminal_nativeInit(JNIEnv* env, jclass clazz, jobject callbacks,
429        jint rows, jint cols) {
430    return reinterpret_cast<jint>(new Terminal(env->NewGlobalRef(callbacks), rows, cols));
431}
432
433static jint com_android_terminal_Terminal_nativeRun(JNIEnv* env, jclass clazz, jint ptr) {
434    Terminal* term = reinterpret_cast<Terminal*>(ptr);
435    return term->run();
436}
437
438static jint com_android_terminal_Terminal_nativeStop(JNIEnv* env, jclass clazz, jint ptr) {
439    Terminal* term = reinterpret_cast<Terminal*>(ptr);
440    return term->stop();
441}
442
443static jint com_android_terminal_Terminal_nativeFlushDamage(JNIEnv* env, jclass clazz, jint ptr) {
444    Terminal* term = reinterpret_cast<Terminal*>(ptr);
445    return term->flushDamage();
446}
447
448static jint com_android_terminal_Terminal_nativeResize(JNIEnv* env,
449        jclass clazz, jint ptr, jint rows, jint cols) {
450    Terminal* term = reinterpret_cast<Terminal*>(ptr);
451    return term->resize(rows, cols);
452}
453
454static int toArgb(VTermColor* color) {
455    return 0xff << 24 | color->red << 16 | color->green << 8 | color->blue;
456}
457
458static bool isCellStyleEqual(VTermScreenCell* a, VTermScreenCell* b) {
459    // TODO: check other attrs beyond just color
460    if (toArgb(&a->fg) != toArgb(&b->fg)) {
461        return false;
462    }
463    if (toArgb(&a->bg) != toArgb(&b->bg)) {
464        return false;
465    }
466    return true;
467}
468
469static jint com_android_terminal_Terminal_nativeGetCellRun(JNIEnv* env,
470        jclass clazz, jint ptr, jint row, jint col, jobject run) {
471    Terminal* term = reinterpret_cast<Terminal*>(ptr);
472
473    jcharArray dataArray = (jcharArray) env->GetObjectField(run, cellRunDataField);
474    ScopedCharArrayRW data(env, dataArray);
475    if (data.get() == NULL) {
476        return -1;
477    }
478
479    VTermScreenCell prevCell, cell;
480    memset(&prevCell, 0, sizeof(VTermScreenCell));
481    memset(&cell, 0, sizeof(VTermScreenCell));
482
483    VTermPos pos = {
484        .row = row,
485        .col = col,
486    };
487
488    unsigned int dataSize = 0;
489    unsigned int colSize = 0;
490    while (pos.col < term->getCols()) {
491        int res = term->getCell(pos, &cell);
492
493        if (colSize == 0) {
494            env->SetIntField(run, cellRunFgField, toArgb(&cell.fg));
495            env->SetIntField(run, cellRunBgField, toArgb(&cell.bg));
496        } else {
497            if (!isCellStyleEqual(&cell, &prevCell)) {
498                break;
499            }
500        }
501        memcpy(&prevCell, &cell, sizeof(VTermScreenCell));
502
503        // TODO: remove this once terminal is resized
504        if (cell.width == 0) {
505            cell.width = 1;
506        }
507
508        // TODO: support full UTF-32 characters
509        // for testing, 0x00020000 should become 0xD840 0xDC00
510        unsigned int size = 1;
511
512        // Only include cell chars if they fit into run
513        if (dataSize + size <= data.size()) {
514            data[dataSize] = cell.chars[0];
515            dataSize += size;
516            colSize += cell.width;
517            pos.col += cell.width;
518        } else {
519            break;
520        }
521    }
522
523    env->SetIntField(run, cellRunDataSizeField, dataSize);
524    env->SetIntField(run, cellRunColSizeField, colSize);
525
526    return 0;
527}
528
529static jint com_android_terminal_Terminal_nativeGetRows(JNIEnv* env, jclass clazz, jint ptr) {
530    Terminal* term = reinterpret_cast<Terminal*>(ptr);
531    return term->getRows();
532}
533
534static jint com_android_terminal_Terminal_nativeGetCols(JNIEnv* env, jclass clazz, jint ptr) {
535    Terminal* term = reinterpret_cast<Terminal*>(ptr);
536    return term->getCols();
537}
538
539static JNINativeMethod gMethods[] = {
540    { "nativeInit", "(Lcom/android/terminal/TerminalCallbacks;II)I", (void*)com_android_terminal_Terminal_nativeInit },
541    { "nativeRun", "(I)I", (void*)com_android_terminal_Terminal_nativeRun },
542    { "nativeStop", "(I)I", (void*)com_android_terminal_Terminal_nativeStop },
543    { "nativeFlushDamage", "(I)I", (void*)com_android_terminal_Terminal_nativeFlushDamage },
544    { "nativeResize", "(III)I", (void*)com_android_terminal_Terminal_nativeResize },
545    { "nativeGetCellRun", "(IIILcom/android/terminal/Terminal$CellRun;)I", (void*)com_android_terminal_Terminal_nativeGetCellRun },
546    { "nativeGetRows", "(I)I", (void*)com_android_terminal_Terminal_nativeGetRows },
547    { "nativeGetCols", "(I)I", (void*)com_android_terminal_Terminal_nativeGetCols },
548};
549
550int register_com_android_terminal_Terminal(JNIEnv* env) {
551    ScopedLocalRef<jclass> localClass(env,
552            env->FindClass("com/android/terminal/TerminalCallbacks"));
553
554    android::terminalCallbacksClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass.get()));
555
556    android::damageMethod = env->GetMethodID(terminalCallbacksClass, "damage", "(IIII)I");
557    android::prescrollMethod = env->GetMethodID(terminalCallbacksClass, "prescroll", "(IIII)I");
558    android::moveRectMethod = env->GetMethodID(terminalCallbacksClass, "moveRect", "(IIIIIIII)I");
559    android::moveCursorMethod = env->GetMethodID(terminalCallbacksClass, "moveCursor",
560            "(IIIII)I");
561    android::setTermPropBooleanMethod = env->GetMethodID(terminalCallbacksClass,
562            "setTermPropBoolean", "(IZ)I");
563    android::setTermPropIntMethod = env->GetMethodID(terminalCallbacksClass, "setTermPropInt",
564            "(II)I");
565    android::setTermPropStringMethod = env->GetMethodID(terminalCallbacksClass, "setTermPropString",
566            "(ILjava/lang/String;)I");
567    android::setTermPropColorMethod = env->GetMethodID(terminalCallbacksClass, "setTermPropColor",
568            "(IIII)I");
569    android::bellMethod = env->GetMethodID(terminalCallbacksClass, "bell", "()I");
570    android::resizeMethod = env->GetMethodID(terminalCallbacksClass, "resize", "(II)I");
571
572    ScopedLocalRef<jclass> cellRunLocal(env,
573            env->FindClass("com/android/terminal/Terminal$CellRun"));
574    cellRunClass = reinterpret_cast<jclass>(env->NewGlobalRef(cellRunLocal.get()));
575    cellRunDataField = env->GetFieldID(cellRunClass, "data", "[C");
576    cellRunDataSizeField = env->GetFieldID(cellRunClass, "dataSize", "I");
577    cellRunColSizeField = env->GetFieldID(cellRunClass, "colSize", "I");
578    cellRunFgField = env->GetFieldID(cellRunClass, "fg", "I");
579    cellRunBgField = env->GetFieldID(cellRunClass, "bg", "I");
580
581    env->GetJavaVM(&gJavaVM);
582
583    return jniRegisterNativeMethods(env, "com/android/terminal/Terminal",
584            gMethods, NELEM(gMethods));
585}
586
587} /* namespace android */
588