Linux, opcje terminala na które termios nie ma wpływu?

0

Cześć,
Najpierw trochę kontekstu. Eksploruję sobie obsługę terminala z poziomu C, bo chciałbym bardziej elastycznie używać procesów. Np. skryptowanie Vima to udręka, a gdybym mógł po prostu wcisnąć mu po prostu klawisze, byłoby lżej. Chodzi o to, żeby mieć otwartych kilka procesów (np. vim, gdb, shell) i swobodnie przełączać je między sobą. Tak więc, mówiąc obrazowo coś jak centrala telefoniczna dla strumieni. One shell to rule them all, że tak powiem ;)
Doszedłem, że w tym celu powinienem sobie tworzyć PTY dla każdego procesu, żeby móc w pełni obsłużyć każdy program i potem wewnątrz mojego programu tworzyć połączenia między poszczególnymi procesami. Jestem w trakcie implementowania funkcji podłączania wejcia i wyjścia procesu prosto na konsolę. W tym celu używam termiosa, żeby wyłączyć echo i buforowanie i wtedy mogę po prostu sobie po prostu przepisywać, a po zamknięciu procesu przywracam poprzednie ustawienia termianala zapisane wcześniej.

Kawałek kodu poglądowego:

int thp_writer(void *ctx) {
    int fd = *((int*)ctx);
    char buff[BUFF_SIZE+1];
    buff[BUFF_SIZE] = 0;
    int n;
    while((n = read(fd, buff, BUFF_SIZE)) > 0) {
        //Kawałek używany do debugowania: czyt dalej
        //for(int i = 0; i < n; i++)
        //    if(buff[i] < 0x20) buff[i] = '?';
        write(STDOUT_FILENO, buff, n);
    }

    thrd_exit(0);
}

void ch_fg(char *const args[], PTY *ptys) {
    PTY *pty = args_to_pty(args, ptys);
    if(pty != NULL) {
        thrd_t thrd_writer;
        if(thrd_create(&thrd_writer, thp_writer, (void*)&(pty->master)) == thrd_error) {
            fprintf(stderr, "Can't create writer thread. :(");
            return;
        }

        struct termios termopt, old_ito, old_oto, old_eto;
        tcgetattr(STDIN_FILENO, &termopt);
        tcgetattr(STDOUT_FILENO, &old_oto);
        tcgetattr(STDERR_FILENO, &old_eto);
        old_ito = termopt;
        termopt.c_lflag &= ~(ICANON | ECHO);
        tcsetattr(STDIN_FILENO, TCSAFLUSH, &termopt);

        fd_set rds;
        int n;
        char c;
        struct timeval tv = { .tv_sec = 2, .tv_usec= 0 };
    
        while(1) {
            FD_ZERO(&rds);
            FD_SET(STDIN_FILENO, &rds);
            if(select(STDIN_FILENO+1, &rds, NULL, NULL, &tv) < 0) {
                break;
            }
            if(!(FD_ISSET(STDIN_FILENO, &rds))) continue;
    
            if((n = read(STDIN_FILENO, &c, 1)) > 0) {
                write(pty->master, &c, 1);
            } 

            int status;
            pid_t pid;
            if((pid = waitpid(-1, &status, WNOHANG)) > 0) {
                printf("pid #%d finished: %d\n", (int)pid, status);
                thrd_join(thrd_writer, &status);
                fsync(STDIN_FILENO);
                fsync(STDOUT_FILENO);
                fsync(STDERR_FILENO);
                tcsetattr(STDIN_FILENO, TCSAFLUSH, &old_ito);
                tcsetattr(STDOUT_FILENO, TCSAFLUSH, &old_oto);
                tcsetattr(STDERR_FILENO, TCSAFLUSH, &old_eto);
                break;
            }
        }
    }
}

