Ctrl K
ComunidadGuías…

utils/utils.go

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

Archivo original ↗Preguntar a KiBot
package utils

import (
	"bufio"
	"context"
	"crypto/rand"
	"crypto/sha256"
	"debug/elf"
	"encoding/binary"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"math/big"
	"net"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"os/user"
	"path/filepath"
	"regexp"
	"runtime"
	"runtime/debug"
	"slices"
	"strconv"
	"strings"
	"syscall"
	"time"

	"golang.org/x/text/cases"
	"golang.org/x/text/language"

	"github.com/getsentry/sentry-go"
	"github.com/spf13/cobra"
	"github.com/spf13/pflag"
	"github.com/spf13/viper"
	"go.keploy.io/server/v3/pkg/models"
	keployLog "go.keploy.io/server/v3/utils/log"
	"go.keploy.io/server/v3/utils/plaintcp"
	"go.uber.org/zap"
	"helm.sh/helm/v3/pkg/strvals"
)

var WarningSign = "\U000026A0"

var ErrCode = 0

// IsShutdownError checks if the error is related to shutdown (EOF, connection closed, etc.)
// This is useful for gracefully handling errors during application shutdown.
func IsShutdownError(err error) bool {
	if err == nil {
		return false
	}
	// Check for EOF errors
	if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
		return true
	}
	// Check error message for common shutdown-related patterns
	errStr := err.Error()
	return strings.Contains(errStr, "EOF") ||
		strings.Contains(errStr, "connection refused") ||
		strings.Contains(errStr, "connection reset") ||
		strings.Contains(errStr, "broken pipe") ||
		strings.Contains(errStr, "use of closed network connection")
}

func ReplaceHost(currentURL string, ipAddress string) (string, error) {
	// Parse the current URL
	parsedURL, err := url.Parse(currentURL)

	if err != nil {
		// Return the original URL if parsing fails
		return currentURL, err
	}

	if ipAddress == "" {
		return currentURL, fmt.Errorf("failed to replace url in case of docker env")
	}

	// Replace hostname with the IP address
	parsedURL.Host = strings.Replace(parsedURL.Host, parsedURL.Hostname(), ipAddress, 1)
	// Return the modified URL
	return parsedURL.String(), nil
}

func ReplaceGrpcHost(authority string, ipAddress string) (string, error) {
	// Check if ipAddress is empty
	if ipAddress == "" {
		return authority, fmt.Errorf("failed to replace authority in case of docker env: empty IP address")
	}

	// Use net.SplitHostPort to properly handle IPv6 addresses
	_, port, err := net.SplitHostPort(authority)
	if err != nil {
		return authority, fmt.Errorf("invalid authority format: %v", err)
	}

	// Replace the host part with ipAddress, keeping the port
	return net.JoinHostPort(ipAddress, port), nil
}

func ReplaceGrpcPort(authority string, port string) (string, error) {
	// Check if port is empty
	if port == "" {
		return authority, fmt.Errorf("failed to replace port in case of docker env: empty port")
	}

	// Use net.SplitHostPort to properly handle IPv6 addresses
	host, _, err := net.SplitHostPort(authority)
	if err != nil {
		// If splitting fails, assume it's just a host without port
		return net.JoinHostPort(authority, port), nil
	}

	// Replace the port part, keeping the host
	return net.JoinHostPort(host, port), nil
}

// ReplaceBaseURL replaces the base URL (scheme + host) of the given URL with the provided baseURL.
// It returns the updated URL as a string or an error if the operation fails.
func ReplaceBaseURL(currentURL string, baseURL string) (string, error) {
	// Parse the current URL
	parsedURL, err := url.Parse(currentURL)
	if err != nil {
		return currentURL, err
	}

	// Check if baseURL is valid
	if baseURL == "" {
		return currentURL, fmt.Errorf("failed to replace baseURL: baseURL is empty")
	}

	// Parse the new baseURL
	newBaseURL, err := url.Parse(baseURL)
	if err != nil {
		return currentURL, fmt.Errorf("invalid baseURL: %w", err)
	}

	// Replace the scheme and host
	parsedURL.Scheme = newBaseURL.Scheme
	parsedURL.Host = newBaseURL.Host

	// Return the updated URL as a string
	return parsedURL.String(), nil
}

func ReplacePort(currentURL string, port string) (string, error) {
	if port == "" {
		return currentURL, fmt.Errorf("failed to replace port in case of docker env")
	}

	parsedURL, err := url.Parse(currentURL)

	if err != nil {
		return currentURL, err
	}

	if parsedURL.Port() == "" {
		parsedURL.Host = parsedURL.Host + ":" + port
	} else {
		parsedURL.Host = strings.Replace(parsedURL.Host, parsedURL.Port(), port, 1)
	}

	return parsedURL.String(), nil
}

// GetReqMeta is what a log says about req: its method, its host, its path's
// shape (URLPathShape) and the names of its query parameters. Never a path
// segment, a query value, a header or the body: those are the application's
// content (a segment may be a token the recording redacted), and
// logs get pasted into issues. A query part with no "=" is not named either,
// since it may be a bare token; a bare token that holds "=" (padded base64,
// ?dG9rZW4=) is named by what comes before its first "=" (dG9rZW4).
func GetReqMeta(req *http.Request) map[string]string {
	reqMeta := map[string]string{}
	if req == nil {
		return reqMeta
	}
	reqMeta["method"] = req.Method
	reqMeta["host"] = req.Host
	if req.URL != nil {
		reqMeta["path"] = URLPathShape(req.URL)
		if keys := queryKeys(req.URL.RawQuery); len(keys) > 0 {
			reqMeta["query keys"] = strings.Join(keys, ",")
		}
	}
	return reqMeta
}

// URLPathShape is u's path in its shape: each segment of the escaped path
// written {}, an empty one (a trailing "/", "//") kept empty, so it names
// how many segments the path has and never a value of one. A log made where
// no recording is at hand names a path this way: keploy does not know there
// which segment holds a secret the recording redacted. "" for a nil u.
func URLPathShape(u *url.URL) string {
	if u == nil {
		return ""
	}
	escaped := u.EscapedPath()
	segs := strings.Split(strings.TrimPrefix(escaped, "/"), "/")
	for i, seg := range segs {
		if seg != "" {
			segs[i] = "{}"
		}
	}
	out := strings.Join(segs, "/")
	if strings.HasPrefix(escaped, "/") {
		out = "/" + out
	}
	return out
}

// queryKeys are the names of rawQuery's name=value parameters, unescaped,
// sorted and each once. A name ends at its part's first "=": a value may
// hold "=" too (base64 padding).
func queryKeys(rawQuery string) []string {
	var keys []string
	for _, part := range strings.Split(rawQuery, "&") {
		k, _, ok := strings.Cut(part, "=")
		if !ok || k == "" {
			continue
		}
		if u, err := url.QueryUnescape(k); err == nil {
			k = u
		}
		keys = append(keys, k)
	}
	slices.Sort(keys)
	return slices.Compact(keys)
}

func IsPassThrough(logger *zap.Logger, req *http.Request, destPort uint, opts models.OutgoingOptions) bool {
	passThrough := false

	for _, bypass := range opts.Rules {
		if bypass.Host != "" {
			regex, err := regexp.Compile(bypass.Host)
			if err != nil {
				LogError(logger, err, "failed to compile the host regex", zap.Any("metadata", GetReqMeta(req)))
				continue
			}
			passThrough = regex.MatchString(req.Host)
			if !passThrough {
				continue
			}
		}
		if bypass.Path != "" {
			regex, err := regexp.Compile(bypass.Path)
			if err != nil {
				LogError(logger, err, "failed to compile the path regex", zap.Any("metadata", GetReqMeta(req)))
				// The rule's host may have matched; a path that does not
				// compile matches no call.
				passThrough = false
				continue
			}
			passThrough = regex.MatchString(req.URL.String())
			if !passThrough {
				continue
			}
		}

		if passThrough {
			if bypass.Port == 0 || bypass.Port == destPort {
				return true
			}
			passThrough = false
		}
	}

	return passThrough
}

func kebabToCamel(s string) string {
	parts := strings.Split(s, "-")
	for i := 1; i < len(parts); i++ {
		parts[i] = cases.Title(language.English).String(parts[i])
	}
	return strings.Join(parts, "")
}

// ConfigKeyAnnotation marks a flag whose value keploy moves into the config
// itself, under the config key the annotation names, so BindFlagsToViper does
// not bind it to a key of its own name. Without the mark, record's
// --pass-through-ports (a port list, which ValidateFlags turns into
// bypassRules) was bound to record.passThroughPorts, which holds telemetry
// rules, and the config failed to load whenever the flag was passed.
const ConfigKeyAnnotation = "keploy.io/config-key"

