moving proc inside
This commit is contained in:
1
pkg/proc
1
pkg/proc
Submodule pkg/proc deleted from ee185b6d80
5
pkg/proc/.gitignore
vendored
Normal file
5
pkg/proc/.gitignore
vendored
Normal 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
24
pkg/proc/LICENSE
Normal 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
97
pkg/proc/README.md
Normal 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.
|
||||
58
pkg/proc/cmd/logtest/logtest.go
Normal file
58
pkg/proc/cmd/logtest/logtest.go
Normal 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
18
pkg/proc/go.mod
Normal 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
31
pkg/proc/go.sum
Normal 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
44
pkg/proc/pkg/app/app.go
Normal 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
|
||||
}
|
||||
28
pkg/proc/pkg/app/app_test.go
Normal file
28
pkg/proc/pkg/app/app_test.go
Normal 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
8
pkg/proc/pkg/app/log.go
Normal 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)
|
||||
88
pkg/proc/pkg/appdata/appdata.go
Normal file
88
pkg/proc/pkg/appdata/appdata.go
Normal 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)
|
||||
}
|
||||
212
pkg/proc/pkg/appdata/appdata_test.go
Normal file
212
pkg/proc/pkg/appdata/appdata_test.go
Normal 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
226
pkg/proc/pkg/cmds/args.go
Normal 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
|
||||
}
|
||||
295
pkg/proc/pkg/cmds/commands.go
Normal file
295
pkg/proc/pkg/cmds/commands.go
Normal 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
|
||||
}
|
||||
278
pkg/proc/pkg/cmds/commands_test.go
Normal file
278
pkg/proc/pkg/cmds/commands_test.go
Normal 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
100
pkg/proc/pkg/cmds/env.go
Normal 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
|
||||
}
|
||||
912
pkg/proc/pkg/cmds/example.go
Normal file
912
pkg/proc/pkg/cmds/example.go
Normal 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
363
pkg/proc/pkg/cmds/help.go
Normal 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
8
pkg/proc/pkg/cmds/log.go
Normal 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)
|
||||
233
pkg/proc/pkg/cmds/marshal.go
Normal file
233
pkg/proc/pkg/cmds/marshal.go
Normal 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
|
||||
}
|
||||
16
pkg/proc/pkg/interrupt/LICENSE
Normal file
16
pkg/proc/pkg/interrupt/LICENSE
Normal 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.
|
||||
8
pkg/proc/pkg/interrupt/README.md
Normal file
8
pkg/proc/pkg/interrupt/README.md
Normal 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.
|
||||
190
pkg/proc/pkg/interrupt/interrupt.go
Normal file
190
pkg/proc/pkg/interrupt/interrupt.go
Normal 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()
|
||||
}
|
||||
8
pkg/proc/pkg/interrupt/log.go
Normal file
8
pkg/proc/pkg/interrupt/log.go
Normal 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)
|
||||
12
pkg/proc/pkg/interrupt/sigterm.go
Normal file
12
pkg/proc/pkg/interrupt/sigterm.go
Normal 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
408
pkg/proc/pkg/log/log.go
Normal 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),
|
||||
}
|
||||
}
|
||||
7
pkg/proc/pkg/log/log_test.go
Normal file
7
pkg/proc/pkg/log/log_test.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package log
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestLevelMap_String(t *testing.T) {
|
||||
log.I.Ln(LvlStr)
|
||||
}
|
||||
8
pkg/proc/pkg/opts/Integer/log.go
Normal file
8
pkg/proc/pkg/opts/Integer/log.go
Normal 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)
|
||||
97
pkg/proc/pkg/opts/Integer/spec.go
Normal file
97
pkg/proc/pkg/opts/Integer/spec.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
48
pkg/proc/pkg/opts/config/interface.go
Normal file
48
pkg/proc/pkg/opts/config/interface.go
Normal 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
|
||||
8
pkg/proc/pkg/opts/duration/log.go
Normal file
8
pkg/proc/pkg/opts/duration/log.go
Normal 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)
|
||||
95
pkg/proc/pkg/opts/duration/spec.go
Normal file
95
pkg/proc/pkg/opts/duration/spec.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
8
pkg/proc/pkg/opts/float/log.go
Normal file
8
pkg/proc/pkg/opts/float/log.go
Normal 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)
|
||||
97
pkg/proc/pkg/opts/float/spec.go
Normal file
97
pkg/proc/pkg/opts/float/spec.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
8
pkg/proc/pkg/opts/list/log.go
Normal file
8
pkg/proc/pkg/opts/list/log.go
Normal 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)
|
||||
118
pkg/proc/pkg/opts/list/spec.go
Normal file
118
pkg/proc/pkg/opts/list/spec.go
Normal 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
|
||||
}
|
||||
}
|
||||
51
pkg/proc/pkg/opts/meta/metadata.go
Normal file
51
pkg/proc/pkg/opts/meta/metadata.go
Normal 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,
|
||||
}
|
||||
}
|
||||
61
pkg/proc/pkg/opts/normalize/addresses.go
Normal file
61
pkg/proc/pkg/opts/normalize/addresses.go
Normal 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
|
||||
}
|
||||
8
pkg/proc/pkg/opts/normalize/log.go
Normal file
8
pkg/proc/pkg/opts/normalize/log.go
Normal 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)
|
||||
47
pkg/proc/pkg/opts/normalize/paths.go
Normal file
47
pkg/proc/pkg/opts/normalize/paths.go
Normal 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
|
||||
}
|
||||
8
pkg/proc/pkg/opts/text/log.go
Normal file
8
pkg/proc/pkg/opts/text/log.go
Normal 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)
|
||||
111
pkg/proc/pkg/opts/text/spec.go
Normal file
111
pkg/proc/pkg/opts/text/spec.go
Normal 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
|
||||
}
|
||||
}
|
||||
8
pkg/proc/pkg/opts/toggle/log.go
Normal file
8
pkg/proc/pkg/opts/toggle/log.go
Normal 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)
|
||||
91
pkg/proc/pkg/opts/toggle/spec.go
Normal file
91
pkg/proc/pkg/opts/toggle/spec.go
Normal 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
8
pkg/proc/pkg/path/log.go
Normal 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
66
pkg/proc/pkg/path/path.go
Normal 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)
|
||||
}
|
||||
67
pkg/proc/pkg/util/apputil.go
Normal file
67
pkg/proc/pkg/util/apputil.go
Normal 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
38
pkg/proc/version.go
Normal 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",
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user