How to Obtain the MSI 'UILevel' Property From a C++ Custom Action - c++

I am trying to get at the 'UILevel' MSI property from within a C++ custom action in order to determine whether or not the user is running in 'no UI mode', but am not having much luck. The function I am calling is passed the MSIHANDLE from a function which I export in my DLL (which may be either a 'deferred' or 'firstsequence' action). What I'm seeing is that MsiGetPropertyW is always returning ERROR_MORE_DATA and the trueLength field is always 0. Here is my code:
bool runningInNoUIMode(MSIHANDLE hInstall)
{
unsigned long nBufLen = 64UL;
WCHAR *wszValue = new WCHAR[nBufLen];
DWORD trueLength = 0UL;
UINT result = ::MsiGetPropertyW(hInstall, L"UILevel", L"", &trueLength); // Get the size of the property value first to see if there is enough storage allocated.
if (ERROR_MORE_DATA == result || nBufLen <= trueLength)
{
if (NULL != wszValue)
{
delete [] wszValue;
}
// Allocate more memory for the property adding one for the null terminator.
nBufLen = trueLength + 1;
wszValue = new WCHAR[nBufLen];
}
if (NULL == wszValue)
{
WcaLog(LOGMSG_STANDARD, "Unable to determine the user interface level the MSI is being run with because we were unable to allocate storage for accessing the 'UILevel' property.");
return false;
}
memset(wszValue, L'\0', nBufLen * sizeof(WCHAR));
result = ::MsiGetPropertyW(hInstall, L"UILevel", wszValue, &trueLength);
if (ERROR_SUCCESS != result)
{
WcaLog(LOGMSG_STANDARD, "Unable to determine the user interface level the MSI is being run with, error code = '%lu'.", result);
delete [] wszValue;
return false;
}
if (0 == wcscmp(L"2", wszValue)) // INSTALLUILEVEL_NONE == 2
{
delete [] wszValue;
return true;
}
delete [] wszValue;
return false;
}
I believe I can work around this for now by passing the 'UILevel' property through WiX and checking for it that way in the C++, but I am curious what the problem here is as well.
I'm using Visual Studio/Visual C++ 2010 on Windows 7 with WiX 3.5.2519.
Thanks for any assistance you can provide!

Another way of making this simpler is to use the MsiEvaluateCondition function.
BOOL bUI = MsiEvaluateCondition(L"UILevel<3");
in C# using Microsoft.Deployment.WindowsIntaller (DTF) it's:
var uiLevel = session["UILevel"];
In C++ there's a sample at MsiGetProperty function:
UINT __stdcall MyCustomAction(MSIHANDLE hInstall)
{
TCHAR* szValueBuf = NULL;
DWORD cchValueBuf = 0;
UINT uiStat = MsiGetProperty(hInstall, TEXT("MyProperty"), TEXT(""), &cchValueBuf);
//cchValueBuf now contains the size of the property's string, without null termination
if (ERROR_MORE_DATA == uiStat)
{
++cchValueBuf; // add 1 for null termination
szValueBuf = new TCHAR[cchValueBuf];
if (szValueBuf)
{
uiStat = MsiGetProperty(hInstall, TEXT("MyProperty"), szValueBuf, &cchValueBuf);
}
}
if (ERROR_SUCCESS != uiStat)
{
if (szValueBuf != NULL)
delete[] szValueBuf;
return ERROR_INSTALL_FAILURE;
}
// custom action uses MyProperty
// ...
delete[] szValueBuf;
return ERROR_SUCCESS;
}

Thanks to #DanielGehriger, we figured out that the problem isn't with the code, but with the scheduling for the custom action. The UILevel MSI property is simply not available when running a deferred custom action (I found that the code worked correctly for a custom action scheduled for firstsequence). I have worked around this limitation by explicitly passing it on custom action data using WiX:
<CustomAction Id="CustomAction.SetProperty" Property="CustomActionCall"
Value="UILEVEL=[UILevel];" />
and then checking for this in the C++ with WcaIsPropertySet and WcaGetProperty. Note that the character case of the property name between square brackets matters here.

Related

How to pass string array from VBScript to COM+

