То, чего я хотел добиться, это: приложение («игра», использующая 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.