diff --git a/lang/core/datetime/now_fact.go b/lang/core/datetime/now.go similarity index 73% rename from lang/core/datetime/now_fact.go rename to lang/core/datetime/now.go index 0aa90c6c..c488de50 100644 --- a/lang/core/datetime/now_fact.go +++ b/lang/core/datetime/now.go @@ -31,9 +31,11 @@ package coredatetime import ( "context" + "fmt" "time" - "github.com/purpleidea/mgmt/lang/funcs/facts" + "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" ) @@ -44,44 +46,58 @@ const ( ) func init() { - facts.ModuleRegister(ModuleName, NowFuncName, func() facts.Fact { return &DateTimeFact{} }) // must register the fact and name + funcs.ModuleRegister(ModuleName, NowFuncName, func() interfaces.Func { return &Now{} }) // must register the fact and name } -// DateTimeFact is a fact which returns the current date and time. -type DateTimeFact struct { - init *facts.Init +// Now is a fact which returns the current date and time. +type Now struct { + init *interfaces.Init } // String returns a simple name for this fact. This is needed so this struct can // satisfy the pgraph.Vertex interface. -func (obj *DateTimeFact) String() string { +func (obj *Now) String() string { return NowFuncName } -// Validate makes sure we've built our struct properly. It is usually unused for -// normal facts that users can use directly. -//func (obj *DateTimeFact) Validate() error { -// return nil -//} +// Validate makes sure we've built our struct properly. +func (obj *Now) Validate() error { + return nil +} // Info returns some static info about itself. -func (obj *DateTimeFact) Info() *facts.Info { - return &facts.Info{ - Pure: false, - Memo: false, - Output: types.NewType("int"), +func (obj *Now) Info() *interfaces.Info { + return &interfaces.Info{ + Pure: false, // non-constant facts can't be pure! + Memo: false, + Fast: false, + Spec: false, + Sig: types.NewType("func() int"), } } // Init runs some startup code for this fact. -func (obj *DateTimeFact) Init(init *facts.Init) error { +func (obj *Now) Init(init *interfaces.Init) error { obj.init = init return nil } // Stream returns the changing values that this fact has over time. -func (obj *DateTimeFact) Stream(ctx context.Context) error { +func (obj *Now) Stream(ctx context.Context) error { defer close(obj.init.Output) // always signal when we're done + + // We always wait for our initial event to start. + select { + case _, ok := <-obj.init.Input: + if ok { + return fmt.Errorf("unexpected input") + } + obj.init.Input = nil + + case <-ctx.Done(): + return nil + } + // XXX: this might be an interesting fact to write because: // 1) will the sleeps from the ticker be in sync with the second ticker? // 2) if we care about a less precise interval (eg: minute changes) can @@ -89,28 +105,33 @@ func (obj *DateTimeFact) Stream(ctx context.Context) error { // 3) is it best to have a delta timer that wakes up before it's needed // and calculates how much longer to sleep for? ticker := time.NewTicker(time.Duration(1) * time.Second) + defer ticker.Stop() // streams must generate an initial event on startup + // even though ticker will send one, we want to be faster to first event startChan := make(chan struct{}) // start signal close(startChan) // kick it off! - defer ticker.Stop() + for { select { - case <-startChan: // kick the loop once at start + case <-startChan: startChan = nil // disable + case <-ticker.C: // received the timer event // pass + case <-ctx.Done(): return nil } - result, err := obj.Call(ctx) + result, err := obj.Call(ctx, nil) if err != nil { return err } select { case obj.init.Output <- result: + case <-ctx.Done(): return nil } @@ -118,7 +139,7 @@ func (obj *DateTimeFact) Stream(ctx context.Context) error { } // Call this fact and return the value if it is possible to do so at this time. -func (obj *DateTimeFact) Call(ctx context.Context) (types.Value, error) { +func (obj *Now) Call(ctx context.Context, args []types.Value) (types.Value, error) { return &types.IntValue{ // seconds since 1970... V: time.Now().Unix(), // .UTC() not necessary }, nil diff --git a/lang/core/example/flipflop_fact.go b/lang/core/example/flipflop.go similarity index 69% rename from lang/core/example/flipflop_fact.go rename to lang/core/example/flipflop.go index bef1bbef..81fb8589 100644 --- a/lang/core/example/flipflop_fact.go +++ b/lang/core/example/flipflop.go @@ -31,10 +31,12 @@ package coreexample import ( "context" + "fmt" "sync" "time" - "github.com/purpleidea/mgmt/lang/funcs/facts" + "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" ) @@ -45,65 +47,85 @@ const ( ) func init() { - facts.ModuleRegister(ModuleName, FlipFlopFuncName, func() facts.Fact { return &FlipFlopFact{} }) // must register the fact and name + funcs.ModuleRegister(ModuleName, FlipFlopFuncName, func() interfaces.Func { return &FlipFlop{} }) // must register the fact and name } -// FlipFlopFact is a fact which flips a bool repeatedly. This is an example fact -// and is not meant for serious computing. This would be better served by a flip +// FlipFlop is a fact which flips a bool repeatedly. This is an example fact and +// is not meant for serious computing. This would be better served by a flip // function which you could specify an interval for. -type FlipFlopFact struct { - init *facts.Init +type FlipFlop struct { + init *interfaces.Init mutex *sync.Mutex value bool } // String returns a simple name for this fact. This is needed so this struct can // satisfy the pgraph.Vertex interface. -func (obj *FlipFlopFact) String() string { +func (obj *FlipFlop) String() string { return FlipFlopFuncName } -// Validate makes sure we've built our struct properly. It is usually unused for -// normal facts that users can use directly. -//func (obj *FlipFlopFact) Validate() error { -// return nil -//} +// Validate makes sure we've built our struct properly. +func (obj *FlipFlop) Validate() error { + return nil +} // Info returns some static info about itself. -func (obj *FlipFlopFact) Info() *facts.Info { - return &facts.Info{ - Output: types.NewType("bool"), +func (obj *FlipFlop) Info() *interfaces.Info { + return &interfaces.Info{ + Pure: false, // non-constant facts can't be pure! + Memo: false, + Fast: false, + Spec: false, + Sig: types.NewType("func() bool"), } } // Init runs some startup code for this fact. -func (obj *FlipFlopFact) Init(init *facts.Init) error { +func (obj *FlipFlop) Init(init *interfaces.Init) error { obj.init = init obj.mutex = &sync.Mutex{} return nil } // Stream returns the changing values that this fact has over time. -func (obj *FlipFlopFact) Stream(ctx context.Context) error { +func (obj *FlipFlop) Stream(ctx context.Context) error { defer close(obj.init.Output) // always signal when we're done + + // We always wait for our initial event to start. + select { + case _, ok := <-obj.init.Input: + if ok { + return fmt.Errorf("unexpected input") + } + obj.init.Input = nil + + case <-ctx.Done(): + return nil + } + // TODO: don't hard code 5 sec interval ticker := time.NewTicker(time.Duration(5) * time.Second) + defer ticker.Stop() // streams must generate an initial event on startup + // even though ticker will send one, we want to be faster to first event startChan := make(chan struct{}) // start signal close(startChan) // kick it off! - defer ticker.Stop() + for { select { - case <-startChan: // kick the loop once at start + case <-startChan: startChan = nil // disable + case <-ticker.C: // received the timer event // pass + case <-ctx.Done(): return nil } - result, err := obj.Call(ctx) + result, err := obj.Call(ctx, nil) if err != nil { return err } @@ -122,9 +144,9 @@ func (obj *FlipFlopFact) Stream(ctx context.Context) error { } // Call this fact and return the value if it is possible to do so at this time. -func (obj *FlipFlopFact) Call(ctx context.Context) (types.Value, error) { +func (obj *FlipFlop) Call(ctx context.Context, args []types.Value) (types.Value, error) { if obj.mutex == nil { - return nil, facts.ErrCantSpeculate + return nil, funcs.ErrCantSpeculate } obj.mutex.Lock() // TODO: could be a read lock value := obj.value diff --git a/lang/core/sys/cpucount_fact.go b/lang/core/sys/cpucount.go similarity index 81% rename from lang/core/sys/cpucount_fact.go rename to lang/core/sys/cpucount.go index a9ce32ff..07dc4b43 100644 --- a/lang/core/sys/cpucount_fact.go +++ b/lang/core/sys/cpucount.go @@ -33,13 +33,15 @@ package coresys import ( "context" + "fmt" "os" "regexp" "strconv" "strings" "sync" - "github.com/purpleidea/mgmt/lang/funcs/facts" + "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" "github.com/purpleidea/mgmt/util/errwrap" "github.com/purpleidea/mgmt/util/socketset" @@ -58,32 +60,39 @@ const ( ) func init() { - facts.ModuleRegister(ModuleName, CPUCountFuncName, func() facts.Fact { return &CPUCountFact{} }) // must register the fact and name + funcs.ModuleRegister(ModuleName, CPUCountFuncName, func() interfaces.Func { return &CPUCount{} }) // must register the fact and name } -// CPUCountFact is a fact that returns the current CPU count. -type CPUCountFact struct { - init *facts.Init +// CPUCount is a fact that returns the current CPU count. +type CPUCount struct { + init *interfaces.Init result types.Value // last calculated output } // String returns a simple name for this fact. This is needed so this struct can // satisfy the pgraph.Vertex interface. -func (obj *CPUCountFact) String() string { +func (obj *CPUCount) String() string { return CPUCountFuncName } +// Validate makes sure we've built our struct properly. +func (obj *CPUCount) Validate() error { + return nil +} + // Info returns static typing info about what the fact returns. -func (obj *CPUCountFact) Info() *facts.Info { - return &facts.Info{ - Pure: false, - Memo: false, - Output: types.NewType("int"), +func (obj *CPUCount) Info() *interfaces.Info { + return &interfaces.Info{ + Pure: false, // non-constant facts can't be pure! + Memo: false, + Fast: false, + Spec: false, + Sig: types.NewType("func() int"), } } -// Init runs startup code for this fact and sets the facts.Init variable. -func (obj *CPUCountFact) Init(init *facts.Init) error { +// Init runs startup code for this fact. +func (obj *CPUCount) Init(init *interfaces.Init) error { obj.init = init return nil } @@ -91,9 +100,21 @@ func (obj *CPUCountFact) Init(init *facts.Init) error { // Stream returns the changing values that this fact has over time. It will // first poll sysfs to get the initial cpu count, and then receives UEvents from // the kernel as CPUs are added/removed. -func (obj CPUCountFact) Stream(ctx context.Context) error { +func (obj CPUCount) Stream(ctx context.Context) error { defer close(obj.init.Output) // signal when we're done + // We always wait for our initial event to start. + select { + case _, ok := <-obj.init.Input: + if ok { + return fmt.Errorf("unexpected input") + } + obj.init.Input = nil + + case <-ctx.Done(): + return nil + } + ss, err := socketset.NewSocketSet(rtmGrps, socketFile, unix.NETLINK_KOBJECT_UEVENT) if err != nil { return errwrap.Wrapf(err, "error creating socket set") @@ -118,6 +139,8 @@ func (obj CPUCountFact) Stream(ctx context.Context) error { defer wg.Done() defer close(eventChan) for { + // XXX: This does *not* generate an initial event on + // startup, so instead, use startChan below... uevent, err := ss.ReceiveUEvent() // calling Shutdown will stop this from blocking if obj.init.Debug { obj.init.Logf("sending uevent SEQNUM: %s", uevent.Data["SEQNUM"]) @@ -133,8 +156,9 @@ func (obj CPUCountFact) Stream(ctx context.Context) error { } }() - startChan := make(chan struct{}) - close(startChan) // trigger the first event + // streams must generate an initial event on startup + startChan := make(chan struct{}) // start signal + close(startChan) // kick it off! for { select { case <-startChan: @@ -158,7 +182,7 @@ func (obj CPUCountFact) Stream(ctx context.Context) error { return nil } - result, err := obj.Call(ctx) + result, err := obj.Call(ctx, nil) if err != nil { return err } @@ -171,6 +195,7 @@ func (obj CPUCountFact) Stream(ctx context.Context) error { select { case obj.init.Output <- result: + case <-ctx.Done(): return nil } @@ -178,7 +203,7 @@ func (obj CPUCountFact) Stream(ctx context.Context) error { } // Call this fact and return the value if it is possible to do so at this time. -func (obj *CPUCountFact) Call(ctx context.Context) (types.Value, error) { +func (obj *CPUCount) Call(ctx context.Context, args []types.Value) (types.Value, error) { count, err := getCPUCount() // TODO: ctx? if err != nil { return nil, errwrap.Wrapf(err, "could not get CPU count") diff --git a/lang/core/sys/cpucount_fact_test.go b/lang/core/sys/cpucount_test.go similarity index 91% rename from lang/core/sys/cpucount_fact_test.go rename to lang/core/sys/cpucount_test.go index e4f93c51..10ced3c4 100644 --- a/lang/core/sys/cpucount_fact_test.go +++ b/lang/core/sys/cpucount_test.go @@ -35,22 +35,25 @@ import ( "context" "testing" - "github.com/purpleidea/mgmt/lang/funcs/facts" + "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" ) func TestSimple(t *testing.T) { - fact := &CPUCountFact{} + fact := &CPUCount{} + input := make(chan types.Value) + close(input) // kick it off! output := make(chan types.Value) - err := fact.Init(&facts.Init{ + err := fact.Init(&interfaces.Init{ + Input: input, Output: output, Logf: func(format string, v ...interface{}) { - t.Logf("cpucount_fact_test: "+format, v...) + t.Logf("cpucount_test: "+format, v...) }, }) if err != nil { - t.Errorf("could not init CPUCountFact") + t.Errorf("could not init CPUCount") return } diff --git a/lang/core/sys/hostname_fact.go b/lang/core/sys/hostname.go similarity index 80% rename from lang/core/sys/hostname_fact.go rename to lang/core/sys/hostname.go index cc8109a9..5419fe90 100644 --- a/lang/core/sys/hostname_fact.go +++ b/lang/core/sys/hostname.go @@ -34,7 +34,8 @@ import ( "fmt" engineUtil "github.com/purpleidea/mgmt/engine/util" - "github.com/purpleidea/mgmt/lang/funcs/facts" + "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" "github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util/errwrap" @@ -54,44 +55,59 @@ const ( ) func init() { - facts.ModuleRegister(ModuleName, HostnameFuncName, func() facts.Fact { return &HostnameFact{} }) // must register the fact and name + funcs.ModuleRegister(ModuleName, HostnameFuncName, func() interfaces.Func { return &Hostname{} }) // must register the fact and name } -// HostnameFact is a function that returns the hostname. +// Hostname is a function that returns the hostname. // TODO: support hostnames that change in the future. -type HostnameFact struct { - init *facts.Init +type Hostname struct { + init *interfaces.Init } // String returns a simple name for this fact. This is needed so this struct can // satisfy the pgraph.Vertex interface. -func (obj *HostnameFact) String() string { +func (obj *Hostname) String() string { return HostnameFuncName } -// Validate makes sure we've built our struct properly. It is usually unused for -// normal facts that users can use directly. -//func (obj *HostnameFact) Validate() error { -// return nil -//} +// Validate makes sure we've built our struct properly. +func (obj *Hostname) Validate() error { + return nil +} // Info returns some static info about itself. -func (obj *HostnameFact) Info() *facts.Info { - return &facts.Info{ - Output: types.NewType("str"), +func (obj *Hostname) Info() *interfaces.Info { + return &interfaces.Info{ + Pure: false, // non-constant facts can't be pure! + Memo: false, + Fast: false, + Spec: false, + Sig: types.NewType("func() str"), } } // Init runs some startup code for this fact. -func (obj *HostnameFact) Init(init *facts.Init) error { +func (obj *Hostname) Init(init *interfaces.Init) error { obj.init = init return nil } // Stream returns the single value that this fact has, and then closes. -func (obj *HostnameFact) Stream(ctx context.Context) error { +func (obj *Hostname) Stream(ctx context.Context) error { defer close(obj.init.Output) // signal that we're done sending + // We always wait for our initial event to start. + select { + case _, ok := <-obj.init.Input: + if ok { + return fmt.Errorf("unexpected input") + } + obj.init.Input = nil + + case <-ctx.Done(): + return nil + } + recurse := false // single file recWatcher, err := recwatch.NewRecWatcher("/etc/hostname", recurse) if err != nil { @@ -120,12 +136,13 @@ func (obj *HostnameFact) Stream(ctx context.Context) error { bus.Signal(signals) // streams must generate an initial event on startup + // XXX: recwatcher should eventually provide this for us startChan := make(chan struct{}) // start signal close(startChan) // kick it off! for { select { - case <-startChan: // kick the loop once at start + case <-startChan: startChan = nil // disable case _, ok := <-signals: @@ -149,7 +166,7 @@ func (obj *HostnameFact) Stream(ctx context.Context) error { } // NOTE: We ask the actual machine instead of using obj.init.Hostname - value, err := obj.Call(ctx) + value, err := obj.Call(ctx, nil) if err != nil { return err } @@ -157,6 +174,7 @@ func (obj *HostnameFact) Stream(ctx context.Context) error { select { case obj.init.Output <- value: // pass + case <-ctx.Done(): return nil } @@ -164,7 +182,7 @@ func (obj *HostnameFact) Stream(ctx context.Context) error { } // Call returns the result of this function. -func (obj *HostnameFact) Call(ctx context.Context) (types.Value, error) { +func (obj *Hostname) Call(ctx context.Context, args []types.Value) (types.Value, error) { conn, err := util.SystemBusPrivateUsable() if err != nil { return nil, errwrap.Wrapf(err, "failed to connect to the private system bus") @@ -183,7 +201,7 @@ func (obj *HostnameFact) Call(ctx context.Context) (types.Value, error) { }, nil } -func (obj *HostnameFact) getHostnameProperty(object dbus.BusObject, property string) (string, error) { +func (obj *Hostname) getHostnameProperty(object dbus.BusObject, property string) (string, error) { propertyObject, err := object.GetProperty("org.freedesktop.hostname1." + property) if err != nil { return "", errwrap.Wrapf(err, "failed to get org.freedesktop.hostname1.%s", property) diff --git a/lang/core/sys/load_fact.go b/lang/core/sys/load.go similarity index 73% rename from lang/core/sys/load_fact.go rename to lang/core/sys/load.go index 0d2bf78d..46d0a743 100644 --- a/lang/core/sys/load_fact.go +++ b/lang/core/sys/load.go @@ -31,9 +31,11 @@ package coresys import ( "context" + "fmt" "time" - "github.com/purpleidea/mgmt/lang/funcs/facts" + "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" "github.com/purpleidea/mgmt/util/errwrap" ) @@ -47,71 +49,89 @@ const ( ) func init() { - facts.ModuleRegister(ModuleName, LoadFuncName, func() facts.Fact { return &LoadFact{} }) // must register the fact and name + funcs.ModuleRegister(ModuleName, LoadFuncName, func() interfaces.Func { return &Load{} }) // must register the fact and name } -// LoadFact is a fact which returns the current system load. -type LoadFact struct { - init *facts.Init +// Load is a fact which returns the current system load. +type Load struct { + init *interfaces.Init } // String returns a simple name for this fact. This is needed so this struct can // satisfy the pgraph.Vertex interface. -func (obj *LoadFact) String() string { +func (obj *Load) String() string { return LoadFuncName } -// Validate makes sure we've built our struct properly. It is usually unused for -// normal facts that users can use directly. -//func (obj *LoadFact) Validate() error { -// return nil -//} +// Validate makes sure we've built our struct properly. +func (obj *Load) Validate() error { + return nil +} // Info returns some static info about itself. -func (obj *LoadFact) Info() *facts.Info { - return &facts.Info{ - Pure: false, - Memo: false, - Output: types.NewType(loadSignature), +func (obj *Load) Info() *interfaces.Info { + return &interfaces.Info{ + Pure: false, // non-constant facts can't be pure! + Memo: false, + Fast: false, + Spec: false, + Sig: types.NewType(fmt.Sprintf("func() %s", loadSignature)), } } // Init runs some startup code for this fact. -func (obj *LoadFact) Init(init *facts.Init) error { +func (obj *Load) Init(init *interfaces.Init) error { obj.init = init return nil } // Stream returns the changing values that this fact has over time. -func (obj *LoadFact) Stream(ctx context.Context) error { +func (obj *Load) Stream(ctx context.Context) error { defer close(obj.init.Output) // always signal when we're done + // We always wait for our initial event to start. + select { + case _, ok := <-obj.init.Input: + if ok { + return fmt.Errorf("unexpected input") + } + obj.init.Input = nil + + case <-ctx.Done(): + return nil + } + // it seems the different values only update once every 5 // seconds, so that's as often as we need to refresh this! // TODO: lookup this value if it's something configurable ticker := time.NewTicker(time.Duration(5) * time.Second) + defer ticker.Stop() // streams must generate an initial event on startup + // even though ticker will send one, we want to be faster to first event startChan := make(chan struct{}) // start signal close(startChan) // kick it off! - defer ticker.Stop() + for { select { - case <-startChan: // kick the loop once at start + case <-startChan: startChan = nil // disable + case <-ticker.C: // received the timer event // pass + case <-ctx.Done(): return nil } - result, err := obj.Call(ctx) + result, err := obj.Call(ctx, nil) if err != nil { return err } select { case obj.init.Output <- result: + case <-ctx.Done(): return nil } @@ -119,7 +139,7 @@ func (obj *LoadFact) Stream(ctx context.Context) error { } // Call this fact and return the value if it is possible to do so at this time. -func (obj *LoadFact) Call(ctx context.Context) (types.Value, error) { +func (obj *Load) Call(ctx context.Context, args []types.Value) (types.Value, error) { x1, x5, x15, err := load() if err != nil { return nil, errwrap.Wrapf(err, "could not read load values") diff --git a/lang/core/sys/uptime_fact.go b/lang/core/sys/uptime.go similarity index 67% rename from lang/core/sys/uptime_fact.go rename to lang/core/sys/uptime.go index 57064e30..fd03c83e 100644 --- a/lang/core/sys/uptime_fact.go +++ b/lang/core/sys/uptime.go @@ -31,9 +31,11 @@ package coresys import ( "context" + "fmt" "time" - "github.com/purpleidea/mgmt/lang/funcs/facts" + "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" "github.com/purpleidea/mgmt/util/errwrap" ) @@ -45,60 +47,86 @@ const ( ) func init() { - facts.ModuleRegister(ModuleName, UptimeFuncName, func() facts.Fact { return &UptimeFact{} }) + funcs.ModuleRegister(ModuleName, UptimeFuncName, func() interfaces.Func { return &Uptime{} }) } -// UptimeFact is a fact which returns the current uptime of your system. -type UptimeFact struct { - init *facts.Init +// Uptime is a fact which returns the current uptime of your system. +type Uptime struct { + init *interfaces.Init } // String returns a simple name for this fact. This is needed so this struct can // satisfy the pgraph.Vertex interface. -func (obj *UptimeFact) String() string { +func (obj *Uptime) String() string { return UptimeFuncName } +// Validate makes sure we've built our struct properly. +func (obj *Uptime) Validate() error { + return nil +} + // Info returns some static info about itself. -func (obj *UptimeFact) Info() *facts.Info { - return &facts.Info{ - Pure: false, - Memo: false, - Output: types.TypeInt, +func (obj *Uptime) Info() *interfaces.Info { + return &interfaces.Info{ + Pure: false, // non-constant facts can't be pure! + Memo: false, + Fast: false, + Spec: false, + Sig: types.NewType("func() int"), } } // Init runs some startup code for this fact. -func (obj *UptimeFact) Init(init *facts.Init) error { +func (obj *Uptime) Init(init *interfaces.Init) error { obj.init = init return nil } // Stream returns the changing values that this fact has over time. -func (obj *UptimeFact) Stream(ctx context.Context) error { +func (obj *Uptime) Stream(ctx context.Context) error { defer close(obj.init.Output) - ticker := time.NewTicker(time.Duration(1) * time.Second) - startChan := make(chan struct{}) - close(startChan) + // We always wait for our initial event to start. + select { + case _, ok := <-obj.init.Input: + if ok { + return fmt.Errorf("unexpected input") + } + obj.init.Input = nil + + case <-ctx.Done(): + return nil + } + + ticker := time.NewTicker(time.Duration(1) * time.Second) defer ticker.Stop() + + // streams must generate an initial event on startup + // even though ticker will send one, we want to be faster to first event + startChan := make(chan struct{}) // start signal + close(startChan) // kick it off! + for { select { case <-startChan: - startChan = nil + startChan = nil // disable + case <-ticker.C: // send + case <-ctx.Done(): return nil } - result, err := obj.Call(ctx) + result, err := obj.Call(ctx, nil) if err != nil { return err } select { case obj.init.Output <- result: + case <-ctx.Done(): return nil } @@ -106,7 +134,7 @@ func (obj *UptimeFact) Stream(ctx context.Context) error { } // Call this fact and return the value if it is possible to do so at this time. -func (obj *UptimeFact) Call(ctx context.Context) (types.Value, error) { +func (obj *Uptime) Call(ctx context.Context, args []types.Value) (types.Value, error) { uptime, err := uptime() // TODO: add ctx? if err != nil { return nil, errwrap.Wrapf(err, "could not read uptime value") diff --git a/lang/core/test/fastcount_fact.go b/lang/core/test/fastcount.go similarity index 69% rename from lang/core/test/fastcount_fact.go rename to lang/core/test/fastcount.go index 3011ac3c..50d46413 100644 --- a/lang/core/test/fastcount_fact.go +++ b/lang/core/test/fastcount.go @@ -31,9 +31,11 @@ package coretest import ( "context" + "fmt" "sync" - "github.com/purpleidea/mgmt/lang/funcs/facts" + "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" ) @@ -45,12 +47,12 @@ const ( ) func init() { - facts.ModuleRegister(ModuleName, FastCountFuncName, func() facts.Fact { return &FastCountFact{} }) // must register the fact and name + funcs.ModuleRegister(ModuleName, FastCountFuncName, func() interfaces.Func { return &FastCount{} }) // must register the fact and name } -// FastCountFact is a fact that counts up as fast as possible from zero forever. -type FastCountFact struct { - init *facts.Init +// FastCount is a fact that counts up as fast as possible from zero forever. +type FastCount struct { + init *interfaces.Init mutex *sync.Mutex count int @@ -58,39 +60,52 @@ type FastCountFact struct { // String returns a simple name for this fact. This is needed so this struct can // satisfy the pgraph.Vertex interface. -func (obj *FastCountFact) String() string { +func (obj *FastCount) String() string { return FastCountFuncName } -// Validate makes sure we've built our struct properly. It is usually unused for -// normal facts that users can use directly. -//func (obj *FastCountFact) Validate() error { -// return nil -//} +// Validate makes sure we've built our struct properly. +func (obj *FastCount) Validate() error { + return nil +} // Info returns some static info about itself. -func (obj *FastCountFact) Info() *facts.Info { - return &facts.Info{ - Pure: false, - Memo: false, - Output: types.NewType("int"), +func (obj *FastCount) Info() *interfaces.Info { + return &interfaces.Info{ + Pure: false, // non-constant facts can't be pure! + Memo: false, + Fast: false, + Spec: false, + Sig: types.NewType("func() int"), } } // Init runs some startup code for this fact. -func (obj *FastCountFact) Init(init *facts.Init) error { +func (obj *FastCount) Init(init *interfaces.Init) error { obj.init = init obj.mutex = &sync.Mutex{} return nil } // Stream returns the changing values that this fact has over time. -func (obj *FastCountFact) Stream(ctx context.Context) error { +func (obj *FastCount) Stream(ctx context.Context) error { defer close(obj.init.Output) // always signal when we're done + // We always wait for our initial event to start. + select { + case _, ok := <-obj.init.Input: + if ok { + return fmt.Errorf("unexpected input") + } + obj.init.Input = nil + + case <-ctx.Done(): + return nil + } + // streams must generate an initial event on startup for { - result, err := obj.Call(ctx) + result, err := obj.Call(ctx, nil) if err != nil { return err } @@ -109,9 +124,9 @@ func (obj *FastCountFact) Stream(ctx context.Context) error { } // Call this fact and return the value if it is possible to do so at this time. -func (obj *FastCountFact) Call(ctx context.Context) (types.Value, error) { +func (obj *FastCount) Call(ctx context.Context, args []types.Value) (types.Value, error) { if obj.mutex == nil { - return nil, facts.ErrCantSpeculate + return nil, funcs.ErrCantSpeculate } obj.mutex.Lock() // TODO: could be a read lock count := obj.count diff --git a/lang/core/test/oneinstance_fact.go b/lang/core/test/oneinstance.go similarity index 81% rename from lang/core/test/oneinstance_fact.go rename to lang/core/test/oneinstance.go index a41c6c8e..b017670f 100644 --- a/lang/core/test/oneinstance_fact.go +++ b/lang/core/test/oneinstance.go @@ -31,10 +31,12 @@ package coretest import ( "context" + "fmt" "sync" - "github.com/purpleidea/mgmt/lang/funcs/facts" + "github.com/purpleidea/mgmt/lang/funcs" "github.com/purpleidea/mgmt/lang/funcs/simple" + "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" ) @@ -92,29 +94,29 @@ func init() { oneInstanceGMutex = &sync.Mutex{} oneInstanceHMutex = &sync.Mutex{} - facts.ModuleRegister(ModuleName, OneInstanceAFuncName, func() facts.Fact { - return &OneInstanceFact{ + funcs.ModuleRegister(ModuleName, OneInstanceAFuncName, func() interfaces.Func { + return &OneInstance{ Name: OneInstanceAFuncName, Mutex: oneInstanceAMutex, Flag: &oneInstanceAFlag, } }) // must register the fact and name - facts.ModuleRegister(ModuleName, OneInstanceCFuncName, func() facts.Fact { - return &OneInstanceFact{ + funcs.ModuleRegister(ModuleName, OneInstanceCFuncName, func() interfaces.Func { + return &OneInstance{ Name: OneInstanceCFuncName, Mutex: oneInstanceCMutex, Flag: &oneInstanceCFlag, } }) - facts.ModuleRegister(ModuleName, OneInstanceEFuncName, func() facts.Fact { - return &OneInstanceFact{ + funcs.ModuleRegister(ModuleName, OneInstanceEFuncName, func() interfaces.Func { + return &OneInstance{ Name: OneInstanceEFuncName, Mutex: oneInstanceEMutex, Flag: &oneInstanceEFlag, } }) - facts.ModuleRegister(ModuleName, OneInstanceGFuncName, func() facts.Fact { - return &OneInstanceFact{ + funcs.ModuleRegister(ModuleName, OneInstanceGFuncName, func() interfaces.Func { + return &OneInstance{ Name: OneInstanceGFuncName, Mutex: oneInstanceGMutex, Flag: &oneInstanceGFlag, @@ -138,7 +140,7 @@ func init() { oneInstanceBMutex.Unlock() return &types.StrValue{V: msg}, nil }, - D: &OneInstanceFact{}, + D: &OneInstance{}, }) simple.ModuleRegister(ModuleName, OneInstanceDFuncName, &simple.Scaffold{ I: &simple.Info{ @@ -157,7 +159,7 @@ func init() { oneInstanceDMutex.Unlock() return &types.StrValue{V: msg}, nil }, - D: &OneInstanceFact{}, + D: &OneInstance{}, }) simple.ModuleRegister(ModuleName, OneInstanceFFuncName, &simple.Scaffold{ I: &simple.Info{ @@ -176,7 +178,7 @@ func init() { oneInstanceFMutex.Unlock() return &types.StrValue{V: msg}, nil }, - D: &OneInstanceFact{}, + D: &OneInstance{}, }) simple.ModuleRegister(ModuleName, OneInstanceHFuncName, &simple.Scaffold{ I: &simple.Info{ @@ -195,7 +197,7 @@ func init() { oneInstanceHMutex.Unlock() return &types.StrValue{V: msg}, nil }, - D: &OneInstanceFact{}, + D: &OneInstance{}, }) } @@ -218,11 +220,11 @@ var ( oneInstanceHMutex *sync.Mutex ) -// OneInstanceFact is a fact which flips a bool repeatedly. This is an example -// fact and is not meant for serious computing. This would be better served by a -// flip function which you could specify an interval for. -type OneInstanceFact struct { - init *facts.Init +// OneInstance is a fact which flips a bool repeatedly. This is an example fact +// and is not meant for serious computing. This would be better served by a flip +// function which you could specify an interval for. +type OneInstance struct { + init *interfaces.Init Name string Mutex *sync.Mutex @@ -231,25 +233,28 @@ type OneInstanceFact struct { // String returns a simple name for this fact. This is needed so this struct can // satisfy the pgraph.Vertex interface. -func (obj *OneInstanceFact) String() string { +func (obj *OneInstance) String() string { return obj.Name } -// Validate makes sure we've built our struct properly. It is usually unused for -// normal facts that users can use directly. -//func (obj *OneInstanceFact) Validate() error { -// return nil -//} +// Validate makes sure we've built our struct properly. +func (obj *OneInstance) Validate() error { + return nil +} // Info returns some static info about itself. -func (obj *OneInstanceFact) Info() *facts.Info { - return &facts.Info{ - Output: types.NewType("str"), +func (obj *OneInstance) Info() *interfaces.Info { + return &interfaces.Info{ + Pure: false, // non-constant facts can't be pure! + Memo: false, + Fast: false, + Spec: false, + Sig: types.NewType("func() str"), } } // Init runs some startup code for this fact. -func (obj *OneInstanceFact) Init(init *facts.Init) error { +func (obj *OneInstance) Init(init *interfaces.Init) error { obj.init = init obj.init.Logf("Init of `%s` @ %p", obj.Name, obj) @@ -264,17 +269,30 @@ func (obj *OneInstanceFact) Init(init *facts.Init) error { } // Stream returns the changing values that this fact has over time. -func (obj *OneInstanceFact) Stream(ctx context.Context) error { +func (obj *OneInstance) Stream(ctx context.Context) error { obj.init.Logf("Stream of `%s` @ %p", obj.Name, obj) defer close(obj.init.Output) // always signal when we're done - result, err := obj.Call(ctx) + // We always wait for our initial event to start. + select { + case _, ok := <-obj.init.Input: + if ok { + return fmt.Errorf("unexpected input") + } + obj.init.Input = nil + + case <-ctx.Done(): + return nil + } + + result, err := obj.Call(ctx, nil) if err != nil { return err } select { case obj.init.Output <- result: + case <-ctx.Done(): return nil } @@ -283,7 +301,7 @@ func (obj *OneInstanceFact) Stream(ctx context.Context) error { } // Call this fact and return the value if it is possible to do so at this time. -func (obj *OneInstanceFact) Call(ctx context.Context) (types.Value, error) { +func (obj *OneInstance) Call(ctx context.Context, args []types.Value) (types.Value, error) { return &types.StrValue{ V: msg, }, nil diff --git a/lang/funcs/facts/facts.go b/lang/funcs/facts/facts.go deleted file mode 100644 index 1f6a3f57..00000000 --- a/lang/funcs/facts/facts.go +++ /dev/null @@ -1,134 +0,0 @@ -// Mgmt -// Copyright (C) James Shubin and the project contributors -// Written by James Shubin and the project contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// -// Additional permission under GNU GPL version 3 section 7 -// -// If you modify this program, or any covered work, by linking or combining it -// with embedded mcl code and modules (and that the embedded mcl code and -// modules which link with this program, contain a copy of their source code in -// the authoritative form) containing parts covered by the terms of any other -// license, the licensors of this program grant you additional permission to -// convey the resulting work. Furthermore, the licensors of this program grant -// the original author, James Shubin, additional permission to update this -// additional permission if he deems it necessary to achieve the goals of this -// additional permission. - -// Package facts provides a framework for language values that change over time. -package facts - -import ( - "context" - "fmt" - - "github.com/purpleidea/mgmt/engine" - "github.com/purpleidea/mgmt/lang/funcs" - "github.com/purpleidea/mgmt/lang/interfaces" - "github.com/purpleidea/mgmt/lang/types" -) - -const ( - // ErrCantSpeculate is an error that explains that we can't speculate - // when trying to Call a function. This often gets called by the Value() - // method of the Expr. This can be useful if we want to distinguish - // between "something is broken" and "I just can't produce a value at - // this time", which can be identified and skipped over. If it's the - // former, then it's okay to error early and shut everything down since - // we know this function is never going to work the way it's called. - ErrCantSpeculate = funcs.ErrCantSpeculate -) - -// registeredFacts is a global map of all possible facts which can be used. You -// should never touch this map directly. Use methods like Register instead. -var registeredFacts = make(map[string]struct{}) // must initialize - -// Register takes a fact and its name and makes it available for use. It is -// commonly called in the init() method of the fact at program startup. There is -// no matching Unregister function. -func Register(name string, fn func() Fact) { - if _, ok := registeredFacts[name]; ok { - panic(fmt.Sprintf("a fact named %s is already registered", name)) - } - f := fn() // don't wrap this more than once! - - metadata, err := funcs.GetFunctionMetadata(f) - if err != nil { - panic(fmt.Sprintf("could not locate fact filename for %s", name)) - } - - //gob.Register(fn()) - funcs.Register(name, func() interfaces.Func { // implement in terms of func interface - return &FactFunc{ - Fact: fn(), // this MUST be a fresh/unique pointer! - - Metadata: metadata, - } - }) - registeredFacts[name] = struct{}{} -} - -// ModuleRegister is exactly like Register, except that it registers within a -// named module. This is a helper function. -func ModuleRegister(module, name string, fn func() Fact) { - Register(module+funcs.ModuleSep+name, fn) -} - -// Info is a static representation of some information about the fact. It is -// used for static analysis and type checking. If you break this contract, you -// might cause a panic. -type Info struct { - Pure bool // is the function pure? (can it be memoized?) - Memo bool // should the function be memoized? (false if too much output) - Fast bool // is the function fast? (avoid speculative execution) - Spec bool // can we speculatively execute it? (true for most) - Output *types.Type // output value type (must not change over time!) - Err error // did this fact validate? -} - -// Init is the structure of values and references which is passed into all facts -// on initialization. -type Init struct { - Hostname string // uuid for the host - //Noop bool - Output chan types.Value // Stream must close `output` chan - World engine.World - Debug bool - Logf func(format string, v ...interface{}) -} - -// Fact is the interface that any valid fact must fulfill. It is very simple, -// but still event driven. Facts should attempt to only send values when they -// have changed. -// TODO: should we support a static version of this interface for facts that -// never change to avoid the overhead of the goroutine and channel listener? -// TODO: should we move this to the interface package? -type Fact interface { - String() string - //Validate() error // currently not needed since no facts are internal - Info() *Info - Init(*Init) error - Stream(context.Context) error -} - -// CallableFact is a function that takes no args, and that can be called -// statically if we want to do it speculatively or from a resource. -type CallableFact interface { - Fact // implement everything in Fact but add the additional requirements - - // Call this fact and return the value if it is possible to do so at - // this time. - Call(ctx context.Context) (types.Value, error) -} diff --git a/lang/funcs/facts/func.go b/lang/funcs/facts/func.go deleted file mode 100644 index f1c7b53a..00000000 --- a/lang/funcs/facts/func.go +++ /dev/null @@ -1,110 +0,0 @@ -// Mgmt -// Copyright (C) James Shubin and the project contributors -// Written by James Shubin and the project contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// -// Additional permission under GNU GPL version 3 section 7 -// -// If you modify this program, or any covered work, by linking or combining it -// with embedded mcl code and modules (and that the embedded mcl code and -// modules which link with this program, contain a copy of their source code in -// the authoritative form) containing parts covered by the terms of any other -// license, the licensors of this program grant you additional permission to -// convey the resulting work. Furthermore, the licensors of this program grant -// the original author, James Shubin, additional permission to update this -// additional permission if he deems it necessary to achieve the goals of this -// additional permission. - -package facts - -import ( - "context" - "fmt" - - docsUtil "github.com/purpleidea/mgmt/docs/util" - "github.com/purpleidea/mgmt/lang/interfaces" - "github.com/purpleidea/mgmt/lang/types" -) - -// FactFunc is a wrapper for the fact interface. It implements the fact -// interface in terms of Func to reduce the two down to a single mechanism. -type FactFunc struct { // implements `interfaces.Func` - *docsUtil.Metadata - - Fact Fact -} - -// String returns a simple name for this function. This is needed so this struct -// can satisfy the pgraph.Vertex interface. -func (obj *FactFunc) String() string { - return obj.Fact.String() -} - -// Validate makes sure we've built our struct properly. -func (obj *FactFunc) Validate() error { - if obj.Fact == nil { - return fmt.Errorf("must specify a Fact in struct") - } - //return obj.Fact.Validate() // currently unused - return nil -} - -// Info returns some static info about itself. -func (obj *FactFunc) Info() *interfaces.Info { - return &interfaces.Info{ - Pure: obj.Fact.Info().Pure, - Memo: obj.Fact.Info().Memo, - Fast: obj.Fact.Info().Fast, - Spec: obj.Fact.Info().Spec, - Sig: &types.Type{ - Kind: types.KindFunc, - // if Ord or Map are nil, this will panic things! - Ord: []string{}, - Map: make(map[string]*types.Type), - Out: obj.Fact.Info().Output, - }, - Err: obj.Fact.Info().Err, - } -} - -// Init runs some startup code for this fact. -func (obj *FactFunc) Init(init *interfaces.Init) error { - return obj.Fact.Init( - &Init{ - Hostname: init.Hostname, - Output: init.Output, - World: init.World, - Debug: init.Debug, - Logf: init.Logf, - }, - ) -} - -// Stream returns the changing values that this function has over time. -func (obj *FactFunc) Stream(ctx context.Context) error { - return obj.Fact.Stream(ctx) -} - -// Call this fact and return the value if it is possible to do so at this time. -func (obj *FactFunc) Call(ctx context.Context, _ []types.Value) (types.Value, error) { - //return obj.Fact.Call(ctx) - - callableFact, ok := obj.Fact.(CallableFact) - if !ok { - return nil, fmt.Errorf("fact is not a CallableFact") - } - - return callableFact.Call(ctx) -} diff --git a/lang/funcs/funcs_test.go b/lang/funcs/funcs_test.go deleted file mode 100644 index 8f531928..00000000 --- a/lang/funcs/funcs_test.go +++ /dev/null @@ -1,35 +0,0 @@ -// Mgmt -// Copyright (C) James Shubin and the project contributors -// Written by James Shubin and the project contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// -// Additional permission under GNU GPL version 3 section 7 -// -// If you modify this program, or any covered work, by linking or combining it -// with embedded mcl code and modules (and that the embedded mcl code and -// modules which link with this program, contain a copy of their source code in -// the authoritative form) containing parts covered by the terms of any other -// license, the licensors of this program grant you additional permission to -// convey the resulting work. Furthermore, the licensors of this program grant -// the original author, James Shubin, additional permission to update this -// additional permission if he deems it necessary to achieve the goals of this -// additional permission. - -//go:build !root - -package funcs - -// most of the testing of this package is inside of the adjacent `facts` package -// because it imports this package and thus lets us use those constructs to test