Skip to content

feat: redémarrer sans redémarrer Windows — quatre gestes de reprise pour un poste sous kiosque - #39

Merged
lostmind84 merged 14 commits into
mainfrom
feat/redemarrer-sans-redemarrer-windows
Jul 31, 2026
Merged

lostmind84 merged 14 commits into
mainfrom
feat/redemarrer-sans-redemarrer-windows

Conversation

@lostmind84

Copy link
Copy Markdown
Owner

Un poste installé est enfermé : sous cage l'unité du kiosque écrit qu'« il n'y a
littéralement rien vers quoi s'échapper », et sous Shell Launcher le kiosque remplace
l'explorateur Windows. Trois besoins n'avaient donc aucune porte — relire un config.json
modifié à la main (le _readme du fichier demandait « arrêtez le service, éditez,
redémarrez »), relancer l'application, redémarrer la machine. La réponse était de couper le
courant.

Les quatre gestes

Geste Coupure Réversible
POST /admin/api/config/reload — relit le fichier tel qu'il est sur le disque page Poste aucune 60 s, automatique
POST /admin/api/restart — le poste s'arrête, son superviseur le relance page Poste ~5 s non
POST · DELETE /admin/api/reboot — l'ordinateur page Poste (rouge) ~1 min 30 s, par bouton
openscale service restart CLI ~5 s non

Les trois routes sont derrière la session d'administration et soumises au même garde
que la mise à jour — station.DowntimeGuard, renommé depuis UpdateGuard parce que la
règle n'a jamais dépendu de ce qui venait après l'arrêt.

ADR-027 n'est pas rouvert

L'ADR refuse un redémarrage exigé par un bloc de configuration : le rechargement à
chaud les couvre tous, network.listen compris, et ça reste vrai. Ce qui est ajouté ici
est du dépannage. Le redémarrage lui-même reste celui que l'ADR appelle légitime : la
station s'arrête par le chemin ordonné de §13.4 avec un code non nul, et c'est le SCM ou
systemd qui la relance. Aucun script neuf, aucun second chemin d'arrêt. ADR-055 pose la
distinction.

Trois choses trouvées en construisant

  1. Une annulation sur deux ne servait à rien. Quand l'échéance et l'annulation tombent
    au même instant, les deux cas du select sont prêts et Go en choisit un au hasard :
    la machine repartait quand même. C'est le verrou, et non le select, qui tranche
    désormais. Le test isolé passait ; c'est la suite complète qui l'a montré, et il tourne
    maintenant cent fois.
  2. Un redémarrage REFUSÉ ne disait rien. La réponse était partie trente secondes plus
    tôt, le poste tournait toujours, et le bénévole regardait un décompte expirer sur rien —
    exactement ce que fait un poste Linux dont la règle polkit manque. D'où ERR-SYS-12 au
    journal technique, et un 16ᵉ contrôle dans doctor qui le dit avant que quelqu'un
    en ait besoin.
  3. Relire un config.json reconstruit depuis un export fermerait l'administration.
    Config.Export retire toujours les deux empreintes, et le contrôle 31 accepte leur
    absence à juste titre — la faute n'est pas dans le fichier, elle est dans le fait de lire
    ce fichier sur ce poste. Refusé en 422, avec le remède.

Deux décisions qui portent le reste

  • Le délai avant le redémarrage machine est applicatif, sur l'horloge injectée, et non
    shutdown /r /t 30. Sous Linux systemctl reboot est immédiat et n'a rien à annuler :
    un délai porté par l'OS donnerait deux comportements pour un bouton — et il ne se
    testerait pas sans redémarrer une machine.
  • Sur retour arrière, la relecture ne réécrit pas le fichier. writeConfig fait
    l'inverse, à juste titre : là le document venait de l'écran, qui en garde une copie. Ici
    il vient de la main de quelqu'un, et l'écraser détruirait le seul exemplaire.

