/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
 * Copyright (C) 2018 Red Hat, Inc.
 */

#include "src/core/nm-default-daemon.h"

#include "nm-keep-alive.h"

#include "settings/nm-settings-connection.h"
#include "libnm-glib-aux/nm-dbus-aux.h"

/*****************************************************************************/

NM_GOBJECT_PROPERTIES_DEFINE(NMKeepAlive, PROP_ALIVE, );

typedef struct {
    GObject *owner;

    NMSettingsConnection *connection;
    GDBusConnection      *dbus_connection;
    char                 *dbus_client;

    GCancellable *dbus_client_confirm_cancellable;
    guint         subscription_id;

    bool armed : 1;
    bool disarmed : 1;

    bool alive : 1;
    bool dbus_client_confirmed : 1;
    bool dbus_client_watching : 1;
    bool connection_was_visible : 1;
} NMKeepAlivePrivate;

struct _NMKeepAlive {
    GObject            parent;
    NMKeepAlivePrivate _priv;
};

struct _NMKeepAliveClass {
    GObjectClass parent;
};

G_DEFINE_TYPE(NMKeepAlive, nm_keep_alive, G_TYPE_OBJECT)

#define NM_KEEP_ALIVE_GET_PRIVATE(self) _NM_GET_PRIVATE(self, NMKeepAlive, NM_IS_KEEP_ALIVE)

/*****************************************************************************/

#define _NMLOG_DOMAIN      LOGD_CORE
#define _NMLOG(level, ...) __NMLOG_DEFAULT(level, _NMLOG_DOMAIN, "keep-alive", __VA_ARGS__)

/*****************************************************************************/

static gboolean _is_alive_dbus_client(NMKeepAlive *self);
static void     cleanup_dbus_watch(NMKeepAlive *self);

/*****************************************************************************/

static gboolean
_is_alive(NMKeepAlive *self)
{
    NMKeepAlivePrivate *priv = NM_KEEP_ALIVE_GET_PRIVATE(self);

    nm_assert(!priv->disarmed);

    if (!priv->armed) {
        /* before arming, the instance is always alive. */
        return TRUE;
    }

    if (priv->dbus_client_watching) {
        if (_is_alive_dbus_client(self)) {
            /* no matter what, the keep-alive is alive, because there is a D-Bus client
             * still around keeping it alive. */
            return TRUE;
        }
        /* the D-Bus client is gone. The only other binding (below) for the connection's
         * visibility cannot keep the instance alive.
         *
         * As such, a D-Bus client watch is authoritative and overrules other conditions (that
         * we have so far). */
        return FALSE;
    }

    if (priv->connection && priv->connection_was_visible
        && !NM_FLAGS_HAS(nm_settings_connection_get_flags(priv->connection),
                         NM_SETTINGS_CONNECTION_INT_FLAGS_VISIBLE)) {
        /* note that we only declare the keep-alive as dead due to invisible
         * connection, if
         *    (1) we monitor a connection, obviously
         *    (2) the connection was visible earlier and is no longer. It was
         *        was invisible all the time, it does not suffice.
         */
        return FALSE;
    }

    /* by default, the instance is alive. */
    return TRUE;
}

static void
_notify_alive(NMKeepAlive *self)
{
    NMKeepAlivePrivate *priv = NM_KEEP_ALIVE_GET_PRIVATE(self);

    if (priv->disarmed) {
        /* once disarmed, the alive state is frozen. */
        return;
    }

    if (priv->alive == _is_alive(self))
        return;
    priv->alive = !priv->alive;
    _LOGD("instance is now %s", priv->alive ? "alive" : "dead");
    _notify(self, PROP_ALIVE);
}

gboolean
nm_keep_alive_is_alive(NMKeepAlive *self)
{
    return NM_KEEP_ALIVE_GET_PRIVATE(self)->alive;
}

/*****************************************************************************/

