Files
fn_registry/bash/functions/browser/delete_chrome_profile.sh
Egutierrez 685224ccb2 fix(browser): guards chromium-por-udd dejan de auto-matchear el propio grep
Bug descubierto al ejecutar el reset real: los guards y los kills usaban
'pgrep -af [c]hromium | grep -F <udd>'. Como la ruta del user-data-dir contiene
la cadena 'chromium' (~/.config/chromium-cdp), el propio proceso grep/ugrep —cuyo
cmdline incluye <udd>— era capturado por pgrep, dando un falso positivo perpetuo:
el guard creía siempre que había un chromium abierto y delete/restore abortaban
con exit 2, y el lazo de cierre nunca convergía.

Fix en delete_chrome_profile, restore_chrome_bookmarks, create_chrome_profile y el
pipeline reset_chrome_profiles: enumerar por PID con 'pgrep -x chromium' (comm
exactamente 'chromium', nunca grep/pgrep/bash) y leer /proc/PID/cmdline para
comprobar el udd. Validado: reset destructivo real de los 4 perfiles completó OK,
cada perfil quedó con solo uBlock + web_proxy y los bookmarks restaurados.
2026-06-06 01:37:51 +02:00

265 lines
12 KiB
Bash

#!/usr/bin/env bash
# delete_chrome_profile — borra por completo uno o varios perfiles Chrome/Chromium:
# elimina la carpeta del perfil y limpia todas las referencias en Local State
# (info_cache, profiles_order, last_active_profiles, last_used, variations_google_groups).
set -euo pipefail
delete_chrome_profile() {
# ── defaults ──────────────────────────────────────────────────────────────
local _user_data_dir=""
local _profiles=()
local _dry_run=0
# ── parse args ─────────────────────────────────────────────────────────────
_usage() {
cat >&2 <<'EOF'
Usage: delete_chrome_profile --user-data-dir <dir> --profile <name> [--profile <name>]... [--dry-run]
--user-data-dir <dir> Ruta raíz del user-data-dir de Chrome/Chromium (obligatorio).
--profile <name> Nombre de la carpeta del perfil, ej. "Default" o "Profile 1"
(repetible, al menos uno obligatorio).
--dry-run Muestra qué borraría y qué claves de Local State quitaría
sin tocar nada.
Exit codes:
0 éxito (o dry-run completado)
1 error de argumento o validación
2 chromium está corriendo (solo en modo real)
EOF
return 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--user-data-dir) _user_data_dir="$2"; shift 2 ;;
--profile) _profiles+=("$2"); shift 2 ;;
--dry-run) _dry_run=1; shift ;;
-h|--help) _usage; return 0 ;;
*) echo "delete_chrome_profile: argumento desconocido: $1" >&2; return 1 ;;
esac
done
# ── validaciones de argumentos ────────────────────────────────────────────
if [[ -z "$_user_data_dir" ]]; then
echo "delete_chrome_profile: --user-data-dir es obligatorio" >&2
return 1
fi
if [[ ${#_profiles[@]} -eq 0 ]]; then
echo "delete_chrome_profile: se requiere al menos un --profile" >&2
return 1
fi
if [[ ! -d "$_user_data_dir" ]]; then
echo "delete_chrome_profile: user-data-dir no encontrado: ${_user_data_dir}" >&2
return 1
fi
local _local_state="${_user_data_dir}/Local State"
if [[ ! -f "$_local_state" ]]; then
echo "delete_chrome_profile: Local State no encontrado: ${_local_state}" >&2
return 1
fi
# ── guard: ningún chromium debe tener ESTE user-data-dir abierto (excepto en dry-run) ──
# Por-udd, no global. Comprobamos por PID con comm=chromium (pgrep -x) y leemos su cmdline,
# para NO auto-matchear el propio `grep`/`pgrep` del pipe: como el path del udd contiene la
# cadena "chromium" (p.ej. ~/.config/chromium-cdp), un `pgrep -af '[c]hromium' | grep <udd>`
# se detecta a sí mismo. pgrep -x chromium solo lista procesos cuyo nombre es exactamente
# "chromium" (el navegador), nunca grep/pgrep/bash.
if [[ $_dry_run -eq 0 ]]; then
local _p _busy=0
for _p in $(pgrep -x chromium 2>/dev/null); do
if tr '\0' ' ' < "/proc/$_p/cmdline" 2>/dev/null | grep -qF -- "$_user_data_dir"; then
_busy=1; break
fi
done
if [[ $_busy -eq 1 ]]; then
echo "delete_chrome_profile: hay un chromium con este user-data-dir abierto — ciérralo antes de borrar perfiles:" >&2
echo " pkill -TERM chromium" >&2
echo "(Chromium reescribe Local State desde memoria al cerrar y desharía el borrado)" >&2
return 2
fi
fi
local _today
_today="$(date +%Y%m%d)"
# ── modo dry-run ──────────────────────────────────────────────────────────
if [[ $_dry_run -eq 1 ]]; then
echo "=== delete_chrome_profile DRY-RUN ===" >&2
local _p
for _p in "${_profiles[@]}"; do
local _pdir="${_user_data_dir}/${_p}"
if [[ -d "$_pdir" ]]; then
echo " [borraría] rm -rf ${_pdir}" >&2
else
echo " [no existe] ${_pdir}" >&2
fi
echo " [Local State] quitaría claves para perfil: '${_p}'" >&2
echo " profile.info_cache.${_p}" >&2
echo " profile.profiles_order (entrada '${_p}')" >&2
echo " profile.last_active_profiles (entrada '${_p}')" >&2
echo " profile.last_used (si == '${_p}', reasignar)" >&2
echo " variations_google_groups.${_p} (si existe)" >&2
done
# Construir JSON de dry-run inline
local _dry_items="" _dry_first=1
for _p in "${_profiles[@]}"; do
local _pdir="${_user_data_dir}/${_p}"
local _sep="" _exists="false"
[[ $_dry_first -eq 0 ]] && _sep=","
_dry_first=0
[[ -d "$_pdir" ]] && _exists="true"
_dry_items+="${_sep}{\"profile\":\"${_p}\",\"dir_exists\":${_exists},\"would_remove\":${_exists},\"local_state_would_clean\":true}"
done
printf '{"dry_run":true,"would_delete":[%s]}\n' "$_dry_items"
return 0
fi
# ── backup de Local State (no sobreescribir el del día) ───────────────────
local _backup="${_local_state}.bak.${_today}"
if [[ ! -f "$_backup" ]]; then
cp "$_local_state" "$_backup"
fi
# ── borrar carpetas de perfil ──────────────────────────────────────────────
local _deleted_results=() # "profile|dir_removed|ls_cleaned"
local _p
for _p in "${_profiles[@]}"; do
local _pdir="${_user_data_dir}/${_p}"
local _dir_removed=false
if [[ -d "$_pdir" ]]; then
rm -rf "$_pdir"
_dir_removed=true
fi
_deleted_results+=("${_p}|${_dir_removed}|false")
done
# ── construir lista Python de perfiles a eliminar ─────────────────────────
local _py_profiles_list=""
for _p in "${_profiles[@]}"; do
_py_profiles_list+="\"${_p}\","
done
_py_profiles_list="[${_py_profiles_list%,}]"
# ── editar Local State con python3 ────────────────────────────────────────
local _ls_cleaned=false
if command -v python3 >/dev/null 2>&1; then
python3 - "$_local_state" "$_py_profiles_list" <<'PY'
import sys, json
ls_path = sys.argv[1]
profiles_to_delete = json.loads(sys.argv[2])
with open(ls_path, "r", encoding="utf-8") as f:
data = json.load(f)
profile_section = data.get("profile", {})
# 1. profile.info_cache — eliminar cada perfil
info_cache = profile_section.get("info_cache", {})
for p in profiles_to_delete:
info_cache.pop(p, None)
# 2. profile.profiles_order — quitar entradas del perfil
if "profiles_order" in profile_section and isinstance(profile_section["profiles_order"], list):
profile_section["profiles_order"] = [
x for x in profile_section["profiles_order"] if x not in profiles_to_delete
]
# 3. profile.last_active_profiles — quitar entradas del perfil
if "last_active_profiles" in profile_section and isinstance(profile_section["last_active_profiles"], list):
profile_section["last_active_profiles"] = [
x for x in profile_section["last_active_profiles"] if x not in profiles_to_delete
]
# 4. profile.last_used — reasignar si apunta a un perfil borrado
last_used = profile_section.get("last_used", "")
if last_used in profiles_to_delete:
remaining = [k for k in info_cache.keys() if k not in profiles_to_delete]
profile_section["last_used"] = remaining[0] if remaining else ""
# 5. variations_google_groups — limpiar entradas del perfil (si existe)
vgg = data.get("variations_google_groups", {})
for p in profiles_to_delete:
vgg.pop(p, None)
with open(ls_path, "w", encoding="utf-8") as f:
json.dump(data, f, separators=(",", ":"))
PY
_ls_cleaned=true
# ── fallback con jq ───────────────────────────────────────────────────────
elif command -v jq >/dev/null 2>&1; then
local _tmp_ls
_tmp_ls="$(mktemp)"
local _jq_expr="."
for _p in "${_profiles[@]}"; do
_jq_expr+=" | del(.profile.info_cache[\"${_p}\"])"
_jq_expr+=" | del(.variations_google_groups[\"${_p}\"])"
_jq_expr+=" | if .profile.profiles_order then .profile.profiles_order -= [\"${_p}\"] else . end"
_jq_expr+=" | if .profile.last_active_profiles then .profile.last_active_profiles -= [\"${_p}\"] else . end"
done
if jq "${_jq_expr}" "$_local_state" > "$_tmp_ls" 2>/dev/null; then
mv "$_tmp_ls" "$_local_state"
_ls_cleaned=true
else
echo "delete_chrome_profile: advertencia — jq falló editando Local State" >&2
rm -f "$_tmp_ls"
fi
else
echo "delete_chrome_profile: advertencia — ni python3 ni jq disponibles; carpetas borradas pero Local State no modificado" >&2
fi
# ── validar que el JSON resultante sigue siendo parseable ─────────────────
if [[ "$_ls_cleaned" == "true" ]]; then
if command -v python3 >/dev/null 2>&1; then
if ! python3 -c "import sys, json; json.load(open(sys.argv[1]))" "$_local_state" 2>/dev/null; then
echo "delete_chrome_profile: JSON de Local State inválido tras edición — restaurando backup" >&2
cp "$_backup" "$_local_state"
return 1
fi
fi
fi
# ── actualizar _deleted_results con ls_cleaned ────────────────────────────
local _updated_results=()
for _entry in "${_deleted_results[@]}"; do
local _ep _edr _els
IFS='|' read -r _ep _edr _els <<< "$_entry"
_updated_results+=("${_ep}|${_edr}|${_ls_cleaned}")
done
# ── leer last_used resultante ──────────────────────────────────────────────
local _new_last_used=""
if command -v python3 >/dev/null 2>&1; then
_new_last_used="$(python3 -c "
import sys, json
data = json.load(open(sys.argv[1]))
print(data.get('profile', {}).get('last_used', ''))
" "$_local_state" 2>/dev/null || echo "")"
fi
# ── construir JSON de resultado inline ────────────────────────────────────
local _result_items="" _res_first=1
for _entry in "${_updated_results[@]+"${_updated_results[@]}"}"; do
local _pn _dr _lc
IFS='|' read -r _pn _dr _lc <<< "$_entry"
local _rsep=""
[[ $_res_first -eq 0 ]] && _rsep=","
_res_first=0
_result_items+="${_rsep}{\"profile\":\"${_pn}\",\"dir_removed\":${_dr},\"local_state_cleaned\":${_lc}}"
done
printf '{"deleted":[%s],"last_used":"%s","backup":"Local State.bak.%s"}\n' \
"$_result_items" "$_new_last_used" "$_today"
}
# ── auto-ejecución ────────────────────────────────────────────────────────────
if [[ "${BASH_SOURCE[0]:-}" == "${0}" ]]; then
delete_chrome_profile "$@"
fi