/*
 * This file is part of steamos-devkit
 * SPDX-License-Identifier: LGPL-2.1+
 *
 * Copyright © 2017-2018 Collabora Ltd
 *
 * This package 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 package 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 package.  If not, see
 * <http://www.gnu.org/licenses/>.
 */

#include <errno.h>
#include <gio/gio.h>
#include <gio/giotypes.h>
#include <gio/gunixoutputstream.h>
#include <glib-unix.h>
#include <glib.h>
#include <glib/gprintf.h>
#include <glib/gstdio.h>
#include <json-glib/json-glib.h>
#include <json-glib/json-gobject.h>
#include <libsoup/soup.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <systemd/sd-daemon.h>
#include <unistd.h>

#include "avahi-service.h"
// #include "lib/backports.h"
#include "json.h"
#include "utils.h"

#include "defines.h"

/* https://www.freedesktop.org/wiki/Software/systemd/hostnamed/ */
#define HOSTNAME_BUS_NAME "org.freedesktop.hostname1"
#define HOSTNAME_OBJECT_PATH "/org/freedesktop/hostname1"
#define HOSTNAME_IFACE HOSTNAME_BUS_NAME
#define PROPERTIES_IFACE "org.freedesktop.DBus.Properties"

typedef struct
{
  GObject parent_instance;

  GKeyFile *config;
  GKeyFile *user_config;
  gchar *machine_name;
  gchar **users;
  const gchar *entry_point_user;

  GMainLoop *loop;
  SoupServer *server;
  uint64_t server_port;
  DkdAvahiService *avahi_service;
  GDBusConnection *system_bus;
  guint hup_signal_watch;
  guint hostname_property_changed_subscription;
  gchar **devkit1_argv;
  gchar *settings;

  struct
    {
      gchar **hooks;
      gchar *entry_point;
      gboolean close_stdout_when_ready;
      gboolean use_default_hooks;
    } options;
} DkdApplication;

typedef struct
{
  GObjectClass parent_class;
} DkdApplicationClass;

#define DKD_TYPE_APPLICATION (dkd_application_get_type ())
#define DKD_APPLICATION(obj) \
  G_TYPE_CHECK_INSTANCE_CAST ((obj), DKD_TYPE_APPLICATION, DkdApplication)
#define DKD_APPLICATION_CLASS(cls) \
  G_TYPE_CHECK_CLASS_CAST ((cls), DKD_TYPE_APPLICATION, DkdApplicationClass)
#define DKD_IS_APPLICATION(obj) \
  G_TYPE_CHECK_INSTANCE_TYPE ((obj), DKD_TYPE_APPLICATION)
#define DKD_IS_APPLICATION_CLASS(cls) \
  G_TYPE_CHECK_CLASS_TYPE ((cls), DKD_TYPE_APPLICATION)
#define DKD_APPLICATION_GET_CLASS(obj) \
  G_TYPE_INSTANCE_GET_CLASS ((obj), DKD_TYPE_APPLICATION, DkdApplicationClass)

static GType dkd_application_get_type (void);

G_DEFINE_TYPE (DkdApplication, dkd_application, G_TYPE_OBJECT)

typedef struct
{
  GTask *task;
  DkdApplication *app;
  SoupMessage *msg;
  gchar *reply;
  guint status;
  gchar *sender_ip;
} AsyncQuery;

static AsyncQuery *
async_query_new (DkdApplication *app,
                 SoupMessage *m)
{
  AsyncQuery *aq = g_new0 (AsyncQuery, 1);
  aq->app = g_object_ref (app);
  aq->msg = g_object_ref (m);
  aq->status = SOUP_STATUS_INTERNAL_SERVER_ERROR;
  return aq;
}

static void
async_query_free (AsyncQuery * aq)
{
  if (aq)
    {
      g_clear_object (&aq->task);
      g_clear_object (&aq->msg);
      g_clear_object (&aq->app);
      g_clear_pointer (&aq->reply, g_free);
      g_clear_pointer (&aq->sender_ip, g_free);
      g_free (aq);
    }
}

