FeNiX 的个人资料FeNiX's Amazing Grace照片日志列表更多 工具 帮助
2007/11/23

Visual Studio安装项目学习笔记

    项目组要开发一个插件,schedule很紧,安装程序由我负责。本来是想用NSIS来做的,原型都做好了,上头却以各种各样的理由否决了,于是只剩下3个途径(只考虑免费的或者已经有license的工具)——WiX(Windows Install XML)、InstallShield和Visual Studio安装项目。WiX很强大,用起来本应是和NSIS差不多的(除了一个是用XML另一个是用脚本语言),但前者因缺乏各种辅助工具、插件、sample code而显得难以上手,希望WixEdit和WiX 3.0会以后完善点吧。从零开始学WiX还不如学InstallShield,因为后者不少同事懂,不过据说bug比较多。从schedule考虑只能用VS了,customizability差一些,以后有时间的话学了InstallShield可以考虑重做。
    于是开始一边上网查资料一边开发原型,发现VS安装项目还是不错的,WYSIWYG就是方便,WYSIAYG的缺陷可以通过编程和修改 MSI文件来克服。下面说明如何实现(仅作备考,都是些很基础的操作)

一、需求
    要安装的主要是一个用C#(Visual Studio 2005)开发的系统服务,安装程序除了拷贝文件之外要做的是:检测并安装.NET Framework 2.0;安装完毕后启动该系统服务;一些快捷方式和注册表项创建;在遇到错误的时候产生log输出。

二、建立安装项目
    在与主项目相同的Solution下建立一个Setup Project(在Setup and Deployment类别下),右击该项目点选Add→Project Output...加入主项目的输出项。安装项目的属性中除了一些显示信息之外有几个比较重要——DetectNewerInstalledVersion、 RemovePreviousVersions、Version、UpgradeCode和ProductCode,前两个就不说明了,后面三个和安装卸载程序有很大关系。一般而言,同一个项目的不同版本应该保持相同的UpgradeCode和不同的ProductCode、Version,在改Version后VS会自动提示生成新 ProductCode,所以不会漏掉。
    加入了项目输出之后,右击安装项目点选View→Launch Conditions可以发现安装程序已经可以检测.NET Framework了,该项的 AllowLaterVersions属性可以根据需要修改。
    右击安装项目选择属性后点击Prerequisites...按钮,可以看见.NET Framework 2.0已经勾选了,我把Windows Installer 3.1 也顺便勾上。下面可以选择是把这些必备程序集成到安装包中还是让用户从网上下载。

三、安装界面
    右击安装项目点选View→User Interface可以修改安装对话框,不过自定义程度实在是很低……比如说我想加个对话框让用户输入用户名和密码,却发现只有明文的text box。网上有人提供修改VS资源的解决办法,这显然不好,像我们公司的项目都是在特定的机器(Build Master)上生成的,总不能改那上面的资源吧。另一个方法就是修改生成的MSI文件,在Aaron Stebner的这篇post上可以找到一个在安装完成对话框中加入“马上运行该程序”勾选框的javascript脚本,其原理是通过MSI的类似SQL数据库操作修改其中的数据。至于MSI里有什么数据可以用Orca查看,找到需要改的项后修改那个javascript脚本,最后把这个脚本放在VS项目的 PostBuildEvents里运行。不过后来因为需要输入的东西太多,在安装程序里输入不大好,改为安装完毕后运行一个config程序来做了。

四、注册表
    类似的,右击安装项目点选View→Registry可以定义注册表操作,可以设置是否在卸载后删除,蛮方便的。

