v0.7.5 : le bouton de mise a jour redemarre l'application (Windows)

Un process ne peut pas se relancer lui-meme : tant qu'il tourne, il occupe le
port et tient son propre dossier de donnees. On delegue donc a un
accompagnateur detache — une copie de nous lancee avec une sous-commande
interne — qui attend notre disparition, migre pendant cette fenetre, puis
relance l'application.

C'est cette fenetre qui manquait depuis le debut. Une mise a jour en place
laisse AJEAN tourner, donc le dossier tenu, donc le renommage echoue avec
« Acces refuse » — que Windows renvoie aussi bien pour un verrou que pour un
manque de droits. D'ou des postes bloques sur l'ancien nom malgre 0.7.1 a
0.7.4. Le retour terrain le confirme : la migration n'a abouti qu'en lancant
l'exe telecharge, seul chemin qui arretait les instances avant de migrer.

installedExePath() est relu APRES la migration avant de relancer : le binaire
installe vit dans le dossier de donnees, il change donc de place avec lui, et
relancer l'ancien chemin echouerait. Repli sur le binaire courant si la cible
a disparu — mieux vaut relancer quelque chose que laisser l'utilisateur sans
application.

L'attente est bornee a 30 s. Un depassement n'interrompt pas la suite : la
migration echouera peut-etre et sera retentee plus tard, alors que rester
coince laisserait l'utilisateur sans rien.

Verifie de bout en bout sur banc d'essai : l'accompagnateur attend la
fermeture du process cible, se termine proprement, et l'application est bien
relancee et ressert l'UI.
This commit is contained in:
nathaninline committed 2026-08-04 17:31:06 +02:00
1 parent 1f773151a6
commit d72578ea55
8 files changed
+127 -13

No files matched your search