/* Runs in worker thread */
static gboolean
writefile (gchar * template, const guint8 * data, gsize len, gboolean newline)
{
  GOutputStream *ostream;
  GError *error = NULL;
  int fd;
  gboolean ret = FALSE;

  /* Create an unique file and write data */
  fd = mkstemp (template);
  if (fd >= 0)
    {
      ostream = g_unix_output_stream_new (fd, FALSE);
      /* Write data to the file */
      if (!g_output_stream_write_all (ostream, data, len, NULL, NULL, &error))
        {
          g_warning ("Failed to write %s: %s", template, error->message);
          g_clear_error (&error);
        }
      else
        {
          /* Add a new line if necessary */
          if (newline)
            {
              if (!g_output_stream_write_all
                  (ostream, "\n", 1, NULL, NULL, &error))
                {
                  g_warning ("Failed to write new line to %s: %s", template,
                             error->message);
                  g_clear_error (&error);
                }
              else
                ret = TRUE;
            }
          else
            ret = TRUE;
        }
      if (!g_output_stream_close (ostream, NULL, &error))
        {
          g_warning ("Failed to close %s: %s", template, error->message);
          g_clear_error (&error);
          ret = FALSE;
        }
      g_clear_object (&ostream);
      close (fd);
    }
  return ret;
}

/* Runs in worker thread */
static gchar *
writekey (SoupMessage * msg)
{
  gchar *filename = NULL;
  const guint8 *data = (const guint8 *) msg->request_body->data;
  gsize len = msg->request_body->length;
  char template[] = "/tmp/devkitd-XXXXXX";
  gboolean found_name = FALSE;

  if (data)
    {
      gsize idx;

      /* Check length */
      if (len >= (64 * 1024))
        goto out;
      /* Check data start with 'ssh-rsa ' */
      if (memcmp ("ssh-rsa ", data, 8) != 0)
        goto out;
      /* Count character until Base 64 data */
      for (idx = 8; idx < len; idx++)
        {
          if (data[idx] != ' ')
            break;
        }
      /* Check Base64 data */
      for (; idx < len; idx++)
        {
          if ((data[idx] == '+') ||
              (data[idx] >= '/' && data[idx] <= '9') ||
              (data[idx] >= 'a' && data[idx] <= 'z') ||
              (data[idx] >= 'A' && data[idx] <= 'Z'))
            continue;
          else if (data[idx] == '=')
            {
              idx++;
              if ((idx < len) && (data[idx] == ' '))
                break;
              else if ((idx < len) && (data[idx] == '='))
                {
                  idx++;
                  if ((idx < len) && (data[idx] == ' '))
                    break;
                }
              goto out;
            }
          else if (data[idx] == ' ')
            break;
          else
            goto out;
        }

      for (; idx < len; idx++)
        {
          if (data[idx] == ' ')
            {
              /* it's a space, the rest is name or magic phrase, don't write to disk */
              if (found_name)
                {
                  len = idx;
                }
              else
                {
                  found_name = TRUE;
                }
            }
          if (data[idx] == '\0')
            goto out;
          if (data[idx] == '\n' && idx != len - 1)
            goto out;
        }
      /* write data to the file */
      if (writefile
          (template, data, len, ((data[len - 1] != '\n') ? TRUE : FALSE)))
        filename = g_strdup (template);
      else
        unlink (template);
    }
out:
  return filename;
}

/* Runs in worker thread */
static void
deletekey (const gchar * filename)
{
  if (filename)
    unlink (filename);
}

