internal/autogen/backendhelp.go
Rama main · réplica de lectura del repositorio original.
package autogen
// backendhelp.go answers "does the selected binary know this flag?" for the
// launch-arguments editor (ui-svelte/launch-args.md). The checked-in table
// (flagtable.go) knows what OUR emitters write; it cannot know about a spelling
// the user's build adds, and it goes stale the day upstream drops one. `exe
// --help` fills the gap, memoised per binary the same way ListBackendDevices
// memoises its listing: the flag set is a property of one file, and a probe is
// a process launch.
import (
"context"
"fmt"
"os"
"os/exec"
"regexp"
"strings"
"sync"
"time"
)
// backendHelpTimeout bounds one --help. It does not touch the device-probe
// budget (backenddev.go): that budget exists to stop config generation hanging
// on wedged backends, while this probe belongs to an interactive edit, where a
// short wait is visible and a timeout is recoverable.
const backendHelpTimeout = 10 * time.Second
// backendFlagRe matches a flag spelling anywhere in a usage block:
//
// -m, --model FNAME path to model
// --no-mmap ...
//
// Descriptions that name another flag ("deprecated, use --flash-attn") match
// too. That is the right failure direction: an extra name can only make
// validation looser, never reject something the binary accepts.
var backendFlagRe = regexp.MustCompile(`(?:^|[\s,(\[])(-{1,2}[A-Za-z][A-Za-z0-9-]*)`)
var (
backendHelpMu sync.Mutex
backendHelpCache = map[string][]string{}
)
// BackendFlags returns the flag spellings `exe --help` advertises, memoised per
// binary. The cache key carries size and mtime so a backend upgraded in place
// is re-probed instead of answering from a stale list.
//
// An error is an ordinary outcome: an older build may have no useful --help, and
// a packaged install may name a binary that is not there yet. Callers fall back
// to the checked-in table alone.
func BackendFlags(exe string) ([]string, error) {
if strings.TrimSpace(exe) == "" {
return nil, fmt.Errorf("no backend exe")
}
key := exe
if st, err := os.Stat(exe); err == nil {
key = fmt.Sprintf("%s|%d|%d", exe, st.Size(), st.ModTime().UnixNano())
}
backendHelpMu.Lock()
cached, hit := backendHelpCache[key]
backendHelpMu.Unlock()
if hit {
if len(cached) == 0 {
return nil, fmt.Errorf("no flags listed by %s", exe)
}
return cached, nil
}
flags, err := probeBackendFlags(exe)
backendHelpMu.Lock()
// A failed probe is cached too: re-running it once per keystroke-sized
// validation pass would dominate the editor on a box where it cannot work.
backendHelpCache[key] = flags
backendHelpMu.Unlock()
if err != nil {
return nil, err
}
if len(flags) == 0 {
return nil, fmt.Errorf("no flags listed by %s", exe)
}
return flags, nil
}
func probeBackendFlags(exe string) ([]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), backendHelpTimeout)
defer cancel()
cmd := exec.CommandContext(ctx, exe, "--help")
hideConsole(cmd)
// Which stream gets the usage has moved across releases, and some builds
// exit non-zero after printing it. Read the pair.
out, err := cmd.CombinedOutput()
if err != nil && len(out) == 0 {
return nil, fmt.Errorf("%s --help: %w", exe, err)
}
return parseBackendHelp(string(out)), nil
}
// parseBackendHelp pulls the distinct flag spellings out of a usage block,
// keeping the order they first appear in (the editor lists them that way).
func parseBackendHelp(out string) []string {
seen := map[string]bool{}
var flags []string
for _, m := range backendFlagRe.FindAllStringSubmatch(out, -1) {
f := m[1]
// A trailing hyphen is grammar ("--flag-") or an unfinished range, not a
// flag; "--" alone is the end-of-options marker.
if f == "-" || f == "--" || strings.HasSuffix(f, "-") || seen[f] {
continue
}
seen[f] = true
flags = append(flags, f)
}
return flags
}