当前位置: 首页IT技术 → 便携软件制作的系列教程

便携软件制作的系列教程

更多

在NSIS中怎么导入注册表。

这有何难,用registry插件嘛:

${registry::RestoreKey} file.reg $var

可是,如果你经常在 RestoreKey 后面用 ${registry:write} ,就会发现,往往导入注册表会失败,或者写入的键值被reg文件中的旧键值覆盖了,这是为什么呢?

原来,${registry::RestoreKey} 这个命令并不会等待导入完成。作者在文档中写了:

${registry::RestoreKey} simply exec regedit: regedit /s “[file]“

执行的是 Exec 而非 ExecWait 。那么,可能 regedit.exe 尚未启动,就开始执行下一行命令了。制作一般的安装包问题不大,但便携软件对执行步骤的顺序要求更加精确。所以,有些人的代码是这样写的:

${registry::RestoreKey} file.reg $0

Sleep 200

睡一会。睡多久?睡一秒还是一年,这种盲人摸象的做法,我们完美主义者是不会使用的。因为这个命令,有些朋友凡是用到registry插件,都习惯性地加上个 sleep,这是完全没有必要的,作者说了:

问:So my question is, what other functions in your plugin behave in the same way (ie do not wait for the registry operation to finish)?

答:registry::RestoreKey is the only one.

那么,用:

ExecWait 'regedit /s "[file]"' $var

不就行了吗?

你又错了,我们制作便携软件的时候,要对自己严格要求,在Vista以上的系统中,不经过UAC验证,是无法执行 regedit /s 这个命令的(即使导入HKCU中的键值也不行)。难道你的每个软件都要用户通过UAC验证以管理员权限运行吗,完全是别有居心!

可是,在UAC环境的测试中,你会发现,即使不通过UAC验证,${registry::RestoreKey} 这个命令也可以完成注册表导入,难道,作者隐瞒了什么?

于是,作为代码盲的你,充满狐疑地打开 NSIS\Include\Registry.nsh ,找到这样一段代码:

!define registry::RestoreKey !insertmacro registry::RestoreKey

!macro registry::RestoreKey _FILE _ERR

registry::_RestoreKey /NOUNLOAD ${_FILE}

Pop ${_ERR}

IntCmp ${_ERR} -2 0 0 +10 ;REGEDIT4 ansi file

SetDetailsPrint none

IfFileExists "$SYSDIR\reg.exe" 0 +4 ;reg.exe used in Windows2K/XP/Vista/7

nsExec::ExecToStack "$SYSDIR\reg.exe" import "${_FILE}"

Pop ${_ERR}

StrCmp ${_ERR} 0 +5 0

IfFileExists "$WINDIR\regedit.exe" 0 +3 ;regedit.exe used in Wine

ExecWait "$WINDIR\regedit.exe" /s "${_FILE}" ${_ERR}

IfErrors 0 +2

StrCpy ${_ERR} -1

SetDetailsPrint lastused

!macroend

真是狡兔三窟!registry::RestoreKey失败后,用reg.exe import,失败后,又用 regedit.exe /s,我们就要有这种不屈不挠的精神,不要让一次执行的失败变成Bug。

眼尖的你发现,关键在于这一行:

nsExec::ExecToStack "$SYSDIR\reg.exe" import "${_FILE}"

原来,虽然regedit /s需要管理员权限,但reg import命令并不需要,这就是${registry::RestoreKey}成功的秘诀。

但是,${registry::RestoreKey}首先尝试用插件导入,而插件并不等待导入结束,所以,我们在应用的时候,要把顺序颠倒一下:

nsExec::ExecToStack "$SYSDIR\reg.exe" import "${_FILE}"

Pop $0

${IfNot} $0 == 0

${registry::RestoreKey} "${_FILE}" $0

Sleep 500

${IfNotThen} $0 == 0 ${|} StrCpy ${_OutVar} Error ${|}

${Endif}

nsExec::ExecToStack是等待运行结束的,首先执行,假如失败,再用${registry::RestoreKey},并暂停0.5秒(比较安全的数值)。当以上动作始终返回Error的时候,我们就应该考虑做个标记,在便携软件结束的时候跳过这一次软件运行中的注册表修改,不覆盖原先的reg文件了。