/* Runs in worker thread */
static gboolean
exec_script (DkdApplication *self,
             const gchar *action,
             const gchar *keypath,
             const gchar *sender_ip,
             const gchar * const *users,
             gchar **script_output)
{
  GSubprocessLauncher *launcher = NULL;
  GSubprocess *subprocess = NULL;
  gchar *script = NULL;
  gboolean ret = FALSE;
  GError *error = NULL;
  GPtrArray *argv = NULL;
  guint i;
  GBytes *local_bytes = NULL;
  gchar *local_utf8 = NULL;

  g_return_val_if_fail (action != NULL, FALSE);
  g_return_val_if_fail (keypath != NULL, FALSE);

  script = dk_find_hook ((const gchar * const *) self->options.hooks,
                         self->options.use_default_hooks,
                         action, &error);

  if (!script)
    {
      g_warning ("%s", error->message);
      g_clear_error (&error);
      return FALSE;
    }

  argv = g_ptr_array_new_with_free_func (g_free);
  launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE | G_SUBPROCESS_FLAGS_STDERR_MERGE);

  if (dk_hook_is_generic_python (script))
    g_ptr_array_add (argv, g_strdup (dk_get_best_python ()));

  g_ptr_array_add (argv, g_strdup (script));
  g_ptr_array_add (argv, g_strdup (keypath));

  if (sender_ip != NULL)
    g_ptr_array_add(argv, g_strdup(sender_ip));

  for (i = 0; users != NULL && users[i] != NULL; i++)
    g_ptr_array_add (argv, g_strdup (users[i]));

  g_ptr_array_add (argv, NULL);

  subprocess = g_subprocess_launcher_spawnv (
      launcher,
      (const gchar * const *) argv->pdata,
      &error);

  if (!subprocess)
    {
      GString *command_line = g_string_new ("");

      for (i = 0; i < argv->len; i++)
        {
          if (argv->pdata[i] != NULL)
            {
              gchar *quoted = g_shell_quote (argv->pdata[i]);

              if (command_line->len > 0)
                g_string_append_c (command_line, ' ');

              g_string_append (command_line, quoted);
              g_free (quoted);
            }
        }

      g_warning ("Failed to create subprocess: %s: %s",
                 command_line->str, error->message);
      g_clear_error (&error);
      g_string_free (command_line, TRUE);
      goto out;
    }
  if (!g_subprocess_communicate (subprocess, NULL, NULL, &local_bytes, NULL, &error))
    {
      g_warning ("Fail to wait subprocess : %s", error->message);
      g_clear_error (&error);
      goto out;
    }

  local_utf8 = g_utf8_make_valid (g_bytes_get_data (local_bytes, NULL), -1);
  g_message ("Subprocess output: %s", local_utf8);

  if (!g_subprocess_get_if_exited (subprocess))
    {
      g_warning ("Subprocess exit abnormally");
      goto out;
    }
  else
    {
      gint exit_status = g_subprocess_get_exit_status (subprocess);
      if (exit_status == 0)
        {
          ret = TRUE;
        }
    }
out:
  if (script_output != NULL)
    *script_output = g_steal_pointer (&local_utf8);
  else
    g_clear_pointer (&local_utf8, g_free);
  g_bytes_unref (local_bytes);
  if (subprocess)
    g_object_unref (subprocess);
  g_clear_object (&launcher);
  g_free (script);
  if (argv)
    g_ptr_array_unref (argv);
  return ret;
}

/* Runs in worker thread */
static void
add_ssh_key_cb (GTask * task, gpointer source_object, gpointer task_data,
                GCancellable * cancellable)
{
  AsyncQuery *aq = task_data;
  gchar *keypath;
  gchar *script_output = NULL;

  g_debug ("add_ssh_key_cb runs in a separate thread");

  keypath = writekey (aq->msg);
  if (!keypath)
    {
      aq->status = SOUP_STATUS_FORBIDDEN;
      goto out;
    }

  if (!exec_script (aq->app, "approve-ssh-key", keypath, aq->sender_ip,
                    NULL,
                    &script_output))
    {
      aq->status = SOUP_STATUS_FORBIDDEN;
      aq->reply = g_strconcat ("approve-ssh-key:\n", script_output, NULL);
      g_clear_pointer (&script_output, g_free);
      goto out;
    }

  aq->reply = g_strconcat ("approve-ssh-key:\n", script_output, NULL);
  g_clear_pointer (&script_output, g_free);

  if (exec_script (aq->app, "install-ssh-key", keypath, NULL,
                   (const gchar * const *) aq->app->users,
                   &script_output))
    {
      aq->status = SOUP_STATUS_OK;
    }
  else
    {
      aq->status = SOUP_STATUS_INTERNAL_SERVER_ERROR;
    }
  aq->reply = g_strconcat (aq->reply, "install-ssh-key:\n", script_output, NULL);
  g_clear_pointer (&script_output, g_free);

out:
  deletekey (keypath);
}

static void
add_ssh_key_completed (GObject * source_object, GAsyncResult * res,
                       gpointer user_data)
{
  AsyncQuery *aq = user_data;

  if (aq->reply)
    soup_message_set_response (aq->msg, "text/plain", SOUP_MEMORY_TAKE,
                               aq->reply, strlen (aq->reply));
  aq->reply = NULL;   /* ownership transferred, do not free */
  soup_message_set_status (aq->msg, aq->status);
  soup_server_unpause_message (aq->app->server, aq->msg);

  async_query_free (aq);
  g_debug ("Task completed");
}

