Связь с другим процессом через каналы (как замена псевдотерминала) без блокировки и без буферизации в Windows, C / C ++ - PullRequest
1 голос
/ 10 февраля 2020

То, чего я хотел добиться, это: приложение («игра», использующая SDL2, но не имеющее отношения к проблеме), которое может отображать на экране несколько вещей, одна из которых может быть «компьютерным терминалом», который пользователь / игрок может взаимодействовать с. Но внутриигровой компьютер будет запускать реальную программу на реальном компьютере с вводом и выводом, перенаправленным в игру. Таким образом, «игра» должна вести себя как терминал для гостевой программы, например, xterm или тому подобное. И я хотел, чтобы это работало как на GNU / Linux, так и на Windows.

Так что я начал делать демо, которое делает именно это и ничего больше. Получает действие клавиатуры и преобразует его в текст, отправленный программе, получает текст из программы и выводит его на экран.

В GNU / Linux мне удалось это сделать. Сделал это с помощью псевдотерминала, который является инструментом именно для этой цели. Я создаю псевдотерминал с помощью openpty (), делаю его неблокирующим, затем делаю fork (), в форке перенаправляю IO на преудотерминал, устанавливаю его в качестве управляющего терминала и, наконец, выполняю execl () для запуска гостевая программа. В основной программе я регулярно выполняю read () и write (). И это отлично работает.

Далее Windows. Windows не поддерживает fork-exe c, поэтому я делаю это Windows способом, используя CreateProcess (). Windows также не имеет псевдотерминалов, поэтому я использую именованные каналы. Я знаю, что каналы не обладают всеми возможностями псевдотерминала, но для моих нужд это нормально. Поэтому в Windows я создаю два канала с помощью CreateNamedPipe (), открываю другие стороны с помощью CreateFile () с наследуемыми дескрипторами, наконец запускаю гостевую программу с помощью CreateProcess () с дескрипторами каналов, установленными в качестве входных и выходных данных. В основной программе я регулярно выполняю ReadFile () и WriteFile ().

Это тоже работает, но не хорошо. Происходят две нежелательные вещи. Во-первых, после того, как канал вывода (гостевой программы) больше не имеет доступных байтов, функция ReadFile () не вернется, даже если каналы были созданы с флагом PIPE_NOWAIT. Но потом я узнал, что чтение блокировалось только в Wine. В реальном Windows чтение было неблокирующим, как и должно быть. Но есть и второе. Канал вывода (гостевой программы) буферизуется системой. Когда гостевая программа записывает в свой вывод (так, в канал), хост-программа не видит никаких байтов для чтения. Только когда гость записывает большое количество байтов, все они go к хосту одновременно. И это происходит, даже если канал открыт на гостевой стороне с помощью FILE_FLAG_NO_BUFFERING. Это неприемлемо, потому что мне нужно взаимодействие в реальном времени.

