moving proc inside

This commit is contained in:
David Vennik
2022-12-30 17:00:09 +00:00
parent f38c562d62
commit 449a263734
48 changed files with 4728 additions and 1 deletions

Submodule pkg/proc deleted from ee185b6d80

5
pkg/proc/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
/.idea/.gitignore
/.idea/modules.xml
/.idea/proc.iml
/.idea/vcs.xml
/.idea/codeStyles/codeStyleConfig.xml

24
pkg/proc/LICENSE Normal file
View File

@@ -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 <https://unlicense.org>

97
pkg/proc/README.md Normal file
View File

@@ -0,0 +1,97 @@
# proc
Process control, logging and child processes
## Badges
<insert badges here
## Description
Golang is a great language for concurrency, but sometimes you want parallelism,
and it's often simpler to design apps as little units that plug together via
RPC APIs. Services don't always have easy start/stop/reconfig/restart
controls, and sometimes they use their own custom configuration scheme that is
complicated to integrate with yours.
In addition, you may want to design your applications as neat little pieces, but
how to attach them together and orchestrate them starting up and coming down
cleanly, and not have to deal with several different ttys to get the logs.
Proc creates a simple framework for creating observable, controllable execution
units out of your code, and more easily integrating the code of others.
This project is the merge of several libraries for logging, spawning and
controlling child processes, and creating an RPC to child processes that
controls the run of the child process. Due to the confusing duplication of
signals as a means of control and the lack of uniformity in signals (ie
windows) the goal of `proc` is to create one way to these things, following
the principles of design used for Go itself.
Initially I was reworking this up from past code but decided that the job
will now be about taking everything from [p9](https://github.com/cybriq/p9),
throwing away the stupid struct definition/access method (it just
complicates things) because that implementation has almost everything in it.
There is many things inside the `pod` subfolder on that repository that need
to be separated. It was my first project and learning how to structure and
architect was a big challenge considering prior to that I had only done my
main work in algorithms.
The `p9` work includes almost everything, and discards some design concepts
that were used in previously used libraries like `urfave/cli` which
unnecessarily complicated the scheme by putting configurations underneath
commands, unnecessary because a program with many run modes still only has
one configuration set and forcing the developer to think of them as a
unified set instead of children of commands saves a lot of complexity and
maintenance.
We may add some more features, such as an init function to each
configuration item, and eliminate the complexity of initial startup code to
put things together that are conceptually and functionally linked.
## Installation
### For developers:
To make nice tagged versions with the version number in the commit as well as
matching to the tag, there is a tool called [bumper](cmd/bumper) that bumps
the version to the next patch version (vX.X.PATCH), embeds this new version
number into [version.go](./version.go) with the matching git commit hash of the
parent commit. This will make importing this library at an exact commit much
more human.
In addition, it makes it easy to make a nice multi line commit message as many
repositories request in their CONTRIBUTION file by replacing ` -- ` with two
carriage returns.
To install:
go install ./bumper/.
To use:
bumper make a commit comment here -- text after double \
hyphen will be separated by a carriage return -- \
anywhere in the text
To automatically bump the minor version use `minor` as the first word of the
comment. For the major version `major`.
## Usage
## Support
## Contributing
## Authors and acknowledgment
David Vennik david@cybriq.systems
## License
Unlicensed: see [here](./LICENSE)
## Project status
In the process of revision and merging together several related libraries that
need to be unified.

View File

@@ -0,0 +1,58 @@
package main
import (
"errors"
"time"
"github.com/cybriq/proc"
log2 "github.com/cybriq/proc/pkg/log"
)
var log = log2.GetLogger(proc.PathBase)
func main() {
log2.App = "logtest"
log2.SetLogLevel(log2.Trace)
log2.CodeLoc = false
log2.SetTimeStampFormat(time.Stamp)
log.I.C(proc.Version)
log.T.Ln("testing")
log.D.Ln("testing")
log.I.Ln("testing")
log.W.Ln("testing")
log.E.Chk(errors.New("testing"))
log.F.Ln("testing")
log.I.S(log2.GetAllSubsystems())
log.I.Ln("setting timestamp format to RFC822Z")
log2.SetTimeStampFormat(time.RFC822Z)
log.I.Ln("setting log level to info and printing from all levels")
log2.SetLogLevel(log2.Info)
log.T.Ln("testing")
log.D.Ln("testing")
log.I.Ln("testing")
log.W.Ln("testing")
log.E.Chk(errors.New("testing"))
log.F.Ln("testing")
log.T.Ln("testing")
log2.CodeLoc = true
log2.SetLogLevel(log2.Trace)
log.I.C(proc.Version)
log.T.Ln("testing")
log.D.Ln("testing")
log.I.Ln("testing")
log.W.Ln("testing")
log.E.Chk(errors.New("testing"))
log.F.Ln("testing")
log.I.S(log2.GetAllSubsystems())
log.I.Ln("setting log level to info and printing from all levels")
log2.SetLogLevel(log2.Info)
log.T.Ln("testing")
log.D.Ln("testing")
log.I.Ln("testing")
log.W.Ln("testing")
log.E.Chk(errors.New("testing"))
log.F.Ln("testing")
log.T.Ln("testing")
}

18
pkg/proc/go.mod Normal file
View File

@@ -0,0 +1,18 @@
module github.com/Indra-Labs/indra/pkg/proc
go 1.19
require (
github.com/cybriq/proc v0.20.10
github.com/davecgh/go-spew v1.1.1
github.com/gookit/color v1.5.2
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/naoina/toml v0.1.1
go.uber.org/atomic v1.10.0
)
require (
github.com/naoina/go-stringutil v0.1.0 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 // indirect
)

31
pkg/proc/go.sum Normal file
View File

@@ -0,0 +1,31 @@
github.com/cybriq/proc v0.20.10 h1:8v6Wq7NtsZ05uyQZbdquMUwb1Zs5TTzJeJh6QucHjDQ=
github.com/cybriq/proc v0.20.10/go.mod h1:b6JDUUwfe8soxWzvAziWA/msrb73O2v6gZEQL+wHYx8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/gookit/color v1.5.2 h1:uLnfXcaFjlrDnQDT+NCBcfhrXqYTx/rcCa6xn01Y8yI=
github.com/gookit/color v1.5.2/go.mod h1:w8h4bGiHeeBpvQVePTutdbERIUf3oJE5lZ8HM0UgXyg=
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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/naoina/go-stringutil v0.1.0 h1:rCUeRUHjBjGTSHl0VC00jUPLz8/F9dDzYI70Hzifhks=
github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=
github.com/naoina/toml v0.1.1 h1:PT/lllxVVN0gzzSqSlHEmP8MJB4MY2U7STGxiouV4X8=
github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=
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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

44
pkg/proc/pkg/app/app.go Normal file
View File

@@ -0,0 +1,44 @@
package app
import (
"github.com/cybriq/proc/pkg/cmds"
)
type App struct {
*cmds.Command
launch *cmds.Command
runArgs []string
cmds.Envs
}
func New(cmd *cmds.Command, args []string) (a *App, err error) {
// Add the default configuration items for datadir/configfile
cmds.GetConfigBase(cmd.Configs, cmd.Name, false)
// Add the help function
cmd.AddCommand(cmds.Help())
a = &App{Command: cmd}
// We first parse the CLI args, in case config file location has been
// specified
if a.launch, _, err = a.Command.ParseCLIArgs(args); log.E.Chk(err) {
return
}
if err = cmd.LoadConfig(); log.E.Chk(err) {
return
}
a.Command, err = cmds.Init(cmd, nil)
a.Envs = cmd.GetEnvs()
if err = a.Envs.LoadFromEnvironment(); log.E.Chk(err) {
return
}
// This is done again, to ensure the effect of CLI args take precedence
if a.launch, a.runArgs, err = a.Command.ParseCLIArgs(args); log.E.Chk(err) {
return
}
return
}
func (a *App) Launch() (err error) {
err = a.launch.Entrypoint(a.Command, a.runArgs)
log.E.Chk(err)
return
}

View File

@@ -0,0 +1,28 @@
package app
import (
"os"
"strings"
"testing"
"github.com/cybriq/proc/pkg/cmds"
)
func TestNew(t *testing.T) {
args1 := "/random/path/to/server_binary --cafile ~/some/cafile --LC=cn node -addrindex --BD 48h30s"
args1s := strings.Split(args1, " ")
var a *App
var err error
if a, err = New(cmds.GetExampleCommands(), args1s); log.E.Chk(err) {
t.FailNow()
}
if err = a.Launch(); log.E.Chk(err) {
t.FailNow()
}
if err = os.RemoveAll(a.Command.Configs["DataDir"].
Expanded()); log.E.Chk(err) {
t.FailNow()
}
}

8
pkg/proc/pkg/app/log.go Normal file
View File

@@ -0,0 +1,8 @@
package app
import (
"github.com/cybriq/proc"
log2 "github.com/cybriq/proc/pkg/log"
)
var log = log2.GetLogger(proc.PathBase)

View File

@@ -0,0 +1,88 @@
package appdata
import (
"os"
"os/user"
"path/filepath"
"runtime"
"strings"
"unicode"
)
// GetDataDir returns an operating system specific directory to be used for
// storing application data for an application.
// See Dir for more details. This unexported version takes an operating system argument primarily to enable the testing
// package to properly test the function by forcing an operating system that is not the currently one.
func GetDataDir(goos, appName string, roaming bool) string {
if appName == "" || appName == "." {
return "."
}
// The caller really shouldn't prepend the appName with a period, but if they do, handle it gracefully by trimming
// it.
appName = strings.TrimPrefix(appName, ".")
appNameUpper := string(unicode.ToUpper(rune(appName[0]))) + appName[1:]
appNameLower := string(unicode.ToLower(rune(appName[0]))) + appName[1:]
// Get the OS specific home directory via the Go standard lib.
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")
}
switch goos {
// Attempt to use the LOCALAPPDATA or APPDATA environment variable on Windows.
case "windows":
// Windows XP and before didn't have a LOCALAPPDATA, so fallback to regular APPDATA when LOCALAPPDATA is not
// set.
appData := os.Getenv("LOCALAPPDATA")
if roaming || appData == "" {
appData = os.Getenv("APPDATA")
}
if appData != "" {
return filepath.Join(appData, appNameUpper)
}
case "darwin":
if homeDir != "" {
return filepath.Join(
homeDir, "Library",
"Application Support", appNameUpper,
)
}
case "plan9":
if homeDir != "" {
return filepath.Join(homeDir, appNameLower)
}
default:
if homeDir != "" {
return filepath.Join(homeDir, "."+appNameLower)
}
}
// Fall back to the current directory if all else fails.
return "."
}
// Dir returns an operating system specific directory to be used for storing application data for an application. The
// appName parameter is the name of the application the data directory is being requested for. This function will
// prepend a period to the appName for POSIX style operating systems since that is standard practice.
//
// An empty appName or one with a single dot is treated as requesting the current directory so only "." will be
// returned. Further, the first character of appName will be made lowercase for POSIX style operating systems and
// uppercase for Mac and Windows since that is standard practice.
//
// The roaming parameter only applies to Windows where it specifies the roaming application data profile (%APPDATA%)
// should be used instead of the local one (%LOCALAPPDATA%) that is used by default. Example results:
//
// dir := Dir("myapp", false)
//
// POSIX (Linux/BSD): ~/.myapp
// Mac OS: $HOME/Library/Application Support/Myapp
// Windows: %LOCALAPPDATA%\Myapp
// Plan 9: $home/myapp
func Dir(appName string, roaming bool) string {
return GetDataDir(runtime.GOOS, appName, roaming)
}

View File

@@ -0,0 +1,212 @@
package appdata_test
import (
"os"
"os/user"
"path/filepath"
"runtime"
"testing"
"unicode"
"github.com/cybriq/proc/pkg/appdata"
)
// TestAppDataDir tests the API for Dir to ensure it gives expected results for various operating systems.
func TestAppDataDir(t *testing.T) {
// App name plus upper and lowercase variants.
appName := "myapp"
appNameUpper := string(unicode.ToUpper(rune(appName[0]))) + appName[1:]
appNameLower := string(unicode.ToLower(rune(appName[0]))) + appName[1:]
// When we're on Windows, set the expected local and roaming directories per the environment vars. When we aren't
// on Windows, the function should return the current directory when forced to provide the Windows path since the
// environment variables won't exist.
winLocal := "."
winRoaming := "."
if runtime.GOOS == "windows" {
localAppData := os.Getenv("LOCALAPPDATA")
roamingAppData := os.Getenv("APPDATA")
if localAppData == "" {
localAppData = roamingAppData
}
winLocal = filepath.Join(localAppData, appNameUpper)
winRoaming = filepath.Join(roamingAppData, appNameUpper)
}
// Get the home directory to use for testing expected results.
var homeDir string
usr, e := user.Current()
if e != nil {
t.Errorf("user.Current: %v", e)
return
}
homeDir = usr.HomeDir
// Mac node data directory.
macAppData := filepath.Join(homeDir, "Library", "Application Support")
tests := []struct {
goos string
appName string
roaming bool
want string
}{
// Various combinations of application name casing, leading period, operating system, and roaming flags.
{"windows", appNameLower, false, winLocal},
{"windows", appNameUpper, false, winLocal},
{"windows", "." + appNameLower, false, winLocal},
{"windows", "." + appNameUpper, false, winLocal},
{"windows", appNameLower, true, winRoaming},
{"windows", appNameUpper, true, winRoaming},
{"windows", "." + appNameLower, true, winRoaming},
{"windows", "." + appNameUpper, true, winRoaming},
{
"linux",
appNameLower,
false,
filepath.Join(homeDir, "."+appNameLower),
},
{
"linux",
appNameUpper,
false,
filepath.Join(homeDir, "."+appNameLower),
},
{
"linux", "." + appNameLower, false,
filepath.Join(homeDir, "."+appNameLower),
},
{
"linux", "." + appNameUpper, false,
filepath.Join(homeDir, "."+appNameLower),
},
{
"darwin",
appNameLower,
false,
filepath.Join(macAppData, appNameUpper),
},
{
"darwin",
appNameUpper,
false,
filepath.Join(macAppData, appNameUpper),
},
{
"darwin", "." + appNameLower, false,
filepath.Join(macAppData, appNameUpper),
},
{
"darwin", "." + appNameUpper, false,
filepath.Join(macAppData, appNameUpper),
},
{
"openbsd", appNameLower, false,
filepath.Join(homeDir, "."+appNameLower),
},
{
"openbsd", appNameUpper, false,
filepath.Join(homeDir, "."+appNameLower),
},
{
"openbsd", "." + appNameLower, false,
filepath.Join(homeDir, "."+appNameLower),
},
{
"openbsd", "." + appNameUpper, false,
filepath.Join(homeDir, "."+appNameLower),
},
{
"freebsd", appNameLower, false,
filepath.Join(homeDir, "."+appNameLower),
},
{
"freebsd", appNameUpper, false,
filepath.Join(homeDir, "."+appNameLower),
},
{
"freebsd", "." + appNameLower, false,
filepath.Join(homeDir, "."+appNameLower),
},
{
"freebsd", "." + appNameUpper, false,
filepath.Join(homeDir, "."+appNameLower),
},
{
"netbsd", appNameLower, false,
filepath.Join(homeDir, "."+appNameLower),
},
{
"netbsd", appNameUpper, false,
filepath.Join(homeDir, "."+appNameLower),
},
{
"netbsd", "." + appNameLower, false,
filepath.Join(homeDir, "."+appNameLower),
},
{
"netbsd", "." + appNameUpper, false,
filepath.Join(homeDir, "."+appNameLower),
},
{"plan9", appNameLower, false, filepath.Join(homeDir, appNameLower)},
{"plan9", appNameUpper, false, filepath.Join(homeDir, appNameLower)},
{
"plan9", "." + appNameLower, false,
filepath.Join(homeDir, appNameLower),
},
{
"plan9", "." + appNameUpper, false,
filepath.Join(homeDir, appNameLower),
},
{
"unrecognized", appNameLower, false,
filepath.Join(homeDir, "."+appNameLower),
},
{
"unrecognized", appNameUpper, false,
filepath.Join(homeDir, "."+appNameLower),
},
{
"unrecognized", "." + appNameLower, false,
filepath.Join(homeDir, "."+appNameLower),
},
{
"unrecognized", "." + appNameUpper, false,
filepath.Join(homeDir, "."+appNameLower),
},
// No application name provided, so expect current directory.
{"windows", "", false, "."},
{"windows", "", true, "."},
{"linux", "", false, "."},
{"darwin", "", false, "."},
{"openbsd", "", false, "."},
{"freebsd", "", false, "."},
{"netbsd", "", false, "."},
{"plan9", "", false, "."},
{"unrecognized", "", false, "."},
// Single dot provided for application name, so expect current
// directory.
{"windows", ".", false, "."},
{"windows", ".", true, "."},
{"linux", ".", false, "."},
{"darwin", ".", false, "."},
{"openbsd", ".", false, "."},
{"freebsd", ".", false, "."},
{"netbsd", ".", false, "."},
{"plan9", ".", false, "."},
{"unrecognized", ".", false, "."},
}
t.Logf("Running %d tests", len(tests))
for i, test := range tests {
ret := TstAppDataDir(test.goos, test.appName, test.roaming)
if ret != test.want {
t.Errorf(
"AppDataDir #%d (%s) does not match - "+
"expected got %s, want %s", i, test.goos, ret,
test.want,
)
continue
}
}
}
// TstAppDataDir makes the internal appDataDir function available to the test package.
func TstAppDataDir(goos, appName string, roaming bool) string {
return appdata.GetDataDir(goos, appName, roaming)
}

