PIKApp/app/errors.c

491 lines
16 KiB
C

/* PIKA - Photo and Image Kooker Application
* a rebranding of The GNU Image Manipulation Program (created with heckimp)
* A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio
*
* Original copyright, applying to most contents (license remains unchanged):
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "config.h"
#define _GNU_SOURCE /* need the POSIX signal API */
#include <stdlib.h>
#include <string.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <gio/gio.h>
#include <glib/gstdio.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <gegl.h>
#include "libpikabase/pikabase.h"
#include "core/core-types.h"
#include "core/pika.h"
#include "core/pikadrawable.h"
#include "core/pikaimage.h"
#include "core/pikaitem.h"
#include "core/pikaparamspecs.h"
#include "config/pikacoreconfig.h"
#include "pdb/pikapdb.h"
#include "errors.h"
#include "pika-log.h"
#ifdef G_OS_WIN32
#include <windows.h>
#endif
/* private variables */
static Pika *the_errors_pika = NULL;
static gboolean use_debug_handler = FALSE;
static PikaStackTraceMode stack_trace_mode = PIKA_STACK_TRACE_QUERY;
static gchar *full_prog_name = NULL;
static gchar *backtrace_file = NULL;
static gchar *backup_path = NULL;
static GFile *backup_file = NULL;
static PikaLogHandler log_domain_handler = 0;
static guint global_handler_id = 0;
/* local function prototypes */
static void pika_message_log_func (const gchar *log_domain,
GLogLevelFlags flags,
const gchar *message,
gpointer data);
static void pika_error_log_func (const gchar *domain,
GLogLevelFlags flags,
const gchar *message,
gpointer data) G_GNUC_NORETURN;
static G_GNUC_NORETURN void pika_eek (const gchar *reason,
const gchar *message,
gboolean use_handler);
/* public functions */
void
errors_init (Pika *pika,
const gchar *_full_prog_name,
gboolean _use_debug_handler,
PikaStackTraceMode _stack_trace_mode,
const gchar *_backtrace_file)
{
g_return_if_fail (PIKA_IS_PIKA (pika));
g_return_if_fail (_full_prog_name != NULL);
g_return_if_fail (full_prog_name == NULL);
#ifdef PIKA_UNSTABLE
g_printerr ("This is a development version of PIKA. "
"Debug messages may appear here.\n\n");
#endif /* PIKA_UNSTABLE */
the_errors_pika = pika;
use_debug_handler = _use_debug_handler ? TRUE : FALSE;
stack_trace_mode = _stack_trace_mode;
full_prog_name = g_strdup (_full_prog_name);
/* Create parent directories for both the crash and backup files. */
backtrace_file = g_path_get_dirname (_backtrace_file);
backup_path = g_build_filename (pika_directory (), "backups", NULL);
g_mkdir_with_parents (backtrace_file, S_IRUSR | S_IWUSR | S_IXUSR);
g_free (backtrace_file);
backtrace_file = g_strdup (_backtrace_file);
g_mkdir_with_parents (backup_path, S_IRUSR | S_IWUSR | S_IXUSR);
g_free (backup_path);
backup_path = g_build_filename (pika_directory (), "backups",
"backup-XXX.xcf", NULL);
backup_file = g_file_new_for_path (backup_path);
log_domain_handler = pika_log_set_handler (FALSE,
G_LOG_LEVEL_WARNING |
G_LOG_LEVEL_MESSAGE |
G_LOG_LEVEL_CRITICAL,
pika_message_log_func, pika);
global_handler_id = g_log_set_handler (NULL,
G_LOG_LEVEL_ERROR | G_LOG_FLAG_FATAL,
pika_error_log_func, pika);
}
void
errors_exit (void)
{
if (log_domain_handler)
{
pika_log_remove_handler (log_domain_handler);
log_domain_handler = 0;
}
if (global_handler_id)
{
g_log_remove_handler (NULL, global_handler_id);
global_handler_id = 0;
}
the_errors_pika = NULL;
if (backtrace_file)
g_free (backtrace_file);
if (full_prog_name)
g_free (full_prog_name);
if (backup_path)
g_free (backup_path);
if (backup_file)
g_object_unref (backup_file);
}
GList *
errors_recovered (void)
{
GList *recovered = NULL;
gchar *backup_path = g_build_filename (pika_directory (), "backups", NULL);
GDir *backup_dir = NULL;
if ((backup_dir = g_dir_open (backup_path, 0, NULL)))
{
const gchar *file;
while ((file = g_dir_read_name (backup_dir)))
{
if (g_str_has_suffix (file, ".xcf"))
{
gchar *path = g_build_filename (backup_path, file, NULL);
if (g_file_test (path, G_FILE_TEST_IS_REGULAR) &&
! g_file_test (path, G_FILE_TEST_IS_SYMLINK))
{
/* A quick basic security check. It is not foolproof,
* but better than nothing to make sure we are not
* trying to read, then delete a folder or a symlink
* to a file outside the backup directory.
*/
recovered = g_list_append (recovered, path);
}
else
{
g_free (path);
}
}
}
g_dir_close (backup_dir);
}
g_free (backup_path);
return recovered;
}
void
pika_fatal_error (const gchar *message)
{
pika_eek ("fatal error", message, TRUE);
}
void
pika_terminate (const gchar *message)
{
pika_eek ("terminated", message, use_debug_handler);
}
/* private functions */
static void
pika_message_log_func (const gchar *log_domain,
GLogLevelFlags flags,
const gchar *message,
gpointer data)
{
Pika *pika = data;
PikaCoreConfig *config = pika->config;
const gchar *msg_domain = NULL;
PikaMessageSeverity severity = PIKA_MESSAGE_WARNING;
gboolean gui_message = TRUE;
PikaDebugPolicy debug_policy;
/* All PIKA messages are processed under the same domain, but
* we need to keep the log domain information for third party
* messages.
*/
if (! log_domain ||
(! g_str_has_prefix (log_domain, "Pika") &&
! g_str_has_prefix (log_domain, "LibPika")))
msg_domain = log_domain;
/* If debug policy requires it, WARNING and CRITICAL errors must be
* routed for appropriate debugging.
*/
g_object_get (G_OBJECT (config),
"debug-policy", &debug_policy,
NULL);
switch (flags & G_LOG_LEVEL_MASK)
{
case G_LOG_LEVEL_WARNING:
severity = PIKA_MESSAGE_BUG_WARNING;
if (debug_policy > PIKA_DEBUG_POLICY_WARNING)
gui_message = FALSE;
break;
case G_LOG_LEVEL_CRITICAL:
severity = PIKA_MESSAGE_BUG_CRITICAL;
if (debug_policy > PIKA_DEBUG_POLICY_CRITICAL)
gui_message = FALSE;
break;
}
if (pika && gui_message)
{
pika_show_message (pika, NULL, severity, msg_domain, message);
}
else
{
const gchar *reason = "Message";
pika_enum_get_value (PIKA_TYPE_MESSAGE_SEVERITY, severity,
NULL, NULL, &reason, NULL);
g_printerr ("%s: %s-%s: %s\n",
pika_filename_to_utf8 (full_prog_name),
log_domain, reason, message);
}
}
static void
pika_error_log_func (const gchar *domain,
GLogLevelFlags flags,
const gchar *message,
gpointer data)
{
pika_fatal_error (message);
}
static void
pika_eek (const gchar *reason,
const gchar *message,
gboolean use_handler)
{
PikaCoreConfig *config = the_errors_pika->config;
gboolean eek_handled = FALSE;
PikaDebugPolicy debug_policy;
GList *iter;
gint num_idx;
gint i = 0;
/* PIKA has 2 ways to handle termination signals and fatal errors: one
* is the stack trace mode which is set at start as command line
* option --stack-trace-mode, this won't change for the length of the
* session and outputs a trace in terminal; the other is set in
* preferences, outputs a trace in a GUI and can change anytime during
* the session.
* The GUI backtrace has priority if it is set.
*/
g_object_get (G_OBJECT (config),
"debug-policy", &debug_policy,
NULL);
/* Let's just always output on stdout at least so that there is a
* trace if the rest fails. */
g_printerr ("%s: %s: %s\n", full_prog_name, reason, message);
#if ! defined (G_OS_WIN32) || defined (HAVE_EXCHNDL)
if (use_handler)
{
#ifndef PIKA_CONSOLE_COMPILATION
if (debug_policy != PIKA_DEBUG_POLICY_NEVER &&
! the_errors_pika->no_interface &&
backtrace_file)
{
FILE *fd;
gboolean has_backtrace = TRUE;
/* If GUI backtrace enabled (it is disabled by default), it
* takes precedence over the command line argument.
*/
#ifdef G_OS_WIN32
const gchar *pikadebug = "pika-debug-tool-" PIKA_TOOL_VERSION ".exe";
#elif defined (PLATFORM_OSX)
const gchar *pikadebug = "pika-debug-tool-" PIKA_TOOL_VERSION;
#else
const gchar *pikadebug = LIBEXECDIR "/pika-debug-tool-" PIKA_TOOL_VERSION;
#endif
gchar *args[9] = { (gchar *) pikadebug, full_prog_name, NULL,
(gchar *) reason, (gchar *) message,
backtrace_file, the_errors_pika->config->last_known_release,
NULL, NULL };
gchar pid[16];
gchar timestamp[16];
g_snprintf (pid, 16, "%u", (guint) getpid ());
args[2] = pid;
g_snprintf (timestamp, 16, "%"G_GINT64_FORMAT, the_errors_pika->config->last_release_timestamp);
args[7] = timestamp;
#ifndef G_OS_WIN32
/* On Win32, the trace has already been processed by ExcHnl
* and is waiting for us in a text file.
*/
fd = g_fopen (backtrace_file, "w");
has_backtrace = pika_stack_trace_print ((const gchar *) full_prog_name,
fd, NULL);
fclose (fd);
#endif
/* We don't care about any return value. If it fails, too
* bad, we just won't have any stack trace.
* We still need to use the sync() variant because we have
* to keep PIKA up long enough for the debugger to get its
* trace.
*/
if (has_backtrace &&
g_file_test (backtrace_file, G_FILE_TEST_IS_REGULAR) &&
g_spawn_async (NULL, args, NULL,
G_SPAWN_SEARCH_PATH | G_SPAWN_STDERR_TO_DEV_NULL | G_SPAWN_STDOUT_TO_DEV_NULL,
NULL, NULL, NULL, NULL))
eek_handled = TRUE;
}
#endif /* !PIKA_CONSOLE_COMPILATION */
#ifndef G_OS_WIN32
if (! eek_handled)
{
switch (stack_trace_mode)
{
case PIKA_STACK_TRACE_NEVER:
break;
case PIKA_STACK_TRACE_QUERY:
{
sigset_t sigset;
sigemptyset (&sigset);
sigprocmask (SIG_SETMASK, &sigset, NULL);
if (the_errors_pika)
pika_gui_ungrab (the_errors_pika);
pika_stack_trace_query ((const gchar *) full_prog_name);
}
break;
case PIKA_STACK_TRACE_ALWAYS:
{
sigset_t sigset;
sigemptyset (&sigset);
sigprocmask (SIG_SETMASK, &sigset, NULL);
pika_stack_trace_print ((const gchar *) full_prog_name,
stdout, NULL);
}
break;
default:
break;
}
}
#endif /* ! G_OS_WIN32 */
}
#endif /* ! G_OS_WIN32 || HAVE_EXCHNDL */
#if defined (G_OS_WIN32) && ! defined (PIKA_CONSOLE_COMPILATION)
/* g_on_error_* don't do anything reasonable on Win32. */
if (! eek_handled && ! the_errors_pika->no_interface)
{
char *utf8 = g_strdup_printf ("%s: %s", reason, message);
wchar_t *utf16 = g_utf8_to_utf16 (utf8, -1, NULL, NULL, NULL);
MessageBoxW (NULL, utf16 ? utf16 : L"Generic error",
L"PIKA", MB_OK | MB_ICONERROR);
g_free (utf16);
g_free (utf8);
}
#endif
/* Let's try to back-up all unsaved images!
* It is not 100%: when I tested with various bugs created on purpose,
* I had cases where saving failed. I am not sure if this is because
* of some memory management along the way to XCF saving or some other
* messed up state of PIKA, but this is normal not to expect too much
* during a crash.
* Nevertheless in various test cases, I had successful backups XCF of
* the work in progress. Yeah!
*/
if (backup_path)
{
/* increase the busy counter, so XCF saving calling
* pika_set_busy() and pika_unset_busy() won't call the GUI
* layer and do whatever windowing system calls to set cursors.
*/
the_errors_pika->busy++;
/* The index of 'XXX' in backup_path string. */
num_idx = strlen (backup_path) - 7;
iter = pika_get_image_iter (the_errors_pika);
for (; iter && i < 1000; iter = iter->next)
{
PikaImage *image = iter->data;
if (! pika_image_is_dirty (image))
continue;
/* This is a trick because we want to avoid any memory
* allocation when the process is abnormally terminated.
* We just assume that you'll never have more than 1000 images
* open (which is already far fetched).
*/
backup_path[num_idx + 2] = '0' + (i % 10);
backup_path[num_idx + 1] = '0' + ((i/10) % 10);
backup_path[num_idx] = '0' + ((i/100) % 10);
/* Saving. */
pika_pdb_execute_procedure_by_name (the_errors_pika->pdb,
pika_get_user_context (the_errors_pika),
NULL, NULL,
"pika-xcf-save",
PIKA_TYPE_RUN_MODE, PIKA_RUN_NONINTERACTIVE,
PIKA_TYPE_IMAGE, image,
G_TYPE_INT, 0,
PIKA_TYPE_OBJECT_ARRAY, NULL,
G_TYPE_FILE, backup_file,
G_TYPE_NONE);
g_rename (g_file_peek_path (backup_file), backup_path);
i++;
}
}
exit (EXIT_FAILURE);
}