/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include <unistd.h>

#include "sd-event.h"

#include "cgroup-util.h"
#include "copy.h"
#include "discover-image.h"
#include "env-file.h"
#include "fd-util.h"
#include "fileio.h"
#include "hashmap.h"
#include "local-addresses.h"
#include "log.h"
#include "machine.h"
#include "machined.h"
#include "namespace-util.h"
#include "os-util.h"
#include "process-util.h"
#include "strv.h"
#include "user-util.h"

int manager_get_machine_by_pidref(Manager *m, const PidRef *pidref, Machine **ret) {
        _cleanup_(pidref_done) PidRef current = PIDREF_NULL;
        Machine *mm = NULL;

        assert(m);
        assert(pidref_is_set(pidref));
        assert(ret);

        for (;;) {
                /* First, compare by leader */
                mm = hashmap_get(m->machines_by_leader, pidref);
                if (mm)
                        break;

                /* Then look for the unit */
                _cleanup_free_ char *unit = NULL;
                if (cg_pidref_get_unit(pidref, &unit) >= 0) {
                        mm = hashmap_get(m->machines_by_unit, unit);
                        if (mm)
                                break;
                }

                /* Maybe this process is in per-user unit? If so, let's go up the process tree, and check
                 * that, we should eventually hit PID 1 of the container tree, which we should be able to
                 * recognize. */
                _cleanup_(pidref_done) PidRef parent = PIDREF_NULL;
                if (pidref_get_ppid_as_pidref(pidref, &parent) < 0)
                        break;

                pidref_done(&current);
                current = TAKE_PIDREF(parent);
                pidref = &current;
        }

        *ret = mm;
        return !!mm;
}

int manager_add_machine(Manager *m, const char *name, Machine **ret) {
        Machine *machine;
        int r;

        assert(m);
        assert(name);

        machine = hashmap_get(m->machines, name);
        if (!machine) {
                r = machine_new(_MACHINE_CLASS_INVALID, name, &machine);
                if (r < 0)
                        return r;

                r = machine_link(m, machine);
                if (r < 0) {
                        machine_free(machine);
                        return r;
                }
        }

        if (ret)
                *ret = machine;

        return 0;
}

int manager_find_machine_for_uid(Manager *m, uid_t uid, Machine **ret_machine, uid_t *ret_internal_uid) {
        Machine *machine;
        int r;

        assert(m);
        assert(uid_is_valid(uid));

        /* Finds the machine for the specified host UID and returns it along with the UID translated into the
         * internal UID inside the machine */

        HASHMAP_FOREACH(machine, m->machines) {
                uid_t converted;

                r = machine_owns_uid(machine, uid, &converted);
                if (r < 0)
                        return r;
                if (r) {
                        if (ret_machine)
                                *ret_machine = machine;

                        if (ret_internal_uid)
                                *ret_internal_uid = converted;

                        return true;
                }
        }

        if (ret_machine)
                *ret_machine = NULL;
        if (ret_internal_uid)
                *ret_internal_uid = UID_INVALID;

        return false;
}

int manager_find_machine_for_gid(Manager *m, gid_t gid, Machine **ret_machine, gid_t *ret_internal_gid) {
        Machine *machine;
        int r;

        assert(m);
        assert(gid_is_valid(gid));

        HASHMAP_FOREACH(machine, m->machines) {
                gid_t converted;

                r = machine_owns_gid(machine, gid, &converted);
                if (r < 0)
                        return r;
                if (r) {
                        if (ret_machine)
                                *ret_machine = machine;

                        if (ret_internal_gid)
                                *ret_internal_gid = converted;

                        return true;
                }
        }

        if (ret_machine)
                *ret_machine = NULL;
        if (ret_internal_gid)
                *ret_internal_gid = GID_INVALID;

        return false;
}