五、自定义操作
    安装了生成的MSI之后就会在控制面板的添加删除程序中加入卸载项,但开始菜单里什么都没有,要在程序项里加入卸载快捷方式有3个方法:
    1. 建一个批处理文件(可以再wrap成exe)执行"msiexec /x <ProductCode>"然后建立快捷方式(右击安装项目点选View→File System处理)
    2. 在system32目录拷个msiexec.exe(可以改名成uninstall.exe之类的)然后建立快捷方式(Arguments项添加"/x [ProductCode]" )
    3. 编程实现添加指向msiexec.exe的快捷方式

    我选择第3种方式,也就是使用Custom Actions,具体方法是在主项目的系统服务主类的Design视图中右击空白部分选择Add Installer。生成一个System.Configuration.Install.Installer的子类ProjectInstaller,打开其Design视图可以看见两个 component,分别设置相应属性。当然也可以用一般的方式生成Installer子类——右击项目添加Installer类,然后在构造方法中构造 System.ServiceProcess.ServiceProcessInstaller和System.ServiceProcess.ServiceInstaller组件。
    在安装项目View→Custom Actions里的Custom Actions根节点中添加有Installer子类的项目主输出(可以看见每个Custom Action的Installer Class属性都自动设成True)后,就可以重写以下的方法实现自定义操作:
    void Install(System.Collections.IDictionary)
    void Commit(System.Collections.IDictionary)
    void Rollback(System.Collections.IDictionary)
    void Uninstall(System.Collections.IDictionary)
    void OnBeforeInstall(System.Collections.IDictionary)
    void OnAfterInstall(System.Collections.IDictionary)
    void OnBeforeRollback(System.Collections.IDictionary)
    void OnBeforeRollback(System.Collections.IDictionary)
    void OnBeforeUninstall(System.Collections.IDictionary)
    void OnAfterUninstall(System.Collections.IDictionary)
    void OnCommitting(System.Collections.IDictionary)
    void OnCommitted(System.Collections.IDictionary)

    比如在安装完毕后启动服务就可以通过重写OnAfterInstall方法实现。添加卸载程序快捷方式需要在Install和Uninstall两个Custom Action的CustomActionData属性里输入“/productcode=[ProductCode] /productname= [ProductName]”,并在项目中添加对Windows Script Host Object Model的Reference以及using IWshRuntimeLibrary,代码(参考了这篇post)如下:
        public override void Install(System.Collections.IDictionary stateSaver)
        {
            base.Install(stateSaver); 

            System.Collections.Specialized.StringDictionary parameters = this.Context.Parameters;

            string pc = parameters["productcode"];

            string pn = parameters["productname"];

            string programShortcutFolder = String.Format(@"{0}\{1}",

                Environment.GetFolderPath(Environment.SpecialFolder.Programs),

                pn

            );

            WshShell shell = new WshShell();

            if (!Directory.Exists(programShortcutFolder))

                Directory.CreateDirectory(programShortcutFolder);

            IWshShortcut shortcut = (IWshShortcut)shell.CreateShortcut(

                programShortcutFolder + @"\Uninstall.lnk"

            );

            shortcut.TargetPath = Environment.GetFolderPath(Environment.SpecialFolder.System) + @"\msiexec.exe";

            shortcut.WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.System);

            shortcut.WindowStyle = 1;

            shortcut.Description = "Description blah blah blah";

            shortcut.IconLocation = Environment.GetFolderPath(Environment.SpecialFolder.System) + @"\msiexec.exe";

            shortcut.Arguments = "/x " + pc;

            shortcut.Save();

        }

 

        public override void Uninstall(System.Collections.IDictionary savedState)

        {

            base.Uninstall(savedState);

 

            System.Collections.Specialized.StringDictionary parameters = this.Context.Parameters;

            string pn = parameters["productname"];

            string programShortcutFolder = String.Format(@"{0}\{1}",

                Environment.GetFolderPath(Environment.SpecialFolder.Programs),

                pn

            );

            if (Directory.Exists(programShortcutFolder))

                Directory.Delete(programShortcutFolder,true);

        }

六、遗留问题
● 虽然可以通过重写Installer的方法来输出log,但这样太局限了,如果有什么其他问题可能要通过运行“setup /l*vx install.log”来查看MSI安装log。
● 默认的卸载界面过于简单,我想在最后显示一个对话框提示已成功卸载,却找不到获取Installer当前Window对象的方法,哪位同学有什么好办法?

P.S. 我一开始没想到向Custom Action传参数,想通过读sln和vdproj文件来获取ProductCode和ProductName,找到了一下几篇文章,还蛮好玩的:
http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=2088820&SiteID=1
http://forums.microsoft.com/msdn/showpost.aspx?postid=59504&siteid=1
http://msdn2.microsoft.com/en-us/library/ms228772(VS.80).aspx
如果哪位同学刚好遇到以下异常的话可以参考一下
System.Runtime.InteropServices.COMException was unhandled
    ErrorCode=-2147418111
    Message="Call was rejected by callee. (Exception from HRESULT: 0x80010001 (RPC_E_CALL_REJECTED))


2007/11/19

OUAT & UNO

    绵羊同学上周末按计划过来聚,我把Yimo同学也拉了过来。这次玩得比上周爽多了,因为安排好了行程,Once Upon A Time和UNO牌也准备好了。我准备的“按摩棒”要素卡果然很有效果,带动全场气氛~
    唯一遗憾的是没从公司借到Wii Sports,借来的Wii又没有装Wiikey,只能玩《はじめてのWii》。这就算了,玩的时候还发现里面没有电池,时间又太晚超市都关了。好不容易才找到两节电池,那游戏双打应该还是不错的说……
    晚上吃饭的时候本来是想补下RGP项目同志的BG和答谢舍友的,可惜WHS和HC同学没办法来……少给两份钱yeah吐舌
    果然两三个人晚上一起玩的话Wii才是王道啊,连一直对Wii不抱希望的FX同学都有兴趣起来了呵呵~PS2游戏就不用大家推荐了
 
附上OUAT材料:
2007/11/12

綿ちゃん、突然来るのはもうやめてくれないかな>_<

本来说好这个周末过来聚的,绵羊同学上周末突然跑过来(对,已经不是第一次了,严重打破我的计划),UNO牌和Once Upon A Time材料都没准备好,只好去街机和唱K了
爽快的是新街口那店有太鼓10了,“晴れハレ”和“もってけ”万岁~虽然比较贵(¥1两歌 compared with ¥0.5三歌 in 太鼓8)
郁闷的是盛哥掉了¥500现金和两张银行卡,本人对该同学的遭遇深表同情并致以亲切的慰问,amen……
 
P.S. 哪位同学推荐一下容易上手比较快热的PS2游戏吧,经常有同学在我这借宿一宵却不知道玩什么好,来来去去就是无双、高达、KOFMIA和GPX,其他碟基本都是RPG和战棋,羞赧尴尬