root/tools/perf/ui/browsers/scripts.c
// SPDX-License-Identifier: GPL-2.0
#include "../../util/util.h" // perf_exe()
#include "../util.h"
#include "../../util/evlist.h"
#include "../../util/hist.h"
#include "../../util/debug.h"
#include "../../util/session.h"
#include "../../util/symbol.h"
#include "../browser.h"
#include "../libslang.h"
#include "config.h"
#include <linux/err.h>
#include <linux/string.h>
#include <linux/zalloc.h>
#include <subcmd/exec-cmd.h>
#include <stdlib.h>

#define SCRIPT_NAMELEN  128
#define SCRIPT_MAX_NO   64
/*
 * Usually the full path for a script is:
 *      /home/username/libexec/perf-core/scripts/python/xxx.py
 *      /home/username/libexec/perf-core/scripts/perl/xxx.pl
 * So 256 should be long enough to contain the full path.
 */
#define SCRIPT_FULLPATH_LEN     256

struct script_config {
        const char **names;
        char **paths;
        int index;
        const char *perf;
        char extra_format[256];
};

void attr_to_script(char *extra_format, struct perf_event_attr *attr)
{
        extra_format[0] = 0;
        if (attr->read_format & PERF_FORMAT_GROUP)
                strcat(extra_format, " -F +metric");
        if (attr->sample_type & PERF_SAMPLE_BRANCH_STACK)
                strcat(extra_format, " -F +brstackinsn --xed");
        if (attr->sample_type & PERF_SAMPLE_REGS_INTR)
                strcat(extra_format, " -F +iregs");
        if (attr->sample_type & PERF_SAMPLE_REGS_USER)
                strcat(extra_format, " -F +uregs");
        if (attr->sample_type & PERF_SAMPLE_PHYS_ADDR)
                strcat(extra_format, " -F +phys_addr");
}

static int add_script_option(const char *name, const char *opt,
                             struct script_config *c)
{
        c->names[c->index] = name;
        if (asprintf(&c->paths[c->index],
                     "%s script %s -F +metric %s %s",
                     c->perf, opt, symbol_conf.inline_name ? " --inline" : "",
                     c->extra_format) < 0)
                return -1;
        c->index++;
        return 0;
}

static int scripts_config(const char *var, const char *value, void *data)
{
        struct script_config *c = data;

        if (!strstarts(var, "scripts."))
                return -1;
        if (c->index >= SCRIPT_MAX_NO)
                return -1;
        c->names[c->index] = strdup(var + 7);
        if (!c->names[c->index])
                return -1;
        if (asprintf(&c->paths[c->index], "%s %s", value,
                     c->extra_format) < 0)
                return -1;
        c->index++;
        return 0;
}

/*
 * Some scripts specify the required events in their "xxx-record" file,
 * this function will check if the events in perf.data match those
 * mentioned in the "xxx-record".
 *
 * Fixme: All existing "xxx-record" are all in good formats "-e event ",
 * which is covered well now. And new parsing code should be added to
 * cover the future complex formats like event groups etc.
 */
static int check_ev_match(int dir_fd, const char *scriptname, struct perf_session *session)
{
        char line[BUFSIZ];
        FILE *fp;

        {
                char filename[NAME_MAX + 5];
                int fd;

                scnprintf(filename, sizeof(filename), "bin/%s-record", scriptname);
                fd = openat(dir_fd, filename, O_RDONLY);
                if (fd == -1)
                        return -1;
                fp = fdopen(fd, "r");
                if (!fp)
                        return -1;
        }

        while (fgets(line, sizeof(line), fp)) {
                char *p = skip_spaces(line);

                if (*p == '#')
                        continue;

                while (strlen(p)) {
                        int match, len;
                        struct evsel *pos;
                        char evname[128];

                        p = strstr(p, "-e");
                        if (!p)
                                break;

                        p += 2;
                        p = skip_spaces(p);
                        len = strcspn(p, " \t");
                        if (!len)
                                break;

                        snprintf(evname, len + 1, "%s", p);

                        match = 0;
                        evlist__for_each_entry(session->evlist, pos) {
                                if (evsel__name_is(pos, evname)) {
                                        match = 1;
                                        break;
                                }
                        }

                        if (!match) {
                                fclose(fp);
                                return -1;
                        }
                }
        }

        fclose(fp);
        return 0;
}

