使用Linux已经有不少年头,也使用过不少Linux的SSH工具,比如SecureCRT,XShell,Putty,SmartTTY,但都未发现有一个工具可以像Windows资源管理器一样操作Linux下的文件的工具,SecureCRT的同门软件SecureFX有那么点感觉,浏览目录、文件结构还是挺不错的,但是在打开、编辑文件上还是不太流畅。

于是就想着如果自己能搞一个出来应该会很不错。那首先得使用SSH协议连接上服务器,网上查了一下,发现libSSH2还不错,文档,示例这些都有,但是没有完整的命令行交互示例。在example中有一个ssh2.c的示例给出了一个框架:

 1    /* Open a SHELL on that pty */
 2    if (libssh2_channel_shell(channel))
 3    {
 4        fprintf(stderr, "Unable to request shell on allocated pty\n");
 5        goto shutdown;
 6    }
 7
 8    /* At this point the shell can be interacted with using
 9     * libssh2_channel_read()
10     * libssh2_channel_read_stderr()
11     * libssh2_channel_write()
12     * libssh2_channel_write_stderr()
13     *
14     * Blocking mode may be (en|dis)abled with: libssh2_channel_set_blocking()
15     * If the server send EOF, libssh2_channel_eof() will return non-0
16     * To send EOF to the server use: libssh2_channel_send_eof()
17     * A channel can be closed with: libssh2_channel_close()
18     * A channel can be freed with: libssh2_channel_free()
19     */
20
21skip_shell:
22    if (channel)
23    {
24        libssh2_channel_free(channel);
25        channel = NULL;
26    }
27
28    /* Other channel types are supported via:
29     * libssh2_scp_send()
30     * libssh2_scp_recv2()
31     * libssh2_channel_direct_tcpip()
32     */

这个框架只是以注释的形式给出了一些API,说在那个位置可以使用这些API来实现与Shell的交互。 但是如果只是单独使用这些API可能只能进行一个回合的交互,达不到一个完整的像见的ssh工具那样输入一个命令后,显示结果,再等待输入下一个命令的效果。

笔者结合另一个示例ssh2_echo.c,添加使用libssh2_poll来监测channel的读写,实现了简单的命令行交互(不支持像top这样可以刷新的命令):

  1enum state
  2{
  3    INCOMPLETED,
  4    COMPLETED,
  5    TIMEOUT
  6};
  7
  8ssize_t handle_read(LIBSSH2_CHANNEL *channel, char *buffer, size_t buf_size, enum state *state, int timeout)
  9{
 10    LIBSSH2_POLLFD fds;
 11    fds.type = LIBSSH2_POLLFD_CHANNEL;
 12    fds.fd.channel = channel;
 13    fds.events = LIBSSH2_POLLFD_POLLIN | LIBSSH2_POLLFD_POLLOUT;
 14
 15    ssize_t read_size = 0;
 16    while (timeout > 0)
 17    {
 18        int rc = (libssh2_poll(&fds, 1, 10));
 19        if (rc < 1)
 20        {
 21            timeout -= 10;
 22            usleep(10000);
 23            continue;
 24        }
 25
 26        if (fds.revents & LIBSSH2_POLLFD_POLLIN)
 27        {
 28            int n = libssh2_channel_read(channel, &buffer[read_size], buf_size - read_size);
 29            if (n == LIBSSH2_ERROR_EAGAIN)
 30            {
 31                continue;
 32            }
 33            else if (n < 0)
 34            {
 35                *state = COMPLETED;
 36                return read_size;
 37            }
 38            else
 39            {
 40                read_size += n;
 41                if (libssh2_channel_eof(channel))
 42                {
 43                    *state = COMPLETED;
 44                    return read_size;
 45                }
 46                char end = buffer[read_size - 2];
 47                if (end == '$' || end == '#')
 48                {
 49                    *state = COMPLETED;
 50                    return read_size;
 51                }
 52            }
 53            if (read_size == buf_size)
 54            {
 55                *state = INCOMPLETED;
 56                return read_size;
 57            }
 58        }
 59        usleep(10000);
 60        timeout -= 10;
 61    }
 62
 63    *state = TIMEOUT;
 64    return 0;
 65}
 66
 67void handle_loop(LIBSSH2_CHANNEL *channel)
 68{
 69    char buffer[8192];
 70    char cmd[64];
 71    int len = 0;
 72    ssize_t n;
 73    while (1)
 74    {
 75        enum state state = INCOMPLETED;
 76        do
 77        {
 78            n = handle_read(channel, buffer, sizeof(buffer) - 1, &state, 3000);
 79            if (state == TIMEOUT)
 80            {
 81                if (len > 0)
 82                {
 83                    cmd[len - 1] = 0;
 84                }
 85                printf("exec cmd:`%s` timeout\n", cmd);
 86                break;
 87            }
 88            buffer[n] = 0;
 89            if (len > 0)
 90            {
 91                printf("%s", &buffer[len + 1]);
 92                len = 0;
 93            }
 94            else
 95            {
 96                printf("%s", buffer);
 97            }
 98        } while (state == INCOMPLETED);
 99
100        fgets(cmd, sizeof(cmd), stdin);
101        len = strlen(cmd);
102        libssh2_channel_write(channel, cmd, len);
103    }
104    libssh2_channel_close(channel);
105}

只需要skip_shell前面调用handle_loop即可。这里还实现了检测读取超时和读取Buffer是否写满,如果写满了则输出了再读取。

这里需要注意libssh2_poll接口,在第一次调用时,一般不会有LIBSSH2_POLLFD_POLLIN事件,需要再次调用才会有,跟了一下代码,第一次调用时在接收数据,准备好数据后,但并没有设置LIBSSH2_POLLFD_POLLIN事件,所以需要循环读取,libssh2_poll的超时参数有的情况不起作用,参见代码:

1if(active_fds) {
2            /* Don't block on the sockets if we have channels/listeners which
3               are ready */
4            timeout_remaining = 0;
5        }

注意需要在支持颜色的Shell终端中执行,否则会有各种颜色标识输出,会干扰正常信息。 可以在VSCode终端、MinGW终端中执行。

以上代码在MinGW64下测试通过。