Files
ARTEX/cmd/artex/main.go
BaaRai e233c02314 Translate all Chinese text to English
Comments, doc comments, UI labels, log and error messages, LLM prompt
strings, markdown docs, test data and assertions. Identifiers, enum
keys, routes, placeholders and code structure are unchanged.

Notes:
- docs/漏洞流量证据.md renamed to docs/vulnerability-traffic-evidence.md
- tests that exercised CJK specific handling (GBK zip names, Chinese
  full text search) now use ASCII equivalents; go build passes and the
  test suite matches the pre translation baseline (remaining failures
  need a PostgreSQL DSN and fail identically on the original)
- frontend mention syntax now uses English kinds (finding, asset, ...)
  matching the backend, chat mentions tests pass
2026-10-08 23:14:34 +02:00

161 lines
5.3 KiB
Go

// Command artex runs the ARTEX backend: the dual SQLite graph stores,
// the event-driven exploration engine, and the JSON HTTP API consumed by the
// shadcn/ui frontend.
package main
import (
"context"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"path/filepath"
"runtime"
"syscall"
"time"
"github.com/Autumn-27/artex/agent"
"github.com/Autumn-27/artex/config"
"github.com/Autumn-27/artex/selfupdate"
"github.com/Autumn-27/artex/server"
)
// version is the build version, injected at release time via
// -ldflags "-X main.version=<tag>". Defaults to "dev" for local builds.
var version = "dev"
const banner = `
_ ____ _____ _______ __
/ \ | _ \_ _| ____\ \/ /
/ _ \ | |_) || | | _| \ /
/ ___ \| _ < | | | |___ / \
/_/ \_\_| \_\|_| |_____/_/\_\
`
// printBanner writes the startup banner + version/runtime info to stdout.
func printBanner(addr string) {
fmt.Print(banner)
fmt.Println(" AI autonomous penetration testing system")
fmt.Printf(" version %s · %s/%s · %s · listening on %s\n\n",
version, runtime.GOOS, runtime.GOARCH, runtime.Version(), addr)
}
// main only maps run's result onto the process exit code. The exit code is part
// of the update protocol — the supervising start script reads it to decide
// whether to relaunch us (see selfupdate.ExitRestart) — so the body has to live
// in a function that can *return* rather than os.Exit past its own defers.
func main() {
os.Exit(run())
}
func run() int {
var (
addr = flag.String("addr", ":8787", "HTTP listen address")
dataDir = flag.String("data", filepath.Join(config.BaseDir(), "data"), "data directory for SQLite stores (default: data/ next to the executable)")
proxy = flag.String("proxy", "127.0.0.1:8788", "traffic recording proxy address (empty to disable)")
)
flag.Parse()
// hand the build version to the server package so GET /api/health can report it
// to the frontend top bar.
server.BuildVersion = version
printBanner(*addr)
// capture backend logs into the in-memory sink (still to stderr) so the /logs
// page can show a live log stream. Do this first, to catch startup logs too.
server.StartLogCapture()
// Self-update bootstrap: swap in a staged binary, or count a post-swap boot
// attempt and roll back if the new build keeps dying. Must run before we open
// the stores or bind a port — this may end with "exit and let the start script
// relaunch me", and there is no point paying for either first.
action, upState := selfupdate.Bootstrap()
server.SetBootUpdateState(upState)
if action == selfupdate.Restart {
return selfupdate.ExitRestart
}
// surface which config file the binary reads (absolute, so `go run`'s relative
// "config.json" — resolved against the CWD — is unambiguous).
cfgPath := config.Path()
if abs, e := filepath.Abs(cfgPath); e == nil {
cfgPath = abs
}
if _, e := os.Stat(cfgPath); e == nil {
log.Printf("[config] config file: %s", cfgPath)
} else {
log.Printf("[config] config file: %s (not found — will only try the environment variable ARTEX_PG_DSN)", cfgPath)
}
sigCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
ctx, shutdown := shutdownContext(sigCtx)
defer shutdown(agent.AbortShutdown)
mgr, err := server.NewManager(*dataDir, *proxy)
if err != nil {
log.Fatalf("open stores: %v", err)
}
defer mgr.Close()
// Surviving this long means a freshly swapped-in build actually works, so drop
// the upgrade marker and stop counting attempts. Until it fires, every boot
// increments the count and a build that keeps dying gets rolled back.
settle := time.AfterFunc(selfupdate.SettleDelay, selfupdate.Settle)
defer settle.Stop()
skillDir := config.SkillDir()
if abs, err := filepath.Abs(skillDir); err == nil {
skillDir = abs
}
log.Printf("[config] skill directory: %s", skillDir)
srv := server.New(ctx, mgr, skillDir, *dataDir, config.BaseDir())
httpSrv := &http.Server{
Addr: *addr,
Handler: srv.Handler(),
ReadHeaderTimeout: 10 * time.Second,
}
go func() {
log.Printf("ARTEX %s backend listening on %s (data=%s, workers=%d)", version, *addr, *dataDir, mgr.Workers())
if err := httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("serve: %v", err)
}
}()
// Two ways out: a signal (normal stop → exit 0, the start script stops looping)
// or a staged update / rollback (→ exit 75, the script relaunches us and the
// bootstrap above installs the new build).
code := 0
select {
case <-ctx.Done():
case <-server.RestartRequested():
code = selfupdate.ExitRestart
shutdown(agent.AbortShutdown)
}
log.Println("shutting down...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = httpSrv.Shutdown(shutdownCtx)
return code
}
// shutdownContext deliberately does not derive from signalCtx. If it did, the
// parent's plain context.Canceled could win the race before AbortShutdown was
// attached to the child, losing the diagnostic cause in every running Agent.
func shutdownContext(signalCtx context.Context) (context.Context, context.CancelCauseFunc) {
ctx, shutdown := context.WithCancelCause(context.Background())
go func() {
select {
case <-signalCtx.Done():
shutdown(agent.AbortShutdown)
case <-ctx.Done():
}
}()
return ctx, shutdown
}