Installation Linux

Le service tourne en openscale, pas en root : install.sh pose
/etc/polkit-1/rules.d/49-openscale-reboot.rules, portée la plus étroite possible — un
compte, une action, ni power-off ni ignore-inhibit. Un test de deploy/ tient ces
bornes en lisant le code de la règle et non ses commentaires.

« Quinze contrôles » disparaît du dépôt

Le nombre était écrit dans une douzaine de commentaires, trois fichiers de tests et cinq
paragraphes de l'architecture ; en ajouter un seizième a fait rougir trois tests et laissé
tout le reste faux en silence. diag.ControlOrder était déjà documenté comme l'autorité sur
le compte — c'est désormais ce que les tests lisent.

Vérification

  • 2 950 tests Go — 2 938 verts, 12 écartés — sur 35 paquets, 0 échec
    (CGO_ENABLED=0 go test ./... -count=1 -v)
  • 796 tests front sur 35 fichiers, 0 échec (npm test) · svelte-check : 0 erreur
  • gofmt, go vet silencieux · boundary et deps verts
  • Passe -race verte sur internal/web, internal/station et deploy/
  • Les trois cibles compilent : windows, linux, linux/arm64

Ce qui reste à valider sur le poste, et qui est dit

platform.Reboot() n'est exercé par aucun test sous Windows ni sous Linux : l'appel
réussirait, et la machine du test redémarrerait. Restent donc à éprouver sur un poste réel
le redémarrage machine lui-même, et sous Linux la règle polkit posée par install.sh.

Hors périmètre, et dit : le kiosque n'est pas redémarrable depuis l'écran — il se
reconnecte seul en SSE, son superviseur le relance en moins de 2 s, et surtout un écran
tactile figé ne peut pas recevoir le clic qui le réparerait. Pas d'extinction de
l'ordinateur non plus : un poste éteint à distance ne se rallume pas à distance.

Un poste sous kiosque n'a aucune sortie : ni relire un config.json édité à la
main, ni relancer le service, ni redémarrer la machine. La seule réponse connue
est de couper le courant.

ADR-027 n'est pas rouvert : il refuse un redémarrage EXIGÉ PAR UN RÉGLAGE, ce
qui reste vrai. Le geste ajouté ici est du dépannage, et il passe par le
mécanisme que l'ADR désigne lui-même comme le seul légitime — l'arrêt propre
avec un code non nul, que le SCM ou systemd rattrape.
Chaque tâche livre quelque chose de testable seul, dans l'ordre où les
suivantes en dépendent : le garde d'abord, l'écran en avant-dernier, les
compteurs de SUIVI.md mesurés en dernier.
…e trois arrêts

Il refusait de couper le poste pour installer une version. Il refusera aussi
de le couper pour redémarrer le service et pour redémarrer l'ordinateur : la
règle n'a jamais dépendu de ce qui venait après l'arrêt, elle protège la
pesée en cours et le catalogue pas encore en service.

Renommage seul. Aucune règle touchée, aucun test réécrit — seules les phrases
qui disaient « la mise à jour » disent maintenant « l'arrêt du poste ».
…poste

Le _readme du fichier demandait d'arrêter le service, d'éditer, puis de
redémarrer — trois gestes qu'un poste sous kiosque ne permet pas.

La relecture rejoue le chemin de restoreConfig : validation complète,
rechargement à chaud, compte à rebours de 60 s. Elle n'écrit rien — le
document EST le fichier — et sur retour arrière elle laisse le fichier tel
quel : il n'en existe aucune copie, contrairement à ce que la page envoie.

Elle refuse un fichier venu d'un export. Config.Export retire les deux
empreintes, le contrôle 31 accepte leur absence à juste titre, et personne
ne rattraperait une administration fermée sur une fiche d'installation
perdue.
Un poste sous kiosque n'a aucune sortie quand quelque chose est figé : ni
console, ni bureau, seulement l'interrupteur.

