fix(uninstall): remove IBus data dir & stop app by PID file not pgrep -f (#597)

* fix(uninstall): remove IBus runtime data directory on uninstall

Discussion #559 reported ~/.local/share/vocalinux-ibus surviving uninstall.
PR #569 fixed launcher wrappers but not this IBus IPC directory (socket,
PID file, stderr log). Stop the engine via engine.pid when present, remove
the directory regardless of --keep-data, and verify it in post-checks.

Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>

* fix(scripts): stop Vocalinux by PID file, not pgrep -f

pgrep -f vocalinux matches any process whose argv mentions the repo
path (editors, shells, test runners) and can kill unrelated local work.
AGENTS.md already warns against this.

- Prefer PIDs from instance.lock and vocalinux-ibus/engine.pid
- Verify each candidate via is_vocalinux_process (python -m vocalinux.main,
  ibus_engine.py under vocalinux, or argv0 basename vocalinux/vocalinux-gui)
- Narrow pgrep fallback only for those entrypoint patterns
- Drop the redundant stop_ibus_engine helper (engine covered via PID file)
- Still remove ~/.local/share/vocalinux-ibus on uninstall (#559)
- Align install.sh and correct the pkill tip in docs/UPDATE.md

Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>

* fix(scripts): detect setuptools console-script Vocalinux processes

Real installs run as `python …/bin/vocalinux`, so argv0 is python and the
previous argv0-basename check missed the live app (instance.lock PID was
skipped). Scan all cmdline args for …/bin/vocalinux or …/bin/vocalinux-gui
instead, which still avoids matching a bare repo directory path.

Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
This commit is contained in:
Jatin K Malik
2026-07-26 21:14:19 -07:00
committed by GitHub
co-authored by Jatin K Malik Cursor Agent
parent 44dac3b4ab
commit 40546e7f07
3 changed files with 222 additions and 46 deletions
+8 -2
View File
@@ -373,13 +373,19 @@ curl -fsSL https://raw.githubusercontent.com/jatinkrmalik/vocalinux/main/install
### Old Version Still Running
```bash
# Kill any running instances
pkill -f vocalinux
# Prefer stopping from the tray, or kill by the PID in the lock file:
kill "$(tr -d '[:space:]' < "${XDG_DATA_HOME:-$HOME/.local/share}/vocalinux/instance.lock")"
# If you used IBus injection, also stop the engine:
kill "$(tr -d '[:space:]' < "${XDG_DATA_HOME:-$HOME/.local/share}/vocalinux-ibus/engine.pid")"
# Start fresh
vocalinux
```
Avoid `pkill -f vocalinux` — it matches any process whose command line
mentions the repo path (editors, shells, test runners) and can kill unrelated work.
### Missing Dependencies
If you see dependency errors:
+102 -22
View File
@@ -44,27 +44,99 @@ print_kde_wayland_ibus_hint() {
print_info "After changing it, restart Vocalinux or log out and back in."
}
get_vocalinux_pids() {
pgrep -f "vocalinux" 2>/dev/null | while read -r pid; do
[ -z "$pid" ] && continue
if [ "$pid" = "$$" ] || [ "$pid" = "$PPID" ]; then
continue
fi
local stat
stat=$(ps -o stat= -p "$pid" 2>/dev/null | awk '{print $1}')
if [[ "$stat" == Z* ]]; then
continue
fi
local cmd
cmd=$(ps -o args= -p "$pid" 2>/dev/null)
if [[ "$cmd" == *"install.sh"* ]] || [[ "$cmd" == *"uninstall.sh"* ]]; then
continue
fi
# Read a numeric PID from a file (instance.lock / engine.pid). Empty if missing/invalid.
read_pid_file() {
local file="$1"
local pid=""
[ -f "$file" ] || return 0
pid=$(tr -d '[:space:]' < "$file" 2>/dev/null || true)
if [[ "$pid" =~ ^[0-9]+$ ]]; then
echo "$pid"
fi
}
# True only for the Vocalinux app / IBus engine — not editors, shells, or tests
# whose argv merely contains the string "vocalinux" (e.g. cwd under the repo).
is_vocalinux_process() {
local pid="$1"
if [ "$pid" = "$$" ] || [ "$pid" = "$PPID" ]; then
return 1
fi
[ -r "/proc/$pid/cmdline" ] || return 1
local stat
stat=$(ps -o stat= -p "$pid" 2>/dev/null | awk '{print $1}')
if [[ "$stat" == Z* ]]; then
return 1
fi
local cmdline
cmdline=$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null)
[ -n "$cmdline" ] || return 1
# Never target the installer/uninstaller themselves
if [[ "$cmdline" == *"install.sh"* || "$cmdline" == *"uninstall.sh"* ]]; then
return 1
fi
# python -m vocalinux.main [--flags]
if [[ "$cmdline" == *"-m vocalinux.main"* ]]; then
return 0
fi
# IBus engine: .../vocalinux/.../ibus_engine.py
if [[ "$cmdline" == *"ibus_engine.py"* && "$cmdline" == *"vocalinux"* ]]; then
return 0
fi
# Console-script entrypoints. Setuptools scripts are executed as
# `python …/bin/vocalinux`, so argv0 is python — scan every arg for the
# script path (require …/bin/vocalinux to avoid matching a repo directory).
local arg
while IFS= read -r -d '' arg; do
case "$arg" in
vocalinux|vocalinux-gui|*/bin/vocalinux|*/bin/vocalinux-gui)
return 0
;;
esac
done < "/proc/$pid/cmdline"
return 1
}
# Prefer the PIDs the app writes; fall back to narrow entrypoint patterns only.
# Deliberately does NOT use `pgrep -f vocalinux` (matches editors/shells/cwd paths).
get_vocalinux_pids() {
local data_home="${XDG_DATA_HOME:-$HOME/.local/share}"
local -A seen=()
local pid candidate
local -a candidates=()
for candidate in \
"$(read_pid_file "$data_home/vocalinux/instance.lock")" \
"$(read_pid_file "$data_home/vocalinux-ibus/engine.pid")"
do
[ -n "$candidate" ] && candidates+=("$candidate")
done
# Fallback when PID files are missing/stale: exact process name or known argv.
while read -r candidate; do
[ -n "$candidate" ] && candidates+=("$candidate")
done < <(
pgrep -u "$(id -u)" -x vocalinux 2>/dev/null || true
pgrep -u "$(id -u)" -x vocalinux-gui 2>/dev/null || true
pgrep -u "$(id -u)" -f -- '-m vocalinux\.main' 2>/dev/null || true
pgrep -u "$(id -u)" -f -- 'vocalinux/.*/ibus_engine\.py' 2>/dev/null || true
)
for pid in "${candidates[@]}"; do
[ -n "${seen[$pid]:-}" ] && continue
seen[$pid]=1
if is_vocalinux_process "$pid"; then
echo "$pid"
fi
done
}
@@ -73,7 +145,13 @@ check_running_processes() {
PIDS=$(get_vocalinux_pids || true)
if [ -n "$PIDS" ]; then
print_warning "Found running Vocalinux process(es): $PIDS"
print_warning "Found running Vocalinux process(es):"
local pid
for pid in $PIDS; do
local cmd
cmd=$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null || echo "?")
print_warning " PID $pid: $cmd"
done
echo ""
if [[ "$NON_INTERACTIVE" == "yes" ]]; then
@@ -89,7 +167,8 @@ check_running_processes() {
fi
print_info "Stopping Vocalinux..."
echo "$PIDS" | xargs -r kill -TERM 2>/dev/null || true
# shellcheck disable=SC2086
echo $PIDS | xargs -r kill -TERM 2>/dev/null || true
sleep 2
local REMAINING_PIDS
@@ -97,7 +176,8 @@ check_running_processes() {
if [ -n "$REMAINING_PIDS" ]; then
print_warning "Some processes still running, forcing termination..."
echo "$REMAINING_PIDS" | xargs -r kill -KILL 2>/dev/null || true
# shellcheck disable=SC2086
echo $REMAINING_PIDS | xargs -r kill -KILL 2>/dev/null || true
sleep 1
fi
+112 -22
View File
@@ -27,27 +27,99 @@ command_exists() {
command -v "$1" >/dev/null 2>&1
}
get_vocalinux_pids() {
pgrep -f "vocalinux" 2>/dev/null | while read -r pid; do
[ -z "$pid" ] && continue
if [ "$pid" = "$$" ] || [ "$pid" = "$PPID" ]; then
continue
fi
local stat
stat=$(ps -o stat= -p "$pid" 2>/dev/null | awk '{print $1}')
if [[ "$stat" == Z* ]]; then
continue
fi
local cmd
cmd=$(ps -o args= -p "$pid" 2>/dev/null)
if [[ "$cmd" == *"install.sh"* ]] || [[ "$cmd" == *"uninstall.sh"* ]]; then
continue
fi
# Read a numeric PID from a file (instance.lock / engine.pid). Empty if missing/invalid.
read_pid_file() {
local file="$1"
local pid=""
[ -f "$file" ] || return 0
pid=$(tr -d '[:space:]' < "$file" 2>/dev/null || true)
if [[ "$pid" =~ ^[0-9]+$ ]]; then
echo "$pid"
fi
}
# True only for the Vocalinux app / IBus engine — not editors, shells, or tests
# whose argv merely contains the string "vocalinux" (e.g. cwd under the repo).
is_vocalinux_process() {
local pid="$1"
if [ "$pid" = "$$" ] || [ "$pid" = "$PPID" ]; then
return 1
fi
[ -r "/proc/$pid/cmdline" ] || return 1
local stat
stat=$(ps -o stat= -p "$pid" 2>/dev/null | awk '{print $1}')
if [[ "$stat" == Z* ]]; then
return 1
fi
local cmdline
cmdline=$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null)
[ -n "$cmdline" ] || return 1
# Never target the installer/uninstaller themselves
if [[ "$cmdline" == *"install.sh"* || "$cmdline" == *"uninstall.sh"* ]]; then
return 1
fi
# python -m vocalinux.main [--flags]
if [[ "$cmdline" == *"-m vocalinux.main"* ]]; then
return 0
fi
# IBus engine: .../vocalinux/.../ibus_engine.py
if [[ "$cmdline" == *"ibus_engine.py"* && "$cmdline" == *"vocalinux"* ]]; then
return 0
fi
# Console-script entrypoints. Setuptools scripts are executed as
# `python …/bin/vocalinux`, so argv0 is python — scan every arg for the
# script path (require …/bin/vocalinux to avoid matching a repo directory).
local arg
while IFS= read -r -d '' arg; do
case "$arg" in
vocalinux|vocalinux-gui|*/bin/vocalinux|*/bin/vocalinux-gui)
return 0
;;
esac
done < "/proc/$pid/cmdline"
return 1
}
# Prefer the PIDs the app writes; fall back to narrow entrypoint patterns only.
# Deliberately does NOT use `pgrep -f vocalinux` (matches editors/shells/cwd paths).
get_vocalinux_pids() {
local data_home="${XDG_DATA_HOME:-$HOME/.local/share}"
local -A seen=()
local pid candidate
local -a candidates=()
for candidate in \
"$(read_pid_file "$data_home/vocalinux/instance.lock")" \
"$(read_pid_file "$data_home/vocalinux-ibus/engine.pid")"
do
[ -n "$candidate" ] && candidates+=("$candidate")
done
# Fallback when PID files are missing/stale: exact process name or known argv.
while read -r candidate; do
[ -n "$candidate" ] && candidates+=("$candidate")
done < <(
pgrep -u "$(id -u)" -x vocalinux 2>/dev/null || true
pgrep -u "$(id -u)" -x vocalinux-gui 2>/dev/null || true
pgrep -u "$(id -u)" -f -- '-m vocalinux\.main' 2>/dev/null || true
pgrep -u "$(id -u)" -f -- 'vocalinux/.*/ibus_engine\.py' 2>/dev/null || true
)
for pid in "${candidates[@]}"; do
[ -n "${seen[$pid]:-}" ] && continue
seen[$pid]=1
if is_vocalinux_process "$pid"; then
echo "$pid"
fi
done
}
@@ -81,7 +153,13 @@ kill_vocalinux_processes() {
PIDS=$(get_vocalinux_pids || true)
if [ -n "$PIDS" ]; then
print_warning "Found running Vocalinux process(es): $PIDS"
print_warning "Found running Vocalinux process(es):"
local pid
for pid in $PIDS; do
local cmd
cmd=$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null || echo "?")
print_warning " PID $pid: $cmd"
done
if [[ "$NON_INTERACTIVE" != "yes" ]]; then
read -p "Kill running Vocalinux process(es)? (Y/n) " -n 1 -r
@@ -93,7 +171,8 @@ kill_vocalinux_processes() {
fi
print_info "Stopping Vocalinux..."
echo "$PIDS" | xargs -r kill -TERM 2>/dev/null || true
# shellcheck disable=SC2086
echo $PIDS | xargs -r kill -TERM 2>/dev/null || true
sleep 2
local REMAINING_PIDS
@@ -101,7 +180,8 @@ kill_vocalinux_processes() {
if [ -n "$REMAINING_PIDS" ]; then
print_warning "Some processes still running, forcing termination..."
echo "$REMAINING_PIDS" | xargs -r kill -KILL 2>/dev/null || true
# shellcheck disable=SC2086
echo $REMAINING_PIDS | xargs -r kill -KILL 2>/dev/null || true
sleep 1
fi
@@ -173,6 +253,7 @@ done
# Define XDG directories
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/vocalinux"
DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/vocalinux"
IBUS_DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/vocalinux-ibus"
DESKTOP_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
AUTOSTART_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/autostart"
ICON_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor/scalable/apps"
@@ -307,6 +388,9 @@ remove_config_and_data() {
else
print_info "Keeping application data directory as requested: $DATA_DIR"
fi
# IBus runtime data (socket, PID file, logs) is recreated on next launch.
safe_remove "$IBUS_DATA_DIR" "IBus runtime data directory"
}
# Function to clean up build artifacts
@@ -405,6 +489,12 @@ verify_uninstallation() {
print_warning "Application data directory still exists: $DATA_DIR"
((ISSUES++))
fi
# IBus runtime data should always be removed
if [ -d "$IBUS_DATA_DIR" ]; then
print_warning "IBus runtime data directory still exists: $IBUS_DATA_DIR"
((ISSUES++))
fi
# Return the number of issues found
return $ISSUES