I to prawie działa, bo uruchamiam sobie vima i podłączam do terminala, wszystko działa super dopóki nie zamknę go. Pomimo, że po jego zamknięciu przywracam ustawienia termiosa po zsynchronizowaniu strumieni standardowych, wraca mi do mojego kodu, ale ignoruje mi wszystkie znaki nowej linii i powrotu karetki, czyli praktycznie nie do użytku jest. Używałem zakomentowanego kawałka kodu, żeby zweryfikować, że vim używa jakiegoś specjalnego kodu ucieczki (escape code), żeby zmienić ustawienia terminala i tak się rzeczywiście dzieje.

Teraz pytanie, jak to właściwie zrobić? Powinienem otworzyć /dev/tty i na nim robić termios czy może użyć jakiegoś specjalnego kodu do resetowania terminala? Może jakieś komentarze jak to lepiej zrobić, co jest potrzebne, a co nie?

0

Jeśli ignoruje znaki powrotu karetki i nowej linii to widocznie masz coś skopane w ustawieniach dyscypliny linii tty-ka.

# stty -a -F `tty`
speed 38400 baud; rows 56; columns 183; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V;
discard = ^O; min = 1; time = 0;
-parenb -parodd -cmspar cs8 -hupcl -cstopb cread -clocal -crtscts
-ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc -ixany -imaxbel iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke -flusho -extproc

Tu są wylistowane parametry dyscypliny linii tty-ka. Sprawdź czy masz dokładnie te same paramtery przed uruchomieniem Twojego programu jak i po. Nie wiem czy dobrze interpretuję Twój kod - ale wyłączasz tryb kanoniczny i echo - więc chyba dobrze byłoby oba te parametry przywrócić żeby była reakcja na eol-a.

0

@vtx: linia #29 zapisuję sobie stare ustawienie termios, w linii #58 przywracam. Jest coś co nie zawiera się w tych wywołaniach?

1
elwis napisał(a):

@vtx: linia #29 zapisuję sobie stare ustawienie termios, w linii #58 przywracam. Jest coś co nie zawiera się w tych wywołaniach?

Aaaa faktycznie w linii 58 przywracasz oryginalne parametry - nie przyjrzałem się do końca. Mea culpa. Według mnie trzeba porównać jednak te parametry tty-ka po i przed, bo być może coś gdzieś przegapiłeś. Acha - dyscypliną linii zawiaduje kernel i to co mu ustawisz w stty-ku bądź w C w Twoim kodzie - on to będzie respektować. Nie jestem pewien czy coś innego może mieć tu jeszcze coś do powiedzenia odnośnie sterowania tty-kiem.

0

Generalnie chyba mam jakiś wyścig, bo zauważyłem, że jak poczekam dłużej i klepnę enter to działa dobrze. Spróbuję wywalić ten wątek bo w sumie jest bez sensu, jak mam selecta i czy aby na pewno fsync() czeka aż wszystko zostanie zapisane?

0
elwis napisał(a):

Generalnie chyba mam jakiś wyścig, bo zauważyłem, że jak poczekam dłużej i klepnę enter to działa dobrze. Spróbuję wywalić ten wątek bo w sumie jest bez sensu, jak mam selecta i czy aby na pewno fsync() czeka aż wszystko zostanie zapisane?

Wycinek z "man 2 fsync":

fsync()  transfers  ("flushes")  all modified in-core data of (i.e., modified buffer cache pages for) the file referred to 
by the file descriptor fd to the disk device (or other permanent storage device) so that all changed information can be
retrieved even if the system crashes or is rebooted.  This includes writing through  or  flushing  a disk cache if present.
The call blocks until the device reports that the transfer has completed.

więc powinien zaczekać. Tylko zastanawiam się do czego potrzebne jest to:

fsync(STDIN_FILENO);

przecież to standardowej wejście, a fsync wymusza zrzucenie buforów na nośnik. Ale może o czymś nie wiem?
Polecam również sprawdzanie statusów wyjściowych funkcji - m.in. tcsetattr(), fsync(), etc.

0

Jeśli vi ci nie spełnia oczekiwań jest jeszcze EMACS, ACME i sam xD.

