背景
Web端打印功能需求,一开始使用Lodop插件同用户本地的打印机做交互,它的功能很强大也很齐全。但还是有很多需求无法覆盖到,比如文字旋转(转图片方式旋转有清晰度问题)、标价签之类的精确打印、打印性能等方面无法满足或者有缺陷。因此需要自行开发一个客户端打印插件服务,方案很简单,服务端生成打印结果的pdf文件,客户端打印插件服务下载pdf文件并调用用户本地打印机进行打印。打印插件服务使用Spring Boot开发并提供获取本地打印机列表、调用打印机等接口服务,由此引申出本文,如何让用户便捷安装该插件服务,并提供用户电脑开机自启动功能。
开发环境说明
操作系统:windows 10
Java开发环境:32位jdk1.8(为了Spring Boot程序能兼容XP)
Python开发环境:Python3.6.5+wxPython4.0.6+PyInstaller3.5
C语言开发环境:VC++6.0(为了制作启动程序exe能够兼容XP)
安装程序制作工具:Inno Setup 5.6.1 -unicode(6.0版本不支持XP,必须下载Unicode版本才能在安装界面支持中文)
MT.exe:为启动程序.exe注入管理员许可证权限
exescope.exe:查看exe程序支持的windows操作系统版本。
解决方案
方案一
实现过程
1、 使用 winsw.exe 将打印插件服务的jar包做成windows服务,并设置为自启动。
winsw.exe 2.3 下载地址
-
新建和exe同名的xml配置文件winsw.xml放到同一目录下,配置说明>>
GemBox GemBox GemBox打印插件服务 .\jre\bin\javaw.exe Automatic -jar ".\lib\gem-box-service.jar -" "-spring.config.location=classpath:/application.yml,./conf/application.properties" reset -
打开当前目录执行以下dos命令
# 安装服务 > winsw.exe install # 启动服务(虽然是自启动模式,但安装后并不会自启动) > net start GemBox # 卸载服务 > winsw.exe uninstall
存在缺陷
Web服务虽然正常启动,但是由于该jar需要同用户本地打印机交互,win10系统下调用打印相关接口无反应,经排查发现是用户权限问题。制作的自启动的windows服务默认是以system用户启动,然而它拿不到同打印机交互的权限,必须以管理员用户模式启动打印插件服务。(虽然,winsw.exe也可以安装为指定用户的自启动服务,但得配置好用户名和密码,无法给不同用户使用)。
能否兼容其他操作系统:XP/win7/vista 未知,没验证过。
方案二
实现过程
为解决以本地用户账户启动插件服务问题,通过观察日常使用的软件程序发现有些软件会在windows的任务计划程序中自动创建一个定时任务,通过定时任务来触发程序的启动。立马开洞脑筋,搜一下如何使用dos命令创建任务计划—> schtasks.exe。
-
使用bat脚本,提供安装、卸载、启动、停止服务脚本。
-
安装脚本 install.bat
@echo off ::进入脚本所在目录 cd /D %~dp0 ::schtasks /Create /tn GemBox /tr %cd%\gembox.bat /sc ONLOGON /RL HIGHEST ::schtasks /Delete /tn testschtask ::先删除,再新建任务计划。 schtasks /Delete /tn GemBox /F ::XP系统不支持 /RL 参数,且程序目录需要双引号 ::用户登录后执行指定的bat脚本,%cd%指向执行脚本的目录。 schtasks /Create /tn GemBox /tr "%cd%\gembox.bat" /sc ONLOGON @echo 'install successfully!' pause
- 启动脚本 gembox.bat
@echo off ::进入脚本所在目录 cd /D %~dp0 ::此处把jre/bin中的javaw.exe重命名为GemBox.exe,为了方便后面按名称杀进程 start .\jre\bin\GemBox -jar .\lib\gem-box-service.jar --spring.config.location=classpath:/application.yml,./conf/application.properties exit
-
停止脚本 stop.bat
@echo off ::强制杀掉指定程序 taskkill /im GemBox.exe /f exit
-
卸载脚本 uninstall.bat
@echo off cd /D %~dp0 taskkill /im GemBox.exe /f schtasks /Delete /tn GemBox /F @echo 'uninstall successfully!' pause
- 执行脚本时,必须以管理员模式运行,否则任务计划创建、删除等无效。
-
存在缺陷
- win7及以上操作系统,需要以管理员模式执行脚本,可能有方案可以让bat脚本默认以管理员模式运行,暂时没去研究。
- 需要用户自己执行bat脚本,加大了沟通成本,而且看起来很low。
- 安装创建的任务计划程序,用户登录时会有dos窗口闪过。暂时没找到好方法隐藏。
方案三
实现过程
-
使用Python 3.6.5+wxPython编写安装、启动、卸载用的exe程序。
通过Python代码调用方案二中的相关dos命令来实现安装、启动程序。
使用PyInstaller将Python脚本编译为exe程序。
-
为了复用,将相关dos命令配置成cmd.json文件,只需一个对应的Python脚本即可创建对应的安装、启动、卸载程序。具体代码不在此处详述。
难点1:win7/win10需要以管理员模式运行,通过ctypes库来处理。
难点2:使用subprocess.run来执行dos命令,达到隐藏dos窗口效果。
-
难点3:取得exe启动程序所在的目录传给dos命令,相关jar包配置文件路径需要指定绝对路径,否则任务计划的自启动程序触发时默认指向的是C:\windows\system32等错误目录,使用os和sys库
# 获取exe执行程序所在目录 os.path.dirname(os.path.realpath(sys.argv[0]))
-
使用Inno Setup包装制品
配置使用说明详见>>
5.6.1-unicode下载地址
5.6.1 中文语言包下载地址,下载后确保文件格式为ANSI编码格式,否则编译报错,同时拷贝文件到Inno Setup安装目录/Languages下,这样创建模板时即可在语言支持列中看到简体中文。
打包后的安装程序在XP下执行报错,6.X 版本以后不再支持XP及以下版本操作系统,最低支持Vista系统(通过exescope.exe即可看到编译出来的setup.exe的操作系统主版本为6副版本为0),没奈何只好装回5.X版本。
5.X 版本部分变量同6.X不一样,比如执行安装程序时的默认安装目录5.X变量定义为{pf},6.X为{autopf}具体详见各个版本的变量说明。
存在缺陷
- PyInstaller编译出来的exe制品太大,10多兆,哪怕内部只是调用了几行dos命令。(加载一些额外的辅助库占用了较大空间,比如wxPython)
- 没使用Inno Setup打包前,直接放到XP中启动程序提示不是有效的win32应用程序,经Google发现我本地装的PyInstaller 3.X 版本不支持XP,只能降低版本,降低版本后发现没法使用,必须基于32位的Python库,我本地装的是64位,实在没功夫重头折腾一遍开发环境,毕竟Python也只是很早以前玩一玩遗留下来的。
方案四(推荐)
实现过程
-
基于方案三,将Python实现exe启动程序改为使用C语言实现,创建一个windows Application工程。
难点1:从Hello World开始重新复习。
-
难点2:如何获取exe执行程序所在目录,赋值给dos命令。
//获取exe执行程序所在目录 TCHAR _szPath[MAX_PATH + 1]={0}; GetModuleFileName(NULL, _szPath, MAX_PATH); (strrchr(_szPath, '\\'))[1] = 0;//删除文件名,只获得路径 字串
-
难点3:如何隐藏dos命令窗口。
#include
#include #include //隐藏dos命令窗口相关代码 #pragma comment(linker, "/subsystem:windows/entry:mainCRTStartup") #include using namespace std; ... int main(){ //隐藏dos命令窗口相关代码 //最优解,注意ShellExecute是异步的,如果要做成同步挺麻烦的,有鉴于C语言功力有限,先简单来。去掉了先杀插件服务进程的逻辑,否则会误杀刚启动的进程。 ShellExecute(0, "open", "cmd.exe",cmdStr.c_str(), 0, SW_HIDE); //废弃方案:如果执行失败会一直重试,而且dos命令窗口一直在。 //WinExec("cmd /c taskkill /im GemBox.exe /f >start.txt",SW_HIDE); //WinExec("start .\\jre\\bin\\GemBox -jar .\\lib\\gem-box-service.jar --spring.config.location=classpath:/application.yml,./conf/application.properties",SW_HIDE); //废弃方案:虽然是同步运行的,但是dos命令窗口无法隐藏,有人说使用start /B 参数,试了没效果。 //system("taskkill /im GemBox.exe /f"); //system("start .\\jre\\bin\\GemBox -jar \".\\lib\\gem-box-service.jar\" \"--spring.config.location=classpath:/application.yml,./conf/application.properties\""); return 0; } - 难点4:如何给exe程序添加图标,通过给C代码源工程添加XX.rc文件导入ico格式图标,重新编译即可。(一搜一大把,此处不列出具体操作步骤)
-
为在win7/win10/vista中以管理员运行gembox.exe启动程序,利用MT.exe注入manifest信息。
-
mt.exe下载地址:https://github.com/eladkarako/mt/blob/master/x64/mt.exe
文件命名为gembox.exe.manifest,配置如下: -
gembox.exe所在目录下执行以下dos命令(win7/win10系统,执行结束后即可发现gembox.exe图标上带了小盾牌):
mt.exe -manifest "gembox.manifest" -outputresource:"gembox.exe"
-
使用Inno Setup打包以上制品,任务计划的创建、服务进程的关闭、卸载等dos命令都在Inno Setup中设置。
-
最终的Inno Setup配置文件内容,供大家参考(含自定义代码)
; Script generated by the Inno Setup Script Wizard. ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! [Setup] PrivilegesRequired=admin ; NOTE: The value of AppId uniquely identifies this application. ; Do not use the same AppId value in installers for other applications. ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) AppId={{9CE6C1B6-6029-48B5-B332-0820678F7629} AppName=GemBox AppVersion=1.0 ;AppVerName=GemBox 1.0 AppPublisher=Shanghai Heading AppPublisherURL=http://www.hd123.com AppSupportURL=http://www.hd123.com AppUpdatesURL=http://www.hd123.com DefaultDirName={pf}\GemBox DefaultGroupName=GemBox DisableProgramGroupPage=yes OutputDir=F:\test OutputBaseFilename=GemBoxSetup Compression=lzma SolidCompression=yes ;安装程序图标 SetupIconFile=F:\test\print.ico ;是否生成卸载程序 Uninstallable=yes ;设置控制面板卸载程序列表中的图标及名称 ;名称 UninstallDisplayName=GemBox ;图标 ;UninstallDisplayIcon=F:\test\uninstall.ico [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl" [Files] Source: "F:\GemBox\gembox.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "F:\GemBox\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs ; NOTE: Don't use "Flags: ignoreversion" on any shared system files [Icons] Name: "{group}\GemBox"; Filename: "{app}\gembox.exe" [Run] Filename: "taskkill.exe"; Parameters:"/im GemBox.exe /F";Flags:runhidden Filename: "{app}\gembox.exe"; Flags: nowait runhidden ;添加任务计划程序,先删再新增 Filename: "schtasks.exe"; Parameters:"/Delete /tn GemBox /F";Flags:runhidden ;RL HIGHEST XP下不支持 Filename: "schtasks.exe"; Parameters:"/Create /tn GemBox /tr ""{\}""{app}{\}gembox.exe{\}"""" /sc ONLOGON";Flags:waituntilidle ;大于XP的版本 ;Filename: "schtasks.exe"; Parameters:"/Create /tn GemBox /tr ""'{app}\gembox.exe'"" /sc ONLOGON /RL HIGHEST";Flags:nowait runhidden [Code] //Exec(ExpandConstant("{cmd}"), "/c dir c:\ >a.txt",ExpandConstant("{app}"), SW_SHOWNORMAL, ewNoWait, ResultCode) ///停止服务 procedure stopService; var ResultCode: Integer; begin Exec(ExpandConstant('{cmd}'), '/c taskkill /im GemBox.exe /f', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); end; function Uninstall():Boolean; var ResultCode : Integer; begin Exec(ExpandConstant('{cmd}'), '/c schtasks.exe /Delete /tn GemBox /F', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); Result :=true; end; function InitializeSetup(): Boolean; begin stopService; Result := true; end; // 卸载前检查关闭**进程 procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); begin if CurUninstallStep = usUninstall then begin stopService; Uninstall(); //删除文件夹 //DeleteFile(ExpandConstant('{app}')); DelTree(ExpandConstant('{app}'),true,true,true); end; end;
存在缺陷
- 总体效果还不错,启动程序制品也才几百k,就是使用了C,一会儿Java一会儿C语言,后续维护挺麻烦的,还好启动程序一般轻易不会动,一次编译所有windows操作系统皆可兼容运行。