/* * \/\/\ * * Copyright (C) 2012-2018 Vito Caputo - * * 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 . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "charts.h" #include "util.h" #include "xserver.h" /* vmon exposes the monitoring charts to the shell in an strace-like cli */ typedef struct vmon_t { vwm_xserver_t *xserver; vwm_charts_t *charts; vwm_chart_t *chart; Atom wm_protocols_atom; Atom wm_delete_atom; Window window; int width, height; Picture picture; int pid; int done; int linger; time_t start_time; int snapshot; char *output_dir; char *name; unsigned n_snapshots; } vmon_t; /* these are fairly arbitrarily chosen to be sane */ #define WIDTH_DEFAULT 800 #define HEIGHT_DEFAULT 600 #define WIDTH_MIN 200 #define HEIGHT_MIN 28 static volatile int got_sigchld; /* return if arg == flag or altflag if provided */ static int is_flag(const char *arg, const char *flag, const char *altflag) { assert(arg); assert(flag); if (!strcmp(arg, flag) || (altflag && !strcmp(arg, altflag))) return 1; return 0; } /* parse integer out of opt, stores parsed opt in *res on success */ static int parse_flag_int(char * const *flag, char * const *end, char * const *opt, int min, int max, int *res) { long int num; char *endp; assert(flag); assert(end); assert(opt); assert(res); if (flag == end || **opt == '\0') { VWM_ERROR("flag \"%s\" expects an integer argument", *flag); return 0; } errno = 0; num = strtol(*opt, &endp, 10); if (errno) { VWM_PERROR("flag \"%s\" integer argument parse error, got \"%s\"", *flag, *opt); return 0; } if (*endp != '\0') { VWM_ERROR("flag \"%s\" integer argument invalid at \"%s\"", *flag, endp); return 0; } if (num < min) { VWM_ERROR("flag \"%s\" integer argument must be >= %i, got \"%s\"", *flag, min, *opt); return 0; } if (num > max) { VWM_ERROR("flag \"%s\" integer argument must be <= %i, got \"%s\"", *flag, max, *opt); return 0; } *res = num; return 1; } /* parse string out of opt, stores parsed opt in newly allocated *res on success */ static int parse_flag_str(char * const *flag, char * const *end, char * const *opt, int min_len, char **res) { char *tmp; assert(flag); assert(end); assert(opt); assert(res); if (flag == end || (!min_len || **opt == '\0')) { VWM_ERROR("flag \"%s\" expects an argument", *flag); return 0; } if (strlen(*opt) < min_len) { VWM_ERROR("flag \"%s\" argument must be longer than %i, got \"%s\"", *flag, min_len, *opt); return 0; } tmp = strdup(*opt); if (!tmp) { VWM_ERROR("unable to duplicate argument \"%s\"", *opt); return 0; } free(*res); *res = tmp; return 1; } /* set vmon->{width,height} to fullscreen dimensions */ static int set_fullscreen(vmon_t *vmon) { XWindowAttributes wattr; assert(vmon); assert(vmon->xserver); if (!XGetWindowAttributes(vmon->xserver->display, XSERVER_XROOT(vmon->xserver), &wattr)) { VWM_ERROR("unable to get root window attributes"); return 0; } vmon->width = wattr.width; vmon->height = wattr.height; return 1; } /* print commandline help */ static void print_help(void) { puts( "\n" "-----------------------------------------------------------------------------\n" " Flag Description\n" "-----------------------------------------------------------------------------\n" " -- Sentinel, subsequent arguments form command to execute\n" " -f --fullscreen Fullscreen window\n" " -h --help Show this help\n" " -n --name Name of chart, shows in window title and output filenames\n" " -l --linger Don't exit when top-level process exits\n" " -p --pid PID of the top-level process to monitor (1 if unspecified)\n" " -x --width Window width\n" " -y --height Window height\n" " -z --hertz Sample rate in hertz\n" " -o --output-dir Directory to store saved output to (\".\" if unspecified)\n" " -s --snapshot Save a PNG snapshot upon receiving SIGCHLD\n" "-----------------------------------------------------------------------------" ); } /* print copyright */ static void print_copyright(void) { puts( "\n" "Copyright (C) 2012-2018 Vito Caputo \n" "\n" "This program comes with ABSOLUTELY NO WARRANTY. This is free software, and\n" "you are welcome to redistribute it under certain conditions. For details\n" "please see the LICENSE file included with this program." ); } /* collect status of child */ static void handle_sigchld(int signum) { got_sigchld = 1; } /* parse and apply argv, implementing an strace-like cli, mutates vmon. */ static int vmon_handle_argv(vmon_t *vmon, int argc, char * const argv[]) { char *const*end = &argv[argc - 1], *const*last = argv; assert(vmon); assert(!vmon->pid); assert(argc > 0); assert(argv); for (argv++; argv <= end; argv++) { if (is_flag(*argv, "-p", "--pid")) { if (vmon->pid) { VWM_ERROR("--pid may only be specified once currently (TODO)"); return 0; } if (!parse_flag_int(argv, end, argv + 1, 0, INT_MAX, &vmon->pid)) return 0; last = ++argv; } else if (is_flag(*argv, "-x", "--width")) { if (!parse_flag_int(argv, end, argv + 1, WIDTH_MIN, INT_MAX, &vmon->width)) return 0; last = ++argv; } else if (is_flag(*argv, "-y", "--height")) { if (!parse_flag_int(argv, end, argv + 1, HEIGHT_MIN, INT_MAX, &vmon->height)) return 0; last = ++argv; } else if (is_flag(*argv, "-o", "--output-dir")) { if (!parse_flag_str(argv, end, argv + 1, 1, &vmon->output_dir)) return 0; last = ++argv; } else if (is_flag(*argv, "-n", "--name")) { if (!parse_flag_str(argv, end, argv + 1, 1, &vmon->name)) return 0; last = ++argv; } else if (is_flag(*argv, "-s", "--snapshot")) { vmon->snapshot = 1; last = argv; } else if (is_flag(*argv, "-f", "--fullscreen")) { if (!set_fullscreen(vmon)) { VWM_ERROR("unable to set fullscreen dimensions"); return 0; } last = argv; } else if (is_flag(*argv, "-l", "--linger")) { vmon->linger = 1; last = argv; } else if (is_flag(*argv, "--", NULL)) { /* stop looking for more flags, anything remaining will be used as a command. */ last = argv; break; } else if (is_flag(*argv, "-z", "--hertz")) { int hertz; if (!parse_flag_int(argv, end, argv + 1, 1, 1000, &hertz)) return 0; vwm_charts_rate_set(vmon->charts, hertz); last = ++argv; } else if (is_flag(*argv, "-h", "--help")) { print_help(); exit(EXIT_SUCCESS); } else { /* stop looking for more flags on first unrecognized thing, assume we're in command territory now */ break; } } /* if more argv remains, treat as a command to run */ if (last != end) { pid_t pid; if (vmon->pid) { VWM_ERROR("combining --pid with a command to run is not yet supported (TODO)\n"); return 0; } last++; if (signal(SIGCHLD, handle_sigchld) == SIG_ERR) { VWM_PERROR("unable to set SIGCHLD handler"); return 0; } pid = fork(); if (pid == -1) { VWM_PERROR("unable to fork"); return 0; } if (pid == 0) { if (prctl(PR_SET_PDEATHSIG, SIGKILL) == -1) { VWM_PERROR("unable to prctl(PR_SET_PDEATHSIG, SIGKILL)"); exit(EXIT_FAILURE); } /* TODO: would be better to synchronize with the monitoring loop starting before the execvp() occurs, * very early program start can be an interesting thing to observe. */ if (execvp(*last, last) == -1) { VWM_PERROR("unable to exec \"%s\"", *last); exit(EXIT_FAILURE); } } vmon->pid = pid; } /* default to PID 1 when no PID or command was supplied */ if (!vmon->pid) vmon->pid = 1; return 1; } /* parse argv, connect to X, create window, attach libvmon to monitored process */ static vmon_t * vmon_startup(int argc, char * const argv[]) { vmon_t *vmon; XRenderPictureAttributes pattr = {}; XWindowAttributes wattr; assert(argv); print_copyright(); vmon = calloc(1, sizeof(vmon_t)); if (!vmon) { VWM_PERROR("unable to allocate vmon_t"); goto _err; } vmon->start_time = time(NULL); vmon->width = WIDTH_DEFAULT; vmon->height = HEIGHT_DEFAULT; vmon->output_dir = strdup("."); if (!vmon->output_dir) { VWM_ERROR("unable to alloc output dir"); goto _err_free; } vmon->xserver = vwm_xserver_open(); if (!vmon->xserver) { VWM_ERROR("unable to open xserver"); goto _err_free; } vmon->wm_delete_atom = XInternAtom(vmon->xserver->display, "WM_DELETE_WINDOW", False); vmon->wm_protocols_atom = XInternAtom(vmon->xserver->display, "WM_PROTOCOLS", False); vmon->charts = vwm_charts_create(vmon->xserver); if (!vmon->charts) { VWM_ERROR("unable to create charts instance"); goto _err_xserver; } if (!vmon_handle_argv(vmon, argc, argv)) { VWM_ERROR("unable to handle arguments"); goto _err_charts; } vmon->chart = vwm_chart_create(vmon->charts, vmon->pid, vmon->width, vmon->height, vmon->name); if (!vmon->chart) { VWM_ERROR("unable to create chart"); goto _err_charts; } vmon->window = XCreateSimpleWindow(vmon->xserver->display, XSERVER_XROOT(vmon->xserver), 0, 0, vmon->width, vmon->height, 1, 0, 0); if (vmon->name) XStoreName(vmon->xserver->display, vmon->window, vmon->name); XGetWindowAttributes(vmon->xserver->display, vmon->window, &wattr); vmon->picture = XRenderCreatePicture(vmon->xserver->display, vmon->window, XRenderFindVisualFormat(vmon->xserver->display, wattr.visual), 0, &pattr); XMapWindow(vmon->xserver->display, vmon->window); XSelectInput(vmon->xserver->display, vmon->window, StructureNotifyMask|ExposureMask); return vmon; _err_charts: vwm_charts_destroy(vmon->charts); _err_xserver: vwm_xserver_close(vmon->xserver); _err_free: free(vmon); _err: return NULL; } /* tear everything down */ static void vmon_shutdown(vmon_t *vmon) { assert(vmon); assert(vmon->xserver); XDestroyWindow(vmon->xserver->display, vmon->window); vwm_chart_destroy(vmon->charts, vmon->chart); vwm_charts_destroy(vmon->charts); vwm_xserver_close(vmon->xserver); free(vmon); } /* resize the chart (in response to configure events) */ static void vmon_resize(vmon_t *vmon, int width, int height) { assert(vmon); vmon->width = width; vmon->height = height; vwm_chart_set_visible_size(vmon->charts, vmon->chart, width, height); } static int vmon_snapshot_as_png(vmon_t *vmon, FILE *output) { XImage *chart_as_ximage; png_bytepp row_pointers; png_infop info_ctx; png_structp png_ctx; assert(vmon); assert(output); vwm_chart_render_as_ximage(vmon->charts, vmon->chart, NULL, &chart_as_ximage); row_pointers = malloc(sizeof(void *) * chart_as_ximage->height); if (!row_pointers) { XDestroyImage(chart_as_ximage); return -ENOMEM; } for (unsigned i = 0; i < chart_as_ximage->height; i++) row_pointers[i] = &((png_byte *)chart_as_ximage->data)[i * chart_as_ximage->bytes_per_line]; png_ctx = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); if (!png_ctx) { free(row_pointers); XDestroyImage(chart_as_ximage); return -ENOMEM; } info_ctx = png_create_info_struct(png_ctx); if (!info_ctx) { png_destroy_write_struct(&png_ctx, NULL); XDestroyImage(chart_as_ximage); free(row_pointers); return -ENOMEM; } png_init_io(png_ctx, output); if (setjmp(png_jmpbuf(png_ctx)) != 0) { png_destroy_write_struct(&png_ctx, &info_ctx); XDestroyImage(chart_as_ximage); free(row_pointers); return -ENOMEM; } /* XXX: I'm sure this is making flawed assumptions about the color format * and type etc, but this makes it work for me and that's Good Enough for now. * One can easily turn runtime mapping of X color formats, endianness, and packing * details to whatever a file format like PNG can express into a tar-filled rabbithole * of fruitless wankery. */ png_set_bgr(png_ctx); png_set_IHDR(png_ctx, info_ctx, chart_as_ximage->width, chart_as_ximage->height, 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); png_write_info(png_ctx, info_ctx); png_write_image(png_ctx, row_pointers); png_write_end(png_ctx, NULL); XDestroyImage(chart_as_ximage); free(row_pointers); return 0; } static int vmon_snapshot(vmon_t *vmon) { struct tm *start_time; char start_str[32]; char path[4096]; char name[200] = {}; FILE *output; int r; assert(vmon); if (vmon->name) { for (int i = 0; i < sizeof(name) - 1 && vmon->name[i]; i++) { char c = vmon->name[i]; switch (c) { /* replace characters relevant to path interpolation */ case '/': c = '\\'; break; case '.': /* no leading dot, and no ".." */ if (i == 0 || (i == 1 && !vmon->name[2])) c = '_'; break; default: } name[i] = c; } } if (mkdir(vmon->output_dir, 0755) == -1 && errno != EEXIST) return -errno; start_time = localtime(&vmon->start_time); strftime(start_str, sizeof(start_str), "%m.%d.%y-%T", start_time); snprintf(path, sizeof(path), "%s/%s%s%s-%u.png", vmon->output_dir, name, vmon->name ? "-" : "", start_str, vmon->n_snapshots++); output = fopen(path, "w+"); if (!output) return -errno; r = vmon_snapshot_as_png(vmon, output); if (r < 0) { fclose(output); return r; } fsync(fileno(output)); fclose(output); return 0; } /* handle the next X event, may block */ static void vmon_process_event(vmon_t *vmon) { XEvent ev; assert(vmon); XNextEvent(vmon->xserver->display, &ev); switch (ev.type) { case ConfigureNotify: vmon_resize(vmon, ev.xconfigure.width, ev.xconfigure.height); vwm_chart_compose(vmon->charts, vmon->chart, NULL); vwm_chart_render(vmon->charts, vmon->chart, PictOpSrc, vmon->picture, 0, 0, vmon->width, vmon->height); break; case Expose: vwm_chart_render(vmon->charts, vmon->chart, PictOpSrc, vmon->picture, 0, 0, vmon->width, vmon->height); break; case ClientMessage: if (ev.xclient.message_type != vmon->wm_protocols_atom) break; if (ev.xclient.data.l[0] != vmon->wm_delete_atom) break; vmon->done = 1; break; default: VWM_TRACE("unhandled event: %x\n", ev.type); } } int main(int argc, char * const argv[]) { vmon_t *vmon; struct pollfd pfd = { .events = POLLIN }; int ret = EXIT_SUCCESS; vmon = vmon_startup(argc, argv); if (!vmon) { VWM_ERROR("error starting vmon"); return EXIT_FAILURE; } pfd.fd = ConnectionNumber(vmon->xserver->display); while (!vmon->done) { int delay; if (vwm_charts_update(vmon->charts, &delay)) { vwm_chart_compose(vmon->charts, vmon->chart, NULL); vwm_chart_render(vmon->charts, vmon->chart, PictOpSrc, vmon->picture, 0, 0, vmon->width, vmon->height); } if (XPending(vmon->xserver->display) || poll(&pfd, 1, delay) > 0) vmon_process_event(vmon); if (got_sigchld) { int status; if (vmon_snapshot(vmon) < 0) VWM_ERROR("error saving snapshot"); got_sigchld = 0; wait(&status); if (WIFEXITED(status)) { ret = WEXITSTATUS(status); if (!vmon->linger) vmon->done = 1; } } } vmon_shutdown(vmon); return ret; }