/*
 * Contains code taken from Flatpak.
 *
 * Copyright © 2014-2019 Red Hat, Inc
 * Copyright © 2017-2019 Collabora Ltd.
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * This program 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 <http://www.gnu.org/licenses/>.
 */

#include "utils.h"

#include <ftw.h>
#include <sys/prctl.h>
#include <sys/signalfd.h>
#include <sys/types.h>
#include <sys/wait.h>

#include <glib.h>
#include <glib-unix.h>
#include <glib/gstdio.h>
#include <gio/gio.h>

#include "libglnx/libglnx.h"

#include "steam-runtime-tools/glib-backports-internal.h"
#include "steam-runtime-tools/resolve-in-sysroot-internal.h"
#include "steam-runtime-tools/utils-internal.h"
#include "flatpak-bwrap-private.h"
#include "flatpak-utils-base-private.h"
#include "flatpak-utils-private.h"

static int my_pid = -1;
static const gchar *my_prgname = NULL;

/**
 * pv_envp_cmp:
 * @p1: a `const char * const *`
 * @p2: a `const char * const *`
 *
 * Compare two environment variables, given as pointers to pointers
 * to the actual `KEY=value` string.
 *
 * In particular this is suitable for sorting a #GStrv using `qsort`.
 *
 * Returns: negative, 0 or positive if `*p1` compares before, equal to
 *  or after `*p2`
 */
int
pv_envp_cmp (const void *p1,
             const void *p2)
{
  const char * const * s1 = p1;
  const char * const * s2 = p2;
  size_t l1 = strlen (*s1);
  size_t l2 = strlen (*s2);
  size_t min;
  const char *tmp;
  int ret;

  tmp = strchr (*s1, '=');

  if (tmp != NULL)
    l1 = tmp - *s1;

  tmp = strchr (*s2, '=');

  if (tmp != NULL)
    l2 = tmp - *s2;

  min = MIN (l1, l2);
  ret = strncmp (*s1, *s2, min);

  /* If they differ before the first '=' (if any) in either s1 or s2,
   * then they are certainly different */
  if (ret != 0)
    return ret;

  ret = strcmp (*s1, *s2);

  /* If they do not differ at all, then they are equal */
  if (ret == 0)
    return ret;

  /* FOO < FOO=..., and FOO < FOOBAR */
  if ((*s1)[min] == '\0')
    return -1;

  /* FOO=... > FOO, and FOOBAR > FOO */
  if ((*s2)[min] == '\0')
    return 1;

  /* FOO= < FOOBAR */
  if ((*s1)[min] == '=' && (*s2)[min] != '=')
    return -1;

  /* FOOBAR > FOO= */
  if ((*s2)[min] == '=' && (*s1)[min] != '=')
    return 1;

  /* Fall back to plain string comparison */
  return ret;
}

/**
 * pv_get_current_dirs:
 * @cwd_p: (out) (transfer full) (optional): Used to return the
 *  current physical working directory, equivalent to `$(pwd -P)`
 *  in a shell
 * @cwd_l: (out) (transfer full) (optional): Used to return the
 *  current logical working directory, equivalent to `$(pwd -L)`
 *  in a shell
 *
 * Return the physical and/or logical working directory.
 *
 * Equivalent to `$(pwd -L)` in a shell.
 */
void
pv_get_current_dirs (gchar **cwd_p,
                     gchar **cwd_l)
{
  g_autofree gchar *cwd = NULL;
  const gchar *pwd;

  g_return_if_fail (cwd_p == NULL || *cwd_p == NULL);
  g_return_if_fail (cwd_l == NULL || *cwd_l == NULL);

  cwd = g_get_current_dir ();

  if (cwd_p != NULL)
    *cwd_p = flatpak_canonicalize_filename (cwd);

  if (cwd_l != NULL)
    {
      pwd = g_getenv ("PWD");

      if (pwd != NULL && _srt_is_same_file (pwd, cwd))
        *cwd_l = g_strdup (pwd);
      else
        *cwd_l = g_strdup (cwd);
    }
}