+6 -8
View File
@@ -1,14 +1,12 @@
## Le renommage du dossier se fait maintenant à la mise à jour
## Le bouton « mettre à jour » redémarre AJEAN tout seul
Les versions 0.7.1 à 0.7.3 n'y parvenaient pas sur les postes Windows sans droits administrateur : le code prévu pour demander l'autorisation n'était jamais atteint. Il ne se déclenchait qu'en relançant l'installation ou en retéléchargeant le programme — deux gestes que personne ne fait pour une mise à jour.
Sous Windows, il fallait jusqu'ici quitter puis relancer l'application à la main pour que la mise à jour prenne effet. C'est fait automatiquement : la page se reconnecte d'elle-même au bout de quelques secondes.
`ajean update`, et le bouton de mise à jour de l'interface, s'en chargent désormais. L'autorisation Windows est demandée à ce moment-là, une fois, et le dossier `C:\ProgramData\jean` devient `C:\ProgramData\ajean`.
Ce redémarrage règle aussi, au passage, le renommage du dossier de données. Tant qu'AJEAN tourne, il tient son propre dossier, et `C:\ProgramData\jean` ne peut pas devenir `C:\ProgramData\ajean` — c'est ce qui laissait certains postes indéfiniment sur l'ancien nom malgré les versions précédentes. Le redémarrage ouvre le court instant où plus rien ne le tient, et la migration s'y termine.
Le raccourci du menu Démarrer et la commande `ajean` sont repointés dans la foulée. Le programme installé vit à l'intérieur du dossier de données : sans cette étape, le déplacement aurait laissé un raccourci mort et une commande introuvable.
Le raccourci du menu Démarrer et la commande `ajean` suivent le déplacement, puisque le programme installé vit à l'intérieur de ce dossier.
Refuser l'autorisation reste sans conséquence : AJEAN continue sur son dossier actuel, et la question reviendra à la prochaine mise à jour.
Si vous êtes déjà sur la dernière version et toujours sur l'ancien dossier, la migration se fera à la mise à jour suivante.
Si quelque chose empêche le renommage, il ne se passe rien : AJEAN redémarre sur son dossier actuel et réessaiera plus tard. Rien n'est jamais copié ni supprimé.
## Mise à jour
@@ -16,6 +14,6 @@ Si vous êtes déjà sur la dernière version et toujours sur l'ancien dossier,
ajean update
```
Sous Windows, télécharger `ajean-windows-amd64.exe` depuis cette page et le lancer fait le même travail.
Ou le bouton de l'interface, qui se charge désormais de tout.
Les binaires restent publiés sous leurs deux noms, `ajean-*` et `jean-*`, le temps que le parc bascule.
Binary file not shown.
Binary file not shown.
+3 -3
View File
@@ -3,13 +3,13 @@
"FileVersion": {
"Major": 0,
"Minor": 7,
"Patch": 4,
"Patch": 5,
"Build": 0
},
"ProductVersion": {
"Major": 0,
"Minor": 7,
"Patch": 4,
"Patch": 5,
"Build": 0
},
"FileFlagsMask": "3f",
@@ -25,7 +25,7 @@
"LegalCopyright": "Copyright (c) 2026 AJEAN contributors. MIT License.",
"OriginalFilename": "ajean.exe",
"ProductName": "AJEAN",
"ProductVersion": "0.7.4",
"ProductVersion": "0.7.5",
"Comments": "https://github.com/nathaninline/ajean — projet open source (MIT)"
},
"VarFileInfo": {
+5 -1
View File
@@ -10,7 +10,7 @@ import (
"strings"
)
const Version = "0.7.4"
const Version = "0.7.5"
// Main est le vrai main() du binaire (cmd/ajean ne fait que l'appeler).
func Main() {
@@ -91,6 +91,10 @@ func Main() {
mustExit(cmdUpdate(args))
case "where", "paths":
mustExit(cmdWhere(args))
case restartArg:
// Sous-commande interne, absente de l'aide : l'accompagnateur detache qui
// attend la fermeture de l'app, migre, puis la relance.
mustExit(cmdRestartAfterUpdate(args))
case migrateHomeArg:
// Sous-commande interne, volontairement absente de l'aide : c'est le
// travail delegue au process elevé par elevateForHomeMigration().
+13
View File
@@ -0,0 +1,13 @@
//go:build !windows
package ajean
// Pendant non-Windows de sys_restart_windows.go : sous Linux le redemarrage
// passe par systemd (restartAfterUpdate), qui sait deja relancer proprement un
// service. Il n'y a pas d'accompagnateur a lancer.
const restartArg = "restart-after-update"
func scheduleAppRestart() (bool, string) { return false, "" }
func cmdRestartAfterUpdate([]string) error { return nil }
+93
View File
@@ -0,0 +1,93 @@
//go:build windows
package ajean
import (
"fmt"
"os"
"strconv"
"time"
)
// Redémarrage propre de l'application après une mise à jour, sous Windows.
//
// Un process ne peut pas se relancer lui-même : tant qu'il tourne, le nouveau
// binaire ne peut pas prendre sa place sur le port, et le dossier de données
// reste tenu. On délègue donc à un ACCOMPAGNATEUR détaché — une copie de nous
// lancée avec une sous-commande interne — qui attend notre disparition, profite
// de cette fenêtre pour terminer la migration, puis relance l'application.
//
// C'est cette fenêtre qui manquait. Une mise à jour en place laisse AJEAN
// tourner, et donc le dossier tenu : le renommage échouait avec « Accès refusé »,
// qui sous Windows ne distingue pas un verrou d'un manque de droits. D'où des
// postes qui restaient indéfiniment sur l'ancien nom.
const restartArg = "restart-after-update"
// scheduleAppRestart lance l'accompagnateur et annonce que le redémarrage est
// pris en charge. L'appelant doit ensuite rendre la main puis quitter, pour que
// la réponse HTTP parte AVANT que le process ne disparaisse.
func scheduleAppRestart() (bool, string) {
exe, err := os.Executable()
if err != nil {
return false, ""
}
cmd := spawnDetached(exe, restartArg, strconv.Itoa(os.Getpid()))
if err := cmd.Start(); err != nil {
return false, ""
}
// L'accompagnateur ne doit pas mourir avec nous : spawnDetached l'a déjà
// sorti de notre groupe de processus, on se contente de l'oublier.
_ = cmd.Process.Release()
go func() {
time.Sleep(1500 * time.Millisecond) // laisser la réponse atteindre le navigateur
os.Exit(0)
}()
return true, "AJEAN redémarre — la page se reconnectera toute seule dans quelques secondes."
}
// cmdRestartAfterUpdate est exécuté par l'accompagnateur détaché.
//
// Il n'écrit rien à l'écran : personne ne le regarde, il n'a pas de console, et
// son seul travail visible est de faire réapparaître l'application.
func cmdRestartAfterUpdate(args []string) error {
if len(args) > 0 {
if pid, err := strconv.Atoi(args[0]); err == nil {
waitForExit(pid, 30*time.Second)
}
}
// L'application vient de se fermer : plus rien ne tient le dossier de
// données, le renommage peut enfin aboutir. postUpdateMigrate ne fait rien
// s'il n'y a pas lieu, et repointe raccourci et PATH s'il a migré.
postUpdateMigrate()
// installedExePath() est relu APRÈS la migration : le binaire a pu changer
// de dossier avec elle, et relancer l'ancien chemin échouerait.
target := installedExePath()
if _, err := os.Stat(target); err != nil {
// La cible a disparu : mieux vaut relancer ce qu'on a que rien du tout.
if self, e := os.Executable(); e == nil {
target = self
}
}
if !launch(target) {
return fmt.Errorf("relance de %s impossible", target)
}
return nil
}
// waitForExit attend la fin du process pid, sans dépasser le délai imparti.
// Un dépassement n'est pas bloquant : on poursuit quand même, quitte à ce que
// la migration échoue et soit retentée plus tard. Rester coincé ici serait pire
// — l'utilisateur se retrouverait sans application du tout.
func waitForExit(pid int, timeout time.Duration) {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
if !pidAlive(pid) {
time.Sleep(500 * time.Millisecond) // laisser les handles se fermer
return
}
time.Sleep(200 * time.Millisecond)
}
}
+7 -1
View File
@@ -405,7 +405,6 @@ func handleUpdateApply(w http.ResponseWriter, r *http.Request) {
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
return
}
postUpdateMigrate()
restarting, msg := restartAfterUpdate()
if !restarting {
msg = restartHintText()
@@ -423,6 +422,13 @@ func handleUpdateApply(w http.ResponseWriter, r *http.Request) {
// et l'UI se reconnecte toute seule (boucle de reconnexion du flux). Renvoie
// (déclenché, message). Non-serveur (poste client, Windows) → (false, "").
func restartAfterUpdate() (bool, string) {
// Windows : pas de superviseur pour nous relancer, on delegue a un
// accompagnateur detache (voir sys_restart_windows.go). C'est aussi ce qui
// cree la fenetre ou plus rien ne tient le dossier de donnees, donc ou la
// migration peut aboutir.
if runtime.GOOS == "windows" {
return scheduleAppRestart()
}
if runtime.GOOS != "linux" || !linkServiceActive() {
return false, ""
}