226
pkg/proc/pkg/cmds/args.go Normal file
View File

@@ -0,0 +1,226 @@
package cmds
import (
"fmt"
"strings"
"github.com/cybriq/proc/pkg/opts/meta"
"github.com/cybriq/proc/pkg/util"
)
// ParseCLIArgs reads a command line argument slice (presumably from
// os.Args), identifies the command to run and
//
// Rules for constructing CLI args:
//
// - Commands are identified by name, and must appear in their hierarchic
// order to invoke subcommands. They are matched as normalised to lower
// case.
//
// - Options can be preceded by "--" or "-", and the full name, or the
// alias, normalised to lower case for matching, and if there is an "="
// after it, the value is after this, otherwise, the next element in the
// args is the value, except booleans, which default to true unless set
// to false. For these, the prefix "no" or similar indicates the
// semantics of the true value.
//
// - Options only match when preceded by their relevant Command, except for
// the root Command, and these options must precede any other command
// options.
//
// - If no command is selected, the root Command.Default is selected. This
// can optionally be used for subcommands as well, though it is unlikely
// needed, if found, the Default of the tip of the Command branch
// selected by the CLI if there is one, otherwise the Command itself.
func (c *Command) ParseCLIArgs(a []string) (run *Command, runArgs []string, err error) {
args := make([]string, len(a))
var cursor int
for i := range a {
if len(a[i]) > 0 {
args[cursor] = a[i]
cursor++
}
}
var segments [][]string
commands := Commands{c}
var depth, last int
var done bool
current := c
cursor = 0
// First pass matches Command names in order to slice up the sections
// where relevant config items will be found.
for !done {
for i := range current.Commands {
if util.Norm(args[cursor]) == util.Norm(current.Commands[i].Name) {
// the command to run is the last, so update each new command
// found
run = current.Commands[i]
commands = append(commands, run)
depth++
current = current.Commands[i]
segments = append(segments, args[last:cursor])
last = cursor
break
}
}
cursor++
// append the remainder to the last segment
if cursor == len(args) {
var tmp []string
for _, item := range args[last:cursor] {
if len(item) > 0 {
tmp = append(tmp, item)
}
}
segments = append(segments, tmp)
done = true
}
}
// The segments that have been cut from args will now provide the root level
// command name, and all subsequent items until the next segment should be
// names found in the configs map.
for i := range segments {
if len(segments[i]) > 0 {
iArgs := segments[i][1:]
cmd := commands[i]
// log.T.Ln(commands[i].Name, "args", iArgs)
// the final command can accept arbitrary arguments, that are passed
// into the endrypoint
runArgs = iArgs
if util.Norm(commands[i].Name) == "help" {
break
}
var cursor int
for cursor < len(iArgs) {
inc := 1
arg := iArgs[cursor]
if len(arg) == 0 {
cursor++
continue
}
log.T.Ln("evaluating", arg, iArgs[cursor:])
if strings.HasPrefix(arg, "-") {
arg = arg[1:]
if strings.HasPrefix(arg, "-") {
arg = arg[1:]
}
if strings.Contains(arg, "=") {
log.T.Ln("value in arg", arg)
split := strings.Split(arg, "=")
if len(split) > 2 {
split = append(split[:1], strings.Join(split[1:], "="))
}
for cfgName := range cmd.Configs {
aliases := cmd.Configs[cfgName].Meta().Aliases()
names := append(
[]string{cfgName}, aliases...)
for _, name := range names {
if util.Norm(name) == util.Norm(split[0]) {
log.T.F("assigning value '%s' to %s",
split[1], split[0])
err = cmd.Configs[cfgName].FromString(split[1])
if log.E.Chk(err) {
return
}
}
}
}
} else {
if len(iArgs) > cursor {
found := false
for cfgName := range cmd.Configs {
aliases := cmd.Configs[cfgName].Meta().Aliases()
names := append(
[]string{cfgName}, aliases...)
for _, name := range names {
if util.Norm(name) == util.Norm(arg) {
// check for booleans, which can only be
// followed by true or false
if cmd.Configs[cfgName].Type() == meta.Bool {
if len(iArgs) <= cursor {
log.I.Ln("bing")
err = cmd.Configs[cfgName].
FromString(iArgs[cursor+1])
} else {
err = cmd.Configs[cfgName].
FromString("true")
}
inc++
found = true
// next value is not a truth value,
// simply assign true and increment
// only 1 to cursor
if err != nil {
log.T.Chk(err)
found = true
log.I.F("assigned value 'true' to %s",
cfgName)
break
}
} else {
log.T.F("assigning value '%s' to %s",
iArgs[cursor+1], cfgName)
err = cmd.Configs[cfgName].
FromString(iArgs[cursor+1])
if log.E.Chk(err) {
return
}
inc++
found = true
}
}
}
}
if !found {
err = fmt.Errorf(
"option not found: '%s' context %v",
arg, segments[i])
return
}
// if this is the last arg, and it's bool, the
// implied value is true
} else if cmd.Configs[arg] != nil && cmd.Configs[arg].Type() == meta.Bool {
err = cmd.Configs[arg].FromString("true")
if log.E.Chk(err) {
return
}
} else {
err = fmt.Errorf("argument '%s' missing value:"+
"context %v", arg, iArgs)
log.E.Chk(err)
return
}
}
} else {
err = fmt.Errorf("argument %s missing '-', context %s, "+
"most likely misspelled subcommand", arg, iArgs)
log.T.Chk(err)
return
}
cursor += inc
}
}
}
// if no Command was found, return the default. If there is no default, the
// top level Command will be returned
if len(c.Default) > 0 && len(segments) < 2 {
run = c
def := c.Default
var lastFound int
for i := range def {
for _, sc := range run.Commands {
if sc.Name == def[i] {
lastFound = i
run = sc
}
}
}
if lastFound != len(def)-1 {
err = fmt.Errorf("default command %v not found at %s", c.Default,
def)
}
}
// log.T.F("will be executing command '%s' %s", run.Name, runArgs)
return
}

View File

@@ -0,0 +1,295 @@
package cmds
import (
"fmt"
"path/filepath"
"runtime"
"strings"
"sync"
log2 "github.com/cybriq/proc/pkg/log"
"github.com/cybriq/proc/pkg/opts/config"
"github.com/cybriq/proc/pkg/opts/meta"
"github.com/cybriq/proc/pkg/opts/text"
"github.com/cybriq/proc/pkg/opts/toggle"
"github.com/cybriq/proc/pkg/path"
"github.com/cybriq/proc/pkg/util"
)
type Op func(c *Command, args []string) error
var NoOp = func(c *Command, args []string) error { return nil }
var Tags = func(s ...string) []string {
return s
}
// Command is a specification for a command and can include any number of
// subcommands, and for each Command a list of options
type Command struct {
path.Path
Name string
Description string
Documentation string
Entrypoint Op
Parent *Command
Commands Commands
Configs config.Opts
Default []string // specifies default subcommand to execute
sync.Mutex
}
// Commands are a slice of Command entries
type Commands []*Command
func (c *Command) AddCommand(cm *Command) {
c.Commands = append(c.Commands, cm)
}
const configFilename = "config.toml"
// GetConfigBase creates an option set that should go in the root of a
// Command specification for an application, providing a data directory path
// and config file path.
//
// This exists in order to simplify setup for application configuration
// persistence.
func GetConfigBase(in config.Opts, appName string, abs bool) {
var defaultDataDir, defaultConfigFile string
switch runtime.GOOS {
case "linux", "aix", "freebsd", "netbsd", "openbsd", "dragonfly":
defaultDataDir = fmt.Sprintf("~/.%s", appName)
defaultConfigFile =
fmt.Sprintf("%s/%s", defaultDataDir, configFilename)
case "windows":
defaultDataDir = fmt.Sprintf("%%LOCALAPPDATA%%\\%s", appName)
defaultConfigFile =
fmt.Sprintf("%%LOCALAPPDATA%%\\%s\\%s", defaultDataDir,
configFilename)
case "darwin":
defaultDataDir = filepath.Join(
"~", "Library",
"Application Support", strings.ToUpper(appName),
)
defaultConfigFile = filepath.Join(defaultDataDir, configFilename)
}
options := config.Opts{
"ConfigFile": text.New(meta.Data{
Aliases: []string{"CF"},
Label: "Configuration File",
Description: "location of configuration file",
Documentation: strings.TrimSpace(`
The configuration file path defines the place where the configuration will be
loaded from at application startup, and where it will be written if changed.
`),
Default: defaultConfigFile,
}, text.NormalizeFilesystemPath(abs, appName)),
"DataDir": text.New(meta.Data{
Aliases: []string{"DD"},
Label: "Data Directory",
Description: "root folder where application data is stored",
Default: defaultDataDir,
}, text.NormalizeFilesystemPath(abs, appName)),
"LogCodeLocations": toggle.New(meta.Data{
Aliases: []string{"LCL"},
Label: "Log Code Locations",
Description: "whether to print code locations in logs",
Documentation: strings.TrimSpace(strings.TrimSpace(`
Toggles on and off the printing of code locations in logs.
`)),
Default: "true",
}, func(o *toggle.Opt) (err error) {
log2.CodeLoc = o.Value().Bool()
return
}),
"LogLevel": text.New(meta.Data{
Aliases: []string{"LL"},
Label: "Log Level",
Description: "Level of logging to print: [ " + log2.LvlStr.String() +
" ]",
Documentation: strings.TrimSpace(`
Log levels are values in ascending order with the following names:
` + log2.LvlStr.String() + `
The level set in this configuration item defines the limit in ascending order
of what log level printers will output. Default is 'info' which means debug and
trace log statements will not print.
`),
Default: log2.GetLevelName(log2.Info),
}, func(o *text.Opt) (err error) {
v := strings.TrimSpace(o.Value().Text())
found := false
lvl := log2.Info
for i := range log2.LvlStr {
ll := log2.GetLevelName(i)
if util.Norm(v) == strings.TrimSpace(ll) {
lvl = i
found = true
}
}
if !found {
err = fmt.Errorf("log level value %s not valid from %v",
v, log2.LvlStr)
_ = o.FromString(log2.GetLevelName(lvl))
}
log2.SetLogLevel(lvl)
return
}),
"LogFilePath": text.New(meta.Data{
Aliases: Tags("LFP"),
Label: "Log To File",
Description: "Write logs to the specified file",
Documentation: strings.TrimSpace(`
Sets the path of the file to write logs to.
`),
Default: filepath.Join(defaultDataDir, "log.txt"),
}, func(o *text.Opt) (err error) {
err = log2.SetLogFilePath(o.Expanded())
return
}, text.NormalizeFilesystemPath(abs, appName)),
"LogToFile": toggle.New(meta.Data{
Aliases: Tags("LTF"),
Label: "Log To File",
Description: "Enable writing of logs",
Documentation: strings.TrimSpace(`
Enables the writing of logs to the file path defined in LogFilePath.
`),
Default: "false",
}, func(o *toggle.Opt) (err error) {
if o.Value().Bool() {
log.T.Ln("starting log file writing")
err = log2.StartLogToFile()
} else {
err = log2.StopLogToFile()
log.T.Ln("stopped log file writing")
}
log.E.Chk(err)
return
}),
}
for i := range options {
in[i] = options[i]
}
}
// Init sets up a Command to be ready to use. Puts the reverse paths into the
// tree structure, puts sane defaults into command launchers, runs the hooks on
// all the defined configuration values, and sets the paths on each Command and
// Option so that they can be directly interrogated for their location.
func Init(c *Command, p path.Path) (cmd *Command, err error) {
if c.Parent != nil {
log.T.Ln("backlinking children of", c.Parent.Name)
}
if c.Entrypoint == nil {
c.Entrypoint = NoOp
}
if p == nil {
p = path.Path{c.Name}
}
c.Path = p // .Parent()
for i := range c.Configs {
c.Configs[i].SetPath(p)
}
for i := range c.Commands {
c.Commands[i].Parent = c
c.Commands[i].Path = p.Child(c.Commands[i].Name)
_, _ = Init(c.Commands[i], p.Child(c.Commands[i].Name))
}
c.ForEach(func(cmd *Command, _ int) bool {
for i := range cmd.Configs {
err = cmd.Configs[i].RunHooks()
if log.E.Chk(err) {
return false
}
}
return true
}, 0, 0, c)
return c, err
}
// GetOpt returns the option at a requested path
func (c *Command) GetOpt(path path.Path) (o config.Option) {
p := make([]string, len(path))
for i := range path {
p[i] = path[i]
}
switch {
case len(p) < 1:
// not found
return
case len(p) > 2:
// search subcommands
for i := range c.Commands {
if util.Norm(c.Commands[i].Name) == util.Norm(p[1]) {
return c.Commands[i].GetOpt(p[1:])
}
}
case len(p) == 2:
// check name matches path, search for config item
if util.Norm(c.Name) == util.Norm(p[0]) {
for i := range c.Configs {
if util.Norm(i) == util.Norm(p[1]) {
return c.Configs[i]
}
}
}
}
return nil
}
func (c *Command) GetCommand(p string) (o *Command) {
pp := strings.Split(p, " ")
path := path.Path(pp)
// log.I.F("%v == %v", path, c.Path)
if path.Equal(c.Path) {
// log.I.Ln("found", c.Path)
return c
}
for i := range c.Commands {
// log.I.Ln(c.Commands[i].Path)
o = c.Commands[i].GetCommand(p)
if o != nil {
return
}
}
return
}
// ForEach runs a closure on every node in the Commands tree, stopping if the
// closure returns false
func (c *Command) ForEach(cl func(*Command, int) bool, hereDepth,
hereDist int, cmd *Command) (ocl func(*Command, int) bool, depth,
dist int, cm *Command) {
ocl = cl
cm = cmd
if hereDepth == 0 {
if !ocl(cm, hereDepth) {
return
}
}
depth = hereDepth + 1
log.T.Ln(path.GetIndent(depth)+"->", depth)
dist = hereDist
for i := range c.Commands {
log.T.Ln(path.GetIndent(depth)+"walking", c.Commands[i].Name, depth,
dist)
if !cl(c.Commands[i], depth) {
return
}
dist++
ocl, depth, dist, cm = c.Commands[i].ForEach(
cl,
depth,
dist,
cm,
)
}
log.T.Ln(path.GetIndent(hereDepth)+"<-", hereDepth)
depth--
return
}