void
pv_search_path_append (GString *search_path,
                       const gchar *item)
{
  g_return_if_fail (search_path != NULL);

  if (item == NULL || item[0] == '\0')
    return;

  if (search_path->len != 0)
    g_string_append (search_path, ":");

  g_string_append (search_path, item);
}

gboolean
pv_run_sync (const char * const * argv,
             const char * const * envp,
             int *exit_status_out,
             char **output_out,
             GError **error)
{
  gsize len;
  gint wait_status;
  g_autofree gchar *output = NULL;
  g_autofree gchar *errors = NULL;
  gsize i;
  g_autoptr(GString) command = g_string_new ("");

  g_return_val_if_fail (argv != NULL, FALSE);
  g_return_val_if_fail (argv[0] != NULL, FALSE);
  g_return_val_if_fail (output_out == NULL || *output_out == NULL, FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  if (exit_status_out != NULL)
    *exit_status_out = -1;

  for (i = 0; argv[i] != NULL; i++)
    {
      g_autofree gchar *quoted = g_shell_quote (argv[i]);

      g_string_append_printf (command, " %s", quoted);
    }

  g_debug ("run:%s", command->str);

  /* We use LEAVE_DESCRIPTORS_OPEN to work around a deadlock in older GLib,
   * and to avoid wasting a lot of time closing fds if the rlimit for
   * maximum open file descriptors is high. Because we're waiting for the
   * subprocess to finish anyway, it doesn't really matter that any fds
   * that are not close-on-execute will get leaked into the child. */
  if (!g_spawn_sync (NULL,  /* cwd */
                     (char **) argv,
                     (char **) envp,
                     (G_SPAWN_SEARCH_PATH |
                      G_SPAWN_LEAVE_DESCRIPTORS_OPEN),
                     NULL, NULL,
                     &output,
                     &errors,
                     &wait_status,
                     error))
    return FALSE;

  g_printerr ("%s", errors);

  if (exit_status_out != NULL)
    {
      if (WIFEXITED (wait_status))
        *exit_status_out = WEXITSTATUS (wait_status);
    }

  len = strlen (output);

  /* Emulate shell $() */
  if (len > 0 && output[len - 1] == '\n')
    output[len - 1] = '\0';

  g_debug ("-> %s", output);

  if (!g_spawn_check_exit_status (wait_status, error))
    return FALSE;

  if (output_out != NULL)
    *output_out = g_steal_pointer (&output);

  return TRUE;
}

/*
 * Returns: (transfer none): The first key in @table in iteration order,
 *  or %NULL if @table is empty.
 */
gpointer
pv_hash_table_get_arbitrary_key (GHashTable *table)
{
  GHashTableIter iter;
  gpointer key = NULL;

  g_hash_table_iter_init (&iter, table);
  if (g_hash_table_iter_next (&iter, &key, NULL))
    return key;
  else
    return NULL;
}

gboolean
pv_boolean_environment (const gchar *name,
                        gboolean def)
{
  const gchar *value = g_getenv (name);

  if (g_strcmp0 (value, "1") == 0)
    return TRUE;

  if (g_strcmp0 (value, "") == 0 || g_strcmp0 (value, "0") == 0)
    return FALSE;

  if (value != NULL)
    g_warning ("Unrecognised value \"%s\" for $%s", value, name);

  return def;
}

/**
 * pv_async_signal_safe_error:
 * @message: A human-readable message
 * @exit_status: Call `_exit` with this status
 *
 * Exit with a fatal error, like g_error(), but async-signal-safe
 * (see signal-safety(7)).
 */
void
pv_async_signal_safe_error (const char *message,
                            int exit_status)
{
  if (write (2, message, strlen (message)) < 0)
    {
      /* Ignore - there's nothing we can do about it anyway - but
       * suppress -Wunused-result. */
    }

  _exit (exit_status);
}

#define PROC_SYS_KERNEL_RANDOM_UUID "/proc/sys/kernel/random/uuid"

/**
 * pv_get_random_uuid:
 * @error: Used to raise an error on failure
 *
 * Return a random UUID (RFC 4122 version 4) as a string.
 * It is a 128-bit quantity, with 122 bits of entropy, and 6 fixed bits
 * indicating the "variant" (type, 0b10) and "version" (subtype, 0b0100).
 *
 * Returns: (transfer full): A random UUID, or %NULL on error
 */
gchar *
pv_get_random_uuid (GError **error)
{
  g_autofree gchar *contents = NULL;

  if (!g_file_get_contents (PROC_SYS_KERNEL_RANDOM_UUID,
                            &contents, NULL, error))
    return NULL;

  g_strchomp (contents);    /* delete trailing newline */

  /* Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx */
  if (strlen (contents) != 36)
    return glnx_null_throw (error, "%s not in expected format",
                            PROC_SYS_KERNEL_RANDOM_UUID);

  return g_steal_pointer (&contents);
}

/**
 * pv_wait_for_child_processes:
 * @main_process: process for which we will report wait-status;
 *  zero or negative if there is no main process
 * @wait_status_out: (out): Used to store the wait status of `@main_process`,
 *  as if from `wait()`, on success
 * @error: Used to raise an error on failure
 *
 * Wait for child processes of this process to exit, until the @main_process
 * has exited. If there is no main process, wait until there are no child
 * processes at all.
 *
 * If the process is a subreaper (`PR_SET_CHILD_SUBREAPER`),
 * indirect child processes whose parents have exited will be reparented
 * to it, so this will have the effect of waiting for all descendants.
 *
 * If @main_process is positive, return when @main_process has exited.
 * Child processes that exited before @main_process will also have been
 * "reaped", but child processes that exit after @main_process will not
 * (use `pv_wait_for_child_processes (0, &error)` to resume waiting).
 *
 * If @main_process is zero or negative, wait for all child processes
 * to exit.
 *
 * This function cannot be called in a process that is using
 * g_child_watch_source_new() or similar functions, because it waits
 * for all child processes regardless of their process IDs, and that is
 * incompatible with waiting for individual child processes.
 *
 * Returns: %TRUE when @main_process has exited, or if @main_process
 *  is zero or negative and all child processes have exited
 */
gboolean
pv_wait_for_child_processes (pid_t main_process,
                             int *wait_status_out,
                             GError **error)
{
  if (wait_status_out != NULL)
    *wait_status_out = -1;

  while (1)
    {
      int wait_status = -1;
      pid_t died = wait (&wait_status);

      if (died < 0)
        {
          if (errno == EINTR)
            {
              continue;
            }
          else if (errno == ECHILD)
            {
              g_debug ("No more child processes");
              break;
            }
          else
            {
              return glnx_throw_errno_prefix (error, "wait");
            }
        }

      g_debug ("Child %lld exited with wait status %d",
               (long long) died, wait_status);

      if (died == main_process)
        {
          if (wait_status_out != NULL)
            *wait_status_out = wait_status;

          return TRUE;
        }
    }

  if (main_process > 0)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
                   "Process %lld was not seen to exit",
                   (long long) main_process);
      return FALSE;
    }

  return TRUE;
}

