Jump to content


This is a ready-only archive of the InstallSite Forum. You cannot post any new content here. / Dies ist ein Archiv des InstallSite Forums. Hier können keine neuen Beiträge veröffentlicht werden.
Photo

Install one or many identical files to many folders


Best Answer deramor , 30 December 2016 - 01:45

I have good news.

The reason I was getting the 267 error was due to when the CA was scheduled.

 

From a scheduling perspective, the new rows to the directory table must be made before Costing starts.

Setting the values for those new entries must happen after Costing ends.

 

A summary of my implementation is:

 

CA1 is in the Execute Sequence, Synchronous, Immediate, Before Cost Init.
Given a list of folders, it creates directory table entries using MsiDatabaseOpenView and MsiViewModify

CA2 is also in the Execute Sequence, Synchronous, Immediate, After Cost Finalize.
Given the same list of folders, assigns a value to each of the new directory table entries.
Then creates DuplicateFile table entries with each of those DirectoryTable entries using MsiDatabaseOpenView and MsiViewModify.

 

Sample code for those interested is:

 

CA1

Given a list of folder values stored in lPaths list.

// Get a handle to the MSI
	hDatabase = MsiGetActiveDatabase(hMSI);
	
	nListResult = ListGetFirstString(lPaths, svString);
	i = 1;
	 
	while (nListResult != END_OF_LIST)
		
		NumToStr(svTempNumber, i);
		
		svDirectoryKey = "DIRECTORYKEY" + svTempNumber;
		
		sQuery = "SELECT * FROM `Directory`";
		
		nResult = MsiDatabaseOpenView(hDatabase, sQuery, hDirectoryView);
		
		//////////////////////////////////////////////////////////////////
		// Add record svDirectoryKey to the directory table
		hRecord = MsiCreateRecord(3);
		
		// Column 1 Directory
		nResult = MsiRecordSetString( hRecord, 1, svDirectoryKey);
		// Column 2 Directory Parent
		nResult = MsiRecordSetString( hRecord, 2, "TARGETDIR");
		// Column 3 Default Directory
		nResult = MsiRecordSetString( hRecord, 3, "DIRECT~" + svTempNumber + "|DIRECTORYKEY" + svTempNumber);
		
		// Add the new record to the table
		nResult = MsiViewModify(hDirectoryView, MSIMODIFY_INSERT_TEMPORARY, hRecord);
		
		MsiCloseHandle (hRecord);
		MsiViewClose (hDirectoryView);
		
		i++;
		
		nListResult = ListGetNextString(lPaths, svString);
		
	endwhile;
	
	MsiCloseHandle (hDatabase);
	ListDestroy (lPaths);

CA2

Given a list of folder values stored in lPaths list.

// Get a handle to the MSI
	hDatabase = MsiGetActiveDatabase(hMSI);
	
	nListResult = ListGetFirstString(lPaths, svString);
	i = 1;
	 
	while (nListResult != END_OF_LIST)
		
		NumToStr(svTempNumber, i);
		
		svDirectoryKey = "DIRECTORYKEY" + svTempNumber;
		
		// Set the directory table entry to the path in the list.
		nResult = MsiSetTargetPath( hMSI, svDirectoryKey, svString );
		
		//////////////////////////////////////////////////////////////////
		// Now wright the duplicate file table entry.
		
		sQuery = "SELECT * FROM `DuplicateFile`";
		
		nResult = MsiDatabaseOpenView(hDatabase, sQuery, hDuplicateView);
	
		hRecord = MsiCreateRecord(5);
	
		NumToStr(svFileKeyModifier, i);
		
		svFileKey = "DuplicateFile" + svFileKeyModifier;
		
		nResult = MsiRecordSetString( hRecord, 1, svFileKey );
		
		nResult = MsiRecordSetString( hRecord, 2, "<THE COMPONENT TO USE AS A TRIGGER>" );
		
		nResult = MsiRecordSetString( hRecord, 3, "<YOUR FILE KEY HERE>" );
	
		nResult = MsiRecordSetString( hRecord, 4, "" );
		
		nResult = MsiRecordSetString( hRecord, 5, svDirectoryKey );
		
		// Write the updated row
		nResult = MsiViewModify(hDuplicateView, MSIMODIFY_INSERT_TEMPORARY, hRecord);
        
        MsiCloseHandle (hRecord);
        MsiViewClose (hDuplicateView);
        		
		i++;
		
		nListResult = ListGetNextString(lPaths, svString);
		
	endwhile;
	
	MsiCloseHandle (hDatabase);
	ListDestroy (lPaths);

