Initial commit: AetherForge Linux (forge-mesh) v0.1.0-dev
Some checks failed
Test / test (push) Has been cancelled
Some checks failed
Test / test (push) Has been cancelled
This commit is contained in:
97
cmd/server/main.go
Normal file
97
cmd/server/main.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"forge-mesh/internal/api"
|
||||
"forge-mesh/internal/config"
|
||||
"forge-mesh/internal/db"
|
||||
"forge-mesh/internal/forge"
|
||||
"forge-mesh/internal/stratum"
|
||||
)
|
||||
|
||||
//go:embed webroot/*
|
||||
var webroot embed.FS
|
||||
|
||||
const version = "0.1.0-dev"
|
||||
|
||||
func main() {
|
||||
configPath := flag.String("config", "data/config.json", "path to config.json")
|
||||
installTmpl := flag.String("install-tmpl", "scripts/install.sh.tpl", "install.sh template path")
|
||||
flag.Parse()
|
||||
|
||||
cfg, err := config.Load(*configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("config: %v", err)
|
||||
}
|
||||
|
||||
if err := cfg.EnsureDataDirs(); err != nil {
|
||||
log.Fatalf("data dirs: %v", err)
|
||||
}
|
||||
|
||||
conn, err := db.Open(cfg.DatabasePath)
|
||||
if err != nil {
|
||||
log.Fatalf("database: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
keyPair, err := forge.LoadOrCreateKey(cfg.Forge.SigningKeyPath)
|
||||
if err != nil {
|
||||
log.Fatalf("signing key: %v", err)
|
||||
}
|
||||
|
||||
staticFS, err := fs.Sub(webroot, "webroot")
|
||||
if err != nil {
|
||||
log.Fatalf("webroot: %v", err)
|
||||
}
|
||||
|
||||
srv, err := api.NewServer(cfg, conn, staticFS, version, *installTmpl, keyPair.PublicKeyHex())
|
||||
if err != nil {
|
||||
log.Fatalf("server: %v", err)
|
||||
}
|
||||
|
||||
stratumProxy := stratum.New(cfg.Stratum)
|
||||
if err := stratumProxy.Start(); err != nil {
|
||||
log.Printf("stratum (non-fatal): %v", err)
|
||||
} else {
|
||||
defer stratumProxy.Close()
|
||||
log.Printf("stratum: XMR %s RVN %s", cfg.Stratum.XMRListen, cfg.Stratum.RVNListen)
|
||||
}
|
||||
|
||||
addr := cfg.ListenAddr
|
||||
log.Printf("forge-mesh-server %s listening on %s (config: %s, db: %s)",
|
||||
version, addr, absPath(*configPath), cfg.DatabasePath)
|
||||
|
||||
httpSrv := &http.Server{Addr: addr, Handler: srv.Handler()}
|
||||
|
||||
go func() {
|
||||
if err := httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
fmt.Fprintf(os.Stderr, "server: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
sigCtx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
<-sigCtx.Done()
|
||||
log.Println("shutting down...")
|
||||
_ = httpSrv.Shutdown(context.Background())
|
||||
}
|
||||
|
||||
func absPath(p string) string {
|
||||
abs, err := filepath.Abs(p)
|
||||
if err != nil {
|
||||
return p
|
||||
}
|
||||
return abs
|
||||
}
|
||||
206
cmd/server/main_test.go
Normal file
206
cmd/server/main_test.go
Normal file
@@ -0,0 +1,206 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestServerHealthViaSubprocess(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
port := freePort(t)
|
||||
cfgPath := writeServerConfig(t, dir, port)
|
||||
|
||||
bin := buildServerBinary(t)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, bin, "-config", cfgPath)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("start server: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
cancel()
|
||||
_ = cmd.Wait()
|
||||
})
|
||||
|
||||
base := fmt.Sprintf("http://127.0.0.1:%d", port)
|
||||
waitForHealth(t, base+"/api/v1/health", 15*time.Second)
|
||||
|
||||
resp, err := http.Get(base + "/api/v1/health")
|
||||
if err != nil {
|
||||
t.Fatalf("health GET: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("health status: got %d want 200", resp.StatusCode)
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Status string `json:"status"`
|
||||
Service string `json:"service"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("decode health: %v", err)
|
||||
}
|
||||
if body.Status != "ok" {
|
||||
t.Fatalf("status: got %q want ok", body.Status)
|
||||
}
|
||||
if body.Service != "forge-mesh-server" {
|
||||
t.Fatalf("service: got %q", body.Service)
|
||||
}
|
||||
if body.Version == "" {
|
||||
t.Fatal("expected version in health response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerUsesConfigDataDirAndPort(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
port := freePort(t)
|
||||
cfgPath := writeServerConfig(t, dir, port)
|
||||
|
||||
cfgData, err := os.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !containsAll(string(cfgData), dir, fmt.Sprintf("127.0.0.1:%d", port)) {
|
||||
t.Fatalf("config missing data_dir or listen_addr: %s", cfgData)
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(dir, "forge-mesh.db")
|
||||
artifacts := filepath.Join(dir, "artifacts")
|
||||
|
||||
bin := buildServerBinary(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, bin, "-config", cfgPath)
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("start: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
cancel()
|
||||
_ = cmd.Wait()
|
||||
}()
|
||||
|
||||
waitForHealth(t, fmt.Sprintf("http://127.0.0.1:%d/api/v1/health", port), 12*time.Second)
|
||||
|
||||
for _, path := range []string{dbPath, artifacts, filepath.Join(dir, "signing.key")} {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Fatalf("expected server to create %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildServerBinary(t *testing.T) string {
|
||||
t.Helper()
|
||||
out := filepath.Join(t.TempDir(), "forge-mesh-server")
|
||||
cmd := exec.Command("go", "build", "-o", out, "./cmd/server")
|
||||
cmd.Dir = repoRoot(t)
|
||||
if outBytes, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("build server: %v\n%s", err, outBytes)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func writeServerConfig(t *testing.T, dir string, port int) string {
|
||||
t.Helper()
|
||||
cfgPath := filepath.Join(dir, "config.json")
|
||||
content := fmt.Sprintf(`{
|
||||
"listen_addr": "127.0.0.1:%d",
|
||||
"data_dir": %q,
|
||||
"database_path": %q,
|
||||
"auth": {
|
||||
"basic_username": "admin",
|
||||
"basic_password": "changeme",
|
||||
"fleet_secret": "test-fleet-secret"
|
||||
},
|
||||
"forge": {
|
||||
"signing_key_path": %q,
|
||||
"artifacts_dir": %q
|
||||
}
|
||||
}`, port, dir, filepath.Join(dir, "forge-mesh.db"),
|
||||
filepath.Join(dir, "signing.key"), filepath.Join(dir, "artifacts"))
|
||||
if err := os.WriteFile(cfgPath, []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return cfgPath
|
||||
}
|
||||
|
||||
func freePort(t *testing.T) int {
|
||||
t.Helper()
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
return ln.Addr().(*net.TCPAddr).Port
|
||||
}
|
||||
|
||||
func waitForHealth(t *testing.T, url string, timeout time.Duration) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
resp, err := http.Get(url)
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return
|
||||
}
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("server did not become healthy at %s within %s", url, timeout)
|
||||
}
|
||||
|
||||
func repoRoot(t *testing.T) string {
|
||||
t.Helper()
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(wd, "go.mod")); err == nil {
|
||||
return wd
|
||||
}
|
||||
parent := filepath.Dir(wd)
|
||||
if parent == wd {
|
||||
t.Fatal("go.mod not found")
|
||||
}
|
||||
wd = parent
|
||||
}
|
||||
}
|
||||
|
||||
func containsAll(s string, parts ...string) bool {
|
||||
for _, p := range parts {
|
||||
if !contains(s, p) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func contains(s, sub string) bool {
|
||||
return len(sub) == 0 || (len(s) >= len(sub) && indexOf(s, sub) >= 0)
|
||||
}
|
||||
|
||||
func indexOf(s, sub string) int {
|
||||
for i := 0; i+len(sub) <= len(s); i++ {
|
||||
if s[i:i+len(sub)] == sub {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
1
cmd/server/webroot/assets/index-BJ9Sa22q.css
Normal file
1
cmd/server/webroot/assets/index-BJ9Sa22q.css
Normal file
File diff suppressed because one or more lines are too long
232
cmd/server/webroot/assets/index-C7yIT9Qp.js
Normal file
232
cmd/server/webroot/assets/index-C7yIT9Qp.js
Normal file
File diff suppressed because one or more lines are too long
5
cmd/server/webroot/forge.svg
Normal file
5
cmd/server/webroot/forge.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||
<rect width="32" height="32" rx="6" fill="#111618"/>
|
||||
<path d="M8 22 L16 8 L24 22 Z" stroke="#00e676" stroke-width="2" fill="none"/>
|
||||
<circle cx="16" cy="18" r="2" fill="#ffb300"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 265 B |
20
cmd/server/webroot/index.html
Normal file
20
cmd/server/webroot/index.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/forge.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AetherForge Command Deck</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script type="module" crossorigin src="/assets/index-C7yIT9Qp.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BJ9Sa22q.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user