static void
server_callback (SoupServer * server, SoupMessage * msg,
                 const char *path, GHashTable * query,
                 SoupClientContext * context, gpointer data)
{
  DkdApplication *app = DKD_APPLICATION (data);
  SoupMessageHeadersIter iter;
  const char *name, *value;

  g_debug ("%s %s HTTP/1.%d", msg->method, path,
           soup_message_get_http_version (msg));
  soup_message_headers_iter_init (&iter, msg->request_headers);
  while (soup_message_headers_iter_next (&iter, &name, &value))
    g_debug ("%s: %s", name, value);
  if (msg->request_body->length)
    g_debug ("%s", msg->request_body->data);

  if ((msg->method == SOUP_METHOD_POST) && strcmp ("/register", path) == 0)
    {
      AsyncQuery *aq;
      GSocketAddress *sockaddr;
      GInetAddress *addr;
      gchar *sender_ip = NULL;
      sockaddr = soup_client_context_get_remote_address (context);
      if (sockaddr != NULL && G_IS_INET_SOCKET_ADDRESS (sockaddr))
        {
          addr = g_inet_socket_address_get_address (
              G_INET_SOCKET_ADDRESS (sockaddr));
          sender_ip = g_inet_address_to_string (addr);
          g_debug ("From %s", sender_ip);
        }
      else
        {
          g_debug ("Unable to get request ip address");
        }
      aq = async_query_new (app, msg);
      aq->task = g_task_new (NULL, NULL, add_ssh_key_completed, aq);
      aq->sender_ip = sender_ip;
      soup_server_pause_message (server, msg);
      g_task_set_task_data (aq->task, aq, NULL);
      g_task_run_in_thread (aq->task, add_ssh_key_cb);
    }
  else if (msg->method == SOUP_METHOD_GET && strcmp ("/login-name", path) == 0)
    {
      soup_message_set_response (msg, "text/plain", SOUP_MEMORY_COPY,
                                 app->entry_point_user,
                                 strlen (app->entry_point_user));
      soup_message_set_status (msg, SOUP_STATUS_OK);
    }
  else if (msg->method == SOUP_METHOD_GET &&
           strcmp ("/properties.json", path) == 0)
    {
      gchar *text;
      JsonNode *node;
      JsonObject *object;
      JsonArray *arr;
      gchar **arg;

      node = json_node_new (JSON_NODE_OBJECT);
      object = json_object_new ();
      json_node_set_object (node, object);
      json_object_set_int_member (object, "txtvers", 1);
      json_object_set_string_member (object, "login",
                                     app->entry_point_user);
      json_object_set_string_member (object, "settings",
                                     app->settings);
      arr = json_array_new ();

      for (arg = app->devkit1_argv; *arg != NULL; arg++)
        json_array_add_string_element (arr, *arg);

      /* This takes ownership: do not free arr now! */
      json_object_set_array_member (object, "devkit1", arr);

      text = dk_json_to_string (node, TRUE);
      soup_message_set_response (msg, "application/json",
                                 SOUP_MEMORY_TAKE,
                                 text, strlen (text));
      soup_message_set_status (msg, SOUP_STATUS_OK);
      json_object_unref (object);
      json_node_free (node);
    }
  else if (msg->method == SOUP_METHOD_GET && query
           && g_hash_table_size (query))
    {
      gchar *command = g_hash_table_lookup (query, "command");
      if (command)
        {
          g_debug ("Received command : %s", command);
          if (!strcmp ("ping", command))
            {
              soup_message_set_response (msg, "text/plain",
                                         SOUP_MEMORY_STATIC, "pong\n", 6);
              soup_message_set_status (msg, SOUP_STATUS_OK);
            }
          else
            {
              soup_message_set_status (msg, SOUP_STATUS_NOT_FOUND);
            }
        }
    }
  else
    {
      soup_message_set_status (msg, SOUP_STATUS_NOT_FOUND);
    }
}

static void
dkd_application_init (DkdApplication *self)
{
  self->config = g_key_file_new ();
  self->user_config = g_key_file_new ();
  self->loop = g_main_loop_new (NULL, TRUE);
  self->server = soup_server_new (SOUP_SERVER_SERVER_HEADER,
                                  "steamos-devkit-service", NULL);
  self->options.hooks = NULL;
  self->options.close_stdout_when_ready = FALSE;
  self->options.use_default_hooks = TRUE;
}

#if !GLIB_CHECK_VERSION(2, 69, 0)
#define g_spawn_check_wait_status(x, e) (g_spawn_check_exit_status (x, e))
#endif

