commit
a0062b93b2
@ -0,0 +1,6 @@ |
||||
.idea |
||||
tmp |
||||
*.db |
||||
*.iml |
||||
.env |
||||
.env.* |
@ -0,0 +1,379 @@ |
||||
package cmd |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"os" |
||||
"strings" |
||||
) |
||||
|
||||
type DyadicValue struct { |
||||
Value any |
||||
Another any |
||||
} |
||||
|
||||
func (d *DyadicValue) GetValue() any { |
||||
if d.Value != nil { |
||||
return d.Value |
||||
} |
||||
return d.Another |
||||
} |
||||
|
||||
type CommandAction func(*Command, map[string]any, ...any) error |
||||
|
||||
type Command struct { |
||||
name string |
||||
usage *DyadicValue |
||||
version *Option |
||||
help *Option |
||||
description string |
||||
longDescription string |
||||
arguments []*Argument |
||||
options []*Option |
||||
types map[string]TypeParser |
||||
globalTypes map[string]TypeParser |
||||
commands []*Command |
||||
program *Command |
||||
action CommandAction |
||||
} |
||||
|
||||
func New(name string, desc ...string) *Command { |
||||
cmd := &Command{ |
||||
types: make(map[string]TypeParser), |
||||
} |
||||
cmd.Version("1.0.0").Help("") |
||||
switch len(desc) { |
||||
case 0: |
||||
return cmd.Name(name) |
||||
case 1: |
||||
return cmd.Name(name).Description(desc[0]) |
||||
default: |
||||
return cmd.Name(name).Description(desc[0], desc[1:]...) |
||||
} |
||||
} |
||||
|
||||
func (c *Command) Name(name string) *Command { |
||||
if len(name) == 0 { |
||||
panic(errors.New("empty command name")) |
||||
} |
||||
|
||||
// 仅仅支持二级,所以只需要检查 Command.program.commands 中是否重复
|
||||
if c.program != nil { |
||||
for _, command := range c.program.commands { |
||||
if command.name == name { |
||||
panic(errors.New("duplicated command name")) |
||||
} |
||||
} |
||||
} |
||||
|
||||
c.name = name |
||||
|
||||
return c |
||||
} |
||||
|
||||
func (c *Command) GetName() string { |
||||
return c.name |
||||
} |
||||
|
||||
func (c *Command) Type(name string, parser func(...string) any) *Command { |
||||
c.types[name] = parser |
||||
return c |
||||
} |
||||
|
||||
func (c *Command) GlobalType(name string, parser func(...string) any) *Command { |
||||
if c.program != nil { |
||||
c.program.GlobalType(name, parser) |
||||
} else { |
||||
c.globalTypes[name] = parser |
||||
} |
||||
return c |
||||
} |
||||
|
||||
func (c *Command) Command(name string, desc ...string) *Command { |
||||
program := c.program |
||||
if program == nil { |
||||
program = c |
||||
} |
||||
|
||||
cmd := New(name, desc...) |
||||
cmd.program = program |
||||
|
||||
program.commands = append(program.commands, cmd) |
||||
|
||||
return cmd |
||||
} |
||||
|
||||
func (c *Command) Version(ver string) *Command { |
||||
if c.version == nil { |
||||
c.VersionOption("-v, --version", "Show version number") |
||||
} |
||||
if len(ver) > 0 { |
||||
c.version.RuntimeValue = []string{ver} |
||||
} else { |
||||
c.version.RuntimeValue = make([]string, 0) |
||||
} |
||||
return c |
||||
} |
||||
|
||||
func (c *Command) VersionOption(expr string, desc ...string) *Command { |
||||
ver, err := ensureOption(c, expr, desc, "version") |
||||
if errors.Is(err, ErrOptionDefinition) { |
||||
panic(ErrVersionDefinition) |
||||
} else if err != nil { |
||||
panic(err) |
||||
} |
||||
if c.help != nil { |
||||
err = hasConflicts(c.help, ver) |
||||
if err != nil { |
||||
panic(errors.New("version option conflicts with help option, " + err.Error())) |
||||
} |
||||
} |
||||
c.version = ver |
||||
return c |
||||
} |
||||
|
||||
func (c *Command) GetVersion(opts ...*ShowOptions) string { |
||||
if c.program != nil { |
||||
return c.program.GetVersion() |
||||
} |
||||
if c.version == nil { |
||||
c.Version("1.0.0") |
||||
} |
||||
return versionToString(c, resolveShowOptions(opts)) |
||||
} |
||||
|
||||
func (c *Command) ShowVersion(opts ...*ShowOptions) error { |
||||
return showMessage(resolveShowOptions(opts), c.GetVersion) |
||||
} |
||||
|
||||
func (c *Command) Usage(usage string) *Command { |
||||
if c.usage == nil { |
||||
c.usage = new(DyadicValue) |
||||
} |
||||
c.usage.Value = usage |
||||
return c |
||||
} |
||||
|
||||
func (c *Command) GetUsage(opts ...*ShowOptions) string { |
||||
if c.usage != nil { |
||||
usage := c.usage.GetValue() |
||||
if usage != nil { |
||||
return usage.(string) |
||||
} |
||||
} |
||||
s := usageToString(c, resolveShowOptions(opts)) |
||||
c.usage = new(DyadicValue) |
||||
c.usage.Another = s |
||||
return s |
||||
} |
||||
|
||||
func (c *Command) ShowUsage(opts ...*ShowOptions) error { |
||||
return showMessage(resolveShowOptions(opts), c.GetUsage) |
||||
} |
||||
|
||||
func (c *Command) Description(desc string, longDesc ...string) *Command { |
||||
c.description = desc |
||||
if len(longDesc) > 0 { |
||||
c.longDescription = strings.Join(longDesc, "\n") |
||||
} |
||||
return c |
||||
} |
||||
|
||||
func (c *Command) GetDescription(opts ...*ShowOptions) string { |
||||
o := resolveShowOptions(opts) |
||||
var s string |
||||
if o.Long { |
||||
s = c.longDescription |
||||
} else { |
||||
s = c.description |
||||
} |
||||
if len(s) == 0 { |
||||
return "" |
||||
} |
||||
if o.Titles { |
||||
return "DESCRIPTION:\n " + s |
||||
} |
||||
return s |
||||
} |
||||
|
||||
func (c *Command) ShowDescription(opts ...*ShowOptions) error { |
||||
return showMessage(resolveShowOptions(opts), c.GetDescription) |
||||
} |
||||
|
||||
func (c *Command) Help(help string) *Command { |
||||
if c.help == nil { |
||||
c.HelpOption("-h, --help", "Show help information") |
||||
} |
||||
if len(help) > 0 { |
||||
c.help.RuntimeValue = []string{help} |
||||
} else { |
||||
c.help.RuntimeValue = make([]string, 0) |
||||
} |
||||
return c |
||||
} |
||||
|
||||
func (c *Command) HelpOption(expr string, desc ...string) *Command { |
||||
help, err := ensureOption(c, expr, desc, "help") |
||||
if errors.Is(err, ErrOptionDefinition) { |
||||
panic(ErrVersionDefinition) |
||||
} else if err != nil { |
||||
panic(err) |
||||
} |
||||
if c.version != nil { |
||||
err = hasConflicts(help, c.version) |
||||
if err != nil { |
||||
panic(errors.New("help option conflicts with version option, " + err.Error())) |
||||
} |
||||
} |
||||
c.help = help |
||||
return c |
||||
} |
||||
|
||||
func (c *Command) GetHelp(opts ...*ShowOptions) string { |
||||
if c.help != nil { |
||||
if len(c.help.RuntimeValue) > 0 { |
||||
return c.help.RuntimeValue[0] |
||||
} |
||||
if c.help.DefaultValue != nil { |
||||
return c.help.DefaultValue.(string) |
||||
} |
||||
} |
||||
s := helpToString(c, resolveShowOptions(opts)) |
||||
c.help.DefaultValue = s |
||||
return s |
||||
} |
||||
|
||||
func (c *Command) ShowHelp(opts ...*ShowOptions) error { |
||||
return showMessage(resolveShowOptions(opts), c.GetHelp) |
||||
} |
||||
|
||||
func (c *Command) Argument(expr string, desc ...string) *Command { |
||||
a, err := createArgument(expr) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
switch len(desc) { |
||||
case 0: |
||||
break |
||||
case 1: |
||||
a.Description = desc[0] |
||||
default: |
||||
a.Description = desc[0] |
||||
a.LongDescription = desc[1] |
||||
} |
||||
var lastArg *Argument |
||||
for _, a2 := range c.arguments { |
||||
lastArg = a2 |
||||
if a2.Name == a.Name { |
||||
panic(errors.New("duplicated arguments definition")) |
||||
} |
||||
} |
||||
if lastArg != nil { |
||||
if lastArg.Variadic { |
||||
panic(errors.New("after variadic argument")) |
||||
} |
||||
if a.Required && !lastArg.Required { |
||||
panic(errors.New("after optional argument")) |
||||
} |
||||
} |
||||
c.arguments = append(c.arguments, a) |
||||
return c |
||||
} |
||||
|
||||
func (c *Command) Option(expr string, desc ...string) *Command { |
||||
o, err := ensureOption(c, expr, desc, "") |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
if c.version != nil { |
||||
err = hasConflicts(c.version, o) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
} |
||||
if c.help != nil { |
||||
err = hasConflicts(c.help, o) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
} |
||||
c.options = append(c.options, o) |
||||
return c |
||||
} |
||||
|
||||
func (c *Command) Action(action func(*Command, map[string]any, ...any) error) *Command { |
||||
c.action = action |
||||
return c |
||||
} |
||||
|
||||
func (c *Command) Run(args ...[]string) error { |
||||
var argv []string |
||||
for _, arg := range args { |
||||
argv = arg |
||||
break |
||||
} |
||||
if argv == nil { |
||||
argv = os.Args[1:] |
||||
} |
||||
|
||||
if c.program != nil { |
||||
return c.program.Run(argv) |
||||
} |
||||
|
||||
command := c |
||||
cmdIdx := -1 |
||||
for i, arg := range argv { |
||||
if !strings.HasPrefix(argv[0], "-") { |
||||
for _, cmd := range c.commands { |
||||
if cmd.name == arg { |
||||
command = cmd |
||||
cmdIdx = i |
||||
break |
||||
} |
||||
} |
||||
break |
||||
} |
||||
} |
||||
if cmdIdx == 0 { |
||||
argv = argv[1:] |
||||
} else if cmdIdx == len(argv)-1 { |
||||
argv = argv[:cmdIdx] |
||||
} else if cmdIdx > -1 { |
||||
argv = append(argv[:cmdIdx], argv[cmdIdx+1:]...) |
||||
} |
||||
|
||||
help, version, options, arguments := parseArgs(argv, command) |
||||
opts := resolveShowOptions([]*ShowOptions{}) |
||||
|
||||
if help { |
||||
return command.ShowHelp(opts) |
||||
} else if version { |
||||
return command.ShowVersion(&ShowOptions{ |
||||
Colors: opts.Colors, |
||||
Types: opts.Types, |
||||
Long: opts.Long, |
||||
Titles: false, |
||||
LineWidth: opts.LineWidth, |
||||
Writer: opts.Writer, |
||||
}) |
||||
} else if command.action == nil { |
||||
fmt.Println("没有操作,则显示帮助文档") |
||||
// 没有操作,则显示帮助文档
|
||||
return command.ShowHelp() |
||||
} else { |
||||
return command.action(command, options, arguments...) |
||||
} |
||||
} |
||||
|
||||
func hasConflicts(o1, o2 *Option) error { |
||||
if len(o1.Short) > 0 && o1.Short == o2.Short { |
||||
return errors.New("duplicated short flag of option definition") |
||||
} |
||||
if o1.Long == o2.Long { |
||||
return errors.New("duplicated long flag of option definition") |
||||
} |
||||
if o1.Name == o2.Name { |
||||
return errors.New("duplicated arguments definition") |
||||
} |
||||
return nil |
||||
} |
@ -0,0 +1,542 @@ |
||||
package cmd |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"regexp" |
||||
"sort" |
||||
"strconv" |
||||
"strings" |
||||
) |
||||
|
||||
var ( |
||||
ErrArgumentDefinition = errors.New("invalid createArgument definition") |
||||
ErrOptionDefinition = errors.New("invalid createOption definition") |
||||
ErrVersionDefinition = errors.New("invalid version definition") |
||||
|
||||
optionArgumentSplitRE *regexp.Regexp = regexp.MustCompile(`[, ] *`) |
||||
tokenRE *regexp.Regexp = regexp.MustCompile("^[a-z]\\w*$") |
||||
) |
||||
|
||||
type Option struct { |
||||
Name string // 参数名称
|
||||
Type string // 数据类型
|
||||
Short string // 段名称,e.g. -w
|
||||
Long string // 长名称,e.g. --watch
|
||||
Description string // 简短描述
|
||||
LongDescription string // 详细描述
|
||||
Variadic bool // 是否支持多个值
|
||||
Required bool // 是否必须
|
||||
DefaultValue any // 默认值
|
||||
RuntimeValue []string // 运行时的值
|
||||
} |
||||
|
||||
func (o *Option) toArgument() *Argument { |
||||
return &Argument{ |
||||
Name: o.Name, |
||||
Type: o.Type, |
||||
Description: o.Description, |
||||
LongDescription: o.LongDescription, |
||||
Variadic: o.Variadic, |
||||
Required: o.Required, |
||||
DefaultValue: o.DefaultValue, |
||||
} |
||||
} |
||||
|
||||
type Argument struct { |
||||
Name string // 参数名称
|
||||
Type string // 数据类型
|
||||
Description string // 简短描述
|
||||
LongDescription string // 详细描述
|
||||
Variadic bool // 是否支持多个值
|
||||
Required bool // 是否必须
|
||||
DefaultValue any // 默认值
|
||||
} |
||||
|
||||
// createArgument 解析参数
|
||||
//
|
||||
// <Name>、<Name:string>、<Name...:string>
|
||||
// [Name]、[Name:string]、[Name...:string]
|
||||
func createArgument(expr string) (*Argument, error) { |
||||
if len(expr) == 0 { |
||||
return nil, ErrArgumentDefinition |
||||
} |
||||
|
||||
var a Argument |
||||
if strings.HasPrefix(expr, "[") { |
||||
if !strings.HasSuffix(expr, "]") { |
||||
return nil, ErrArgumentDefinition |
||||
} |
||||
a.Required = true |
||||
} else if strings.HasPrefix(expr, "<") { |
||||
if !strings.HasSuffix(expr, ">") { |
||||
return nil, ErrArgumentDefinition |
||||
} |
||||
a.Required = false |
||||
} else { |
||||
return nil, ErrArgumentDefinition |
||||
} |
||||
|
||||
// 移除 [] <>
|
||||
expr = expr[1 : len(expr)-1] |
||||
|
||||
// 没有参数名称
|
||||
if len(expr) == 0 { |
||||
return nil, ErrArgumentDefinition |
||||
} |
||||
|
||||
parts := strings.Split(expr, ":") |
||||
switch len(parts) { |
||||
case 1: |
||||
a.Name = parts[0] |
||||
case 2: |
||||
a.Name = parts[0] |
||||
a.Type = parts[1] |
||||
// 支持多个值
|
||||
if strings.HasPrefix(a.Type, "...") { |
||||
a.Type = a.Type[3:] |
||||
a.Variadic = true |
||||
} |
||||
default: |
||||
return nil, ErrArgumentDefinition |
||||
} |
||||
|
||||
// 验证名称是否合法
|
||||
if !tokenRE.MatchString(a.Name) { |
||||
return nil, ErrArgumentDefinition |
||||
} |
||||
// 类型名称错误
|
||||
if len(a.Type) > 0 && !tokenRE.MatchString(a.Type) { |
||||
fmt.Println(a.Type) |
||||
return nil, ErrArgumentDefinition |
||||
} |
||||
|
||||
return &a, nil |
||||
} |
||||
|
||||
func createOption(expr string) (*Option, error) { |
||||
if len(expr) == 0 { |
||||
return nil, ErrArgumentDefinition |
||||
} |
||||
|
||||
var o Option |
||||
shortIndex := -1 |
||||
longIndex := -1 |
||||
nameIndex := -1 |
||||
parts := optionArgumentSplitRE.Split(expr, -1) |
||||
for i, part := range parts { |
||||
barLen := computeBarLen(part) |
||||
|
||||
// 解析的时参数
|
||||
if barLen == 0 { |
||||
// 存在多个参数
|
||||
if nameIndex > -1 { |
||||
return nil, ErrOptionDefinition |
||||
} |
||||
nameIndex = i |
||||
a, err := createArgument(part) |
||||
// 参数格式错误
|
||||
if errors.Is(err, ErrArgumentDefinition) { |
||||
return nil, ErrOptionDefinition |
||||
} |
||||
o.Name = a.Name |
||||
o.Type = a.Type |
||||
o.Variadic = a.Variadic |
||||
o.Required = a.Required |
||||
} else if barLen == 1 { |
||||
// 短名称重复
|
||||
if shortIndex > -1 { |
||||
return nil, ErrOptionDefinition |
||||
} |
||||
shortIndex = i |
||||
o.Short = part[barLen:] |
||||
} else if barLen == 2 { |
||||
if longIndex > -1 { |
||||
return nil, ErrOptionDefinition |
||||
} |
||||
longIndex = i |
||||
o.Long = part[barLen:] |
||||
} else { |
||||
// 3个或超过3个横杠
|
||||
return nil, ErrOptionDefinition |
||||
} |
||||
|
||||
// 严格检查顺序
|
||||
var positionError bool |
||||
if nameIndex > -1 { |
||||
positionError = nameIndex < longIndex || nameIndex < shortIndex |
||||
} else if longIndex > -1 { |
||||
positionError = longIndex < shortIndex |
||||
} |
||||
if positionError { |
||||
return nil, ErrOptionDefinition |
||||
} |
||||
} |
||||
|
||||
// 验证短名称是否合法
|
||||
if shortIndex > -1 && (len(o.Short) != 1 || !tokenRE.MatchString(o.Short)) { |
||||
return nil, ErrOptionDefinition |
||||
} |
||||
|
||||
// 未解析到长名称或者长名称不合法
|
||||
if longIndex == -1 || !tokenRE.MatchString(o.Long) { |
||||
return nil, ErrOptionDefinition |
||||
} |
||||
|
||||
// 未指定名称,则使用 long flag
|
||||
if nameIndex == -1 { |
||||
o.Name = o.Long |
||||
} |
||||
|
||||
return &o, nil |
||||
} |
||||
|
||||
func computeBarLen(str string) int { |
||||
for i, s := range str { |
||||
if s != '-' { |
||||
return i |
||||
} |
||||
} |
||||
return 0 |
||||
} |
||||
|
||||
type TypeParser func(...string) any |
||||
|
||||
func parseValue(cmd *Command, typ string, values []string, defaultValue any, variadic bool) any { |
||||
if parser, ok := getTypeParser(cmd, typ); ok { |
||||
return parser(values...) |
||||
} else if l := len(values); l > 0 { |
||||
switch typ { |
||||
case "bool": |
||||
if l == 0 { |
||||
if variadic { |
||||
return []bool{} |
||||
} |
||||
return true |
||||
} |
||||
var parsed []bool |
||||
for _, value := range values { |
||||
v, e := strconv.ParseBool(value) |
||||
if e != nil { |
||||
panic(errors.New("invalid value of type " + typ)) |
||||
} |
||||
if variadic { |
||||
parsed = append(parsed, v) |
||||
} else if l == 1 { |
||||
return v |
||||
} else { |
||||
panic(errors.New("found multi values")) |
||||
} |
||||
} |
||||
return parsed |
||||
case "string": |
||||
if variadic { |
||||
return values[:] |
||||
} else if l == 0 { |
||||
return "" |
||||
} else if l == 1 { |
||||
return values[0] |
||||
} else { |
||||
panic(errors.New("found multi values")) |
||||
} |
||||
case "float": |
||||
if l == 0 { |
||||
if variadic { |
||||
return []float64{} |
||||
} |
||||
return float64(0) |
||||
} |
||||
var parsed []float64 |
||||
for _, value := range values { |
||||
v, e := strconv.ParseFloat(value, 64) |
||||
if e != nil { |
||||
panic(errors.New("invalid value of type " + typ)) |
||||
} |
||||
if variadic { |
||||
parsed = append(parsed, v) |
||||
} else if l == 1 { |
||||
return v |
||||
} else { |
||||
panic(errors.New("found multi values")) |
||||
} |
||||
} |
||||
return parsed |
||||
case "int": |
||||
if l == 0 { |
||||
if variadic { |
||||
return []int64{} |
||||
} |
||||
return int64(0) |
||||
} |
||||
var parsed []int64 |
||||
for _, value := range values { |
||||
v, e := strconv.ParseInt(value, 10, 64) |
||||
if e != nil { |
||||
panic(errors.New("invalid value of type " + typ)) |
||||
} |
||||
if variadic { |
||||
parsed = append(parsed, v) |
||||
} else if l == 1 { |
||||
return v |
||||
} else { |
||||
panic(errors.New("found multi values")) |
||||
} |
||||
} |
||||
return parsed |
||||
case "uint": |
||||
if l == 0 { |
||||
if variadic { |
||||
return []uint64{} |
||||
} |
||||
return int64(0) |
||||
} |
||||
var parsed []uint64 |
||||
for _, value := range values { |
||||
v, e := strconv.ParseUint(value, 10, 64) |
||||
if e != nil { |
||||
panic(errors.New("invalid value of type " + typ)) |
||||
} |
||||
if variadic { |
||||
parsed = append(parsed, v) |
||||
} else if l == 1 { |
||||
return v |
||||
} else { |
||||
panic(errors.New("found multi values")) |
||||
} |
||||
} |
||||
return parsed |
||||
default: |
||||
if variadic { |
||||
return values[:] |
||||
} else if l == 0 { |
||||
return nil |
||||
} else if l == 1 { |
||||
return values[0] |
||||
} else { |
||||
panic(errors.New("found multi values")) |
||||
} |
||||
} |
||||
} else if defaultValue != nil { |
||||
return defaultValue |
||||
} else { |
||||
return nil |
||||
} |
||||
} |
||||
|
||||
func getTypeParser(cmd *Command, typ string) (TypeParser, bool) { |
||||
if cmd.types != nil { |
||||
if parser, ok := cmd.types[typ]; ok { |
||||
return parser, true |
||||
} |
||||
} |
||||
if cmd.globalTypes != nil { |
||||
if parser, ok := cmd.globalTypes[typ]; ok { |
||||
return parser, true |
||||
} |
||||
} |
||||
if cmd.program != nil && cmd.program.globalTypes != nil { |
||||
parser, ok := cmd.program.globalTypes[typ] |
||||
return parser, ok |
||||
} |
||||
return nil, false |
||||
} |
||||
|
||||
func parseArgs(args []string, cmd *Command) (help bool, version bool, options map[string]any, arguments []any) { |
||||
var removes []int |
||||
var foundOption *Option |
||||
var variadicValues []string // 可变参数值
|
||||
var variadicOption *Option // 可变参数选项
|
||||
variadicStart := -1 // 可变参数开始位置
|
||||
variadicStop := -1 // 可变参数结束位置(不包含)
|
||||
foundPosition := -1 |
||||
|
||||
findOption := func(key string, isLong, isShort bool) *Option { |
||||
for _, o := range cmd.options { |
||||
if (isLong && o.Long == key) || (isShort && o.Short == key) { |
||||
return o |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
saveParsedVariadicValues := func() { |
||||
if variadicOption == nil { |
||||
return |
||||
} |
||||
for i := variadicStart; i < variadicStop; i++ { |
||||
removes = append(removes, i) |
||||
} |
||||
variadicOption.RuntimeValue = variadicValues[:] |
||||
variadicOption = nil |
||||
variadicValues = []string{} |
||||
variadicStart = -1 |
||||
variadicStop = -1 |
||||
} |
||||
|
||||
saveFound := func(value string) { |
||||
if foundOption == nil { |
||||
return |
||||
} |
||||
if len(value) > 0 { |
||||
foundOption.RuntimeValue = []string{value} |
||||
} |
||||
removes = append(removes, foundPosition, foundPosition+1) // foundPosition+1 可能会越界
|
||||
foundOption = nil |
||||
foundPosition = -1 |
||||
} |
||||
|
||||
for i := 0; i < len(args); i++ { |
||||
v := args[i] |
||||
|
||||
barLen := computeBarLen(v) |
||||
|
||||
var isShort bool |
||||
var isLong bool |
||||
|
||||
switch barLen { |
||||
case 0: |
||||
if variadicOption != nil { |
||||
variadicValues = append(variadicValues, v) |
||||
} else /*if foundOption != nil*/ { |
||||
saveFound(v) |
||||
} |
||||
continue |
||||
case 1: |
||||
isShort = true |
||||
v = v[1:] |
||||
case 2: |
||||
isLong = true |
||||
v = v[2:] |
||||
default: |
||||
panic("invalid argument") |
||||
} |
||||
|
||||
// 保存之前找到的参数
|
||||
variadicStop = i |
||||
saveParsedVariadicValues() |
||||
saveFound("") |
||||
|
||||
// 解析参数
|
||||
var val string |
||||
var key string |
||||
var equalsSign bool |
||||
if j := strings.IndexByte(v, '='); j > -1 { |
||||
equalsSign = true |
||||
key = v[:j] |
||||
if j < len(v)-1 { |
||||
val = v[j+1:] |
||||
} |
||||
} else { |
||||
key = v |
||||
} |
||||
|
||||
// 是不是帮助命令
|
||||
if cmd.help != nil && ((isShort && cmd.help.Short == key) || (isLong && cmd.help.Long == key)) { |
||||
help = true |
||||
removes = append(removes, i) |
||||
continue |
||||
} |
||||
|
||||
// 是不是版本命令
|
||||
if cmd.version != nil && ((isShort && cmd.version.Short == key) || (isLong && cmd.version.Long == key)) { |
||||
version = true |
||||
removes = append(removes, i) |
||||
continue |
||||
} |
||||
|
||||
opt := findOption(key, isLong, isShort) |
||||
if opt == nil { |
||||
// TODO show help information
|
||||
panic("unsupported flag \"" + v + "\"") |
||||
} |
||||
|
||||
foundOption = opt |
||||
foundPosition = i |
||||
|
||||
// 后面的不解析
|
||||
if equalsSign { |
||||
if opt.Variadic { |
||||
// TODO show help information
|
||||
panic("invalid values for flag \"" + v + "\"") |
||||
} |
||||
|
||||
saveFound(val) |
||||
continue |
||||
} |
||||
|
||||
if opt.Variadic { |
||||
foundOption = nil |
||||
foundPosition = -1 |
||||
variadicOption = opt |
||||
variadicStart = i |
||||
} |
||||
} |
||||
|
||||
if variadicOption != nil { |
||||
variadicStop = len(args) |
||||
saveParsedVariadicValues() |
||||
} else { |
||||
saveFound("") |
||||
} |
||||
|
||||
// 移除上面解析 flags 标记的参数
|
||||
temp := args[:] |
||||
sort.Sort(sort.Reverse(sort.IntSlice(removes))) |
||||
for _, remove := range removes { |
||||
l := len(temp) |
||||
if remove >= l { |
||||
continue |
||||
} |
||||
if l == 0 { |
||||
break |
||||
} |
||||
if remove != len(temp)-1 { |
||||
temp = append(temp[:remove], temp[remove+1:]...) |
||||
} else { |
||||
temp = temp[:remove] |
||||
} |
||||
} |
||||
|
||||
// 参数赋值
|
||||
arguments = make([]any, len(cmd.arguments)) |
||||
l := len(temp) - 1 |
||||
for i, arg := range cmd.arguments { |
||||
// 可变参数
|
||||
if arg.Variadic { |
||||
if i <= l { |
||||
arguments[i] = parseValue(cmd, arg.Type, temp[i:], arg.DefaultValue, true) |
||||
} else if arg.DefaultValue == nil { |
||||
if arg.Required { |
||||
panic(errors.New("missing argument with " + arg.Name)) |
||||
} |
||||
arguments[i] = []any{} |
||||
} else if list, ok := arg.DefaultValue.([]any); ok { |
||||
arguments[i] = list |
||||
} else { |
||||
panic(errors.New("invalid default value, except a(n) `[]any`")) |
||||
} |
||||
break |
||||
} |
||||
|
||||
// 找到输入的参数
|
||||
if i <= l { |
||||
arguments[i] = parseValue(cmd, arg.Type, []string{temp[i]}, arg.DefaultValue, false) |
||||
} else if arg.DefaultValue != nil { |
||||
// 启用默认值
|
||||
arguments[i] = arg.DefaultValue |
||||
} else if arg.Required { |
||||
panic(errors.New("missing argument with " + arg.Name)) |
||||
} else { |
||||
arguments[i] = nil |
||||
} |
||||
} |
||||
|
||||
// 解析出需要的可选参数
|
||||
options = make(map[string]any) |
||||
for _, o := range cmd.options { |
||||
options[o.Name] = parseValue(cmd, o.Type, o.RuntimeValue, o.DefaultValue, o.Variadic) |
||||
o.RuntimeValue = nil |
||||
} |
||||
|
||||
return |
||||
} |
@ -0,0 +1,388 @@ |
||||
package cmd |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"github.com/mattn/go-isatty" |
||||
"io" |
||||
"os" |
||||
"regexp" |
||||
"strings" |
||||
) |
||||
|
||||
var ( |
||||
colorRE = regexp.MustCompile(`\x1b[;?[0-9]+m`) |
||||
defaultShowOptions = &ShowOptions{Colors: true, LineWidth: 80, Titles: true} |
||||
) |
||||
|
||||
type ShowWriter interface { |
||||
io.Writer |
||||
Fd() uintptr |
||||
} |
||||
|
||||
type ShowOptions struct { |
||||
Colors bool |
||||
Types bool |
||||
Long bool |
||||
Titles bool |
||||
LineWidth int |
||||
Writer ShowWriter |
||||
} |
||||
|
||||
func length(str string) int { |
||||
return len(colorRE.ReplaceAllString(str, "")) |
||||
} |
||||
|
||||
func colorize(enable bool, color int, str string) string { |
||||
if enable { |
||||
return fmt.Sprintf("\x1b[0;%dm%s\x1b[0m", color, str) |
||||
} |
||||
return str |
||||
} |
||||
|
||||
func Blue(str string, opts ...*ShowOptions) string { |
||||
return colorize(resolveShowOptions(opts).Colors, 34, str) |
||||
} |
||||
|
||||
func Red(str string, opts ...*ShowOptions) string { |
||||
return colorize(resolveShowOptions(opts).Colors, 31, str) |
||||
} |
||||
|
||||
func Yellow(str string, opts ...*ShowOptions) string { |
||||
return colorize(resolveShowOptions(opts).Colors, 33, str) |
||||
} |
||||
|
||||
func Cyan(str string, opts ...*ShowOptions) string { |
||||
return colorize(resolveShowOptions(opts).Colors, 35, str) |
||||
} |
||||
|
||||
func Green(str string, opts ...*ShowOptions) string { |
||||
return colorize(resolveShowOptions(opts).Colors, 32, str) |
||||
} |
||||
|
||||
func resolveShowOptions(opts []*ShowOptions) *ShowOptions { |
||||
for _, opt := range opts { |
||||
if opt.LineWidth == 0 { |
||||
opt.LineWidth = 80 |
||||
} |
||||
return opt |
||||
} |
||||
return defaultShowOptions |
||||
} |
||||
|
||||
func showMessage(opts *ShowOptions, f func(...*ShowOptions) string) error { |
||||
out := opts.Writer |
||||
if out == nil { |
||||
out = os.Stdout |
||||
} |
||||
if opts.Colors && (os.Getenv("TERM") == "dumb" || (!isatty.IsTerminal(out.Fd()) && !isatty.IsCygwinTerminal(out.Fd()))) { |
||||
opts.Colors = false |
||||
} |
||||
_, err := fmt.Fprintln(out, f(opts)) |
||||
return err |
||||
} |
||||
|
||||
func versionToString(cmd *Command, opts *ShowOptions) string { |
||||
var str string |
||||
if len(cmd.version.RuntimeValue) > 0 { |
||||
str = cmd.version.RuntimeValue[0] |
||||
} else { |
||||
str = cmd.version.DefaultValue.(string) |
||||
} |
||||
if opts.Colors { |
||||
str = Green(str, opts) |
||||
} |
||||
if opts.Titles { |
||||
str = "VERSION: " + str |
||||
} |
||||
return str |
||||
} |
||||
|
||||
func usageToString(cmd *Command, opts *ShowOptions) string { |
||||
var str string |
||||
if opts.Titles { |
||||
str += "USAGE:" |
||||
} |
||||
if cmd.program != nil { |
||||
str += " " + Cyan(cmd.program.name, opts) |
||||
} |
||||
str += " " + Cyan(cmd.name, opts) |
||||
if len(cmd.options) > 0 { |
||||
str += " " + Yellow("[", opts) + Cyan("OPTIONS", opts) + Yellow("]", opts) |
||||
} |
||||
for _, a := range cmd.arguments { |
||||
str += " " + argumentToString(a, opts) |
||||
} |
||||
return strings.TrimSpace(str) |
||||
} |
||||
|
||||
func argumentToString(a *Argument, opts *ShowOptions) string { |
||||
var str string |
||||
if a.Required { |
||||
str += Yellow("<", opts) |
||||
} else { |
||||
str += Yellow("[", opts) |
||||
} |
||||
str += Cyan(a.Name, opts) |
||||
if opts.Types && len(a.Type) > 0 { |
||||
str += ":" |
||||
if a.Variadic { |
||||
str += "..." |
||||
} |
||||
str += Green(a.Type, opts) |
||||
} |
||||
if a.Required { |
||||
str += Yellow(">", opts) |
||||
} else { |
||||
str += Yellow("]", opts) |
||||
} |
||||
return str |
||||
} |
||||
|
||||
func helpToString(cmd *Command, opts *ShowOptions) string { |
||||
var str string |
||||
if usage := cmd.GetUsage(opts); len(usage) > 0 { |
||||
str += usage + "\n" |
||||
} |
||||
if version := cmd.GetVersion(opts); len(version) > 0 { |
||||
str += version + "\n" |
||||
} |
||||
if desc := cmd.GetDescription(opts); len(desc) > 0 { |
||||
str += "\n" + desc + "\n\n" |
||||
} |
||||
if options := optionsToString(cmd, opts); len(options) > 0 { |
||||
str += options + "\n" |
||||
} |
||||
if arguments := argumentsToString(cmd, opts); len(arguments) > 0 { |
||||
str += arguments + "\n" |
||||
} |
||||
if commands := commandsToString(cmd, opts); len(commands) > 0 { |
||||
str += commands + "\n" |
||||
} |
||||
return strings.TrimSpace(str) + "\n" |
||||
} |
||||
|
||||
func commandsToString(cmd *Command, opts *ShowOptions) string { |
||||
if len(cmd.commands) == 0 { |
||||
return "" |
||||
} |
||||
tb := &table{ |
||||
gaps: []string{" ", ""}, |
||||
rows: [][]string{}, |
||||
sizes: []int{0, 0}, |
||||
} |
||||
for _, o := range cmd.commands { |
||||
tb.AddRow( |
||||
Cyan(o.name, opts), |
||||
o.description, |
||||
) |
||||
} |
||||
str := tb.String(" ", "", opts.LineWidth) |
||||
if opts.Titles { |
||||
return "SUBCOMMANDS:\n" + str |
||||
} |
||||
return str |
||||
} |
||||
|
||||
func argumentsToString(cmd *Command, opts *ShowOptions) string { |
||||
if len(cmd.arguments) == 0 { |
||||
return "" |
||||
} |
||||
t := &table{ |
||||
gaps: []string{" ", " ", ""}, |
||||
sizes: []int{0, 0, 0}, |
||||
rows: [][]string{}, |
||||
} |
||||
if !opts.Types { |
||||
t.gaps[1] = "" |
||||
} |
||||
for _, a := range cmd.arguments { |
||||
var typ string |
||||
if opts.Types && len(a.Type) > 0 { |
||||
if a.Required { |
||||
typ += Yellow("<", opts) |
||||
} else { |
||||
typ += Yellow("[", opts) |
||||
} |
||||
typ += Cyan(a.Type, opts) |
||||
if a.Required { |
||||
typ += Yellow("<", opts) |
||||
} else { |
||||
typ += Yellow("[", opts) |
||||
} |
||||
} |
||||
t.AddRow( |
||||
Cyan(a.Name, opts), |
||||
typ, |
||||
a.Description, |
||||
) |
||||
} |
||||
if opts.Titles { |
||||
return "ARGUMENTS:\n" + t.String(" ", "", opts.LineWidth) + "\n" |
||||
} |
||||
return t.String(" ", "", opts.LineWidth) + "\n" |
||||
} |
||||
|
||||
func optionsToString(cmd *Command, opts *ShowOptions) string { |
||||
if len(cmd.options) == 0 { |
||||
return "" |
||||
} |
||||
t := &table{ |
||||
gaps: []string{" ", " ", ""}, |
||||
sizes: []int{0, 0, 0}, |
||||
rows: [][]string{}, |
||||
} |
||||
for _, o := range cmd.options { |
||||
t.AddRow( |
||||
optionFlagsToString(o, opts), |
||||
argumentToString(o.toArgument(), opts), |
||||
o.Description, |
||||
) |
||||
} |
||||
str := t.String(" ", "", opts.LineWidth) |
||||
if opts.Titles { |
||||
return "OPTIONS:\n" + str + "\n" |
||||
} |
||||
return str + "\n" |
||||
} |
||||
|
||||
func optionFlagsToString(o *Option, opts *ShowOptions) string { |
||||
var str string |
||||
if len(o.Short) > 0 { |
||||
str += Blue("-"+o.Short, opts) + ", " |
||||
} else { |
||||
// 保留 short 的位置
|
||||
str += " " |
||||
} |
||||
return str + Blue("--"+o.Long, opts) |
||||
} |
||||
|
||||
func ensureOption(c *Command, expr string, desc []string, owner string) (*Option, error) { |
||||
o1, err := createOption(expr) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
switch len(desc) { |
||||
case 0: |
||||
break |
||||
case 1: |
||||
o1.Description = desc[0] |
||||
default: |
||||
o1.Description = desc[0] |
||||
o1.LongDescription = desc[1] |
||||
} |
||||
for _, o2 := range c.options { |
||||
err := hasConflicts(o2, o1) |
||||
if err != nil { |
||||
if len(owner) > 0 { |
||||
return nil, errors.New(owner + " option conflicts with an optional parameter") |
||||
} |
||||
return nil, errors.New("option conflicts with another optional parameter") |
||||
} |
||||
} |
||||
return o1, nil |
||||
} |
||||
|
||||
type table struct { |
||||
gaps []string |
||||
sizes []int |
||||
rows [][]string |
||||
} |
||||
|
||||
var spaceRE = regexp.MustCompile(`[\t\n\v\f\r]+`) |
||||
var noNewlineSpaceRE = regexp.MustCompile(`[\t\v\f\r]+]`) |
||||
|
||||
func (t *table) AddRow(columns ...string) { |
||||
l := len(columns) |
||||
width := len(t.sizes) |
||||
for i, column := range columns { |
||||
column = strings.TrimSpace(column) |
||||
if i < l-1 { |
||||
column = spaceRE.ReplaceAllString(column, "") |
||||
} else { |
||||
column = noNewlineSpaceRE.ReplaceAllString(column, "") |
||||
} |
||||
columns[i] = column |
||||
size := length(column) |
||||
if size > t.sizes[i] { |
||||
t.sizes[i] = size |
||||
} |
||||
} |
||||
for i := l; i < width; i++ { |
||||
columns = append(columns, "") |
||||
} |
||||
t.rows = append(t.rows, columns) |
||||
} |
||||
|
||||
func (t *table) String(linePrefix, lineSuffix string, lineWidth int) string { |
||||
lastColumnIndex := len(t.sizes) - 1 |
||||
lastColumnWidth := 0 |
||||
|
||||
var newlinePrefix string |
||||
var str string |
||||
|
||||
for j, row := range t.rows { |
||||
line := linePrefix |
||||
for i, col := range row { |
||||
width := t.sizes[i] |
||||
// 不是最后一行
|
||||
if i < lastColumnIndex { |
||||
line += col |
||||
count := length(col) |
||||
if count < width { |
||||
line += strings.Repeat(" ", width-count) |
||||
} |
||||
line += t.gaps[i] |
||||
continue |
||||
} |
||||
|
||||
// 最后一行换行
|
||||
if lastColumnWidth == 0 { |
||||
lastColumnWidth = lineWidth - length(line) - length(lineSuffix) |
||||
if lastColumnWidth < 10 { |
||||
lastColumnWidth = 10 |
||||
} |
||||
newlinePrefix = strings.Repeat(" ", length(line)) |
||||
} |
||||
|
||||
for _, cline := range strings.Split(col, "\n") { |
||||
var s string |
||||
var w int |
||||
var x int |
||||
var y int |
||||
for _, word := range strings.Split(cline, " ") { |
||||
n := length(word) |
||||
if w+n > lastColumnWidth { |
||||
if y > 0 { |
||||
line += "\n" + newlinePrefix |
||||
} |
||||
line += s |
||||
y += 1 |
||||
s = "" |
||||
w = 0 |
||||
x = 0 |
||||
continue |
||||
} |
||||
if x > 0 { |
||||
s += " " |
||||
} |
||||
s += word |
||||
w += n |
||||
x += 1 |
||||
} |
||||
if x > 0 { |
||||
if y > 0 { |
||||
line += "\n" + newlinePrefix |
||||
} |
||||
line += s |
||||
} |
||||
} |
||||
} |
||||
if j > 0 { |
||||
str += "\n" |
||||
} |
||||
str += line |
||||
} |
||||
|
||||
return str |
||||
} |
@ -0,0 +1,138 @@ |
||||
package db |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"encoding/xml" |
||||
"fmt" |
||||
"github.com/pelletier/go-toml/v2" |
||||
"gopkg.in/ini.v1" |
||||
"gopkg.in/yaml.v3" |
||||
"os" |
||||
"strconv" |
||||
"strings" |
||||
) |
||||
|
||||
// AppConfig 程序启动配置
|
||||
type AppConfig struct { |
||||
Name string `json:"name,omitempty" yaml:"name,omitempty" toml:"name,multiline,omitempty" xml:"name,omitempty" ini:"name,omitempty"` // 应用名称
|
||||
Description string `json:"description,omitempty" yaml:"description,omitempty" toml:"description,multiline,omitempty" xml:"description,omitempty" ini:"description,omitempty"` // 应用描述
|
||||
Script string `json:"script,omitempty" yaml:"script,omitempty" toml:"script,multiline,omitempty" xml:"script,omitempty" ini:"script,omitempty"` // 执行脚本
|
||||
Args []string `json:"args,omitempty" yaml:"args,omitempty" toml:"args,multiline,omitempty" xml:"args,omitempty" ini:"args,omitempty"` // 启动参数
|
||||
Stdin string `json:"stdin,omitempty" yaml:"stdin,omitempty" toml:"stdin,multiline,omitempty" xml:"stdin,omitempty" ini:"stdin,omitempty"` // 标准输入数据
|
||||
Cwd string `json:"Cwd,omitempty" yaml:"Cwd,omitempty" toml:"Cwd,multiline,omitempty" xml:"Cwd,omitempty" ini:"cwd,omitempty"` // 工作目录
|
||||
Env map[string]string `json:"env,omitempty" yaml:"env,omitempty" toml:"env,multiline,omitempty" xml:"env,omitempty" ini:"env,omitempty"` // 自定义环境变量
|
||||
Interpreter string `json:"interpreter,omitempty" yaml:"interpreter,omitempty" toml:"interpreter,multiline,omitempty" xml:"interpreter,omitempty" ini:"interpreter,omitempty"` // 脚本解释程序
|
||||
InterpreterArgs []string `json:"interpreter_args,omitempty" yaml:"interpreter_args,omitempty" toml:"interpreter_args,multiline,omitempty" xml:"interpreter_args,omitempty" ini:"interpreterArgs,omitempty"` // 解释程序参数
|
||||
RerunOnError *RerunOnError `json:"rerunOnError" yaml:"rerunOnError,omitempty" toml:"rerunOnError,multiline,omitempty" xml:"rerunOnError,omitempty" ini:"rerunOnError,omitempty"` // 错误重启策略
|
||||
Custom map[string]any `json:"custom,omitempty" yaml:"custom,omitempty" toml:"custom,multiline,omitempty" xml:"custom,omitempty" ini:"custom,omitempty"` // 其它自定义参数
|
||||
} |
||||
|
||||
type Unmarshaller func(data []byte, v any) error |
||||
|
||||
func ParseInput(input string) ([]*App, error) { |
||||
var configs []AppConfig |
||||
var err error |
||||
var app *App |
||||
|
||||
if IsAppID(input) { |
||||
var id int |
||||
if id, err = strconv.Atoi(input); err != nil { |
||||
return nil, fmt.Errorf("invalid identifier") |
||||
} |
||||
app, err = FindApp(WithAppID(uint(id))) |
||||
} else if strings.HasSuffix(input, ".pmt.json") { |
||||
configs, err = Unmarshal(input, json.Unmarshal) |
||||
} else if strings.HasSuffix(input, ".pmt.toml") { |
||||
configs, err = Unmarshal(input, toml.Unmarshal) |
||||
} else if strings.HasSuffix(input, ".pmt.yaml") { |
||||
configs, err = Unmarshal(input, yaml.Unmarshal) |
||||
} else if strings.HasSuffix(input, ".pmt.xml") { |
||||
configs, err = Unmarshal(input, xml.Unmarshal) |
||||
} else if strings.HasSuffix(input, ".pmt.ini") { |
||||
configs, err = Unmarshal(input, func(data []byte, v any) error { |
||||
if info, err := ini.Load(data); err != nil { |
||||
return err |
||||
} else { |
||||
return info.MapTo(v) |
||||
} |
||||
}) |
||||
} else if file, ex := os.Stat(input); ex == nil { |
||||
if file.IsDir() { |
||||
return nil, fmt.Errorf("unsupported directoty entry") |
||||
} |
||||
|
||||
if app, err = scriptToApp(input); err != nil { |
||||
return nil, err |
||||
} |
||||
} else { |
||||
app, err = FindApp(WithAppName(input)) |
||||
} |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if app != nil { |
||||
return []*App{app}, nil |
||||
} |
||||
if len(configs) == 0 { |
||||
return nil, fmt.Errorf("not found") |
||||
} |
||||
var apps []*App |
||||
for _, cfg := range configs { |
||||
apps = append(apps, optionsToApp(&cfg)) |
||||
} |
||||
return apps, err |
||||
} |
||||
|
||||
func scriptToApp(script string) (*App, error) { |
||||
name := ToSnakeCase(script) |
||||
app, err := FindApp(WithAppName(name)) |
||||
if err == nil { |
||||
if app.Script != script { |
||||
return nil, fmt.Errorf("应用 %s 已存在", script) |
||||
} |
||||
return app, nil |
||||
} |
||||
return &App{ |
||||
PID: -1, |
||||
Name: name, |
||||
Script: script, |
||||
}, nil |
||||
} |
||||
|
||||
func optionsToApp(opts *AppConfig) *App { |
||||
return &App{ |
||||
PID: -1, |
||||
Name: opts.Name, |
||||
Description: opts.Description, |
||||
Script: opts.Script, |
||||
Command: nil, |
||||
Arguments: opts.Args, |
||||
Stdin: opts.Stdin, |
||||
Cwd: opts.Cwd, |
||||
Environments: opts.Env, |
||||
Interpreter: opts.Interpreter, |
||||
InterpreterArgs: opts.InterpreterArgs, |
||||
RerunOnError: opts.RerunOnError, |
||||
Options: opts.Custom, |
||||
} |
||||
} |
||||
|
||||
// Unmarshal 读取配置文件
|
||||
func Unmarshal(file string, unmarshal Unmarshaller) ([]AppConfig, error) { |
||||
bts, err := os.ReadFile(file) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
var config AppConfig |
||||
if err = unmarshal(bts, &config); err == nil { |
||||
return []AppConfig{config}, nil |
||||
} |
||||
|
||||
var configs []AppConfig |
||||
if unmarshal(bts, &configs) == nil { |
||||
return configs, nil |
||||
} |
||||
|
||||
return nil, err |
||||
} |
@ -0,0 +1,117 @@ |
||||
package db |
||||
|
||||
import ( |
||||
"gorm.io/driver/sqlite" |
||||
"gorm.io/gorm" |
||||
"time" |
||||
) |
||||
|
||||
var DB *gorm.DB |
||||
|
||||
func init() { |
||||
var err error |
||||
DB, err = gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{}) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
} |
||||
|
||||
type App struct { |
||||
ID uint `json:"id" gorm:"primarykey"` // 应用ID
|
||||
PID int `json:"pid"` // 应用PID
|
||||
Name string `json:"name" gorm:"unique"` // 应用名称
|
||||
Description string `json:"description"` // 应用描述
|
||||
Script string `json:"script"` // 启动脚本
|
||||
Command []string `json:"command" gorm:"serializer:json"` // 启动命令
|
||||
Arguments []string `json:"arguments" gorm:"serializer:json"` // 启动参数
|
||||
Stdin string `json:"stdin"` // 标准输入数据
|
||||
Cwd string `json:"cwd"` // 工作目录
|
||||
Environments map[string]string `json:"environments" gorm:"serializer:json"` // 自定义环境变量
|
||||
Interpreter string `json:"interpreter"` // 脚本解释程序
|
||||
InterpreterArgs []string `json:"interpreter_args" gorm:"serializer:json"` // 解释程序参数
|
||||
Options map[string]any `json:"options" gorm:"serializer:json"` // 其它参数
|
||||
RerunOnError *RerunOnError `json:"rerun_on_error" gorm:"serializer:json"` // 错误重启策略
|
||||
StartCount int `json:"start_count"` // 应用启动次数
|
||||
ErrorCount int `json:"error_count"` // 错误次数,配合 RerunOnError 使用
|
||||
Status uint8 `json:"status"` // 应用状态
|
||||
CreatedAt time.Time `json:"created_at"` // 创建时间
|
||||
} |
||||
|
||||
// RerunOnError 错误重启策略
|
||||
type RerunOnError struct { |
||||
Enable bool `json:"enable"` // 是否支持错误重试
|
||||
Count int64 `json:"count_on_error"` // 允许重试次数
|
||||
} |
||||
|
||||
func SaveApp(app *App) error { |
||||
if app.ID > 0 { |
||||
return DB.Save(app).Error |
||||
} else { |
||||
return DB.Create(app).Error |
||||
} |
||||
} |
||||
|
||||
func WithAppID(id uint) func(*gorm.DB) *gorm.DB { |
||||
return func(db *gorm.DB) *gorm.DB { |
||||
return db.Where("id=?", id) |
||||
} |
||||
} |
||||
|
||||
func WithAppName(name string) func(*gorm.DB) *gorm.DB { |
||||
return func(db *gorm.DB) *gorm.DB { |
||||
return db.Where("name=?", name) |
||||
} |
||||
} |
||||
|
||||
func WithAppStatus(statuses ...uint32) func(*gorm.DB) *gorm.DB { |
||||
return func(db *gorm.DB) *gorm.DB { |
||||
switch len(statuses) { |
||||
case 0: |
||||
return db |
||||
case 1: |
||||
return db.Where("status=?", statuses[0]) |
||||
default: |
||||
return db.Where("status IN ?", statuses) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// FindApp 查找一个 App
|
||||
func FindApp(opts ...func(*gorm.DB) *gorm.DB) (*App, error) { |
||||
var app App |
||||
err := DB.Model(&App{}).Scopes(opts...).First(&app).Error |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return &app, nil |
||||
} |
||||
|
||||
// FindApps 查找多个 App
|
||||
func FindApps(opts ...func(*gorm.DB) *gorm.DB) ([]App, error) { |
||||
var apps []App |
||||
err := DB.Model(&App{}).Scopes(opts...).Find(&apps).Error |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return apps, nil |
||||
} |
||||
|
||||
func ConfigPid(pid int) func(*gorm.DB) *gorm.DB { |
||||
return func(db *gorm.DB) *gorm.DB { |
||||
return db.Set("pid", pid) |
||||
} |
||||
} |
||||
|
||||
func ConfigStatus(status uint8) func(*gorm.DB) *gorm.DB { |
||||
return func(db *gorm.DB) *gorm.DB { |
||||
return db.Set("status", status) |
||||
} |
||||
} |
||||
|
||||
func IncreaseStartCounter(db *gorm.DB) *gorm.DB { |
||||
return db.Set("starts", gorm.Expr("starts + ?", 1)) |
||||
} |
||||
|
||||
func UpdateApp(id uint, opts ...func(*gorm.DB) *gorm.DB) error { |
||||
return DB.Model(&App{}).Scopes(opts...).Where("id=?", id).Error |
||||
} |
@ -0,0 +1,279 @@ |
||||
package db |
||||
|
||||
import ( |
||||
"errors" |
||||
"os" |
||||
"path/filepath" |
||||
"strconv" |
||||
"sync" |
||||
"syscall" |
||||
) |
||||
|
||||
const ( |
||||
StatusUnknown uint8 = iota // 未知状态
|
||||
StatusStarting // 正在启动
|
||||
StatusRunning // 正在运行
|
||||
StatusStopping // 正在停止
|
||||
StatusStopped // 已经停止
|
||||
) |
||||
|
||||
type Proc struct { |
||||
sync.RWMutex |
||||
tempDir string // 数据缓存目录
|
||||
stdinFile string // 数据输入文件
|
||||
stdoutFile string // 数据输出文件
|
||||
stderrFile string // 错误输出文件
|
||||
pidfile string // 记录PID的文件
|
||||
starts int // 重启次数
|
||||
status uint8 // 程序状态
|
||||
pid int // 进程PID
|
||||
app *App // 应用信息
|
||||
process *os.Process // 系统进程
|
||||
} |
||||
|
||||
func NewProc(app *App) *Proc { |
||||
tempDir, err := TempDir(app.Name) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
|
||||
proc := &Proc{ |
||||
tempDir: tempDir, |
||||
stdinFile: filepath.Join(tempDir, "stdin.txt"), |
||||
stdoutFile: filepath.Join(tempDir, "stdout.txt"), |
||||
stderrFile: filepath.Join(tempDir, "stderr.txt"), |
||||
pidfile: filepath.Join(tempDir, "pid.txt"), |
||||
starts: app.StartCount, |
||||
status: app.Status, |
||||
pid: app.PID, |
||||
app: app, |
||||
process: nil, |
||||
} |
||||
|
||||
inner, err := os.FindProcess(app.PID) |
||||
if err == nil && inner.Signal(syscall.Signal(0)) == nil { |
||||
proc.process = inner |
||||
} |
||||
|
||||
return proc |
||||
} |
||||
|
||||
// Start 启动进程
|
||||
func (p *Proc) Start() error { |
||||
p.RLock() |
||||
if p.status != StatusUnknown && p.status != StatusStopped { |
||||
p.RUnlock() |
||||
return errors.New("process was running") |
||||
} |
||||
stdinFile := p.stdinFile |
||||
stdoutFile := p.stdoutFile |
||||
stderrFile := p.stderrFile |
||||
app := p.app |
||||
p.setStatus(StatusStarting) |
||||
p.RUnlock() |
||||
|
||||
// 确定数据输入文件
|
||||
stdin, err := GetFile(stdinFile) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if len(app.Stdin) > 0 { |
||||
err = WriteFile(stdinFile, []byte(app.Stdin)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
// 确定数据输出文件
|
||||
// TODO 监听内容输入,记录到数据库
|
||||
stdout, err := GetFile(stdoutFile) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// 确定错误输出文件
|
||||
// TODO 监听内容输入,记录到数据库
|
||||
stderr, err := GetFile(stderrFile) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// 确定工作目录
|
||||
cwd := app.Cwd |
||||
if len(cwd) == 0 { |
||||
cwd, _ = os.Getwd() |
||||
} |
||||
|
||||
// 确定进程属性
|
||||
attr := &os.ProcAttr{ |
||||
Dir: cwd, |
||||
Env: os.Environ(), |
||||
Files: []*os.File{stdin, stdout, stderr}, |
||||
//Sys: &syscall.SysProcAttr{HideWindow: true},
|
||||
} |
||||
|
||||
// 确定启动参数
|
||||
var args []string |
||||
if len(app.InterpreterArgs) > 0 { |
||||
args = append(args, app.InterpreterArgs...) |
||||
} |
||||
if len(app.Script) > 0 { |
||||
args = append(args, app.Script) |
||||
} |
||||
if len(app.Command) > 0 { |
||||
args = append(args, app.Command...) |
||||
} |
||||
if len(app.Arguments) > 0 { |
||||
args = append(args, app.Arguments...) |
||||
} |
||||
|
||||
// 启动进程
|
||||
process, err := os.StartProcess(app.Interpreter, args, attr) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
p.Lock() |
||||
defer p.Unlock() |
||||
|
||||
p.process = process |
||||
p.pid = process.Pid |
||||
p.starts++ |
||||
|
||||
err = WriteFile(p.pidfile, []byte(strconv.Itoa(p.pid))) |
||||
if err != nil { |
||||
go p.Stop(true) |
||||
return err |
||||
} |
||||
|
||||
err = UpdateApp(app.ID, |
||||
ConfigStatus(StatusRunning), |
||||
ConfigPid(process.Pid), |
||||
IncreaseStartCounter) |
||||
if err != nil { |
||||
go p.Stop(true) |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// Stop 停止进程
|
||||
func (p *Proc) Stop(force ...bool) error { |
||||
p.Lock() |
||||
if p.process == nil { |
||||
p.Unlock() |
||||
return errors.New("process does not exist") |
||||
} |
||||
p.setStatus(StatusStopping) |
||||
p.Unlock() |
||||
|
||||
// 确定是否优雅关停
|
||||
gracefully := true |
||||
if len(force) > 0 { |
||||
gracefully = force[0] == false |
||||
} |
||||
|
||||
p.Lock() |
||||
defer p.Unlock() |
||||
|
||||
var err error |
||||
if gracefully { |
||||
err = p.process.Signal(syscall.SIGTERM) |
||||
} else { |
||||
err = p.process.Signal(syscall.SIGKILL) |
||||
p.process.Release() |
||||
} |
||||
|
||||
p.setStatus(StatusStopped) |
||||
UpdateApp(p.app.ID, ConfigPid(-1), ConfigStatus(StatusStopped)) |
||||
DeleteFile(p.pidfile) |
||||
|
||||
return err |
||||
} |
||||
|
||||
// Restart 重启进程
|
||||
func (p *Proc) Restart(force ...bool) error { |
||||
// 如果进程可用则停止
|
||||
if p.IsAlive() { |
||||
err := p.Stop(force...) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
|
||||
return p.Start() |
||||
} |
||||
|
||||
// Wait 等待进程结束
|
||||
func (p *Proc) Wait() (*os.ProcessState, error) { |
||||
p.RLock() |
||||
defer p.RUnlock() |
||||
if p.process == nil { |
||||
return nil, errors.New("process does not started") |
||||
} |
||||
return p.process.Wait() |
||||
} |
||||
|
||||
func (p *Proc) setStatus(status uint8) { |
||||
UpdateApp(p.app.ID, ConfigStatus(status)) |
||||
} |
||||
|
||||
// IsAlive 判断进程是否激活
|
||||
func (p *Proc) IsAlive() bool { |
||||
p.RLock() |
||||
pid := p.pid |
||||
p.RUnlock() |
||||
|
||||
if proc, err := os.FindProcess(pid); err != nil { |
||||
return false |
||||
} else { |
||||
return proc.Signal(syscall.Signal(0)) == nil |
||||
} |
||||
} |
||||
|
||||
// Release 释放进程资源
|
||||
func (p *Proc) Release() error { |
||||
p.Lock() |
||||
defer p.Unlock() |
||||
var err error |
||||
if p.process != nil { |
||||
err = p.process.Release() |
||||
} |
||||
if de := DeleteFile(p.pidfile); de != nil { |
||||
return de |
||||
} |
||||
if de := DeleteFile(p.stdinFile); de != nil { |
||||
return de |
||||
} |
||||
if de := DeleteFile(p.stdoutFile); de != nil { |
||||
return de |
||||
} |
||||
if de := DeleteFile(p.stderrFile); de != nil { |
||||
return de |
||||
} |
||||
if de := DeleteFile(p.tempDir); de != nil { |
||||
return err |
||||
} |
||||
return err |
||||
} |
||||
|
||||
// Status 获取运行状态
|
||||
func (p *Proc) Status() uint8 { |
||||
p.RLock() |
||||
defer p.RUnlock() |
||||
return p.status |
||||
} |
||||
|
||||
// Pid 获取进程PID
|
||||
func (p *Proc) Pid() int { |
||||
p.RLock() |
||||
defer p.RUnlock() |
||||
return p.pid |
||||
} |
||||
|
||||
func (p *Proc) NotifyStopped() { |
||||
p.Lock() |
||||
defer p.Unlock() |
||||
p.pid = -1 |
||||
} |
@ -0,0 +1,66 @@ |
||||
package db |
||||
|
||||
import ( |
||||
"os" |
||||
"path/filepath" |
||||
"regexp" |
||||
"strings" |
||||
) |
||||
|
||||
var ( |
||||
appIdRE = regexp.MustCompile(`^\d+$`) |
||||
matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") |
||||
matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") |
||||
) |
||||
|
||||
func ToSnakeCase(str string) string { |
||||
snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}") |
||||
snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}") |
||||
return strings.ToLower(snake) |
||||
} |
||||
|
||||
func IsAppID(s string) bool { |
||||
return len(s) > 0 && appIdRE.MatchString(s) |
||||
} |
||||
|
||||
// WriteFile will write the info on array of bytes b to filepath. It will set the file
|
||||
// permission mode to 0660
|
||||
// Returns an error in case there's any.
|
||||
func WriteFile(filepath string, b []byte) error { |
||||
return os.WriteFile(filepath, b, 0660) |
||||
} |
||||
|
||||
// GetFile will open filepath.
|
||||
// Returns a tuple with a file and an error in case there's any.
|
||||
func GetFile(filepath string) (*os.File, error) { |
||||
return os.OpenFile(filepath, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0777) |
||||
} |
||||
|
||||
// DeleteFile will delete filepath permanently.
|
||||
// Returns an error in case there's any.
|
||||
func DeleteFile(filepath string) error { |
||||
_, err := os.Stat(filepath) |
||||
if err != nil { |
||||
if os.IsNotExist(err) { |
||||
return nil |
||||
} |
||||
return err |
||||
} |
||||
return os.Remove(filepath) |
||||
} |
||||
|
||||
func TempDir(appName string) (string, error) { |
||||
homeDir, err := os.UserHomeDir() |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
tempDir, err := filepath.Abs(homeDir + "/.pmt/" + appName) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
err = os.MkdirAll(tempDir, os.ModePerm) |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
return tempDir, nil |
||||
} |
@ -0,0 +1,74 @@ |
||||
package db |
||||
|
||||
import ( |
||||
"flag" |
||||
"fmt" |
||||
"log" |
||||
"os" |
||||
"os/exec" |
||||
"time" |
||||
) |
||||
|
||||
// 移除不需要的参数
|
||||
func strip(slice []string, remove string) []string { |
||||
for i := 0; i < len(slice); { |
||||
if slice[i] == remove { |
||||
if i != len(slice)-1 { |
||||
slice = append(slice[:i], slice[i+1:]...) |
||||
} else { |
||||
slice = slice[:i] |
||||
} |
||||
} else { |
||||
i++ |
||||
} |
||||
} |
||||
return slice |
||||
} |
||||
|
||||
func subProcess(args []string) *exec.Cmd { |
||||
cmd := exec.Command(args[0], args[1:]...) |
||||
cmd.Stdin = os.Stdin |
||||
cmd.Stdout = os.Stdout |
||||
cmd.Stderr = os.Stderr |
||||
err := cmd.Start() |
||||
if err != nil { |
||||
fmt.Fprintf(os.Stderr, "[-] Error: %s\n", err) |
||||
} |
||||
return cmd |
||||
} |
||||
|
||||
func main() { |
||||
daemon := flag.Bool("daemon", false, "run in daemon") |
||||
forever := flag.Bool("forever", false, "run forever") |
||||
flag.Parse() |
||||
|
||||
// 启用守护模式
|
||||
if *daemon { |
||||
subProcess(strip(os.Args, "-daemon")) |
||||
fmt.Printf("[*] Daemon running in pid: %d PPID: %d\n", os.Getpid(), os.Getppid()) |
||||
os.Exit(0) |
||||
} |
||||
|
||||
if *forever { |
||||
for { |
||||
cmd := subProcess(strip(os.Args, "-forever")) |
||||
fmt.Printf("[*] Forever running in pid: %d PPID: %d\n", os.Getpid(), os.Getppid()) |
||||
if err := cmd.Wait(); err != nil { |
||||
fmt.Println(err) |
||||
os.Exit(1) |
||||
} |
||||
} |
||||
} |
||||
|
||||
fmt.Printf("[*] Service running in pid: %d PPID: %d\n", os.Getpid(), os.Getppid()) |
||||
fp, _ := os.OpenFile("./dosomething.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) |
||||
log.SetOutput(fp) |
||||
for { |
||||
log.Printf("DoSomething running in pid: %d PPID: %d\n", os.Getpid(), os.Getppid()) |
||||
time.Sleep(time.Second * 5) |
||||
} |
||||
} |
||||
|
||||
func run() { |
||||
|
||||
} |
@ -0,0 +1,21 @@ |
||||
module hupeh.vip/pm |
||||
|
||||
go 1.19 |
||||
|
||||
require ( |
||||
github.com/mattn/go-isatty v0.0.17 |
||||
github.com/pelletier/go-toml/v2 v2.0.6 |
||||
github.com/tint/env v1.0.2 |
||||
gopkg.in/ini.v1 v1.67.0 |
||||
gopkg.in/yaml.v3 v3.0.1 |
||||
gorm.io/driver/sqlite v1.4.4 |
||||
gorm.io/gorm v1.24.3 |
||||
) |
||||
|
||||
require ( |
||||
github.com/jinzhu/inflection v1.0.0 // indirect |
||||
github.com/jinzhu/now v1.1.5 // indirect |
||||
github.com/joho/godotenv v1.4.0 // indirect |
||||
github.com/mattn/go-sqlite3 v1.14.15 // indirect |
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect |
||||
) |
@ -0,0 +1,41 @@ |
||||
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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= |
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= |
||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= |
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= |
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= |
||||
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= |
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= |
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= |
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= |
||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= |
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= |
||||
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= |
||||
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= |
||||
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/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= |
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= |
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= |
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= |
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= |
||||
github.com/tint/env v1.0.2 h1:1dUtj11RH07dSJQzbXKGHCjfbqYI7O7mUMdnAnnGDyQ= |
||||
github.com/tint/env v1.0.2/go.mod h1:SqqhryvPryCX0gRezG0zzett+Ib3RB+AWvKo3dYInVo= |
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= |
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= |
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= |
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= |
||||
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= |
||||
gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc= |
||||
gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= |
||||
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= |
||||
gorm.io/gorm v1.24.3 h1:WL2ifUmzR/SLp85CSURAfybcHnGZ+yLSGSxgYXlFBHg= |
||||
gorm.io/gorm v1.24.3/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= |
@ -0,0 +1,197 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
"github.com/tint/env" |
||||
"hupeh.vip/pm/cmd" |
||||
"hupeh.vip/pm/db" |
||||
"os" |
||||
"os/exec" |
||||
"strconv" |
||||
) |
||||
|
||||
//import (
|
||||
// "flag"
|
||||
// "fmt"
|
||||
// "log"
|
||||
// "os"
|
||||
// "os/exec"
|
||||
// "time"
|
||||
//)
|
||||
//
|
||||
//// 移除不需要的参数
|
||||
//func strip(slice []string, remove string) []string {
|
||||
// for i := 0; i < len(slice); {
|
||||
// if slice[i] == remove {
|
||||
// if i != len(slice)-1 {
|
||||
// slice = append(slice[:i], slice[i+1:]...)
|
||||
// } else {
|
||||
// slice = slice[:i]
|
||||
// }
|
||||
// } else {
|
||||
// i++
|
||||
// }
|
||||
// }
|
||||
// return slice
|
||||
//}
|
||||
//
|
||||
//func subProcess(args []string) *exec.Cmd {
|
||||
// cmd := exec.Command(args[0], args[1:]...)
|
||||
// cmd.Stdin = os.Stdin
|
||||
// cmd.Stdout = os.Stdout
|
||||
// cmd.Stderr = os.Stderr
|
||||
// err := cmd.Start()
|
||||
// if err != nil {
|
||||
// fmt.Fprintf(os.Stderr, "[-] Error: %s\n", err)
|
||||
// }
|
||||
// return cmd
|
||||
//}
|
||||
//
|
||||
//func main() {
|
||||
// daemon := flag.Bool("daemon", false, "run in daemon")
|
||||
// forever := flag.Bool("forever", false, "run forever")
|
||||
// flag.Parse()
|
||||
//
|
||||
// // 启用守护模式
|
||||
// if *daemon {
|
||||
// subProcess(strip(os.Arguments, "-daemon"))
|
||||
// fmt.Printf("[*] Daemon running in pid: %d PPID: %d\n", os.Getpid(), os.Getppid())
|
||||
// os.Exit(0)
|
||||
// }
|
||||
//
|
||||
// if *forever {
|
||||
// for {
|
||||
// cmd := subProcess(strip(os.Arguments, "-forever"))
|
||||
// fmt.Printf("[*] Forever running in pid: %d PPID: %d\n", os.Getpid(), os.Getppid())
|
||||
// if err := cmd.Wait(); err != nil {
|
||||
// fmt.Println(err)
|
||||
// os.Exit(1)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// fp, _ := os.OpenFile("./dosomething.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
|
||||
// log.SetOutput(fp)
|
||||
// log.Printf("[*] Service running in pid: %d PPID: %d\n", os.Getpid(), os.Getppid())
|
||||
// defer log.Println("exit ...")
|
||||
// for {
|
||||
// log.Printf("DoSomething running in pid: %d PPID: %d\n", os.Getpid(), os.Getppid())
|
||||
// time.Sleep(time.Second * 5)
|
||||
// }
|
||||
//}
|
||||
|
||||
func main() { |
||||
env.Setup() |
||||
|
||||
switch env.String("PMT_CURRENT_FUNCTION") { |
||||
case "start": |
||||
case "stop": |
||||
case "shell": |
||||
case "list": |
||||
default: |
||||
runCli() |
||||
} |
||||
} |
||||
|
||||
func runCli() { |
||||
cmd.New("pm"). |
||||
Name("pm"). |
||||
Description("description"). |
||||
|
||||
// start 命令
|
||||
Command("start", "Start a program in daemon"). |
||||
Argument("[names:...string]", "file/name/id"). |
||||
Option("-w, --watch <watch:boolean>", "Watches folder changes"). |
||||
Option("-c, --cwd <cwd:string>", "Sets working directory "+cmd.Yellow("Sets working directory")+" Sets working directory "+cmd.Yellow("Sets working directory")+" Sets working directory "+cmd.Yellow(" Sets working directory")+" Sets working directory "+cmd.Yellow("Sets working directory")+" Sets working directory "+cmd.Yellow("Sets working directory")+" End ..."). |
||||
Option("-n, --name", "Sets program name"). |
||||
Option("-e, --env", "Sets current environment name"). |
||||
Option("-i, --interpreter", "Sets interpreter name"). |
||||
Action(func(c *cmd.Command, m map[string]any, a ...any) error { |
||||
if len(a) != 1 { |
||||
return c.ShowHelp() |
||||
} |
||||
|
||||
var apps []*db.App |
||||
scripts := a[0].([]string) |
||||
for _, script := range scripts { |
||||
loads, err := db.ParseInput(script) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
apps = append(apps, loads...) |
||||
} |
||||
|
||||
if len(apps) == 0 { |
||||
fmt.Println("No app found") |
||||
return nil |
||||
} |
||||
|
||||
for _, app := range apps { |
||||
if app.Status == db.StatusRunning { |
||||
fmt.Println(cmd.Yellow(app.Name), cmd.Green("running")) |
||||
continue |
||||
} |
||||
if app.ID == 0 { |
||||
if err := db.SaveApp(app); err != nil { |
||||
fmt.Println(cmd.Yellow(app.Name), cmd.Red("failed for save")) |
||||
return err |
||||
} |
||||
} |
||||
if err := runStart(app.ID); err != nil { |
||||
fmt.Println(cmd.Yellow(app.Name), cmd.Red("failed")) |
||||
fmt.Println(" ", err.Error()) |
||||
} |
||||
} |
||||
fmt.Println("over") |
||||
return nil |
||||
}). |
||||
|
||||
// stop 命令
|
||||
Command("stop", "Stops a program"). |
||||
Argument("<names:...string>", "file/name/id"). |
||||
Action(func(c *cmd.Command, m map[string]any, a ...any) error { |
||||
fmt.Println("停止程序") |
||||
fmt.Println(a...) |
||||
return nil |
||||
}). |
||||
|
||||
// 执行程序
|
||||
Run() |
||||
} |
||||
|
||||
func runStart(id uint) error { |
||||
proc := subProcess( |
||||
[]string{os.Args[0], "-id", strconv.Itoa(int(id))}, |
||||
[]string{"PMT_CURRENT_FUNCTION=start"}, |
||||
) |
||||
return proc.Wait() |
||||
} |
||||
|
||||
func subProcess(args []string, env []string) *exec.Cmd { |
||||
cmd := exec.Command(args[0], args[1:]...) |
||||
cmd.Env = append(os.Environ(), env...) |
||||
cmd.Stdin = os.Stdin |
||||
cmd.Stdout = os.Stdout |
||||
cmd.Stderr = os.Stderr |
||||
err := cmd.Start() |
||||
if err != nil { |
||||
fmt.Fprintf(os.Stderr, "[-] Error: %s\n", err) |
||||
} |
||||
return cmd |
||||
} |
||||
|
||||
// 移除不需要的参数
|
||||
func strip(slice []string, remove string) []string { |
||||
for i := 0; i < len(slice); { |
||||
if slice[i] == remove { |
||||
if i != len(slice)-1 { |
||||
slice = append(slice[:i], slice[i+1:]...) |
||||
} else { |
||||
slice = slice[:i] |
||||
} |
||||
} else { |
||||
i++ |
||||
} |
||||
} |
||||
return slice |
||||
} |
@ -0,0 +1,3 @@ |
||||
go build -ldflags "-s -w" main.go |
||||
|
||||
main.exe -daemon -forever |
@ -0,0 +1,17 @@ |
||||
package tools |
||||
|
||||
type StartCommand struct { |
||||
App string `json:"app"` |
||||
Watch bool `json:"watch"` |
||||
Env string `json:"env"` |
||||
Name string `json:"name"` |
||||
Cwd string `json:"cwd"` |
||||
} |
||||
|
||||
func RunStart(apps ...string) { |
||||
|
||||
} |
||||
|
||||
func (s *StartCommand) Execute() error { |
||||
return nil |
||||
} |
Loading…
Reference in new issue