// BindFlagsToViper binds each flag of cmd to the config key of its own name,
// at the top level and under viperKeyPrefix (the command's name by default),
// and that key to its environment variable. A flag marked with
// ConfigKeyAnnotation is left out.
func BindFlagsToViper(logger *zap.Logger, cmd *cobra.Command, viperKeyPrefix string) error {
	var bindErr error
	cmd.Flags().VisitAll(func(flag *pflag.Flag) {
		if _, ok := flag.Annotations[ConfigKeyAnnotation]; ok {
			return
		}
		camelCaseName := kebabToCamel(flag.Name)
		err := viper.BindPFlag(camelCaseName, flag)
		if err != nil {
			LogError(logger, err, "failed to bind flag Name to flag")
			bindErr = err
		}
		// Construct the Viper key and the env variable name
		if viperKeyPrefix == "" {
			viperKeyPrefix = cmd.Name()
		}
		viperKey := viperKeyPrefix + "." + camelCaseName
		envVarName := strings.ToUpper(viperKeyPrefix + "_" + camelCaseName)
		envVarName = strings.ReplaceAll(envVarName, ".", "_") // Why do we need this?

		// Bind the flag to Viper with the constructed key
		err = viper.BindPFlag(viperKey, flag)
		if err != nil {
			LogError(logger, err, "failed to bind flag to config")
			bindErr = err
		}

		// Tell Viper to also read this flag's value from the corresponding env variable
		err = viper.BindEnv(viperKey, envVarName)
		logger.Debug("Binding flag to viper", zap.String("viperKey", viperKey), zap.String("envVarName", envVarName))
		if err != nil {
			LogError(logger, err, "failed to bind environment variables to config")
			bindErr = err
		}
	})
	return bindErr
}

//func ModifyToSentryLogger(ctx context.Context, logger *zap.Logger, client *sentry.Client, configDb *configdb.ConfigDb) *zap.Logger {
//	cfg := zapsentry.Configuration{
//		Level:             zapcore.ErrorLevel, //when to send message to sentry
//		EnableBreadcrumbs: true,               // enable sending breadcrumbs to Sentry
//		BreadcrumbLevel:   zapcore.InfoLevel,  // at what level should we sent breadcrumbs to sentry
//		Tags: map[string]string{
//			"component": "system",
//		},
//	}
//
//	core, err := zapsentry.NewCore(cfg, zapsentry.NewSentryClientFromClient(client))
//	//in case of err it will return noop core. So we don't need to attach it to log.
//	if err != nil {
//		logger.Debug("failed to init zap", zap.Error(err))
//		return logger
//	}
//
//	logger = zapsentry.AttachCoreToLogger(core, logger)
//	kernelVersion := ""
//	if runtime.GOOS == "linux" {
//		cmd := exec.CommandContext(ctx, "uname", "-r")
//		kernelBytes, err := cmd.Output()
//		if err != nil {
//			logger.Debug("failed to get kernel version", zap.Error(err))
//		} else {
//			kernelVersion = string(kernelBytes)
//		}
//	}
//
//	arch := runtime.GOARCH
//	installationID, err := configDb.GetInstallationId(ctx)
//	if err != nil {
//		logger.Debug("failed to get installationID", zap.Error(err))
//	}
//	sentry.ConfigureScope(func(scope *sentry.Scope) {
//		scope.SetTag("Keploy Version", Version)
//		scope.SetTag("Linux Kernel Version", kernelVersion)
//		scope.SetTag("Architecture", arch)
//		scope.SetTag("Installation ID", installationID)
//	})
//	return logger
//}

// LogError logs the error with the provided fields if the error is not context.Canceled.
func LogError(logger *zap.Logger, err error, msg string, fields ...zap.Field) {
	if logger == nil {
		fmt.Println("Failed to log error. Logger is nil.")
		return
	}
	if !errors.Is(err, context.Canceled) {
		logger.Error(msg, append(fields, zap.Error(err))...)
	}
}

func DeleteFileIfExists(logger *zap.Logger, name string) (err error) {
	//Check if file exists
	_, err = os.Stat(name)
	if os.IsNotExist(err) {
		return nil
	}
	//If it does, remove it.
	err = os.Remove(name)
	if err != nil {
		if runtime.GOOS == "windows" && strings.Contains(strings.ToLower(err.Error()), "used by another process") {
			logger.Debug("skipping removal of file still in use during shutdown", zap.String("file", name), zap.Error(err))
			return nil
		}
		LogError(logger, err, "Error removing file")
		return err
	}

	return nil
}

type GitHubRelease struct {
	TagName string               `json:"tag_name"`
	Body    string               `json:"body"`
	Assets  []GitHubReleaseAsset `json:"assets"`
}

// GitHubReleaseAsset is one file of a release. Digest is GitHub's own
// "sha256:<hex>" of it, which `keploy update` checks the download against.
type GitHubReleaseAsset struct {
	Name   string `json:"name"`
	Digest string `json:"digest"`
}

var ErrGitHubAPIUnresponsive = errors.New("GitHub API is unresponsive")

var Emoji = "\U0001F430" + " Keploy:"
var ConfigGuide = `
# Visit [https://keploy.io/docs/running-keploy/configuration-file/] to learn about using keploy through configration file.
`

// ErrNoAnswer is returned when there was nobody to answer the question: stdin
// reached EOF without a line. It is NOT the same as a "no", and a caller that
// conflates them reports a decision nobody made -- which is how `keploy config
// --generate` came to write nothing in CI and exit 0. Callers that only need a
// safe default may still treat it as false; callers about to skip work on the
// strength of the answer must not.
//
// A TTY check is not a substitute: CI runners that allocate a pty (docker -t,
// Jenkins, GitLab) look interactive and still deliver EOF.
var ErrNoAnswer = errors.New("no answer: stdin ended without one")

// AskForConfirmation asks the user for confirmation. A user must type in "yes" or "no" and
// then press enter. It has fuzzy matching, so "y", "Y", "yes", "YES", and "Yes" all count as
// confirmations. An unrecognized answer or a Ctrl+C is a graceful "no"; an
// EOF is ErrNoAnswer, because nobody declined -- nobody was there.
func AskForConfirmation(ctx context.Context, s string) (bool, error) {
	reader := bufio.NewReader(os.Stdin)

	fmt.Printf("%s [y/n]: ", s)

	respCh := make(chan string, 1)
	errCh := make(chan error, 1)

	go func() {
		response, err := reader.ReadString('\n')
		// ReadString hands back what it read AND io.EOF when the input ends
		// without a newline -- which is what `printf yes | keploy ...` and a
		// heredoc with no final newline both look like. Discarding the line
		// and reporting only the error turned a plain "yes" into "there was
		// nobody to answer": first a silent wrong "no", and then, once EOF
		// became ErrNoAnswer, a refusal asserting that nobody had replied to
		// a question somebody had just answered.
		if err != nil && !(errors.Is(err, io.EOF) && strings.TrimSpace(response) != "") {
			errCh <- err
			return
		}
		respCh <- response
	}()

	select {
	case <-ctx.Done():
		// Cobra caught SIGINT (Ctrl+C) and cancelled its root context
		return false, nil
	case err := <-errCh:
		// A closed / non-TTY stdin (io.EOF) is not a failure and not a
		// decline: there is simply nobody to answer. Say which it was, so a
		// caller can refuse out loud instead of recording a decision that was
		// never made.
		if errors.Is(err, io.EOF) {
			return false, ErrNoAnswer
		}
		return false, err
	case response := <-respCh:
		response = strings.ToLower(strings.TrimSpace(response))
		if response == "y" || response == "yes" {
			return true, nil
		}
		return false, nil
	}
}

// CheckFileExists reports whether anything is at this path -- including a
// symlink whose target does not exist.
//
// Lstat, not Stat. Stat follows the link, so a DANGLING keploy.yml symlink
// read as "no file here": `keploy config --generate` never asked before
// overwriting, and wrote through the link to a path the user never named.
func CheckFileExists(path string) bool {
	if _, err := os.Lstat(path); os.IsNotExist(err) {
		return false
	}
	return true
}

var Version string

// DevVersion is the version of a build that was not given one.
const DevVersion = "3-dev"

var VersionIdentifier string
var LogFile *os.File

func GetVersionAsComment() string {
	return fmt.Sprintf("# Generated by Keploy (%s)\n", Version)
}

// ConfigHeader tops a generated keploy.yml. It says what the file holds --
// only what differs from the defaults -- because a developer opening a short
// file is otherwise entitled to think Keploy forgot the rest, and names the
// command that prints the rest so nobody has to go looking for it.
func ConfigHeader() string {
	return GetVersionAsComment() +
		"# Only the settings that differ from Keploy's defaults are written here;\n" +
		"# everything else uses its default. To read or save the full set:\n" +
		"#   keploy config defaults            # print them\n" +
		"#   keploy config defaults -o FILE    # save them\n"
}

// crashReportLogTail is how much of the end of this run's log a crash report
// carries: the log holds up to log.FileCapBytes, the native agent's lines and
// --debug's among them, and a crash report must stay well under Sentry's
// 1 MB event limit, and be read into no more memory than it sends.
const crashReportLogTail = 512 << 10

// attachLogFileToSentry attaches the end of logFile, this run's log, to the
// crash report (crashReportLogTail): read through its name only while that is
// still this run's log, never whatever a symlink there points at (a root
// run's crash would otherwise send the link's target).
func attachLogFileToSentry(logFile *os.File) error {
	content, err := keployLog.ReadLogFileTail(logFile, crashReportLogTail)
	if err != nil {
		return fmt.Errorf("error reading log file: %s", err.Error())
	}

	sentry.ConfigureScope(func(scope *sentry.Scope) {
		scope.SetExtra("logfile", string(content))
	})
	sentry.Flush(time.Second * 5)
	return nil
}

