Merge pull request #3023 from opencloud-eu/dependabot/go_modules/github.com/testcontainers/testcontainers-go/modules/opensearch-0.43.0

build(deps): bump github.com/testcontainers/testcontainers-go/modules/opensearch from 0.42.0 to 0.43.0
This commit is contained in:
Ralf Haferkamp
2026-06-26 12:53:53 +02:00
committed by GitHub
30 changed files with 1133 additions and 119 deletions

8
go.mod
View File

@@ -82,8 +82,8 @@ require (
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/test-go/testify v1.1.4
github.com/testcontainers/testcontainers-go v0.42.0
github.com/testcontainers/testcontainers-go/modules/opensearch v0.42.0
github.com/testcontainers/testcontainers-go v0.43.0
github.com/testcontainers/testcontainers-go/modules/opensearch v0.43.0
github.com/theckman/yacspin v0.13.12
github.com/thejerf/suture/v4 v4.0.6
github.com/tidwall/gjson v1.19.0
@@ -292,7 +292,7 @@ require (
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.2.0 // indirect
github.com/moby/moby/api v1.54.1 // indirect
github.com/moby/moby/api v1.54.2 // indirect
github.com/moby/moby/client v0.4.0 // indirect
github.com/moby/patternmatcher v0.6.1 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
@@ -347,7 +347,7 @@ require (
github.com/sethvargo/go-diceware v0.5.0 // indirect
github.com/sethvargo/go-password v0.3.1 // indirect
github.com/shamaton/msgpack/v2 v2.4.1 // indirect
github.com/shirou/gopsutil/v4 v4.26.3 // indirect
github.com/shirou/gopsutil/v4 v4.26.5 // indirect
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect

16
go.sum
View File

@@ -866,8 +866,8 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4=
github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw=
github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g=
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
@@ -1115,8 +1115,8 @@ github.com/shamaton/msgpack/v2 v2.4.1 h1:JtJ141QoQ3NqgPDsjq2v9VXlaON8SiQOwEaoNLE
github.com/shamaton/msgpack/v2 v2.4.1/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
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/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c h1:aqg5Vm5dwtvL+YgDpBcK1ITf3o96N/K7/wsRXQnUTEs=
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c/go.mod h1:owqhoLW1qZoYLZzLnBw+QkPP9WZnjlSWihhxAJC1+/M=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
@@ -1192,10 +1192,10 @@ github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhg
github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE=
github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU=
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/modules/opensearch v0.42.0 h1:lXPr5NypYR0O8qcej+JQfsQTOTc3CRv+c4wz3VbBFLI=
github.com/testcontainers/testcontainers-go/modules/opensearch v0.42.0/go.mod h1:KJ+m9onB785g9VDRDuhvUvsbkRjnFp3Uz+CHoHpPieA=
github.com/testcontainers/testcontainers-go v0.43.0 h1:oEQx5MW2DGd9z3AeEQfB2lPM0eLs7ztyaGRu75bFo5A=
github.com/testcontainers/testcontainers-go v0.43.0/go.mod h1:+VxkT2NQnKOZPKi6praMuMKYHYyOGXr0XSBSlSMCzFo=
github.com/testcontainers/testcontainers-go/modules/opensearch v0.43.0 h1:a1ipjF7d/VxPX1dgVPIk4F+t6YkgMbE2OtBuRQCHJt8=
github.com/testcontainers/testcontainers-go/modules/opensearch v0.43.0/go.mod h1:OWSeUDiGMUy30iMsAltIJIo9uh/CleLv6KyxjYOsgR8=
github.com/thanhpk/randstr v1.0.6 h1:psAOktJFD4vV9NEVb3qkhRSMvYh4ORRaj1+w/hn4B+o=
github.com/thanhpk/randstr v1.0.6/go.mod h1:M/H2P1eNLZzlDwAzpkkkUvoyNNMbzRGhESZuEQk3r0U=
github.com/theckman/yacspin v0.13.12 h1:CdZ57+n0U6JMuh2xqjnjRq5Haj6v1ner2djtLQRzJr4=

View File

@@ -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.

View File

@@ -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")

View File

@@ -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")

View File

@@ -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)
}

View File

@@ -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:
//
// <proto>:
// \t<count> <description>
// \t\t<count> <sub-description>
//
// 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 "<number> <description>".
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) {

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

353
vendor/github.com/shirou/gopsutil/v4/net/net_netbsd.go generated vendored Normal file
View File

@@ -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
}

View File

@@ -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)
}
// 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
}
return argSlice, err
// 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

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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"

View File

@@ -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": [

View File

@@ -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

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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())
}
}
}

View File

@@ -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)
}

View File

@@ -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
}
return err
}
i++
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 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
}
return err
}
i++
}
return nil
}

8
vendor/modules.txt vendored
View File

@@ -1086,7 +1086,7 @@ github.com/moby/docker-image-spec/specs-go/v1
github.com/moby/go-archive
github.com/moby/go-archive/compression
github.com/moby/go-archive/tarheader
# github.com/moby/moby/api v1.54.1
# github.com/moby/moby/api v1.54.2
## explicit; go 1.24
github.com/moby/moby/api/pkg/authconfig
github.com/moby/moby/api/pkg/stdcopy
@@ -1995,7 +1995,7 @@ github.com/shamaton/msgpack/v2/internal/encoding
github.com/shamaton/msgpack/v2/internal/stream/decoding
github.com/shamaton/msgpack/v2/internal/stream/encoding
github.com/shamaton/msgpack/v2/time
# 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
@@ -2070,7 +2070,7 @@ github.com/tchap/go-patricia/v2/patricia
## explicit
github.com/test-go/testify/mock
github.com/test-go/testify/require
# 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
@@ -2080,7 +2080,7 @@ github.com/testcontainers/testcontainers-go/internal/core
github.com/testcontainers/testcontainers-go/internal/core/network
github.com/testcontainers/testcontainers-go/log
github.com/testcontainers/testcontainers-go/wait
# github.com/testcontainers/testcontainers-go/modules/opensearch v0.42.0
# github.com/testcontainers/testcontainers-go/modules/opensearch v0.43.0
## explicit; go 1.25.0
github.com/testcontainers/testcontainers-go/modules/opensearch
# github.com/theckman/yacspin v0.13.12