Bash

程式 - 與國際象棋引擎stockfish / FIFO / Bash重定向通信

  • August 29, 2020

我正在嘗試編寫一個小國際象棋程序 - 實際上更像是一個國際象棋 GUI。當玩家與電腦對戰時,國際象棋 GUI 應在後台使用 stockfish 國際象棋引擎。

我安裝了 stockfish 並可以在終端中執行它並通過 STDIN 和 STDOUT 與它通信,例如我可以輸入“isready”,stockfish 以“readyok”響應。

現在我試圖能夠通過 linux 上的一些 IPC 方法從國際象棋 GUI 與 stockfish 進行通信。首先研究了管道,但由於管道單向通信而放棄了這一點。然後我閱讀了 FIFO 和 Bash 重定向,現在就嘗試一下。它有點工作,因為我可以從stockfish 讀取一行輸出。但這僅適用於第一行。然後,當我通過 FIFO 向 stockfish 發送“isready”並嘗試從 stockfish 讀取下一個輸出時,沒有響應。我使用 Bash 重定向將 stockfish 的 STDIN 和 STDOUT 重定向到 FIFO。

我執行此腳本以在一個終端中啟動 stockfish:

#!/bin/bash

rm /tmp/to_stockchess -f
mkfifo /tmp/to_stockchess

rm /tmp/from_stockchess -f
mkfifo /tmp/from_stockchess

stockfish < /tmp/to_stockchess > /tmp/from_stockchess

我用 ./stockfish.sh 呼叫這個腳本

例如,我有這個 c 程序(我是 C 新手)

#include <stdio.h> 
#include <stdlib.h>


int main(void) {
   
   FILE *fpi;
   fpi = fopen("/tmp/to_stockchess", "w");

   FILE *fpo;
   fpo = fopen ("/tmp/from_stockchess", "r");

   char * line = NULL;
   size_t len = 0;
   ssize_t read;

   read = getline(&line, &len, fpo);
   printf("Retrieved line of length %zu:\n", read);
   printf("%s", line);

   fprintf(fpi, "isready\n");

   read = getline(&line, &len, fpo);
   printf("Retrieved line of length %zu:\n", read);
   printf("%s", line);

   fclose (fpi);
   fclose (fpo);

   return 0;
}

終端中程序的輸出(但程序不會停止,它會等待):

Retrieved line of length 74:
Stockfish 11 64 POPCNT by T. Romstad, M. Costalba, J. Kiiski, G. Linscott

唉,不能連續工作(我現在沒有循環,只是試圖在沒有循環的情況下讀取兩次或更多次),例如,stockfish 腳本在一個終端中終止(而不是連續執行),在從鱈魚輸出先進先出。或者我可以從stockfish 輸出FIFO 中讀取一行輸出。如果有更簡單的方法通過 STDIN 和 STDOUT 對stockfish 進行 IPC,我也可以嘗試。謝謝你。