View File

@@ -0,0 +1,278 @@
package cmds
import (
"fmt"
"os"
"strings"
"testing"
log2 "github.com/cybriq/proc/pkg/log"
"github.com/cybriq/proc/pkg/opts/config"
"github.com/cybriq/proc/pkg/path"
)
func TestCommand_Foreach(t *testing.T) {
log2.SetLogLevel(log2.Info)
cm, _ := Init(GetExampleCommands(), nil)
log.I.Ln("spewing only droptxindex")
cm.ForEach(func(cmd *Command, _ int) bool {
if cmd.Name == "droptxindex" {
log.I.S(cmd)
}
return true
}, 0, 0, cm)
log.I.Ln("printing name of all commands found on search")
cm.ForEach(func(cmd *Command, depth int) bool {
log.I.Ln()
log.I.F("%s%s #(%d)", path.GetIndent(depth), cmd.Path, depth)
for i := range cmd.Configs {
log.I.F("%s%s -%s %v #%v (%d)", path.GetIndent(depth),
cmd.Configs[i].Path(), i, cmd.Configs[i].String(), cmd.Configs[i].Meta().Aliases(), depth)
}
return true
}, 0, 0, cm)
}
func TestCommand_MarshalText(t *testing.T) {
log2.SetLogLevel(log2.Info)
o, _ := Init(GetExampleCommands(), nil)
// log.I.S(o)
conf, err := o.MarshalText()
if log.E.Chk(err) {
t.FailNow()
}
log.I.Ln("\n" + string(conf))
}
func TestCommand_UnmarshalText(t *testing.T) {
log2.SetLogLevel(log2.Info)
o, _ := Init(GetExampleCommands(), nil)
var conf []byte
var err error
conf, err = o.MarshalText()
if log.E.Chk(err) {
t.FailNow()
}
err = o.UnmarshalText(conf)
if err != nil {
t.FailNow()
}
}
func TestCommand_ParseCLIArgs(t *testing.T) {
log2.SetLogLevel(log2.Trace)
args1 := "/random/path/to/server_binary --cafile ~/some/cafile --LC=cn node -addrindex --BD=5m"
args1s := strings.Split(args1, " ")
ec := GetExampleCommands()
o, _ := Init(ec, nil)
run, _, err := o.ParseCLIArgs(args1s)
if log.E.Chk(err) {
t.FailNow()
}
_, _ = run, err
args3 := "node -addrindex --BD 48h30s dropaddrindex somegarbage --autoports"
args3s := strings.Split(args3, " ")
run, _, err = o.ParseCLIArgs(args3s)
// This one must fail, 'somegarbage' is not a command and has no -/-- prefix
log.T.Ln("error expected")
if err == nil {
t.FailNow()
}
args4 := "/random/path/to/server_binary --lcl"
args4s := strings.Split(args4, " ")
run, _, err = o.ParseCLIArgs(args4s)
if log.E.Chk(err) {
t.FailNow()
}
args5 := "/random/path/to/server_binary --cafile ~/some/cafile --LC=cn --lcl"
args5s := strings.Split(args5, " ")
run, _, err = o.ParseCLIArgs(args5s)
if log.E.Chk(err) {
t.FailNow()
}
args2 := "/random/path/to/server_binary node -addrindex --BD=48h30s -RPCMaxConcurrentReqs -16 dropaddrindex"
args2s := strings.Split(args2, " ")
run, _, err = o.ParseCLIArgs(args2s)
if log.E.Chk(err) {
t.FailNow()
}
}
func TestCommand_GetEnvs(t *testing.T) {
log2.SetLogLevel(log2.Info)
o, _ := Init(GetExampleCommands(), nil)
envs := o.GetEnvs()
var out []string
err := envs.ForEach(func(env string, opt config.Option) error {
out = append(out, env)
return nil
})
for i := range out { // verifying ordering groups subcommands
log.I.Ln(out[i])
}
if err != nil {
t.FailNow()
}
}
var testSeparator = fmt.Sprintf("%s\n", strings.Repeat("-", 72))
func TestCommand_Help(t *testing.T) {
log2.SetLogLevel(log2.Debug)
ex := GetExampleCommands()
ex.AddCommand(Help())
o, _ := Init(ex, nil)
o.Commands = append(o.Commands)
args1 := "/random/path/to/server_binary help"
fmt.Println(args1)
args1s := strings.Split(args1, " ")
run, args, err := o.ParseCLIArgs(args1s)
if log.E.Chk(err) {
t.FailNow()
}
err = run.Entrypoint(o, args)
if log.E.Chk(err) {
t.FailNow()
}
fmt.Print(testSeparator)
args1 = "/random/path/to/server_binary help loglevel"
fmt.Println(args1)
args1s = strings.Split(args1, " ")
run, args, err = o.ParseCLIArgs(args1s)
if log.E.Chk(err) {
t.FailNow()
}
err = run.Entrypoint(o, args)
if log.E.Chk(err) {
t.FailNow()
}
fmt.Print(testSeparator)
args1 = "/random/path/to/server_binary help help"
fmt.Println(args1)
args1s = strings.Split(args1, " ")
run, args, err = o.ParseCLIArgs(args1s)
if log.E.Chk(err) {
t.FailNow()
}
err = run.Entrypoint(o, args)
if log.E.Chk(err) {
t.FailNow()
}
fmt.Print(testSeparator)
args1 = "/random/path/to/server_binary help node"
fmt.Println(args1)
args1s = strings.Split(args1, " ")
run, args, err = o.ParseCLIArgs(args1s)
if log.E.Chk(err) {
t.FailNow()
}
err = run.Entrypoint(o, args)
if log.E.Chk(err) {
t.FailNow()
}
fmt.Print(testSeparator)
args1 = "/random/path/to/server_binary help rpcconnect"
fmt.Println(args1)
args1s = strings.Split(args1, " ")
run, args, err = o.ParseCLIArgs(args1s)
if log.E.Chk(err) {
t.FailNow()
}
err = run.Entrypoint(o, args)
if log.E.Chk(err) {
t.FailNow()
}
fmt.Print(testSeparator)
args1 = "/random/path/to/server_binary help kopach rpcconnect"
fmt.Println(args1)
args1s = strings.Split(args1, " ")
run, args, err = o.ParseCLIArgs(args1s)
if log.E.Chk(err) {
t.FailNow()
}
err = run.Entrypoint(o, args)
if log.E.Chk(err) {
t.FailNow()
}
fmt.Print(testSeparator)
args1 = "/random/path/to/server_binary help node rpcconnect"
fmt.Println(args1)
args1s = strings.Split(args1, " ")
run, args, err = o.ParseCLIArgs(args1s)
if log.E.Chk(err) {
t.FailNow()
}
err = run.Entrypoint(o, args)
if log.E.Chk(err) {
t.FailNow()
}
fmt.Print(testSeparator)
args1 = "/random/path/to/server_binary help nodeoff"
fmt.Println(args1)
args1s = strings.Split(args1, " ")
run, args, err = o.ParseCLIArgs(args1s)
if log.E.Chk(err) {
t.FailNow()
}
err = run.Entrypoint(o, args)
if log.E.Chk(err) {
t.FailNow()
}
fmt.Print(testSeparator)
args1 = "/random/path/to/server_binary help user"
fmt.Println(args1)
args1s = strings.Split(args1, " ")
run, args, err = o.ParseCLIArgs(args1s)
if log.E.Chk(err) {
t.FailNow()
}
err = run.Entrypoint(o, args)
if log.E.Chk(err) {
t.FailNow()
}
fmt.Print(testSeparator)
args1 = "/random/path/to/server_binary help file"
fmt.Println(args1)
args1s = strings.Split(args1, " ")
run, args, err = o.ParseCLIArgs(args1s)
if log.E.Chk(err) {
t.FailNow()
}
err = run.Entrypoint(o, args)
if log.E.Chk(err) {
t.FailNow()
}
}
func TestCommand_LogToFile(t *testing.T) {
log2.SetLogLevel(log2.Trace)
ex := GetExampleCommands()
ex.AddCommand(Help())
ex, _ = Init(ex, nil)
ex.GetOpt(path.From("pod123 loglevel")).FromString("debug")
var err error
// this will create a place we can write the logs
if err = ex.SaveConfig(); log.E.Chk(err) {
err = os.RemoveAll(ex.Configs["ConfigFile"].Expanded())
if log.E.Chk(err) {
}
t.FailNow()
}
lfp := ex.GetOpt(path.From("pod123 logfilepath"))
o := ex.GetOpt(path.From("pod123 logtofile"))
o.FromString("true")
log.I.F("%s", lfp)
o.FromString("false")
var b []byte
if b, err = os.ReadFile(lfp.Expanded()); log.E.Chk(err) {
t.FailNow()
}
str := string(b)
log.I.F("'%s'", str)
if !strings.Contains(str, lfp.String()) {
t.FailNow()
}
if err := os.RemoveAll(ex.Configs["DataDir"].Expanded()); log.E.Chk(err) {
}
}

100
pkg/proc/pkg/cmds/env.go Normal file
View File

@@ -0,0 +1,100 @@
package cmds
import (
"os"
"sort"
"strings"
"github.com/cybriq/proc/pkg/opts/config"
"github.com/cybriq/proc/pkg/path"
)
type Env struct {
Name path.Path
Opt config.Option
}
type Envs []Env
func (e Envs) ForEach(fn func(env string, opt config.Option) (err error)) (err error) {
for i := range e {
var name []string
for j := range e[i].Name {
name = append(name, strings.ToUpper(e[i].Name[j]))
}
err = fn(strings.Join(name, "_"), e[i].Opt)
if err != nil {
return
}
}
return
}
func (e Envs) LoadFromEnvironment() (err error) {
err = e.ForEach(func(env string, opt config.Option) (err error) {
v, exists := os.LookupEnv(env)
if exists {
err = opt.FromString(v)
if log.D.Chk(err) {
return err
}
}
return
})
return
}
func (e Envs) Len() int {
return len(e)
}
func (e Envs) Less(i, j int) (res bool) {
li, lj := len(e[i].Name), len(e[j].Name)
if li < lj {
return true
}
cursor := -1
for {
res = false
cursor++
if strings.Join(e[i].Name[:cursor], "_") <
strings.Join(e[j].Name[:cursor], "_") {
res = true
}
if cursor >= li || cursor >= lj {
break
}
}
return
}
func (e Envs) Swap(i, j int) {
e[i], e[j] = e[j], e[i]
}
// GetEnvs walks a Command tree and returns a slice containing all environment
// variable names and the related config.Option.
func (c *Command) GetEnvs(path ...string) (envs Envs) {
if path == nil {
path = []string{c.Name}
}
for {
for i := range c.Configs {
envs = append(envs, Env{
Name: append(path, i),
Opt: c.Configs[i],
})
}
if len(c.Commands) > 0 {
for i := range c.Commands {
envs = append(envs,
c.Commands[i].GetEnvs(
append(path, c.Commands[i].Name)...)...,
)
}
}
break
}
sort.Sort(envs)
return
}

View File