不过,当你翻阅 PortableApps.com Launcher 的源代码时,却发现关于注册表导入,仅仅用了一行:

${registry::RestoreKey} $DataDirectory\settings\$0.reg $R9

可为什么感觉上PAL那么稳定,极少出错呢?我猜是因为PAL的代码非常繁杂,每个实际动作以前都有一堆工作,又是读Launcher.ini,又是转换变量,又是检测PAF平台,慢悠悠的,慢工出细活吧!

例1:

设想某一天,某妞将可移动磁盘插入电脑A,电脑A为她的U盘分配了盘符 F: 。该妞使用U盘上的便携软件打开了储存于U盘上的几个文档:

F:\1.doc

F:\2.doc

……

在拔出U盘的时候,她甚至没有为最后一个文件存档,反正所有进度都会自动保存嘛!

过了几日,该妞试图在电脑B继续她的工作,插入U盘以后,电脑B为她的U盘分配了盘符G: 。当她打开便携软件的时候,她会看到“最近文档”列表那里显示着什么呢?

仍然是:

F:\1.doc

F:\2.doc

……

当她试图恢复上一次”自动保存“的文档,却提示“找不到……文件”。于是,她发怒了,发誓再也不来你的网站。

这正常吗?太正常了,上次你就是在F盘打开文件的嘛。可是你还能完美地使用“最近文件”列表、继续上一次的工作吗?

我们干革命,就是要勇于把正常变为不正常。于是,我们需要盘符替换。

什么是盘符替换

所谓盘符替换 (Driver Letter Replacement),就是在检测到盘符相对上一次运行时改变的时候,将某些文件中的旧盘符替换为新盘符,以实现用户数据的完美衔接。以上述例子为例,就是在盘符转变为 G: 的时候,将最近文档列表替换为:

G:\1.doc

G:\2.doc

……

让用户体会不到盘符改变带来的变化,而顺利继续上一次的工作进程。

什么是路径替换

例2:

设想某一个软件,在配置文件中保存了大量包含软件路径的数据,这些数据在软件首次运行时生成,指向软件的插件、模板等目录,假如这些目录设置错误,该软件便无法正常运行。

而某一天,某妞将该便携软件从同盘符的一个目录移动到另一个目录,例如:从 f:\XXXPortable 移动到 f:\PortableApps\XXXPortable 。

那么,如何保证该软件正常运行呢?假如包含路径的设置项不多,我们可以一个个地写入,而假如类似设置很多(例如ACDSee),或者数量不定,难道也要一个个写入吗?所以,我们需要在检测到路径改变时,将所有的 XXXPortable 替换为 PortableApps\XXXPortable 。

目录格式

在 PortableApps.com Launcher 中,提供了四种类型的目录格式,分别是:

%VARIABLE% : 正向单斜杠。例如:%PAL:AppDir% = x:\portableapps\xxxportable\app 。主要应对ini、xml等普通配置文件。

%VARIABLE:ForwardSlash% : 反向单斜杠。例如:%PAL:AppDir:ForwardSlash% = x:/portableapps/xxxportable/app

%VARIABLE:DoubleBackslash% : 正向双斜杠。例如:%PAL:AppDir:DoubleBackslash% = x:\\portableapps\\xxxportable\\app 。主要应用于注册表(.reg)文件。

%VARIABLE:java.util.prefs% : 反向多斜杠。例如: %PAL:AppDir:java.util.prefs% = /X:///Portable/Apps///App/Name/Portable///App 。主要应用于java程序。

我们需要根据替换文件的类型选择相应的目录形式。假如遇到这四种情况以外的目录形式,则要靠 Custom Code 解决。

实现原理与流程

为了兼顾例1与例2的两种情况,避免两种情况同时发生,我们要将盘符替换与路径替换分开,那就是:先替换盘符,再替换不带盘符的路径。

在引导过程中,读取上一次记录的INI文件,判断是否盘符/路径改变。

若改变,则读取上一次的盘符/路径,转换为正确形式。

读取当前的盘符/路径,转换为正确形式。

在文件中替换旧盘符为新盘符。

在文件中替换旧路径为新路径。

将当前的盘符、路径写入INI文件,以便下一次读取。

在 PortableApps.com Launcher 中实现:

