Skip to content

Commit 27dac29

Browse files
committed
Add the ps command to sdb.
1 parent d0cb398 commit 27dac29

File tree

11 files changed

+3067
-8
lines changed

11 files changed

+3067
-8
lines changed

sdb/commands/linux/ps.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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+
class Ps(Threads):
31+
"""
32+
Locate and print information about processes
33+
34+
POTENTIAL COLUMNS
35+
task - address of the task_struct
36+
uid - user id
37+
pid - the pid of the thread's process
38+
time - cumulative CPU time, "[DD-]HH:MM:SS" format
39+
state - the state of the thread
40+
prio - the priority of the thread
41+
comm - the thread's command
42+
cmdline - the thread's command line (when available)
43+
44+
EXAMPLE
45+
sdb> ps -e
46+
task uid pid time stime ppid cmd
47+
------------------ ---- ---- ------- ------- ----- ----------------
48+
0xffff9995001a0000 1000 4387 0:00:00 0:00:00 1218 inotify_reader
49+
0xffff9995001a2d00 1000 4382 0:00:00 0:00:00 1218 slack
50+
0xffff9995001a4380 1000 4383 0:00:04 0:00:00 1218 slack
51+
0xffff9995001a5a00 1000 4554 0:11:00 0:09:56 4118 AudioIP~ent RPC
52+
0xffff99950a430000 1000 4284 0:00:00 0:00:00 4118 HTML5 Parser
53+
0xffff99950a431680 1000 4157 0:00:12 0:00:08 1218 localStorage DBa
54+
...
55+
"""
56+
57+
names = ["ps"]
58+
input_type = "struct task_struct *"
59+
output_type = "struct task_struct *"
60+
61+
FIELDS: Dict[str, Callable[[drgn.Object], Union[str, int]]] = {
62+
"task":
63+
lambda obj: hex(obj.value_()),
64+
"uid":
65+
lambda obj: int(obj.real_cred.uid.val),
66+
"pid":
67+
lambda obj: int(obj.pid),
68+
"time":
69+
lambda obj: str(
70+
datetime.timedelta(seconds=int(obj.utime) / 1000 / 1000)),
71+
"stime":
72+
lambda obj: str(
73+
datetime.timedelta(seconds=int(obj.stime) / 1000 / 1000)),
74+
"ppid":
75+
lambda obj: int(obj.parent.pid),
76+
"stat":
77+
lambda obj: str(Stacks.task_struct_get_state(obj)),
78+
"cmd":
79+
lambda obj: str(obj.comm.string_().decode("utf-8")),
80+
}
81+
82+
@classmethod
83+
def _init_parser(cls, name: str) -> argparse.ArgumentParser:
84+
parser = super()._init_parser(name)
85+
parser.add_argument('-e', '--every', action='store_true', \
86+
help="Select all processes. Identical to -A")
87+
parser.add_argument('-A', '--all', action='store_true', \
88+
help="Select all processes. Identical to -e")
89+
parser.add_argument('-C', '--C', type=str, \
90+
help="Print only the process IDs of a command")
91+
parser.add_argument('-x', '--x', action='store_true', \
92+
help="Show PID, TIME, CMD")
93+
parser.add_argument('--no-headers', '--no-heading', \
94+
action='store_true', \
95+
help="Show the output without headers.")
96+
parser.add_argument('-p', '--pid', type=int, nargs="+", \
97+
help="Select by process ID. Identical to --pid.")
98+
parser.add_argument('-P', '--ppid', type=int, nargs="+", \
99+
help="Select by parent process ID. \
100+
This selects the processes with \
101+
a parent process ID in the pid list."
102+
)
103+
parser.add_argument('-o',
104+
'--format',
105+
type=str,
106+
help="User-defined format. \
107+
format is a single argument in the form of a blank-separated \
108+
or comma-separated list, which offers a way to specify individual\
109+
output columns. Headers may be renamed (ps -o pid,ppid) as desired. "
110+
)
111+
return parser
112+
113+
def get_table_key(self) -> str:
114+
return "pid"
115+
116+
def get_filtered_fields(self) -> List[str]:
117+
fields = list(self.FIELDS.keys())
118+
fields_option_ae = [
119+
x for x in fields if x in "task, uid, pid, ppid, stime, time, cmd"
120+
]
121+
fields_option_x = [x for x in fields if x in "pid, stat, time, cmd"]
122+
fields_option_default = [x for x in fields if x in "pid, time, cmd"]
123+
124+
if self.args.every or self.args.all:
125+
fields = fields_option_ae
126+
elif self.args.x:
127+
fields = fields_option_x
128+
elif self.args.format:
129+
fields_option_o = 'task,' + self.args.format.lower()
130+
fields_option_o = [x for x in fields if x in fields_option_o]
131+
fields = fields_option_o
132+
else:
133+
fields = fields_option_default
134+
return fields
135+
136+
def show_headers(self) -> bool:
137+
return not self.args.no_headers
138+
139+
def no_input(self) -> Iterable[drgn.Object]:
140+
cmds = self.args.C.split(",") if self.args.C else []
141+
pids = self.args.pid if self.args.pid else []
142+
ppids = self.args.ppid if self.args.ppid else []
143+
for obj in for_each_task(sdb.get_prog()):
144+
if self.args.pid:
145+
if obj.pid not in pids:
146+
continue
147+
if self.args.C:
148+
cmd = str(obj.comm.string_().decode("utf-8"))
149+
if cmd not in cmds:
150+
continue
151+
if self.args.ppid:
152+
if obj.parent.pid not in ppids:
153+
continue
154+
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)