tak więc, mówiąc obrazowo coś jak centrala telefoniczna dla strumieni.

By zarządzać strumieniami nie ma potrzeby babrać się w PTY.

One shell to rule them all, że tak powiem ;)

Poza patlogicznymi przypadkami ułańskiej fantazji niektórych twórców shelli, shell nie powinien w ogóle schodzić w swym kodzie do poziomu pty.

Nie myślałeś o podejściu gdzie cię interesuje tylko stdin, stdout i stderr oraz pipe między nimi? Do tego nie trzeba się bawić w babranie w pty. Wyjaśnij dlaczego schodzisz do tego poziomu? Czego nie da rady uzyskać z poziomu powłoki Bourna/Basha plus programów typu tee i podobnych działających w userspace? Z POSIX pipe i funkcją POSIX dup2() idzie sporo zdziałać. Wzmiankowany tmux też to i owo potrafi.

Jeśli jednak to faktycznie takie rewolucyjne co tworzysz to będziemy świadkami narodzin czegoś naprawdę przełomowego w ciągu ostatnich 52 lat istnienia UNIXów i 30 istnienia Plan9. :D

Stąd moje wątpliwości.

0
Satanistyczny Awatar napisał(a):

By zarządzać strumieniami nie ma potrzeby babrać się w PTY.

Z tego co pamiętam, na przykład Vim nie za bardzo da się uruchomić z przekierowanymi strumieniami, będzie marudzić, że nie ma pełnego terminala. Natomiast chciałbym móc również i takich programów używać. Poza tym, nie powiedziałbym, że to jest babranie. Poniekąd to jest trochę mniej kodu niż stworzenie trzech strumieni osobno.
Chcę tylko lepszej integracji między programami. Co z tego wyjdzie, to się okaże. Ważne, że jest fun, co nie? :)

vtx napisał(a):

więc powinien zaczekać. Tylko zastanawiam się do czego potrzebne jest to:

fsync(STDIN_FILENO);

To zdecydowanie dobra uwaga. Pewnie z rozpędu. :)

0

Zapodaj waszmość przykładowy ekhhhm... pipeline, że tak to ujmę, co chcesz ze sobą kleić bo mnie ciekawość opanowała co się pichci. Chcesz kleić kilka programów razem po kolei z vimem na końcu/początku przesyłu? Czy może vim będzie odbiorcą strumieni z wielu programów bez zabawy w przesyły między czymś więcej i chesz przełączać na bieżąco?

0

@Satanistyczny Awatar: W przypadku vima, jedyny przypadek jaki na tą chwile mam w głowie jest taki, że pracuję w Vimie, normalnie wyświetla mi się w terminalu, ale z poziomu tej mojej centali przechwycam skrót klawiszowy, który odpala skrypt, którego wynikiem jest zapodanie do Vima konkretnych klawiszy. Na przykład mam odpalonego na innym pty GDB, który jest w danym miejscu kodu. Wciśnięcie klawisza powoduje wykonanie działanie na GDB lub przejśćie bezpośrednio do wiersza poleceń GDB. Myślę, że takie rozwiązanie miałoby większe możliwości (a na pewno byłoby łatwiejsze w utrzymaniu) niż jakakolwiek dedykowana wtyczka do vima.

0

Niestety nie rozumiem scenariusza, to ma być na 3 terminalach? Na 2? Na 1? Masz 3 okna na pierwszym gdb na drugim vim na trzecim centrala? Czy inaczej? Na jednym i sobie hulasz między pty?

0

