编程开源技术交流,分享技术与知识

网站首页 > 开源技术 正文

NSIS插件开发步骤和总结

wxchong 2024-06-14 13:22:44 开源技术 39 ℃ 0 评论

背景和目的:

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 Extractfunc
ReadRegStr $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)


作者:夏继亮

来源-微信公众号:三七互娱技术团队

出处:https://mp.weixin.qq.com/s/9ozdXD110i6Fw61VUStR3g

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表