fix(web): improve version parsing and timeout behavior
This commit is contained in:
parent
18ec2631aa
commit
c2d2ee6ae8
3 changed files with 216 additions and 27 deletions
|
|
@ -35,14 +35,15 @@ func getPidPath() string {
|
|||
}
|
||||
|
||||
func isProcessRunning(pid int) bool {
|
||||
if runtime.GOOS == "windows" {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
cmd := exec.Command("tasklist", "/FI", fmt.Sprintf("PID eq %d", pid))
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(string(output), strconv.Itoa(pid))
|
||||
} else if runtime.GOOS == "darwin" {
|
||||
case "darwin":
|
||||
cmd := exec.Command("ps", "aux")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
"github.com/sipeed/picoclaw/web/backend/utils"
|
||||
)
|
||||
|
||||
type systemVersionResponse struct {
|
||||
|
|
@ -14,18 +22,116 @@ type systemVersionResponse struct {
|
|||
GoVersion string `json:"go_version"`
|
||||
}
|
||||
|
||||
var (
|
||||
// Reuse the launcher gateway startup window so embedded/slow devices
|
||||
// have enough time for first-run command initialization.
|
||||
versionCmdTimeout = gatewayStartupWindow
|
||||
findPicoclawBinaryForInfo = utils.FindPicoclawBinary
|
||||
runPicoclawVersionOutput = executePicoclawVersion
|
||||
versionLinePattern = regexp.MustCompile(`\bpicoclaw\s+([^\s(]+)(?:\s+\(git:\s*([^)]+)\))?`)
|
||||
ansiEscapePattern = regexp.MustCompile(`\x1b\[[0-9;]*m`)
|
||||
)
|
||||
|
||||
func (h *Handler) registerVersionRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /api/system/version", h.handleGetVersion)
|
||||
}
|
||||
|
||||
// handleGetVersion returns runtime version information for web clients.
|
||||
func (h *Handler) handleGetVersion(w http.ResponseWriter, _ *http.Request) {
|
||||
buildTime, goVer := config.FormatBuildInfo()
|
||||
versionInfo := h.resolveSystemVersionInfo()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(systemVersionResponse{
|
||||
json.NewEncoder(w).Encode(versionInfo)
|
||||
}
|
||||
|
||||
// resolveSystemVersionInfo prefers the actual picoclaw binary version output,
|
||||
// and falls back to launcher build metadata when command execution fails.
|
||||
func (h *Handler) resolveSystemVersionInfo() systemVersionResponse {
|
||||
buildTime, goVer := config.FormatBuildInfo()
|
||||
fallback := systemVersionResponse{
|
||||
Version: config.GetVersion(),
|
||||
GitCommit: config.GitCommit,
|
||||
BuildTime: buildTime,
|
||||
GoVersion: goVer,
|
||||
})
|
||||
}
|
||||
|
||||
execPath := strings.TrimSpace(findPicoclawBinaryForInfo())
|
||||
if execPath == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), versionCmdTimeout)
|
||||
defer cancel()
|
||||
|
||||
output, err := runPicoclawVersionOutput(ctx, execPath)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
|
||||
parsed, ok := parsePicoclawVersionOutput(output)
|
||||
if !ok {
|
||||
return fallback
|
||||
}
|
||||
|
||||
if parsed.GoVersion == "" {
|
||||
parsed.GoVersion = fallback.GoVersion
|
||||
if parsed.GoVersion == "" {
|
||||
parsed.GoVersion = runtime.Version()
|
||||
}
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
// executePicoclawVersion runs version commands against the discovered picoclaw
|
||||
// executable. It tries subcommand form first, then --version fallback.
|
||||
func executePicoclawVersion(ctx context.Context, execPath string) (string, error) {
|
||||
out, err := exec.CommandContext(ctx, execPath, "version").CombinedOutput()
|
||||
if err == nil {
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
flagOut, flagErr := exec.CommandContext(ctx, execPath, "--version").CombinedOutput()
|
||||
if flagErr == nil {
|
||||
return string(flagOut), nil
|
||||
}
|
||||
|
||||
return string(out), fmt.Errorf("failed to execute version command: %w", err)
|
||||
}
|
||||
|
||||
// parsePicoclawVersionOutput extracts version/build/go fields from CLI output.
|
||||
// It accepts banner/ANSI-decorated output and only requires the version line.
|
||||
func parsePicoclawVersionOutput(raw string) (systemVersionResponse, bool) {
|
||||
var result systemVersionResponse
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(raw))
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(ansiEscapePattern.ReplaceAllString(scanner.Text(), ""))
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if match := versionLinePattern.FindStringSubmatch(line); len(match) > 0 {
|
||||
result.Version = strings.TrimSpace(match[1])
|
||||
if len(match) > 2 {
|
||||
result.GitCommit = strings.TrimSpace(match[2])
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if buildValue, ok := strings.CutPrefix(line, "Build:"); ok {
|
||||
result.BuildTime = strings.TrimSpace(buildValue)
|
||||
continue
|
||||
}
|
||||
|
||||
if goValue, ok := strings.CutPrefix(line, "Go:"); ok {
|
||||
result.GoVersion = strings.TrimSpace(goValue)
|
||||
}
|
||||
}
|
||||
|
||||
if result.Version == "" {
|
||||
return systemVersionResponse{}, false
|
||||
}
|
||||
|
||||
return result, true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"runtime"
|
||||
|
|
@ -10,22 +12,88 @@ import (
|
|||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
)
|
||||
|
||||
func TestGetSystemVersion(t *testing.T) {
|
||||
func TestGetSystemVersionUsesPicoclawBinaryInfo(t *testing.T) {
|
||||
originalVersion := config.Version
|
||||
originalGitCommit := config.GitCommit
|
||||
originalBuildTime := config.BuildTime
|
||||
originalGoVersion := config.GoVersion
|
||||
originalFinder := findPicoclawBinaryForInfo
|
||||
originalRunner := runPicoclawVersionOutput
|
||||
t.Cleanup(func() {
|
||||
config.Version = originalVersion
|
||||
config.GitCommit = originalGitCommit
|
||||
config.BuildTime = originalBuildTime
|
||||
config.GoVersion = originalGoVersion
|
||||
findPicoclawBinaryForInfo = originalFinder
|
||||
runPicoclawVersionOutput = originalRunner
|
||||
})
|
||||
|
||||
config.Version = "v1.2.3"
|
||||
config.GitCommit = "deadbeef"
|
||||
config.BuildTime = "2026-03-27T12:34:56Z"
|
||||
config.GoVersion = "go1.24.1"
|
||||
config.Version = "dev"
|
||||
config.GitCommit = ""
|
||||
config.BuildTime = ""
|
||||
config.GoVersion = ""
|
||||
|
||||
findPicoclawBinaryForInfo = func() string { return "picoclaw" }
|
||||
runPicoclawVersionOutput = func(_ context.Context, _ string) (string, error) {
|
||||
return "🦞 picoclaw v1.2.3 (git: deadbeef)\n Build: 2026-03-27T12:34:56Z\n Go: go1.25.8\n", nil
|
||||
}
|
||||
|
||||
h := NewHandler("")
|
||||
mux := http.NewServeMux()
|
||||
h.RegisterRoutes(mux)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/system/version", nil)
|
||||
mux.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
|
||||
var got systemVersionResponse
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
|
||||
t.Fatalf("unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
if got.Version != "v1.2.3" {
|
||||
t.Fatalf("version = %q, want %q", got.Version, "v1.2.3")
|
||||
}
|
||||
if got.GitCommit != "deadbeef" {
|
||||
t.Fatalf("git_commit = %q, want %q", got.GitCommit, "deadbeef")
|
||||
}
|
||||
if got.BuildTime != "2026-03-27T12:34:56Z" {
|
||||
t.Fatalf("build_time = %q, want %q", got.BuildTime, "2026-03-27T12:34:56Z")
|
||||
}
|
||||
if got.GoVersion != "go1.25.8" {
|
||||
t.Fatalf("go_version = %q, want %q", got.GoVersion, "go1.25.8")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSystemVersionFallsBackToLauncherInfoWhenCommandFails(t *testing.T) {
|
||||
originalVersion := config.Version
|
||||
originalGitCommit := config.GitCommit
|
||||
originalBuildTime := config.BuildTime
|
||||
originalGoVersion := config.GoVersion
|
||||
originalFinder := findPicoclawBinaryForInfo
|
||||
originalRunner := runPicoclawVersionOutput
|
||||
t.Cleanup(func() {
|
||||
config.Version = originalVersion
|
||||
config.GitCommit = originalGitCommit
|
||||
config.BuildTime = originalBuildTime
|
||||
config.GoVersion = originalGoVersion
|
||||
findPicoclawBinaryForInfo = originalFinder
|
||||
runPicoclawVersionOutput = originalRunner
|
||||
})
|
||||
|
||||
config.Version = "v9.9.9"
|
||||
config.GitCommit = "cafebabe"
|
||||
config.BuildTime = "2026-03-27T10:43:34+0000"
|
||||
config.GoVersion = "go1.25.8"
|
||||
|
||||
findPicoclawBinaryForInfo = func() string { return "picoclaw" }
|
||||
runPicoclawVersionOutput = func(_ context.Context, _ string) (string, error) {
|
||||
return "", errors.New("binary unavailable")
|
||||
}
|
||||
|
||||
h := NewHandler("")
|
||||
mux := http.NewServeMux()
|
||||
|
|
@ -58,16 +126,40 @@ func TestGetSystemVersion(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGetSystemVersionUsesRuntimeGoVersionFallback(t *testing.T) {
|
||||
func TestParsePicoclawVersionOutput(t *testing.T) {
|
||||
raw := "\u001b[1;31m████\u001b[0m\n🦞 picoclaw 18ec263 (git: 18ec2631)\n Build: 2026-03-27T10:43:34+0000\n Go: go1.25.8\n"
|
||||
got, ok := parsePicoclawVersionOutput(raw)
|
||||
if !ok {
|
||||
t.Fatal("parsePicoclawVersionOutput() should parse valid output")
|
||||
}
|
||||
if got.Version != "18ec263" {
|
||||
t.Fatalf("version = %q, want %q", got.Version, "18ec263")
|
||||
}
|
||||
if got.GitCommit != "18ec2631" {
|
||||
t.Fatalf("git_commit = %q, want %q", got.GitCommit, "18ec2631")
|
||||
}
|
||||
if got.BuildTime != "2026-03-27T10:43:34+0000" {
|
||||
t.Fatalf("build_time = %q, want %q", got.BuildTime, "2026-03-27T10:43:34+0000")
|
||||
}
|
||||
if got.GoVersion != "go1.25.8" {
|
||||
t.Fatalf("go_version = %q, want %q", got.GoVersion, "go1.25.8")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveSystemVersionInfoFallsBackRuntimeGoVersion(t *testing.T) {
|
||||
originalVersion := config.Version
|
||||
originalGitCommit := config.GitCommit
|
||||
originalBuildTime := config.BuildTime
|
||||
originalGoVersion := config.GoVersion
|
||||
originalFinder := findPicoclawBinaryForInfo
|
||||
originalRunner := runPicoclawVersionOutput
|
||||
t.Cleanup(func() {
|
||||
config.Version = originalVersion
|
||||
config.GitCommit = originalGitCommit
|
||||
config.BuildTime = originalBuildTime
|
||||
config.GoVersion = originalGoVersion
|
||||
findPicoclawBinaryForInfo = originalFinder
|
||||
runPicoclawVersionOutput = originalRunner
|
||||
})
|
||||
|
||||
config.Version = "dev"
|
||||
|
|
@ -75,23 +167,13 @@ func TestGetSystemVersionUsesRuntimeGoVersionFallback(t *testing.T) {
|
|||
config.BuildTime = ""
|
||||
config.GoVersion = ""
|
||||
|
||||
findPicoclawBinaryForInfo = func() string { return "picoclaw" }
|
||||
runPicoclawVersionOutput = func(_ context.Context, _ string) (string, error) {
|
||||
return "picoclaw v1.0.0\n", nil
|
||||
}
|
||||
|
||||
h := NewHandler("")
|
||||
mux := http.NewServeMux()
|
||||
h.RegisterRoutes(mux)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/system/version", nil)
|
||||
mux.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
|
||||
var got systemVersionResponse
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
|
||||
t.Fatalf("unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
got := h.resolveSystemVersionInfo()
|
||||
if got.GoVersion != runtime.Version() {
|
||||
t.Fatalf("go_version = %q, want runtime version %q", got.GoVersion, runtime.Version())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue