Files
sysprof/src/helpers.c
2021-02-24 17:57:03 -08:00

557 lines
14 KiB
C

/* helpers.c
*
* Copyright 2019 Christian Hergert <chergert@redhat.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#define G_LOG_DOMAIN "helpers"
#include "config.h"
#include <fcntl.h>
#include <gio/gio.h>
#include <errno.h>
#ifdef __linux__
# include <linux/perf_event.h>
# include <sys/syscall.h>
#endif
#include <string.h>
#include <unistd.h>
#include "helpers.h"
#ifdef __linux__
static gboolean
linux_list_processes (gint32 **processes,
gsize *n_processes)
{
g_autoptr(GDir) dir = NULL;
g_autoptr(GArray) pids = NULL;
const gchar *name;
if (!(dir = g_dir_open ("/proc/", 0, NULL)))
return FALSE;
pids = g_array_new (FALSE, FALSE, sizeof (gint32));
while ((name = g_dir_read_name (dir)))
{
if (g_ascii_isalnum (*name))
{
gchar *endptr = NULL;
gint64 val = g_ascii_strtoll (name, &endptr, 10);
if (endptr != NULL && *endptr == 0 && val < G_MAXINT && val >= 0)
{
gint32 v32 = val;
g_array_append_val (pids, v32);
}
}
}
*n_processes = pids->len;
*processes = (gint32 *)(gpointer)g_array_free (g_steal_pointer (&pids), FALSE);
return TRUE;
}
#endif
gboolean
helpers_list_processes (gint32 **processes,
gsize *n_processes)
{
g_return_val_if_fail (processes != NULL, FALSE);
g_return_val_if_fail (n_processes != NULL, FALSE);
*processes = NULL;
*n_processes = 0;
#ifdef __linux__
return linux_list_processes (processes, n_processes);
#else
return FALSE;
#endif
}
#ifdef __linux__
static int
_perf_event_open (struct perf_event_attr *attr,
pid_t pid,
int cpu,
int group_fd,
unsigned long flags)
{
g_assert (attr != NULL);
/* Quick sanity check */
if (attr->sample_period < 100000 && attr->type != PERF_TYPE_TRACEPOINT)
return -EINVAL;
return syscall (__NR_perf_event_open, attr, pid, cpu, group_fd, flags);
}
#endif
gboolean
helpers_perf_event_open (GVariant *options,
gint32 pid,
gint32 cpu,
gint group_fd,
guint64 flags,
gint *out_fd)
{
#ifndef __linux__
*out_fd = -1;
return FALSE;
#else
struct perf_event_attr attr = {0};
GVariantIter iter;
GVariant *value;
gchar *key;
gint32 disabled = 0;
gint32 wakeup_events = 149;
gint32 type = 0;
guint64 sample_period = 0;
guint64 sample_type = 0;
guint64 config = 0;
gint clockid = CLOCK_MONOTONIC;
gint comm = 0;
gint mmap_ = 0;
gint task = 0;
gint exclude_idle = 0;
gint use_clockid = 0;
gint sample_id_all = 0;
g_assert (out_fd != NULL);
*out_fd = -1;
g_variant_iter_init (&iter, options);
while (g_variant_iter_loop (&iter, "{sv}", &key, &value))
{
if (FALSE) {}
else if (strcmp (key, "disabled") == 0)
{
if (!g_variant_is_of_type (value, G_VARIANT_TYPE_BOOLEAN))
goto bad_arg;
disabled = g_variant_get_boolean (value);
}
else if (strcmp (key, "wakeup_events") == 0)
{
if (!g_variant_is_of_type (value, G_VARIANT_TYPE_UINT32))
goto bad_arg;
wakeup_events = g_variant_get_uint32 (value);
}
else if (strcmp (key, "sample_id_all") == 0)
{
if (!g_variant_is_of_type (value, G_VARIANT_TYPE_BOOLEAN))
goto bad_arg;
sample_id_all = g_variant_get_boolean (value);
}
else if (strcmp (key, "clockid") == 0)
{
if (!g_variant_is_of_type (value, G_VARIANT_TYPE_INT32))
goto bad_arg;
clockid = g_variant_get_int32 (value);
}
else if (strcmp (key, "comm") == 0)
{
if (!g_variant_is_of_type (value, G_VARIANT_TYPE_BOOLEAN))
goto bad_arg;
comm = g_variant_get_boolean (value);
}
else if (strcmp (key, "exclude_idle") == 0)
{
if (!g_variant_is_of_type (value, G_VARIANT_TYPE_BOOLEAN))
goto bad_arg;
exclude_idle = g_variant_get_boolean (value);
}
else if (strcmp (key, "mmap") == 0)
{
if (!g_variant_is_of_type (value, G_VARIANT_TYPE_BOOLEAN))
goto bad_arg;
mmap_ = g_variant_get_boolean (value);
}
else if (strcmp (key, "config") == 0)
{
if (!g_variant_is_of_type (value, G_VARIANT_TYPE_UINT64))
goto bad_arg;
config = g_variant_get_uint64 (value);
}
else if (strcmp (key, "sample_period") == 0)
{
if (!g_variant_is_of_type (value, G_VARIANT_TYPE_UINT64))
goto bad_arg;
sample_period = g_variant_get_uint64 (value);
}
else if (strcmp (key, "sample_type") == 0)
{
if (!g_variant_is_of_type (value, G_VARIANT_TYPE_UINT64))
goto bad_arg;
sample_type = g_variant_get_uint64 (value);
}
else if (strcmp (key, "task") == 0)
{
if (!g_variant_is_of_type (value, G_VARIANT_TYPE_BOOLEAN))
goto bad_arg;
task = g_variant_get_boolean (value);
}
else if (strcmp (key, "type") == 0)
{
if (!g_variant_is_of_type (value, G_VARIANT_TYPE_UINT32))
goto bad_arg;
type = g_variant_get_uint32 (value);
}
else if (strcmp (key, "use_clockid") == 0)
{
if (!g_variant_is_of_type (value, G_VARIANT_TYPE_BOOLEAN))
goto bad_arg;
use_clockid = g_variant_get_boolean (value);
}
continue;
bad_arg:
errno = EINVAL;
return FALSE;
}
attr.comm = !!comm;
attr.config = config;
attr.disabled = disabled;
attr.exclude_idle = !!exclude_idle;
attr.mmap = !!mmap_;
attr.sample_id_all = sample_id_all;
attr.sample_period = sample_period;
attr.sample_type = sample_type;
attr.task = !!task;
attr.type = type;
attr.wakeup_events = wakeup_events;
#ifdef HAVE_PERF_CLOCKID
if (!use_clockid || clockid < 0)
attr.clockid = CLOCK_MONOTONIC;
else
attr.clockid = clockid;
attr.use_clockid = use_clockid;
#endif
attr.size = sizeof attr;
errno = 0;
*out_fd = _perf_event_open (&attr, pid, cpu, group_fd, flags);
return *out_fd >= 0;
#endif
}
gboolean
helpers_get_proc_file (const gchar *path,
gchar **contents,
gsize *len)
{
g_autofree gchar *canon = NULL;
g_autoptr(GFile) file = NULL;
g_assert (path != NULL);
g_assert (contents != NULL);
g_assert (len != NULL);
*contents = NULL;
*len = 0;
/* Canonicalize filename */
file = g_file_new_for_path (path);
canon = g_file_get_path (file);
return g_file_is_native (file) &&
(g_str_has_prefix (canon, "/proc/") || g_str_has_prefix (canon, "/sys/")) &&
g_file_get_contents (canon, contents, len, NULL);
}
gboolean
helpers_get_proc_fd (const gchar *path,
gint *out_fd)
{
g_autofree gchar *canon = NULL;
g_autoptr(GFile) file = NULL;
g_assert (path != NULL);
g_assert (out_fd != NULL);
/* Canonicalize filename */
file = g_file_new_for_path (path);
canon = g_file_get_path (file);
return g_file_is_native (file) &&
(g_str_has_prefix (canon, "/proc/") || g_str_has_prefix (canon, "/sys/")) &&
-1 != (*out_fd = open (canon, O_RDONLY | O_CLOEXEC));
}
gboolean
helpers_can_see_pids (void)
{
g_autofree gchar *contents = NULL;
gsize len = 0;
if (g_file_test ("/.flatpak-info", G_FILE_TEST_EXISTS))
return FALSE;
if (helpers_get_proc_file ("/proc/mounts", &contents, &len))
{
g_auto(GStrv) lines = g_strsplit (contents, "\n", 0);
for (guint i = 0; lines[i]; i++)
{
if (!g_str_has_prefix (lines[i], "proc /proc "))
continue;
if (strstr (lines[i], "hidepid=") && !strstr (lines[i], "hidepid=0"))
return FALSE;
return TRUE;
}
}
return TRUE;
}
static void
helpers_list_processes_worker (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable)
{
g_autofree gint32 *processes = NULL;
gsize n_processes;
g_assert (G_IS_TASK (task));
g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
if (helpers_list_processes (&processes, &n_processes))
{
GArray *ar;
ar = g_array_new (FALSE, FALSE, sizeof (gint32));
g_array_append_vals (ar, processes, n_processes);
g_task_return_pointer (task,
g_steal_pointer (&ar),
(GDestroyNotify) g_array_unref);
return;
}
g_task_return_new_error (task,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"Failed to list processes");
}
void
helpers_list_processes_async (GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
g_autoptr(GTask) task = NULL;
g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
task = g_task_new (NULL, cancellable, callback, user_data);
g_task_set_source_tag (task, helpers_list_processes_async);
g_task_run_in_thread (task, helpers_list_processes_worker);
}
gboolean
helpers_list_processes_finish (GAsyncResult *result,
gint32 **processes,
gsize *n_processes,
GError **error)
{
g_autoptr(GArray) ret = NULL;
g_return_val_if_fail (G_IS_TASK (result), FALSE);
if ((ret = g_task_propagate_pointer (G_TASK (result), error)))
{
if (n_processes)
*n_processes = ret->len;
if (processes)
*processes = (gint32 *)(gpointer)g_array_free (ret, FALSE);
return TRUE;
}
else
{
if (processes)
*processes = NULL;
if (n_processes)
*n_processes = 0;
}
return FALSE;
}
static gboolean
needs_escape (const gchar *str)
{
for (; *str; str++)
{
if (g_ascii_isspace (*str) || *str == '\'' || *str == '"')
return TRUE;
}
return FALSE;
}
static void
postprocess_cmdline (gchar **str,
gsize len)
{
g_autoptr(GPtrArray) parts = g_ptr_array_new_with_free_func (g_free);
g_autofree gchar *instr = NULL;
const gchar *begin = NULL;
if (len == 0)
return;
instr = *str;
for (gsize i = 0; i < len; i++)
{
if (!begin && instr[i])
{
begin = &instr[i];
}
else if (begin && instr[i] == '\0')
{
if (needs_escape (begin))
g_ptr_array_add (parts, g_shell_quote (begin));
else
g_ptr_array_add (parts, g_strdup (begin));
begin = NULL;
}
}
/* If the last byte was not \0, as can happen with prctl(), then we need
* to add it here manually.
*/
if (begin)
{
if (needs_escape (begin))
g_ptr_array_add (parts, g_shell_quote (begin));
else
g_ptr_array_add (parts, g_strdup (begin));
}
g_ptr_array_add (parts, NULL);
*str = g_strjoinv (" ", (gchar **)parts->pdata);
}
static void
postprocess_rstrip (gchar **str,
gsize len)
{
g_strchomp (*str);
}
static void
add_pid_proc_file_to (gint pid,
const gchar *name,
GVariantDict *dict,
void (*postprocess) (gchar **, gsize))
{
g_autofree gchar *path = NULL;
g_autofree gchar *contents = NULL;
gsize len;
g_assert (pid > -1);
g_assert (name != NULL);
g_assert (dict != NULL);
path = g_strdup_printf ("/proc/%d/%s", pid, name);
if (g_file_get_contents (path, &contents, &len, NULL))
{
if (postprocess)
postprocess (&contents, len);
g_variant_dict_insert (dict, name, "s", contents);
}
}
GVariant *
helpers_get_process_info (const gchar *attributes)
{
GVariantBuilder builder;
g_autofree gint *processes = NULL;
gsize n_processes = 0;
gboolean want_statm;
gboolean want_cmdline;
gboolean want_comm;
gboolean want_maps;
gboolean want_mountinfo;
gboolean want_cgroup;
if (attributes == NULL)
attributes = "";
want_statm = !!strstr (attributes, "statm");
want_cmdline = !!strstr (attributes, "cmdline");
want_maps = !!strstr (attributes, "maps");
want_mountinfo = !!strstr (attributes, "mountinfo");
want_comm = !!strstr (attributes, "comm");
want_cgroup = !!strstr (attributes, "cgroup");
g_variant_builder_init (&builder, G_VARIANT_TYPE ("aa{sv}"));
if (helpers_list_processes (&processes, &n_processes))
{
for (guint i = 0; i < n_processes; i++)
{
gint pid = processes[i];
GVariantDict dict;
g_variant_dict_init (&dict, NULL);
g_variant_dict_insert (&dict, "pid", "i", pid, NULL);
if (want_statm)
add_pid_proc_file_to (pid, "statm", &dict, postprocess_rstrip);
if (want_cmdline)
add_pid_proc_file_to (pid, "cmdline", &dict, postprocess_cmdline);
if (want_comm)
add_pid_proc_file_to (pid, "comm", &dict, postprocess_rstrip);
if (want_maps)
add_pid_proc_file_to (pid, "maps", &dict, postprocess_rstrip);
if (want_mountinfo)
add_pid_proc_file_to (pid, "mountinfo", &dict, postprocess_rstrip);
if (want_cgroup)
add_pid_proc_file_to (pid, "cgroup", &dict, postprocess_rstrip);
g_variant_builder_add_value (&builder, g_variant_dict_end (&dict));
}
}
return g_variant_take_ref (g_variant_builder_end (&builder));
}