static gboolean
dkd_application_identify (DkdApplication *self,
                          GError **error)
{
  GSubprocessLauncher *launcher = NULL;
  GSubprocess *subprocess = NULL;
  JsonParser *parser = NULL;
  JsonReader *reader = NULL;
  JsonNode *root = NULL;
  gchar *script = NULL;
  GBytes *stdout_buf = NULL;
  gboolean ret = FALSE;
  const gchar *machine_name;

  script = dk_find_hook ((const gchar * const *) self->options.hooks,
                         self->options.use_default_hooks,
                         "devkit-1-identify", error);

  if (script == NULL)
    goto out;

  launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE);

  if (dk_hook_is_generic_python (script))
    subprocess = g_subprocess_launcher_spawn (launcher, error,
                                              dk_get_best_python (),
                                              script, NULL);
  else
    subprocess = g_subprocess_launcher_spawn (launcher, error, script, NULL);

  if (!subprocess)
    {
      g_prefix_error (error, "Failed to create subprocess '%s': ", script);
      goto out;
    }

  if (!g_subprocess_communicate (subprocess, NULL, NULL, &stdout_buf,
                                 NULL, error))
    {
      g_prefix_error (error, "Failed to communicate with subprocess '%s': ",
                      script);
      goto out;
    }

  if (!g_spawn_check_wait_status (g_subprocess_get_status (subprocess),
                                  error))
    {
      g_prefix_error (error, "Subprocess '%s' failed: ", script);
      goto out;
    }

  parser = json_parser_new ();

  if (!dk_json_parser_load_from_bytes (parser, stdout_buf, error))
    {
      g_prefix_error (error, "Failed to parse output of subprocess '%s': ",
                      script);
      goto out;
    }

  root = json_parser_get_root (parser);

  if (root == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "Subprocess '%s' returned empty result", script);
      goto out;
    }

  if (json_node_get_node_type (root) != JSON_NODE_OBJECT)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "Subprocess '%s' did not return a JSON object", script);
      goto out;
    }

  reader = json_reader_new (root);
  json_reader_read_member (reader, "machine_name");
  machine_name = json_reader_get_string_value (reader);

  if (machine_name != NULL)
    {
      g_free (self->machine_name);
      self->machine_name = g_strdup (machine_name);
    }

  json_reader_end_member (reader);
  ret = TRUE;

out:
  g_clear_pointer (&stdout_buf, g_bytes_unref);
  g_clear_object (&launcher);
  g_clear_object (&subprocess);
  g_clear_object (&parser);
  g_clear_object (&reader);
  g_free (script);
  return ret;
}

static gboolean
dkd_application_load_configuration (DkdApplication *self,
                                    GError **error)
{
  gchar *user_config_path = g_build_path("/", g_get_home_dir(), ".config", PACKAGE "/" PACKAGE ".conf", NULL);
  const gchar * const search_dirs[] =
    {
      "/etc",
      DEVKIT_DATADIR,
      NULL
    };

  GError *local_error = NULL;
  gchar *port;

  if (!dkd_application_identify (self, error)) {
    g_free (user_config_path);
    return FALSE;
  }

  if (self->machine_name == NULL)
    {
      /* Last resort: use gethostname() even if the result is something
       * useless like steamos or localhost. This will only happen the
       * first time. On reloads, the previous machine_name will be
       * carried over if the identify script can't come up with
       * anything better. */
      self->machine_name = g_strdup (g_get_host_name ());
    }

  if (!g_key_file_load_from_dirs (self->config, PACKAGE "/" PACKAGE ".conf",
                                  (const gchar **) search_dirs,
                                  NULL, G_KEY_FILE_NONE, &local_error))
    {
      if (g_error_matches (local_error, G_KEY_FILE_ERROR,
                           G_KEY_FILE_ERROR_NOT_FOUND))
        {
          g_clear_error (&local_error);
          g_debug ("Site config file not found");
        }
      else
        {
          g_propagate_prefixed_error (error, local_error,
                                      "Failed to load configuration: ");
        }
    }

  /* Next load user config to try to override any defaults in system config */
  if (!g_key_file_load_from_file (self->user_config, user_config_path,
                                  G_KEY_FILE_NONE, &local_error))
    {
      // Ignore since user config is not required
      g_clear_error (&local_error);
      g_debug ("User config file not found");
    }
  g_free (user_config_path);

  // Now load server_port, since we are going to do listen_all after we return
  port = g_key_file_get_string (self->config, "Settings", "Port", NULL);
  if (port == NULL)
    {
      // Use default value unless user config has one
      self->server_port = SERVICE_PORT;
    }
  else
    {
      self->server_port = atol (port);
      // Invalid port or atol failed.
      if (self->server_port == 0)
        {
          self->server_port = SERVICE_PORT;
        }
    }

  port = g_key_file_get_string (self->user_config, "Settings", "Port", NULL);
  if (port == NULL)
    {
      // Ignore, we set default above or have system wide setting
    }
  else
    {
      // TODO: Make sure result is valid?
      self->server_port = atol (port);
    }

  return TRUE;
}