Le bouton refait l'arrêt ordonné de §13.4 — le même que Ctrl+C et que
l'arrêt du SCM, pas un second chemin — et rend un code non nul. C'est le
code qui fait tout : le SCM applique alors les reprises de §15.2, systemd
son Restart=always. Aucun script neuf.

Sans superviseur, la route répond 501 : « openscale serve » dans un
terminal n'est relancé par personne, et le bouton arrêterait un poste que
rien ne rallume.
shutdown.exe /r /t 0 et systemctl reboot. Pas d'API Win32 : activer
SeShutdownPrivilege demande trois appels qu'aucune machine de compilation
ne peut exercer, et dont l'échec serait un poste qui ne dit rien.

Le délai n'est pas ici. Il vit dans l'application, sur l'horloge injectée,
pour être le même des deux côtés — systemctl reboot est immédiat et n'a
rien à annuler — et pour se prouver sans redémarrer une machine.

Aucun test n'appelle Reboot() sous Windows ni sous Linux : l'appel
réussirait, et la machine du test redémarrerait.
…viser

Sous cage l'unité du kiosque dit qu'il n'y a « littéralement rien vers quoi
s'échapper », et sous Shell Launcher le kiosque remplace l'explorateur : le
seul recours d'un bénévole était de couper le courant.

Le délai vit dans l'application, sur l'horloge injectée, et non dans
shutdown.exe. C'est ce qui lui donne le même comportement sous Linux — où
systemctl reboot est immédiat et n'a rien à annuler — et ce qui rend
l'armement, l'annulation et l'échéance prouvables sans redémarrer une
machine. La passe -race est verte sur ces six tests.

Le garde refuse pendant une pesée, comme pour une mise à jour : ce qu'il
protège est le client debout devant l'écran.
Les deux moitiés existaient. Ce qui est neuf est la règle entre les deux :
l'erreur de l'arrêt est rendue sans tenter le démarrage, parce qu'un
service qu'on n'a pas su arrêter ne se redémarre pas, et qu'enchaîner
répondrait « le service est déjà démarré » à la place de « accès refusé ».

Les deux moitiés voyagent comme des fonctions : platform.StopService parle
au SCM, et un test qui l'appellerait vraiment arrêterait l'OpenScale
installé sur la machine du développeur.
… fois sur deux

Quand l'échéance et l'annulation tombent au même instant, les deux cas du
select sont prêts et Go en choisit un AU HASARD : la moitié des
annulations laissaient la machine partir quand même.

C'est désormais le verrou, et non le select, qui tranche : la goroutine
réclame le compte à rebours avant d'appeler platform.Reboot, sous le même
mutex que Cancel prend. Un compte à rebours annulé — ou réarmé depuis —
n'appartient plus à personne.

Le test isolé passait ; c'est la suite complète qui l'a montré. Il tourne
maintenant cent fois.
…eur, et le dit quand il ne l'a pas

Le service tourne en « openscale », pas en root : sans règle polkit,
systemctl reboot est refusé et le bouton ne sert à rien. La règle est la
plus étroite possible — un compte, une action, ni power-off ni
ignore-inhibit — et un test de deploy/ tient ces bornes, en lisant le code
de la règle et non ses commentaires.

Corrige au passage un silence que je venais d'introduire : un redémarrage
REFUSÉ ne disait rien. Le 202 était parti trente secondes plus tôt, le
poste tournait toujours, et le bénévole regardait un décompte expirer sur
rien. C'est exactement ce que fait un poste Linux sans la règle. Il est
maintenant écrit au journal technique, en ERR-SYS-12, distinct du
ERR-SYS-11 de « ce poste ne sait pas faire » : les deux remèdes n'ont rien
en commun.
…marrer l'ordinateur

