mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
app de bureau : UN SEUL process proprietaire de la conversation
La conversation est un objet en memoire persiste dans conversation.json : deux process qui la servent = deux fils divergents qui s'ecrasent. Sous Linux le service jean-link sert deja les DEUX surfaces (UI locale :8090 + tunnel) pour cette raison ; le worker detache ajoute en v0.5.4 rompait ce principe sur Mac/Windows -> conversations differentes entre le local et app.ajean.link. L'app fait desormais tourner le tunnel dans son propre process, avec le MEME mux que l'UI locale, et supprime au demarrage tout worker detache rescape. Corollaire assume : l'acces distant vit tant que l'app est ouverte (sur un portable qui s'endort, c'etait deja le cas en pratique).
This commit is contained in:
1 parent
f8e856ed7c
commit
1118a15273
2 files changed
+135
-54
No files matched your search
+13
-16
@@ -39,27 +39,24 @@ func cmdApp(args []string) error {
|
||||
return openBrowser(url)
|
||||
}
|
||||
|
||||
// Sert l'UI en tâche de fond ; l'icône tray tient le premier plan.
|
||||
go func() { _ = http.Serve(ln, newWebMux()) }()
|
||||
// UN SEUL process propriétaire de la conversation, comme le service jean-link
|
||||
// sous Linux : l'app sert l'UI locale ET le tunnel avec le MÊME mux. Deux
|
||||
// process qui servent la conversation (objet en mémoire persisté dans
|
||||
// conversation.json) donnent deux fils divergents entre le local et
|
||||
// app.ajean.link — le bug qu'on a vécu avec le worker détaché.
|
||||
appOwnsLink = true
|
||||
appWebMux = newWebMux()
|
||||
go func() { _ = http.Serve(ln, appWebMux) }()
|
||||
|
||||
// Accès distant déjà configuré ? On relance le tunnel. Sous Linux systemd s'en
|
||||
// charge au boot, mais sur un poste de bureau (macOS/Windows) personne ne le
|
||||
// fait : sans ça le token reste enregistré et le panneau affiche « service
|
||||
// arrêté » à chaque démarrage de l'app.
|
||||
go func() {
|
||||
if readLinkToken() == "" {
|
||||
return
|
||||
}
|
||||
// Worker rescapé d'une AUTRE copie de l'app (ancienne version, copie
|
||||
// translocatée…) : il tourne toujours — le tunnel est détaché exprès — et
|
||||
// continue de servir l'UI distante avec du code périmé. On le remplace.
|
||||
if linkWorkerIsStale() {
|
||||
_ = linkServiceCtl("restart")
|
||||
return
|
||||
}
|
||||
if !linkServiceActive() {
|
||||
_ = linkServiceCtl("start")
|
||||
}
|
||||
// Worker détaché rescapé (version antérieure, autre copie de l'app) : il
|
||||
// possède SA propre conversation et écrase la nôtre. On le supprime avant
|
||||
// de prendre la main.
|
||||
killForeignLinkWorker()
|
||||
startAppLink(appWebMux)
|
||||
}()
|
||||
|
||||
sp := showSplash("Lancement de Jean en cours…")
|
||||
|
||||
+122
-38
@@ -31,6 +31,7 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
@@ -260,6 +261,103 @@ func runLinkForeground() error {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tunnel DANS le process de l'app (macOS/Windows).
|
||||
//
|
||||
// Le modèle Linux est : UN SEUL process possède la conversation et sert les deux
|
||||
// surfaces (UI locale :8090 + tunnel). C'est ce qui rend le fil identique en
|
||||
// local et sur app.ajean.link — la conversation est un objet EN MÉMOIRE (voir
|
||||
// chat_conversation.go), simplement persisté sur disque ; deux process qui la
|
||||
// servent = deux fils divergents qui s'écrasent dans conversation.json.
|
||||
//
|
||||
// Sur un poste de bureau, l'app EST ce process propriétaire : elle sert déjà
|
||||
// :8090, donc elle fait aussi tourner le tunnel elle-même au lieu de déléguer à
|
||||
// un worker détaché. L'accès distant vit donc aussi longtemps que Jean.app est
|
||||
// ouverte — sur un portable qui s'endort, c'est de toute façon la réalité.
|
||||
|
||||
var appLink struct {
|
||||
mu sync.Mutex
|
||||
running bool
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
// appOwnsLink : vrai dans le process de l'app de bureau, qui pilote le tunnel
|
||||
// en interne. linkServiceCtl s'y adapte pour ne PAS lancer de worker concurrent.
|
||||
// appWebMux est le mux servi par l'app — réutilisé pour le tunnel afin que les
|
||||
// deux surfaces partagent la même conversation.
|
||||
var (
|
||||
appOwnsLink bool
|
||||
appWebMux *http.ServeMux
|
||||
)
|
||||
|
||||
// startAppLink démarre la boucle de lien dans ce process, en servant le mux de
|
||||
// l'app (donc la MÊME conversation que l'UI locale). Idempotent.
|
||||
func startAppLink(mux *http.ServeMux) {
|
||||
appLink.mu.Lock()
|
||||
defer appLink.mu.Unlock()
|
||||
if appLink.running {
|
||||
return
|
||||
}
|
||||
token := readLinkToken()
|
||||
if token == "" {
|
||||
return
|
||||
}
|
||||
stop := make(chan struct{})
|
||||
appLink.running, appLink.stop = true, stop
|
||||
handler := newLinkHandler(mux)
|
||||
oaiTLS := oaiTLSConfig()
|
||||
go func() {
|
||||
backoff := time.Second
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
default:
|
||||
}
|
||||
_ = runLinkSession(token, handler, oaiTLS)
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case <-time.After(backoff):
|
||||
}
|
||||
if backoff < 30*time.Second {
|
||||
backoff *= 2
|
||||
if backoff > 30*time.Second {
|
||||
backoff = 30 * time.Second
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// stopAppLink arrête la boucle interne (la session en cours se termine d'elle-même).
|
||||
func stopAppLink() {
|
||||
appLink.mu.Lock()
|
||||
defer appLink.mu.Unlock()
|
||||
if !appLink.running {
|
||||
return
|
||||
}
|
||||
close(appLink.stop)
|
||||
appLink.running = false
|
||||
}
|
||||
|
||||
func appLinkRunning() bool {
|
||||
appLink.mu.Lock()
|
||||
defer appLink.mu.Unlock()
|
||||
return appLink.running
|
||||
}
|
||||
|
||||
// killForeignLinkWorker tue un worker de lien détaché encore en vie (laissé par
|
||||
// une version antérieure ou une autre copie de l'app). Indispensable AVANT que
|
||||
// l'app ne prenne la main : sinon deux process servent la même conversation et
|
||||
// les fils divergent entre l'UI locale et app.ajean.link.
|
||||
func killForeignLinkWorker() {
|
||||
if pid := linkUserPID(); pid > 0 {
|
||||
killTree(pid)
|
||||
_ = os.Remove(linkPIDPath())
|
||||
}
|
||||
}
|
||||
|
||||
// serveLocalWebMux sert l'UI web locale sur :8090 avec le mux fourni — le MÊME
|
||||
// que celui exposé dans le tunnel. Le process jean-link devient ainsi l'unique
|
||||
// propriétaire de la conversation, partagée entre l'accès local et l'accès
|
||||
@@ -289,6 +387,24 @@ func linkLogPath() string { return filepath.Join(JeanHome(), linkServiceName+".l
|
||||
// composait jamais le tunnel.
|
||||
func linkServiceCtl(action string) error {
|
||||
if runtime.GOOS != "linux" {
|
||||
// Dans l'app de bureau, le tunnel tourne DANS ce process (conversation
|
||||
// unique) : on ne lance surtout pas un worker séparé.
|
||||
if appOwnsLink {
|
||||
switch action {
|
||||
case "stop":
|
||||
stopAppLink()
|
||||
case "start", "restart":
|
||||
stopAppLink()
|
||||
killForeignLinkWorker()
|
||||
startAppLink(appWebMux)
|
||||
if !appLinkRunning() {
|
||||
return fmt.Errorf("tunnel non démarré (clé de liaison absente ?)")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("action inconnue: %s", action)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return linkUserSvcCtl(action)
|
||||
}
|
||||
bin, pre := "systemctl", []string{}
|
||||
@@ -313,47 +429,15 @@ func linkServiceCtl(action string) error {
|
||||
// Linux, processus suivi par fichier PID ailleurs).
|
||||
func linkServiceActive() bool {
|
||||
if runtime.GOOS != "linux" {
|
||||
if appOwnsLink {
|
||||
return appLinkRunning()
|
||||
}
|
||||
return linkUserPID() > 0
|
||||
}
|
||||
out, _ := exec.Command("systemctl", "is-active", linkServiceName).Output()
|
||||
return strings.TrimSpace(string(out)) == "active"
|
||||
}
|
||||
|
||||
// linkUserExe renvoie le binaire qui a lancé le worker de lien en cours (2e ligne
|
||||
// du fichier PID), "" si inconnu. Sert à repérer un worker rescapé d'une ANCIENNE
|
||||
// copie de l'app : le tunnel étant détaché exprès pour survivre à la fermeture de
|
||||
// Jean.app, un worker lancé depuis une copie translocatée (ou simplement une
|
||||
// version précédente) continue de servir l'UI distante avec du code périmé —
|
||||
// symptôme vécu : l'alerte App Translocation visible sur app.ajean.link alors que
|
||||
// l'app locale, elle, tourne bien depuis /Applications.
|
||||
func linkUserExe() string {
|
||||
b, err := os.ReadFile(linkPIDPath())
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
lines := strings.SplitN(strings.TrimSpace(string(b)), "\n", 2)
|
||||
if len(lines) < 2 {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(lines[1])
|
||||
}
|
||||
|
||||
// linkWorkerIsStale : un worker tourne, mais depuis un autre binaire que nous.
|
||||
func linkWorkerIsStale() bool {
|
||||
if runtime.GOOS == "linux" || linkUserPID() == 0 {
|
||||
return false
|
||||
}
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if p, err := filepath.EvalSymlinks(exe); err == nil {
|
||||
exe = p
|
||||
}
|
||||
running := linkUserExe()
|
||||
return running != "" && running != exe
|
||||
}
|
||||
|
||||
// linkUserPID renvoie le PID du worker de lien s'il tourne vraiment, 0 sinon
|
||||
// (fichier absent, illisible, ou process mort → on nettoie le fichier obsolète).
|
||||
func linkUserPID() int {
|
||||
@@ -400,7 +484,7 @@ func linkUserSvcCtl(action string) error {
|
||||
return err
|
||||
}
|
||||
if p, err := filepath.EvalSymlinks(self); err == nil {
|
||||
self = p // même forme que la comparaison de linkWorkerIsStale
|
||||
self = p
|
||||
}
|
||||
if err := os.MkdirAll(JeanHome(), 0o755); err != nil {
|
||||
return err
|
||||
@@ -417,8 +501,8 @@ func linkUserSvcCtl(action string) error {
|
||||
return fmt.Errorf("démarrage de « jean link serve »: %w", err)
|
||||
}
|
||||
pid := cmd.Process.Pid
|
||||
// PID + binaire d'origine : la 2e ligne permet de détecter plus tard un worker
|
||||
// rescapé d'une ancienne copie de l'app (voir linkWorkerIsStale).
|
||||
// PID + binaire d'origine : la 2e ligne dit QUELLE copie de l'app a lancé ce
|
||||
// worker — précieux pour diagnostiquer un process rescapé d'une autre version.
|
||||
if err := os.WriteFile(linkPIDPath(), []byte(strconv.Itoa(pid)+"\n"+self+"\n"), 0o644); err != nil {
|
||||
return fmt.Errorf("écriture du PID: %w", err)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user