mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Windows : sous-systeme console, pour qu'ajean se comporte en vraie commande
Dans un terminal, cmd rendait la main avant qu'ajean n'ait rien ecrit : l'invite se reaffichait puis la sortie arrivait par-dessus. Windows n'attend jamais la fin d'un programme en sous-systeme graphique. La meme cause empechait les redirections et les tubes, et rendait 'ajean chat' inutilisable (le shell et ajean se disputaient l'entree). Le binaire passe donc en sous-systeme console. La fenetre noire au double-clic, que le sous-systeme graphique evitait, est traitee autrement : quand aucun autre processus ne partage notre console, c'est que Windows l'a creee pour nous, donc que personne ne nous a lances depuis un terminal. On la masque et on la libere. Les raccourcis demandent en plus un demarrage minimise. La relance interne (apres mise a jour) passe desormais 'app' explicitement : la detection ne repond qu'a la question du double-clic, et un processus sans console du tout ne doit pas avoir a y repondre. Verifie : ordre d'affichage correct en session cmd, redirection et tube fonctionnels, 'ajean' sans argument affiche l'aide, et aucune fenetre console visible au lancement facon double-clic.
This commit is contained in:
1 parent
922b7a6862
commit
aa965227fc
4 files changed
+68
-85
No files matched your search
@@ -107,9 +107,10 @@ jobs:
|
||||
windows/amd64:ajean-windows.exe windows/arm64:ajean-windows-arm.exe; do
|
||||
target=${t%%:*}; name=${t#*:}
|
||||
os=${target%/*}; arch=${target#*/}; ldflags="-s -w"
|
||||
# Windows : sous-système GUI → aucune console noire au double-clic
|
||||
# (l'app se rattache à la console du terminal parent si lancée en CLI).
|
||||
[ "$os" = windows ] && ldflags="$ldflags -H=windowsgui"
|
||||
# Windows : sous-système CONSOLE, pour que `cmd` attende la fin du
|
||||
# programme et que redirections, tubes et commandes interactives
|
||||
# fonctionnent. La console créée au double-clic est refermée par le
|
||||
# programme lui-même (voir setupConsole, sys_console_windows.go).
|
||||
GOOS=$os GOARCH=$arch CGO_ENABLED=0 \
|
||||
go build -trimpath -ldflags="$ldflags" -o "dist/$name" ./cmd/ajean
|
||||
done
|
||||
|
||||
+4
-2
@@ -42,9 +42,11 @@ Séparer les deux services garde son intérêt : redémarrer l'interface est ins
|
||||
|
||||
**« Quitter » arrête vraiment tout.** Le moteur tourne dans un processus détaché, qui survit volontairement à la fermeture de l'interface pour garder le modèle chargé entre deux ouvertures. Mais après un « Quitter » depuis la zone de notification, plus rien ne le pilotait et il conservait des dizaines de gigaoctets de mémoire, sans la moindre fenêtre pour l'expliquer. Quitter décharge désormais le modèle. Ailleurs, rien ne change : sous Linux et macOS le moteur appartient à systemd ou launchd, et fermer une interface n'a pas à arrêter un service système.
|
||||
|
||||
**La redirection et les tubes fonctionnent.** `ajean where > fichier.txt` n'écrivait rien dans le fichier, et `ajean help | findstr quelquechose` ne transmettait rien : tout partait sur la console. Le programme réouvrait sa sortie sur la console pour avoir où écrire, et écrasait ce faisant ce que le shell lui avait branché.
|
||||
**AJEAN se comporte enfin comme une vraie commande.** Dans un terminal, `ajean` rendait la main avant d'avoir écrit quoi que ce soit : l'invite se réaffichait, et la sortie du programme arrivait par-dessus, comme si la commande avait été tapée deux fois.
|
||||
|
||||
À savoir : lancé depuis `cmd`, AJEAN rend la main immédiatement et son affichage arrive après la nouvelle invite. C'est le prix du sous-système graphique, celui-là même qui évite la fenêtre noire au double-clic.
|
||||
La même cause empêchait `ajean where > fichier.txt` d'écrire dans le fichier, `ajean help | findstr ...` de transmettre quoi que ce soit, et rendait `ajean chat` inutilisable depuis un terminal, le shell et AJEAN se disputant le clavier. Le binaire était compilé en sous-système graphique, et Windows n'attend jamais la fin d'un programme de ce type.
|
||||
|
||||
Il est désormais en sous-système console. La contrepartie, une fenêtre noire au double-clic, est traitée par le programme lui-même : constatant qu'aucun terminal ne l'a lancé, il referme aussitôt la console que Windows lui a allouée. Les raccourcis posés à l'installation demandent en plus un démarrage minimisé, si bien qu'elle n'est jamais peinte à l'écran.
|
||||
|
||||
**Plus de question au premier lancement.** Le double-clic demandait s'il fallait installer AJEAN ou seulement le lancer. La question n'avait qu'une réponse utile : rester à l'emplacement du fichier téléchargé ne donne pas une installation exploitable, sans raccourci, sans rien dans le PATH, et avec une application qui disparaît le jour où l'on vide ses téléchargements. L'installation se fait donc directement, et le message qui suit dit ce qui a été fait au lieu de demander une permission.
|
||||
|
||||
|
||||
@@ -4,96 +4,67 @@ package ajean
|
||||
|
||||
// sys_console_windows.go — gestion de la console sous Windows.
|
||||
//
|
||||
// Le binaire est compilé en sous-système GUI (`-H=windowsgui`) : au double-clic,
|
||||
// Windows n'alloue AUCUNE console → plus jamais de « fenêtre noire de terminal »
|
||||
// qui s'ouvre et se ferme. En contrepartie, un binaire GUI lancé depuis un
|
||||
// terminal (cmd/PowerShell) n'écrit nulle part par défaut : on se rattache alors
|
||||
// explicitement à la console du parent et on réouvre stdout/stderr/stdin dessus,
|
||||
// pour que l'usage CLI (`ajean web`, `ajean status`, …) reste lisible.
|
||||
// Le binaire est compilé en sous-système CONSOLE. C'est ce qui fait que `cmd`
|
||||
// ATTEND la fin du programme : avec un binaire graphique, il rendait la main
|
||||
// aussitôt et réaffichait son invite AVANT la sortie d'AJEAN, ce qui donnait
|
||||
//
|
||||
// C:\Users\Nathan>ajean
|
||||
//
|
||||
// C:\Users\Nathan>ajean 0.8.0 — manager llama.cpp + UI web
|
||||
//
|
||||
// La même cause cassait la redirection et les tubes, et rendait `ajean chat`
|
||||
// inutilisable depuis un terminal (le shell et AJEAN se disputaient l'entrée).
|
||||
//
|
||||
// EN CONTREPARTIE, Windows alloue une console à tout lancement sans terminal —
|
||||
// un double-clic. On la referme alors immédiatement : voir setupConsole. Les
|
||||
// raccourcis créés par l'installation demandent en plus un démarrage minimisé,
|
||||
// si bien que cette console n'est jamais peinte à l'écran.
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// setupConsole rattache le process à la console de son parent si elle existe.
|
||||
// Retourne true si on dispose d'une console (→ usage CLI), false sinon (double-
|
||||
// clic sans console → expérience « application »).
|
||||
// setupConsole décide dans quel monde on tourne et renvoie true en usage CLI.
|
||||
//
|
||||
// Le signal est le nombre de processus rattachés à notre console : si nous
|
||||
// sommes le SEUL, c'est que Windows l'a créée pour nous, donc qu'aucun terminal
|
||||
// ne nous a lancés. On la fait disparaître et on bascule en mode application.
|
||||
// Un shell rattaché (cmd, PowerShell, Windows Terminal) en fait au moins deux.
|
||||
func setupConsole() bool {
|
||||
// Relevé AVANT le rattachement, et c'est tout l'enjeu : AttachConsole
|
||||
// REMPLACE lui-même les handles standard du processus par ceux de la
|
||||
// console. Interrogé après, on ne voit donc plus jamais qu'une console, et
|
||||
// la trace de la redirection posée par le shell est déjà perdue.
|
||||
outRedirected := stdRedirected(stdOutputHandle)
|
||||
errRedirected := stdRedirected(stdErrorHandle)
|
||||
inRedirected := stdRedirected(stdInputHandle)
|
||||
|
||||
const attachParentProcess = ^uintptr(0) // (DWORD)-1 = ATTACH_PARENT_PROCESS
|
||||
k := syscall.NewLazyDLL("kernel32.dll")
|
||||
r, _, _ := k.NewProc("AttachConsole").Call(attachParentProcess)
|
||||
if r == 0 {
|
||||
return false // aucune console parente = lancé par double-clic (mode app)
|
||||
}
|
||||
// Réouvre les flux standard sur la console fraîchement rattachée : sans ça,
|
||||
// os.Stdout/Stderr pointent sur des handles invalides (sous-système GUI).
|
||||
|
||||
// Combien de processus partagent notre console ? Nous seul (1) = personne ne
|
||||
// nous a lancés depuis un terminal, Windows a créé cette console pour nous :
|
||||
// c'est un double-clic. Un shell rattaché en fait au moins deux.
|
||||
//
|
||||
// MAIS seulement ceux que le shell n'a pas déjà branchés ailleurs. Réouvrir
|
||||
// systématiquement sur CONOUT$ écrasait la redirection : « ajean where >
|
||||
// fichier.txt » n'écrivait rien dans le fichier, et « ajean where | findstr »
|
||||
// ne transmettait rien au tube — tout partait sur la console. On ne touche
|
||||
// donc qu'aux flux réellement inutilisables.
|
||||
if !outRedirected {
|
||||
if con, err := os.OpenFile("CONOUT$", os.O_WRONLY, 0); err == nil {
|
||||
os.Stdout = con
|
||||
}
|
||||
}
|
||||
if !errRedirected {
|
||||
if con, err := os.OpenFile("CONOUT$", os.O_WRONLY, 0); err == nil {
|
||||
os.Stderr = con
|
||||
}
|
||||
}
|
||||
if !inRedirected {
|
||||
if cin, err := os.OpenFile("CONIN$", os.O_RDONLY, 0); err == nil {
|
||||
os.Stdin = cin
|
||||
// Le compte vaut 0 quand il n'y a aucune console (parent détaché). On répond
|
||||
// alors « CLI », le cas contraire étant couvert autrement : les relances
|
||||
// internes passent « app » explicitement (voir launch, sys_firstrun_windows.go).
|
||||
// Mieux vaut afficher une aide inutile que démarrer un serveur en silence.
|
||||
var pids [8]uint32
|
||||
n, _, _ := k.NewProc("GetConsoleProcessList").Call(
|
||||
uintptr(unsafe.Pointer(&pids[0])), uintptr(len(pids)))
|
||||
if n == 1 {
|
||||
if hwnd, _, _ := k.NewProc("GetConsoleWindow").Call(); hwnd != 0 {
|
||||
// Masquer AVANT de libérer : sinon la fenêtre reste peinte le temps
|
||||
// que Windows la détruise.
|
||||
const swHide = 0
|
||||
_, _, _ = k.NewProc("ShowWindow").Call(hwnd, swHide)
|
||||
}
|
||||
_, _, _ = k.NewProc("FreeConsole").Call()
|
||||
return false
|
||||
}
|
||||
|
||||
// Lancé depuis un terminal : les flux standard sont déjà ceux du shell,
|
||||
// redirections et tubes compris. Il n'y a rien à rebrancher, seulement à
|
||||
// régler l'encodage et les couleurs.
|
||||
configureConsole()
|
||||
return true
|
||||
}
|
||||
|
||||
// Handles standard, tels que les nomme GetStdHandle.
|
||||
const (
|
||||
stdInputHandle = ^uintptr(9) // -10
|
||||
stdOutputHandle = ^uintptr(10) // -11
|
||||
stdErrorHandle = ^uintptr(11) // -12
|
||||
)
|
||||
|
||||
// stdRedirected dit si un flux standard est DÉJÀ branché sur un fichier ou un
|
||||
// tube par le processus parent. Dans ce cas il ne faut surtout pas le remplacer
|
||||
// par la console : c'est ce qui cassait « > fichier » et « | commande ».
|
||||
//
|
||||
// Un flux de type « caractère » (la console elle-même) ou inutilisable n'est pas
|
||||
// considéré comme redirigé : on le réouvre alors sur CONOUT$/CONIN$, ce dont un
|
||||
// binaire GUI a besoin pour écrire quelque part.
|
||||
func stdRedirected(which uintptr) bool {
|
||||
const (
|
||||
invalidHandle = ^uintptr(0) // INVALID_HANDLE_VALUE
|
||||
fileTypeDisk = 1
|
||||
fileTypePipe = 3
|
||||
)
|
||||
k := syscall.NewLazyDLL("kernel32.dll")
|
||||
h, _, _ := k.NewProc("GetStdHandle").Call(which)
|
||||
if h == 0 || h == invalidHandle {
|
||||
return false
|
||||
}
|
||||
t, _, _ := k.NewProc("GetFileType").Call(h)
|
||||
return t == fileTypeDisk || t == fileTypePipe
|
||||
}
|
||||
|
||||
// configureConsole passe la console en UTF-8 + traitement des séquences ANSI
|
||||
// (couleurs/curseur), comme le faisait l'init() au chargement — mais APRÈS le
|
||||
// rattachement, sinon ça ciblait une console encore inexistante.
|
||||
// (couleurs/curseur).
|
||||
func configureConsole() {
|
||||
const (
|
||||
cpUTF8 = 65001
|
||||
|
||||
@@ -29,11 +29,11 @@ import (
|
||||
//
|
||||
// Maintenant, deux cas seulement :
|
||||
//
|
||||
// 1. AJEAN n'est pas installé. On DEMANDE : c'est le seul moment où
|
||||
// l'utilisateur gagne à savoir ce qu'on pose sur sa machine et où. Puis
|
||||
// copie + PATH + raccourcis, et on démarre depuis la copie installée. Un
|
||||
// refus vaut « pas maintenant » et la question revient au prochain lancement
|
||||
// du fichier : mémorisé, il ne laissait AUCUN moyen d'installer AJEAN.
|
||||
// 1. AJEAN n'est pas installé. On installe, sans demander : rester à
|
||||
// l'emplacement du fichier téléchargé ne donne pas une installation
|
||||
// utilisable, la question n'avait donc qu'une réponse. Copie + PATH +
|
||||
// raccourcis, un message qui dit ce qui a été fait, et on démarre depuis la
|
||||
// copie installée.
|
||||
//
|
||||
// 2. AJEAN est déjà installé. Le fichier téléchargé se comporte en installeur de
|
||||
// mise à jour, avec trois situations bien distinctes (voir runAsInstaller) :
|
||||
@@ -131,7 +131,12 @@ func appFirstRun() bool {
|
||||
// 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.
|
||||
func launch(target string) bool {
|
||||
cmd := exec.Command(target)
|
||||
// « app » est passé EXPLICITEMENT plutôt que de compter sur la détection du
|
||||
// mode application. Celle-ci répond à une seule question — « ai-je été
|
||||
// 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")
|
||||
cmd.Dir = filepath.Dir(target)
|
||||
return cmd.Start() == nil
|
||||
}
|
||||
@@ -404,6 +409,10 @@ foreach ($d in @($progs, [Environment]::GetFolderPath('Desktop'))) {
|
||||
$s.TargetPath=$t
|
||||
$s.WorkingDirectory=(Split-Path $t)
|
||||
$s.Description='AJEAN, votre IA locale'
|
||||
# Minimisé (7) : le binaire étant en sous-système console, Windows lui alloue
|
||||
# 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.Save()
|
||||
$done+=$d
|
||||
} catch {}
|
||||
|
||||
Reference in new issue
Block a user