使用Linux已经有不少年头,也使用过不少Linux的SSH工具,比如SecureCRT,XShell,Putty,SmartTTY,但都未发现有一个工具可以像Windows资源管理器一样操作Linux下的文件的工具,SecureCRT的同门软件SecureFX有那么点感觉,浏览目录、文件结构还是挺不错的,但是在打开、编辑文件上还是不太流畅。
于是就想着如果自己能搞一个出来应该会很不错。那首先得使用SSH协议连接上服务器,网上查了一下,发现libSSH2还不错,文档,示例这些都有,但是没有完整的命令行交互示例。在example中有一个ssh2.c的示例给出了一个框架:
/* Open a SHELL on that pty */
if (libssh2_channel_shell(channel))
{
fprintf(stderr, "Unable to request shell on allocated pty\n");
goto shutdown;
}
/* At this point the shell can be interacted with using
* libssh2_channel_read()
* libssh2_channel_read_stderr()
* libssh2_channel_write()
* libssh2_channel_write_stderr()
*
* Blocking mode may be (en|dis)abled with: libssh2_channel_set_blocking()
* If the server send EOF, libssh2_channel_eof() will return non-0
* To send EOF to the server use: libssh2_channel_send_eof()
* A channel can be closed with: libssh2_channel_close()
* A channel can be freed with: libssh2_channel_free()
*/
skip_shell:
if (channel)
{
libssh2_channel_free(channel);
channel = NULL;
}
/* Other channel types are supported via:
* libssh2_scp_send()
* libssh2_scp_recv2()
* libssh2_channel_direct_tcpip()
*/
这个框架只是以注释的形式给出了一些API,说在那个位置可以使用这些API来实现与Shell的交互。
但是如果只是单独使用这些API可能只能进行一个回合的交互,达不到一个完整的像见的ssh工具那样输入一个命令后,显示结果,再等待输入下一个命令的效果。
笔者结合另一个示例ssh2_echo.c,添加使用libssh2_poll来监测channel的读写,实现了简单的命令行交互(不支持像top这样可以刷新的命令):
enum state
{
INCOMPLETED,
COMPLETED,
TIMEOUT
};
ssize_t handle_read(LIBSSH2_CHANNEL *channel, char *buffer, size_t buf_size, enum state *state, int timeout)
{
LIBSSH2_POLLFD fds;
fds.type = LIBSSH2_POLLFD_CHANNEL;
fds.fd.channel = channel;
fds.events = LIBSSH2_POLLFD_POLLIN | LIBSSH2_POLLFD_POLLOUT;
ssize_t read_size = 0;
while (timeout > 0)
{
int rc = (libssh2_poll(&fds, 1, 10));
if (rc < 1)
{
timeout -= 10;
usleep(10000);
continue;
}
if (fds.revents & LIBSSH2_POLLFD_POLLIN)
{
int n = libssh2_channel_read(channel, &buffer[read_size], buf_size - read_size);
if (n == LIBSSH2_ERROR_EAGAIN)
{
continue;
}
else if (n < 0)
{
*state = COMPLETED;
return read_size;
}
else
{
read_size += n;
if (libssh2_channel_eof(channel))
{
*state = COMPLETED;
return read_size;
}
char end = buffer[read_size - 2];
if (end == '$' || end == '#')
{
*state = COMPLETED;
return read_size;
}
}
if (read_size == buf_size)
{
*state = INCOMPLETED;
return read_size;
}
}
usleep(10000);
timeout -= 10;
}
*state = TIMEOUT;
return 0;
}
void handle_loop(LIBSSH2_CHANNEL *channel)
{
char buffer[8192];
char cmd[64];
int len = 0;
ssize_t n;
while (1)
{
enum state state = INCOMPLETED;
do
{
n = handle_read(channel, buffer, sizeof(buffer) - 1, &state, 3000);
if (state == TIMEOUT)
{
if (len > 0)
{
cmd[len - 1] = 0;
}
printf("exec cmd:`%s` timeout\n", cmd);
break;
}
buffer[n] = 0;
if (len > 0)
{
printf("%s", &buffer[len + 1]);
len = 0;
}
else
{
printf("%s", buffer);
}
} while (state == INCOMPLETED);
fgets(cmd, sizeof(cmd), stdin);
len = strlen(cmd);
libssh2_channel_write(channel, cmd, len);
}
libssh2_channel_close(channel);
}
只需要skip_shell前面调用handle_loop即可。这里还实现了检测读取超时和读取Buffer是否写满,如果写满了则输出了再读取。
这里需要注意libssh2_poll接口,在第一次调用时,一般不会有LIBSSH2_POLLFD_POLLIN事件,需要再次调用才会有,跟了一下代码,第一次调用时在接收数据,准备好数据后,但并没有设置LIBSSH2_POLLFD_POLLIN事件,所以需要循环读取,libssh2_poll的超时参数有的情况不起作用,参见代码:
if(active_fds) {
/* Don't block on the sockets if we have channels/listeners which
are ready */
timeout_remaining = 0;
}
注意需要在支持颜色的Shell终端中执行,否则会有各种颜色标识输出,会干扰正常信息。 可以在VSCode终端、MinGW终端中执行。
以上代码在MinGW64下测试通过。