ae841ceedb
Cinco funciones nuevas (dominio browser, grupo navegator) que cierran los gaps de gestión de perfiles, más un pipeline que las orquesta: - backup_chrome_bookmarks / restore_chrome_bookmarks: backup y restore de los archivos Bookmarks (copia byte a byte verbatim para preservar el checksum interno; en Chromium 148 los bookmarks no están bajo el super_mac de Secure Preferences). Guard por user-data-dir (no global). - delete_chrome_profile: borra la carpeta del perfil + limpia su entrada en Local State (info_cache, profiles_order, last_active_profiles, last_used). - create_chrome_profile: lanza chromium headless (vía systemd-run) para que la managed policy instale la whitelist de extensiones, y asigna el nombre legible en Local State. Mata todo el árbol de chromium del udd antes de editar Local State (los hijos zygote/gpu no repiten --user-data-dir pero referencian la ruta). - list_chrome_profile_extensions (Go): lista extensiones de un perfil con ID/name/version/location/enabled/fromPolicy. 7 unit tests. - reset_chrome_profiles (pipeline): backup -> cerrar chromium -> delete -> create -> restore -> verify. Destructivo (--yes), --dry-run seguro. Validado: unit tests Go verdes, backup/restore byte-idéntico, delete limpia Local State, create instala la forcelist global (uBlock + web_proxy) en perfiles nuevos.
305 lines
13 KiB
Bash
305 lines
13 KiB
Bash
#!/usr/bin/env bash
|
|
# create_chrome_profile — crea un perfil Chrome/Chromium nuevo en un user-data-dir,
|
|
# opcionalmente lanzando chromium headless para que la managed policy instale las
|
|
# extensiones forzadas (uBlock, web_proxy). Edita Local State para asignar el nombre
|
|
# legible al perfil.
|
|
|
|
set -euo pipefail
|
|
|
|
create_chrome_profile() {
|
|
# ── defaults ──────────────────────────────────────────────────────────────
|
|
local _udd=""
|
|
local _profile_dir=""
|
|
local _name=""
|
|
local _port=9250
|
|
local _chrome_path=""
|
|
local _no_launch=0
|
|
local _timeout_sec=25
|
|
local _dry_run=0
|
|
|
|
# ── parse args ─────────────────────────────────────────────────────────────
|
|
_usage() {
|
|
cat >&2 <<'EOF'
|
|
Usage: create_chrome_profile --user-data-dir <dir> --profile <dir-name> --name <legible>
|
|
[--port N] [--chrome-path <path>] [--no-launch] [--timeout-sec N] [--dry-run]
|
|
|
|
--user-data-dir Raíz del user-data-dir de Chrome/Chromium (obligatorio).
|
|
--profile Nombre de la carpeta del perfil dentro de user-data-dir, ej: Default,
|
|
"Profile 1", Automation (obligatorio).
|
|
--name Nombre legible que aparece en el selector de perfil, ej: Work, Aurgi
|
|
(obligatorio).
|
|
--port Puerto CDP para el lanzamiento headless. Default: 9250.
|
|
Usar un puerto distinto al 9222 global para no chocar.
|
|
--chrome-path Ruta explícita al binario chromium/chrome. Auto-detecta si se omite.
|
|
--no-launch No lanza chromium. Crea la carpeta y edita Local State offline.
|
|
El perfil no tendrá extensiones instaladas; útil para tests/CRUD.
|
|
--timeout-sec Segundos esperando a que Preferences aparezca tras el lanzamiento.
|
|
Default: 25.
|
|
--dry-run Describe las acciones sin lanzar ni escribir nada.
|
|
|
|
Exit codes:
|
|
0 éxito
|
|
1 error de argumento o validación
|
|
2 lock: ya hay un chromium usando este user-data-dir
|
|
3 timeout esperando a que Preferences se cree
|
|
4 error editando Local State (JSON inválido tras escritura)
|
|
EOF
|
|
return 1
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--user-data-dir) _udd="$2"; shift 2 ;;
|
|
--profile) _profile_dir="$2"; shift 2 ;;
|
|
--name) _name="$2"; shift 2 ;;
|
|
--port) _port="$2"; shift 2 ;;
|
|
--chrome-path) _chrome_path="$2"; shift 2 ;;
|
|
--no-launch) _no_launch=1; shift ;;
|
|
--timeout-sec) _timeout_sec="$2"; shift 2 ;;
|
|
--dry-run) _dry_run=1; shift ;;
|
|
-h|--help) _usage; return 0 ;;
|
|
*) echo "create_chrome_profile: argumento desconocido: $1" >&2; return 1 ;;
|
|
esac
|
|
done
|
|
|
|
# ── validaciones obligatorias ──────────────────────────────────────────────
|
|
if [[ -z "$_udd" ]]; then
|
|
echo "create_chrome_profile: --user-data-dir es obligatorio" >&2
|
|
return 1
|
|
fi
|
|
if [[ -z "$_profile_dir" ]]; then
|
|
echo "create_chrome_profile: --profile es obligatorio" >&2
|
|
return 1
|
|
fi
|
|
if [[ -z "$_name" ]]; then
|
|
echo "create_chrome_profile: --name es obligatorio" >&2
|
|
return 1
|
|
fi
|
|
|
|
local _profile_path="${_udd}/${_profile_dir}"
|
|
local _local_state="${_udd}/Local State"
|
|
local _prefs_file="${_profile_path}/Preferences"
|
|
|
|
# ── guard: lock por user-data-dir ─────────────────────────────────────────
|
|
# Dos procesos chromium no pueden compartir el mismo user-data-dir.
|
|
if [[ $_dry_run -eq 0 && $_no_launch -eq 0 ]]; then
|
|
local _singleton="${_udd}/SingletonLock"
|
|
if [[ -e "$_singleton" ]]; then
|
|
echo "create_chrome_profile: ya hay un chromium corriendo con --user-data-dir=${_udd}" >&2
|
|
echo " (encontrado: ${_singleton})" >&2
|
|
echo " Ciérralo o usa un user-data-dir distinto." >&2
|
|
return 2
|
|
fi
|
|
fi
|
|
|
|
# ── detección del binario chromium ────────────────────────────────────────
|
|
local _bin=""
|
|
if [[ -n "$_chrome_path" ]]; then
|
|
if [[ ! -x "$_chrome_path" ]]; then
|
|
echo "create_chrome_profile: binario no encontrado o no ejecutable: ${_chrome_path}" >&2
|
|
return 1
|
|
fi
|
|
_bin="$_chrome_path"
|
|
elif [[ $_no_launch -eq 0 ]]; then
|
|
for _candidate in chromium chromium-browser google-chrome brave-browser; do
|
|
if command -v "$_candidate" &>/dev/null; then
|
|
_bin="$_candidate"
|
|
break
|
|
fi
|
|
done
|
|
if [[ -z "$_bin" ]]; then
|
|
echo "create_chrome_profile: no se encontró binario chromium en PATH" >&2
|
|
echo " Probados: chromium, chromium-browser, google-chrome, brave-browser" >&2
|
|
echo " Usa --chrome-path o --no-launch." >&2
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
# ── modo dry-run ──────────────────────────────────────────────────────────
|
|
if [[ $_dry_run -eq 1 ]]; then
|
|
echo "=== create_chrome_profile DRY-RUN ===" >&2
|
|
echo " user-data-dir : ${_udd}" >&2
|
|
echo " profile : ${_profile_dir}" >&2
|
|
echo " name : ${_name}" >&2
|
|
if [[ $_no_launch -eq 1 ]]; then
|
|
echo " modo : --no-launch (sin chromium)" >&2
|
|
echo " acciones : mkdir -p ${_profile_path}" >&2
|
|
echo " editar ${_local_state} → info_cache + profiles_order" >&2
|
|
else
|
|
echo " binario : ${_bin}" >&2
|
|
echo " puerto CDP : ${_port}" >&2
|
|
echo " timeout : ${_timeout_sec}s" >&2
|
|
echo " acciones : systemd-run unit=create-prof-<rand> chromium headless" >&2
|
|
echo " poll Preferences hasta ${_timeout_sec}s" >&2
|
|
echo " systemctl --user stop unit" >&2
|
|
echo " editar ${_local_state} → info_cache + profiles_order" >&2
|
|
fi
|
|
printf '{"profile":"%s","name":"%s","launched":false,"preferences_created":false,"dry_run":true}\n' \
|
|
"$_profile_dir" "$_name"
|
|
return 0
|
|
fi
|
|
|
|
# ── crear directorio del perfil ───────────────────────────────────────────
|
|
mkdir -p "$_profile_path"
|
|
|
|
# ── también asegurar que user-data-dir existe ──────────────────────────────
|
|
mkdir -p "$_udd"
|
|
|
|
# ── modo --no-launch: solo estructura + Local State ────────────────────────
|
|
local _launched=false
|
|
local _prefs_created=false
|
|
|
|
if [[ $_no_launch -eq 1 ]]; then
|
|
_update_local_state "$_udd" "$_local_state" "$_profile_dir" "$_name"
|
|
if [[ -f "$_prefs_file" ]]; then
|
|
_prefs_created=true
|
|
fi
|
|
printf '{"profile":"%s","name":"%s","launched":false,"preferences_created":%s}\n' \
|
|
"$_profile_dir" "$_name" "$_prefs_created"
|
|
return 0
|
|
fi
|
|
|
|
# ── lanzar chromium headless vía systemd-run ──────────────────────────────
|
|
# systemd-run --user aísla el proceso del cgroup del agente (evita exit-144).
|
|
# NO se pasa --disable-extensions para que la managed policy instale las
|
|
# extensiones force-listed (uBlock, web_proxy).
|
|
local _rand
|
|
_rand="$(tr -dc 'a-z0-9' </dev/urandom | head -c 8 2>/dev/null || echo "$$")"
|
|
local _unit="create-prof-${_rand}"
|
|
|
|
systemd-run \
|
|
--user \
|
|
--collect \
|
|
--unit="$_unit" \
|
|
--setenv=DISPLAY=:0 \
|
|
--setenv=XAUTHORITY="${HOME}/.Xauthority" \
|
|
"$_bin" \
|
|
"--user-data-dir=${_udd}" \
|
|
"--profile-directory=${_profile_dir}" \
|
|
"--headless=new" \
|
|
"--no-first-run" \
|
|
"--remote-debugging-port=${_port}" \
|
|
"--remote-allow-origins=*" \
|
|
"about:blank" 2>/dev/null || true
|
|
|
|
_launched=true
|
|
|
|
# ── poll: esperar a que Preferences exista ────────────────────────────────
|
|
local _elapsed=0
|
|
while [[ $_elapsed -lt $_timeout_sec ]]; do
|
|
if [[ -f "$_prefs_file" ]]; then
|
|
_prefs_created=true
|
|
break
|
|
fi
|
|
sleep 1
|
|
(( _elapsed++ )) || true
|
|
done
|
|
|
|
# ── detener el unit Y matar TODO el árbol de chromium de este udd ───────────
|
|
# Necesario para poder editar Local State sin que Chrome lo sobreescriba. Ni el
|
|
# `systemctl stop` ni un `pkill -f --user-data-dir=` bastan: los procesos hijos
|
|
# (zygote/gpu/renderer) no repiten el flag --user-data-dir pero sí referencian la
|
|
# ruta del user-data-dir en otros argumentos. Los matamos por PID seleccionando
|
|
# los procesos chromium cuyo cmdline contiene la ruta del udd (seguro: no mata
|
|
# este propio script porque filtramos por '[c]hromium').
|
|
systemctl --user kill -s SIGKILL "$_unit" 2>/dev/null || true
|
|
systemctl --user stop "$_unit" 2>/dev/null || true
|
|
local _wait=0 _pids
|
|
while :; do
|
|
_pids=$(pgrep -af '[c]hromium' 2>/dev/null | grep -F -- "${_udd}" | awk '{print $1}')
|
|
[[ -z "$_pids" ]] && break
|
|
# shellcheck disable=SC2086
|
|
kill -TERM $_pids 2>/dev/null || true
|
|
sleep 0.5
|
|
(( _wait++ )) || true
|
|
if [[ $_wait -ge 20 ]]; then
|
|
_pids=$(pgrep -af '[c]hromium' 2>/dev/null | grep -F -- "${_udd}" | awk '{print $1}')
|
|
# shellcheck disable=SC2086
|
|
[[ -n "$_pids" ]] && kill -9 $_pids 2>/dev/null || true
|
|
break
|
|
fi
|
|
done
|
|
rm -f "${_udd}/SingletonLock" 2>/dev/null || true
|
|
|
|
if [[ "$_prefs_created" == false ]]; then
|
|
echo "create_chrome_profile: timeout (${_timeout_sec}s) esperando a que se cree: ${_prefs_file}" >&2
|
|
echo " El directorio del perfil puede existir pero está vacío." >&2
|
|
printf '{"profile":"%s","name":"%s","launched":true,"preferences_created":false,"error":"timeout"}\n' \
|
|
"$_profile_dir" "$_name"
|
|
return 3
|
|
fi
|
|
|
|
# ── editar Local State para asignar nombre legible ────────────────────────
|
|
_update_local_state "$_udd" "$_local_state" "$_profile_dir" "$_name"
|
|
|
|
printf '{"profile":"%s","name":"%s","launched":true,"preferences_created":true}\n' \
|
|
"$_profile_dir" "$_name"
|
|
}
|
|
|
|
# ── helper: editar Local State con python3 ────────────────────────────────────
|
|
# Crea/actualiza info_cache.<profile_dir> con name + is_using_default_name=false
|
|
# y añade profile_dir a profiles_order si no está.
|
|
_update_local_state() {
|
|
local _udd="$1"
|
|
local _local_state="$2"
|
|
local _profile_dir="$3"
|
|
local _name="$4"
|
|
local _today
|
|
_today="$(date +%Y%m%d)"
|
|
|
|
# Si Local State no existe, crear una estructura mínima
|
|
if [[ ! -f "$_local_state" ]]; then
|
|
printf '{"profile":{"info_cache":{},"profiles_order":[]}}\n' > "$_local_state"
|
|
fi
|
|
|
|
# Backup antes de modificar (no sobreescribir el del mismo día)
|
|
local _backup="${_local_state}.bak.${_today}"
|
|
if [[ ! -f "$_backup" ]]; then
|
|
cp "$_local_state" "$_backup"
|
|
fi
|
|
|
|
# Editar con python3
|
|
if ! python3 - "$_local_state" "$_profile_dir" "$_name" <<'PY'; then
|
|
import sys, json
|
|
|
|
ls_path = sys.argv[1]
|
|
prof_dir = sys.argv[2]
|
|
prof_name = sys.argv[3]
|
|
|
|
with open(ls_path, "r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
|
|
# Asegurar estructura profile
|
|
profile_section = data.setdefault("profile", {})
|
|
info_cache = profile_section.setdefault("info_cache", {})
|
|
|
|
# Crear o actualizar la entrada del perfil en info_cache
|
|
entry = info_cache.setdefault(prof_dir, {})
|
|
entry["name"] = prof_name
|
|
entry["is_using_default_name"] = False
|
|
|
|
# Añadir a profiles_order si no está
|
|
order = profile_section.setdefault("profiles_order", [])
|
|
if prof_dir not in order:
|
|
order.append(prof_dir)
|
|
|
|
with open(ls_path, "w", encoding="utf-8") as f:
|
|
json.dump(data, f, separators=(",", ":"))
|
|
PY
|
|
echo "create_chrome_profile: error editando Local State con python3" >&2
|
|
return 4
|
|
fi
|
|
|
|
# Validar JSON tras escritura
|
|
if ! python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$_local_state" 2>/dev/null; then
|
|
echo "create_chrome_profile: JSON inválido tras escribir Local State; restaurando backup" >&2
|
|
cp "$_backup" "$_local_state"
|
|
return 4
|
|
fi
|
|
}
|
|
|
|
# ── auto-ejecución ────────────────────────────────────────────────────────────
|
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
create_chrome_profile "$@"
|
|
fi
|