typedef struct
{
  GError *error;
  GMainContext *context;
  GSource *wait_source;
  GSource *kill_source;
  GSource *sigchld_source;
  gchar *children_file;
  /* GINT_TO_POINTER (pid) => arbitrary */
  GHashTable *sent_sigterm;
  /* GINT_TO_POINTER (pid) => arbitrary */
  GHashTable *sent_sigkill;
  /* Scratch space to build temporary strings */
  GString *buffer;
  /* 0, SIGTERM or SIGKILL */
  int sending_signal;
  /* Nonzero if wait_source has been attached to context */
  guint wait_source_id;
  guint kill_source_id;
  guint sigchld_source_id;
  /* TRUE if we reach a point where we have no more child processes. */
  gboolean finished;
} TerminationData;

/*
 * Free everything in @data.
 */
static void
termination_data_clear (TerminationData *data)
{
  if (data->wait_source_id != 0)
    {
      g_source_destroy (data->wait_source);
      data->wait_source_id = 0;
    }

  if (data->kill_source_id != 0)
    {
      g_source_destroy (data->kill_source);
      data->kill_source_id = 0;
    }

  if (data->sigchld_source_id != 0)
    {
      g_source_destroy (data->sigchld_source);
      data->sigchld_source_id = 0;
    }

  if (data->wait_source != NULL)
    g_source_unref (g_steal_pointer (&data->wait_source));

  if (data->kill_source != NULL)
    g_source_unref (g_steal_pointer (&data->kill_source));

  if (data->sigchld_source != NULL)
    g_source_unref (g_steal_pointer (&data->sigchld_source));

  g_clear_pointer (&data->children_file, g_free);
  g_clear_pointer (&data->context, g_main_context_unref);
  g_clear_error (&data->error);
  g_clear_pointer (&data->sent_sigterm, g_hash_table_unref);
  g_clear_pointer (&data->sent_sigkill, g_hash_table_unref);

  if (data->buffer != NULL)
    g_string_free (g_steal_pointer (&data->buffer), TRUE);
}