Ниже приведен код, который устанавливает процесс и каналы / псевдотерминал:

    bool LemlTerm::runCommand() {
#ifdef LEML_WIN32
    char ptyName[256];
    HANDLE ptyInM = NULL;
    HANDLE ptyInS = NULL;
    HANDLE ptyOutM = NULL;
    HANDLE ptyOutS = NULL;
    SECURITY_ATTRIBUTES sAtr;

    LPCSTR applName = "load.exe";
    LPSTR cmdLine = (LPSTR)"load.exe test";
    STARTUPINFO stInfo;
    PROCESS_INFORMATION prInfo; 

    if (commandRunning)
        return false;

    fail=false;

    sAtr.nLength = sizeof(SECURITY_ATTRIBUTES); 
    sAtr.bInheritHandle = TRUE; 
    sAtr.lpSecurityDescriptor = NULL; 

    snprintf(ptyName,256,"\\\\.\\pipe\\%08lx_LEML_PTY_IN",(unsigned long)GetCurrentProcessId());
    printf ("ptyIn: %s\n",ptyName);

    ptyInM = CreateNamedPipe (
        (LPCSTR) ptyName,
        PIPE_ACCESS_OUTBOUND,
        PIPE_NOWAIT,
        1,
        TERMINAL_W,
        TERMINAL_W,
        0, //timeout
        NULL);

    if (ptyInM == INVALID_HANDLE_VALUE) {
        fail=true;
        snprintf(errorText,256,"Could not open ptyM: %s.",ptyName);
        return false;
    }

    ptyInS = CreateFile (
        (LPCSTR) ptyName,
        GENERIC_READ,
        0,
        &sAtr,
        OPEN_EXISTING,
        FILE_FLAG_NO_BUFFERING,
        NULL);

    if (ptyInS == INVALID_HANDLE_VALUE) {
        fail=true;
        snprintf(errorText,256,"Could not open ptyS: %s.",ptyName);
        CloseHandle(ptyInM);
        return false;
    }

    snprintf(ptyName,256,"\\\\.\\pipe\\%08lx_LEML_PTY_OUT",(unsigned long)GetCurrentProcessId());
    printf ("ptyOut: %s\n",ptyName);

    ptyOutM = CreateNamedPipe (
        (LPCSTR) ptyName,
        PIPE_ACCESS_INBOUND,
        PIPE_NOWAIT,
        1,
        TERMINAL_W,
        TERMINAL_W,
        0,  //timeout
        NULL);

    if (ptyOutM == INVALID_HANDLE_VALUE) {
        fail=true;
        snprintf(errorText,256,"Could not open ptyM: %s.",ptyName);
        CloseHandle(ptyInS);
        CloseHandle(ptyInM);
        return false;
    }

    ptyOutS = CreateFile (
        (LPCSTR) ptyName,
        GENERIC_WRITE,
        0,
        &sAtr,
        OPEN_EXISTING,
        FILE_FLAG_NO_BUFFERING,
        NULL);

    if (ptyOutS == INVALID_HANDLE_VALUE) {
        fail=true;
        snprintf(errorText,256,"Could not open ptyS: %s.",ptyName);
        CloseHandle(ptyOutM);
        CloseHandle(ptyInS);
        CloseHandle(ptyInM);
        return false;
    }

    ptyOpen = true;


    ZeroMemory (&prInfo, sizeof(PROCESS_INFORMATION));
    ZeroMemory (&stInfo, sizeof(STARTUPINFO));
    stInfo.cb = sizeof(stInfo); 
    stInfo.hStdError = ptyOutS;
    stInfo.hStdOutput = ptyOutS;
    stInfo.hStdInput = ptyInS;
    stInfo.dwXCountChars = (DWORD)width;
    stInfo.dwYCountChars = (DWORD)((mode==modeHR)?heightHR:heightLR);
    stInfo.wShowWindow = SW_HIDE;
    stInfo.dwFlags |= STARTF_USESTDHANDLES | STARTF_USECOUNTCHARS |  STARTF_USESHOWWINDOW;

    if (!CreateProcess (
        applName,
        cmdLine,
        NULL,
        NULL,
        TRUE,
        // CREATE_NEW_CONSOLE,
        DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP,
        NULL, //ENV!!
        NULL, //CWD!!
        &stInfo,
        &prInfo
    )) {
        fail = true;
        snprintf(errorText,256,"Could not CreateProcess.");
        CloseHandle(ptyOutS);
        CloseHandle(ptyOutM);
        CloseHandle(ptyInS);
        CloseHandle(ptyInM);
        ptyOpen = false;
        return false;
    }

    CloseHandle(ptyOutS);
    CloseHandle(ptyInS);
    CloseHandle(prInfo.hThread);
    ptyIn = ptyInM;
    ptyOut = ptyOutM;
    pidHandle = prInfo.hProcess;
    pid = prInfo.dwProcessId;
    commandRunning = true;

    return true;
#else
    int ptyM, ptyS;
    int i;
    struct termios termp;
    int flags;

    if (commandRunning)
        return false;

    fail=false;

    i=openpty(&ptyM,&ptyS,NULL,NULL,NULL);
    if (i<0) {
        fail = true;
        snprintf(errorText,256,"Could not open pty.");
        return false;
    }
    i=tcgetattr(ptyM, &termp);
    // cfmakeraw(&termp);
    i|=tcsetattr(ptyM, TCSANOW, &termp);
    flags=fcntl(ptyM, F_GETFL);
    flags|=O_NONBLOCK;
    i|=fcntl(ptyM, F_SETFL, flags);
    if (i<0) {
        fail = true;
        snprintf(errorText,256,"Could not modify pty mode");
        close(ptyM);
        close(ptyS);
        return false;
    }
    ptyOpen = true;

    switch (pid=fork()) {
    case -1:
        fail = true;
        snprintf(errorText,256,"Could not fork");
        close(ptyM);
        close(ptyS);
        ptyOpen = false;
        // printf("LOL FAIL FORK\n");
        return false;
        break;
    case 0:
        setsid();
        dup2(ptyS, STDIN_FILENO);
        dup2(ptyS, STDOUT_FILENO);
        dup2(ptyS, STDERR_FILENO);
        if (ioctl(ptyS, TIOCSCTTY, NULL) < 0)
            exit(1);
        close(ptyM);
        close(ptyS);
        clearenv();
        // printf("I AM FORK\n");
        execl("./load","./load","test",NULL);
        printf("LOL FAIL EXEC");
        exit(1);
        break;
    default:
        commandRunning = true;
        close(ptyS);
        pty=ptyM;
        // printf("SUCCESS FORK\n");
        break;
    }
    return true;
#endif
}

