安装包制作工具(二)

前情提要

上一篇说到经过调试解决了制作处理的安装包工具不能运行是因为打包图标的bug导致的,因为这里依赖了一个RH.exe的工具来提取、替换exe文件的icon文件。之前没有处理好这个工具的依赖路径,导致时隔这么久拿出来用就罢工了,借着这次记录的时机也顺道给理清楚了。

节选了一段关于rh.exe的介绍

rh.exe 也就是ResourceHacker是一个专业的资源编辑工具,主要用于编译、查看、反编译以及再编译 32 位和 64 位 Windows 可执行文件的资源。可以添加、修改、或删除这些文件内的单独资源。Resource Hacker(TM) 可以创建和编译资源脚本文件 (*.rc),也可以编辑资源文件 (*.res)。

书接上文

上次介绍了核心原理以及最终效果图,那这次就主要从代码设计层面讲一讲实现的思路。先看一下整理的包含主要功能的脑图

代码脑图

具体实现每个stage还是有得讨论的,先写到这里明天再具体分析实现每个stage的方式以及遇到的问题。

代码分析

挑核心部分的代码说明一下,其实整个流程以及核心逻辑都不复杂,考验的是遇到各种新的细节问题要怎么解决才更优雅。

打包信息到文件尾部读取(Maker: EndStage)

前文有说实现MakeInstaller的核心原理就是得让知道exe知道自己扮演的角色,从自身读取数据并解压。

下面这段代码就是完成文件打包后写入exe标识的流程。这里写入了4kb的空白字符到压缩流中,这里其实没有必要所有字节的偏移量都是明确写在exe文件的尾部的。这里当时思考的时候就写完了,也没有必要删除。

  1. 写入文件标识前必须关闭压缩文件流(用于压缩要打包的文件)

  2. 重新打开文件流并移动文件指针到尾部

  3. 依次写入压缩文件的大小、校验标识、打包类型

        public override bool Run()
        {
            base.Run();
            int byteCount = 0;
            //写入4k 空白到压缩流,防止解压时读取到标识符号。
            var buff = maker.buff;
            for (int i = 0; i < Maker.STREAM_BUFF_SIZE; ++i)
            {
                buff[i] = 0;
            }
            maker.zipStream.Write(buff, 0, Maker.STREAM_BUFF_SIZE);
            maker.zipStream.Flush();
            maker.zipStream.Close();

            FileStream appendStream = new FileStream(maker.outputFile, FileMode.Append, FileAccess.Write);
            appendStream.Seek(0, SeekOrigin.End);
            Packer packer = maker.packer;

            Packer.PackInt(buff, ref byteCount, maker.packerSize);
            appendStream.Write(buff, 0, byteCount);
            appendStream.Write(Maker.BAKED_FLAG, 0, Maker.BAKED_FLAG.Length);
            //打包类型
            buff[0] = (byte)ExeType.Packed;
            appendStream.Write(buff, 0, 1);

            appendStream.Flush();
            appendStream.Close();
            return true;
        }
    }

从文件尾部读取信息(Installer:LoadPackConfigStage)

从文件尾部读取过程跟打包过程顺序相反,根据预先定义好的标识符、标识数据所占byte大小计算出文件偏移量,就开始读取数据。

    var packerFile = Process.GetCurrentProcess().MainModule.FileName;
    FileStream selfStream = new FileStream(packerFile, FileMode.Open, FileAccess.Read);
    const int sizeShift = 4;
    var selfFileInfo = new FileInfo(packerFile);
    long flagShift = selfFileInfo.Length - FLAG_SIZE - 1;// last byte is ExeType
    flagShift = flagShift - sizeShift;// packerSize is at the front of flag;

    byte[] buff = new byte[FLAG_SIZE + 5];
    selfStream.Seek(flagShift, SeekOrigin.Begin);
    selfStream.Read(buff, 0, FLAG_SIZE + 5);

多次写入文件

