Jac Goudsmit wants to merge 4 commits from /u/jac_goudsmit/log4cpp/ to master, 2017-10-24
The NTEventLogAppender can now be instantiated with a location of the NTEventLogAppender.dll. By specifying a full path to the file, it's not necessary to make sure the file is in the PATH.
Also added a constructor that doesn't initialize the registry and keeps the appender closed.
Also fixed a few minor bugs and improved internal consistency.
The changes should be fully backwards compatible, i.e. any existing code that uses the NTEventLogAppender as intended, should still work the same way.
Please see check-in comments in the branch for many more details.
Commit | Date | |
---|---|---|
[19cf8a]
(bug-148-improve-nteventlogappender)
by
![]() NTEventLogAppender: Changing the code so member variables don't get changed after construction |
2017-10-01 06:07:32 | Tree |
[2ede7f]
by
![]() NTEventLogAppender: Improved flexibility, fixed bug 148 The old version of addRegistryInfo would always use "NTEventLogAppender.dll" without drive or directory, so it was necessary to put the DLL in the PATH or change the PATH to add the directory where the DLL is stored. This is undesirable and unnecessary: if the full path of the DLL is stored in the registry, it doesn' t need to be in the PATH. Another problem was that the reopen() function would always get the registry key to check if it existed, which should not be necessary in most cases. And if the registry key already existed, the function wouldn't save the values so it was impossible to deliberately change the values. Furthermore, the addRegistryInfo() function returned void, which made it impossible to notify the caller of a failure, which is not impossible; keep in mind that the registry can only be written if the program runs as administrator. Finally, the protected registry functions used TCHAR in their prototypes, which is a very bad idea: programs that are compiled in Unicode and use these functions would be incompatible with the log4cpp.dll because the DLL and the program would disagree on what a TCHAR is. This results in linker problems that are potentially very difficult or impossible to debug, especially for beginning Windows programmers. I moved the registry modifying code into an overloaded reopen() function that takes a parameter for the name, and a parameter for the DLL location. If the DLL location is blank, it uses "NTEventLogAppender.dll" for backwards compatibility. reopen() with parameters always attempts to write the values into the registry unless it's unable to open the key. If the source name (which is used as registry subkey) is blank, the parameterized reopen() fails. The non-parameterized reopen() doesn't operate on the registry, it merely calls ::RegisterEventSource. The protected functions that operate on the registry were corrected to use LPCSTR (const char *) instead of TCHAR * (without const). Existing software that calls these functions should still work. Two constructors were added: one that accepts the DLL path as a parameter and one that creates the instance in closed state, i.e. without calling reopen(). This fixes the problem that derived classes might not want the constructor to call the reopen() function, and because virtual functions aren't really virtual when called from a constructor, it would not be enough to override the function in the derived class. Other changes: |
2017-08-13 08:30:49 | Tree |
[e4df8c]
by
![]() NTEventLogAppender: Improved internal consistency; deprecated open() The old open() function was only safe to call when the appender is in closed state, otherwise it would leak a handle. Therefore it made sense to move the functionality of the open() function to the reopen() function, so that close() is called first, and so that any failure can be reported as a "false" result. The open() function now delegates to reopen(). I added a \deprecated tag so that Doxygen will flag it as such. I didn't add a __declspec(deprecated) because I think Visual C++ 6 won't handle it correctly. I also rewrote the comments for reopen() and close() to indicate that these functions correctly handle corner cases (opening when already open, closing when already closed). |
2017-08-13 06:18:55 | Tree |
[90c122]
by
![]() NTEventLogAppender: Improved internal consistency, removed tabs - close() now resets _hEventSource to NULL to prevent dangling handle |
2017-08-13 02:35:51 | Tree |
Hello Jac,
Thank you for this great amount of work and sorry that I haven't replied earlier.
I will seek through the changes and let you know if there are any additions I wish to be considered.
Alex.
I've looked through the commits for changing NTLogAppender and got there few questions. Would you tell please:
- In your client code do you call reopen() explicitly in some cases?
- Did you try to compile the project both with Unicode turned on and off? I am asking that because Windows have pair of functions for API calls.
E.g, call to RegSetValueEx() resolves to RegSetValueExA() (Ansi) or RegSetValueExW() (Unicode)
It could be reasonable to use more wide type LPCTSTR instead of concrete LPCSTR.
(quick link to that is https://stackoverflow.com/a/8481533/532541 )
Last edit: Alexander Perepelkin 2017-08-21
Hi Alex,
Whether I personally call reopen in my project is not important. But reopen is a public function so it CAN be called. If you're asking this to find out why I moved the registry code to an overloaded reopen(), the reasons for that are:
The old open() didn't guard internal consistency: if a subclass would call open() twice in a row without calling close(), open() would leak a handle.
To fix this, a call to close() could have been added to the open() function
But with the open() function calling close first, it basically (semantically) becomes a reopen. There is already an override virtual function called reopen() so why not move the functionality there.
Calling reopen on NTEventLogAppender may not make much sense because it's not like you're closing a file, deleting it and then reopening it or something. But in a derived class, someone might want to reimplement reopen() for a good reason. Perhaps they want to use reopen to delete all previously logged events for the logger, and then start logging again. Who knows.
In the old version, reopen would always check the registry to make sure that at least the registry key is present. But in most use cases (at least if the program is running with sufficient access rights), the event logging starts right after construction and it's unnecessary to create the key and/or check if the registry key exists after construction, especially if it's impossible to change the source name anyway. So there's no reason to put the registry in the reopen() code if all the program wants to do is unregister and re-register the event source.
The constructor of the old code always called open() to start logging. But as I mentioned in the bug report, open() is a virtual function and in C++, when a virtual function is called from a constructor, it always executes the implementation that belongs to the constructor, not the final class that's being constructed. That means that even if I write a subclass with an override for open(), NTEventLogAppender::open would still be called from the constructor, and would still create registry keys and values that my subclass may not want to create. For example, my subclass may not know yet what the source name is that a different part of my application wants to use for logging. Or it may want to use a different resource DLL. Or it may want to call ImpersonateLoggedOnUser to make sure that it can modify the registry from a non-elevated application. So I added a constructor that doesn't execute the registry code to make it possible to accomplish this. But that means of course that I also had to make it possible to call a function to make the registry changes at a later stage (after construction). For this, an overloaded virtual reopen() seemed like the best idea.
I don't know if the Log4CPP took any measures to
#undef _UNICODE
before#include <tchar.h>
but if it doesn't, a project that has_UNICODE
defined and includes log4cpp/NTEventLogAppender.h, will see an external declaration of (say)HKEY regGetKey(wchar_t *subkey, DWORD *disposition);
becauseTCHAR *
is interpreted aswchar_t *
instead ofchar *
. So when someone wants to write a derived class that calls this function, their compiler will complain if they want to call the function with e.g."mysubkey"
because that's not a wide-character string. So they'll useL"mysubkey"
orTEXT("mysubkey~)
which will compile without errors or warnings. But then the linker will look for a public symbol in any library (including the import library of log4cpp) for the function with the wchar_t pointer and it won't find it, because the log4cpp library was compiled without Unicode, so the mangled function name has a different symbol in the import library and the DLL than what the linker of this user is looking for.Because Windows has an API that's mostly based on C and PASCAL, overloading between two functions with the same name but different parameters (char vs. wchar_t) is impossible. So Windows uses a preprocessor trick to do this: When you call CreafeFile you really call CreateFileA or CreateFileW depending on your Unicode setting.
The old NTEventLogAppender code only provides one implementation of the registry functions, so unless it takes measures to define its own TCHAR type, you're making the preprocessor and compiler promise something that isn't delivered by the library: a Unicode version of the functions. There are two ways to solve this: either provide two versions of the functions or change the declaration of the function into a non-ambiguous one. Since the registry functions aren't really a core functionality of the NTEventLogAppender anyway (they are arguably just made available as protected functions for convenience), I decided to do the latter: change the declaration so that it would be non-ambiguous. There's just one problem with my change: The change from
char *
toLPCSTR
(which translated toconst char *
) will also change the mangled signature of the functions, so that those who use the registry functions will have to recompile and relink to the import library (if you simply replace the DLLs in an existing binary project, you'll get an execution error if the registry functions are used). But no code has to be changed in any client projects that use the registry functions. I figured this would be a small and acceptable price to pay.Sorry this got so long. Thanks for reading and thanks for considering my changes!
===Jac
I asked you whether you call it or not because you are the first user of this new code and may have a pattern.
Please put some thoughts into the following:
- you reasonably introduced new constructor so the dllLocation can be specified explicitly. After location of dll is specified for the process, it is hardly possible that the same process will call reopen(const std::string &sourceName, const std::string &dllLocation) and change any of parameters. Therefore dllLocation is preferably to become constant field of the class:
and reopen() be the only public overload for reopening
I'll get back to TCHAR etc. a bit later.
Last edit: Alexander Perepelkin 2017-08-23
If I understand you correctly you want to store the DLL location at construction time, and then not allow the client code to change it anymore, so it's not possible to change the location.
I disagree with that. I think there are possible use cases where the Appender is created when the program is started, but where the DLL location is not yet known until later, or may change later. For one thing, the registry key where the subkey and values need to be stored, is read-only unless the program runs as administrator. So an application might do an impersonation to get sufficient access rights to the registry, and then initialize the registry values and keys.
If I understand you correctly, you would like to separate the registry code into a separate function that's private and gets called from the constructor as well as from reopen(), but keep the other code.
I see the advantage in that: the constructors would call that function instead of a virtual function (which is undesirable as we discussed because it might confuse people), and it wouldn't unnecessarily call close() (via reopen) as part of the construction.
I think that's a good idea but I would do it slightly differently. How about if I write a static public function NTEventLogAppender::InitRegistry that creates or updates a registry key for event logging, and takes parameters for all the 4 values that are stored in the registry, with default values that correspond to the current situation?
===Jac
Last edit: Jac Goudsmit 2017-08-23
1) Object of NTEventLogAppender class is initialized automatically using values suplied in config file.
It's not responsibility of the library to track changes of the configuration in registry. If something gets changed in configuration, logging subsystem may be shutdown and reinitialized by the directive from the application. This is why name, source and dllLocation must be immutable fields.
2) I see the public static method is a bit overkill for the task. The only need here is to move virtual method away from ctr, this is well solved by having separate private impl method. public static would be visible outside and confuse potential users of the class.
I'm sorry that it's taking me a while to get back on this; I've been very busy with work and other projects. I hope to fix the code in my repo later this week, then I'll update this discussion. Thanks!
===Jac
Hi Alex,
I just changed the code so that the member variables of the NTEventLogAppender class are not changed after construction.
That means some things that were possible before this latest change (19cf8a969762599a198e0f276238d4b86977ffc9) like changing the DLL location on the fly, aren't possible anymore (as you requested), but I made it so that they can still be accomplished by creating a subclass.
Thanks for considering!
===Jac