65.9K
CodeProject 正在变化。 阅读更多。
Home

为 RDesktop 编写插件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.70/5 (13投票s)

2009年12月1日

CPOL

6分钟阅读

viewsIcon

34186

本文主要面向 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 运行的,并且它的 stdoutstdin 已连接到虚拟通道的输入和输出。因此,插件只需运行一个无限循环,在该循环中它从 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 日:初始发布
© . All rights reserved.