static void
connection_flags_changed(NMSettingsConnection *connection, NMKeepAlive *self)
{
    NMKeepAlivePrivate *priv = NM_KEEP_ALIVE_GET_PRIVATE(self);

    if (!priv->connection_was_visible
        && NM_FLAGS_HAS(nm_settings_connection_get_flags(priv->connection),
                        NM_SETTINGS_CONNECTION_INT_FLAGS_VISIBLE)) {
        /* the profile was never visible but now it becomes visible.
         * Remember that.
         *
         * Before this happens (that is, if the device was invisible all along),
         * the keep alive instance is considered alive (w.r.t. watching the connection).
         *
         * The reason is to allow a user to manually activate an invisible profile and keep
         * it alive. At least, as long until the user logs out the first time (which is the
         * first time, the profiles changes from visible to invisible).
         *
         * Yes, that is odd. How to improve? */
        priv->connection_was_visible = TRUE;
    }
    _notify_alive(self);
}

static void
_set_settings_connection_watch_visible(NMKeepAlive          *self,
                                       NMSettingsConnection *connection,
                                       gboolean              emit_signal)
{
    NMKeepAlivePrivate                   *priv           = NM_KEEP_ALIVE_GET_PRIVATE(self);
    gs_unref_object NMSettingsConnection *old_connection = NULL;

    if (priv->connection == connection)
        return;

    if (priv->connection) {
        g_signal_handlers_disconnect_by_func(priv->connection,
                                             G_CALLBACK(connection_flags_changed),
                                             self);
        old_connection = g_steal_pointer(&priv->connection);
    }

    if (connection && !priv->disarmed) {
        priv->connection = g_object_ref(connection);
        priv->connection_was_visible =
            NM_FLAGS_HAS(nm_settings_connection_get_flags(priv->connection),
                         NM_SETTINGS_CONNECTION_INT_FLAGS_VISIBLE);
        g_signal_connect(priv->connection,
                         NM_SETTINGS_CONNECTION_FLAGS_CHANGED,
                         G_CALLBACK(connection_flags_changed),
                         self);
    }

    if (emit_signal)
        _notify_alive(self);
}

void
nm_keep_alive_set_settings_connection_watch_visible(NMKeepAlive          *self,
                                                    NMSettingsConnection *connection)
{
    _set_settings_connection_watch_visible(self, connection, TRUE);
}

/*****************************************************************************/

