cover_image

NSIS插件开发步骤和总结

夏继亮 三七互娱技术团队
2023年01月04日 10:46

背景和目的:

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 $0        SendMessage $PROGBAR ${PBM_SETPOS} $0 0 ; $PROGBAR:进度条FunctionEnd Function ExtractfuncReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows NT\CurrentVersion" "CurrentVersion"  GetFunctionAddress $R9 DownLoadCallBack  inetc::get  /SILENT /callback $R9 "https://xxxxx/test_dir/test.7z"  "D:/test/test.7z"  /end  ;新增一个/callback命令,用来传递回调函数,在插件中会新增callback命令的解析  Pop $1  ; 写入值($1="ok"表示下载成功)  Push $1 FunctionEnd

以下是插件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 params        szPassword[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]); // Downloading                popstring(szStatus[ST_CONNECTING]); // Connecting                lstrcpy(szStatus[ST_URLOPEN], szStatus[ST_CONNECTING]);                popstring(szDownloading);// file name                popstring(szConnecting);// received                popstring(szProgress);// file size                popstring(szSecond);// remaining time                popstring(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]; // Bytes                    else                        *(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)

继续滑动看下一个
三七互娱技术团队
向上滑动看下一个