以ACDSee Portable为例,我们需要在引导过程中替换注册表文件 HKCU.reg 中的旧盘符\路径为新。

[FileWrite1]

Type=Replace

File=%PAL:DataDir%\settings\HKCU.reg

Find=%PAL:LastDrive%\\

Replace=%PAL:Drive%\\

[FileWrite2]

Type=Replace

File=%PAL:DataDir%\settings\HKCU.reg

Find=%PAL:LastPackagePartialDir:DoubleBackslash%

Replace=%PAL:PackagePartialDir:DoubleBackslash%

效果如下:

x:\\

替换为:

y:\\

\\xxx\\AppNamePortable

替换为:

\\yyy\\AppNamePortable

请注意,在 [FileWrite1] (盘符替换)中,我在 %PAL:LastDrive% 后面加上了双斜杠。这是因为,%PAL:LastDrive% 是不带斜杠的(x:)。可能出现这种情况:替换 D:,把 DWORD: 的最后两个字母也替换了。难道PortableApps.com的人不担心这种情况吗?我认为使用PAL替换盘符的时候都要注意这一点,替换盘符一定要加斜杠。

在 Custom Code 中实现:

PortableApps.com Launcher 是一个死板的网站的死板的程序员做出的死板的工具,在险峻难料的革命事业中,我们要坚决摒弃教条主义思想。许多时候稍有变化,我们就需要用到 Custom Code 。那么,在NSIS语言中怎样实现呢?

例如,一个程序以这样的形式在 Data\File.txt 记录路径:

F__PortableApps_App_Portable

“:”、“\”、“空格”三种符号都转换为下划线。让我们来写一段 Custom Code 来解决它。

${SegmentPrePrimary}

; 替换盘符

; 首先读取Ini文件中的记录

ReadINIStr $0 $DataDirectory\settings\$AppIDSettings.ini $AppIDSettings LastDrive

; 替换三种符号为下划线

${WordReplace} "$0\" "\" "_" "+" "$R0"

${WordReplace} "$R0" ":" "_" "+" "$R0"

${WordReplace} "$R0" " " "_" "+" "$R0"

; 得到当前盘符

StrCpy $R1 "$AppDirectory" 3

; 替换三种符号为下划线

${WordReplace} "$R1" "\" "_" "+" "$R1"

${WordReplace} "$R1" ":" "_" "+" "$R1"

${WordReplace} "$R1" " " "_" "+" "$R1"

; 在文件中替换

${ReplaceInFileCS} "$DataDirectory\File.txt" $R0 $R1

; 替换路径

; 首先读取Ini文件中的记录

ReadINIStr $0 $DataDirectory\settings\$AppIDSettings.ini $AppIDSettings LastDirectory

; 替换两种符号为下划线

${WordReplace} "$0" "\" "_" "+" "$R0"

${WordReplace} "$R0" " " "_" "+" "$R0"

; 得到当前路径(不带盘符)

StrCpy $R1 "$AppDirectory" "" 2

; 替换两种符号为下划线

${WordReplace} "$R1" "\" "_" "+" "$R1"

${WordReplace} "$R1" " " "_" "+" "$R1"

; 在文件中替换

${ReplaceInFileCS} "$DataDirectory\File.txt" $R0 $R1

!macroend

请注意文件的编码,如果是 UTF-16LE 编码,用 ${ReplaceInFileUTF16LECS} 。若需要忽略大小写,取消最后的“CS”。

在原始NSIS脚本中使用,需要另外:

!include "TextReplace.nsh"

!include "ReplaceInFileWithTextReplace.nsh"

注意事项

盘符与路径替换是一种简单地衔接工作环境的方法,但我认为,在应用中需要注意以下几点:

一定要确定你替换的是盘符/路径,而非别的东西。例如,使用PAL的时候,在盘符后加入斜杠,替换 x:\ 而不是 x: 。

自行撰写代码时,注意所替换文件的编码。

在替换大文件或多次替换之间,加入Sleep。否则可能遇到替换失败。

对于重要路径,最好在替换后手动写入一次,以保障无误。盘符替换依赖INI文件中的记录,假如一次记录与实际衔接不上,可能从此都衔接不上了。