@@ -0,0 +1,912 @@
package cmds
import (
"crypto/rand"
"encoding/base32"
"fmt"
"runtime"
integer "github.com/cybriq/proc/pkg/opts/Integer"
"github.com/cybriq/proc/pkg/opts/config"
"github.com/cybriq/proc/pkg/opts/duration"
"github.com/cybriq/proc/pkg/opts/float"
"github.com/cybriq/proc/pkg/opts/list"
"github.com/cybriq/proc/pkg/opts/meta"
"github.com/cybriq/proc/pkg/opts/text"
"github.com/cybriq/proc/pkg/opts/toggle"
)
const lorem = `
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum.`
// GetExampleCommands returns available subcommands in hypothetical Parallelcoin
// Pod example for testing (derived from btcd and btcwallet plus
// parallelcoin kopach miner)
func GetExampleCommands() (c *Command) {
c = &Command{
Name: "pod123",
Description: "All in one everything for parallelcoin",
Documentation: lorem,
Default: Tags("gui"),
Configs: config.Opts{
"AutoPorts": toggle.New(meta.Data{
Label: "Automatic Ports",
Tags: Tags("node", "wallet"),
Description: "RPC and controller ports are randomized, use with controller for automatic peer discovery",
Documentation: lorem,
Default: "false",
}),
"AutoListen": toggle.New(meta.Data{
Aliases: Tags("AL"),
Tags: Tags("node", "wallet"),
Label: "Automatic Listeners",
Description: "automatically update inbound addresses dynamically according to discovered network interfaces",
Documentation: lorem,
Default: "false",
}),
"CAFile": text.New(meta.Data{
Aliases: Tags("CA"),
Tags: Tags("node", "wallet", "tls"),
Label: "Certificate Authority File",
Description: "certificate authority file for TLS certificate validation",
Documentation: lorem,
}),
"CPUProfile": text.New(meta.Data{
Aliases: Tags("CPR"),
Tags: Tags("node", "wallet", "kopach", "worker"),
Label: "CPU Profile",
Description: "write cpu profile to this file",
Documentation: lorem,
}),
"DisableRPC": toggle.New(meta.Data{
Aliases: Tags("NRPC"),
Tags: Tags("node", "wallet"),
Label: "Disable RPC",
Description: "disable rpc servers, as well as kopach controller",
Documentation: lorem,
Default: "false",
}),
"Discovery": toggle.New(meta.Data{
Aliases: Tags("DI"),
Tags: Tags("node"),
Label: "Disovery",
Description: "enable LAN peer discovery in GUI",
Documentation: lorem,
Default: "false",
}),
"Hilite": list.New(meta.Data{
Aliases: Tags("HL"),
Tags: Tags(
"node",
"wallet",
"ctl",
"kopach",
"worker",
),
Label: "Hilite",
Description: "list of packages that will print with attention getters",
Documentation: lorem,
}),
"Locale": text.New(meta.Data{
Aliases: Tags("LC"),
Tags: Tags(
"node",
"wallet",
"ctl",
"kopach",
"worker",
),
Label: "Language",
Description: "user interface language i18 localization",
Documentation: lorem,
Options: Tags("en"),
Default: "en",
}),
"LimitPass": text.New(meta.Data{
Aliases: Tags("LP"),
Tags: Tags("node", "wallet"),
Label: "Limit Password",
Description: "limited user password",
Documentation: lorem,
Default: genPassword(),
}),
"LimitUser": text.New(meta.Data{
Aliases: Tags("LU"),
Tags: Tags("node", "wallet"),
Label: "Limit Username",
Description: "limited user name",
Documentation: lorem,
Default: "limituser",
}),
"LogFilter": list.New(meta.Data{
Aliases: Tags("LF"),
Tags: Tags(
"node",
"wallet",
"ctl",
"kopach",
"worker",
),
Label: "Log Filter",
Description: "list of packages that will not print logs",
Documentation: lorem,
}),
"OneTimeTLSKey": toggle.New(meta.Data{
Aliases: Tags("OTK"),
Tags: Tags("node", "wallet"),
Label: "One Time TLS Key",
Description: "generate a new TLS certificate pair at startup, but only write the certificate to disk",
Documentation: lorem,
Default: "false",
}),
"Password": text.New(meta.Data{
Aliases: Tags("PW"),
Tags: Tags("node", "wallet"),
Label: "Password",
Description: "password for client RPC connections",
Documentation: lorem,
Default: genPassword(),
}),
"PipeLog": toggle.New(meta.Data{
Aliases: Tags("PL"),
Label: "Pipe Logger",
Tags: Tags(
"node",
"wallet",
"ctl",
"kopach",
"worker",
),
Description: "enable pipe based logger IPC",
Documentation: lorem,
Default: "false",
}),
"Profile": text.New(meta.Data{
Aliases: Tags("HPR"),
Tags: Tags("node", "wallet", "ctl", "kopach",
"worker"),
Label: "Profile",
Description: "http profiling on given port (1024-40000)",
// Type: "",
Documentation: lorem,
}),
"RPCCert": text.New(meta.Data{
Aliases: Tags("RC"),
Tags: Tags("node", "wallet"),
Label: "RPC Cert",
Description: "location of RPC TLS certificate",
Documentation: lorem,
Default: "~/.pod/rpc.cert",
}),
"RPCKey": text.New(meta.Data{
Aliases: Tags("RK"),
Tags: Tags("node", "wallet"),
Label: "RPC Key",
Description: "location of rpc TLS key",
Documentation: lorem,
Default: "~/.pod/rpc.key",
}),
"RunAsService": toggle.New(meta.Data{
Aliases: Tags("RS"),
Label: "Run As Service",
Description: "shuts down on lock timeout",
Documentation: lorem,
Default: "false",
}),
"Save": toggle.New(meta.Data{
Aliases: Tags("SV"),
Label: "Save Configuration",
Description: "save opts given on commandline",
Documentation: lorem,
Default: "false",
}),
"ServerTLS": toggle.New(meta.Data{
Aliases: Tags("ST"),
Tags: Tags("node", "wallet"),
Label: "Server TLS",
Description: "enable TLS for the wallet connection to node RPC server",
Documentation: lorem,
Default: "true",
}),
"ClientTLS": toggle.New(meta.Data{
Aliases: Tags("CT"),
Tags: Tags("node", "wallet"),
Label: "TLS",
Description: "enable TLS for RPC client connections",
Documentation: lorem,
Default: "true",
},
),
"TLSSkipVerify": toggle.New(meta.Data{
Aliases: Tags("TSV"),
Tags: Tags("node", "wallet"),
Label: "TLS Skip Verify",
Description: "skip TLS certificate verification (ignore CA errors)",
Documentation: lorem,
Default: "false",
}),
"Username": text.New(meta.Data{
Aliases: Tags("UN"),
Tags: Tags("node", "wallet"),
Label: "Username",
Description: "username for client RPC connections",
Documentation: lorem,
Default: "username",
}),
"UseWallet": toggle.New(meta.Data{
Aliases: Tags("WC"),
Tags: Tags("ctl"),
Label: "Connect to Wallet",
Description: "set ctl to connect to wallet instead of chain server",
Documentation: lorem,
Default: "false",
}),
"WalletOff": toggle.New(meta.Data{
Aliases: Tags("WO"),
Tags: Tags("wallet"),
Label: "Wallet Off",
Description: "turn off the wallet backend",
Documentation: lorem,
Default: "false",
}),
},
Commands: Commands{
{
Name: "gui",
Description: "ParallelCoin GUI Wallet/Miner/Explorer",
Documentation: lorem,
Configs: config.Opts{
"DarkTheme": toggle.New(meta.Data{
Aliases: Tags("DT"),
Tags: Tags("gui"),
Label: "Dark Theme",
Description: "sets dark theme for GUI",
Documentation: lorem,
Default: "false",
}),
},
},
{
Name: "version",
Description: "print version and exit",
Documentation: lorem,
},
{
Name: "ctl",
Description: "command line wallet and chain RPC client",
Documentation: lorem,
},
{
Name: "node",
Description: "ParallelCoin blockchain node",
Documentation: lorem,
Entrypoint: func(c *Command, args []string) error {
log.I.Ln("running node")
return nil
},
Commands: []*Command{
{
Name: "dropaddrindex",
Description: "drop the address database index",
Documentation: lorem,
},
{
Name: "droptxindex",
Description: "drop the transaction database index",
Documentation: lorem,
},
{
Name: "dropcfindex",
Description: "drop the cfilter database index",
Documentation: lorem,
},
{
Name: "dropindexes",
Description: "drop all of the indexes",
Documentation: lorem,
},
{
Name: "resetchain",
Description: "deletes the current blockchain cache to force redownload",
Documentation: lorem,
},
},
Configs: config.Opts{
"AddCheckpoints": list.New(meta.Data{
Aliases: Tags("AC"),
Tags: Tags("node"),
Label: "Add Checkpoints",
Description: "add custom checkpoints",
Documentation: lorem,
}),
"AddPeers": list.New(meta.Data{
Aliases: Tags("AP"),
Tags: Tags("node"),
Label: "Add Peers",
Description: "manually adds addresses to try to connect to",
Documentation: lorem,
}),
"AddrIndex": toggle.New(meta.Data{
Aliases: Tags("AI"),
Tags: Tags("node"),
Label: "Address Index",
Description: "maintain a full address-based transaction index which makes the searchrawtransactions RPC available",
Documentation: lorem,
Default: "false",
}),
"BanDuration": duration.New(meta.Data{
Aliases: Tags("BD"),
Tags: Tags("node", "policy"),
Label: "Ban Duration",
Description: "how long a ban of a misbehaving peer lasts",
Documentation: lorem,
Default: "24h0m0s",
}),
"BanThreshold": integer.New(meta.Data{
Aliases: Tags("BT"),
Tags: Tags("node", "policy"),
Label: "Ban Threshold",
Description: "ban score that triggers a ban (default 100)",
Documentation: lorem,
Default: "100",
}),
"BlockMaxSize": integer.New(meta.Data{
Aliases: Tags("BMXS"),
Tags: Tags("node", "policy"),
Label: "Block Max Size",
Description: "maximum block size in bytes to be used when creating a block",
Documentation: lorem,
Default: "999000",
}),
"BlockMaxWeight": integer.New(meta.Data{
Aliases: Tags("BMXW"),
Tags: Tags("node", "policy"),
Label: "Block Max Weight",
Description: "maximum block weight to be used when creating a block",
Documentation: lorem,
Default: "3996000",
}),
"BlockMinSize": integer.New(meta.Data{
Aliases: Tags("BMS"),
Tags: Tags("node", "policy"),
Label: "Block Min Size",
Description: "minimum block size in bytes to be used when creating a block",
Documentation: lorem,
Default: "1000",
}),
"BlockMinWeight": integer.New(meta.Data{
Aliases: Tags("BMW"),
Tags: Tags("node"),
Label: "Block Min Weight",
Description: "minimum block weight to be used when creating a block",
Documentation: lorem,
Default: "4000",
}),
"BlockPrioritySize": integer.New(meta.Data{
Aliases: Tags("BPS"),
Tags: Tags("node"),
Label: "Block Priority Size",
Description: "size in bytes for high-priority/low-fee transactions when creating a block",
Documentation: lorem,
Default: "50000",
}),
"BlocksOnly": toggle.New(meta.Data{
Aliases: Tags("BO"),
Tags: Tags("node"),
Label: "Blocks Only",
Description: "do not accept transactions from remote peers",
Documentation: lorem,
Default: "false",
}),
"ConnectPeers": list.New(meta.Data{
Aliases: Tags("CPS"),
Tags: Tags("node"),
Label: "Connect Peers",
Description: "connect ONLY to these addresses (disables inbound connections)",
Documentation: lorem,
}),
"Controller": toggle.New(meta.Data{
Aliases: Tags("CN"),
Tags: Tags("node"),
Label: "Enable Controller",
Description: "delivers mining jobs over multicast",
Documentation: lorem,
Default: "false",
}),
"DbType": text.New(meta.Data{
Aliases: Tags("DB"),
Tags: Tags("node"),
Label: "Database Type",
Description: "type of database storage engine to use for node (" +
"only one right now, ffldb)",
Documentation: lorem,
Options: Tags("ffldb"),
Default: "ffldb",
}),
"DisableBanning": toggle.New(meta.Data{
Aliases: Tags("NB"),
Tags: Tags("node"),
Label: "Disable Banning",
Description: "disables banning of misbehaving peers",
Documentation: lorem,
Default: "false",
}),
"DisableCheckpoints": toggle.New(meta.Data{
Aliases: Tags("NCP"),
Tags: Tags("node"),
Label: "Disable Checkpoints",
Description: "disables all checkpoints",
Documentation: lorem,
Default: "false",
}),
"DisableDNSSeed": toggle.New(meta.Data{
Aliases: Tags("NDS"),
Tags: Tags("node"),
Label: "Disable DNS Seed",
Description: "disable seeding of addresses to peers",
Documentation: lorem,
Default: "false",
}),
"DisableListen": toggle.New(meta.Data{
Aliases: Tags("NL"),
Tags: Tags("node", "wallet"),
Label: "Disable Listen",
Description: "disables inbound connections for the peer to peer network",
Documentation: lorem,
Default: "false",
}),
"ExternalIPs": list.New(meta.Data{
Aliases: Tags("EI"),
Tags: Tags("node"),
Label: "External IP Addresses",
Description: "extra addresses to tell peers they can connect to",
Documentation: lorem,
}),
"FreeTxRelayLimit": float.New(meta.Data{
Aliases: Tags("LR"),
Tags: Tags("node"),
Label: "Free Tx Relay Limit",
Description: "limit relay of transactions with no transaction fee to the given amount in thousands of bytes per minute",
Documentation: lorem,
Default: "15.0",
}),
"LAN": toggle.New(meta.Data{
Tags: Tags("node"),
Label: "LAN Testnet Mode",
Description: "run without any connection to nodes on the internet (does not apply on mainnet)",
Documentation: lorem,
Default: "false",
}),
"MaxOrphanTxs": integer.New(meta.Data{
Aliases: Tags("MO"),
Tags: Tags("node"),
Label: "Max Orphan Txs",
Description: "max number of orphan transactions to keep in memory",
Documentation: lorem,
Default: "100",
}),
"MaxPeers": integer.New(meta.Data{
Aliases: Tags("MP"),
Tags: Tags("node"),
Label: "Max Peers",
Description: "maximum number of peers to hold connections with",
Documentation: lorem,
Default: "25",
}),
"MinRelayTxFee": float.New(meta.Data{
Aliases: Tags("MRTF"),
Tags: Tags("node"),
Label: "Min Relay Transaction Fee",
Description: "the minimum transaction fee in DUO/kB to be considered a non-zero fee",
Documentation: lorem,
Default: "0.00001000",
}),
"Network": text.New(meta.Data{
Aliases: Tags("NW"),
Tags: Tags("node", "wallet"),
Label: "Network",
Description: "connect to this network:",
Options: []string{
"mainnet",
"testnet",
"regtestnet",
"simnet",
},
Documentation: lorem,
Default: "mainnet",
}),
"NoCFilters": toggle.New(meta.Data{
Aliases: Tags("NCF"),
Tags: Tags("node"),
Label: "No CFilters",
Description: "disable committed filtering (CF) support",
Documentation: lorem,
Default: "false",
}),
"NodeOff": toggle.New(meta.Data{
Aliases: Tags("NO"),
Tags: Tags("node"),
Label: "Node Off",
Description: "turn off the node backend",
Documentation: lorem,
Default: "false",
}),
"NoPeerBloomFilters": toggle.New(meta.Data{
Aliases: Tags("NPBF"),
Tags: Tags("node"),
Label: "No Peer Bloom Filters",
Description: "disable bloom filtering support",
Documentation: lorem,
Default: "false",
}),
"NoRelayPriority": toggle.New(meta.Data{
Aliases: Tags("NRPR"),
Tags: Tags("node"),
Label: "No Relay Priority",
Description: "do not require free or low-fee transactions to have high priority for relaying",
Documentation: lorem,
Default: "false",
}),
"OnionEnabled": toggle.New(meta.Data{
Aliases: Tags("OE"),
Tags: Tags("node"),
Label: "Onion Enabled",
Description: "enable tor proxy",
Documentation: lorem,
Default: "false",
}),
"OnionProxyAddress": text.New(meta.Data{
Aliases: Tags("OPA"),
Tags: Tags("node"),
Label: "Onion Proxy Address",
Description: "address of tor proxy you want to connect to",
Documentation: lorem,
Default: "127.0.0.1:1108",
}),
"OnionProxyPass": text.New(meta.Data{
Aliases: Tags("OPW"),
Tags: Tags("node"),
Label: "Onion Proxy Password",
Description: "password for tor proxy",
Documentation: lorem,
Default: genPassword(),
}),
"OnionProxyUser": text.New(meta.Data{
Aliases: Tags("OU"),
Tags: Tags("node"),
Label: "Onion Proxy Username",
Description: "tor proxy username",
Documentation: lorem,
Default: "onionproxyuser",
}),
"P2PConnect": list.New(meta.Data{
Aliases: Tags("P2P"),
Tags: Tags("node"),
Label: "P2P Connect",
Description: "list of addresses reachable from connected networks",
Documentation: lorem,
Default: "127.0.0.1:11048",
}),
"P2PListeners": list.New(meta.Data{
Aliases: Tags("LA"),
Tags: Tags("node"),
Label: "P2PListeners",
Description: "list of addresses to bind the node listener to",
Documentation: lorem,
Default: "127.0.0.1:11048,127.0.0.11:11048",
}),
"ProxyAddress": text.New(meta.Data{
Aliases: Tags("PA"),
Tags: Tags("node"),
Label: "Proxy",
Description: "address of proxy to connect to for outbound connections",
Documentation: lorem,
Default: "127.0.0.1:8989",
}),
"ProxyPass": text.New(meta.Data{
Aliases: Tags("PPW"),
Tags: Tags("node"),
Label: "Proxy Pass",
Description: "proxy password, if required",
Documentation: lorem,
Default: genPassword(),
}),
"ProxyUser": text.New(meta.Data{
Aliases: Tags("PU"),
Tags: Tags("node"),
Label: "ProxyUser",
Description: "proxy username, if required",
Documentation: lorem,
Default: "proxyuser",
}),
"RejectNonStd": toggle.New(meta.Data{
Aliases: Tags("REJ"),
Tags: Tags("node"),
Label: "Reject Non Std",
Description: "reject non-standard transactions regardless of the default settings for the active network",
Documentation: lorem,
Default: "false",
}),
"RelayNonStd": toggle.New(meta.Data{
Aliases: Tags("RNS"),
Tags: Tags("node"),
Label: "Relay Nonstandard Transactions",
Description: "relay non-standard transactions regardless of the default settings for the active network",
Documentation: lorem,
Default: "false",
}),
"RPCConnect": text.New(meta.Data{
Aliases: Tags("RA"),
Tags: Tags("node"),
Label: "RPC Connect",
Description: "address of full node RPC for wallet" +
" to connect to",
Documentation: lorem,
Default: "127.0.0.1:11048",
}),
"RPCListeners": list.New(meta.Data{
Aliases: Tags("RL"),
Tags: Tags("node"),
Label: "Node RPC Listeners",
Description: "addresses to listen for RPC connections",
Documentation: lorem,
Default: "127.0.0.1:11048",
}),
"RPCMaxClients": integer.New(meta.Data{
Aliases: Tags("RMXC"),
Tags: Tags("node"),
Label: "Maximum Node RPC Clients",
Description: "maximum number of clients for regular RPC",
Documentation: lorem,
Default: "10",
}),
"RPCMaxConcurrentReqs": integer.New(meta.Data{
Aliases: Tags("RMCR"),
Tags: Tags("node"),
Label: "Maximum Node RPC Concurrent Reqs",
Description: "maximum number of requests to process concurrently",
Documentation: lorem,
Default: fmt.Sprint(runtime.NumCPU()),
}),
"RPCMaxWebsockets": integer.New(meta.Data{
Aliases: Tags("RMWS"),
Tags: Tags("node"),
Label: "Maximum Node RPC Websockets",
Description: "maximum number of websocket clients to allow",
Documentation: lorem,
Default: "25",
}),
"RPCQuirks": toggle.New(meta.Data{
Aliases: Tags("RQ"),
Tags: Tags("node"),
Label: "Emulate Bitcoin Core RPC Quirks",
Description: "enable bugs that replicate bitcoin core RPC's JSON",
Documentation: lorem,
Default: "false",
}),
"SigCacheMaxSize": integer.New(meta.Data{
Aliases: Tags("SCM"),
Tags: Tags("node"),
Label: "Signature Cache Max Size",
Description: "the maximum number of entries in the signature verification cache",
Documentation: lorem,
Default: "100000",
}),
"Solo": toggle.New(meta.Data{
Label: "Solo Generate",
Tags: Tags("node"),
Description: "mine even if not connected to a network",
Documentation: lorem,
Default: "false",
}),
"TorIsolation": toggle.New(meta.Data{
Aliases: Tags("TI"),
Tags: Tags("node"),
Label: "Tor Isolation",
Description: "makes a separate proxy connection for each connection",
Documentation: lorem,
Default: "false",
}),
"TrickleInterval": duration.New(meta.Data{
Aliases: Tags("TKI"),
Tags: Tags("node"),
Label: "Trickle Interval",
Description: "minimum time between attempts to send new inventory to a connected peer",
Documentation: lorem,
Default: "1s",
}),
"TxIndex": toggle.New(meta.Data{
Aliases: Tags("TXI"),
Tags: Tags("node"),
Label: "Tx Index",
Description: "maintain a full hash-based transaction index which makes all transactions available via the getrawtransaction RPC",
Documentation: lorem,
Default: "true",
}),
"UPNP": toggle.New(meta.Data{
Aliases: Tags("UP"),
Tags: Tags("node"),
Label: "UPNP",
Description: "enable UPNP for NAT traversal",
Documentation: lorem,
Default: "false",
}),
"UserAgentComments": list.New(meta.Data{
Aliases: Tags("UA"),
Tags: Tags("node"),
Label: "User Agent Comments",
Description: "comment to add to the user agent -- See BIP 14 for more information",
Documentation: lorem,
}),
"UUID": integer.New(meta.Data{
Label: "UUID",
Description: "instance unique id (32bit random value) (json mangles big 64 bit integers due to float64 numbers)",
Documentation: lorem,
}),
"Whitelists": list.New(meta.Data{
Aliases: Tags("WL"),
Tags: Tags("node"),
Label: "Whitelists",
Description: "peers that you don't want to ever ban",
Documentation: lorem,
}),
},
},
{
Name: "wallet",
Description: "run the wallet server (requires a chain node to function)",
Documentation: lorem,
Commands: []*Command{
{
Name: "drophistory",
Description: "reset the wallet transaction history",
Configs: config.Opts{},
},
},
Configs: config.Opts{
"File": text.New(meta.Data{
Aliases: Tags("WF"),
Tags: Tags("wallet"),
Label: "Wallet File",
Description: "wallet database file",
Documentation: lorem,
Default: "~/.pod/mainnet/wallet.db",
}),
"Pass": text.New(meta.Data{
Aliases: Tags("WPW"),
Label: "Wallet Pass",
Tags: Tags("wallet"),
Description: "password encrypting public data in wallet - only hash is stored" +
" so give on command line or in environment POD_WALLETPASS",
Documentation: lorem,
Default: genPassword(),
}),
"RPCListeners": list.New(meta.Data{
Aliases: Tags("WRL"),
Tags: Tags("wallet"),
Label: "Wallet RPC Listeners",
Description: "addresses for wallet RPC server to listen on",
Documentation: lorem,
}),
"RPCMaxClients": integer.New(
meta.Data{
Aliases: Tags("WRMC"),
Tags: Tags("wallet"),
Label: "Legacy RPC Max Clients",
Description: "maximum number of RPC clients allowed for wallet RPC",
Documentation: lorem,
}),
"RPCMaxWebsockets": integer.New(meta.Data{
Aliases: Tags("WRMWS"),
Tags: Tags("wallet"),
Label: "Legacy RPC Max Websockets",
Description: "maximum number of websocket clients allowed for wallet RPC",
Documentation: lorem,
Default: "25",
}),
"Server": text.New(meta.Data{
Aliases: Tags("WS"),
Tags: Tags("wallet"),
Label: "Wallet Server",
Description: "node address to connect wallet server to",
Documentation: lorem,
},
),
},
},
{
Name: "kopach",
Description: "standalone multicast miner for easy mining farm deployment",
Documentation: lorem,
Configs: config.Opts{
"Generate": toggle.New(meta.Data{
Aliases: Tags("GB"),
Tags: Tags("node", "kopach"),
Label: "Generate Blocks",
Description: "turn on Kopach CPU miner",
Documentation: lorem,
Default: "false",
}),
"GenThreads": integer.New(meta.Data{
Aliases: Tags("GT"),
Tags: Tags("kopach"),
Label: "Generate Threads",
Description: "number of threads to mine with",
Documentation: lorem,
Default: fmt.Sprint(runtime.NumCPU() / 2),
}),
"MulticastPass": text.New(meta.Data{
Aliases: Tags("PM"),
Tags: Tags("node", "kopach"),
Label: "Multicast Pass",
Description: "password that encrypts the connection to the mining controller",
Documentation: lorem,
Default: genPassword(),
}),
},
},
{
Name: "worker",
Description: "single thread worker process, normally started by kopach",
},
},
}
GetConfigBase(c.Configs, c.Name, false)
return
}
const (
// RecommendedSeedLen is the recommended length in bytes for a seed to a master node.
RecommendedSeedLen = 32 // 256 bits
// HardenedKeyStart is the index at which a hardened key starts. Each extended key has 2^31 normal child keys and
// 2^31 hardned child keys. Thus the range for normal child keys is [0, 2^31 - 1] and the range for hardened child
// keys is [2^31, 2^32 - 1].
HardenedKeyStart = 0x80000000 // 2^31
// MinSeedBytes is the minimum number of bytes allowed for a seed to a master node.
MinSeedBytes = 16 // 128 bits
// MaxSeedBytes is the maximum number of bytes allowed for a seed to a master node.
MaxSeedBytes = 64 // 512 bits
// serializedKeyLen is the length of a serialized public or private extended key. It consists of 4 bytes version, 1
// byte depth, 4 bytes fingerprint, 4 bytes child number, 32 bytes chain code, and 33 bytes public/private key data.
serializedKeyLen = 4 + 1 + 4 + 4 + 32 + 33 // 78 bytes
// maxUint8 is the max positive integer which can be serialized in a uint8
maxUint8 = 1<<8 - 1
)
var (
ErrInvalidSeedLen = fmt.Errorf(
"seed length must be between %d and %d "+
"bits", MinSeedBytes*8, MaxSeedBytes*8,
)
)
// GenerateSeed returns a cryptographically secure random seed that can be used as the input for the NewMaster function
// to generate a new master node. The length is in bytes and it must be between 16 and 64 (128 to 512 bits). The
// recommended length is 32 (256 bits) as defined by the RecommendedSeedLen constant.
func GenerateSeed(length uint8) ([]byte, error) {
// Per [BIP32], the seed must be in range [MinSeedBytes, MaxSeedBytes].
if length < MinSeedBytes || length > MaxSeedBytes {
return nil, ErrInvalidSeedLen
}
buf := make([]byte, length)
_, e := rand.Read(buf)
if log.E.Chk(e) {
return nil, e
}
return buf, nil
}
func genPassword() string {
s, e := GenerateSeed(20)
if e != nil {
panic("can't do nothing without entropy! " + e.Error())
}
out := make([]byte, 32)
base32.StdEncoding.Encode(out, s)
return string(out)
}