Как вы можете видеть, это та же самая функция, выполненная дважды, один раз для Windows и один раз для GNU / Linux, выбранный # ifdef.

И вот код, который осуществляет связь:

bool LemlTerm::perform() {
    bool nextline;
    unsigned short canRead1, canRead2, canWrite1, canWrite2;
#ifdef LEML_WIN32
    DWORD n;
#else
    ssize_t n;
#endif

    fail = false;

    ++blinkCount;
    if(ringCount)
        --ringCount;

    if (commandRunning) {
#ifdef LEML_WIN32
        if (WaitForSingleObject(pidHandle,0) != WAIT_TIMEOUT) {
            CloseHandle(pidHandle);
            commandRunning = false;
        }
#else
        if (waitpid (pid, NULL, WNOHANG) > 0 )
            commandRunning = false;
#endif
    }
    //TODO: how and when to close pty!!

    canWrite1 = (outRd - outWr - 1)&0xff;
    if(canWrite1 > (256-outWr)) {
        canWrite2 = canWrite1 + outWr - 256;
        canWrite1 -= canWrite2;
    } else {
        canWrite2 = 0;
    }

    printf("rd:%hu wr:%hu cw:%hu+%hu=%hu\n",outRd, outWr, canWrite1, canWrite2, canWrite1+canWrite2);

    if (canWrite1) {
#ifdef LEML_WIN32
        ReadFile(ptyOut, outputBuffer+outWr, (DWORD)canWrite1, &n, NULL);
#else       
        n=read(pty, outputBuffer+outWr, (size_t)canWrite1);
#endif
        printf("%hu\n",(unsigned short*)n);
        if (n>0) {
            outWr += n;
            outWr &= 0xff;
            if ((n==canWrite1) && canWrite2) {
#ifdef LEML_WIN32
                ReadFile(ptyOut, outputBuffer+outWr, (DWORD)canWrite2, &n, NULL);
#else       
                n=read(pty, outputBuffer+outWr, (size_t)canWrite2);
#endif
                printf("%hu\n",(unsigned short*)n);
                if (n>0) {
                    outWr += n;
                    outWr &= 0xff;
                }
            }
        }
    }

    for (; outWr!=outRd; outRd=(outRd+1)&0xff) {
        screenChanged = true;
        nextline=false;

        switch (outputBuffer[outRd]) {
        case 7: //bell
            ringCount += 0x10;
            break;
        case 8: //backspace
            outputBuffer[outRd]=' ';
            if (cursorX>0) {
                --cursorX;
            } else if (cursorY>0) {
                --cursorY;
                cursorX = width;
            }
            textBuffer[(bufferY+cursorY)%heightHR][cursorX] = ' ';
            break;
        case 9: //tab
            cursorX &= ~0x7;
            cursorX += 8;
            if(cursorX >= width) {
                cursorX = 0;
                nextline = true;
            }
            break;
        case 10: //newline
            cursorX = 0;
        case 11: //vt
        case 12: //ff
            nextline = true;
            break;
        case 13: //cr
            cursorX = 0;
            break;
        default:
            textBuffer[(bufferY+cursorY)%heightHR][cursorX] = outputBuffer[outRd];
            ++cursorX;
            if(cursorX >= width) {
                cursorX = 0;
                nextline = true;
            }
        }

        if (nextline) {
            // cursorX = 0;
            if (cursorY < ((mode==modeHR)?heightHR:heightLR) - 1) {
                ++cursorY;
            } else {
                ++bufferY;
                bufferY %= heightHR;
            }
            // memset(textBuffer[(bufferY+cursorY)%heightHR],(bufferY+cursorY)%heightHR,width);
            memset(textBuffer[(bufferY+cursorY)%heightHR],' ',width);

            //optional! for simulate scrolling delay...
            outRd=(outRd+1)&0xff;
            break; 
        }
        // //SUPER SLOW
        // outRd=(outRd+1)&0xff;
        // break; 
    }

    canRead1 = (inWr-inRd)&0x1f;
    if(canRead1 > (32-inRd)) {
        canRead2 = canRead1 + inRd - 32;
        canRead1 -= canRead2;
    } else {
        canRead2 = 0;
    }


    if (canRead1) {
        printf("rd:%hu wr:%hu cr:%hu+%hu=%hu\n",inRd, inWr, canRead1, canRead2, canRead1+canRead2);
#ifdef LEML_WIN32
        WriteFile(ptyIn, inputBuffer+inRd, (DWORD)canRead1, &n, NULL);
#else       
        n=write(pty, inputBuffer+inRd, (size_t)canRead1);
#endif
        printf("%hu\n",(unsigned short*)n);
        if (n>0) {
            inRd += n;
            inRd &= 0x1f;
            if ((n==canRead1) && canRead2) {
#ifdef LEML_WIN32
                WriteFile(ptyIn, inputBuffer+inRd, (DWORD)canRead2, &n, NULL);
#else
                n=write(pty, inputBuffer+inRd, (size_t)canRead2);
#endif
                printf("%hu\n",(unsigned short*)n);
                if (n>0) {
                    inRd += n;
                    inRd &= 0x1f;
                }
            }
        }
    }

    return updateScreen();
}