Since you're already working with C, then I'd suggest you manage stockchessin C as well. There is a library function,popen()` that will give you a unidirectional pipe to a process – that doesn’t suite your use case. You can, however, set it up yourself.

Consider the following example program:

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

/**
* Creates two pipes, forks, and runns the given command.  One pipe is
* connected between the given *out and the standard input stream of the child;
* the other pipe is connected between the given *in and the standard output
* stream of the child.
*
* Returns the pid of the child on success, -1 otherwise.  On error, errno
* will be set accordingly.
*/
int bi_popen(const char* const command, FILE** const in, FILE** const out)
{
   const int READ_END = 0;
   const int WRITE_END = 1;
   const int INVALID_FD = -1;

   int to_child[2] = { INVALID_FD, INVALID_FD };
   int to_parent[2] = { INVALID_FD, INVALID_FD };

   *in = NULL;
   *out = NULL;

   if (command == NULL || in == NULL || out == NULL) {
       errno = EINVAL;
       goto bail;
   }

   if (pipe(to_child) < 0) {
       goto bail;
   }

   if (pipe(to_parent) < 0) {
       goto bail;
   }

   const pid_t pid = fork();
   if (pid < 0) {
       goto bail;
   }

   if (pid == 0) { // Child
       if (dup2(to_child[READ_END], STDIN_FILENO) < 0) {
           perror("dup2");
           exit(1);
       }
       close(to_child[READ_END]);
       close(to_child[WRITE_END]);

       if (dup2(to_parent[WRITE_END], STDOUT_FILENO) < 0) {
           perror("dup2");
           exit(1);
       }
       close(to_parent[READ_END]);
       close(to_parent[WRITE_END]);

       execlp(command, command, NULL);
       perror("execlp");
       exit(1);
   }

   // Parent
   close(to_child[READ_END]);
   to_child[READ_END] = INVALID_FD;

   close(to_parent[WRITE_END]);
   to_parent[WRITE_END] = INVALID_FD;

   *in = fdopen(to_parent[READ_END], "r");
   if (*in == NULL) {
       goto bail;
   }
   to_parent[READ_END] = INVALID_FD;

   *out = fdopen(to_child[WRITE_END], "w");
   if (*out == NULL) {
       goto bail;
   }
   to_child[WRITE_END] = INVALID_FD;

   setvbuf(*out, NULL, _IONBF, BUFSIZ);

   return pid;

bail:
   ; // Goto label must be a statement, this is an empty statement
   const int old_errno = errno;

   if (*in != NULL) {
       fclose(*in);
   }

   if (*out != NULL) {
       fclose(*out);
   }

   for (int i = 0; i < 2; ++i) {
       if (to_child[i] != INVALID_FD) {
           close(to_child[i]);
       }
       if (to_parent[i] != INVALID_FD) {
           close(to_parent[i]);
       }
   }

   errno = old_errno;
   return -1;
}

int main(void)
{
   FILE* in = NULL;
   FILE* out = NULL;
   char* line = NULL;
   size_t size = 0;

   const int pid = bi_popen("/bin/bash", &in, &out);
   if (pid < 0) {
       perror("bi_popen");
       return 1;
   }

   fprintf(out, "ls -l a.out\n");
   getline(&line, &size, in);
   printf("-> %s", line);

   fprintf(out, "pwd\n");
   getline(&line, &size, in);
   printf("-> %s", line);

   fprintf(out, "date\n");
   getline(&line, &size, in);
   printf("-> %s", line);

   // Since in this case we can tell the child to terminate, we'll do so
   // and wait for it to terminate before we close down.
   fprintf(out, "exit\n");
   waitpid(pid, NULL, 0);

   fclose(in);
   fclose(out);

   return 0;
}

In the program, I have defined a function bi_popen. The function take as input the path of a program to run as well as two FILE*: in for input from the command and out for output to the command.

bi_popen sets up two pipes, one for communicating from the parent process to the child process, and another for communicating from the child process to the parent process.

Next, bi_popen forks, creating a new process. The child process connects its standard output to the write-end of the pipe to the parent, and connects its standard input to the read-end of the pipe from the parent. It then cleans up the old pipe file descriptors and uses execlp to replace the running process with the given command. That new program inherits the standard input/output configuration with the pipes. On success, execlp never returns.

In the case of the parent — when fork returns a non-zero value —, the parent process closes the unnecessary ends of the pipe, and uses fdopen to create FILE* associated with the relevant pipe file descriptors. It updates the in and out output parameters with those values. Finally, it uses execlp on the output FILE* to make it unbuffered (so that you don’t have to explicitly flush content you send to the child process).

The main function is an example of how to use the bi_popen function. It calls bi_popen with the command as /bin/bash. Anything written to the out stream is sent to bash to execute. Anything bash prints to standard output is available for reading from in.

Here’s an example run of the program:

$ ./a.out
-> -rwxr-xr-x 1 user group 20400 Aug 29 17:09 a.out
-> /home/user/src/bidirecitonal_popen
-> Sat Aug 29 05:10:52 PM EDT 2020

Note that main writes a series of commands to the child process (here, bash), and the command responded with the expected output.

In your case, you could replace “/bin/bash” with “stockfish”, then use out to write commands to stockfish and in to read the responses.`

引用自:https://unix.stackexchange.com/questions/606861