1#!/usr/bin/python 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 17from __future__ import print_function 18from bcc import BPF 19from subprocess import call 20import argparse 21from sys import argv 22import sys 23from os import stat 24 25def usage(): 26 print("USAGE: %s [-Ch] {PTS | /dev/ttydev} # try -h for help" % argv[0]) 27 exit() 28 29# arguments 30examples = """examples: 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) 38""" 39parser = argparse.ArgumentParser( 40 description="Snoop output from a pts or tty device, eg, a shell", 41 formatter_class=argparse.RawDescriptionHelpFormatter, 42 epilog=examples) 43parser.add_argument("-C", "--noclear", action="store_true", 44 help="don't clear the screen") 45parser.add_argument("device", default="-1", 46 help="path to a tty device (eg, /dev/tty0) or pts number") 47parser.add_argument("-s", "--datasize", default="256", 48 help="size of the transmitting buffer (default 256)") 49parser.add_argument("-c", "--datacount", default="16", 50 help="number of times we check for 'data-size' data (default 16)") 51parser.add_argument("--ebpf", action="store_true", 52 help=argparse.SUPPRESS) 53args = parser.parse_args() 54debug = 0 55 56if args.device == "-1": 57 usage() 58 59path = args.device 60if path.find('/') != 0: 61 path = "/dev/pts/" + path 62try: 63 pi = stat(path) 64except: 65 print("Unable to read device %s. Exiting." % path) 66 exit() 67 68# define BPF program 69bpf_text = """ 70#include <uapi/linux/ptrace.h> 71#include <linux/fs.h> 72#include <linux/uio.h> 73 74#define BUFSIZE USER_DATASIZE 75struct data_t { 76 int count; 77 char buf[BUFSIZE]; 78}; 79 80BPF_ARRAY(data_map, struct data_t, 1); 81BPF_PERF_OUTPUT(events); 82 83static int do_tty_write(void *ctx, const char __user *buf, size_t count) 84{ 85 int zero = 0, i; 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 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 } 115 116 return 0; 117}; 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) 124int 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 133KFUNC_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; 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) 146 if (from->type != (ITER_IOVEC + WRITE)) 147 return 0; 148#else 149 if (from->iter_type != ITER_IOVEC) 150 return 0; 151 if (from->data_source != WRITE) 152 return 0; 153#endif 154 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 163""" 164 165bpf_text = bpf_text.replace('PTS', str(pi.st_ino)) 166if debug or args.ebpf: 167 print(bpf_text) 168 if args.ebpf: 169 exit() 170 171bpf_text = bpf_text.replace('USER_DATASIZE', '%s' % args.datasize) 172bpf_text = bpf_text.replace('USER_DATACOUNT', '%s' % args.datacount) 173 174# initialize BPF 175b = BPF(text=bpf_text) 176 177if not args.noclear: 178 call("clear") 179 180# process event 181def print_event(cpu, data, size): 182 event = b["events"].event(data) 183 print("%s" % event.buf[0:event.count].decode('utf-8', 'replace'), end="") 184 sys.stdout.flush() 185 186# loop with callback to print_event 187b["events"].open_perf_buffer(print_event) 188while 1: 189 try: 190 b.perf_buffer_poll() 191 except KeyboardInterrupt: 192 exit() 193