From a0062b93b225e51aa3ff1884068fbe015ffdc778 Mon Sep 17 00:00:00 2001 From: hupeh Date: Thu, 5 Jan 2023 23:17:35 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 + cmd/command.go | 379 ++++++++++++++++++++++++++++++++++ cmd/option.go | 542 +++++++++++++++++++++++++++++++++++++++++++++++++ cmd/print.go | 388 +++++++++++++++++++++++++++++++++++ db/config.go | 138 +++++++++++++ db/db.go | 117 +++++++++++ db/proc.go | 279 +++++++++++++++++++++++++ db/utils.go | 66 ++++++ db/worker.go | 74 +++++++ go.mod | 21 ++ go.sum | 41 ++++ main.go | 197 ++++++++++++++++++ readme.md | 3 + tools/start.go | 17 ++ 14 files changed, 2268 insertions(+) create mode 100644 .gitignore create mode 100644 cmd/command.go create mode 100644 cmd/option.go create mode 100644 cmd/print.go create mode 100644 db/config.go create mode 100644 db/db.go create mode 100644 db/proc.go create mode 100644 db/utils.go create mode 100644 db/worker.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 readme.md create mode 100644 tools/start.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d3af11 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea +tmp +*.db +*.iml +.env +.env.* \ No newline at end of file diff --git a/cmd/command.go b/cmd/command.go new file mode 100644 index 0000000..be22cda --- /dev/null +++ b/cmd/command.go @@ -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 +} diff --git a/cmd/option.go b/cmd/option.go new file mode 100644 index 0000000..87a96d1 --- /dev/null +++ b/cmd/option.go @@ -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] +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 +} diff --git a/cmd/print.go b/cmd/print.go new file mode 100644 index 0000000..b929134 --- /dev/null +++ b/cmd/print.go @@ -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 +} diff --git a/db/config.go b/db/config.go new file mode 100644 index 0000000..739de23 --- /dev/null +++ b/db/config.go @@ -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 +} diff --git a/db/db.go b/db/db.go new file mode 100644 index 0000000..277efd8 --- /dev/null +++ b/db/db.go @@ -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 +} diff --git a/db/proc.go b/db/proc.go new file mode 100644 index 0000000..1f18e6d --- /dev/null +++ b/db/proc.go @@ -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 +} diff --git a/db/utils.go b/db/utils.go new file mode 100644 index 0000000..618eb7c --- /dev/null +++ b/db/utils.go @@ -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 +} diff --git a/db/worker.go b/db/worker.go new file mode 100644 index 0000000..c763111 --- /dev/null +++ b/db/worker.go @@ -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() { + +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7c0aed4 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..28a5e25 --- /dev/null +++ b/go.sum @@ -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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..aff6bf2 --- /dev/null +++ b/main.go @@ -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 ", "Watches folder changes"). + Option("-c, --cwd ", "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("", "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 +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..05aa0c3 --- /dev/null +++ b/readme.md @@ -0,0 +1,3 @@ +go build -ldflags "-s -w" main.go + +main.exe -daemon -forever \ No newline at end of file diff --git a/tools/start.go b/tools/start.go new file mode 100644 index 0000000..7d176fc --- /dev/null +++ b/tools/start.go @@ -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 +}