使用 DTF (wix) 以编程方式将 cabinet 文件添加到 msi
Adding cabinet file to msi programatically with DTF (wix)
手头任务介绍: 不耐烦可略过
我工作的公司不是软件公司,而是专注于机械和热力学工程问题。
为了帮助解决他们的系统设计挑战,他们开发了一种软件来计算更换单个组件对系统的影响。
该软件已经很老了,用 FORTRAN 编写,已经发展了 30 年,这意味着我们无法快速重写或更新它。
正如您想象的那样,该软件的安装方式也发生了变化,但比系统的其他部分慢得多,这意味着打包是由一个批处理脚本完成的,该脚本从不同的地方收集文件,并将它们放在一个文件夹,然后将其编译成 iso,刻录到 CD,并随邮件发送。
你们这些年轻的程序员(我 30 岁),可能希望程序加载 dll,但在链接后会相当独立。即使代码由多个 类,来自不同的命名空间等组成。
但是在 FORTRAN 70 中.. 没那么多。这意味着它本身的软件包含对预构建模块的惊人数量的调用(阅读:seperate programs)..
我们需要能够通过 Internet 进行分发,就像任何其他现代公司已经有一段时间能够做到的那样。为此,我们可以让 *.iso 可下载吗?
嗯,不幸的是不,iso 包含几个用户特定的文件。
正如您想象的那样,如果有成千上万的用户,那将是成千上万个几乎相同的 iso。
此外,我们不想将旧的基于 FORTRAN 的安装软件转换为真正的安装包,我们所有其他(和更现代的)程序都是打包为 MSI 的 C# 程序..
但是在我们的服务器上使用这个旧软件编译单个 msi 的时间接近 10 秒,因此当用户请求时,我们根本无法构建 msi。 (如果多个用户同时请求,服务器将无法在请求超时前完成。)
我们也不能预先构建用户特定的 msi 并缓存它们,因为我们会 运行 服务器内存不足..(每个发布版本总计约 15 GB)
任务描述 tl:dr;
以下是我想做的事情:(灵感来自 Christopher Painter 的评论)
- 创建一个基本 MSI,使用虚拟文件而不是用户特定文件
- 使用用户特定文件为每个用户创建 cab 文件
- 在请求时使用“_Stream”将用户特定的 cab 文件注入到基本 msi 的临时副本中 table.
- 将引用插入到媒体 table 中,其中包含一个新的 'DiskID' 和一个 'LastSequence' 对应于额外的文件,以及注入的 cab 文件的名称。
- 使用新 cab 文件中用户特定文件的名称、新序列号(在新 cab 文件序列范围内)和文件大小更新文件table。
问题
我的代码无法完成刚刚描述的任务。我可以从 msi 读取很好,但从未插入 cabinet 文件。
还有:
如果我以 DIRECT 模式打开 msi,它会破坏媒体 table,如果我以 TRANSACTION 模式打开它,它根本无法改变任何东西..
在直接模式下,媒体 table 中的现有行被替换为:
DiskId: 1
LastSequence: -2145157118
Cabinet: "Name of action to invoke, either in the engine or the handler DLL."
我做错了什么?
下面我提供了涉及注入新 cab 文件的片段。
片段 1
public string createCabinetFileForMSI(string workdir, List<string> filesToArchive)
{
//create temporary cabinet file at this path:
string GUID = Guid.NewGuid().ToString();
string cabFile = GUID + ".cab";
string cabFilePath = Path.Combine(workdir, cabFile);
//create a instance of Microsoft.Deployment.Compression.Cab.CabInfo
//which provides file-based operations on the cabinet file
CabInfo cab = new CabInfo(cabFilePath);
//create a list with files and add them to a cab file
//now an argument, but previously this was used as test:
//List<string> filesToArchive = new List<string>() { @"C:\file1", @"C:\file2" };
cab.PackFiles(workdir, filesToArchive, filesToArchive);
//we will ned the path for this file, when adding it to an msi..
return cabFile;
}
片段 2
public int insertCabFileAsNewMediaInMSI(string cabFilePath, string pathToMSIFile, int numberOfFilesInCabinet = -1)
{
//open the MSI package for editing
pkg = new InstallPackage(pathToMSIFile, DatabaseOpenMode.Direct); //have also tried direct, while database was corrupted when writing.
return insertCabFileAsNewMediaInMSI(cabFilePath, numberOfFilesInCabinet);
}
片段 3
public int insertCabFileAsNewMediaInMSI(string cabFilePath, int numberOfFilesInCabinet = -1)
{
if (pkg == null)
{
throw new Exception("Cannot insert cabinet file into non-existing MSI package. Please Supply a path to the MSI package");
}
int numberOfFilesToAdd = numberOfFilesInCabinet;
if (numberOfFilesInCabinet < 0)
{
CabInfo cab = new CabInfo(cabFilePath);
numberOfFilesToAdd = cab.GetFiles().Count;
}
//create a cab file record as a stream (embeddable into an MSI)
Record cabRec = new Record(1);
cabRec.SetStream(1, cabFilePath);
/*The Media table describes the set of disks that make up the source media for the installation.
we want to add one, after all the others
DiskId - Determines the sort order for the table. This number must be equal to or greater than 1,
for out new cab file, it must be > than the existing ones...
*/
//the baby SQL service in the MSI does not support "ORDER BY `` DESC" but does support order by..
IList<int> mediaIDs = pkg.ExecuteIntegerQuery("SELECT `DiskId` FROM `Media` ORDER BY `DiskId`");
int lastIndex = mediaIDs.Count - 1;
int DiskId = mediaIDs.ElementAt(lastIndex) + 1;
//wix name conventions of embedded cab files is "#cab" + DiskId + ".cab"
string mediaCabinet = "cab" + DiskId.ToString() + ".cab";
//The _Streams table lists embedded OLE data streams.
//This is a temporary table, created only when referenced by a SQL statement.
string query = "INSERT INTO `_Streams` (`Name`, `Data`) VALUES ('" + mediaCabinet + "', ?)";
pkg.Execute(query, cabRec);
Console.WriteLine(query);
/*LastSequence - File sequence number for the last file for this new media.
The numbers in the LastSequence column specify which of the files in the File table
are found on a particular source disk.
Each source disk contains all files with sequence numbers (as shown in the Sequence column of the File table)
less than or equal to the value in the LastSequence column, and greater than the LastSequence value of the previous disk
(or greater than 0, for the first entry in the Media table).
This number must be non-negative; the maximum limit is 32767 files.
/MSDN
*/
IList<int> sequences = pkg.ExecuteIntegerQuery("SELECT `LastSequence` FROM `Media` ORDER BY `LastSequence`");
lastIndex = sequences.Count - 1;
int LastSequence = sequences.ElementAt(lastIndex) + numberOfFilesToAdd;
query = "INSERT INTO `Media` (`DiskId`, `LastSequence`, `Cabinet`) VALUES (" + DiskId.ToString() + "," + LastSequence.ToString() + ",'#" + mediaCabinet + "')";
Console.WriteLine(query);
pkg.Execute(query);
return DiskId;
}
更新:愚蠢的我,忘记了 "committing" 在交易模式下 - 但现在它和直接模式下一样,所以问题没有真正的改变。
我会自己回答这个问题,因为我刚刚了解了一些我以前不知道的有关 DIRECT 模式的知识,并且不想将其保留在这里以便最终重新 google..
显然,只有在程序最终崩溃之前关闭数据库句柄,我们才能成功更新 MSI。
为了回答问题,这个析构函数应该这样做。
~className()
{
if (pkg != null)
{
try
{
pkg.Close();
}
catch (Exception ex)
{
//rollback not included as we edit directly?
//do nothing..
//atm. we just don't want to break anything if database was already closed, without dereferencing
}
}
}
添加正确的关闭语句后,MSI 变大了
(并且媒体行已添加到媒体 table :))
我将 post 整个 class 来解决这个任务,当它完成并经过测试时,
但我会在 SO 的相关问题中这样做。
the related question on SO
手头任务介绍: 不耐烦可略过
我工作的公司不是软件公司,而是专注于机械和热力学工程问题。 为了帮助解决他们的系统设计挑战,他们开发了一种软件来计算更换单个组件对系统的影响。 该软件已经很老了,用 FORTRAN 编写,已经发展了 30 年,这意味着我们无法快速重写或更新它。
正如您想象的那样,该软件的安装方式也发生了变化,但比系统的其他部分慢得多,这意味着打包是由一个批处理脚本完成的,该脚本从不同的地方收集文件,并将它们放在一个文件夹,然后将其编译成 iso,刻录到 CD,并随邮件发送。
你们这些年轻的程序员(我 30 岁),可能希望程序加载 dll,但在链接后会相当独立。即使代码由多个 类,来自不同的命名空间等组成。
但是在 FORTRAN 70 中.. 没那么多。这意味着它本身的软件包含对预构建模块的惊人数量的调用(阅读:seperate programs)..
我们需要能够通过 Internet 进行分发,就像任何其他现代公司已经有一段时间能够做到的那样。为此,我们可以让 *.iso 可下载吗?
嗯,不幸的是不,iso 包含几个用户特定的文件。 正如您想象的那样,如果有成千上万的用户,那将是成千上万个几乎相同的 iso。
此外,我们不想将旧的基于 FORTRAN 的安装软件转换为真正的安装包,我们所有其他(和更现代的)程序都是打包为 MSI 的 C# 程序.. 但是在我们的服务器上使用这个旧软件编译单个 msi 的时间接近 10 秒,因此当用户请求时,我们根本无法构建 msi。 (如果多个用户同时请求,服务器将无法在请求超时前完成。) 我们也不能预先构建用户特定的 msi 并缓存它们,因为我们会 运行 服务器内存不足..(每个发布版本总计约 15 GB)
任务描述 tl:dr;
以下是我想做的事情:(灵感来自 Christopher Painter 的评论)
- 创建一个基本 MSI,使用虚拟文件而不是用户特定文件
- 使用用户特定文件为每个用户创建 cab 文件
- 在请求时使用“_Stream”将用户特定的 cab 文件注入到基本 msi 的临时副本中 table.
- 将引用插入到媒体 table 中,其中包含一个新的 'DiskID' 和一个 'LastSequence' 对应于额外的文件,以及注入的 cab 文件的名称。
- 使用新 cab 文件中用户特定文件的名称、新序列号(在新 cab 文件序列范围内)和文件大小更新文件table。
问题
我的代码无法完成刚刚描述的任务。我可以从 msi 读取很好,但从未插入 cabinet 文件。
还有:
如果我以 DIRECT 模式打开 msi,它会破坏媒体 table,如果我以 TRANSACTION 模式打开它,它根本无法改变任何东西..
在直接模式下,媒体 table 中的现有行被替换为:
DiskId: 1
LastSequence: -2145157118
Cabinet: "Name of action to invoke, either in the engine or the handler DLL."
我做错了什么?
下面我提供了涉及注入新 cab 文件的片段。
片段 1
public string createCabinetFileForMSI(string workdir, List<string> filesToArchive)
{
//create temporary cabinet file at this path:
string GUID = Guid.NewGuid().ToString();
string cabFile = GUID + ".cab";
string cabFilePath = Path.Combine(workdir, cabFile);
//create a instance of Microsoft.Deployment.Compression.Cab.CabInfo
//which provides file-based operations on the cabinet file
CabInfo cab = new CabInfo(cabFilePath);
//create a list with files and add them to a cab file
//now an argument, but previously this was used as test:
//List<string> filesToArchive = new List<string>() { @"C:\file1", @"C:\file2" };
cab.PackFiles(workdir, filesToArchive, filesToArchive);
//we will ned the path for this file, when adding it to an msi..
return cabFile;
}
片段 2
public int insertCabFileAsNewMediaInMSI(string cabFilePath, string pathToMSIFile, int numberOfFilesInCabinet = -1)
{
//open the MSI package for editing
pkg = new InstallPackage(pathToMSIFile, DatabaseOpenMode.Direct); //have also tried direct, while database was corrupted when writing.
return insertCabFileAsNewMediaInMSI(cabFilePath, numberOfFilesInCabinet);
}
片段 3
public int insertCabFileAsNewMediaInMSI(string cabFilePath, int numberOfFilesInCabinet = -1)
{
if (pkg == null)
{
throw new Exception("Cannot insert cabinet file into non-existing MSI package. Please Supply a path to the MSI package");
}
int numberOfFilesToAdd = numberOfFilesInCabinet;
if (numberOfFilesInCabinet < 0)
{
CabInfo cab = new CabInfo(cabFilePath);
numberOfFilesToAdd = cab.GetFiles().Count;
}
//create a cab file record as a stream (embeddable into an MSI)
Record cabRec = new Record(1);
cabRec.SetStream(1, cabFilePath);
/*The Media table describes the set of disks that make up the source media for the installation.
we want to add one, after all the others
DiskId - Determines the sort order for the table. This number must be equal to or greater than 1,
for out new cab file, it must be > than the existing ones...
*/
//the baby SQL service in the MSI does not support "ORDER BY `` DESC" but does support order by..
IList<int> mediaIDs = pkg.ExecuteIntegerQuery("SELECT `DiskId` FROM `Media` ORDER BY `DiskId`");
int lastIndex = mediaIDs.Count - 1;
int DiskId = mediaIDs.ElementAt(lastIndex) + 1;
//wix name conventions of embedded cab files is "#cab" + DiskId + ".cab"
string mediaCabinet = "cab" + DiskId.ToString() + ".cab";
//The _Streams table lists embedded OLE data streams.
//This is a temporary table, created only when referenced by a SQL statement.
string query = "INSERT INTO `_Streams` (`Name`, `Data`) VALUES ('" + mediaCabinet + "', ?)";
pkg.Execute(query, cabRec);
Console.WriteLine(query);
/*LastSequence - File sequence number for the last file for this new media.
The numbers in the LastSequence column specify which of the files in the File table
are found on a particular source disk.
Each source disk contains all files with sequence numbers (as shown in the Sequence column of the File table)
less than or equal to the value in the LastSequence column, and greater than the LastSequence value of the previous disk
(or greater than 0, for the first entry in the Media table).
This number must be non-negative; the maximum limit is 32767 files.
/MSDN
*/
IList<int> sequences = pkg.ExecuteIntegerQuery("SELECT `LastSequence` FROM `Media` ORDER BY `LastSequence`");
lastIndex = sequences.Count - 1;
int LastSequence = sequences.ElementAt(lastIndex) + numberOfFilesToAdd;
query = "INSERT INTO `Media` (`DiskId`, `LastSequence`, `Cabinet`) VALUES (" + DiskId.ToString() + "," + LastSequence.ToString() + ",'#" + mediaCabinet + "')";
Console.WriteLine(query);
pkg.Execute(query);
return DiskId;
}
更新:愚蠢的我,忘记了 "committing" 在交易模式下 - 但现在它和直接模式下一样,所以问题没有真正的改变。
我会自己回答这个问题,因为我刚刚了解了一些我以前不知道的有关 DIRECT 模式的知识,并且不想将其保留在这里以便最终重新 google..
显然,只有在程序最终崩溃之前关闭数据库句柄,我们才能成功更新 MSI。
为了回答问题,这个析构函数应该这样做。
~className()
{
if (pkg != null)
{
try
{
pkg.Close();
}
catch (Exception ex)
{
//rollback not included as we edit directly?
//do nothing..
//atm. we just don't want to break anything if database was already closed, without dereferencing
}
}
}
添加正确的关闭语句后,MSI 变大了 (并且媒体行已添加到媒体 table :))
我将 post 整个 class 来解决这个任务,当它完成并经过测试时, 但我会在 SO 的相关问题中这样做。 the related question on SO