Najpierw mam swoją powłokę (że tak to nazwę, choć może być mylące jak widać). Odpalam sobie w niej GDB i VIMa. Gdb jest w trakcie pracy i dajmy na to siedzi sobie na breakpoincie, ale aktywnego mam vima i on zajmuje 100% ekranu. i dajmy na to chcę

  1. Wcisnąć MOD+B żeby przełączyć się na GDB (nic nadzwyczajnego w stosunku do tmuxa, ale warto wspomnieć)
  2. Wcisnąć MOD+N żeby wysłać GDBowi polecenie next i automatycznie przesunąć kursor w miejsce następnego kroku.
  3. Może nawet wcisnąć MOD+X żeby koło wszystkich zmiennych widocznych na ekranie pojawiły się ich wartości zassane z GDB
  4. Dajmy na to, że mam oba te procesy w tle i wpisuję polecenie otwierające nowego vima z otwartym plikiem, ale otwiera mi od razu gotową sesję (ewentualnie 2 okno?)

Możliwości jest wiele… Może nie brzmi jakoś niezwykle, bo są do tego wtyczki, ale to byłoby to rozwiązanie uniwersalne i myślę, że gdybym miał takie coś dopiero bym się przekonał co to może dać. :)

0

Myślę, że mam wyjaśnienie tego co się dzieje. Mianowicie, zamknięcie procesu nie gwarantuje, że cały jej output będzie odczytany i zinterpretowany przez emulator terminala. Co więcej, to chyba jest bug w xterm, którego używałem, bo aplikacja zachowuje się inaczej i nie udało mi się w ogóle zreprodukować błędu na urxvt. Ostatecznie i tak pewnie będę musial wejść w szczegóły tych escape kodów i wycinać w razie potrzeby. Wklejam ostateczny kod po refactorze, może komuś się przyda:

typedef int (*TsbFun)(struct termios termopt, void *ctx);
int term_sandbox(TsbFun action, void *ctx) {
    struct termios ito, oto, eto;
    tcgetattr(STDIN_FILENO, &ito);
    tcgetattr(STDOUT_FILENO, &oto);
    tcgetattr(STDERR_FILENO, &eto);

    struct termios termopt = ito;
    int ans = action(termopt, ctx);

    tcsetattr(STDIN_FILENO, TCSAFLUSH, &ito);
    tcsetattr(STDOUT_FILENO, TCSAFLUSH, &oto);
    tcsetattr(STDERR_FILENO, TCSAFLUSH, &eto);
    
    return ans;
}

int rewrite_fds(uint count, int *ifds, int *ofds, int waitmask) {
    char buff[BUFF_SIZE+1];
    buff[BUFF_SIZE] = 0;

    fd_set rds;
    struct timeval tv = { .tv_sec = 2, .tv_usec= 0 };
    int maxfd = ifds[0];
    for(uint i = 1; i < count; i++)
        if(ifds[i] > maxfd)
            maxfd = ifds[i];
    maxfd++;

    int deadmask = 0;
    do {
        FD_ZERO(&rds);
        for(uint i = 0; i < count; i++) {
            FD_SET(ifds[i], &rds);
        }

        if(select(maxfd, &rds, NULL, NULL, &tv) < 0) {
            perror("select");
            return -1;
        }

        deadmask = 0;
        for(uint i = 0; i < count; i++) {
            if(FD_ISSET(ifds[i], &rds)) {
                int n = read(ifds[i], buff, BUFF_SIZE);
                if(n > 0) {
                    write(ofds[i], buff, n);
                } else {
                    deadmask |= 1 << i;
                }
            }
        }
    } while((deadmask & waitmask) == 0);

    return deadmask;
}

int pty_foreground(struct termios termopt, PTY *pty) {
    termopt.c_lflag &= ~(ICANON | ECHO);
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &termopt);

    rewrite_fds(2, (int[]) { STDIN_FILENO, pty->master },
                   (int[]) { pty->master, STDOUT_FILENO },
                   0x02);

    int status;
    waitpid(pty->pid, &status, 0);
    return status;
}

void ch_fg(char *const args[], PTY *ptys) {
    PTY *pty = args_to_pty(args, ptys);
    if(pty != NULL) {
        int status = term_sandbox((TsbFun)&pty_foreground, pty);
        printf("pid #%d finished with status %d\n", (int)pty->pid, status);
        *pty = NO_PTY;
    }
}

1 użytkowników online, w tym zalogowanych: 0, gości: 1