363
pkg/proc/pkg/cmds/help.go Normal file
View File

@@ -0,0 +1,363 @@
package cmds
import (
"bytes"
"fmt"
"os"
"sort"
"strings"
"sync"
"text/tabwriter"
"github.com/cybriq/proc/pkg/opts/config"
"github.com/cybriq/proc/pkg/util"
)
// Help is a default top level command that subsequent
func Help() (h *Command) {
h = &Command{
Path: nil,
Name: "help",
Description: "Print help information, optionally multiple keywords" +
" can be given and will be searched to generate output",
Documentation: "Uses partial matching, if result is ambiguous, prints general top " +
"level\nhelp and the list of partial match terms.\n\n" +
"If single term exactly matches a command, the command help will" +
" be printed.\n\n" +
"Otherwise, a list showing all items, their paths, and " +
"description are shown.\n\n" +
"Use their full path and the full " +
"documentation for the item will be shown.\n\n" +
"Note that in all cases, options are only recognised after their\n" +
"related subcommand.",
Entrypoint: HelpEntrypoint,
Parent: nil,
Commands: nil,
Configs: nil,
Default: nil,
Mutex: sync.Mutex{},
}
return
}
// IndentTextBlock adds an arbitrary number of tabs to the front of a text
// block that is presumed to already be flowed to ~80 columns.
func IndentTextBlock(s string, tabs int) (o string) {
s = strings.TrimSpace(s)
split := strings.Split(s, strings.Repeat("\n", tabs))
for i := range split {
split[i] = "\t" + split[i]
}
return strings.Join(split, "\n")
}
type CommandInfo struct {
name, description string
}
type CommandInfos []*CommandInfo
func (c CommandInfos) Len() int { return len(c) }
func (c CommandInfos) Less(i, j int) bool { return c[i].name < c[j].name }
func (c CommandInfos) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func HelpEntrypoint(c *Command, args []string) (err error) {
if args == nil {
// no args given, just print top level general help
return
}
foundCommands := &[]*Command{}
fops := make(map[string]config.Option)
foundOptions := &fops
foundCommandWhole := false
foundOptionWhole := false
c.ForEach(func(cm *Command, depth int) bool {
for i := range args {
// check for match of current command name
if strings.Contains(util.Norm(cm.Name), util.Norm(args[i])) {
if util.Norm(cm.Name) == util.Norm(args[i]) {
if len(args) == 1 {
foundCommandWhole = true
*foundCommands = append(*foundCommands, cm)
break
}
}
*foundCommands = append(*foundCommands, cm)
}
// check for matches on configs
for ops := range cm.Configs {
// log.I.Ln(ops, cm.Name, Norm(ops), Norm(args[i]))
if strings.Contains(util.Norm(ops), util.Norm(args[i])) {
// in the case of specifying a command and an option
// and the option is from the command, and there is
// only two args, and the option is fully named, not
// just partial matched, clear found options and
// break to return one command one option,
// which later is recognised to indicate show detail
if len(args) == 2 && len(*foundCommands) == 1 &&
util.Norm(ops) == util.Norm(args[i]) {
if cm.Configs[ops].Path().Equal(cm.Path) {
*foundOptions = make(map[string]config.Option)
(*foundOptions)[ops] = cm.Configs[ops]
foundOptionWhole = true
return false
}
} else {
(*foundOptions)[ops] = cm.Configs[ops]
}
}
}
}
return true
}, 0, 0, c)
var out string
out += fmt.Sprintf("%s - %s\n\n", c.Name, c.Description)
var b bytes.Buffer
w := new(tabwriter.Writer)
// minwidth, tabwidth, padding, padchar, flags
w.Init(&b, 8, 8, 0, '\t', 0)
// log.I.S(bufWriter.String())
switch {
case foundCommandWhole && len(args) == 1:
cm := (*foundCommands)[0]
// Print command help information
var outs CommandInfos
for i := range cm.Commands {
outs = append(outs,
&CommandInfo{
name: cm.Commands[i].Name,
description: cm.Commands[i].Description,
})
}
sort.Sort(outs)
// out += fmt.Sprintf("\n%s - %s\n\n", cm.Path, cm.Description)
out += fmt.Sprintf(
"Help information for command '%s':\n\n",
args[0])
out += fmt.Sprintf("Documentation:\n\n%s\n\n",
IndentTextBlock(cm.Documentation, 1))
if len(cm.Commands) > 0 {
out += fmt.Sprintf("The commands are:\n\n")
for i := range outs {
def := ""
if len(cm.Default) > 0 {
if outs[i].name == cm.Default[len(cm.Default)-1] {
def = " *"
}
}
if _, e := fmt.Fprintf(w, "\t%s %s%s\n",
outs[i].name, outs[i].description,
def); e != nil {
_, _ = fmt.Fprintln(os.Stderr, "error printing columns")
} else {
w.Flush()
out += b.String()
b.Reset()
}
}
if cm.Default != nil {
out += "\n\t\t* indicates default if no subcommand given\n\n"
} else {
out += "\n"
}
out += fmt.Sprintf("for more information on subcommands:\n\n")
out += fmt.Sprintf("\t%s help <subcommand>\n\n", os.Args[0])
}
if len(cm.Configs) > 0 {
out += "Available configuration options on this command:\n\n"
out += fmt.Sprintf("\t-%s %v - %s (default: '%s')\n",
"flag", "[alias1 alias2]", "description", "default")
out += "\t\t(prefix '-' can also be '--', value can follow after space or with '=' and no space)\n\n"
var opts []string
for i := range c.Configs {
opts = append(opts, i)
}
sort.Strings(opts)
for i := range opts {
aliases := c.Configs[opts[i]].Meta().Aliases()
for j := range aliases {
aliases[j] = strings.ToLower(aliases[j])
}
var al string
if len(aliases) > 0 {
al = fmt.Sprint(aliases, " ")
}
out += fmt.Sprintf("\t-%s %v\n\t\t%s (default: '%s')\n", strings.ToLower(opts[i]),
al,
c.Configs[opts[i]].Meta().Description(),
c.Configs[opts[i]].Meta().Default())
}
out += fmt.Sprintf(
"\nUse 'help %s <option>' to get details on option.\n",
cm.Name)
}
case len(*foundOptions) == 1 &&
(len(*foundCommands) == 0 ||
foundOptionWhole):
// For this case there is only one option, and one match, so we
// will print the full details as it is unambiguous.
for i := range *foundOptions {
// there is only one but a range statement grabs it
op := (*foundOptions)[i]
om := op.Meta()
path := op.Path().TrimPrefix().String()
if len(path) > 0 {
path = path + " "
}
search := strings.Join(args, " ")
if len(args) > 1 {
out += fmt.Sprintf(
"Help information for search terms '%s':\n\n", search)
} else {
out += fmt.Sprintf("Help information for option '%s'\n\n",
i)
}
if len(path) > 1 {
out += fmt.Sprintf("Command Path:\n\n\t%s\n\n", path)
}
out += fmt.Sprintf("%s [-%s]\n\n", i, strings.ToLower(i))
out += fmt.Sprintf("\t%s\n\n", om.Description())
out += fmt.Sprintf("Default:\n\n\t%s %s--%s=%s\n\n",
c.Name, path, strings.ToLower(i), om.Default())
out += fmt.Sprintf("Documentation:\n\n%s\n\n",
IndentTextBlock(om.Documentation(), 1))
}
case len(*foundCommands) > 0 || len(*foundOptions) > 0:
// if the text was not a command and one of its options, just
// show all partial matches of both commands and options in
// summary with their relevant paths
plural := ""
search := strings.Join(args, " ")
if len(args) > 1 {
plural = "s"
}
out += fmt.Sprintf(
"Help information for search term%s '%s':\n\n",
plural, search)
if len(*foundCommands)+len(*foundOptions) > 1 {
out += "Multiple matches found:\n\n"
}
if len(*foundCommands) > 0 {
plural := ""
if len(*foundCommands) > 1 {
plural = "s"
}
out += fmt.Sprintf("Command%s\n\n", plural)
for i := range *foundCommands {
cm := (*foundCommands)[i]
fmt.Fprintf(w, "\t%s\t %s\n",
strings.ToLower(cm.Name), cm.Description)
}
out += "\n"
}
if len(*foundOptions) > 0 {
out += fmt.Sprintf("Options:\n\n")
for i := range *foundOptions {
op := (*foundOptions)[i]
om := op.Meta()
path := op.Path().TrimPrefix().String()
if len(path) > 0 {
path = path + " "
}
fmt.Fprintf(w, "\t%s -%s=%s\t%s\n",
op.Path(),
strings.ToLower(i),
om.Default(),
om.Description())
}
}
w.Flush()
out += b.String()
b.Reset()
out += "\n"
default:
cm := c
// Print command help information
out += "Usage:\n\n"
out += fmt.Sprintf("\t%s [arguments] [<subcommand> [arguments]]\n\n",
cm.Name)
var outs CommandInfos
for i := range cm.Commands {
outs = append(outs,
&CommandInfo{
name: cm.Commands[i].Name,
description: cm.Commands[i].Description,
})
}
sort.Sort(outs)
// log.I.S(outs)
plural := ""
pluralVerb := "is"
if len(c.Commands) > 1 {
plural = "s"
pluralVerb = "are"
}
out += fmt.Sprintf("The command%s %s:\n\n", plural, pluralVerb)
var b bytes.Buffer
w := new(tabwriter.Writer)
// minwidth, tabwidth, padding, padchar, flags
w.Init(&b, 8, 8, 0, '\t', 0)
if len(c.Commands) > 0 {
for i := range outs {
def := ""
if len(cm.Default) > 0 {
if outs[i].name == cm.Default[len(cm.Default)-1] {
def = " *"
}
}
if _, e := fmt.Fprintf(w, "\t%s\t %s\n",
outs[i].name+def, outs[i].description,
); e != nil {
_, _ = fmt.Fprintln(os.Stderr, "error printing columns")
} else {
}
}
w.Flush()
out += b.String()
b.Reset()
if cm.Default != nil && cm.Default[0] != cm.Name {
out += "\n\t* indicates default if no subcommand given\n\n"
} else {
out += "\n"
}
}
out += "Available configuration options at top level:\n\n"
var opts []string
for i := range c.Configs {
opts = append(opts, i)
}
sort.Strings(opts)
for i := range opts {
aliases := c.Configs[opts[i]].Meta().Aliases()
for j := range aliases {
aliases[j] = strings.ToLower(aliases[j])
}
var al string
if len(aliases) > 0 {
al = fmt.Sprint(aliases, " ")
}
_, _ = fmt.Fprintf(w, "\t-%s\t%v\n",
strings.ToLower(opts[i])+" "+al,
c.Configs[opts[i]].Meta().Description()+" - default: "+
c.Configs[opts[i]].Meta().Default(),
)
}
fmt.Fprint(w, "\n\tFormat of configuration items:\n\n")
fmt.Fprintf(w, "\t\t-%s\t%v\t\n",
"flag [alias1 alias2]", "description (default: )")
fmt.Fprint(w, "\t\t(prefix '-' can also be '--', value can follow after space or with '=' and no space)\n\n")
w.Flush()
out += b.String()
b.Reset()
out += fmt.Sprintf("For more information:\n\n")
out += fmt.Sprintf("\t%s help <subcommand>\n\n", c.Name)
out += "\tUse 'help <option>' to get details on option.\n"
out += "\tUse 'help help' to learn more about the help function.\n\n"
}
fmt.Print(out)
return
}

