Skip to content

Commit 3c4429f

Browse files
authored
gh-135852: Remove out of tree pywin32 dependency for NTEventLogHandler (GH-137860)
Add RegisterEventSource(), DeregisterEventSource(), ReportEvent() and a number of EVENTLOG_* constants to _winapi.
1 parent 96ab379 commit 3c4429f

File tree

6 files changed

+358
-41
lines changed

6 files changed

+358
-41
lines changed

Lib/logging/handlers.py

Lines changed: 52 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,46 +1129,54 @@ class NTEventLogHandler(logging.Handler):
11291129
"""
11301130
A handler class which sends events to the NT Event Log. Adds a
11311131
registry entry for the specified application name. If no dllname is
1132-
provided, win32service.pyd (which contains some basic message
1132+
provided and pywin32 installed, win32service.pyd (which contains some basic message
11331133
placeholders) is used. Note that use of these placeholders will make
11341134
your event logs big, as the entire message source is held in the log.
11351135
If you want slimmer logs, you have to pass in the name of your own DLL
11361136
which contains the message definitions you want to use in the event log.
11371137
"""
11381138
def __init__(self, appname, dllname=None, logtype="Application"):
11391139
logging.Handler.__init__(self)
1140-
try:
1141-
import win32evtlogutil, win32evtlog
1142-
self.appname = appname
1143-
self._welu = win32evtlogutil
1144-
if not dllname:
1145-
dllname = os.path.split(self._welu.__file__)
1140+
import _winapi
1141+
self._winapi = _winapi
1142+
self.appname = appname
1143+
if not dllname:
1144+
# backward compatibility
1145+
try:
1146+
import win32evtlogutil
1147+
dllname = os.path.split(win32evtlogutil.__file__)
11461148
dllname = os.path.split(dllname[0])
11471149
dllname = os.path.join(dllname[0], r'win32service.pyd')
1148-
self.dllname = dllname
1149-
self.logtype = logtype
1150-
# Administrative privileges are required to add a source to the registry.
1151-
# This may not be available for a user that just wants to add to an
1152-
# existing source - handle this specific case.
1153-
try:
1154-
self._welu.AddSourceToRegistry(appname, dllname, logtype)
1155-
except Exception as e:
1156-
# This will probably be a pywintypes.error. Only raise if it's not
1157-
# an "access denied" error, else let it pass
1158-
if getattr(e, 'winerror', None) != 5: # not access denied
1159-
raise
1160-
self.deftype = win32evtlog.EVENTLOG_ERROR_TYPE
1161-
self.typemap = {
1162-
logging.DEBUG : win32evtlog.EVENTLOG_INFORMATION_TYPE,
1163-
logging.INFO : win32evtlog.EVENTLOG_INFORMATION_TYPE,
1164-
logging.WARNING : win32evtlog.EVENTLOG_WARNING_TYPE,
1165-
logging.ERROR : win32evtlog.EVENTLOG_ERROR_TYPE,
1166-
logging.CRITICAL: win32evtlog.EVENTLOG_ERROR_TYPE,
1167-
}
1168-
except ImportError:
1169-
print("The Python Win32 extensions for NT (service, event "\
1170-
"logging) appear not to be available.")
1171-
self._welu = None
1150+
except ImportError:
1151+
pass
1152+
self.dllname = dllname
1153+
self.logtype = logtype
1154+
# Administrative privileges are required to add a source to the registry.
1155+
# This may not be available for a user that just wants to add to an
1156+
# existing source - handle this specific case.
1157+
try:
1158+
self._add_source_to_registry(appname, dllname, logtype)
1159+
except PermissionError:
1160+
pass
1161+
self.deftype = _winapi.EVENTLOG_ERROR_TYPE
1162+
self.typemap = {
1163+
logging.DEBUG: _winapi.EVENTLOG_INFORMATION_TYPE,
1164+
logging.INFO: _winapi.EVENTLOG_INFORMATION_TYPE,
1165+
logging.WARNING: _winapi.EVENTLOG_WARNING_TYPE,
1166+
logging.ERROR: _winapi.EVENTLOG_ERROR_TYPE,
1167+
logging.CRITICAL: _winapi.EVENTLOG_ERROR_TYPE,
1168+
}
1169+
1170+
@staticmethod
1171+
def _add_source_to_registry(appname, dllname, logtype):
1172+
import winreg
1173+
1174+
key_path = f"SYSTEM\\CurrentControlSet\\Services\\EventLog\\{logtype}\\{appname}"
1175+
1176+
with winreg.CreateKey(winreg.HKEY_LOCAL_MACHINE, key_path) as key:
1177+
if dllname:
1178+
winreg.SetValueEx(key, "EventMessageFile", 0, winreg.REG_EXPAND_SZ, dllname)
1179+
winreg.SetValueEx(key, "TypesSupported", 0, winreg.REG_DWORD, 7) # All types are supported
11721180