static gboolean
dkd_application_publish (DkdApplication *self,
                         GError **error)
{
  const gchar * const standard_users[] = { "root", "desktop", "steam" };
  GPtrArray *users = NULL;
  gsize length = 0;
  gchar **settings = g_key_file_get_keys (self->config, "Settings", &length, NULL);
  gsize user_length = 0;
  gchar **user_settings = g_key_file_get_keys (self->user_config, "Settings", &user_length, NULL);

  JsonNode *node;
  JsonObject *object;

  guint s = 0;
  gchar *key;

  if (length > 0)
    key = settings[s];
  else
    key = NULL;

  node = json_node_new (JSON_NODE_OBJECT);
  object = json_object_new ();
  json_node_set_object (node, object);

  while (key != NULL) {
    gchar *value = g_key_file_get_string (self->config, "Settings", key, NULL);
    if (value != NULL) {
      json_object_set_string_member (object, key,
                                     value);
    }
    key = settings[s++];
  }

  // Now override system settings with user settings if present
  s = 0;
  if (length > 0)
    g_strfreev (settings);

  if (user_length > 0)
    key = user_settings[0];
  else
    key = NULL;

  while (key != NULL) {
    gchar *value = g_key_file_get_string (self->user_config, "Settings", key, NULL);
    if (value != NULL) {
      json_object_set_string_member (object, key, value);
    }
    key = user_settings[s++];
  }

  self->settings = dk_json_to_string (node, FALSE);

  users = g_ptr_array_new_with_free_func (g_free);

  if (geteuid () == 0)
    {
      gchar **configured_users = NULL;
      guint i;

      configured_users = g_key_file_get_string_list (self->config,
                                                     "Users",
                                                     "ShellUsers",
                                                     NULL, NULL);

      if (configured_users != NULL)
        {
          for (i = 0; configured_users[i] != NULL; i++)
            g_ptr_array_add (users, g_strdup (configured_users[i]));
        }
      else
        {
          for (i = 0; i < G_N_ELEMENTS (standard_users); i++)
            g_ptr_array_add (users, g_strdup (standard_users[i]));
        }

      g_strfreev (configured_users);
    }
  else
    {
      gchar **configured_users = NULL;
      guint i;

      configured_users = g_key_file_get_string_list (self->user_config,
                                                     "Users",
                                                     "ShellUsers",
                                                     NULL, NULL);

      if (configured_users != NULL)
        {
          for (i = 0; configured_users[i] != NULL; i++)
            g_ptr_array_add (users, g_strdup (configured_users[i]));
        }
      else
        {
          // No user config, so just use current username
          g_ptr_array_add (users, g_strdup (g_get_user_name ()));
        }
    }

  /* Check the length before appending the NULL that makes it into
   * a GStrv */
  if (users->len == 1)
    {
      /* There is only one user, so we can run new-game and ensure-game
       * as that user directly */
      self->entry_point_user = g_ptr_array_index (users, 0);
    }
  else
    {
      /* We need to be root to enable swapping between users */
      self->entry_point_user = "root";
    }

  g_ptr_array_add (users, NULL);

  /* "Steal" contents of GPtrArray as a gchar ** */
  self->users = (gchar **) g_ptr_array_free (users, FALSE);

  if (self->avahi_service == NULL)
    {
      self->avahi_service = dkd_avahi_service_new ();

      if (!dkd_avahi_service_start (self->avahi_service,
                                    self->machine_name,
                                    self->server_port,
                                    self->entry_point_user,
                                    (const gchar * const *) self->devkit1_argv,
                                    self->settings,
                                    self->loop, error))
        {
          g_prefix_error (error, "Failed to start mDNS service: ");
          return FALSE;
        }
    }
  else if (!dkd_avahi_service_reconfigure (self->avahi_service,
                                           self->machine_name,
                                           self->server_port,
                                           self->entry_point_user,
                                           (const gchar * const *) self->devkit1_argv,
                                            self->settings,
                                           error))
    {
      g_prefix_error (error, "Failed to reconfigure mDNS service: ");
      return FALSE;
    }

  return TRUE;
}

static void
dkd_application_reconfigure (DkdApplication *self)
{
  GError *error = NULL;

  if (!dkd_application_load_configuration (self, &error))
    {
      g_warning ("Could not reload configuration: %s", error->message);
      g_clear_error (&error);
    }
  else if (!dkd_application_publish (self, &error))
    {
      g_warning ("Could not apply new configuration: %s", error->message);
      g_clear_error (&error);
    }
}