8
pkg/proc/pkg/cmds/log.go Normal file
View File

@@ -0,0 +1,8 @@
package cmds
import (
"github.com/cybriq/proc"
log2 "github.com/cybriq/proc/pkg/log"
)
var log = log2.GetLogger(proc.PathBase)

View File

@@ -0,0 +1,233 @@
package cmds
import (
"encoding"
"fmt"
"io"
"os"
"sort"
"strings"
"time"
integer "github.com/cybriq/proc/pkg/opts/Integer"
"github.com/cybriq/proc/pkg/opts/duration"
"github.com/cybriq/proc/pkg/opts/float"
"github.com/cybriq/proc/pkg/opts/list"
"github.com/cybriq/proc/pkg/opts/meta"
"github.com/cybriq/proc/pkg/opts/text"
"github.com/cybriq/proc/pkg/opts/toggle"
path2 "github.com/cybriq/proc/pkg/path"
"github.com/naoina/toml"
)
type Entry struct {
path path2.Path
name string
value interface{}
}
func (e Entry) String() string {
return fmt.Sprint(e.path, "/", e.name, "=", e.value)
}
type Entries []Entry
func (e Entries) Len() int {
return len(e)
}
func (e Entries) Less(i, j int) bool {
iPath, jPath := e[i].String(), e[j].String()
return iPath < jPath
}
func (e Entries) Swap(i, j int) {
e[i], e[j] = e[j], e[i]
}
func walk(p []string, v interface{}, in Entries) (o Entries) {
o = append(o, in...)
var parent []string
for i := range p {
parent = append(parent, p[i])
}
switch vv := v.(type) {
case map[string]interface{}:
for i := range vv {
switch vvv := vv[i].(type) {
case map[string]interface{}:
o = walk(append(parent, i), vvv, o)
default:
o = append(o, Entry{
path: append(parent, i),
name: i,
value: vv[i],
})
}
}
}
return
}
var _ encoding.TextMarshaler = &Command{}
func (c *Command) MarshalText() (text []byte, err error) {
c.ForEach(func(cmd *Command, depth int) bool {
if cmd == nil {
log.I.Ln("cmd empty")
return true
}
cfgNames := make([]string, 0, len(cmd.Configs))
for i := range cmd.Configs {
cfgNames = append(cfgNames, i)
}
if len(cfgNames) < 1 {
return true
}
if cmd.Name != "" {
var cmdPath string
current := cmd.Parent
for current != nil {
if current.Name != "" {
cmdPath = current.Name + "."
}
current = current.Parent
}
text = append(text,
[]byte("# "+cmdPath+cmd.Name+": "+cmd.Description+"\n")...)
text = append(text,
[]byte("["+cmdPath+cmd.Name+"]"+"\n\n")...)
}
sort.Strings(cfgNames)
for _, i := range cfgNames {
md := cmd.Configs[i].Meta()
lq, rq := "", ""
st := cmd.Configs[i].String()
df := md.Default()
switch cmd.Configs[i].Type() {
case meta.Duration, meta.Text:
lq, rq = "\"", "\""
case meta.List:
lq, rq = "[ \"", "\" ]"
st = strings.ReplaceAll(st, ",", "\", \"")
df = strings.ReplaceAll(df, ",", "\", \"")
if st == "" {
lq, rq = "[ ", "]"
}
}
text = append(text,
[]byte("# "+i+" - "+md.Description()+
" - default: "+lq+df+rq+"\n")...)
text = append(text,
[]byte(i+" = "+lq+st+rq+"\n")...)
}
text = append(text, []byte("\n")...)
return true
}, 0, 0, c)
return
}
var _ encoding.TextUnmarshaler = &Command{}
func (c *Command) UnmarshalText(t []byte) (err error) {
var out interface{}
err = toml.Unmarshal(t, &out)
oo := walk([]string{}, out, []Entry{})
sort.Sort(oo)
for i := range oo {
op := c.GetOpt(oo[i].path)
if op != nil {
switch op.Type() {
case meta.Bool:
v := op.(*toggle.Opt)
nv, ok := oo[i].value.(bool)
if ok {
v.FromValue(nv)
}
log.T.Ln("setting value of", oo[i].path, "to", nv)
case meta.Duration:
v := op.(*duration.Opt)
nv, ok := oo[i].value.(time.Duration)
if ok {
v.FromValue(nv)
}
log.T.Ln("setting value of", oo[i].path, "to", nv)
case meta.Float:
v := op.(*float.Opt)
nv, ok := oo[i].value.(float64)
if ok {
v.FromValue(nv)
}
log.T.Ln("setting value of", oo[i].path, "to", nv)
case meta.Integer:
v := op.(*integer.Opt)
nv, ok := oo[i].value.(int64)
if ok {
v.FromValue(nv)
}
log.T.Ln("setting value of", oo[i].path, "to", nv)
case meta.List:
v := op.(*list.Opt)
nv, ok := oo[i].value.([]interface{})
var strings []string
for i := range nv {
strings = append(strings, nv[i].(string))
}
if ok {
v.FromValue(strings)
}
log.T.Ln("setting value of", oo[i].path, "to", nv)
case meta.Text:
v := op.(*text.Opt)
nv, ok := oo[i].value.(string)
if ok {
v.FromValue(nv)
}
log.T.Ln("setting value of", oo[i].path, "to", nv)
default:
log.E.Ln("option type unknown:", oo[i].path, op.Type())
}
} else {
log.D.Ln("option not found:", oo[i].path)
}
}
return nil
}
func (c *Command) LoadConfig() (err error) {
cfgFile := c.GetOpt(path2.Path{c.Name, "ConfigFile"})
var file io.Reader
if file, err = os.Open(cfgFile.Expanded()); err != nil {
log.T.F("creating config file at path: '%s'", cfgFile.Expanded())
// If no config found, create data dir and drop the default in place
return c.SaveConfig()
} else {
var all []byte
all, err = io.ReadAll(file)
err = c.UnmarshalText(all)
if log.E.Chk(err) {
return
}
}
return
}
func (c *Command) SaveConfig() (err error) {
datadir := c.GetOpt(path2.Path{c.Name, "DataDir"})
if err = os.MkdirAll(datadir.Expanded(), 0700); log.E.Chk(err) {
return err
}
var f *os.File
cfgFile := c.GetOpt(path2.From(c.Name + " configfile"))
f, err = os.OpenFile(cfgFile.Expanded(), os.O_RDWR|os.O_CREATE, 0666)
if log.E.Chk(err) {
return
}
var cf []byte
if cf, err = c.MarshalText(); log.E.Chk(err) {
return
}
_, err = f.Write(cf)
log.E.Chk(err)
return
}

View File

@@ -0,0 +1,16 @@
ISC License
Copyright (c) 2013-2017 The btcsuite developers
Copyright (c) 2015-2016 The Decred developers
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@@ -0,0 +1,8 @@
# interrupt
Handle shutdowns cleanly and enable hot reload
Based on the shutdown handling code in
[btcwallet](https://github.com/btcsuite/btcwallet).
As such the ISC license applies to this code.

View File

@@ -0,0 +1,190 @@
package interrupt
import (
"fmt"
"os"
"os/exec"
"os/signal"
"runtime"
"strings"
"syscall"
"go.uber.org/atomic"
"github.com/kardianos/osext"
)
type HandlerWithSource struct {
Source string
Fn func()
}
var (
Restart bool // = true
requested atomic.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 = make(chan struct{})
// 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(chan struct{})
)
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() {
log.I.Ln(
"running interrupt callbacks",
len(interruptCallbacks),
strings.Repeat(" ", 48),
interruptCallbackSources,
)
// run handlers in LIFO order.
for i := range interruptCallbacks {
idx := len(interruptCallbacks) - 1 - i
log.I.Ln("running callback", idx,
interruptCallbackSources[idx])
interruptCallbacks[idx]()
}
log.I.Ln("interrupt handlers finished")
close(HandlersDone)
if Restart {
var file string
var e error
file, e = osext.Executable()
if e != nil {
log.I.Ln(e)
return
}
log.I.Ln("restarting")
if runtime.GOOS != "windows" {
e = syscall.Exec(file, os.Args, os.Environ())
if e != nil {
log.I.Ln(e)
}
} else {
log.I.Ln("doing windows restart")
// procAttr := new(os.ProcAttr)
// procAttr.Files = []*os.File{os.Stdin, os.Stdout, os.Stderr}
// os.StartProcess(os.Args[0], os.Args[1:], procAttr)
var s []string
// s = []string{"cmd.exe", "/C", "start"}
s = append(s, os.Args[0])
// s = append(s, "--delaystart")
s = append(s, os.Args[1:]...)
cmd := exec.Command(s[0], s[1:]...)
log.I.Ln("windows restart done")
if e = cmd.Start(); e != nil {
log.I.Ln(e)
}
// // select{}
// os.Exit(0)
}
}
// 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)
log.I.Ln("received interrupt signal", sig)
requested.Store(true)
invokeCallbacks()
// pprof.Lookup("goroutine").WriteTo(os.Stderr, 2)
// }
break out
case <-ShutdownRequestChan:
// if !requested {
log.I.Ln("received shutdown request - shutting down...")
requested.Store(true)
invokeCallbacks()
break out
// }
case handler := <-addHandlerChan:
// if !requested {
log.D.Ln("adding handler")
interruptCallbacks =
append(interruptCallbacks, handler.Fn)
interruptCallbackSources =
append(interruptCallbackSources, handler.Source)
// }
case <-HandlersDone:
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)
log.I.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)
log.I.Ln("interrupt requested", f, l, requested.Load())
if requested.Load() {
log.I.Ln("requested again")
return
}
requested.Store(true)
close(ShutdownRequestChan)
// qu.PrintChanState()
var ok bool
select {
case _, ok = <-ShutdownRequestChan:
default:
}
log.I.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
log.I.Ln("requesting restart")
Request()
}
// Requested returns true if an interrupt has been requested
func Requested() bool {
return requested.Load()
}

View File

@@ -0,0 +1,8 @@
package interrupt
import (
"github.com/cybriq/proc"
log2 "github.com/cybriq/proc/pkg/log"
)
var log = log2.GetLogger(proc.PathBase)

View File

@@ -0,0 +1,12 @@
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
package interrupt
import (
"os"
"syscall"
)
func init() {
signals = []os.Signal{os.Interrupt, syscall.SIGTERM}
}

408
pkg/proc/pkg/log/log.go Normal file
View File

