]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-135852: Remove out of tree pywin32 dependency for NTEventLogHandler (GH-137860)
authorAN Long <aisk@users.noreply.github.com>
Wed, 31 Dec 2025 10:50:50 +0000 (19:50 +0900)
committerGitHub <noreply@github.com>
Wed, 31 Dec 2025 10:50:50 +0000 (12:50 +0200)
Add RegisterEventSource(), DeregisterEventSource(), ReportEvent()
and a number of EVENTLOG_* constants to _winapi.

Lib/logging/handlers.py
Lib/test/test_logging.py
Lib/test/test_winapi.py
Misc/NEWS.d/next/Library/2025-08-17-00-28-50.gh-issue-135852.lQqOjQ.rst [new file with mode: 0644]
Modules/_winapi.c
Modules/clinic/_winapi.c.h

index 4a07258f8d6d07ef487d0f3d2773109be8bf720d..575f2babbc47853f263cf5ab16b350d1de65389f 100644 (file)
@@ -1129,7 +1129,7 @@ class NTEventLogHandler(logging.Handler):
     """
     A handler class which sends events to the NT Event Log. Adds a
     registry entry for the specified application name. If no dllname is
-    provided, win32service.pyd (which contains some basic message
+    provided and pywin32 installed, win32service.pyd (which contains some basic message
     placeholders) is used. Note that use of these placeholders will make
     your event logs big, as the entire message source is held in the log.
     If you want slimmer logs, you have to pass in the name of your own DLL
@@ -1137,38 +1137,46 @@ class NTEventLogHandler(logging.Handler):
     """
     def __init__(self, appname, dllname=None, logtype="Application"):
         logging.Handler.__init__(self)
-        try:
-            import win32evtlogutil, win32evtlog
-            self.appname = appname
-            self._welu = win32evtlogutil
-            if not dllname:
-                dllname = os.path.split(self._welu.__file__)
+        import _winapi
+        self._winapi = _winapi
+        self.appname = appname
+        if not dllname:
+            # backward compatibility
+            try:
+                import win32evtlogutil
+                dllname = os.path.split(win32evtlogutil.__file__)
                 dllname = os.path.split(dllname[0])
                 dllname = os.path.join(dllname[0], r'win32service.pyd')
-            self.dllname = dllname
-            self.logtype = logtype
-            # Administrative privileges are required to add a source to the registry.
-            # This may not be available for a user that just wants to add to an
-            # existing source - handle this specific case.
-            try:
-                self._welu.AddSourceToRegistry(appname, dllname, logtype)
-            except Exception as e:
-                # This will probably be a pywintypes.error. Only raise if it's not
-                # an "access denied" error, else let it pass
-                if getattr(e, 'winerror', None) != 5:  # not access denied
-                    raise
-            self.deftype = win32evtlog.EVENTLOG_ERROR_TYPE
-            self.typemap = {
-                logging.DEBUG   : win32evtlog.EVENTLOG_INFORMATION_TYPE,
-                logging.INFO    : win32evtlog.EVENTLOG_INFORMATION_TYPE,
-                logging.WARNING : win32evtlog.EVENTLOG_WARNING_TYPE,
-                logging.ERROR   : win32evtlog.EVENTLOG_ERROR_TYPE,
-                logging.CRITICAL: win32evtlog.EVENTLOG_ERROR_TYPE,
-         }
-        except ImportError:
-            print("The Python Win32 extensions for NT (service, event "\
-                        "logging) appear not to be available.")
-            self._welu = None
+            except ImportError:
+                pass
+        self.dllname = dllname
+        self.logtype = logtype
+        # Administrative privileges are required to add a source to the registry.
+        # This may not be available for a user that just wants to add to an
+        # existing source - handle this specific case.
+        try:
+            self._add_source_to_registry(appname, dllname, logtype)
+        except PermissionError:
+            pass
+        self.deftype = _winapi.EVENTLOG_ERROR_TYPE
+        self.typemap = {
+            logging.DEBUG: _winapi.EVENTLOG_INFORMATION_TYPE,
+            logging.INFO: _winapi.EVENTLOG_INFORMATION_TYPE,
+            logging.WARNING: _winapi.EVENTLOG_WARNING_TYPE,
+            logging.ERROR: _winapi.EVENTLOG_ERROR_TYPE,
+            logging.CRITICAL: _winapi.EVENTLOG_ERROR_TYPE,
+        }
+
+    @staticmethod
+    def _add_source_to_registry(appname, dllname, logtype):
+        import winreg
+
+        key_path = f"SYSTEM\\CurrentControlSet\\Services\\EventLog\\{logtype}\\{appname}"
+
+        with winreg.CreateKey(winreg.HKEY_LOCAL_MACHINE, key_path) as key:
+            if dllname:
+                winreg.SetValueEx(key, "EventMessageFile", 0, winreg.REG_EXPAND_SZ, dllname)
+            winreg.SetValueEx(key, "TypesSupported", 0, winreg.REG_DWORD, 7)  # All types are supported
 
     def getMessageID(self, record):
         """
@@ -1209,15 +1217,20 @@ class NTEventLogHandler(logging.Handler):
         Determine the message ID, event category and event type. Then
         log the message in the NT event log.
         """
