NSIS插件开发步骤和总结
背景和目的:
NSIS是一个比较小众的技术,但是使用范围极广,在Windows系统中安装软件,很多软件的安装包都是使用NSIS开发的。
使用NSIS开发安装包,免不了要用点插件。这次准备搞个网络安装, intec插件可以实现下载,但是不能回传包体大小、下载进度、已下载大小,其他的插件没有符合要求的。那就自己下手去改装了。
实现基本步骤:
NSIS插件的基本原理,在NSIS插件基础代码框架,遵照NSIS的规则进行插件开发,按照插件调用规则,对dll动态链接库的调用。所以先来看下NSIS插件开发的基本步骤:
1、从NSIS官网下载一个插件下来,查看下代码结构
2、了解插件开发规则和框架中一些函数的使用方法。
3、熟悉插件调用规则,理解调用接口的参数含义。
从NSIS官网下载一个插件下来,查看下代码结构。
pluginapi.c和pluginapi.h:NSIS提供的参数操作类,主要对栈参数和用户变量进行读写操作。例如:popint/popstring和pushint/pushstring
crt.cpp:主要是字符的转换,比如char和wchar的互相转换,便于输出ansi和unicode格式编码的插件,支持不同编码格式的安装脚本,还有常用类型的转换。
nsis_tchar.h:兼容ANSI和Unicode编码的字符和类型转换操作
代码结构其实很简单,简单的插件开发常用函数也不多,很容易理解。对外的函数接口模板只有一个
extern "C"void __declspec(dllexport) __cdecl function(HWND hwndParent,int string_size,TCHAR *variables,stack_t **stacktop,extra_parameters *extra){EXDLL_INIT();}
NSIS中调用第三方库,所有的接口都是按照这个模板来写的。
接口参数说明:
hwndParent:当前调用接口的窗口句柄(即NSIS生成的安装包安装界面窗口)
string_size:用户变量个数
variables:用户变量(是不是也可以成为共享变量?),主要是一些不需要声明的变量,但是可以直接赋值,且可以在插件中读取和写入。如下所示
$0, $1, $2, $3, $4, $5, $6, $7, $8, $9, $R0, $R1, $R2, $R3, $R4, $R5, $R6, $R7, $R8, $R9
$INSTDIR,$OUTDIR, $CMDLINE, $LANGUAGE
stacktop:参数堆栈,传入的参数就被保存在这个堆栈里面,通过popint/popstring和pushint/pushstring可以进行读取和写入操作
extra:传递各种其他类型的变量、堆栈、常量、句柄等,实现回调
实现回调:
int nFunc = popint();extra->ExecuteCodeSegment( nFunc - 1, hwndParent);
一定要注意 ExecuteCodeSegment 的调用要减1
外部调用规则:
规则:插件名::插件函数 命令行 $0(共享参数) Param(自定义参数)
例如:plugindll::function /NOUNLOAD $0 Param
熟悉了基本的流程和调用规则,现在开始对intec插件进行改造。主要是添加获取下载进度数据的回调。以下是修改intec插件的get方法,实现下载进度的同步和回调:
NSIS脚本中intec调用实例:
Function DownLoadCallBack; 0-当前进度(百分比)Pop $0; 1-累计大小Pop $1; 2-已下载大小Pop $2; 3-下载速度;Pop $3; 4-剩余时间;Pop $4;更新包下载进度; 当前进度push $0SendMessage $PROGBAR ${PBM_SETPOS} $0 0 ; $PROGBAR:进度条FunctionEndFunction ExtractfuncReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows NT\CurrentVersion" "CurrentVersion"GetFunctionAddress $R9 DownLoadCallBackinetc::get /SILENT /callback $R9 "https://xxxxx/test_dir/test.7z" "D:/test/test.7z" /end;新增一个/callback命令,用来传递回调函数,在插件中会新增callback命令的解析Pop $1; 写入值($1="ok"表示下载成功)Push $1FunctionEnd
以下是插件intec.dll的主要代码,get方法是按照NSIS的接口规则实现的http/https的get请求接口。主要新增一个callback命令行参数,实现对回调方法的传递
extern "C"void __declspec(dllexport) __cdecl get(HWND hwndParent,int string_size,TCHAR *variables,stack_t **stacktop,extra_parameters *extra){HANDLE hThread;DWORD dwThreadId;MSG msg;TCHAR szUsername[64]=TEXT(""), // proxy paramsszPassword[64]=TEXT("");EXDLL_INIT();g_pluginExtra = extra;g_hwndParent = hwndParent;TCHAR* variable0 = getuservariable(INST_R0); // variable0被赋的值是NSIS代码中,获取到的系统版本数据 $R0,此处仅作为验证variables的获取//以下是对命令行的解析while(!popstring(url) && *url == TEXT('/')){//MessageBox(NULL, url, "test", MB_OK);if(lstrcmpi(url, TEXT("/silent")) == 0)silent = true;else if(lstrcmpi(url, TEXT("/weaksecurity")) == 0)g_ignorecertissues = true;else if(lstrcmpi(url, TEXT("/caption")) == 0)popstring(szCaption);else if(lstrcmpi(url, TEXT("/username")) == 0)popstring(szUsername);else if(lstrcmpi(url, TEXT("/password")) == 0)popstring(szPassword);………………此处代码省略,便于阅读else if(lstrcmpi(url, TEXT("/translate")) == 0){if(popup){popstring(szUrl);popstring(szStatus[ST_DOWNLOAD]); // Downloadingpopstring(szStatus[ST_CONNECTING]); // Connectinglstrcpy(szStatus[ST_URLOPEN], szStatus[ST_CONNECTING]);popstring(szDownloading);// file namepopstring(szConnecting);// receivedpopstring(szProgress);// file sizepopstring(szSecond);// remaining timepopstring(szRemaining);// total time}else{popstring(szDownloading);popstring(szConnecting);popstring(szSecond);popstring(szMinute);popstring(szHour);popstring(szPlural);popstring(szProgress);popstring(szRemaining);}}else if(lstrcmpi(url, TEXT("/banner")) == 0){popup = true;szBanner = (TCHAR*)LocalAlloc(LPTR, string_size * sizeof(TCHAR));popstring(szBanner);}else if (lstrcmpi(url, TEXT("/callback")) == 0){//新增对callback命令的解析,获取回调函数地址;g_progressCallback = popint();TCHAR* buf12 = (TCHAR*)LocalAlloc(LPTR, 8 * sizeof(TCHAR));wsprintf(buf12, TEXT("%d"), g_progressCallback);}………………}pushstring(url);…………}
插件执行下载时,下载中状态会调用fileTransfer。在fileTransfer方法中,添加回调方法的调用,实现对下载进度的捕获。
void fileTransfer(HANDLE localFile, HINTERNET hFile){static BYTE data_buf[1024*8];BYTE *dw;DWORD rslt = 0;DWORD bytesDone;status = ST_DOWNLOAD;while(status == ST_DOWNLOAD){if(fput){if(!ReadFile(localFile, data_buf, rslt = sizeof(data_buf), &bytesDone, NULL)){status = ERR_FILEREAD;break;}if(bytesDone == 0) // EOF reached{status = ST_OK;break;}while(bytesDone > 0){dw = data_buf;if(!InternetWriteFile(hFile, dw, bytesDone, &rslt) || rslt == 0){status = ERR_TRANSFER;break;}dw += rslt;cnt += rslt;bytesDone -= rslt;}//MessageBox(NULL, "fput", "progress", MB_OK);}else{if(!InternetReadFile(hFile, data_buf, sizeof(data_buf), &rslt)){status = ERR_TRANSFER;break;}if(rslt == 0) // EOF reached or cable disconnect{// on cable disconnect returns TRUE and 0 bytes. is cnt == 0 OK (zero file size)?// cannot check this if reply is chunked (no content-length, http 1.1)status = (fs != NOT_AVAILABLE && cnt < fs) ? ERR_TRANSFER : ST_OK;break;}if(szToStack){for (DWORD i = 0; cntToStack < g_stringsize && i < rslt; i++, cntToStack++)if (convToStack)*((BYTE*)szToStack + cntToStack) = data_buf[i]; // Byteselse*(szToStack + cntToStack) = data_buf[i]; // ? to TCHARs}else if(!WriteFile(localFile, data_buf, rslt, &bytesDone, NULL) ||rslt != bytesDone){status = ERR_FILEWRITE;break;}cnt += rslt;//MessageBox(NULL, "not fput", "progress", MB_OK);}//此处实现对下载进度的回调if (g_progressCallback != -1){static TCHAR buf[32];wsprintf(buf, TEXT("%lu"), cnt / 1024);pushstring(buf);wsprintf(buf, TEXT("%lu"), fs != NOT_AVAILABLE ? fs / 1024 : 0);pushstring(buf);wsprintf(buf, TEXT("%lu"), fs > 0 && fs != NOT_AVAILABLE ? MulDiv(100, cnt, fs) : 0);//MessageBox(NULL, buf, "progress", MB_OK);pushstring(buf);g_pluginExtra->ExecuteCodeSegment(g_progressCallback - 1, g_hwndParent);}}}
至此对intec的改造完成,主要是获取下载进度,同步到NSIS中的进度条中,具体的进度计算方法可以自己定义。下载好的7z压缩包,可以使用nsis7z插件进行解压,从而实现网络安装。
NSIS7Z plug-in :http://nsis.sourceforge.net/Nsis7z_plug-in
回调函数的传递,也可以不通过添加命令行。比如在插件C++代码,直接通过getuservariable函数获取,另外也可以根据插件提供的/TRANSLATE 命令传递,示例如下:
具体实现方式我尚未探索,只是作为一个思路提供出来。intec的用法和nsisdl类似:
LangString DESC_REMAINING ${LANG_ENGLISH} " (%d %s%s remaining)"LangString DESC_PROGRESS ${LANG_ENGLISH} "%d.%01dkB/s" ;"%dkB (%d%%) of %dkB @ %d.%01dkB/s"LangString DESC_PLURAL ${LANG_ENGLISH} "s"LangString DESC_HOUR ${LANG_ENGLISH} "hour"LangString DESC_MINUTE ${LANG_ENGLISH} "minute"LangString DESC_SECOND ${LANG_ENGLISH} "second"LangString DESC_CONNECTING ${LANG_ENGLISH} "Connecting..."LangString DESC_DOWNLOADING ${LANG_ENGLISH} "Downloading %s"LangString DESC_SHORTDOTNET ${LANG_ENGLISH} "Microsoft .Net Framework 1.1"LangString DESC_LONGDOTNET ${LANG_ENGLISH} "Microsoft .Net Framework 1.1"LangString DESC_DOTNET_DECISION ${LANG_ENGLISH} "$(DESC_SHORTDOTNET) is required.$\nIt is strongly \advised that you install$\n$(DESC_SHORTDOTNET) before continuing.$\nIf you choose to continue, \you will need to connect$\nto the internet before proceeding.$\nWould you like to continue with \the installation?"LangString SEC_DOTNET ${LANG_ENGLISH} "$(DESC_SHORTDOTNET) "LangString DESC_INSTALLING ${LANG_ENGLISH} "Installing"LangString DESC_DOWNLOADING1 ${LANG_ENGLISH} "Downloading"LangString DESC_DOWNLOADFAILED ${LANG_ENGLISH} "Download Failed:"LangString ERROR_DOTNET_DUPLICATE_INSTANCE ${LANG_ENGLISH} "The $(DESC_SHORTDOTNET) Installer is \already running."LangString ERROR_NOT_ADMINISTRATOR ${LANG_ENGLISH} "$(DESC_000022)"LangString ERROR_INVALID_PLATFORM ${LANG_ENGLISH} "$(DESC_000023)"LangString DESC_DOTNET_TIMEOUT ${LANG_ENGLISH} "The installation of the $(DESC_SHORTDOTNET) \has timed out."LangString ERROR_DOTNET_INVALID_PATH ${LANG_ENGLISH} "The $(DESC_SHORTDOTNET) Installation$\n\was not found in the following location:$\n"LangString ERROR_DOTNET_FATAL ${LANG_ENGLISH} "A fatal error occurred during the installation$\n\of the $(DESC_SHORTDOTNET)."LangString FAILED_DOTNET_INSTALL ${LANG_ENGLISH} "The installation of $(PRODUCT_NAME) will$\n\continue. However, it may not function properly$\nuntil $(DESC_SHORTDOTNET)$\nis installed."……………………nsisdl::download /TRANSLATE "$(DESC_DOWNLOADING)" "$(DESC_CONNECTING)" \"$(DESC_SECOND)" "$(DESC_MINUTE)" "$(DESC_HOUR)" "$(DESC_PLURAL)" \"$(DESC_PROGRESS)" "$(DESC_REMAINING)" \/TIMEOUT=30000 "$URL_DOTNET" "$PLUGINSDIR\dotnetfx.exe"DetailPrint "$(DESC_DOWNLOADING1) $(DESC_SHORTDOTNET)..."
以上就是开发NSIS插件的主要知识,实际实践过程需要在踩坑的过程中磨砺自己的编程技巧。
另外补充一些与本次插件开发的一些知识点:
1、NSIS获取系统版本和判断系统位数
通过读取注册表,获取系统版本
ReadRegStr $1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion" VersionNumber
引入:!include "x64.nsh"
Section;64位系统${If} ${RunningX64};这里进行相应的操作${Else};这里进行相应的操作${EndIf}SectionEnd
2、C中获取char数组长度
strlen只能用char*做参数,且该char数组必须是以''/0''结尾的;
sizeof 即使在字符数组没有终止符'/0' 的时候,也能够计算出数组“长度”的原因,但这里的“长度”实际上是:编译器分配给该数组变量的内存大小!
示例:
char chs[] = {'a', 'c', '\0', 'z', '3','d'}; // sizeof(chs) = 6; 而strlen(chs) = 2。
3、wsprintf用法
sprintf 单字节版本的C/C++库函数
swprintf 宽字节版本的C/C++库函数
而我们上面的wsprintf和上面两个函数看起来很相似,大家不要搞混淆了啊,wsprintf最前面的w不是代表Wide,宽字节的意思了,而是Windows的W,代表是windows的API函数了,其实它是一个宏这在上面已经说过了,真正的API函数其实是wsprintfA和wsprintfW这两个,在不严格的情况下通常我们也说wsprintf是函数。
wsprintf(缓冲区,格式,要格式化的值);wsprintf只能输出字符,字符串和整型数据,要输出任意类型应该用swprintf3002
关于格式:
%d:输入输出为整形 %ld 长整型 %hd短整型 %hu无符号整形 %u %lu
%s:输入输出为字符串 %c字符
%f:输入输出为浮点型 %lf双精度浮点型。
参考文档:
Inetc plug-in - NSIS (sourceforge.io)