Journal d'installation : la progression de Strata visible, plus de lignes en double

- Les barres de progression qui se redessinent avec \r (téléchargement du
  modèle par l'installeur de Strata) n'apparaissaient jamais : le journal ne
  découpait que sur \n et restait figé pendant tout le téléchargement. Une
  version toutes les 10 s est montrée ; \r\n reste une fin de ligne.
- Le suivi du job relançait une requête chaque seconde sans attendre la
  précédente : quand le serveur était occupé, deux réponses ajoutaient les
  mêmes lignes au journal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5.5 committed 2026-10-07 10:54:09 +02:00
1 parent 5016396998
commit ac6d8ad144
4 files changed
+82 -6

No files matched your search

+29 -4
View File
@@ -44,20 +44,45 @@ func emitBuildLine(line string) {
// Sert à téer la sortie des commandes de runStep quand un sink est actif.
// (mutex : stdout et stderr d'une même commande peuvent écrire en parallèle)
type sinkWriter struct {
mu sync.Mutex
buf []byte
mu sync.Mutex
buf []byte
lastProg time.Time // dernière ligne de progression (\r) émise
}
// sinkProgressEvery : une barre de progression (tqdm, téléchargements de
// l'installeur de Strata) se redessine sur place avec \r, sans jamais finir sa
// ligne. On en montre une version toutes les 10 s : sans elle, un téléchargement
// de 76 Go laissait le journal figé pendant une heure ; toutes, elles le noieraient.
const sinkProgressEvery = 10 * time.Second
func (s *sinkWriter) Write(p []byte) (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
s.buf = append(s.buf, p...)
for {
i := strings.IndexByte(string(s.buf), '\n')
i := strings.IndexAny(string(s.buf), "\r\n")
if i < 0 {
break
}
emitBuildLine(strings.TrimRight(string(s.buf[:i]), "\r"))
if s.buf[i] == '\n' {
emitBuildLine(strings.TrimRight(string(s.buf[:i]), "\r"))
s.buf = s.buf[i+1:]
continue
}
// \r : fin de ligne Windows (\r\n) ou barre qui se redessine. Le \n peut
// arriver dans l'écriture suivante : on attend de le savoir.
if i+1 >= len(s.buf) {
break
}
if s.buf[i+1] == '\n' {
emitBuildLine(string(s.buf[:i]))
s.buf = s.buf[i+2:]
continue
}
if prog := strings.TrimSpace(string(s.buf[:i])); prog != "" && time.Since(s.lastProg) >= sinkProgressEvery {
s.lastProg = time.Now()
emitBuildLine(prog)
}
s.buf = s.buf[i+1:]
}
return len(p), nil
+29
View File
@@ -0,0 +1,29 @@
package loki
import (
"reflect"
"testing"
"time"
)
// Une barre de progression (\r sans \n) doit apparaître dans le journal, sans
// le noyer ; \r\n reste une fin de ligne ordinaire, même coupé entre deux écritures.
func TestSinkWriterProgression(t *testing.T) {
var got []string
setBuildSink(func(l string) { got = append(got, l) })
defer setBuildSink(nil)
s := &sinkWriter{}
s.Write([]byte("Step 5\r"))
s.Write([]byte("\n modèle : 1 / 76 GB\r modèle : 2 / 76 GB\r"))
s.Write([]byte(" modèle : 3 / 76 GB\r"))
s.lastProg = time.Now().Add(-time.Minute) // 10 s écoulées
s.Write([]byte(" modèle : 40 / 76 GB\r[ok] fini\n"))
// « 3 » finissait sa propre écriture : il attendait de savoir si un saut de
// ligne suivait, et part donc avec l'écriture suivante.
want := []string{"Step 5", "modèle : 1 / 76 GB", "modèle : 3 / 76 GB", "[ok] fini"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("lignes %q, attendu %q", got, want)
}
}
+12 -1
View File
@@ -9097,10 +9097,21 @@ function lcStartPolling(){
lcPollJob();
}
// Une seule requête à la fois : l'intervalle d'une seconde relançait un appel
// avant la réponse du précédent quand le serveur était occupé (téléchargement,
// accès distant), et les deux ajoutaient les MÊMES lignes au journal.
let lcPollInflight = false;
async function lcPollJob(quiet){
if(lcPollInflight) return;
lcPollInflight = true;
const from = lcLogNext;
let j;
try{ j = await jget('/api/llamacpp/job?from='+lcLogNext); }catch(_){ return; }
try{ j = await jget('/api/llamacpp/job?from='+from); }catch(_){ return; }
finally{ lcPollInflight = false; }
if(!j.exists) return;
// Le journal a été remis à zéro ou avancé entre-temps (nouveau job) : ces
// lignes ne suivent plus ce qui est affiché.
if(from !== lcLogNext) return;
const phaseEl = document.getElementById('lc-job-phase');
if(j.lines && j.lines.length){
const pre = document.getElementById('lc-log');
+12 -1
View File
@@ -453,10 +453,21 @@ function lcStartPolling(){
lcPollJob();
}
// Une seule requête à la fois : l'intervalle d'une seconde relançait un appel
// avant la réponse du précédent quand le serveur était occupé (téléchargement,
// accès distant), et les deux ajoutaient les MÊMES lignes au journal.
let lcPollInflight = false;
async function lcPollJob(quiet){
if(lcPollInflight) return;
lcPollInflight = true;
const from = lcLogNext;
let j;
try{ j = await jget('/api/llamacpp/job?from='+lcLogNext); }catch(_){ return; }
try{ j = await jget('/api/llamacpp/job?from='+from); }catch(_){ return; }
finally{ lcPollInflight = false; }
if(!j.exists) return;
// Le journal a été remis à zéro ou avancé entre-temps (nouveau job) : ces
// lignes ne suivent plus ce qui est affiché.
if(from !== lcLogNext) return;
const phaseEl = document.getElementById('lc-job-phase');
if(j.lines && j.lines.length){
const pre = document.getElementById('lc-log');