(La segunda parte de este artículo está en https://blog.rastersoft.com/?p=1386)
Hace unas semanas me compré un cacharro de tipo AndroidTV (este en concreto). Sin embargo ya tenía claro que no quería tener Android, sino un linux como $DEITY manda. Por eso busqué uno con el mismo chip que mi tablet. Por desgracia esta vez no conseguí que arrancase desde una tarjeta externa (parece que han cambiado algo en el cargador). Pero como a cabezota no me gana nadie, decidí buscar alguna alternativa.
Por supuesto no quería eliminar el Android que ya trae, más que nada por si me lo cargo y acabo con un bonito pisapapeles. Por eso decidí intentar una ruta intermedia: una vez arrancado el sistema, lanzaría un programa que mataría todos los procesos de Android y lanzaría una Debian almacenada en un disco externo, desde un entorno chroot, con su entorno gráfico y todo. De esta manera, si no enchufo el disco el sistema arrancaría Android normalmente, y si lo enchufo, arrancaría Linux.
Obviamente la cosa no es tan sencilla. Para empezar, los diversos procesos de Android se vuelven a lanzar automáticamente si mueren, por lo que no podemos utilizar un simple kill pid. Afortunadamente existen los comandos start y stop que lanzan y detienen todo el entorno gráfico. Para probarlos utilicé adb, disponible en el entorno de desarrollo de Android. Para ello primero lancé el servidor con
Y luego cada vez que necesitaba una consola en el dispositivo, lo conectaba con el cable USB y lanzaba:
Y efectivamente, si ejecutaba stop el entorno de Android desaparecía, quedando la pantalla congelada. Ejecutando start volvía a arrancar todo. Afortunadamente, aunque mi dispositivo no está completamente rooteado, si es lo suficientemente libre como para que, por defecto, entre como root desde esta shell, además de incluir un busybox con chroot. Con esto ya tengo resuelto el primer gran problema. Es cierto que todavía quedan varios servicios en marcha, como el gestor de red, pero es un problema secundario que ya resolveremos luego.
OJO: el servicio adb en el dispositivo Android tarda un poco en ser lanzado. Si acabáis de arrancarlo, sed pacientes. Por otro lado, a veces la lista de dispositivos USB con Android no está actualizada en Linux, así que es posible que haya que tocar un poco en udev para que reconozca el nuestro.
Autoarranque
Si sólo quisiese lanzar manualmente el nuevo entorno ya tendría el problema resuelto; pero yo quiero un dispositivo que arranque Debian incluso si no estoy (por ejemplo si se va la luz y luego vuelve), por lo que no queda más remedio que meter las narices en el proceso de arranque de Android.
Alguno puede pensar que ir a tan bajo nivel es exagerado, pero hay un motivo: la primera idea que se me ocurrió fue buscar algún programa nativo de Android que en el arranque lance automáticamente un script que yo le pase. Encontré varios, pero fue entonces cuando descubrí que mi cacharro no está rooteado del todo, por lo que ese script no podía lanzar un chroot ni nada por el estilo. Así pues, no queda más remedio que meter las narices más abajo.
Como en cualquier sistema Linux, una vez arrancado el núcleo se ejecuta el proceso init. Pero aquí acaban los paralelismos, porque en Android este proceso es muy diferente del SysV Init: aquí hay un conjunto de ficheros .rc con una serie de indicaciones de qué procesos hay que lanzar, en qué orden, con qué permisos, de qué dependen, y, sobre todo, si es necesario volver a lanzarlos si mueren. Estos ficheros .rc se encuentran en el raíz del sistema de ficheros, y ese es primer problema serio con el que nos encontramos: en general, el sistema raíz es de tipo rootfs. Este sistema de ficheros es un tipo de ramfs, que se inicializa durante el arranque con unos valores predeterminados, y además está montado como sólo lectura. Aunque en principio podemos hacer mount -o remount,rw / para permitir escritura, cualquier cambio que hagamos desaparecerá al reiniciar. La única manera de modificar el sistema de archivos es recompilarlo y grabar la partición entera, y eso es algo que no quiero hacer por el riesgo de cascar el sistema. Sin embargo, en /system tenemos una partición que sí es modificable, porque es de tipo ext4, así que intentaremos meter nuestro autoarranque ahí. En caso de que, por defecto, dicha partición esté montada como sólo lectura, podemos volverla de lectura/escritura con:
mount -o remount,rw /system
y devolverla a sólo lectura con
mount -o remount,ro /system
El formato de los ficheros .rc es relativamente sencillo: si una línea comienza por on XXXXX, las líneas siguientes definen los comandos a realizar cuando ocurra el evento XXXXX. Pero si empieza por service XXXXX, entonces las siguientes líneas definen a dicho servicio XXXXX, que se debe lanzar al inicio. Un ejemplo de servicio sería este:
service console /system/bin/sh
class core
console
disabled
user shell
group log
Estas líneas definen el servicio console, que se lanza ejecutando el binario /system/bin/sh. Además, debe lanzarse como usuario shell y grupo log. Si estos dos parámetros no se incluyen se lanza como root.
Una línea especial, que nos interesa especialmente, es oneshot. Cuando un servicio la incluye significa que debe lanzarse una única vez durante el arranque, y si se muere no debe lanzarse de nuevo. En cambio, aquellos servicios que no incluyen oneshot se vuelven a lanzar si mueren. El motivo de explicar esto es que lo que vamos a hacer es buscar un servicio de tipo oneshot, y reemplazar su binario por un script que primero llame al binario original, y luego haga lo que a nosotros nos interese. En mi caso tuve más suerte, y encontré este servicio:
service flash_recovery /system/etc/install-recovery.sh
class main
oneshot
Por el nombre, tiene pinta de ser lo que se lanza cuando se borran las preferencias y se quiere dejar el sistema como recién comprado. Si vamos a /system/etc, vemos que no existe el fichero install-recovery.sh, supongo que porque sólo se creará cuando se quieran borrar los datos; pero eso significa que nosotros podemos meter en su lugar un ejecutable o un script y éste se ejecutará como root cada vez que arranque el sistema. Y además, al estar en /system podemos editarlo a nuestro antojo ¡Justo lo que estábamos buscando!
Lanzamiento condicional
Ahora que ya tenemos donde meter nuestro código, la primera idea que se nos viene a la cabeza es montar un sistema Debian en, por ejemplo, /system/debian, escribir un script que monte una copia de proc, sys y dev, y meterlo directamente en /system/etc/install-recovery.sh. Esto tiene varios problemas:
- Las unidades en formato Ext2/3/4 no se montan automáticamente desde Android, por lo que tendríamos que hacerlo a mano.
- Además, si hay varias unidades USB, tendríamos que ver cual es la correcta de todas ellas
- Según como lo hagamos podemos perder por completo el acceso a Android.
- Tendremos el sistema hardcoded, grabado en piedra, y para cambiar cualquier cosa tendremos que volver a entrar en la flash de nuestro dispositivo.
La opción por la que me he decantado es la de disponer de un disco duro externo con el sistema Debian, e incluir en él un pequeño fichero ejecutable con un nombre específico. De esa manera el único cambio que hay que hacer en nuestro sistema Android es añadir un pequeño programa que se lance durante el inicio y que compruebe todas las unidades USB en busca de dicho ejecutable, y si lo encuentra, que lo lance. De esa manera podemos meter ahí toda la magia y reducir al mínimo los cambios a realizar en nuestro dispositivo Android.
Lo primero que hice fue crear un pequeño programa que comprobase constantemente si aparecía un nuevo dispositivo USB; en caso de que así fuese, intentaría montarlo como una partición EXT4 en un punto concreto y ejecutar un fichero runlinux.sh en él. Al principio lo hice todo con comandos shell, pero el resultado era bastante chapucero porque lo que hacía era comprobar de manera periódica (cada cinco segundos) si había dispositivos. Además, cada comprobación implicaba ejecutar varios grep, sed y otros comandos, lo que aumentaba el consumo de CPU. Aunque una vez lanzado el sistema Debian esta comprobación se detenía, sí seguía en marcha mientras se estuviese trabajando en Android. Por eso al final lo reescribí en C y utilizando inotify, de manera que no hace falta realizar una espera activa, sino que el programa sólo se despierta cuando se inserta un nuevo dispositivo. El código de este programa se puede bajar desde aquí. Para compilarlo basta con utilizar el comando
arm-linux-androideabi-gcc launch_debian.c -o launch_debian
El compilador arm-linux-androideabi está disponible en Debian y derivadas en el paquete gcc-arm-linux-androideabi. A continuación hay que copiar el binario a /system/bin en nuestro dispositivo Android, y añadir el siguiente script en /system/etc/install-recovery.sh:
#!/system/bin/sh
/system/bin/launch_debian &
Este script lo que hace es lanzar launch_debian en segundo plano, y éste se quedará esperando a que se conecte un nuevo dispositivo USB con alguna partición EXT4. En caso de que eso ocurra, montará dicha partición (con las opciones noatime y nodiratime; para discos duros normales no es necesario, pero es interesante para unidades flash) e intentará ejecutar el fichero runlinux.sh, pasándole como primer parámetro la ruta donde está montada la partición (por defecto /system/debian), y como segundo parámetro el dispositivo que se ha montado (por ejemplo, sdb2). Una vez que termine de ejecutarse correctamente dicho fichero, launch_debian morirá. Pero si falla la ejecución o no existe dicho fichero, el programa seguirá probando con el resto de dispositivos.
El motivo de poner un script intermedio en lugar de meter el ejecutable directamente en /system/etc/install-recovery.sh es porque así, si por cualquier motivo el sistema Android sobreescribe dicho fichero, puedo recuperarlo fácilmente desde una consola, en lugar de necesitar transferir un binario.
Generando el sistema de ficheros
Ahora que ya tenemos todo listo en el sistema Android queda generar el entorno Debian, que será el que ejecutemos, así como un script que lo lance mediante una llamada a chroot.
Vamos a empezar con el entorno Debian. Para ello formatearemos un disco en EXT3 o en EXT4 y, desde un sistema Debian o derivado (Ubuntu, por ejemplo) bajamos el paquete debootstrap. A continuación montamos manualmente el disco donde queremos generar el sistema (porque, por defecto, los escritorios lo montan con las opciones nodev y noexec, y debootstrap se niega a trabajar en esas condiciones), y desde una línea de comandos teclearemos (asumiendo que el disco donde queremos instalar el sistema base está en /mnt):
sudo debootstrap --arch=armhf --foreign --variant=minbase wheezy /mnt http://ftp.us.debian.org/debian
En este caso estamos instalando Debian wheezy, que es la rama testing a la hora de escribir este artículo. En el futuro puede ser necesario cambiar a otra rama, según se desee.
Una vez ha terminado de instalarse, editamos el fichero etc/resolv.conf para añadir un DNS por defecto. En mi caso he utilizado los de google, pero se puede utilizar cualquiera. Mi fichero quedó así:
# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
#nameserver 127.0.1.1
nameserver 8.8.8.8
nameserver 8.8.4.4
Ahora desmontamos el disco y lo conectamos a nuestro sistema Android. Luego ejecutamos ./adb shell para entrar en él, y procederemos a montar la unidad donde instalamos el sistema en, por ejemplo, /mnt/usb_storage. Una vez hecho esto, y tras asegurarnos de que tenemos conexión a internet desde nuestro dispositivo Android, ejecutamos los siguientes comandos (también en nuestro dispositivo Android mediante adb shell) para entrar en el entorno chroot y terminar la instalación del sistema base:
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH
export HOME=/root
export LD_LIBRARY_PATH=
mount -o bind /proc /mnt/usb_storage/proc
mount -o bind /sys /mnt/usb_storage/sys
mount -o bind /dev /mnt/usb_storage/dev
mount -o bind /dev/pts /mnt/usb_storage/dev/pts
busybox chroot /mnt/usb_storage /bin/bash -l
debootstrap/debootstrap --second-stage
Con esto terminamos la instalación del sistema base, pero ahora toca configurarlo. En primer lugar vamos a configurar APT para poder bajar paquetes y demás. Para ello salimos del entorno chroot, desmontamos el disco, lo montamos en nuestro ordenador y editamos el fichero /etc/apt/sources.list para añadir las siguientes líneas:
deb http://ftp.de.debian.org/debian wheezy main contrib non-free
deb http://ftp.de.debian.org/debian wheezy-updates main contrib non-free
deb http://security.debian.org/ wheezy/updates main contrib non-free
Ahora podemos volver a montar el disco en nuestro sistema Android y volver a lanzar el entorno chroot. Vamos a instalar ahora las herramientas básicas que nos faltan para terminar de configurar el sistema con autoarranque y poder dedicarnos a jugar con él. Para ello vamos a instalar el servicio de gestión de redes, para tener acceso a internet completo, el servidor SSH para poder gestionar todo de manera remota, ifconfig, ping, el cliente DHCP y un editor de textos (normalmente uso nano pero da problemas desde adb; por eso uso vim de manera temporal) para editar los últimos ficheros necesarios para que todo funcione automáticamente. Para ello, una vez que estamos de nuevo dentro de la jaula chroot (¡¡¡no olvidarse de los exports!!!), ejecutamos los siguientes comandos:
apt-get update
apt-get dist-upgrade -y
apt-get install ifupdown openssh-server net-tools vim iputils-ping isc-dhcp-client
El siguiente paso es crear el fichero runlinux.sh, que será ejecutado desde nuestro entorno Android por launch_debian. Este script preparará el entorno para lanzar una sesión chroot, y debe contener las siguientes líneas:
#!/system/bin/sh
# stop the Android system
stop
sleep 1
# stop the daemons to ensure that
# they don't disturb the debian system
# (can't kill them because INIT would
# relaunch them)
# Also allows to send them to SWAP
busybox killall -SIGSTOP netd
busybox killall -SIGSTOP vold
busybox killall -SIGSTOP displayd
busybox killall -SIGSTOP ueventd
busybox killall -SIGSTOP debuggerd
busybox killall -SIGSTOP rild
busybox killall -SIGSTOP drmserver
busybox killall -SIGSTOP mediaserver
busybox killall -SIGSTOP installd
busybox killall -SIGSTOP servicemanager
# mount proc, sys, dev and dev/pts
mount -o bind /proc $1/proc
mount -o bind /sys $1/sys
mount -o bind /dev $1/dev
mount -o bind /dev/pts $1/dev/pts
export HOME=/
export LD_LIBRARY_PATH=
export PATH=/sbin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
# set the Framebuffer devices where standard apps expect them
cp -a /dev/graphics/* /dev
# launch our Debian system
/system/bin/busybox chroot $1 /bin/system.sh
Lo primero que hace este script es detener el entorno Android, de manera que liberamos la memoria consumida por éste. A continuación envía una señal SIGSTOP a diversos procesos de Android que siguen en marcha, como el gestor de red. Esto es necesario porque, si no, interferiría con las herramientas de Debian. Por otro lado, como ya expliqué antes, no podemos matarlos porque el sistema init los lanzaría de nuevo.
El siguiente paso consiste en montar proc, sys, dev y dev/pts en el sistema Debian. Vemos que utiliza el primer parámetro, pues launch_debian pasa ahí la ruta donde se montó la partición.
A continuación inicializamos las variables de entorno. Vemos que borramos LD_LIBRARY_PATH. Esto es así porque algunos Android la utilizan para añadir otras rutas con bibliotecas, pero en nuestro caso nos interferiría.
Luego se copian todos los ficheros de dispositivo situados en /dev/graphics a /dev. Esto es así porque en Android los dispositivos framebuffer están en esa ruta alternativa, por lo que tenemos que copiarlos a donde las aplicaciones de Linux esperan encontrarlos.
Por último ejecutamos nuestro chroot. Cabe indicar que es necesario poner la ruta completa de busybox porque hemos sobreescrito la variable de entorno PATH. Vemos que lanzamos el script /bin/system.sh. Este script será quien lance todo lo que queramos lanzar en nuestro sistema Debian (servidor X, demonios…). En el caso actual lo que hice fue poner en él las siguientes líneas:
#!/bin/bash
service networking restart
service ssh restart
Con esto inicializo la red (necesario si el disco estaba enchufado al encender el dispositivo Android, porque launch_debian lo detectará tan rápido que no dará tiempo a que arranque Android y configure la red), y lanzo el servidor ssh, que me permitirá entrar de manera remota. Para que estos dos comandos funcionen, sin embargo, es imprescindible configurar algunas cosas primero:
- Para que ssh funcione, lo primero que es necesario es ponerle clave al usuario root. Para ello utilizamos el comando passwd. Una vez hecho esto editamos /etc/ssh/sshd_config para ajustar la configuración.
Con todo esto ya tenemos el sistema base configurado y listo. Salimos de nuestra jaula chroot, ejecutamos sync por si acaso, y reiniciamos nuestro dispositivo Android. Si todo va bien, el entorno Android no debería ni siquiera arrancar, y deberíamos poder entrar mediante ssh.
El siguiente paso será añadir BitTorrent, servidor FTP y algún cliente multimedia para utilizarlo como equipo de salón, pero eso lo dejo para futuras entradas.