#!/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