1
votes

I'd like to insert cab file in my msi package at installation time. Is this possible? There is immediate custom action:

UINT __stdcall CreateDataCab(MSIHANDLE hInstall) {
    MSIHANDLE hRec = MsiCreateRecord(1);
    UINT status = MsiRecordSetStream(hRec, 1, T("C:\\work\\Data2.cab"));

    MSIHANDLE hDb = MsiGetActiveDatabase(hInstall);
    MSIHANDLE hView = 0;
    status = MsiDatabaseOpenView(hDb, _T("INSERT INTO `_Streams` (`Name`, `Data`) VALUES ('Data2.cab', ?)"), &hView);
    status = MsiViewExecute(hView, hRec);
    status = MsiViewClose(hView);
    status = MsiCloseHandle(hView);
    status = MsiCloseHandle(hRec);
    status = MsiCloseHandle(hDb);
}

It is executed at the very beginning of INSTALL action. Value of status variable is always ERROR_SUCCESS. So everything seems to be fine. However in msi log I can see error triggered:

Action start 14:29:27: INSTALL.
MSI (s) (4C:14) [14:29:27:049]: Running ExecuteSequence
MSI (s) (4C:14) [14:29:27:049]: Doing action: SetSilentInstall
Action start 14:29:27: SetSilentInstall.
MSI (s) (4C:14) [14:29:27:051]: PROPERTY CHANGE: Adding SILENT property. Its value is '/s'.
Action ended 14:29:27: SetSilentInstall. Return value 1.
MSI (s) (4C:14) [14:29:27:052]: Doing action: CreateDataCab
Action start 14:29:27: CreateDataCab.
MSI (s) (4C:1C) [14:29:27:072]: Invoking remote custom action. DLL: C:\windows\Installer\MSI3EC2.tmp, Entrypoint: CreateDataCab
MSI (s) (4C:B4) [14:29:27:074]: Generating random cookie.
MSI (s) (4C:B4) [14:29:27:079]: Created Custom Action Server with PID 32604 (0x7F5C).
MSI (s) (4C:14) [14:29:27:147]: Running as a service.
MSI (s) (4C:14) [14:29:27:151]: Hello, I'm your 64bit Impersonated custom action server.
MSI (s) (4C!50) [14:32:22:069]: Note: 1: 2263 2: Data2.cab 3: -2147287035 

Note: 1: 2263 2: Data2.cab 3: -2147287035 log record indicates MSI error 2263 Could not open stream [2]. occured in custom action. Hex value of -2147287035 is FFFFFFFF80030005 which is likely to be STG_E_ACCESSDENIED HRESULT - Access Denied. My first guess for this error was that MSI service can't access source file when trying to fetch it into the record. I relaxed permissions of C:\work\Data2.cab but no luck. Then I decided to use path for not existing file in MsiRecordSetStream() call. With this change MsiRecordSetStream() call returned 161 error. As there is no branching in CreateDataCab() it get executed up to the end and all MSI functions after MsiRecordSetStream() succeeded. msi log:

Action start 16:11:02: CreateDataCab.
MSI (s) (4C:D8) [16:11:02:596]: Invoking remote custom action. DLL: C:\windows\Installer\MSI4154.tmp, Entrypoint: JreCreateDataCab
MSI (s) (4C:54) [16:11:02:597]: Generating random cookie.
MSI (s) (4C:54) [16:11:02:605]: Created Custom Action Server with PID 34668 (0x876C).
MSI (s) (4C:6C) [16:11:02:682]: Running as a service.
MSI (s) (4C:6C) [16:11:02:684]: Hello, I'm your 64bit Impersonated custom action server.
MSI (s) (4C!74) [16:11:18:565]: Note: 1: 1101 2: C:\work\foo.cab 3: 2 
MSI (s) (4C!74) [16:15:08:291]: Note: 1: 2263 2: Data2.cab 3: -2147287035

Note: 1: 1101 2: C:\work\Data2.cab 3: 2 indicates MSI error 1101 Could not open file stream: [2] occured. This is extra error in the msi log, Note: 1: 2263 2: Data2.cab 3: -2147287035 still there.

