在我们目前的业务开发中,遇到了PPT转PDF的业务需求。如何能够无损并且快速的进行转换,是我们遇到的一个不大不小的问题。
经过各种调查研究,使用C#的COM接口直接操作微软家的Office PowerPoint,让其将PPT文件另存为PDF,是一个比较稳妥,资源消耗也比较小的好办法。
基本的代码如下图所示,很简单。
但是这是个单进程模式,转换效率跟不上,服务器利用率也不高。
怎么办?
多开几个进程不就好了!
于是,我就一口气打开了三个进程,然后……
根本跑不起来……
我在Google上找了找原因,在一个很不起眼的网页写着(原文记不清了,而且我再也没找到那个网页):
PowerPoint不像Word和Excel,PowerPoint只能单进程运行,同时只能执行一项导出指令
大概意思就是说,PowerPoint在进程内部维护了一个导出队列,同时只能有一个文件进行导出,其他的要排队,但是Word和Excel就能多进程运行。
是这样么?我开始了一次不太漫长的追寻之路。
太长不看版结论:是这样的,这项技术称为"MDI多文档窗口程序",但是Word和Excel可以通过命令行参数 "/w" 强行启动多个进程同时执行导出,而PowerPoint只能借助不同用户身份开启多个进程同时导出,详情请搜索“runas”命令。
首先,我去确定下这个“MDI”是如何工作的。
下面文字摘自网络:
多文档界面 (MDI) 允许创建在单个容器窗体中包含多个窗体的应用程序。象 Microsoft Excel 与 Microsoft Word 这样的应用程序就具有多文档界面。
MDI 应用程序允许用户同时显示多个文档,每个文档显示在它自己的窗口中。文档或子窗口被包含在父窗口中,父窗口为应用程序中所有的子窗口提供工作空间。例如:Microsoft Excel 允许创建并显示不同样式的多文档窗口。每个子窗口都被限制在 Excel 父窗口的区域之内。当最小化 Excel 时,所有的文档窗口也被最小化,只有父窗口的图标显示在任务栏中。
比较直观的展示就是:
以上两张图就是开启MDI和关闭MDI的进程展示。可以看到,当开始了MDI时,虽然打开了两个文档,但是实际上只有一个Word进程在工作。当关闭了MDI模式,每个文档都会新建一个Word进程。
微软官方的文档中关于新建Office程序的命令行参数,是这么记录的:
但是很遗憾,PowerPoint并不支持这个命令行参数,所有的PPT文档都是在一个PowerPoint进程中打开的。
官方并没有给我们一个方案,那么我们有没有其他的方案去实现单机多个PowerPoint进程呢?
我大概思考了下,有一下几个思路:
虚拟机
Docker
沙盒软件(多开软件)
虚拟机
虚拟机采用了虚拟化技术,能够很完美的模拟出一个操作环境,那么就可以完美运行office程序,但是额外的性能消耗很大,单个PowerPoint程序的内存消耗在150M-300M之间,一个虚拟机的消耗能够轻松达到这个数值的五倍之多。本来单机能够支持二十个进程,结果现在只能支持四个,差距太大,不考虑。
Docker
Docker也是虚拟化技术,能够模拟出一套完全隔离的内存空间和系统资源。经过微软官方和Docker开发组长达两年的合作,Docker推出了完美支持windows系统的的docker for windows.
与之前推出的windows版不同,Docker for windows是完全针对于windows的内核开发。可以支持Windows Server和一个比较小的Nano windows镜像。
但是很遗憾,这次我们没法用,因为Docker不支持有UI界面的程序运行。
沙盒/多开软件
这个应该也是Windows使用过程中经常会遇到的软件。尤其是玩游戏的网友,应该都用过各种游戏的多开助手。网游为了限制游戏内资源分布,把握游戏发展进程,往往会限制单机运行客户端的数量,而各种助手又能破解掉这种限制。那么我们能不能使用这种方式去开启多个PowerPoint进程呢?
事实上是可以的,我这里测试使用的是windows最著名的沙盒软件 Sandboxie
将PowerPoint添加到Sandboxie中,然后点击运行,我们就可以惊喜的发现,任务管理器中出现了两个PowerPoint的进程。
这时候,我思考的问题是:Why?
为什么没有通过虚拟化技术,也能够骗过PowerPoint的识别,开启出两个进程呢?
先让我们来看看这两个程有什么区别。我这里使用了 **ProcessExplorer **软件来查看进程的详细信息。
左边是沙盒启动的进程,右边是正常启动的进程。
可以看到,两个进程启动的文件,运行目录,命令行参数之类的基本信息都完全一样,但是有两个地方不同:
1. 父进程ID
沙盒启动的进程,父进程id是一个不存在的进程,而正常启动的进程,父进程是window桌面进程 explorer.exe。
2. 启动用户名
沙盒启动的进程,用户名是匿名用户Anonymous,而正常启动的进程,用户名就是我们当前登陆的用户名。
事出有异必有妖,那么导致两个进程独立运行的原因,肯定就因为以上两个原因中的一个。
我使用了HideTools(一款能够修改进程信息的软件),修改了正常打开的PowerPoint程序的父进程id,如下图。
那么当我又正常打开了一个PowerPoint程序,会不会产生三个独立运行的进程呢?
事实证明,并没有。
那么,我们几乎能够确定,让PowerPoint能够多进程同时执行的关键点,就在于“打开程序的用户不同”上。
其实到了这里,我们大概能明白这是怎么一回事了。作为经常在Linux下开发程序的服务器端人员,这时候才想起来这么一回事,其实是有点羞愧的。
Linux下的权限管理和进程分配使用非常广泛,服务器程序的用户一般都是www-data,php-fpm的用户是php,管理员使用root账户,开发人员使用对文件拥有只读权限的dev账户等等。不同的用户,可访问和可分配的资源都是相互独立。互不干扰的。
而同样的,windows下也有着一套用户管理和访问限制体系。
在Linux下,我们可以使用sudo切换进程的执行用户,而在windows下,我们可以使用runas命令去达到同样的效果。
RUNAS 用法:
RUNAS [/noprofile | /profile] [/env] [/savecred | /netonly] /user:<UserName> programRUNAS使用示例:
runas /noprofile /user:mymachine\administrator cmd
说明:使用本机上的Administrator管理员身份执行CMD,/noprofile为不加载该用户的配置信息。runas /profile /env /user:mydomain\admin “mmc %windir%\system32\dsa.msc”
说明:使用本机上的admin身份扫行msc控制台。 /profile为指定加载用户配置文件。 /env 表示使用当前环境。runas /env /user:user@domain.microsoft.com “notepad \”my file.txt\””
说明:使用域用户身份运行,并指定使用notepad打开my file.txt文档。实际应用实例:
@echo off
runas /user:Administrator /sa “C:\Program Files\Internet Explorer\iexplore.exe”
说明:以管理员身份运行IE浏览器。像这样,我们将命令保存为批处理后,只要在用户电脑上运行这个批处理(第一次输入管理员密码),以后用户只要双击该文件就可会以管理员身份执行命令中所指定的程序了。
那么我们只需要在服务器上预先建立几个账户,甚至可以在程序中动态创建,然后分配到一个合适权限的组(比如一个ppt用户组,只开放一个转换文件夹的读写权限),然后使用runas 命令,就能达到多进程同时执行,同时导出的效果。
还是最开始我们使用的四行代码,打包成exe文件,
然后在shell中使用runas,如下图,换几个用户多执行几次。
这里要指定命令行参数 /profile 加载用户配置文件,不然默认分配的内存是不够启动PowerPoint程序的。
然后~ BOOM!
我们成功启动了多个发布进程,并且都在正常发布。
由于runas必须手动输入密码,我们可以使用sanur等windows下的管道工具,或者指定rnas 的/savecred选项,做到免密启动。
如果有c#的编程基础,可以使用c#的Process类,指定进程启动信息StartInfo的username和password,就可以不使用这种命令行方式。