It is important to note that I also needed to implement Stephan's suggested change to my Settings.xml file.

I added <DuplicateFile MSI="Keep" MSM="Keep"/> to the section <EmptyTableDisposition MSI="Drop" MSM="Drop">

 

I suppose I could have alternatively just changed the defaults to Keep and Keep but I would rather not change things do dramatically.

 

Lastly, I had to make sure both the Directory table edits and the DuplicateFIle table edits were made on both install and uninstall.  If not uninstall, the duplicate files would be orphaned. 

 

The only limitation here is if the list of locations become shorter between install and removal, some files will not get removed.

I see no way around this at present but I don't think this is a show stopper and can be handled in documentation.

Go to the full post


6 replies to this topic

deramor

deramor
  • Full Members
  • 187 posts

Posted 18 October 2016 - 19:48

Hello all,

 

I am trying to figure out if some functionality is possible in Windows Installer.

 

Assume I have a plug-in to Visual Studio I want to make available to all installed versions of Visual Studio.

 

Is it possible to do this by including only one copy of the plug-in in my installer and have it applied to many copies of Visual Studio?  Both known and possible future versions assuming Microsoft follows the same versioning and registration scheme in the registry?

 

If any of you are familiar with .Net Reflector, it does something to install itself under many Visual Studio versions.

 

I don't know if all this logic was hard coded or if it is dynamic.  I would prefer to not have hard coded features and components for every version of Visual Studio I know about.  It would force me to update the installer in order to support a new version of VS.

 

Is this possible?  Any thoughts?



Stefan Krueger

Stefan Krueger

    InstallSite.org

  • Administrators
  • 13,269 posts

Posted 24 October 2016 - 17:48

The DuplicateFile table might help but you may need to add rows dynamically



deramor

deramor
  • Full Members
  • 187 posts

Posted 21 December 2016 - 02:02

Sorry for the delay.  My priorities were changed.

 

I looked into the duplicate file table and it would appear to be the functionality that I am looking for.

I am running into some issues creating the custom action to dynamically modify the table at runtime.

As a proof of concept, I created the following Installscript code:

function MyFunction(hMSI)
LIST lPaths;
HWND hDatabase, hCBOView, hRec;
STRING sQuery, svString, svFileKey, svFileKeyModifier;
NUMBER nResult, i; 
begin

	lPaths = ListCreate(STRINGLIST);
	
	ListAddString(lPaths, "C:\\Users\\deramor\\Documents\\DuplicateFilesTest\\Test1", AFTER);
	ListAddString(lPaths, "C:\\Users\\deramor\\Documents\\DuplicateFilesTest\\Test2", AFTER);
	ListAddString(lPaths, "C:\\Users\\deramor\\Documents\\DuplicateFilesTest\\Test3", AFTER);
	ListAddString(lPaths, "C:\\Users\\deramor\\Documents\\DuplicateFilesTest\\Test4", AFTER);
	
	
	hDatabase = MsiGetActiveDatabase(hMSI);
	
	sQuery = "SELECT * FROM DuplicateFile";
	// WHERE Property='DUPLICATEFILEPROP'
	nResult = MsiDatabaseOpenView(hDatabase, sQuery, hCBOView);
	
	if(nResult == ERROR_BAD_QUERY_SYNTAX || ERROR_INVALID_HANDLE) then
		MessageBox("Error in MsiDatabaseOpenView", SEVERE);
	endif;
	
	hRec = MsiCreateRecord(5);
	
	nResult = ListGetFirstString(lPaths, svString);
	
	i = 1;
	
	while (nResult != END_OF_LIST)
		NumToStr(svFileKeyModifier, i);
		
		svFileKey = "DpulicateFile" + svFileKeyModifier;
		
		MsiRecordSetString( hRec, 1, svFileKey );
		
		MsiRecordSetString( hRec, 2, "NewComponent1" );
		
		MsiRecordSetString( hRec, 3, "teradynereplacementandsparep" );
	
		MsiRecordSetString( hRec, 4, "" );
		
		MsiRecordSetString( hRec, 5, svString );
		
		// Write the updated row
		MsiViewModify(hCBOView, MSIMODIFY_INSERT_TEMPORARY, hRec);
        		
		i++;
		
		nResult = ListGetNextString(lPaths, svString);
		
	endwhile;
		
end;

I am getting 1615 back from MsiDatabaseOpenView meaning ERROR_BAD_QUERY_SYNTAX.

So I can not get too far with this.  Anyone have a suggestion on what the correct SQL query would be to select the DuplicateFile table in the MSI?

This code was adapted from a similar process I used to dynamically populate a combobox in a different installer.

 