@@ -0,0 +1,408 @@
package log
import (
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"sync"
"time"
"github.com/cybriq/proc"
"github.com/davecgh/go-spew/spew"
"github.com/gookit/color"
)
// log is your generic Logger creation invocation that uses the version data
// in version.go that provides the current compilation path prefix for making
// relative paths for log printing code locations.
var log = GetLogger(proc.PathBase)
// LogLevel is a code representing a scale of importance and context for log
// entries.
type LogLevel int32
// The LogLevel settings used in proc
const (
Off LogLevel = iota
Fatal
Error
Check
Warn
Info
Debug
Trace
)
type LevelMap map[LogLevel]string
func (l LevelMap) String() (s string) {
ss := make([]string, len(l))
for i := range l {
ss[i] = strings.TrimSpace(l[i])
}
return strings.Join(ss, " ")
}
// LvlStr is a map that provides the uniform width strings that are printed
// to identify the LogLevel of a log entry.
var LvlStr = LevelMap{
Off: "off ",
Fatal: "fatal",
Error: "error",
Warn: "warn ",
Info: "info ",
Check: "check",
Debug: "debug",
Trace: "trace",
}
func GetLevelName(ll LogLevel) string {
return strings.TrimSpace(LvlStr[ll])
}
type (
// Println prints lists of interfaces with spaces in between
Println func(a ...interface{})
// Printf prints like fmt.Println surrounded by log details
Printf func(format string, a ...interface{})
// Prints prints a spew.Sdump for an interface slice
Prints func(a ...interface{})
// Printc accepts a function so that the extra computation can be avoided if
// it is not being viewed
Printc func(closure func() string)
// Chk is a shortcut for printing if there is an error, or returning true
Chk func(e error) bool
// LevelPrinter defines a set of terminal printing primitives that output
// with extra data, time, level, and code location
LevelPrinter struct {
Ln Println
// F prints like fmt.Println surrounded by log details
F Printf
// S uses spew.dump to show the content of a variable
S Prints
// C accepts a function so that the extra computation can be avoided if
// it is not being viewed
C Printc
// Chk is a shortcut for printing if there is an error, or returning
// true
Chk Chk
}
// LevelSpec is a key pair of log level and the text colorizer used
// for it.
LevelSpec struct {
Name string
Colorizer func(format string, a ...interface{}) string
}
// Logger is a set of log printers for the various LogLevel items.
Logger struct {
F, E, W, I, D, T LevelPrinter
}
)
// gLS is a helper to make more compact declarations of LevelSpec names and
// colors by using the LogLevel LvlStr map.
func gLS(lvl LogLevel, r, g, b byte) LevelSpec {
return LevelSpec{
Name: LvlStr[lvl],
Colorizer: color.Bit24(r, g, b, false).Sprintf,
}
}
// LevelSpecs specifies the id, string name and color-printing function
var LevelSpecs = map[LogLevel]LevelSpec{
Off: gLS(Off, 0, 0, 0),
Fatal: gLS(Fatal, 255, 0, 0),
Error: gLS(Error, 255, 128, 0),
Check: gLS(Check, 255, 255, 0),
Warn: gLS(Warn, 255, 255, 0),
Info: gLS(Info, 0, 255, 0),
Debug: gLS(Debug, 0, 128, 255),
Trace: gLS(Trace, 128, 0, 255),
}
var (
tty io.Writer = os.Stderr
file *os.File
path string
writer = tty
writerMx sync.Mutex
logLevel = Info
// App is the name of the application. Change this at the beginning of
// an application main.
App = " main"
// allSubsystems stores all package subsystem names found in the current
// application.
allSubsystems []string
CodeLoc = true
)
func GetAllSubsystems() (o []string) {
writerMx.Lock()
defer writerMx.Unlock()
o = make([]string, len(allSubsystems))
for i := range allSubsystems {
o[i] = allSubsystems[i]
}
return
}
func SetLogFilePath(p string) (err error) {
writerMx.Lock()
defer writerMx.Unlock()
if file != nil {
err = StopLogToFile()
if err != nil {
return err
}
path = p
err = StartLogToFile()
} else {
path = p
}
return
}
func StartLogToFile() (err error) {
writerMx.Lock()
defer writerMx.Unlock()
file, err = os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600)
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", path)
return err
}
writer = io.MultiWriter(tty, file)
return
}
func StopLogToFile() (err error) {
writerMx.Lock()
defer writerMx.Unlock()
writer = tty
if file != nil {
err = file.Close()
file = nil
}
return
}
func SetLogLevel(l LogLevel) {
writerMx.Lock()
defer writerMx.Unlock()
logLevel = l
}
// 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]+))
/usr/local/bin/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.
func GetLoc(skip int, 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
}
// LocTimeStampFormat is a custom time format that provides millisecond precision.
var LocTimeStampFormat = "2006-01-02T15:04:05.000000Z07:00"
var timeStampFormat = time.Stamp
// SetTimeStampFormat sets a custom timeStampFormat for the logger
func SetTimeStampFormat(format string) {
timeStampFormat = format
}
// getTimeText is a helper that returns the current time with the
// timeStampFormat that is configured.
func getTimeText(tsf string) string {
return time.Now().Format(tsf)
}
// joinStrings constructs a string from a slice of interface same as Println but
// without the terminal newline
func joinStrings(sep string, a ...interface{}) func() (o string) {
return func() (o string) {
for i := range a {
o += fmt.Sprint(a[i])
if i < len(a)-1 {
o += sep
}
}
return
}
}
// logPrint is the generic log printing function that provides the base
// format for log entries.
func logPrint(
level LogLevel,
subsystem string,
printFunc func() string,
) func() {
return func() {
if level > Off && level <= logLevel {
formatString := "%v%s%s%-6v %s\n"
loc := ""
tsf := timeStampFormat
if CodeLoc {
formatString = "%-58v%s%s%-6v %s\n"
loc = GetLoc(3, subsystem)
tsf = LocTimeStampFormat
}
var app string
if len(App) > 0 {
fmt.Sprint(" [" + App + "]")
}
s := fmt.Sprintf(
formatString,
loc,
color.Gray.Sprint(getTimeText(tsf)),
app,
LevelSpecs[level].Colorizer(
" "+LevelSpecs[level].Name+" ",
),
printFunc(),
)
writerMx.Lock()
defer writerMx.Unlock()
fmt.Fprintf(writer, s)
}
}
}
// 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)
}
// Add 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 Add(pathBase string) (subsystem string) {
var ok bool
var file string
_, file, _, ok = runtime.Caller(2)
if ok {
r := strings.Split(file, pathBase)
fromRoot := filepath.Base(file)
if len(r) > 1 {
fromRoot = r[1]
}
split := strings.Split(fromRoot, "/")
subsystem = strings.Join(split[:len(split)-1], "/")
writerMx.Lock()
defer writerMx.Unlock()
allSubsystems = append(allSubsystems, subsystem)
sortSubsystemsList()
}
return
}
// GetLogger returns a set of LevelPrinter with their subsystem preloaded
func GetLogger(pathBase string) (l *Logger) {
ss := Add(pathBase)
// fmt.Println("subsystems:", allSubsystems)
return &Logger{
getOnePrinter(Fatal, ss),
getOnePrinter(Error, ss),
getOnePrinter(Warn, ss),
getOnePrinter(Info, ss),
getOnePrinter(Debug, ss),
getOnePrinter(Trace, ss),
}
}
// The collection of the different types of log print functions,
// includes spew.Dump, closure and error check printers.
func _ln(l LogLevel, ss string) Println {
return func(a ...interface{}) {
logPrint(l, ss, joinStrings(" ", a...))()
}
}
func _f(level LogLevel, subsystem string) Printf {
return func(format string, a ...interface{}) {
logPrint(
level, subsystem, func() string {
return fmt.Sprintf(format, a...)
},
)()
}
}
func _s(level LogLevel, subsystem string) Prints {
return func(a ...interface{}) {
logPrint(
level, subsystem, func() string {
return fmt.Sprint("spew:\n\n" + spew.Sdump(a...))
},
)()
}
}
func _c(level LogLevel, subsystem string) Printc {
return func(closure func() string) {
logPrint(level, subsystem, closure)()
}
}
func _chk(level LogLevel, subsystem string) Chk {
return func(e error) (is bool) {
if e != nil {
logPrint(level, subsystem,
joinStrings(" ", e.Error()))()
is = true
}
return
}
}
func getOnePrinter(level LogLevel, 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),
}
}

View File

@@ -0,0 +1,7 @@
package log
import "testing"
func TestLevelMap_String(t *testing.T) {
log.I.Ln(LvlStr)
}

View File

@@ -0,0 +1,8 @@
package integer
import (
"github.com/cybriq/proc"
log2 "github.com/cybriq/proc/pkg/log"
)
var log = log2.GetLogger(proc.PathBase)

View File

@@ -0,0 +1,97 @@
package integer
import (
"strconv"
"strings"
"github.com/cybriq/proc/pkg/opts/config"
"github.com/cybriq/proc/pkg/opts/meta"
"github.com/cybriq/proc/pkg/path"
"go.uber.org/atomic"
)
type Opt struct {
p path.Path
m meta.Metadata
v atomic.Int64
h []Hook
}
func (o *Opt) Path() (p path.Path) {
return o.p
}
func (o *Opt) SetPath(p path.Path) {
o.p = p
}
var _ config.Option = &Opt{}
type Hook func(*Opt) error
func New(m meta.Data, h ...Hook) (o *Opt) {
o = &Opt{m: meta.New(m, meta.Integer), h: h}
_ = o.FromString(m.Default)
return
}
func (o *Opt) Meta() meta.Metadata { return o.m }
func (o *Opt) Type() meta.Type { return o.m.Typ }
func (o *Opt) ToOption() config.Option { return o }
func (o *Opt) RunHooks() (e error) {
for i := range o.h {
e = o.h[i](o)
if e != nil {
return
}
}
return
}
func (o *Opt) FromValue(v int64) *Opt {
o.v.Store(v)
return o
}
func (o *Opt) FromString(s string) (e error) {
s = strings.TrimSpace(s)
var p int64
p, e = strconv.ParseInt(s, 10, 64)
if e != nil {
return e
}
o.v.Store(p)
e = o.RunHooks()
return
}
func (o *Opt) String() (s string) {
return strconv.FormatInt(o.v.Load(), 10)
}
func (o *Opt) Expanded() (s string) {
return o.String()
}
func (o *Opt) SetExpanded(s string) {
err := o.FromString(s)
log.E.Chk(err)
}
func (o *Opt) Value() (c config.Concrete) {
c = config.NewConcrete()
c.Integer = func() int64 { return o.v.Load() }
return
}
func Clamp(o *Opt, min, max int64) func(*Opt) {
return func(o *Opt) {
v := o.v.Load()
if v < min {
o.v.Store(min)
} else if v > max {
o.v.Store(max)
}
}
}

View File

@@ -0,0 +1,48 @@
package config
import (
"time"
"github.com/cybriq/proc/pkg/opts/meta"
"github.com/cybriq/proc/pkg/path"
)
// Concrete is a struct of functions that return the concrete values. Only the
// intended type will return a value, the rest always return zero.
type Concrete struct {
Bool func() bool
Duration func() time.Duration
Float func() float64
Integer func() int64
List func() []string
Text func() string
}
// NewConcrete provides a Concrete with all functions returning zero values
func NewConcrete() Concrete {
return Concrete{
func() bool { return false },
func() time.Duration { return 0 },
func() float64 { return 0 },
func() int64 { return 0 },
func() []string { return nil },
func() string { return "" },
}
}
// Option interface reads and writes string formats for options and returns a
// Concrete value to the appropriate concrete value, with the type indicated.
type Option interface {
FromString(s string) (e error)
String() (s string)
Expanded() (s string)
SetExpanded(s string)
Value() (c Concrete)
Type() (t meta.Type)
Meta() (md meta.Metadata)
RunHooks() (err error)
Path() (p path.Path)
SetPath(p path.Path)
}
type Opts map[string]Option

View File

@@ -0,0 +1,8 @@
package duration
import (
"github.com/cybriq/proc"
log2 "github.com/cybriq/proc/pkg/log"
)
var log = log2.GetLogger(proc.PathBase)

View File

@@ -0,0 +1,95 @@
package duration
import (
"fmt"
"strings"
"time"
"github.com/cybriq/proc/pkg/opts/config"
"github.com/cybriq/proc/pkg/opts/meta"
"github.com/cybriq/proc/pkg/path"
"go.uber.org/atomic"
)
type Opt struct {
p path.Path
m meta.Metadata
v atomic.Duration
h []Hook
}
func (o *Opt) Path() (p path.Path) {
return o.p
}
func (o *Opt) SetPath(p path.Path) {
o.p = p
}
var _ config.Option = &Opt{}
type Hook func(*Opt)
func New(m meta.Data, h ...Hook) (o *Opt) {
o = &Opt{m: meta.New(m, meta.Duration), h: h}
_ = o.FromString(m.Default)
return
}
func (o *Opt) Meta() meta.Metadata { return o.m }
func (o *Opt) Type() meta.Type { return o.m.Typ }
func (o *Opt) ToOption() config.Option { return o }
func (o *Opt) RunHooks() (e error) {
for i := range o.h {
o.h[i](o)
}
return
}
func (o *Opt) FromValue(v time.Duration) *Opt {
o.v.Store(v)
return o
}
func (o *Opt) FromString(s string) (e error) {
s = strings.TrimSpace(s)
var d time.Duration
d, e = time.ParseDuration(s)
if e != nil {
return e
}
o.v.Store(d)
e = o.RunHooks()
return
}
func (o *Opt) String() (s string) {
return fmt.Sprint(o.v.Load())
}
func (o *Opt) Expanded() (s string) {
return o.String()
}
func (o *Opt) SetExpanded(s string) {
err := o.FromString(s)
log.E.Chk(err)
}
func (o *Opt) Value() (c config.Concrete) {
c = config.NewConcrete()
c.Duration = func() time.Duration { return o.v.Load() }
return
}
func Clamp(o *Opt, min, max time.Duration) func(*Opt) {
return func(o *Opt) {
v := o.v.Load()
if v < min {
o.v.Store(min)
} else if v > max {
o.v.Store(max)
}
}
}

View File

@@ -0,0 +1,8 @@
package float
import (
"github.com/cybriq/proc"
log2 "github.com/cybriq/proc/pkg/log"
)
var log = log2.GetLogger(proc.PathBase)

View File

@@ -0,0 +1,97 @@
package float
import (
"strconv"
"strings"
"github.com/cybriq/proc/pkg/opts/config"
"github.com/cybriq/proc/pkg/opts/meta"
"github.com/cybriq/proc/pkg/path"
"go.uber.org/atomic"
)
type Opt struct {
p path.Path
m meta.Metadata
v atomic.Float64
h []Hook
}
func (o *Opt) Path() (p path.Path) {
return o.p
}
func (o *Opt) SetPath(p path.Path) {
o.p = p
}
var _ config.Option = &Opt{}
type Hook func(*Opt) error
func New(m meta.Data, h ...Hook) (o *Opt) {
o = &Opt{m: meta.New(m, meta.Float), h: h}
_ = o.FromString(m.Default)
return
}
func (o *Opt) Meta() meta.Metadata { return o.m }
func (o *Opt) Type() meta.Type { return o.m.Typ }
func (o *Opt) ToOption() config.Option { return o }
func (o *Opt) RunHooks() (e error) {
for i := range o.h {
e = o.h[i](o)
if e != nil {
return
}
}
return
}
func (o *Opt) FromValue(v float64) *Opt {
o.v.Store(v)
return o
}
func (o *Opt) FromString(s string) (e error) {
s = strings.TrimSpace(s)
var p float64
p, e = strconv.ParseFloat(s, 64)
if e != nil {
return e
}
o.v.Store(p)
e = o.RunHooks()
return
}
func (o *Opt) String() (s string) {
return strconv.FormatFloat(o.v.Load(), 'f', -1, 64)
}
func (o *Opt) Expanded() (s string) {
return o.String()
}
func (o *Opt) SetExpanded(s string) {
err := o.FromString(s)
log.E.Chk(err)
}
func (o *Opt) Value() (c config.Concrete) {
c = config.NewConcrete()
c.Float = func() float64 { return o.v.Load() }
return
}
func Clamp(o *Opt, min, max float64) func(*Opt) {
return func(o *Opt) {
v := o.v.Load()
if v < min {
o.v.Store(min)
} else if v > max {
o.v.Store(max)
}
}
}