static gboolean
hup_signal_cb (gpointer user_data)
{
  DkdApplication *self = DKD_APPLICATION (user_data);

  dkd_application_reconfigure (self);
  return G_SOURCE_CONTINUE;
}

static void
hostname_changed_cb (GDBusConnection *system_bus G_GNUC_UNUSED,
                     const gchar *sender G_GNUC_UNUSED,
                     const gchar *object_path G_GNUC_UNUSED,
                     const gchar *iface G_GNUC_UNUSED,
                     const gchar *signal G_GNUC_UNUSED,
                     GVariant *parameters G_GNUC_UNUSED,
                     gpointer user_data)
{
  DkdApplication *self = DKD_APPLICATION (user_data);

  /* Ignore the parameters: any property change potentially causes a
   * machine identity change */
  dkd_application_reconfigure (self);
}

static void
system_bus_get_cb (GObject *source_object G_GNUC_UNUSED,
                   GAsyncResult *result,
                   gpointer user_data)
{
  DkdApplication *self = DKD_APPLICATION (user_data);
  GError *error = NULL;

  self->system_bus = g_bus_get_finish (result, &error);

  if (self->system_bus != NULL)
    {
      self->hostname_property_changed_subscription =
          g_dbus_connection_signal_subscribe (
              self->system_bus, HOSTNAME_BUS_NAME, PROPERTIES_IFACE,
              "PropertiesChanged", HOSTNAME_OBJECT_PATH, HOSTNAME_IFACE,
              G_DBUS_SIGNAL_FLAGS_NONE, hostname_changed_cb, self, NULL);
    }
  else
    {
      g_warning ("Unable to connect to system bus: %s", error->message);
      g_clear_error (&error);
    }

  g_object_unref (self);
}

static gboolean
deprecated_option_cb (const char *option,
                      const char *value,
                      gpointer option_group_data,
                      GError **error)
{
  g_printerr ("steamos-devkit-service: Ignoring deprecated option \"%s\"\n",
              option);
  return TRUE;
}

static gboolean
dkd_application_start (DkdApplication *self,
                       int *argcp,
                       gchar ***argvp,
                       GError **error)
{
  gboolean ret = FALSE;
  GOptionContext *context = NULL;
  GOptionEntry entries[] =
    {
      { "hooks", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME_ARRAY,
        &self->options.hooks,
        "Directory to read to find hook scripts (can be repeated, "
        "most important first)",
        "DIR" },
      { "entry-point", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME,
        &self->options.entry_point,
        "devkit-1 entry point" },
      { "close-stdout-when-ready", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
        &self->options.close_stdout_when_ready,
        "Close the standard output file descriptor (which should be a pipe) "
        "when ready to receive requests" },
      { "default-hooks", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
        &self->options.use_default_hooks,
        "Use the default search path for hook scripts after anything "
        "specified with --hooks [default]" },
      { "no-default-hooks", '\0', G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE,
        &self->options.use_default_hooks,
        "Don't use the default search path for hook scripts after "
        "anything specified with --hooks" },
      { "system-log", '\0', G_OPTION_FLAG_HIDDEN|G_OPTION_FLAG_NO_ARG,
        G_OPTION_ARG_CALLBACK, deprecated_option_cb, "Does nothing" },
      { "no-system-log", '\0', G_OPTION_FLAG_HIDDEN|G_OPTION_FLAG_NO_ARG,
        G_OPTION_ARG_CALLBACK, deprecated_option_cb, "Does nothing" },
      { NULL }
    };
  GSList *uris = NULL;
  GSList *u;
  gchar **hook;
  gchar *cwd = NULL;
  GPtrArray *devkit1_argv;

  context = g_option_context_new ("- devkit discovery server");
  g_option_context_add_main_entries (context, entries, NULL);

  if (!g_option_context_parse (context, argcp, argvp, error))
    goto out;

  /* Make hooks absolute */
  for (hook = self->options.hooks; hook != NULL && *hook != NULL; hook++)
    {
      if (*hook[0] != '/')
        {
          gchar *tmp = *hook;

          if (cwd == NULL)
            cwd = g_get_current_dir ();

          *hook = g_build_filename (cwd, tmp, NULL);
          g_free (tmp);
        }
    }

  devkit1_argv = g_ptr_array_new_with_free_func (g_free);
  if (self->options.entry_point != NULL)
    {
      g_ptr_array_add (devkit1_argv, g_strdup (self->options.entry_point));
    }
  else
    {
      g_ptr_array_add (devkit1_argv, g_strdup ("devkit-1"));
    }

  if (self->options.hooks != NULL && self->options.hooks[0] != NULL)
    {
      gchar **iter;

      for (iter = self->options.hooks; *iter != NULL; iter++)
        g_ptr_array_add (devkit1_argv,
                         g_strdup_printf ("--hooks=%s", *iter));
    }

  if (!self->options.use_default_hooks)
    g_ptr_array_add (devkit1_argv, g_strdup ("--no-default-hooks"));

  g_ptr_array_add (devkit1_argv, NULL);

  /* "Steal" contents of GPtrArray as a gchar ** */
  g_strfreev (self->devkit1_argv);
  self->devkit1_argv = (gchar **) g_ptr_array_free (devkit1_argv, FALSE);

  g_bus_get (G_BUS_TYPE_SYSTEM, NULL, system_bus_get_cb,
             g_object_ref (self));

  self->hup_signal_watch = g_unix_signal_add (SIGHUP, hup_signal_cb, self);

  if (!dkd_application_load_configuration (self, error))
    goto out;

  if (!soup_server_listen_all (self->server, self->server_port, 0, error))
    {
      // TODO: Maybe try the next port, etc. until one works, then save the
      // value in user_config or config...
      g_prefix_error (error, "Failed to listen on port: %ld",
                      self->server_port);
      goto out;
    }

  soup_server_add_handler (self->server, "/", server_callback, self, NULL);

  uris = soup_server_get_uris (self->server);

  for (u = uris; u; u = u->next)
    {
      gchar *str = soup_uri_to_string (u->data, FALSE);

      g_debug ("Listening on %s", str);
      g_free (str);
      soup_uri_free (u->data);
    }

  if (!dkd_application_publish (self, error))
    goto out;

  if (self->options.close_stdout_when_ready)
    {
      if (dup2 (STDERR_FILENO, STDOUT_FILENO) != STDOUT_FILENO)
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                       "Unable to make stdout a copy of stderr: %s",
                       g_strerror (errno));
          goto out;
        }
    }

  ret = TRUE;

