编程 - 与国际象棋引擎 stockfish / FIFO / Bash 重定向进行通信

编程 - 与国际象棋引擎 stockfish / FIFO / Bash 重定向进行通信

我正在尝试编写一个小国际象棋程序 - 实际上更多的是国际象棋 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 脚本在一个终端终止(而不是连续运行)鳕鱼输出 FIFO。或者我可以只从 stockfish 输出 FIFO 读取一行输出。如果有更简单的方法通过 STDIN 和 STDOUT 与 stockfish 进行 IPC,我也可以尝试。谢谢。

答案1

既然您已经在使用 C 语言,那么我建议您stockchess也使用 C 语言进行管理。有一个库函数,popen()它会给你一个单向管道到一个进程——不适合您的用例。不过,您可以自行设置。

考虑以下示例程序:

#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;
}

在程序中,我定义了一个函数bi_popen。该函数将要运行的程序的路径作为输入以及两个FILE*in用于输入命令和out输出命令。

bi_popen设置两个管道,一个用于从父进程到子进程通信,另一个用于从子进程到父进程通信。

接下来bi_popen fork,创建一个新流程。子进程将其标准输出连接到父级管道的写入端,并将其标准输入连接到父级管道的读取端。然后它会清理旧的管道文件描述符,并用execlp给定的命令替换正在运行的进程。该新程序继承了管道的标准输入/输出配置。成功之后,execlp一去不复返。

在父进程的情况下---当fork返回非零值时---,父进程关闭管道不必要的末端,并使用fdopen创建FILE*与相关管道相关的文件描述符。它用这些值更新in和输出参数。out最后,它使用execlp输出FILE*使其不缓冲(这样您就不必显式刷新发送到子进程的内容)。

main函数是如何使用该函数的示例bi_popen。它bi_popen使用命令 as进行调用/bin/bash。写入out流的任何内容都会发送到 bash 来执行。 bash 打印到标准输出的任何内容都可以从中读取in

下面是该程序的运行示例:

$ ./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

请注意,main向子进程写入一系列命令(此处为bash),并且该命令以预期的输出进行响应。

在您的情况下,您可以将“/bin/bash”替换为“stockfish”,然后用于out写入命令stockfishin读取响应。

相关内容