G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (TerminationData, termination_data_clear)

/*
 * Do whatever the next step for pv_terminate_all_child_processes() is.
 *
 * First, reap child processes that already exited, without blocking.
 *
 * Then, act according to the phase we are in:
 * - before wait_period: do nothing
 * - after wait_period but before grace_period: send SIGTERM
 * - after wait_period and grace_period: send SIGKILL
 */
static void
termination_data_refresh (TerminationData *data)
{
  g_autofree gchar *contents = NULL;
  gboolean has_child = FALSE;
  const char *p;
  char *endptr;

  if (data->error != NULL)
    return;

  g_debug ("Checking for child processes");

  while (1)
    {
      int wait_status = -1;
      pid_t died = waitpid (-1, &wait_status, WNOHANG);

      if (died < 0)
        {
          if (errno == EINTR)
            {
              continue;
            }
          else if (errno == ECHILD)
            {
              /* No child processes at all. We'll double-check this
               * a bit later. */
              break;
            }
          else
            {
              glnx_throw_errno_prefix (&data->error, "wait");
              return;
            }
        }
      else if (died == 0)
        {
          /* No more child processes have exited, but at least one is
           * still running. */
          break;
        }

      /* This process has gone away, so remove any record that we have
       * sent it signals. If the pid is reused, we'll want to send
       * the same signals again. */
      g_debug ("Process %d exited", died);
      g_hash_table_remove (data->sent_sigkill, GINT_TO_POINTER (died));
      g_hash_table_remove (data->sent_sigterm, GINT_TO_POINTER (died));
    }

  /* See whether we have any remaining children. These could be direct
   * child processes, or they could be children we adopted because
   * their parent was one of our descendants and has exited, leaving the
   * child to be reparented to us (their (great)*grandparent) because we
   * are a subreaper. */
  if (!g_file_get_contents (data->children_file, &contents, NULL, &data->error))
    return;

  g_debug ("Child tasks: %s", contents);

  for (p = contents;
       p != NULL && *p != '\0';
       p = endptr)
    {
      guint64 maybe_child;
      int child;
      GHashTable *already;

      while (*p != '\0' && g_ascii_isspace (*p))
        p++;

      if (*p == '\0')
        break;

      maybe_child = g_ascii_strtoull (p, &endptr, 10);

      if (*endptr != '\0' && !g_ascii_isspace (*endptr))
        {
          glnx_throw (&data->error, "Non-numeric string found in %s: %s",
                      data->children_file, p);
          return;
        }

      if (maybe_child > G_MAXINT)
        {
          glnx_throw (&data->error, "Out-of-range number found in %s: %s",
                      data->children_file, p);
          return;
        }

      child = (int) maybe_child;
      g_string_printf (data->buffer, "/proc/%d", child);

      /* If the task is just a thread, it won't have a /proc/%d directory
       * in its own right. We don't kill threads, only processes. */
      if (!g_file_test (data->buffer->str, G_FILE_TEST_IS_DIR))
        {
          g_debug ("Task %d is a thread, not a process", child);
          continue;
        }

      has_child = TRUE;

      if (data->sending_signal == 0)
        break;
      else if (data->sending_signal == SIGKILL)
        already = data->sent_sigkill;
      else
        already = data->sent_sigterm;

      if (!g_hash_table_contains (already, GINT_TO_POINTER (child)))
        {
          g_debug ("Sending signal %d to process %d",
                   data->sending_signal, child);
          g_hash_table_add (already, GINT_TO_POINTER (child));

          if (kill (child, data->sending_signal) < 0)
            g_warning ("Unable to send signal %d to process %d: %s",
                       data->sending_signal, child, g_strerror (errno));

          /* In case the child is stopped, wake it up to receive the signal */
          if (kill (child, SIGCONT) < 0)
            g_warning ("Unable to send SIGCONT to process %d: %s",
                       child, g_strerror (errno));

          /* When the child terminates, we will get SIGCHLD and come
           * back to here. */
        }
    }

  if (!has_child)
    data->finished = TRUE;
}

