mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
v0.7.4 : migrer le dossier depuis le chemin de mise a jour (Windows)
L'elevation ajoutee en 0.7.3 n'etait jamais atteinte. Elle n'etait cablee que dans `install` et dans l'installeur de mise a jour (double-clic sur l'exe telecharge) ; or les utilisateurs font `ajean update` ou cliquent le bouton de l'interface. restartAfterUpdate ne fait rien sous Windows, et au redemarrage appFirstRun sort avant d'arriver la (l'application se reconnait comme etant deja l'installee). Aucun poste ne migrait donc jamais. postUpdateMigrate est appele apres une mise a jour reussie, cote CLI comme cote UI web. Une hypothese a ete invalidee au passage, et elle changeait tout : on croyait qu'un binaire en cours d'execution empechait de renommer le dossier qui le contient. C'est faux — Windows verrouille le fichier .exe, pas le nom de ses dossiers parents. Verifie sur banc d'essai. La migration peut donc se faire depuis l'application en marche, ce qui rend ce chemin possible. Consequence non anticipee, traitee ici : le binaire installe vit DANS le dossier de donnees. Le renommer deplace donc le programme, et laisse un raccourci du menu Demarrer et une entree de PATH pointant dans le vide. relinkAfterMigration repointe le raccourci, ajoute le nouveau dossier bin au PATH et retire l'ancien. Course de donnees corrigee : retryHomeMigration reinitialisait le cache du dossier resolu sans verrou, alors qu'il est desormais appele depuis un handler HTTP pendant que le serveur sert d'autres requetes. sync.Once est remplace par un RWMutex, et les reinitialisations directes des installeurs Unix passent par resetResolvedHome.
This commit is contained in:
1 parent
8365447de0
commit
1f773151a6
11 files changed
+117
-23
No files matched your search
+8
-4
@@ -1,10 +1,14 @@
|
||||
## Le renommage du dossier se termine tout seul sous Windows
|
||||
## Le renommage du dossier se fait maintenant à la mise à jour
|
||||
|
||||
Jusqu'ici, sur un ordinateur où vous n'êtes pas administrateur, `C:\ProgramData\jean` ne pouvait pas devenir `C:\ProgramData\ajean` : renommer ce dossier demande un droit qu'un compte standard n'a pas. Le renommage restait donc à moitié fait, indéfiniment.
|
||||
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.
|
||||
|
||||
L'installation demande désormais l'autorisation Windows nécessaire, le temps du renommage, puis rend la main. La fenêtre apparaît au moment où vous installez ou mettez à jour AJEAN — jamais au démarrage ordinaire, et jamais si elle ne sert à rien : ni quand le dossier porte déjà le bon nom, ni quand vous avez choisi vous-même son emplacement.
|
||||
`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`.
|
||||
|
||||
Refuser cette autorisation est une réponse valable. AJEAN continue alors d'utiliser son dossier actuel, exactement comme avant. Rien n'est perdu, rien n'est bloqué, et la question reviendra à la prochaine installation.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Mise à jour
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -3,13 +3,13 @@
|
||||
"FileVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 7,
|
||||
"Patch": 3,
|
||||
"Patch": 4,
|
||||
"Build": 0
|
||||
},
|
||||
"ProductVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 7,
|
||||
"Patch": 3,
|
||||
"Patch": 4,
|
||||
"Build": 0
|
||||
},
|
||||
"FileFlagsMask": "3f",
|
||||
@@ -25,7 +25,7 @@
|
||||
"LegalCopyright": "Copyright (c) 2026 AJEAN contributors. MIT License.",
|
||||
"OriginalFilename": "ajean.exe",
|
||||
"ProductName": "AJEAN",
|
||||
"ProductVersion": "0.7.3",
|
||||
"ProductVersion": "0.7.4",
|
||||
"Comments": "https://github.com/nathaninline/ajean — projet open source (MIT)"
|
||||
},
|
||||
"VarFileInfo": {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const Version = "0.7.3"
|
||||
const Version = "0.7.4"
|
||||
|
||||
// Main est le vrai main() du binaire (cmd/ajean ne fait que l'appeler).
|
||||
func Main() {
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// sys_install_darwin.go — installation macOS via launchd (LaunchDaemon), équivalent
|
||||
@@ -94,7 +93,7 @@ func cmdInstall(args []string) error {
|
||||
if err := migrateLayout(defaultLayoutPlan()); err != nil {
|
||||
return err
|
||||
}
|
||||
homeOnce, homePath = sync.Once{}, "" // le dossier a pu changer à l'instant
|
||||
resetResolvedHome() // le dossier a pu changer à l'instant
|
||||
|
||||
ajeanHome := defaultAjeanHome()
|
||||
if v := os.Getenv("JEAN_HOME"); v != "" {
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const configTemplate = `# Configuration JEAN — édite-moi puis: ajean restart
|
||||
@@ -83,7 +82,7 @@ func cmdInstall(args []string) error {
|
||||
if err := migrateLayout(defaultLayoutPlan()); err != nil {
|
||||
return err
|
||||
}
|
||||
homeOnce, homePath = sync.Once{}, "" // le dossier a pu changer à l'instant
|
||||
resetResolvedHome() // le dossier a pu changer à l'instant
|
||||
|
||||
ajeanHome := defaultAjeanHome()
|
||||
if v := os.Getenv("JEAN_HOME"); v != "" {
|
||||
|
||||
@@ -33,8 +33,13 @@ import (
|
||||
// 3. Aucune suppression, jamais. Le pire cas est « rien n'a bougé ».
|
||||
|
||||
var (
|
||||
homeOnce sync.Once
|
||||
homePath string
|
||||
// homeMu protège la résolution du dossier. Un simple sync.Once ne suffisait
|
||||
// pas : retryHomeMigration le RÉINITIALISE, et il est appelé depuis le
|
||||
// handler HTTP de mise à jour pendant que le serveur sert d'autres requêtes.
|
||||
// Réécrire ces variables sans verrou était une course de données.
|
||||
homeMu sync.RWMutex
|
||||
homeResolved bool
|
||||
homePath string
|
||||
|
||||
// migrationDeferred retient pourquoi le dossier n'a pas pu être renommé,
|
||||
// pour que seules les commandes qui parlent d'emplacements en fassent état.
|
||||
@@ -76,7 +81,19 @@ func renameCause(err error) string {
|
||||
// N'est PAS appelé quand $AJEAN_HOME/$JEAN_HOME ou /etc/default/* imposent un
|
||||
// chemin : un choix explicite de l'utilisateur ne se migre pas.
|
||||
func migratedDefaultHome() string {
|
||||
homeOnce.Do(func() { homePath = resolveDefaultHome() })
|
||||
homeMu.RLock()
|
||||
if homeResolved {
|
||||
p := homePath
|
||||
homeMu.RUnlock()
|
||||
return p
|
||||
}
|
||||
homeMu.RUnlock()
|
||||
|
||||
homeMu.Lock()
|
||||
defer homeMu.Unlock()
|
||||
if !homeResolved { // un autre appelant a pu résoudre entre les deux verrous
|
||||
homePath, homeResolved = resolveDefaultHome(), true
|
||||
}
|
||||
return homePath
|
||||
}
|
||||
|
||||
@@ -93,14 +110,17 @@ func resolveDefaultHome() string {
|
||||
// resterait sur l'ancien chemin. L'installateur, lui, a une fenêtre où tout est
|
||||
// arrêté : c'est là qu'on retente.
|
||||
//
|
||||
// À N'APPELER QUE dans cette fenêtre, et avant que quoi que ce soit d'autre
|
||||
// n'ait ouvert de fichier de données : réinitialiser le cache n'est ni atomique
|
||||
// ni sûr vis-à-vis des goroutines, et surtout les chemins déjà calculés par
|
||||
// l'appelant deviennent obsolètes (voir migrateThenResolveTarget).
|
||||
// L'accès au cache est protégé par homeMu, mais cela ne rend pas l'appel anodin
|
||||
// pour autant : les chemins DÉJÀ CALCULÉS par l'appelant deviennent obsolètes
|
||||
// dès que la migration aboutit (voir migrateThenResolveTarget et
|
||||
// relinkAfterMigration, qui reconstruisent ce qui en dépend).
|
||||
func retryHomeMigration() bool {
|
||||
before := migratedDefaultHome()
|
||||
homeOnce = sync.Once{}
|
||||
homePath = ""
|
||||
before := migratedDefaultHome() // force la résolution si elle n'a pas eu lieu
|
||||
|
||||
homeMu.Lock()
|
||||
homeResolved = false
|
||||
homeMu.Unlock()
|
||||
|
||||
return migratedDefaultHome() != before
|
||||
}
|
||||
|
||||
@@ -243,3 +263,12 @@ func adoptLegacyStateFiles(home string) {
|
||||
_ = os.Rename(from, to)
|
||||
}
|
||||
}
|
||||
|
||||
// resetResolvedHome oublie le dossier résolu, pour que le prochain appel reparte
|
||||
// du disque. Utilisé par `install` juste après avoir migré l'agencement : sans
|
||||
// ça, la suite de l'installation continuerait de viser l'ancien emplacement.
|
||||
func resetResolvedHome() {
|
||||
homeMu.Lock()
|
||||
homeResolved = false
|
||||
homeMu.Unlock()
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !windows
|
||||
|
||||
package ajean
|
||||
|
||||
// Pendant non-Windows de sys_postupdate_windows.go : la migration d'agencement
|
||||
// se fait dans restartAfterUpdate (voir sys_layout.go), qui tourne deja en root.
|
||||
func postUpdateMigrate() {}
|
||||
@@ -0,0 +1,51 @@
|
||||
//go:build windows
|
||||
|
||||
package ajean
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Migration du dossier juste après une mise à jour, sous Windows.
|
||||
//
|
||||
// C'est le chaînon qui manquait. Les utilisateurs ne relancent pas `install` et
|
||||
// ne retéléchargent pas l'exécutable : ils font `ajean update`, ou cliquent le
|
||||
// bouton de l'interface. Or ce chemin-là ne tentait rien — l'élévation n'était
|
||||
// câblée que dans `install` et dans l'installeur de mise à jour. Résultat : un
|
||||
// poste sans droits administrateur restait sur %ProgramData%\jean pour toujours,
|
||||
// sans qu'aucune fenêtre ne propose jamais de le régler.
|
||||
//
|
||||
// Contrairement à ce qu'on pourrait croire, le binaire en cours d'exécution
|
||||
// n'empêche PAS de renommer le dossier qui le contient : Windows verrouille le
|
||||
// fichier .exe lui-même, pas le nom de ses dossiers parents. Vérifié sur banc
|
||||
// d'essai. La migration peut donc se faire depuis l'application en marche.
|
||||
func postUpdateMigrate() {
|
||||
if !retryHomeMigration() && !elevateForHomeMigration() {
|
||||
return
|
||||
}
|
||||
fmt.Printf("%s dossier de données migré vers %s\n", green("✓"), AjeanHome())
|
||||
relinkAfterMigration()
|
||||
}
|
||||
|
||||
// relinkAfterMigration répare ce que le déplacement laisse derrière lui.
|
||||
//
|
||||
// Le binaire installé vit DANS le dossier de données (%AJEAN_HOME%\bin). Le
|
||||
// renommer déplace donc aussi le programme : le raccourci du menu Démarrer et
|
||||
// l'entrée de PATH désignent un chemin qui n'existe plus. Sans cette étape, la
|
||||
// migration « réussit » et l'utilisateur se retrouve avec un raccourci mort et
|
||||
// une commande `ajean` introuvable — soit exactement ce qu'on cherchait à éviter.
|
||||
func relinkAfterMigration() {
|
||||
target := installedExePath()
|
||||
binDir := filepath.Dir(target)
|
||||
|
||||
if _, err := addToUserPath(binDir); err != nil {
|
||||
fmt.Printf(" %s PATH non mis à jour (%v) — ajoute %s à la main\n", dim("•"), err, binDir)
|
||||
}
|
||||
// L'ancienne entrée ne désigne plus rien : la laisser encombrerait le PATH
|
||||
// d'un chemin mort, et ferait échouer un `jean` tapé par habitude.
|
||||
if legacyBin := filepath.Join(legacyDefaultHome(), "bin"); legacyBin != binDir {
|
||||
_, _ = removeFromUserPath(legacyBin)
|
||||
}
|
||||
ensureShortcuts(target)
|
||||
}
|
||||
@@ -251,7 +251,11 @@ func cmdUpdate(args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("✓ jean mis à jour en %s\n", newVer)
|
||||
fmt.Printf("✓ ajean mis à jour en %s\n", newVer)
|
||||
// Sous Windows, c'est ICI qu'on peut enfin migrer le dossier : `update` est
|
||||
// le geste délibéré de l'utilisateur, et c'est le seul que la plupart
|
||||
// feront. No-op ailleurs (Unix migre dans restartAfterUpdate, déjà en root).
|
||||
postUpdateMigrate()
|
||||
printRestartHint()
|
||||
return nil
|
||||
}
|
||||
@@ -401,6 +405,7 @@ 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()
|
||||
|
||||
Reference in new issue
Block a user