/*
 * Copyright (C) 2013-2021 Red Hat, Inc.
 *
 * 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 2 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA.
 */

#include "config.h"

#include "backends/native/meta-device-pool-private.h"

#include <fcntl.h>
#include <gio/gunixfdlist.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>
#include <sys/types.h>

#include "backends/native/meta-launcher.h"
#include "meta/util.h"

#include "meta-dbus-login1.h"

struct _MetaDeviceFile
{
  MetaDevicePool *pool;

  grefcount ref_count;

  char *path;
  int major;
  int minor;
  int fd;
  MetaDeviceFileFlags flags;
  uint32_t tags[META_DEVICE_FILE_N_TAGS];
};

struct _MetaDevicePool
{
  GObject parent;

  MetaDbusLogin1Session *session_proxy;

  GMutex mutex;

  GList *files;
};

G_DEFINE_TYPE (MetaDevicePool, meta_device_pool, G_TYPE_OBJECT)

static void
release_device_file (MetaDevicePool *pool,
                     MetaDeviceFile *file);

static MetaDeviceFile *
meta_device_file_new (MetaDevicePool      *pool,
                      const char          *path,
                      int                  major,
                      int                  minor,
                      int                  fd,
                      MetaDeviceFileFlags  flags)
{
  MetaDeviceFile *file;

  file = g_new0 (MetaDeviceFile, 1);

  file->pool = pool;
  g_ref_count_init (&file->ref_count);

  file->path = g_strdup (path);
  file->major = major;
  file->minor = minor;
  file->fd = fd;
  file->flags = flags;

  return file;
}

static void
meta_device_file_free (MetaDeviceFile *file)
{
  g_free (file->path);
  g_free (file);
}

int
meta_device_file_get_fd (MetaDeviceFile *device_file)
{
  g_assert (!g_ref_count_compare (&device_file->ref_count, 0));

  return device_file->fd;
}

const char *
meta_device_file_get_path (MetaDeviceFile *device_file)
{
  return device_file->path;
}

void
meta_device_file_tag (MetaDeviceFile     *device_file,
                      MetaDeviceFileTags  tag,
                      uint32_t            value)
{
  device_file->tags[tag] |= value;
}

uint32_t
meta_device_file_has_tag (MetaDeviceFile     *device_file,
                          MetaDeviceFileTags  tag,
                          uint32_t            value)
{
  return (device_file->tags[tag] & value) == value;
}

static MetaDeviceFile *
meta_device_file_acquire_locked (MetaDeviceFile *file)
{
  g_ref_count_inc (&file->ref_count);
  return file;
}

MetaDeviceFile *
meta_device_file_acquire (MetaDeviceFile *file)
{
  g_mutex_lock (&file->pool->mutex);
  meta_topic (META_DEBUG_BACKEND, "Acquiring device file '%s'", file->path);
  meta_device_file_acquire_locked (file);
  g_mutex_unlock (&file->pool->mutex);

  return file;
}

void
meta_device_file_release (MetaDeviceFile *file)
{
  g_warn_if_fail (file->fd != -1);

  release_device_file (file->pool, file);
}

MetaDevicePool *
meta_device_file_get_pool (MetaDeviceFile *device_file)
{
  return device_file->pool;
}

static MetaDeviceFile *
find_device_file_from_path (MetaDevicePool *pool,
                            const char     *path)
{
  GList *l;

  for (l = pool->files; l; l = l->next)
    {
      MetaDeviceFile *file = l->data;

      if (g_strcmp0 (file->path, path) == 0)
        return file;
    }

  return NULL;
}

static gboolean
take_device (MetaDbusLogin1Session  *session_proxy,
             int                     dev_major,
             int                     dev_minor,
             int                    *out_fd,
             GCancellable           *cancellable,
             GError                **error)
{
  g_autoptr (GVariant) fd_variant = NULL;
  g_autoptr (GUnixFDList) fd_list = NULL;
  int fd = -1;

  if (!meta_dbus_login1_session_call_take_device_sync (session_proxy,
                                                       dev_major,
                                                       dev_minor,
                                                       NULL,
                                                       &fd_variant,
                                                       NULL, /* paused */
                                                       &fd_list,
                                                       cancellable,
                                                       error))
    return FALSE;

  fd = g_unix_fd_list_get (fd_list, g_variant_get_handle (fd_variant), error);
  if (fd == -1)
    return FALSE;

  *out_fd = fd;
  return TRUE;
}