void manager_gc(Manager *m, bool drop_not_started) {
        Machine *machine;

        assert(m);

        while ((machine = LIST_POP(gc_queue, m->machine_gc_queue))) {
                machine->in_gc_queue = false;

                /* First, if we are not closing yet, initiate stopping */
                if (machine_may_gc(machine, drop_not_started) &&
                    machine_get_state(machine) != MACHINE_CLOSING) {
                        log_debug("Stopping machine '%s' due to GC.", machine->name);
                        machine_stop(machine);
                }

                /* Now, the stop probably made this referenced
                 * again, but if it didn't, then it's time to let it
                 * go entirely. */
                if (machine_may_gc(machine, drop_not_started)) {
                        log_debug("Finalizing machine '%s' due to GC.", machine->name);
                        machine_finalize(machine);
                        machine_free(machine);
                }
        }
}

static int on_deferred_gc(sd_event_source *s, void *userdata) {
        manager_gc(userdata, /* drop_not_started= */ true);
        return 0;
}

void manager_enqueue_gc(Manager *m) {
        int r;

        assert(m);

        if (m->deferred_gc_event_source) {
                r = sd_event_source_set_enabled(m->deferred_gc_event_source, SD_EVENT_ONESHOT);
                if (r < 0)
                        log_warning_errno(r, "Failed to enable GC event source, ignoring: %m");

                return;
        }

        r = sd_event_add_defer(m->event, &m->deferred_gc_event_source, on_deferred_gc, m);
        if (r < 0)
                return (void) log_warning_errno(r, "Failed to allocate GC event source, ignoring: %m");

        r = sd_event_source_set_priority(m->deferred_gc_event_source, SD_EVENT_PRIORITY_IDLE);
        if (r < 0)
                log_warning_errno(r, "Failed to tweak priority of event source, ignoring: %m");

        (void) sd_event_source_set_description(m->deferred_gc_event_source, "deferred-gc");
}

int machine_get_addresses(Machine *machine, struct local_address **ret_addresses) {
        assert(machine);
        assert(ret_addresses);

        switch (machine->class) {

        case MACHINE_HOST: {
                _cleanup_free_ struct local_address *addresses = NULL;
                int n;

                n = local_addresses(/* context= */ NULL, /* ifindex= */ 0, AF_UNSPEC, &addresses);
                if (n < 0)
                        return log_debug_errno(n, "Failed to get local addresses: %m");

                *ret_addresses = TAKE_PTR(addresses);
                return n;
        }

        case MACHINE_CONTAINER: {
                _cleanup_(pidref_done) PidRef child = PIDREF_NULL;
                _cleanup_close_pair_ int pair[2] = EBADF_PAIR;
                _cleanup_close_ int netns_fd = -EBADF;
                int r;

                r = pidref_in_same_namespace(/* pid1= */ NULL, &machine->leader, NAMESPACE_NET);
                if (r < 0)
                        return log_debug_errno(r, "Failed to check if container has private network: %m");
                if (r > 0)
                        return -ENONET;

                r = pidref_namespace_open(&machine->leader,
                                          /* ret_pidns_fd= */ NULL,
                                          /* ret_mntns_fd= */ NULL,
                                          &netns_fd,
                                          /* ret_userns_fd= */ NULL,
                                          /* ret_root_fd= */ NULL);
                if (r < 0)
                        return log_debug_errno(r, "Failed to open namespace: %m");

                if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, pair) < 0)
                        return log_debug_errno(errno, "Failed to call socketpair(): %m");

                r = namespace_fork("(sd-addrns)",
                                   "(sd-addr)",
                                   FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGKILL,
                                   /* pidns_fd= */ -EBADF,
                                   /* mntns_fd= */ -EBADF,
                                   netns_fd,
                                   /* userns_fd= */ -EBADF,
                                   /* root_fd= */ -EBADF,
                                   &child);
                if (r < 0)
                        return log_debug_errno(r, "Failed to fork(): %m");
                if (r == 0) {
                        _cleanup_free_ struct local_address *addresses = NULL;

                        pair[0] = safe_close(pair[0]);

                        int n = local_addresses(/* context= */ NULL, /* ifindex= */ 0, AF_UNSPEC, &addresses);
                        if (n < 0) {
                                log_debug_errno(n, "Failed to get local addresses: %m");
                                _exit(EXIT_FAILURE);
                        }

                        FOREACH_ARRAY(a, addresses, n) {
                                r = write(pair[1], a, sizeof(*a));
                                if (r < 0) {
                                        log_debug_errno(errno, "Failed to write to socket: %m");
                                        _exit(EXIT_FAILURE);
                                }
                        }

                        _exit(EXIT_SUCCESS);
                }

                pair[1] = safe_close(pair[1]);

                _cleanup_free_ struct local_address *list = NULL;
                size_t n_list = 0;

                for (;;) {
                        ssize_t n;
                        struct local_address la;

                        n = read(pair[0], &la, sizeof(la));
                        if (n < 0)
                                return log_debug_errno(errno, "Failed to read from socket(): %m");
                        if (n == 0)
                                break;
                        if ((size_t) n < sizeof(la))
                                return log_debug_errno(SYNTHETIC_ERRNO(EIO), "Received unexpectedly short message");

                        r = add_local_address(&list,
                                              &n_list,
                                              la.ifindex,
                                              la.scope,
                                              la.family,
                                              &la.address);
                        if (r < 0)
                                return log_debug_errno(r, "Failed to add local address: %m");
                }

                r = pidref_wait_for_terminate_and_check("(sd-addrns)", &child, /* flags= */ 0);
                if (r < 0)
                        return log_debug_errno(r, "Failed to wait for child: %m");
                if (r != EXIT_SUCCESS)
                        return log_debug_errno(SYNTHETIC_ERRNO(ESHUTDOWN), "Child died abnormally");

                *ret_addresses = TAKE_PTR(list);
                return (int) n_list;
        }

        default:
                return -EOPNOTSUPP;
        }
}

