diff --git a/services/android-capture/default.nix b/services/android-capture/default.nix index f5e4b75..a15a37c 100644 --- a/services/android-capture/default.nix +++ b/services/android-capture/default.nix @@ -101,6 +101,82 @@ let echo "${target}: $latest" ''; + # ---- Emulateur AVD en service ------------------------------------------- + # Le runner d'androidenv cree l'AVD dans un dossier temporaire, boote, puis sort + # en laissant qemu tourner. Lance tel quel comme ExecStart, systemd verrait le + # service « termine » alors que l'emulateur tourne encore : on l'enveloppe pour + # rester vivant tant que qemu tient la capture, ce qui donne un service que + # systemd suit vraiment et qu'un `systemctl stop` arrete, emulateur compris. + # + # Le chemin du runner est epingle volontairement : il vient d'une flake separee + # (le projet hinge-re, qui construit l'image Play Store). Si cette flake change + # d'image ou d'options, ce chemin change et il faut le mettre a jour ici comme + # dans son justfile. + emulatorRunner = "/nix/store/kyfmkz5jayxkz1gg6s1p9lsgdgjf6mf2-hinge-test/bin/run-test-emulator"; + + emulatorScript = pkgs.writeShellScript "android-emulator" '' + set -eu + export HOME=${stateDir} + # avdmanager (qui cree l'AVD) est du Java et reclame JAVA_HOME : le runner + # appelle la version NON encapsulee de l'Android SDK, celle qui n'embarque + # pas le JDK. On reprend le JDK qu'androidenv utilise lui-meme dans son + # wrapper (openjdk 17), un JDK plus recent fait echouer ces cmdline-tools. + export JAVA_HOME=${pkgs.jdk17} + export PATH=${pkgs.jdk17}/bin:${pkgs.procps}/bin:${pkgs.coreutils}/bin:$PATH + export NIX_ANDROID_EMULATOR_FLAGS="-no-window -no-audio -gpu swiftshader_indirect -no-snapshot -memory 3072 -no-metrics -tcpdump ${stateDir}/pcap/avd/emu.pcap" + # Le dossier des APK est cree par le service lui-meme : c'est lui qui le + # consomme, et une regle tmpfiles ne garantit pas sa presence au premier + # demarrage qui suit l'ajout de la regle. + install -d -m 0770 ${stateDir}/apks + ${emulatorRunner} & + runner=$! + + # On attend l'apparition de la capture, borne dans le temps, et on + # echoue si l'emulateur ne demarre pas plutot que de rester en vie pour rien. + demarre=0 + for _ in $(seq 1 90); do + if pgrep -f "tcpdump ${stateDir}/pcap/avd/emu.pcap" >/dev/null 2>&1; then + demarre=1 + break + fi + # Runner sorti sans lancer qemu : inutile d'attendre la fin du delai. + kill -0 "$runner" 2>/dev/null || break + sleep 2 + done + if [ "$demarre" != 1 ]; then + echo "l'emulateur n'a pas demarre" >&2 + exit 1 + fi + + # Reste vivant tant que la capture de cette cible est ouverte : aucun autre + # process de la machine ne porte ce chemin de fichier. + while pgrep -f "tcpdump ${stateDir}/pcap/avd/emu.pcap" >/dev/null 2>&1; do + sleep 10 + done + ''; + + # Installe les APK de travail des que l'appareil a fini de booter. L'AVD etant + # recree a chaque demarrage, l'installation fait partie du cycle de vie du + # service : on depose un fichier dans apks/ (dossier accessible en ecriture au + # groupe) et c'est repris au boot suivant. + installApkScript = pkgs.writeShellScript "android-avd-install" '' + set -u + adb=${pkgs.android-tools}/bin/adb + for _ in $(seq 1 60); do + $adb wait-for-device >/dev/null 2>&1 && break + sleep 5 + done + for _ in $(seq 1 60); do + [ "$($adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" = "1" ] && break + sleep 5 + done + for apk in ${stateDir}/apks/*.apk; do + [ -f "$apk" ] || continue + echo "installation de $apk" + $adb install -r "$apk" || echo "echec de l'installation de $apk" + done + ''; + hardening = { User = user; Group = user; @@ -130,15 +206,63 @@ in [ "d ${stateDir} 0750 ${user} ${user} -" "d ${stateDir}/digest 0770 ${user} ${user} -" + "d ${stateDir}/apks 0770 ${user} ${user} -" ] ++ lib.mapAttrsToList (target: _: "d ${stateDir}/pcap/${target} 0770 ${user} ${user} -") captures; - # Une unite de capture par cible qui a une interface hote, plus un digest et un - # timer par cible. Une seule affectation a systemd.services : deux clefs - # identiques dans le meme attrebat seraient une erreur d'evaluation. + # Une seule affectation a systemd.services : en declarer deux dans le meme + # attrebat (services.android-emulator puis services = ...) est une erreur + # d'evaluation, donc tout est fusionne ici. systemd.services = + { + # L'emulateur lui-meme, en service : sans lui la cible avd ne vit que tant + # qu'une session interactive veut bien le lancer. L'AVD est recree a chaque + # demarrage (le runner d'androidenv travaille dans un dossier temporaire), + # d'ou l'installation des APK deposes dans apks/ juste apres le boot. + android-emulator = { + description = "Emulateur Android (AVD) alimentant la cible de capture avd"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + # PATH n'est pas herite d'une session interactive : sans jdk explicite, + # avdmanager (Java) sort en « JAVA_HOME is not set » et l'AVD n'est jamais + # cree. Sans procps ni coreutils, les `seq` et `pgrep` du script sont + # introuvables et l'unite se croit terminee aussitot. + path = with pkgs; [ jdk procps coreutils android-tools ]; + serviceConfig = hardening // { + # Type simple : l'unite est consideree demarree des le fork, donc un + # switch n'attend jamais le boot de l'AVD. Le script reste vivant pour + # que systemd suive l'emulateur, et echoue borne dans le temps si + # l'emulateur ne demarre pas. + ExecStart = "${emulatorScript}"; + Restart = "on-failure"; + RestartSec = "20s"; + # qemu demande du temps pour rendre la main proprement. + TimeoutStopSec = "60s"; + # /dev/kvm est en 0666 sur cette machine : aucun groupe kvm a rejoindre. + }; + }; + + # L'installation des APK de travail attend le boot d'un AVD neuf, donc + # plusieurs minutes : unite separee pour que l'emulateur demarre tout de + # suite et que le systeme n'attende pas un boot Android. + android-avd-install = { + description = "Installation des APK de travail dans l'AVD"; + # Pas de wantedBy : une oneshot tiree par multi-user.target retient toute + # l'activation, donc le demarrage du systeme, pendant le boot Android. + # C'est un timer qui la declenche, et un timer ne retient rien. + after = [ "android-emulator.service" ]; + path = with pkgs; [ android-tools coreutils ]; + serviceConfig = hardening // { + Type = "oneshot"; + ExecStart = "${installApkScript}"; + # Un AVD neuf peut mettre dix minutes a booter sur un hote charge. + TimeoutStartSec = "900"; + }; + }; + } + // lib.mapAttrs' (target: ifaces: lib.nameValuePair "android-capture-${target}" { @@ -169,7 +293,18 @@ in }) captures; - systemd.timers = lib.mapAttrs' + systemd.timers = + { + android-avd-install = { + description = "Installation des APK de travail dans l'AVD apres le boot"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnBootSec = "2min"; + Unit = "android-avd-install.service"; + }; + }; + } + // lib.mapAttrs' (target: _: lib.nameValuePair "android-capture-digest-${target}" { description = "Digest periodique de la capture Android (${target})";