static gboolean
get_device_info_from_path (const char *path,
                           int        *out_major,
                           int        *out_minor)
{
  int ret;
  struct stat st;

  ret = stat (path, &st);
  if (ret < 0 || !S_ISCHR (st.st_mode))
    return FALSE;

  *out_major = major (st.st_rdev);
  *out_minor = minor (st.st_rdev);
  return TRUE;
}

MetaDeviceFile *
meta_device_pool_open (MetaDevicePool       *pool,
                       const char           *path,
                       MetaDeviceFileFlags   flags,
                       GError              **error)
{
  g_autoptr (GMutexLocker) locker = NULL;
  MetaDeviceFile *file;
  int major = -1, minor = -1;
  int fd;

  locker = g_mutex_locker_new (&pool->mutex);

  file = find_device_file_from_path (pool, path);
  if (file)
    {
      g_warn_if_fail (file->flags == flags);
      meta_device_file_acquire_locked (file);
      return file;
    }

  if (flags & META_DEVICE_FILE_FLAG_TAKE_CONTROL)
    {
      meta_topic (META_DEBUG_BACKEND,
                  "Opening and taking control of device file '%s'",
                  path);

      if (!pool->session_proxy)
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
                       "Can't take control without logind session");
          return NULL;
        }

      if (!get_device_info_from_path (path, &major, &minor))
        {
          g_set_error (error,
                       G_IO_ERROR,
                       G_IO_ERROR_NOT_FOUND,
                       "Could not get device info for path %s: %m", path);
          return NULL;
        }

      if (!take_device (pool->session_proxy, major, minor, &fd, NULL, error))
        return NULL;
    }
  else
    {
      int open_flags;

      meta_topic (META_DEBUG_BACKEND,
                  "Opening device file '%s'",
                  path);

      if (flags & META_DEVICE_FILE_FLAG_READ_ONLY)
        open_flags = O_RDONLY;
      else
        open_flags = O_RDWR;
      open_flags |= O_CLOEXEC;

      do
        {
          fd = open (path, open_flags);
        }
      while (fd == -1 && errno == EINTR);

      if (fd == -1)
        {
          g_set_error (error, G_IO_ERROR, g_io_error_from_errno (errno),
                       "Failed to open device '%s': %s",
                       path, g_strerror (errno));
          return NULL;
        }
    }

  file = meta_device_file_new (pool, path, major, minor, fd, flags);
  pool->files = g_list_prepend (pool->files, file);

  return file;
}

static void
release_device_file (MetaDevicePool *pool,
                     MetaDeviceFile *file)
{
  g_autoptr (GMutexLocker) locker = NULL;
  g_autoptr (GError) error = NULL;

  locker = g_mutex_locker_new (&pool->mutex);

  meta_topic (META_DEBUG_BACKEND, "Releasing device file '%s'", file->path);

  if (!g_ref_count_dec (&file->ref_count))
    return;

  pool->files = g_list_remove (pool->files, file);

  if (file->flags & META_DEVICE_FILE_FLAG_TAKE_CONTROL)
    {
      MetaDbusLogin1Session *session_proxy;

      meta_topic (META_DEBUG_BACKEND,
                  "Releasing control of and closing device file '%s'",
                  file->path);

      session_proxy = pool->session_proxy;
      if (!meta_dbus_login1_session_call_release_device_sync (session_proxy,
                                                              file->major,
                                                              file->minor,
                                                              NULL, &error))
        {
          g_warning ("Could not release device '%s' (%d,%d): %s",
                     file->path,
                     file->major, file->minor,
                     error->message);
        }
    }
  else
    {
      meta_topic (META_DEBUG_BACKEND,
                  "Closing device file '%s'",
                  file->path);
    }

  close (file->fd);

  meta_device_file_free (file);
}

MetaDevicePool *
meta_device_pool_new (MetaLauncher *launcher)
{
  MetaDevicePool *pool;

  pool = g_object_new (META_TYPE_DEVICE_POOL, NULL);

  if (launcher)
    pool->session_proxy = meta_launcher_get_session_proxy (launcher);

  return pool;
}

static void
meta_device_pool_finalize (GObject *object)
{
  MetaDevicePool *pool = META_DEVICE_POOL (object);

  g_mutex_clear (&pool->mutex);
  g_warn_if_fail (!pool->files);

  G_OBJECT_CLASS (meta_device_pool_parent_class)->finalize (object);
}

static void
meta_device_pool_init (MetaDevicePool *pool)
{
  g_mutex_init (&pool->mutex);
}

static void
meta_device_pool_class_init (MetaDevicePoolClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = meta_device_pool_finalize;
}