11731181
def getMessageID(self, record):
11741182
"""
@@ -1209,15 +1217,20 @@ def emit(self, record):
12091217
Determine the message ID, event category and event type. Then
12101218
log the message in the NT event log.
12111219
"""
1212-
if self._welu:
1220+
try:
1221+
id = self.getMessageID(record)
1222+
cat = self.getEventCategory(record)
1223+
type = self.getEventType(record)
1224+
msg = self.format(record)
1225+
1226+
# Get a handle to the event log
1227+
handle = self._winapi.RegisterEventSource(None, self.appname)
12131228
try:
1214-
id = self.getMessageID(record)
1215-
cat = self.getEventCategory(record)
1216-
type = self.getEventType(record)
1217-
msg = self.format(record)
1218-
self._welu.ReportEvent(self.appname, id, cat, type, [msg])
1219-
except Exception:
1220-
self.handleError(record)
1229+
self._winapi.ReportEvent(handle, type, cat, id, msg)
1230+
finally:
1231+
self._winapi.DeregisterEventSource(handle)
1232+
except Exception:
1233+
self.handleError(record)
12211234

12221235
def close(self):
12231236
"""

Lib/test/test_logging.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7248,8 +7248,8 @@ def test_compute_rollover(self, when=when, interval=interval, exp=exp):
72487248
setattr(TimedRotatingFileHandlerTest, name, test_compute_rollover)
72497249

72507250

7251-
@unittest.skipUnless(win32evtlog, 'win32evtlog/win32evtlogutil/pywintypes required for this test.')
72527251
class NTEventLogHandlerTest(BaseTest):
7252+
@unittest.skipUnless(win32evtlog, 'win32evtlog/win32evtlogutil/pywintypes required for this test.')
72537253
def test_basic(self):
72547254
logtype = 'Application'
72557255
elh = win32evtlog.OpenEventLog(None, logtype)
@@ -7283,6 +7283,17 @@ def test_basic(self):
72837283
msg = 'Record not found in event log, went back %d records' % GO_BACK
72847284
self.assertTrue(found, msg=msg)
72857285

7286+
@unittest.skipUnless(sys.platform == "win32", "Windows required for this test")
7287+
def test_without_pywin32(self):
7288+
h = logging.handlers.NTEventLogHandler('python_test')
7289+
self.addCleanup(h.close)
7290+
7291+
# Verify that the handler uses _winapi module
7292+
self.assertIsNotNone(h._winapi, "_winapi module should be available")
7293+
7294+
r = logging.makeLogRecord({'msg': 'Hello!'})
7295+
h.emit(r)
7296+
72867297

72877298
class MiscTestCase(unittest.TestCase):
72887299
def test__all__(self):

Lib/test/test_winapi.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Test the Windows-only _winapi module
22

3+
import errno
34
import os
45
import pathlib
56
import re
@@ -156,3 +157,38 @@ def test_namedpipe(self):
156157
pipe2.write(b'testdata')
157158
pipe2.flush()
158159
self.assertEqual((b'testdata', 8), _winapi.PeekNamedPipe(pipe, 8)[:2])
160+
161+
def test_event_source_registration(self):
162+
source_name = "PythonTestEventSource"
163+
164+
handle = _winapi.RegisterEventSource(None, source_name)
165+
self.addCleanup(_winapi.DeregisterEventSource, handle)
166+
self.assertNotEqual(handle, _winapi.INVALID_HANDLE_VALUE)
167+
168+
with self.assertRaises(OSError) as cm:
169+
_winapi.RegisterEventSource(None, "")
170+
self.assertEqual(cm.exception.errno, errno.EINVAL)
171+
172+
with self.assertRaises(OSError) as cm:
173+
_winapi.DeregisterEventSource(_winapi.INVALID_HANDLE_VALUE)
174+
self.assertEqual(cm.exception.errno, errno.EBADF)
175+
176+
def test_report_event(self):
177+
source_name = "PythonTestEventSource"
178+
179+
handle = _winapi.RegisterEventSource(None, source_name)
180+
self.assertNotEqual(handle, _winapi.INVALID_HANDLE_VALUE)
181+
self.addCleanup(_winapi.DeregisterEventSource, handle)
182+
183+
_winapi.ReportEvent(handle, _winapi.EVENTLOG_SUCCESS, 1, 1002,
184+
"Test message 1")
185+
186+
with self.assertRaises(TypeError):
187+
_winapi.ReportEvent(handle, _winapi.EVENTLOG_SUCCESS, 1, 1002, 42)
188+
189+
with self.assertRaises(TypeError):
190+
_winapi.ReportEvent(handle, _winapi.EVENTLOG_SUCCESS, 1, 1002, None)
191+
192+
with self.assertRaises(ValueError):
193+
_winapi.ReportEvent(handle, _winapi.EVENTLOG_SUCCESS, 1, 1002,
194+
"Test message \0 with embedded null character")
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add :func:`!_winapi.RegisterEventSource`,
2+
:func:`!_winapi.DeregisterEventSource` and :func:`!_winapi.ReportEvent`.
3+
Using these functions in :class:`~logging.handlers.NTEventLogHandler`
4+
to replace :mod:`!pywin32`.

Modules/_winapi.c

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2982,6 +2982,103 @@ _winapi_CopyFile2_impl(PyObject *module, LPCWSTR existing_file_name,
29822982
Py_RETURN_NONE;
29832983
}
29842984

2985+
/*[clinic input]
2986+
_winapi.RegisterEventSource -> HANDLE
2987+
2988+
unc_server_name: LPCWSTR(accept={str, NoneType})
2989+
The UNC name of the server on which the event source should be registered.
2990+
If None, registers the event source on the local computer.
2991+
source_name: LPCWSTR
2992+
The name of the event source to register.
2993+
/
2994+
2995+
Retrieves a registered handle to the specified event log.
2996+
[clinic start generated code]*/
2997+
2998+
static HANDLE
2999+
_winapi_RegisterEventSource_impl(PyObject *module, LPCWSTR unc_server_name,
3000+
LPCWSTR source_name)
3001+
/*[clinic end generated code: output=e376c8950a89ae8f input=9d01059ac2156d0c]*/
3002+
{
3003+
HANDLE handle;
3004+
3005+
Py_BEGIN_ALLOW_THREADS
3006+
handle = RegisterEventSourceW(unc_server_name, source_name);
3007+
Py_END_ALLOW_THREADS
3008+
3009+
if (handle == NULL) {
3010+
PyErr_SetFromWindowsErr(0);
3011+
return INVALID_HANDLE_VALUE;
3012+
}
3013+
3014+
return handle;
3015+
}
3016+
3017+
/*[clinic input]
3018+
_winapi.DeregisterEventSource
3019+
3020+
handle: HANDLE
3021+
The handle to the event log to be deregistered.
3022+
/
3023+
3024+
Closes the specified event log.
3025+
[clinic start generated code]*/
3026+
3027+
static PyObject *
3028+
_winapi_DeregisterEventSource_impl(PyObject *module, HANDLE handle)
3029+
/*[clinic end generated code: output=7387ff34c7358bce input=947593cf67641f16]*/
3030+
{
3031+
BOOL success;
3032+
3033+
Py_BEGIN_ALLOW_THREADS
3034+
success = DeregisterEventSource(handle);
3035+
Py_END_ALLOW_THREADS
3036+
3037+
if (!success) {
3038+
return PyErr_SetFromWindowsErr(0);
3039+
}
3040+
3041+
Py_RETURN_NONE;
3042+
}
3043+
3044+
/*[clinic input]
3045+
_winapi.ReportEvent
3046+
3047+
handle: HANDLE
3048+
The handle to the event log.
3049+
type: unsigned_short(bitwise=False)
3050+
The type of event being reported.
3051+
category: unsigned_short(bitwise=False)
3052+
The event category.
3053+
event_id: unsigned_int(bitwise=False)
3054+
The event identifier.
3055+
string: LPCWSTR
3056+
A string to be inserted into the event message.
3057+
/
3058+
3059+
Writes an entry at the end of the specified event log.
3060+
[clinic start generated code]*/
3061+
3062+
static PyObject *
3063+
_winapi_ReportEvent_impl(PyObject *module, HANDLE handle,
3064+
unsigned short type, unsigned short category,
3065+
unsigned int event_id, LPCWSTR string)
3066+
/*[clinic end generated code: output=4281230b70a2470a input=8fb3385b8e7a6d3d]*/
3067+
{
3068+
BOOL success;
3069+
3070+
Py_BEGIN_ALLOW_THREADS
3071+
success = ReportEventW(handle, type, category, event_id, NULL, 1, 0,
3072+
&string, NULL);
3073+
Py_END_ALLOW_THREADS
3074+
3075+
if (!success) {
3076+
return PyErr_SetFromWindowsErr(0);
3077+
}
3078+
3079+
Py_RETURN_NONE;
3080+
}
3081+
29853082

29863083
static PyMethodDef winapi_functions[] = {
29873084
_WINAPI_CLOSEHANDLE_METHODDEF
@@ -2994,6 +3091,7 @@ static PyMethodDef winapi_functions[] = {
29943091
_WINAPI_CREATEPIPE_METHODDEF
29953092
_WINAPI_CREATEPROCESS_METHODDEF
29963093
_WINAPI_CREATEJUNCTION_METHODDEF
3094+
_WINAPI_DEREGISTEREVENTSOURCE_METHODDEF
29973095
_WINAPI_DUPLICATEHANDLE_METHODDEF
29983096
_WINAPI_EXITPROCESS_METHODDEF
29993097
_WINAPI_GETCURRENTPROCESS_METHODDEF
@@ -3010,6 +3108,8 @@ static PyMethodDef winapi_functions[] = {
30103108
_WINAPI_OPENMUTEXW_METHODDEF
30113109
_WINAPI_OPENPROCESS_METHODDEF
30123110
_WINAPI_PEEKNAMEDPIPE_METHODDEF
3111+
_WINAPI_REGISTEREVENTSOURCE_METHODDEF
3112+
_WINAPI_REPORTEVENT_METHODDEF
30133113
_WINAPI_LCMAPSTRINGEX_METHODDEF
30143114
_WINAPI_READFILE_METHODDEF
30153115
_WINAPI_RELEASEMUTEX_METHODDEF
@@ -3082,6 +3182,12 @@ static int winapi_exec(PyObject *m)
30823182
WINAPI_CONSTANT(F_DWORD, ERROR_PIPE_CONNECTED);
30833183
WINAPI_CONSTANT(F_DWORD, ERROR_PRIVILEGE_NOT_HELD);
30843184
WINAPI_CONSTANT(F_DWORD, ERROR_SEM_TIMEOUT);
3185+
WINAPI_CONSTANT(F_DWORD, EVENTLOG_SUCCESS);
3186+
WINAPI_CONSTANT(F_DWORD, EVENTLOG_AUDIT_FAILURE);
3187+
WINAPI_CONSTANT(F_DWORD, EVENTLOG_AUDIT_SUCCESS);
3188+
WINAPI_CONSTANT(F_DWORD, EVENTLOG_ERROR_TYPE);
3189+
WINAPI_CONSTANT(F_DWORD, EVENTLOG_INFORMATION_TYPE);
3190+
WINAPI_CONSTANT(F_DWORD, EVENTLOG_WARNING_TYPE);
30853191
WINAPI_CONSTANT(F_DWORD, FILE_FLAG_FIRST_PIPE_INSTANCE);
30863192
WINAPI_CONSTANT(F_DWORD, FILE_FLAG_OVERLAPPED);
30873193
WINAPI_CONSTANT(F_DWORD, FILE_GENERIC_READ);

0 commit comments

Comments
 (0)