feat(android-capture): emulateur AVD en service, installation des APK deposes
L'emulateur etait le maillon manquant : la capture tournait deja en service, mais la cible avd ne vivait que si une session interactive la lancait. - android-emulator : AVD headless avec -tcpdump vers pcap/avd/. JAVA_HOME force au JDK 17 qu'androidenv utilise lui-meme, parce que le runner appelle la version non encapsulee du SDK, celle qui reclame JAVA_HOME et sans laquelle l'AVD n'est jamais cree. - android-avd-install : timer qui installe au boot les APK deposes dans apks/. - aucune unite bloquante : ni wantedBy sur une oneshot d'installation, ni ExecStartPost qui attend le boot Android. Une unite laissee en cours de demarrage bloque toute activation nixos-rebuild, qui sort en « unit already loaded ». - le dossier apks/ est cree par le service, une regle tmpfiles ne suffisait pas. Verifie sur tower : switch code 0, qemu actif sous le service, emu.pcap ecrit et croissant sous l'utilisateur de capture, co.hinge.app installe dans l'AVD du service.
This commit is contained in:
@@ -101,6 +101,82 @@ let
|
|||||||
echo "${target}: $latest"
|
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 = {
|
hardening = {
|
||||||
User = user;
|
User = user;
|
||||||
Group = user;
|
Group = user;
|
||||||
@@ -130,15 +206,63 @@ in
|
|||||||
[
|
[
|
||||||
"d ${stateDir} 0750 ${user} ${user} -"
|
"d ${stateDir} 0750 ${user} ${user} -"
|
||||||
"d ${stateDir}/digest 0770 ${user} ${user} -"
|
"d ${stateDir}/digest 0770 ${user} ${user} -"
|
||||||
|
"d ${stateDir}/apks 0770 ${user} ${user} -"
|
||||||
]
|
]
|
||||||
++ lib.mapAttrsToList
|
++ lib.mapAttrsToList
|
||||||
(target: _: "d ${stateDir}/pcap/${target} 0770 ${user} ${user} -")
|
(target: _: "d ${stateDir}/pcap/${target} 0770 ${user} ${user} -")
|
||||||
captures;
|
captures;
|
||||||
|
|
||||||
# Une unite de capture par cible qui a une interface hote, plus un digest et un
|
# Une seule affectation a systemd.services : en declarer deux dans le meme
|
||||||
# timer par cible. Une seule affectation a systemd.services : deux clefs
|
# attrebat (services.android-emulator puis services = ...) est une erreur
|
||||||
# identiques dans le meme attrebat seraient une erreur d'evaluation.
|
# d'evaluation, donc tout est fusionne ici.
|
||||||
systemd.services =
|
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'
|
lib.mapAttrs'
|
||||||
(target: ifaces:
|
(target: ifaces:
|
||||||
lib.nameValuePair "android-capture-${target}" {
|
lib.nameValuePair "android-capture-${target}" {
|
||||||
@@ -169,7 +293,18 @@ in
|
|||||||
})
|
})
|
||||||
captures;
|
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: _:
|
(target: _:
|
||||||
lib.nameValuePair "android-capture-digest-${target}" {
|
lib.nameValuePair "android-capture-digest-${target}" {
|
||||||
description = "Digest periodique de la capture Android (${target})";
|
description = "Digest periodique de la capture Android (${target})";
|
||||||
|
|||||||
Reference in New Issue
Block a user