Add read-only task context inheritance, ordered task-level LLM failover, live Worker controls, cascading task cleanup, FIFO task concurrency, cancellation diagnostics, UI updates, tests, build script, and changelog.
125 lines
3.7 KiB
Go
125 lines
3.7 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/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 自主渗透测试系统")
|
|
fmt.Printf(" 版本 %s · %s/%s · %s · 监听 %s\n\n",
|
|
version, runtime.GOOS, runtime.GOARCH, runtime.Version(), addr)
|
|
}
|
|
|
|
func main() {
|
|
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", ":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()
|
|
|
|
// 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] 配置文件: %s", cfgPath)
|
|
} else {
|
|
log.Printf("[config] 配置文件: %s (不存在 — 将仅尝试环境变量 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()
|
|
|
|
skillDir := config.SkillDir()
|
|
if abs, err := filepath.Abs(skillDir); err == nil {
|
|
skillDir = abs
|
|
}
|
|
log.Printf("[config] skill 目录: %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)
|
|
}
|
|
}()
|
|
|
|
<-ctx.Done()
|
|
log.Println("shutting down...")
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
_ = httpSrv.Shutdown(shutdownCtx)
|
|
}
|
|
|
|
// 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
|
|
}
|