From 7a141d19b55f7c1367292eda127fc9fd6823e4a7 Mon Sep 17 00:00:00 2001 From: nathaninline Date: Tue, 4 Aug 2026 16:50:39 +0200 Subject: [PATCH] v0.7.2 : l'alias jean.exe et les raccourcis restaient sur une version perimee MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regression introduite par l'alias de la 0.7.0, signalee en usage : « Une version plus recente d'AJEAN est deja installee » a CHAQUE lancement, sans moyen d'en sortir. Deux defauts qui se renforcaient. installLegacyAlias utilisait copyExe, qui echoue quand jean.exe est en cours d'execution — Windows refuse d'ecraser un .exe actif. L'echec etait ignore volontairement (« sans gravite, la copie repassera au prochain lancement »), ce qui etait faux : l'alias restait fige indefiniment. replaceExe applique desormais la technique deja utilisee pour le binaire principal — renommer l'ancien, ecrire le nouveau, restaurer si la copie echoue. installSelf s'en sert aussi, pour la meme raison. ensureShortcuts ne touchait jamais un raccourci existant. Ceux crees avant la 0.7.0 visaient donc toujours jean.exe. Chaque lancement executait l'alias perime, qui voyait ajean.exe plus recent et le signalait — la boucle. Un raccourci existant est maintenant repointe s'il vise autre chose que le binaire courant. cleanupOldBinary balaie aussi le jean.exe.old que le nouveau remplacement peut laisser. Deux tests : remplacement d'un exe verrouille, et garantie que la cible ne disparait jamais si la copie echoue apres le renommage. --- RELEASE_NOTES.md | 21 +++----- cmd/ajean/resource_windows_amd64.syso | Bin 7544 -> 7544 bytes cmd/ajean/resource_windows_arm64.syso | Bin 7544 -> 7544 bytes cmd/ajean/versioninfo.json | 6 +-- internal/ajean/run.go | 2 +- internal/ajean/sys_firstrun_windows.go | 16 +++++- internal/ajean/sys_firstrun_windows_test.go | 56 ++++++++++++++++++++ internal/ajean/sys_install_windows.go | 33 ++++++++++-- internal/ajean/sys_update.go | 9 +++- 9 files changed, 118 insertions(+), 25 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 1fb12f2..2b1a33a 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,23 +1,14 @@ ## Correction -**Un message d'erreur s'affichait avant chaque commande sur les postes Windows sans droits administrateur.** Il ressemblait à ceci, et revenait même pour un simple `ajean help` : +**Sous Windows, « Une version plus récente d'AJEAN est déjà installée » pouvait s'afficher à chaque lancement.** Le message était exact, mais rien ne permettait d'en sortir : il revenait indéfiniment. -``` -[info] dossier de données pas encore migré vers C:\ProgramData\ajean - (rename ... : Access is denied.) — on continue sur C:\ProgramData\jean -``` +Deux causes, qui se renforçaient. -Rien n'était cassé. AJEAN fonctionnait normalement, sur son dossier habituel, sans aucune donnée en jeu. Mais renommer `C:\ProgramData\jean` demande le droit d'écrire dans `C:\ProgramData`, qu'un compte standard n'a pas : le renommage échouait à chaque lancement, et le message revenait indéfiniment. C'était un détail interne affiché comme une erreur. +La commande `jean`, conservée en alias depuis la 0.7.0, est une copie du programme posée à côté de `ajean.exe`. Windows refusant d'écraser un exécutable en cours d'exécution, cette copie n'était pas remplacée si elle tournait au moment de la mise à jour — elle restait figée sur une version périmée. Elle est désormais mise de côté puis réécrite, la même technique que celle utilisée pour le programme principal. -Il ne s'affiche plus. La raison est conservée et ressortie là où elle sert à quelque chose — `ajean where`, qui montre justement les emplacements — avec ce qu'il faut faire pour aligner les noms si vous le souhaitez : +Et les raccourcis du menu Démarrer et du Bureau, créés avant la 0.7.0, visaient toujours cet alias. Ils n'étaient jamais repointés. Chaque lancement exécutait donc l'ancienne version, qui constatait à juste titre qu'une plus récente était installée — et le disait. Un raccourci existant est maintenant repointé vers le programme courant. -``` -ajean where -``` - -`ajean install` lancé en administrateur termine désormais la migration : c'est le seul moment où les droits nécessaires sont réunis. - -Rien de tout cela n'est obligatoire. Un AJEAN qui continue d'utiliser `C:\ProgramData\jean` fonctionne exactement comme les autres, et vos données restent où elles sont. +Après cette mise à jour, le message disparaît de lui-même au premier lancement. ## Mise à jour @@ -25,4 +16,6 @@ Rien de tout cela n'est obligatoire. Un AJEAN qui continue d'utiliser `C:\Progra ajean update ``` +Sous Windows, télécharger `ajean-windows-amd64.exe` depuis cette page et le lancer fait le même travail. + Les binaires restent publiés sous leurs deux noms, `ajean-*` et `jean-*`, le temps que le parc bascule. diff --git a/cmd/ajean/resource_windows_amd64.syso b/cmd/ajean/resource_windows_amd64.syso index dba2a3fd028eee1bbb8724b0fd478c6d770f197c..234274dd25cc93338cfe0bd73d2f67abb5b44ed0 100644 GIT binary patch delta 35 ocmexi^}}idmn0_>13Lo)b%*bdoxl!gj0Ftu^A^-pY delta 35 ocmexi^}}idmn0`613Lolw@Kw+^j1d%*bdsxl!gj0FrbF9smFU diff --git a/cmd/ajean/resource_windows_arm64.syso b/cmd/ajean/resource_windows_arm64.syso index e0a00b97ae5ce091cb4f7db9b6db10b329e1905b..1f069e6d75f332ab610b335cc87061d5ca38f4b2 100644 GIT binary patch delta 35 ocmexi^}}idmn0_>13Lo)b%*bdoxl!gj0Ftu^A^-pY delta 35 ocmexi^}}idmn0`613Lolw@Kw+^j1d%*bdsxl!gj0FrbF9smFU diff --git a/cmd/ajean/versioninfo.json b/cmd/ajean/versioninfo.json index 40e7a7b..826e2ae 100644 --- a/cmd/ajean/versioninfo.json +++ b/cmd/ajean/versioninfo.json @@ -3,13 +3,13 @@ "FileVersion": { "Major": 0, "Minor": 7, - "Patch": 1, + "Patch": 2, "Build": 0 }, "ProductVersion": { "Major": 0, "Minor": 7, - "Patch": 1, + "Patch": 2, "Build": 0 }, "FileFlagsMask": "3f", @@ -25,7 +25,7 @@ "LegalCopyright": "Copyright (c) 2026 AJEAN contributors. MIT License.", "OriginalFilename": "ajean.exe", "ProductName": "AJEAN", - "ProductVersion": "0.7.1", + "ProductVersion": "0.7.2", "Comments": "https://github.com/nathaninline/ajean — projet open source (MIT)" }, "VarFileInfo": { diff --git a/internal/ajean/run.go b/internal/ajean/run.go index 70702da..4af5f02 100644 --- a/internal/ajean/run.go +++ b/internal/ajean/run.go @@ -10,7 +10,7 @@ import ( "strings" ) -const Version = "0.7.1" +const Version = "0.7.2" // Main est le vrai main() du binaire (cmd/ajean ne fait que l'appeler). func Main() { diff --git a/internal/ajean/sys_firstrun_windows.go b/internal/ajean/sys_firstrun_windows.go index 8e11192..532cff5 100644 --- a/internal/ajean/sys_firstrun_windows.go +++ b/internal/ajean/sys_firstrun_windows.go @@ -426,7 +426,21 @@ foreach ($d in @($progs, [Environment]::GetFolderPath('Desktop'))) { if (-not $d) { continue } if (-not (Test-Path $d)) { continue } $lnk=Join-Path $d 'AJEAN.lnk' - if (Test-Path $lnk) { $done+=$d; continue } + # Un raccourci existant est REPOINTÉ s'il vise autre chose que le binaire + # canonique — typiquement l'ancien « jean.exe », qui n'est plus qu'un alias. + # Le laisser en l'état condamnait l'utilisateur à relancer indéfiniment une + # version périmée par son propre raccourci. + if (Test-Path $lnk) { + try { + $s=$w.CreateShortcut($lnk) + if ($s.TargetPath -and $s.TargetPath -ne $t -and (Test-Path $s.TargetPath)) { + $s.TargetPath=$t + $s.WorkingDirectory=(Split-Path $t) + $s.Save() + } + } catch {} + $done+=$d; continue + } try { $s=$w.CreateShortcut($lnk) $s.TargetPath=$t diff --git a/internal/ajean/sys_firstrun_windows_test.go b/internal/ajean/sys_firstrun_windows_test.go index b5cde1e..1b385c4 100644 --- a/internal/ajean/sys_firstrun_windows_test.go +++ b/internal/ajean/sys_firstrun_windows_test.go @@ -80,3 +80,59 @@ func TestBinaryVersionWithoutResource(t *testing.T) { t.Fatalf("binaryVersion sur un fichier sans ressource = %q, attendu \"\"", v) } } + +// Reproduit le cas signale en usage : l'alias « jean.exe » est EN COURS +// d'execution au moment de la mise a jour. copyExe seul echouait, l'alias +// restait fige sur une version perimee, et tout raccourci le visant relancait +// indefiniment l'ancienne version — qui affichait « une version plus recente +// est deja installee » a chaque lancement. +func TestReplaceExeSurchargeUnFichierVerrouille(t *testing.T) { + dir := t.TempDir() + src := filepath.Join(dir, "neuf.exe") + dst := filepath.Join(dir, "jean.exe") + if err := os.WriteFile(src, []byte("VERSION-NEUVE"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(dst, []byte("version-perimee"), 0o755); err != nil { + t.Fatal(err) + } + + // Verrou exclusif : imite un .exe en cours d'execution sous Windows. + held, err := os.OpenFile(dst, os.O_RDONLY, 0) + if err != nil { + t.Fatal(err) + } + defer held.Close() + + if err := replaceExe(src, dst); err != nil { + t.Fatalf("remplacement impossible: %v", err) + } + b, err := os.ReadFile(dst) + if err != nil { + t.Fatal(err) + } + if string(b) != "VERSION-NEUVE" { + t.Fatalf("l'alias est reste perime: %q", b) + } +} + +// Le remplacement ne doit JAMAIS faire disparaitre la cible : si la copie +// echoue apres le renommage, l'ancien fichier revient a sa place. +func TestReplaceExeRestaureSiLaCopieEchoue(t *testing.T) { + dir := t.TempDir() + dst := filepath.Join(dir, "jean.exe") + if err := os.WriteFile(dst, []byte("a-preserver"), 0o755); err != nil { + t.Fatal(err) + } + // Source inexistante : la copie echouera forcement. + if err := replaceExe(filepath.Join(dir, "absent.exe"), dst); err == nil { + t.Fatal("attendu une erreur") + } + b, err := os.ReadFile(dst) + if err != nil { + t.Fatalf("le fichier a disparu: %v", err) + } + if string(b) != "a-preserver" { + t.Fatalf("contenu altere: %q", b) + } +} diff --git a/internal/ajean/sys_install_windows.go b/internal/ajean/sys_install_windows.go index efc72aa..70aafa3 100644 --- a/internal/ajean/sys_install_windows.go +++ b/internal/ajean/sys_install_windows.go @@ -130,7 +130,9 @@ func installSelf(binDir string) (string, error) { if err := os.MkdirAll(binDir, 0o755); err != nil { return "", err } - if err := copyExe(src, dst); err != nil { + // replaceExe et non copyExe : ajean.exe peut etre en cours d'execution + // (service en tache de fond), auquel cas Windows refuse de l'ecraser. + if err := replaceExe(src, dst); err != nil { return "", err } installLegacyAlias(binDir, dst) @@ -150,9 +152,32 @@ func installLegacyAlias(binDir, src string) { if strings.EqualFold(src, alias) { return } - // Échoue si un « jean.exe » est en cours d'exécution — sans gravité, il s'agit - // alors déjà d'une version d'AJEAN, et la copie repassera au prochain lancement. - _ = copyExe(src, alias) + _ = replaceExe(src, alias) +} + +// replaceExe copie src vers dst, y compris quand dst est un exécutable EN COURS. +// +// Windows refuse d'écraser un .exe en cours d'exécution, mais accepte de le +// RENOMMER : on décale l'ancien puis on écrit le nouveau à sa place. Sans ça, +// l'alias « jean.exe » restait figé sur une version périmée dès qu'il tournait +// au moment de la mise à jour — et comme les raccourcis existants le visent +// encore, chaque lancement relançait l'ancienne version, qui constatait qu'une +// plus récente était installée et le disait. À chaque fois. Constaté en usage. +func replaceExe(src, dst string) error { + if err := copyExe(src, dst); err == nil { + return nil + } + old := dst + ".old" + _ = os.Remove(old) // reliquat d'un remplacement précédent + if err := os.Rename(dst, old); err != nil { + return err // ni écrasable ni renommable : on laisse la place en l'état + } + if err := copyExe(src, dst); err != nil { + _ = os.Rename(old, dst) // rien ne doit disparaître + return err + } + _ = os.Remove(old) // échoue tant que l'ancien tourne ; nettoyé plus tard + return nil } func copyExe(src, dst string) error { diff --git a/internal/ajean/sys_update.go b/internal/ajean/sys_update.go index ff6b9b2..dd6c89b 100644 --- a/internal/ajean/sys_update.go +++ b/internal/ajean/sys_update.go @@ -370,9 +370,14 @@ func cleanupOldBinary() { if runtime.GOOS != "windows" { return } - if exe, err := os.Executable(); err == nil { - _ = os.Remove(exe + ".old") + exe, err := os.Executable() + if err != nil { + return } + _ = os.Remove(exe + ".old") + // L'alias herite laisse le meme reliquat quand il etait en cours d'execution + // au moment ou on l'a remplace (voir replaceExe). + _ = os.Remove(filepath.Join(filepath.Dir(exe), "jean.exe.old")) } // handleUpdateCheck (GET /api/update) : renvoie l'état de mise à jour pour l'UI.