Skip to content

Commit 6d01920

Browse files
committed
Add the ps command to sdb.
1 parent d0cb398 commit 6d01920

File tree

11 files changed

+3068
-8
lines changed

11 files changed

+3068
-8
lines changed

sdb/commands/linux/ps.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#
2+
# Copyright 2021 Datto Inc.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
17+
# pylint: disable=missing-docstring
18+
19+
import datetime
20+
import argparse
21+
from typing import Callable, Dict, Iterable, List, Union
22+
23+
import drgn
24+
from drgn.helpers.linux.pid import for_each_task
25+
26+
import sdb
27+
from sdb.commands.stacks import Stacks
28+
from sdb.commands.threads import Threads
29+
30+
31+
class Ps(Threads):
32+
"""
33+
Locate and print information about processes
34+
35+
POTENTIAL COLUMNS
36+
task - address of the task_struct
37+
uid - user id
38+
pid - the pid of the thread's process
39+
time - cumulative CPU time, "[DD-]HH:MM:SS" format
40+
state - the state of the thread
41+
prio - the priority of the thread
42+
comm - the thread's command
43+
cmdline - the thread's command line (when available)
44+
45+
EXAMPLE
46+
sdb> ps -e
47+
task uid pid time stime ppid cmd
48+
------------------ ---- ---- ------- ------- ----- ----------------
49+
0xffff9995001a0000 1000 4387 0:00:00 0:00:00 1218 inotify_reader
50+
0xffff9995001a2d00 1000 4382 0:00:00 0:00:00 1218 slack
51+
0xffff9995001a4380 1000 4383 0:00:04 0:00:00 1218 slack
52+
0xffff9995001a5a00 1000 4554 0:11:00 0:09:56 4118 AudioIP~ent RPC
53+
0xffff99950a430000 1000 4284 0:00:00 0:00:00 4118 HTML5 Parser
54+
0xffff99950a431680 1000 4157 0:00:12 0:00:08 1218 localStorage DBa
55+
...
56+
"""
57+
58+
names = ["ps"]
59+
input_type = "struct task_struct *"
60+
output_type = "struct task_struct *"
61+
62+
FIELDS: Dict[str, Callable[[drgn.Object], Union[str, int]]] = {
63+
"task":
64+
lambda obj: hex(obj.value_()),
65+
"uid":
66+
lambda obj: int(obj.real_cred.uid.val),
67+
"pid":
68+
lambda obj: int(obj.pid),
69+
"time":
70+
lambda obj: str(
71+
datetime.timedelta(seconds=int(obj.utime) / 1000 / 1000)),
72+
"stime":
73+
lambda obj: str(
74+
datetime.timedelta(seconds=int(obj.stime) / 1000 / 1000)),
75+
"ppid":
76+
lambda obj: int(obj.parent.pid),
77+
"stat":
78+
lambda obj: str(Stacks.task_struct_get_state(obj)),
79+
"cmd":
80+
lambda obj: str(obj.comm.string_().decode("utf-8")),
81+
}
82+
83+
@classmethod
84+
def _init_parser(cls, name: str) -> argparse.ArgumentParser:
85+
parser = super()._init_parser(name)
86+
parser.add_argument('-e', '--every', action='store_true', \
87+
help="Select all processes. Identical to -A")
88+
parser.add_argument('-A', '--all', action='store_true', \
89+
help="Select all processes. Identical to -e")
90+
parser.add_argument('-C', '--C', type=str, \
91+
help="Print only the process IDs of a command")
92+
parser.add_argument('-x', '--x', action='store_true', \
93+
help="Show PID, TIME, CMD")
94+
parser.add_argument('--no-headers', '--no-heading', \
95+
action='store_true', \
96+
help="Show the output without headers.")
97+
parser.add_argument('-p', '--pid', type=int, nargs="+", \
98+
help="Select by process ID. Identical to --pid.")
99+
parser.add_argument('-P', '--ppid', type=int, nargs="+", \
100+
help="Select by parent process ID. \
101+
This selects the processes with \
102+
a parent process ID in the pid list."
103+
)
104+
parser.add_argument('-o',
105+
'--format',
106+
type=str,
107+
help="User-defined format. \
108+
format is a single argument in the form of a blank-separated \
109+
or comma-separated list, which offers a way to specify individual\
110+
output columns. Headers may be renamed (ps -o pid,ppid) as desired. "
111+
)
112+
return parser
113+
114+
def get_table_key(self) -> str:
115+
return "pid"
116+
117+
def get_filtered_fields(self) -> List[str]:
118+
fields = list(self.FIELDS.keys())
119+
fields_option_ae = [
120+
x for x in fields if x in "task, uid, pid, ppid, stime, time, cmd"
121+
]
122+
fields_option_x = [x for x in fields if x in "pid, stat, time, cmd"]
123+
fields_option_default = [x for x in fields if x in "pid, time, cmd"]
124+
125+
if self.args.every or self.args.all:
126+
fields = fields_option_ae
127+
elif self.args.x:
128+
fields = fields_option_x
129+
elif self.args.format:
130+
fields_option_o = 'task,' + self.args.format.lower()
131+
fields_option_o = [x for x in fields if x in fields_option_o]
132+
fields = fields_option_o
133+
else:
134+
fields = fields_option_default
135+
return fields
136+
137+
def show_headers(self) -> bool:
138+
return not self.args.no_headers
139+
140+
def no_input(self) -> Iterable[drgn.Object]:
141+
cmds = self.args.C.split(",") if self.args.C else []
142+
pids = self.args.pid if self.args.pid else []
143+
ppids = self.args.ppid if self.args.ppid else []
144+
for obj in for_each_task(sdb.get_prog()):
145+
if self.args.pid:
146+
if obj.pid not in pids:
147+
continue
148+
if self.args.C:
149+
cmd = str(obj.comm.string_().decode("utf-8"))
150+
if cmd not in cmds:
151+
continue
152+
if self.args.ppid:
153+
if obj.parent.pid not in ppids:
154+
continue
155+
yield obj

