Ctrl K
ComunidadGuías…

main.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// 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, &notExist) || 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
}