-        if self._welu:
+        try:
+            id = self.getMessageID(record)
+            cat = self.getEventCategory(record)
+            type = self.getEventType(record)
+            msg = self.format(record)
+
+            # Get a handle to the event log
+            handle = self._winapi.RegisterEventSource(None, self.appname)
             try:
-                id = self.getMessageID(record)
-                cat = self.getEventCategory(record)
-                type = self.getEventType(record)
-                msg = self.format(record)
-                self._welu.ReportEvent(self.appname, id, cat, type, [msg])
-            except Exception:
-                self.handleError(record)
+                self._winapi.ReportEvent(handle, type, cat, id, msg)
+            finally:
+                self._winapi.DeregisterEventSource(handle)
+        except Exception:
+            self.handleError(record)
 
     def close(self):
         """
index 848084e6e368784f8c01d070ed60d131d26aeee5..05dcea6ce0e98a7b8fed7cdc3b7b4fbe84c8db5e 100644 (file)
@@ -7248,8 +7248,8 @@ for when, exp in (('S', 1),
         setattr(TimedRotatingFileHandlerTest, name, test_compute_rollover)
 
 
-@unittest.skipUnless(win32evtlog, 'win32evtlog/win32evtlogutil/pywintypes required for this test.')
 class NTEventLogHandlerTest(BaseTest):
+    @unittest.skipUnless(win32evtlog, 'win32evtlog/win32evtlogutil/pywintypes required for this test.')
     def test_basic(self):
         logtype = 'Application'
         elh = win32evtlog.OpenEventLog(None, logtype)
@@ -7283,6 +7283,17 @@ class NTEventLogHandlerTest(BaseTest):
         msg = 'Record not found in event log, went back %d records' % GO_BACK
         self.assertTrue(found, msg=msg)
 
+    @unittest.skipUnless(sys.platform == "win32", "Windows required for this test")
+    def test_without_pywin32(self):
+        h = logging.handlers.NTEventLogHandler('python_test')
+        self.addCleanup(h.close)
+
+        # Verify that the handler uses _winapi module
+        self.assertIsNotNone(h._winapi, "_winapi module should be available")
+
+        r = logging.makeLogRecord({'msg': 'Hello!'})
+        h.emit(r)
+
 
 class MiscTestCase(unittest.TestCase):
     def test__all__(self):
index e64208330ad2f9e91b1efd0f95a7494aeff7736d..a1c0b80d47e4d444f89f017eb9c8fa31ca50394d 100644 (file)
@@ -1,5 +1,6 @@
 # Test the Windows-only _winapi module
 
+import errno
 import os
 import pathlib
 import re
@@ -156,3 +157,38 @@ class WinAPITests(unittest.TestCase):
             pipe2.write(b'testdata')
             pipe2.flush()
             self.assertEqual((b'testdata', 8), _winapi.PeekNamedPipe(pipe, 8)[:2])
+
+    def test_event_source_registration(self):
+        source_name = "PythonTestEventSource"
+
+        handle = _winapi.RegisterEventSource(None, source_name)
+        self.addCleanup(_winapi.DeregisterEventSource, handle)
+        self.assertNotEqual(handle, _winapi.INVALID_HANDLE_VALUE)
+
+        with self.assertRaises(OSError) as cm:
+            _winapi.RegisterEventSource(None, "")
+        self.assertEqual(cm.exception.errno, errno.EINVAL)
+
+        with self.assertRaises(OSError) as cm:
+            _winapi.DeregisterEventSource(_winapi.INVALID_HANDLE_VALUE)
+        self.assertEqual(cm.exception.errno, errno.EBADF)
+
+    def test_report_event(self):
+        source_name = "PythonTestEventSource"
+
+        handle = _winapi.RegisterEventSource(None, source_name)
+        self.assertNotEqual(handle, _winapi.INVALID_HANDLE_VALUE)
+        self.addCleanup(_winapi.DeregisterEventSource, handle)
+
+        _winapi.ReportEvent(handle, _winapi.EVENTLOG_SUCCESS, 1, 1002,
+                            "Test message 1")
+
+        with self.assertRaises(TypeError):
+            _winapi.ReportEvent(handle, _winapi.EVENTLOG_SUCCESS, 1, 1002, 42)
+
+        with self.assertRaises(TypeError):
+            _winapi.ReportEvent(handle, _winapi.EVENTLOG_SUCCESS, 1, 1002, None)
+
+        with self.assertRaises(ValueError):
+            _winapi.ReportEvent(handle, _winapi.EVENTLOG_SUCCESS, 1, 1002,
+                                "Test message \0 with embedded null character")
diff --git a/Misc/NEWS.d/next/Library/2025-08-17-00-28-50.gh-issue-135852.lQqOjQ.rst b/Misc/NEWS.d/next/Library/2025-08-17-00-28-50.gh-issue-135852.lQqOjQ.rst
new file mode 100644 (file)
index 0000000..7527cac
--- /dev/null
@@ -0,0 +1,4 @@
+Add :func:`!_winapi.RegisterEventSource`,
+:func:`!_winapi.DeregisterEventSource` and :func:`!_winapi.ReportEvent`.
+Using these functions in :class:`~logging.handlers.NTEventLogHandler`
+to replace :mod:`!pywin32`.
index 2aebe44c70921d347aabe279d6e58cfef6ff8207..ca16b06f83010a520e8218c8bfc1ea68d1a02570 100644 (file)
@@ -2982,6 +2982,103 @@ _winapi_CopyFile2_impl(PyObject *module, LPCWSTR existing_file_name,
     Py_RETURN_NONE;
 }
 
+/*[clinic input]
+_winapi.RegisterEventSource -> HANDLE
+
+    unc_server_name: LPCWSTR(accept={str, NoneType})
+        The UNC name of the server on which the event source should be registered.
+        If None, registers the event source on the local computer.
+    source_name: LPCWSTR
+        The name of the event source to register.
+    /
+
+Retrieves a registered handle to the specified event log.
+[clinic start generated code]*/
+
+static HANDLE
+_winapi_RegisterEventSource_impl(PyObject *module, LPCWSTR unc_server_name,
+                                 LPCWSTR source_name)
+/*[clinic end generated code: output=e376c8950a89ae8f input=9d01059ac2156d0c]*/
+{
+    HANDLE handle;
+
+    Py_BEGIN_ALLOW_THREADS
+    handle = RegisterEventSourceW(unc_server_name, source_name);
+    Py_END_ALLOW_THREADS
+
+    if (handle == NULL) {
+        PyErr_SetFromWindowsErr(0);
+        return INVALID_HANDLE_VALUE;
+    }
+
+    return handle;
+}
+
+/*[clinic input]
+_winapi.DeregisterEventSource
+
+    handle: HANDLE
+        The handle to the event log to be deregistered.
+    /
+
+Closes the specified event log.
+[clinic start generated code]*/
+
+static PyObject *
+_winapi_DeregisterEventSource_impl(PyObject *module, HANDLE handle)
+/*[clinic end generated code: output=7387ff34c7358bce input=947593cf67641f16]*/
+{
+    BOOL success;
+
+    Py_BEGIN_ALLOW_THREADS
+    success = DeregisterEventSource(handle);
+    Py_END_ALLOW_THREADS
+
+    if (!success) {
+        return PyErr_SetFromWindowsErr(0);
+    }
+
+    Py_RETURN_NONE;
+}
+
+/*[clinic input]
+_winapi.ReportEvent
+
+    handle: HANDLE
+        The handle to the event log.
+    type: unsigned_short(bitwise=False)
+        The type of event being reported.
+    category: unsigned_short(bitwise=False)
+        The event category.
+    event_id: unsigned_int(bitwise=False)
+        The event identifier.
+    string: LPCWSTR
+        A string to be inserted into the event message.
+    /
+
+Writes an entry at the end of the specified event log.
+[clinic start generated code]*/
+
+static PyObject *
+_winapi_ReportEvent_impl(PyObject *module, HANDLE handle,
+                         unsigned short type, unsigned short category,
+                         unsigned int event_id, LPCWSTR string)
+/*[clinic end generated code: output=4281230b70a2470a input=8fb3385b8e7a6d3d]*/
+{
+    BOOL success;
+
+    Py_BEGIN_ALLOW_THREADS
+    success = ReportEventW(handle, type, category, event_id, NULL, 1, 0,
+                           &string, NULL);
+    Py_END_ALLOW_THREADS
+
+    if (!success) {
+        return PyErr_SetFromWindowsErr(0);
+    }
+
+    Py_RETURN_NONE;
+}
+
 
 static PyMethodDef winapi_functions[] = {
     _WINAPI_CLOSEHANDLE_METHODDEF
@@ -2994,6 +3091,7 @@ static PyMethodDef winapi_functions[] = {
     _WINAPI_CREATEPIPE_METHODDEF
     _WINAPI_CREATEPROCESS_METHODDEF
     _WINAPI_CREATEJUNCTION_METHODDEF
+    _WINAPI_DEREGISTEREVENTSOURCE_METHODDEF
     _WINAPI_DUPLICATEHANDLE_METHODDEF
     _WINAPI_EXITPROCESS_METHODDEF
     _WINAPI_GETCURRENTPROCESS_METHODDEF
@@ -3010,6 +3108,8 @@ static PyMethodDef winapi_functions[] = {
     _WINAPI_OPENMUTEXW_METHODDEF
     _WINAPI_OPENPROCESS_METHODDEF
     _WINAPI_PEEKNAMEDPIPE_METHODDEF
+    _WINAPI_REGISTEREVENTSOURCE_METHODDEF
+    _WINAPI_REPORTEVENT_METHODDEF
     _WINAPI_LCMAPSTRINGEX_METHODDEF
     _WINAPI_READFILE_METHODDEF
     _WINAPI_RELEASEMUTEX_METHODDEF
@@ -3082,6 +3182,12 @@ static int winapi_exec(PyObject *m)
     WINAPI_CONSTANT(F_DWORD, ERROR_PIPE_CONNECTED);
     WINAPI_CONSTANT(F_DWORD, ERROR_PRIVILEGE_NOT_HELD);
     WINAPI_CONSTANT(F_DWORD, ERROR_SEM_TIMEOUT);
+    WINAPI_CONSTANT(F_DWORD, EVENTLOG_SUCCESS);
+    WINAPI_CONSTANT(F_DWORD, EVENTLOG_AUDIT_FAILURE);
+    WINAPI_CONSTANT(F_DWORD, EVENTLOG_AUDIT_SUCCESS);
+    WINAPI_CONSTANT(F_DWORD, EVENTLOG_ERROR_TYPE);
+    WINAPI_CONSTANT(F_DWORD, EVENTLOG_INFORMATION_TYPE);
+    WINAPI_CONSTANT(F_DWORD, EVENTLOG_WARNING_TYPE);
     WINAPI_CONSTANT(F_DWORD, FILE_FLAG_FIRST_PIPE_INSTANCE);
     WINAPI_CONSTANT(F_DWORD, FILE_FLAG_OVERLAPPED);
     WINAPI_CONSTANT(F_DWORD, FILE_GENERIC_READ);
index bd685e75d9344fe47d1bae8b88b3d35b1ce29111..00cce91dca43b1c302cf22337a50a4f6a1dc87df 100644 (file)
@@ -2184,7 +2184,154 @@ exit:
     return return_value;
 }
 
+PyDoc_STRVAR(_winapi_RegisterEventSource__doc__,
+"RegisterEventSource($module, unc_server_name, source_name, /)\n"
+"--\n"
+"\n"
+"Retrieves a registered handle to the specified event log.\n"
+"\n"
+"  unc_server_name\n"
+"    The UNC name of the server on which the event source should be registered.\n"
+"    If None, registers the event source on the local computer.\n"
+"  source_name\n"
+"    The name of the event source to register.");
+
+#define _WINAPI_REGISTEREVENTSOURCE_METHODDEF    \
+    {"RegisterEventSource", _PyCFunction_CAST(_winapi_RegisterEventSource), METH_FASTCALL, _winapi_RegisterEventSource__doc__},
+
+static HANDLE
+_winapi_RegisterEventSource_impl(PyObject *module, LPCWSTR unc_server_name,
+                                 LPCWSTR source_name);
+
+static PyObject *
+_winapi_RegisterEventSource(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
+{
+    PyObject *return_value = NULL;
+    LPCWSTR unc_server_name = NULL;
+    LPCWSTR source_name = NULL;
+    HANDLE _return_value;
+
+    if (!_PyArg_CheckPositional("RegisterEventSource", nargs, 2, 2)) {
+        goto exit;
+    }
+    if (args[0] == Py_None) {
+        unc_server_name = NULL;
+    }
+    else if (PyUnicode_Check(args[0])) {
+        unc_server_name = PyUnicode_AsWideCharString(args[0], NULL);
+        if (unc_server_name == NULL) {
+            goto exit;
+        }
+    }
+    else {
+        _PyArg_BadArgument("RegisterEventSource", "argument 1", "str or None", args[0]);
+        goto exit;
+    }
+    if (!PyUnicode_Check(args[1])) {
+        _PyArg_BadArgument("RegisterEventSource", "argument 2", "str", args[1]);
+        goto exit;
+    }
+    source_name = PyUnicode_AsWideCharString(args[1], NULL);
+    if (source_name == NULL) {
+        goto exit;
+    }
+    _return_value = _winapi_RegisterEventSource_impl(module, unc_server_name, source_name);
+    if ((_return_value == INVALID_HANDLE_VALUE) && PyErr_Occurred()) {
+        goto exit;
+    }
+    if (_return_value == NULL) {
+        Py_RETURN_NONE;
+    }
+    return_value = HANDLE_TO_PYNUM(_return_value);
+
+exit:
+    /* Cleanup for unc_server_name */
+    PyMem_Free((void *)unc_server_name);
+    /* Cleanup for source_name */
+    PyMem_Free((void *)source_name);
+
+    return return_value;
+}
+
+PyDoc_STRVAR(_winapi_DeregisterEventSource__doc__,
+"DeregisterEventSource($module, handle, /)\n"
+"--\n"
+"\n"
+"Closes the specified event log.\n"
+"\n"
+"  handle\n"
+"    The handle to the event log to be deregistered.");
+
+#define _WINAPI_DEREGISTEREVENTSOURCE_METHODDEF    \
+    {"DeregisterEventSource", (PyCFunction)_winapi_DeregisterEventSource, METH_O, _winapi_DeregisterEventSource__doc__},
+
+static PyObject *
+_winapi_DeregisterEventSource_impl(PyObject *module, HANDLE handle);
+
+static PyObject *
+_winapi_DeregisterEventSource(PyObject *module, PyObject *arg)
+{
+    PyObject *return_value = NULL;
+    HANDLE handle;
+
+    if (!PyArg_Parse(arg, "" F_HANDLE ":DeregisterEventSource", &handle)) {
+        goto exit;
+    }
+    return_value = _winapi_DeregisterEventSource_impl(module, handle);
+
+exit:
+    return return_value;
+}
+
+PyDoc_STRVAR(_winapi_ReportEvent__doc__,
+"ReportEvent($module, handle, type, category, event_id, string, /)\n"
+"--\n"
+"\n"
+"Writes an entry at the end of the specified event log.\n"
+"\n"
+"  handle\n"
+"    The handle to the event log.\n"
+"  type\n"
+"    The type of event being reported.\n"
+"  category\n"
+"    The event category.\n"
+"  event_id\n"
+"    The event identifier.\n"
+"  string\n"
+"    A string to be inserted into the event message.");
+
+#define _WINAPI_REPORTEVENT_METHODDEF    \
+    {"ReportEvent", _PyCFunction_CAST(_winapi_ReportEvent), METH_FASTCALL, _winapi_ReportEvent__doc__},
+
+static PyObject *
+_winapi_ReportEvent_impl(PyObject *module, HANDLE handle,
+                         unsigned short type, unsigned short category,
+                         unsigned int event_id, LPCWSTR string);
+
+static PyObject *
+_winapi_ReportEvent(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
+{
+    PyObject *return_value = NULL;
+    HANDLE handle;
+    unsigned short type;
+    unsigned short category;
+    unsigned int event_id;
+    LPCWSTR string = NULL;
+
+    if (!_PyArg_ParseStack(args, nargs, "" F_HANDLE "O&O&O&O&:ReportEvent",
+        &handle, _PyLong_UnsignedShort_Converter, &type, _PyLong_UnsignedShort_Converter, &category, _PyLong_UnsignedInt_Converter, &event_id, _PyUnicode_WideCharString_Converter, &string)) {
+        goto exit;
+    }
+    return_value = _winapi_ReportEvent_impl(module, handle, type, category, event_id, string);
+
+exit:
+    /* Cleanup for string */
+    PyMem_Free((void *)string);
+
+    return return_value;
+}
+
 #ifndef _WINAPI_GETSHORTPATHNAME_METHODDEF
     #define _WINAPI_GETSHORTPATHNAME_METHODDEF
 #endif /* !defined(_WINAPI_GETSHORTPATHNAME_METHODDEF) */
-/*[clinic end generated code: output=4581fd481c3c6293 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=4ab94eaee93a0a90 input=a9049054013a1b77]*/