其实核心原理就是这么简单的东西,不过这里需要有一点要注意的是整个创建安装包的过程中会分两次文件打开文件进行写入操作。

  1. 创建输出文件流,拷贝文件MakeInstaller.exe到头部

  2. 创建压缩流,将要打包的文件信息、文件写入压缩流

  3. 关闭压缩流、输出文件流,重新打开文件流写入安装包标识

    这里压缩流其实应该可以不用关闭,Stream.Flush()应该可以保证zipStream里面不会有残留缓存数据影响后续写入的标识信息。不过当时这么设计应该是有一定考量的,可能当时验证过如果不关闭会有异常,也可能是直接采取了更保险的方式懒得验证了,对整个流程来说这点消耗并不算什么。

            maker.outputStream = new FileStream(maker.outputFile, FileMode.Create, FileAccess.Write);
            maker.packerSize = maker.AddFileToStream(maker.packerFile, maker.outputStream);
            // write 512 empty byte for safe;
            for (int i = 0; i < Maker.PACK_START_SHIFT; ++i)
            {
                maker.buff[i] = 0;
            }

            maker.outputStream.Write(maker.buff, 0, Maker.PACK_START_SHIFT);
            maker.zipStream = new GZipStream(maker.outputStream, CompressionMode.Compress);
            maker.packer = new Packer(maker.zipStream);
            maker.packer.Pack(PackType.CONFIG);

            //config maybe changed after check.
            var serializer = new JavaScriptSerializer();
            maker.configContent = serializer.Serialize(maker.config);
            maker.packer.Pack(maker.configContent);
  1. 因为exe的加载流程是操作系统从文件头部开始读取信息并加载如内核运行,所以安装包的元数据信息只能写在文件最末端。

  2. 这里有个小知识点,一个文件流正常写入一段后接着再创建压缩流写入能够正常工作正是文件指针偏移量的作用

写注册表

能够写入注册表才算一个比较完整的安装包,注册表就是为了告诉系统安装包的版本信息、安装路径、占用磁盘大小等常规信息,这样能够做到更加的合规合法,方便用户通过标准流程卸载。

这里贴一下注册表的关键代码,当时也有踩不少坑

//制作安装包的注册表配置
    public class RegValue
    {
        public string Type { get; set; }
        public string Path { get; set; }
        public string Name { get; set; }
        public Dictionary<string,string> Values { get; set; }
    }
//写入注册表
       private void WriteRegValue(RegValue val)
        {
            RegistryKey rootKey = null;
            switch(val.Type)
            {
                case "HKEY_LOCAL_MACHINE": rootKey = Registry.LocalMachine; break;
                case "HKEY_CURRENT_USER": rootKey = Registry.CurrentUser; break;
                case "HKEY_CLASSES_ROOT": rootKey = Registry.ClassesRoot;break;
                case "HKEY_USERS": rootKey = Registry.Users;break;
                case "HKEY_CURRENT_CONFIG": rootKey = Registry.CurrentConfig;break;
                case "HKEY_PERFORMANCE_DATA": rootKey = Registry.PerformanceData; break;
            }

            if (rootKey == null)
            {
                OnMessage(string.Format("Invalid Regist Type : {0}", val.Type), 0, false);
                return;
            }

            RegistryKey contanerKey = rootKey.OpenSubKey(val.Path, true);
            RegistryKey newKey = contanerKey.CreateSubKey(val.Name);
            var newKeyValues = val.Values;
            foreach(var newVal in newKeyValues)
            {
                string parsed_val = maker.ParseVarString(newVal.Value);
                long long_val;
                if(long.TryParse(parsed_val, out long_val))
                {
                    newKey.SetValue(newVal.Key, long_val, RegistryValueKind.DWord);
                }
                else
                {
                    newKey.SetValue(newVal.Key, parsed_val);
                }
            }
            newKey.Close();
        }

哪里下载

上传到了 SourceForce

其实有很多更完善的免费的工具可以用,感兴趣的可以联系获取源代码、license,当时做这个工具的原因就是自己喜欢造轮子,究其根本,不喜欢被限制。

关于安装包制作工具的内容就写到这里了,下一篇写最近开发中经常用到的工具"ClickAction"

公告
本博客基于TinyBlog搭建,关注公众号CoderThing