firestarter/firestarter

648 lines
17 KiB
Bash
Executable File

#! /bin/bash
#
# firestarter
# A desktop environment startup script
# Copyright (C) 2019 Vintage Salt <rehashedsalt@cock.li>
#
# Distributed under terms of the MIT license.
#
set -e
# Read-only set-once variables
declare -r _name="$(basename -- "$0")"
declare -r _sessionid="$(< /proc/self/sessionid)"
# Options
declare _optconfigdir="${XDG_CONFIG_HOME:-$HOME/.config}/$_name"
declare _optdatadir="${XDG_DATA_HOME:-$HOME/.local/share}/$_name"
declare _optrundir="${XDG_RUNTIME_DIR:-/run/user/$UID}/$_name/$DISPLAY"
declare _optlogdir="$_optdatadir/logs"
declare _optdryrun
declare -i _opthelp
declare -i _optverbose
# Working variables
declare -a _args
declare -i _hasdbus
declare _return
# Junk FD used for read waiting
exec 1023<> <(:)
# Helper functions
log() {
# Print a line to the terminal if _optverbose is greater than $2
# $2 defaults to 0
# loglevel 0: Daily-use messages
# loglevel 1: Detailed but not quite debugging
# loglevel 2: Definitely debugging
[ -z "$1" ] && return 1
if (( _optverbose >= ${2:-0} )); then
printf "%s - %s\\n" "$(date -Iseconds)" "$1"
fi
}
warn() {
# Print a yellow line to the terminal, respecting _optverbose
[ -z "$1" ] && return 1
if (( _optverbose >= ${2:-0} )); then
if [ -t 1 ]; then
printf "\\e[33m%s\\e[0m\\n" "$1"
else
printf "%s - WARN: %s\\n" "$(date -Iseconds)" "$1"
fi
fi
}
error() {
# Print a red line to the terminal, exit if $2 is specified
[ -z "$1" ] && return 1
if [ -t 2 ]; then
printf "\\e[31m%s\\e[0m\\n" "$1" 1>&2
else
printf "ERROR: %s\\n" "$1" 1>&2
fi
[ -z "$2" ] && return
exit "${2:-1}"
}
has() {
# Parse out all arguments and try to find them in path
# If an argument cannot be found, set _return and fail
for prog in "$@"; do
if ! command -v "$prog" > /dev/null 2>&1; then
_return="$prog"
return 1
fi
done
return 0
}
# Core program functions
gettarget() {
# Parse a defaults file to get a target program
[ -z "$1" ] && return 1
[ -d "$1" ] && return 1
[ -r "$1" ] || return 1
# Every odd line is a condition
# Every even one is a target
if [ -d "$1" ]; then
warn "Target is a directory: $1" 2
return 50
fi
if ! [ -x "$1" ]; then
warn "Target is disabled: $1" 2
return 51
fi
local firstline
while read -r checkline; do
if [ -z "$firstline" ]; then
if [ "$checkline" = "#.fsdefaults" ]; then
firstline=1
continue
else
return 50
fi
fi
[ "${checkline#"#"}" != "$checkline" ] && continue
read -r execline
if bash -c "$checkline" > /dev/null 2>&1; then
_return="$execline"
return 0
else
continue
fi
done < "$1"
return 2
}
fskill() {
# Kill any number of PIDs, validating inputs n stuff
for pid in $@; do
# Sanity checks
[ -z "$pid" ] && continue
[ "$pid" -gt 0 ] 2> /dev/null || warn "Invalid PID: $pid" 2
[ -d "/proc/$pid" ] || warn "Process is already dead: $pid" 2
kill "$pid"
(
exec 1023<> <(:)
for (( i=0; i<10; i++ )); do
kill -0 "$pid" 2> /dev/null || return 0
read -t 1 -u 1023 || :
done
warn "Force stopping unresponsive process: $pid"
kill -9 "$pid"
)
done
}
fsstop() {
# Stop a running service by name
for service in $@; do
pidfile="$_optrundir"/"$service".pid
if ! [ -f "$pidfile" ]; then
warn "Service is not already running: $service"
continue
fi
pid="$(< $pidfile)"
[ "$pid" -gt 0 ] 2> /dev/null || error "PID is invalid for service: $service (PID $pid)" 51
[ -d "/proc/$pid" ] || warn "Service is already dead: $service"
[ -d "/proc/$pid" ] && fskill "$pid"
rm "$pidfile"
return 0
done
}
fsexec() {
# Execute an fsdefaults file
[ -z "$1" ] && return 1
local pid
local file="$1"
local filename="$(basename -- "$file")"
local logfile="$_optlogdir/$filename.log"
log "Inspecting configuration file: $filename" 2
if gettarget "$file"; then
# It's a defaults file with a selected target
log "File is an fsdefaults file" 2
target="$_return"
log "Found target for $filename: \"$target\""
[ -n "$_optdryrun" ] && return
if [ -f "$logfile" ]; then
[ -f "$logfile.old" ] && rm "$logfile.old"
mv "$logfile" "$logfile.old"
fi
bash -c "$target" > "$logfile" 2>&1 &
pid="$!"
elif [ $? = 50 ] && [ -x "$file" ]; then
# It's a shell script or executable symlink
log "File is an executable" 2
log "Executing file: \"$filename\""
[ -n "$_optdryrun" ] && return
"$file" > "$logfile" 2>&1 &
pid="$!"
else
warn "Could not execute target: \"$filename\""
fi
if [ -n "$pid" ]; then
[ -d "$_optrundir" ] || error "Run directory does not exist: $_optrundir"
printf "$pid" > "$_optrundir/$filename.pid"
fi
}
fslist() {
# List all valid services and what they point to
for file in "$_optconfigdir"/*; do
if ! [ -e "$file" ]; then
error "No configuration files found" 70
fi
# Skip our logs directory
[ -d "$file" ] && continue
if [ -t 1 ]; then
local len=16
local status
local errrorline
if gettarget "$file"; then
status="\e[32m●\e[0m"
errorline="$_return"
else
local targeterror="$?"
case $targeterror in
2)
status="\e[31m●\e[0m"
errorline="\e[31mNo matches found\e[0m"
;;
50)
status="\e[33m●\e[0m"
errorline="\e[33mNot an fsdefaults service\e[0m"
;;
51)
status="\e[90m○\e[0m"
errorline="\e[90mDisabled\e[0m"
;;
*)
status="\e[31m●\e[0m"
errorline="\e[31mNot a valid file\e[0m"
;;
esac
fi
printf "$status %-${len}.${len}s$errorline\n" "$(basename -- "$file")"
else
if gettarget "$file"; then
echo "$(basename -- "$file")"
fi
fi
done
}
fsstatus() {
# List statistics about firestarter
if [ -z "$FIRESTARTER" ]; then
printf "\e[31m●\e[0m Not running\n"
exit 1
fi
# Current process status
local psline
if [ -d "/proc/$FIRESTARTER" ]; then
psline="\e[32m●\e[0m Running (PID $FIRESTARTER)"
else
psline="\e[31m●\e[0m Dead (PID $FIRESTARTER)"
fi
printf "$psline\n"
# Display information
local displayline
if [ "$FIRESTARTER_DISPLAY" == "$DISPLAY" ]; then
displayline="On display: \e[32m$FIRESTARTER_DISPLAY\e[0m"
elif [ -z "$FIRESTARTER_DISPLAY" ]; then
displayline="On display: \e[31mUnset\e[0m"
else
displayline="On display: \e[31m$FIRESTARTER_DISPLAY\e[0m (currently on $DISPLAY)"
fi
printf "\t$displayline\n"
# Configuration information
if [ -n "$FS_DIEONWM" ]; then
if gettarget "$_optconfigdir/wm"; then
printf "\tWill die when \e[34m$_return\e[0m exits\n"
else
local targeterror=$?
local errorline="\t\e[31mFS_DIEONWM is set but"
case $targeterror in
2)
errorline="$errorline wm has no matches"
;;
50)
errorline="$errorline wm is not a Firestarter service"
;;
51)
errorline="$errorline wm is disabled"
;;
*)
errorline="$errorline wm did not resolve"
;;
esac
printf "$errorline\n"
fi
fi
# Service information
if [ -d "$_optrundir" ]; then
local len=16
local status
local description
# TODO: This does not handle services that are running but had their configs removed
for file in $(echo "$_optconfigdir"/*); do
[ -d "$file" ] && continue
local name="$(basename -- "$file" .pid)"
local service="$_optconfigdir/$name"
local pidfile="$_optrundir/$name.pid"
local pid=""
if [ -f "$pidfile" ]; then
# Service has a pidfile and SHOULD be running
local pid="$(< "$pidfile")"
if [ -z "$pid" ]; then
# PID is empty
status="\e[31m○\e[0m"
description="No PID"
elif ! [ "$pid" -gt 0 ] 2> /dev/null; then
# PID is not a number greater than 0
status="\e[31m○\e[0m"
description="\e[31mInvalid PID\e[0m ($pid)"
elif ! [ -d "/proc/$pid" ]; then
# PID is valid, but does not exist in /proc (i.e. is dead)
status="\e[31m●\e[0m"
description="\e[31mDead\e[0m (PID $pid)"
elif ! [ -x "$service" ]; then
# PID is good, but service is not enabled
status="\e[33m●\e[0m"
description="Running, disabled (PID $pid)"
else
# PID is good, time for secondary validation
status="\e[32m●\e[0m"
description="Running (PID $pid)"
fi
else
# Service does not have a pidfile and SHOULD be stopped
if [ -x "$service" ]; then
status="\e[90m●\e[0m"
description="\e[90mStopped\e[0m"
elif ! [ -x "$service" ]; then
status="\e[90m○\e[0m"
description="\e[90mDisabled\e[0m"
fi
fi
printf "\t$status %-${len}.${len}s $description\n" "$name"
done
fi
}
step_preexecute() {
# Special things that can't use simple configuration files
[ -n "$_optdryrun" ] && return 0
# Execute a user rc if it exists
[ -r "$HOME/.firestarterrc" ] && . "$HOME/.firestarterrc"
[ -n "$FIRESTARTER" ] && [ "$FIRESTARTER_DISPLAY" == "$DISPLAY" ] && error "Firestarter is already running on $DISPLAY: $FIRESTARTER" 55
export FIRESTARTER="$BASHPID"
export FIRESTARTER_DISPLAY="$DISPLAY"
export XDG_CURRENT_DESKTOP="${XDG_CURRENT_DESKTOP:-firestarter}"
# Create required directories
for dir in $_optconfigdir $_optdatadir $_optrundir $_optlogdir; do
if [ -z "$dir" ]; then
error "A required directory was not provided" 41
fi
if ! mkdir -p "$dir"; then
error "Failed to create critical directory: $dir" 41
fi
done
# dbus
if [ -n "$DBUS_SESSION_BUS_ADDRESS" ]; then
# We already have a bus started; use it
export DBUS_SESSION_BUS_ADDRESS="unix:path=$XDG_RUNTIME_DIR/bus"
_hasdbus=1
elif [ -z "$DBUS_SESSION_BUS_ADDRESS" ] && has dbus-launch; then
# We have dbus but haven't started it yet
eval "$(dbus-launch --exit-with-session --sh-syntax)"
_hasdbus=1
else
warn "Did not start dbus; some applications may misbehave"
fi
# Nest protection
if [ -n "$_hasdbus" ]; then
log "Exporting to dbus: FIRESTARTER FIRESTARTER_DISPLAY"
dbus-update-activation-environment --systemd FIRESTARTER FIRESTARTER_DISPLAY
fi
# IME settings
if has uim; then
export GTK_IM_MODULE='uim'
export QT_IM_MODULE='uim'
export XMODIFIERS='@im=uim'
elif has ibus; then
export GTK_IM_MODULE='ibus'
export QT_IM_MODULE='ibus'
export XMODIFIERS='@im=ibus'
fi
if [ -n "$_hasdbus" ]; then
log "Exporting to dbus: GTK_IM_MODULE QT_IM_MODULE XMODIFIERS"
dbus-update-activation-environment --systemd GTK_IM_MODULE QT_IM_MODULE XMODIFIERS
fi
# kcminit/Qt settings
if has qt5ct; then
log "Initializing qt5ct"
if [ -z "$QT_QPA_PLATFORMTHEME" ]; then
export QT_QPA_PLATFORMTHEME="qt5ct"
log "Exporting QT_QPA_PLATFORMTHEME as \"$QT_QPA_PLATFORMTHEME\"" 2
else
log "Using existing theme setting \"$QT_QPA_PLATFORMTHEME\"" 2
fi
if [ -z "$QT_AUTO_SCREEN_SCALE_FACTOR" ]; then
export QT_AUTO_SCREEN_SCALE_FACTOR="0"
log "Exporting QT_AUTO_SCREEN_SCALE_FACTOR as \"$QT_AUTO_SCREEN_SCALE_FACTOR\"" 2
else
log "Using existing scale factor \"$QT_AUTO_SCREEN_SCALE_FACTOR\"" 2
fi
if [ -n "$_hasdbus" ]; then
log "Exporting to dbus: QT_QPA_PLATFORMTHEME QT_AUTO_SCREEN_SCALE_FACTOR"
dbus-update-activation-environment --systemd QT_QPA_PLATFORMTHEME QT_AUTO_SCREEN_SCALE_FACTOR
fi
fi
if has kcminit; then
log "Initializing KDE Control Module settings"
kcminit >/dev/null 2>&1
# Disabled here because an XDGCD of KDE implies kded and other KDE parts, breaking copypasta and other things
#export XDG_CURRENT_DESKTOP="KDE"
fi
# X-specific stuff
if [ -n "$DISPLAY" ]; then
# xhost
if has xhost; then
if xhost +si:localuser:"$(id -un)" >/dev/null 2>&1; then
log "Session open to other sessions by this user"
else
warn "Failed to open session via xhost"
fi
fi
# xresources
if has xrdb && [ -r "$HOME/.Xresources" ]; then
if xrdb "$HOME/.Xresources" >/dev/null 2>&1; then
log "Loaded in .Xresources"
else
warn "Failed to load .Xresources"
fi
fi
# xsetroot
if has xsetroot; then
log "Setting root window properties"
xsetroot -cursor_name left_ptr
fi
# xset
if has xset; then
log "Disabling bell"
xset -b
log "Disabling DPMS"
xset s off -dpms
fi
fi
}
step_postexecute() {
# Wait for the WM to initialize, if one was found and we have the tools
if [ -z "$FS_NOWAITWM" ] && gettarget "$_optconfigdir/wm" && has xprop grep; then
log "Waiting for WM to initialize: \"$_return\""
for (( i=0; i<10; i++ )); do
line="$(xprop -root | grep ^_NET_WM_NAME | grep -o '"\S*"$')"
if [ -n "$line" ]; then
log "WM has initialized, _NET_WM_NAME atom reads: $line"
break
fi
read -t 1 -u 1023
done
fi
# Dumb polybar workaround
killall polybar -SIGUSR1
# Propogate environment variables
if [ -n "$_hasdbus" ]; then
has dbus-update-activation-environment && \
dbus-update-activation-environment --verbose --systemd --all >/dev/null 2>&1
fi
# Start XDG autostarters, if they exist
if [ -z "$_optdryrun" ]; then
if has dex; then
dex -a >/dev/null 2>&1
elif has fbautostart; then
fbautostart > /dev/null 2>&1
elif has xdg-autostart; then
xdg-autostart ${XDG_CURRENT_DESKTOP:-firestarter}
else
warn "Could not find an XDG autostarter"
fi
fi
}
step_wait() {
[ -n "$_optdryrun" ] && exit 0
trap step_logout EXIT
log "Checking for window manager" 2
if [ -n "$FS_DIEONWM" ] && gettarget "$_optconfigdir/wm" && has strings; then
target="$_return"
for job in $(jobs -p); do
# Trailing space here is due to an idiosyncracy with strings
if [ "$target " = "$(cat /proc/$job/cmdline | strings -1 -s ' ')" ]; then
log "Waiting for WM to exit: \"$target\""
wait "$job"
exit 0
fi
done
warn "Could not find WM: \"$target\""
fi
log "Waiting for programs to exit"
wait
exit 0
}
step_logout() {
log "Logging out"
if [ -n "$_optrundir" ] && [ -d "$_optrundir" ]; then
rm -rf "$_optrundir"
fi
if has loginctl && [ -z "$FS_NOLOGINCTL" ]; then
# Use loginctl if possible
if [ -n "$_sessionid" ]; then
loginctl terminate-session "$_sessionid"
fi
else
# Otherwise just brute it out
fskill $(jobs -p)
fi
return 0
}
printhelp() {
cat << EOF
Usage: $_name [OPTION]... {COMMAND}
-d Perform a dry run; print what programs would have been
executed instead of doing so
-h Print this help text
-v Print more status messages. Stacks
Commands:
If no command is specified, firestarter will behave as though init had been
provided
init Start all services and wait
list List all valid services
logout Log out of an existing Firestarter session
restart Stop and restart the provided services
start Start the provided services, fork, and exit
status Show status information
stop Stop the provided services
Environment Variables:
FS_DIEONWM If nonempty, end the session when the WM dies.
FS_NOLOGINCTL Don't invoke loginctl to end the session. Good for
testing.
FS_NOWAITWM If nonempty, skip waiting for the WM to initialize
Copyright (c) 2019 rehashedsalt@cock.li
Licensed under the MIT license
EOF
}
firestart() {
# Really main firestarter function
local action="${_args[0]}"
[ -z "$action" ] && action=init
case "$action" in
init)
[ -n "$_optdryrun" ] && log "Performing a dry run"
step_preexecute
for file in "$_optconfigdir"/*; do
if ! [ -e "$file" ]; then
error "No configuration files found" 70
fi
# Skip our logs directory
[ "$_optlogdir" == "$file" ] && continue
fsexec "$file"
done
step_wait
step_logout
;;
ls|list)
fslist
;;
restart)
for file in "${_args[@]:1}"; do
fsstop "$file"
fsexec "$_optconfigdir"/"$file"
done
;;
start)
for file in "${_args[@]:1}"; do
fsexec "$_optconfigdir"/"$file"
done
;;
st|stat|status)
fsstatus
;;
stop)
for file in "${_args[@]:1}"; do
fsstop "$file"
done
;;
logout)
if [ -n "$FIRESTARTER" ] && [ -d "/proc/$FIRESTARTER" ] && [ "$FIRESTARTER" -gt 0 ] 2> /dev/null; then
log "Killing PID $FIRESTARTER"
kill $FIRESTARTER
else
error "\$FIRESTARTER is unset or references a dead process" 1
fi
;;
*)
error "Unknown action: $action" 51
;;
esac
}
# Main
main() {
# Parse out arguments
while [ -n "$1" ]; do
# Parse out flags
while getopts ":dhv" opt; do
case $opt in
d)
_optdryrun=1
;;
h)
_opthelp=1
;;
v)
_optverbose+=1
;;
:)
error "Option requires argument: -$OPTARG" 2
;;
*)
error "Invalid option: -$OPTARG" 2
;;
esac
done
# Store arguments
shift $((OPTIND - 1))
if [ -n "$1" ]; then
_args+=("$1")
shift
fi
unset OPTIND
done
# Early hook for help
[ -n "$_opthelp" ] && printhelp && exit 0
# Ensure our running environment is sane
if ! [ -d "$HOME" ] || ! [ -r "$HOME" ]; then
error "Home directory not found or inaccessable: \"$HOME\"" 54
fi
if ! [ -d "$_optconfigdir" ]; then
if [ -f "$_optconfigdir" ]; then
error "Config directory is a file, should be directory: \"$_optconfigdir\"" 52
fi
if ! mkdir -p "$_optconfigdir" > /dev/null 2>&1; then
error "Failed to find or create config directory: \"$_optconfigdir\"" 52
fi
fi
# Validate core program dependencies
log "Validating dependencies" 2
if ! has basename sort; then
error "Failed to find program: $_return" 1
fi
# Fixes random SIGALRM bug
trap : SIGALRM
# Do the do
firestart
exit 0
}
main "$@"