/*
 * Move from wait period to grace period: start sending SIGTERM to
 * child processes.
 */
static gboolean
start_sending_sigterm (gpointer user_data)
{
  TerminationData *data = user_data;

  g_debug ("Wait period finished, starting to send SIGTERM...");

  if (data->sending_signal == 0)
    data->sending_signal = SIGTERM;

  termination_data_refresh (data);

  data->wait_source_id = 0;
  return G_SOURCE_REMOVE;
}

/*
 * End of grace period: start sending SIGKILL to child processes.
 */
static gboolean
start_sending_sigkill (gpointer user_data)
{
  TerminationData *data = user_data;

  g_debug ("Grace period finished, starting to send SIGKILL...");

  data->sending_signal = SIGKILL;
  termination_data_refresh (data);

  data->kill_source_id = 0;
  return G_SOURCE_REMOVE;
}

/*
 * Called when at least one child process has exited, resulting in
 * SIGCHLD to this process.
 */
static gboolean
sigchld_cb (int sfd,
            G_GNUC_UNUSED GIOCondition condition,
            gpointer user_data)
{
  TerminationData *data = user_data;
  struct signalfd_siginfo info;
  ssize_t size;

  size = read (sfd, &info, sizeof (info));

  if (size < 0)
    {
      if (errno != EINTR && errno != EAGAIN)
        g_warning ("Unable to read struct signalfd_siginfo: %s",
                   g_strerror (errno));
    }
  else if (size != sizeof (info))
    {
      g_warning ("Expected struct signalfd_siginfo of size %"
                 G_GSIZE_FORMAT ", got %" G_GSSIZE_FORMAT,
                 sizeof (info), size);
    }

  g_debug ("One or more child processes exited");
  termination_data_refresh (data);
  return G_SOURCE_CONTINUE;
}

/**
 * pv_terminate_all_child_processes:
 * @wait_period: If greater than 0, wait this many microseconds before
 *  sending `SIGTERM`
 * @grace_period: If greater than 0, after @wait_period plus this many
 *  microseconds, use `SIGKILL` instead of `SIGTERM`. If 0, proceed
 *  directly to sending `SIGKILL`.
 * @error: Used to raise an error on failure
 *
 * Make sure all child processes are terminated.
 *
 * If a child process catches `SIGTERM` but does not exit promptly and
 * does not pass the signal on to its descendants, note that its
 * descendant processes are not guaranteed to be terminated gracefully
 * with `SIGTERM`; they might only receive `SIGKILL`.
 *
 * Return when all child processes have exited or when an error has
 * occurred.
 *
 * This function cannot be called in a process that is using
 * g_child_watch_source_new() or similar functions.
 *
 * The process must be a subreaper, and must have `SIGCHLD` blocked.
 *
 * Returns: %TRUE on success.
 */
gboolean
pv_terminate_all_child_processes (GTimeSpan wait_period,
                                  GTimeSpan grace_period,
                                  GError **error)
{
  g_auto(TerminationData) data = {};
  sigset_t mask;
  int is_subreaper = -1;
  glnx_autofd int sfd = -1;

  if (prctl (PR_GET_CHILD_SUBREAPER, (unsigned long) &is_subreaper, 0, 0, 0) != 0)
    return glnx_throw_errno_prefix (error, "prctl PR_GET_CHILD_SUBREAPER");

  if (is_subreaper != 1)
    return glnx_throw (error, "Process is not a subreaper");

  sigemptyset (&mask);

  if (pthread_sigmask (SIG_BLOCK, NULL, &mask) != 0)
    return glnx_throw_errno_prefix (error, "pthread_sigmask");

  if (!sigismember (&mask, SIGCHLD))
    return glnx_throw (error, "Process has not blocked SIGCHLD");

  data.children_file = g_strdup_printf ("/proc/%d/task/%d/children",
                                        getpid (), getpid ());
  data.context = g_main_context_new ();
  data.sent_sigterm = g_hash_table_new (NULL, NULL);
  data.sent_sigkill = g_hash_table_new (NULL, NULL);
  data.buffer = g_string_new ("/proc/2345678901");

  if (wait_period > 0 && grace_period > 0)
    {
      data.wait_source = g_timeout_source_new (wait_period / G_TIME_SPAN_MILLISECOND);

      g_source_set_callback (data.wait_source, start_sending_sigterm,
                             &data, NULL);
      data.wait_source_id = g_source_attach (data.wait_source, data.context);
    }
  else if (grace_period > 0)
    {
      start_sending_sigterm (&data);
    }

  if (wait_period + grace_period > 0)
    {
      data.kill_source = g_timeout_source_new ((wait_period + grace_period) / G_TIME_SPAN_MILLISECOND);

      g_source_set_callback (data.kill_source, start_sending_sigkill,
                             &data, NULL);
      data.kill_source_id = g_source_attach (data.kill_source, data.context);
    }
  else
    {
      start_sending_sigkill (&data);
    }

  sigemptyset (&mask);
  sigaddset (&mask, SIGCHLD);

  sfd = signalfd (-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);

  if (sfd < 0)
    return glnx_throw_errno_prefix (error, "signalfd");

  data.sigchld_source = g_unix_fd_source_new (sfd, G_IO_IN);
  g_source_set_callback (data.sigchld_source,
                         (GSourceFunc) G_CALLBACK (sigchld_cb), &data, NULL);
  data.sigchld_source_id = g_source_attach (data.sigchld_source, data.context);

  termination_data_refresh (&data);

  while (data.error == NULL
         && !data.finished
         && (data.wait_source_id != 0
             || data.kill_source_id != 0
             || data.sigchld_source_id != 0))
    g_main_context_iteration (data.context, TRUE);

  if (data.error != NULL)
    {
      g_propagate_error (error, g_steal_pointer (&data.error));
      return FALSE;
    }

  return TRUE;
}

/*
 * @str: A path
 * @prefix: A possible prefix
 *
 * The same as flatpak_has_path_prefix(), but instead of a boolean,
 * return the part of @str after @prefix (non-%NULL but possibly empty)
 * if @str has prefix @prefix, or %NULL if it does not.
 *
 * Returns: (nullable) (transfer none): the part of @str after @prefix,
 *  or %NULL if @str is not below @prefix
 */
const char *
pv_get_path_after (const char *str,
                   const char *prefix)
{
  while (TRUE)
    {
      /* Skip consecutive slashes to reach next path
         element */
      while (*str == '/')
        str++;
      while (*prefix == '/')
        prefix++;

      /* No more prefix path elements? Done! */
      if (*prefix == 0)
        return str;

      /* Compare path element */
      while (*prefix != 0 && *prefix != '/')
        {
          if (*str != *prefix)
            return NULL;
          str++;
          prefix++;
        }

      /* Matched prefix path element,
         must be entire str path element */
      if (*str != '/' && *str != 0)
        return NULL;
    }
}

/**
 * pv_current_namespace_path_to_host_path:
 * @current_env_path: a path in the current environment
 *
 * Returns: (transfer full): The @current_env_path converted to the host
 *  system, or a copy of @current_env_path if we are not in a Flatpak
 *  environment or it's unknown how to convert the given path.
 */