自从去年心血来潮开了个头,这个教程就一直没了下文,我要用实际行动粉碎虎头蛇尾的谣言,同志们,今天来谈谈 DefaultData。

刚开始制作便携软件的朋友常犯的一个错误是,将软件的默认配置保存到 Data 目录中。何以说是错误呢:

PortableApps.com格式便携软件在安装后,Data目录必须是空的。Data目录中的文件必须在首次运行后生成。

合格的P.A格式便携软件,用户可以随时删除Data目录,将便携软件恢复到初始状态。

因此,如果某些默认配置在软件第一次运行时必须导入,我们应该将它保存到DefaultData目录中。

DefaultData 的诞生

在一个不可考证的从前,John T. Haller 同志(PortableApps.com 的创始人)开始制作他的第一个便携软件:Firefox Portable。在移动介质运行的 Firefox 浏览器应该有如下调整:关闭磁盘缓存,不检测默认浏览器,不设置默认下载目录,同时,他希望在Firefox的默认书签内加入他的网站地址。如何实现以上默认设置的调整呢?

直接修改程序?吃力不讨好。于是,他在 App 目录下新建了 DefaultData 目录,将一份配置好的最简化的配置保存于此。在Firefox首次运行时,DefaultData 目录的内容会被复制到 Data 目录,以实现设置默认配置的目的。于是,今天的 Firefox Portable(以及所有标准P.A格式便携软件)的结构,就成了这个样子:

-\ <--- Directory with FirefoxPortable.exe

+\App\

+\AppInfo\

+\firefox\

+\DefaultData\

+\profile\

+\settings\

+\plugins\

+\Data\

FirefoxPortable.exe

什么是 DefaultData

通过以上叙述我们已经知道,DefaultData 是 PortableApps.com 格式便携软件的标准部件之一,它位于 App\DefaultData ,是软件的默认配置。在首次运行时,它被复制到 Data 目录,DefaultData 内部的文件结构应该和 Data 目录完全一致。

DefaultData 怎样工作?

DefaultData在首次运行时复制到 Data 目录,作为初始的程序配置。判断是否首次运行有几种不同的方式:

Firefox Portable 的方式:

Firefox Portable 通过 NSIS 语言写成,它通过检查 Data\Profile\prefs.js是否存在来判断是否首次运行,假如 Data\Profile\prefs.js 不存在,则复制默认配置到Data目录。这种方式较为灵活,可根据不同软件的具体情况选择不同的判断物:

ProfileWork:

;=== Check for an existing profile

IfFileExists "$PROFILEDIRECTORY\prefs.js" ProfileFound

;=== No profile was found

StrCmp $ISDEFAULTDIRECTORY "true" CopyDefaultProfile CreateProfile

CopyDefaultProfile:

CreateDirectory "$EXEDIR\Data"

CreateDirectory "$EXEDIR\Data\plugins"

CreateDirectory "$EXEDIR\Data\profile"

CreateDirectory "$EXEDIR\Data\settings"

CopyFiles /SILENT $EXEDIR\App\DefaultData\plugins\*.* $EXEDIR\Data\plugins

CopyFiles /SILENT $EXEDIR\App\DefaultData\profile\*.* $EXEDIR\Data\profile

PortableApps.com Launcher 的方式:

PortableApps.com Launcher 通过检查 Data\settings目录是否存在判断首次运行,PAL在运行一次以后必然创建 Data\settings 目录,如果此目录不存在,则判断为首次运行,并复制 DefaultData:

${IfNot} ${FileExists} $EXEDIR\Data\settings

CreateDirectory $EXEDIR\Data\settings

${If} ${FileExists} $EXEDIR\App\DefaultData\*.*

CopyFiles /SILENT $EXEDIR\App\DefaultData\*.* $EXEDIR\Data

${EndIf}

${EndIf}

第三种方式:

在制作具有中国特色的便携软件时,有时会碰到更为复杂的情况。为了保证软件始终从默认配置的基础上启动,我们可以分别判断多个目录,缺少哪一个,就复制哪一个:

例一,

若Data\Profile不存在则复制DefaultData\Profile,若Data\Plugins不存在则复制DefaultData\Plugins:

${IfNot} ${FileExists} $EXEDIR\Data\Profile

CreateDirectory $EXEDIR\Data\Profile