I have written a C++ COM which is running as COM+ application.
I am trying to access COM functionality from VBScript (ASP application).
I am able to call a function of COM from VBScript which takes a string. But when I try to call a COM function which takes an array of string, I could get length of array but I could not retrieve elements from that array at COM side.
VBScript (ASP application)
dim myComObj
Set myComObj = Server.CreateObject("ProgId_PerlCOMSimple.1")
Dim myArray(3)
myArray(0) = "Clean Underwear"
myArray(1) = "Vacuum Cleaner"
myArray(2) = "New Computer"
myArray(3) = "Talking Bass"
strDfStatus = myComObj.TestArray1 (myArray)
C++ COM which runs as COM+ application (through dllHost.exe)
STDMETHODIMP CPerlCOMSimple::TestArray1(VARIANT* testArray, LONG* lResult)
{
// TODO: Add your implementation code here
*lResult = testArray->parray->rgsabound->cElements;
BSTR** StrPtr = 0;
//LONG* pVals;
long LowerBound = 0;
long UpperBound = 0;
int i;
SafeArrayGetLBound(testArray->parray, 1, &LowerBound);
SafeArrayGetUBound(testArray->parray, 1, &UpperBound);
SafeArrayAccessData(testArray->parray, (void**)&pVals);
for (i = LowerBound; i <= UpperBound; ++i)
{
BSTR* lVal = StrPtr[i];
lVal++;
}
SafeArrayUnaccessData(testArray->parray);
return S_OK;
}
VBScript will not generate a SAFEARRAY with vartype VT_BSTR, which is what you are expecting. It will have VARTYPE VT_VARIANT.
// check all your parameters
if(testarray == null) return E_INVALIDARG;
if(testarray->vartype != VT_ARRAY|VT_BSTR
&& testarray->vartype != VT_ARRAY|VT_VARIANT)
return E_INVALIDARG;
if(testarray->parray == null) return E_INVALIDARG;
// Now we have established we have an array, and that it
// is either a string array or a variant array.
VARTYPE vt = VT_EMPTY;
SafeArrayGetVarType(testarray->pArray, &vt);
// Now we need to take different actions based on the vartype.
if(vt == VT_BSTR){
// we have an array of strings
// Proceed as above.
}else if(vt == VT_VARIANT){
// we have an array of variants, probably supplied by VBScript
// Read them one by one and use VariantChangeType to get a string
}else{
// We have some other array type we don't support
return E_INVALIDARG;
}

Creating a new Registery Key with a default string value

What I am ultimately trying to do is either add an entirely new subkey with a name called "XtraFileLogging" with some default value, currently it's "NOTHING!!!", or I want to extract the value from the subkey named "XtraFileLogging".
Why am I having to call SetValue after a call to SetKeyValue when trying to add an entirely new subkey?
The following code works but it seems like I am using an extra step with a call to SetValue seeing as how SetKeyValue does the following https://msdn.microsoft.com/en-us/library/e2ey539x.aspx
CRegKey oRegKey;
bool setKeyWorked = false;
if (oRegKey.Open(HKEY_LOCAL_MACHINE, _T("SOFTWARE\\EXTRA\\PROG2\\CONFIG")) == ERROR_SUCCESS)
{
LPCTSTR keyName = _T("XtraFileLogging");
LPCTSTR keysValue = _T("NOTHING!!!");
if (oRegKey.SetKeyValue(keyName, keysValue) == ERROR_SUCCESS)
{
setKeyWorked = true;
if (oRegKey.SetValue(keysValue, keyName) == ERROR_SUCCESS)
{
setKeyWorked = true;
}
else
{
setKeyWorked = false;
}
}
}
oRegKey.Close();
Why is it when i try either of the following SetKeyValue statements without the extra call to SetValue that the key doesn't show up?
if (oRegKey.SetKeyValue(keyName, keysValue) == ERROR_SUCCESS)
{
setKeyWorked = true;
}
Or
if (oRegKey.SetKeyValue(keyName, keysValue, keyName) == ERROR_SUCCESS)
{
setKeyWorked = true;
}
I even attach to the process and debug it and every time I can see that setKeyWorked is equal to true. Unfortunately without the extra call to SetValue afterwards the key never ends up getting created which I verified using regedit.

SHFileOperation cannot remove folder

I am writing a C++ Custom action for WiX that will be called during installation to remove any leftovers installed by the installer. Consider the following code:
UINT __stdcall DeleteResidue(MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
LPWSTR lpFolderPath = NULL;
std::wstring temp;
SHFILEOPSTRUCT shFile;
hr = WcaInitialize(hInstall, "DeleteResidue");
ExitOnFailure(hr, "Failed to initialize");
hr = WcaGetProperty(L"LPCOMMAPPDATAFOLDER",&lpFolderPath);
ExitOnFailure(hr, "Failure in Finding Common App Data Folder");
temp = std::wstring(lpFolderPath);
temp+=L"\0\0";
//Stop the LPA and LPA Monitor Service. Then delete the residue.
WcaLog(LOGMSG_STANDARD, "Doing Delete Residue");
ZeroMemory(&shFile, sizeof(shFile));
shFile.hwnd = NULL;
shFile.wFunc = FO_DELETE;
shFile.pFrom = temp.c_str();
shFile.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI;
BOOL res = DirectoryExists(lpFolderPath);
if(res)
{
WcaLog(LOGMSG_STANDARD, "The directory exist");
int result = SHFileOperation(&shFile);
if(!result)
WcaLog(LOGMSG_STANDARD, "The directory should have deleted by now");
else
WcaLog(LOGMSG_STANDARD, "The directory could not be deleted.Error code %d", result);
}
else
{
WcaLog(LOGMSG_STANDARD, "It Seems the Installed Folder is No more there");
}
LExit:
er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
return WcaFinalize(er);
}
In above code, we get C:\ProgramData inLPCOMMAPPDATAFOLDER. The doc states that pFrom should be double null terminated. However the return value of the code is 0x2 i.e. ERROR_FILE_NOT_FOUND. What is wrong in the code above?
You are not double nul terminating the pFrom.
You have a standard string (which includes the null terminator when you call .c_str() on it).
temp = std::wstring(lpFolderPath);
You then concatenate an empty string onto it:
temp+=L"\0\0";
This leaves the original string unchanged. This is because the std::string::operator+(const wchar_t*) takes a null terminated string. The fact that you have 2 nulls is immaterial it only reads up to the first null. It doesn't even add that null as what you've effectively given it is an empty string and the result of concatenating an empty string to something else is a no-op.
There's a few ways to solve this, but probably easiest is to change
temp+=L"\0\0";
to
temp.push_back(L'\0');
which explicitly adds another nul to the string so when you eventaully call temp.c_str() you'll get back the double nul-terminated string you need.

Why the CString(LPCTSTR lpsz) constructor check the high two bytes of lpsz?

I am reading the source code of CString in MFC. I am very curious about the implementation way of constructor CString::CString(LPCTSTR lpsz).
In my understanding, before copying the string indicated by lpsz, it only needs check whether lpsz is NULL but no need to combine with checking if HIWORD(lpsz) is NULL.
Is any MFC guy passing here and willing to give some explanations?
CString::CString(LPCTSTR lpsz)
{
Init();
if (lpsz != NULL && HIWORD(lpsz) == NULL)
{
UINT nID = LOWORD((DWORD)lpsz);
if (!LoadString(nID))
TRACE1("Warning: implicit LoadString(%u) failed\n", nID);
}
else
{
int nLen = SafeStrlen(lpsz);
if (nLen != 0)
{
AllocBuffer(nLen);
memcpy(m_pchData, lpsz, nLen*sizeof(TCHAR));
}
}
}
It checks whether it is passed an actual pointer or an integer resource identifier from MAKEINTRESOURCE. In the latter case it loads the string from the resources.
That is for loading a string resource. See the LoadString() call.

Why, after using 'CryptSetHashParam', can I no longer add data to my MD5 hash object?