View File

@@ -0,0 +1,8 @@
package list
import (
"github.com/cybriq/proc"
log2 "github.com/cybriq/proc/pkg/log"
)
var log = log2.GetLogger(proc.PathBase)

View File

@@ -0,0 +1,118 @@
package list
import (
"strings"
"github.com/cybriq/proc/pkg/opts/config"
"github.com/cybriq/proc/pkg/opts/meta"
"github.com/cybriq/proc/pkg/opts/normalize"
"github.com/cybriq/proc/pkg/path"
"go.uber.org/atomic"
)
type Opt struct {
p path.Path
m meta.Metadata
v atomic.Value
x atomic.Value
h []Hook
}
var _ config.Option = &Opt{}
func (o *Opt) Path() (p path.Path) {
return o.p
}
func (o *Opt) SetPath(p path.Path) {
o.p = p
}
type Hook func(*Opt) error
func New(m meta.Data, h ...Hook) (o *Opt) {
o = &Opt{m: meta.New(m, meta.List), h: h}
_ = o.FromString(m.Default)
return
}
func (o *Opt) Meta() meta.Metadata { return o.m }
func (o *Opt) Type() meta.Type { return o.m.Typ }
func (o *Opt) ToOption() config.Option { return o }
func (o *Opt) RunHooks() (e error) {
for i := range o.h {
e = o.h[i](o)
if e != nil {
return
}
}
return
}
func (o *Opt) FromValue(v []string) *Opt {
o.v.Store(v)
return o
}
func (o *Opt) FromString(s string) (e error) {
s = strings.TrimSpace(s)
split := strings.Split(s, ",")
o.v.Store(split)
e = o.RunHooks()
return
}
func (o *Opt) String() (s string) {
return strings.Join(o.v.Load().([]string), ",")
}
func (o *Opt) Expanded() (s string) {
return o.String()
}
func (o *Opt) SetExpanded(s string) {
err := o.FromString(s)
log.E.Chk(err)
}
func (o *Opt) Value() (c config.Concrete) {
c = config.NewConcrete()
c.List = func() []string { return o.v.Load().([]string) }
return
}
// NormalizeNetworkAddress checks correctness of a network address
// specification, and adds a default path if needed, and enforces whether the
// port requires root permission and clamps it if not.
func NormalizeNetworkAddress(defaultPort string,
userOnly bool) func(*Opt) error {
return func(o *Opt) (e error) {
var a []string
a, e = normalize.Addresses(
o.v.Load().([]string), defaultPort, userOnly)
if !log.E.Chk(e) {
o.x.Store(a)
}
return
}
}
// NormalizeFilesystemPath cleans a directory specification, expands the ~ home
// folder shortcut, and if abs is set to true, returns the absolute path from
// filesystem root
func NormalizeFilesystemPath(abs bool, appName string) func(*Opt) error {
return func(o *Opt) (e error) {
strings := o.v.Load().([]string)
for i := range strings {
var cleaned string
cleaned, e = normalize.ResolvePath(strings[i], appName, abs)
if !log.E.Chk(e) {
strings[i] = cleaned
}
}
o.x.Store(strings)
return
}
}

View File

@@ -0,0 +1,51 @@
package meta
type Type string
// Type has same name as string for neater comparisons.
const (
Bool Type = "Bool"
Duration Type = "Duration"
Float Type = "Float"
Integer Type = "Integer"
List Type = "List"
Text Type = "Text"
)
// Data is the specification for a Metadata
type Data struct {
Aliases []string
Tags []string
Label string
Description string
Documentation string
Default string
Options []string
}
// Metadata is a set of accessor functions that never write to the store and
// thus do not create race conditions.
type Metadata struct {
Aliases func() []string
Tags func() []string
Label func() string
Description func() string
Documentation func() string
Default func() string
Options func() []string
Typ Type
}
// New loads Data into a Metadata.
func New(d Data, t Type) Metadata {
return Metadata{
func() []string { return d.Aliases },
func() []string { return d.Tags },
func() string { return d.Label },
func() string { return d.Description },
func() string { return d.Documentation },
func() string { return d.Default },
func() []string { return d.Options },
t,
}
}

View File

@@ -0,0 +1,61 @@
package normalize
import (
"net"
"strconv"
)
// Address returns addr with the passed default port appended if there is not
// already a port specified.
func Address(addr, defaultPort string, userOnly bool) (a string, e error) {
var p string
a, p, e = net.SplitHostPort(addr)
if log.E.Chk(e) || p == "" {
return net.JoinHostPort(a, defaultPort), e
}
if userOnly {
p = ClampPortRange(p, defaultPort, 1024, 65535)
} else {
p = ClampPortRange(p, defaultPort, 1, 65535)
}
return net.JoinHostPort(a, p), e
}
// 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, userOnly bool) (a []string,
e error) {
for i := range addrs {
addrs[i], e = Address(addrs[i], defaultPort, userOnly)
}
a = RemoveDuplicateAddresses(addrs)
return
}
// 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
}
func ClampPortRange(port, defaultPort string, min, max int) string {
p, err := strconv.Atoi(port)
if err != nil {
return defaultPort
}
if p < min {
port = strconv.FormatInt(int64(min), 10)
} else if p > max {
port = strconv.FormatInt(int64(max), 10)
}
return port
}

View File

@@ -0,0 +1,8 @@
package normalize
import (
"github.com/cybriq/proc"
log2 "github.com/cybriq/proc/pkg/log"
)
var log = log2.GetLogger(proc.PathBase)

View File

@@ -0,0 +1,47 @@
package normalize
import (
"os"
"os/user"
"path/filepath"
"strings"
"github.com/cybriq/proc/pkg/appdata"
)
func ResolvePath(input, appName string, abs bool) (cleaned string, e error) {
if strings.HasPrefix(input, "~") {
homeDir := getHomeDir()
input = strings.Replace(input, "~", homeDir, 1)
cleaned = filepath.Clean(input)
} else {
if abs {
if cleaned, e = filepath.Abs(cleaned); log.E.Chk(e) {
return
}
// if the path is relative, either ./ or not starting with a / then
// we assume the path is relative to the app data directory
} else if !strings.HasPrefix(string(os.PathSeparator), cleaned) ||
strings.HasPrefix("."+string(os.PathSeparator), cleaned) {
cleaned = filepath.Join(appdata.Dir(appName, false), cleaned)
}
}
return
}
func getHomeDir() (homeDir string) {
var usr *user.User
var e error
if usr, e = user.Current(); !log.E.Chk(e) {
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")
}
return homeDir
}

View File

@@ -0,0 +1,8 @@
package text
import (
"github.com/cybriq/proc"
log2 "github.com/cybriq/proc/pkg/log"
)
var log = log2.GetLogger(proc.PathBase)

View File

@@ -0,0 +1,111 @@
package text
import (
"strings"
"github.com/cybriq/proc/pkg/opts/config"
"github.com/cybriq/proc/pkg/opts/meta"
"github.com/cybriq/proc/pkg/opts/normalize"
"github.com/cybriq/proc/pkg/path"
"go.uber.org/atomic"
)
type Opt struct {
p path.Path
m meta.Metadata
v atomic.String
x atomic.String
h []Hook
}
func (o *Opt) Path() (p path.Path) {
return o.p
}
func (o *Opt) SetPath(p path.Path) {
o.p = p
}
var _ config.Option = &Opt{}
type Hook func(*Opt) error
func New(m meta.Data, h ...Hook) (o *Opt) {
o = &Opt{m: meta.New(m, meta.Text), h: h}
_ = o.FromString(m.Default)
return
}
func (o *Opt) Meta() meta.Metadata { return o.m }
func (o *Opt) Type() meta.Type { return o.m.Typ }
func (o *Opt) ToOption() config.Option { return o }
func (o *Opt) RunHooks() (e error) {
for i := range o.h {
e = o.h[i](o)
if e != nil {
return
}
}
return
}
func (o *Opt) FromValue(v string) *Opt {
o.v.Store(v)
return o
}
func (o *Opt) FromString(s string) (e error) {
s = strings.TrimSpace(s)
o.v.Store(s)
e = o.RunHooks()
return
}
func (o *Opt) String() (s string) {
return o.v.Load()
}
func (o *Opt) Expanded() (s string) {
return o.x.Load()
}
func (o *Opt) SetExpanded(s string) {
o.x.Store(s)
}
func (o *Opt) Value() (c config.Concrete) {
c = config.NewConcrete()
c.Text = func() string { return o.v.Load() }
return
}
// NormalizeNetworkAddress checks correctness of a network address
// specification, and adds a default path if needed, and enforces whether the
// port requires root permission and clamps it if not.
func NormalizeNetworkAddress(defaultPort string,
userOnly bool) func(*Opt) error {
return func(o *Opt) (e error) {
var a string
a, e = normalize.Address(o.v.Load(), defaultPort, userOnly)
if !log.E.Chk(e) {
o.x.Store(a)
}
return
}
}
// NormalizeFilesystemPath cleans a directory specification, expands the ~ home
// folder shortcut, and if abs is set to true, returns the absolute path from
// filesystem root.
func NormalizeFilesystemPath(abs bool, appName string) func(*Opt) error {
return func(o *Opt) (e error) {
var cleaned string
cleaned, e = normalize.ResolvePath(o.v.Load(), appName, abs)
if !log.E.Chk(e) {
o.x.Store(cleaned)
}
return
}
}

View File

@@ -0,0 +1,8 @@
package toggle
import (
"github.com/cybriq/proc"
log2 "github.com/cybriq/proc/pkg/log"
)
var log = log2.GetLogger(proc.PathBase)

View File

@@ -0,0 +1,91 @@
package toggle
import (
"fmt"
"strconv"
"strings"
"github.com/cybriq/proc/pkg/opts/config"
"github.com/cybriq/proc/pkg/path"
"go.uber.org/atomic"
"github.com/cybriq/proc/pkg/opts/meta"
)
type Opt struct {
p path.Path
m meta.Metadata
v atomic.Bool
h []Hook
}
func (o *Opt) Path() (p path.Path) {
return o.p
}
func (o *Opt) SetPath(p path.Path) {
o.p = p
}
var _ config.Option = &Opt{}
type Hook func(*Opt) error
func New(m meta.Data, h ...Hook) (o *Opt) {
m.Default = "false"
o = &Opt{m: meta.New(m, meta.Bool), h: h}
_ = o.FromString(m.Default)
return
}
func (o *Opt) Meta() meta.Metadata { return o.m }
func (o *Opt) Type() meta.Type { return o.m.Typ }
func (o *Opt) ToOption() config.Option { return o }
func (o *Opt) RunHooks() (e error) {
for i := range o.h {
e = o.h[i](o)
if e != nil {
return
}
}
return
}
func (o *Opt) FromValue(v bool) *Opt {
o.v.Store(v)
return o
}
func (o *Opt) FromString(s string) (e error) {
s = strings.TrimSpace(s)
switch s {
case "f", "false", "off", "-":
o.v.Store(false)
case "t", "true", "on", "+":
o.v.Store(true)
default:
return fmt.Errorf("string '%s' does not parse to boolean", s)
}
e = o.RunHooks()
return
}
func (o *Opt) String() (s string) {
return strconv.FormatBool(o.v.Load())
}
func (o *Opt) Expanded() (s string) {
return o.String()
}
func (o *Opt) SetExpanded(s string) {
err := o.FromString(s)
log.E.Chk(err)
}
func (o *Opt) Value() (c config.Concrete) {
c = config.NewConcrete()
c.Bool = func() bool { return o.v.Load() }
return
}

8
pkg/proc/pkg/path/log.go Normal file
View File

@@ -0,0 +1,8 @@
package path
import (
"github.com/cybriq/proc"
log2 "github.com/cybriq/proc/pkg/log"
)
var log = log2.GetLogger(proc.PathBase)

66
pkg/proc/pkg/path/path.go Normal file
View File

@@ -0,0 +1,66 @@
package path
import (
"strings"
"github.com/cybriq/proc/pkg/util"
)
type Path []string
func (p Path) TrimPrefix() Path {
if len(p) > 1 {
return p[1:]
}
return p[:0]
}
func (p Path) String() string {
return strings.Join(p, " ")
}
func From(s string) (p Path) {
p = strings.Split(s, " ")
return
}
func (p Path) Parent() (p1 Path) {
if len(p) > 0 {
p1 = p[:len(p)-1]
}
return
}
func (p Path) Child(child string) (p1 Path) {
p1 = append(p, child)
// log.I.Ln(p, p1)
return
}
func (p Path) Common(p2 Path) (o Path) {
for i := range p {
if len(p2) < i {
if p[i] == p2[i] {
o = append(o, p[i])
}
}
}
return
}
func (p Path) Equal(p2 Path) bool {
if len(p) == len(p2) {
for i := range p {
if util.Norm(p[i]) !=
util.Norm(p2[i]) {
return false
}
}
return true
}
return false
}
func GetIndent(d int) string {
return strings.Repeat("\t", d)
}

View File

@@ -0,0 +1,67 @@
package util
import (
"os"
"path/filepath"
"runtime"
"strings"
)
// 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
}
func Norm(s string) string {
return strings.ToLower(s)
}

38
pkg/proc/version.go Normal file
View File

@@ -0,0 +1,38 @@
package proc
import (
"fmt"
)
var (
// URL is the git URL for the repository.
URL = "github.com/cybriq/proc"
// GitRef is the gitref, as in refs/heads/branchname.
GitRef = "refs/heads/master"
// ParentGitCommit is the commit hash of the parent HEAD.
ParentGitCommit = "85b3415a92a7b1cd3ce86d040105408aa5159e0c"
// BuildTime stores the time when the current binary was built.
BuildTime = "2022-12-30T16:05:24Z"
// SemVer lists the (latest) git tag on the release.
SemVer = "v0.20.10"
// PathBase is the path base returned from runtime caller.
PathBase = "/home/loki/src/github.com/cybriq/proc/"
// Major is the major number from the tag.
Major = 0
// Minor is the minor number from the tag.
Minor = 20
// Patch is the patch version number from the tag.
Patch = 10
)
// Version returns a pretty printed version information string.
func Version() string {
return fmt.Sprint(
"\nRepository Information\n",
"\tGit repository: "+URL+"\n",
"\tBranch: "+GitRef+"\n",
"\tParentGitCommit: "+ParentGitCommit+"\n",
"\tBuilt: "+BuildTime+"\n",
"\tSemVer: "+SemVer+"\n",
)
}