#!/bin/sh -eu # shellcheck disable=SC2059 # use of $bold et al. in printf format str # defaults use_bundled_libva_default=0 # if not run from AppImage (eg. extracted), use default values export APPIMAGE="${APPIMAGE-none}" ARGV0=${ARGV0-$0} bold=$(tput bold || true) red=$(tput setaf 1 || true) reset=$(tput sgr0 || true) bold_str=${bold}%s${reset} DIR=$(dirname "$(realpath "$0")") readonly AI_LIB_PATH="$DIR"/usr/lib n_ld_library_path="$AI_LIB_PATH"${LD_LIBRARY_PATH:+":$LD_LIBRARY_PATH"} LD_PRELOAD=${LD_PRELOAD-} # there is an issue with running_from_path() which evaluates this executable # as being system-installed #export PATH=$DIR/usr/bin:$PATH export QT_QPA_FONTDIR="$DIR/usr/share/fonts" export UG_FONT_DIR="$DIR/usr/share/fonts" QT_PLUGIN_PATH=$(set -- "$DIR"/usr/lib/qt?/plugins; echo "$1") export QT_PLUGIN_PATH export QT_QPA_PLATFORM_PLUGIN_PATH="$QT_PLUGIN_PATH/platforms" export MAGICK_CONFIGURE_PATH="$DIR/etc/IM" export MAGICK_CODER_FILTER_PATH="$DIR/usr/share/IM/filters" export MAGICK_CODER_MODULE_PATH="$DIR/usr/share/IM/coders" get_tools() {( find "$DIR/usr/bin" -mindepth 1 -exec basename {} \; | tr '\n' ' ' )} # handle symlinks, so eg. if symlinked to hd-rum-transcode, ruh the transcoder # (no need to use `U.traGrid...AppImage -o hd-rum-transcode`) get_tool_from_progname() {( prog=$(basename "${ARGV0?}") # shellcheck disable=SC2046 # intentional set -- $(get_tools) while [ $# -gt 0 ]; do if [ "$prog" = "$1" ]; then echo "$1" return fi shift done )} # create convenience symlinks for the AppImage mk_symlinks() {( if [ "$APPIMAGE" = none ]; then abs_path=$(realpath "$0") dstdir=. else abs_path=$APPIMAGE dstdir=$(dirname "$APPIMAGE") fi printf "Creating symlinks for ${bold_str}in $bold_str pointing to \ ${bold_str}...\n" "$(get_tools)" "$dstdir" "$abs_path" cd "$dstdir" # shellcheck disable=SC2046 # intentional set -- $(get_tools) while [ $# -gt 0 ]; do ln -s "$abs_path" "$1" || true shift done )} ## @param $1 non-empty to show full help usage() { full=${1:+1} printf "usage:\n" printf "\t${bold}${red}%s${reset} ${bold}[--gui [args]]${reset}\n" "$ARGV0" printf "\t\tinvokes GUI\n" printf "\n" printf "\t${bold}${red}%s${reset} ${bold}--appimage-help${reset}\n" "$ARGV0" printf "\t\tprints AppImage related options\n" printf "\n" printf "\t${bold}${red}%s${reset} ${bold}-h | --help | --fullhelp${reset}\n" "$ARGV0" printf "\t\tprints this help (extended version with ${bold}--fullhelp${reset})\n" printf "\n" printf "\t${bold}${red}%s${reset} ${bold}--ug-help${reset}\n" "$ARGV0" printf "\t\tprints UltraGrid help (as '-h' if it weren't run from AppImage)\n" printf "\n" printf "\t${bold}${red}%s${reset} ${bold}-m|--man [uv|hd-rum-transcode]${reset}\n" "$ARGV0" printf "\t\tprints manual page\n" printf "\n" printf "\t${bold}${red}%s${reset} ${bold}-u|--update [args]${reset}\n" "$ARGV0" printf "\t\tupdates AppImage (args will be passed to appimageupdatetool)\n" printf "\n" printf "\t${bold}${red}%s${reset} ${bold}-o|--tool uv --help${reset}\n" "$ARGV0" printf "\t\tprints command-line UltraGrid help\n" printf "\n" printf "\t${bold}${red}%s${reset} ${bold}-o|--tool [args]${reset}\n" "$ARGV0" printf "\t\tinvokes specified tool\n" printf "\t\ttool may be one of: ${bold}%s${reset}\n" "$(get_tools)" printf "\n" if [ "$full" ]; then printf "\t${bold}${red}%s${reset} ${bold}--mk-symlinks${reset}\n" "$ARGV0" printf "\t\tmake symlinks for the above tools to allow direct run\n" printf "\n" fi printf "\t${bold}${red}%s${reset} ${bold}args${reset}\n" "$ARGV0" printf "\t\tinvokes command-line UltraGrid\n" printf "\n" if [ ! "$full" ]; then return fi printf "environment variables:\n" printf "\tAPPIMAGE_DEBUG: print debug info + used Firejail options\n" printf "\tULTRAGRID_AUTOUPDATE: autoupdate interval in days (0 - check always); -1 - disable update advice\n" printf "\tULTRAGRID_BUNDLED_LIBVA: 1 - use bundled libva; 0 - use system libva (if available), default %d\n" $use_bundled_libva_default printf "\tULTRAGRID_USE_FIREJAIL: run the UltraGrid executable with firejail. If\n" printf "\t the variable contains a profile path (ends with\n" printf "\t '.profile'), it will be used.\n" printf "\tFIREJAIL_OPTS: custom options to be passed to Firejail\n" printf "\n" } update_check_days=90 ## Trigger update if $ULTRAGRID_AUTOUPDATE days passed. ## If $ULTRAGRID_AUTOUPDATE unset or empty, print update hint if UG binary is older than $update_check_days days. handle_autoupdates() {( if [ "${ULTRAGRID_AUTOUPDATE-0}" -eq -1 ]; then return fi if [ "$APPIMAGE" = none ]; then return fi if expr "$APPIMAGE" : '.*continuous' > /dev/null; then update_check_days=30 fi if [ "${ULTRAGRID_AUTOUPDATE-}" ] && [ "$ULTRAGRID_AUTOUPDATE" -ge 0 ]; then update_check_days=$ULTRAGRID_AUTOUPDATE fi appimage_mtime=$(stat -c %Y "$APPIMAGE") curr_timestamp=$(date +%s) if [ "$curr_timestamp" -lt $((appimage_mtime + update_check_days * 24 * 60 * 60)) ]; then return fi if [ "${ULTRAGRID_AUTOUPDATE-}" ] && [ "$ULTRAGRID_AUTOUPDATE" -ge 0 ]; then echo "Running the UltraGrid update because the interval" \ "specified by ULTRAGRID_AUTOUPDATE env variable has passed..." # update AppImage mtime otherwise if there are no updates # after elapsing the interval, the check would run always touch "$APPIMAGE" if "$DIR/appimageupdatetool" -j "$APPIMAGE"; then debug_msg "No UltraGrid AppImage update available" else if "$DIR/appimageupdatetool" "$APPIMAGE"; then # prevent update loops if the new version not # stored for some reason ULTRAGRID_AUTOUPDATE=-1 exec "$APPIMAGE" "$@" # launch updated version fi fi return fi printf "UltraGrid binary older than %d days, consider checking updates:\n" "$update_check_days" printf "\n" printf "%s -u\n" "$ARGV0" printf "\t- updates AppImage\n" printf "%s -u -j; [ \$? -eq 1 ] && echo Update available || echo No update available\n" "$ARGV0" printf "\t- check for update without actually updating\n" printf "%s -u -h\n" "$ARGV0" printf "\t- prints update options\n" printf "\n" printf "Hint: you can set environment variable ULTRAGRID_AUTOUPDATE to 1 for daily automatic update or -1 to suppress the above message.\n" printf "\n" )} debug_msg() { if [ -z "${APPIMAGE_DEBUG:-}" ]; then return fi echo "$@" } ## Tries to find system libva. If found it is preloaded (+libva-drm+libva-x11) ## @retval 0 if preloaded; 1 otherwise set_libva_ld_preload() { [ -f "$AI_LIB_PATH/ultragrid/ultragrid_vcompress_libavcodec.so" ] || return 0 LOADER=$(get_loader) [ -x "$LOADER" ] || return 1 get_ai_lib_path() {( LD_LIBRARY_PATH=$n_ld_library_path LD_TRACE_LOADED_OBJECTS=1 \ "$LOADER" "$AI_LIB_PATH/ultragrid/ultragrid_vcompress_\ libavcodec.so" | grep "$1" | grep -v 'not found' | awk '{print $3}' )} AI_LIBAVCODEC_LIB=$(get_ai_lib_path libavcodec.so) check_libva_deps() { # check if all liva*.so satisfied from sys [ -n "$1" ] || return 1 ! LD_TRACE_LOADED_OBJECTS=1 "$LOADER" "$1" | grep -q 'libva.*not found' } check_libva_deps "$AI_LIBAVCODEC_LIB" && check_libva_deps \ "$(get_ai_lib_path libavutil.so)" || return 1 LIBVA_LIB=$(LD_TRACE_LOADED_OBJECTS=1 "$LOADER" "$AI_LIBAVCODEC_LIB" | grep libva.so | grep -v 'not found' | awk '{print $3}') [ -n "$LIBVA_LIB" ] || return 1 libva_libs=$LIBVA_LIB # add also libva-drm, libva-x11 if present for n in libva-drm libva-x11; do NAME=$(echo "$LIBVA_LIB" | sed s/libva/$n/) if [ -f "$NAME" ]; then libva_libs=$libva_libs:$NAME fi done # preload also VDPAU libvdpau_lib=$(LD_TRACE_LOADED_OBJECTS=1 "$LOADER" "$AI_LIBAVCODEC_LIB"\ | grep libvdpau.so | grep -v 'not found' | awk '{print $3}') if [ -n "$libvdpau_lib" ]; then libva_libs=$libva_libs:$libvdpau_lib fi S_LD_PRELOAD=$LD_PRELOAD export LD_PRELOAD="$libva_libs${LD_PRELOAD:+:$LD_PRELOAD}" # check if lavc still binds with system libva if LD_LIBRARY_PATH=$n_ld_library_path LD_BIND_NOW=1 LD_WARN=1 \ LD_TRACE_LOADED_OBJECTS=1 "$LOADER" \ "$AI_LIBAVCODEC_LIB" 2>&1 | grep -q 'undefined symbol'; then LD_PRELOAD=$S_LD_PRELOAD return 1 fi } ## Tries to set LD_PRELOAD to system libva (see set_libva_ld_preload()). If failed, sets path to bundled libva drivers. setup_vaapi() { ULTRAGRID_BUNDLED_LIBVA=${ULTRAGRID_BUNDLED_LIBVA:-$use_bundled_libva_default} if [ "$ULTRAGRID_BUNDLED_LIBVA" -ne 1 ]; then if ! set_libva_ld_preload; then echo "${bold}${red}Could not set system libva, using bundled libraries instead!${reset}" >&2 ULTRAGRID_BUNDLED_LIBVA=1 fi fi if [ "$ULTRAGRID_BUNDLED_LIBVA" -eq 1 ]; then if [ -d "$DIR/usr/lib/va" ] && [ -z "${LIBVA_DRIVERS_PATH:-}" ]; then export LIBVA_DRIVERS_PATH="$AI_LIB_PATH/va" fi fi } setup_wayland() { # use bundled Wayland libs only when not present in system sdl_l=$DIR/usr/lib/libSDL2-2.0.so.0 LOADER=$(get_loader) if [ -x "$LOADER" ] && [ -f "$sdl_l" ]; then ldd=$(LD_TRACE_LOADED_OBJECTS=1 "$LOADER" "$sdl_l") if ! echo "$ldd" | grep libwayland | grep -q 'not found'; then return # we have libwayland* in system fi elif [ -n "${WAYLAND_DISPLAY-}" ]; then # fallback - don't have SDL2, return # use env var instead fi debug_msg "Using bundled libwayland*" n_ld_library_path="$n_ld_library_path:$AI_LIB_PATH/wayland" } add_whitelist() { # expand tilde expansion (like ~/video.mp4; only single ~ now) # shellcheck disable=SC2088 # intentional if ! expr "$1" : '~/' >/dev/null; then path="$1" else path"$HOME${1#\~}" fi if contains_mount "$path"; then disable_mount=no fi if expr "$path" : /tmp >/dev/null; then printf -- "$separator--read-write=\"%s\" --mkdir=\"%s\"" "$path" "$path" else printf -- "$separator--whitelist=\"%s\"" "$path" fi separator=' ' abs_path=$(realpath "$path") # resolve also symlinks if [ "$abs_path" != "$path" ]; then add_whitelist "$abs_path" fi } # whitelist path that may not exist (file that will be created), so eg. also the parent whitelist_file_path() { path=$1 add_whitelist "$path" if expr "$n" : 'dump:' >/dev/null || expr "$n" : '--record' >/dev/null then printf -- "$separator--mkdir=\"%s\"" "$path" # may not exist fi if expr "$n" : '.*file[:=]' >/dev/null || expr "$n" : '.*filename' \ >/dev/null; then parent_dir=$(dirname "path") if [ "$parent_dir" = /dev ] || [ "$parent_dir" = /tmp ]; then return fi # whitelist the parent folder to allow file creation printf -- "$separator--whitelist=\"%s\"" "$parent_dir" fi } contains_mount() { expr "$1" : '.*/mnt' >/dev/null || expr "$1" : '.*/media' >/dev/null || expr "$1" : '.*/run/mount' >/dev/null || expr "$1" : '.*/run/media' >/dev/null } ## Parse params to get custom rules like whitelists that may be needed to add ## @todo spaces inside paths doesn't work get_custom_firejail_rules() { separator='' playback_path=$(echo "$@" | sed -n 's/.*--playback \([^ :]*\).*/\1/p') if [ -n "$playback_path" ]; then add_whitelist "$playback_path" fi # print every argument of "filename=", "file[:=]", "dump:" or "--record=" pattern for n in "$@"; do file_path=$(echo "$n" | sed -n \ -e 's/.*filename=\([^:]*\).*/\1/p' \ -e 's/.*\(n\|name\)=\([^:]*\).*/\2/p' \ -e 's/.*file[:=]\([^:]*\).*/\1/p' \ -e 's/dump:\([^:]*\).*/\1/p' \ -e 's/^--record=\([^:]*\).*/\1/p') if [ -n "$file_path" ]; then whitelist_file_path "$file_path" fi done # -d dump or -d dummy:dump_to_file if expr "$*" : '.*dump' >/dev/null || expr "$*" : '.*record' >/dev/null; then add_whitelist "$(pwd)" fi if ! expr "$DIR" : /tmp >/dev/null; then # add AppRun path if not in tmp (== extracted AppImage) add_whitelist "$DIR" fi if [ -z "$separator" ]; then # no whitelist was set printf -- "--private" fi # screen pw - requires root (could be disabled by default.profile) and dbus if expr "$*" : '.*screen' >/dev/null; then printf " --ignore=noroot" elif firejail --version | grep -iq "d-\{0,1\}bus.*enabled"; then printf " --dbus-system=filter \ --dbus-system.talk=org.freedesktop.Avahi" fi if ! contains_mount "$*" && [ "${disable_mount-}" != no ] then printf " --disable-mnt" fi } # shellcheck source=/dev/null . "$DIR/scripts/preload.sh" setup_vaapi setup_wayland # ultragrid_aplay_jack.so is not used because it loads JACK with dlopen, # while portaudio is linked directly to JACK library set_ld_preload ultragrid_aplay_portaudio.so libjack set_ld_preload ultragrid_vidcap_pipewire.so libpipewire ## @param $@ /dev/null || { echo "Firejail not present in system!"; exit 1; } fj_opt_file=$(mktemp) if expr "$ULTRAGRID_USE_FIREJAIL" : '.*\.profile' >/dev/null; then FIREJAIL_OPTS="${FIREJAIL_OPTS+$FIREJAIL_OPTS }--profile=$ULTRAGRID_USE_FIREJAIL" else FJ_TMPDIR=${TMPDIR-/tmp/ultragrid-$(id -u)} FIREJAIL_OPTS="${FIREJAIL_OPTS+$FIREJAIL_OPTS }--caps.drop=all\ --nonewprivs --protocol=unix,inet,inet6,netlink --seccomp\ --private-bin=bash --private-opt=none --mkdir=$FJ_TMPDIR\ --read-write=$FJ_TMPDIR --writable-var --ignore=private-tmp" FIREJAIL_OPTS="$FIREJAIL_OPTS $(get_custom_firejail_rules "$@") --private-etc=alsa,group,hostname,ld.so.conf,ld.so.cache,ld.so.conf.d,nsswitch.conf,passwd,resolv.conf --ignore=novideo" if ! expr "$FIREJAIL_OPTS" : '.*--read-write="/\{1,\}tmp/*"' \ > /dev/null; then FIREJAIL_OPTS="--read-only=/tmp $FIREJAIL_OPTS" fi fi if firejail --help | grep -q -- --keep-var-tmp; then FIREJAIL_OPTS="$FIREJAIL_OPTS --keep-var-tmp" fi if [ "$tool" = hd-rum-av.sh ]; then FIREJAIL_OPTS="$FIREJAIL_OPTS --private-bin=basename,dirname,\ expr,kill,ps,sed,seq,sh,tput,tr,tty,uname" fi echo "firejail --env=LD_PRELOAD=${LD_PRELOAD} --env=LD_LIBRARY_PATH=${n_ld_library_path}${FJ_TMPDIR+ --env=TMPDIR=${FJ_TMPDIR}} $FIREJAIL_OPTS " > "$fj_opt_file" if [ "${APPIMAGE_DEBUG:-}" ]; then opts=$(cat "$fj_opt_file") printf "Firejail arguments:\n\t%s\n\n" "$opts" fi } # run only if Firejail is used to workaround not restored TIO by UG if FJ kills # the UG process prematurelly; otherwise not needed (exec is used) fj_sigaction() { trap '' TERM # inhibit following signal to ourselves if ps -o cmd >/dev/null 2>&1; then pgid=$(ps -o pgid= -p $$ | tr -d ' ') fi if [ $$ -eq "$pgid" ]; then kill -- -$$ else echo "pgid $pgid not pid of the script ($$), not sending kill" \ >&2 fi trap - INT TERM wait stty "$stty_orig" echo "Restored TIO" >&2 } run() { setup_firejail "$@" tool=$1 shift export LD_LIBRARY_PATH="$n_ld_library_path" if [ -n "${fj_opt_file-}" ] && [ "$tool" != uv-qt ]; then stty_orig=$(stty -g) trap fj_sigaction INT TERM # shellcheck disable=SC2046 # intentional eval $(cat "$fj_opt_file"; rm "$fj_opt_file") "$DIR/usr/bin/$tool" '"$@"' else exec "$DIR/usr/bin/$tool" "$@" fi } # TODO remove if [ "${FIREJAIL_DEBUG-}" ]; then export APPIMAGE_DEBUG="$FIREJAIL_DEBUG" printf "${bold}Use APPIMAGE_DEBUG instead of FIREJAIL_DEBUG \ (replacement)${reset}\n" fi if [ "${APPIMAGE_DEBUG-undef}" = undef ] && [ "${ULTRAGRID_VERBOSE-}" ]; then APPIMAGE_DEBUG=$ULTRAGRID_VERBOSE fi tool=$(get_tool_from_progname) if [ "$tool" ]; then set -- -o "$tool" "$@" fi if [ $# -eq 0 ] || [ "${1-}" = "--gui" ]; then handle_autoupdates "$@" if [ $# -eq 0 ]; then usage; else shift; fi if [ -x "$DIR/usr/bin/uv-qt" ]; then run uv-qt --with-uv "$DIR/usr/bin/uv" "$@" else echo "GUI was not compiled in!" >&2 exit 1 fi elif [ "$1" = "-o" ] || [ "$1" = "--tool" ]; then handle_autoupdates "$@" TOOL=$2 shift 2 if [ "$TOOL" = help ]; then printf "available tools: ${bold}%s${reset}\n" "$(get_tools)" exit 0 fi # shellcheck disable=SC2086 run "$TOOL" "$@" elif [ "$1" = "-h" ] || [ "$1" = "--help" ]; then usage exit 0 elif [ "$1" = "--ug-help" ]; then run uv -h elif [ "$1" = "--fullhelp" ]; then usage full exit 0 elif { [ $# -eq 1 ] || [ $# -eq 2 ]; } && { [ "$1" = "-m" ] || [ "$1" = "--man" ]; }; then PAGE=${2:-uv} man -l "$DIR/usr/share/man/man1/$PAGE.1" elif [ "$1" = "-u" ] || [ "$1" = "--update" ]; then shift if [ -d "$DIR/appimageupdatetool-lib" ]; then export LD_LIBRARY_PATH="$DIR/appimageupdatetool-lib" fi touch "$APPIMAGE" # update AppImage mtime to avoid update notices if there are no updates avalable but were checked for "$DIR/appimageupdatetool" ${1+"$@" }"$APPIMAGE" elif [ "$1" = "--mk-symlinks" ]; then mk_symlinks else handle_autoupdates "$@" run uv "$@" fi