I am trying to use the Microsoft 'Crypt...' functions to generate an MD5 hash key from the data that is added to the hash object. I am also trying to use the 'CryptSetHashParam' to set the hash object to a particular hash value before adding data to it.
According to the Microsoft documentation (if I am interpreting it correctly), you should be able to do this by creating a duplicate hash of the original object, use the 'CryptGetHashParam' function to retrieve the hash size then use 'CryptSetHashParam' on the original object to set the hash value accordingly. I am aware that after using 'CryptGetHashParam' you are unable to add additional data to a hash object (which is why I thought you needed to create a duplicate), but I can't add data to either the original hash object or the duplicate hash object after using either 'CryptGetHashParam' (as expected), or 'CryptSetHashParam' (which I didn't expect).
Below are code extracts of the class I am writing and an example of how I am using the class functions:
The result I get after running the code is:
"AddDataToHash function failed - Errorcode: 2148073484.", which translates to: "Hash not valid for use in specified state.".
I've tried many different ways to try and get this working as intended, but the result is always the same. I accept that I am doing something wrong, but I can't see what it is I'm doing wrong. Any ideas please?
CLASS CONSTRUCTOR INITIALISATION.
CAuthentication::CAuthentication()
{
m_dwLastError = ERROR_SUCCESS;
m_hCryptProv = NULL;
m_hHash = NULL;
m_hDuplicateHash = NULL;
if(!CryptAcquireContext(&m_hCryptProv, NULL, NULL, PROV_RSA_FULL, CRYPT_MACHINE_KEYSET))
{
m_dwLastError = GetLastError();
if (m_dwLastError == 0x80090016 )
{
if(!CryptAcquireContext(&m_hCryptProv, NULL, NULL, PROV_RSA_FULL, CRYPT_NEWKEYSET | CRYPT_MACHINE_KEYSET))
{
m_dwLastError = GetLastError();
m_hCryptProv = NULL;
}
}
}
if(!CryptCreateHash(m_hCryptProv, CALG_MD5, 0, 0, &m_hHash))
{
m_dwLastError = GetLastError();
m_hHash = NULL;
}
}
FUNCTION USED TO SET THE HASH VALUE OF THE HASH OBJECT.
bool CAuthentication::SetHashKeyString(char* pszKeyBuffer)
{
bool bHashStringSet = false;
DWORD dwHashSize = 0;
DWORD dwHashLen = sizeof(DWORD);
BYTE byHash[DIGITAL_SIGNATURE_LENGTH / 2]={0};
if(pszKeyBuffer != NULL && strlen(pszKeyBuffer) == DIGITAL_SIGNATURE_LENGTH)
{
if(CryptDuplicateHash(m_hHash, NULL, 0, &m_hDuplicateHash))
{
if(CryptGetHashParam(m_hDuplicateHash, HP_HASHSIZE, reinterpret_cast<BYTE*>(&dwHashSize), &dwHashLen, 0))
{
if (dwHashSize == DIGITAL_SIGNATURE_LENGTH / 2)
{
char*pPtr = pszKeyBuffer;
ULONG ulTempVal = 0;
for(ULONG ulIdx = 0; ulIdx < dwHashSize; ulIdx++)
{
sscanf(pPtr, "%02X", &ulTempVal);
byHash[ulIdx] = static_cast<BYTE>(ulTempVal);
pPtr+= 2;
}
if(CryptSetHashParam(m_hHash, HP_HASHVAL, &byHash[0], 0))
{
bHashStringSet = true;
}
else
{
pszKeyBuffer = "";
m_dwLastError = GetLastError();
}
}
}
else
{
m_dwLastError = GetLastError();
}
}
else
{
m_dwLastError = GetLastError();
}
}
if(m_hDuplicateHash != NULL)
{
CryptDestroyHash(m_hDuplicateHash);
}
return bHashStringSet;
}
FUNCTION USED TO ADD DATA FOR HASHING.
bool CAuthentication::AddDataToHash(BYTE* pbyHashBuffer, ULONG ulLength)
{
bool bHashDataAdded = false;
if(CryptHashData(m_hHash, pbyHashBuffer, ulLength, 0))
{
bHashDataAdded = true;
}
else
{
m_dwLastError = GetLastError();
}
return bHashDataAdded;
}
MAIN FUNCTION CLASS USAGE:
CAuthentication auth;
.....
auth.SetHashKeyString("0DD72A4F2B5FD48EF70B775BEDBCA14C");
.....
if(!auth.AddDataToHash(pbyHashBuffer, ulDataLen))
{
TRACE("CryptHashData function failed - Errorcode: %lu.\n", auth.GetAuthError());
}
You can't do it because it doesn't make any sense. CryptGetHashParam with the HP_HASHVAL option finalizes the hash, so there is no way to add data to it. If you want to "fork" the hash so that you can finalize it at some point as well as add data to it, you must duplicate the hash object prior to finalizing. Then you add the data to one of the hash objects and finalize the other. For example, you might do this if you wanted record a cumulative hash after every 1024 bytes of a data stream. You should not call CryptSetHashParam on the hash object that you are continuing to add data to.
CryptSetHashParam with the HP_HASHVAL option is a brutal hack to overcome a limitation in the CryptoAPI. The CryptoAPI will only sign a hash object, so if you want to sign some data that might have been hashed or generated outside of CAPI, you have to "jam" it into a hash object.
EDIT:
Based on your comment, I think you are looking for a way to serialize the hash object. I cannot find any evidence that CryptoAPI supports this. There are alternatives, however, that are basically variants of my "1024 bytes" example above. If you are hashing a sequence of files, you could simply compute and save the hash of each file. If you really need to boil it down to one value, then you can compute a modified hash where the first piece of data you hash for file i is the finalized hash for files 0, 1, 2, ..., i-1. So:
H-1 = empty,
Hi = MD5 (Hi-1 || filei)
As you go along, you can save the last successfully computed Hi value. In case of interruption, you can restart at file i+1. Note that, like any message digest, the above is completely sensitive to both order and content. This is something to consider on a dynamically changing file system. If files can be added or changed during the hashing operation, the meaning of the hash value will be affected. It might be rendered meaningless. You might want to be certain that both the sequence and content of the files you are hashing is frozen during the entire duration of the hash.