Windows : comparer les chemins canonises, pas les chaines brutes

Windows expose le meme fichier sous plusieurs ecritures (forme courte 8.3
« ADMINI~1 » contre « Administrateur », casse, liens). Les deux comparaisons de
chemins du premier lancement le supposaient identique des deux cotes :

- runningPIDs ratait l'instance en cours -> on remplacait le binaire en croyant
  l'application arretee, elle continuait de tourner en ancienne version sans que
  rien ne l'indique. C'est le bug corrige en 0.6.12, revenu par une autre porte.
- appFirstRun : l'application installee se serait prise pour une copie
  telechargee et se serait relancee, en BOUCLE INFINIE.

Constate sur banc d'essai Windows (JEAN_HOME sous un chemin en forme courte).
This commit is contained in:
nathaninline committed 2026-08-04 12:37:59 +02:00
1 parent 1d58a7b1b9
commit 5728abf93e
1 file changed
+39 -10
+39 -10
View File
@@ -82,11 +82,11 @@ func appFirstRun() bool {
if err != nil { if err != nil {
return false return false
} }
if resolved, err := filepath.EvalSymlinks(exe); err == nil {
exe = resolved
}
target := installedExePath() target := installedExePath()
if strings.EqualFold(exe, target) { // canonPath des DEUX côtés : si l'égalité rate parce que les chemins sont
// écrits différemment (forme courte 8.3, casse, lien), l'application installée
// se prend pour une copie téléchargée et se relance… en boucle infinie.
if canonPath(exe) == canonPath(target) {
// On EST l'application installée. On en profite pour garantir que les // On EST l'application installée. On en profite pour garantir que les
// raccourcis existent : sans ça, quelqu'un qui perd son raccourci ne // raccourcis existent : sans ça, quelqu'un qui perd son raccourci ne
// retrouve plus AJEAN, et n'a aucune raison de relancer le fichier // retrouve plus AJEAN, et n'a aucune raison de relancer le fichier
@@ -248,25 +248,54 @@ func replaceInstalled(target string) error {
// runningPIDs liste les processus qui exécutent exactement ce fichier. On compare // runningPIDs liste les processus qui exécutent exactement ce fichier. On compare
// le CHEMIN, pas le nom : tuer par nom d'image (« jean.exe ») emporterait aussi // le CHEMIN, pas le nom : tuer par nom d'image (« jean.exe ») emporterait aussi
// le processus courant et toute autre copie sans rapport. // le processus courant et toute autre copie sans rapport.
//
// La comparaison se fait ICI, sur des chemins canonisés, et non dans le script
// PowerShell : Windows expose le même fichier sous plusieurs écritures (forme
// courte 8.3 « ADMINI~1 » contre « Administrateur », casse variable, liens). Un
// simple -ieq entre chaînes rate alors l'instance en cours, et on remplace le
// binaire en croyant l'application arrêtée : elle continue de tourner en
// ancienne version, sans que rien ne l'indique. Constaté sur banc d'essai.
func runningPIDs(target string) []int { func runningPIDs(target string) []int {
ps := fmt.Sprintf(`$t=%s want := canonPath(target)
Get-Process -ErrorAction SilentlyContinue | ForEach-Object { ps := `Get-Process -ErrorAction SilentlyContinue | ForEach-Object {
try { if ($_.Path -ieq $t) { $_.Id } } catch { } try { if ($_.Path) { "$($_.Id)|$($_.Path)" } } catch { }
}`, psQuote(target)) }`
out, err := hideCmd(exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", ps)).Output() out, err := hideCmd(exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", ps)).Output()
if err != nil { if err != nil {
return nil return nil
} }
var pids []int var pids []int
self := os.Getpid() self := os.Getpid()
for _, line := range strings.Fields(string(out)) { for _, line := range strings.Split(string(out), "\n") {
if n, err := strconv.Atoi(line); err == nil && n != self { i := strings.IndexByte(line, '|')
if i < 0 {
continue
}
n, err := strconv.Atoi(strings.TrimSpace(line[:i]))
if err != nil || n == self {
continue
}
if canonPath(strings.TrimSpace(line[i+1:])) == want {
pids = append(pids, n) pids = append(pids, n)
} }
} }
return pids return pids
} }
// canonPath ramène un chemin Windows à une écriture unique et comparable :
// résolution des liens et de la forme courte 8.3 quand le fichier existe, puis
// minuscules (le système de fichiers est insensible à la casse).
func canonPath(p string) string {
p = strings.TrimSpace(p)
if p == "" {
return ""
}
if long, err := filepath.EvalSymlinks(p); err == nil {
p = long
}
return strings.ToLower(filepath.Clean(p))
}
// stopProcesses arrête les instances listées, puis laisse le port se libérer : // stopProcesses arrête les instances listées, puis laisse le port se libérer :
// sans cette attente, l'instance qu'on relance trouve :8090 encore occupé et se // sans cette attente, l'instance qu'on relance trouve :8090 encore occupé et se
// contente d'ouvrir le navigateur sur une application en train de mourir. // contente d'ouvrir le navigateur sur une application en train de mourir.