为 RDesktop 编写插件






4.70/5 (13投票s)
本文主要面向 Linux 开发者。文章介绍了一种为开源软件编写进程外插件的方法。
目录
关于本文:何时值得阅读
本文主要面向 Linux 开发者。文章介绍了一种为开源软件编写进程外插件的方法——即,插件作为软件的一部分运行,但在另一个进程中,这样它们的代码就可以保持闭源。
通常没有必要使用本文介绍的方法。Rdesktop 是自由软件,您可以随时修改其源代码以满足您的需求。然而,这意味着您也必须公开您的代码,因为 GPL 许可证要求如此。如果您不想这样做,请继续阅读,您将学会如何规避 GPL 要求,编写一个闭源但能作为 Rdesktop 代码一部分运行的插件。
您也可以阅读本文,以了解有关以下内容的有趣信息:
- RDP 协议
- Rdesktop – 一款开源 RDP 客户端
- 在 *nix 系统上进行进程间通信的一种简单方法。
让我们开始吧。
Rdesktop。RDP 协议
这适用于那些不知道这是什么的人。远程桌面协议 (RDP) 是微软开发的一项专有协议,用于为用户提供对另一台计算机的图形界面。这非常方便——您可以坐在自己的桌子前,几乎无差别地使用另一台计算机,操作起来就像在本地一样(当然,前提是您有良好的网络连接——高速、低延迟等)。几乎所有操作系统都有 RDP 客户端——Windows、Linux、Mac OS,它们都使用 RDP 协议连接到 RDP 服务器——您想要操作的远程主机。2008 年,微软公开了 RDP 规范,现在可以在其网站上找到:http://msdn.microsoft.com/en-us/library/cc216513(PROT.10).aspx。
Rdesktop 是 Windows 终端服务的一个开源客户端。它目前运行在大多数基于 X Window 系统的 UNIX 平台。它支持大多数基本的 RDP 协议功能以及许多协议扩展,包括音频重定向、剪贴板、本地文件系统和本地设备重定向。Rdesktop 在 GNU 公共许可证 (GPL) 下发布。
与其他许多 *nix 程序一样,Rdesktop 是一个命令行应用程序。它具有许多不同的输入参数来配置远程会话——用户凭据、服务器地址、桌面尺寸、颜色深度;以及要重定向到远程计算机的本地设备的描述——串行端口、打印机、声音、磁盘。
在解析命令行并启动与远程主机的会话后,rdesktop 进入一个无限循环,在该循环中它读取传入数据(即服务器发送的数据)并发送一些数据作为响应。与服务器的连接是通过所谓的虚拟通道建立的。微软有规定
“虚拟通道 是软件扩展,可用于为远程桌面服务应用程序添加功能增强。功能增强的示例可能包括:支持特殊类型的硬件、音频或其他对远程桌面服务 远程桌面协议 (RDP) 提供的核心功能进行的补充。RDP 协议提供对多个虚拟通道的多路复用管理。
虚拟通道应用程序包含两部分:客户端组件和服务器端组件。服务器端组件是在远程桌面会话主机 (RD Session Host) 服务器上运行的可执行程序。客户端组件是一个 DLL,在远程桌面连接 (RDC) 客户端程序运行时必须加载到客户端计算机的内存中。
虚拟通道可以独立于 RDP 协议为远程桌面连接 (RDC) 客户端添加功能增强。通过虚拟通道支持,可以在不更新客户端或服务器软件或 RDP 协议的情况下添加新功能。”
因此,虚拟通道只是两个端点——例如客户端和服务器程序——可以独立于底层协议相互连接的一种方式。在远程桌面服务中,协议是 RDP,但也可以是任何其他提供类似功能的协议。
为 rdesktop 编写插件:使用 OOP Patch
如上所述,您可以随时修改 rdesktop 源代码来添加或更改所需的功能。但还有另一种方法——如果您不想将修改后的代码公开给所有人,您可以创建一个单独的程序作为 rdesktop 的附加组件。
有一个补丁使得 rdesktop 中的虚拟通道功能对第三方可见——即,使得能够创建和使用由单独程序实现的附加虚拟通道。该补丁主要由 Simon Guerrero 编写。您可以在此处获取该补丁:Sourceforge.net(页面上的详细描述已过时——它涉及旧版本的补丁,并未反映其当前状态。它只是无用的)。应用此补丁后,rdesktop 会获得一个额外的“重定向 (-r)”参数——“-r addin”。参数的完整格式为
-r addin:<channelname>:</path/to/executable>[:arg1[:arg2:]...]
其中
<channelname> - name of the desired virtual channel;
</path/to/executable> - path to the VC handler;
[:arg1[:arg2:]...] - optional parameters passed to the handler by rdesktop.
关于 VC 处理程序的一些话。当 rdesktop 创建处理程序进程和相应的虚拟通道时,它会将 VC 输出连接到进程的标准输入 (stdin
),反之亦然——其标准输出 (stdout
) 连接到 VC 的输入。可选参数作为命令行参数传递给进程。因此,处理程序要做的就是从 stdin 读取传入的 VC 数据,并将传出的 VC 数据写入 stdout。非常简单明了的方案;下面将更详细地介绍。
从原理到代码
好的,这就是它的工作原理。代码不那么优雅,但它能工作并完成需要的工作。那么,让我们来看看吧。
客户端部分:插件
当插件启动时,它知道它是由 rdesktop 运行的,并且它的 stdout
和 stdin
已连接到虚拟通道的输入和输出。因此,插件只需运行一个无限循环,在该循环中它从 VC 读取数据,并在需要时发送一些数据作为响应。它还设置一个 SIGUSR1
信号处理程序,以便 rdesktop 可以正确终止插件任务。当 rdesktop 断开与远程计算机的连接时,它将向所有插件发送 SIGUSR1
,插件在接收到 SIGUSR1
时应停止工作。
static int g_end_flag = 0;
// rdesktop sends us a close event by sending sigusr1
void sigusr1_handler(int signum)
{
g_end_flag = 1;
}
// we're launched by rdesktop with the following parameters:
// 1) our ends of read and write pipes that connects us to rdesktop
// are passed as stdin and stdout;
// 2) all parameters are passed via argv[]
int main(int argc, char **argv)
{
char *data = NULL;
unsigned long datalen = 0;
int pipe_to_read = -1;
int pipe_to_write = -1;
int i;
// set up the SIGUSR1 handler
struct sigaction sa;
sa.sa_handler = sigusr1_handler;
sigaction(SIGUSR1, &sa, NULL);
pipe_to_write = dup(STDOUT_FILENO);
pipe_to_read = dup(STDIN_FILENO);
while (!g_end_flag)
{
ssize_t bytes_read = read(pipe_to_read, &datalen, sizeof(unsigned long));
if (g_end_flag)
break;
if (bytes_read <= 0)
{
perror("pipe read");
break;
}
data = malloc (datalen);
ssize_t all_read = 0;
do
{
bytes_read = read(pipe_to_read, data + all_read, datalen - all_read);
all_read += bytes_read;
}
while (bytes_read > 0 && all_read < datalen && !g_end_flag);
// just send the received data back
if (bytes_read > 0)
{
write(pipe_to_write, &datalen, sizeof(unsigned long));
write(pipe_to_write, data, datalen);
}
free(data);
data = NULL;
}
end:
if (data != NULL)
free(data);
close(pipe_to_read);
close(pipe_to_write);
return 0;
}
客户端部分:rdesktop
当 rdesktop 找到 '-r addin'
参数时,
case 'r':
if (str_startswith(optarg, "addin"))
{
it initializes the add-in, and add it to the list of add-ins:
init_external_addin(addin_name, addin_path, p, &addin_data[addin_count]);
if (addin_data[addin_count].pid != 0)
{
addin_count++;
}
在 addin init
函数中,rdesktop 准备 addin 参数,创建其进程并将其连接到管道。
void init_external_addin(char *addin_name, char *addin_path, char *args,
ADDIN_DATA *addin_data)
{
char *p;
char *current_arg;
char *argv[256];
char argv_buffer[256][256];
int i;
int readpipe[2],writepipe[2];
pid_t child;
/* Initialize addin structure */
memset(addin_data, 0, sizeof(ADDIN_DATA));
/* Go through the list of args, adding each one to argv */
argv[0] = addin_path;
i = 1;
p=current_arg=args;
while (current_arg != 0 && current_arg[0] != "\0")
{
p=next_arg(p, ":");;
if (p != 0 && *p != "\0")
*(p - 1) = "\0";
strcpy(argv_buffer[i], current_arg);
argv[i]=argv_buffer[i];
i++;
current_arg=p;
}
argv[i] = NULL;
/* Create pipes */
if (pipe(readpipe) < 0 || pipe(writepipe) < 0)
{
perror("pipes for addin");
return;
}
/* Fork process */
if ((child = fork()) < 0)
{
perror("fork for addin");
return;
}
/* Child */
if (child == 0)
{
/* Set stdin and stdout of child to relevant pipe ends */
dup2(writepipe[0],0);
dup2(readpipe[1],1);
/* Close all fds as they are not needed now */
close(readpipe[0]);
close(readpipe[1]);
close(writepipe[0]);
close(writepipe[1]);
execvp((char *)argv[0], (char **)argv);
perror("Error executing child");
_exit(128);
}
else
{
strcpy(addin_data->name, addin_name);
/* Close child end fd"s */
close(readpipe[1]);
close(writepipe[0]);
addin_data->pipe_read=readpipe[0];
addin_data->pipe_write=writepipe[1];
addin_data->vchannel=channel_register(addin_name,
CHANNEL_OPTION_INITIALIZED |
CHANNEL_OPTION_ENCRYPT_RDP |
CHANNEL_OPTION_COMPRESS_RDP,
addin_callback);
if (!addin_data->vchannel)
{
perror("Channel register failed");
return;
}
else
addin_data->pid=child;
}
}
每当从 addin VC 接收到数据时,都会调用 addin_callback()
函数。
/* Generic callback for delivering data to third party add-ins */
void addin_callback(STREAM s, char *name)
{
pid_t pid;
int pipe_read;
int pipe_write;
uint32 blocksize;
/* s->p is the start and s->end is the end plus 1 */
blocksize = s->end - s->p;
/* look up for the add-in by the VC name */
lookup_addin(name, &pid, &pipe_read, &pipe_write);
if (!pid)
perror("Can"t locate addin");
else
{
/* Prepend the block with the block size so that the
add-in can identify blocks */
write(pipe_write, &blocksize, sizeof(uint32));
write(pipe_write, s->p, blocksize);
}
}
通过将 addin 管道端添加到文件描述符集合中来读取 addin 的数据
/* Add the add-in pipes to the set of file descriptors */
void addin_add_fds(int *n, fd_set * rfds)
{
extern ADDIN_DATA addin_data[];
extern int addin_count;
int i;
for (i = 0; i < addin_count; i++)
{
FD_SET(addin_data[i].pipe_read, rfds);
*n = MAX(*n, addin_data[i].pipe_read);
}
}
然后使用集合进行 select()
。
select(n, &rfds, &wfds, NULL, &tv);
描述符在无限循环中进行检查。
/* Check the add-in pipes for data to write */
void addin_check_fds(fd_set * rfds)
{
extern ADDIN_DATA addin_data[];
extern int addin_count;
int i;
char buffer[1024];
ssize_t bytes_read;
STREAM s;
for (i = 0; i < addin_count; i++)
{
if (FD_ISSET(addin_data[i].pipe_read, rfds))
{
bytes_read = read(addin_data[i].pipe_read, buffer, 1024);
if (bytes_read > 0)
{
/* write to appropriate vc */
s = channel_init(addin_data[i].vchannel, bytes_read);
memcpy(s->p, buffer, bytes_read);
s->p += bytes_read;
s->end = s->p;
channel_send(s, addin_data[i].vchannel);
}
}
}
}
服务器端
在服务器端,您应该打开相应的虚拟通道并等待客户端的请求。您可以在补丁页面上找到一个示例。
结束
好了,这就是全部内容。希望这些信息有所帮助。您可以将此方法不仅用于 rdesktop,还可以用于其他程序——每次您想为某个您可以修改其代码的程序编写闭源附加组件时。
祝您好运!
历史
- 2009 年 12 月 1 日:初始发布