sdb/commands/stacks.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,8 +359,8 @@ def print_header(self) -> None:
359359
#
360360
@staticmethod
361361
def aggregate_stacks(
362-
objs: Iterable[drgn.Object]
363-
) -> List[Tuple[Tuple[str, Tuple[int, ...]], List[drgn.Object]]]:
362+
objs: Iterable[drgn.Object]) \
363+
-> List[Tuple[Tuple[str, Tuple[int, ...]], List[drgn.Object]]]:
364364
stack_aggr: Dict[Tuple[str, Tuple[int, ...]],
365365
List[drgn.Object]] = defaultdict(list)
366366
for task in objs:

sdb/commands/threads.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
# pylint: disable=missing-docstring
1818

1919
from textwrap import shorten
20-
from typing import Callable, Dict, Iterable, Union
20+
from typing import Callable, Dict, Iterable, List, Union
2121

2222
import drgn
2323
from drgn.helpers.linux.pid import for_each_task
@@ -85,13 +85,26 @@ class Threads(sdb.Locator, sdb.PrettyPrinter):
8585
"cmdline": _cmdline,
8686
}
8787

88+
def get_filtered_fields(self) -> List[str]:
89+
return list(self.FIELDS.keys())
90+
91+
def get_table_key(self) -> str:
92+
# pylint: disable=R0201
93+
return "task"
94+
95+
def show_headers(self) -> bool:
96+
# pylint: disable=R0201
97+
return True
98+
8899
def pretty_print(self, objs: Iterable[drgn.Object]) -> None:
89-
fields = list(Threads.FIELDS.keys())
90-
table = Table(fields, None, {"task": str})
100+
fields = self.get_filtered_fields()
101+
table_key = self.get_table_key()
102+
103+
table = Table(fields, None, {table_key: str})
91104
for obj in objs:
92-
row_dict = {field: Threads.FIELDS[field](obj) for field in fields}
93-
table.add_row(row_dict["task"], row_dict)
94-
table.print_()
105+
row_dict = {field: self.FIELDS[field](obj) for field in fields}
106+
table.add_row(row_dict[table_key], row_dict)
107+
table.print_(print_headers=self.show_headers())
95108

96109
def no_input(self) -> Iterable[drgn.Object]:
97110
yield from for_each_task(sdb.get_prog())

0 commit comments

Comments
 (0)