Alexey Ivanov | cc01a9c | 2019-01-16 09:50:46 -0800 | [diff] [blame] | 1 | #!/usr/bin/python |
Brendan Gregg | 6049d3f | 2016-10-16 12:33:50 -0700 | [diff] [blame] | 2 | # @lint-avoid-python-3-compatibility-imports |
| 3 | # |
| 4 | # ttysnoop Watch live output from a tty or pts device. |
| 5 | # For Linux, uses BCC, eBPF. Embedded C. |
| 6 | # |
| 7 | # Due to a limited buffer size (see BUFSIZE), some commands (eg, a vim |
| 8 | # session) are likely to be printed a little messed up. |
| 9 | # |
| 10 | # Copyright (c) 2016 Brendan Gregg. |
| 11 | # Licensed under the Apache License, Version 2.0 (the "License") |
| 12 | # |
| 13 | # Idea: from ttywatcher. |
| 14 | # |
| 15 | # 15-Oct-2016 Brendan Gregg Created this. |
| 16 | |
| 17 | from __future__ import print_function |
| 18 | from bcc import BPF |
Brendan Gregg | 6049d3f | 2016-10-16 12:33:50 -0700 | [diff] [blame] | 19 | from subprocess import call |
| 20 | import argparse |
| 21 | from sys import argv |
| 22 | import sys |
| 23 | from os import stat |
| 24 | |
| 25 | def usage(): |
| 26 | print("USAGE: %s [-Ch] {PTS | /dev/ttydev} # try -h for help" % argv[0]) |
| 27 | exit() |
| 28 | |
| 29 | # arguments |
| 30 | examples = """examples: |
Jiri Olsa | 20061e8 | 2021-04-09 19:24:12 +0200 | [diff] [blame] | 31 | ./ttysnoop /dev/pts/2 # snoop output from /dev/pts/2 |
| 32 | ./ttysnoop 2 # snoop output from /dev/pts/2 (shortcut) |
| 33 | ./ttysnoop /dev/console # snoop output from the system console |
| 34 | ./ttysnoop /dev/tty0 # snoop output from /dev/tty0 |
| 35 | ./ttysnoop /dev/pts/2 -s 1024 # snoop output from /dev/pts/2 with data size 1024 |
| 36 | ./ttysnoop /dev/pts/2 -c 2 # snoop output from /dev/pts/2 with 2 checks for 256 bytes of data in buffer |
| 37 | (potentially retrieving 512 bytes) |
Brendan Gregg | 6049d3f | 2016-10-16 12:33:50 -0700 | [diff] [blame] | 38 | """ |
| 39 | parser = argparse.ArgumentParser( |
| 40 | description="Snoop output from a pts or tty device, eg, a shell", |
| 41 | formatter_class=argparse.RawDescriptionHelpFormatter, |
| 42 | epilog=examples) |
| 43 | parser.add_argument("-C", "--noclear", action="store_true", |
| 44 | help="don't clear the screen") |
| 45 | parser.add_argument("device", default="-1", |
| 46 | help="path to a tty device (eg, /dev/tty0) or pts number") |
Jiri Olsa | 20061e8 | 2021-04-09 19:24:12 +0200 | [diff] [blame] | 47 | parser.add_argument("-s", "--datasize", default="256", |
| 48 | help="size of the transmitting buffer (default 256)") |
| 49 | parser.add_argument("-c", "--datacount", default="16", |
| 50 | help="number of times we check for 'data-size' data (default 16)") |
Nathan Scott | cf0792f | 2018-02-02 16:56:50 +1100 | [diff] [blame] | 51 | parser.add_argument("--ebpf", action="store_true", |
| 52 | help=argparse.SUPPRESS) |
Brendan Gregg | 6049d3f | 2016-10-16 12:33:50 -0700 | [diff] [blame] | 53 | args = parser.parse_args() |
| 54 | debug = 0 |
| 55 | |
| 56 | if args.device == "-1": |
| 57 | usage() |
| 58 | |
| 59 | path = args.device |
| 60 | if path.find('/') != 0: |
| 61 | path = "/dev/pts/" + path |
| 62 | try: |
| 63 | pi = stat(path) |
| 64 | except: |
| 65 | print("Unable to read device %s. Exiting." % path) |
| 66 | exit() |
| 67 | |
| 68 | # define BPF program |
| 69 | bpf_text = """ |
| 70 | #include <uapi/linux/ptrace.h> |
| 71 | #include <linux/fs.h> |
Jiri Olsa | bc1e013 | 2021-05-09 17:36:36 +0200 | [diff] [blame] | 72 | #include <linux/uio.h> |
Brendan Gregg | 6049d3f | 2016-10-16 12:33:50 -0700 | [diff] [blame] | 73 | |
Jiri Olsa | 20061e8 | 2021-04-09 19:24:12 +0200 | [diff] [blame] | 74 | #define BUFSIZE USER_DATASIZE |
Brendan Gregg | 6049d3f | 2016-10-16 12:33:50 -0700 | [diff] [blame] | 75 | struct data_t { |
| 76 | int count; |
| 77 | char buf[BUFSIZE]; |
| 78 | }; |
| 79 | |
Jiri Olsa | ebd63c1 | 2021-04-09 17:14:21 +0200 | [diff] [blame] | 80 | BPF_ARRAY(data_map, struct data_t, 1); |
Brendan Gregg | 6049d3f | 2016-10-16 12:33:50 -0700 | [diff] [blame] | 81 | BPF_PERF_OUTPUT(events); |
| 82 | |
Jiri Olsa | bc1e013 | 2021-05-09 17:36:36 +0200 | [diff] [blame] | 83 | static int do_tty_write(void *ctx, const char __user *buf, size_t count) |
Brendan Gregg | 6049d3f | 2016-10-16 12:33:50 -0700 | [diff] [blame] | 84 | { |
Jiri Olsa | 20061e8 | 2021-04-09 19:24:12 +0200 | [diff] [blame] | 85 | int zero = 0, i; |
Jiri Olsa | ebd63c1 | 2021-04-09 17:14:21 +0200 | [diff] [blame] | 86 | struct data_t *data; |
| 87 | |
| 88 | /* We can't read data to map data before v4.11 */ |
| 89 | #if LINUX_VERSION_CODE < KERNEL_VERSION(4, 11, 0) |
| 90 | struct data_t _data = {}; |
| 91 | |
| 92 | data = &_data; |
| 93 | #else |
| 94 | data = data_map.lookup(&zero); |
| 95 | if (!data) |
| 96 | return 0; |
| 97 | #endif |
| 98 | |
Jiri Olsa | 20061e8 | 2021-04-09 19:24:12 +0200 | [diff] [blame] | 99 | #pragma unroll |
| 100 | for (i = 0; i < USER_DATACOUNT; i++) { |
| 101 | // bpf_probe_read_user() can only use a fixed size, so truncate to count |
| 102 | // in user space: |
| 103 | if (bpf_probe_read_user(&data->buf, BUFSIZE, (void *)buf)) |
| 104 | return 0; |
| 105 | if (count > BUFSIZE) |
| 106 | data->count = BUFSIZE; |
| 107 | else |
| 108 | data->count = count; |
| 109 | events.perf_submit(ctx, data, sizeof(*data)); |
| 110 | if (count < BUFSIZE) |
| 111 | return 0; |
| 112 | count -= BUFSIZE; |
| 113 | buf += BUFSIZE; |
| 114 | } |
Brendan Gregg | 6049d3f | 2016-10-16 12:33:50 -0700 | [diff] [blame] | 115 | |
| 116 | return 0; |
| 117 | }; |
Jiri Olsa | bc1e013 | 2021-05-09 17:36:36 +0200 | [diff] [blame] | 118 | |
| 119 | /** |
| 120 | * commit 9bb48c82aced (v5.11-rc4) tty: implement write_iter |
| 121 | * changed arguments of tty_write function |
| 122 | */ |
| 123 | #if LINUX_VERSION_CODE < KERNEL_VERSION(5, 11, 0) |
| 124 | int kprobe__tty_write(struct pt_regs *ctx, struct file *file, |
| 125 | const char __user *buf, size_t count) |
| 126 | { |
| 127 | if (file->f_inode->i_ino != PTS) |
| 128 | return 0; |
| 129 | |
| 130 | return do_tty_write(ctx, buf, count); |
| 131 | } |
| 132 | #else |
| 133 | KFUNC_PROBE(tty_write, struct kiocb *iocb, struct iov_iter *from) |
| 134 | { |
| 135 | const char __user *buf; |
| 136 | const struct kvec *kvec; |
| 137 | size_t count; |
| 138 | |
| 139 | if (iocb->ki_filp->f_inode->i_ino != PTS) |
| 140 | return 0; |
Sina Radmehr | 68f294f | 2021-09-11 03:18:33 +0430 | [diff] [blame] | 141 | /** |
| 142 | * commit 8cd54c1c8480 iov_iter: separate direction from flavour |
| 143 | * `type` is represented by iter_type and data_source seperately |
| 144 | */ |
| 145 | #if LINUX_VERSION_CODE < KERNEL_VERSION(5, 14, 0) |
Jiri Olsa | bc1e013 | 2021-05-09 17:36:36 +0200 | [diff] [blame] | 146 | if (from->type != (ITER_IOVEC + WRITE)) |
| 147 | return 0; |
Sina Radmehr | 68f294f | 2021-09-11 03:18:33 +0430 | [diff] [blame] | 148 | #else |
| 149 | if (from->iter_type != ITER_IOVEC) |
| 150 | return 0; |
| 151 | if (from->data_source != WRITE) |
| 152 | return 0; |
| 153 | #endif |
| 154 | |
Jiri Olsa | bc1e013 | 2021-05-09 17:36:36 +0200 | [diff] [blame] | 155 | |
| 156 | kvec = from->kvec; |
| 157 | buf = kvec->iov_base; |
| 158 | count = kvec->iov_len; |
| 159 | |
| 160 | return do_tty_write(ctx, kvec->iov_base, kvec->iov_len); |
| 161 | } |
| 162 | #endif |
Brendan Gregg | 6049d3f | 2016-10-16 12:33:50 -0700 | [diff] [blame] | 163 | """ |
| 164 | |
| 165 | bpf_text = bpf_text.replace('PTS', str(pi.st_ino)) |
Nathan Scott | cf0792f | 2018-02-02 16:56:50 +1100 | [diff] [blame] | 166 | if debug or args.ebpf: |
Brendan Gregg | 6049d3f | 2016-10-16 12:33:50 -0700 | [diff] [blame] | 167 | print(bpf_text) |
Nathan Scott | cf0792f | 2018-02-02 16:56:50 +1100 | [diff] [blame] | 168 | if args.ebpf: |
| 169 | exit() |
Brendan Gregg | 6049d3f | 2016-10-16 12:33:50 -0700 | [diff] [blame] | 170 | |
Jiri Olsa | 20061e8 | 2021-04-09 19:24:12 +0200 | [diff] [blame] | 171 | bpf_text = bpf_text.replace('USER_DATASIZE', '%s' % args.datasize) |
| 172 | bpf_text = bpf_text.replace('USER_DATACOUNT', '%s' % args.datacount) |
| 173 | |
Brendan Gregg | 6049d3f | 2016-10-16 12:33:50 -0700 | [diff] [blame] | 174 | # initialize BPF |
| 175 | b = BPF(text=bpf_text) |
| 176 | |
Brendan Gregg | 6049d3f | 2016-10-16 12:33:50 -0700 | [diff] [blame] | 177 | if not args.noclear: |
| 178 | call("clear") |
| 179 | |
| 180 | # process event |
| 181 | def print_event(cpu, data, size): |
Xiaozhou Liu | 51d62d3 | 2019-02-15 13:03:05 +0800 | [diff] [blame] | 182 | event = b["events"].event(data) |
jeromemarchand | b96ebcd | 2018-10-10 01:58:15 +0200 | [diff] [blame] | 183 | print("%s" % event.buf[0:event.count].decode('utf-8', 'replace'), end="") |
Brendan Gregg | 6049d3f | 2016-10-16 12:33:50 -0700 | [diff] [blame] | 184 | sys.stdout.flush() |
| 185 | |
| 186 | # loop with callback to print_event |
| 187 | b["events"].open_perf_buffer(print_event) |
| 188 | while 1: |
Jerome Marchand | 5167127 | 2018-12-19 01:57:24 +0100 | [diff] [blame] | 189 | try: |
| 190 | b.perf_buffer_poll() |
| 191 | except KeyboardInterrupt: |
| 192 | exit() |