// HandleRecovery handles the common logic for recovering from a panic.
func HandleRecovery(logger *zap.Logger, r interface{}, errMsg string) {
	// With no crash-report client (telemetry off, or not yet resolved: see
	// ResolveCrashReports) nothing is reported, so the log is not read into
	// memory for a report that goes nowhere.
	if sentry.CurrentHub().Client() != nil {
		err := attachLogFileToSentry(LogFile)
		if err != nil {
			LogError(logger, err, "failed to attach log file to sentry")
		}
		sentry.CaptureException(errors.New(fmt.Sprint(r)))
	}
	// Get the stack trace
	stackTrace := debug.Stack()
	LogError(logger, nil, errMsg, zap.String("stack trace", string(stackTrace)))
}

// Recover recovers from a panic and logs the stack trace to Sentry.
// It also stops the global context.
func Recover(logger *zap.Logger) {
	if logger == nil {
		fmt.Println(Emoji + "Failed to recover from panic. Logger is nil.")
		return
	}
	// No flush on a clean return: Recover is deferred on hot goroutines, and a
	// flush waits on the process's one Sentry transport worker. Only a
	// recovered panic has an event to flush (below).
	if r := recover(); r != nil {
		HandleRecovery(logger, r, "Recovered from panic")
		// Before Stop: the process may exit as soon as the global context ends.
		sentry.Flush(2 * time.Second)
		err := Stop(logger, fmt.Sprintf("Recovered from: %s", r))
		if err != nil {
			LogError(logger, err, "failed to stop the global context")
		}
	}
}

// GenerateGithubActions generates a GitHub Actions workflow file for Keploy
func GenerateGithubActions(logger *zap.Logger, appCmd string) {
	// Determine the path based on the alias "keploy"
	logger.Debug("Generating GitHub Actions workflow file")
	// Define the content of the GitHub Actions workflow file
	actionsFileContent := `name: Keploy
on:
  push:
    branches:
      - main
  pull_request:
    types: [opened, reopened, synchronize]
jobs:
  e2e-test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Test-Report
        uses: keploy/testgpt@main
        with:
          working-directory: ./
          keploy-path: ./
          command: ` + appCmd + `
`

	// Define the file path where the GitHub Actions workflow file will be saved
	filePath := "/githubactions/keploy.yml"

	//create the file path
	if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
		logger.Error("Error creating directory for GitHub Actions workflow file", zap.Error(err))
		return
	}
	// Write the content to the file
	if err := os.WriteFile(filePath, []byte(actionsFileContent), 0644); err != nil {
		logger.Error("Error writing GitHub Actions workflow file", zap.Error(err))
		return
	}

	logger.Info("GitHub Actions workflow file generated successfully", zap.String("path", filePath))
}

// GetLatestGitHubRelease fetches the latest version and release body from GitHub releases with a timeout.
func GetLatestGitHubRelease(ctx context.Context, logger *zap.Logger) (GitHubRelease, error) {
	// GitHub repository details
	repoOwner := "keploy"
	repoName := "keploy"

	return getGitHubRelease(ctx, logger, fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", repoOwner, repoName))
}

// getGitHubRelease reads the release at apiURL. Anything but a 200 carrying a
// tag is an error: a rate-limited answer decodes to an empty release, which
// read as a version other than the running one -- an update to "".
func getGitHubRelease(ctx context.Context, logger *zap.Logger, apiURL string) (GitHubRelease, error) {
	client := http.Client{
		Timeout: 4 * time.Second,
	}

	req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil)
	if err != nil {
		return GitHubRelease{}, err
	}

	resp, err := client.Do(req)
	if err != nil {
		var netErr net.Error
		if errors.As(err, &netErr) && netErr.Timeout() {
			return GitHubRelease{}, ErrGitHubAPIUnresponsive
		}
		return GitHubRelease{}, err
	}
	defer func() {
		if err := resp.Body.Close(); err != nil {
			LogError(logger, err, "failed to close response body")
		}
	}()

	if resp.StatusCode != http.StatusOK {
		return GitHubRelease{}, fmt.Errorf("GitHub answered %s for %s", resp.Status, apiURL)
	}
	var release GitHubRelease
	if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
		return GitHubRelease{}, err
	}
	if release.TagName == "" {
		return GitHubRelease{}, fmt.Errorf("GitHub's answer for %s names no release", apiURL)
	}
	return release, nil
}

// ExtractCmdTypeFromArgs pulls an explicit --cmd-type out of raw argv.
//
// ShouldReexecWithSudo has to decide whether keploy needs root before cobra
// has parsed anything and before any config file is read, so it cannot ask
// the resolved config what the command type is. Returns "" when the flag is
// absent; the caller decides what an unrecognised value means.
func ExtractCmdTypeFromArgs(args []string) string {
	// Both spellings: cli/provider's aliasNormalizeFunc maps cmdType ->
	// cmd-type, so cobra accepts --cmdType and Changed("cmd-type") is true
	// for it. Matching only the kebab form would let the camelCase spelling
	// silently skip this privilege decision — and the repo's own CI scripts
	// use camelCase aliases throughout.
	return strings.ToLower(strings.TrimSpace(extractFlagFromArgs(args, "--cmd-type", "--cmdType")))
}

// ExtractFromContainerFromArgs pulls an explicit --from-container out of raw
// argv, for the same reason ExtractCmdTypeFromArgs exists: the flag implies a
// docker run, and therefore root, and that has to be known before cobra parses.
//
// Without this, `keploy mock record --from-container app` with no --cmd-type
// starts unprivileged and dies later writing /proc/sys/kernel/perf_event_paranoid.
func ExtractFromContainerFromArgs(args []string) string {
	// NOT lowercased: this is a container name, and docker names are
	// case-sensitive. ExtractCmdTypeFromArgs lowercases because kind names are
	// lowercase by definition.
	return strings.TrimSpace(extractFlagFromArgs(args, "--from-container", "--fromContainer"))
}

// extractFlagFromArgs returns the value of the last occurrence of any of the
// given flag names in raw argv, in either `--flag value` or `--flag=value`
// form. Returns "" when absent.
func extractFlagFromArgs(args []string, names ...string) string {
	// Last occurrence wins, matching how cobra resolves a repeated flag.
	found := ""
	for i := 0; i < len(args); i++ {
		arg := args[i]
		if arg == "--" {
			break // everything after the terminator is positional
		}
		for _, name := range names {
			if arg == name {
				if i+1 < len(args) {
					found = args[i+1]
					i++
				} else {
					found = ""
				}
				break
			}
			if strings.HasPrefix(arg, name+"=") {
				found = arg[len(name)+1:]
				break
			}
		}
	}
	return found
}

// ExtractCommandFromArgs parses os.Args to find the value of -c or --command flag.
// Returns empty string if not found.
func ExtractCommandFromArgs(args []string) string {
	for i := 0; i < len(args); i++ {
		arg := args[i]

		// Check for -c or --command
		if arg == "-c" || arg == "--command" {
			// Next argument is the command value
			if i+1 < len(args) {
				return args[i+1]
			}
			return ""
		}

		// Check for -c=value or --command=value format
		if len(arg) > 3 && arg[:3] == "-c=" {
			return arg[3:]
		}
		if len(arg) > 10 && arg[:10] == "--command=" {
			return arg[10:]
		}
	}
	return ""
}

// FindDockerCmd checks if the cli is related to docker or not, it also returns if it is a docker compose file.
//
// Podman's spellings are the same kinds: its CLI takes Docker's run and start
// flags, and `podman compose` runs a compose provider. Which engine a command
// runs on is engine.Detect's to say; a build that cannot drive Podman refuses
// the command (engine.Supported), rather than running it as a native process
// that keploy would capture nothing from.
func FindDockerCmd(cmd string) CmdType {
	if cmd == "" {
		return Empty
	}
	// Lowercased, and with its words joined by one space: `docker  run` and
	// `docker<TAB>run` are `docker run` to the shell, and so to keploy.
	cmdLower := strings.Join(strings.Fields(strings.ToLower(cmd)), " ")

	// Define patterns for Docker and Docker Compose
	dockerRunPatterns := []string{"docker run", "sudo docker run", "docker container run", "sudo docker container run",
		"podman run", "sudo podman run", "podman container run", "sudo podman container run"}
	dockerStartPatterns := []string{"docker start", "sudo docker start", "docker container start", "sudo docker container start",
		"podman start", "sudo podman start", "podman container start", "sudo podman container start"}
	dockerComposePatterns := []string{"docker-compose", "sudo docker-compose", "docker compose", "sudo docker compose",
		"podman-compose", "sudo podman-compose", "podman compose", "sudo podman compose"}

	// Check for Docker Compose command patterns and file extensions
	for _, pattern := range dockerComposePatterns {
		if strings.HasPrefix(cmdLower, pattern) {
			return DockerCompose
		}
	}
	// Check for Docker start command patterns
	for _, pattern := range dockerStartPatterns {
		if strings.HasPrefix(cmdLower, pattern) {
			return DockerStart
		}
	}
	// Check for Docker run command patterns
	for _, pattern := range dockerRunPatterns {
		if strings.HasPrefix(cmdLower, pattern) {
			return DockerRun
		}
	}
	return Native
}