gchar *
pv_current_namespace_path_to_host_path (const gchar *current_env_path)
{
  gchar *path_on_host = NULL;

  g_return_val_if_fail (g_path_is_absolute (current_env_path),
                        g_strdup (current_env_path));

  if (g_file_test ("/.flatpak-info", G_FILE_TEST_IS_REGULAR))
    {
      struct stat via_current_env_stat;
      struct stat via_persist_stat;
      const gchar *home = g_getenv ("HOME");
      const gchar *after = NULL;

      if (home == NULL)
        home = g_get_home_dir ();

      if (home != NULL)
        after = pv_get_path_after (current_env_path, home);

      /* If we are inside a Flatpak container, usually, the home
       * folder is '${HOME}/.var/app/${FLATPAK_ID}' on the host system */
      if (after != NULL)
        {
          path_on_host = g_build_filename (home,
                                           ".var",
                                           "app",
                                           g_getenv ("FLATPAK_ID"),
                                           after,
                                           NULL);

          if (lstat (path_on_host, &via_persist_stat) < 0)
            {
              /* The file doesn't exist in ~/.var/app, so assume it was
               * exposed via --filesystem */
              g_clear_pointer (&path_on_host, g_free);
            }
          else if (lstat (current_env_path, &via_current_env_stat) == 0
                   && (via_persist_stat.st_dev != via_current_env_stat.st_dev
                       || via_persist_stat.st_ino != via_current_env_stat.st_ino))
            {
              /* The file exists in ~/.var/app, but is not the same there -
              * presumably a different version was mounted over the top via
              * --filesystem */
              g_clear_pointer (&path_on_host, g_free);
            }
        }

      after = pv_get_path_after (current_env_path, "/run/host");

      /* In a Flatpak container, usually, '/run/host' is the root of the
       * host system */
      if (after != NULL && path_on_host == NULL)
        path_on_host = g_build_filename ("/", after, NULL);
    }
  /* Either we are not in a Flatpak container or it's not obvious how the
   * container to host translation should happen. Just keep the same path. */
  if (path_on_host == NULL)
    path_on_host = g_strdup (current_env_path);

  return path_on_host;
}

static const char *
get_level_prefix (GLogLevelFlags log_level)
{
  if (log_level & (G_LOG_FLAG_RECURSION
                   | G_LOG_FLAG_FATAL
                   | G_LOG_LEVEL_ERROR
                   | G_LOG_LEVEL_CRITICAL))
    return "Internal error";

  if (log_level & PV_LOG_LEVEL_FAILURE)
    return "E";

  if (log_level & G_LOG_LEVEL_WARNING)
    return "W";

  if (log_level & G_LOG_LEVEL_MESSAGE)
    return "N";   /* consistent with apt, which calls this a "notice" */

  if (log_level & G_LOG_LEVEL_INFO)
    return "I";

  if (log_level & G_LOG_LEVEL_DEBUG)
    return "D";

  return "?!";
}

/**
 * log_handler_with_timestamp:
 * @log_domain: the log domain of the message
 * @log_level: the log level of the message
 * @message: the message to process
 * @user_data: not used
 */
static void
pv_log_to_stderr_with_timestamp (const gchar *log_domain,
                                 GLogLevelFlags log_level,
                                 const gchar *message,
                                 gpointer user_data)
{
  g_autoptr(GDateTime) date_time = g_date_time_new_now_local ();

  /* We can't use the format specifier "%f" for microseconds because it
   * was introduced in GLib 2.66 and we are targeting an older version */
  g_autofree gchar *timestamp = g_date_time_format (date_time, "%T");

  g_printerr ("%s.%06i: %s[%d]: %s: %s\n", timestamp,
              g_date_time_get_microsecond (date_time),
              my_prgname, my_pid, get_level_prefix (log_level), message);

  if (log_level & (G_LOG_FLAG_RECURSION
                   | G_LOG_FLAG_FATAL
                   | G_LOG_LEVEL_ERROR))
    G_BREAKPOINT ();
}

