使用Win32 事务
Win32 transactionsEver since Windows Vista, Windows has the ability to perform actions in a transactional way, making it possible to update files, registry keys, named pipes, … as part of a transaction that can be committed or rolled back as one operation where either all operations are made active, or none are made active. The way Microsoft has implemented transactions is basically to add two sets of functionality: functions to manage the transaction object itself, and functions to connect your operations to those transactions.
https://embed.plnkr.co/plunk/jW20EgJCQEUMKqhh
https://embed.plnkr.co/plunk/wTE5yrMEpkPzteJ6
https://embed.plnkr.co/plunk/O37abmZRkTPGWhNd
https://embed.plnkr.co/plunk/K7sdk5HveZHqH0w8
https://embed.plnkr.co/plunk/wiW76aFX1iuzb5BT
https://embed.plnkr.co/plunk/S2BweNe1fwhZa1nT
https://embed.plnkr.co/plunk/oNHnd6cTAUG0tHUq
https://embed.plnkr.co/plunk/IIf4Cdwy4jTxqQZQ
https://embed.plnkr.co/plunk/7pOm7PIauuxxGpnl
https://embed.plnkr.co/plunk/Oc1NuNtS6SkLrJkV
https://embed.plnkr.co/plunk/YRTFZHUSg4ENafVZ
https://embed.plnkr.co/plunk/dlif88abutUjJiaT
https://embed.plnkr.co/plunk/YOyt7ds5WbUJ80m4
https://embed.plnkr.co/plunk/OJ6kxXEBjdQgOzv2
https://embed.plnkr.co/plunk/pPTRMDWb7UvJSWyT
https://embed.plnkr.co/plunk/m9UDK1WPkIuyhOuu
https://embed.plnkr.co/plunk/ptUSZmf8ZkCmyHop
https://embed.plnkr.co/plunk/qjm6iVN6JDr2qa4h
https://embed.plnkr.co/plunk/hz8ne7YaZg7I9Zg6
https://embed.plnkr.co/plunk/1lYB6QR48w6ynRyl
https://embed.plnkr.co/plunk/qjyyl3U9K3bXpCj7
The former are the easiest. Of those, we will look at CreateTransaction, CommitTransaction and RollbackTransaction in more detail. Note that there is a much larger suite of management routines. These are beyond the scope of this article. You can read more about those here https://docs.microsoft.com/en-us/windows/win32/ktm/transactions.
The latter aren’t exactly difficult to use, but they are ugly and can be a bit nuanced. I will explain two of them in detail: CreateFileTransacted and RegCreateKeyTransacted. There is a great number of operations for which Microsoft has implemented transaction support, and they basically just took the every function they wanted to add support to, glued Transacted to the function name, and updated the parameter list to create a new function.
For the sake of completeness, I need to point out that it is possible to implement custom transaction managers and resource managers. If you have some sort of object management implemented in your system design, then it is possible to make it transaction aware so that it can work together with the rest of the Win32 transaction eco system. That too is well beyond the scope of this article. You can read more about that here: https://docs.microsoft.com/en-us/windows/win32/ktm/kernel-transaction-manager-portal
Sadly there is one regrettable thing that I must mention. Because there hasn’t been a significant adoption of the technology, Microsoft is putting notices in the documentation, encouraging users to find other solutions for NTFS transactions, and is warning that NTFS transaction may be removed in the future.
I do hope it doesn’t come to that because imo NTFS transactions are a phenomenal technology that should have been pushed harder. Registry and other transactions do not come with that warning at this time. That alone is worth putting in the time for, because the ability to handle complex registry updates in a transactional manner is great.
Creating the transaction
This is perhaps the simplest part of the process. You can simply create a transaction handle and that’s it. Microsoft has documented it as follows:
Copy Code
HANDLE CreateTransaction(
LPSECURITY_ATTRIBUTES lpTransactionAttributes,
LPGUID UOW,
DWORD CreateOptions,
DWORD IsolationLevel,
DWORD IsolationFlags,
DWORD Timeout,
LPWSTR Description);
UOW, IsolationLevel and IsolationFlags are reserved parameters so we just ignore them. lpTransactionAttributes is a way to assign a specific security descriptor to the transaction, which is something you don’t need if you’re a 3d party developer like us who create and use the transaction in a single process where the ACL comes from either the Primary user or the Impersonation user. The only parameter the may be of use is the Description parameter.
Because we don’t need these parameters in most cases, I have create a wrapper for it. It is possible to reuse the same function name and create an overloaded function but I prefer to do it like this, to make it obvious this is not an official function, and to make it clear to another developer that this is indeed a simplified function with a more limited scope.
//
// This function create a transaction with default settings, which is what
// is appropriate in most cases.
//
HANDLE CreateTransactionSimple(LPWSTR Description){
return CreateTransaction(
NULL, //Using default security.
NULL, //Reserved
0, //Create options, only relevant for inheriting handles
0, //Reserved
0, //Reserved
0, //Timeout
Description); //User readable description
}
CommitTransaction and RollbackTransaction do not require additional explanation because they just take the transaction handle as input and either commit or roll back the transaction respectively.
CreateFileTransacted
The CreateFileTransacted function name expands to either an ASCII or Unicode version. I’m showing the Unicode version. Most of these parameters are the same as the non-transactional version. I’m not going to describe them here. Of interest are only the three last parameters.
HANDLE CreateFileTransactedW(
LPCWSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile,
HANDLE hTransaction,
PUSHORT pusMiniVersion,
PVOID lpExtendedParameter);
lpExtendedParameter is reserved so we just pass in NULL. hTransaction is the transa1ction we are using. If the file handle is created, it is linked to the transaction. Subsequent file operations can all be done using the normal APIs which don’t make a difference between transacted files or other types of file.
pusMiniversion is a not used except in very special cases. What that parameter does is if you are opening that file from multiple places, who gets to see which version of that file while transactions are ongoing. By default, the transaction that is modifying the file sees the dirty view of the file, and other clients see the view of the file as it was when it was last committed.
I created a simplified wrapper for this function too
//
// This function acts as a simplified wrapper for creating a transacted file handle to hide an
// obnoxiously long argument list that has several reserved parameters, and a couple that are
// fine with default values for our use.
//
HANDLE CreateFileTransactedSimple(
const LPCTSTR& filepath, //the file we want to create or open
DWORD desiredAccess, //the type of requested access
DWORD createDisposition, //optional specifier to determine if we want to open, or create, or always create, ...
const HANDLE& transaction)//the transaction under which this filehandle is covered.
{
return CreateFileTransacted(
filepath,
desiredAccess,
FILE_SHARE_READ, //We allow others to open the file for read access.
//For newly created files this is pointless because noone
//will see the file until we commit. But for previously
//created files, other clients see the last committed file
//while we are still updating it.
NULL, //File security will be default
createDisposition,
FILE_ATTRIBUTE_NORMAL,//the file is a regular file
NULL, //no template file is used
transaction,
NULL, //No need for a special miniversion of the file
NULL); //reserved
}
Aside from the parameters that are reserved or unused, the wrapper also specifies that the file is a regular file without special attributes (compressed, encrypted, ….). It also specifies that when we are using it, others can open the file for reading. For newly created files this is pointless because they won’t even see the file. But if we open an existing file, others can still see the previous view until we commit.
RegCreateKeyTransacted
This function creates a registry key handle which may be used for registry operations under a transaction. It is documented as follows:
LSTATUS RegCreateKeyTransactedW(
HKEY hKey,
LPCWSTR lpSubKey,
DWORD Reserved,
LPWSTR lpClass,
DWORD dwOptions,
REGSAM samDesired,
const LPSECURITY_ATTRIBUTES lpSecurityAttributes,
PHKEY phkResult,
LPDWORD lpdwDisposition,
HANDLE hTransaction,
PVOID pExtendedParemeter
);
As with the previous function, I’m not going to cover the parameters that are also in the regular function call. Only the last two parameters are of interest, and the are conceptually the same as with the previous function. As soon as the registry key handle is created, the handle is linked to the transaction and you can use the normal APIs for using it.
For this function I also wrote a wrapper:
//
// Simplified wrapper for creating a registry key under a transaction to hide an
// obnoxiously long argument list that has several reserved parameters, and a couple that are
// fine with default values for our use.
//
LSTATUS RegCreateKeyTransactedSimple(
HKEY parentKey, //location where we want to open a new key
const LPCTSTR& regkey, //keyname
REGSAM samDesired, //requested rights
HKEY& regkeyhandle, //resulting registry key of the child.
const HANDLE& transaction)//transaction under which the key is opened.
{
return RegCreateKeyTransacted(
parentKey,
regkey,
0, //reserved
NULL, //user class. can be ignored
REG_OPTION_NON_VOLATILE,//the change is to be permanent
samDesired,
NULL, //security attributes. NULL -> default security inherited
®keyhandle,
NULL, //disposition feedback ->> was it created or opened? don't care.
transaction,
NULL); //reserved
}
Creating the application for our scenario
With all that out of the way, we can put everything together.
The overall structure
The structure of the client is very simple and easy to understand. After we create the transaction, we do all the relevant configurations. At the end, we either commit or roll back. It couldn’t be simpler and it’s definitely much less lines of code and much more reliable than a bunch of manually created data backup and restore code.
//Create the transaction covering the actions in this example
HANDLE transaction = CreateTransactionSimple();
if (transaction == INVALID_HANDLE_VALUE)
{
cout << "Failed to create Win32 Transaction\n";
return GetLastError();
}
// … Do stuff here
//Commit or rollback the transaction depending on whether the changes were
//successful and not cancelled by the user
if (error == NO_ERROR){
cout << "Committing transaction\n";
CommitTransaction(transaction);
}
else{
cout << "Rolling back the transaction\n";
RollbackTransaction(transaction);
}
CloseHandle(transaction);
}
Getting the user input
The logic of our program is that we get a new filename from the user which is acting as our pretend settings file.
Note that we don’t do any input validation on purpose here. The user can provide a filename with illegal characters. This will trigger an error and can be used to demonstrate the effectiveness of the transactions in dealing with errors.
Since the rest of the application is TCHAR aware, but the standard library doesn’t work with this concept, I implement these two options explicitly.
#ifdef _UNICODE
cout << "Enter the name of the file to be created:\n";
wstring filename;
getline(wcin, filename);
cout << "Enter the root folder or press enter for c:\\temp\\:\n";
wstring rootfolder;
getline(wcin, rootfolder);
if (rootfolder.empty())
rootfolder = wstring(TEXT("C:\\TEMP\\"));
wstring filepath = rootfolder + filename;
#else
cout << "Enter the name of the file to be created:\n";
string filename;
getline(cin, filename);
cout << "Enter the root folder or press enter for c:\\temp\\:\n";
string rootfolder;
getline(cin, rootfolder);
if (rootfolder.empty())
rootfolder = string(TEXT("C:\\TEMP\\"));
string filepath = rootfolder + filename;
#endif
Writing the registry
Here we update the registry after opening a registry key using the transaction we create earlier. You’ll note that the functions for updating the registry are the same ones you are used to. Although here I wrapped the SetRegValueEx function to hide the ugly typecasting that is required to write a string to the registry.
//Location in the registry where the path to the newly create file is to be stored
HKEY registryRoot = HKEY_CURRENT_USER;
LPCTSTR regkey = TEXT("Win32Transaction");
LPCTSTR valueName = TEXT("ConfigFile");
DWORD error = NO_ERROR;
//Open or create a registry key which is connected to the transaction
HKEY regkeyhandle = NULL;
if (ERROR_SUCCESS != RegCreateKeyTransactedSimple(
registryRoot, regkey, KEY_READ | KEY_WRITE, regkeyhandle, transaction))
error = GetLastError();
//write the path of the file to the registry. At this point no error checks have been
//performed so there is still the possibility that the 2nd half of this example will
//trigger an error.
if (error == NO_ERROR) {
if (ERROR_SUCCESS != SetRegValueExTString(regkeyhandle, valueName, filepath.c_str()))
error = GetLastError();
else
if (ERROR_SUCCESS != RegCloseKey(regkeyhandle))
error = GetLastError();
}
Writing the file
As with the registry access, after we create the file handle we can use the normal file IO functions for performing file IO.
Copy Code
//Create a file of which the path is based on the user input.
//No input validation was done so this part can trigger an error
if (error == NO_ERROR) {
HANDLE fileHandle = CreateFileTransactedSimple(
filepath.c_str(), GENERIC_READ | GENERIC_WRITE, CREATE_ALWAYS, transaction);
if (fileHandle == INVALID_HANDLE_VALUE)
error = GetLastError();
else
{
//so far so good, put something in the file and close it.
if (WriteFileTString(fileHandle, TEXT("Hello transacted world!")))
{
if (!CloseHandle(fileHandle))
error = GetLastError();
}
else
error = GetLastError();
}
}
Optionally choosing to roll back
For our purposes, we give the user the choice between committing or rolling back the changes. We do this to give them the time to manually check the registry or the folder on disk and verify that the changes are not yet active.
In the real world, you would probably not do this, although you could make an overview of the changes that were made, ask the user to review them before activating everything.
if (error == NO_ERROR)
{
char choice;
do {
cout << "Changes have been made.\n";
cout << "Enter C to commit or R to rollback.\n";
cin >> choice;
if(__isascii(choice) && islower(choice))
choice = _toupper(choice);
} while (choice != 'C' && choice != 'R');
if(choice == 'R')
error = ERROR_CANCELLED;
}
else {
cout << "An error was detected during the changes.\n";
}
Running the application
The source code for this application is included with the article, as is a built version which you can run on your system. I built it against the static runtime libraries so you can run it without needing a specific version of the runtime library installed.
The registry setting is stored under HKEY_CURRENT_USER which should never be a security problem. The file is stored in C:\temp unless you provide an alternative.
Conclusion
As you can see, Win32 Transactions are an incredibly powerful feature that deserves a lot more attention than it has received so far. I encourage everyone to look at them in more detail, and use them where appropriate. Not only can transactions save you a ton of manual coding, but they will also improve the reliability of your program.
页:
[1]