// RunSubcommandEnd is the offset in cmd just past the run subcommand of a
// docker or podman (by base name), taken as `run` or `container run` right
// after the engine word, where the flags for the container it starts go; -1
// when there is none. A docker-named word without that after it (the value of
// sudo's -u, a directory) is passed over.
func RunSubcommandEnd(cmd string) int {
	type word struct {
		text string
		end  int
	}
	var words []word
	isSpace := func(c byte) bool { return c == ' ' || c == '\t' || c == '\n' || c == '\r' }
	for i := 0; i < len(cmd); {
		for i < len(cmd) && isSpace(cmd[i]) {
			i++
		}
		from := i
		for i < len(cmd) && !isSpace(cmd[i]) {
			i++
		}
		if i > from {
			words = append(words, word{cmd[from:i], i})
		}
	}
	for i, w := range words {
		base := strings.ToLower(w.text[strings.LastIndexAny(w.text, `/\`)+1:])
		if base = strings.TrimSuffix(base, ".exe"); base != "docker" && base != "podman" {
			continue
		}
		next := i + 1
		if next < len(words) && strings.EqualFold(words[next].text, "container") {
			next++
		}
		if next < len(words) && strings.EqualFold(words[next].text, "run") {
			return words[next].end
		}
	}
	return -1
}

type CmdType string

// CmdType constants
const (
	DockerRun     CmdType = "docker-run"
	DockerStart   CmdType = "docker-start"
	DockerCompose CmdType = "docker-compose"
	// FromContainer takes an ALREADY RUNNING container and re-creates it under
	// keploy's namespaces through the Docker Engine API, rather than being
	// handed a command string to splice flags into and shell out. It is the
	// only kind with no user command at all - see HasUserCommand.
	FromContainer CmdType = "from-container"
	Native        CmdType = "native"
	Empty         CmdType = ""
)

// Keys returns an array containing the keys of the given map.
func Keys(m map[string][]string) []string {
	keys := make([]string, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	return keys
}

//func FetchHomeDirectory(isNewConfigPath bool) string {
//	var configFolder = "/.keploy-config"
//
//	if isNewConfigPath {
//		configFolder = "/.keploy"
//	}
//	if runtime.GOOS == "windows" {
//		home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
//		if home == "" {
//			home = os.Getenv("USERPROFILE")
//		}
//		return home + configFolder
//	}
//
//	return os.Getenv("HOME") + configFolder
//}

func GetAbsPath(path string) (string, error) {
	absPath, err := filepath.Abs(path)
	if err != nil {
		return "", err
	}
	return absPath, nil
}

func GetCurrentBinaryPath() (string, error) {
	executable, err := os.Executable()
	if err != nil {
		return "", err
	}

	// Resolve the full path (to avoid issues with symbolic links)
	executablePath, err := filepath.EvalSymlinks(executable)
	if err != nil {
		return "", err
	}

	return executablePath, nil
}

func ToAbsPath(logger *zap.Logger, originalPath string) string {
	path := originalPath
	//if user provides relative path
	if len(path) > 0 && path[0] != '/' {
		absPath, err := filepath.Abs(path)
		if err != nil {
			LogError(logger, err, "failed to get the absolute path from relative path")
		}
		path = absPath
	} else if len(path) == 0 { // if user doesn't provide any path
		cdirPath, err := os.Getwd()
		if err != nil {
			LogError(logger, err, "failed to get the path of current directory")
		}
		path = cdirPath
	}
	path += "/keploy"
	return path
}

// makeDirectory creates a directory if not exists with all user access
func makeDirectory(path string) error {
	err := os.MkdirAll(path, 0777)
	if err != nil {
		return err
	}
	return nil
}

// SetCoveragePath takes a goCovPath and sets the coverage path accordingly.
// It returns an error if the path is a file or if the path does not exist.
func SetCoveragePath(logger *zap.Logger, goCovPath string) (string, error) {
	if goCovPath == "" {
		// Calculate the current path and create a coverage-reports directory
		currentPath, err := GetAbsPath("")
		if err != nil {
			LogError(logger, err, "failed to get the current working directory")
			return "", err
		}
		goCovPath = currentPath + "/coverage-reports"
		if err := makeDirectory(goCovPath); err != nil {
			LogError(logger, err, "failed to create coverage-reports directory", zap.String("CoverageReportPath", goCovPath))
			return "", err
		}
		return goCovPath, nil
	}

	goCovPath, err := GetAbsPath(goCovPath)
	if err != nil {
		LogError(logger, err, "failed to get the absolute path for the coverage report path", zap.String("CoverageReportPath", goCovPath))
		return "", err
	}
	// Check if the path is a directory
	dirInfo, err := os.Stat(goCovPath)
	if err != nil {
		if os.IsNotExist(err) {
			LogError(logger, err, "the provided path does not exist", zap.String("CoverageReportPath", goCovPath))
			return "", err
		}
		LogError(logger, err, "failed to check the coverage report path", zap.String("CoverageReportPath", goCovPath))
		return "", err
	}
	if !dirInfo.IsDir() {
		msg := "the coverage report path is not a directory. Please provide a valid path to a directory for go coverage reports"

		LogError(logger, nil, msg, zap.String("CoverageReportPath", goCovPath))
		return "", errors.New("the path provided is not a directory")
	}

	return goCovPath, nil
}

type ErrType string

// ErrType constants to get the type of error, during init or runtime
const (
	Init    ErrType = "init"
	Runtime ErrType = "runtime"
)

type CmdError struct {
	Type ErrType
	Err  error
}

// InterruptProcessTree interrupts an entire process tree using the given signal
func InterruptProcessTree(logger *zap.Logger, ppid int, sig syscall.Signal) error {
	_, _, err := interruptProcessTree(logger, ppid, sig, nil, false, nil)
	return err
}

// InterruptProcessTreeKilled is InterruptProcessTree, and says why it had to
// kill, "" when it did not: a process group's leader had not exited
// TreeInterruptGrace after sig, and the group was sent SIGKILL; or, on
// Windows, the tree had not exited windowsInterruptGrace after its
// CTRL_BREAK, and was force-killed (taskkill /F). A stop that could not tell
// that the tree had exited without a kill says so too: one whose signal could
// not be sent, whose force-kill failed, or whose state it could not read. As
// InterruptProcessTree, it waits for each group's leader, not for the rest of
// the group.
func InterruptProcessTreeKilled(logger *zap.Logger, ppid int, sig syscall.Signal) (killed string, err error) {
	killed, _, err = interruptProcessTree(logger, ppid, sig, nil, false, nil)
	return killed, err
}

// Reached is a process that a signal sent to its process group reached: its
// pid and its start time (in clock ticks after boot, as /proc/<pid>/stat
// gives it), which together name it while it runs, and whether it ignored
// the signal when the signal was sent.
type Reached struct {
	PID     int
	Start   uint64
	Ignores bool
}

// InterruptProcessTreeReached is InterruptProcessTreeKilled, and also says
// which processes its signals reached: each process of a group it signalled,
// as /proc showed that group just before its signal, and whether the process
// ignored the signal then. A process the group gained as /proc was read is
// not among them. Off Linux it says none.
func InterruptProcessTreeReached(logger *zap.Logger, ppid int, sig syscall.Signal) (killed string, reached []Reached, err error) {
	return interruptProcessTree(logger, ppid, sig, groupReached, false, nil)
}

// InterruptProcessTreeStop is InterruptProcessTreeReached for a stop that
// waits on the app's exit, not for a number of seconds (a test set's end, a
// recording's end, under the proven switch): it waits for each group's
// leader to exit, on its exit event (Linux, stop_events_linux.go), and kills
// a group only when it made no progress on its way out for StopStallBudget
// of the time both it and keploy could run (ExitClock). A starved group,
// whether the host is or only the group is, takes as long as it takes.
// Elsewhere it is InterruptProcessTreeReached, with its grace.
//
// Each step a group takes on its way out is a movement of moved, the
// teardown scope of the run whose app it stops (nil: none).
func InterruptProcessTreeStop(logger *zap.Logger, ppid int, sig syscall.Signal, moved *TeardownScope) (killed string, reached []Reached, err error) {
	return interruptProcessTree(logger, ppid, sig, groupReached, true, moved)
}

// groupStopByEvents, where it is set (Linux), is how InterruptProcessTreeStop
// waits for a group it signalled: until its leader exits, while its members
// (as the signal reached them) make progress. It kills the group on a stall,
// or at the user's Ctrl+C, and says why ("" when it did not kill).
// Each step the group takes on its way out is a movement of moved.
var groupStopByEvents func(logger *zap.Logger, leader int, members []Reached, moved *TeardownScope) (killed string)

// TreeInterruptGrace is how long InterruptProcessTree waits for each process
// group of the tree to exit after its signal before it sends SIGKILL.
const TreeInterruptGrace = 3 * time.Second

// windowsInterruptGrace is how long InterruptProcessTree waits on Windows for
// the tree to exit after its CTRL_BREAK before it ends it with taskkill.
const windowsInterruptGrace = 10 * time.Second

// interruptProcessTree is the work of the four above. snapshot, when set,
// reads which processes a group's signal is about to reach; byEvents (with
// snapshot) has it wait for each group on its exit event, under the stall
// rule, rather than for TreeInterruptGrace.
func interruptProcessTree(logger *zap.Logger, ppid int, sig syscall.Signal, snapshot func(pgid int, sig syscall.Signal) []Reached, byEvents bool, moved *TeardownScope) (killed string, reached []Reached, err error) {
	// Windows: /proc and process group semantics don't exist. Prefer console ctrl events, fallback to taskkill.
	if runtime.GOOS == "windows" {
		logger.Debug("Interrupting process tree on windows", zap.Int("pid", ppid), zap.String("signal", sig.String()))

		// List ALL descendant processes (recursively) before attempting to kill
		listAllCmd := exec.Command("powershell", "-Command",
			fmt.Sprintf(`$parent = %d
$processes = Get-CimInstance Win32_Process
function Get-ChildProcesses($parentId) {
    $children = $processes | Where-Object {$_.ParentProcessId -eq $parentId}
    foreach ($child in $children) {
        $child
        Get-ChildProcesses $child.ProcessId
    }
}
Get-ChildProcesses $parent | Select-Object ProcessId, ParentProcessId, Name, CommandLine | Format-List`, ppid))
		if output, err := listAllCmd.CombinedOutput(); err == nil {
			logger.Debug("ALL descendant processes before kill", zap.Int("parent_pid", ppid), zap.String("output", string(output)))
		} else {
			logger.Debug("Failed to list descendant processes", zap.Int("pid", ppid), zap.Error(err), zap.String("output", string(output)))
		}

		// Prefer a console Ctrl+Break event so the app can exit cleanly.
		// why is what a kill from here on is said with.
		why := fmt.Sprintf("it had not exited %s after keploy's CTRL_BREAK (the grace keploy's stop gives it on Windows), so keploy killed it", windowsInterruptGrace)
		if sig == syscall.SIGINT {
			if err := SendSignal(logger, -ppid, sig); err == nil {
				if err := waitForProcessExit(ppid, windowsInterruptGrace, logger); err != nil {
					logger.Error("error waiting for process to exit", zap.Int("pid", ppid), zap.Error(err))
				}
				if running, err := isProcessRunning(ppid); err == nil && !running {
					return "", nil, nil
				}
			} else {
				logger.Debug("failed to send console ctrl event", zap.Int("pid", ppid), zap.Error(err))
				why = fmt.Sprintf("keploy could not send it a CTRL_BREAK (%v), so keploy killed it", err)
			}
		} else {
			why = fmt.Sprintf("keploy cannot send %s on Windows, so keploy killed it", sig)
		}

		// Try graceful taskkill as a fallback (without /F).
		cmd := exec.Command("taskkill", "/PID", strconv.Itoa(ppid), "/T")
		if output, err := cmd.CombinedOutput(); err != nil {
			logger.Debug("taskkill graceful failed", zap.Int("pid", ppid), zap.Error(err), zap.String("output", string(output)))
		} else {
			logger.Debug("taskkill graceful succeeded", zap.Int("pid", ppid), zap.String("output", string(output)))
		}

		// Force kill as the last resort if the process tree is still alive,
		// or its state cannot be read: either way the stop says it killed,
		// as it cannot tell that the tree exited on its own. So does a force
		// kill that failed.
		running, err := isProcessRunning(ppid)
		if err != nil {
			logger.Debug("failed to check process state after graceful taskkill", zap.Int("pid", ppid), zap.Error(err))
			why = fmt.Sprintf("%s; and keploy could not read whether it had exited (%v)", strings.TrimSuffix(why, ", so keploy killed it"), err) + ", so keploy killed it"
		}
		if running || err != nil {
			killed = why
			forcedCmd := exec.Command("taskkill", "/PID", strconv.Itoa(ppid), "/T", "/F")
			if output, ferr := forcedCmd.CombinedOutput(); ferr != nil {
				logger.Error("taskkill forced failed", zap.Int("pid", ppid), zap.Error(ferr), zap.String("output", string(output)))
				killed = fmt.Sprintf("%s, and its taskkill /F failed (%v)", why, ferr)
			} else {
				logger.Debug("taskkill forced succeeded", zap.Int("pid", ppid), zap.String("output", string(output)))
			}
		}

		// Check for remaining descendant processes after kill
		verifyAllCmd := exec.Command("powershell", "-Command",
			fmt.Sprintf(`$parent = %d
$processes = Get-CimInstance Win32_Process
function Get-ChildProcesses($parentId) {
    $children = $processes | Where-Object {$_.ParentProcessId -eq $parentId}
    foreach ($child in $children) {
        $child
        Get-ChildProcesses $child.ProcessId
    }
}
Get-ChildProcesses $parent | Select-Object ProcessId, ParentProcessId, Name | Format-List`, ppid))
		if output, err := verifyAllCmd.CombinedOutput(); err == nil {
			logger.Debug("ALL descendant processes after kill", zap.Int("parent_pid", ppid), zap.String("output", string(output)))
		}

		// Wait a short while for processes to exit (same helper used elsewhere)
		if err := waitForProcessExit(ppid, 3*time.Second, logger); err != nil {
			logger.Error("error waiting for process to exit", zap.Int("pid", ppid), zap.Error(err))
		}
		return killed, nil, nil
	}

	// Non-windows (existing logic)
	logger.Debug("Interrupting process tree", zap.Int("pid", ppid), zap.String("signal", sig.String()))

	var uniqueProcess []int
	children, groupOf, err := processTree(ppid)
	if err != nil {
		// The tree cannot be read: its leader's own group (keploy starts
		// the app in a group of its own) is still interrupted, waited for
		// and, if need be, killed, as below. Returning here sent no signal
		// at all, and the app was killed when the command's WaitDelay ran
		// out, unsaid. A process of the tree in a group of its own is then
		// not interrupted.
		logger.Warn("keploy could not read the application's process tree; it interrupts the application's own process group only",
			zap.Int("pid", ppid), zap.Error(err))
		uniqueProcess = []int{ppid}
	} else {
		children = append(children, ppid)

		logger.Debug("Found child PIDs", zap.Ints("children", children))

		uniqueProcess, err = uniqueProcessGroups(children, groupOf)
		if err != nil {
			logger.Error("failed to find unique process groups", zap.Int("pid", ppid), zap.Error(err))
			uniqueProcess = children
		}
	}

	logger.Debug("Unique process groups", zap.Ints("uniqueProcess", uniqueProcess))

	// Send signal to interrupt each process and wait for them to exit one by one
	for _, pid := range uniqueProcess {
		var members []Reached
		if snapshot != nil {
			members = snapshot(pid, sig)
		}
		err := signalGroup(logger, -pid, sig)
		if err != nil {
			logger.Error("error sending signal to the process group id", zap.Int("pgid", pid), zap.Error(err))
			// The group was not asked to stop: what ends it now (the
			// command's WaitDelay kills it) is no exit of its own.
			if killed == "" {
				killed = fmt.Sprintf("its process group %d could not be signalled (%v), so keploy could not let it stop on its own", pid, err)
			}
			continue
		}
		reached = append(reached, members...)
		// A stop that waits on the app's exit kills only a group that
		// stopped making progress on its way out.
		if byEvents && snapshot != nil && groupStopByEvents != nil {
			if why := groupStopByEvents(logger, pid, members, moved); why != "" && killed == "" {
				killed = why
			}
			continue
		}
		// Wait for this particular process to exit with a timeout of 3 seconds
		err = waitForProcessExit(pid, TreeInterruptGrace, logger)
		if err != nil {
			logger.Error("error waiting for process to exit", zap.Int("pid", pid), zap.Error(err))
		}
		// If it ignored the graceful signal (or was too contention-slow to exit
		// within the wait), escalate to SIGKILL so it cannot keep holding resources
		// (e.g. a listening port) and race the next app start with
		// "address already in use" (the go-docker-timefreeze flake).
		if running, rerr := isProcessRunning(pid); rerr != nil {
			logger.Debug("could not verify process state after graceful signal", zap.Int("pid", pid), zap.Error(rerr))
		} else if running {
			logger.Debug("process still alive after graceful signal; escalating to SIGKILL", zap.Int("pgid", pid))
			if killed == "" {
				killed = fmt.Sprintf("a process of it had not exited %s later (the grace keploy's stop gives each of its process groups), so keploy killed it", TreeInterruptGrace)
			}
			if killErr := SendSignal(logger, -pid, syscall.SIGKILL); killErr != nil {
				logger.Error("error sending SIGKILL to the process group id", zap.Int("pgid", pid), zap.Error(killErr))
			} else {
				_ = waitForProcessExit(pid, 3*time.Second, logger)
			}
		}
	}
	return killed, reached, nil
}

// processTree and signalGroup are the tree interrupt's reads of the process
// tree and its signals to a process group; a test fails them.
var (
	processTree = findChildPIDs
	signalGroup = SendSignal
)

// waitForProcessExit waits for the process to exit or times out after the specified duration
func waitForProcessExit(pid int, timeout time.Duration, logger *zap.Logger) error {
	// Create a timeout channel
	timeoutCh := time.After(timeout)

	// Loop to check if the process is running
	for {
		select {
		case <-timeoutCh:
			// If the timeout is reached, log the timeout and break
			logger.Debug("Timed out waiting for process to exit", zap.Int("pid", pid))
			return nil
		default:
			// Check if the process is running
			isRunning, err := isProcessRunning(pid)
			if err != nil {
				return err
			}

			if !isRunning {
				logger.Debug("Process exited", zap.Int("pid", pid))
				return nil
			}

			// Wait for 1 second before checking again
			time.Sleep(300 * time.Millisecond)
		}
	}
}

// isProcessRunning checks if a particular process is still running using os.FindProcess
func isProcessRunning(pid int) (bool, error) {
	// On Windows use PowerShell to check PID presence because signalling with 0 fails.
	if runtime.GOOS == "windows" {
		// Use exit code instead of parsing output to avoid locale-dependent string matching.
		// PowerShell exits with 0 when process exists, 1 when not found.
		cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command",
			fmt.Sprintf("if (Get-Process -Id %d -ErrorAction SilentlyContinue) { exit 0 } else { exit 1 }", pid))
		err := cmd.Run()
		if err != nil {
			// Non-zero exit code means process not found
			if exitError, ok := err.(*exec.ExitError); ok && exitError.ExitCode() != 0 {
				return false, nil
			}
			// Other errors (like command not found) are treated as errors
			return false, err
		}
		// Exit code 0 means process was found
		return true, nil
	}

	// POSIX: try to find the process and signal 0 to check existence
	process, err := os.FindProcess(pid)
	if err != nil {
		return false, err // Process lookup failed
	}

	// Attempt to signal the process (0 means no action, just error check)
	err = process.Signal(syscall.Signal(0))
	if err != nil {
		// If error occurs when signaling, it means the process is no longer running
		// If there’s another error, consider the process as still running
		if errors.Is(err, os.ErrProcessDone) || err == syscall.ESRCH {
			return false, nil
		}
		return true, nil
	}

	// No error means the process is running
	return true, nil
}

// uniqueProcessGroups returns the process group of each of pids, as groupOf
// reads it, each group once.
func uniqueProcessGroups(pids []int, groupOf func(pid int) (int, error)) ([]int, error) {
	uniqueGroups := make(map[int]bool)
	var uniqueGPIDs []int

	for _, pid := range pids {
		pgid, err := groupOf(pid)
		if err != nil {
			return nil, err
		}
		if !uniqueGroups[pgid] {
			uniqueGroups[pgid] = true
			uniqueGPIDs = append(uniqueGPIDs, pgid)
		}
	}

	return uniqueGPIDs, nil
}

// ephemeralPortRange reports the kernel's local port range and whether it could
// actually be determined.
//
// known=false means "this platform's range is unknown to us", NOT "assume
// Linux". Guessing is worse than not acting: macOS and Windows both default to
// 49152-65535, so assuming the Linux 32768-60999 would make GetAvailablePort
// allocate from 61000-65534 — entirely INSIDE the real ephemeral range on those
// platforms — while shrinking the pool from 16384 to 4535 and making collisions
// materially more likely than plain :0.
func ephemeralPortRange() (lo, hi uint32, known bool) {
	return ephemeralPortRangeFrom(os.ReadFile)
}

// ephemeralPortRangeFrom takes the file reader as a parameter so the
// unknown-platform path is testable. On Linux the read always succeeds, which
// would otherwise leave that branch dead code no test can reach — and it is the
// branch that decides whether the allocator acts at all.
func ephemeralPortRangeFrom(readFile func(string) ([]byte, error)) (lo, hi uint32, known bool) {
	b, err := readFile("/proc/sys/net/ipv4/ip_local_port_range")
	if err != nil {
		return 0, 0, false
	}
	return parseEphemeralPortRange(string(b))
}

// Linux's documented default, used when ip_local_port_range exists but its
// contents are malformed.
const (
	defaultEphemeralLo uint32 = 32768
	defaultEphemeralHi uint32 = 60999
)

// parseEphemeralPortRange parses the two-field content of ip_local_port_range.
// Split out from the file read so its fallbacks are testable: on a normal Linux
// box the read always succeeds, which would leave every fallback branch dead
// code no test could reach.
func parseEphemeralPortRange(content string) (lo, hi uint32, ok bool) {
	f := strings.Fields(content)
	if len(f) != 2 {
		return defaultEphemeralLo, defaultEphemeralHi, true
	}
	l, err1 := strconv.ParseUint(f[0], 10, 32)
	h, err2 := strconv.ParseUint(f[1], 10, 32)
	if err1 != nil || err2 != nil || l == 0 || h < l || h > 65535 {
		return defaultEphemeralLo, defaultEphemeralHi, true
	}
	return uint32(l), uint32(h), true
}

// GetAvailablePort finds and returns an available port on the system.
//
// It deliberately avoids the kernel's local port range, because that range is
// where EVERY bind(0) allocator on the machine draws from — including a second
// keploy process doing exactly this, and Docker's dynamic host-port publisher.
// The probe listener must be CLOSED before the number can be handed to whoever
// will actually bind it, so anything drawing from the same range can take it in
// between, and the eventual holder is a LIVE process that never releases it —
// which is why the agent's bind retry cannot recover and the run dies with
// "keploy-agent did not become ready in time".
//
// Measured on Linux 6.12 with the default range 32768-60999: 3000/3000
// bind(":0") allocations landed inside it, none above. (Outbound connections
// are NOT the thief — the kernel partitions the two, handing bind(0) odd ports
// and connect() even ones: 1500/1500 vs 1000/1000 in the same measurement. The
// port that failed in CI, 34467, is odd, i.e. it came from a bind(0)-class
// allocator, not from an outbound socket.)
//
// So candidates come from ABOVE the range's high-water mark. This NARROWS the
// window rather than closing it — nothing short of passing the listener itself
// closes it — but the port no longer competes with every other bind(0) on the
// host.
//
// Its probe listeners are plain TCP, never Go's default MPTCP: for as long as
// one is open it listens on every interface.
func GetAvailablePort() (uint32, error) {
	return availablePortAbove(hostEphemeralRange, 0)
}

// hostEphemeralRange reads the host's ephemeral port range for
// GetAvailablePort and GetAvailableContainerPort (ephemeralPortRange).
var hostEphemeralRange = ephemeralPortRange

// containerEphemeralHi is the top of a container's ephemeral port range: the
// kernel gives each new network namespace Linux's default range, 32768-60999,
// whatever the host's is.
const containerEphemeralHi = defaultEphemeralHi

// GetAvailableContainerPort is GetAvailablePort for a port a container
// listens on that the host publishes as itself (127.0.0.1:P:P), as a Docker
// agent's: free on the host, and above the container's own ephemeral range
// too. On a user-defined network, as every compose stack's is, Docker's
// embedded DNS resolver listens in the container's namespace, on 127.0.0.11,
// on a TCP and a UDP port the kernel drew from that range before the
// container's process ran. A port the CLI found free on the host could be one
// of them, and the process in the container then cannot bind it, nor move off
// it: the CLI reaches it through the publish. A host whose range ends below
// 60999, or reaches 65535, or is unknown (macOS, Windows), would otherwise
// hand out ports inside it.
func GetAvailableContainerPort() (uint32, error) {
	return availablePortAbove(hostEphemeralRange, containerEphemeralHi)
}

// availablePortAbove is GetAvailablePort with the host's ephemeral range read
// by ephemeral (ephemeralPortRange's signature), whose candidates are above
// floor too.
func availablePortAbove(ephemeral func() (lo, hi uint32, known bool), floor uint32) (uint32, error) {
	top := floor
	if _, ephemeralHi, known := ephemeral(); known && ephemeralHi < 65535 && ephemeralHi > top {
		top = ephemeralHi
	}
	if top > 0 {
		lo := top + 1
		span := 65536 - lo // inclusive of 65535
		// A random start, not a scan from the bottom. Scanning upward returns
		// the LOWEST free port every time, so two keploy processes started
		// moments apart — the ordinary CI pattern — are handed an identical
		// number, each having verified it free; whichever binds second loses.
		start := lo
		if n, err := rand.Int(rand.Reader, big.NewInt(int64(span))); err == nil {
			start = lo + uint32(n.Int64())
		}
		for i := uint32(0); i < span; i++ {
			port := lo + ((start - lo + i) % span)
			ln, err := plaintcp.Listen(context.Background(), "tcp", fmt.Sprintf(":%d", port))
			if err != nil {
				continue
			}
			// A failed Close leaves this process holding the port for its
			// lifetime, so the candidate is unusable AND leaked — return the
			// error rather than continuing and leaking one fd per candidate.
			if err := ln.Close(); err != nil {
				return 0, fmt.Errorf("failed to release probe listener on port %d: %w", port, err)
			}
			return port, nil
		}
	}

	// Either the platform's ephemeral range is unknown, or every port above it
	// is taken, or the range reaches 65535 leaving no space (with no floor).
	// Fall back to the OS assignment rather than failing: a port that may race
	// is strictly better than no port at all, and this is the historical
	// behaviour. A container's port that then meets one of the resolver's
	// fails the agent's start at its bind, as no port at all would.
	listener, err := plaintcp.Listen(context.Background(), "tcp", ":0")
	if err != nil {
		return 0, fmt.Errorf("failed to find available port: %w", err)
	}
	defer listener.Close()

	addr := listener.Addr().(*net.TCPAddr)
	return uint32(addr.Port), nil
}

// isPortAvailable checks if a specific port is available on the system, with
// a plain TCP probe listener (as GetAvailablePort's).
func isPortAvailable(port uint32) bool {
	listener, err := plaintcp.Listen(context.Background(), "tcp", fmt.Sprintf(":%d", port))
	if err != nil {
		return false
	}
	defer listener.Close()
	return true
}

// EnsureAvailablePorts checks if the proxy and DNS ports are available.
// If they are available, it returns them unchanged.
// If not, it allocates new available ports for them.
//
// exclude lists ports already handed out in this same setup. Nothing binds
// them yet — the agent, proxy and DNS ports are all allocated before any of
// them is used — so without this the second and third draws can legitimately
// return a port the first already claimed, and two subsystems then race for
// it. Narrowing allocation to above the ephemeral range makes the pool smaller
// and that coincidence correspondingly more likely, so the exclusion is not
// optional bookkeeping.
func EnsureAvailablePorts(port uint32, exclude ...uint32) (uint32, error) {
	taken := make(map[uint32]struct{}, len(exclude))
	for _, p := range exclude {
		taken[p] = struct{}{}
	}
	if _, clash := taken[port]; !clash && isPortAvailable(port) {
		return port, nil
	}
	var newPort uint32
	var err error
	// Bounded: a handful of draws is plenty to miss a set this small, and a
	// bound means an exhausted pool surfaces as an error rather than a hang.
	for i := 0; i < 16; i++ {
		newPort, err = GetAvailablePort()
		if err != nil {
			break
		}
		if _, clash := taken[newPort]; !clash {
			return newPort, nil
		}
	}
	if err == nil {
		return 0, fmt.Errorf("failed to allocate a port distinct from %v", exclude)
	}
	return 0, fmt.Errorf("failed to allocate new port: %w", err)
}

func EnsureRmBeforeName(cmd string) string {
	parts := strings.Split(cmd, " ")
	rmIndex := -1
	nameIndex := -1

	for i, part := range parts {
		if part == "--rm" {
			rmIndex = i
		} else if part == "--name" {
			nameIndex = i
			break // Assuming --name will always have an argument, we can break here
		}
	}
	if rmIndex == -1 && nameIndex != -1 {
		parts = append(parts[:nameIndex], append([]string{"--rm"}, parts[nameIndex:]...)...)
	}

	return strings.Join(parts, " ")
}

// dockerLongFlagValue returns the value of the first occurrence of any of
// names in cmd, accepting both "--flag value" and "--flag=value". Docker
// accepts either spelling for every long flag, so a parser that understands
// only one of them silently reads nothing from perfectly valid commands.
func dockerLongFlagValue(cmd string, names ...string) string {
	parts := strings.Fields(cmd)
	for i, part := range parts {
		for _, name := range names {
			if part == name && i+1 < len(parts) {
				return parts[i+1]
			}
			// An empty "--flag=" is not a value: keep scanning, so it cannot
			// swallow a real occurrence later in the command.
			if value, ok := strings.CutPrefix(part, name+"="); ok && value != "" {
				return value
			}
		}
	}
	return ""
}

// ContainerNameFromDockerRun extracts the value of the --name flag from a
// `docker run` command, supporting both "--name foo" and "--name=foo". It
// returns "" when no --name is present. Used to free a leftover container name
// before a re-run when the --container-name flag wasn't supplied, and to
// resolve the container keploy must instrument.
func ContainerNameFromDockerRun(cmd string) string {
	return dockerLongFlagValue(cmd, "--name")
}

// NetworkNameFromDockerRun extracts the value of --network or --net from a
// `docker run` command, in either spelling. It returns "" when neither is
// present.
func NetworkNameFromDockerRun(cmd string) string {
	return dockerLongFlagValue(cmd, "--network", "--net")
}

func isGoBinary(logger *zap.Logger, filePath string) bool {
	f, err := elf.Open(filePath)
	if err != nil {
		logger.Debug(fmt.Sprintf("failed to open file %s", filePath), zap.Error(err))
		return false
	}
	defer func() {
		if err := f.Close(); err != nil {
			LogError(logger, err, "failed to close file", zap.String("file", filePath))
		}
	}()

	// Check for section names typical to Go binaries
	sections := []string{".go.buildinfo", ".gopclntab"}
	for _, section := range sections {
		if sect := f.Section(section); sect != nil {
			return true
		}
	}
	return false
}

// DetectLanguage detects the language of the test command and returns the executable
func DetectLanguage(logger *zap.Logger, cmd string) (models.Language, string) {
	if cmd == "" {
		return models.Unknown, ""
	}
	fields := strings.Fields(cmd)

	// Find the actual executable by skipping environment variable assignments
	executable := ""
	for _, field := range fields {
		// Skip environment variable assignments (KEY=VALUE format)
		if strings.Contains(field, "=") && !strings.HasPrefix(field, "/") {
			continue
		}
		// This is the actual executable
		executable = field
		break
	}

	if executable == "" {
		return models.Unknown, ""
	}

	// Check for Python
	pythonRegex := regexp.MustCompile(`(?i)(^|.*/)(python(\d+(\.\d+)*)?)$`)
	if pythonRegex.MatchString(executable) {
		return models.Python, executable
	}

	// Check for Node.js
	if executable == "node" || executable == "npm" || executable == "yarn" {
		return models.Javascript, executable
	}

	// Check for Java
	if executable == "java" {
		return models.Java, executable
	}

	// Check for Go
	if executable == "go" || (isGoBinary(logger, executable)) {
		return models.Go, executable
	}

	return models.Unknown, executable
}

// FileExists checks if a file exists and is not a directory at the given path.
func FileExists(path string) (bool, error) {
	fileInfo, err := os.Stat(path)
	if err != nil {
		if os.IsNotExist(err) {
			return false, nil
		}
		return false, err
	}
	return fileInfo.Mode().IsRegular(), nil
}

// ExpandPath expands a given path, replacing the tilde with the user's home directory
func ExpandPath(path string) (string, error) {
	if strings.HasPrefix(path, "~/") {
		homeDir, err := getHomeDir()
		if err != nil {
			return "", err
		}
		return strings.Replace(path, "~", homeDir, 1), nil
	}
	return path, nil
}

// getHomeDir retrieves the appropriate home directory based on the execution context
func getHomeDir() (string, error) {
	if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" {
		if usr, err := user.Lookup(sudoUser); err == nil {
			return usr.HomeDir, nil
		}
	}
	// Fallback to the current user's home directory
	if usr, err := user.Current(); err == nil {
		return usr.HomeDir, nil
	}
	// Fallback if neither method works
	return "", errors.New("failed to retrieve current user info")
}

func IsDockerCmd(kind CmdType) bool {
	return (kind == DockerRun || kind == DockerStart || kind == DockerCompose || kind == FromContainer)
}

// HasUserCommand reports whether this kind carries a command string that keploy
// runs through a shell. Every kind did until FromContainer, which launches the
// app through the Docker Engine API instead - so the checks written against
// "there is a command here" (detach-mode scanning, name extraction, the
// wrapped-command exec itself) have nothing to act on and must be skipped
// rather than run against an empty string.
func HasUserCommand(kind CmdType) bool {
	return kind != FromContainer && kind != Empty
}

// PermissionError holds information about files/directories with permission issues
type PermissionError struct {
	Path     string
	OwnerUID uint32
	IsRead   bool // true if it's a read permission issue, false if write
}

// AddToGitIgnore adds an entry to the .gitignore file if it doesn't already exist.
func AddToGitIgnore(logger *zap.Logger, path string, ignoreString string) error {
	gitignorePath := path + "/.gitignore"

	file, err := os.OpenFile(gitignorePath, os.O_RDWR|os.O_CREATE, 0644)
	if err != nil {
		return fmt.Errorf("error opening or creating .gitignore file: %v", err)
	}

	defer func() {
		if err := file.Close(); err != nil {
			logger.Error("error closing .gitignore file", zap.Error(err))
		}
	}()

	scanner := bufio.NewScanner(file)
	found := false
	for scanner.Scan() {
		if strings.TrimSpace(scanner.Text()) == ignoreString {
			found = true
			break
		}
	}

	if !found {
		if _, err := file.WriteString("\n" + ignoreString + "\n"); err != nil {
			return fmt.Errorf("error writing to .gitignore file: %v", err)
		}
	}

	return nil
}

func Hash(data []byte) string {
	hasher := sha256.New()
	hasher.Write(data)
	return hex.EncodeToString(hasher.Sum(nil))
}

// cdIntoOneThatExists is what a user does about a working directory that is
// gone.
const cdIntoOneThatExists = "cd into a directory that exists and run keploy again"

// GetLastDirectory is the name of the directory keploy runs in. It fails once
// that directory is gone, on every platform, in the same words.
func GetLastDirectory() (string, error) {
	return lastDirectory(os.Getwd)
}

func lastDirectory(getwd func() (string, error)) (string, error) {
	// Get the current working directory
	dir, err := getwd()
	if err == nil {
		err = confirmWorkingDirectory(dir)
	}
	// Linux's Getwd fails here, and on macOS the stat of its answer does.
	if errors.Is(err, os.ErrNotExist) {
		return "", fmt.Errorf("the working directory is gone: %w; %s", err, cdIntoOneThatExists)
	}
	if err != nil {
		return "", err
	}

	// Extract the base (last directory)
	lastDir := filepath.Base(dir)
	return lastDir, nil
}

// confirmWorkingDirectory fails unless dir, os.Getwd's answer, still leads to
// the directory the process is in. Getwd does not say so itself: on Linux it
// fails once the directory is removed, but macOS's getcwd goes on answering the
// path the directory last had, which by then leads nowhere or to a newer
// directory of that name. A path this user may not walk, as under `sudo -u`
// below a private directory, says nothing of the kind, and is let through.
func confirmWorkingDirectory(dir string) error {
	here, err := os.Stat(".")
	if err == nil {
		var there os.FileInfo
		there, err = os.Stat(dir)
		if err == nil && !os.SameFile(here, there) {
			return fmt.Errorf("the working directory is gone: %s is another directory now; %s", dir, cdIntoOneThatExists)
		}
	}
	if errors.Is(err, os.ErrPermission) {
		return nil
	}
	return err
}

func IsFileEmpty(filePath string) (bool, error) {
	fileInfo, err := os.Stat(filePath)
	if err != nil {
		return false, err
	}
	return fileInfo.Size() == 0, nil
}

func IsXMLResponse(resp *models.HTTPResp) bool {
	if resp == nil || resp.Header == nil {
		return false
	}

	contentType, exists := resp.Header["Content-Type"]
	if !exists || contentType == "" {
		return false
	}
	return strings.Contains(contentType, "application/xml") || strings.Contains(contentType, "text/xml")
}

// TrimSpaces removes unwanted spaces around unescaped ',' and '='
func TrimSpaces(input string) string {
	var output strings.Builder
	var lastWasEscape bool  // tracks if the previous rune was a backslash
	var skippingSpaces bool // set when we just wrote a separator

	for _, ch := range input {
		// after writing a separator, drop any spaces
		if skippingSpaces {
			if ch == ' ' {
				continue
			}
			skippingSpaces = false
		}

		// handle escape character
		if ch == '\\' && !lastWasEscape {
			lastWasEscape = true
			output.WriteRune(ch)
			continue
		}

		// if this is an unescaped separator, trim before & skip after
		if (ch == ',' || ch == '=') && !lastWasEscape {
			// remove trailing spaces before the separator
			trimmed := strings.TrimRight(output.String(), " ")
			output.Reset()
			output.WriteString(trimmed)

			// write the separator itself
			output.WriteRune(ch)

			// skip any spaces that follow
			skippingSpaces = true
			lastWasEscape = false
			continue
		}

		// normal character (or escaped separator)
		output.WriteRune(ch)
		lastWasEscape = false
	}

	return output.String()
}

func ParseMetadata(metadataStr string) (map[string]interface{}, error) {
	if metadataStr == "" {
		return nil, nil
	}
	m := make(map[string]interface{})
	if err := strvals.ParseInto(metadataStr, m); err != nil {
		return nil, fmt.Errorf("cannot parse metadata: %w", err)
	}
	return m, nil
}

// // XMLToMap converts an XML string into a map[string]interface{}
// func XMLToMap(data string) (map[string]any, error) {
// 	mv, err := mxj.NewMapXml([]byte(data))
// 	if err != nil {
// 		return nil, err
// 	}
// 	return mv, nil
// }

// // MapToXML converts a map[string]interface{} into an XML string
// func MapToXML(data map[string]any) (string, error) {
// 	mv := mxj.Map(data)
// 	xmlBytes, err := mv.Xml()
// 	if err != nil {
// 		return "", err
// 	}
// 	return string(xmlBytes), nil
// }

func NetworkToHostShort(net uint16) uint16 {
	b := make([]byte, 2)
	binary.BigEndian.PutUint16(b, net)
	return binary.LittleEndian.Uint16(b)
}

func ParseGRPCPath(p string) (serviceFull, method string, err error) {
	// Trim whitespace and validate input
	p = strings.TrimSpace(p)
	if p == "" {
		return "", "", fmt.Errorf("gRPC path cannot be empty")
	}

	// Store original path for error messages
	originalPath := p
	p = strings.TrimPrefix(p, "/")

	// Split path into components
	parts := strings.Split(p, "/")
	if len(parts) != 2 {
		return "", "", fmt.Errorf("invalid gRPC path %q: expected format '/package.Service/Method', got %d path segments", originalPath, len(parts))
	}

	serviceFull = strings.TrimSpace(parts[0])
	method = strings.TrimSpace(parts[1])

	// Validate service name is not empty
	if serviceFull == "" {
		return "", "", fmt.Errorf("invalid gRPC path %q: service name cannot be empty", originalPath)
	}

	// Validate method name is not empty
	if method == "" {
		return "", "", fmt.Errorf("invalid gRPC path %q: method name cannot be empty", originalPath)
	}

	// Validate service name format (should contain at least package.Service)
	if !strings.Contains(serviceFull, ".") {
		return "", "", fmt.Errorf("invalid service name %q in path %q: expected format 'package.Service'", serviceFull, originalPath)
	}

	// Validate service name doesn't start or end with dots
	if strings.HasPrefix(serviceFull, ".") || strings.HasSuffix(serviceFull, ".") {
		return "", "", fmt.Errorf("invalid service name %q in path %q: cannot start or end with '.'", serviceFull, originalPath)
	}

	// Validate service name doesn't contain consecutive dots
	if strings.Contains(serviceFull, "..") {
		return "", "", fmt.Errorf("invalid service name %q in path %q: cannot contain consecutive dots", serviceFull, originalPath)
	}

	// Validate method name format (basic identifier validation)
	if !isValidGRPCIdentifier(method) {
		return "", "", fmt.Errorf("invalid method name %q in path %q: must be a valid identifier", method, originalPath)
	}

	// Validate service parts are valid identifiers
	serviceParts := strings.Split(serviceFull, ".")
	for i, part := range serviceParts {
		if !isValidGRPCIdentifier(part) {
			return "", "", fmt.Errorf("invalid service name component %q at position %d in path %q: must be a valid identifier", part, i, originalPath)
		}
	}

	return serviceFull, method, nil
}

// isValidGRPCIdentifier validates if a string is a valid gRPC identifier
// gRPC identifiers must start with a letter and contain only letters, digits, and underscores
func isValidGRPCIdentifier(name string) bool {
	if len(name) == 0 {
		return false
	}

	// First character must be a letter or underscore
	first := name[0]
	if !((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z') || first == '_') {
		return false
	}

	// Remaining characters must be letters, digits, or underscores
	for i := 1; i < len(name); i++ {
		c := name[i]
		if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_') {
			return false
		}
	}

	return true
}

func GetContainerIPv4() (string, error) {
	// Get all network interfaces
	interfaces, err := net.Interfaces()
	if err != nil {
		return "", err
	}

	// Iterate over the interfaces
	for _, i := range interfaces {
		// Skip down or loopback interfaces
		if i.Flags&net.FlagUp == 0 || i.Flags&net.FlagLoopback != 0 {
			continue
		}

		// Get the addresses for the current interface
		addrs, err := i.Addrs()
		if err != nil {
			continue
		}

		// Iterate over the addresses
		for _, addr := range addrs {
			var ip net.IP
			// The address can be of type *net.IPNet or *net.IPAddr
			if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
				// Check if it's an IPv4 address
				if ipnet.IP.To4() != nil {
					ip = ipnet.IP
				}
			}

			if ip != nil {
				// Found a valid IPv4 address, return it
				return ip.String(), nil
			}
		}
	}

	// Tagged, so the agent that cannot start without it says so in its exit
	// status: a container with no IPv4 address but loopback's (docker
	// --network none, an IPv6-only network) is the environment to change, not
	// a privilege to grant. Only an agent started with --is-docker asks (its
	// hooks, as they load): they send the connections of the applications it
	// serves to this address. A native one is reached over loopback and needs
	// no address of its own.
	return "", fmt.Errorf("%w: could not find a non-loopback IP for the container", ErrEnvironmentUnsupported)
}

// GetFullCommandUsed returns the full command-line used to run the current process.
// It reconstructs the command from os.Args, adding quoting for arguments with spaces or quotes.
func GetFullCommandUsed() string {
	args := os.Args

	// Build the command string with proper quoting for arguments containing spaces
	var parts []string
	for _, arg := range args {
		if strings.Contains(arg, " ") || strings.Contains(arg, "\"") {
			// Quote the argument if it contains spaces or quotes
			parts = append(parts, fmt.Sprintf("%q", arg))
		} else {
			parts = append(parts, arg)
		}
	}

	cmdStr := strings.Join(parts, " ")

	return cmdStr
}