mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
MAJ Windows : ne plus echouer sur un ecartement encore verrouille
Le remplacement du binaire renommait l'ancien en un nom FIXE (.old). Si un
processus tournait encore depuis ce .old (mise a jour precedente), le fichier
n'etait ni supprimable ni ecrasable : le renommage sortait en "Acces refuse"
(errno 5), que os.IsPermission rapporte comme un defaut de DROITS. L'UI
affichait donc "droits insuffisants, relance en administrateur", conseil faux :
aucun privilege ne remplace l'image d'un executable en cours d'execution.
- renameAside() ecarte sous un nom UNIQUE (.old-<horodatage>) : le renommage
passe meme si un ancien ecartement est verrouille (verifie sur banc d'essai
avec deux processus vivants)
- les trois endroits qui faisaient la manoeuvre l'utilisent (update, install,
premier lancement), et le menage ramasse le nom fixe herite + les uniques
- message d'erreur : Windows renvoie le meme code pour "droits insuffisants" et
"fichier utilise", les deux causes sont donc nommees
Aussi : la section Cartes graphiques restait affichee avec une seule carte.
L'attribut hidden etait sans effet, .pe-group{display:flex} l'emportant sur la
regle par defaut du navigateur.
This commit is contained in:
1 parent
e57fc0966e
commit
ae38aca9ba
6 files changed
+126
-12
No files matched your search
@@ -250,9 +250,9 @@ func migrateThenResolveTarget(target string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func replaceInstalled(target string) error {
|
func replaceInstalled(target string) error {
|
||||||
old := target + ".old"
|
removeOldBinaries(target)
|
||||||
_ = os.Remove(old)
|
old, err := renameAside(target) // nom unique, cf. renameAside
|
||||||
if err := os.Rename(target, old); err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := installSelf(filepath.Dir(target)); err != nil {
|
if _, err := installSelf(filepath.Dir(target)); err != nil {
|
||||||
|
|||||||
@@ -170,9 +170,11 @@ func replaceExe(src, dst string) error {
|
|||||||
if err := copyExe(src, dst); err == nil {
|
if err := copyExe(src, dst); err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
old := dst + ".old"
|
removeOldBinaries(dst) // reliquats des remplacements précédents
|
||||||
_ = os.Remove(old) // reliquat d'un remplacement précédent
|
// Nom unique : un écartement encore verrouillé par un ancien processus ne
|
||||||
if err := os.Rename(dst, old); err != nil {
|
// doit pas bloquer celui-ci (voir renameAside).
|
||||||
|
old, err := renameAside(dst)
|
||||||
|
if err != nil {
|
||||||
return err // ni écrasable ni renommable : on laisse la place en l'état
|
return err // ni écrasable ni renommable : on laisse la place en l'état
|
||||||
}
|
}
|
||||||
if err := copyExe(src, dst); err != nil {
|
if err := copyExe(src, dst); err != nil {
|
||||||
|
|||||||
@@ -201,7 +201,10 @@ func applyUpdate() (string, error) {
|
|||||||
// droits d'écrire son propre binaire : la commande exacte à lancer.
|
// droits d'écrire son propre binaire : la commande exacte à lancer.
|
||||||
func updatePermissionError(exe string) error {
|
func updatePermissionError(exe string) error {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
return fmt.Errorf("droits insuffisants pour remplacer %s — relance AJEAN en administrateur puis réessaie", exe)
|
// Windows renvoie le MÊME code (5, accès refusé) pour « droits
|
||||||
|
// insuffisants » et pour « fichier utilisé par un processus » : on nomme
|
||||||
|
// les deux causes au lieu d'affirmer la mauvaise.
|
||||||
|
return fmt.Errorf("impossible de remplacer %s : soit un autre AJEAN utilise ce fichier (ferme l'application et arrête le service, puis réessaie), soit les droits manquent (relance AJEAN en administrateur)", exe)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("droits insuffisants pour remplacer %s (le binaire appartient à root) — lance la mise à jour en ligne de commande : sudo ajean update", exe)
|
return fmt.Errorf("droits insuffisants pour remplacer %s (le binaire appartient à root) — lance la mise à jour en ligne de commande : sudo ajean update", exe)
|
||||||
}
|
}
|
||||||
@@ -353,9 +356,8 @@ func fileSize(p string) int64 {
|
|||||||
// cours : on renomme l'ancien en .old (supprimé au prochain lancement).
|
// cours : on renomme l'ancien en .old (supprimé au prochain lancement).
|
||||||
func replaceBinary(exe, tmp string) error {
|
func replaceBinary(exe, tmp string) error {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
old := exe + ".old"
|
old, err := renameAside(exe)
|
||||||
_ = os.Remove(old) // nettoyage d'une éventuelle MAJ précédente
|
if err != nil {
|
||||||
if err := os.Rename(exe, old); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := os.Rename(tmp, exe); err != nil {
|
if err := os.Rename(tmp, exe); err != nil {
|
||||||
@@ -368,6 +370,35 @@ func replaceBinary(exe, tmp string) error {
|
|||||||
return os.Rename(tmp, exe)
|
return os.Rename(tmp, exe)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// renameAside écarte un fichier en le renommant sous un nom UNIQUE.
|
||||||
|
//
|
||||||
|
// Un nom FIXE (« .old ») est un piège sous Windows : si un processus tourne
|
||||||
|
// ENCORE depuis le .old d'une mise à jour précédente, ce fichier ne peut être ni
|
||||||
|
// supprimé ni écrasé. Le renommage échoue alors avec « Accès refusé » (errno 5),
|
||||||
|
// que os.IsPermission rapporte comme un problème de droits — d'où le message
|
||||||
|
// « relance AJEAN en administrateur », un conseil FAUX : aucun privilège ne
|
||||||
|
// permet de remplacer l'image d'un exécutable en cours d'exécution. Reproduit
|
||||||
|
// puis vérifié corrigé sur banc d'essai, deux processus vivants sur les deux
|
||||||
|
// noms : avec un suffixe unique le renommage passe.
|
||||||
|
func renameAside(path string) (string, error) {
|
||||||
|
aside := fmt.Sprintf("%s.old-%d", path, time.Now().UnixNano())
|
||||||
|
if err := os.Rename(path, aside); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return aside, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeOldBinaries supprime les écartements laissés par les remplacements
|
||||||
|
// précédents (nom fixe hérité ET noms uniques). Best-effort : ceux dont le
|
||||||
|
// processus tourne encore résistent, on réessaiera au prochain lancement.
|
||||||
|
func removeOldBinaries(path string) {
|
||||||
|
_ = os.Remove(path + ".old")
|
||||||
|
matches, _ := filepath.Glob(path + ".old-*")
|
||||||
|
for _, m := range matches {
|
||||||
|
_ = os.Remove(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// cleanupOldBinary supprime silencieusement le .old laissé par une MAJ Windows
|
// cleanupOldBinary supprime silencieusement le .old laissé par une MAJ Windows
|
||||||
// précédente (le fichier n'était pas supprimable tant que l'exe tournait).
|
// précédente (le fichier n'était pas supprimable tant que l'exe tournait).
|
||||||
func cleanupOldBinary() {
|
func cleanupOldBinary() {
|
||||||
@@ -378,10 +409,10 @@ func cleanupOldBinary() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_ = os.Remove(exe + ".old")
|
removeOldBinaries(exe)
|
||||||
// L'alias herite laisse le meme reliquat quand il etait en cours d'execution
|
// L'alias herite laisse le meme reliquat quand il etait en cours d'execution
|
||||||
// au moment ou on l'a remplace (voir replaceExe).
|
// au moment ou on l'a remplace (voir replaceExe).
|
||||||
_ = os.Remove(filepath.Join(filepath.Dir(exe), "jean.exe.old"))
|
removeOldBinaries(filepath.Join(filepath.Dir(exe), "jean.exe"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleUpdateCheck (GET /api/update) : renvoie l'état de mise à jour pour l'UI.
|
// handleUpdateCheck (GET /api/update) : renvoie l'état de mise à jour pour l'UI.
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package ajean
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Le remplacement du binaire doit fonctionner MÊME si un écartement précédent
|
||||||
|
// est toujours là et impossible à supprimer (processus encore vivant dessus).
|
||||||
|
// Avec un nom fixe « .old », le renommage échouait alors en « Accès refusé »,
|
||||||
|
// que jean traduisait par « droits insuffisants, relance en administrateur » :
|
||||||
|
// un conseil faux, aucun privilège ne remplace une image en cours d'exécution.
|
||||||
|
func TestRenameAsideNeBloquePasSurUnAncienEcartement(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
exe := filepath.Join(dir, "ajean.exe")
|
||||||
|
if err := os.WriteFile(exe, []byte("v1"), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Un écartement d'une mise à jour précédente occupe déjà le nom fixe.
|
||||||
|
if err := os.WriteFile(exe+".old", []byte("v0"), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
aside, err := renameAside(exe)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("renameAside a échoué alors qu'un .old existe déjà : %v", err)
|
||||||
|
}
|
||||||
|
if aside == exe+".old" {
|
||||||
|
t.Error("nom d'écartement fixe : c'est précisément ce qui bloquait")
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(filepath.Base(aside), "ajean.exe.old-") {
|
||||||
|
t.Errorf("nom inattendu : %s", aside)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(exe); !os.IsNotExist(err) {
|
||||||
|
t.Error("le binaire n'a pas été écarté")
|
||||||
|
}
|
||||||
|
// Deux écartements successifs ne doivent jamais se marcher dessus.
|
||||||
|
if err := os.WriteFile(exe, []byte("v2"), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
aside2, err := renameAside(exe)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if aside2 == aside {
|
||||||
|
t.Error("deux écartements ont produit le même nom")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Le ménage doit ramasser l'ancien nom fixe ET les noms uniques.
|
||||||
|
func TestRemoveOldBinaries(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
exe := filepath.Join(dir, "ajean.exe")
|
||||||
|
for _, n := range []string{exe + ".old", exe + ".old-1", exe + ".old-2"} {
|
||||||
|
if err := os.WriteFile(n, []byte("x"), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(exe, []byte("v1"), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
removeOldBinaries(exe)
|
||||||
|
restes, _ := filepath.Glob(exe + ".old*")
|
||||||
|
if len(restes) != 0 {
|
||||||
|
t.Errorf("écartements non nettoyés : %v", restes)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(exe); err != nil {
|
||||||
|
t.Errorf("le binaire courant a été supprimé : %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -567,6 +567,11 @@ textarea.sctl{min-height:76px;resize:vertical;line-height:1.5}
|
|||||||
.pe-val::-webkit-outer-spin-button,.pe-val::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
|
.pe-val::-webkit-outer-spin-button,.pe-val::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
|
||||||
/* Un groupe = un petit titre + une liste encadrée aux coins arrondis. */
|
/* Un groupe = un petit titre + une liste encadrée aux coins arrondis. */
|
||||||
.pe-group{display:flex;flex-direction:column;gap:8px}
|
.pe-group{display:flex;flex-direction:column;gap:8px}
|
||||||
|
/* `display:flex` sur la classe l'emporte sur la règle par défaut du navigateur
|
||||||
|
pour [hidden] : sans cette ligne, masquer un groupe via l'attribut hidden
|
||||||
|
n'avait AUCUN effet visible (section « Cartes graphiques » qui restait
|
||||||
|
affichée avec une seule carte). */
|
||||||
|
.pe-group[hidden]{display:none}
|
||||||
.pe-gh{font-size:12px;font-weight:600;color:var(--dim);padding:0 6px}
|
.pe-gh{font-size:12px;font-weight:600;color:var(--dim);padding:0 6px}
|
||||||
.pe-list{background:var(--panel);border:1px solid var(--border);border-radius:12px;overflow:hidden}
|
.pe-list{background:var(--panel);border:1px solid var(--border);border-radius:12px;overflow:hidden}
|
||||||
/* Ligne : libellé à gauche, contrôle à droite, séparateur fin entre lignes. */
|
/* Ligne : libellé à gauche, contrôle à droite, séparateur fin entre lignes. */
|
||||||
|
|||||||
@@ -541,6 +541,11 @@ textarea.sctl{min-height:76px;resize:vertical;line-height:1.5}
|
|||||||
.pe-val::-webkit-outer-spin-button,.pe-val::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
|
.pe-val::-webkit-outer-spin-button,.pe-val::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
|
||||||
/* Un groupe = un petit titre + une liste encadrée aux coins arrondis. */
|
/* Un groupe = un petit titre + une liste encadrée aux coins arrondis. */
|
||||||
.pe-group{display:flex;flex-direction:column;gap:8px}
|
.pe-group{display:flex;flex-direction:column;gap:8px}
|
||||||
|
/* `display:flex` sur la classe l'emporte sur la règle par défaut du navigateur
|
||||||
|
pour [hidden] : sans cette ligne, masquer un groupe via l'attribut hidden
|
||||||
|
n'avait AUCUN effet visible (section « Cartes graphiques » qui restait
|
||||||
|
affichée avec une seule carte). */
|
||||||
|
.pe-group[hidden]{display:none}
|
||||||
.pe-gh{font-size:12px;font-weight:600;color:var(--dim);padding:0 6px}
|
.pe-gh{font-size:12px;font-weight:600;color:var(--dim);padding:0 6px}
|
||||||
.pe-list{background:var(--panel);border:1px solid var(--border);border-radius:12px;overflow:hidden}
|
.pe-list{background:var(--panel);border:1px solid var(--border);border-radius:12px;overflow:hidden}
|
||||||
/* Ligne : libellé à gauche, contrôle à droite, séparateur fin entre lignes. */
|
/* Ligne : libellé à gauche, contrôle à droite, séparateur fin entre lignes. */
|
||||||
|
|||||||
Reference in new issue
Block a user