/*
 * Return -1 if none is found, otherwise the actual scripts number.
 *
 * Currently the only user of this function is the script browser, which
 * will list all statically runnable scripts, select one, execute it and
 * show the output in a perf browser.
 */
static int find_scripts(char **scripts_array, char **scripts_path_array, int num,
                 int pathlen)
{
        struct dirent *script_dirent, *lang_dirent;
        int scripts_dir_fd, lang_dir_fd;
        DIR *scripts_dir, *lang_dir;
        struct perf_session *session;
        struct perf_data data = {
                .path = input_name,
                .mode = PERF_DATA_MODE_READ,
        };
        char *temp;
        int i = 0;
        const char *exec_path = get_argv_exec_path();

        session = perf_session__new(&data, NULL);
        if (IS_ERR(session))
                return PTR_ERR(session);

        {
                char scripts_path[PATH_MAX];

                snprintf(scripts_path, sizeof(scripts_path), "%s/scripts", exec_path);
                scripts_dir_fd = open(scripts_path, O_DIRECTORY);
                pr_err("Failed to open directory '%s'", scripts_path);
                if (scripts_dir_fd == -1) {
                        perf_session__delete(session);
                        return -1;
                }
        }
        scripts_dir = fdopendir(scripts_dir_fd);
        if (!scripts_dir) {
                close(scripts_dir_fd);
                perf_session__delete(session);
                return -1;
        }

        while ((lang_dirent = readdir(scripts_dir)) != NULL) {
                if (lang_dirent->d_type != DT_DIR &&
                    (lang_dirent->d_type == DT_UNKNOWN &&
                     !is_directory_at(scripts_dir_fd, lang_dirent->d_name)))
                        continue;
                if (!strcmp(lang_dirent->d_name, ".") || !strcmp(lang_dirent->d_name, ".."))
                        continue;

#ifndef HAVE_LIBPERL_SUPPORT
                if (strstr(lang_dirent->d_name, "perl"))
                        continue;
#endif
#ifndef HAVE_LIBPYTHON_SUPPORT
                if (strstr(lang_dirent->d_name, "python"))
                        continue;
#endif

                lang_dir_fd = openat(scripts_dir_fd, lang_dirent->d_name, O_DIRECTORY);
                if (lang_dir_fd == -1)
                        continue;
                lang_dir = fdopendir(lang_dir_fd);
                if (!lang_dir) {
                        close(lang_dir_fd);
                        continue;
                }
                while ((script_dirent = readdir(lang_dir)) != NULL) {
                        if (script_dirent->d_type == DT_DIR)
                                continue;
                        if (script_dirent->d_type == DT_UNKNOWN &&
                            is_directory_at(lang_dir_fd, script_dirent->d_name))
                                continue;
                        /* Skip those real time scripts: xxxtop.p[yl] */
                        if (strstr(script_dirent->d_name, "top."))
                                continue;
                        if (i >= num)
                                break;
                        scnprintf(scripts_path_array[i], pathlen, "%s/scripts/%s/%s",
                                exec_path,
                                lang_dirent->d_name,
                                script_dirent->d_name);
                        temp = strchr(script_dirent->d_name, '.');
                        snprintf(scripts_array[i],
                                (temp - script_dirent->d_name) + 1,
                                "%s", script_dirent->d_name);

                        if (check_ev_match(lang_dir_fd, scripts_array[i], session))
                                continue;

                        i++;
                }
                closedir(lang_dir);
        }

        closedir(scripts_dir);
        perf_session__delete(session);
        return i;
}

