/*
 * Copyright (C) 2019 Canonical Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * 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, see <http://www.gnu.org/licenses/>.
 *
 */

// For AT_EMPTY_PATH and O_PATH
#define _GNU_SOURCE

#include "cgroup-freezer-support.h"

#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include "cgroup-support.h"
#include "cleanup-funcs.h"
#include "string-utils.h"
#include "utils.h"

static const char *freezer_cgroup_dir = "/sys/fs/cgroup/freezer";

void sc_cgroup_freezer_join(const char *snap_name, pid_t pid) {
    char buf[PATH_MAX] = {0};
    sc_must_snprintf(buf, sizeof buf, "snap.%s", snap_name);
    sc_cgroup_create_and_join(freezer_cgroup_dir, buf, pid);
}

bool sc_cgroup_freezer_occupied(const char *snap_name) {
    // Format the name of the cgroup hierarchy.
    char buf[PATH_MAX] = {0};
    sc_must_snprintf(buf, sizeof buf, "snap.%s", snap_name);

    // Open the freezer cgroup directory.
    int cgroup_fd SC_CLEANUP(sc_cleanup_close) = -1;
    cgroup_fd = open(freezer_cgroup_dir, O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (cgroup_fd < 0) {
        die("cannot open freezer cgroup (%s)", freezer_cgroup_dir);
    }
    // Open the proc directory.
    int proc_fd SC_CLEANUP(sc_cleanup_close) = -1;
    proc_fd = open("/proc", O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (proc_fd < 0) {
        die("cannot open /proc");
    }
    // Open the hierarchy directory for the given snap.
    int hierarchy_fd SC_CLEANUP(sc_cleanup_close) = -1;
    hierarchy_fd = openat(cgroup_fd, buf, O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (hierarchy_fd < 0) {
        if (errno == ENOENT) {
            return false;
        }
        die("cannot open freezer cgroup hierarchy for snap %s", snap_name);
    }
    // Open the "cgroup.procs" file. Alternatively we could open the "tasks"
    // file and see per-thread data but we don't need that.
    int cgroup_procs_fd SC_CLEANUP(sc_cleanup_close) = -1;
    cgroup_procs_fd = openat(hierarchy_fd, "cgroup.procs", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
    if (cgroup_procs_fd < 0) {
        die("cannot open cgroup.procs file for freezer cgroup hierarchy for snap %s", snap_name);
    }

    FILE *cgroup_procs SC_CLEANUP(sc_cleanup_file) = NULL;
    cgroup_procs = fdopen(cgroup_procs_fd, "r");
    if (cgroup_procs == NULL) {
        die("cannot convert cgroups.procs file descriptor to FILE");
    }
    cgroup_procs_fd = -1;  // cgroup_procs_fd will now be closed by fclose.

    char *line_buf SC_CLEANUP(sc_cleanup_string) = NULL;
    size_t line_buf_size = 0;
    ssize_t num_read;
    struct stat statbuf;
    for (;;) {
        num_read = getline(&line_buf, &line_buf_size, cgroup_procs);
        if (num_read < 0 && errno != 0) {
            die("cannot read next PID belonging to snap %s", snap_name);
        }
        if (num_read <= 0) {
            break;
        } else {
            if (line_buf[num_read - 1] == '\n') {
                line_buf[num_read - 1] = '\0';
            } else {
                die("could not find newline in cgroup.procs");
            }
        }
        debug("found process id: %s\n", line_buf);

        if (fstatat(proc_fd, line_buf, &statbuf, AT_SYMLINK_NOFOLLOW) < 0) {
            // The process may have died already.
            if (errno != ENOENT) {
                die("cannot stat /proc/%s", line_buf);
            }
            continue;
        }
        debug("found live process %s belonging to user %d", line_buf, statbuf.st_uid);
        return true;
    }

    return false;
}
