Using registry transactions, it is possible to chain multiple independent operations together and have them all succeed at once, or have them rolled back if somewhere in the chain there is an error. In a previous article, I explained transactions in general. In this article, I provide a real-world scenario, and describe how transactions can be used to mitigate problems.
Introduction
Registry transactions are a powerful technology. Whenever you are making multiple changes to the registry, you typically want them all to succeed or fail together. You don't want to be left with a half modified registry that may result in applications no longer working correctly, or even being unable to uninstall them successfully. In my previous article, I explained transactions for registry and file access. In this article, I am providing a real-world usecase where such technology makes a real difference.
Background
Recently, I was programming the code for a COM server to unregister itself from the registered classes list. Because of a typo, something went wrong. And it was nothing trivial either. My process consisted of two parts. The first part was supposed to delete a registry subtree, the second part was a separate registry action that depended on the first.
Unfortunately, due to the typo, I deleted HKEY_CURRENT_USER
which surprisingly worked. The second change failed because of the typo. Had I used transactions, then nothing would have happened except I would have had to spend 5 minutes looking for the typo. Now, I destroyed my user profile, which cost me quite some time to restore.
So I figured this would be a good place to provide a robust Win32 helper function to tie a couple of actions together and demonstrate the power of transactions at the same time.
Using the Code
The use case for the code is that we want to delete all registry keys and values underneath a subkey, and then the subkey itself. For this, I have created the helper function, w32_RegDeleteTreeTransacted
.
LSTATUS w32_RegDeleteTreeTransacted(
HKEY hKeyRoot, LPCTSTR subKey, bool deleteSubKey, HANDLE transaction = INVALID_HANDLE_VALUE);
The parameter list is fairly self-evident. We want to delete everything under the subkey of a registry key (typically one of the named ones like HKEY_CURRENT_USER
). That is the first two parameters covered. The deleteSubkey
parameter specifies whether the subkey
itself needs to be deleted as well.
The transaction parameter is optional. There are several use cases where the operation is part of a larger set of changes which may be covered by an overall transaction. Using the transaction parameter, this change can be integrated in that overall transaction.
Input Validation
if (hKeyRoot == NULL)
return ERROR_INVALID_PARAMETER;
if ((hKeyRoot == HKEY_CLASSES_ROOT ||
hKeyRoot == HKEY_CURRENT_CONFIG ||
hKeyRoot == HKEY_CURRENT_USER ||
hKeyRoot == HKEY_LOCAL_MACHINE ||
hKeyRoot == HKEY_USERS) && subKey == NULL)
return ERROR_INVALID_PARAMETER;
if(deleteSubKey && subKey == NULL)
return ERROR_INVALID_PARAMETER;
Obviously, the root of the subkey cannot be NULL
. And if the root is a well known registry key, then the subkey cannot be NULL
. If a NULL
is supplied to the base API RegDeleteTree
for the subkey, then it ignores the subkey and instead deletes everything underneath the root. And while that it technically possible, I cannot think of a single problem for which deleting everything under a root key is a valid solution.
And finally, as I mentioned, while it is legal to not specify a subkey if the root is already a previously opened registry key, we cannot delete a subkey if none was supplied.
Transaction Management
HANDLE localTransaction = INVALID_HANDLE_VALUE;
if (transaction == INVALID_HANDLE_VALUE) {
localTransaction = w32_CreateTransaction();
if (localTransaction == INVALID_HANDLE_VALUE)
return GetLastError();
}
else
localTransaction = transaction;
if (transaction == INVALID_HANDLE_VALUE) {
if (status != ERROR_SUCCESS) {
RollbackTransaction(localTransaction);
CloseHandle(localTransaction);
return GetLastError();
}
else {
CommitTransaction(localTransaction);
CloseHandle(localTransaction);
return NO_ERROR;
}
}
As I mentioned, the transaction parameter is optional. If none is supplied, these changes are going to be covered by one that is local to this function and create in the beginning of the function call. At the end, we determine what to do based on the input value. If there was an overall transaction, then we do nothing and leave the decision to the party controlling the overall transaction. Otherwise, we commit or roll back based on the error status.
Doing the Work
HKEY hKey = NULL;
DWORD status = ERROR_SUCCESS;
if (status == ERROR_SUCCESS)
status = RegOpenKeyTransacted(
hKeyRoot, subKey, 0, KEY_WRITE | KEY_READ, &hKey, localTransaction, NULL);
if (status == ERROR_SUCCESS)
status = RegDeleteTree(hKey, NULL);
if (status == ERROR_SUCCESS) {
if (deleteSubKey) {
status = RegDeleteKeyTransacted(
hKeyRoot, subKey, KEY_WRITE | KEY_READ, 0, localTransaction, NULL);
}
}
if(hKey != NULL)
CloseHandle(hKey);
The main functionality is first to open a registry key and delete everything underneath that key. The RegDeleteTree
API itself is not transaction aware, but that's the cool thing: it doesn't need to be. If we opened the handle as transacted, then the operations on that handle will be transacted.
The second part is we delete the subkey itself. For this, there is a transacted API.
And that's it! We now have a helper function that performs multiple operations (deleting a tree under a subkey, and deleting the subkey itself) in a safe and predictable manner. Putting it all together, this is the final implementation:
LSTATUS w32_RegDeleteTreeTransacted(
HKEY hKeyRoot, LPCTSTR subKey, bool deleteSubKey, HANDLE transaction = INVALID_HANDLE_VALUE);
LSTATUS w32_RegDeleteTreeTransacted(
HKEY hKeyRoot, LPCTSTR subKey, bool deleteSubKey, HANDLE transaction) {
if (hKeyRoot == NULL)
return ERROR_INVALID_PARAMETER;
if ((hKeyRoot == HKEY_CLASSES_ROOT ||
hKeyRoot == HKEY_CURRENT_CONFIG ||
hKeyRoot == HKEY_CURRENT_USER ||
hKeyRoot == HKEY_LOCAL_MACHINE ||
hKeyRoot == HKEY_USERS) && subKey == NULL)
return ERROR_INVALID_PARAMETER;
if(deleteSubKey && subKey == NULL)
return ERROR_INVALID_PARAMETER;
HANDLE localTransaction = INVALID_HANDLE_VALUE;
if (transaction == INVALID_HANDLE_VALUE) {
localTransaction = w32_CreateTransaction();
if (localTransaction == INVALID_HANDLE_VALUE)
return GetLastError();
}
else
localTransaction = transaction;
HKEY hKey = NULL;
DWORD status = ERROR_SUCCESS;
if (status == ERROR_SUCCESS)
status = RegOpenKeyTransacted(
hKeyRoot, subKey, 0, KEY_WRITE | KEY_READ, &hKey, localTransaction, NULL);
if (status == ERROR_SUCCESS)
status = RegDeleteTree(hKey, NULL);
if (status == ERROR_SUCCESS) {
if (deleteSubKey) {
status = RegDeleteKeyTransacted(
hKeyRoot, subKey, KEY_WRITE | KEY_READ, 0, localTransaction, NULL);
}
}
if (hKey != NULL)
CloseHandle(hKey);
if (transaction == INVALID_HANDLE_VALUE) {
if (status != ERROR_SUCCESS) {
RollbackTransaction(localTransaction);
CloseHandle(localTransaction);
return GetLastError();
}
else {
CommitTransaction(localTransaction);
CloseHandle(localTransaction);
return NO_ERROR;
}
}
else {
return NO_ERROR;
}
}
Points of Interest
By now, it should be clear that the possibilities of transactions are endless. They can really make your code more robust and prevent half executed changes from ruining your configuration without requiring you to write a whole lot of additional rollback code yourself.
The reason I wrote this second article is that the first article was more generic, and this article solved a problem I ran into.
The article is licensed under the MIT license. Feel free to use this helper function as well as the others in the Win32Helpers.h/cpp file to make your life easier.
History
- V1. 17AUG2022: First version