/**
 * pv_log_to_stderr:
 * @log_domain: the log domain of the message
 * @log_level: the log level of the message
 * @message: the message to process
 * @user_data: not used
 */
static void
pv_log_to_stderr (const gchar *log_domain,
                  GLogLevelFlags log_level,
                  const gchar *message,
                  gpointer user_data)
{
  g_printerr ("%s[%d]: %s: %s\n",
              my_prgname, my_pid, get_level_prefix (log_level), message);

  if (log_level & (G_LOG_FLAG_RECURSION
                   | G_LOG_FLAG_FATAL
                   | G_LOG_LEVEL_ERROR))
    G_BREAKPOINT ();
}

/**
 * pv_set_up_logging:
 * @opt_verbose: If the log should be more verbose
 */
void
pv_set_up_logging (gboolean opt_verbose)
{
  GLogLevelFlags log_levels = (G_LOG_LEVEL_ERROR
                               | G_LOG_LEVEL_CRITICAL
                               | PV_LOG_LEVEL_FAILURE
                               | G_LOG_LEVEL_WARNING
                               | G_LOG_LEVEL_MESSAGE);
  gboolean opt_timestamp = pv_boolean_environment ("PRESSURE_VESSEL_LOG_WITH_TIMESTAMP",
                                                   FALSE);
  gboolean opt_info = pv_boolean_environment ("PRESSURE_VESSEL_LOG_INFO", FALSE);

  my_pid = getpid ();
  my_prgname = g_get_prgname ();

  if (opt_info)
    log_levels |= G_LOG_LEVEL_INFO;

  if (opt_verbose)
    log_levels |= G_LOG_LEVEL_DEBUG | G_LOG_LEVEL_INFO;

  g_log_set_handler (G_LOG_DOMAIN, log_levels,
                     opt_timestamp ? pv_log_to_stderr_with_timestamp : pv_log_to_stderr,
                     NULL);
}

/**
 * pv_delete_dangling_symlink:
 * @dirfd: An open file descriptor for a directory
 * @debug_path: Path to directory represented by @dirfd, used in debug messages
 * @name: A filename in @dirfd that is thought to be a symbolic link
 *
 * If @name exists in @dirfd and is a symbolic link whose target does not
 * exist, delete it.
 */
void
pv_delete_dangling_symlink (int dirfd,
                            const char *debug_path,
                            const char *name)
{
  struct stat stat_buf, lstat_buf;

  g_return_if_fail (dirfd >= 0);
  g_return_if_fail (name != NULL);
  g_return_if_fail (strcmp (name, "") != 0);
  g_return_if_fail (strcmp (name, ".") != 0);
  g_return_if_fail (strcmp (name, "..") != 0);

  if (fstatat (dirfd, name, &lstat_buf, AT_SYMLINK_NOFOLLOW) == 0)
    {
      if (!S_ISLNK (lstat_buf.st_mode))
        {
          g_debug ("Ignoring %s/%s: not a symlink",
                   debug_path, name);
        }
      else if (fstatat (dirfd, name, &stat_buf, 0) == 0)
        {
          g_debug ("Ignoring %s/%s: symlink target still exists",
                   debug_path, name);
        }
      else if (errno != ENOENT)
        {
          int saved_errno = errno;

          g_debug ("Ignoring %s/%s: fstatat(!NOFOLLOW): %s",
                   debug_path, name, g_strerror (saved_errno));
        }
      else
        {
          g_debug ("Target of %s/%s no longer exists, deleting it",
                   debug_path, name);

          if (unlinkat (dirfd, name, 0) != 0)
            {
              int saved_errno = errno;

              g_debug ("Could not delete %s/%s: unlinkat: %s",
                       debug_path, name, g_strerror (saved_errno));
            }
        }
    }
  else if (errno == ENOENT)
    {
      /* Silently ignore: symlink doesn't exist so we don't need to
       * delete it */
    }
  else
    {
      int saved_errno = errno;

      g_debug ("Ignoring %s/%s: fstatat(NOFOLLOW): %s",
               debug_path, name, g_strerror (saved_errno));
    }
}