Stepping in CreateDataCab() with debugger resulted in the following observations:

  1. Note: 1: 1101 2: C:\work\foo.cab 3: 2 error is added to the log right after failed MsiRecordSetStream() call when it returns error 161.
  2. Note: 1: 2263 2: Data2.cab 3: -2147287035 error is added to the log right after successful MsiCloseHandle(hView) call.
  3. Dumping contents of _Streams table at the beginning of CreateDataCab() and at the end indicates that the new record is added to the table successfully.

This is what I have at the moment. Seems like the following happens:

  1. MsiRecordSetStream() for the new record works fine. System can fetch file into the record if correct path is specified;
  2. SQL request to insert new record to _Streams table succeeds;
  3. Data field of newly added record contains no data.

I'm confused that MSI functions executed in custom action all pass but there is error in msi log file. Also changes to _Streams table are partially successful: new record added, but the record contains no data. Seems like it is potentially possible to update _Streams table at run-time, but something goes wrong with storing data in the table.

I have no issues with updating _Streams in msi files at build time. All works fine. But doing this at run-time from custom action is broken for me. Am I missing something in my CA function? Is it ever legal to alter _Streams at run-time?

UPD: I made some more investigation of the problem. I reworked my msi to call CreateDataCab() twice from two CA. Also modified CreateDataCab() itself. Pseudo code is following:

CreateDataCab() {
    dumpStream();

    openDb;
    _Streams["Data1.cab"].Data = "c:/work/Data1.cab";
    commitDb;
    closeDb;

    dumpStream();
}

dumpStream() {
    openDb;
    if ("Data1.cab" in _Streams) {
        save _Streams["Data1.cab"].Data to "c:/work/saved.cab";
    }
    closeDb;
}

When CreateDataCab() is called for the first CA the first dumpStream() outputs nothing, but the second on writes something to c:/work/saved.cab. This file appears binary equal to c:/work/Data1.cab which was used as a source for record stream. This indicates that stream data can be updated in _Streams table just fine. The data persists and remains in the db not to see that it was inserted and retrieved in two different MsiGetActiveDatabase()/MsiDatabaseClose() scopes. Very nice, however Note: 1: 2263 2: Data2.cab 3: -2147287035 is still in the log.

When CreateDataCab() is called for the second CA things are not so nice. The first dumpStream() empties c:/work/saved.cab file. This means that Data1.cab record is still in _Streams table, but with empty stream data.

As it was noted that _Streams table is special I experimented with Binary table. Attempt to modify existing record resulted in Note: 1: 2259 2: 3: 4: error (Database: [2] Table(s) Update failed), i.e. "UPDATE Binary SET Data = ? WHERE Name='Data1.cab'" SQL query doesn't work. So I reworked CA code to use MsiViewModify(hView, MSIMODIFY_INSERT_TEMPORARY, hRec) and removed Data1.cab record from Binary table before running installation.

The first CreateDataCab() CA worked fine: insert OK, no errors in msi log, the second dumpStream() OK. However when CreateDataCab() was called for the second CA, the first dumpStream() dumped zero bytes and subsequent MsiViewModify(hView, MSIMODIFY_INSERT_TEMPORARY, hRec) failed.

Results for _Streams and Binary table are identical:

  1. Inserted records persist across CA calls.
  2. Stream data in added records doesn't persist across CA calls and set zero length in the second CA.

Inserting new temporary records in Binary table results in no error messages in msi log. The good news is that it is possible to alter records with stream data from CA. Not so bad news is that in Binary table it is impossible to change data in existing records. Bad news is that adding/changing stream data corrupts db.

I'll be very happy if somebody can challenge my conclusions.

1

1 Answers

1
votes

At run time, the database is read only. You have to use INSERT ... TEMPORARY instead of just INSERT, and I'm not certain whether it will work for the _Streams table. (I also generally have better luck with using the record-based API instead of the SQL-based API, i.e. open a view to the table, build a record, and then in this case MsiViewModify(hView, MSIMODIFY_INSERT_TEMPORARY, hRec); but I don't think that's the problem here.)

That said, this use case sounds extremely unusual to me, so I'd be quite wary. Because the record is temporary, you will have to do this whenever the MSI is run - not just first time installation. Be sure to test all the scenarios you will want to support, including uninstall, repair, and any upgrade approaches. Especially if you may deliver minor upgrade patches.