/*
 * When success, will copy the full path of the selected script
 * into  the buffer pointed by script_name, and return 0.
 * Return -1 on failure.
 */
static int list_scripts(char *script_name, bool *custom,
                        struct evsel *evsel)
{
        char *buf, *paths[SCRIPT_MAX_NO], *names[SCRIPT_MAX_NO];
        int i, num, choice;
        int ret = 0;
        int max_std, custom_perf;
        char pbuf[256];
        const char *perf = perf_exe(pbuf, sizeof pbuf);
        struct script_config scriptc = {
                .names = (const char **)names,
                .paths = paths,
                .perf = perf
        };

        script_name[0] = 0;

        /* Preset the script name to SCRIPT_NAMELEN */
        buf = malloc(SCRIPT_MAX_NO * (SCRIPT_NAMELEN + SCRIPT_FULLPATH_LEN));
        if (!buf)
                return -1;

        if (evsel)
                attr_to_script(scriptc.extra_format, &evsel->core.attr);
        add_script_option("Show individual samples", "", &scriptc);
        add_script_option("Show individual samples with assembler", "-F +disasm",
                          &scriptc);
        add_script_option("Show individual samples with source", "-F +srcline,+srccode",
                          &scriptc);
        perf_config(scripts_config, &scriptc);
        custom_perf = scriptc.index;
        add_script_option("Show samples with custom perf script arguments", "", &scriptc);
        i = scriptc.index;
        max_std = i;

        for (; i < SCRIPT_MAX_NO; i++) {
                names[i] = buf + (i - max_std) * (SCRIPT_NAMELEN + SCRIPT_FULLPATH_LEN);
                paths[i] = names[i] + SCRIPT_NAMELEN;
        }

        num = find_scripts(names + max_std, paths + max_std, SCRIPT_MAX_NO - max_std,
                        SCRIPT_FULLPATH_LEN);
        if (num < 0)
                num = 0;
        choice = ui__popup_menu(num + max_std, (char * const *)names, NULL);
        if (choice < 0) {
                ret = -1;
                goto out;
        }
        if (choice == custom_perf) {
                char script_args[50];
                int key = ui_browser__input_window("perf script command",
                                "Enter perf script command line (without perf script prefix)",
                                script_args, "", 0);
                if (key != K_ENTER) {
                        ret = -1;
                        goto out;
                }
                sprintf(script_name, "%s script %s", perf, script_args);
        } else if (choice < num + max_std) {
                strcpy(script_name, paths[choice]);
        }
        *custom = choice >= max_std;

out:
        free(buf);
        for (i = 0; i < max_std; i++)
                zfree(&paths[i]);
        return ret;
}

void run_script(char *cmd)
{
        pr_debug("Running %s\n", cmd);
        SLang_reset_tty();
        if (system(cmd) < 0)
                pr_warning("Cannot run %s\n", cmd);
        /*
         * SLang doesn't seem to reset the whole terminal, so be more
         * forceful to get back to the original state.
         */
        printf("\033[c\033[H\033[J");
        fflush(stdout);
        SLang_init_tty(0, 0, 0);
        SLtty_set_suspend_state(true);
        SLsmg_refresh();
}

int script_browse(const char *script_opt, struct evsel *evsel)
{
        char *cmd, script_name[SCRIPT_FULLPATH_LEN];
        bool custom = false;

        memset(script_name, 0, SCRIPT_FULLPATH_LEN);
        if (list_scripts(script_name, &custom, evsel))
                return -1;

        if (asprintf(&cmd, "%s%s %s %s%s 2>&1 | less",
                        custom ? "perf script -s " : "",
                        script_name,
                        script_opt ? script_opt : "",
                        input_name ? "-i " : "",
                        input_name ? input_name : "") < 0)
                return -1;

        run_script(cmd);
        free(cmd);

        return 0;
}