-
Notifications
You must be signed in to change notification settings - Fork 13
/
Copy pathpsaux.bash
executable file
·270 lines (228 loc) · 9.58 KB
/
psaux.bash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
#!/bin/bash
# so initially i was hoping you could get everything from /proc/<pid>/status
# because it's easy to parse (in most cases) but apparently you can't get
# things like the cpu% :(
#while read -ra data; do
# case ${data[0]} in
# State:) stat=${data[1]};;
# Uid:) uid=${data[1]};;
# Gid:) gid=${data[1]};;
# VmSize:) vsz=${data[1]};;
# VmRSS:) rss=${data[1]};;
# esac
#done < "$dir"/status
# it would have been so easy!!!!
#
# SO INSTEAD LET'S DO IT AAAAAALL __PROPERLY__ (with zero forking)
# you can test that it doesn't fork with
# strace -fe fork,clone,clone3 -o strout ./psaux.bash; cat strout
users=()
read_passwd() {
local IFS=: fields
while read -ra fields; do
if (( ${#fields[0]} > 8 )); then
users[fields[2]]=${fields[0]::7}+
else
users[fields[2]]=${fields[0]}
fi
done < /etc/passwd
}
IFS=$' \t\n'
devices=()
resolve_devices() {
# ok so this seems to always have 4 == tty/ttyS, 5 == console/ptmx and 136 == pts
# but let's just do it properly
local fields
while read -ra fields; do
if [[ ${fields[0]} == [0-9]* ]]; then
devices[fields[0]]+=${fields[1]}" "
fi
done < /proc/devices
}
ttyname() {
local major minor device fmt
(( major = $1 >> 8, minor = $1 & 0xff ))
# now let's play the fun game of guess whatever the fuck the name could be
REPLY=?
for device in ${devices[major]}; do
# ps checks for these:
#lookup("/dev/pts/%s");
#lookup("/dev/%s"); <- i don't think this can happen in this context?
#lookup("/dev/tty%s");
#lookup("/dev/pty%s"); <- seems like it can't happen on my machine?
#lookup("/dev/%snsole"); <- i hope this doesn't happen????
# but we're cooler so we're also checking for stuff like ttyS[0-9]+
REPLY=$device/$minor
for fmt in %s/%s %s%s; do
printf -v REPLY "$fmt" "$device" "$minor"
[[ -e /dev/$REPLY ]] && return
done
done
}
process_uid=()
process_pid=()
process_cpu=()
process_mem=()
process_vsz=()
process_rss=()
process_tty=()
process_stat=()
process_start=()
process_time=()
process_command=()
widths=()
add_process() {
process_uid[$2]=$1
process_pid[$2]=$2
process_cpu[$2]=$3
process_mem[$2]=$4
process_vsz[$2]=$5
process_rss[$2]=$6
process_tty[$2]=$7
process_stat[$2]=$8
process_start[$2]=$9
process_time[$2]=${10}
process_command[$2]=${11}
local i width
for (( i = 1; i <= $#; i++)) do
width=${@:i:1} width=${#width}
(( widths[i-1] = width > widths[i-1] ? width : widths[i-1] ))
done
}
get_term_size() {
local oldrow oldcol
# in interactive mode bash enables checkwinsize which reports $LINES and $COLUMNS and reacts nicely to sigwinch
# unfortunately checkwinsize is fucking unusable and broken in 30 different ways in non interactive scripts
#
# the normal, reliable way to check the terminal size requires an ioctl we don't have direct access to
# (altho bash itself does, and it will immediately use it for [[ -t ]], but you can't have it because fuck you)
#
# one could in theory start a script with #!/bin/bash -i and use checkwinsize
# it kinda works but it sucks because it sources all your dotfiles and whatevers
#
# so let's manually ask the terminal with the raw ansi codes
# (which seems to work on my terminal. if it doesn't work on yours maybe you need a better terminal?)
# (tested on terminator 2.1.3, which surely is the most common terminal in the world and the only one people care about)
if [[ -t 1 ]]; then
# get the current position
IFS='[;' read -sdR -p $'\e[6n' _ oldrow oldcol
# hide cursor and move it to the end of the screen
printf '\e[%s' '?25l' '9999;9999H' # your terminal is smaller than 9999x9999
# finally get a reasonable estimate
IFS='[;' read -sdR -p $'\e[6n' _ LINES COLUMNS
# show cursor again and go back to the old position
printf '\e[%s' '?25h' "$oldrow;1H"
# (if we were not at col 1, we just ignore it because we use \n anyway)
else
COLUMNS=20000 # whatever
fi
}
# turns out that the most difficult problem in computer science is aligning things
# this one function looks simple but it took so fucking long
printall() {
get_term_size
# USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
printf -v fmt '%%-%ds %%%ds %%%ds %%%ds %%%ds %%%ds %%-%ds %%-%ds %%%ss %%%ss %%-.%ds' "${widths[@]}"
local i line
for i in "${process_pid[@]}"; do
printf -v line "$fmt" \
"${process_uid[i]}" \
"${process_pid[i]}" \
"${process_cpu[i]}" \
"${process_mem[i]}" \
"${process_vsz[i]}" \
"${process_rss[i]}" \
"${process_tty[i]}" \
"${process_stat[i]}" \
"${process_start[i]}" \
"${process_time[i]}" \
"${process_command[i]}"
printf "%.${COLUMNS}s\n" "$line"
done
}
almost_ps_aux() {
read_passwd
resolve_devices
read _ memtotal _ < /proc/meminfo
read boottime _ < /proc/uptime
boottime=${boottime%%[!0-9]*}
local REPLY
local cmdline stat status # various fds
local dir pid cmd_line stat_fields status_fields name user state tty cpu start vsz rss time time_of_day # variables
local sys_clk_tck=100 # hardcoded from my unistd.h
add_process USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
# bash<5 doesn't have epochseconds, so try to get it
[[ $EPOCHSECONDS ]] || printf -v EPOCHSECONDS '%(%s)T' -1
# however i've not yet checked any of this in any other bash so this might not be very useful after all
printf -v time_of_day '%(10#%H*3600+10#%M*60+10#%S)T' # omg haxxx
time_of_day=$(($time_of_day))
for dir in /proc/[1-9]*; do
pid=${dir#/proc/}
[[ $cmdline ]] && exec {cmdline}>&-
[[ $stat ]] && exec {stat}>&-
[[ $status ]] && exec {status}>&-
# if something can't be opened, skip this process entirely, it's probably either dead already or inaccessible so wtf can we do about it
{
exec {cmdline}< "$dir"/cmdline || continue
exec {stat}< "$dir"/stat || continue
exec {status}< "$dir"/status || continue
} 2>/dev/null
cmd_line=()
while read -rd '' -u "$cmdline"; do
cmd_line+=("$REPLY")
done
# if cmd_line is empty, this might be a kernel thread
# this is not always the case because you can just wipe your own cmdline, and idfk how to detect if something was _actually_ a k thread
# htop literally does the same thing as this
# ps places square brackets around these process names for whatever reason
# linux escapes newlines in the file name here, so it's guaranteed to all fit on a single line
read -ru "$status" _ name
while read -ru "$status" -a status_fields; do
case ${status_fields[0]} in
VmLck:) vmlocked=${status_fields[1]} ;;
Uid:) uid=${status_fields[2]} ;; # this seems to be the only reliable way to get the uid from bash using builtins only
esac
done
(( ! ${#cmd_line[@]} )) && cmd_line[0]=[$name]
read -rd '' -u "$stat"
# splitting on spaces here is fine because the rest are all numbers, and it removes the trailing newline we got with read -d ''
# (putting pid and the empty fields in here makes it so the offsets match the docs for proc/pid/stat
#
# note: we don't care about the comm field at all because we already have cmdline
# it could contain parentheses and spaces and stuff, but it's always terminated by the last ) in the file
stat_fields=(. "$pid" . ${REPLY##*) })
state=${stat_fields[3]}
(( vmlocked )) && state+=L
(( stat_fields[19] > 0 )) && state+=N
(( stat_fields[19] < 0 )) && state+='<'
(( stat_fields[6] == pid )) && state+=s
(( stat_fields[20] != 1 )) && state+=l
(( stat_fields[8] == stat_fields[5] )) && state+=+
ttyname "${stat_fields[7]}"; tty=$REPLY
start=$((boottime-(stat_fields[22] / sys_clk_tck)))
cpu=$(((stat_fields[14]+stat_fields[15]) * 1000 / sys_clk_tck))
if (( start )); then
cpu=$((cpu/start)) cpu=$((${cpu::-1})).${cpu: -1}
else
cpu=0.0
fi
# if this was at least yesterday
if (( start >= time_of_day )); then
printf -v start '%(%b%d)T' "$((EPOCHSECONDS-start))"
else
printf -v start '%(%H:%M)T' "$((EPOCHSECONDS-start))"
fi
vsz=$((stat_fields[23]/1024))
rss=$((stat_fields[24] * 4096 / 1024)) # hugepages unsupported for now
mem=$((rss*1000/memtotal)) mem=$((${mem::-1})).${mem: -1}
time=$(((stat_fields[14]+stat_fields[15]) / sys_clk_tck)) # seems correct, probably slightly wrong tho???
printf -v time '%d:%02d' "$((time/60))" "$((time%60))" # ps aux seems to always use this exact format i think???
#add_process "user=${users[uid]-?}" "pid=$pid" "cpu=$cpu" "mem=$mem" "vsz=$vsz" "rss=$rss" "tty=$tty" "state=$state" "start=$start" "time=$time" "cmdline=<${cmd_line[*]}>"
add_process "${users[uid]-?}" "$pid" "$cpu" "$mem" "$vsz" "$rss" "$tty" "$state" "$start" "$time" "${cmd_line[*]}"
done
[[ $cmdline ]] && exec {cmdline}>&-
[[ $stat ]] && exec {stat}>&-
[[ $status ]] && exec {status}>&-
printall
}
almost_ps_aux