main.go
Rama main · réplica de lectura del repositorio original.
// Package main is the entry point for the keploy application.
package main
import (
"context"
"crypto/x509"
"errors"
"fmt"
"io"
"net/http"
"os"
"runtime"
"runtime/debug"
"strconv"
"strings"
"time"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"go.keploy.io/server/v3/cli"
"go.keploy.io/server/v3/cli/provider"
"go.keploy.io/server/v3/config"
"go.keploy.io/server/v3/pkg/models"
userDb "go.keploy.io/server/v3/pkg/platform/yaml/configdb/user"
"go.keploy.io/server/v3/utils"
"go.keploy.io/server/v3/utils/log"
"go.uber.org/zap"
_ "net/http/pprof"
"runtime/pprof"
// Mozilla's root list, which pkg/agent/proxy/tls already links, as Go's
// system roots when the host has none (a distroless image, or a k8s
// sidecar whose /etc/ssl/certs is an empty volume). keploy loads its own
// roots before it points SSL_CERT_FILE at files meant for the app, so
// without this such a host would leave keploy's own TLS with no roots.
_ "golang.org/x/crypto/x509roots/fallback"
)
// version is the version of the server and will be injected during build by ldflags, same with dsn
// see https://goreleaser.com/customization/build/
var version string
var dsn string
var apiServerURI = "http://localhost:8083"
var gitHubClientID = "Iv23liFBvIVhL29i9BAp"
func main() {
// Load this process's TLS roots before anything can change what
// crypto/x509 would read: keploy points SSL_CERT_FILE at files meant for
// the app, and in native mode adds its CA to the system store
// (pkg/agent/proxy/tls, loadOwnRoots).
_, _ = x509.SystemCertPool()
setVersion()
ctx := utils.NewCtx()
// An agent's teardown records its steps, and its long stages as work in
// progress, through to the MarkDetachDone below: the CLI that stops it may
// take one that stops taking them for wedged (utils.DeclareTeardownSteps).
utils.DeclareTeardownSteps()
start(ctx)
// An agent has detached everything by now: what is left is the exit
// (utils.MarkDetachDone). Its report is the agent's last, and nothing
// after it is watched, so nothing may come between the two, and
// os.Exit's argument calls nothing (TestMainReportsItsTeardownSteps).
utils.MarkDetachDone()
os.Exit(utils.ErrCode)
}
func setVersion() {
if version == "" {
version = utils.DevVersion
}
utils.Version = version
utils.VersionIdentifier = "version"
}
// newLogger is runLogger; a test swaps in one that fails. log.New itself does
// not fail where it cannot create its temp log file: it logs to the console.
var newLogger = runLogger
// runLogger is log.New, less the log file for a run whose stdout is a document
// (provider.WantsMachineOutput: --json, `report --format json`): such a run
// names no log, so it creates none, and one killed before its flags are
// checked leaves nothing in the temp directory.
func runLogger() (*zap.Logger, *os.File, error) {
if provider.WantsMachineOutput(os.Args) {
logger, err := log.NewWithoutFile()
return logger, nil, err
}
return log.New()
}
func start(ctx context.Context) {
logger, logFile, err := newLogger()
if err != nil {
fmt.Println("Failed to start the logger for the CLI", err)
utils.ErrCode = 1
return
}
utils.LogFile = logFile
// The first deferred call, so the last to run: every return of start()
// from here on, the early ones included (a failed sudo re-exec), ends the
// log, and what the other deferred calls log reaches it first.
defer func() {
if os.Getenv("KEPLOY_INDOCKER") != "true" {
// This run's own temp log (log.LogFilePattern), closed and
// removed; whatever else is at its name is not this run's. A
// native agent's log is its CLI's (log.ClientLogFileFlag), which
// it only closes: the CLI removes it.
if err := log.CloseLogFile(utils.LogFile); err != nil {
utils.LogError(logger, err, "Failed to close and remove Keploy Logs")
}
utils.LogFile = nil
}
}()
// If KEPLOY_DEBUG_FILE is set, tee debug-level log records into that
// file in addition to whatever sinks log.New configured. Used by
// `keploy cloud replay` to capture the agent container's debug
// stream into the user's keploy folder via a bind mount; the
// support-bundle pipeline picks the file up automatically since it
// lives under cfg.Path. Also useful as a generic "give me the full
// debug log without --debug printing it to my terminal" knob for
// any keploy invocation.
debugFile, debugSink := maybeAttachDebugFileSink(logger)
if debugFile != nil {
defer func() {
if debugSink != nil {
if ferr := debugSink.Flush(); ferr != nil {
logger.Warn("KEPLOY_DEBUG_FILE: flush failed", zap.Error(ferr))
}
}
if cerr := debugFile.Close(); cerr != nil {
logger.Warn("KEPLOY_DEBUG_FILE: close failed", zap.Error(cerr))
}
}()
}
// Start pprof HTTP server for live profiling if PPROF_PORT is set
if pprofPort := os.Getenv("PPROF_PORT"); pprofPort != "" {
go func() {
addr := "localhost:" + pprofPort
logger.Info("pprof server starting", zap.String("addr", addr))
if err := http.ListenAndServe(addr, nil); err != nil {
logger.Error("pprof server failed; check that PPROF_PORT is a valid port and not already in use, or unset the env var to disable",
zap.String("port", pprofPort), zap.Error(err))
}
}()
}
// Set soft memory limit for GC if GOMEMLIMIT_MB is set.
// This makes Go's GC more aggressive when approaching the limit,
// reducing peak memory during load without affecting normal operation.
if memLimitMB := os.Getenv("GOMEMLIMIT_MB"); memLimitMB != "" {
mb, err := strconv.ParseInt(memLimitMB, 10, 64)
if err != nil || mb <= 0 {
logger.Info("invalid GOMEMLIMIT_MB value, ignoring; expected a positive integer (e.g. GOMEMLIMIT_MB=150)",
zap.String("value", memLimitMB))
} else {
debug.SetMemoryLimit(mb * 1024 * 1024)
logger.Info("GOMEMLIMIT set", zap.Int64("MB", mb))
}
}
// Start background memory monitor only when GOMEMLIMIT_MB is set.
// Calls FreeOSMemory on sharp heap drops to force fast OS page release.
// Gated to avoid periodic ReadMemStats overhead for commands that don't need it.
if os.Getenv("GOMEMLIMIT_MB") != "" {
go func() {
var prevInUse uint64
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
var m runtime.MemStats
runtime.ReadMemStats(&m)
if prevInUse > 0 && m.HeapInuse < prevInUse/2 {
debug.FreeOSMemory()
}
prevInUse = m.HeapInuse
}
}
}()
}
// Early check: If Docker command detected and not running as root, re-exec with sudo
// This must happen before any other initialization to ensure clean process handoff
if utils.ShouldReexecWithSudo() {
// ReexecWithSudo calls syscall.Exec which replaces the process, so this
// line is only reached if there's an error, which it has said. The run
// ends here, through start()'s deferred calls, which remove its log.
if err := utils.ReexecWithSudo(logger); err != nil {
utils.ErrCode = utils.ExitKeployError
}
return
}
// Nudge users of this open-source build toward Keploy. Placed AFTER the
// sudo re-exec gate (mirroring where the logo prints via the cobra
// PreRunE in cli/provider/cmd.go) so the original process is already
// replaced by syscall.Exec before this runs — guarantees the banner
// prints exactly once across the docker/cloud-replay re-exec path.
// Same-binary-only: lives in this main package, the enterprise
// binary has its own main.go and doesn't compile this in.
printEnterpriseUpgradeBanner()
if cpuProfile := os.Getenv("CPU_PROFILE"); cpuProfile != "" {
f, err := os.Create(cpuProfile)
if err != nil {
logger.Error("could not create CPU profile", zap.Error(err))
} else {
if err := pprof.StartCPUProfile(f); err != nil {
logger.Error("could not start CPU profile", zap.Error(err))
f.Close()
} else {
logger.Info("CPU profiling enabled", zap.String("file", cpuProfile))
defer func() {
pprof.StopCPUProfile()
f.Close()
logger.Info("CPU profiling stopped", zap.String("file", cpuProfile))
}()
}
}
}
if heapProfile := os.Getenv("HEAP_PROFILE"); heapProfile != "" {
logger.Info("Heap profiling enabled", zap.String("file", heapProfile))
defer func() {
f, err := os.Create(heapProfile)
if err != nil {
logger.Error("could not create Heap profile", zap.Error(err))
return
}
defer f.Close()
runtime.GC() // get up-to-date statistics
if err := pprof.WriteHeapProfile(f); err != nil {
logger.Error("could not write Heap profile", zap.Error(err))
} else {
logger.Info("Heap profile written", zap.String("file", heapProfile))
}
}()
}
defer func() {
if os.Getenv("KEPLOY_INDOCKER") != "true" {
if err := utils.DeleteFileIfExists(logger, "docker-compose-tmp.yaml"); err != nil {
return
}
}
}()
defer utils.Recover(logger)
// The 'umask' command is commonly used in various operating systems to regulate the permissions of newly created files.
// These 'umask' values subtract from the permissions assigned by the process, effectively lowering the permissions.
// For example, if a file is created with permissions '777' and the 'umask' is '022', the resulting permissions will be '755',
// reducing certain permissions for security purposes.
// Setting 'umask' to '0' ensures that 'keploy' can precisely control the permissions of the files it creates.
// However, it's important to note that this approach may not work in scenarios involving mounted volumes,
// as the 'umask' is set by the host system, and cannot be overridden by 'keploy' or individual processes.
oldMask := utils.SetUmask()
defer utils.RestoreUmask(oldMask)
// Armed only: the crash-report client starts once the command has read
// --disable-tele and keploy.yml, and only with telemetry on
// (utils.ResolveCrashReports, from ValidateFlags).
utils.ArmCrashReports(dsn)
conf := config.New()
conf.APIServerURL = apiServerURI
conf.GitHubClientID = gitHubClientID
// Capture the full command used for test runs (to be stored in report)
conf.Test.CmdUsed = utils.GetFullCommandUsed()
userDb := userDb.New(logger, conf)
conf.InstallationID, err = userDb.GetInstallationID(ctx)
if err != nil {
errMsg := "failed to get installation id"
utils.LogError(logger, err, errMsg)
// Returned, not os.Exit(1): the run ends through its deferred calls,
// which remove its temp log.
utils.ErrCode = utils.ExitKeployError
return
}
svcProvider := provider.NewServiceProvider(logger, conf)
cmdConfigurator := provider.NewCmdConfigurator(logger, conf)
rootCmd := cli.Root(ctx, logger, svcProvider, cmdConfigurator)
// ExecuteC's command is the one cobra was running when it failed (the
// root at worst): a usage error points at its --help, so a flag record
// does not have points at `keploy record --help`, where record's flags
// are, not at the root's, which lists only commands.
if cmd, err := rootCmd.ExecuteC(); err != nil {
err = cli.HintUnknownCommand(rootCmd, err)
utils.ErrCode = finalExitCode(err, utils.ErrCode, cmd.CommandPath(), os.Stderr)
}
// Restore keploy folder ownership if running under sudo (for Docker mode)
// so the next native run does not hit permission issues.
//
// Only after a command that can WRITE there. conf.Path is non-empty only
// once ValidateFlags has run, which is how this used to be gated -- an
// incidental signal, and one that turned `sudo keploy --version` into a
// recursive chown of the working tree the moment the config default for
// path stopped being the empty string. The commands that create a keploy/
// directory are the ones that get the restore.
if conf.Path != "" && writesKeployFolder(rootCmd, os.Args[1:]) {
utils.RestoreKeployFolderOwnership(logger, conf.Path)
}
}
// writesKeployFolder reports whether the command that just ran is one that
// can create files under conf.Path. Everything else -- version, help, config,
// login, a command that failed before it started -- has nothing to restore,
// and a chown -R it never earned is a destructive thing to do on the strength
// of a guess.
func writesKeployFolder(root *cobra.Command, args []string) bool {
writes := map[string]bool{
"record": true, "test": true, "mock": true, "normalize": true,
"templatize": true, "contract": true, "import": true, "export": true,
"sanitize": true, "diff": true,
}
// Cobra resolves it, because argv cannot be read by eye: a root flag that
// takes a separate value (--storage-format json) puts a non-flag word
// before the command, and reading the first such word called that word
// the command. `keploy --storage-format json record` then answered "json",
// the restore was skipped, and a docker-mode record under sudo left the
// developer's keploy/ tree root-owned.
if root == nil {
return false
}
cmd, _, err := root.Find(args)
if err != nil || cmd == nil {
return false
}
// A command that only printed its own help wrote nothing.
if help, hErr := cmd.Flags().GetBool("help"); hErr == nil && help {
return false
}
for c := cmd; c != nil; c = c.Parent() {
if writes[c.Name()] {
return true
}
}
return false
}
// maybeAttachDebugFileSink reads KEPLOY_DEBUG_FILE and, if set, opens that
// path and attaches a debug-level file sink onto logger via the same
// in-place pointee mutation pattern the rest of the codebase uses for
// runtime logger swaps (see ChangeLogLevel / RedirectToStderr). Returns
// the opened file and sink handle so the caller can defer Flush + Close.
//
// All errors are non-fatal — if the file can't be opened (read-only mount,
// permission, etc.), the process continues with the original logger.
func maybeAttachDebugFileSink(logger *zap.Logger) (*os.File, *log.DebugFileSink) {
path := strings.TrimSpace(os.Getenv("KEPLOY_DEBUG_FILE"))
if path == "" {
return nil, nil
}
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
if err != nil {
logger.Warn("KEPLOY_DEBUG_FILE set but could not be opened; continuing without debug capture",
zap.String("path", path), zap.Error(err))
return nil, nil
}
wrapped, sink := log.AddDebugFileSink(logger, f, log.FileCapBytes)
if wrapped == nil || sink == nil {
_ = f.Close()
return nil, nil
}
*logger = *wrapped
// Publish the sink so cross-package helpers (e.g. the agent's
// per-test-set rotation in pkg/agent/routes) can reach it without
// threading the sink through every constructor.
log.SetDebugFileSink(sink)
logger.Debug("KEPLOY_DEBUG_FILE attached", zap.String("path", path))
return f, sink
}
// printEnterpriseUpgradeBanner emits a high-visibility nudge to install
// Keploy from keploy.io — free with an account — which adds the broader
// protocol/dependency set, native macOS and Windows recording, and the AI
// features that this open-source build doesn't ship, and Podman. User-facing text names
// no editions: the product is just "keploy".
//
// Lives in the OSS binary's main.go (not in cli/root.go) so the
// enterprise binary — which has its own main.go and does not import
// this file — naturally never prints it. Avoids cross-module flag
// plumbing through go.keploy.io/server/v3 entirely.
//
// Kept in main.go (not a sibling file in the same package) because the
// Windows CI build invokes `go build ./main.go` — a single-file build
// that excludes sibling .go files in the same package. Splitting this
// out broke build-windows-amd64.
//
// De-duplication mirrors the logo's pattern (CmdConfigurator.ValidateFlags
// in cli/provider/cmd.go): the call site sits AFTER the sudo re-exec gate
// in start(), so the pre-exec process is already replaced before this
// runs (sudo / docker / cloud-replay re-exec paths print only once). On
// top of that:
// - Skip when os.Args[1] == "agent" — the inner subprocess that
// `keploy record/test` spawns via exec.Command. Mirrors the logo's
// `if cmd.Name() != "agent"` skip.
// - Skip when KEPLOY_INDOCKER=true — the containerized agent child of
// an outer host invocation (existing keploy convention).
//
// Banner goes to stderr so it never contaminates a piped stdout
// (e.g. `keploy --version | grep`, `keploy config | yq ...`).
//
// ANSI suppression honors:
// - NO_COLOR=<any non-empty value> (industry-standard, see no-color.org)
// - --disable-ansi flag if present anywhere in os.Args (matches the
// cobra flag the rest of the CLI respects; checked manually here
// because cobra hasn't parsed yet at this point in startup).
func printEnterpriseUpgradeBanner() {
// Skip for the spawned agent child (keploy record/test launches
// `keploy agent --port ... --proxy-port ...` via exec.Command).
// Same dedup signal the logo uses inside ValidateFlags.
if len(os.Args) >= 2 && os.Args[1] == "agent" {
return
}
// Skip when running as a containerized agent child of an outer
// keploy invocation. The host-side process already printed; this
// in-container instance shouldn't print again. KEPLOY_INDOCKER is
// set by the parent that spawned the container.
if os.Getenv("KEPLOY_INDOCKER") == "true" {
return
}
disableAnsi := os.Getenv("NO_COLOR") != ""
if !disableAnsi {
for _, arg := range os.Args[1:] {
if arg == "--disable-ansi" || arg == "--disable-ansi=true" {
disableAnsi = true
break
}
}
}
orange := "\033[38;5;208m"
bold := "\033[1m"
dim := "\033[2m"
reset := "\033[0m"
if disableAnsi {
orange, bold, dim, reset = "", "", "", ""
}
bar := "═══════════════════════════════════════════════════════════════════════════════"
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr, orange+bar+reset)
fmt.Fprintf(os.Stderr, " %s🚀 TRY THE FULL KEPLOY (FREE)%s\n", bold+orange, reset)
fmt.Fprintln(os.Stderr, " This is Keploy's open-source build. Keploy from keploy.io (free with an account) adds:")
fmt.Fprintln(os.Stderr, " • PostgreSQL, MongoDB, gRPC, HTTP/2, Kafka — on top of this build's HTTP + MySQL")
fmt.Fprintln(os.Stderr, " • Recording apps running natively on macOS and Windows")
fmt.Fprintln(os.Stderr, " • Recording and testing apps that run in Podman")
fmt.Fprintln(os.Stderr, " • AI-powered test generation, sandbox replay, MCP for AI agents")
fmt.Fprintln(os.Stderr, " (Claude Code, Cursor, Copilot, Gemini, …)")
fmt.Fprintln(os.Stderr, " "+dim+"Install:"+reset+" "+bold+models.InstallCommand+reset)
fmt.Fprintln(os.Stderr, orange+bar+reset)
fmt.Fprintln(os.Stderr)
}
// finalExitCode is the process's exit code once the root command has
// returned: whatever already mirrors the wrapped runner, else the error's.
//
// `keploy mock` promises to propagate the test runner's exit code, and it puts
// that code in utils.ErrCode. A command that ALSO returns an error -- a runner
// that died while keploy was still bringing the environment up -- had it
// overwritten here with a generic 1, so the same crash exited 7 or 1 depending
// on which of two racing paths reported it. The contract every caller relies
// on is unchanged: an error still exits non-zero, because a mirrored code is
// never 0.
func finalExitCode(err error, current int, cmdPath string, w io.Writer) int {
if err == nil {
return current
}
if current != 0 {
return current
}
return exitCodeForCmdErr(err, cmdPath, w)
}
// isFlagError reports whether err is one of pflag's command-line parse errors.
// Asked only of a usage error, to decide whether it has been printed already:
// whether it IS a usage error is utils.IsUsageError's call, made from the tag.
func isFlagError(err error) bool {
var notExist *pflag.NotExistError
var valueRequired *pflag.ValueRequiredError
var invalidValue *pflag.InvalidValueError
var invalidSyntax *pflag.InvalidSyntaxError
return errors.As(err, ¬Exist) || errors.As(err, &valueRequired) ||
errors.As(err, &invalidValue) || errors.As(err, &invalidSyntax)
}
// exitCodeForCmdErr maps an error returned by the root command onto the
// process exit code, writing the error (unless it was printed where it was
// raised) and the hint to run cmdPath's --help to w for a usage error.
//
// Any non-nil error means the command did not run successfully, so the
// process must not report success. This used to convert only "unknown
// command" and "unknown shorthand" into a non-zero exit; every other error
// fell through with utils.ErrCode still 0. That silently exited 0 for all
// flag-parsing errors - CmdConfigurator.AddFlags sets SilenceErrors and
// prints them through SetFlagErrorFunc, so `keploy test --typo` showed a red
// error message and still reported success to the shell and to CI.
func exitCodeForCmdErr(err error, cmdPath string, w io.Writer) int {
if err == nil {
return 0
}
if utils.IsUsageError(err) {
// A mistyped or unknown command/verb, a flag that could not be parsed,
// or the wrong number of arguments is a usage error, not a Keploy
// failure — the command never ran. ExitUsageError (8) lets CI tell
// that apart from a real failure (1). Covers unknown `mock` verbs,
// which previously printed help and exited 0 (design §P0b).
//
// A flag error was printed where it was raised, by the command's
// flag-error func; nothing has printed anything else yet (every
// command silences cobra's own printing), so say it here.
if !isFlagError(err) {
fmt.Fprintln(w, "Error:", err.Error())
}
fmt.Fprintf(w, "Run '%s --help' for usage.\n", cmdPath)
return utils.ExitUsageError
}
return utils.ExitKeployError
}