Windows : version de fichier, fenetres de console et remplacement d'un meme build

Trois defauts signales a l'usage.

1. Le binaire 0.8.0 se DECLARAIT 0.7.13 a Windows : j'avais bump la constante Go
   sans toucher a cmd/ajean/versioninfo.json ni regenerer les .syso. D'ou le
   « version en cours 0.7.13 / ce fichier 0.8.0 » a chaque relance du fichier
   telecharge. Ressource remise a 0.8.0, .syso regeneres, et un test garde
   desormais les deux en phase.

2. Des fenetres de console apparaissaient : la relance apres installation et
   l'ouverture du navigateur creaient un enfant sans masquer sa fenetre, ce qui
   se voit maintenant que le binaire est en sous-systeme console. Les deux
   passent par hideCmd. Les raccourcis DEJA poses recoivent en plus le demarrage
   minimise, qui ne s'appliquait qu'aux nouveaux.

3. Deux builds de meme version n'etaient jamais remplaces : relancer un
   telechargement plus recent d'une preversion republiee demarrait simplement
   l'ancienne copie. On compare desormais les fichiers (taille puis SHA-256) et
   non les seuls numeros.

Au passage, l'icone est epinglee sur le raccourci : Windows garde parfois en
cache celle d'un binaire remplace et affiche un carre blanc.
This commit is contained in:
nathaninline committed 2026-08-07 22:30:01 +02:00
1 parent 6bf304573c
commit 68d6d2032a
6 files changed
+118 -8

No files matched your search