My second question is when should an action like this be scheduled?  Currently I have it between Cost Init and Cost Finalize as Immediate execution.


Edited by deramor, 21 December 2016 - 02:16.


deramor

deramor
  • Full Members
  • 187 posts

Posted 22 December 2016 - 01:27

Quick update.  Looks like you can't select a table that has no records since the Installshield build process (and maybe wix as well) omits empty tables.  Opening a view to a table that doesn't exist will generate an error. (ERROR_BAD_QUERY_SYNTAX)

I suppose it would make more sense to have additional error codes but then I never tried to get extended error information with MsiGetLastErrorRecord().

 

A side note, I manually populated the DuplicateFile table and saw the behavior work as Stephan predicted.  After running a variant of the code above, I was also able to open a view to the table.  I am now more confident that this is a real solution.  I just need to work through the syntax and algorithm.  Hopefully I can share this with the community since google searches are not very helpful.  Stay tuned.



Stefan Krueger

Stefan Krueger

    InstallSite.org

  • Administrators
  • 13,269 posts

Posted 22 December 2016 - 16:47

Thanks for the update.

I don't think there's a checkbox in the IDE for this but you could look at Settings.xml in C:\Program Files (x86)\InstallShield\2016\Support\0409. It has a section <EmptyTableDisposition MSI="Drop" MSM="Drop"> after which you can list exceptions, i.e. which empty tables you want to keep.



deramor

deramor
  • Full Members
  • 187 posts

Posted 27 December 2016 - 17:56

That works thank you.  I no longer have to keep a dummy entry in the table only to be forced to delete it later. Just working through the logic now to add both directory entries and then DuplicateFile entries to get a working prototype.

 

Edit:

Consider the following code:

	// Get a handle to the MSI
	hDatabase = MsiGetActiveDatabase(hMSI);
	
	nListResult = ListGetFirstString(lPaths, svString);
	i = 1;
	
	while (nListResult != END_OF_LIST)
		
		NumToStr(svTempNumber, i);
		
		sQuery = "SELECT * FROM `Directory`";
		
		// Create a view object and run the query.
		nResult = MsiDatabaseOpenView(hDatabase, sQuery, hCBOView);
		
		// Add record svDirectoryKey + svTempNumber to the directory table
		hRec = MsiCreateRecord(3);
		
		svDirectoryKey = "DIRECTORYKEY";
		
		nResult = MsiRecordSetString( hRec, 1, svDirectoryKey + svTempNumber);
		nResult = MsiRecordSetString( hRec, 2, "TARGETDIR");
		nResult = MsiRecordSetString( hRec, 3, svDirectoryKey + svTempNumber);
		
		// Write the updated row
		nResult = MsiViewModify(hCBOView, MSIMODIFY_INSERT_TEMPORARY, hRec); //Not sure if it should be Temp or not.
		
		MsiCloseHandle (hRec);
		MsiCloseHandle (hCBOView);
		
		// Set the directory table entry to the path in the list.
		nResult = MsiSetTargetPath( hMSI, svDirectoryKey + svTempNumber, svString);

// some additional code and the endwhile, get next string and i++

I get an error when I attempt to set the path for the directory table entry.

ERROR_DIRECTORY or 267

nResult = MsiSetTargetPath( hMSI, svDirectoryKey + svTempNumber, svString);

 

The code above is doing:

 - For each Path in the list

 - Write a Directory table entry in the form DIRECTORYKEY1 to DIRECTORYKEYn

 - Set the path of the directory entry DIRECTORYKEY1 to the value found in the list.

 

Am I missing some critical step when updating the directory table?

I tried to use MsiViewModify with MSIMODIFY_INSERT instead of MSIMODIFY_INSERT_TEMPORARY but got errors.

 

Should the query instead be INSERT INTO?

If that is the case, should I just not use MsiCreateRecord and MsiRecordSetString and instead do it all in the query?

 

Something like:

INSERT INTO `Directory` (Directory, Directory_Parent, DefaultDir) VALUES (DIRECTORYKEYn, TARGETDIR, DIRECTORYKEYn)


Edited by deramor, 27 December 2016 - 21:36.


deramor

deramor
  • Full Members
  • 187 posts

Posted 30 December 2016 - 01:45   Best Answer

I have good news.

The reason I was getting the 267 error was due to when the CA was scheduled.

 

From a scheduling perspective, the new rows to the directory table must be made before Costing starts.

Setting the values for those new entries must happen after Costing ends.

 

A summary of my implementation is:

 

