• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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