Binary file not shown.
Binary file not shown.
+5 -5
View File
@@ -2,14 +2,14 @@
"FixedFileInfo": {
"FileVersion": {
"Major": 0,
"Minor": 7,
"Patch": 13,
"Minor": 8,
"Patch": 0,
"Build": 0
},
"ProductVersion": {
"Major": 0,
"Minor": 7,
"Patch": 13,
"Minor": 8,
"Patch": 0,
"Build": 0
},
"FileFlagsMask": "3f",
@@ -25,7 +25,7 @@
"LegalCopyright": "Copyright (c) 2026 AJEAN contributors. MIT License.",
"OriginalFilename": "ajean.exe",
"ProductName": "AJEAN",
"ProductVersion": "0.7.13",
"ProductVersion": "0.8.0",
"Comments": "https://github.com/nathaninline/ajean — projet open source (MIT)"
},
"VarFileInfo": {
+71 -2
View File
@@ -3,7 +3,10 @@
package ajean
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net"
"os"
"os/exec"
@@ -127,6 +130,51 @@ func appFirstRun() bool {
return launch(target)
}
// exeSelf renvoie le chemin réel du binaire en cours (liens résolus).
func exeSelf() string {
p, err := os.Executable()
if err != nil {
return ""
}
if r, err := filepath.EvalSymlinks(p); err == nil {
return r
}
return p
}
// sameFile dit si deux fichiers ont exactement le même contenu. Taille d'abord :
// deux binaires qui diffèrent ont presque toujours des tailles différentes, et
// c'est immédiat, là où le hachage lit 15 Mo.
func sameFile(a, b string) bool {
if a == "" || b == "" {
return false
}
fa, err1 := os.Stat(a)
fb, err2 := os.Stat(b)
if err1 != nil || err2 != nil {
return false
}
if fa.Size() != fb.Size() {
return false
}
ha, err1 := hashFile(a)
hb, err2 := hashFile(b)
return err1 == nil && err2 == nil && ha == hb
}
func hashFile(p string) (string, error) {
f, err := os.Open(p)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
// launch démarre la copie installée et demande à l'appelant de rendre la main.
// Renvoie false si le lancement échoue, auquel cas l'exécutable courant prend le
// relais : mieux vaut démarrer depuis le mauvais dossier que pas du tout.
@@ -136,7 +184,11 @@ func launch(target string) bool {
// double-cliqué ? » — en regardant qui partage notre console. Un processus
// lancé sans console du tout (relance après mise à jour) ne peut pas y
// répondre, et se serait retrouvé à afficher l'aide au lieu de démarrer.
cmd := exec.Command(target, "app")
// hideCmd : le binaire est en sous-système console, donc Windows allouerait
// une console à l'enfant et on verrait une fenêtre noire apparaître. Comme
// « app » est passé explicitement, l'absence de console ne gêne pas la
// détection du mode application.
cmd := hideCmd(exec.Command(target, "app"))
cmd.Dir = filepath.Dir(target)
return cmd.Start() == nil
}
@@ -156,6 +208,14 @@ func runAsInstaller(target string) bool {
if installed != "" {
cmp = semver.Compare(ensureV(Version), ensureV(installed))
}
// Même numéro de version ne veut pas dire même binaire : une préversion
// republiée en garde le sien. Sans cette comparaison des fichiers, relancer
// le nouveau téléchargement démarrait simplement l'ancienne copie installée,
// sans rien remplacer et sans rien dire. On traite alors ce cas comme une
// mise à jour ordinaire.
if cmp == 0 && !sameFile(exeSelf(), target) {
cmp = 1
}
running := runningPIDs(target)
switch {
@@ -396,11 +456,19 @@ foreach ($d in @($progs, [Environment]::GetFolderPath('Desktop'))) {
if (Test-Path $lnk) {
try {
$s=$w.CreateShortcut($lnk)
$chg=$false
if ($s.TargetPath -and $s.TargetPath -ne $t -and (Test-Path $s.TargetPath)) {
$s.TargetPath=$t
$s.WorkingDirectory=(Split-Path $t)
$s.Save()
$chg=$true
}
# Les raccourcis posés avant le passage en sous-système console n'ont pas
# le démarrage minimisé, et laissent donc apparaître une fenêtre noire.
if ($s.WindowStyle -ne 7) { $s.WindowStyle=7; $chg=$true }
# Icône épinglée sur l'exe : sans ça, Windows garde parfois en cache
# l'icône d'un binaire remplacé et affiche un carré blanc.
if ($s.IconLocation -ne "$t,0") { $s.IconLocation="$t,0"; $chg=$true }
if ($chg) { $s.Save() }
} catch {}
$done+=$d; continue
}
@@ -413,6 +481,7 @@ foreach ($d in @($progs, [Environment]::GetFolderPath('Desktop'))) {
# une console au lancement. AJEAN la referme aussitôt, mais demander un
# démarrage minimisé garantit qu'elle n'est jamais peinte à l'écran.
$s.WindowStyle=7
$s.IconLocation="$t,0"
$s.Save()
$done+=$d
} catch {}
+1 -1
View File
@@ -63,7 +63,7 @@ func defaultEditor() string { return "notepad" }
// openBrowser ouvre l'URL dans le navigateur par défaut. rundll32 évite les
// pièges de quoting de `cmd /c start`.
func openBrowser(url string) error {
return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
return hideCmd(exec.Command("rundll32", "url.dll,FileProtocolHandler", url)).Start()
}
// hideCmd empêche une commande externe d'ouvrir une fenêtre de console
+41
View File
@@ -0,0 +1,41 @@
package ajean
import (
"encoding/json"
"fmt"
"os"
"testing"
)
// La ressource de version Windows (cmd/ajean/versioninfo.json) doit suivre la
// constante Version. Elles ont divergé en 0.8.0 : la constante était à jour, la
// ressource restait à 0.7.13. Conséquence peu lisible — le binaire installé se
// DÉCLARAIT 0.7.13 auprès de Windows, si bien que relancer le fichier téléchargé
// proposait indéfiniment « appliquer la mise à jour ? » sur une version déjà
// installée. Ce test rend l'oubli impossible.
func TestVersionInfoWindowsSuitLaVersion(t *testing.T) {
b, err := os.ReadFile("../../cmd/ajean/versioninfo.json")
if err != nil {
t.Fatal(err)
}
var vi struct {
FixedFileInfo struct {
FileVersion struct{ Major, Minor, Patch int }
ProductVersion struct{ Major, Minor, Patch int }
}
StringFileInfo struct{ ProductVersion string }
}
if err := json.Unmarshal(b, &vi); err != nil {
t.Fatal(err)
}
f := vi.FixedFileInfo.FileVersion
for name, got := range map[string]string{
"FixedFileInfo.FileVersion": fmt.Sprintf("%d.%d.%d", f.Major, f.Minor, f.Patch),
"FixedFileInfo.ProductVersion": fmt.Sprintf("%d.%d.%d", vi.FixedFileInfo.ProductVersion.Major, vi.FixedFileInfo.ProductVersion.Minor, vi.FixedFileInfo.ProductVersion.Patch),
"StringFileInfo.ProductVersion": vi.StringFileInfo.ProductVersion,
} {
if got != Version {
t.Errorf("%s = %s, attendu %s — pense à `go generate ./cmd/ajean` après un bump", name, got, Version)
}
}
}