491 lines
16 KiB
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);
|
||
|
}
|