out:
  if (context != NULL)
    g_option_context_free (context);

  g_free (cwd);
  g_slist_free (uris);
  return ret;
}

static void
dkd_application_dispose (GObject *object)
{
  DkdApplication *self = DKD_APPLICATION (object);

  if (self->system_bus != NULL &&
      self->hostname_property_changed_subscription != 0)
    {
      g_dbus_connection_signal_unsubscribe (
          self->system_bus,
          self->hostname_property_changed_subscription);
      self->hostname_property_changed_subscription = 0;
    }

  g_clear_object (&self->system_bus);

  if (self->hup_signal_watch != 0)
    {
      g_source_remove (self->hup_signal_watch);
      self->hup_signal_watch = 0;
    }

  if (self->server != NULL)
    {
      soup_server_remove_handler (self->server, "/");
      soup_server_disconnect (self->server);
      g_clear_object (&self->server);
    }

  g_clear_object (&self->avahi_service);

  G_OBJECT_CLASS (dkd_application_parent_class)->dispose (object);
}

static void
dkd_application_finalize (GObject *object)
{
  DkdApplication *self = DKD_APPLICATION (object);

  g_clear_pointer (&self->config, g_key_file_unref);
  g_clear_pointer (&self->user_config, g_key_file_unref);
  self->entry_point_user = NULL;
  g_clear_pointer (&self->users, g_strfreev);
  g_clear_pointer (&self->loop, g_main_loop_unref);

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

static void
dkd_application_class_init (DkdApplicationClass *cls)
{
  GObjectClass *object_class = G_OBJECT_CLASS (cls);

  object_class->dispose = dkd_application_dispose;
  object_class->finalize = dkd_application_finalize;
}

int
main (int argc, char **argv)
{
  DkdApplication *app;
  GError *error = NULL;
  int ret = 0;

  app = g_object_new (DKD_TYPE_APPLICATION, NULL);

  if (!dkd_application_start (app, &argc, &argv, &error))
    {
      if (error != NULL)
        {
          g_warning ("%s", error->message);
          g_clear_error (&error);
        }
      else
        {
           g_warning("Unable to start devkit daemon application");
        }
      ret = 1;
      goto out;
    }

  sd_notify (0, "READY=1");
  g_main_loop_run (app->loop);

out:
  if (app != NULL && app->avahi_service != NULL)
    dkd_avahi_service_stop (app->avahi_service);

  g_clear_object (&app);

  return ret;
}