summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-07-13 11:57:27 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-07-13 11:57:27 +0000
commit154547056ca1ae074c68a9a5aa15925d082f9482 (patch)
tree22f6ffc4c586c5bb9e6c03b1b92c8db5a52943f2 /src
parentInitial commit. (diff)
download0xtools-154547056ca1ae074c68a9a5aa15925d082f9482.tar.xz
0xtools-154547056ca1ae074c68a9a5aa15925d082f9482.zip
Adding upstream version 2.0.3.upstream/2.0.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src')
-rw-r--r--src/xcapture.c464
1 files changed, 464 insertions, 0 deletions
diff --git a/src/xcapture.c b/src/xcapture.c
new file mode 100644
index 0000000..65cb2e4
--- /dev/null
+++ b/src/xcapture.c
@@ -0,0 +1,464 @@
+/*
+ * 0x.Tools xCapture - sample thread activity from Linux procfs [https://0x.tools]
+ * Copyright 2019-2021 Tanel Poder
+ *
+ * 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 2 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, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ */
+
+#define XCAP_VERSION "1.2.6"
+
+#define _GNU_SOURCE
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <assert.h>
+#include <string.h>
+#include <errno.h>
+#include <time.h>
+#include <sys/time.h>
+#include <dirent.h>
+#include <fcntl.h>
+#include <asm/unistd.h>
+#include <linux/limits.h>
+#include <pwd.h>
+#include <sys/stat.h>
+#include <ctype.h>
+#include <unistd.h>
+
+#include <syscall_names.h>
+
+#define WSP " \n" // whitespace
+#define MAXFILEBUF 4096
+
+int DEBUG = 0;
+
+char filebuf[MAXFILEBUF]; // filebuf global temp area by design
+char statbuf[MAXFILEBUF]; // filebuf global temp area by design (special for /proc/PID/stat value passing optimization)
+char exclude_states[10] = "XZIS"; // do not show tasks in Sleeping state by default
+
+char *output_dir = NULL; // use stdout if output_dir is not set
+int header_printed = 0;
+char output_format = 'S'; // S -> space-delimited fixed output format, C -> CSV
+char outsep = ' ';
+int pad = 1; // output field padding (for space-delimited fixed-width output)
+
+const char *getusername(uid_t uid)
+{
+ struct passwd *pw = getpwuid(uid);
+ if (pw)
+ {
+ return pw->pw_name;
+ }
+
+ return "-";
+}
+
+
+int readfile(int pid, int tid, const char *name, char *buf)
+{
+ int fd, bytes = 0;
+ char path[256];
+
+ tid ? sprintf(path, "/proc/%d/task/%d/%s", pid, tid, name) : sprintf(path, "/proc/%d/%s", pid, name);
+
+ fd = open(path, O_RDONLY);
+ if (fd == -1) {
+ if (DEBUG) fprintf(stderr, "error opening file %s\n", path);
+ return -1;
+ }
+
+ bytes = read(fd, buf, MAXFILEBUF);
+ close(fd);
+
+ // handle errors, empty records and missing string terminators in input
+ assert(bytes >= -1);
+ switch (bytes) {
+ case -1:
+ if (DEBUG) fprintf(stderr, "read(%s) returned %d\n", path, bytes);
+ buf[bytes] = '-';
+ buf[bytes + 1] = 0;
+ bytes = 2;
+ break;
+ case 0:
+ buf[bytes] = '-';
+ buf[bytes + 1] = 0;
+ bytes = 2;
+ break;
+ case 1:
+ buf[bytes] = 0;
+ bytes = 2;
+ break;
+ default: // bytes >= 2
+ if (bytes < MAXFILEBUF)
+ buf[bytes] = 0;
+ else
+ buf[MAXFILEBUF-1] = 0;
+ }
+ return bytes;
+}
+
+int outputstack(char *str) {
+ int i;
+
+ // find the end and start of function name in the stack
+ // example input lines (different number of fields):
+ // [<ffffffff8528428c>] vfs_read+0x8c/0x130
+ // [<ffffffffc03b03f4>] xfs_file_fsync+0x224/0x240 [xfs]
+ for (i=strlen(str)-1; i>=0; i--) {
+ if (str[i] == '+') str[i] = '\0';
+ if (str[i] == ' ' && str[i-1] == ']') { // ignore spaces _after_ the function name
+ if (strcmp(str+i+1, "entry_SYSCALL_64_after_hwframe") &&
+ strcmp(str+i+1, "do_syscall_64") &&
+ strcmp(str+i+1, "0xffffffffffffffff\n")
+ ) {
+ fprintf(stdout, "->%s()", str+i+1);
+ }
+ }
+ }
+ return 0;
+}
+
+// this function changes the input str (tokenizes it in place)
+int outputfields(char *str, char *mask, char *sep) {
+ int i;
+ char *field, *pos;
+
+ // special case for stack trace handling, we don't want to split the input string before calling outputstack()
+ if (mask[0] == 't')
+ return outputstack(str);
+
+ for (i=0; i<strlen(mask); i++) {
+ if ((field = strsep(&str, sep)) != NULL) {
+ switch (mask[i]) {
+ case '.': // skip field
+ break;
+ case 'e': // extract Executable file name from full path
+ pos = strrchr(field, '/');
+ if (pos)
+ fprintf(stdout, "%s%c", pos, outsep);
+ else
+ fprintf(stdout, "%s%c", field, outsep);
+ break;
+ case 'E': // same as above, but wider output
+ pos = strrchr(field, '/');
+ if (pos)
+ fprintf(stdout, pad ? "%-20s%c" : "%s%c", pos+1, outsep);
+ else
+ fprintf(stdout, pad ? "%-20s%c" : "%s%c", field, outsep);
+ break;
+ case 'o': // just output string as is
+ fprintf(stdout, "%s%c", field, outsep);
+ break;
+ case 'O': // just output string as is, padded to 30 chars
+ fprintf(stdout, pad ? "%-30s%c" : "%s%c", field, outsep);
+ break;
+ case 'x': // print in hex
+ fprintf(stdout, pad ? "0x%llx " : "0x%llx%c", atoll(field), outsep);
+ break;
+ case 's': // convert syscall number to name, the input starts with either:
+ // >= 0 (syscall), -1 (in kernel without syscall) or 'running' (likely userspace)
+ fprintf(stdout, "%s%c", field[0]=='r' ? "[running]" : field[0]=='-' ? "[no_syscall]" : sysent0[atoi(field)].name, outsep);
+ break;
+ case 'S': // same as above, but wider output
+ fprintf(stdout, pad ? "%-30s%c" : "%s%c", field[0]=='r' ? "[running]" : field[0]=='-' ? "[no_syscall]" : sysent0[atoi(field)].name, outsep);
+ break;
+ case 't': // we shouldn't get here thanks to the if statement above
+ break;
+ default:
+ fprintf(stderr, "Error: Wrong char '%c' in mask %s\n", mask[i], mask);
+ exit(1);
+ }
+ }
+ else break;
+ }
+
+ return i;
+}
+
+// currently a fixed string, will make this dynamic together with command line option support
+int outputheader(char *add_columns) {
+
+ fprintf(stdout, pad ? "%-23s %7s %7s %-16s %-2s %-30s %-30s %-30s" : "%s,%s,%s,%s,%s,%s,%s,%s",
+ output_dir ? "TS" : "DATE TIME", "PID", "TID", "USERNAME", "ST", "COMMAND", "SYSCALL", "WCHAN");
+ if (strcasestr(add_columns, "exe")) fprintf(stdout, pad ? " %-20s" : ",%s", "EXE");
+ if (strcasestr(add_columns, "nspid")) fprintf(stdout, pad ? " %12s" : ",%s", "NSPID");
+ if (strcasestr(add_columns, "cmdline")) fprintf(stdout, pad ? " %-30s" : ",%s", "CMDLINE");
+ if (strcasestr(add_columns, "kstack")) fprintf(stdout, pad ? " %s" : ",%s", "KSTACK");
+ fprintf(stdout, "\n");
+ return 1;
+}
+
+// partial entry happens when /proc/PID/stat disappears before we manage to read it
+void outputprocpartial(int pid, int tid, char *sampletime, uid_t proc_uid, long nspid, char *add_columns, char *message) {
+
+ header_printed = header_printed ? 1 : outputheader(add_columns);
+
+ fprintf(stdout, pad ? "%-23s %7d %7d %-16s %-2c %-30s %-30s %-30s" : "%s,%d,%d,%s,%c,%s,%s,%s",
+ sampletime, pid, tid, getusername(proc_uid), '-', message, "-", "-");
+
+ if (strcasestr(add_columns, "exe")) fprintf(stdout, pad ? " %-20s" : ",%s", "-");
+ if (strcasestr(add_columns, "nspid")) fprintf(stdout, pad ? " %12s" : ",%s", "-");
+ if (strcasestr(add_columns, "cmdline")) fprintf(stdout, pad ? " %-30s" : ",%s", "-");
+ if (strcasestr(add_columns, "kstack")) fprintf(stdout, pad ? " %s" : ",%s", "-");
+ fprintf(stdout, "\n");
+}
+
+int outputprocentry(int pid, int tid, char *sampletime, uid_t proc_uid, long nspid, char *add_columns) {
+
+ int b;
+ char task_status; // used for early bailout, filtering by task status
+ char sympath[64];
+ char *fieldend;
+
+ // if printing out only the /proc/PID entry (not TID), then we have just read the relevant stat file into filebuf
+ // in the calling function. this callflow-dependent optimization avoids an 'expensive' /proc/PID/stat read
+ b = tid ? readfile(pid, tid, "stat", statbuf) : strlen(statbuf);
+ fieldend = strstr(statbuf, ") ");
+
+ if (b > 0 && fieldend) { // the 1st field end "not null" check is due to /proc not having read consistency (rarely in-flux values are shown as \0\0\0\0\0\0\0...
+
+ // this task_status check operation has to come before any outputfields() calls as they modify filebuf global var
+ task_status = *(fieldend + 2); // find where the 3rd field - after a ")" starts
+
+ if (!strchr(exclude_states, task_status)) { // task status is not in X,Z,I (S)
+
+ // only print header (in stdout mode) when there are any samples to report
+ header_printed = header_printed ? 1 : outputheader(add_columns);
+
+ fprintf(stdout, pad ? "%-23s %7d %7d %-16s %-2c " : "%s,%d,%d,%s,%c,", sampletime, pid, tid, getusername(proc_uid), task_status);
+ outputfields(statbuf, ".O", WSP); // .O......x for PF_ flags
+
+ b = readfile(pid, tid, "syscall", filebuf);
+ if (b > 0) { outputfields(filebuf, "S", WSP); } else { fprintf(stdout, pad ? "%-30s " : "%s,", "-"); }
+
+ b = readfile(pid, tid, "wchan", filebuf);
+ if (b > 0) { outputfields(filebuf, "O", ". \n"); } else { fprintf(stdout, pad ? "%-30s " : "%s,", "-"); }
+
+ if (strcasestr(add_columns, "exe")) {
+ tid ? sprintf(sympath, "/proc/%d/task/%d/exe", pid, tid) : sprintf(sympath, "/proc/%d/exe", pid);
+ b = readlink(sympath, filebuf, PATH_MAX);
+ if (b > 0) { filebuf[b] = 0 ; outputfields(filebuf, "E", WSP); } else { fprintf(stdout, pad ? "%-20s " : "%s,", "-"); }
+ }
+
+ if (strcasestr(add_columns, "nspid")) {
+ fprintf(stdout, pad ? "%12ld%c" : "%ld%c", nspid, outsep);
+ }
+
+ if (strcasestr(add_columns, "cmdline")) {
+ b = readfile(pid, tid, "cmdline", filebuf); // contains spaces and \0s within data TODO escape (or just print argv[0])
+ if (b > 0) { fprintf(stdout, pad ? "%-30s%c" : "%s%c", filebuf, outsep); } else { fprintf(stdout, pad ? "%-30s%c" : "%s%c", "-", outsep); }
+ }
+
+ if (strcasestr(add_columns, "kstack")) {
+ b = readfile(pid, tid, "stack", filebuf);
+ if (b > 0) { outputfields(filebuf, "t", WSP); } else { fprintf(stdout, "-"); }
+ }
+
+ fprintf(stdout, "\n");
+ }
+ }
+ else {
+ outputprocpartial(pid, tid, sampletime, proc_uid, nspid, add_columns, "[task_entry_lost(read)]");
+ return 1;
+ }
+
+ return 0;
+}
+
+void printhelp() {
+ const char *helptext =
+ "by Tanel Poder [https://0x.tools]\n\n"
+ "Usage:\n"
+ " xcapture [options]\n\n"
+ " By default, sample all /proc tasks in states R, D every second and print to stdout\n\n"
+ " Options:\n"
+ " -a capture tasks in additional states, even the ones Sleeping (S)\n"
+ " -A capture tasks in All states, including Zombie (Z), Exiting (X), Idle (I)\n"
+ " -c <c1,c2> print additional columns (for example: -c exe,cmdline,nspid,kstack)\n"
+ " -d <N> seconds between samples (default: 1.0)\n"
+ " -E <string> custom task state Exclusion filter (default: XZIS)\n"
+ " -h display this help message\n"
+ " -o <dirname> write wide output into hourly CSV files in this directory instead of stdout\n";
+
+ fprintf(stderr, "\n0x.Tools xcapture v%s %s\n", XCAP_VERSION, helptext);
+}
+
+float timedifference_msec(struct timeval t0, struct timeval t1)
+{
+ return (t1.tv_sec - t0.tv_sec) * 1000.0f + (t1.tv_usec - t0.tv_usec) / 1000.0f;
+}
+
+int main(int argc, char **argv)
+{
+ char outbuf[BUFSIZ];
+ char outpath[PATH_MAX];
+ char dirpath[PATH_MAX]; // used for /proc stuff only, so no long paths
+ DIR *pd, *td;
+ struct dirent *pde, *tde; // process level and thread/task level directory entries in /proc
+
+ char timebuf[80], usec_buf[9];
+ struct timeval tmnow,loop_iteration_start_time,loop_iteration_end_time;
+ float loop_iteration_msec;
+ float sleep_for_msec;
+ struct tm *tm;
+ int prevhour = -1; // used for detecting switch to a new hour for creating a new output file
+ int interval_msec = 1000;
+
+ struct stat pidstat, nspstat;
+ uid_t proc_uid;
+ long nspid;
+
+ int nthreads = 0;
+ int mypid = getpid();
+
+ // argument handling
+ char *add_columns = ""; // keep "" as a default value and not NULL
+ int c;
+
+ while ((c = getopt (argc, argv, "aAc:d:E:ho:")) != -1)
+ switch (c) {
+ case 'a':
+ strncpy(exclude_states, "XZI", sizeof(exclude_states));
+ break;
+ case 'A':
+ strncpy(exclude_states, "", sizeof(exclude_states));
+ break;
+ case 'c':
+ add_columns = optarg;
+ break;
+ case 'd':
+ interval_msec = atof(optarg) * 1000;
+ if (interval_msec <= 0 || interval_msec > 3600000) {
+ fprintf(stderr, "Option -d has invalid value for capture interval - %s (%d)\n", optarg, interval_msec);
+ return 1;
+ }
+ break;
+ case 'E':
+ strncpy(exclude_states, optarg, sizeof(exclude_states));
+ break;
+ case 'h':
+ printhelp();
+ exit(1);
+ break;
+ case 'o':
+ output_dir = optarg;
+ output_format = 'C'; // CSV
+ outsep = ',';
+ pad = 0;
+ if (!strlen(add_columns)) add_columns = "nspid,exe,kstack";
+ break;
+ case '?':
+ if (strchr("cEd", optopt))
+ fprintf(stderr, "Option -%c requires an argument.\n", optopt);
+ else if (isprint (optopt))
+ fprintf(stderr, "Unknown option `-%c'.\n", optopt);
+ else
+ fprintf(stderr, "Unknown option character `\\x%x'.\n", optopt);
+ return 1;
+ default:
+ abort();
+ }
+ // end argument handling
+
+ setbuf(stdout, outbuf);
+
+ fprintf(stderr, "\n0xTools xcapture v%s by Tanel Poder [https://0x.tools]\n\nSampling /proc...\n\n", XCAP_VERSION);
+
+ while (1) {
+
+ gettimeofday(&tmnow, NULL);
+ gettimeofday(&loop_iteration_start_time, NULL);
+ tm = localtime(&tmnow.tv_sec);
+
+ if (output_dir) {
+ if (prevhour != tm->tm_hour) {
+ strftime(timebuf, 30, "%Y-%m-%d.%H", tm);
+ snprintf(outpath, sizeof(outpath), "%s/%s.csv", output_dir, timebuf);
+ if (!freopen(outpath, "a", stdout)) { fprintf(stderr, "Error opening output file\n"); exit(1); };
+ setbuf(stdout, outbuf); // is this needed after freopen?
+ prevhour = tm->tm_hour;
+ header_printed = outputheader(add_columns);
+ }
+ }
+ else {
+ header_printed = 0; // dynamic stdout header printing decision is made later on
+ }
+
+ strftime(timebuf, 30, pad ? "%Y-%m-%d %H:%M:%S" : "%Y-%m-%d %H:%M:%S", tm); // currently same format for both outputs
+ strcat(timebuf, ".");
+ sprintf(usec_buf, "%03d", (int)tmnow.tv_usec/1000); // ms resolution should be ok for infrequent sampling
+ strcat(timebuf, usec_buf);
+
+ pd = opendir("/proc");
+ if (!pd) { fprintf(stderr, "/proc listing error='%s', this shouldn't happen\n", strerror(errno)); exit(1); }
+
+ while ((pde = readdir(pd))) { // /proc/PID
+ if (pde->d_name[0] >= '0' && pde->d_name[0] <= '9' && atoi(pde->d_name) != mypid) {
+ sprintf(dirpath, "/proc/%s", pde->d_name);
+ proc_uid = stat(dirpath, &pidstat) ? -1 : pidstat.st_uid;
+ sprintf(dirpath, "/proc/%s/ns/pid", pde->d_name);
+ nspid = stat(dirpath, &nspstat) ? -1 : nspstat.st_ino;
+
+ // if not multithreaded, read current /proc/PID/x files for efficiency. "nthreads" is 20th field in proc/PID/stat
+ if (readfile(atoi(pde->d_name), 0, "stat", statbuf) > 0) {
+ sscanf(statbuf, "%*s %*s %*s %*s %*s %*s %*s %*s %*s %*s %*s %*s %*s %*s %*s %*s %*s %*s %*s %u", &nthreads);
+
+ if (nthreads > 1) {
+ sprintf(dirpath, "/proc/%s/task", pde->d_name);
+
+ td = opendir(dirpath);
+ if (td) {
+
+ while ((tde = readdir(td))) { // proc/PID/task/TID
+ if (tde->d_name[0] >= '0' && tde->d_name[0] <= '9') {
+ outputprocentry(atoi(pde->d_name), atoi(tde->d_name), timebuf, proc_uid, nspid, add_columns);
+ }
+ }
+ }
+ else {
+ outputprocpartial(atoi(pde->d_name), -1, timebuf, proc_uid, nspid, add_columns, "[task_entry_lost(list)]");
+ }
+ closedir(td);
+ }
+ else { // nthreads <= 1, therefore pid == tid
+ outputprocentry(atoi(pde->d_name), atoi(pde->d_name), timebuf, proc_uid, nspid, add_columns);
+ }
+
+ } // readfile(statbuf)
+ else {
+ outputprocpartial(atoi(pde->d_name), -1, timebuf, proc_uid, nspid, add_columns, "[proc_entry_lost(list)]");
+ if (DEBUG) fprintf(stderr, "proc entry disappeared /proc/%s/stat, len=%zu, errno=%s\n", pde->d_name, strlen(statbuf), strerror(errno));
+ }
+ }
+ }
+ closedir(pd);
+
+ if (!output_dir && header_printed) fprintf(stdout, "\n");
+
+ fflush(stdout);
+
+ // sleep for the requested interval minus time spent taking the previous sample
+ gettimeofday(&loop_iteration_end_time, NULL);
+ loop_iteration_msec = timedifference_msec(loop_iteration_start_time, loop_iteration_end_time);
+ sleep_for_msec = interval_msec - loop_iteration_msec;
+ if (sleep_for_msec > 0) usleep(sleep_for_msec * 1000);
+
+ }
+
+ return 0;
+}