CA1 is in the Execute Sequence, Synchronous, Immediate, Before Cost Init.
Given a list of folders, it creates directory table entries using MsiDatabaseOpenView and MsiViewModify

CA2 is also in the Execute Sequence, Synchronous, Immediate, After Cost Finalize.
Given the same list of folders, assigns a value to each of the new directory table entries.
Then creates DuplicateFile table entries with each of those DirectoryTable entries using MsiDatabaseOpenView and MsiViewModify.

 

Sample code for those interested is:

 

CA1

Given a list of folder values stored in lPaths list.

// Get a handle to the MSI
	hDatabase = MsiGetActiveDatabase(hMSI);
	
	nListResult = ListGetFirstString(lPaths, svString);
	i = 1;
	 
	while (nListResult != END_OF_LIST)
		
		NumToStr(svTempNumber, i);
		
		svDirectoryKey = "DIRECTORYKEY" + svTempNumber;
		
		sQuery = "SELECT * FROM `Directory`";
		
		nResult = MsiDatabaseOpenView(hDatabase, sQuery, hDirectoryView);
		
		//////////////////////////////////////////////////////////////////
		// Add record svDirectoryKey to the directory table
		hRecord = MsiCreateRecord(3);
		
		// Column 1 Directory
		nResult = MsiRecordSetString( hRecord, 1, svDirectoryKey);
		// Column 2 Directory Parent
		nResult = MsiRecordSetString( hRecord, 2, "TARGETDIR");
		// Column 3 Default Directory
		nResult = MsiRecordSetString( hRecord, 3, "DIRECT~" + svTempNumber + "|DIRECTORYKEY" + svTempNumber);
		
		// Add the new record to the table
		nResult = MsiViewModify(hDirectoryView, MSIMODIFY_INSERT_TEMPORARY, hRecord);
		
		MsiCloseHandle (hRecord);
		MsiViewClose (hDirectoryView);
		
		i++;
		
		nListResult = ListGetNextString(lPaths, svString);
		
	endwhile;
	
	MsiCloseHandle (hDatabase);
	ListDestroy (lPaths);

CA2

Given a list of folder values stored in lPaths list.

// Get a handle to the MSI
	hDatabase = MsiGetActiveDatabase(hMSI);
	
	nListResult = ListGetFirstString(lPaths, svString);
	i = 1;
	 
	while (nListResult != END_OF_LIST)
		
		NumToStr(svTempNumber, i);
		
		svDirectoryKey = "DIRECTORYKEY" + svTempNumber;
		
		// Set the directory table entry to the path in the list.
		nResult = MsiSetTargetPath( hMSI, svDirectoryKey, svString );
		
		//////////////////////////////////////////////////////////////////
		// Now wright the duplicate file table entry.
		
		sQuery = "SELECT * FROM `DuplicateFile`";
		
		nResult = MsiDatabaseOpenView(hDatabase, sQuery, hDuplicateView);
	
		hRecord = MsiCreateRecord(5);
	
		NumToStr(svFileKeyModifier, i);
		
		svFileKey = "DuplicateFile" + svFileKeyModifier;
		
		nResult = MsiRecordSetString( hRecord, 1, svFileKey );
		
		nResult = MsiRecordSetString( hRecord, 2, "<THE COMPONENT TO USE AS A TRIGGER>" );
		
		nResult = MsiRecordSetString( hRecord, 3, "<YOUR FILE KEY HERE>" );
	
		nResult = MsiRecordSetString( hRecord, 4, "" );
		
		nResult = MsiRecordSetString( hRecord, 5, svDirectoryKey );
		
		// Write the updated row
		nResult = MsiViewModify(hDuplicateView, MSIMODIFY_INSERT_TEMPORARY, hRecord);
        
        MsiCloseHandle (hRecord);
        MsiViewClose (hDuplicateView);
        		
		i++;
		
		nListResult = ListGetNextString(lPaths, svString);
		
	endwhile;
	
	MsiCloseHandle (hDatabase);
	ListDestroy (lPaths);

It is important to note that I also needed to implement Stephan's suggested change to my Settings.xml file.

I added <DuplicateFile MSI="Keep" MSM="Keep"/> to the section <EmptyTableDisposition MSI="Drop" MSM="Drop">

 

I suppose I could have alternatively just changed the defaults to Keep and Keep but I would rather not change things do dramatically.

 

Lastly, I had to make sure both the Directory table edits and the DuplicateFIle table edits were made on both install and uninstall.  If not uninstall, the duplicate files would be orphaned. 

 

The only limitation here is if the list of locations become shorter between install and removal, some files will not get removed.

I see no way around this at present but I don't think this is a show stopper and can be handled in documentation.