diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..54ffb55 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,93 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build Commands + +```bash +# Build entire project +go build ./... + +# Run tests +go test ./... + +# Run a single test +go test -run TestName ./path/to/package + +# Cross-platform builds (supported targets) +GOOS=darwin GOARCH=amd64 go build ./... # macOS +GOOS=windows GOARCH=amd64 go build ./... # Windows +GOOS=android GOARCH=arm64 go build ./... # Android +GOOS=js GOARCH=wasm go build ./... # WebAssembly + +# Run example applications +go run ./cmd/hello +go run ./cmd/clipboard +go run ./cmd/iconchooser +``` + +## Architecture Overview + +Prevara (formerly "gel" - Gio Elements) is a widget toolkit built on top of [Gio](https://gioui.org). It provides a fluent API for building GUIs with method chaining. + +### Core Components + +**Window & Theme** (`window.go`, `theme.go`): Entry points for applications. `Window` embeds both `Theme` (styling) and `app.Window` (platform integration). Create windows via `NewWindowP9(quit)` for default theme or `NewWindow(theme)` for custom themes. + +**App Framework** (`app.go`): Multi-page application scaffold with title bar, sidebar, status bar, and page navigation. Create with `window.App(size, activePage, breakpoint)`. + +**Fluent Widget Pattern**: All widgets follow a builder pattern where configuration methods return `*Widget` for chaining, and `Fn` is the final method that returns `l.Widget` (Gio's layout function type). Example: +```go +w.Flex().Vertical().SpaceEvenly(). + Rigid(w.H1("Title").Color("Primary").Fn). + Flexed(1, bodyWidget). + Fn +``` + +**Widget Pool** (`pools.go`, `pooltypes.go`): Object pooling for stateful widgets (Bool, List, Clickable, Editor, IncDec, Checkable). Access via `window.WidgetPool.GetXxx()`. Call `pool.Reset()` at frame start to reclaim widgets. + +### Layout Widgets + +- `Flex` / `VFlex`: Horizontal/vertical flex layouts with Rigid/Flexed children +- `Stack`: Z-axis stacking with alignment +- `Inset`: Padding/margins +- `Direction`: Directional alignment wrapper +- `Fill`: Background fills with alignment + +### Input Widgets + +- `Clickable`: Click/press handling +- `Bool`: Boolean state widget +- `Editor`: Text input +- `Checkable` / `Checkbox`: Toggle controls +- `Slider` / `IntSlider`: Value sliders +- `IncDec`: Increment/decrement buttons + +### Display Widgets + +- `Label`: Text display (with H1-H6, Body1/2, Caption, etc. presets) +- `Icon` / `IconButton`: Icon rendering +- `Button` / `ButtonLayout`: Clickable buttons +- `ProgressBar` / `Indefinite`: Progress indicators +- `List` / `WrapList`: Scrollable lists + +### Supporting Packages + +- `pkg/log`: Colorized structured logging. Initialize with `log.GetLogPrinterSet()` returning F, E, W, I, D, T level printers +- `pkg/qu`: Channel utilities for quit signals (`qu.T()` creates quit channel, `qu.C` is the channel type) +- `pkg/interrupt`: Signal handling with cross-platform restart support +- `pkg/opts`: Configuration option types (binary, text, integer, float, duration, list) +- `fonts/p9fonts`: Default Plan9-style font collection +- `clipboard`: Platform-specific clipboard operations (X11 on Linux) + +### Logging Convention + +Each package has a `log.go` file that initializes package-level loggers: +```go +var F, E, W, I, D, T = log.GetLogPrinterSet(log.AddLoggerSubsystem(version.PathBase)) +``` +Use `D.Ln()` for debug, `I.Ln()` for info, `E.Chk(err)` for error checking, etc. + +### Color System + +Colors are referenced by name strings (e.g., "Primary", "DocBg", "DocText", "PanelBg"). The theme maintains light/dark variants toggled via `theme.Dark.Flip()`. diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6510eec --- /dev/null +++ b/go.sum @@ -0,0 +1,41 @@ +eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY= +eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA= +gioui.org v0.9.0 h1:4u7XZwnb5kzQW91Nz/vR0wKD6LdW9CaVF96r3rfy4kc= +gioui.org v0.9.0/go.mod h1:CjNig0wAhLt9WZxOPAusgFD8x8IRvqt26LdDBa3Jvao= +gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= +gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA= +gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= +github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc h1:7D+Bh06CRPCJO3gr2F7h1sriovOZ8BMhca2Rg85c2nk= +github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-text/typesetting v0.3.1 h1:ESHfFntFnJOigjEeEiTc3OGXqggC1eSAAqHkG9ZB+yA= +github.com/go-text/typesetting v0.3.1/go.mod h1:vIRUT25mLQaSh4C8H/lIsKppQz/Gdb8Pu/tNwpi52ts= +github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8 h1:4KCscI9qYWMGTuz6BpJtbUSRzcBrUSSE0ENMJbNSrFs= +github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o= +github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0= +github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E= +github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA= +github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= +golang.org/x/exp/shiny v0.0.0-20251125195548-87e1e737ad39 h1:fy+QQHOvRvUJ5ZJigptKDpFN332kInaZSFvlb0CrwGA= +golang.org/x/exp/shiny v0.0.0-20251125195548-87e1e737ad39/go.mod h1:p7Wr/QhhC3SjhTsG6HN+87un+wDRHZIBEPvfbo51ToQ= +golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ= +golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= diff --git a/pkg/apputil/apputil.go b/pkg/apputil/apputil.go new file mode 100644 index 0000000..6df6db1 --- /dev/null +++ b/pkg/apputil/apputil.go @@ -0,0 +1,62 @@ +package apputil + +import ( + "os" + "path/filepath" + "runtime" +) + +// EnsureDir checks a file could be written to a path, creates the directories as needed +func EnsureDir(fileName string) { + dirName := filepath.Dir(fileName) + if _, serr := os.Stat(dirName); serr != nil { + merr := os.MkdirAll(dirName, os.ModePerm) + if merr != nil { + panic(merr) + } + } +} + +// FileExists reports whether the named file or directory exists. +func FileExists(filePath string) bool { + _, e := os.Stat(filePath) + return e == nil +} + +// MinUint32 is a helper function to return the minimum of two uint32s. This avoids a math import and the need to cast +// to floats. +func MinUint32(a, b uint32) uint32 { + if a < b { + return a + } + return b +} + +// PrependForWindows runs a command with a terminal +func PrependForWindows(args []string) []string { + if runtime.GOOS == "windows" { + args = append( + []string{ + "cmd.exe", + "/C", + }, + args..., + ) + } + return args +} + +// PrependForWindowsWithStart runs a process independently +func PrependForWindowsWithStart(args []string) []string { + if runtime.GOOS == "windows" { + args = append( + []string{ + "cmd.exe", + "/C", + "start", + }, + args..., + ) + } + return args +} diff --git a/pkg/interrupt/README.md b/pkg/interrupt/README.md new file mode 100644 index 0000000..fc97615 --- /dev/null +++ b/pkg/interrupt/README.md @@ -0,0 +1,2 @@ +# interrupt +Handle shutdowns cleanly easy restarts (theoretically) diff --git a/pkg/interrupt/cmd/ctrlctest.go b/pkg/interrupt/cmd/ctrlctest.go new file mode 100644 index 0000000..16b2e33 --- /dev/null +++ b/pkg/interrupt/cmd/ctrlctest.go @@ -0,0 +1,14 @@ +package main + +import ( + "fmt" + + "git.mleku.dev/mleku/prevara/pkg/interrupt" +) + +func main() { + interrupt.AddHandler(func() { + fmt.Println("IT'S THE END OF THE WORLD!") + }) + <-interrupt.HandlersDone +} diff --git a/pkg/interrupt/log.go b/pkg/interrupt/log.go new file mode 100644 index 0000000..8a9d463 --- /dev/null +++ b/pkg/interrupt/log.go @@ -0,0 +1,8 @@ +package interrupt + +import ( + "git.mleku.dev/mleku/prevara/pkg/log" + "git.mleku.dev/mleku/prevara/version" +) + +var F, E, W, I, D, T = log.GetLogPrinterSet(log.AddLoggerSubsystem(version.PathBase)) diff --git a/pkg/interrupt/main.go b/pkg/interrupt/main.go new file mode 100644 index 0000000..3e7040a --- /dev/null +++ b/pkg/interrupt/main.go @@ -0,0 +1,152 @@ +package interrupt + +import ( + "fmt" + "os" + "os/signal" + "runtime" + "strings" + + uberatomic "go.uber.org/atomic" + + "git.mleku.dev/mleku/prevara/pkg/qu" +) + +type HandlerWithSource struct { + Source string + Fn func() +} + +var ( + Restart bool // = true + requested uberatomic.Bool + // ch is used to receive SIGINT (Ctrl+C) signals. + ch chan os.Signal + // signals is the list of signals that cause the interrupt + signals = []os.Signal{os.Interrupt} + // ShutdownRequestChan is a channel that can receive shutdown requests + ShutdownRequestChan = qu.T() + // addHandlerChan is used to add an interrupt handler to the list of + // handlers to be invoked on SIGINT (Ctrl+C) signals. + addHandlerChan = make(chan HandlerWithSource) + // HandlersDone is closed after all interrupt handlers run the first time + // an interrupt is signaled. + HandlersDone = make(qu.C) +) + +var interruptCallbacks []func() +var interruptCallbackSources []string + +// Listener listens for interrupt signals, registers interrupt callbacks, +// and responds to custom shutdown signals as required +func Listener() { + invokeCallbacks := func() { + D.Ln( + "running interrupt callbacks", + len(interruptCallbacks), + strings.Repeat(" ", 48), + interruptCallbackSources, + ) + // run handlers in LIFO order. + for i := range interruptCallbacks { + idx := len(interruptCallbacks) - 1 - i + D.Ln("running callback", idx, interruptCallbackSources[idx]) + interruptCallbacks[idx]() + } + D.Ln("interrupt handlers finished") + HandlersDone.Q() + if Restart { + doRestart() + } + // time.Sleep(time.Second * 3) + // os.Exit(1) + // close(HandlersDone) + } +out: + for { + select { + case sig := <-ch: + // if !requested { + // L.Printf("\r>>> received signal (%s)\n", sig) + D.Ln("received interrupt signal", sig) + requested.Store(true) + invokeCallbacks() + // pprof.Lookup("goroutine").WriteTo(os.Stderr, 2) + // } + break out + case <-ShutdownRequestChan.Wait(): + // if !requested { + W.Ln("received shutdown request - shutting down...") + requested.Store(true) + invokeCallbacks() + break out + // } + case handler := <-addHandlerChan: + // if !requested { + // D.Ln("adding handler") + interruptCallbacks = append(interruptCallbacks, handler.Fn) + interruptCallbackSources = append(interruptCallbackSources, handler.Source) + // } + case <-HandlersDone.Wait(): + break out + } + } +} + +// AddHandler adds a handler to call when a SIGINT (Ctrl+C) is received. +func AddHandler(handler func()) { + // Create the channel and start the main interrupt handler which invokes all other callbacks and exits if not + // already done. + _, loc, line, _ := runtime.Caller(1) + msg := fmt.Sprintf("%s:%d", loc, line) + D.Ln("handler added by:", msg) + if ch == nil { + ch = make(chan os.Signal) + signal.Notify(ch, signals...) + go Listener() + } + addHandlerChan <- HandlerWithSource{ + msg, handler, + } +} + +// Request programmatically requests a shutdown +func Request() { + _, f, l, _ := runtime.Caller(1) + D.Ln("interrupt requested", f, l, requested.Load()) + if requested.Load() { + D.Ln("requested again") + return + } + requested.Store(true) + ShutdownRequestChan.Q() + // qu.PrintChanState() + var ok bool + select { + case _, ok = <-ShutdownRequestChan: + default: + } + D.Ln("shutdownrequestchan", ok) + if ok { + close(ShutdownRequestChan) + } +} + +// GoroutineDump returns a string with the current goroutine dump in order to show what's going on in case of timeout. +func GoroutineDump() string { + buf := make([]byte, 1<<18) + n := runtime.Stack(buf, true) + return string(buf[:n]) +} + +// RequestRestart sets the reset flag and requests a restart +func RequestRestart() { + Restart = true + D.Ln("requesting restart") + Request() +} + +// Requested returns true if an interrupt has been requested +func Requested() bool { + return requested.Load() +} diff --git a/pkg/interrupt/restart_js.go b/pkg/interrupt/restart_js.go new file mode 100644 index 0000000..2550a4a --- /dev/null +++ b/pkg/interrupt/restart_js.go @@ -0,0 +1,8 @@ +//go:build js || wasm + +package interrupt + +func doRestart() { + // Restart is not supported in WASM/JS environment + D.Ln("restart not supported in WASM/JS environment") +} diff --git a/pkg/interrupt/restart_unix.go b/pkg/interrupt/restart_unix.go new file mode 100644 index 0000000..4b013bc --- /dev/null +++ b/pkg/interrupt/restart_unix.go @@ -0,0 +1,23 @@ +//go:build !windows && !js && !wasm + +package interrupt + +import ( + "os" + "syscall" + + "github.com/kardianos/osext" +) + +func doRestart() { + file, e := osext.Executable() + if e != nil { + E.Ln(e) + return + } + D.Ln("restarting") + e = syscall.Exec(file, os.Args, os.Environ()) + if e != nil { + F.Ln(e) + } +} diff --git a/pkg/interrupt/restart_windows.go b/pkg/interrupt/restart_windows.go new file mode 100644 index 0000000..507a6eb --- /dev/null +++ b/pkg/interrupt/restart_windows.go @@ -0,0 +1,26 @@ +//go:build windows + +package interrupt + +import ( + "os" + "os/exec" + + "github.com/kardianos/osext" +) + +func doRestart() { + file, e := osext.Executable() + if e != nil { + E.Ln(e) + return + } + D.Ln("doing windows restart") + var s []string + s = append(s, file) + s = append(s, os.Args[1:]...) + cmd := exec.Command(s[0], s[1:]...) + D.Ln("windows restart done") + if e = cmd.Start(); E.Chk(e) { + } +} diff --git a/pkg/interrupt/sigterm.go b/pkg/interrupt/sigterm.go new file mode 100644 index 0000000..8a3ae4b --- /dev/null +++ b/pkg/interrupt/sigterm.go @@ -0,0 +1,14 @@ +//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris +// +build darwin dragonfly freebsd linux netbsd openbsd solaris + +package interrupt + +import ( + "os" + "syscall" +) + +func init() { + + signals = []os.Signal{os.Interrupt, syscall.SIGTERM} +} diff --git a/pkg/log/LICENSE b/pkg/log/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/pkg/log/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/pkg/log/logg.go b/pkg/log/logg.go new file mode 100644 index 0000000..69524e4 --- /dev/null +++ b/pkg/log/logg.go @@ -0,0 +1,575 @@ +package log + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "sort" + "strings" + "sync" + "time" + + "github.com/davecgh/go-spew/spew" + "github.com/gookit/color" + uberatomic "go.uber.org/atomic" +) + +const ( + _Off = iota + _Fatal + _Error + _Chek + _Warn + _Info + _Debug + _Trace +) + +type ( + // LevelPrinter defines a set of terminal printing primitives that output with + // extra data, time, log logLevelList, and code location + LevelPrinter struct { + // Ln prints lists of interfaces with spaces in between + Ln func(a ...interface{}) + // F prints like fmt.Println surrounded by log details + F func(format string, a ...interface{}) + // S prints a spew.Sdump for an interface slice + S func(a ...interface{}) + // C accepts a function so that the extra computation can be avoided if it is + // not being viewed + C func(closure func() string) + // Chk is a shortcut for printing if there is an error, or returning true + Chk func(e error) bool + } + logLevelList struct { + Off, Fatal, Error, Check, Warn, Info, Debug, Trace int32 + } + LevelSpec struct { + ID int32 + Name string + Colorizer func(format string, a ...interface{}) string + } + + // Entry is a log entry to be printed as json to the log file + Entry struct { + Time time.Time + Level string + Package string + CodeLocation string + Text string + } +) + +var ( + logger_started = time.Now() + App = " pod" + AppColorizer = color.White.Sprint + // sep is just a convenient shortcut for this very longwinded expression + sep = string(os.PathSeparator) + currentLevel = uberatomic.NewInt32(logLevels.Info) + // writer can be swapped out for any io.*writer* that you want to use instead of + // stdout. + writer io.Writer = os.Stderr + // allSubsystems stores all of the package subsystem names found in the current + // application + allSubsystems []string + // highlighted is a text that helps visually distinguish a log entry by category + highlighted = make(map[string]struct{}) + // logFilter specifies a set of packages that will not pr logs + logFilter = make(map[string]struct{}) + // mutexes to prevent concurrent map accesses + highlightMx, _logFilterMx sync.Mutex + // logLevels is a shorthand access that minimises possible Name collisions in the + // dot import + logLevels = logLevelList{ + Off: _Off, + Fatal: _Fatal, + Error: _Error, + Check: _Chek, + Warn: _Warn, + Info: _Info, + Debug: _Debug, + Trace: _Trace, + } + // LevelSpecs specifies the id, string name and color-printing function + LevelSpecs = []LevelSpec{ + {logLevels.Off, "off ", color.Bit24(0, 0, 0, false).Sprintf}, + {logLevels.Fatal, "fatal", color.Bit24(128, 0, 0, false).Sprintf}, + {logLevels.Error, "error", color.Bit24(255, 0, 0, false).Sprintf}, + {logLevels.Check, "check", color.Bit24(255, 255, 0, false).Sprintf}, + {logLevels.Warn, "warn ", color.Bit24(0, 255, 0, false).Sprintf}, + {logLevels.Info, "info ", color.Bit24(255, 255, 0, false).Sprintf}, + {logLevels.Debug, "debug", color.Bit24(0, 128, 255, false).Sprintf}, + {logLevels.Trace, "trace", color.Bit24(128, 0, 255, false).Sprintf}, + } + Levels = []string{ + Off, + Fatal, + Error, + Check, + Warn, + Info, + Debug, + Trace, + } + LogChanDisabled = uberatomic.NewBool(true) + LogChan chan Entry +) + +const ( + Off = "off" + Fatal = "fatal" + Error = "error" + Warn = "warn" + Info = "info" + Check = "check" + Debug = "debug" + Trace = "trace" +) + +// AddLogChan adds a channel that log entries are sent to +func AddLogChan() (ch chan Entry) { + LogChanDisabled.Store(false) + if LogChan != nil { + panic("warning warning") + } + // L.Writer.Write.Store( false + LogChan = make(chan Entry) + return LogChan +} + +// GetLogPrinterSet returns a set of LevelPrinter with their subsystem preloaded +func GetLogPrinterSet(subsystem string) (Fatal, Error, Warn, Info, Debug, Trace LevelPrinter) { + return _getOnePrinter(_Fatal, subsystem), + _getOnePrinter(_Error, subsystem), + _getOnePrinter(_Warn, subsystem), + _getOnePrinter(_Info, subsystem), + _getOnePrinter(_Debug, subsystem), + _getOnePrinter(_Trace, subsystem) +} + +func _getOnePrinter(level int32, subsystem string) LevelPrinter { + return LevelPrinter{ + Ln: _ln(level, subsystem), + F: _f(level, subsystem), + S: _s(level, subsystem), + C: _c(level, subsystem), + Chk: _chk(level, subsystem), + } +} + +// SetLogLevel sets the log level via a string, which can be truncated down to +// one character, similar to nmcli's argument processor, as the first letter is +// unique. This could be used with a linter to make larger command sets. +func SetLogLevel(l string) { + if l == "" { + l = "info" + } + // fmt.Fprintln(os.Stderr, "setting log level", l) + lvl := logLevels.Info + for i := range LevelSpecs { + if LevelSpecs[i].Name[:1] == l[:1] { + lvl = LevelSpecs[i].ID + } + } + currentLevel.Store(lvl) +} + +// SetLogWriter atomically changes the log io.Writer interface +func SetLogWriter(wr io.Writer) { + // w := unsafe.Pointer(writer) + // c := unsafe.Pointer(wr) + // atomic.SwapPointer(&w, c) + writer = wr +} + +func SetLogWriteToFile(path, appName string) (e error) { + // copy existing log file to dated log file as we will truncate it per + // session + path = filepath.Join(path, "log"+appName) + if _, e = os.Stat(path); e == nil { + var b []byte + b, e = ioutil.ReadFile(path) + if e == nil { + ioutil.WriteFile(path+fmt.Sprint(time.Now().Unix()), b, 0600) + } + } + var fileWriter *os.File + if fileWriter, e = os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, + 0600); e != nil { + fmt.Fprintln(os.Stderr, "unable to write log to", path, "error:", e) + return + } + mw := io.MultiWriter(os.Stderr, fileWriter) + fileWriter.Write([]byte("logging to file '" + path + "'\n")) + mw.Write([]byte("logging to file '" + path + "'\n")) + SetLogWriter(mw) + return +} + +// SortSubsystemsList sorts the list of subsystems, to keep the data read-only, +// call this function right at the top of the main, which runs after +// declarations and main/init. Really this is just here to alert the reader. +func SortSubsystemsList() { + sort.Strings(allSubsystems) + // fmt.Fprintln( + // os.Stderr, + // spew.Sdump(allSubsystems), + // spew.Sdump(highlighted), + // spew.Sdump(logFilter), + // ) +} + +// AddLoggerSubsystem adds a subsystem to the list of known subsystems and returns the +// string so it is nice and neat in the package logg.go file +func AddLoggerSubsystem(pathBase string) (subsystem string) { + // var split []string + var ok bool + var file string + _, file, _, ok = runtime.Caller(1) + if ok { + r := strings.Split(file, pathBase) + // fmt.Fprintln(os.Stderr, version.PathBase, r) + fromRoot := filepath.Base(file) + if len(r) > 1 { + fromRoot = r[1] + } + split := strings.Split(fromRoot, "/") + // fmt.Fprintln(os.Stderr, version.PathBase, "file", file, r, fromRoot, split) + subsystem = strings.Join(split[:len(split)-1], "/") + // fmt.Fprintln(os.Stderr, "adding subsystem", subsystem) + allSubsystems = append(allSubsystems, subsystem) + } + return +} + +// StoreHighlightedSubsystems sets the list of subsystems to highlight +func StoreHighlightedSubsystems(highlights []string) (found bool) { + highlightMx.Lock() + highlighted = make(map[string]struct{}, len(highlights)) + for i := range highlights { + highlighted[highlights[i]] = struct{}{} + } + highlightMx.Unlock() + return +} + +// LoadHighlightedSubsystems returns a copy of the map of highlighted subsystems +func LoadHighlightedSubsystems() (o []string) { + highlightMx.Lock() + o = make([]string, len(logFilter)) + var counter int + for i := range logFilter { + o[counter] = i + counter++ + } + highlightMx.Unlock() + sort.Strings(o) + return +} + +// StoreSubsystemFilter sets the list of subsystems to filter +func StoreSubsystemFilter(filter []string) { + _logFilterMx.Lock() + logFilter = make(map[string]struct{}, len(filter)) + for i := range filter { + logFilter[filter[i]] = struct{}{} + } + _logFilterMx.Unlock() +} + +// LoadSubsystemFilter returns a copy of the map of filtered subsystems +func LoadSubsystemFilter() (o []string) { + _logFilterMx.Lock() + o = make([]string, len(logFilter)) + var counter int + for i := range logFilter { + o[counter] = i + counter++ + } + _logFilterMx.Unlock() + sort.Strings(o) + return +} + +// _isHighlighted returns true if the subsystem is in the list to have attention +// getters added to them +func _isHighlighted(subsystem string) (found bool) { + highlightMx.Lock() + _, found = highlighted[subsystem] + highlightMx.Unlock() + return +} + +// AddHighlightedSubsystem adds a new subsystem Name to the highlighted list +func AddHighlightedSubsystem(hl string) struct{} { + highlightMx.Lock() + highlighted[hl] = struct{}{} + highlightMx.Unlock() + return struct{}{} +} + +// _isSubsystemFiltered returns true if the subsystem should not pr logs +func _isSubsystemFiltered(subsystem string) (found bool) { + _logFilterMx.Lock() + _, found = logFilter[subsystem] + _logFilterMx.Unlock() + return +} + +// AddFilteredSubsystem adds a new subsystem Name to the highlighted list +func AddFilteredSubsystem(hl string) struct{} { + _logFilterMx.Lock() + logFilter[hl] = struct{}{} + _logFilterMx.Unlock() + return struct{}{} +} + +func getTimeText(level int32) string { + // since := time.Now().Sub(logger_started).Round(time.Millisecond).String() + // diff := 12 - len(since) + // if diff > 0 { + // since = strings.Repeat(" ", diff) + since + " " + // } + return color.Bit24(99, 99, 99, false).Sprint(time.Now(). + Format(time.StampMilli)) +} + +func _ln(level int32, subsystem string) func(a ...interface{}) { + return func(a ...interface{}) { + if level <= currentLevel.Load() && !_isSubsystemFiltered(subsystem) { + printer := fmt.Sprintf + if _isHighlighted(subsystem) { + printer = color.Bold.Sprintf + } + fmt.Fprintf( + writer, + printer( + "%-58v%s%s%-6v %s\n", + getLoc(2, level, subsystem), + getTimeText(level), + color.Bit24(20, 20, 20, true). + Sprint(AppColorizer(" "+App)), + LevelSpecs[level].Colorizer( + color.Bit24(20, 20, 20, true). + Sprint(" "+LevelSpecs[level].Name+" "), + ), + AppColorizer(joinStrings(" ", a...)), + ), + ) + } + } +} + +func _f(level int32, subsystem string) func(format string, a ...interface{}) { + return func(format string, a ...interface{}) { + if level <= currentLevel.Load() && !_isSubsystemFiltered(subsystem) { + printer := fmt.Sprintf + if _isHighlighted(subsystem) { + printer = color.Bold.Sprintf + } + fmt.Fprintf( + writer, + printer( + "%-58v%s%s%-6v %s\n", + getLoc(2, level, subsystem), + getTimeText(level), + color.Bit24(20, 20, 20, true). + Sprint(AppColorizer(" "+App)), + LevelSpecs[level].Colorizer( + color.Bit24(20, 20, 20, true). + Sprint(" "+LevelSpecs[level].Name+" "), + ), + AppColorizer(fmt.Sprintf(format, a...)), + ), + ) + } + } +} + +func _s(level int32, subsystem string) func(a ...interface{}) { + return func(a ...interface{}) { + if level <= currentLevel.Load() && !_isSubsystemFiltered(subsystem) { + printer := fmt.Sprintf + if _isHighlighted(subsystem) { + printer = color.Bold.Sprintf + } + fmt.Fprintf( + writer, + printer( + "%-58v%s%s%s%s%s\n", + getLoc(2, level, subsystem), + getTimeText(level), + color.Bit24(20, 20, 20, true). + Sprint(AppColorizer(" "+App)), + LevelSpecs[level].Colorizer( + color.Bit24(20, 20, 20, true). + Sprint(" "+LevelSpecs[level].Name+" "), + ), + AppColorizer( + " spew:", + ), + fmt.Sprint( + color.Bit24(20, 20, 20, true).Sprint("\n\n"+spew.Sdump(a)), + "\n", + ), + ), + ) + } + } +} + +func _c(level int32, subsystem string) func(closure func() string) { + return func(closure func() string) { + if level <= currentLevel.Load() && !_isSubsystemFiltered(subsystem) { + printer := fmt.Sprintf + if _isHighlighted(subsystem) { + printer = color.Bold.Sprintf + } + fmt.Fprintf( + writer, + printer( + "%-58v%s%s%-6v %s\n", + getLoc(2, level, subsystem), + getTimeText(level), + color.Bit24(20, 20, 20, true). + Sprint(AppColorizer(" "+App)), + LevelSpecs[level].Colorizer( + color.Bit24(20, 20, 20, true). + Sprint(" "+LevelSpecs[level].Name+" "), + ), + AppColorizer(closure()), + ), + ) + } + } +} + +func _chk(level int32, subsystem string) func(e error) bool { + return func(e error) bool { + if level <= currentLevel.Load() && !_isSubsystemFiltered(subsystem) { + if e != nil { + printer := fmt.Sprintf + if _isHighlighted(subsystem) { + printer = color.Bold.Sprintf + } + fmt.Fprintf( + writer, + printer( + "%-58v%s%s%-6v %s\n", + getLoc(2, level, subsystem), + getTimeText(level), + color.Bit24(20, 20, 20, true). + Sprint(AppColorizer(" "+App)), + LevelSpecs[level].Colorizer( + color.Bit24(20, 20, 20, true). + Sprint(" "+LevelSpecs[level].Name+" "), + ), + LevelSpecs[level].Colorizer(joinStrings(" ", e.Error())), + ), + ) + return true + } + } + return false + } +} + +// joinStrings constructs a string from an slice of interface same as Println but +// without the terminal newline +func joinStrings(sep string, a ...interface{}) (o string) { + for i := range a { + o += fmt.Sprint(a[i]) + if i < len(a)-1 { + o += sep + } + } + return +} + +// getLoc calls runtime.Caller and formats as expected by source code editors +// for terminal hyperlinks +// +// Regular expressions and the substitution texts to make these clickable in +// Tilix and other RE hyperlink configurable terminal emulators: +// +// This matches the shortened paths generated in this command and printed at +// the very beginning of the line as this logger prints: +// +// ^((([\/a-zA-Z@0-9-_.]+/)+([a-zA-Z@0-9-_.]+)):([0-9]+)) +// +// goland --line $5 $GOPATH/src/github.com/p9c/matrjoska/$2 +// +// I have used a shell variable there but tilix doesn't expand them, +// so put your GOPATH in manually, and obviously change the repo subpath. +// +// Change the path to use with another repository's logging output ( +// someone with more time on their hands could probably come up with +// something, but frankly the custom links feature of Tilix has the absolute +// worst UX I have encountered since the 90s... +// Maybe in the future this library will be expanded with a tool that more +// intelligently sets the path, ie from CWD or other cleverness. +// +// This matches full paths anywhere on the commandline delimited by spaces: +// +// ([/](([\/a-zA-Z@0-9-_.]+/)+([a-zA-Z@0-9-_.]+)):([0-9]+)) +// +// goland --line $5 /$2 +// +// Adapt the invocation to open your preferred editor if it has the capability, +// the above is for Jetbrains Goland +func getLoc(skip int, level int32, subsystem string) (output string) { + _, file, line, _ := runtime.Caller(skip) + defer func() { + if r := recover(); r != nil { + fmt.Fprintln(os.Stderr, "getloc panic on subsystem", subsystem, file) + } + }() + split := strings.Split(file, subsystem) + if len(split) < 2 { + output = fmt.Sprint( + color.White.Sprint(subsystem), + color.Gray.Sprint( + file, ":", line, + ), + ) + } else { + output = fmt.Sprint( + color.White.Sprint(subsystem), + color.Gray.Sprint( + split[1], ":", line, + ), + ) + } + return +} + +// DirectionString is a helper function that returns a string that represents the direction of a connection (inbound or outbound). +func DirectionString(inbound bool) string { + if inbound { + return "inbound" + } + return "outbound" +} + +func PickNoun(n int, singular, plural string) string { + if n == 1 { + return singular + } + return plural +} + +func FileExists(filePath string) bool { + _, e := os.Stat(filePath) + return e == nil +} + +func Caller(comment string, skip int) string { + _, file, line, _ := runtime.Caller(skip + 1) + o := fmt.Sprintf("%s: %s:%d", comment, file, line) + // L.Debug(o) + return o +} diff --git a/pkg/log/readme.md b/pkg/log/readme.md new file mode 100644 index 0000000..2c585ce --- /dev/null +++ b/pkg/log/readme.md @@ -0,0 +1,23 @@ +# logg + +This is a very simple, but practical library for +logging in applications. Currenty in process of +superseding the logi library, once the pipe logger +is converted, as it is integrated into the block +sync progress logger. + +To use it, create a file in a package you want +to add the logger to with the name `log.go`, and then: + +```bash +go run ./pkg/logg/deploy/. +``` + +from the root of the github.com/p9c/p9 repository +and it replicates its template with the altered +package name to match the folder name - so you need +to adhere to the rule that package names and the folder +name are the same. + +The library includes functions to toggle the filtering, +highlight and filtering sets while it is running. \ No newline at end of file diff --git a/pkg/opts/LICENSE b/pkg/opts/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/pkg/opts/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/pkg/opts/README.md b/pkg/opts/README.md new file mode 100644 index 0000000..a657a56 --- /dev/null +++ b/pkg/opts/README.md @@ -0,0 +1,7 @@ +# opts +Types for use with a generator for easy to use CLI and environment variables +reading and concurrent safe configuration system for applications. + +With the generator and these types you can maintain *one* specification for +your application's configuration system and update with one click or go +generate command. diff --git a/pkg/opts/binary/binary.go b/pkg/opts/binary/binary.go new file mode 100644 index 0000000..cfca41e --- /dev/null +++ b/pkg/opts/binary/binary.go @@ -0,0 +1,154 @@ +package binary + +import ( + "encoding/json" + "fmt" + "strings" + + uberatomic "go.uber.org/atomic" + + "git.mleku.dev/mleku/prevara/pkg/opts/meta" + "git.mleku.dev/mleku/prevara/pkg/opts/opt" +) + +// Opt stores an boolean configuration value +type Opt struct { + meta.Data + hook []Hook + value *uberatomic.Bool + Def bool +} + +type Hook func(b bool) error + +// New creates a new Opt with default values set +func New(m meta.Data, def bool, hook ...Hook) *Opt { + return &Opt{value: uberatomic.NewBool(def), Data: m, Def: def, hook: hook} +} + +// SetName sets the name for the generator +func (x *Opt) SetName(name string) { + x.Data.Option = strings.ToLower(name) + x.Data.Name = name +} + +// Type returns the receiver wrapped in an interface for identifying its type +func (x *Opt) Type() interface{} { + return x +} + +// GetMetadata returns the metadata of the opt type +func (x *Opt) GetMetadata() *meta.Data { + return &x.Data +} + +// ReadInput sets the value from a string. +// The value can be right up against the keyword or separated by a '='. +func (x *Opt) ReadInput(input string) (o opt.Option, e error) { + // if the input is empty, the user intends the opposite of the default + if input == "" { + x.value.Store(!x.Def) + return + } + if strings.HasPrefix(input, "=") { + // the following removes leading and trailing characters + input = strings.Join(strings.Split(input, "=")[1:], "=") + } + input = strings.ToLower(input) + switch input { + case "t", "true", "+": + e = x.Set(true) + case "f", "false", "-": + e = x.Set(false) + default: + e = fmt.Errorf("input on opt %s: '%s' is not valid for a boolean flag", x.Name(), input) + } + return +} + +// LoadInput sets the value from a string (this is the same as the above but differs for Strings) +func (x *Opt) LoadInput(input string) (o opt.Option, e error) { + return x.ReadInput(input) +} + +// Name returns the name of the opt +func (x *Opt) Name() string { + return x.Data.Option +} + +// AddHooks appends callback hooks to be run when the value is changed +func (x *Opt) AddHooks(hook ...Hook) { + x.hook = append(x.hook, hook...) +} + +// SetHooks sets a new slice of hooks +func (x *Opt) SetHooks(hook ...Hook) { + x.hook = hook +} + +// True returns whether the value is set to true (it returns the value) +func (x *Opt) True() bool { + return x.value.Load() +} + +// False returns whether the value is false (it returns the inverse of the value) +func (x *Opt) False() bool { + return !x.value.Load() +} + +// Flip changes the value to its opposite +func (x *Opt) Flip() { + I.Ln("flipping", x.Name(), "to", !x.value.Load()) + x.value.Toggle() +} + +func (x *Opt) runHooks(b bool) (e error) { + for i := range x.hook { + if e = x.hook[i](b); E.Chk(e) { + break + } + } + return +} + +// Set changes the value currently stored +func (x *Opt) Set(b bool) (e error) { + if e = x.runHooks(b); E.Chk(e) { + I.Ln("setting", x.Name(), "to", b) + x.value.Store(b) + } + return +} + +// String returns a string form of the value +func (x *Opt) String() string { + return fmt.Sprint(x.Data.Option, ": ", x.True()) +} + +// T sets the value to true +func (x *Opt) T() *Opt { + x.value.Store(true) + return x +} + +// F sets the value to false +func (x *Opt) F() *Opt { + x.value.Store(false) + return x +} + +// MarshalJSON returns the json representation of a Opt +func (x *Opt) MarshalJSON() (b []byte, e error) { + v := x.value.Load() + return json.Marshal(&v) +} + +// UnmarshalJSON decodes a JSON representation of a Opt +func (x *Opt) UnmarshalJSON(data []byte) (e error) { + v := x.value.Load() + if e = json.Unmarshal(data, &v); E.Chk(e) { + return + } + e = x.Set(v) + return +} diff --git a/pkg/opts/binary/log.go b/pkg/opts/binary/log.go new file mode 100644 index 0000000..2e0364f --- /dev/null +++ b/pkg/opts/binary/log.go @@ -0,0 +1,44 @@ +package binary + +import ( + "git.mleku.dev/mleku/prevara/pkg/log" + + "git.mleku.dev/mleku/prevara/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/opts/cmds/commands.go b/pkg/opts/cmds/commands.go new file mode 100644 index 0000000..7ee6ed2 --- /dev/null +++ b/pkg/opts/cmds/commands.go @@ -0,0 +1,106 @@ +package cmds + +// Commands are a slice of Command entries +type Commands []Command + +// Command is a specification for a command and can include any number of subcommands +type Command struct { + Name string + Title string + Description string + Entrypoint func(c interface{}) error + Commands Commands + Colorizer func(a ...interface{}) string + AppText string + Parent *Command +} + +func (c Commands) PopulateParents(parent *Command) { + if parent != nil { + T.Ln("backlinking children of", parent.Name) + } + for i := range c { + c[i].Parent = parent + c[i].Commands.PopulateParents(&c[i]) + } +} + +// GetAllCommands returns all of the available command names +func (c Commands) GetAllCommands() (o []string) { + c.ForEach(func(cm Command) bool { + o = append(o, cm.Name) + o = append(o, cm.Commands.GetAllCommands()...) + return true + }, 0, 0, + ) + return +} + +var tabs = "\t\t\t\t\t" + +// Find the Command you are looking for. Note that the namespace is assumed to be flat, no duplicated names on different +// levels, as it returns on the first one it finds, which goes depth-first recursive +func (c Commands) Find( + name string, hereDepth, hereDist int, skipFirst bool, +) (found bool, depth, dist int, cm *Command, e error) { + if c == nil { + dist = hereDist + depth = hereDepth + return + } + if hereDist == 0 { + D.Ln("searching for command:", name) + } + depth = hereDepth + 1 + T.Ln(tabs[:depth]+"->", depth) + dist = hereDist + for i := range c { + T.Ln(tabs[:depth]+"walking", c[i].Name, depth, dist) + dist++ + if c[i].Name == name { + if skipFirst { + continue + } + dist-- + T.Ln(tabs[:depth]+"found", name, "at depth", depth, "distance", dist) + found = true + cm = &c[i] + e = nil + return + } + if found, depth, dist, cm, e = c[i].Commands.Find(name, depth, dist, false); E.Chk(e) { + T.Ln(tabs[:depth]+"error", c[i].Name) + return + } + if found { + return + } + } + T.Ln(tabs[:hereDepth]+"<-", hereDepth) + if hereDepth == 0 { + D.Ln("search text", name, "not found") + } + depth-- + return +} + +func (c Commands) ForEach(fn func(Command) bool, hereDepth, hereDist int) (ret bool, depth, dist int, e error) { + if c == nil { + dist = hereDist + depth = hereDepth + return + } + depth = hereDepth + 1 + T.Ln(tabs[:depth]+"->", depth) + dist = hereDist + for i := range c { + T.Ln(tabs[:depth]+"walking", c[i].Name, depth, dist) + if !fn(c[i]) { + // if the closure returns false break out of the loop + return + } + } + T.Ln(tabs[:hereDepth]+"<-", hereDepth) + depth-- + return +} diff --git a/pkg/opts/cmds/commands_test.go b/pkg/opts/cmds/commands_test.go new file mode 100644 index 0000000..a9668c3 --- /dev/null +++ b/pkg/opts/cmds/commands_test.go @@ -0,0 +1,60 @@ +package cmds + +import ( + "testing" +) + +func TestCommands_GetAllCommands(t *testing.T) { + cm := GetCommands() + I.S(cm.GetAllCommands()) +} + +// GetCommands returns available subcommands in Parallelcoin Pod +func GetCommands() (c Commands) { + c = Commands{ + {Name: "gui", Title: "ParallelCoin GUI Wallet/Miner/Explorer", + Entrypoint: func(c interface{}) error { return nil }, + }, + {Name: "version", Title: "print version and exit", + Entrypoint: func(c interface{}) error { return nil }, + }, + {Name: "ctl", Title: "command line wallet and chain RPC client", + Entrypoint: func(c interface{}) error { return nil }, + }, + {Name: "node", Title: "ParallelCoin blockchain node", + Entrypoint: func(c interface{}) error { return nil }, + Commands: []Command{ + {Name: "dropaddrindex", Title: "drop the address database index", + Entrypoint: func(c interface{}) error { return nil }, + }, + {Name: "droptxindex", Title: "drop the transaction database index", + Entrypoint: func(c interface{}) error { return nil }, + }, + {Name: "dropcfindex", Title: "drop the cfilter database index", + Entrypoint: func(c interface{}) error { return nil }, + }, + {Name: "dropindexes", Title: "drop all of the indexes", + Entrypoint: func(c interface{}) error { return nil }, + }, + {Name: "resetchain", Title: "deletes the current blockchain cache to force redownload", + Entrypoint: func(c interface{}) error { return nil }, + }, + }, + }, + {Name: "wallet", Title: "run the wallet server (requires a chain node to function)", + Entrypoint: func(c interface{}) error { return nil }, + Commands: []Command{ + {Name: "drophistory", Title: "reset the wallet transaction history", + Entrypoint: func(c interface{}) error { return nil }, + }, + }, + }, + {Name: "kopach", Title: "standalone multicast miner for easy mining farm deployment", + Entrypoint: func(c interface{}) error { return nil }, + }, + {Name: "worker", Title: "single thread worker process, normally started by kopach", + Entrypoint: func(c interface{}) error { return nil }, + }, + } + return +} diff --git a/pkg/opts/cmds/log.go b/pkg/opts/cmds/log.go new file mode 100644 index 0000000..218d963 --- /dev/null +++ b/pkg/opts/cmds/log.go @@ -0,0 +1,44 @@ +package cmds + +import ( + "git.mleku.dev/mleku/prevara/pkg/log" + + "git.mleku.dev/mleku/prevara/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/opts/duration/duration.go b/pkg/opts/duration/duration.go new file mode 100644 index 0000000..f7a6b79 --- /dev/null +++ b/pkg/opts/duration/duration.go @@ -0,0 +1,136 @@ +package duration + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + uberatomic "go.uber.org/atomic" + + "git.mleku.dev/mleku/prevara/pkg/opts/meta" + "git.mleku.dev/mleku/prevara/pkg/opts/opt" + "git.mleku.dev/mleku/prevara/pkg/opts/sanitizers" +) + +// Opt stores an time.Duration configuration value +type Opt struct { + meta.Data + hook []Hook + clamp func(input time.Duration) (result time.Duration) + Min, Max time.Duration + Value *uberatomic.Duration + Def time.Duration +} + +type Hook func(d time.Duration) error + +// New creates a new Opt with a given default value set +func New(m meta.Data, def time.Duration, min, max time.Duration, hook ...Hook) *Opt { + return &Opt{ + Value: uberatomic.NewDuration(def), + Data: m, + Def: def, + Min: min, + Max: max, + hook: hook, + clamp: sanitizers.ClampDuration(min, max), + } +} + +// SetName sets the name for the generator +func (x *Opt) SetName(name string) { + x.Data.Option = strings.ToLower(name) + x.Data.Name = name +} + +// Type returns the receiver wrapped in an interface for identifying its type +func (x *Opt) Type() interface{} { + return x +} + +// GetMetadata returns the metadata of the opt type +func (x *Opt) GetMetadata() *meta.Data { + return &x.Data +} + +// ReadInput sets the value from a string +func (x *Opt) ReadInput(input string) (o opt.Option, e error) { + if input == "" { + e = fmt.Errorf("duration opt %s %v may not be empty", x.Name(), x.Data.Aliases) + return + } + if strings.HasPrefix(input, "=") { + // the following removes leading and trailing '=' + input = strings.Join(strings.Split(input, "=")[1:], "=") + } + var v time.Duration + if v, e = time.ParseDuration(input); E.Chk(e) { + return + } + if e = x.Set(v); E.Chk(e) { + } + return +} + +// LoadInput sets the value from a string (this is the same as the above but differs for Strings) +func (x *Opt) LoadInput(input string) (o opt.Option, e error) { + return x.ReadInput(input) +} + +// Name returns the name of the opt +func (x *Opt) Name() string { + return x.Data.Option +} + +// AddHooks appends callback hooks to be run when the value is changed +func (x *Opt) AddHooks(hook ...Hook) { + x.hook = append(x.hook, hook...) +} + +// SetHooks sets a new slice of hooks +func (x *Opt) SetHooks(hook ...Hook) { + x.hook = hook +} + +// V returns the value stored +func (x *Opt) V() time.Duration { + return x.Value.Load() +} + +func (x *Opt) runHooks(d time.Duration) (e error) { + for i := range x.hook { + if e = x.hook[i](d); E.Chk(e) { + break + } + } + return +} + +// Set the value stored +func (x *Opt) Set(d time.Duration) (e error) { + d = x.clamp(d) + if e = x.runHooks(d); !E.Chk(e) { + x.Value.Store(d) + } + return +} + +// String returns a string representation of the value +func (x *Opt) String() string { + return fmt.Sprintf("%s: %v", x.Data.Option, x.V()) +} + +// MarshalJSON returns the json representation +func (x *Opt) MarshalJSON() (b []byte, e error) { + v := x.Value.Load() + return json.Marshal(&v) +} + +// UnmarshalJSON decodes a JSON representation +func (x *Opt) UnmarshalJSON(data []byte) (e error) { + v := x.Value.Load() + e = json.Unmarshal(data, &v) + e = x.Set(v) + return +} diff --git a/pkg/opts/duration/log.go b/pkg/opts/duration/log.go new file mode 100644 index 0000000..eb44087 --- /dev/null +++ b/pkg/opts/duration/log.go @@ -0,0 +1,44 @@ +package duration + +import ( + "git.mleku.dev/mleku/prevara/pkg/log" + + "git.mleku.dev/mleku/prevara/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/opts/float/float.go b/pkg/opts/float/float.go new file mode 100644 index 0000000..4f50251 --- /dev/null +++ b/pkg/opts/float/float.go @@ -0,0 +1,136 @@ +package float + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + "git.mleku.dev/mleku/prevara/pkg/opts/meta" + "git.mleku.dev/mleku/prevara/pkg/opts/opt" + "git.mleku.dev/mleku/prevara/pkg/opts/sanitizers" + + uberatomic "go.uber.org/atomic" +) + +// Opt stores an float64 configuration value +type Opt struct { + meta.Data + hook []Hook + Min, Max float64 + clamp func(input float64) (result float64) + Value *uberatomic.Float64 + Def float64 +} + +type Hook func(f float64) error + +// New returns a new Opt value set to a default value +func New(m meta.Data, def float64, min, max float64, hook ...Hook) *Opt { + return &Opt{ + Value: uberatomic.NewFloat64(def), + Data: m, + Def: def, + Min: min, + Max: max, + hook: hook, + clamp: sanitizers.ClampFloat(min, max), + } +} + +// SetName sets the name for the generator +func (x *Opt) SetName(name string) { + x.Data.Option = strings.ToLower(name) + x.Data.Name = name +} + +// Type returns the receiver wrapped in an interface for identifying its type +func (x *Opt) Type() interface{} { + return x +} + +// GetMetadata returns the metadata of the opt type +func (x *Opt) GetMetadata() *meta.Data { + return &x.Data +} + +// ReadInput sets the value from a string +func (x *Opt) ReadInput(input string) (o opt.Option, e error) { + if input == "" { + e = fmt.Errorf("floating point number opt %s %v may not be empty", x.Name(), x.Data.Aliases) + return + } + if strings.HasPrefix(input, "=") { + // the following removes leading and trailing '=' + input = strings.Join(strings.Split(input, "=")[1:], "=") + } + var v float64 + if v, e = strconv.ParseFloat(input, 64); E.Chk(e) { + return + } + if e = x.Set(v); E.Chk(e) { + } + return x, e +} + +// LoadInput sets the value from a string (this is the same as the above but differs for Strings) +func (x *Opt) LoadInput(input string) (o opt.Option, e error) { + return x.ReadInput(input) +} + +// Name returns the name of the opt +func (x *Opt) Name() string { + return x.Data.Option +} + +// AddHooks appends callback hooks to be run when the value is changed +func (x *Opt) AddHooks(hook ...Hook) { + x.hook = append(x.hook, hook...) +} + +// SetHooks sets a new slice of hooks +func (x *Opt) SetHooks(hook ...Hook) { + x.hook = hook +} + +// V returns the value stored +func (x *Opt) V() float64 { + return x.Value.Load() +} + +func (x *Opt) runHooks(f float64) (e error) { + for i := range x.hook { + if e = x.hook[i](f); E.Chk(e) { + break + } + } + return +} + +// Set the value stored +func (x *Opt) Set(f float64) (e error) { + f = x.clamp(f) + if e = x.runHooks(f); !E.Chk(e) { + x.Value.Store(f) + } + return +} + +// String returns a string representation of the value +func (x *Opt) String() string { + return fmt.Sprintf("%s: %0.8f", x.Data.Option, x.V()) +} + +// MarshalJSON returns the json representation of +func (x *Opt) MarshalJSON() (b []byte, e error) { + v := x.Value.Load() + return json.Marshal(&v) +} + +// UnmarshalJSON decodes a JSON representation of +func (x *Opt) UnmarshalJSON(data []byte) (e error) { + v := x.Value.Load() + e = json.Unmarshal(data, &v) + e = x.Set(v) + return +} diff --git a/pkg/opts/float/log.go b/pkg/opts/float/log.go new file mode 100644 index 0000000..c97093c --- /dev/null +++ b/pkg/opts/float/log.go @@ -0,0 +1,44 @@ +package float + +import ( + "git.mleku.dev/mleku/prevara/pkg/log" + + "git.mleku.dev/mleku/prevara/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/opts/integer/int.go b/pkg/opts/integer/int.go new file mode 100644 index 0000000..6f04b1c --- /dev/null +++ b/pkg/opts/integer/int.go @@ -0,0 +1,136 @@ +package integer + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + uberatomic "go.uber.org/atomic" + + "git.mleku.dev/mleku/prevara/pkg/opts/meta" + "git.mleku.dev/mleku/prevara/pkg/opts/opt" + "git.mleku.dev/mleku/prevara/pkg/opts/sanitizers" +) + +// Opt stores an int configuration value +type Opt struct { + meta.Data + hook []Hook + Min, Max int + clamp func(input int) (result int) + Value *uberatomic.Int64 + Def int64 +} + +type Hook func(i int) error + +// New creates a new Opt with a given default value +func New(m meta.Data, def int64, min, max int, hook ...Hook) *Opt { + return &Opt{ + Value: uberatomic.NewInt64(def), + Data: m, + Def: def, + Min: min, + Max: max, + hook: hook, + clamp: sanitizers.ClampInt(min, max), + } +} + +// SetName sets the name for the generator +func (x *Opt) SetName(name string) { + x.Data.Option = strings.ToLower(name) + x.Data.Name = name +} + +// Type returns the receiver wrapped in an interface for identifying its type +func (x *Opt) Type() interface{} { + return x +} + +// GetMetadata returns the metadata of the opt type +func (x *Opt) GetMetadata() *meta.Data { + return &x.Data +} + +// ReadInput sets the value from a string +func (x *Opt) ReadInput(input string) (o opt.Option, e error) { + if input == "" { + e = fmt.Errorf("integer number opt %s %v may not be empty", x.Name(), x.Data.Aliases) + return + } + if strings.HasPrefix(input, "=") { + // the following removes leading and trailing '=' + input = strings.Join(strings.Split(input, "=")[1:], "=") + } + var v int64 + if v, e = strconv.ParseInt(input, 10, 64); E.Chk(e) { + return + } + if e = x.Set(int(v)); E.Chk(e) { + } + return x, e +} + +// LoadInput sets the value from a string (this is the same as the above but differs for Strings) +func (x *Opt) LoadInput(input string) (o opt.Option, e error) { + return x.ReadInput(input) +} + +// Name returns the name of the opt +func (x *Opt) Name() string { + return x.Data.Option +} + +// AddHooks appends callback hooks to be run when the value is changed +func (x *Opt) AddHooks(hook ...Hook) { + x.hook = append(x.hook, hook...) +} + +// SetHooks sets a new slice of hooks +func (x *Opt) SetHooks(hook ...Hook) { + x.hook = hook +} + +// V returns the stored int +func (x *Opt) V() int { + return int(x.Value.Load()) +} + +func (x *Opt) runHooks(ii int) (e error) { + for i := range x.hook { + if e = x.hook[i](ii); E.Chk(e) { + break + } + } + return +} + +// Set the value stored +func (x *Opt) Set(i int) (e error) { + i = x.clamp(i) + if e = x.runHooks(i); !E.Chk(e) { + x.Value.Store(int64(i)) + } + return +} + +// String returns the string stored +func (x *Opt) String() string { + return fmt.Sprintf("%s: %d", x.Data.Option, x.V()) +} + +// MarshalJSON returns the json representation of +func (x *Opt) MarshalJSON() (b []byte, e error) { + v := x.Value.Load() + return json.Marshal(&v) +} + +// UnmarshalJSON decodes a JSON representation of +func (x *Opt) UnmarshalJSON(data []byte) (e error) { + v := x.Value.Load() + e = json.Unmarshal(data, &v) + e = x.Set(int(v)) + return +} diff --git a/pkg/opts/integer/log.go b/pkg/opts/integer/log.go new file mode 100644 index 0000000..a5d1e0e --- /dev/null +++ b/pkg/opts/integer/log.go @@ -0,0 +1,44 @@ +package integer + +import ( + "git.mleku.dev/mleku/prevara/pkg/log" + + "git.mleku.dev/mleku/prevara/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/opts/list/log.go b/pkg/opts/list/log.go new file mode 100644 index 0000000..b9acbba --- /dev/null +++ b/pkg/opts/list/log.go @@ -0,0 +1,44 @@ +package list + +import ( + "git.mleku.dev/mleku/prevara/pkg/log" + + "git.mleku.dev/mleku/prevara/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/opts/list/strings.go b/pkg/opts/list/strings.go new file mode 100644 index 0000000..4f9f725 --- /dev/null +++ b/pkg/opts/list/strings.go @@ -0,0 +1,166 @@ +package list + +import ( + "encoding/json" + "fmt" + "strings" + "sync/atomic" + + "git.mleku.dev/mleku/prevara/pkg/opts/normalize" + + "git.mleku.dev/mleku/prevara/pkg/opts/meta" + "git.mleku.dev/mleku/prevara/pkg/opts/opt" + "git.mleku.dev/mleku/prevara/pkg/opts/sanitizers" +) + +// Opt stores a string slice configuration value +type Opt struct { + meta.Data + hook []Hook + Value *atomic.Value + Def []string +} + +type Hook func(s []string) error + +// New creates a new Opt with default values set +func New(m meta.Data, def []string, hook ...Hook) *Opt { + as := &atomic.Value{} + as.Store(def) + return &Opt{Value: as, Data: m, Def: def, hook: hook} +} + +// SetName sets the name for the generator +func (x *Opt) SetName(name string) { + x.Data.Option = strings.ToLower(name) + x.Data.Name = name +} + +// Type returns the receiver wrapped in an interface for identifying its type +func (x *Opt) Type() interface{} { + return x +} + +// GetMetadata returns the metadata of the opt type +func (x *Opt) GetMetadata() *meta.Data { + return &x.Data +} + +// ReadInput adds the value from a string. For this opt this means appending to the list +func (x *Opt) ReadInput(input string) (o opt.Option, e error) { + if input == "" { + e = fmt.Errorf("string opt %s %v may not be empty", x.Name(), x.Data.Aliases) + return + } + if strings.HasPrefix(input, "=") { + input = strings.Join(strings.Split(input, "=")[1:], "=") + } + // if value has a comma in it, it's a list of items, so split them and append them + slice := x.S() + if strings.Contains(input, ",") { + split := strings.Split(input, ",") + for i := range split { + var cleaned string + if cleaned, e = sanitizers.StringType(x.Data.Type, split[i], x.Data.DefaultPort); E.Chk(e) { + return + } + if cleaned != "" { + I.Ln("setting value for", x.Data.Name, cleaned) + split[i] = cleaned + } + } + e = x.Set(append(slice, split...)) + } else { + var cleaned string + if cleaned, e = sanitizers.StringType(x.Data.Type, input, x.Data.DefaultPort); E.Chk(e) { + return + } + if cleaned != "" { + I.Ln("setting value for", x.Data.Name, cleaned) + input = cleaned + } + if e = x.Set(append(slice, input)); E.Chk(e) { + } + + } + // ensure there is no duplicates + e = x.Set(normalize.RemoveDuplicateAddresses(x.V())) + return x, e +} + +// LoadInput sets the value from a string. For this opt this replacing the list +func (x *Opt) LoadInput(input string) (o opt.Option, e error) { + old := x.V() + _ = x.Set([]string{}) + if o, e = x.ReadInput(input); E.Chk(e) { + // if input failed to parse, restore its prior state + _ = x.Set(old) + } + return +} + +// Name returns the name of the opt +func (x *Opt) Name() string { + return x.Data.Option +} + +// AddHooks appends callback hooks to be run when the value is changed +func (x *Opt) AddHooks(hook ...Hook) { + x.hook = append(x.hook, hook...) +} + +// SetHooks sets a new slice of hooks +func (x *Opt) SetHooks(hook ...Hook) { + x.hook = hook +} + +// V returns the stored value +func (x *Opt) V() []string { + return x.Value.Load().([]string) +} + +// Len returns the length of the slice of strings +func (x *Opt) Len() int { + return len(x.S()) +} + +func (x *Opt) runHooks(s []string) (e error) { + for i := range x.hook { + if e = x.hook[i](s); E.Chk(e) { + break + } + } + return +} + +// Set the slice of strings stored +func (x *Opt) Set(ss []string) (e error) { + if e = x.runHooks(ss); !E.Chk(e) { + x.Value.Store(ss) + } + return +} + +// S returns the value as a slice of string +func (x *Opt) S() []string { + return x.Value.Load().([]string) +} + +// String returns a string representation of the value +func (x *Opt) String() string { + return fmt.Sprint(x.Data.Option, ": ", x.S()) +} + +// MarshalJSON returns the json representation of +func (x *Opt) MarshalJSON() (b []byte, e error) { + xs := x.Value.Load().([]string) + return json.Marshal(xs) +} + +// UnmarshalJSON decodes a JSON representation of +func (x *Opt) UnmarshalJSON(data []byte) (e error) { + var v []string + e = json.Unmarshal(data, &v) + x.Value.Store(v) + return +} diff --git a/pkg/opts/meta/data.go b/pkg/opts/meta/data.go new file mode 100644 index 0000000..6ba45e1 --- /dev/null +++ b/pkg/opts/meta/data.go @@ -0,0 +1,24 @@ +package meta + +type ( + // Data is the information about the opt to be used by interface code and other presentations of the data + Data struct { + Option string + Aliases []string + Group string + Tags []string + Label string + Description string + Documentation string + Type string + Options []string + OmitEmpty bool + Name string + DefaultPort int + } +) + +func (m Data) GetAllOptionStrings() (opts []string) { + opts = append([]string{m.Option}, m.Aliases...) + return opts +} diff --git a/pkg/opts/meta/log.go b/pkg/opts/meta/log.go new file mode 100644 index 0000000..b0869ca --- /dev/null +++ b/pkg/opts/meta/log.go @@ -0,0 +1,44 @@ +package meta + +import ( + "git.mleku.dev/mleku/prevara/pkg/log" + + "git.mleku.dev/mleku/prevara/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/opts/normalize/log.go b/pkg/opts/normalize/log.go new file mode 100644 index 0000000..ba334b9 --- /dev/null +++ b/pkg/opts/normalize/log.go @@ -0,0 +1,44 @@ +package normalize + +import ( + "git.mleku.dev/mleku/prevara/pkg/log" + + "git.mleku.dev/mleku/prevara/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/opts/normalize/normalize.go b/pkg/opts/normalize/normalize.go new file mode 100644 index 0000000..3cf3954 --- /dev/null +++ b/pkg/opts/normalize/normalize.go @@ -0,0 +1,44 @@ +package normalize + +import ( + "net" +) + +// address returns addr with the passed default port appended if there is not +// already a port specified. +func address(addr, defaultPort string) string { + var e error + if _, _, e = net.SplitHostPort(addr); E.Chk(e) { + return net.JoinHostPort(addr, defaultPort) + } + return addr +} + +// Addresses returns a new slice with all the passed peer addresses normalized +// with the given default port, and all duplicates removed. +func Addresses(addrs []string, defaultPort string) []string { + for i := range addrs { + addrs[i] = address(addrs[i], defaultPort) + } + return RemoveDuplicateAddresses(addrs) +} + +// RemoveDuplicateAddresses returns a new slice with all duplicate entries in +// addrs removed. +func RemoveDuplicateAddresses(addrs []string) (result []string) { + result = make([]string, 0, len(addrs)) + seen := map[string]struct{}{} + for _, val := range addrs { + if _, ok := seen[val]; !ok { + result = append(result, val) + seen[val] = struct{}{} + } + } + return result +} + +// StringSliceAddresses normalizes a slice of addresses +func StringSliceAddresses(a []string, port string) { + variable := a + a = Addresses(variable, port) +} diff --git a/pkg/opts/opt/log.go b/pkg/opts/opt/log.go new file mode 100644 index 0000000..fe2ea85 --- /dev/null +++ b/pkg/opts/opt/log.go @@ -0,0 +1,44 @@ +package opt + +import ( + "git.mleku.dev/mleku/prevara/pkg/log" + + "git.mleku.dev/mleku/prevara/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/opts/opt/option.go b/pkg/opts/opt/option.go new file mode 100644 index 0000000..37667f3 --- /dev/null +++ b/pkg/opts/opt/option.go @@ -0,0 +1,21 @@ +package opt + +import ( + "git.mleku.dev/mleku/prevara/pkg/opts/meta" +) + +type ( + // Option is an interface to simplify concurrent-safe access to a variety of types of configuration item + Option interface { + LoadInput(input string) (o Option, e error) + ReadInput(input string) (o Option, e error) + GetMetadata() *meta.Data + Name() string + String() string + MarshalJSON() (b []byte, e error) + UnmarshalJSON(data []byte) (e error) + GetAllOptionStrings() []string + Type() interface{} + SetName(string) + } +) diff --git a/pkg/opts/sanitizers/log.go b/pkg/opts/sanitizers/log.go new file mode 100644 index 0000000..700df6f --- /dev/null +++ b/pkg/opts/sanitizers/log.go @@ -0,0 +1,44 @@ +package sanitizers + +import ( + "git.mleku.dev/mleku/prevara/pkg/log" + + "git.mleku.dev/mleku/prevara/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/opts/sanitizers/numbers.go b/pkg/opts/sanitizers/numbers.go new file mode 100644 index 0000000..c65d411 --- /dev/null +++ b/pkg/opts/sanitizers/numbers.go @@ -0,0 +1,41 @@ +package sanitizers + +import ( + "time" +) + +func ClampInt(min, max int) func(input int) (result int) { + return func(input int) (result int) { + if input > max { + return max + } + if input < min { + return min + } + return input + } +} + +func ClampFloat(min, max float64) func(input float64) (result float64) { + return func(input float64) (result float64) { + if input > max { + return max + } + if input < min { + return min + } + return input + } +} + +func ClampDuration(min, max time.Duration) func(input time.Duration) (result time.Duration) { + return func(input time.Duration) (result time.Duration) { + if input > max { + return max + } + if input < min { + return min + } + return input + } +} diff --git a/pkg/opts/sanitizers/strings.go b/pkg/opts/sanitizers/strings.go new file mode 100644 index 0000000..12f8b31 --- /dev/null +++ b/pkg/opts/sanitizers/strings.go @@ -0,0 +1,72 @@ +package sanitizers + +import ( + "fmt" + "net" + "os" + "os/user" + "path/filepath" + "strings" +) + +const ( + NetAddress = "netaddress" + Password = "password" + FilePath = "filepath" + Directory = "directory" +) + +func StringType(typ, input string, defaultPort int) (cleaned string, e error) { + switch typ { + case NetAddress: + var h, p string + if h, p, e = net.SplitHostPort(input); E.Chk(e) { + e = fmt.Errorf("address value '%s' not a valid address", input) + return + } + if p == "" { + cleaned = net.JoinHostPort(h, fmt.Sprint(defaultPort)) + } + case Password: + // password type is mainly here for the input method of the app using this config library + case FilePath: + if strings.HasPrefix(input, "~") { + var homeDir string + var usr *user.User + var e error + if usr, e = user.Current(); e == nil { + homeDir = usr.HomeDir + } + // Fall back to standard HOME environment variable that works for most POSIX OSes if the directory from the Go + // standard lib failed. + if e != nil || homeDir == "" { + homeDir = os.Getenv("HOME") + } + + input = strings.Replace(input, "~", homeDir, 1) + } + if cleaned, e = filepath.Abs(filepath.Clean(input)); E.Chk(e) { + } + case Directory: + if strings.HasPrefix(input, "~") { + var homeDir string + var usr *user.User + var e error + if usr, e = user.Current(); e == nil { + homeDir = usr.HomeDir + } + // Fall back to standard HOME environment variable that works for most POSIX OSes if the directory from the Go + // standard lib failed. + if e != nil || homeDir == "" { + homeDir = os.Getenv("HOME") + } + + input = strings.Replace(input, "~", homeDir, 1) + } + if cleaned, e = filepath.Abs(filepath.Clean(input)); E.Chk(e) { + } + default: + cleaned = input + } + return +} diff --git a/pkg/opts/text/log.go b/pkg/opts/text/log.go new file mode 100644 index 0000000..89b2f1b --- /dev/null +++ b/pkg/opts/text/log.go @@ -0,0 +1,44 @@ +package text + +import ( + "git.mleku.dev/mleku/prevara/pkg/log" + + "git.mleku.dev/mleku/prevara/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = logg.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = logg.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/opts/text/string.go b/pkg/opts/text/string.go new file mode 100644 index 0000000..4e29034 --- /dev/null +++ b/pkg/opts/text/string.go @@ -0,0 +1,187 @@ +package text + +import ( + "encoding/json" + "fmt" + "strings" + "sync/atomic" + + "git.mleku.dev/mleku/prevara/pkg/opts/meta" + "git.mleku.dev/mleku/prevara/pkg/opts/opt" + "git.mleku.dev/mleku/prevara/pkg/opts/sanitizers" +) + +// Opt stores a string configuration value +type Opt struct { + meta.Data + hook []Hook + Value *atomic.Value + Def string +} + +type Hook func(s []byte) error + +// New creates a new Opt with a given default value set +func New(m meta.Data, def string, hook ...Hook) *Opt { + v := &atomic.Value{} + v.Store([]byte(def)) + return &Opt{Value: v, Data: m, Def: def, hook: hook} +} + +// SetName sets the name for the generator +func (x *Opt) SetName(name string) { + x.Data.Option = strings.ToLower(name) + x.Data.Name = name +} + +// Type returns the receiver wrapped in an interface for identifying its type +func (x *Opt) Type() interface{} { + return x +} + +// GetMetadata returns the metadata of the opt type +func (x *Opt) GetMetadata() *meta.Data { + return &x.Data +} + +// ReadInput sets the value from a string +func (x *Opt) ReadInput(input string) (o opt.Option, e error) { + if input == "" { + e = fmt.Errorf("string opt %s %v may not be empty", x.Name(), x.Data.Aliases) + return + } + if strings.HasPrefix(input, "=") { + // the following removes leading `=` and retains any following instances of `=` + input = strings.Join(strings.Split(input, "=")[1:], "=") + } + if x.Data.Options != nil { + var matched string + e = fmt.Errorf("option value not found '%s'", input) + for _, i := range x.Data.Options { + op := i + if len(i) >= len(input) { + op = i[:len(input)] + } + if input == op { + if e == nil { + return x, fmt.Errorf("ambiguous short option value '%s' matches multiple options: %s, %s", input, matched, i) + } + matched = i + e = nil + } else { + continue + } + } + if E.Chk(e) { + return + } + input = matched + } else { + var cleaned string + if cleaned, e = sanitizers.StringType(x.Data.Type, input, x.Data.DefaultPort); E.Chk(e) { + return + } + if cleaned != "" { + I.Ln("setting value for", x.Data.Name, cleaned) + input = cleaned + } + } + e = x.Set(input) + return x, e +} + +// LoadInput sets the value from a string +func (x *Opt) LoadInput(input string) (o opt.Option, e error) { + return x.ReadInput(input) +} + +// Name returns the name of the opt +func (x *Opt) Name() string { + return x.Data.Option +} + +// AddHooks appends callback hooks to be run when the value is changed +func (x *Opt) AddHooks(hook ...Hook) { + x.hook = append(x.hook, hook...) +} + +// SetHooks sets a new slice of hooks +func (x *Opt) SetHooks(hook ...Hook) { + x.hook = hook +} + +// V returns the stored string +func (x *Opt) V() string { + return string(x.Value.Load().([]byte)) +} + +// Empty returns true if the string is empty +func (x *Opt) Empty() bool { + return len(x.Value.Load().([]byte)) == 0 +} + +// Bytes returns the raw bytes in the underlying storage +// note that this returns a copy because anything done to the slice affects +// all accesses afterwards, thus there is also a zero function +// todo: make an option for the byte buffer to be MMU fenced to prevent +// +// elevated privilege processes from accessing this memory. +func (x *Opt) Bytes() []byte { + byt := x.Value.Load().([]byte) + o := make([]byte, len(byt)) + copy(o, byt) + return o +} + +// Zero the bytes +func (x *Opt) Zero() { + byt := x.Value.Load().([]byte) + for i := range byt { + byt[i] = 0 + } + x.Value.Store(byt) +} + +func (x *Opt) runHooks(s []byte) (e error) { + for i := range x.hook { + if e = x.hook[i](s); E.Chk(e) { + break + } + } + return +} + +// Set the value stored +func (x *Opt) Set(s string) (e error) { + if e = x.runHooks([]byte(s)); !E.Chk(e) { + x.Value.Store([]byte(s)) + } + return +} + +// SetBytes sets the string from bytes +func (x *Opt) SetBytes(s []byte) (e error) { + if e = x.runHooks(s); !E.Chk(e) { + x.Value.Store(s) + } + return +} + +// Opt returns a string representation of the value +func (x *Opt) String() string { + return fmt.Sprintf("%s: '%s'", x.Data.Option, x.V()) +} + +// MarshalJSON returns the json representation +func (x *Opt) MarshalJSON() (b []byte, e error) { + v := string(x.Value.Load().([]byte)) + return json.Marshal(&v) +} + +// UnmarshalJSON decodes a JSON representation +func (x *Opt) UnmarshalJSON(data []byte) (e error) { + v := x.Value.Load().([]byte) + e = json.Unmarshal(data, &v) + x.Value.Store(v) + return +} diff --git a/pkg/qu/LICENSE b/pkg/qu/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/pkg/qu/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/pkg/qu/README.md b/pkg/qu/README.md new file mode 100644 index 0000000..c961883 --- /dev/null +++ b/pkg/qu/README.md @@ -0,0 +1,8 @@ +# qu +### observable signal channels + +This is a wrapper around `chan struct{}` that forgives some common mistakes +like sending on closed channels and closing cllosed channels, as well as +printing logs about when channels are created, waited on, sent to and closed. + +This library makes debugging concurrent code a lot easier. IMHO. YMMV. diff --git a/pkg/qu/log.go b/pkg/qu/log.go new file mode 100644 index 0000000..753c12a --- /dev/null +++ b/pkg/qu/log.go @@ -0,0 +1,44 @@ +package qu + +import ( + "git.mleku.dev/mleku/prevara/pkg/log" + + "git.mleku.dev/mleku/prevara/version" +) + +var subsystem = log.AddLoggerSubsystem(version.PathBase) +var F, E, W, I, D, _T log.LevelPrinter = log.GetLogPrinterSet(subsystem) + +func init() { + // to filter out this package, uncomment the following + // var _ = log.AddFilteredSubsystem(subsystem) + + // to highlight this package, uncomment the following + // var _ = log.AddHighlightedSubsystem(subsystem) + + // these are here to test whether they are working + // F.Ln("F.Ln") + // E.Ln("E.Ln") + // W.Ln("W.Ln") + // I.Ln("I.Ln") + // D.Ln("D.Ln") + // F.Ln("T.Ln") + // F.F("%s", "F.F") + // E.F("%s", "E.F") + // W.F("%s", "W.F") + // I.F("%s", "I.F") + // D.F("%s", "D.F") + // T.F("%s", "T.F") + // F.C(func() string { return "F.C" }) + // E.C(func() string { return "E.C" }) + // W.C(func() string { return "W.C" }) + // I.C(func() string { return "I.C" }) + // D.C(func() string { return "D.C" }) + // T.C(func() string { return "T.C" }) + // F.C(func() string { return "F.C" }) + // E.Chk(errors.New("E.Chk")) + // W.Chk(errors.New("W.Chk")) + // I.Chk(errors.New("I.Chk")) + // D.Chk(errors.New("D.Chk")) + // T.Chk(errors.New("T.Chk")) +} diff --git a/pkg/qu/quit.go b/pkg/qu/quit.go new file mode 100644 index 0000000..eb0c729 --- /dev/null +++ b/pkg/qu/quit.go @@ -0,0 +1,178 @@ +package qu + +import ( + "strings" + "sync" + "time" + + "git.mleku.dev/mleku/prevara/pkg/log" + "go.uber.org/atomic" +) + +// C is your basic empty struct signalling channel +type C chan struct{} + +var ( + createdList []string + createdChannels []C + mx sync.Mutex + logEnabled = atomic.NewBool(false) +) + +// SetLogging switches on and off the channel logging +func SetLogging(on bool) { + logEnabled.Store(on) +} + +func l(a ...interface{}) { + if logEnabled.Load() { + D.Ln(a...) + } +} + +// T creates an unbuffered chan struct{} for trigger and quit signalling (momentary and breaker switches) +func T() C { + mx.Lock() + defer mx.Unlock() + msg := log.Caller("chan from", 1) + l("created", msg) + createdList = append(createdList, msg) + o := make(C) + createdChannels = append(createdChannels, o) + return o +} + +// Ts creates a buffered chan struct{} which is specifically intended for signalling without blocking, generally one is +// the size of buffer to be used, though there might be conceivable cases where the channel should accept more signals +// without blocking the caller +func Ts(n int) C { + mx.Lock() + defer mx.Unlock() + msg := log.Caller("buffered chan from", 1) + l("created", msg) + createdList = append(createdList, msg) + o := make(C, n) + createdChannels = append(createdChannels, o) + return o +} + +// Q closes the channel, which makes it emit a nil every time it is selected +func (c C) Q() { + l(func() (o string) { + loc := getLocForChan(c) + mx.Lock() + defer mx.Unlock() + if !testChanIsClosed(c) { + close(c) + return "closing chan from " + loc + log.Caller("\n"+strings.Repeat(" ", 48)+"from", 1) + } else { + return "from" + log.Caller("", 1) + "\n" + strings.Repeat(" ", 48) + + "channel " + loc + " was already closed" + } + }(), + ) +} + +// Signal sends struct{}{} on the channel which functions as a momentary switch, useful in pairs for stop/start +func (c C) Signal() { + l(func() (o string) { return "signalling " + getLocForChan(c) }()) + c <- struct{}{} +} + +// Wait should be placed with a `<-` in a select case in addition to the channel variable name +func (c C) Wait() <-chan struct{} { + l(func() (o string) { return "waiting on " + getLocForChan(c) + log.Caller("at", 1) }()) + return c +} + +// testChanIsClosed allows you to see whether the channel has been closed so you can avoid a panic by trying to close or +// signal on it +func testChanIsClosed(ch C) (o bool) { + if ch == nil { + return true + } + select { + case <-ch: + o = true + default: + } + return +} + +// getLocForChan finds which record connects to the channel in question +func getLocForChan(c C) (s string) { + s = "not found" + mx.Lock() + for i := range createdList { + if i >= len(createdChannels) { + break + } + if createdChannels[i] == c { + s = createdList[i] + } + } + mx.Unlock() + return +} + +// once a minute clean up the channel cache to remove closed channels no longer in use +func init() { + go func() { + for { + <-time.After(time.Minute) + D.Ln("cleaning up closed channels") + var c []C + var ll []string + mx.Lock() + for i := range createdChannels { + if i >= len(createdList) { + break + } + if testChanIsClosed(createdChannels[i]) { + } else { + c = append(c, createdChannels[i]) + ll = append(ll, createdList[i]) + } + } + createdChannels = c + createdList = ll + mx.Unlock() + } + }() +} + +// PrintChanState creates an output showing the current state of the channels being monitored +// This is a function for use by the programmer while debugging +func PrintChanState() { + mx.Lock() + for i := range createdChannels { + if i >= len(createdList) { + break + } + if testChanIsClosed(createdChannels[i]) { + _T.Ln(">>> closed", createdList[i]) + } else { + _T.Ln("<<< open", createdList[i]) + } + } + mx.Unlock() +} + +// GetOpenChanCount returns the number of qu channels that are still open +// todo: this needs to only apply to unbuffered type +func GetOpenChanCount() (o int) { + mx.Lock() + var c int + for i := range createdChannels { + if i >= len(createdChannels) { + break + } + if testChanIsClosed(createdChannels[i]) { + c++ + } else { + o++ + } + } + mx.Unlock() + return +} diff --git a/version/logversion.go b/version/logversion.go new file mode 100644 index 0000000..e8a6f04 --- /dev/null +++ b/version/logversion.go @@ -0,0 +1,16 @@ +package version + +import ( + "path/filepath" + "runtime" + + "git.mleku.dev/mleku/prevara/pkg/log" +) + +var F, E, W, I, D, T log.LevelPrinter + +func init() { + _, file, _, _ := runtime.Caller(0) + verPath := filepath.Dir(file) + "/" + F, E, W, I, D, T = log.GetLogPrinterSet(log.AddLoggerSubsystem(verPath)) +} diff --git a/version/version.go b/version/version.go new file mode 100644 index 0000000..23fb259 --- /dev/null +++ b/version/version.go @@ -0,0 +1,48 @@ +package version + +//go:generate go run ./update/. + +import ( + "fmt" +) + +var ( + + // URL is the git URL for the repository + URL = "github.com/p9c/p9" + // GitRef is the gitref, as in refs/heads/branchname + GitRef = "refs/heads/main" + // GitCommit is the commit hash of the current HEAD + GitCommit = "40a7d8327e70dca9576a7257ad320106d62ee72f" + // BuildTime stores the time when the current binary was built + BuildTime = "2021-05-03T14:08:30+02:00" + // Tag lists the Tag on the build, adding a + to the newest Tag if the commit is + // not that commit + Tag = "v0.0.2+" + // PathBase is the path base returned from runtime caller + PathBase = "/home/loki/src/github.com/p9c/p9/" + // Major is the major number from the tag + Major = 0 + // Minor is the minor number from the tag + Minor = 0 + // Patch is the patch version number from the tag + Patch = 2 + // Meta is the extra arbitrary string field from Semver spec + Meta = "" +) + +// Get returns a pretty printed version information string +func Get() string { + return fmt.Sprint( + "\nRepository Information\n"+ + "\tGit repository: "+URL+"\n", + "\tBranch: "+GitRef+"\n"+ + "\tCommit: "+GitCommit+"\n"+ + "\tBuilt: "+BuildTime+"\n"+ + "\tTag: "+Tag+"\n", + "\tMajor:", Major, "\n", + "\tMinor:", Minor, "\n", + "\tPatch:", Patch, "\n", + "\tMeta: ", Meta, "\n", + ) +}