在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。