使用libSSH2实现与Linux Shell的命令行交互
使用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下测试通过。
- 原文作者:Witton
- 原文链接:https://wittonbell.github.io/posts/2023/2023-04-03-使用libSSH2实现与Linux-Shell的命令行交互/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议. 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。