com_android_terminal_Terminal.cpp revision a76e33884c55bbd5db7e512b7687210cc3f635cf
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 true
38
39namespace android {
40
41/*
42 * JavaVM reference
43 */
44static JavaVM* gJavaVM;
45
46/*
47 * Callback class reference
48 */
49static jclass terminalCallbacksClass;
50
51/*
52 * Callback methods
53 */
54static jmethodID damageMethod;
55static jmethodID prescrollMethod;
56static jmethodID moveRectMethod;
57static jmethodID moveCursorMethod;
58static jmethodID setTermPropBooleanMethod;
59static jmethodID setTermPropIntMethod;
60static jmethodID setTermPropStringMethod;
61static jmethodID setTermPropColorMethod;
62static jmethodID bellMethod;
63static jmethodID resizeMethod;
64
65/*
66 * CellRun class
67 */
68static jclass cellRunClass;
69static jfieldID cellRunDataField;
70static jfieldID cellRunDataSizeField;
71static jfieldID cellRunColSizeField;
72/*
73 * Terminal session
74 */
75class Terminal {
76public:
77    Terminal(jobject callbacks, int rows, int cols);
78    ~Terminal();
79
80    int run();
81
82    size_t write(const char *bytes, size_t len);
83
84    int flushDamage();
85    int resize(short unsigned int rows, short unsigned int cols);
86
87    int getCell(VTermPos pos, VTermScreenCell* cell);
88
89    int getRows() const;
90    int getCols() const;
91
92    jobject getCallbacks() const;
93
94private:
95    int mMasterFd;
96    VTerm *mVt;
97    VTermScreen *mVts;
98
99    jobject mCallbacks;
100    short unsigned int mRows;
101    short unsigned int mCols;
102};
103
104static JNIEnv* getEnv() {
105    JNIEnv* env;
106
107    if (gJavaVM->AttachCurrentThread(&env, NULL) < 0) {
108        return NULL;
109    }
110
111    return env;
112}
113
114/*
115 * VTerm event handlers
116 */
117
118static int term_damage(VTermRect rect, void *user) {
119    Terminal* term = reinterpret_cast<Terminal*>(user);
120    ALOGW("term_damage");
121
122    JNIEnv* env = getEnv();
123    if (env == NULL) {
124        ALOGE("term_damage: couldn't get JNIEnv");
125        return 0;
126    }
127
128    return env->CallIntMethod(term->getCallbacks(), damageMethod, rect.start_row, rect.end_row,
129            rect.start_col, rect.end_col);
130}
131
132static int term_prescroll(VTermRect rect, void *user) {
133    Terminal* term = reinterpret_cast<Terminal*>(user);
134    ALOGW("term_prescroll");
135
136    JNIEnv* env = getEnv();
137    if (env == NULL) {
138        ALOGE("term_prescroll: couldn't get JNIEnv");
139        return 0;
140    }
141
142    return env->CallIntMethod(term->getCallbacks(), prescrollMethod);
143}
144
145static int term_moverect(VTermRect dest, VTermRect src, void *user) {
146    Terminal* term = reinterpret_cast<Terminal*>(user);
147    ALOGW("term_moverect");
148
149    JNIEnv* env = getEnv();
150    if (env == NULL) {
151        ALOGE("term_moverect: couldn't get JNIEnv");
152        return 0;
153    }
154
155    return env->CallIntMethod(term->getCallbacks(), moveRectMethod);
156}
157
158static int term_movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user) {
159    Terminal* term = reinterpret_cast<Terminal*>(user);
160    ALOGW("term_movecursor");
161
162    JNIEnv* env = getEnv();
163    if (env == NULL) {
164        ALOGE("term_movecursor: couldn't get JNIEnv");
165        return 0;
166    }
167
168    return env->CallIntMethod(term->getCallbacks(), moveCursorMethod);
169}
170
171static int term_settermprop(VTermProp prop, VTermValue *val, void *user) {
172    Terminal* term = reinterpret_cast<Terminal*>(user);
173    ALOGW("term_settermprop");
174
175    JNIEnv* env = getEnv();
176    if (env == NULL) {
177        ALOGE("term_settermprop: couldn't get JNIEnv");
178        return 0;
179    }
180
181    switch (vterm_get_prop_type(prop)) {
182    case VTERM_VALUETYPE_BOOL:
183        return env->CallIntMethod(term->getCallbacks(), setTermPropBooleanMethod,
184                static_cast<jboolean>(val->boolean));
185    case VTERM_VALUETYPE_INT:
186        return env->CallIntMethod(term->getCallbacks(), setTermPropIntMethod, prop, val->number);
187    case VTERM_VALUETYPE_STRING:
188        return env->CallIntMethod(term->getCallbacks(), setTermPropStringMethod, prop,
189                env->NewStringUTF(val->string));
190    case VTERM_VALUETYPE_COLOR:
191        return env->CallIntMethod(term->getCallbacks(), setTermPropIntMethod, prop, val->color.red,
192                val->color.green, val->color.blue);
193    default:
194        ALOGE("unknown callback type");
195        return 0;
196    }
197}
198
199static int term_setmousefunc(VTermMouseFunc func, void *data, void *user) {
200    Terminal* term = reinterpret_cast<Terminal*>(user);
201    ALOGW("term_setmousefunc");
202    return 1;
203}
204
205static int term_bell(void *user) {
206    Terminal* term = reinterpret_cast<Terminal*>(user);
207    ALOGW("term_bell");
208
209    JNIEnv* env = getEnv();
210    if (env == NULL) {
211        ALOGE("term_bell: couldn't get JNIEnv");
212        return 0;
213    }
214
215    return env->CallIntMethod(term->getCallbacks(), bellMethod);
216}
217
218static int term_resize(int rows, int cols, void *user) {
219    Terminal* term = reinterpret_cast<Terminal*>(user);
220    ALOGW("term_resize");
221
222    JNIEnv* env = getEnv();
223    if (env == NULL) {
224        ALOGE("term_bell: couldn't get JNIEnv");
225        return 0;
226    }
227
228    return env->CallIntMethod(term->getCallbacks(), resizeMethod);
229}
230
231static VTermScreenCallbacks cb = {
232    .damage = term_damage,
233    .prescroll = term_prescroll,
234    .moverect = term_moverect,
235    .movecursor = term_movecursor,
236    .settermprop = term_settermprop,
237    .setmousefunc = term_setmousefunc,
238    .bell = term_bell,
239    .resize = term_resize,
240};
241
242Terminal::Terminal(jobject callbacks, int rows, int cols) :
243        mCallbacks(callbacks), mRows(rows), mCols(cols) {
244    /* Create VTerm */
245    mVt = vterm_new(rows, cols);
246    vterm_parser_set_utf8(mVt, 1);
247
248    /* Set up screen */
249    mVts = vterm_obtain_screen(mVt);
250    vterm_screen_enable_altscreen(mVts, 1);
251    vterm_screen_set_callbacks(mVts, &cb, this);
252    // TODO: switch back to VTERM_DAMAGE_SCROLL?
253    vterm_screen_set_damage_merge(mVts, VTERM_DAMAGE_CELL);
254    vterm_screen_reset(mVts, 1);
255}
256
257Terminal::~Terminal() {
258    close(mMasterFd);
259    vterm_free(mVt);
260}
261
262int Terminal::run() {
263    /* None of the docs about termios explain how to construct a new one of
264     * these, so this is largely a guess */
265    struct termios termios = {
266        .c_iflag = ICRNL|IXON|IUTF8,
267        .c_oflag = OPOST|ONLCR|NL0|CR0|TAB0|BS0|VT0|FF0,
268        .c_cflag = CS8|CREAD,
269        .c_lflag = ISIG|ICANON|IEXTEN|ECHO|ECHOE|ECHOK,
270        /* c_cc later */
271    };
272
273    cfsetispeed(&termios, B38400);
274    cfsetospeed(&termios, B38400);
275
276    termios.c_cc[VINTR]    = 0x1f & 'C';
277    termios.c_cc[VQUIT]    = 0x1f & '\\';
278    termios.c_cc[VERASE]   = 0x7f;
279    termios.c_cc[VKILL]    = 0x1f & 'U';
280    termios.c_cc[VEOF]     = 0x1f & 'D';
281    termios.c_cc[VSTART]   = 0x1f & 'Q';
282    termios.c_cc[VSTOP]    = 0x1f & 'S';
283    termios.c_cc[VSUSP]    = 0x1f & 'Z';
284    termios.c_cc[VREPRINT] = 0x1f & 'R';
285    termios.c_cc[VWERASE]  = 0x1f & 'W';
286    termios.c_cc[VLNEXT]   = 0x1f & 'V';
287    termios.c_cc[VMIN]     = 1;
288    termios.c_cc[VTIME]    = 0;
289
290    struct winsize size = { mRows, mCols, 0, 0 };
291
292    int stderr_save_fd = dup(2);
293    if (stderr_save_fd < 0) {
294        ALOGE("failed to dup stderr - %s", strerror(errno));
295    }
296
297    pid_t kid = forkpty(&mMasterFd, NULL, &termios, &size);
298    if (kid == 0) {
299        /* Restore the ISIG signals back to defaults */
300        signal(SIGINT, SIG_DFL);
301        signal(SIGQUIT, SIG_DFL);
302        signal(SIGSTOP, SIG_DFL);
303        signal(SIGCONT, SIG_DFL);
304
305        FILE *stderr_save = fdopen(stderr_save_fd, "a");
306
307        if (!stderr_save) {
308            ALOGE("failed to open stderr - %s", strerror(errno));
309        }
310
311        char *shell = "/system/bin/sh"; //getenv("SHELL");
312#ifdef USE_TEST_SHELL
313        char *args[4] = {shell, "-c", "x=1; while true; do echo \"stop echoing yourself! ($x)\"; x=$(( $x + 1 )); sleep 0.5; done", NULL};
314#else
315        char *args[2] = {shell, NULL};
316#endif
317
318        execvp(shell, args);
319        fprintf(stderr_save, "Cannot exec(%s) - %s\n", shell, strerror(errno));
320        _exit(1);
321    }
322
323    while (1) {
324        char buffer[4096];
325        ssize_t bytes = ::read(mMasterFd, buffer, sizeof buffer);
326        ALOGD("Read %d bytes:", bytes);
327
328        if (bytes == 0) {
329            ALOGD("read() found EOF");
330            break;
331        }
332        if (bytes == -1) {
333            ALOGE("read() failed: %s", strerror(errno));
334            return 1;
335        }
336
337        vterm_push_bytes(mVt, buffer, bytes);
338    }
339
340    return 1;
341}
342
343size_t Terminal::write(const char *bytes, size_t len) {
344    return ::write(mMasterFd, bytes, len);
345}
346
347int Terminal::flushDamage() {
348    vterm_screen_flush_damage(mVts);
349    return 0;
350}
351
352int Terminal::resize(short unsigned int rows, short unsigned int cols) {
353    ALOGD("resize(%d, %d)", rows, cols);
354    // TODO: wait for resize event to propegate back from shell?
355    mRows = rows;
356    mCols = cols;
357    struct winsize size = { rows, cols, 0, 0 };
358    ioctl(mMasterFd, TIOCSWINSZ, &size);
359    // TODO: vterm_set_size?
360    return 0;
361}
362
363int Terminal::getCell(VTermPos pos, VTermScreenCell* cell) {
364    return vterm_screen_get_cell(mVts, pos, cell);
365}
366
367int Terminal::getRows() const {
368    return mRows;
369}
370
371int Terminal::getCols() const {
372    return mCols;
373}
374
375jobject Terminal::getCallbacks() const {
376    return mCallbacks;
377}
378
379/*
380 * JNI glue
381 */
382
383static jint com_android_terminal_Terminal_nativeInit(JNIEnv* env, jclass clazz, jobject callbacks,
384        jint rows, jint cols) {
385    return reinterpret_cast<jint>(new Terminal(env->NewGlobalRef(callbacks), rows, cols));
386}
387
388static jint com_android_terminal_Terminal_nativeRun(JNIEnv* env, jclass clazz, jint ptr) {
389    Terminal* term = reinterpret_cast<Terminal*>(ptr);
390    return term->run();
391}
392
393static jint com_android_terminal_Terminal_nativeFlushDamage(JNIEnv* env, jclass clazz, jint ptr) {
394    Terminal* term = reinterpret_cast<Terminal*>(ptr);
395    return term->flushDamage();
396}
397
398static jint com_android_terminal_Terminal_nativeResize(JNIEnv* env,
399        jclass clazz, jint ptr, jint rows, jint cols) {
400    Terminal* term = reinterpret_cast<Terminal*>(ptr);
401    return term->resize(rows, cols);
402}
403
404static jint com_android_terminal_Terminal_nativeGetCellRun(JNIEnv* env,
405        jclass clazz, jint ptr, jint row, jint col, jobject run) {
406    Terminal* term = reinterpret_cast<Terminal*>(ptr);
407
408    jcharArray dataArray = (jcharArray) env->GetObjectField(run, cellRunDataField);
409    ScopedCharArrayRW data(env, dataArray);
410    if (data.get() == NULL) {
411        return -1;
412    }
413
414    VTermScreenCell cell;
415    memset(&cell, 0, sizeof(VTermScreenCell));
416
417    VTermPos pos = {
418        .row = row,
419        .col = col,
420    };
421
422    unsigned int dataSize = 0;
423    unsigned int colSize = 0;
424    while (pos.col < term->getCols()) {
425        int res = term->getCell(pos, &cell);
426
427        // TODO: terminate this loop once text style changes
428
429        // TODO: remove this once terminal is resized
430        if (cell.width == 0) {
431            cell.width = 1;
432        }
433
434        // TODO: support full UTF-32 characters
435        // for testing, 0x00020000 should become 0xD840 0xDC00
436        unsigned int size = 1;
437
438        // Only include cell chars if they fit into run
439        if (dataSize + size <= data.size()) {
440            data[dataSize] = cell.chars[0];
441            dataSize += size;
442            colSize += cell.width;
443            pos.col += cell.width;
444        } else {
445            break;
446        }
447    }
448
449    env->SetIntField(run, cellRunDataSizeField, dataSize);
450    env->SetIntField(run, cellRunColSizeField, colSize);
451
452    return 0;
453}
454
455static jint com_android_terminal_Terminal_nativeGetRows(JNIEnv* env, jclass clazz, jint ptr) {
456    Terminal* term = reinterpret_cast<Terminal*>(ptr);
457    return term->getRows();
458}
459
460static jint com_android_terminal_Terminal_nativeGetCols(JNIEnv* env, jclass clazz, jint ptr) {
461    Terminal* term = reinterpret_cast<Terminal*>(ptr);
462    return term->getCols();
463}
464
465static JNINativeMethod gMethods[] = {
466    { "nativeInit", "(Lcom/android/terminal/TerminalCallbacks;II)I", (void*)com_android_terminal_Terminal_nativeInit },
467    { "nativeRun", "(I)I", (void*)com_android_terminal_Terminal_nativeRun },
468    { "nativeFlushDamage", "(I)I", (void*)com_android_terminal_Terminal_nativeFlushDamage },
469    { "nativeResize", "(III)I", (void*)com_android_terminal_Terminal_nativeResize },
470    { "nativeGetCellRun", "(IIILcom/android/terminal/Terminal$CellRun;)I", (void*)com_android_terminal_Terminal_nativeGetCellRun },
471    { "nativeGetRows", "(I)I", (void*)com_android_terminal_Terminal_nativeGetRows },
472    { "nativeGetCols", "(I)I", (void*)com_android_terminal_Terminal_nativeGetCols },
473};
474
475int register_com_android_terminal_Terminal(JNIEnv* env) {
476    ScopedLocalRef<jclass> localClass(env,
477            env->FindClass("com/android/terminal/TerminalCallbacks"));
478
479    android::terminalCallbacksClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass.get()));
480
481    android::damageMethod = env->GetMethodID(terminalCallbacksClass, "damage", "(IIII)I");
482    android::prescrollMethod = env->GetMethodID(terminalCallbacksClass, "prescroll", "(IIII)I");
483    android::moveRectMethod = env->GetMethodID(terminalCallbacksClass, "moveRect", "(IIIIIIII)I");
484    android::moveCursorMethod = env->GetMethodID(terminalCallbacksClass, "moveCursor",
485            "(IIIIIIIII)I");
486    android::setTermPropBooleanMethod = env->GetMethodID(terminalCallbacksClass,
487            "setTermPropBoolean", "(IZ)I");
488    android::setTermPropIntMethod = env->GetMethodID(terminalCallbacksClass, "setTermPropInt",
489            "(II)I");
490    android::setTermPropStringMethod = env->GetMethodID(terminalCallbacksClass, "setTermPropString",
491            "(ILjava/lang/String;)I");
492    android::setTermPropColorMethod = env->GetMethodID(terminalCallbacksClass, "setTermPropColor",
493            "(IIII)I");
494    android::bellMethod = env->GetMethodID(terminalCallbacksClass, "bell", "()I");
495    android::resizeMethod = env->GetMethodID(terminalCallbacksClass, "resize", "(II)I");
496
497    ScopedLocalRef<jclass> cellRunLocal(env,
498            env->FindClass("com/android/terminal/Terminal$CellRun"));
499    cellRunClass = reinterpret_cast<jclass>(env->NewGlobalRef(cellRunLocal.get()));
500    cellRunDataField = env->GetFieldID(cellRunClass, "data", "[C");
501    cellRunDataSizeField = env->GetFieldID(cellRunClass, "dataSize", "I");
502    cellRunColSizeField = env->GetFieldID(cellRunClass, "colSize", "I");
503
504    env->GetJavaVM(&gJavaVM);
505
506    return jniRegisterNativeMethods(env, "com/android/terminal/Terminal",
507            gMethods, NELEM(gMethods));
508}
509
510} /* namespace android */
511