diff --git a/docs/reference/glib/running.xml b/docs/reference/glib/running.xml
index 8e4ffeca3..86765c868 100644
--- a/docs/reference/glib/running.xml
+++ b/docs/reference/glib/running.xml
@@ -256,6 +256,69 @@ How to run and debug your GLib application
+
+ G_DEBUGGER
+
+
+ When running on Windows, if set to a non-empty string, GLib will
+ try to interpret the contents of this environment variable as
+ a command line to a debugger, and run it if the process crashes.
+ The debugger command line should contain %p and %e substitution
+ tokens, which GLib will replace with the process ID of the crashing
+ process and a handle to an event that the debugger should signal
+ to let GLib know that the debugger successfully attached to the
+ process. If %e is absent, or if the debugger is not able to
+ signal events, GLib will resume execution after 60 seconds.
+ If %p is absent, the debugger won't know which process to attach to,
+ and GLib will also resume execution after 60 seconds.
+
+
+ Additionally, even if G_DEBUGGER is not set, GLib would still
+ try to print basic exception information (code and address) into
+ stderr.
+
+
+ By default the debugger gets a new console allocated for it.
+ Set the G_DEBUGGER_OLD_CONSOLE environment variable to any
+ non-empty string to make the debugger inherit the console of
+ the crashing process. Normally this is only used by the GLib
+ testsuite.
+
+
+ The exception handler is written with the aim of making it as
+ simple as possible, to minimize the risk of it invoking
+ buggy functions or running buggy code, which would result
+ in exceptions being raised recursively. Because of that
+ it lacks most of the amenities that one would expect of GLib.
+ Namely, it does not support Unicode, so it is highly advisable
+ to only use ASCII characters in G_DEBUGGER.
+
+
+ See also G_VEH_CATCH.
+
+
+
+
+ G_VEH_CATCH
+
+
+ Catching some exceptions can break the program, since Windows
+ will sometimes use exceptions for execution flow control and
+ other purposes other than signalling a crash.
+
+
+ The G_VEH_CATCH environment variable augments
+ Vectored Exception Handling
+ on Windows (see G_DEBUGGER), allowing GLib to catch more
+ exceptions. Set this variable to a comma-separated list of
+ hexademical exception codes that should additionally be caught.
+
+
+ By default GLib will only catch Access Violation, Stack Overflow and
+ Illegal Instruction exceptions.
+
+
+
diff --git a/glib/gbacktrace.c b/glib/gbacktrace.c
index e83079985..34cb1ce46 100644
--- a/glib/gbacktrace.c
+++ b/glib/gbacktrace.c
@@ -125,6 +125,10 @@ volatile gboolean glib_on_error_halt = TRUE;
* If "[P]roceed" is selected, the function returns.
*
* This function may cause different actions on non-UNIX platforms.
+ *
+ * On Windows consider using the `G_DEBUGGER` environment
+ * variable (see [Running GLib Applications](glib-running.html)) and
+ * calling g_on_error_stack_trace() instead.
*/
void
g_on_error_query (const gchar *prg_name)
@@ -207,6 +211,12 @@ g_on_error_query (const gchar *prg_name)
* gdk_init().
*
* This function may cause different actions on non-UNIX platforms.
+ *
+ * When running on Windows, this function is *not* called by
+ * g_on_error_query(). If called directly, it will raise an
+ * exception, which will crash the program. If the `G_DEBUGGER` environment
+ * variable is set, a debugger will be invoked to attach and
+ * handle that exception (see [Running GLib Applications](glib-running.html)).
*/
void
g_on_error_stack_trace (const gchar *prg_name)
diff --git a/glib/glib-init.c b/glib/glib-init.c
index 6ffe7933b..ed800dca1 100644
--- a/glib/glib-init.c
+++ b/glib/glib-init.c
@@ -286,6 +286,7 @@ DllMain (HINSTANCE hinstDLL,
{
case DLL_PROCESS_ATTACH:
glib_dll = hinstDLL;
+ g_crash_handler_win32_init ();
g_clock_win32_init ();
#ifdef THREADS_WIN32
g_thread_win32_init ();
@@ -306,6 +307,7 @@ DllMain (HINSTANCE hinstDLL,
if (lpvReserved == NULL)
g_thread_win32_process_detach ();
#endif
+ g_crash_handler_win32_deinit ();
break;
default:
diff --git a/glib/glib-init.h b/glib/glib-init.h
index 695dc044b..f44b9a4d2 100644
--- a/glib/glib-init.h
+++ b/glib/glib-init.h
@@ -36,6 +36,8 @@ void g_thread_win32_thread_detach (void);
void g_thread_win32_init (void);
void g_console_win32_init (void);
void g_clock_win32_init (void);
+void g_crash_handler_win32_init (void);
+void g_crash_handler_win32_deinit (void);
extern HMODULE glib_dll;
#endif
diff --git a/glib/gwin32-private.c b/glib/gwin32-private.c
new file mode 100644
index 000000000..917bcd1b5
--- /dev/null
+++ b/glib/gwin32-private.c
@@ -0,0 +1,77 @@
+/* gwin32-private.c - private glib functions for gwin32.c
+ *
+ * Copyright 2019 Руслан Ижбулатов
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see .
+ */
+
+/* Copy @cmdline into @debugger, and substitute @pid for `%p`
+ * and @event for `%e`.
+ * If @debugger_size (in bytes) is overflowed, return %FALSE.
+ * Also returns %FALSE when `%` is followed by anything other
+ * than `e` or `p`.
+ */
+static gboolean
+_g_win32_subst_pid_and_event (char *debugger,
+ gsize debugger_size,
+ const char *cmdline,
+ DWORD pid,
+ guintptr event)
+{
+ gsize i = 0, dbg_i = 0;
+/* These are integers, and they can't be longer than 20 characters
+ * even when they are 64-bit and in decimal notation.
+ * Use 30 just to be sure.
+ */
+#define STR_BUFFER_SIZE 30
+ char pid_str[STR_BUFFER_SIZE] = {0};
+ gsize pid_str_len;
+ char event_str[STR_BUFFER_SIZE] = {0};
+ gsize event_str_len;
+#undef STR_BUFFER_SIZE
+ snprintf (pid_str, G_N_ELEMENTS (pid_str), "%lu", pid);
+ pid_str[G_N_ELEMENTS (pid_str) - 1] = 0;
+ pid_str_len = strlen (pid_str);
+ snprintf (event_str, G_N_ELEMENTS (pid_str), "%Iu", event);
+ event_str[G_N_ELEMENTS (pid_str) - 1] = 0;
+ event_str_len = strlen (event_str);
+
+ while (cmdline[i] != 0 && dbg_i < debugger_size)
+ {
+ if (cmdline[i] != '%')
+ debugger[dbg_i++] = cmdline[i++];
+ else if (cmdline[i + 1] == 'p')
+ {
+ gsize j = 0;
+ while (j < pid_str_len && dbg_i < debugger_size)
+ debugger[dbg_i++] = pid_str[j++];
+ i += 2;
+ }
+ else if (cmdline[i + 1] == 'e')
+ {
+ gsize j = 0;
+ while (j < event_str_len && dbg_i < debugger_size)
+ debugger[dbg_i++] = event_str[j++];
+ i += 2;
+ }
+ else
+ return FALSE;
+ }
+ if (dbg_i < debugger_size)
+ debugger[dbg_i] = 0;
+ else
+ return FALSE;
+
+ return TRUE;
+}
diff --git a/glib/gwin32.c b/glib/gwin32.c
index 8a7ab3aeb..6d97e4c0e 100644
--- a/glib/gwin32.c
+++ b/glib/gwin32.c
@@ -1018,4 +1018,218 @@ g_console_win32_init (void)
}
}
+/* This is a handle to the Vectored Exception Handler that
+ * we install on library initialization. If installed correctly,
+ * it will be non-NULL. Only used to later de-install the handler
+ * on library de-initialization.
+ */
+static void *WinVEH_handle = NULL;
+
+#include "gwin32-private.c"
+
+/**
+ * Handles exceptions (useful for debugging).
+ * Issues a DebugBreak() call if the process is being debugged (not really
+ * useful - if the process is being debugged, this handler won't be invoked
+ * anyway). If it is not, runs a debugger from G_DEBUGGER env var,
+ * substituting first %p in it for PID, and the first %e for the event handle -
+ * that event should be set once the debugger attaches itself (otherwise the
+ * only way out of WaitForSingleObject() is to time out after 1 minute).
+ * For example, G_DEBUGGER can be set to the following command:
+ * ```
+ * gdb.exe -ex "attach %p" -ex "signal-event %e" -ex "bt" -ex "c"
+ * ```
+ * This will make GDB attach to the process, signal the event (GDB must be
+ * recent enough for the signal-event command to be available),
+ * show the backtrace and resume execution, which should make it catch
+ * the exception when Windows re-raises it again.
+ * The command line can't be longer than MAX_PATH (260 characters).
+ *
+ * This function will only stop (and run a debugger) on the following exceptions:
+ * * EXCEPTION_ACCESS_VIOLATION
+ * * EXCEPTION_STACK_OVERFLOW
+ * * EXCEPTION_ILLEGAL_INSTRUCTION
+ * To make it stop at other exceptions one should set the G_VEH_CATCH
+ * environment variable to a list of comma-separated hexademical numbers,
+ * where each number is the code of an exception that should be caught.
+ * This is done to prevent GLib from breaking when Windows uses
+ * exceptions to shuttle information (SetThreadName(), OutputDebugString())
+ * or for control flow.
+ *
+ * This function deliberately avoids calling any GLib code.
+ */
+static LONG __stdcall
+g_win32_veh_handler (PEXCEPTION_POINTERS ExceptionInfo)
+{
+ EXCEPTION_RECORD *er;
+ char debugger[MAX_PATH + 1];
+ const char *debugger_env = NULL;
+ const char *catch_list;
+ gboolean catch = FALSE;
+ STARTUPINFO si;
+ PROCESS_INFORMATION pi;
+ HANDLE event;
+ SECURITY_ATTRIBUTES sa;
+
+ if (ExceptionInfo == NULL ||
+ ExceptionInfo->ExceptionRecord == NULL)
+ return EXCEPTION_CONTINUE_SEARCH;
+
+ er = ExceptionInfo->ExceptionRecord;
+
+ switch (er->ExceptionCode)
+ {
+ case EXCEPTION_ACCESS_VIOLATION:
+ case EXCEPTION_STACK_OVERFLOW:
+ case EXCEPTION_ILLEGAL_INSTRUCTION:
+ case EXCEPTION_BREAKPOINT: /* DebugBreak() raises this */
+ break;
+ default:
+ catch_list = getenv ("G_VEH_CATCH");
+
+ while (!catch &&
+ catch_list != NULL &&
+ catch_list[0] != 0)
+ {
+ unsigned long catch_code;
+ char *end;
+ errno = 0;
+ catch_code = strtoul (catch_list, &end, 16);
+ if (errno != NO_ERROR)
+ break;
+ catch_list = end;
+ if (catch_list != NULL && catch_list[0] == ',')
+ catch_list++;
+ if (catch_code == er->ExceptionCode)
+ catch = TRUE;
+ }
+
+ if (catch)
+ break;
+
+ return EXCEPTION_CONTINUE_SEARCH;
+ }
+
+ if (IsDebuggerPresent ())
+ {
+ /* This shouldn't happen, but still try to
+ * avoid recursion with EXCEPTION_BREAKPOINT and
+ * DebugBreak().
+ */
+ if (er->ExceptionCode != EXCEPTION_BREAKPOINT)
+ DebugBreak ();
+ return EXCEPTION_CONTINUE_EXECUTION;
+ }
+
+ fprintf (stderr,
+ "Exception code=0x%lx flags=0x%lx at 0x%p",
+ er->ExceptionCode,
+ er->ExceptionFlags,
+ er->ExceptionAddress);
+
+ switch (er->ExceptionCode)
+ {
+ case EXCEPTION_ACCESS_VIOLATION:
+ fprintf (stderr,
+ ". Access violation - attempting to %s at address 0x%p\n",
+ er->ExceptionInformation[0] == 0 ? "read data" :
+ er->ExceptionInformation[0] == 1 ? "write data" :
+ er->ExceptionInformation[0] == 8 ? "execute data" :
+ "do something bad",
+ (void *) er->ExceptionInformation[1]);
+ break;
+ case EXCEPTION_IN_PAGE_ERROR:
+ fprintf (stderr,
+ ". Page access violation - attempting to %s at address 0x%p with status %Ix\n",
+ er->ExceptionInformation[0] == 0 ? "read from an inaccessible page" :
+ er->ExceptionInformation[0] == 1 ? "write to an inaccessible page" :
+ er->ExceptionInformation[0] == 8 ? "execute data in page" :
+ "do something bad with a page",
+ (void *) er->ExceptionInformation[1],
+ er->ExceptionInformation[2]);
+ break;
+ default:
+ fprintf (stderr, "\n");
+ break;
+ }
+
+ fflush (stderr);
+
+ debugger_env = getenv ("G_DEBUGGER");
+
+ if (debugger_env == NULL)
+ return EXCEPTION_CONTINUE_SEARCH;
+
+ /* Create an inheritable event */
+ memset (&si, 0, sizeof (si));
+ memset (&pi, 0, sizeof (pi));
+ memset (&sa, 0, sizeof (sa));
+ si.cb = sizeof (si);
+ sa.nLength = sizeof (sa);
+ sa.bInheritHandle = TRUE;
+ event = CreateEvent (&sa, FALSE, FALSE, NULL);
+
+ /* Put process ID and event handle into debugger commandline */
+ if (!_g_win32_subst_pid_and_event (debugger, G_N_ELEMENTS (debugger),
+ debugger_env, GetCurrentProcessId (),
+ (guintptr) event))
+ {
+ CloseHandle (event);
+ return EXCEPTION_CONTINUE_SEARCH;
+ }
+
+ /* Run the debugger */
+ debugger[MAX_PATH] = '\0';
+ if (0 != CreateProcessA (NULL,
+ debugger,
+ NULL,
+ NULL,
+ TRUE,
+ getenv ("G_DEBUGGER_OLD_CONSOLE") != NULL ? 0 : CREATE_NEW_CONSOLE,
+ NULL,
+ NULL,
+ &si,
+ &pi))
+ {
+ CloseHandle (pi.hProcess);
+ CloseHandle (pi.hThread);
+ /* If successful, wait for 60 seconds on the event
+ * we passed. The debugger should signal that event.
+ * 60 second limit is here to prevent us from hanging
+ * up forever in case the debugger does not support
+ * event signalling.
+ */
+ WaitForSingleObject (event, 60000);
+ }
+
+ CloseHandle (event);
+
+ /* Now the debugger is present, and we can try
+ * resuming execution, re-triggering the exception,
+ * which will be caught by debugger this time around.
+ */
+ if (IsDebuggerPresent ())
+ return EXCEPTION_CONTINUE_EXECUTION;
+
+ return EXCEPTION_CONTINUE_SEARCH;
+}
+
+void
+g_crash_handler_win32_init (void)
+{
+ if (WinVEH_handle != NULL)
+ return;
+
+ WinVEH_handle = AddVectoredExceptionHandler (0, &g_win32_veh_handler);
+}
+
+void
+g_crash_handler_win32_deinit (void)
+{
+ if (WinVEH_handle != NULL)
+ RemoveVectoredExceptionHandler (WinVEH_handle);
+
+ WinVEH_handle = NULL;
+}
+
#endif
diff --git a/glib/tests/meson.build b/glib/tests/meson.build
index d54fc41fa..2b3f902ab 100644
--- a/glib/tests/meson.build
+++ b/glib/tests/meson.build
@@ -140,6 +140,9 @@ if host_machine.system() == 'windows'
},
}
endif
+ glib_tests += {
+ 'win32' : {},
+ }
else
glib_tests += {
'include' : {},
diff --git a/glib/tests/win32.c b/glib/tests/win32.c
new file mode 100644
index 000000000..d4b1fc523
--- /dev/null
+++ b/glib/tests/win32.c
@@ -0,0 +1,173 @@
+/* Unit test for VEH on Windows
+ * Copyright (C) 2019 Руслан Ижбулатов
+ *
+ * This work is provided "as is"; redistribution and modification
+ * in whole or in part, in any medium, physical or electronic is
+ * permitted without restriction.
+ *
+ * This work is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * In no event shall the authors or contributors be liable for any
+ * direct, indirect, incidental, special, exemplary, or consequential
+ * damages (including, but not limited to, procurement of substitute
+ * goods or services; loss of use, data, or profits; or business
+ * interruption) however caused and on any theory of liability, whether
+ * in contract, strict liability, or tort (including negligence or
+ * otherwise) arising in any way out of the use of this software, even
+ * if advised of the possibility of such damage.
+ */
+
+#include "config.h"
+
+#include
+#include
+#include
+#include
+
+static char *argv0 = NULL;
+
+#include "../gwin32-private.c"
+
+static void
+test_subst_pid_and_event (void)
+{
+ const gchar not_enough[] = "too long when %e and %p are substituted";
+ gchar debugger_3[3];
+ gchar debugger_not_enough[G_N_ELEMENTS (not_enough)];
+ gchar debugger_enough[G_N_ELEMENTS (not_enough) + 1];
+ gchar debugger_big[65535] = {0};
+ gchar *output;
+ DWORD be = 0xFFFFFFFF;
+ guintptr bp = (guintptr) G_MAXSIZE;
+
+ /* %f is not valid */
+ g_assert_false (_g_win32_subst_pid_and_event (debugger_3, G_N_ELEMENTS (debugger_3),
+ "%f", 0, 0));
+
+ g_assert_false (_g_win32_subst_pid_and_event (debugger_3, G_N_ELEMENTS (debugger_3),
+ "string longer than 10", 0, 0));
+ /* 200 is longer than %e, so the string doesn't fit by 1 byte */
+ g_assert_false (_g_win32_subst_pid_and_event (debugger_not_enough, G_N_ELEMENTS (debugger_not_enough),
+ "too long when %e and %p are substituted", 10, 200));
+
+ /* This should fit */
+ g_assert_true (_g_win32_subst_pid_and_event (debugger_enough, G_N_ELEMENTS (debugger_enough),
+ "too long when %e and %p are substituted", 10, 200));
+ g_assert_cmpstr (debugger_enough, ==, "too long when 200 and 10 are substituted");
+
+ g_assert_true (_g_win32_subst_pid_and_event (debugger_big, G_N_ELEMENTS (debugger_big),
+ "multipl%e big %e %entries and %pids are %provided here", bp, be));
+ output = g_strdup_printf ("multipl%lu big %lu %luntries and %lluids are %llurovided here", be, be, be, (guint64) bp, (guint64) bp);
+ g_assert_cmpstr (debugger_big, ==, output);
+ g_free (output);
+}
+
+/* Crash with access violation */
+static void
+test_access_violation (void)
+{
+ int *integer = NULL;
+ /* Use SEM_NOGPFAULTERRORBOX to prevent an error dialog
+ * from being shown.
+ */
+ DWORD dwMode = SetErrorMode (SEM_NOGPFAULTERRORBOX);
+ SetErrorMode (dwMode | SEM_NOGPFAULTERRORBOX);
+ *integer = 1;
+ SetErrorMode (dwMode);
+}
+
+/* Crash with illegal instruction */
+static void
+test_illegal_instruction (void)
+{
+ DWORD dwMode = SetErrorMode (SEM_NOGPFAULTERRORBOX);
+ SetErrorMode (dwMode | SEM_NOGPFAULTERRORBOX);
+ RaiseException (EXCEPTION_ILLEGAL_INSTRUCTION, 0, 0, NULL);
+ SetErrorMode (dwMode);
+}
+
+static void
+test_veh_crash_access_violation (void)
+{
+ g_unsetenv ("G_DEBUGGER");
+ /* Run a test that crashes */
+ g_test_trap_subprocess ("/win32/subprocess/access_violation", 0, 0);
+ g_test_trap_assert_failed ();
+ g_test_trap_assert_stderr ("Exception code=0xc0000005*");
+}
+
+static void
+test_veh_crash_illegal_instruction (void)
+{
+ g_unsetenv ("G_DEBUGGER");
+ /* Run a test that crashes */
+ g_test_trap_subprocess ("/win32/subprocess/illegal_instruction", 0, 0);
+ g_test_trap_assert_failed ();
+ g_test_trap_assert_stderr ("Exception code=0xc000001d*");
+}
+
+static void
+test_veh_debug (void)
+{
+ /* Run a test that crashes and runs a debugger */
+ g_test_trap_subprocess ("/win32/subprocess/debugee", 0, 0);
+ g_test_trap_assert_failed ();
+ g_test_trap_assert_stderr ("Exception code=0xc0000005*Debugger invoked, attaching to*");
+}
+
+static void
+test_veh_debugee (void)
+{
+ /* Set up a debugger to be run on crash */
+ gchar *command = g_strdup_printf ("%s %s", argv0, "%p %e");
+ g_setenv ("G_DEBUGGER", command, TRUE);
+ /* Because the "debugger" here is not really a debugger,
+ * it can't write into stderr of this process, unless
+ * we allow it to inherit our stderr.
+ */
+ g_setenv ("G_DEBUGGER_OLD_CONSOLE", "1", TRUE);
+ g_free (command);
+ /* Crash */
+ test_access_violation ();
+}
+
+static void
+veh_debugger (int argc, char *argv[])
+{
+ char *end;
+ DWORD pid = strtoul (argv[1], &end, 10);
+ guintptr event = (guintptr) _strtoui64 (argv[2], &end, 10);
+ /* Unfreeze the debugee and announce ourselves */
+ SetEvent ((HANDLE) event);
+ CloseHandle ((HANDLE) event);
+ g_fprintf (stderr, "Debugger invoked, attaching to %lu and signalling %" G_GUINTPTR_FORMAT, pid, event);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ argv0 = argv[0];
+
+ g_test_init (&argc, &argv, NULL);
+
+ if (argc > 2)
+ {
+ veh_debugger (argc, argv);
+ return 0;
+ }
+
+ g_test_add_func ("/win32/substitute-pid-and-event", test_subst_pid_and_event);
+
+ g_test_add_func ("/win32/veh/access_violation", test_veh_crash_access_violation);
+ g_test_add_func ("/win32/veh/illegal_instruction", test_veh_crash_illegal_instruction);
+ g_test_add_func ("/win32/veh/debug", test_veh_debug);
+
+ g_test_add_func ("/win32/subprocess/debugee", test_veh_debugee);
+ g_test_add_func ("/win32/subprocess/access_violation", test_access_violation);
+ g_test_add_func ("/win32/subprocess/illegal_instruction", test_illegal_instruction);
+
+ return g_test_run();
+}