mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-20 13:38:11 +02:00
Rebaptise AJEAN en Loki (fork, lignée conservée)
- module github.com/R0m1k3/Loki, cmd/loki, internal/loki (package loki) - LOKI_HOME, LOKI_MODEL_DIRS, LOKI_SERVICE, LOKI_DL_CONNS ; /etc/loki ; units loki-engine / loki-ui ; binaire et aide CLI - updateRepo pointe sur R0m1k3/Loki (l'auto-update ne tirera plus les binaires AJEAN amont) Conservé à l'identique : le domaine ajean.link (service de tunnel amont), les littéraux de migration 0.7.x (migrate_07.go), RELEASE_NOTES.md et LICENSE (historique et licence de l'amont). go build/vet/test : verts.
This commit is contained in:
157 files changed
+928
-928
No files matched your search
@@ -0,0 +1,439 @@
|
||||
//go:build windows
|
||||
|
||||
package loki
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// init makes the Windows console behave like a modern terminal: UTF-8 so the
|
||||
// Unicode glyphs loki prints (✓ ▶ …) and child-process output don't turn into
|
||||
// mojibake (the default OEM codepage, e.g. cp850, renders "✓" as "├ö"), and VT
|
||||
// processing so the ANSI colour/cursor escapes (including the build progress
|
||||
// line) are interpreted instead of printed literally. Best-effort: a redirected
|
||||
// or legacy console just keeps its defaults.
|
||||
func init() {
|
||||
const (
|
||||
cpUTF8 = 65001
|
||||
enableVirtualTerminalProcessing = 0x0004
|
||||
stdOutputHandle = ^uintptr(10) // -11 as DWORD
|
||||
)
|
||||
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||
_, _, _ = kernel32.NewProc("SetConsoleOutputCP").Call(uintptr(cpUTF8))
|
||||
_, _, _ = kernel32.NewProc("SetConsoleCP").Call(uintptr(cpUTF8))
|
||||
|
||||
getStdHandle := kernel32.NewProc("GetStdHandle")
|
||||
getConsoleMode := kernel32.NewProc("GetConsoleMode")
|
||||
setConsoleMode := kernel32.NewProc("SetConsoleMode")
|
||||
h, _, _ := getStdHandle.Call(stdOutputHandle)
|
||||
var mode uint32
|
||||
if r, _, _ := getConsoleMode.Call(h, uintptr(unsafe.Pointer(&mode))); r != 0 {
|
||||
_, _, _ = setConsoleMode.Call(h, uintptr(mode|enableVirtualTerminalProcessing))
|
||||
}
|
||||
}
|
||||
|
||||
// homeRoot is the parent directory of the data root: %ProgramData% (machine-wide,
|
||||
// the closest analogue to /etc), falling back to %LOCALAPPDATA% for unprivileged
|
||||
// setups. Ancien et nouveau dossier partagent ce parent, ce qui garantit que la
|
||||
// migration loki → loki est un rename intra-volume (voir sys_migrate.go).
|
||||
func homeRoot() string {
|
||||
if pd := os.Getenv("ProgramData"); pd != "" {
|
||||
return pd
|
||||
}
|
||||
if la := os.Getenv("LOCALAPPDATA"); la != "" {
|
||||
return la
|
||||
}
|
||||
return os.TempDir()
|
||||
}
|
||||
|
||||
// defaultLokiHome est la racine des données quand $LOKI_HOME est absent.
|
||||
func defaultLokiHome() string { return filepath.Join(homeRoot(), "loki") }
|
||||
|
||||
// defaultEditor is used by `loki edit` when $EDITOR is unset.
|
||||
func defaultEditor() string { return "notepad" }
|
||||
|
||||
// openBrowser ouvre l'URL dans le navigateur par défaut. rundll32 évite les
|
||||
// pièges de quoting de `cmd /c start`.
|
||||
func openBrowser(url string) error {
|
||||
return hideCmd(exec.Command("rundll32", "url.dll,FileProtocolHandler", url)).Start()
|
||||
}
|
||||
|
||||
// hideCmd empêche une commande externe d'ouvrir une fenêtre de console
|
||||
// (CREATE_NO_WINDOW). Indispensable quand Loki tourne sans console (mode app) :
|
||||
// sinon chaque `nvidia-smi`/`git`/… ferait clignoter une fenêtre noire. Fusionne
|
||||
// avec les flags déjà présents pour ne pas écraser un éventuel détachement.
|
||||
func hideCmd(cmd *exec.Cmd) *exec.Cmd {
|
||||
const createNoWindow = 0x08000000
|
||||
if cmd.SysProcAttr == nil {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||
}
|
||||
cmd.SysProcAttr.HideWindow = true
|
||||
cmd.SysProcAttr.CreationFlags |= createNoWindow
|
||||
return cmd
|
||||
}
|
||||
|
||||
// totalRAMGB renvoie la RAM physique totale en Go (GlobalMemoryStatusEx).
|
||||
func totalRAMGB() float64 {
|
||||
var m struct {
|
||||
dwLength uint32
|
||||
dwMemoryLoad uint32
|
||||
ullTotalPhys uint64
|
||||
ullAvailPhys uint64
|
||||
ullTotalPageFile uint64
|
||||
ullAvailPageFile uint64
|
||||
ullTotalVirtual uint64
|
||||
ullAvailVirtual uint64
|
||||
ullAvailExtendedVirtual uint64
|
||||
}
|
||||
m.dwLength = uint32(unsafe.Sizeof(m))
|
||||
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||
r, _, _ := kernel32.NewProc("GlobalMemoryStatusEx").Call(uintptr(unsafe.Pointer(&m)))
|
||||
if r == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(m.ullTotalPhys) / (1024 * 1024 * 1024)
|
||||
}
|
||||
|
||||
// setLibraryPath ensures llama-server can load its dependent DLLs. Windows
|
||||
// resolves them via PATH (and the binary's own directory), so we prepend dir.
|
||||
// For a CUDA build we must also add the CUDA Toolkit's bin: ggml-cuda.dll links
|
||||
// against cublas64_*/cublasLt64_*.dll which live there, not next to the binary —
|
||||
// without it the server dies with 0xC0000135 (DLL not found) unless the launching
|
||||
// shell happened to have CUDA on PATH.
|
||||
func setLibraryPath(dir string) {
|
||||
parts := []string{dir}
|
||||
if nvcc := findNvcc(); nvcc != "" {
|
||||
binDir := filepath.Dir(nvcc) // …\CUDA\vX.Y\bin
|
||||
parts = append(parts, binDir)
|
||||
// CUDA 13+ a déplacé les DLL runtime (cublas64_*, cublasLt64_*, cudart64_*)
|
||||
// dans bin\x64\ ; sur CUDA 12 elles sont directement dans bin\. On ajoute
|
||||
// les deux pour couvrir les deux layouts.
|
||||
if x64 := filepath.Join(binDir, "x64"); isDir(x64) {
|
||||
parts = append(parts, x64)
|
||||
}
|
||||
}
|
||||
if existing := os.Getenv("PATH"); existing != "" {
|
||||
parts = append(parts, existing)
|
||||
}
|
||||
_ = os.Setenv("PATH", strings.Join(parts, string(os.PathListSeparator)))
|
||||
}
|
||||
|
||||
// libraryPathEnv : même chose pour une commande ponctuelle (ex. --list-devices
|
||||
// depuis l'UI), sans modifier le PATH du process web.
|
||||
func libraryPathEnv(dir string) []string {
|
||||
parts := []string{dir}
|
||||
if nvcc := findNvcc(); nvcc != "" {
|
||||
binDir := filepath.Dir(nvcc)
|
||||
parts = append(parts, binDir)
|
||||
if x64 := filepath.Join(binDir, "x64"); isDir(x64) {
|
||||
parts = append(parts, x64)
|
||||
}
|
||||
}
|
||||
if existing := os.Getenv("PATH"); existing != "" {
|
||||
parts = append(parts, existing)
|
||||
}
|
||||
return append(os.Environ(), "PATH="+strings.Join(parts, string(os.PathListSeparator)))
|
||||
}
|
||||
|
||||
// execServer runs llama-server as a child process and waits for it. Windows has
|
||||
// no exec() that replaces the current image, so `loki serve` stays alive as the
|
||||
// parent (this is the detached process the service supervisor tracks).
|
||||
// args[0] is the binary path; the rest are its arguments.
|
||||
func execServer(bin string, args []string) error {
|
||||
cmd := hideCmd(exec.Command(bin, args[1:]...)) // pas de console pour llama-server (mode app)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = os.Environ()
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// wingetIDs maps a tool's command name to its winget package ID.
|
||||
var wingetIDs = map[string]string{
|
||||
"git": "Git.Git",
|
||||
"cmake": "Kitware.CMake",
|
||||
"ninja": "Ninja-build.Ninja",
|
||||
}
|
||||
|
||||
// autoInstallTool installs a missing build tool via winget (bundled with Windows
|
||||
// 10/11 and Server 2025). Returns an error if winget is absent or the install
|
||||
// fails; the caller re-checks availability afterwards.
|
||||
func autoInstallTool(name string) error {
|
||||
if _, err := exec.LookPath("winget"); err != nil {
|
||||
return fmt.Errorf("winget introuvable — installe %s manuellement", name)
|
||||
}
|
||||
id, ok := wingetIDs[name]
|
||||
if !ok {
|
||||
id = name
|
||||
}
|
||||
cmd := hideCmd(exec.Command("winget", "install", "--id", id, "-e",
|
||||
"--accept-source-agreements", "--accept-package-agreements",
|
||||
"--disable-interactivity", "--silent"))
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// refreshToolPath reloads the process PATH from the Windows registry (Machine +
|
||||
// User), so tools just installed by winget become resolvable without restarting
|
||||
// the shell.
|
||||
func refreshToolPath() {
|
||||
ps := `$m=[Environment]::GetEnvironmentVariable('Path','Machine')
|
||||
$u=[Environment]::GetEnvironmentVariable('Path','User')
|
||||
Write-Output ((@($m,$u) | Where-Object { $_ }) -join ';')`
|
||||
out, err := hideCmd(exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", ps)).Output()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if merged := strings.TrimSpace(string(out)); merged != "" {
|
||||
_ = os.Setenv("PATH", merged)
|
||||
}
|
||||
}
|
||||
|
||||
// vswherePath returns the location of vswhere.exe, the official tool for
|
||||
// locating Visual Studio / Build Tools installs. It ships in a fixed spot.
|
||||
func vswherePath() string {
|
||||
base := os.Getenv("ProgramFiles(x86)")
|
||||
if base == "" {
|
||||
base = os.Getenv("ProgramFiles")
|
||||
}
|
||||
return filepath.Join(base, "Microsoft Visual Studio", "Installer", "vswhere.exe")
|
||||
}
|
||||
|
||||
// msvcInstallVersion returns the major version of the newest MSVC install that
|
||||
// has the C++ toolchain (e.g. "17"), or "" if none is found.
|
||||
func msvcInstallVersion() string {
|
||||
vs := vswherePath()
|
||||
if _, err := os.Stat(vs); err != nil {
|
||||
return ""
|
||||
}
|
||||
out, err := hideCmd(exec.Command(vs, "-latest", "-products", "*",
|
||||
"-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
|
||||
"-property", "installationVersion")).Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
ver := strings.TrimSpace(string(out))
|
||||
if ver == "" {
|
||||
return ""
|
||||
}
|
||||
if i := strings.IndexByte(ver, '.'); i > 0 {
|
||||
return ver[:i]
|
||||
}
|
||||
return ver
|
||||
}
|
||||
|
||||
// msvcGenerator returns the CMake generator name for the installed MSVC, falling
|
||||
// back to VS 2022 (the version `ensureCompiler` installs).
|
||||
func msvcGenerator() string {
|
||||
switch msvcInstallVersion() {
|
||||
case "16":
|
||||
return "Visual Studio 16 2019"
|
||||
case "15":
|
||||
return "Visual Studio 15 2017"
|
||||
default:
|
||||
return "Visual Studio 17 2022"
|
||||
}
|
||||
}
|
||||
|
||||
// ensureCompiler makes sure an MSVC C++ toolchain is available, installing the
|
||||
// Visual Studio 2022 Build Tools (VCTools workload) via winget if not. This is a
|
||||
// large download but keeps `loki llamacpp install` fully unattended on a bare
|
||||
// Windows box.
|
||||
func ensureCompiler() error {
|
||||
if msvcInstallVersion() != "" {
|
||||
return nil
|
||||
}
|
||||
if _, err := exec.LookPath("winget"); err != nil {
|
||||
return fmt.Errorf("compilateur C++ absent et winget introuvable — installe « Visual Studio Build Tools » (charge de travail C++) manuellement")
|
||||
}
|
||||
fmt.Printf("%s compilateur C++ absent — installation des Build Tools MSVC (gros téléchargement, une seule fois)…\n", yellow("[info]"))
|
||||
cmd := hideCmd(exec.Command("winget", "install", "--id", "Microsoft.VisualStudio.2022.BuildTools", "-e",
|
||||
"--accept-source-agreements", "--accept-package-agreements",
|
||||
"--disable-interactivity",
|
||||
"--override", "--quiet --wait --norestart --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"))
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("installation des Build Tools MSVC échouée: %w", err)
|
||||
}
|
||||
if msvcInstallVersion() == "" {
|
||||
return fmt.Errorf("Build Tools installés mais toolchain C++ introuvable — relance la commande ou vérifie l'installation Visual Studio")
|
||||
}
|
||||
fmt.Printf("%s compilateur C++ prêt.\n", green("✓"))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureAccelerator installs the CUDA Toolkit when an NVIDIA GPU is present but
|
||||
// nvcc isn't, so the build can target the GPU instead of falling back to CPU.
|
||||
// Best-effort: any failure just leaves the machine on the CPU path (the caller
|
||||
// ignores the return value). The CUDA download is large; we only trigger it when
|
||||
// a GPU is actually detected.
|
||||
func ensureAccelerator() {
|
||||
if !hasNvidiaGPU() {
|
||||
return // pas de GPU NVIDIA visible → rien à installer, build CPU
|
||||
}
|
||||
if findNvcc() != "" {
|
||||
return // toolkit déjà présent
|
||||
}
|
||||
if _, err := exec.LookPath("winget"); err != nil {
|
||||
fmt.Printf("%s GPU NVIDIA détecté mais CUDA Toolkit absent et winget introuvable — build CPU (installe le CUDA Toolkit pour l'accélération GPU)\n", yellow("[info]"))
|
||||
return
|
||||
}
|
||||
fmt.Printf("%s GPU NVIDIA détecté — installation du CUDA Toolkit pour l'accélération GPU (gros téléchargement, une seule fois)…\n", yellow("[info]"))
|
||||
cmd := hideCmd(exec.Command("winget", "install", "--id", "Nvidia.CUDA", "-e",
|
||||
"--accept-source-agreements", "--accept-package-agreements",
|
||||
"--disable-interactivity"))
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Printf("%s installation du CUDA Toolkit échouée (%v) — on continue en CPU\n", yellow("[warn]"), err)
|
||||
return
|
||||
}
|
||||
refreshToolPath()
|
||||
if findNvcc() != "" {
|
||||
fmt.Printf("%s CUDA Toolkit prêt — build GPU activé.\n", green("✓"))
|
||||
} else {
|
||||
fmt.Printf("%s CUDA Toolkit installé mais nvcc introuvable dans cette session — relance la commande pour activer le GPU\n", yellow("[info]"))
|
||||
}
|
||||
}
|
||||
|
||||
// ensureCudaVSIntegration vérifie que l'intégration MSBuild de CUDA (fichiers
|
||||
// « CUDA x.y.props/targets » dans BuildCustomizations de Visual Studio) est en
|
||||
// place — sans elle, le générateur Visual Studio échoue sur le cryptique
|
||||
// « No CUDA toolset found » (CMakeDetermineCompilerId). Cas typiques : CUDA
|
||||
// installé dans un chemin custom (ex. F:\Cuda) sans cocher « Visual Studio
|
||||
// Integration », ou VS (ré)installé APRÈS CUDA — l'installeur NVIDIA n'intègre
|
||||
// que les VS présents au moment où il tourne. On tente d'abord de copier les
|
||||
// fichiers depuis le toolkit (extras\visual_studio_integration\MSBuildExtensions) ;
|
||||
// si ça échoue (droits), on explique quoi faire au lieu de laisser l'erreur
|
||||
// CMake brute. Best-effort : sans vswhere/VS détectable on laisse cmake juger.
|
||||
func ensureCudaVSIntegration(toolkitDir string) error {
|
||||
vs := vswherePath()
|
||||
if _, err := os.Stat(vs); err != nil {
|
||||
return nil
|
||||
}
|
||||
out, err := hideCmd(exec.Command(vs, "-latest", "-products", "*",
|
||||
"-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
|
||||
"-property", "installationPath")).Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
installPath := strings.TrimSpace(string(out))
|
||||
if installPath == "" {
|
||||
return nil
|
||||
}
|
||||
dsts, _ := filepath.Glob(filepath.Join(installPath, "MSBuild", "Microsoft", "VC", "*", "BuildCustomizations"))
|
||||
for _, d := range dsts {
|
||||
if m, _ := filepath.Glob(filepath.Join(d, "CUDA *.props")); len(m) > 0 {
|
||||
return nil // intégration déjà en place
|
||||
}
|
||||
}
|
||||
// Absente : tentative de réparation depuis le toolkit lui-même.
|
||||
src := filepath.Join(toolkitDir, "extras", "visual_studio_integration", "MSBuildExtensions")
|
||||
files, _ := filepath.Glob(filepath.Join(src, "*"))
|
||||
copied := 0
|
||||
for _, dst := range dsts {
|
||||
ok := len(files) > 0
|
||||
for _, f := range files {
|
||||
b, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dst, filepath.Base(f)), b, 0o644); err != nil {
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
copied++
|
||||
}
|
||||
}
|
||||
if copied > 0 {
|
||||
fmt.Printf("%s intégration Visual Studio de CUDA absente — réparée (fichiers copiés depuis %s)\n", yellow("[fix]"), src)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(`l'intégration Visual Studio de CUDA est absente : aucun fichier « CUDA x.y.props » sous
|
||||
%s\MSBuild\Microsoft\VC\<version>\BuildCustomizations
|
||||
Sans elle, CMake échoue sur « No CUDA toolset found ». Pour corriger, au choix :
|
||||
1. relance l'installeur du CUDA Toolkit (installation personnalisée) et coche « CUDA → Visual Studio Integration » — Visual Studio doit déjà être installé à ce moment-là ;
|
||||
2. ou copie (en admin) les fichiers de
|
||||
%s
|
||||
vers le dossier BuildCustomizations ci-dessus, puis relance loki llamacpp install`, installPath, src)
|
||||
}
|
||||
|
||||
// cudaPathEnv returns the CUDA toolkit env vars the MSBuild CUDA integration
|
||||
// needs (CUDA_PATH and the version-specific CUDA_PATH_Vx_y), derived from the
|
||||
// toolkit root, e.g. "...\CUDA\v13.3" → CUDA_PATH_V13_3. Returns NUL-free
|
||||
// "KEY=VAL" strings.
|
||||
func cudaPathEnv(toolkitDir string) []string {
|
||||
out := []string{"CUDA_PATH=" + toolkitDir}
|
||||
// Le dossier se nomme "v13.3" → variable CUDA_PATH_V13_3.
|
||||
ver := strings.TrimPrefix(filepath.Base(toolkitDir), "v")
|
||||
if ver != "" {
|
||||
out = append(out, "CUDA_PATH_V"+strings.ReplaceAll(ver, ".", "_")+"="+toolkitDir)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// newShellCmd builds the command used by the run_shell tool. hideCmd évite un
|
||||
// flash de console à chaque commande d'agent quand Loki tourne en mode app.
|
||||
func newShellCmd(ctx context.Context, command string) *exec.Cmd {
|
||||
return hideCmd(exec.CommandContext(ctx, "cmd", "/C", command))
|
||||
}
|
||||
|
||||
// ramUsageMB renvoie (utilisée, totale) en Mo pour /api/ram (GlobalMemoryStatusEx).
|
||||
func ramUsageMB() (used, total int) {
|
||||
var m struct {
|
||||
dwLength uint32
|
||||
dwMemoryLoad uint32
|
||||
ullTotalPhys uint64
|
||||
ullAvailPhys uint64
|
||||
ullTotalPageFile uint64
|
||||
ullAvailPageFile uint64
|
||||
ullTotalVirtual uint64
|
||||
ullAvailVirtual uint64
|
||||
ullAvailExtendedVirtual uint64
|
||||
}
|
||||
m.dwLength = uint32(unsafe.Sizeof(m))
|
||||
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||
if r, _, _ := kernel32.NewProc("GlobalMemoryStatusEx").Call(uintptr(unsafe.Pointer(&m))); r == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
const mb = 1024 * 1024
|
||||
return int((m.ullTotalPhys - m.ullAvailPhys) / mb), int(m.ullTotalPhys / mb)
|
||||
}
|
||||
|
||||
// --- Supervision de processus détachés (worker de lien, service). Pendant
|
||||
// Windows de sys_platform_unix.go.
|
||||
|
||||
// spawnDetached prépare une commande détachée et SANS console : en mode app
|
||||
// (double-clic) le moindre enfant console ferait clignoter une fenêtre noire.
|
||||
func spawnDetached(name string, args ...string) *exec.Cmd {
|
||||
cmd := exec.Command(name, args...)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
HideWindow: true,
|
||||
CreationFlags: createNewProcessGroup | detachedProcess | createNoWindow,
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// pidAlive : alias de processAlive (API commune avec Unix).
|
||||
func pidAlive(pid int) bool { return processAlive(pid) }
|
||||
|
||||
// killTree arrête le process et toute sa descendance (taskkill /T).
|
||||
func killTree(pid int) {
|
||||
if pid <= 0 {
|
||||
return
|
||||
}
|
||||
_ = hideCmd(exec.Command("taskkill", "/PID", strconv.Itoa(pid), "/T", "/F")).Run()
|
||||
}
|
||||
Reference in new issue
Block a user