mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-18 04:36:44 +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
@@ -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