1533 lines
31 KiB
C
1533 lines
31 KiB
C
|
// SPDX-License-Identifier: GPL-2.0
|
||
|
#include <internal/lib.h>
|
||
|
#include <subcmd/parse-options.h>
|
||
|
#include <api/fd/array.h>
|
||
|
#include <api/fs/fs.h>
|
||
|
#include <linux/zalloc.h>
|
||
|
#include <linux/string.h>
|
||
|
#include <linux/limits.h>
|
||
|
#include <string.h>
|
||
|
#include <sys/file.h>
|
||
|
#include <signal.h>
|
||
|
#include <stdlib.h>
|
||
|
#include <time.h>
|
||
|
#include <stdio.h>
|
||
|
#include <unistd.h>
|
||
|
#include <errno.h>
|
||
|
#include <sys/inotify.h>
|
||
|
#include <libgen.h>
|
||
|
#include <sys/types.h>
|
||
|
#include <sys/socket.h>
|
||
|
#include <sys/un.h>
|
||
|
#include <sys/stat.h>
|
||
|
#include <sys/signalfd.h>
|
||
|
#include <sys/wait.h>
|
||
|
#include <poll.h>
|
||
|
#include "builtin.h"
|
||
|
#include "perf.h"
|
||
|
#include "debug.h"
|
||
|
#include "config.h"
|
||
|
#include "util.h"
|
||
|
|
||
|
#define SESSION_OUTPUT "output"
|
||
|
#define SESSION_CONTROL "control"
|
||
|
#define SESSION_ACK "ack"
|
||
|
|
||
|
/*
|
||
|
* Session states:
|
||
|
*
|
||
|
* OK - session is up and running
|
||
|
* RECONFIG - session is pending for reconfiguration,
|
||
|
* new values are already loaded in session object
|
||
|
* KILL - session is pending to be killed
|
||
|
*
|
||
|
* Session object life and its state is maintained by
|
||
|
* following functions:
|
||
|
*
|
||
|
* setup_server_config
|
||
|
* - reads config file and setup session objects
|
||
|
* with following states:
|
||
|
*
|
||
|
* OK - no change needed
|
||
|
* RECONFIG - session needs to be changed
|
||
|
* (run variable changed)
|
||
|
* KILL - session needs to be killed
|
||
|
* (session is no longer in config file)
|
||
|
*
|
||
|
* daemon__reconfig
|
||
|
* - scans session objects and does following actions
|
||
|
* for states:
|
||
|
*
|
||
|
* OK - skip
|
||
|
* RECONFIG - session is killed and re-run with new config
|
||
|
* KILL - session is killed
|
||
|
*
|
||
|
* - all sessions have OK state on the function exit
|
||
|
*/
|
||
|
enum daemon_session_state {
|
||
|
OK,
|
||
|
RECONFIG,
|
||
|
KILL,
|
||
|
};
|
||
|
|
||
|
struct daemon_session {
|
||
|
char *base;
|
||
|
char *name;
|
||
|
char *run;
|
||
|
char *control;
|
||
|
int pid;
|
||
|
struct list_head list;
|
||
|
enum daemon_session_state state;
|
||
|
time_t start;
|
||
|
};
|
||
|
|
||
|
struct daemon {
|
||
|
const char *config;
|
||
|
char *config_real;
|
||
|
char *config_base;
|
||
|
const char *csv_sep;
|
||
|
const char *base_user;
|
||
|
char *base;
|
||
|
struct list_head sessions;
|
||
|
FILE *out;
|
||
|
char perf[PATH_MAX];
|
||
|
int signal_fd;
|
||
|
time_t start;
|
||
|
};
|
||
|
|
||
|
static struct daemon __daemon = {
|
||
|
.sessions = LIST_HEAD_INIT(__daemon.sessions),
|
||
|
};
|
||
|
|
||
|
static const char * const daemon_usage[] = {
|
||
|
"perf daemon {start|signal|stop|ping} [<options>]",
|
||
|
"perf daemon [<options>]",
|
||
|
NULL
|
||
|
};
|
||
|
|
||
|
static volatile sig_atomic_t done;
|
||
|
|
||
|
static void sig_handler(int sig __maybe_unused)
|
||
|
{
|
||
|
done = true;
|
||
|
}
|
||
|
|
||
|
static struct daemon_session *daemon__add_session(struct daemon *config, char *name)
|
||
|
{
|
||
|
struct daemon_session *session = zalloc(sizeof(*session));
|
||
|
|
||
|
if (!session)
|
||
|
return NULL;
|
||
|
|
||
|
session->name = strdup(name);
|
||
|
if (!session->name) {
|
||
|
free(session);
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
session->pid = -1;
|
||
|
list_add_tail(&session->list, &config->sessions);
|
||
|
return session;
|
||
|
}
|
||
|
|
||
|
static struct daemon_session *daemon__find_session(struct daemon *daemon, char *name)
|
||
|
{
|
||
|
struct daemon_session *session;
|
||
|
|
||
|
list_for_each_entry(session, &daemon->sessions, list) {
|
||
|
if (!strcmp(session->name, name))
|
||
|
return session;
|
||
|
}
|
||
|
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
static int get_session_name(const char *var, char *session, int len)
|
||
|
{
|
||
|
const char *p = var + sizeof("session-") - 1;
|
||
|
|
||
|
while (*p != '.' && *p != 0x0 && len--)
|
||
|
*session++ = *p++;
|
||
|
|
||
|
*session = 0;
|
||
|
return *p == '.' ? 0 : -EINVAL;
|
||
|
}
|
||
|
|
||
|
static int session_config(struct daemon *daemon, const char *var, const char *value)
|
||
|
{
|
||
|
struct daemon_session *session;
|
||
|
char name[100];
|
||
|
|
||
|
if (get_session_name(var, name, sizeof(name) - 1))
|
||
|
return -EINVAL;
|
||
|
|
||
|
var = strchr(var, '.');
|
||
|
if (!var)
|
||
|
return -EINVAL;
|
||
|
|
||
|
var++;
|
||
|
|
||
|
session = daemon__find_session(daemon, name);
|
||
|
|
||
|
if (!session) {
|
||
|
/* New session is defined. */
|
||
|
session = daemon__add_session(daemon, name);
|
||
|
if (!session)
|
||
|
return -ENOMEM;
|
||
|
|
||
|
pr_debug("reconfig: found new session %s\n", name);
|
||
|
|
||
|
/* Trigger reconfig to start it. */
|
||
|
session->state = RECONFIG;
|
||
|
} else if (session->state == KILL) {
|
||
|
/* Current session is defined, no action needed. */
|
||
|
pr_debug("reconfig: found current session %s\n", name);
|
||
|
session->state = OK;
|
||
|
}
|
||
|
|
||
|
if (!strcmp(var, "run")) {
|
||
|
bool same = false;
|
||
|
|
||
|
if (session->run)
|
||
|
same = !strcmp(session->run, value);
|
||
|
|
||
|
if (!same) {
|
||
|
if (session->run) {
|
||
|
free(session->run);
|
||
|
pr_debug("reconfig: session %s is changed\n", name);
|
||
|
}
|
||
|
|
||
|
session->run = strdup(value);
|
||
|
if (!session->run)
|
||
|
return -ENOMEM;
|
||
|
|
||
|
/*
|
||
|
* Either new or changed run value is defined,
|
||
|
* trigger reconfig for the session.
|
||
|
*/
|
||
|
session->state = RECONFIG;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static int server_config(const char *var, const char *value, void *cb)
|
||
|
{
|
||
|
struct daemon *daemon = cb;
|
||
|
|
||
|
if (strstarts(var, "session-")) {
|
||
|
return session_config(daemon, var, value);
|
||
|
} else if (!strcmp(var, "daemon.base") && !daemon->base_user) {
|
||
|
if (daemon->base && strcmp(daemon->base, value)) {
|
||
|
pr_err("failed: can't redefine base, bailing out\n");
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
daemon->base = strdup(value);
|
||
|
if (!daemon->base)
|
||
|
return -ENOMEM;
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static int client_config(const char *var, const char *value, void *cb)
|
||
|
{
|
||
|
struct daemon *daemon = cb;
|
||
|
|
||
|
if (!strcmp(var, "daemon.base") && !daemon->base_user) {
|
||
|
daemon->base = strdup(value);
|
||
|
if (!daemon->base)
|
||
|
return -ENOMEM;
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static int check_base(struct daemon *daemon)
|
||
|
{
|
||
|
struct stat st;
|
||
|
|
||
|
if (!daemon->base) {
|
||
|
pr_err("failed: base not defined\n");
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
if (stat(daemon->base, &st)) {
|
||
|
switch (errno) {
|
||
|
case EACCES:
|
||
|
pr_err("failed: permission denied for '%s' base\n",
|
||
|
daemon->base);
|
||
|
return -EACCES;
|
||
|
case ENOENT:
|
||
|
pr_err("failed: base '%s' does not exists\n",
|
||
|
daemon->base);
|
||
|
return -EACCES;
|
||
|
default:
|
||
|
pr_err("failed: can't access base '%s': %s\n",
|
||
|
daemon->base, strerror(errno));
|
||
|
return -errno;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ((st.st_mode & S_IFMT) != S_IFDIR) {
|
||
|
pr_err("failed: base '%s' is not directory\n",
|
||
|
daemon->base);
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static int setup_client_config(struct daemon *daemon)
|
||
|
{
|
||
|
struct perf_config_set *set = perf_config_set__load_file(daemon->config_real);
|
||
|
int err = -ENOMEM;
|
||
|
|
||
|
if (set) {
|
||
|
err = perf_config_set(set, client_config, daemon);
|
||
|
perf_config_set__delete(set);
|
||
|
}
|
||
|
|
||
|
return err ?: check_base(daemon);
|
||
|
}
|
||
|
|
||
|
static int setup_server_config(struct daemon *daemon)
|
||
|
{
|
||
|
struct perf_config_set *set;
|
||
|
struct daemon_session *session;
|
||
|
int err = -ENOMEM;
|
||
|
|
||
|
pr_debug("reconfig: started\n");
|
||
|
|
||
|
/*
|
||
|
* Mark all sessions for kill, the server config
|
||
|
* will set following states, see explanation at
|
||
|
* enum daemon_session_state declaration.
|
||
|
*/
|
||
|
list_for_each_entry(session, &daemon->sessions, list)
|
||
|
session->state = KILL;
|
||
|
|
||
|
set = perf_config_set__load_file(daemon->config_real);
|
||
|
if (set) {
|
||
|
err = perf_config_set(set, server_config, daemon);
|
||
|
perf_config_set__delete(set);
|
||
|
}
|
||
|
|
||
|
return err ?: check_base(daemon);
|
||
|
}
|
||
|
|
||
|
static int daemon_session__run(struct daemon_session *session,
|
||
|
struct daemon *daemon)
|
||
|
{
|
||
|
char buf[PATH_MAX];
|
||
|
char **argv;
|
||
|
int argc, fd;
|
||
|
|
||
|
if (asprintf(&session->base, "%s/session-%s",
|
||
|
daemon->base, session->name) < 0) {
|
||
|
perror("failed: asprintf");
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
if (mkdir(session->base, 0755) && errno != EEXIST) {
|
||
|
perror("failed: mkdir");
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
session->start = time(NULL);
|
||
|
|
||
|
session->pid = fork();
|
||
|
if (session->pid < 0)
|
||
|
return -1;
|
||
|
if (session->pid > 0) {
|
||
|
pr_info("reconfig: ruining session [%s:%d]: %s\n",
|
||
|
session->name, session->pid, session->run);
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
if (chdir(session->base)) {
|
||
|
perror("failed: chdir");
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
fd = open("/dev/null", O_RDONLY);
|
||
|
if (fd < 0) {
|
||
|
perror("failed: open /dev/null");
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
dup2(fd, 0);
|
||
|
close(fd);
|
||
|
|
||
|
fd = open(SESSION_OUTPUT, O_RDWR|O_CREAT|O_TRUNC, 0644);
|
||
|
if (fd < 0) {
|
||
|
perror("failed: open session output");
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
dup2(fd, 1);
|
||
|
dup2(fd, 2);
|
||
|
close(fd);
|
||
|
|
||
|
if (mkfifo(SESSION_CONTROL, 0600) && errno != EEXIST) {
|
||
|
perror("failed: create control fifo");
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
if (mkfifo(SESSION_ACK, 0600) && errno != EEXIST) {
|
||
|
perror("failed: create ack fifo");
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
scnprintf(buf, sizeof(buf), "%s record --control=fifo:%s,%s %s",
|
||
|
daemon->perf, SESSION_CONTROL, SESSION_ACK, session->run);
|
||
|
|
||
|
argv = argv_split(buf, &argc);
|
||
|
if (!argv)
|
||
|
exit(-1);
|
||
|
|
||
|
exit(execve(daemon->perf, argv, NULL));
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
static pid_t handle_signalfd(struct daemon *daemon)
|
||
|
{
|
||
|
struct daemon_session *session;
|
||
|
struct signalfd_siginfo si;
|
||
|
ssize_t err;
|
||
|
int status;
|
||
|
pid_t pid;
|
||
|
|
||
|
/*
|
||
|
* Take signal fd data as pure signal notification and check all
|
||
|
* the sessions state. The reason is that multiple signals can get
|
||
|
* coalesced in kernel and we can receive only single signal even
|
||
|
* if multiple SIGCHLD were generated.
|
||
|
*/
|
||
|
err = read(daemon->signal_fd, &si, sizeof(struct signalfd_siginfo));
|
||
|
if (err != sizeof(struct signalfd_siginfo)) {
|
||
|
pr_err("failed to read signal fd\n");
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
list_for_each_entry(session, &daemon->sessions, list) {
|
||
|
if (session->pid == -1)
|
||
|
continue;
|
||
|
|
||
|
pid = waitpid(session->pid, &status, WNOHANG);
|
||
|
if (pid <= 0)
|
||
|
continue;
|
||
|
|
||
|
if (WIFEXITED(status)) {
|
||
|
pr_info("session '%s' exited, status=%d\n",
|
||
|
session->name, WEXITSTATUS(status));
|
||
|
} else if (WIFSIGNALED(status)) {
|
||
|
pr_info("session '%s' killed (signal %d)\n",
|
||
|
session->name, WTERMSIG(status));
|
||
|
} else if (WIFSTOPPED(status)) {
|
||
|
pr_info("session '%s' stopped (signal %d)\n",
|
||
|
session->name, WSTOPSIG(status));
|
||
|
} else {
|
||
|
pr_info("session '%s' Unexpected status (0x%x)\n",
|
||
|
session->name, status);
|
||
|
}
|
||
|
|
||
|
session->state = KILL;
|
||
|
session->pid = -1;
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static int daemon_session__wait(struct daemon_session *session, struct daemon *daemon,
|
||
|
int secs)
|
||
|
{
|
||
|
struct pollfd pollfd = {
|
||
|
.fd = daemon->signal_fd,
|
||
|
.events = POLLIN,
|
||
|
};
|
||
|
time_t start;
|
||
|
|
||
|
start = time(NULL);
|
||
|
|
||
|
do {
|
||
|
int err = poll(&pollfd, 1, 1000);
|
||
|
|
||
|
if (err > 0) {
|
||
|
handle_signalfd(daemon);
|
||
|
} else if (err < 0) {
|
||
|
perror("failed: poll\n");
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
if (start + secs < time(NULL))
|
||
|
return -1;
|
||
|
} while (session->pid != -1);
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static bool daemon__has_alive_session(struct daemon *daemon)
|
||
|
{
|
||
|
struct daemon_session *session;
|
||
|
|
||
|
list_for_each_entry(session, &daemon->sessions, list) {
|
||
|
if (session->pid != -1)
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
static int daemon__wait(struct daemon *daemon, int secs)
|
||
|
{
|
||
|
struct pollfd pollfd = {
|
||
|
.fd = daemon->signal_fd,
|
||
|
.events = POLLIN,
|
||
|
};
|
||
|
time_t start;
|
||
|
|
||
|
start = time(NULL);
|
||
|
|
||
|
do {
|
||
|
int err = poll(&pollfd, 1, 1000);
|
||
|
|
||
|
if (err > 0) {
|
||
|
handle_signalfd(daemon);
|
||
|
} else if (err < 0) {
|
||
|
perror("failed: poll\n");
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
if (start + secs < time(NULL))
|
||
|
return -1;
|
||
|
} while (daemon__has_alive_session(daemon));
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static int daemon_session__control(struct daemon_session *session,
|
||
|
const char *msg, bool do_ack)
|
||
|
{
|
||
|
struct pollfd pollfd = { .events = POLLIN, };
|
||
|
char control_path[PATH_MAX];
|
||
|
char ack_path[PATH_MAX];
|
||
|
int control, ack = -1, len;
|
||
|
char buf[20];
|
||
|
int ret = -1;
|
||
|
ssize_t err;
|
||
|
|
||
|
/* open the control file */
|
||
|
scnprintf(control_path, sizeof(control_path), "%s/%s",
|
||
|
session->base, SESSION_CONTROL);
|
||
|
|
||
|
control = open(control_path, O_WRONLY|O_NONBLOCK);
|
||
|
if (!control)
|
||
|
return -1;
|
||
|
|