CopyFiles /Silent $EXEDIR\App\DefaultData\Profile\*.* $EXEDIR\Data\Profile

${EndIf}

${IfNot} ${FileExists} $EXEDIR\Data\Plugins

CreateDirectory $EXEDIR\Data\Plugins

CopyFiles /Silent $EXEDIR\App\DefaultData\Plugins\*.* $EXEDIR\Data\Plugins

${EndIf}

例二,

在迅雷便携版中应用到的,检测任何一个 DefaultData 中的目录,如果在 Data 目录中不存在,都复制过去:

Section Main

; ......

; CopyDefaultData:

StrLen $R0 "$EXEDIR\App\DefaultData\"

${Locate} "$EXEDIR\App\DefaultData" "/L=D" CopyDefaultData

; ......

SectionEnd

Function CopyDefaultData

StrCpy $R1 $R9 "" $R0

${IfNot} ${FileExists} "$EXEDIR\Data\$R1"

CreateDirectory "$EXEDIR\Data\$R1"

CopyFiles /Silent "$R9\*.*" "$EXEDIR\Data\$R1"

${Endif}

Push $0

FunctionEnd

DefaultData 能做什么

修改默认配置

例如,在 Evernote Portable 中,将以下内容保存为 App\DefaultData\settings\EvernotePortable.reg:

Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\Software\Evernote\Evernote]

"UpdateToPreReleaseVersion"=dword:00000000

"CheckForUpdatesAtLaunch"=dword:00000000

则会在首次运行时复制为 Data\settings\EvernotePortable.reg ,接着导入注册表,实现默认关闭自动升级的目的。

程序本身的默认/初始配置

通过 Total Uninstall 监测软件安装,可发现某些软件在首次安装后会在配置目录中写入一些文件,这些文件必须放到 DefaultData 目录,以保证程序的完整性,以及让用户随时可以删除 Data 目录恢复软件初始配置。

通过 DefaultData 新建文件夹

在 PortableApps.com Launcher 中,如果你希望使用 FilesMove 来移动文件,必须保证 Data 目录中有这个文件的父目录,否则移动会失败,例如:

[FilesMove]

config\file.txt=%PAL:AppDir%\AppName

在此例中,假如Data\config目录不存在,那么file.txt就无法被移动到Data目录。

解决办法是,创建 App\DefaultData\config ,那么,首次运行时,App\DefaultData\config 会被复制为 Data\config ,以实现新建文件夹的目的。

不过,在大多数情况下,将单个文件保存到 Data\settings 中是更好的方法,PAL会自动创建此文件夹,避免了通过 DefaultData 来创建的麻烦。

其它用途

在制作具有中国特色的便携软件时,有时我们希望一些软件配置永远是“一次性”的(例如广告目录),那么,我们将一份干净的初始配置保存到 DefaultData 中,在每次软件启动时复制到配置目录,在软件结束时删除掉复制的副本。以保证软件的洁净。

注意事项

DefaultData 是 Data 目录的初始状态,其目录、文件结构必须和 Data 目录完全一致。假如你在设计便携软件时设定将注册表导出到 Data\settings\AppNamePortable.reg,那么你应该将默认配置保存为 App\DefaultData\settings\AppNamePortable.reg 。否则无法奏效。

DefaultData 是软件初始配置的一份存档,应该尽量保持精简。仅仅保留最必要的部分。你不应该将整个配置好的 Data 保存为 DefaultData,那样浪费空间,延长首次启动的时间,而应该找出真正有必要的、不可缺少的修改部分,保存为 DefaultData 。如果你的 DefaultData 超过1M,那么就该想想办法了。

DefaultData 应该保留最通用的部分,如果你的 DefaultData 中存在关于你的计算机的信息,例如:installdir=c:\Program Files\AppName ,那么是非常不专业的,我们要严格要求自己。

通过 Total Uninstall 监视软件安装,可发现某些软件自身的默认配置。有时候这些配置很重要(例如一个初始的数据库),请别忘了把它们保存到 DefaultData。

 

热门评论
最新评论
昵称:
表情: 高兴 可 汗 我不要 害羞 好 下下下 送花 屎 亲亲
字数: 0/500 (您的评论需要经过审核才能显示)