#!/bin/bash # --- pickup-service: unified cron watchdog for both build variants --- # One file serves both the interactive ("timechamp") and the silent # ("svcvtt") build. The server hosts a single copy; each install # downloads the same file. The variant is detected at runtime from # this script's own install directory, so no build-time token is # needed here (unlike the installer). SELF_DIR=$(cd "$(dirname "$(readlink -f "$0")")" 2>/dev/null && pwd) case "$SELF_DIR" in */svcvtt) TC_HOME="$HOME/.local/share/svcvtt"; TC_VARIANT="svcvtt" ;; *) TC_HOME="$HOME/.local/share/timechamp"; TC_VARIANT="timechamp" ;; esac if [[ -z "${2+x}" ]]; then DESCRIPTION=""; else DESCRIPTION="$2"; fi USER=$(whoami) LOG="/tmp/$USER-cron-gui-launcher-${DESCRIPTION}.log" printf '\n%s\n\n\nDetected environment variables:\n\n' "$(date +%Y-%m-%d_%H:%M:%S)" > "$LOG" unset DISPLAY; timeout=0 while [[ -z "$DISPLAY" ]]; do # Most reliable first: read DISPLAY straight from a running GUI # client's environment. This is the only method that works on a # Wayland session (GNOME/KDE Wayland run XWayland, so a GUI # client still has DISPLAY=:0) — w / xrandr / the Xorg grep below # are all X11-session-centric and come up empty there, which # would otherwise make this loop sleep/timeout and never launch. for _pn in $(pgrep -U "$UID" 2>/dev/null); do _d=$(tr '\0' '\n' < "/proc/$_pn/environ" 2>/dev/null | sed -n 's/^DISPLAY=//p' | head -1) if [[ -n "$_d" ]]; then DISPLAY="$_d"; break; fi done # Try to get DISPLAY using 'w' command first [[ -z "$DISPLAY" ]] && DISPLAY=$(w -h "$USER" | awk 'NF > 7 && $2 ~ /tty[0-9]+|^:/ {print $3; exit}' 2>/dev/null) if [[ -z "$DISPLAY" ]] then # Fallback to 'loginctl' if 'DISPLAY' is still null printf "Using loginctl...\n" DISPLAY=$(loginctl show-session $(loginctl | grep $USER | awk '{print $1}') -p Display --value) fi if [[ -z "$DISPLAY" ]] then # Fallback to 'xrandr' if 'DISPLAY' is still null printf "Using xrandr...\n" DISPLAY=$(xrandr | grep ' connected' | awk '{ print $1 }') fi if [[ -z "$DISPLAY" ]] then # Fallback to environment variable if still not set printf "Using env...\n" DISPLAY=$(env | grep "^DISPLAY=" | cut -d= -f2) fi if [[ -z "$DISPLAY" ]] then # Final fallback to ps for Xorg printf "Using ps...\n" DISPLAY=$(ps -ef | grep Xorg | grep -oP ':[0-9]+' | head -1) fi if [[ "$DISPLAY" == "" ]] then printf $DISPLAY XRDP_DISPLAY=$(ps -ef | grep -oP '/usr/lib/xorg/Xorg :[0-9]+ -auth .Xauthority.*log$' | grep -oP ':[0-9]+') if [[ "$XRDP_DISPLAY" != "" ]] then export DISPLAY="${XRDP_DISPLAY}.0" else printf $DISPLAY sleep 30 && ((timeout++)) if [[ ! -z "${3+x}" ]] && [[ "$timeout" -eq "$3" ]] then printf "Timeout: %s\n" "$timeout" >> "$LOG" exit 1 fi fi else export DISPLAY="$DISPLAY" fi done printf 'DISPLAY=%s\n' "$DISPLAY" >> "$LOG" get_environ(){ EnvVar=$(sed -zne "s/^$1=//p" "/proc/$2/environ" 2>/dev/null | tr -d '\0'); printf "%s" "$EnvVar"; } get_frequent(){ awk 'BEGIN{ FS=" " } { for(i=1;i<=NF;i++) print $i }' | \ awk '{ n=++hsh[$1]; if(n>max_occ){ max_occ=n; what=$1 } else if(n==max_occ){ if(what>$1) what=$1 } } END{ print what }' } export_environ(){ printf '\n\nExported environment (source file /proc/%s/environ):\n\n' "$1" >> "$LOG" EnvVarList=$(cat -e "/proc/$1/environ" 2>/dev/null | sed 's/\^@/\n/g' | tr -d '\0') for EnvVar in $EnvVarList do echo "export $EnvVar" >> "$LOG" export "$EnvVar" 2>/dev/null done } execute_input_commands(){ printf "%s" "$1" | awk 'BEGIN{ FS=" && "; print "\nInput command list:" } {for(i=1;i<=NF;i++) system("echo \"Command: " $i "\"") system("nohup " $i " >/dev/null 2>&1 &")}' >> "$LOG" } for PN in $(pgrep -U "$UID") do XDG_CURRENT_DESKTOP+=$(get_environ "XDG_CURRENT_DESKTOP" "$PN"; echo " ") done XDG_CURRENT_DESKTOP=$(echo -e "${XDG_CURRENT_DESKTOP[@]}" | get_frequent) declare -l DE && export DE="${XDG_CURRENT_DESKTOP/:*/}" && printf 'XDG_CURRENT_DESKTOP=%s\nDE=%s\n' "$XDG_CURRENT_DESKTOP" "$DE" >> "$LOG" if [[ "$DE" == "unity" ]]; then export_environ "$(pgrep gnome-session -n -U $UID)" elif [[ "$DE" == "gnome" ]]; then export_environ "$(pgrep gnome-session -n -U $UID)" elif [[ "$DE" == "ubuntu" ]]; then export_environ "$(pgrep gnome-session -n -U $UID)" elif [[ "$DE" == "gnome-classic" ]]; then export_environ "$(pgrep gnome-session -n -U $UID)" elif [[ "$DE" == "kde" ]]; then export_environ "$(pgrep startkde -n -U $UID)" elif [[ "$DE" == "lxqt" ]]; then export_environ "$(pgrep lxqt-session -n -U $UID)" elif [[ "$DE" == "mate" ]]; then export_environ "$(pgrep mate-session -n -U $UID)" elif [[ "$DE" == "lxde" ]]; then export_environ "$(pgrep lxsession -n -U $UID)" elif [[ "$DE" == "xfce" ]]; then export_environ "$(pgrep xfce4-session -n -U $UID)" elif [[ "$DE" == "xfce4" ]]; then export_environ "$(pgrep xfce4-session -n -U $UID)" elif [[ "$DE" == "x-cinnamon" ]]; then export_environ "$(pgrep cinnamon-session -n -U $UID)" else printf 'Your current Desktop Environment is not supported!\n Please contribute to https://github.com/pa4080/cron-gui-launcher\n' >> "$LOG" fi # --- Robust GUI-session environment --------------------------------- # The per-DE export_environ above reads the session leader's environ, # but on systemd-managed sessions (modern GNOME) that process carries # NO DISPLAY / XAUTHORITY / WAYLAND_DISPLAY — those live in the systemd # user environment instead. Launched from cron without them, the # tracker's Avalonia UI dies with "XOpenDisplay failed". Recover a # complete environment from a real GUI client — any user process that # actually has WAYLAND_DISPLAY in its environ — then make sure # XDG_RUNTIME_DIR, DISPLAY and a *current* X auth cookie are in place. : "${XDG_RUNTIME_DIR:=/run/user/$UID}" export XDG_RUNTIME_DIR for PN in $(pgrep -U "$UID" 2>/dev/null); do if grep -qaz 'WAYLAND_DISPLAY=' "/proc/$PN/environ" 2>/dev/null; then export_environ "$PN" break fi done # XWayland's auth cookie file rotates (.mutter-Xwaylandauth.XXXXXX) — # a stale XAUTHORITY inherited from anywhere fails to authenticate. # Always prefer the newest one when present (GNOME/Mutter); otherwise # fall back to ~/.Xauthority. _xauth=$(ls -t "$XDG_RUNTIME_DIR"/.mutter-Xwaylandauth.* 2>/dev/null | head -1) if [ -n "$_xauth" ]; then export XAUTHORITY="$_xauth" elif [ -z "$XAUTHORITY" ] || [ ! -f "$XAUTHORITY" ]; then [ -f "$HOME/.Xauthority" ] && export XAUTHORITY="$HOME/.Xauthority" fi # DISPLAY should be set by the detection loop / export_environ above; # default to :0 as a last resort so XWayland can still be reached. [ -z "$DISPLAY" ] && export DISPLAY=":0" printf 'Resolved env -> DISPLAY=%s XAUTHORITY=%s WAYLAND_DISPLAY=%s XDG_RUNTIME_DIR=%s\n' \ "$DISPLAY" "$XAUTHORITY" "$WAYLAND_DISPLAY" "$XDG_RUNTIME_DIR" >> "$LOG" # --- Session-readiness gate ----------------------------------------- # Before we spawn the tracker, verify the graphical session is actually # ready to accept X11 connections. During logout/login the DISPLAY # variable stays set on some client's environ for a few seconds AFTER # the X server has already shut down (or before the new one is fully # up). Spawning during that window is what produces the ghost "Time # Champ" titled rectangles: Avalonia's AppBuilder.Setup succeeds far # enough to allocate the top-level window, then dies with # "XOpenDisplay failed" — and the compositor keeps the orphan surface # visible for a few seconds until garbage-collected. # # Gate: try a lightweight X server ping (xset -> xdpyinfo -> xrandr, # whichever is present). If none succeed within ~10s of retries, bail # quietly — cron will retry us on the next minute. loginctl session # state must also be "active"; anything else (opening / closing) means # we're mid-transition and should hold. _session_ready() { # X ping — first tool that exists on the host wins. local _ok=0 for _t in xset xdpyinfo xrandr; do if command -v "$_t" >/dev/null 2>&1; then case "$_t" in xset) "$_t" -display "$DISPLAY" q >/dev/null 2>&1 && _ok=1 ;; xdpyinfo) "$_t" -display "$DISPLAY" >/dev/null 2>&1 && _ok=1 ;; xrandr) "$_t" --display "$DISPLAY" --query >/dev/null 2>&1 && _ok=1 ;; esac [ "$_ok" -eq 1 ] && break fi done [ "$_ok" -eq 1 ] || return 1 # If we're on Wayland, the compositor socket must exist too. On pure # X11 hosts WAYLAND_DISPLAY is empty and this block is a no-op. if [ -n "$WAYLAND_DISPLAY" ] && [ -n "$XDG_RUNTIME_DIR" ]; then [ -S "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" ] || return 1 fi # loginctl session state — "active" is the only value that means the # user is fully logged in and the graphical stack is up. "opening" / # "closing" fire during the logout/login transition. if command -v loginctl >/dev/null 2>&1; then local _sid _state _sid=$(loginctl list-sessions --no-legend 2>/dev/null | awk -v u="$USER" '$3 == u {print $1; exit}') if [ -n "$_sid" ]; then _state=$(loginctl show-session "$_sid" -p State --value 2>/dev/null) [ "$_state" = "active" ] || return 1 fi fi return 0 } _gate_attempts=0 while ! _session_ready; do _gate_attempts=$((_gate_attempts + 1)) if [ "$_gate_attempts" -ge 4 ]; then printf 'Session not ready after %d attempts — deferring to next cron run.\n' \ "$_gate_attempts" >> "$LOG" exit 0 fi sleep 3 done printf 'Session-readiness gate passed on attempt %d.\n' "$_gate_attempts" >> "$LOG" # Tray icon is an interactive-build concern only; the silent build # ships no visible icon, so this whole block is skipped for svcvtt. if [ "$TC_VARIANT" = "timechamp" ] then if [ -e $TC_HOME/timechamp_logo.png ] then printf "TimeChamp Icon Exists\n" else printf "TimeChamp Icon does not Exists\n" wget -P ~/.cache https://tracker.timechamp.io/linux/timechamp_logo.png mv ~/.cache/timechamp_logo.png $TC_HOME/ rm ~/.local/share/applications/${TC_VARIANT}.desktop fi fi # The @reboot cron entry must carry a literal path (cron has no # $TC_HOME), so the variant name is expanded here while $HOME is # left for cron to resolve at boot. (crontab -l 2>/dev/null | grep -Fq "@reboot rm -f \$HOME/.local/share/$TC_VARIANT/exit.txt") || (crontab -l 2>/dev/null; echo "@reboot rm -f \$HOME/.local/share/$TC_VARIANT/exit.txt") | crontab - currentutcdatetime=$(date --utc +'%Y-%m-%dT%H:%M:%SZ') printf "Current UTC dateTime: $currentutcdatetime\n" if [ -e $TC_HOME/Pulse.txt ] then recentpulsedatetime=$(<$TC_HOME/Pulse.txt) printf "Recent pulse dateTime: $recentpulsedatetime\n" else recentpulsedatetime=$(date --utc +'%Y-%m-%dT%H:%M:%SZ') fi CURRENT=$(date +%s -d $currentutcdatetime) RECENT=$(date +%s -d $recentpulsedatetime) MINUTES=$(( ($CURRENT - $RECENT) / 60 )) if [[ -e $TC_HOME/exit.txt && $(<$TC_HOME/exit.txt) == "true" ]] then RESTART="NO" else RESTART="YES" fi # Process-name patterns used to decide whether the tracker is already # running. process2 is matched against the full command line (pgrep # -f); process3/process4 against the comm name (pgrep -x, exact and # CASE-SENSITIVE). The Linux Wayland tracker's binary is # TimeChamp.Tracker.Linux.Wayland and Program.cs sets its comm via # prctl to exactly "Timechamp" (interactive) / "Svcvtt" (silent). # These MUST match — a mismatch makes the watchdog think the tracker # is dead every minute, so it deletes the .desktop file and relaunches, # which flickers the dash icon between the app logo and a gear. # # process2 anchors to the tracker's actual install path so # `pgrep -f` cannot false-positive on ANY other program that just # happens to have the string "TimeChamp.Tracker.Linux.Wayland" in # its command line (a file manager viewing a source file in the # repo, a text editor with the log4net.config open, `grep` output # piped through less, etc). Without the anchor the watchdog thought # the tracker was running every minute and refused to relaunch it # after Exit — the "service file not picking up update / not # starting the app" symptom. process=time-champ.-tra process1=TimeChamp.Track process2=/executables/usr/bin/TimeChamp\.Tracker\.Linux\.Wayland process3=Timechamp process4=Svcvtt specified_file="TimeChamp.AppImage" most_recent_directory="" most_recent_timestamp=0 # Loop through directories in the search path for dir in $TC_HOME/*/; do printf "DirectoryPath: $dir\n" # Only timestamped install directories (yyyyMMddHHmmss — 14 digits) # are version folders. Skip anything else — notably the capture # helper's bin/ — so it is never deleted as a "stale install". dirname=$(basename "$dir") if ! [[ "$dirname" =~ ^[0-9]{14}$ ]]; then continue fi # Check if the directory contains the specified file if [ -f "$dir/$specified_file" ]; then # Get the creation time of the directory timestamp=$(stat -c %Y "$dir") # Check if this directory is more recent than the previous one if [ "$timestamp" -gt "$most_recent_timestamp" ]; then most_recent_directory="$dir" most_recent_timestamp="$timestamp" fi else if [ -d "$dir" ]; then printf "Deleting directory: $dir\n" rm -rf "$dir" fi fi done if [ -n "$most_recent_directory" ] then LATESTAUTOUPDATE="YES" printf "$most_recent_directory is NOT NULL\n" else LATESTAUTOUPDATE="NO" printf "$most_recent_directory is NULL\n" fi # Killing Application based on the killme txt file if [[ -e $TC_HOME/killme.txt && $(<$TC_HOME/killme.txt) == "true" ]] then KILL="YES" else KILL="NO" fi if ((pgrep -x "$process" && pgrep -x "$process1") || pgrep -f "$process2" || pgrep -x "$process3" || pgrep -x "$process4") > /dev/null then printf "$MINUTES minutes & kill is $KILL & Restart is $RESTART " if [ "$MINUTES" -ge 4 ] || [ "$KILL" == "YES" ]; then killall -9 time-champ.-tracker.-electron killall -9 TimeChamp.Tracker.Electron killall -9 TimeChamp.Tracker.Avalonia killall -9 TimeChamp.AppImage killall -9 TimeChamp killall -9 Svcvtt killall -9 "time-champ.-tracker.-electron --no-sandbox" rm -r ~/.cache/tracker rm -r ~/.local/share/tracker rm -r ~/.cache/tracker3 rm -r ~/.local/share/tracker3 rm -r ~/.config/time-champ.-tracker.-electron printf "making process restart due to deadlock\n" if [ "$LATESTAUTOUPDATE" == "YES" ] then chmod +x "${most_recent_directory}TimeChamp.AppImage" if [ -d "${most_recent_directory}executables" ]; then printf "Folder exists: ${most_recent_directory}executables\n" else printf "Folder does not exist: ${most_recent_directory}executables\n" "${most_recent_directory}TimeChamp.AppImage" --appimage-extract mv squashfs-root "${most_recent_directory}executables" fi nohup "${most_recent_directory}executables/AppRun" & else chmod +x $TC_HOME/TimeChamp.AppImage nohup $TC_HOME/TimeChamp.AppImage --no-sandbox & fi else printf "Process running\n" #exit fi else printf "process not found running" if [ "$RESTART" == "NO" ] then killall -9 time-champ.-tracker.-electron killall -9 TimeChamp.Tracker.Electron killall -9 TimeChamp.Tracker.Avalonia killall -9 TimeChamp.AppImage killall -9 TimeChamp killall -9 Svcvtt killall -9 "time-champ.-tracker.-electron --no-sandbox" rm -r ~/.cache/tracker rm -r ~/.local/share/tracker else printf "Process running\n" exit rm -r ~/.cache/tracker3 rm -r ~/.local/share/tracker3 rm -r ~/.config/time-champ.-tracker.-electron printf "User exited manually\n" else rm -r ~/.local/share/applications/${TC_VARIANT}.desktop sleep 10 killall -9 time-champ.-tracker.-electron killall -9 TimeChamp.Tracker.Electron killall -9 TimeChamp.Tracker.Avalonia killall -9 TimeChamp.AppImage killall -9 TimeChamp killall -9 Svcvtt killall -9 "time-champ.-tracker.-electron --no-sandbox" rm -r ~/.cache/tracker rm -r ~/.local/share/tracker rm -r ~/.cache/tracker3 rm -r ~/.local/share/tracker3 rm -r ~/.config/time-champ.-tracker.-electron printf "making process start\n" if [ "$LATESTAUTOUPDATE" == "YES" ] then chmod +x "${most_recent_directory}TimeChamp.AppImage" if [ -d "${most_recent_directory}executables" ]; then printf "Folder exists: ${most_recent_directory}executables\n" else printf "Folder does not exist: ${most_recent_directory}executables\n" "${most_recent_directory}TimeChamp.AppImage" --appimage-extract mv squashfs-root "${most_recent_directory}executables" fi nohup "${most_recent_directory}executables/AppRun" & else chmod +x $TC_HOME/TimeChamp.AppImage nohup $TC_HOME/TimeChamp.AppImage --no-sandbox & fi fi fi echo "TimeChamp Tracker Completed " #!/bin/bash set -e # wmctrl is kept inside the tracker's own folder ($TC_HOME, e.g. # ~/.local/share/timechamp) — alongside pickup-service and the rest — # rather than ~/.local/bin. That keeps everything the tracker owns in # one place and avoids touching the user's PATH or ~/.bashrc. The # tracker locates it there by explicit path. WMCTRL_DEST="$TC_HOME/wmctrl" if [ -x "$WMCTRL_DEST" ]; then echo " wmctrl already exists at $WMCTRL_DEST" exit 0 fi echo " Detecting package manager and downloading wmctrl (no sudo install)..." mkdir -p "$TC_HOME" TMP_DIR=$(mktemp -d) cd "$TMP_DIR" if command -v apt-get &>/dev/null; then echo " Debian/Ubuntu detected" apt-get download wmctrl dpkg-deb -x *.deb . mv ./usr/bin/wmctrl "$WMCTRL_DEST" elif command -v dnf &>/dev/null; then echo " Fedora detected" dnf download wmctrl rpm2cpio *.rpm | cpio -idmv mv ./usr/bin/wmctrl "$WMCTRL_DEST" elif command -v yum &>/dev/null; then echo " RHEL/CentOS detected" yum install --downloadonly --downloaddir="$TMP_DIR" wmctrl rpm2cpio *.rpm | cpio -idmv mv ./usr/bin/wmctrl "$WMCTRL_DEST" elif command -v zypper &>/dev/null; then echo " openSUSE detected" zypper download wmctrl rpm2cpio *.rpm | cpio -idmv mv ./usr/bin/wmctrl "$WMCTRL_DEST" elif command -v pacman &>/dev/null; then echo " Arch Linux detected" PKG=$(pacman -Sp --noconfirm wmctrl) curl -LO "$PKG" bsdtar -xf *.pkg.tar.* usr/bin/wmctrl mv ./usr/bin/wmctrl "$WMCTRL_DEST" elif command -v apk &>/dev/null; then echo " Alpine detected" apk fetch wmctrl tar -xzf *.apk mv ./usr/bin/wmctrl "$WMCTRL_DEST" else echo " Unsupported Linux distribution!" echo "Please install wmctrl manually from your package manager." exit 1 fi chmod +x "$WMCTRL_DEST" echo "wmctrl binary downloaded to: $WMCTRL_DEST" cd - rm -rf "$TMP_DIR" exit