Migration : reecrire aussi les chemins sous leur forme JSON echappee

Trouve en rejouant la migration dans un bac a sable isole. Les fichiers
JSON du dossier de donnees (model_dirs.json, mcp.json, webprefs.json) sont
ecrits par json.Marshal : sous Windows chaque antislash y est DOUBLE. La
reecriture ne cherchait que la forme native et la forme a slashs, elle
laissait donc ces fichiers pointer vers l'ancien dossier.

Pire, la fonction d'echappement etait ecrite avec des litteraux bruts et
remplacait en fait un antislash par lui-meme : elle ne doublait rien. Le
remplacement natif matchait alors la forme JSON et produisait un fichier
invalide (« invalid character 'U' in string escape code »), c'est-a-dire la
liste des dossiers de modeles perdue. Un bug pire que celui qu'on corrigeait.

jsonEscapePath utilise desormais des litteraux entre guillemets, avec un
commentaire explicite sur le piege ("\\" est UN antislash, "\\\\" en fait
deux). Test dedie qui verifie que le chemin est bien reecrit ET que le JSON
reste analysable apres coup.

Verifie en bac a sable sur les trois ecritures simultanement (native,
slashs, JSON echappe) : les trois sont reecrites, le JSON se reparse et
pointe sur le nouveau dossier.
This commit is contained in:
nathaninline committed 2026-08-04 14:33:03 +02:00
1 parent 8a2b5c6687
commit a58103ff7e
2 files changed
+68 -3

No files matched your search

+24 -3
View File
@@ -129,13 +129,22 @@ func configFilesToRewrite(home string) []string {
// dossier a bougé, la ligne pointe dans le vide, et llama-server ne démarre plus.
// Ça concerne tous ceux qui ont fait un `llamacpp install`, donc le cas nominal.
//
// On couvre les deux écritures de séparateur, Windows acceptant indifféremment
// « \ » et « / » dans une même valeur. Best-effort par fichier : un fichier
// illisible est sauté sans compromettre les autres.
// Trois écritures d'un même chemin doivent être couvertes, sans quoi on croit
// avoir tout réécrit alors qu'il reste des références mortes :
//
// C:\ProgramData\jean forme native, dans config.env
// C:/ProgramData/jean Windows accepte les deux séparateurs dans une valeur
// C:\\ProgramData\\jean forme JSON, où l'antislash est échappé (model_dirs.json,
// mcp.json, webprefs.json sont écrits par json.Marshal)
//
// Best-effort par fichier : un fichier illisible est sauté sans compromettre
// les autres.
func rewriteHomeReferences(oldHome, newHome string) {
variants := [][2]string{{oldHome, newHome}}
if slash := filepath.ToSlash(oldHome); slash != oldHome {
variants = append(variants, [2]string{slash, filepath.ToSlash(newHome)})
// Forme JSON : chaque antislash est doublé.
variants = append(variants, [2]string{jsonEscapePath(oldHome), jsonEscapePath(newHome)})
}
for _, path := range configFilesToRewrite(newHome) {
b, err := os.ReadFile(path)
@@ -163,6 +172,18 @@ func rewriteHomeReferences(oldHome, newHome string) {
adoptLegacyStateFiles(newHome)
}
// jsonEscapePath renvoie l'écriture d'un chemin telle qu'elle apparaît DANS un
// fichier JSON : sous Windows, json.Marshal double chaque antislash. Le
// remplacement littéral doit donc chercher cette forme-là, sinon model_dirs.json
// et mcp.json gardent des chemins morts.
//
// Attention en modifiant : "\\" est UN antislash et "\\\\" en est deux. Une
// version antérieure utilisait des littéraux bruts et remplaçait en fait un
// antislash par lui-même, ce qui laissait passer le remplacement natif et
// produisait un JSON invalide (« invalid character 'U' in string escape code »),
// donc une liste de dossiers de modèles perdue. Couvert par un test.
func jsonEscapePath(p string) string { return strings.ReplaceAll(p, "\\", "\\\\") }
// adoptLegacyStateFiles reprend les fichiers d'état nommés d'après le service
// (jean.pid / jean.log → ajean.pid / ajean.log). Le fichier PID dit si le
// service tourne : ne pas le reprendre reviendrait à croire qu'il est arrêté et
+44
View File
@@ -1,6 +1,7 @@
package ajean
import (
"encoding/json"
"os"
"path/filepath"
"runtime"
@@ -172,3 +173,46 @@ func TestMigrateHomeAdoptsServiceStateFiles(t *testing.T) {
t.Fatalf("PID perdu: %q", b)
}
}
// Les fichiers JSON (model_dirs.json, mcp.json, webprefs.json) sont ecrits par
// json.Marshal : sous Windows chaque antislash y est DOUBLE. Chercher la forme
// native ne les trouve donc pas, et on croirait avoir tout reecrit en laissant
// des references mortes. Constate en bac a sable.
func TestMigrateHomeRewritesJSONEscapedPaths(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("l'echappement JSON des separateurs ne concerne que Windows")
}
root := t.TempDir()
legacy := filepath.Join(root, "jean")
target := filepath.Join(root, "ajean")
mkHome(t, legacy)
esc := jsonEscapePath
body := `{"dirs":["` + esc(filepath.Join(legacy, "models")) + `"]}`
if err := os.WriteFile(filepath.Join(legacy, "model_dirs.json"), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
migrateHome(target, legacy)
b, err := os.ReadFile(filepath.Join(target, "model_dirs.json"))
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(b), esc(legacy)) {
t.Errorf("reference JSON a l'ancien dossier non reecrite:\n%s", b)
}
if !strings.Contains(string(b), esc(target)) {
t.Errorf("le JSON ne pointe pas vers le nouveau dossier:\n%s", b)
}
// Le fichier doit rester du JSON valide apres reecriture.
var parsed struct {
Dirs []string `json:"dirs"`
}
if err := json.Unmarshal(b, &parsed); err != nil {
t.Fatalf("JSON casse par la reecriture: %v\n%s", err, b)
}
if len(parsed.Dirs) != 1 || !strings.HasPrefix(parsed.Dirs[0], target) {
t.Fatalf("chemin decode inattendu: %+v", parsed.Dirs)
}
}