static void
get_name_owner_cb(const char *name_owner, GError *error, gpointer user_data)
{
    NMKeepAlive        *self;
    NMKeepAlivePrivate *priv;

    if (!name_owner && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
        return;

    self = user_data;
    priv = NM_KEEP_ALIVE_GET_PRIVATE(self);

    if (name_owner && nm_streq(name_owner, priv->dbus_client)) {
        /* all good, the name is confirmed. */
        return;
    }

    _LOGD("DBus client for keep alive is not on the bus");
    cleanup_dbus_watch(self);
    _notify_alive(self);
}

static gboolean
_is_alive_dbus_client(NMKeepAlive *self)
{
    NMKeepAlivePrivate *priv = NM_KEEP_ALIVE_GET_PRIVATE(self);

    if (!priv->dbus_client)
        return FALSE;

    if (!priv->dbus_client_confirmed) {
        /* it's unconfirmed that the D-Bus client is really alive.
         * It looks like it is, but as we are claiming that to be
         * the case, issue an async GetNameOwner call to make sure. */
        priv->dbus_client_confirmed           = TRUE;
        priv->dbus_client_confirm_cancellable = g_cancellable_new();

        nm_dbus_connection_call_get_name_owner(priv->dbus_connection,
                                               priv->dbus_client,
                                               -1,
                                               priv->dbus_client_confirm_cancellable,
                                               get_name_owner_cb,
                                               self);
    }
    return TRUE;
}

static void
cleanup_dbus_watch(NMKeepAlive *self)
{
    NMKeepAlivePrivate *priv = NM_KEEP_ALIVE_GET_PRIVATE(self);

    if (!priv->dbus_client)
        return;

    _LOGD("Cleanup DBus client watch");

    nm_clear_g_cancellable(&priv->dbus_client_confirm_cancellable);
    nm_clear_g_free(&priv->dbus_client);
    if (priv->dbus_connection) {
        g_dbus_connection_signal_unsubscribe(priv->dbus_connection,
                                             nm_steal_int(&priv->subscription_id));
        g_clear_object(&priv->dbus_connection);
    }
}

static void
name_owner_changed_cb(GDBusConnection *connection,
                      const char      *sender_name,
                      const char      *object_path,
                      const char      *interface_name,
                      const char      *signal_name,
                      GVariant        *parameters,
                      gpointer         user_data)
{
    NMKeepAlive *self = NM_KEEP_ALIVE(user_data);
    const char  *old_owner;
    const char  *new_owner;

    g_variant_get(parameters, "(&s&s&s)", NULL, &old_owner, &new_owner);

    if (!nm_streq0(new_owner, ""))
        return;

    _LOGD("DBus client for keep alive disappeared from bus");
    cleanup_dbus_watch(self);
    _notify_alive(self);
}

void
nm_keep_alive_set_dbus_client_watch(NMKeepAlive     *self,
                                    GDBusConnection *connection,
                                    const char      *client_address)
{
    NMKeepAlivePrivate *priv = NM_KEEP_ALIVE_GET_PRIVATE(self);

    if (priv->disarmed)
        return;

    cleanup_dbus_watch(self);

    if (client_address) {
        _LOGD("Registering dbus client watch for keep alive");

        priv->dbus_client           = g_strdup(client_address);
        priv->dbus_client_watching  = TRUE;
        priv->dbus_client_confirmed = FALSE;
        priv->dbus_connection       = g_object_ref(connection);
        priv->subscription_id =
            nm_dbus_connection_signal_subscribe_name_owner_changed(priv->dbus_connection,
                                                                   priv->dbus_client,
                                                                   name_owner_changed_cb,
                                                                   self,
                                                                   NULL);
    } else
        priv->dbus_client_watching = FALSE;

    _notify_alive(self);
}

/*****************************************************************************/

/**
 * nm_keep_alive_arm:
 * @self: the #NMKeepAlive
 *
 * A #NMKeepAlive instance is unarmed by default. That means, it's
 * alive and stays alive until being armed. Arming means, that the conditions
 * start to be actively evaluated, that the alive state may change, and
 * that property changed signals are emitted.
 *
 * The opposite is nm_keep_alive_disarm() which freezes the alive state
 * for good. Once disarmed, the instance cannot be armed again. Arming an
 * instance multiple times has no effect. Arming an already disarmed instance
 * also has no effect. */
void
nm_keep_alive_arm(NMKeepAlive *self)
{
    NMKeepAlivePrivate *priv = NM_KEEP_ALIVE_GET_PRIVATE(self);

    if (!priv->armed) {
        priv->armed = TRUE;
        _notify_alive(self);
    }
}

/**
 * nm_keep_alive_disarm:
 * @self: the #NMKeepAlive instance
 *
 * Once the instance is disarmed, it will not change its alive state
 * anymore and will not emit anymore property changed signals about
 * alive state changed.
 *
 * As such, it will also free internal resources (since they no longer
 * affect the externally visible state).
 *
 * Once disarmed, the instance is frozen and cannot change anymore.
 */
void
nm_keep_alive_disarm(NMKeepAlive *self)
{
    NMKeepAlivePrivate *priv = NM_KEEP_ALIVE_GET_PRIVATE(self);

    priv->disarmed = TRUE;

    /* release internal data. */
    _set_settings_connection_watch_visible(self, NULL, FALSE);
    cleanup_dbus_watch(self);
}

/**
 * nm_keep_alive_destroy:
 * @self: (nullable): the #NMKeepAlive instance to destroy.
 *
 * This does 3 things in one:
 *
 *   - set owner to %NULL
 *   - disarm the instance.
 *   - unref @self.
 */
void
nm_keep_alive_destroy(NMKeepAlive *self)
{
    if (!self)
        return;
    _nm_keep_alive_set_owner(self, NULL);
    nm_keep_alive_disarm(self);
    g_object_unref(self);
}

/*****************************************************************************/

static void
get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
    NMKeepAlive *self = NM_KEEP_ALIVE(object);

    switch (prop_id) {
    case PROP_ALIVE:
        g_value_set_boolean(value, nm_keep_alive_is_alive(self));
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

/*****************************************************************************/

/**
 * nm_keep_alive_get_owner:
 * @self: the #NMKeepAlive
 *
 * Returns: the owner instance associated with this @self. This commonly
 *   is set to be the target instance, which @self guards for being alive.
 *   Returns a gpointer, but of course it's some GObject instance. */
gpointer /* GObject * */
nm_keep_alive_get_owner(NMKeepAlive *self)
{
    NMKeepAlivePrivate *priv = NM_KEEP_ALIVE_GET_PRIVATE(self);

    nm_assert(!priv->owner || G_IS_OBJECT(priv->owner));

    return priv->owner;
}

/**
 * _nm_keep_alive_set_owner:
 * @self: the #NMKeepAlive
 * @owner: the owner to set or unset.
 *
 * Sets or unsets the owner instance. Think of the owner the target
 * instance that is guarded by @self. It's the responsibility of the
 * owner to set and properly unset this pointer. As the owner also
 * controls the lifetime of the NMKeepAlive instance.
 *
 * This API is not to be called by everybody, but only the owner of
 * @self.
 */
void
_nm_keep_alive_set_owner(NMKeepAlive *self, GObject *owner)
{
    NMKeepAlivePrivate *priv = NM_KEEP_ALIVE_GET_PRIVATE(self);

    nm_assert(!owner || G_IS_OBJECT(owner));

    /* it's bad style to reset the owner object. You are supposed to
     * set it once, and clear it once. That's it. */
    nm_assert(!owner || !priv->owner);

    /* optimally, we would take a reference to @owner. But the
     * owner already owns a reference to the keep-alive, so we cannot
     * just own a reference back.
     *
     * We could register a weak-pointer here. But instead, declare that
     * owner is required to set itself as owner when creating the
     * keep-alive instance, and unset itself when it lets go of the
     * keep-alive instance (at latest, when the owner itself gets destroyed).
     */
    priv->owner = owner;
}

/*****************************************************************************/

static void
nm_keep_alive_init(NMKeepAlive *self)
{
    NMKeepAlivePrivate *priv = NM_KEEP_ALIVE_GET_PRIVATE(self);

    priv->alive = TRUE;

    nm_assert(priv->alive == _is_alive(self));
}

NMKeepAlive *
nm_keep_alive_new(void)
{
    return g_object_new(NM_TYPE_KEEP_ALIVE, NULL);
}

static void
dispose(GObject *object)
{
    NMKeepAlive *self = NM_KEEP_ALIVE(object);

    nm_assert(!NM_KEEP_ALIVE_GET_PRIVATE(self)->owner);

    /* disarm also happens to free all resources. */
    nm_keep_alive_disarm(self);
}

static void
nm_keep_alive_class_init(NMKeepAliveClass *keep_alive_class)
{
    GObjectClass *object_class = G_OBJECT_CLASS(keep_alive_class);

    object_class->get_property = get_property;
    object_class->dispose      = dispose;

    obj_properties[PROP_ALIVE] = g_param_spec_string(NM_KEEP_ALIVE_ALIVE,
                                                     "",
                                                     "",
                                                     NULL,
                                                     G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
}
