diff --git a/go.mod b/go.mod index 3da86911..5793ef8b 100644 --- a/go.mod +++ b/go.mod @@ -47,7 +47,7 @@ require ( github.com/sourcegraph/jsonrpc2 v0.2.1 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 - github.com/testcontainers/testcontainers-go v0.42.0 + github.com/testcontainers/testcontainers-go v0.43.0 github.com/vektra/mockery/v2 v2.53.6 github.com/vishvananda/netlink v1.3.1 github.com/vishvananda/netns v0.0.5 @@ -171,7 +171,7 @@ require ( github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect github.com/safchain/ethtool v0.6.2 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect - github.com/shirou/gopsutil/v4 v4.26.3 // indirect + github.com/shirou/gopsutil/v4 v4.26.5 // indirect github.com/sirupsen/logrus v1.9.4 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect diff --git a/go.sum b/go.sum index 20dae804..02b55572 100644 --- a/go.sum +++ b/go.sum @@ -430,6 +430,8 @@ github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88ee github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/shirou/gopsutil/v4 v4.26.5 h1:RPcBXkpz7kOj9PqGFQOlBPZHsyaPvPVQc098y9RmCNM= +github.com/shirou/gopsutil/v4 v4.26.5/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/sourcegraph/jsonrpc2 v0.2.1 h1:2GtljixMQYUYCmIg7W9aF2dFmniq/mOr2T9tFRh6zSQ= @@ -456,6 +458,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= +github.com/testcontainers/testcontainers-go v0.43.0 h1:oEQx5MW2DGd9z3AeEQfB2lPM0eLs7ztyaGRu75bFo5A= +github.com/testcontainers/testcontainers-go v0.43.0/go.mod h1:+VxkT2NQnKOZPKi6praMuMKYHYyOGXr0XSBSlSMCzFo= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= diff --git a/vendor/github.com/shirou/gopsutil/v4/cpu/cpu_windows.go b/vendor/github.com/shirou/gopsutil/v4/cpu/cpu_windows.go index a6000a3c..c15a085b 100644 --- a/vendor/github.com/shirou/gopsutil/v4/cpu/cpu_windows.go +++ b/vendor/github.com/shirou/gopsutil/v4/cpu/cpu_windows.go @@ -25,6 +25,7 @@ var ( procGetLogicalProcessorInformationEx = common.Modkernel32.NewProc("GetLogicalProcessorInformationEx") procGetSystemFirmwareTable = common.Modkernel32.NewProc("GetSystemFirmwareTable") procCallNtPowerInformation = common.ModPowrProf.NewProc("CallNtPowerInformation") + procGetActiveProcessorGroupCount = common.Modkernel32.NewProc("GetActiveProcessorGroupCount") ) type win32_Processor struct { //nolint:revive //FIXME @@ -263,6 +264,21 @@ func perCPUTimes() ([]TimesStat, error) { // makes call to Windows API function to retrieve performance information for each core func perfInfo() ([]win32_SystemProcessorPerformanceInformation, error) { + // On hosts with more than 64 logical CPUs Windows splits CPUs into Processor Groups + // (up to 64 logical CPUs per group). The non-Ex NtQuerySystemInformation only returns + // data for the calling thread's group, so whenever the Ex variant is available we + // iterate every active group and concatenate the results. See issue #887. + if common.ProcNtQuerySystemInformationEx.Find() == nil { + return perfInfoAllGroups() + } + return perfInfoSingleGroup() +} + +// perfInfoSingleGroup queries SystemProcessorPerformanceInformation via the non-Ex +// NtQuerySystemInformation call. This is the legacy fallback for environments where +// NtQuerySystemInformationEx cannot be resolved; it only returns data for the calling +// thread's processor group. +func perfInfoSingleGroup() ([]win32_SystemProcessorPerformanceInformation, error) { // Make maxResults large for safety. // We can't invoke the api call with a results array that's too small. // If we have more than 2056 cores on a single host, then it's probably the future. @@ -286,16 +302,61 @@ func perfInfo() ([]win32_SystemProcessorPerformanceInformation, error) { // check return code for errors if retCode != 0 { - return nil, fmt.Errorf("call to NtQuerySystemInformation returned %d. err: %s", retCode, err.Error()) + return nil, fmt.Errorf("call to NtQuerySystemInformation returned 0x%x: %w", retCode, err) } // calculate the number of returned elements based on the returned size numReturnedElements := retSize / win32_SystemProcessorPerformanceInfoSize // trim results to the number of returned elements - resultBuffer = resultBuffer[:numReturnedElements] + return resultBuffer[:numReturnedElements], nil +} - return resultBuffer, nil +// perfInfoAllGroups queries SystemProcessorPerformanceInformation for every active +// processor group via NtQuerySystemInformationEx and concatenates the results. The +// group index is passed as the InputBuffer per the Ex calling convention documented at +// https://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/sysinfo/queryex.htm +func perfInfoAllGroups() ([]win32_SystemProcessorPerformanceInformation, error) { + // GetActiveProcessorGroupCount returns 0 only on failure; propagate the error + // rather than silently defaulting to a single group and returning partial data. + r, _, callErr := procGetActiveProcessorGroupCount.Call() + if r == 0 { + return nil, fmt.Errorf("GetActiveProcessorGroupCount returned 0: %w", callErr) + } + groupCount := uint16(r) + + var result []win32_SystemProcessorPerformanceInformation + for g := uint16(0); g < groupCount; g++ { + numLP := windows.GetActiveProcessorCount(g) + if numLP == 0 { + return nil, fmt.Errorf("GetActiveProcessorCount returned 0 for processor group %d", g) + } + // buffer sized exactly for this group's logical CPU count + buf := make([]win32_SystemProcessorPerformanceInformation, numLP) + bufSize := uintptr(win32_SystemProcessorPerformanceInfoSize) * uintptr(numLP) + var retSize uint32 + // InputBuffer is a USHORT (2 bytes) holding the target processor group index. + group := g + retCode, _, err := common.ProcNtQuerySystemInformationEx.Call( + win32_SystemProcessorPerformanceInformationClass, // System Information Class -> SystemProcessorPerformanceInformation + uintptr(unsafe.Pointer(&group)), // InputBuffer: pointer to USHORT group index + unsafe.Sizeof(group), // InputBufferLength: sizeof(USHORT) = 2 + uintptr(unsafe.Pointer(&buf[0])), // pointer to first element in result buffer + bufSize, // size of the buffer in memory + uintptr(unsafe.Pointer(&retSize)), // pointer to the size of the returned results the windows proc will set this + ) + if retCode != 0 { + return nil, fmt.Errorf("call to NtQuerySystemInformationEx(group=%d) returned 0x%x: %w", g, retCode, err) + } + // Guard against a retSize that is not a whole number of entries or exceeds + // the allocated buffer (e.g. CPU hot-add racing with GetActiveProcessorCount). + if retSize%win32_SystemProcessorPerformanceInfoSize != 0 || uintptr(retSize) > bufSize { + return nil, fmt.Errorf("NtQuerySystemInformationEx(group=%d) returned unexpected retSize=%d (bufSize=%d)", g, retSize, bufSize) + } + n := retSize / win32_SystemProcessorPerformanceInfoSize + result = append(result, buf[:n]...) + } + return result, nil } // SystemInfo is an equivalent representation of SYSTEM_INFO in the Windows API. diff --git a/vendor/github.com/shirou/gopsutil/v4/internal/common/common_windows.go b/vendor/github.com/shirou/gopsutil/v4/internal/common/common_windows.go index 31df6efe..3778e4d9 100644 --- a/vendor/github.com/shirou/gopsutil/v4/internal/common/common_windows.go +++ b/vendor/github.com/shirou/gopsutil/v4/internal/common/common_windows.go @@ -73,6 +73,7 @@ var ( ProcGetSystemTimes = Modkernel32.NewProc("GetSystemTimes") ProcNtQuerySystemInformation = ModNt.NewProc("NtQuerySystemInformation") + ProcNtQuerySystemInformationEx = ModNt.NewProc("NtQuerySystemInformationEx") ProcRtlGetNativeSystemInformation = ModNt.NewProc("RtlGetNativeSystemInformation") ProcRtlNtStatusToDosError = ModNt.NewProc("RtlNtStatusToDosError") ProcNtQueryInformationProcess = ModNt.NewProc("NtQueryInformationProcess") diff --git a/vendor/github.com/shirou/gopsutil/v4/mem/mem_netbsd.go b/vendor/github.com/shirou/gopsutil/v4/mem/mem_netbsd.go index 8ef539ca..2efa1199 100644 --- a/vendor/github.com/shirou/gopsutil/v4/mem/mem_netbsd.go +++ b/vendor/github.com/shirou/gopsutil/v4/mem/mem_netbsd.go @@ -61,6 +61,7 @@ func SwapMemory() (*SwapMemoryStat, error) { return SwapMemoryWithContext(context.Background()) } +// Reference: https://man.netbsd.org/swapctl.8 func SwapMemoryWithContext(ctx context.Context) (*SwapMemoryStat, error) { out, err := invoke.CommandWithContext(ctx, "swapctl", "-sk") if err != nil { @@ -71,7 +72,7 @@ func SwapMemoryWithContext(ctx context.Context) (*SwapMemoryStat, error) { var total, used, free uint64 _, err = fmt.Sscanf(line, - "total: %d 1K-blocks allocated, %d used, %d available", + "total: %d KBytes allocated, %d KBytes used, %d KBytes available", &total, &used, &free) if err != nil { return nil, errors.New("failed to parse swapctl output") diff --git a/vendor/github.com/shirou/gopsutil/v4/net/net.go b/vendor/github.com/shirou/gopsutil/v4/net/net.go index 1d1f9f08..384e5673 100644 --- a/vendor/github.com/shirou/gopsutil/v4/net/net.go +++ b/vendor/github.com/shirou/gopsutil/v4/net/net.go @@ -286,12 +286,25 @@ func IOCountersByFile(pernic bool, filename string) ([]IOCountersStat, error) { return IOCountersByFileWithContext(context.Background(), pernic, filename) } -// ProtoCounters returns network statistics for the entire system +// ProtoCounters returns network statistics for the entire system. // If protocols is empty then all protocols are returned, otherwise // just the protocols in the list are returned. +// // Available protocols: // [ip,icmp,icmpmsg,tcp,udp,udplite] -// Not Implemented for FreeBSD, Windows, OpenBSD, Darwin +// +// Key naming contract: +// The keys of the returned Stats map use MIB-II (RFC 1213) identifier +// names — e.g. "InSegs", "OutSegs", "RetransSegs", "ActiveOpens", +// "InDatagrams", "NoPorts". This contract was established by the +// original Linux implementation, which sources its keys from +// /proc/net/snmp (whose headers are MIB-II names). Per-platform +// implementations are expected to map their native counter sources +// (e.g. AIX `netstat -s`, BSD sysctl) to these MIB-II names. +// Native counters that have no MIB-II equivalent are not exposed +// through this API. +// +// Not Implemented for FreeBSD, Windows, OpenBSD, Darwin, Solaris. func ProtoCounters(protocols []string) ([]ProtoCountersStat, error) { return ProtoCountersWithContext(context.Background(), protocols) } diff --git a/vendor/github.com/shirou/gopsutil/v4/net/net_aix.go b/vendor/github.com/shirou/gopsutil/v4/net/net_aix.go index 4531dd44..9f2e145f 100644 --- a/vendor/github.com/shirou/gopsutil/v4/net/net_aix.go +++ b/vendor/github.com/shirou/gopsutil/v4/net/net_aix.go @@ -31,8 +31,240 @@ func ConntrackStatsWithContext(_ context.Context, _ bool) ([]ConntrackStat, erro return nil, common.ErrNotImplementedError } -func ProtoCountersWithContext(_ context.Context, _ []string) ([]ProtoCountersStat, error) { - return nil, common.ErrNotImplementedError +func ProtoCountersWithContext(ctx context.Context, protocols []string) ([]ProtoCountersStat, error) { + out, err := invoke.CommandWithContext(ctx, "netstat", "-s") + if err != nil { + return nil, err + } + return parseNetstatS(string(out), protocols) +} + +// parseNetstatS parses "netstat -s" output on AIX. +// +// Format: +// +// : +// \t +// \t\t +// +// Descriptions containing parenthetical sub-counts (e.g. "(6302116893 bytes)") +// are normalised by stripping the parenthetical before matching. +func parseNetstatS(output string, protocols []string) ([]ProtoCountersStat, error) { + var stats []ProtoCountersStat + var currentProto string + var currentStats map[string]int64 + + for _, line := range strings.Split(output, "\n") { + // Protocol header: no leading whitespace, ends with ":" + if line != "" && line[0] != '\t' { + if currentProto != "" && len(currentStats) > 0 { + if len(protocols) == 0 || common.StringsHas(protocols, currentProto) { + stats = append(stats, ProtoCountersStat{ + Protocol: currentProto, + Stats: currentStats, + }) + } + } + currentProto = strings.TrimSuffix(strings.TrimSpace(line), ":") + currentStats = make(map[string]int64) + continue + } + + if currentProto == "" { + continue + } + + // Count leading tabs to track indentation depth (1 = top-level metric). + depth := 0 + rest := line + for rest != "" && rest[0] == '\t' { + depth++ + rest = rest[1:] + } + if depth == 0 || rest == "" { + continue + } + + // Split " ". + spaceIdx := strings.IndexByte(rest, ' ') + if spaceIdx <= 0 { + continue + } + val, err := strconv.ParseInt(rest[:spaceIdx], 10, 64) + if err != nil { + continue + } + // Normalise: remove parenthetical sub-counts like "(6302116893 bytes)". + desc := normaliseNetstatDesc(strings.TrimSpace(rest[spaceIdx+1:])) + + if key := aixProtoKey(currentProto, depth, desc); key != "" { + currentStats[key] += val + } + } + + if currentProto != "" && len(currentStats) > 0 { + if len(protocols) == 0 || common.StringsHas(protocols, currentProto) { + stats = append(stats, ProtoCountersStat{ + Protocol: currentProto, + Stats: currentStats, + }) + } + } + + return stats, nil +} + +// normaliseNetstatDesc strips a single parenthetical from a netstat -s description, +// e.g. "data packets (6302116893 bytes) retransmitted" → "data packets retransmitted". +func normaliseNetstatDesc(s string) string { + start := strings.Index(s, "(") + if start == -1 { + return s + } + end := strings.LastIndex(s, ")") + if end < start { + return s + } + return strings.Join(strings.Fields(s[:start]+s[end+1:]), " ") +} + +// aixProtoKey maps a normalised AIX netstat -s description to a ProtoCountersStat key. +// depth is the tab-indentation level (1 = top-level line under the protocol header). +// Returns "" for lines that should be ignored. +func aixProtoKey(proto string, depth int, desc string) string { + switch proto { + case "tcp": + return aixTCPKey(depth, desc) + case "udp": + return aixUDPKey(desc) + case "ip": + return aixIPKey(depth, desc) + case "ipv6": + return aixIPv6Key(depth, desc) + } + return "" +} + +func aixTCPKey(depth int, desc string) string { + switch { + // Top-level totals — depth check avoids matching sub-lines like "N data packets sent". + case depth == 1 && desc == "packets sent": + return "OutSegs" + case depth == 1 && desc == "packets received": + return "InSegs" + // Sub-line: "data packets NNN bytes retransmitted" → normalised "data packets retransmitted" + case strings.Contains(desc, "retransmitted"): + return "RetransSegs" + case desc == "connection requests": + return "ActiveOpens" + case desc == "connection accepts": + return "PassiveOpens" + case desc == "embryonic connections dropped": + return "AttemptFails" + case strings.HasPrefix(desc, "discarded for bad checksums"): + return "InCsumErrors" + // Other input errors accumulate into InErrs. + case strings.HasPrefix(desc, "discarded for bad header") || + strings.HasPrefix(desc, "discarded because packet too short"): + return "InErrs" + } + return "" +} + +func aixUDPKey(desc string) string { + switch { + case desc == "delivered": + return "InDatagrams" + // Both unicast and broadcast "dropped due to no socket" map to NoPorts. + case strings.Contains(desc, "dropped due to no socket"): + return "NoPorts" + case desc == "bad checksums": + return "InCsumErrors" + case desc == "socket buffer overflows": + return "RcvbufErrors" + case desc == "datagrams output": + return "OutDatagrams" + case desc == "incomplete headers" || desc == "bad data length fields": + return "InErrors" + } + return "" +} + +func aixIPKey(depth int, desc string) string { + switch { + case desc == "total packets received": + return "InReceives" + // All header-error variants accumulate into InHdrErrors. + case desc == "bad header checksums" || + desc == "with size smaller than minimum" || + desc == "with data size < data length" || + desc == "with header length < data size" || + desc == "with data length < header length" || + desc == "with bad options" || + desc == "with incorrect version number": + return "InHdrErrors" + case strings.Contains(desc, "unknown/unsupported protocol"): + return "InUnknownProtos" + case desc == "packets for this host": + return "InDelivers" + case depth == 1 && desc == "packets forwarded": + return "ForwDatagrams" + case desc == "packets sent from this host": + return "OutRequests" + case desc == "packets reassembled ok": + return "ReasmOKs" + case strings.HasPrefix(desc, "fragments dropped after timeout"): + return "ReasmFails" + case desc == "fragments received": + return "ReasmReqds" + case strings.HasPrefix(desc, "fragments dropped"): + return "InDiscards" + case desc == "fragments created": + return "FragCreates" + case desc == "output packets discarded due to no route": + return "OutNoRoutes" + case strings.HasPrefix(desc, "output packets dropped due to no bufs"): + return "OutDiscards" + } + return "" +} + +func aixIPv6Key(depth int, desc string) string { + switch { + case desc == "total packets received": + return "InReceives" + case desc == "with size smaller than minimum" || + desc == "with data size < data length" || + desc == "with incorrect version number" || + desc == "with illegal source": + return "InHdrErrors" + case strings.Contains(desc, "unknown/unsupported protocol"): + return "InUnknownProtos" + case desc == "input packets without enough memory": + return "InDiscards" + case desc == "packets for this host": + return "InDelivers" + case depth == 1 && desc == "packets forwarded": + return "ForwDatagrams" + case desc == "packets sent from this host": + return "OutRequests" + case desc == "packets reassembled ok": + return "ReasmOKs" + case strings.HasPrefix(desc, "fragments dropped after timeout"): + return "ReasmFails" + case desc == "fragments received": + return "ReasmReqds" + case strings.HasPrefix(desc, "fragments dropped"): + return "InDiscards" + case desc == "fragments created": + return "FragCreates" + case desc == "output packets discarded due to no route": + return "OutNoRoutes" + case strings.HasPrefix(desc, "output packets dropped due to no bufs") || + desc == "output packets without enough memory": + return "OutDiscards" + } + return "" } func parseNetstatNetLine(line string) (ConnectionStat, error) { diff --git a/vendor/github.com/shirou/gopsutil/v4/net/net_aix_nocgo.go b/vendor/github.com/shirou/gopsutil/v4/net/net_aix_nocgo.go index 834534d3..27d37a99 100644 --- a/vendor/github.com/shirou/gopsutil/v4/net/net_aix_nocgo.go +++ b/vendor/github.com/shirou/gopsutil/v4/net/net_aix_nocgo.go @@ -78,6 +78,35 @@ func parseNetstatI(output string) ([]IOCountersStat, error) { return ret, nil } +// parseEntstat extracts BytesSent and BytesRecv from entstat output. +// The entstat two-column Transmit/Receive format (including the "Bytes:" +// line) has been stable across AIX 4.3 through 7.3 (over 25 years). +// The output has a two-column layout with Transmit on the left and Receive +// on the right, e.g.: +// +// Bytes: 3509236040 Bytes: 4547812126 +func parseEntstat(output string) (bytesSent, bytesRecv uint64) { + for _, line := range strings.Split(output, "\n") { + if !strings.Contains(line, "Bytes:") { + continue + } + // Split on "Bytes:" to get: ["", " 3509236040 ", " 4547812126"] + parts := strings.Split(line, "Bytes:") + if len(parts) >= 2 { + if v, err := strconv.ParseUint(strings.TrimSpace(parts[1]), 10, 64); err == nil { + bytesSent = v + } + } + if len(parts) >= 3 { + if v, err := strconv.ParseUint(strings.TrimSpace(parts[2]), 10, 64); err == nil { + bytesRecv = v + } + } + return + } + return +} + func IOCountersWithContext(ctx context.Context, pernic bool) ([]IOCountersStat, error) { out, err := invoke.CommandWithContext(ctx, "netstat", "-idn") if err != nil { @@ -88,6 +117,24 @@ func IOCountersWithContext(ctx context.Context, pernic bool) ([]IOCountersStat, if err != nil { return nil, err } + + // Populate BytesSent/BytesRecv via entstat for each interface. + // entstat only works on hardware/virtual ethernet adapters; it fails + // on loopback (lo0) with errno 19, which is silently skipped. Errors + // on other interfaces are propagated since they indicate a real problem. + for i := range iocounters { + entOut, err := invoke.CommandWithContext(ctx, "entstat", iocounters[i].Name) + if err != nil { + // entstat fails on loopback (lo0) with errno 19 — this is expected. + // For other interfaces, propagate the error. + if iocounters[i].Name == "lo0" { + continue + } + return nil, err + } + iocounters[i].BytesSent, iocounters[i].BytesRecv = parseEntstat(string(entOut)) + } + if !pernic { return getIOCountersAll(iocounters), nil } diff --git a/vendor/github.com/shirou/gopsutil/v4/net/net_fallback.go b/vendor/github.com/shirou/gopsutil/v4/net/net_fallback.go index 29c2a148..0c0e57da 100644 --- a/vendor/github.com/shirou/gopsutil/v4/net/net_fallback.go +++ b/vendor/github.com/shirou/gopsutil/v4/net/net_fallback.go @@ -1,5 +1,5 @@ // SPDX-License-Identifier: BSD-3-Clause -//go:build !aix && !darwin && !linux && !freebsd && !openbsd && !windows && !solaris +//go:build !aix && !darwin && !linux && !freebsd && !openbsd && !windows && !solaris && !netbsd package net diff --git a/vendor/github.com/shirou/gopsutil/v4/net/net_linux.go b/vendor/github.com/shirou/gopsutil/v4/net/net_linux.go index a3dd17aa..96a78ff8 100644 --- a/vendor/github.com/shirou/gopsutil/v4/net/net_linux.go +++ b/vendor/github.com/shirou/gopsutil/v4/net/net_linux.go @@ -69,6 +69,9 @@ func IOCountersByFileWithContext(_ context.Context, pernic bool, filename string } fields := strings.Fields(strings.TrimSpace(statsPart)) + if len(fields) < 13 { + continue + } bytesRecv, err := strconv.ParseUint(fields[0], 10, 64) if err != nil { return ret, err diff --git a/vendor/github.com/shirou/gopsutil/v4/net/net_netbsd.go b/vendor/github.com/shirou/gopsutil/v4/net/net_netbsd.go new file mode 100644 index 00000000..d0e44fbf --- /dev/null +++ b/vendor/github.com/shirou/gopsutil/v4/net/net_netbsd.go @@ -0,0 +1,353 @@ +// SPDX-License-Identifier: BSD-3-Clause +//go:build netbsd + +package net + +import ( + "context" + "fmt" + "os/exec" + "regexp" + "strconv" + "strings" + "syscall" + + "github.com/shirou/gopsutil/v4/internal/common" +) + +var portMatch = regexp.MustCompile(`(.*)\.(\d+)$`) + +// parseNetstat parses the output of "netstat -inb" (mode "inb") or +// "netstat -ind" (mode "ind") and merges results into iocs. +// +// NetBSD netstat column layout (0-indexed fields after strings.Fields): +// +// -inb with Address (6 fields): Name Mtu Network Address Ibytes Obytes +// -inb without Address (5 fields): Name Mtu Network Ibytes Obytes +// +// -ind with Address (11 fields): Name Mtu Network Address Ipkts Ierrs Idrops Opkts Oerrs Colls Odrops +// -ind without Address (10 fields): Name Mtu Network Ipkts Ierrs Idrops Opkts Oerrs Colls Odrops +// +// The Address field is present for non-loopback interfaces and absent for +// loopback (lo0). We detect this via field count and set base accordingly. +// +// Reference: https://man.netbsd.org/netstat.1 +func parseNetstat(output, mode string, iocs map[string]IOCountersStat) error { + // Minimum field counts when Address is absent. + minFields := map[string]int{ + "inb": 5, + "ind": 10, + } + // Field count when Address is present (base = 1). + addrFields := map[string]int{ + "inb": 6, + "ind": 11, + } + + seen := make([]string, 0) + + for _, line := range strings.Split(output, "\n") { + values := strings.Fields(line) + if len(values) < 1 || values[0] == "Name" { + continue + } + if common.StringsHas(seen, values[0]) { + continue + } + if len(values) < minFields[mode] { + continue + } + + base := 0 + if len(values) >= addrFields[mode] { + base = 1 + } + + seen = append(seen, values[0]) + + n, present := iocs[values[0]] + if !present { + n = IOCountersStat{Name: values[0]} + } + + switch mode { + case "inb": + recv, err := parseUint(values[base+3]) + if err != nil { + return err + } + sent, err := parseUint(values[base+4]) + if err != nil { + return err + } + n.BytesRecv = recv + n.BytesSent = sent + + case "ind": + // Ipkts Ierrs Idrops Opkts Oerrs Colls Odrops + pktsRecv, err := parseUint(values[base+3]) + if err != nil { + return err + } + errin, err := parseUint(values[base+4]) + if err != nil { + return err + } + dropin, err := parseUint(values[base+5]) + if err != nil { + return err + } + pktsSent, err := parseUint(values[base+6]) + if err != nil { + return err + } + errout, err := parseUint(values[base+7]) + if err != nil { + return err + } + // values[base+8] = Colls (not mapped to IOCountersStat) + dropout, err := parseUint(values[base+9]) + if err != nil { + return err + } + n.PacketsRecv = pktsRecv + n.Errin = errin + n.Dropin = dropin + n.PacketsSent = pktsSent + n.Errout = errout + n.Dropout = dropout + } + + iocs[n.Name] = n + } + return nil +} + +func parseUint(s string) (uint64, error) { + if s == "-" { + return 0, nil + } + return strconv.ParseUint(s, 10, 64) +} + +// Deprecated: use process.PidsWithContext instead +func PidsWithContext(_ context.Context) ([]int32, error) { + return nil, common.ErrNotImplementedError +} + +func IOCountersWithContext(ctx context.Context, pernic bool) ([]IOCountersStat, error) { + netstat, err := exec.LookPath("netstat") + if err != nil { + return nil, err + } + + outBytes, err := invoke.CommandWithContext(ctx, netstat, "-inb") + if err != nil { + return nil, err + } + outPackets, err := invoke.CommandWithContext(ctx, netstat, "-ind") + if err != nil { + return nil, err + } + + iocs := make(map[string]IOCountersStat) + + if err := parseNetstat(string(outBytes), "inb", iocs); err != nil { + return nil, err + } + if err := parseNetstat(string(outPackets), "ind", iocs); err != nil { + return nil, err + } + + ret := make([]IOCountersStat, 0, len(iocs)) + for _, ioc := range iocs { + ret = append(ret, ioc) + } + + if !pernic { + return getIOCountersAll(ret), nil + } + + return ret, nil +} + +func IOCountersByFileWithContext(ctx context.Context, pernic bool, _ string) ([]IOCountersStat, error) { + return IOCountersWithContext(ctx, pernic) +} + +func FilterCountersWithContext(_ context.Context) ([]FilterStat, error) { + return nil, common.ErrNotImplementedError +} + +func ConntrackStatsWithContext(_ context.Context, _ bool) ([]ConntrackStat, error) { + return nil, common.ErrNotImplementedError +} + +func ProtoCountersWithContext(_ context.Context, _ []string) ([]ProtoCountersStat, error) { + return nil, common.ErrNotImplementedError +} + +func parseNetstatLine(line string) (ConnectionStat, error) { + f := strings.Fields(line) + if len(f) < 5 { + return ConnectionStat{}, fmt.Errorf("wrong line,%s", line) + } + + var netType, netFamily uint32 + switch f[0] { + case "tcp": + netType = syscall.SOCK_STREAM + netFamily = syscall.AF_INET + case "udp": + netType = syscall.SOCK_DGRAM + netFamily = syscall.AF_INET + case "tcp6": + netType = syscall.SOCK_STREAM + netFamily = syscall.AF_INET6 + case "udp6": + netType = syscall.SOCK_DGRAM + netFamily = syscall.AF_INET6 + default: + return ConnectionStat{}, fmt.Errorf("unknown type, %s", f[0]) + } + + laddr, raddr, err := parseNetstatAddr(f[3], f[4], netFamily) + if err != nil { + return ConnectionStat{}, fmt.Errorf("failed to parse netaddr, %s %s", f[3], f[4]) + } + + n := ConnectionStat{ + Fd: uint32(0), // not supported + Family: uint32(netFamily), + Type: uint32(netType), + Laddr: laddr, + Raddr: raddr, + Pid: int32(0), // not supported + } + if len(f) == 6 { + n.Status = f[5] + } + + return n, nil +} + +func parseAddr(l string, family uint32) (Addr, error) { + matches := portMatch.FindStringSubmatch(l) + if matches == nil { + return Addr{}, fmt.Errorf("wrong addr, %s", l) + } + host := matches[1] + port := matches[2] + if host == "*" { + switch family { + case syscall.AF_INET: + host = "0.0.0.0" + case syscall.AF_INET6: + host = "::" + default: + return Addr{}, fmt.Errorf("unknown family, %d", family) + } + } + lport, err := strconv.ParseInt(port, 10, 32) + if err != nil { + return Addr{}, err + } + return Addr{IP: host, Port: uint32(lport)}, nil +} + +func parseNetstatAddr(local, remote string, family uint32) (laddr, raddr Addr, err error) { + laddr, err = parseAddr(local, family) + if err != nil { + return laddr, raddr, err + } + if remote != "*.*" { + raddr, err = parseAddr(remote, family) + if err != nil { + return laddr, raddr, err + } + } + return laddr, raddr, err +} + +func ConnectionsWithContext(ctx context.Context, kind string) ([]ConnectionStat, error) { + var ret []ConnectionStat + + args := []string{"-na"} + switch strings.ToLower(kind) { + default: + fallthrough + case "", "all", "inet": + // nothing to add + case "inet4": + args = append(args, "-finet") + case "inet6": + args = append(args, "-finet6") + case "tcp": + args = append(args, "-ptcp") + case "tcp4": + args = append(args, "-ptcp", "-finet") + case "tcp6": + args = append(args, "-ptcp", "-finet6") + case "udp": + args = append(args, "-pudp") + case "udp4": + args = append(args, "-pudp", "-finet") + case "udp6": + args = append(args, "-pudp", "-finet6") + case "unix": + return ret, common.ErrNotImplementedError + } + + netstat, err := exec.LookPath("netstat") + if err != nil { + return nil, err + } + out, err := invoke.CommandWithContext(ctx, netstat, args...) + if err != nil { + return nil, err + } + for _, line := range strings.Split(string(out), "\n") { + if !strings.HasPrefix(line, "tcp") && !strings.HasPrefix(line, "udp") { + continue + } + n, err := parseNetstatLine(line) + if err != nil { + continue + } + ret = append(ret, n) + } + + return ret, nil +} + +func ConnectionsPidWithContext(_ context.Context, _ string, _ int32) ([]ConnectionStat, error) { + return nil, common.ErrNotImplementedError +} + +func ConnectionsMaxWithContext(_ context.Context, _ string, _ int) ([]ConnectionStat, error) { + return nil, common.ErrNotImplementedError +} + +func ConnectionsPidMaxWithContext(_ context.Context, _ string, _ int32, _ int) ([]ConnectionStat, error) { + return nil, common.ErrNotImplementedError +} + +func ConnectionsWithoutUidsWithContext(ctx context.Context, kind string) ([]ConnectionStat, error) { + return ConnectionsMaxWithoutUidsWithContext(ctx, kind, 0) +} + +func ConnectionsMaxWithoutUidsWithContext(ctx context.Context, kind string, maxConn int) ([]ConnectionStat, error) { + return ConnectionsPidMaxWithoutUidsWithContext(ctx, kind, 0, maxConn) +} + +func ConnectionsPidWithoutUidsWithContext(ctx context.Context, kind string, pid int32) ([]ConnectionStat, error) { + return ConnectionsPidMaxWithoutUidsWithContext(ctx, kind, pid, 0) +} + +func ConnectionsPidMaxWithoutUidsWithContext(ctx context.Context, kind string, pid int32, maxConn int) ([]ConnectionStat, error) { + return connectionsPidMaxWithoutUidsWithContext(ctx, kind, pid, maxConn) +} + +func connectionsPidMaxWithoutUidsWithContext(_ context.Context, _ string, _ int32, _ int) ([]ConnectionStat, error) { + return nil, common.ErrNotImplementedError +} diff --git a/vendor/github.com/shirou/gopsutil/v4/process/process_darwin.go b/vendor/github.com/shirou/gopsutil/v4/process/process_darwin.go index 35c34adb..6e1d0ce3 100644 --- a/vendor/github.com/shirou/gopsutil/v4/process/process_darwin.go +++ b/vendor/github.com/shirou/gopsutil/v4/process/process_darwin.go @@ -375,29 +375,49 @@ func (p *Process) cmdlineSlice() ([]string, error) { if err != nil { return nil, err } - // The first bytes hold the nargs int, skip it. - args := bytes.Split((pargs)[unsafe.Sizeof(int(0)):], []byte{0}) - var argStr string - // The first element is the actual binary/command path. - // command := args[0] - var argSlice []string - // var envSlice []string - // All other, non-zero elements are arguments. The first "nargs" elements - // are the arguments. Everything else in the slice is then the environment - // of the process. - for _, arg := range args[1:] { - argStr = string(arg) - if argStr != "" { - if nargs > 0 { - argSlice = append(argSlice, argStr) - nargs-- - continue - } - break - // envSlice = append(envSlice, argStr) - } - } - return argSlice, err + // procArgs reads nargs as a 4-byte uint32; skip exactly those 4 bytes + // (matches Apple's ps using sizeof(nargs)). The previous code used + // unsafe.Sizeof(int(0)) which is 8 on 64-bit and would discard the + // first 4 bytes of exec_path — harmless only because chunks[0] is + // dropped, but logically wrong. + return parseCmdline(pargs[4:], nargs), nil +} + +// parseCmdline extracts argv from the kern.procargs2 buffer with the leading +// nargs int already stripped. Layout: +// +// exec_path \0 [padding \0...] argv[0] \0 ... argv[nargs-1] \0 envp[0] \0 ... +// +// Empty argv elements within the nargs count are preserved — skipping them +// would advance past argv[nargs-1] into envp, leaking environment values +// (potentially secrets) into Cmdline output. +// +// Known limitation: a process whose argv[0] is itself an empty string is +// indistinguishable from padding by this parser, since XNU does not expose +// the exec/argv alignment boundary. Such a process will still see one envp +// entry leak. Fixing it requires libgetargv-style alignment math against +// the XNU exec layout, which is out of scope for this change. +func parseCmdline(args []byte, nargs int) []string { + chunks := bytes.Split(args, []byte{0}) + if len(chunks) <= 1 { + return nil + } + // Skip exec_path (chunks[0]) and any padding NULs before argv[0]. + i := 1 + for ; i < len(chunks) && len(chunks[i]) == 0; i++ { + } + if nargs > len(chunks)-i { + nargs = len(chunks) - i + } + if nargs < 0 { + nargs = 0 + } + argSlice := make([]string, 0, nargs) + for ; nargs > 0; nargs-- { + argSlice = append(argSlice, string(chunks[i])) + i++ + } + return argSlice } // cmdNameWithContext returns the command name (including spaces) without any arguments diff --git a/vendor/github.com/shirou/gopsutil/v4/process/process_linux.go b/vendor/github.com/shirou/gopsutil/v4/process/process_linux.go index 764523dc..dc2f03bb 100644 --- a/vendor/github.com/shirou/gopsutil/v4/process/process_linux.go +++ b/vendor/github.com/shirou/gopsutil/v4/process/process_linux.go @@ -417,8 +417,7 @@ func (p *Process) MemoryMapsWithContext(ctx context.Context, grouped bool) (*[]M if len(field) < 2 { continue } - v := strings.Trim(field[1], "kB") // remove last "kB" - v = strings.TrimSpace(v) + v := strings.TrimSpace(strings.TrimSuffix(field[1], " kB")) t, err := strconv.ParseUint(v, 10, 64) if err != nil { return m, err @@ -924,49 +923,49 @@ func (p *Process) fillFromStatusWithContext(ctx context.Context) error { } p.numCtxSwitches.Involuntary = v case "VmRSS": - value = strings.Trim(value, " kB") // remove last "kB" + value = strings.TrimSpace(strings.TrimSuffix(value, " kB")) v, err := strconv.ParseUint(value, 10, 64) if err != nil { return err } p.memInfo.RSS = v * 1024 case "VmSize": - value = strings.Trim(value, " kB") // remove last "kB" + value = strings.TrimSpace(strings.TrimSuffix(value, " kB")) v, err := strconv.ParseUint(value, 10, 64) if err != nil { return err } p.memInfo.VMS = v * 1024 case "VmSwap": - value = strings.Trim(value, " kB") // remove last "kB" + value = strings.TrimSpace(strings.TrimSuffix(value, " kB")) v, err := strconv.ParseUint(value, 10, 64) if err != nil { return err } p.memInfo.Swap = v * 1024 case "VmHWM": - value = strings.Trim(value, " kB") // remove last "kB" + value = strings.TrimSpace(strings.TrimSuffix(value, " kB")) v, err := strconv.ParseUint(value, 10, 64) if err != nil { return err } p.memInfo.HWM = v * 1024 case "VmData": - value = strings.Trim(value, " kB") // remove last "kB" + value = strings.TrimSpace(strings.TrimSuffix(value, " kB")) v, err := strconv.ParseUint(value, 10, 64) if err != nil { return err } p.memInfo.Data = v * 1024 case "VmStk": - value = strings.Trim(value, " kB") // remove last "kB" + value = strings.TrimSpace(strings.TrimSuffix(value, " kB")) v, err := strconv.ParseUint(value, 10, 64) if err != nil { return err } p.memInfo.Stack = v * 1024 case "VmLck": - value = strings.Trim(value, " kB") // remove last "kB" + value = strings.TrimSpace(strings.TrimSuffix(value, " kB")) v, err := strconv.ParseUint(value, 10, 64) if err != nil { return err @@ -1046,6 +1045,9 @@ func (p *Process) fillFromTIDStatWithContext(ctx context.Context, tid int32) (ui } // Indexing from one, as described in `man proc` about the file /proc/[pid]/stat fields := splitProcStat(contents) + if len(fields) < 23 { + return 0, 0, nil, 0, 0, 0, nil, fmt.Errorf("malformed stat file: expected at least 23 fields, got %d", len(fields)) + } terminal, err := strconv.ParseUint(fields[7], 10, 64) if err != nil { diff --git a/vendor/github.com/shirou/gopsutil/v4/process/process_windows.go b/vendor/github.com/shirou/gopsutil/v4/process/process_windows.go index a1e28be6..19020e1f 100644 --- a/vendor/github.com/shirou/gopsutil/v4/process/process_windows.go +++ b/vendor/github.com/shirou/gopsutil/v4/process/process_windows.go @@ -324,6 +324,9 @@ func (p *Process) NameWithContext(ctx context.Context) (string, error) { if p.Pid == 0 { return "System Idle Process", nil } + if p.name != "" { + return p.name, nil + } if p.Pid == 4 { return "System", nil } @@ -333,7 +336,9 @@ func (p *Process) NameWithContext(ctx context.Context) (string, error) { return "", fmt.Errorf("could not get Name: %w", err) } - return filepath.Base(exe), nil + name := filepath.Base(exe) + p.name = name + return name, nil } func (*Process) TgidWithContext(_ context.Context) (int32, error) { @@ -598,6 +603,8 @@ func (p *Process) NumThreadsWithContext(_ context.Context) (int32, error) { p.setPpid(ppid) } + p.numThreads = ret + return ret, nil } @@ -900,6 +907,32 @@ func getFromSnapProcess(pid int32) (int32, int32, string, error) { //nolint:unpa return 0, 0, "", fmt.Errorf("couldn't find pid: %d", pid) } +func buildSnapProcessMap() (map[uint32]windows.ProcessEntry32, error) { + snap, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0) + if err != nil { + return nil, err + } + defer windows.CloseHandle(snap) + + var pe32 windows.ProcessEntry32 + pe32.Size = uint32(unsafe.Sizeof(pe32)) + if err := windows.Process32First(snap, &pe32); err != nil { + return nil, err + } + + snapMap := make(map[uint32]windows.ProcessEntry32) + for { + snapMap[pe32.ProcessID] = pe32 + if err := windows.Process32Next(snap, &pe32); err != nil { + if errors.Is(err, windows.ERROR_NO_MORE_FILES) { + break + } + return nil, err + } + } + return snapMap, nil +} + func ProcessesWithContext(ctx context.Context) ([]*Process, error) { out := []*Process{} @@ -908,11 +941,25 @@ func ProcessesWithContext(ctx context.Context) ([]*Process, error) { return out, fmt.Errorf("could not get Processes %w", err) } + // Note: The PID enumeration and snapshot creation are separate calls + // and may not be perfectly consistent due to timing. + snapMap, err := buildSnapProcessMap() + if err != nil { + return out, fmt.Errorf("could not build process snapshot: %w", err) + } + for _, pid := range pids { p, err := NewProcessWithContext(ctx, pid) if err != nil { continue } + + if entry, ok := snapMap[uint32(pid)]; ok { + p.name = windows.UTF16ToString(entry.ExeFile[:]) + p.setPpid(int32(entry.ParentProcessID)) + p.numThreads = int32(entry.Threads) + } + out = append(out, p) } diff --git a/vendor/github.com/testcontainers/testcontainers-go/Pipfile b/vendor/github.com/testcontainers/testcontainers-go/Pipfile index f7a1fb06..ca5c98c2 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/Pipfile +++ b/vendor/github.com/testcontainers/testcontainers-go/Pipfile @@ -8,7 +8,7 @@ verify_ssl = true [packages] mkdocs = "==1.5.3" mkdocs-codeinclude-plugin = "==0.3.1" -mkdocs-include-markdown-plugin = "==7.2.2" +mkdocs-include-markdown-plugin = "==7.3.0" mkdocs-material = "==9.5.18" mkdocs-markdownextradata-plugin = "==0.2.6" diff --git a/vendor/github.com/testcontainers/testcontainers-go/Pipfile.lock b/vendor/github.com/testcontainers/testcontainers-go/Pipfile.lock index 111787b0..2a6138cc 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/Pipfile.lock +++ b/vendor/github.com/testcontainers/testcontainers-go/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d2dc50d3b1c6818dd8a8fb4fa7a60013292b7f173db53d3e996bf43b4191ad70" + "sha256": "9836de74487210f57ac1040d06b403490e5171be35a8b5816a5361b4a2d02e01" }, "pipfile-spec": 6, "requires": { @@ -177,11 +177,11 @@ }, "click": { "hashes": [ - "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", - "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6" + "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", + "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96" ], "markers": "python_version >= '3.10'", - "version": "==8.3.1" + "version": "==8.4.1" }, "colorama": { "hashes": [ @@ -200,11 +200,12 @@ }, "idna": { "hashes": [ - "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", - "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" + "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", + "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc" ], + "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==3.11" + "version": "==3.15" }, "importlib-metadata": { "hashes": [ @@ -353,12 +354,12 @@ }, "mkdocs-include-markdown-plugin": { "hashes": [ - "sha256:f052ccb741eccf498116b826c1d78a2d761c56747372594709441cee0963fbc9", - "sha256:f2ec4487cf32d3e33ca528f9366f20fb9280ded9c8d1630eb2bbda244962dcd1" + "sha256:2800126746452e31c2e321bbd43c8190b356e0de353e20cbc16a34a3c3d6796c", + "sha256:5b5c99b5d3c9b9ce0114a9e60353bbafb6be53a26c2d3b74ec6b767a7a8e55ca" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==7.2.2" + "version": "==7.3.0" }, "mkdocs-markdownextradata-plugin": { "hashes": [ @@ -388,11 +389,11 @@ }, "packaging": { "hashes": [ - "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", - "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529" + "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", + "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661" ], "markers": "python_version >= '3.8'", - "version": "==26.0" + "version": "==26.2" }, "paginate": { "hashes": [ @@ -402,19 +403,19 @@ }, "pathspec": { "hashes": [ - "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", - "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723" + "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", + "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189" ], "markers": "python_version >= '3.9'", - "version": "==1.0.4" + "version": "==1.1.1" }, "platformdirs": { "hashes": [ - "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", - "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868" + "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", + "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a" ], "markers": "python_version >= '3.10'", - "version": "==4.9.4" + "version": "==4.10.0" }, "pygments": { "hashes": [ @@ -641,11 +642,12 @@ }, "urllib3": { "hashes": [ - "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", - "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4" + "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", + "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897" ], - "markers": "python_version >= '3.9'", - "version": "==2.6.3" + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==2.7.0" }, "watchdog": { "hashes": [ diff --git a/vendor/github.com/testcontainers/testcontainers-go/commons-test.mk b/vendor/github.com/testcontainers/testcontainers-go/commons-test.mk index 50f8a2e9..a3381c5d 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/commons-test.mk +++ b/vendor/github.com/testcontainers/testcontainers-go/commons-test.mk @@ -15,7 +15,7 @@ $(GOBIN)/mockery: $(call go_install,github.com/vektra/mockery/v2@v2.53.4) $(GOBIN)/gci: - $(call go_install,github.com/daixiang0/gci@v0.13.5) + $(call go_install,github.com/daixiang0/gci@v0.14.0) .PHONY: install install: $(GOBIN)/golangci-lint $(GOBIN)/gotestsum $(GOBIN)/mockery diff --git a/vendor/github.com/testcontainers/testcontainers-go/docker.go b/vendor/github.com/testcontainers/testcontainers-go/docker.go index 03633f49..4346531b 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/docker.go +++ b/vendor/github.com/testcontainers/testcontainers-go/docker.go @@ -87,6 +87,8 @@ type DockerContainer struct { // logProductionCancel is used to signal the log production to stop. logProductionCancel context.CancelCauseFunc logProductionCtx context.Context + // logProductionDone is closed when the log production goroutine exits. + logProductionDone chan struct{} logProductionTimeout *time.Duration logger log.Logger @@ -813,16 +815,19 @@ func (c *DockerContainer) startLogProduction(ctx context.Context, opts ...LogPro // Setup the log production context which will be used to stop the log production. c.logProductionCtx, c.logProductionCancel = context.WithCancelCause(ctx) + c.logProductionDone = make(chan struct{}) // We capture context cancel function to avoid data race with multiple // calls to startLogProduction. - go func(cancel context.CancelCauseFunc) { + go func(cancel context.CancelCauseFunc, done chan struct{}) { // Ensure the context is cancelled when log productions completes // so that GetLogProductionErrorChannel functions correctly. defer cancel(nil) + // Signal that the goroutine has exited so stopLogProduction can drain. + defer close(done) c.logProducer(stdout, stderr) - }(c.logProductionCancel) + }(c.logProductionCancel, c.logProductionDone) return nil } @@ -903,9 +908,38 @@ func (c *DockerContainer) stopLogProduction() error { return nil } - // Signal the log production to stop. + // Wait for the log production goroutine to finish draining any buffered + // logs before cancelling. When the container has already exited, the + // goroutine will reach EOF naturally and close logProductionDone on its + // own. The bounded timeout prevents blocking indefinitely when the + // container is still actively streaming (e.g. Stop() called on a running + // container). + if c.logProductionDone != nil { + select { + case <-c.logProductionDone: + // Goroutine already finished naturally; nothing more to do. + return nil + case <-time.After(minLogProductionTimeout): + // Timed out waiting for natural exit; force-cancel now. + } + } + + // Signal the log production to stop (for still-running containers). c.logProductionCancel(errLogProductionStop) + // Wait for the goroutine to acknowledge the cancellation. Context + // cancellation propagates into the Docker transport and should unblock + // stdcopy.StdCopy promptly, but we bound the wait to match + // minLogProductionTimeout to guard against stuck kernel socket reads or + // daemon transport failures that might not honour context cancellation. + if c.logProductionDone != nil { + select { + case <-c.logProductionDone: + case <-time.After(minLogProductionTimeout): + c.logger.Printf("timeout waiting for log production goroutine to exit; a goroutine may have leaked") + } + } + if err := context.Cause(c.logProductionCtx); err != nil { switch { case errors.Is(err, errLogProductionStop): @@ -1861,6 +1895,7 @@ func (p *DockerProvider) SaveImagesWithOpts(ctx context.Context, output string, func SaveDockerImageWithPlatforms(platforms ...specs.Platform) SaveImageOption { return func(opts *saveImageOptions) error { opts.dockerSaveOpts = append(opts.dockerSaveOpts, client.ImageSaveWithPlatforms(platforms...)) + opts.platforms = append(opts.platforms, platforms...) return nil } @@ -1871,6 +1906,27 @@ func (p *DockerProvider) PullImage(ctx context.Context, img string) error { return p.attemptToPullImage(ctx, img, client.ImagePullOptions{}) } +// PullImageWithOpts pulls image from registry, passing options to the provider. +func (p *DockerProvider) PullImageWithOpts(ctx context.Context, img string, opts ...PullImageOption) error { + pullOpts := pullImageOptions{} + + for _, opt := range opts { + if err := opt(&pullOpts); err != nil { + return fmt.Errorf("applying pull image option: %w", err) + } + } + + return p.attemptToPullImage(ctx, img, pullOpts.dockerPullOpts) +} + +func PullDockerImageWithPlatform(platform specs.Platform) PullImageOption { + return func(opts *pullImageOptions) error { + opts.dockerPullOpts.Platforms = append(opts.dockerPullOpts.Platforms, platform) + + return nil + } +} + var permanentClientErrors = []func(error) bool{ errdefs.IsNotFound, errdefs.IsInvalidArgument, diff --git a/vendor/github.com/testcontainers/testcontainers-go/generic.go b/vendor/github.com/testcontainers/testcontainers-go/generic.go index dc5ee1cc..57dbe760 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/generic.go +++ b/vendor/github.com/testcontainers/testcontainers-go/generic.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "maps" - "strings" "sync" "github.com/testcontainers/testcontainers-go/internal/core" @@ -78,14 +77,6 @@ func GenericContainer(ctx context.Context, req GenericContainerRequest) (Contain } if err != nil { // At this point `c` might not be nil. Give the caller an opportunity to call Destroy on the container. - // TODO: Remove this debugging. - if strings.Contains(err.Error(), "toomanyrequests") { - // Debugging information for rate limiting. - cfg, err := getDockerConfig() - if err == nil { - fmt.Printf("XXX: too many requests: %+v", cfg) - } - } return c, fmt.Errorf("create container: %w", err) } diff --git a/vendor/github.com/testcontainers/testcontainers-go/image.go b/vendor/github.com/testcontainers/testcontainers-go/image.go index 38557e77..0314f594 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/image.go +++ b/vendor/github.com/testcontainers/testcontainers-go/image.go @@ -2,8 +2,10 @@ package testcontainers import ( "context" + "fmt" "github.com/moby/moby/client" + specs "github.com/opencontainers/image-spec/specs-go/v1" ) // ImageInfo represents summary information of an image @@ -14,14 +16,43 @@ type ImageInfo struct { type saveImageOptions struct { dockerSaveOpts []client.ImageSaveOption + platforms []specs.Platform } type SaveImageOption func(*saveImageOptions) error +type pullImageOptions struct { + dockerPullOpts client.ImagePullOptions +} + +type PullImageOption func(*pullImageOptions) error + +// ResolveSaveImageOptions applies save image options and returns the platform to +// use for a coordinated containerd import, if exactly one platform was requested. +func ResolveSaveImageOptions(opts ...SaveImageOption) (*specs.Platform, error) { + saveOpts := saveImageOptions{} + + for _, opt := range opts { + if err := opt(&saveOpts); err != nil { + return nil, fmt.Errorf("applying save image option: %w", err) + } + } + + switch len(saveOpts.platforms) { + case 0: + return nil, nil + case 1: + return &saveOpts.platforms[0], nil + default: + return nil, fmt.Errorf("at most one platform is supported, got %d", len(saveOpts.platforms)) + } +} + // ImageProvider allows manipulating images type ImageProvider interface { ListImages(context.Context) ([]ImageInfo, error) SaveImages(context.Context, string, ...string) error SaveImagesWithOpts(context.Context, string, []string, ...SaveImageOption) error PullImage(context.Context, string) error + PullImageWithOpts(context.Context, string, ...PullImageOption) error } diff --git a/vendor/github.com/testcontainers/testcontainers-go/internal/config/config.go b/vendor/github.com/testcontainers/testcontainers-go/internal/config/config.go index deb8f0a9..262f40d6 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/internal/config/config.go +++ b/vendor/github.com/testcontainers/testcontainers-go/internal/config/config.go @@ -11,7 +11,7 @@ import ( "github.com/magiconair/properties" ) -const ReaperDefaultImage = "testcontainers/ryuk:0.13.0" +const ReaperDefaultImage = "testcontainers/ryuk:0.14.0" var ( tcConfig Config diff --git a/vendor/github.com/testcontainers/testcontainers-go/internal/version.go b/vendor/github.com/testcontainers/testcontainers-go/internal/version.go index ebe1f043..d18d432b 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/internal/version.go +++ b/vendor/github.com/testcontainers/testcontainers-go/internal/version.go @@ -1,4 +1,4 @@ package internal // Version is the next development version of the application -const Version = "0.42.0" +const Version = "0.43.0" diff --git a/vendor/github.com/testcontainers/testcontainers-go/mkdocs.yml b/vendor/github.com/testcontainers/testcontainers-go/mkdocs.yml index b77c004f..123271cf 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/mkdocs.yml +++ b/vendor/github.com/testcontainers/testcontainers-go/mkdocs.yml @@ -55,10 +55,11 @@ nav: - HostPort: features/wait/host_port.md - HTTP: features/wait/http.md - Log: features/wait/log.md - - Multi: features/wait/multi.md - SQL: features/wait/sql.md - TLS: features/wait/tls.md - Walk: features/wait/walk.md + - All: features/wait/all.md + - Any: features/wait/any.md - features/files_and_mounts.md - features/follow_logs.md - features/garbage_collector.md @@ -85,6 +86,7 @@ nav: - modules/consul.md - modules/couchbase.md - modules/databend.md + - modules/dex.md - modules/dind.md - modules/dockermcpgateway.md - modules/dockermodelrunner.md @@ -162,4 +164,4 @@ nav: - Getting help: getting_help.md edit_uri: edit/main/docs/ extra: - latest_version: v0.42.0 + latest_version: v0.43.0 diff --git a/vendor/github.com/testcontainers/testcontainers-go/port_forwarding.go b/vendor/github.com/testcontainers/testcontainers-go/port_forwarding.go index 493a4071..e4639e3a 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/port_forwarding.go +++ b/vendor/github.com/testcontainers/testcontainers-go/port_forwarding.go @@ -20,7 +20,7 @@ import ( const ( // hubSshdImage { - sshdImage string = "testcontainers/sshd:1.3.0" + sshdImage string = "testcontainers/sshd:1.4.0" // } // HostInternal is the internal hostname used to reach the host from the container, diff --git a/vendor/github.com/testcontainers/testcontainers-go/reaper.go b/vendor/github.com/testcontainers/testcontainers-go/reaper.go index f42c5bf4..776529d0 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/reaper.go +++ b/vendor/github.com/testcontainers/testcontainers-go/reaper.go @@ -367,6 +367,20 @@ func (r *reaperSpawner) fromContainer(ctx context.Context, sessionID string, pro }, nil } +// defaultRyukWaitStrategy returns the wait strategy used when creating a new +// reaper container. It combines a log match for "Started" (the moment ryuk's +// TCP listener is bound inside the container, which is the readiness signal +// also used by other Testcontainers libraries) with a host port-mapping check, +// to guard against the docker-proxy accepting TCP connections before the ryuk +// process is actually listening — a race that surfaced once the ryuk binary +// was UPX-compressed. +func defaultRyukWaitStrategy(port string) wait.Strategy { + return wait.ForAll( + wait.ForLog("Started"), + wait.ForListeningPort(port), + ) +} + // newReaper creates a connected Reaper with a sessionID to identify containers // and a provider to use. func (r *reaperSpawner) newReaper(ctx context.Context, sessionID string, provider ReaperProvider) (reaper *Reaper, err error) { @@ -378,7 +392,7 @@ func (r *reaperSpawner) newReaper(ctx context.Context, sessionID string, provide Image: config.ReaperDefaultImage, ExposedPorts: []string{port.String()}, Labels: core.DefaultLabels(sessionID), - WaitingFor: wait.ForListeningPort(port.String()), + WaitingFor: defaultRyukWaitStrategy(port.String()), Name: reaperContainerNameFromSessionID(sessionID), HostConfigModifier: func(hc *container.HostConfig) { hc.AutoRemove = true diff --git a/vendor/github.com/testcontainers/testcontainers-go/requirements.txt b/vendor/github.com/testcontainers/testcontainers-go/requirements.txt index eb23d2ba..a2970c67 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/requirements.txt +++ b/vendor/github.com/testcontainers/testcontainers-go/requirements.txt @@ -1,5 +1,5 @@ mkdocs==1.5.3 mkdocs-codeinclude-plugin==0.3.1 -mkdocs-include-markdown-plugin==7.2.1 +mkdocs-include-markdown-plugin==7.2.2 mkdocs-material==9.5.18 mkdocs-markdownextradata-plugin==0.2.6 diff --git a/vendor/github.com/testcontainers/testcontainers-go/wait/any.go b/vendor/github.com/testcontainers/testcontainers-go/wait/any.go new file mode 100644 index 00000000..15d9c150 --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/wait/any.go @@ -0,0 +1,128 @@ +package wait + +import ( + "context" + "errors" + "fmt" + "reflect" + "strings" + "time" +) + +// Implement interface +var ( + _ Strategy = (*AnyMultiStrategy)(nil) + _ StrategyTimeout = (*AnyMultiStrategy)(nil) +) + +type AnyMultiStrategy struct { + // all Strategies should have a startupTimeout to avoid waiting infinitely + timeout *time.Duration + deadline *time.Duration + + // additional properties + Strategies []Strategy +} + +// WithStartupTimeoutDefault sets the default timeout for all inner wait strategies. +func (ms *AnyMultiStrategy) WithStartupTimeoutDefault(timeout time.Duration) *AnyMultiStrategy { + ms.timeout = &timeout + return ms +} + +// WithDeadline sets a time.Duration which limits all wait strategies. +func (ms *AnyMultiStrategy) WithDeadline(deadline time.Duration) *AnyMultiStrategy { + ms.deadline = &deadline + return ms +} + +// ForAny returns a WaitStrategy that waits for any of the supplied conditions +// to become true (after which it cancels the remaining ones). +// +// Failures are not permitted: any strategy which fails will have its error +// immediately returned. +func ForAny(strategies ...Strategy) *AnyMultiStrategy { + return &AnyMultiStrategy{ + Strategies: strategies, + } +} + +func (ms *AnyMultiStrategy) Timeout() *time.Duration { + return ms.timeout +} + +// String returns a human-readable description of the wait strategy. +func (ms *AnyMultiStrategy) String() string { + if len(ms.Strategies) == 0 { + return "any of: (none)" + } + + var strategies []string + for _, strategy := range ms.Strategies { + if strategy == nil || reflect.ValueOf(strategy).IsNil() { + continue + } + if s, ok := strategy.(fmt.Stringer); ok { + strategies = append(strategies, s.String()) + } else { + strategies = append(strategies, fmt.Sprintf("%T", strategy)) + } + } + + // Always include "any of:" prefix to make it clear this is a AnyMultiStrategy + // even when there's only one strategy after filtering out nils. + return "any of: [" + strings.Join(strategies, ", ") + "]" +} + +func (ms *AnyMultiStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error { + if len(ms.Strategies) == 0 { + return errors.New("no wait strategy supplied") + } + + ctx, cancel := context.WithCancel(ctx) + defer cancel() // All remaining strategies will stop when this fires. + + if ms.deadline != nil { + ctx, cancel = context.WithTimeout(ctx, *ms.deadline) + defer cancel() + } + + resCh := make(chan error, len(ms.Strategies)) + var valid int + + for _, strategy := range ms.Strategies { + if strategy == nil || reflect.ValueOf(strategy).IsNil() { + // A module could be appending strategies after part of the container initialization, + // and use wait.ForAny on a not initialized strategy. + // In this case, we just skip the nil strategy. + continue + } + valid++ + + strategyCtx := ctx + // Set default Timeout when strategy implements StrategyTimeout + if st, ok := strategy.(StrategyTimeout); ok { + if ms.Timeout() != nil && st.Timeout() == nil { + strategyCtx, cancel = context.WithTimeout(ctx, *ms.Timeout()) + defer cancel() + } + } + go func() { resCh <- strategy.WaitUntilReady(strategyCtx, target) }() + } + + if valid == 0 { + return nil + } + + for { + select { + case err := <-resCh: + if err != nil { + return err + } + return nil + case <-ctx.Done(): + return fmt.Errorf("timed out waiting for strategies: %w", ctx.Err()) + } + } +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/wait/sql.go b/vendor/github.com/testcontainers/testcontainers-go/wait/sql.go index 5d0228a0..e328be28 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/wait/sql.go +++ b/vendor/github.com/testcontainers/testcontainers-go/wait/sql.go @@ -17,7 +17,7 @@ var ( const defaultForSQLQuery = "SELECT 1" // ForSQL constructs a new waitForSql strategy for the given driver -func ForSQL(port string, driver string, url func(host string, port string) string) *waitForSQL { +func ForSQL(port string, driver string, url func(host string, port network.Port) string) *waitForSQL { return &waitForSQL{ Port: port, URL: url, @@ -31,7 +31,7 @@ func ForSQL(port string, driver string, url func(host string, port string) strin type waitForSQL struct { timeout *time.Duration - URL func(host string, port string) string + URL func(host string, port network.Port) string Driver string Port string startupTimeout time.Duration @@ -114,7 +114,7 @@ func (w *waitForSQL) WaitUntilReady(ctx context.Context, target StrategyTarget) } } - db, err := sql.Open(w.Driver, w.URL(host, port.String())) + db, err := sql.Open(w.Driver, w.URL(host, port)) if err != nil { return fmt.Errorf("sql.Open: %w", err) } diff --git a/vendor/github.com/testcontainers/testcontainers-go/wait/walk.go b/vendor/github.com/testcontainers/testcontainers-go/wait/walk.go index 98f5755e..04cb1bad 100644 --- a/vendor/github.com/testcontainers/testcontainers-go/wait/walk.go +++ b/vendor/github.com/testcontainers/testcontainers-go/wait/walk.go @@ -59,23 +59,33 @@ func walk(root *Strategy, visit VisitFunc) error { return err } - if s, ok := (*root).(*MultiStrategy); ok { - var i int - for range s.Strategies { - if err := walk(&s.Strategies[i], visit); err != nil { - if errors.Is(err, ErrVisitRemove) { - s.Strategies = slices.Delete(s.Strategies, i, i+1) - if errors.Is(err, VisitStop) { - return VisitStop - } - continue - } + switch s := (*root).(type) { + case *MultiStrategy: + if err := walkAndMutate(&s.Strategies, visit); err != nil { + return err + } + case *AnyMultiStrategy: + if err := walkAndMutate(&s.Strategies, visit); err != nil { + return err + } + } - return err + return nil +} + +func walkAndMutate(strategies *[]Strategy, visit VisitFunc) error { + for i := 0; i < len(*strategies); { + if err := walk(&(*strategies)[i], visit); err != nil { + if errors.Is(err, ErrVisitRemove) { + *strategies = slices.Delete(*strategies, i, i+1) + if errors.Is(err, VisitStop) { + return VisitStop + } + continue } - i++ + return err } + i++ } - return nil } diff --git a/vendor/modules.txt b/vendor/modules.txt index 98ce2a99..c89160b5 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -828,7 +828,7 @@ github.com/safchain/ethtool ## explicit; go 1.23.0 github.com/sagikazarmark/locafero github.com/sagikazarmark/locafero/internal/queue -# github.com/shirou/gopsutil/v4 v4.26.3 +# github.com/shirou/gopsutil/v4 v4.26.5 ## explicit; go 1.24.0 github.com/shirou/gopsutil/v4/common github.com/shirou/gopsutil/v4/cpu @@ -878,7 +878,7 @@ github.com/stretchr/testify/require # github.com/subosito/gotenv v1.6.0 ## explicit; go 1.18 github.com/subosito/gotenv -# github.com/testcontainers/testcontainers-go v0.42.0 +# github.com/testcontainers/testcontainers-go v0.43.0 ## explicit; go 1.25.0 github.com/testcontainers/testcontainers-go github.com/testcontainers/testcontainers-go/exec