Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion pkg/baseboard/baseboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,14 @@ func (i *Info) String() string {
// New returns a pointer to an Info struct containing information about the
// host's baseboard
func New(opts ...*option.Option) (*Info, error) {
ctx := context.New(opts...)
return NewWithContext(context.New(opts...))
}

// New returns a pointer to an Info struct containing information about the
// host's baseboard, reusing a given context.
// Use this function when you want to consume this package from another,
// ensuring the two see a coherent set of resources.
func NewWithContext(ctx *context.Context) (*Info, error) {
info := &Info{ctx: ctx}
if err := ctx.Do(info.load); err != nil {
return nil, err
Expand Down
9 changes: 8 additions & 1 deletion pkg/bios/bios.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,14 @@ func (i *Info) String() string {
// New returns a pointer to a Info struct containing information
// about the host's BIOS
func New(opts ...*option.Option) (*Info, error) {
ctx := context.New(opts...)
return NewWithContext(context.New(opts...))
}

// New returns a pointer to a Info struct containing information
// about the host's BIOS, reusing a given context.
// Use this function when you want to consume this package from another,
// ensuring the two see a coherent set of resources.
func NewWithContext(ctx *context.Context) (*Info, error) {
info := &Info{ctx: ctx}
if err := ctx.Do(info.load); err != nil {
return nil, err
Expand Down
9 changes: 8 additions & 1 deletion pkg/block/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,14 @@ type Info struct {
// New returns a pointer to an Info struct that describes the block storage
// resources of the host system.
func New(opts ...*option.Option) (*Info, error) {
ctx := context.New(opts...)
return NewWithContext(context.New(opts...))
}

// New returns a pointer to an Info struct that describes the block storage
// resources of the host system, reusing a given context.
// Use this function when you want to consume this package from another,
// ensuring the two see a coherent set of resources.
func NewWithContext(ctx *context.Context) (*Info, error) {
info := &Info{ctx: ctx}
if err := ctx.Do(info.load); err != nil {
return nil, err
Expand Down
9 changes: 8 additions & 1 deletion pkg/chassis/chassis.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,14 @@ func (i *Info) String() string {
// New returns a pointer to a Info struct containing information
// about the host's chassis
func New(opts ...*option.Option) (*Info, error) {
ctx := context.New(opts...)
return NewWithContext(context.New(opts...))
}

// New returns a pointer to a Info struct containing information
// about the host's chassis, reusing a given context.
// Use this function when you want to consume this package from another,
// ensuring the two see a coherent set of resources.
func NewWithContext(ctx *context.Context) (*Info, error) {
info := &Info{ctx: ctx}
if err := ctx.Do(info.load); err != nil {
return nil, err
Expand Down
65 changes: 58 additions & 7 deletions pkg/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
package context

import (
"fmt"

"github.com/jaypipes/ghw/pkg/option"
"github.com/jaypipes/ghw/pkg/snapshot"
)
Expand All @@ -22,6 +24,8 @@ type Context struct {
PathOverrides option.PathOverrides
snapshotUnpackedPath string
alert option.Alerter
refCount int
err error
}

// New returns a Context struct pointer that has had various options set on it
Expand Down Expand Up @@ -53,10 +57,17 @@ func New(opts ...*option.Option) *Context {
ctx.PathOverrides = merged.PathOverrides
}

// New is not allowed to return error - it would break the established API.
// so the only way out is to actually do the checks here and record the error,
// and return it later, at the earliest possible occasion, in Setup()
if ctx.SnapshotPath != "" && ctx.Chroot != option.DefaultChroot {
// The env/client code supplied a value, but we are will overwrite it when unpacking shapshots!
ctx.err = fmt.Errorf("Conflicting options: chroot %q and snapshot path %q", ctx.Chroot, ctx.SnapshotPath)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fromanirh instead of the error tracking and refcounting, why don't we simply panic() here? I think panic() is entirely logical considering this situation would occur only when the user has explicitly made a mistake in their argument-passing.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a note: refcounting is meant to solve a different facet of the problem (handling nested context in such a way we perform the cleanup in the expected order)
In general I prefer to avoid to panic() inside packages, this should always be an application layer decision - not a library layer decision, so I'd really try hard to find another approach if you don't like the admittedly awkward way I'm using here.
Let me look for other options

}
return ctx
}

// FromEnv returns an Option that has been populated from the environs or
// FromEnv returns a Context that has been populated from the environs or
Comment thread
ffromani marked this conversation as resolved.
// default options values
func FromEnv() *Context {
chrootVal := option.EnvOrDefaultChroot()
Expand Down Expand Up @@ -89,8 +100,17 @@ func (ctx *Context) Do(fn func() error) error {
// You should call `Setup` just once. It is safe to call `Setup` if you don't make
// use of optional extra features - `Setup` will do nothing.
func (ctx *Context) Setup() error {
if ctx.err != nil {
return ctx.err
}
if ctx.RefCount() > 1 {
// someone else came before and fixed things already!
ctx.incRefCount()
return nil
}
if ctx.SnapshotPath == "" {
// nothing to do!
// nothing to do
ctx.incRefCount()
return nil
}

Expand All @@ -108,26 +128,57 @@ func (ctx *Context) Setup() error {
}
_, err = snapshot.UnpackInto(ctx.SnapshotPath, root, flags)
}
if err != nil {
return err
if err == nil {
ctx.Chroot = root
ctx.incRefCount()
return nil
}

ctx.Chroot = root
return nil
// not ready: must try again
return err
}

// Teardown releases any resource acquired by Setup.
// You should always call `Teardown` if you called `Setup` to free any resources
// acquired by `Setup`. Check `Do` for more automated management.
func (ctx *Context) Teardown() error {
if ctx.RefCount() > 1 {
// too early, the last one will do the cleaning
ctx.decRefCount()
return nil
}
if ctx.snapshotUnpackedPath == "" {
// if the client code provided the unpack directory,
// then it is also in charge of the cleanup.
ctx.decRefCount()
return nil
}
return snapshot.Cleanup(ctx.snapshotUnpackedPath)
err := snapshot.Cleanup(ctx.snapshotUnpackedPath)
if err == nil {
ctx.decRefCount()
return nil
}
// else need to try again later
return err
}

func (ctx *Context) Warn(msg string, args ...interface{}) {
ctx.alert.Printf("WARNING: "+msg, args...)
}

func (ctx *Context) IsReady() bool {
return ctx.refCount > 0
}

// RefCount() must be used only in testing code
func (ctx *Context) RefCount() int {
return ctx.refCount
}

func (ctx *Context) incRefCount() {
ctx.refCount++
}

func (ctx *Context) decRefCount() {
ctx.refCount--
}
91 changes: 91 additions & 0 deletions pkg/context/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,94 @@ func TestSnapshotContext(t *testing.T) {
t.Fatalf("Expected the uncompressed dir to be deleted: %s", uncompressedDir)
}
}

// nolint: gocyclo
func TestContextReadiness(t *testing.T) {
ctx := context.New()
if ctx.IsReady() {
t.Fatalf("context ready before Setup()")
}

ctx.Do(func() error {
if !ctx.IsReady() {
t.Fatalf("context NOT ready inside Do()")
}
return nil
})

if ctx.IsReady() {
t.Fatalf("context ready after Teardown()")
}
}

// nolint: gocyclo
func TestContextReadinessNested(t *testing.T) {
ctx := context.New()
if ctx.IsReady() {
t.Fatalf("context ready before Setup()")
}

ctx.Do(func() error {
if !ctx.IsReady() {
t.Fatalf("context NOT ready inside outer Do()")
}
ctx.Do(func() error {
if !ctx.IsReady() {
t.Fatalf("context NOT ready inside inner Do()")
}
return nil
})
if !ctx.IsReady() {
t.Fatalf("context NOT ready after inner Do()")
}
return nil
})

if ctx.IsReady() {
t.Fatalf("context ready after Teardown() - refcount = %d", ctx.RefCount())
}
}

// nolint: gocyclo
func TestContextReadinessDeeplyNested(t *testing.T) {
// we don't expect more nesting than this atm
ctx := context.New()
if ctx.IsReady() {
t.Fatalf("context ready before Setup()")
}
if ctx.RefCount() != 0 {
t.Fatalf("context refcount unexpected value %d", ctx.RefCount())
}

ctx.Do(func() error {
if !ctx.IsReady() {
t.Fatalf("context NOT ready inside outer Do()")
}
ctx.Do(func() error {
if !ctx.IsReady() {
t.Fatalf("context NOT ready inside middle Do()")
}
ctx.Do(func() error {
if !ctx.IsReady() {
t.Fatalf("context NOT ready inside inner Do()")
}
return nil
})
if !ctx.IsReady() {
t.Fatalf("context NOT ready after inner Do()")
}
return nil
})
if !ctx.IsReady() {
t.Fatalf("context NOT ready after middle Do()")
}
return nil
})

if ctx.IsReady() {
t.Fatalf("context ready after Teardown() - refcount = %d", ctx.RefCount())
}
if ctx.RefCount() != 0 {
t.Fatalf("context refcount unexpected value %d", ctx.RefCount())
}
}
9 changes: 8 additions & 1 deletion pkg/cpu/cpu.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,14 @@ type Info struct {
// New returns a pointer to an Info struct that contains information about the
// CPUs on the host system
func New(opts ...*option.Option) (*Info, error) {
ctx := context.New(opts...)
return NewWithContext(context.New(opts...))
}

// New returns a pointer to an Info struct that contains information about the
// CPUs on the host system, reusing a given context.
// Use this function when you want to consume this package from another,
// ensuring the two see a coherent set of resources.
func NewWithContext(ctx *context.Context) (*Info, error) {
info := &Info{ctx: ctx}
if err := ctx.Do(info.load); err != nil {
return nil, err
Expand Down
9 changes: 8 additions & 1 deletion pkg/gpu/gpu.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,14 @@ type Info struct {
// New returns a pointer to an Info struct that contains information about the
// graphics cards on the host system
func New(opts ...*option.Option) (*Info, error) {
ctx := context.New(opts...)
return NewWithContext(context.New(opts...))
}

// New returns a pointer to an Info struct that contains information about the
// graphics cards on the host system, reusing a given context.
// Use this function when you want to consume this package from another,
// ensuring the two see a coherent set of resources.
func NewWithContext(ctx *context.Context) (*Info, error) {
info := &Info{ctx: ctx}
if err := ctx.Do(info.load); err != nil {
return nil, err
Expand Down
11 changes: 10 additions & 1 deletion pkg/memory/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,17 @@ type Info struct {
Modules []*Module `json:"modules"`
}

// New returns a pointer to an Info struct containing information about the
// host's memory.
func New(opts ...*option.Option) (*Info, error) {
ctx := context.New(opts...)
return NewWithContext(context.New(opts...))
}

// New returns a pointer to an Info struct containing information about the
// host's memory, reusing a given context.
// Use this function when you want to consume this package from another,
// ensuring the two see a coherent set of resources.
func NewWithContext(ctx *context.Context) (*Info, error) {
info := &Info{ctx: ctx}
if err := ctx.Do(info.load); err != nil {
return nil, err
Expand Down
9 changes: 8 additions & 1 deletion pkg/net/net.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,14 @@ type Info struct {
// New returns a pointer to an Info struct that contains information about the
// network interface controllers (NICs) on the host system
func New(opts ...*option.Option) (*Info, error) {
ctx := context.New(opts...)
return NewWithContext(context.New(opts...))
}

// New returns a pointer to an Info struct that contains information about the
// network interface controllers (NICs) on the host system, reusing a given context.
// Use this function when you want to consume this package from another,
// ensuring the two see a coherent set of resources.
func NewWithContext(ctx *context.Context) (*Info, error) {
info := &Info{ctx: ctx}
if err := ctx.Do(info.load); err != nil {
return nil, err
Expand Down
7 changes: 5 additions & 2 deletions pkg/option/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import (
)

const (
defaultChroot = "/"
DefaultChroot = "/"
)

const (
envKeyChroot = "GHW_CHROOT"
envKeyDisableWarnings = "GHW_DISABLE_WARNINGS"
envKeyDisableTools = "GHW_DISABLE_TOOLS"
Expand Down Expand Up @@ -57,7 +60,7 @@ func EnvOrDefaultChroot() string {
if val, exists := os.LookupEnv(envKeyChroot); exists {
return val
}
return defaultChroot
return DefaultChroot
}

// EnvOrDefaultSnapshotPath returns the value of the GHW_SNAPSHOT_PATH environs variable
Expand Down
22 changes: 13 additions & 9 deletions pkg/pci/pci.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,18 +160,22 @@ func New(opts ...*option.Option) (*Info, error) {
func NewWithContext(ctx *context.Context) (*Info, error) {
// by default we don't report NUMA information;
// we will only if are sure we are running on NUMA architecture
arch := topology.ARCHITECTURE_SMP
topo, err := topology.NewWithContext(ctx)
if err == nil {
arch = topo.Architecture
} else {
ctx.Warn("error detecting system topology: %v", err)
}
info := &Info{
arch: arch,
arch: topology.ARCHITECTURE_SMP,
ctx: ctx,
}
if err := ctx.Do(info.load); err != nil {
// we do this trick because we need to make sure ctx.Setup() gets
// a chance to run before any subordinate package is created reusing
// our context.
if err := ctx.Do(func() error {
topo, err := topology.NewWithContext(ctx)
if err == nil {
info.arch = topo.Architecture
} else {
ctx.Warn("error detecting system topology: %v", err)
}
return info.load()
}); err != nil {
return nil, err
}
return info, nil
Expand Down
Loading