La réponse est invisible jusqu'au moment où elle coûte : sous Linux, un
poste sans sa règle polkit marche parfaitement, jusqu'au soir où un
bénévole touche le seul bouton qui l'aurait sauvé.

Le contrôle cherche la RÈGLE et non le droit : demander le droit à polkit
veut dire appeler systemctl reboot, et la réponse honnête à « ce poste
peut-il redémarrer ? » ne vaut pas qu'on redémarre la machine.

Au passage, « quinze » disparaît du code. Le nombre était écrit dans une
douzaine de commentaires, trois fichiers de tests et cinq paragraphes de
l'architecture ; en ajouter un seizième a fait rougir trois tests et laissé
tout le reste faux en silence. ControlOrder était déjà documenté comme
l'autorité sur le compte — c'est désormais ce que les tests lisent.
Trois boutons rangés par brutalité croissante. Le troisième est rouge,
demande une confirmation, puis affiche un décompte annulable : c'est ce qui
rend offrable un acte que rien ne défait une fois la machine partie.

Le refus lui-même n'est pas recopié dans la rubrique — App.svelte en fait
déjà une bannière, et deux exemplaires de la même phrase se lisent comme
deux pannes. Ce qui est propre à la rubrique, les fautes d'un fichier
refusé une par ligne, s'affiche sous la même forme que la page Poste :
bande d'avertissement, jamais du texte rouge, que §14.2 inventorie.

L'en-tête de la page disait « There is no restart button », en citant
ADR-027 : elle dit maintenant ce que l'ADR refuse vraiment — un
redémarrage exigé par un réglage — et ce que ces boutons sont, des
réparations pour un poste où aucune console n'est atteignable.
L'architecture disait « pas de POST /admin/api/restart » sans distinguer un
redémarrage exigé par un réglage — toujours refusé, le rechargement à chaud
couvre tous les blocs — d'un redémarrage de dépannage, qu'aucun écran
n'offrait sur un poste sans console ni bureau. ADR-055 pose la distinction ;
§14.4, §14.5 et §15.3 la reflètent.

TROUBLESHOOTING.md gagne une entrée par symptôme — « quelque chose est figé
et on n'a accès à rien » — parce qu'un bénévole ne cherche pas un code, il
cherche ce qu'il voit. Le glossaire tranche « poste » (l'application) contre
« ordinateur » (la machine), et porte les quatre codes neufs.

Compteurs de SUIVI.md MESURÉS : 2 950 tests Go sur 35 paquets, 796 tests
front, -race verte sur les trois paquets touchés.
INVOCATION_ID EST HÉRITÉ. Tout enfant d'un service systemd le lit, si bien
que « ce processus est-il supervisé ? » répondait oui à un shell, à un
script lancé par systemd-run et — mesuré sur le coureur GitHub Actions, qui
est lui-même un service — au binaire de test. Le bouton aurait alors arrêté
un processus que rien ne relance, ce qu'il existe précisément pour éviter.
S'y ajoute donc la condition qui distingue le processus PRINCIPAL d'une
unité de sa descendance : systemd le fork directement, son parent est PID 1.
C'est le raisonnement que notify.go applique déjà à WATCHDOG_PID.

Le 16e contrôle lisait le VRAI système dans un test unitaire. Il était vert
sur une machine Windows et rouge sur le coureur, qui n'a pas de règle
polkit et n'en aura jamais. La question passe maintenant par l'interface
Machine, comme les quatorze autres, et le banc décide de la réponse : les
trois cas — autorisé, refusé, sans objet — sont couverts sans dépendre de
la machine qui exécute les tests.

J'avais écarté cette injection « pour ne pas toucher l'interface ». C'était
le mauvais arbitrage, et c'est la CI qui l'a montré.
@lostmind84
lostmind84 merged commit 2920a7a into main Jul 31, 2026
6 checks passed
@lostmind84
lostmind84 deleted the feat/redemarrer-sans-redemarrer-windows branch July 31, 2026 21:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant