Created
July 29, 2024 13:32
-
-
Save CHERTS/af8269f683121ed9439332f9a1505a12 to your computer and use it in GitHub Desktop.
ps aux written entirely in bash without ever forking
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# ps aux written entirely in bash without ever forking | |
# Author: Isabella Bosia | |
# Github: https://github.com/izabera/ps/tree/develop | |
# | |
# 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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment