diff --git a/src/meson.build b/src/meson.build index 903559ec..6b2bb828 100644 --- a/src/meson.build +++ b/src/meson.build @@ -27,8 +27,15 @@ if get_option('sysprofd') == 'bundled' or get_option('libsysprof') namespace: 'IpcLegacy', ) + ipc_agent_src = gnome.gdbus_codegen('ipc-agent', + sources: 'org.gnome.Sysprof.Agent.xml', + interface_prefix: 'org.gnome.Sysprof.', + namespace: 'Ipc', + ) + install_data(['org.gnome.Sysprof3.Service.xml', - 'org.gnome.Sysprof2.xml'], + 'org.gnome.Sysprof2.xml', + 'org.gnome.Sysprof.Agent.xml'], install_dir: join_paths(datadir, 'dbus-1/interfaces'), ) endif diff --git a/src/org.gnome.Sysprof.Agent.xml b/src/org.gnome.Sysprof.Agent.xml new file mode 100644 index 00000000..2a87bbc1 --- /dev/null +++ b/src/org.gnome.Sysprof.Agent.xml @@ -0,0 +1,36 @@ + + + + + + + + The signal number to deliver. + + + + + The log message to be displayed. + + + + diff --git a/src/tools/meson.build b/src/tools/meson.build index 9e9ae5f3..0f6536bb 100644 --- a/src/tools/meson.build +++ b/src/tools/meson.build @@ -12,6 +12,14 @@ if get_option('libsysprof') install_dir: get_option('bindir'), install: true, ) + + sysprof_agent = executable('sysprof-agent', + ['sysprof-agent.c', ipc_agent_src], + dependencies: [libsysprof_dep], + c_args: tools_cflags, + install_dir: get_option('bindir'), + install: true, + ) endif sysprof_cat = executable('sysprof-cat', 'sysprof-cat.c', diff --git a/src/tools/sysprof-agent.c b/src/tools/sysprof-agent.c new file mode 100644 index 00000000..0cb617ae --- /dev/null +++ b/src/tools/sysprof-agent.c @@ -0,0 +1,654 @@ +/* sysprof-agent.c + * + * Copyright 2022 Christian Hergert + * + * 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 . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "config.h" + +#include +#include +#include + +#include +#include +#include + +#include + +#include "ipc-agent.h" + +#define BUFFER_SIZE (4096L*16L) /* 64KB */ + +static gboolean forward_fd_func (const char *option_name, + const char *option_value, + gpointer data, + GError **error); + +static GMainLoop *main_loop; +static GSubprocess *subprocess; +static char *subprocess_ident; +static gboolean subprocess_finished; +static IpcAgent *service; +static int exit_code = EXIT_SUCCESS; +static int read_fd = -1; +static int write_fd = -1; +static int pty_fd = -1; +static char *directory; +static char *capture_filename; +static GArray *forward_fds; +static char **env; +static gboolean clear_env; +static gboolean aid_battery; +static gboolean aid_compositor; +static gboolean aid_cpu; +static gboolean aid_disk; +static gboolean aid_energy; +static gboolean aid_gjs; +static gboolean aid_memory; +static gboolean aid_memprof; +static gboolean aid_net; +static gboolean aid_perf; +static gboolean aid_tracefd; +static gboolean no_throttle; +static const GOptionEntry options[] = { + { "read-fd", 0, 0, G_OPTION_ARG_INT, &read_fd, "The read side of the FD to use for D-Bus" }, + { "write-fd", 0, 0, G_OPTION_ARG_INT, &write_fd, "The write side of the FD to use for D-Bus" }, + { "forward-fd", 0, 0, G_OPTION_ARG_CALLBACK, forward_fd_func, "The FD to forward to the subprocess" }, + { "directory", 0, 0, G_OPTION_ARG_FILENAME, &directory, "The directory to run spawn the subprocess from", "PATH" }, + { "capture", 0, 0, G_OPTION_ARG_FILENAME, &capture_filename, "The filename to save the sysprof capture to", "PATH" }, + { "clear-env", 0, 0, G_OPTION_ARG_NONE, &clear_env, "Clear environment instead of inheriting" }, + { "env", 0, 0, G_OPTION_ARG_STRING_ARRAY, &env, "Add an environment variable to the spawned process", "KEY=VALUE" }, + { "cpu", 0, 0, G_OPTION_ARG_NONE, &aid_cpu, "Track CPU usage and frequency" }, + { "gjs", 0, 0, G_OPTION_ARG_NONE, &aid_gjs, "Record stack traces within GJS" }, + { "perf", 0, 0, G_OPTION_ARG_NONE, &aid_perf, "Record stack traces with perf" }, + { "memory", 0, 0, G_OPTION_ARG_NONE, &aid_memory, "Record basic system memory usage" }, + { "memprof", 0, 0, G_OPTION_ARG_NONE, &aid_memprof, "Record stack traces during memory allocations" }, + { "disk", 0, 0, G_OPTION_ARG_NONE, &aid_disk, "Record disk usage information" }, + { "net", 0, 0, G_OPTION_ARG_NONE, &aid_net, "Record network usage information" }, + { "energy", 0, 0, G_OPTION_ARG_NONE, &aid_energy, "Record energy usage using RAPL" }, + { "battery", 0, 0, G_OPTION_ARG_NONE, &aid_battery, "Record battery charge and discharge rates" }, + { "compositor", 0, 0, G_OPTION_ARG_NONE, &aid_compositor, "Record GNOME Shell compositor information" }, + { "no-throttle", 0, 0, G_OPTION_ARG_NONE, &no_throttle, "Disable CPU throttling" }, + { "tracefd", 0, 0, G_OPTION_ARG_NONE, &aid_tracefd, "Provide TRACEFD to subprocess" }, + { NULL } +}; + +G_GNUC_PRINTF (1, 2) +static void +message (const char *format, + ...) +{ + g_autofree char *formatted = NULL; + va_list args; + + if (service == NULL) + return; + + va_start (args, format); + formatted = g_strdup_vprintf (format, args); + va_end (args); + + ipc_agent_emit_log (service, formatted); +} + +#define GBP_TYPE_SPAWN_SOURCE (gbp_spawn_source_get_type()) +G_DECLARE_FINAL_TYPE (GbpSpawnSource, gbp_spawn_source, GBP, SPAWN_SOURCE, GObject) + +struct _GbpSpawnSource +{ + GObject parent_instance; +}; + +static void +gbp_spawn_source_modify_spawn (SysprofSource *source, + SysprofSpawnable *spawnable) +{ + g_assert (GBP_IS_SPAWN_SOURCE (source)); + g_assert (SYSPROF_IS_SPAWNABLE (spawnable)); + + if (forward_fds == NULL) + return; + + for (guint i = 0; i < forward_fds->len; i++) + { + int fd = g_array_index (forward_fds, int, i); + sysprof_spawnable_take_fd (spawnable, dup (fd), fd); + } + + if (pty_fd != -1) + { + sysprof_spawnable_take_fd (spawnable, dup (pty_fd), STDIN_FILENO); + sysprof_spawnable_take_fd (spawnable, dup (pty_fd), STDOUT_FILENO); + sysprof_spawnable_take_fd (spawnable, dup (pty_fd), STDERR_FILENO); + } +} + +static void +gbp_spawn_source_start (SysprofSource *source) +{ + sysprof_source_emit_ready (source); +} + +static void +gbp_spawn_source_stop (SysprofSource *source) +{ + sysprof_source_emit_finished (source); +} + +static void +spawn_source_init (SysprofSourceInterface *iface) +{ + iface->modify_spawn = gbp_spawn_source_modify_spawn; + iface->start = gbp_spawn_source_start; + iface->stop = gbp_spawn_source_stop; +} + +G_DEFINE_FINAL_TYPE_WITH_CODE (GbpSpawnSource, gbp_spawn_source, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (SYSPROF_TYPE_SOURCE, spawn_source_init)) + +static void +gbp_spawn_source_class_init (GbpSpawnSourceClass *klass) +{ +} + +static void +gbp_spawn_source_init (GbpSpawnSource *self) +{ +} + +#define IPC_TYPE_AGENT_IMPL (ipc_agent_impl_get_type()) +G_DECLARE_FINAL_TYPE (IpcAgentImpl, ipc_agent_impl, IPC, SYPSROF_IMPL, IpcAgentSkeleton) + +struct _IpcAgentImpl +{ + IpcAgentSkeleton parent_instance; +}; + +static gboolean +handle_force_exit (IpcAgent *sysprof, + GDBusMethodInvocation *invocation) +{ + if (subprocess && !subprocess_finished) + g_subprocess_force_exit (subprocess); + + ipc_agent_complete_force_exit (sysprof, invocation); + + return TRUE; +} + +static gboolean +handle_send_signal (IpcAgent *sysprof, + GDBusMethodInvocation *invocation, + int signum) +{ + if (subprocess && !subprocess_finished) + g_subprocess_send_signal (subprocess, signum); + + ipc_agent_complete_send_signal (sysprof, invocation); + + return TRUE; +} + +static void +service_iface_init (IpcAgentIface *iface) +{ + iface->handle_force_exit = handle_force_exit; + iface->handle_send_signal = handle_send_signal; +} + +G_DEFINE_FINAL_TYPE_WITH_CODE (IpcAgentImpl, ipc_agent_impl, IPC_TYPE_AGENT_SKELETON, + G_IMPLEMENT_INTERFACE (IPC_TYPE_AGENT, service_iface_init)) + +static void +ipc_agent_impl_class_init (IpcAgentImplClass *klass) +{ +} + +static void +ipc_agent_impl_init (IpcAgentImpl *self) +{ +} + +static gboolean +forward_fd_func (const char *option_name, + const char *option_value, + gpointer data, + GError **error) +{ + int fd; + + if (forward_fds == NULL) + forward_fds = g_array_new (FALSE, FALSE, sizeof (int)); + + errno = 0; + + if (!(fd = atoi (option_value)) && errno != 0) + { + int errsv = errno; + g_set_error (error, + G_IO_ERROR, + g_io_error_from_errno (errsv), + "--forward-fd must contain a file-descriptor: %s", + g_strerror (errsv)); + return FALSE; + } + + if (fd < 0) + { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_INVAL, + "--forward-fd must be 0 or a positive integer"); + return FALSE; + } + + g_array_append_val (forward_fds, fd); + + return TRUE; +} + +G_GNUC_NULL_TERMINATED +static void +add_source (SysprofProfiler *profiler, + gboolean enabled, + GType source_type, + ...) +{ + g_autoptr(SysprofSource) source = NULL; + const char *first_property; + va_list args; + + g_assert (SYSPROF_IS_PROFILER (profiler)); + g_assert (g_type_is_a (source_type, SYSPROF_TYPE_SOURCE)); + + if (!enabled) + return; + + va_start (args, source_type); + first_property = va_arg (args, const char *); + if (first_property != NULL) + source = (SysprofSource *)g_object_new_valist (source_type, first_property, args); + else + source = g_object_new (source_type, NULL); + va_end (args); + + g_assert (!source || SYSPROF_IS_SOURCE (source)); + + if (source != NULL) + sysprof_profiler_add_source (profiler, source); + else + g_printerr ("Failed to create source of type \"%s\"\n", + g_type_name (source_type)); +} + +static void +profiler_failed_cb (SysprofProfiler *profiler, + const GError *error) +{ + g_assert (SYSPROF_IS_LOCAL_PROFILER (profiler)); + g_assert (error != NULL); + + g_printerr ("Profiling failed: %s", error->message); + exit_code = EXIT_FAILURE; + g_main_loop_quit (main_loop); +} + +static void +profiler_stopped_cb (SysprofProfiler *profiler) +{ + g_assert (SYSPROF_IS_LOCAL_PROFILER (profiler)); + + g_main_loop_quit (main_loop); +} + +static void +subprocess_spawned_cb (SysprofLocalProfiler *profiler, + GSubprocess *new_subprocess) +{ + g_assert (SYSPROF_IS_LOCAL_PROFILER (profiler)); + g_assert (G_IS_SUBPROCESS (new_subprocess)); + + g_set_object (&subprocess, new_subprocess); + + subprocess_ident = g_strdup (g_subprocess_get_identifier (subprocess)); + + message ("Created process %s", subprocess_ident); +} + +static void +subprocess_finished_cb (SysprofLocalProfiler *profiler, + GSubprocess *new_subprocess) +{ + g_assert (SYSPROF_IS_LOCAL_PROFILER (profiler)); + g_assert (G_IS_SUBPROCESS (new_subprocess)); + + subprocess_finished = TRUE; + + message ("Process %s exited", subprocess_ident); +} + +static void +split_argv (int argc, + char **argv, + int *our_argc, + char ***our_argv, + int *sub_argc, + char ***sub_argv) +{ + gboolean found_split = FALSE; + + *our_argc = 0; + *our_argv = g_new0 (char *, 1); + + *sub_argc = 0; + *sub_argv = g_new0 (char *, 1); + + for (int i = 0; i < argc; i++) + { + if (g_strcmp0 (argv[i], "--") == 0) + { + found_split = TRUE; + } + else if (found_split) + { + (*sub_argv) = g_realloc_n (*sub_argv, *sub_argc + 2, sizeof (char *)); + (*sub_argv)[*sub_argc] = g_strdup (argv[i]); + (*sub_argv)[*sub_argc+1] = NULL; + (*sub_argc)++; + } + else + { + (*our_argv) = g_realloc_n (*our_argv, *our_argc + 2, sizeof (char *)); + (*our_argv)[*our_argc] = g_strdup (argv[i]); + (*our_argv)[*our_argc+1] = NULL; + (*our_argc)++; + } + } +} + +static void +warn_error (GError **error) +{ + if (*error) + { + g_warning ("%s", (*error)->message); + g_clear_error (error); + } +} + +static GDBusConnection * +create_connection (GIOStream *stream, + GError **error) +{ + GDBusConnection *ret; + + g_assert (G_IS_IO_STREAM (stream)); + g_assert (main_loop != NULL); + g_assert (error != NULL); + + if ((ret = g_dbus_connection_new_sync (stream, NULL, + G_DBUS_CONNECTION_FLAGS_DELAY_MESSAGE_PROCESSING, + NULL, NULL, error))) + { + g_dbus_connection_set_exit_on_close (ret, FALSE); + g_signal_connect_swapped (ret, "closed", G_CALLBACK (g_main_loop_quit), main_loop); + } + + return ret; +} + +int +main (int argc, + char *argv[]) +{ + g_autoptr(SysprofCaptureWriter) writer = NULL; + g_autoptr(SysprofProfiler) profiler = NULL; + g_autoptr(GDBusConnection) connection = NULL; + g_autoptr(GDBusConnection) session_bus = NULL; + g_autoptr(GDBusConnection) system_bus = NULL; + g_autoptr(GOptionContext) context = NULL; + g_autoptr(GError) error = NULL; + g_auto(GStrv) our_argv = NULL; + g_auto(GStrv) sub_argv = NULL; + GMainContext *main_context; + int our_argc = -1; + int sub_argc = -1; + + sysprof_clock_init (); + + g_set_prgname ("sysprof-agent"); + g_set_application_name ("sysprof-agent"); + + /* Ignore SIGPIPE as we're using pipes to IPC */ + signal (SIGPIPE, SIG_IGN); + + /* Split argv into pre/post -- command split */ + split_argv (argc, argv, &our_argc, &our_argv, &sub_argc, &sub_argv); + g_assert (our_argc >= 0); + g_assert (sub_argc >= 0); + + /* Parse command line options pre -- */ + context = g_option_context_new ("-- COMMAND"); + g_option_context_add_main_entries (context, options, NULL); + if (!g_option_context_parse (context, &our_argc, &our_argv, &error)) + { + g_printerr ("%s\n", error->message); + return EXIT_FAILURE; + } + + /* Make sure we have a filename to capture to */ + if (capture_filename == NULL) + { + g_printerr ("You must provide --capture=PATH\n"); + return EXIT_FAILURE; + } + + /* Setup main loop, we'll need it going forward for things + * like async D-Bus, waiting for child processes, etc. + */ + main_loop = g_main_loop_new (NULL, FALSE); + + /* First spin up our bus connections */ + if (!(session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error))) + warn_error (&error); + if (!(system_bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, NULL))) + warn_error (&error); + + /* Now setup our private p2p D-Bus connection to the controller */ + if (read_fd != -1 || write_fd != -1) + { + g_autoptr(GIOStream) stream = NULL; + g_autoptr(GInputStream) in_stream = NULL; + g_autoptr(GOutputStream) out_stream = NULL; + + /* Both must be set, not just one side */ + if (read_fd == -1 || write_fd == -1) + { + g_printerr ("You must specify both --read-fd and --write-fd\n"); + return EXIT_FAILURE; + } + + /* We need these FDs non-blocking for async IO */ + if (!g_unix_set_fd_nonblocking (read_fd, TRUE, &error) || + !g_unix_set_fd_nonblocking (write_fd, TRUE, &error)) + { + g_printerr ("Failed to set FDs in nonblocking mode: %s\n", + error->message); + return EXIT_FAILURE; + } + + /* Create stream using FDs provided to us */ + in_stream = g_unix_input_stream_new (read_fd, FALSE); + out_stream = g_unix_output_stream_new (write_fd, FALSE); + stream = g_simple_io_stream_new (in_stream, out_stream); + + /* Create connection using our private stream from the controller */ + if (!(connection = create_connection (stream, &error))) + { + g_printerr ("Failed to setup P2P D-Bus connection: %s\n", + error->message); + return EXIT_FAILURE; + } + + /* Now export our service at "/" (but don't start processing messages + * until we start the profiler, further on. + */ + service = g_object_new (IPC_TYPE_AGENT_IMPL, NULL); + if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (service), + connection, "/", &error)) + { + g_printerr ("Failed to export service over D-Bus connection: %s", + error->message); + return EXIT_FAILURE; + } + } + + /* Now start setting up our profiler */ + profiler = sysprof_local_profiler_new (); + + /* We might not even know our real subprocess in the case we are going + * through another indirection layer like flatpak-spawn, so just assume + * we're profiling the entire system as that will be necessary to include + * the PID we really care about. + */ + sysprof_profiler_set_whole_system (profiler, TRUE); + + /* If -- was ommitted or there are no commands, just profile the entire + * system without spawning anything. Really only useful when testing the + * agent without a D-Bus service. + */ + if (sub_argc >= 0) + { + sysprof_profiler_set_spawn (profiler, TRUE); + sysprof_profiler_set_spawn_inherit_environ (profiler, !clear_env); + sysprof_profiler_set_spawn_argv (profiler, (const char * const *)sub_argv); + sysprof_profiler_set_spawn_env (profiler, (const char * const *)env); + + if (directory != NULL) + sysprof_profiler_set_spawn_cwd (profiler, directory); + } + + /* Now open the writer for our session */ + if (!(writer = sysprof_capture_writer_new (capture_filename, BUFFER_SIZE))) + { + int errsv = errno; + g_printerr ("Failed to open capture writer: %s\n", + g_strerror (errsv)); + return EXIT_FAILURE; + } + + /* Attach writer to the profiler */ + sysprof_profiler_set_writer (profiler, writer); + + /* Add all request sources */ + add_source (profiler, TRUE, GBP_TYPE_SPAWN_SOURCE, NULL); + add_source (profiler, TRUE, SYSPROF_TYPE_PROC_SOURCE, NULL); + add_source (profiler, TRUE, SYSPROF_TYPE_SYMBOLS_SOURCE, NULL); + add_source (profiler, aid_battery, SYSPROF_TYPE_BATTERY_SOURCE, NULL); + add_source (profiler, aid_compositor, SYSPROF_TYPE_PROXY_SOURCE, + "bus-type", G_BUS_TYPE_SESSION, + "bus-name", "org.gnome.Shell", + "object-path", "/org/gnome/Sysprof3/Profiler", + NULL); + add_source (profiler, aid_cpu, SYSPROF_TYPE_HOSTINFO_SOURCE, NULL); + add_source (profiler, aid_disk, SYSPROF_TYPE_DISKSTAT_SOURCE, NULL); + add_source (profiler, aid_energy, SYSPROF_TYPE_PROXY_SOURCE, + "bus-type", G_BUS_TYPE_SYSTEM, + "bus-name", "org.gnome.Sysprof3", + "object-path", "/org/gnome/Sysprof3/RAPL", + NULL); + add_source (profiler, aid_gjs, SYSPROF_TYPE_GJS_SOURCE, NULL); + add_source (profiler, aid_memory, SYSPROF_TYPE_MEMORY_SOURCE, NULL); + add_source (profiler, aid_memprof, SYSPROF_TYPE_MEMPROF_SOURCE, NULL); + add_source (profiler, aid_net, SYSPROF_TYPE_NETDEV_SOURCE, NULL); + add_source (profiler, aid_perf, SYSPROF_TYPE_PERF_SOURCE, NULL); + add_source (profiler, aid_tracefd, SYSPROF_TYPE_TRACEFD_SOURCE, + "envvar", "SYSPROF_TRACE_FD", + NULL); + add_source (profiler, no_throttle, SYSPROF_TYPE_GOVERNOR_SOURCE, + "disable-governor", TRUE, + NULL); + + /* Bail when we've failed or finished and track the subprocess + * so that we can deliver signals to it. + */ + g_signal_connect (profiler, + "failed", + G_CALLBACK (profiler_failed_cb), + NULL); + g_signal_connect (profiler, + "stopped", + G_CALLBACK (profiler_stopped_cb), + NULL); + g_signal_connect (profiler, + "subprocess-spawned", + G_CALLBACK (subprocess_spawned_cb), + NULL); + g_signal_connect (profiler, + "subprocess-finished", + G_CALLBACK (subprocess_finished_cb), + NULL); + + /* Start the profiler */ + sysprof_profiler_start (profiler); + + /* Now tell the connection to start processing messages that are + * delivered from the controller, or signals destined back. + */ + if (connection != NULL) + g_dbus_connection_start_message_processing (connection); + + /* Wait for profiler to finish */ + g_main_loop_run (main_loop); + + /* Notify that some more work needs to proceed */ + message ("Extracting callgraph symbols"); + + /* Let anything in-flight finish */ + main_context = g_main_loop_get_context (main_loop); + while (g_main_context_pending (main_context)) + g_main_context_iteration (main_context, FALSE); + + /* Now make sure our bits are on disk */ + sysprof_capture_writer_flush (writer); + + /* Try to exit the same way as the subprocess did to propagate that + * back into Builder who is watching *this* process. + */ + if (subprocess_finished) + { + g_assert (G_IS_SUBPROCESS (subprocess)); + g_assert (g_subprocess_get_if_exited (subprocess) || + g_subprocess_get_if_signaled (subprocess)); + + if (g_subprocess_get_if_signaled (subprocess)) + { + int signum = g_subprocess_get_term_sig (subprocess); + /* Try to exit in the same manner, or SIGKILL if that doesn't + * work, or just EXIT_FAILURE as last resort. + */ + raise (signum); + raise (SIGKILL); + return EXIT_FAILURE; + } + + exit_code = g_subprocess_get_exit_status (subprocess); + } + + return exit_code; +}