#define EXIT_NOT_FOUND 2

int machine_get_os_release(Machine *machine, char ***ret_os_release) {
        _cleanup_strv_free_ char **l = NULL;
        int r;

        assert(machine);
        assert(ret_os_release);

        switch (machine->class) {

        case MACHINE_HOST:
                r = load_os_release_pairs(/* root= */ NULL, &l);
                if (r < 0)
                        return log_debug_errno(r, "Failed to load OS release information: %m");

                break;

        case MACHINE_CONTAINER: {
                _cleanup_close_ int mntns_fd = -EBADF, root_fd = -EBADF, pidns_fd = -EBADF;
                _cleanup_(pidref_done) PidRef child = PIDREF_NULL;
                _cleanup_close_pair_ int pair[2] = EBADF_PAIR;
                _cleanup_fclose_ FILE *f = NULL;

                r = pidref_namespace_open(&machine->leader,
                                          &pidns_fd,
                                          &mntns_fd,
                                          /* ret_netns_fd= */ NULL,
                                          /* ret_userns_fd= */ NULL,
                                          &root_fd);
                if (r < 0)
                        return log_debug_errno(r, "Failed to open namespace: %m");

                if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, pair) < 0)
                        return log_debug_errno(errno, "Failed to call socketpair(): %m");

                r = namespace_fork("(sd-osrelns)",
                                   "(sd-osrel)",
                                   FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGKILL,
                                   pidns_fd,
                                   mntns_fd,
                                   /* netns_fd= */ -EBADF,
                                   /* userns_fd= */ -EBADF,
                                   root_fd,
                                   &child);
                if (r < 0)
                        return log_debug_errno(r, "Failed to fork(): %m");
                if (r == 0) {
                        _cleanup_close_ int fd = -EBADF;

                        pair[0] = safe_close(pair[0]);

                        r = open_os_release(/* root= */ NULL, /* ret_path= */ NULL, &fd);
                        if (r == -ENOENT)
                                _exit(EXIT_NOT_FOUND);
                        if (r < 0) {
                                log_debug_errno(r, "Failed to read OS release: %m");
                                _exit(EXIT_FAILURE);
                        }

                        r = copy_bytes(fd, pair[1], UINT64_MAX, /* copy_flags= */ 0);
                        if (r < 0) {
                                log_debug_errno(r, "Failed to write to fd: %m");
                                _exit(EXIT_FAILURE);
                        }

                        _exit(EXIT_SUCCESS);
                }

                pair[1] = safe_close(pair[1]);

                f = take_fdopen(&pair[0], "r");
                if (!f)
                        return log_debug_errno(errno, "Failed to fdopen(): %m");

                r = load_env_file_pairs(f, "/etc/os-release", &l);
                if (r < 0)
                        return log_debug_errno(r, "Failed to load OS release information: %m");

                r = pidref_wait_for_terminate_and_check("(sd-osrelns)", &child, /* flags= */ 0);
                if (r < 0)
                        return log_debug_errno(r, "Failed to wait for child: %m");
                if (r == EXIT_NOT_FOUND)
                        return -ENOENT;
                if (r != EXIT_SUCCESS)
                        return log_debug_errno(SYNTHETIC_ERRNO(ESHUTDOWN), "Child died abnormally");

                break;
        }

        default:
                return -EOPNOTSUPP;
        }

        *ret_os_release = TAKE_PTR(l);
        return 0;
}