Это функция, которая вызывается один раз за итерацию основного l oop (один раз для фрейма) Если проверяет, выполняется ли еще гостевая программа, читает из вывода программ и записывает его в буфер циклического вывода, проходит через буфер циклического вывода и помещает содержимое Информация о буфере экрана, чтение из кольцевого буфера ввода (данные поступают туда при работе с клавиатурой) и запись его на вход программы, вызывает обновление экрана. И при выполнении всего этого печатается некоторая отладочная информация.

Я не знаю, что я делаю неправильно, что приводит к блокировке (только в Wine) и нежелательной буферизации (в Windows).

Мой вопрос: что еще мне нужно сделать, чтобы установить связь по каналам без блокирования и без постоянной буферизации на Windows?

Я знаю, что существует ConPTY (windows 'новый псевдотерминальный эквивалент). Но это новая функция, доступная с Windows 10 1809. Единственная причина, по которой я рассматриваю Windows, заключается в том, что многие люди используют только Windows, и я бы хотел, чтобы они могли использовать программы, которые я делаю. Использование этой новой функции исключит всех, кто использует старые системы, поэтому многие люди (включая меня, поскольку у меня нет этой новой Windows 10).

Также я знаю, что каналы не обладают всеми функциями псевдотерминалов, такими как редактирование строк, отправка сигналов в программы, последовательности управления и т. д. c. и если мне нужны какие-либо из этих функций, я должен был бы сделать их сам, но кроме редактирования строк (что я могу сделать), мне, вероятно, не понадобятся они.

Я включил только код, который связано с межпроцессным взаимодействием. Но при необходимости я могу предоставить всю демонстрацию. Демонстрация написана на C ++, однако эта проблема в равной степени относится к C.

...