#!/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 --profile --name [--port N] [--chrome-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- 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/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 # Matar por PID los procesos cuyo comm es exactamente "chromium" (pgrep -x) y cuyo cmdline # contiene la ruta del udd. Usamos pgrep -x para NO auto-matchear grep/pgrep: el path del udd # contiene la cadena "chromium" (~/.config/chromium-cdp). local _wait=0 _p _pids while :; do _pids="" for _p in $(pgrep -x chromium 2>/dev/null); do tr '\0' ' ' < "/proc/$_p/cmdline" 2>/dev/null | grep -qF -- "$_udd" && _pids="$_pids $_p" done [[ -z "${_pids// }" ]] && break # shellcheck disable=SC2086 kill -TERM $_pids 2>/dev/null || true sleep 0.5 (( _wait++ )) || true if [[ $_wait -ge 20 ]]; then # shellcheck disable=SC2086 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. 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