static int image_flush_cache(sd_event_source *s, void *userdata) {
        Manager *m = ASSERT_PTR(userdata);

        assert(s);

        hashmap_clear(m->image_cache);
        return 0;
}

int manager_acquire_image(Manager *m, const char *name, Image **ret) {
        int r;

        assert(m);
        assert(name);

        Image *existing = hashmap_get(m->image_cache, name);
        if (existing) {
                if (ret)
                        *ret = existing;
                return 0;
        }

        if (!m->image_cache_defer_event) {
                r = sd_event_add_defer(m->event, &m->image_cache_defer_event, image_flush_cache, m);
                if (r < 0)
                        return log_debug_errno(r, "Failed to add deferred event: %m");

                r = sd_event_source_set_priority(m->image_cache_defer_event, SD_EVENT_PRIORITY_IDLE);
                if (r < 0)
                        return log_debug_errno(r, "Failed to set source priority for event: %m");
        }

        r = sd_event_source_set_enabled(m->image_cache_defer_event, SD_EVENT_ONESHOT);
        if (r < 0)
                return log_debug_errno(r, "Failed to enable source: %m") ;

        _cleanup_(image_unrefp) Image *image = NULL;
        r = image_find(m->runtime_scope, IMAGE_MACHINE, name, NULL, &image);
        if (r < 0)
                return log_debug_errno(r, "Failed to find image: %m");

        image->userdata = m;

        r = hashmap_ensure_put(&m->image_cache, &image_hash_ops, image->name, image);
        if (r < 0)
                return r;

        if (ret)
                *ret = image;

        TAKE_PTR(image);
        return 0;
}

int rename_image_and_update_cache(Manager *m, Image *image, const char* new_name) {
        int r;

        assert(m);
        assert(image);
        assert(new_name);

        /* The image is cached with its name, hence it is necessary to remove from the cache before renaming. */
        assert_se(hashmap_remove_value(m->image_cache, image->name, image));

        r = image_rename(image, new_name, m->runtime_scope);
        if (r < 0) {
                image = image_unref(image);
                return r;
        }

        /* Then save the object again in the cache. */
        r = hashmap_put(m->image_cache, image->name, image);
        if (r < 0) {
                image = image_unref(image);
                log_debug_errno(r, "Failed to put renamed image into cache, ignoring: %m");
        }

        return 0;
}
