Merge pull request 'Rétablir le redémarrage à chaud de SpringBoard' (#6) from agent/redemarrage-interface into ipod_touch_2g

This commit was merged in pull request #6.
This commit is contained in:
Mahtan committed 2026-10-01 12:15:33 +02:00
commit 6cdda3bc13
18 files changed
+732 -56

No files matched your search

+36 -13
View File
@@ -2,6 +2,38 @@
État au 2026-10-01. Environnement : conteneur Arch Linux, utilisateur `dev` avec sudo, sans `CAP_NET_ADMIN`, sans `/dev/ppp` ni TUN.
## Redémarrage à chaud de l'interface
Le blocage de `respring` est reproduit sur 5H11a : le nouveau SpringBoard reste
bloqué dans `IOServiceClose`, après l'ouverture d'AppleM2TVOut. Le modèle de
sortie TV limitait ses interruptions à deux pour toute la vie de QEMU. Il répond
maintenant à chaque soumission, conserve les événements masqués et acquitte les
bits demandés. Le LCD dispose également de registres distincts pour le masque
(0x08) et l'état W1C (0x0c), conformément aux accès du pilote AppleM2CLCD.
Pour le reset matériel, les états LCD, TV et MIPI sont réinitialisés. La machine
réalise désormais le périphérique MIPI : sans cette étape, son callback de reset
n'était jamais appelé, ce qui faisait échouer la lecture de l'identifiant de dalle.
Le tactile libère les transferts/trames en cours, annule ses timers et relâche
son interruption. SPI vide ses FIFO et abaisse aussi sa ligne d'interruption.
`ipod-emu respring -W` et `ipod-emu reset -W` synchronisent la NAND, mémorisent
la position du journal puis attendent uniquement les nouveaux marqueurs. Le
client `unlock` utilise la même position. `ipod-agent respring` et `ipod-qmp reset`
restent des commandes brutes ; utiliser le lanceur pour attendre le nouveau boot.
Régressions : neuf tests tactile/SPI et trois tests d'affichage sous ASan/UBSan,
cinq tests du lanceur et trois resets QMP/QTest des périphériques d'affichage
réels. Le test natif `tests/test-display-session.py` vérifie le déverrouillage,
la calculatrice au premier plan, HTTP local, le fichier témoin en NAND et les PID
QEMU/relais conservés. Diagnostics : `work/interface-20261001/`.
Validation native finale : trois resprings et deux resets complets par firmware
(5H11a et 5F138), avec les mêmes PID QEMU/relais. La calculatrice affiche
`7 × 8 = 56` après les relances sur les deux versions. Les resprings complets
prennent environ 26–28 s, les resets 64–80 s dans cet environnement partagé.
Synthèse : `work/interface-20261001/validation-finale.json`.
La session utilisateur sur VNC 5900 reste sur le binaire précédemment déployé.
## Reprise PPP après reset matériel
L'UART possède maintenant un callback de reset : registres et FIFO remis à
@@ -20,19 +52,10 @@ suffit donc pas à qualifier la stabilité du service.
Les deux régressions UART passent sous ASan/UBSan, dont trois reconnexions avec
émission et réception sur un vrai backend socket QEMU.
La reprise réseau et le retour de SpringBoard sont deux validations distinctes.
Les essais complets montrent encore une défaillance de l'affichage après le
reset : sur 5H11a, le modèle MIPI garde son état et le pilote de dalle échoue ;
une correction expérimentale de MIPI permet au pilote de démarrer mais laisse
SpringBoard bloqué dans `IOServiceClose` depuis la finalisation d'un objet
IOMobileFramebuffer. Le compteur matériel, la RAM et plusieurs autres callbacks
ont été explorés sans résoudre ce blocage. Ces essais ne font pas partie du
correctif PPP. `ipod-emu wait` peut reconnaître les anciens marqueurs du journal
après un reset brut ; ne pas le prendre comme preuve du nouveau boot.
Diagnostics conservés dans `work/ppp-20261001` : journaux, pile des threads de
SpringBoard, correctifs expérimentaux et validation réseau. La session utilisateur
sur VNC 5900 et ses images d'origine ont été conservées.
Les premiers essais du correctif PPP laissaient encore l'affichage bloqué après
reset. Les causes et les corrections sont décrites dans la section précédente.
Les anciens diagnostics sont conservés dans `work/ppp-20261001` ; ils précèdent
la correction de la sortie TV et ne décrivent plus l'état actuel du code.
## Dernière mise à jour — RTC D1759
+50 -20
View File
@@ -71,28 +71,35 @@ conserver sa session précédente ou utiliser un environnement de test séparé.
Le dossier de diagnostic `work/reprise-20260930` conserve une session avec l'agent
v11 et HelloEmu, indépendante de la session d'origine.
## Reprise PPP après un reset matériel
Le reset de l'UART vide les registres et la FIFO, baisse son IRQ et ferme la
connexion série. `ipod-netd` voit cette fermeture, se reconnecte au même socket
et renégocie LCP/IPCP avec le nouveau `pppd`. Le relais et QEMU gardent leur PID ;
la NAND de session est conservée.
Pour diagnostiquer un reset sur une **session de test séparée** :
## Redémarrage à chaud
```bash
ipod-agent sync # synchroniser les écritures avant le reset brut
ipod-qmp reset # system_reset, sans synchronisation implicite
ipod-agent wait 90 # attendre le retour du réseau
ipod-agent ping
ipod-agent net # vérifier ppp0 up et configd publié
ipod-emu respring -W # relance SpringBoard, attend puis déverrouille
ipod-emu reset -W # redémarre iOS dans le même processus QEMU
```
Le retour de PPP ne valide pas celui de l'interface : SpringBoard peut encore
échouer ou se bloquer après un reset matériel. `ipod-emu wait` examine le journal
complet et peut reconnaître les marqueurs de l'ancien boot. Pour retrouver
l'interface, arrêter puis relancer QEMU avec **le même firmware**, sans `-f`.
Les corrections complètes du reset de l'affichage restent à traiter séparément.
Les deux commandes synchronisent les écritures avant la relance. Elles conservent
la NAND de session et les processus QEMU et `ipod-netd`. Sans `-W`, elles rendent
la main après la demande ; `ipod-emu wait` peut attendre la suite.
L'attente ne lit que les nouveaux marqueurs du journal, puis vérifie le
déverrouillage. Aucun glissement automatique n'est effectué si un code est activé.
La sortie TV émulée répond à chaque soumission, sans l'ancien quota de deux
interruptions qui bloquait SpringBoard dans `IOServiceClose`. Le LCD sépare le
masque d'interruptions de leur acquittement. Un reset réinitialise aussi le
contrôleur MIPI, l'état du tactile et les interruptions SPI.
Le reset de l'UART ferme la connexion série. `ipod-netd` se reconnecte au même
socket et renégocie LCP/IPCP avec le nouveau `pppd`. Pour vérifier le réseau :
```bash
ipod-agent ping
ipod-agent net # ppp0 up et configd publié
```
`ipod-agent respring` et `ipod-qmp reset` restent les accès bruts, sans attente
ni mise à jour du point de départ du journal. Avant un reset brut, exécuter
`ipod-agent sync`. Préférer les commandes `ipod-emu` pour un cycle complet.
## Tester nos apps
@@ -166,6 +173,8 @@ Depuis le dépôt, les tests sans firmware sont :
```bash
contrib/ipod-emu/tests/run-uart # reset des registres/FIFO/IRQ et trois reconnexions socket, ASan/UBSan
contrib/ipod-emu/tests/run-display # LCD, MIPI, sortie TV et resets, ASan/UBSan
python3 contrib/ipod-emu/tests/test-reset-launcher.py # synchronisation et nouveaux marqueurs
contrib/ipod-emu/tests/run-rtc # protocole I²C et horloge D1759, ASan/UBSan
contrib/ipod-emu/tests/run-multitouch # build QEMU arm existant, Ninja, compilateur avec ASan/UBSan
python3 contrib/ipod-emu/tests/test-transfers.py # Python et compilateur C, sockets locales
@@ -225,14 +234,35 @@ résultat détaillé de la validation du 1er octobre sont décrits dans `HANDOFF
Le décalage écrit par iOS reste en mémoire pendant la session ; une nouvelle instance QEMU repart
de sa base RTC. Les IRQ d'alarme matérielle ne sont pas émulées. Voir [RTC.md](RTC.md).
L'image de base est en français, région France, fuseau Europe/Paris.
- **Relance de SpringBoard** : `respring` peut bloquer la reconstruction du catalogue d'apps. `push-app` redémarre
donc l'émulateur sans reconstruire l'image ni réinitialiser la NAND. Avec l'agent v11, le déverrouillage puis
- **Relance de SpringBoard** : `ipod-emu respring -W` attend le retour de l'interface et son déverrouillage.
`push-app` garde son redémarrage de QEMU, sans reconstruire l'image ni réinitialiser la NAND. Avec l'agent v11, le déverrouillage puis
le lancement de HelloEmu au premier plan sont validés après cette installation. Les corrections GPIO/SPI
ont permis huit cycles veille/réveil alternant Home et Power sur 5H11a. Le déverrouillage automatique
continue à vérifier l'état de l'écran.
- **Veille** : l'agent v10 pose `SleepDisabled` sur `IOPMrootDomain`, le SoC ne sachant pas reprendre une veille profonde.
- Pas de son, pas d'accélération graphique (SpringBoard tourne avec `LK_ENABLE_MBX2D=0`).
## Régression native du redémarrage
Avec un firmware local, le test QMP/QTest vérifie trois resets des vrais
périphériques d'affichage (CPU arrêté, NAND en lecture seule) :
```bash
python3 contrib/ipod-emu/tests/test-display-reset.py --firmware /chemin/images/5H11a
```
Pour vérifier iOS, préparer et démarrer une **session de test séparée**, avec un
port d'agent distinct. Le test suivant relance cette session, ouvre la calculatrice
et conserve les captures et résultats JSON. Il vérifie le déverrouillage, les PID,
la persistance d'un fichier témoin et HTTP avec un serveur local temporaire :
```bash
python3 contrib/ipod-emu/tests/test-display-session.py \
--emu-home /chemin/session-test --agent-port 7789 \
--action respring --cycles 3 --output /chemin/resultats-respring
# Même commande avec --action reset pour le redémarrage complet d'iOS.
```
## Reconstruire les images
`ipod-build-fw 5H11a` télécharge l'IPSW iPod2,1 chez Apple et les clés sur api.ipsw.me, génère la NOR, déchiffre et
+6
View File
@@ -96,7 +96,13 @@ def unlock(a):
sys.exit("Un code de verrouillage est activé : déverrouiller manuellement.")
# Le pilote tactile doit avoir terminé son démarrage avant tout glissement.
try:
offset_path = os.path.join(emu_home(), "run", "boot-log-offset")
offset = 0
if os.path.exists(offset_path):
with open(offset_path) as f:
offset = int(f.read())
with open(os.path.join(emu_home(), "run", "serial.log"), "rb") as f:
f.seek(offset)
serial = f.read()
except OSError as e:
sys.exit(f"Console de session inaccessible : {e}")
+32 -3
View File
@@ -19,6 +19,8 @@ Usage : ipod-emu start [-f] [-W] [-w FW] démarre (headless, VNC $VNC, QMP $RU
-f repart d'une NAND vierge (sinon garde l'état de la session précédente)
-W attend que SpringBoard soit prêt à recevoir des touches
-w FW firmware à utiliser (défaut : $FW ; dispo : $(ls "$EMU_HOME/images" 2>/dev/null | tr '\n' ' '))
ipod-emu respring [-W] relance SpringBoard dans la session courante
ipod-emu reset [-W] synchronise la NAND et redémarre iOS dans le même QEMU
ipod-emu wait [SECONDES] attend SpringBoard (défaut 300 s) — n'envoyez PAS de touch avant :
le pilote tactile doit avoir terminé son démarrage
ipod-emu stop | status | log | console
@@ -27,9 +29,16 @@ USAGE
exit "${1:-0}"
}
boot_log() {
local offset
offset=$(cat "$RUN/boot-log-offset" 2>/dev/null || echo 0)
[[ "$offset" =~ ^[0-9]+$ ]] || return 1
tail -c "+$((offset + 1))" "$RUN/serial.log"
}
ready() {
local n; n=$(grep -ac "AppleMultitouchSPIUserClient::attach" "$RUN/serial.log" 2>/dev/null); [ "${n:-0}" -ge 2 ] &&
grep -aq 'SCHelper.*"EXIT"' "$RUN/serial.log"
local n; n=$(boot_log | grep -ac "AppleMultitouchSPIUserClient::attach"); [ "${n:-0}" -ge 2 ] &&
grep -a 'SCHelper.*"EXIT"' <(boot_log) >/dev/null
}
wait_ready() {
@@ -37,7 +46,7 @@ wait_ready() {
# Le RTC D1759 fournit l'heure avant le démarrage des services iOS.
until ready; do
[ -n "$(pid)" ] || { echo "L'émulateur s'est arrêté (voir $RUN/qemu.log)" >&2; return 1; }
grep -aq 'Fatal Exception\|MACH Reboot' "$RUN/serial.log" && { echo "Kernel panic / reboot (voir $RUN/serial.log)" >&2; return 1; }
grep -a 'Fatal Exception\|MACH Reboot' <(boot_log) >/dev/null && { echo "Kernel panic / reboot (voir $RUN/serial.log)" >&2; return 1; }
[ $((SECONDS - s)) -lt "$t" ] || { echo "Timeout : SpringBoard pas prêt après ${t}s" >&2; return 1; }
sleep 2
done
@@ -82,6 +91,8 @@ start)
fi
stop_netd
rm -f "$RUN/qmp.sock" "$RUN/serial.log"
printf '0\n' > "$RUN/boot-log-offset.tmp"
mv "$RUN/boot-log-offset.tmp" "$RUN/boot-log-offset"
mopts=$(cat "$img/machine-opts" 2>/dev/null || true) # ex. ",legacy-nor=off"
if [[ "$mopts" == *",nand-write=on"* ]] &&
[ "$(cat "$RUN/nand/.ipod-nand-format" 2>/dev/null || true)" != "vsvfl-production-v1" ]; then
@@ -124,6 +135,24 @@ start)
echo "Logs iOS : $RUN/serial.log"
[ $waitr = 0 ] || wait_ready
;;
respring|reset)
waitr=0
while getopts "W" o; do case $o in W) waitr=1 ;; *) usage 1 ;; esac; done
shift $((OPTIND - 1))
[ "$#" = 0 ] || usage 1
pid >/dev/null || { echo "L'émulateur n'est pas lancé" >&2; exit 1; }
timeout 15 "$TOOLS_DIR/ipod-agent" sync
offset=$(stat -c %s "$RUN/serial.log")
printf '%s\n' "$offset" > "$RUN/boot-log-offset.tmp"
mv "$RUN/boot-log-offset.tmp" "$RUN/boot-log-offset"
if [ "$cmd" = reset ]; then
"$TOOLS_DIR/ipod-qmp" -s "$RUN/qmp.sock" reset
echo "Reset demandé ; QEMU et NAND conservés"
else
timeout 15 "$TOOLS_DIR/ipod-agent" respring
fi
[ "$waitr" = 0 ] || wait_ready
;;
wait) wait_ready "${1:-300}" ;;
stop)
p=$(pid) || { stop_netd; echo "Pas lancé"; exit 0; }
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""Compile et exécute les régressions LCD, MIPI et sortie TV avec ASan/UBSan sur un build QEMU existant."""
import argparse
import json
import os
from pathlib import Path
import shlex
import subprocess
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--build', type=Path)
a = p.parse_args()
root = Path(__file__).resolve().parents[3]
build = (a.build or root / 'build').resolve()
source = Path(__file__).with_name('test-display.c')
commands = json.loads((build / 'compile_commands.json').read_text())
c = next(c for c in commands if c['file'].endswith('ipod_touch_lcd.c'))
old_root = str(Path(c['directory']).parent)
args = shlex.split(c['command'].replace(old_root, str(root)))
obj = str(build / 'test-ipod-display.o')
args[args.index('-c') + 1] = str(source)
args[args.index('-o') + 1] = obj
args[args.index('-MF') + 1] = obj + '.d'
args += ['-fsanitize=address,undefined', '-fno-omit-frame-pointer']
subprocess.run(args, cwd=build, check=True)
links = json.loads(subprocess.check_output(
['ninja', '-C', str(build), '-t', 'compdb', '-x', 'c_LINKER_RSP', 'c_LINKER'], text=True))
link = next(c for c in links if c.get('output') == 'qemu-system-arm')
args = shlex.split(link['command'].replace(old_root, str(root)))
args = [x for x in args if x not in (
'qemu-system-arm.p/system_main.c.o',
'libqemu-arm-softmmu.fa.p/hw_arm_ipod_touch_lcd.c.o',
'libqemu-arm-softmmu.fa.p/hw_arm_ipod_touch_mipi_dsi.c.o',
'libqemu-arm-softmmu.fa.p/hw_arm_ipod_touch_tvout.c.o')]
out = str(build / 'test-ipod-display')
args[args.index('-o') + 1] = out
args += [obj, '-fsanitize=address,undefined']
subprocess.run(args, cwd=build, check=True)
# QEMU conserve volontairement ses registres de types et sa boucle globale à la sortie.
env = dict(os.environ, ASAN_OPTIONS='detect_leaks=0', UBSAN_OPTIONS='halt_on_error=1')
subprocess.run([out], env=env, check=True)
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Vérifie le reset des périphériques d’affichage de la machine, CPU arrêté et NAND en lecture seule."""
import argparse
import json
from pathlib import Path
import socket
import subprocess
import tempfile
import time
def main():
parser = argparse.ArgumentParser(description=__doc__)
root = Path(__file__).resolve().parents[3]
parser.add_argument('--qemu', type=Path, default=root / 'build/qemu-system-arm')
parser.add_argument('--firmware', type=Path, required=True, help='dossier avec bootrom, nor.bin et nand/')
args = parser.parse_args()
fw = args.firmware.resolve()
with tempfile.TemporaryDirectory(prefix='ipod-reset-') as directory:
directory = Path(directory)
paths = [directory / name for name in ['qmp.sock', 'qtest.sock']]
with (directory / 'qemu.log').open('w+') as log:
proc = subprocess.Popen([
str(args.qemu.resolve()), '-M',
f'iPod-Touch,bootrom={fw}/bootrom,nor={fw}/nor.bin,nand={fw}/nand,legacy-nor=off,nand-write=off',
'-cpu', 'max', '-m', '2G', '-S', '-display', 'none', '-serial', 'null', '-serial', 'null',
'-qmp', f'unix:{paths[0]},server=on,wait=off',
'-qtest', f'unix:{paths[1]},server=on,wait=off'], stdout=log, stderr=log)
connections = []
try:
deadline = time.monotonic() + 10
while not all(p.exists() for p in paths):
if proc.poll() is not None or time.monotonic() >= deadline:
log.seek(0)
raise RuntimeError(log.read())
time.sleep(.02)
for path in paths:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.settimeout(5)
sock.connect(str(path))
connections.append((sock, sock.makefile('rwb')))
qmp, test = [f for _, f in connections]
json.loads(qmp.readline())
def command(name):
qmp.write((json.dumps({'execute': name}) + '\n').encode())
qmp.flush()
while True:
r = json.loads(qmp.readline())
if 'error' in r:
raise RuntimeError(r['error'])
if 'return' in r:
return r['return']
def access(line):
test.write((line + '\n').encode())
test.flush()
r = test.readline().decode().strip()
assert r.startswith('OK'), r
return int(r.split()[1], 0) if len(r.split()) > 1 else None
command('qmp_capabilities')
# Registres d'affichage de la machine réelle : le reset doit atteindre MIPI.
regs = [0x38900004, 0x38900008, 0x38900024, 0x3d800008,
0x39100004, 0x39200004, 0x39300000, 0x39300284]
rom = access('readl 0')
for cycle in range(3):
for address in regs:
access(f'writel {address:#x} 0x1234')
assert access(f'readl {address:#x}') == 0x1234, hex(address)
# Abandonner une lecture au milieu de la réponse d'identification de la dalle.
assert access('readl 0x3d80003c') == 0x31a
command('system_reset')
for address in regs:
assert access(f'readl {address:#x}') == 0, hex(address)
assert access('readl 0x3d80003c') == 0x31a
assert access('readl 0x3d80003c') == 0x00a1d13c
assert access('readl 0') == rom
print('Trois resets d’affichage : registres effacés, réponse MIPI réinitialisée, BootROM conservée.')
command('quit')
proc.wait(timeout=5)
finally:
for sock, stream in connections:
stream.close()
sock.close()
if proc.poll() is None:
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait()
if __name__ == '__main__':
main()
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""Relances natives dans une session de test déjà démarrée (firmware non distribué)."""
import argparse
import http.server
import json
import os
from pathlib import Path
import runpy
import subprocess
import threading
import time
import uuid
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--emu-home', type=Path, required=True)
p.add_argument('--agent-port', type=int, required=True)
p.add_argument('--action', choices=('respring', 'reset'), required=True)
p.add_argument('--cycles', type=int, default=3)
p.add_argument('--output', type=Path, required=True)
args = p.parse_args()
if args.cycles < 1:
p.error('--cycles doit être positif')
home = args.emu_home.resolve()
run = home / 'run'
bindir = Path(__file__).resolve().parents[1] / 'bin'
os.environ.update(IPOD_EMU_HOME=str(home), IPOD_AGENT_PORT=str(args.agent_port))
Agent = runpy.run_path(str(bindir / 'ipod-agent'))['Agent']
def call(*fields, data=None):
a = Agent(timeout=10)
try:
ok, body, detail = a.call(*fields, data=data)
if not ok:
raise RuntimeError(body.decode(errors='replace'))
return body
finally:
a.f.close()
a.s.close()
def springboard():
return next(line.split()[0] for line in call('ps').decode().splitlines()
if line.endswith(' SpringBoard'))
nonce = uuid.uuid4().hex.encode()
marker = '/private/var/mobile/Library/test-display-' + nonce.decode()
class Fixture(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-Length', str(len(nonce)))
self.end_headers()
self.wfile.write(nonce)
def log_message(self, *unused):
pass
server = http.server.ThreadingHTTPServer(('127.0.0.1', 0), Fixture)
threading.Thread(target=server.serve_forever, daemon=True).start()
url = f'http://10.0.2.2:{server.server_port}/preuve'
args.output.mkdir(parents=True, exist_ok=True)
qpid = (run / 'qemu.pid').read_text()
npid = (run / 'netd.pid').read_text()
inode = (run / 'nand').stat().st_ino
results = []
try:
call('put', marker, '600', len(nonce), data=nonce)
for cycle in range(args.cycles):
oldpid = springboard()
offset = (run / 'serial.log').stat().st_size
started = time.monotonic()
subprocess.run([str(bindir / 'ipod-emu'), args.action, '-W'],
check=True, timeout=340)
newpid = springboard()
fresh = (run / 'serial.log').read_bytes()[offset:]
if args.action == 'reset':
assert b'Darwin Kernel Version' in fresh
else:
assert oldpid != newpid
assert b'Darwin Kernel Version' not in fresh
assert b'locked=0 passcode=0' in call('screen')
assert (run / 'qemu.pid').read_text() == qpid
assert (run / 'netd.pid').read_text() == npid
assert (run / 'nand').stat().st_ino == inode
assert call('get', marker) == nonce
assert nonce in call('fetch', url)
call('launch', 'com.apple.calculator', '-')
time.sleep(3)
assert call('front').strip() == b'com.apple.calculator'
capture = args.output.resolve() / f'{args.action}-{cycle + 1}.png'
subprocess.run([str(bindir / 'ipod-qmp'), 'screenshot', str(capture)],
check=True, timeout=15)
result = dict(cycle=cycle + 1, seconds=round(time.monotonic() - started, 1),
qemu_pid=int(qpid), netd_pid=int(npid), springboard_pid=newpid,
unlocked=True, nand_preserved=True, http=True,
net=call('net').decode(), front='com.apple.calculator')
results.append(result)
print(json.dumps(result, ensure_ascii=False), flush=True)
(args.output / 'validation.json').write_text(
json.dumps(results, ensure_ascii=False, indent=2) + '\n')
call('rm', marker)
call('sync')
finally:
server.shutdown()
server.server_close()
if __name__ == '__main__':
main()
+119
View File
@@ -0,0 +1,119 @@
/* Le masque LCD et l'acquittement doivent survivre à plusieurs resprings. */
#include "qemu/osdep.h"
#include "qemu/main-loop.h"
#include "sysemu/cpus.h"
#include "hw/arm/ipod_touch_lcd.c"
#define ipod_touch_machine_types test_mipi_register_types
#include "hw/arm/ipod_touch_mipi_dsi.c"
#undef ipod_touch_machine_types
#include "hw/arm/ipod_touch_tvout.c"
static void irq_changed(void *opaque, int n, int level)
{
*(int *)opaque = level;
}
static void test_interrupt_reenable(void)
{
IPodTouchLCDState *s = IPOD_TOUCH_LCD(object_new(TYPE_IPOD_TOUCH_LCD));
int level = 0;
qemu_irq irq = qemu_allocate_irq(irq_changed, &level, 0);
sysbus_connect_irq(SYS_BUS_DEVICE(s), 0, irq);
s->refresh_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, refresh_timer_tick, s);
for (int cycle = 0; cycle < 3; cycle++) {
/* Vblank masqué : état en attente, aucune interruption CPU. */
ipod_touch_lcd_write(s, 0x8, 0, 4);
refresh_timer_tick(s);
g_assert_cmpint(level, ==, 0);
g_assert_cmpuint(ipod_touch_lcd_read(s, 0xc, 4), ==, 1);
ipod_touch_lcd_write(s, 0xc, 0xff, 4);
g_assert_cmpuint(ipod_touch_lcd_read(s, 0xc, 4), ==, 0);
/* Nouveau client : le masque réactive les vblanks après cet ACK. */
ipod_touch_lcd_write(s, 0x8, 1, 4);
g_assert_cmpint(level, ==, 0);
refresh_timer_tick(s);
g_assert_cmpint(level, ==, 1);
ipod_touch_lcd_write(s, 0xc, 2, 4);
g_assert_cmpint(level, ==, 1);
ipod_touch_lcd_write(s, 0xc, 1, 4);
g_assert_cmpint(level, ==, 0);
g_assert_cmpuint(ipod_touch_lcd_read(s, 0x8, 4), ==, 1);
refresh_timer_tick(s);
g_assert_cmpint(level, ==, 1);
ipod_touch_lcd_write(s, 0x8, 0, 4);
g_assert_cmpint(level, ==, 0);
ipod_touch_lcd_write(s, 0x8, 1, 4);
g_assert_cmpint(level, ==, 1);
}
ipod_touch_lcd_write(s, 0x24, 0x12340000, 4);
device_cold_reset(DEVICE(s));
g_assert_cmpint(level, ==, 0);
g_assert_cmpuint(ipod_touch_lcd_read(s, 0x8, 4), ==, 0);
g_assert_cmpuint(ipod_touch_lcd_read(s, 0xc, 4), ==, 0);
g_assert_cmpuint(ipod_touch_lcd_read(s, 0x24, 4), ==, 0);
timer_del(s->refresh_timer);
timer_free(s->refresh_timer);
object_unref(OBJECT(s));
qemu_free_irq(irq);
}
static void test_tvout_submissions(void)
{
IPodTouchTVOutState *s = IPOD_TOUCH_TVOUT(object_new(TYPE_IPOD_TOUCH_TVOUT));
int level = 0;
qemu_irq irq = qemu_allocate_irq(irq_changed, &level, 0);
sysbus_connect_irq(SYS_BUS_DEVICE(s), 0, irq);
/* L'ancien quota global cessait de répondre dès la troisième soumission. */
for (int cycle = 0; cycle < 20; cycle++) {
ipod_touch_tvout_mixer1_write(s, MXR_STATUS, 0x85, 4);
g_assert_cmpint(level, ==, 1);
ipod_touch_tvout_sdo_write(s, SDO_IRQ, 2, 4);
g_assert_cmpint(level, ==, 1);
ipod_touch_tvout_sdo_write(s, SDO_IRQ, 1, 4);
g_assert_cmpint(level, ==, 0);
}
ipod_touch_tvout_sdo_write(s, SDO_IRQMASK, 1, 4);
ipod_touch_tvout_mixer1_write(s, MXR_STATUS, 0x85, 4);
g_assert_cmpint(level, ==, 0);
g_assert_cmpuint(ipod_touch_tvout_sdo_read(s, SDO_IRQ, 4), ==, 1);
ipod_touch_tvout_sdo_write(s, SDO_IRQMASK, 0, 4);
g_assert_cmpint(level, ==, 1);
device_cold_reset(DEVICE(s));
g_assert_cmpint(level, ==, 0);
g_assert_cmpuint(ipod_touch_tvout_sdo_read(s, SDO_IRQ, 4), ==, 0);
ipod_touch_tvout_mixer1_write(s, MXR_STATUS, 0x85, 4);
g_assert_cmpint(level, ==, 1);
object_unref(OBJECT(s));
qemu_free_irq(irq);
}
static void test_panel_reset(void)
{
IPodTouchMIPIDSIState *s = IPOD_TOUCH_MIPI_DSI(object_new(TYPE_IPOD_TOUCH_MIPI_DSI));
for (int cycle = 0; cycle < 3; cycle++) {
ipod_touch_mipi_dsi_write(s, REG_CLKCTRL, DSIM_CLKCTRL_TX_REQUEST_HSCLK, 4);
g_assert_cmpuint(ipod_touch_mipi_dsi_read(s, 0, 4) & rDSIM_STATUS_TxReadyHsClk, !=, 0);
g_assert_cmpuint(ipod_touch_mipi_dsi_read(s, REG_RXFIFO, 4), ==, 0x31a);
/* Un reset pendant une réponse doit recommencer par son en-tête. */
device_cold_reset(DEVICE(s));
g_assert_cmpuint(ipod_touch_mipi_dsi_read(s, 0, 4) & rDSIM_STATUS_TxReadyHsClk, ==, 0);
g_assert_cmpuint(ipod_touch_mipi_dsi_read(s, REG_RXFIFO, 4), ==, 0x31a);
g_assert_cmpuint(ipod_touch_mipi_dsi_read(s, REG_RXFIFO, 4), ==, 0x00a1d13c);
}
object_unref(OBJECT(s));
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
module_call_init(MODULE_INIT_QOM);
qemu_init_main_loop(&error_abort);
qemu_init_cpu_loop();
qemu_mutex_lock_iothread();
g_test_add_func("/ipod-lcd/interrupt-reenable", test_interrupt_reenable);
g_test_add_func("/ipod-tvout/repeated-submissions", test_tvout_submissions);
g_test_add_func("/ipod-mipi/panel-reset", test_panel_reset);
return g_test_run();
}
+42
View File
@@ -109,6 +109,14 @@ static void test_spi_abort(void)
response[0] = transfer(spi->mt, MT_CMD_GET_INTERFACE_VERSION);
for (int i = 1; i < 16; i++) response[i] = transfer(spi->mt, 0);
g_assert_cmpuint(response[2], ==, MT_INTERFACE_VERSION);
spi->last_irq = 1;
fifo8_push(&spi->tx_fifo, 0x12);
fifo8_push(&spi->rx_fifo, 0x34);
device_cold_reset(DEVICE(spi));
g_assert_cmpuint(spi->last_irq, ==, 0);
g_assert_true(fifo8_is_empty(&spi->tx_fifo));
g_assert_true(fifo8_is_empty(&spi->rx_fifo));
g_assert_cmpuint(ipod_touch_spi_read(spi, R_TXCNT, 4), ==, 0);
timer_free(spi->mt->touch_timer);
timer_free(spi->mt->touch_end_timer);
g_free(spi->mt->next_frame);
@@ -181,6 +189,39 @@ static void test_gpio_ranges(void)
object_unref(OBJECT(s));
}
static void test_reset(void)
{
IPodTouchMultitouchState *s = new_sensor();
IPodTouchSYSICState *sysic = IPOD_TOUCH_SYSIC(object_new(TYPE_IPOD_TOUCH_SYSIC));
s->sysic = sysic;
ipod_touch_multitouch_on_touch(s);
/* Capture d'une trame, puis interruption au milieu d'une autre commande. */
transfer(s, MT_CMD_FRAME_READ);
for (int i = 1; i < 16; i++) transfer(s, 0);
transfer(s, MT_CMD_GET_INTERFACE_VERSION);
s->hbpp_atn_ack_response[0] = 0xaa;
g_assert_true(timer_pending(s->touch_timer));
g_assert_nonnull(s->read_frame);
g_assert_nonnull(s->out_buffer);
device_cold_reset(DEVICE(s));
g_assert_false(timer_pending(s->touch_timer));
g_assert_false(timer_pending(s->touch_end_timer));
g_assert_false(s->touch_down);
g_assert_null(s->read_frame);
g_assert_null(s->in_buffer);
g_assert_null(s->out_buffer);
g_assert_cmpuint(s->cur_cmd, ==, 0);
g_assert_cmpuint(s->hbpp_atn_ack_response[0], ==, 0);
g_assert_cmpuint(sysic->gpio_int_status[3], ==, 0);
g_assert_cmpuint(s->next_frame->finger_data.event, ==, MT_EVENT_TOUCH_FULL_END);
transfer(s, MT_CMD_GET_INTERFACE_VERSION);
transfer(s, 0);
g_assert_cmpuint(transfer(s, 0), ==, MT_INTERFACE_VERSION);
destroy_sensor(s);
object_unref(OBJECT(sysic));
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
@@ -194,6 +235,7 @@ int main(int argc, char **argv)
g_test_add_func("/ipod-multitouch/spi-abort", test_spi_abort);
g_test_add_func("/ipod-multitouch/reports", test_reports);
g_test_add_func("/ipod-multitouch/velocity", test_velocity);
g_test_add_func("/ipod-multitouch/reset", test_reset);
g_test_add_func("/ipod-input/buttons", test_buttons);
g_test_add_func("/ipod-input/gpio-ranges", test_gpio_ranges);
return g_test_run();
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""Vérifie le reset du lanceur sans QEMU : synchronisation et nouveau journal de boot."""
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import time
import unittest
ROOT = Path(__file__).resolve().parents[3]
BOOT = b'AppleMultitouchSPIUserClient::attach\n' * 2 + b'SCHelper "EXIT"\n'
class ResetLauncher(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.home = Path(self.tmp.name)
self.run = self.home / 'run'
self.run.mkdir()
(self.run / 'nand').mkdir()
(self.run / 'nand/temoin').write_bytes(b'NAND conservee')
(self.run / 'qemu.pid').write_text(str(os.getpid()))
# Une ancienne erreur et beaucoup de données ne doivent pas contaminer le nouveau boot.
self.old = b'Fatal Exception\x00\n' + b'x' * 100000 + b'\n' + BOOT
(self.run / 'serial.log').write_bytes(self.old)
tools = self.home / 'bin'
tools.mkdir()
self.launcher = tools / 'ipod-emu'
shutil.copy2(ROOT / 'contrib/ipod-emu/bin/ipod-emu', self.launcher)
scripts = {
'ipod-agent': '''#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' "$1" >> "$IPOD_EMU_HOME/actions"
if [ "$1" = sync ] && [ "${IPOD_TEST_SYNC_FAIL:-0}" = 1 ]; then exit 1; fi
''',
'ipod-qmp': '''#!/usr/bin/env bash
set -euo pipefail
printf 'reset\n' >> "$IPOD_EMU_HOME/actions"
'''
}
for name, content in scripts.items():
path = tools / name
path.write_text(content)
path.chmod(0o755)
self.env = dict(os.environ, IPOD_EMU_HOME=str(self.home))
def tearDown(self):
self.tmp.cleanup()
def test_reset_preserves_nand(self):
subprocess.run([str(self.launcher), 'reset'], env=self.env,
capture_output=True, check=True, timeout=5)
self.assertEqual((self.home / 'actions').read_text().splitlines(), ['sync', 'reset'])
self.assertEqual(int((self.run / 'boot-log-offset').read_text()), len(self.old))
self.assertEqual((self.run / 'nand/temoin').read_bytes(), b'NAND conservee')
self.assertEqual((self.run / 'qemu.pid').read_text(), str(os.getpid()))
def test_wait_ignores_previous_boot(self):
proc = subprocess.Popen([str(self.launcher), 'reset', '-W'], env=self.env,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
try:
deadline = time.monotonic() + 5
while not (self.run / 'boot-log-offset').exists():
self.assertLess(time.monotonic(), deadline)
self.assertIsNone(proc.poll())
time.sleep(.02)
# L'ancien journal contient déjà tous les marqueurs, mais l'attente doit continuer.
time.sleep(3.5)
self.assertIsNone(proc.poll())
self.assertNotIn('unlock', (self.home / 'actions').read_text())
with (self.run / 'serial.log').open('ab') as log:
log.write(BOOT)
out, err = proc.communicate(timeout=15)
self.assertEqual(proc.returncode, 0, err.decode())
self.assertIn('SpringBoard prêt', out.decode())
self.assertEqual((self.home / 'actions').read_text().splitlines(), ['sync', 'reset', 'unlock'])
finally:
if proc.poll() is None:
proc.kill()
proc.communicate()
def test_sync_failure_prevents_reset(self):
env = dict(self.env, IPOD_TEST_SYNC_FAIL='1')
r = subprocess.run([str(self.launcher), 'reset'], env=env,
capture_output=True, timeout=5)
self.assertNotEqual(r.returncode, 0)
self.assertEqual((self.home / 'actions').read_text().splitlines(), ['sync'])
self.assertFalse((self.run / 'boot-log-offset').exists())
def test_respring_preserves_session(self):
subprocess.run([str(self.launcher), 'respring'], env=self.env,
capture_output=True, check=True, timeout=5)
self.assertEqual((self.home / 'actions').read_text().splitlines(), ['sync', 'respring'])
self.assertEqual(int((self.run / 'boot-log-offset').read_text()), len(self.old))
self.assertEqual((self.run / 'nand/temoin').read_bytes(), b'NAND conservee')
self.assertEqual((self.run / 'qemu.pid').read_text(), str(os.getpid()))
def test_invalid_option_does_not_restart(self):
r = subprocess.run([str(self.launcher), 'reset', '-f'], env=self.env,
capture_output=True, timeout=5)
self.assertNotEqual(r.returncode, 0)
self.assertFalse((self.home / 'actions').exists())
if __name__ == '__main__':
unittest.main()
+1
View File
@@ -508,6 +508,7 @@ static void ipod_touch_machine_init(MachineState *machine)
IPodTouchMIPIDSIState *mipi_dsi_state = IPOD_TOUCH_MIPI_DSI(dev);
nms->mipi_dsi_state = mipi_dsi_state;
memory_region_add_subregion(sysmem, MIPI_DSI_MEM_BASE, &mipi_dsi_state->iomem);
sysbus_realize(SYS_BUS_DEVICE(dev), &error_fatal);
// init LCD
dev = qdev_new("ipodtouch.lcd");
+25 -8
View File
@@ -16,8 +16,10 @@ static uint64_t ipod_touch_lcd_read(void *opaque, hwaddr addr, unsigned size)
return 2;
case 0x4:
return s->lcd_con;
case 0x8:
return s->int_enable;
case 0xC:
return 0x1; //s->unknown1;
return s->int_status;
case 0x20:
return s->w1_display_depth_info;
case 0x24:
@@ -46,10 +48,14 @@ static void ipod_touch_lcd_write(void *opaque, hwaddr addr, uint64_t val, unsign
case 0x4:
s->lcd_con = val;
break;
case 0x8:
s->int_enable = val;
qemu_set_irq(s->irq, !!(s->int_status & s->int_enable));
break;
case 0xC:
s->render = val;
qemu_irq_lower(s->irq);
// qemu_irq_raise(s->irq);
/* Acquittement W1C, indépendant du masque d'interruptions. */
s->int_status &= ~val;
qemu_set_irq(s->irq, !!(s->int_status & s->int_enable));
break;
case 0x20:
s->w1_display_depth_info = val;
@@ -174,10 +180,8 @@ static void refresh_timer_tick(void *opaque)
{
IPodTouchLCDState *s = (IPodTouchLCDState *)opaque;
if (s->render == 0x1)
qemu_irq_raise(s->irq);
else if (s->render == 0xFF)
qemu_irq_lower(s->irq);
s->int_status |= 1;
qemu_set_irq(s->irq, !!(s->int_status & s->int_enable));
timer_mod(s->refresh_timer, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + NANOSECONDS_PER_SECOND / 60);//LCD_REFRESH_RATE_FREQUENCY);
}
@@ -207,11 +211,24 @@ static void ipod_touch_lcd_init(Object *obj)
sysbus_init_irq(sbd, &s->irq);
}
static void ipod_touch_lcd_reset(DeviceState *dev)
{
IPodTouchLCDState *s = IPOD_TOUCH_LCD(dev);
s->lcd_con = s->int_enable = s->int_status = 0;
s->w1_display_resolution_info = s->w1_framebuffer_base = 0;
s->w1_hspan = s->w1_display_depth_info = 0;
s->invalidate = 1;
lcd_brightness = 255;
qemu_irq_lower(s->irq);
}
static void ipod_touch_lcd_class_init(ObjectClass *klass, void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);
dc->realize = ipod_touch_lcd_realize;
dc->reset = ipod_touch_lcd_reset;
}
static const TypeInfo ipod_touch_lcd_info = {
+10
View File
@@ -77,11 +77,21 @@ static void ipod_touch_mipi_dsi_init(Object *obj)
s->return_panel_id = 0;
}
static void ipod_touch_mipi_dsi_reset(DeviceState *dev)
{
IPodTouchMIPIDSIState *s = IPOD_TOUCH_MIPI_DSI(dev);
s->pkthdr_reg = s->clkctrl_reg = 0;
s->return_panel_id = false;
qemu_irq_lower(s->irq);
}
static void ipod_touch_mipi_dsi_class_init(ObjectClass *klass, void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);
dc->realize = ipod_touch_mipi_dsi_realize;
dc->reset = ipod_touch_mipi_dsi_reset;
}
static const TypeInfo ipod_touch_mipi_dsi_info = {
+25
View File
@@ -467,9 +467,34 @@ static void ipod_touch_multitouch_realize(SSIPeripheral *d, Error **errp)
s->next_frame = get_frame(s, MT_EVENT_TOUCH_FULL_END, 0, 0, 0, 0, 0, 0);
}
static void ipod_touch_multitouch_reset(DeviceState *dev)
{
IPodTouchMultitouchState *s = IPOD_TOUCH_MULTITOUCH(dev);
finish_transfer(s);
timer_del(s->touch_timer);
timer_del(s->touch_end_timer);
g_clear_pointer(&s->read_frame, g_free);
g_clear_pointer(&s->next_frame, g_free);
memset(s->hbpp_atn_ack_response, 0, sizeof s->hbpp_atn_ack_response);
s->touch_down = false;
s->touch_x = s->touch_y = s->prev_touch_x = s->prev_touch_y = 0;
s->frame_counter = 0;
s->last_frame_timestamp = 0;
if (s->sysic) {
s->sysic->gpio_int_status[3] &= ~(1 << 13);
if (!s->sysic->gpio_int_status[3]) {
qemu_irq_lower(s->sysic->gpio_irqs[3]);
}
}
s->next_frame = get_frame(s, MT_EVENT_TOUCH_FULL_END, 0, 0, 0, 0, 0, 0);
}
static void ipod_touch_multitouch_class_init(ObjectClass *klass, void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);
SSIPeripheralClass *k = SSI_PERIPHERAL_CLASS(klass);
dc->reset = ipod_touch_multitouch_reset;
k->realize = ipod_touch_multitouch_realize;
k->transfer = ipod_touch_multitouch_transfer;
}
+3 -1
View File
@@ -263,6 +263,8 @@ static void ipod_touch_spi_reset(DeviceState *d)
memset(s->regs, 0, sizeof(s->regs));
fifo8_reset(&s->tx_fifo);
fifo8_reset(&s->rx_fifo);
s->last_irq = 0;
qemu_irq_lower(s->irq);
}
static uint32_t base_addr = 0;
@@ -333,4 +335,4 @@ static void ipod_touch_spi_register_types(void)
type_register_static(&ipod_touch_spi_info);
}
type_init(ipod_touch_spi_register_types)
type_init(ipod_touch_spi_register_types)
+26 -8
View File
@@ -1,6 +1,11 @@
#include "hw/arm/ipod_touch_tvout.h"
#include "qapi/error.h"
static void tvout_update_irq(IPodTouchTVOutState *s)
{
qemu_set_irq(s->irq, !!(s->sdo_irq & ~s->sdo_irq_mask & 1));
}
static uint64_t ipod_touch_tvout_sdo_read(void *opaque, hwaddr offset, unsigned size)
{
IPodTouchTVOutState *s = (IPodTouchTVOutState *)opaque;
@@ -37,11 +42,12 @@ static void ipod_touch_tvout_sdo_write(void *opaque, hwaddr offset, uint64_t val
s->sdo_config = value;
return;
case SDO_IRQ:
s->sdo_irq = 0x0;
qemu_irq_lower(s->irq);
s->sdo_irq &= ~value;
tvout_update_irq(s);
return;
case SDO_IRQMASK:
s->sdo_irq_mask = value;
tvout_update_irq(s);
return;
}
}
@@ -72,10 +78,10 @@ static void ipod_touch_tvout_mixer1_write(void *opaque, hwaddr offset, uint64_t
switch(offset) {
case MXR_STATUS:
if((value & 0x1) && (s->sdo_irq_mask & 1) == 0 && s->irq_count < 2) {
s->sdo_irq = 0x1;
s->irq_count += 1; // ugly hack for now
qemu_irq_raise(s->irq);
/* Chaque soumission doit pouvoir terminer, même après respring. */
if (value & 1) {
s->sdo_irq |= 1;
tvout_update_irq(s);
}
break;
case MXR_CFG:
@@ -157,9 +163,21 @@ static void ipod_touch_tvout_init(Object *obj)
sysbus_init_irq(sbd, &s->irq);
}
static void ipod_touch_tvout_reset(DeviceState *dev)
{
IPodTouchTVOutState *s = IPOD_TOUCH_TVOUT(dev);
s->mixer1_status = s->mixer1_cfg = 0;
s->mixer2_status = s->mixer2_cfg = 0;
s->sdo_clkcon = s->sdo_config = 0;
s->sdo_irq = s->sdo_irq_mask = 0;
tvout_update_irq(s);
}
static void ipod_touch_tvout_class_init(ObjectClass *klass, void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);
dc->reset = ipod_touch_tvout_reset;
}
static const TypeInfo ipod_touch_tvout_type_info = {
@@ -175,4 +193,4 @@ static void ipod_touch_tvout_register_types(void)
type_register_static(&ipod_touch_tvout_type_info);
}
type_init(ipod_touch_tvout_register_types)
type_init(ipod_touch_tvout_register_types)
+2 -1
View File
@@ -32,7 +32,8 @@ typedef struct IPodTouchLCDState
uint32_t w1_hspan;
uint32_t w1_display_depth_info;
uint32_t render;
uint32_t int_enable;
uint32_t int_status;
QEMUTimer *refresh_timer;
} IPodTouchLCDState;
+1 -2
View File
@@ -38,7 +38,6 @@ typedef struct IPodTouchTVOutState {
uint32_t sdo_irq;
uint32_t sdo_irq_mask;
uint32_t irq_count;
} IPodTouchTVOutState;
#endif
#endif