From 24282364a254805bdda9512ad110b741a1564d39 Mon Sep 17 00:00:00 2001 From: "STARAI\\Star" Date: Sun, 20 Oct 2024 09:03:45 +0800 Subject: [PATCH] 0 --- README.md | 31 ++ go.mod | 27 ++ go.sum | 32 ++ main.go | 811 +++++++++++++++++++++++++++++++++++++++++++ templates/_gitignore | 7 + templates/_main.go | 197 +++++++++++ tests/.gitignore | 7 + tests/apigo.yml | 25 ++ tests/go.mod | 70 ++++ tests/main.go | 205 +++++++++++ 10 files changed, 1412 insertions(+) create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 templates/_gitignore create mode 100644 templates/_main.go create mode 100644 tests/.gitignore create mode 100644 tests/apigo.yml create mode 100644 tests/go.mod create mode 100644 tests/main.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..e798a80 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# ag - tools for apigo.cloud + +## Install + +```shell +go install apigo.cc/apigo/ag@latest +``` + +## Usage + +``` +tools for apigo.cloud + +Usage: + ag [command] [...] + ag [short command] [...] + +Commands: + init [i] init a new project for empty dir + init plugin [i p] init a new plugin project for empty dir + run [r] will exec `go run .` + watch run [rr] [...] run project use gowatch, if project files changed will restart auto, ... args see gowatch help https://github.com/ssgo/tool + test [t] will exec `go test -v .`, will exec into tests if exists tests dir + watch test [tt] [...] test project use gowatch, if project files changed will restart auto, ... args see gowatch help https://github.com/ssgo/tool + tidy [td] tidy project, find imported plugins from .js files add import code to jsImports.go, export typescript code for used plugins into "plugins/" + tags [] show git tags + commit [co] comment commit git repo and push, comment is need + tag+ [t+] [version] add git tag push, if no new tag specified will use last tag +1 + +``` + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..09d481d --- /dev/null +++ b/go.mod @@ -0,0 +1,27 @@ +module apigo.cc/apigo/ag + +go 1.18 + +require ( + github.com/ssgo/httpclient v1.7.7 + github.com/ssgo/u v1.7.9 +) + +require ( + github.com/dlclark/regexp2 v1.11.4 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect + github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect + github.com/ssgo/tool v0.4.27 // indirect + golang.org/x/sys v0.26.0 // indirect +) + +require ( + apigo.cc/gojs v0.0.2 + github.com/ssgo/config v1.7.7 // indirect + github.com/ssgo/log v1.7.7 // indirect + github.com/ssgo/standard v1.7.7 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/text v0.19.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f780b76 --- /dev/null +++ b/go.sum @@ -0,0 +1,32 @@ +apigo.cc/gojs v0.0.2 h1:MMJfnFuQHNOVFnxiNhfMa4Yrr/CtCcW6sO27DZux/DE= +apigo.cc/gojs v0.0.2/go.mod h1:iuTCYlxSnz2ARxaYigk5sHpUEjLCSKdtKXcG9xwxKxY= +github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= +github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q= +github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U= +github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= +github.com/ssgo/config v1.7.7 h1:gYYDuEEdesH41oNtvKHuwaua9quU655O8TRd3pBcuK8= +github.com/ssgo/config v1.7.7/go.mod h1:4mnR3YLGkmK+YnVT0NAbfUpvxqGyjyEElag+bH4zb0c= +github.com/ssgo/httpclient v1.7.7 h1:ex7pEwEpDaNtm334b04F0EQ62rPCNNxjOMi9tosXvGA= +github.com/ssgo/httpclient v1.7.7/go.mod h1:YA1MaQaThr8F9U1gnbUZhvUZY8ppAeI0A5GksPaVeYE= +github.com/ssgo/log v1.7.7 h1:EjgPGDTAEz+ApNyluLpof9QlftxBiqh1Jt3e3E0h/m4= +github.com/ssgo/log v1.7.7/go.mod h1:5E2Mkk+np9SCU84bX+lxBOG8QBD3XVkvFCbFi0RiKyk= +github.com/ssgo/standard v1.7.7 h1:5tnlcr9Nmftp7JI3jszYCEbW7VgS5HHsGueD+yWxbh0= +github.com/ssgo/standard v1.7.7/go.mod h1:LZcn56DzHu8OlDXrUPLI6h+RZbZRXhkmiKh6PSE8eDs= +github.com/ssgo/tool v0.4.27 h1:tB2VvEWt0jSazel2/G+/JmhsYesJWCeRLEdyV2r5QeM= +github.com/ssgo/tool v0.4.27/go.mod h1:qIQYzXya36WWVypWP5T/AgKwKBwZkEvMRfjACkAeUuw= +github.com/ssgo/u v1.7.9 h1:m1wcJWQg13+NbqpG+c2z3yWO+PC7qE3PIwKfXhQqdPg= +github.com/ssgo/u v1.7.9/go.mod h1:dUG/PBG5k9fSM7SOp8RZLsK0KytNxhtenpoLgjhfxpY= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..aef72ca --- /dev/null +++ b/main.go @@ -0,0 +1,811 @@ +package main + +import ( + "archive/zip" + _ "embed" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strconv" + "strings" + "text/template" + "time" + + "github.com/ssgo/httpclient" + "github.com/ssgo/u" +) + +var version = "v0.0.1" + +//go:embed templates/_main.go +var mainCodeTPL string + +//go:embed templates/_gitignore +var gitignoreTPL string + +var goPath = "go" + +type Command struct { + Name string + ShortName string + Args string + Comment string + Func func([]string) bool +} + +type Config struct { + Name string + Version string + Main string + Output string + Target map[string]string + Module map[string]string + ModuleAlias map[string]string + ExtraImport map[string]string + CacheFile []string +} + +var commands = []Command{ + {"init", "i", "", "init a new project for empty dir", initProject}, + {"build", "b", "", "build project for current os", buildProject}, + // {"init plugin", "i p", "", "init a new plugin project for empty dir", initPluginProject}, + // {"run", "r", "", "will exec `go run .`", runProject}, + // {"watch run", "rr", "[...]", "run project use sskey, if project files changed will restart auto, ... args see sskey help https://github.com/ssgo/tool", devProject}, + // {"test", "t", "", "will exec `go test -v .`, will exec into tests if exists tests dir", testProject}, + // {"watch test", "tt", "[...]", "test project use sskey, if project files changed will restart auto, ... args see sskey help https://github.com/ssgo/tool", devTestProject}, + // {"tidy", "td", "", "tidy project, find imported plugins from .js files add import code to jsImports.go, export typescript code for used plugins into \"plugins/\"", tidy}, + // {"tags", "", "", "show git tags", showGitTags}, + // {"commit", "co", "comment", "commit git repo and push, comment is need", commitGitRepo}, + // {"commit and +tag", "co+", "comment", "commit git repo and push, than update tag, comment is need", commitAndTagGitRepo}, + // {"update tag", "t+", "[version]", "add git tag push, if no new tag specified will use last tag +1", addGitTag}, + +} + +var hc = httpclient.GetClient(30 * time.Second) +var cachePath = ".ag" + +func init() { + hc.SetGlobalHeader("Content-Type", "application/json") + if homePath, err := os.UserHomeDir(); err == nil { + cachePath = filepath.Join(homePath, ".ag", "cache") + } +} + +func showGitTags(args []string) bool { + return nil == runCommand("git", "tag", "-l", "v*", "--sort=-taggerdate", "--format=%(refname:short) %(taggerdate:short) %(*objectname:short)") +} + +func commitAndTagGitRepo(args []string) bool { + if commitGitRepo(args) { + return addGitTag([]string{}) + } + return false +} + +func commitGitRepo(args []string) bool { + comment := strings.Join(args, " ") + if comment != "" { + if err := runCommand("git", "commit", "-a", "-m", comment); err == nil { + if err := runCommand("git", "push"); err == nil { + return true + } else { + fmt.Println("git push failed:", err.Error()) + } + } else { + fmt.Println("git commit failed:", err.Error()) + } + } else { + fmt.Println("commit message is empty") + } + return false +} + +func addGitTag(args []string) bool { + newVer := "" + if len(args) > 0 { + newVer = args[0] + } + if newVer == "" { + if outs, err := u.RunCommand("git", "tag", "-l", "v*", "--sort=taggerdate"); err == nil { + oldVer := "v0.0.0" + for i := len(outs) - 1; i >= 0; i-- { + if outs[i][0] == 'v' && strings.IndexByte(outs[i], '.') != -1 { + oldVer = outs[len(outs)-1] + break + } + } + + if len(args) > 0 { + newVer = args[0] + } else { + versionParts := strings.Split(oldVer, ".") + v, err := strconv.Atoi(versionParts[len(versionParts)-1]) + if err != nil { + v = 0 + } + versionParts[len(versionParts)-1] = strconv.Itoa(v + 1) + newVer = strings.Join(versionParts, ".") + } + } else { + fmt.Println("get last tag failed:", err.Error()) + } + } + + if newVer != "" { + if err := runCommand("git", "tag", "-a", newVer, "-m", "update tag by 'ag tag+'"); err == nil { + if err := runCommand("git", "push", "origin", newVer); err != nil { + fmt.Println("git push failed:", err.Error()) + } + } else { + fmt.Println("git add tag failed:", err.Error()) + } + } + return false +} + +func findTool() (sskeyPath string, logVPath string) { + sskeyPath = "sskey" + logVPath = "logv" + if binPath, err := exec.LookPath("sskey"); err == nil && binPath != "" { + sskeyPath = binPath + } + if binPath, err := exec.LookPath("logv"); err == nil && binPath != "" { + logVPath = binPath + } + + if sskeyPath == "sskey" || logVPath == "logv" { + _ = runCommand(goPath, "get", "-u", "github.com/ssgo/tool") + if sskeyPath == "sskey" { + _ = runCommand(goPath, "install", "github.com/ssgo/tool/sskey") + } + if logVPath == "logv" { + _ = runCommand(goPath, "install", "github.com/ssgo/tool/logv") + } + } + return sskeyPath, logVPath +} + +func parseRepo(url string) (apiUrl, owner, repo string) { + name := "" + if strings.HasPrefix(url, "github.com/") { + apiUrl = "https://api.github.com/" + name = url[11:] + } else if strings.HasPrefix(url, "gitee.com/") { + apiUrl = "https://gitee.com/api/v5/" + name = url[10:] + } else if strings.HasPrefix(url, "apigo.cc/") { + apiUrl = "https://apigo.cc/api/v1/" + name = url[16:] + } else { + apiUrl = "https://apigo.cc/api/v1/" + if strings.ContainsRune(url, '/') { + name = url + } else { + name = "apigo/" + url + } + } + + if !strings.ContainsRune(name, '/') { + name = name + "/" + name + } + + a := strings.Split(name, "/") + owner = a[0] + repo = a[1] + + return +} + +type Tag struct { + Name string + Commit struct { + Sha string + Url string + } + Zipball_url string + Tarball_url string +} + +func fetchRepo(name string) (repoPath string) { + apiUrl, owner, repo := parseRepo(name) + tagsUrl := apiUrl + filepath.Join("repos", owner, repo, "tags") + tags := make([]Tag, 0) + fmt.Println("try to fetch tags", tagsUrl) + r := hc.Get(tagsUrl) + if r.Error == nil { + r.To(&tags) + } else if apiUrl != "https://api.github.com/" { + tagsUrl = "https://api.github.com/" + filepath.Join("repos", owner, repo, "tags") + fmt.Println("try to fetch tags", tagsUrl) + r = hc.Get(tagsUrl) + if r.Error == nil { + r.To(&tags) + } else { + fmt.Println("fetch tags error", r.Error) + } + } + if len(tags) > 0 { + lastVersion := tags[0].Name + zipUrl := tags[0].Zipball_url + tagPath := filepath.Join(cachePath, owner, repo, lastVersion) + if !u.FileExists(tagPath) { + zipFile := filepath.Join(cachePath, owner, repo, lastVersion+".zip") + fmt.Println("Download file ", zipUrl) + _, err := hc.Download(zipFile, zipUrl, func(start, end int64, ok bool, finished, total int64) { + fmt.Printf(" %.2f%%", float32(finished)/float32(total)*100) + }) + fmt.Println() + if err != nil { + fmt.Println(u.BRed("download file failed")) + return + } + if u.FileExists(zipFile) { + if reader, err := zip.OpenReader(zipFile); err == nil { + for _, f := range reader.File { + toFile := filepath.Join(tagPath, f.Name) + if f.FileInfo().IsDir() { + //fmt.Println("extract dir", f.Name, toFile) + os.MkdirAll(toFile, 0755) + } else { + //fmt.Println("extract file", f.Name, toFile) + u.CheckPath(toFile) + if fp, err := os.OpenFile(toFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644); err == nil { + if rd, err := f.Open(); err == nil { + if _, err := io.Copy(fp, rd); err != nil { + fmt.Println(u.BRed("write file failed"), toFile, err.Error()) + } else { + //fmt.Println(u.BGreen("write file success"), toFile) + } + _ = rd.Close() + } else { + fmt.Println(u.BRed("open file in zip failed"), f.Name, err.Error()) + } + _ = fp.Close() + } else { + fmt.Println(u.BRed("open dst file failed"), toFile, err.Error()) + } + } + } + _ = reader.Close() + } else { + fmt.Println(u.BRed("open zip file failed"), zipFile, err.Error()) + } + _ = os.Remove(zipFile) + } + } + return filepath.Join(tagPath, repo) + } else { + fmt.Println(u.BRed("no repo found for"), name) + return "" + } +} + +func getConfig() *Config { + conf := &Config{ + Name: "", + Version: "v0.0.1", + Main: "main.js", + Output: "dist", + Module: map[string]string{}, + ModuleAlias: map[string]string{}, + ExtraImport: map[string]string{}, + CacheFile: []string{}, + } + u.LoadYaml("apigo.yml", conf) + + if conf.Name == "" { + if pathname, err := os.Getwd(); err != nil { + conf.Name = "apigo" + } else { + conf.Name = filepath.Base(pathname) + } + } + + if conf.Target == nil { + conf.Target = map[string]string{ + "darwin": "amd64 arm64", + "linux": "amd64 arm64", + "windows": "amd64", + } + } + + if conf.Module == nil { + conf.Module = map[string]string{} + } + + if conf.ModuleAlias == nil { + conf.ModuleAlias = map[string]string{} + } + + if conf.ExtraImport == nil { + conf.ExtraImport = map[string]string{} + } + + if conf.CacheFile == nil { + conf.CacheFile = []string{} + } + + for pkgName, pkgVersion := range conf.Module { + if pkgVersion == "" { + conf.Module[pkgName] = "latest" + } + + aliasName := pkgName + if strings.HasPrefix(aliasName, "apigo.cc/gojs/") { + aliasName = aliasName[14:] + } else if strings.HasPrefix(aliasName, "apigo.cc/") { + aliasName = aliasName[9:] + } + if aliasName != pkgName && conf.ModuleAlias[aliasName] == "" { + conf.ModuleAlias[aliasName] = pkgName + } + } + + for pkgName, pkgVersion := range conf.ExtraImport { + if pkgVersion == "" { + conf.ExtraImport[pkgName] = "latest" + } + } + return conf +} + +func initProject(args []string) bool { + // if u.FileExists("go.mod") { + // fmt.Println(u.Red("go.mod exists, are you sure to init this project?")) + // sure := 'n' + // _, _ = fmt.Scan(&sure) + // if sure != 'y' && sure != 'Y' { + // return false + // } + // } + if u.FileExists("main.go") || u.FileExists("go.mod") { + fmt.Println(u.Red("main.go or go.mod is exists, are you sure to overwrite it? (y/n)")) + sure := 'n' + _, _ = fmt.Scan(&sure) + if sure != 'y' && sure != 'Y' { + return false + } + } + + if !u.FileExists("apigo.yml") { + refProjName := "default" + if len(args) > 0 { + refProjName = args[0] + } + // TODO check name is path + repoPath := fetchRepo(refProjName) + if u.FileExists(repoPath) { + if err := CopyFile(repoPath, "."); err == nil { + fmt.Println(u.Green("copy project files success"), repoPath) + } else { + fmt.Println(u.BRed("copy project files failed"), repoPath, err.Error()) + return false + } + } else { + fmt.Println(u.BRed("fetch repo failed"), repoPath) + return false + } + } + + conf := getConfig() + writeFile("main.go", mainCodeTPL, conf) + + _ = runCommand(goPath, "mod", "init", conf.Name) + _ = runCommand(goPath, "mod", "edit", "-go=1.18") + + for pkgName, pkgVersion := range conf.Module { + _ = runCommand(goPath, "get", pkgName+"@"+pkgVersion) + } + + for pkgName, pkgVersion := range conf.ExtraImport { + _ = runCommand(goPath, "get", pkgName+"@"+pkgVersion) + } + + if !u.FileExists(".gitignore") { + writeFile(".gitignore", gitignoreTPL, nil) + } + _ = runCommand(goPath, "mod", "tidy") + findTool() + fmt.Println(u.BGreen("new project " + conf.Name + " created")) + fmt.Println(u.Cyan("run \"ag build\" to use it")) + fmt.Println(u.Cyan("run \"ag test\" to test")) + fmt.Println(u.Cyan("run \"ag build all\" to publish")) + return true +} + +func buildProject(args []string) bool { + conf := getConfig() + + return false +} + +func CopyFile(from, to string) error { + fromStat, _ := os.Stat(from) + if fromStat.IsDir() { + // copy dir + for _, f := range u.ReadDirN(from) { + err := CopyFile(filepath.Join(from, f.Name), filepath.Join(to, f.Name)) + if err != nil { + return err + } + } + return nil + } else { + // copy file + toStat, err := os.Stat(to) + if err == nil && toStat.IsDir() { + to = filepath.Join(to, filepath.Base(from)) + } + u.CheckPath(to) + if writer, err := os.OpenFile(to, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err == nil { + defer writer.Close() + if reader, err := os.OpenFile(from, os.O_RDONLY, 0644); err == nil { + defer reader.Close() + _, err = io.Copy(writer, reader) + return err + } else { + return err + } + } else { + return err + } + } +} + +// func _runProject(args []string, isWatch bool) bool { +// _ = runCommand(goPath, "mod", "tidy") +// goBinPath, logVPath := findTool() +// if isWatch { +// args = append(args, "-pt", ".go,.js,.yml", "run", ".") +// } else { +// goBinPath = goPath +// args = append(args, "run", ".") +// } +// return nil == runCommandPipeWithEnv(logVPath, goBinPath, goRunEnv, args...) +// } + +// func runProject(args []string) bool { +// return _runProject(args, false) +// } + +// func devProject(args []string) bool { +// return _runProject(args, true) +// } + +// func testProject(args []string) bool { +// return _testProject(args, false) +// } + +// func devTestProject(args []string) bool { +// return _testProject(args, true) +// } + +// func _testProject(args []string, isWatch bool) bool { +// if u.FileExists("tests") { +// if u.FileExists(filepath.Join("tests", "go.mod")) { +// _ = os.Chdir("tests") +// if isWatch { +// args = append(args, "-p", "..") +// } +// isRun := false +// for _, f := range u.ReadDirN(".") { +// if strings.HasSuffix(f.Name, ".go") { +// goStr := u.ReadFileN(f.FullName) +// if strings.Contains(goStr, "package main") && !strings.Contains(goStr, "package main_test") { +// isRun = true +// } +// break +// } +// } +// if !isRun { +// args = append(args, "test", "-v", ".") +// } else { +// args = append(args, "run", ".") +// } +// } else { +// args = append(args, "test", "-v", "tests") +// } +// } else { +// args = append(args, "test", "-v", ".") +// } +// _ = runCommand(goPath, "mod", "tidy") +// sskeyPath, logVPath := findTool() +// if isWatch { +// args2 := append([]string{"-pt", ".go,.js,.yml"}, args...) +// _ = runCommandPipeWithEnv(logVPath, sskeyPath, goRunEnv, args2...) +// } else { +// _ = runCommandPipeWithEnv(logVPath, goPath, goRunEnv, args...) +// } +// return true +// } + +var pkgMatcher = regexp.MustCompile(`(?im)^\s*(import)?\s*_\s+"([\w-_/.]+)"`) +var importMatcher = regexp.MustCompile(`(?im)^\s*import\s+([\w{}, ]+)\s+from\s+['"]([\w./\\\- ]+)['"]`) + +func makeJsImports(path string, jsImports *[]string, pkgName string) { + if u.FileExists(filepath.Join(path, "go.mod")) { + if m := modNameMatcher.FindStringSubmatch(u.ReadFileN(filepath.Join(path, "go.mod"))); m != nil { + if pkgName != m[1] { + return + } + } + } + for _, f := range u.ReadDirN(path) { + filename := f.Name + fullName := filepath.Join(path, filename) + if !strings.HasPrefix(filename, ".") && !strings.HasPrefix(filename, "_") && filename != "_makePluginCode" && filename != "node_modules" && filename != "www" && filename != "src" { + if f.IsDir { + makeJsImports(fullName, jsImports, pkgName) + } else if strings.HasSuffix(filename, ".js") { + if code, err := u.ReadFile(fullName); err == nil { + for _, m := range importMatcher.FindAllStringSubmatch(code, 1024) { + if !strings.HasPrefix(m[2], ".") && !strings.HasPrefix(m[2], "_") && m[2] != "current-plugin" && m[2] != "console" && m[2] != "logger" { + checkFile := filepath.Join(path, m[2]) + checkFileInfo := u.GetFileInfo(checkFile) + if checkFileInfo != nil && checkFileInfo.IsDir { + checkFile = filepath.Join(checkFile, "index.js") + } else if !strings.HasSuffix(checkFile, ".js") { + checkFile += ".js" + } + //fmt.Println(u.BMagenta(checkFile), u.FileExists(checkFile)) + if !u.FileExists(checkFile) { + *jsImports = u.AppendUniqueString(*jsImports, m[2]) + } + } + } + } + } + } + } +} + +func makePluginCodeImports(from string, imports *[]string, parentModuleName string) { + jsImports := make([]string, 0) + currentParentModuleName := parentModuleName + if u.FileExists(filepath.Join(from, "go.mod")) { + pkgName := "" + if m := modNameMatcher.FindStringSubmatch(u.ReadFileN(filepath.Join(from, "go.mod"))); m != nil { + pkgName = m[1] + } + makeJsImports(from, &jsImports, pkgName) + parentModuleName = pkgName + } + + currentPkgName := "" + for _, f := range u.ReadDirN(from) { + if f.IsDir { + if !strings.HasPrefix(f.Name, ".") { + makePluginCodeImports(filepath.Join(from, f.Name), imports, parentModuleName) + } + } else { + if strings.HasSuffix(f.Name, ".go") && !strings.HasPrefix(f.Name, ".") && f.Name != "makePluginCode.go" { + if code, err := u.ReadFile(filepath.Join(from, f.Name)); err == nil { + if m := pkgNameMatcher.FindStringSubmatch(code); m != nil { + if currentPkgName+"_test" != m[1] { + currentPkgName = m[1] + } + } + for _, m := range pkgMatcher.FindAllStringSubmatch(code, 1024) { + if m[2] != "current-plugin" && !strings.HasPrefix(f.Name, "jsImports") { + *imports = u.AppendUniqueString(*imports, m[2]) + } + } + } + } + } + } + + if currentPkgName != "" && u.FileExists(filepath.Join(from, "go.mod")) { + pkgList := make([]string, 0) + for _, plgPkg := range jsImports { + if plgPkg != currentParentModuleName && !u.StringIn(*imports, plgPkg) { + pkgList = append(pkgList, plgPkg) + *imports = u.AppendUniqueString(*imports, plgPkg) + } + } + if len(pkgList) > 0 { + _ = u.WriteFile(filepath.Join(from, u.StringIf(strings.HasSuffix(currentPkgName, "_test"), "jsImports_test.go", "jsImports.go")), `package `+currentPkgName+` +import _ "`+strings.Join(pkgList, "\"\nimport _ \"")+`" +`) + } + } +} + +func writeFile(filename string, fileContent string, data any) { + if fp, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600); err == nil { + tpl, _ := template.New(filename).Parse(fileContent) + if err = tpl.Execute(fp, data); err != nil { + fmt.Println(u.Red(err.Error())) + } + _ = fp.Close() + } else { + fmt.Println(u.Red(err.Error())) + } +} + +var replaceMatcher = regexp.MustCompile(`([a-zA-Z0-9._\-/]+)\s+(v[0-9.]+)\s+=>\s+([a-zA-Z0-9._\-/\\]+)`) +var modNameMatcher = regexp.MustCompile(`(?m)^module\s+([a-zA-Z0-9._\-/]+)$`) +var pkgNameMatcher = regexp.MustCompile(`(?m)^package\s+([a-zA-Z0-9._\-/]+)$`) + +// func tidy(args []string) bool { +// // 判断是否可执行项目(不创建指向项目本身的 import _) +// isMainProject := false +// isEmptyProject := true +// if files, err := os.ReadDir("."); err == nil { +// for _, f := range files { +// if !f.IsDir() && strings.HasSuffix(f.Name(), ".go") { +// isEmptyProject = false +// code, _ := u.ReadFile(f.Name()) +// if strings.Contains(code, "package main") || strings.Contains(code, "func main(") { +// isMainProject = true +// break +// } +// } +// } +// } + +// // 扫描用到的插件(import _) +// imports := make([]string, 0) +// makePluginCodeImports(".", &imports, "") + +// findGoModCode, _ := u.ReadFile("go.mod") +// goModCode := "module main\ngo 1.18\n" +// currentModuleName := "current-project" +// if !isMainProject { +// if m := modNameMatcher.FindStringSubmatch(findGoModCode); m != nil { +// currentModuleName = m[1] +// } +// goModCode += "require " + currentModuleName + " v0.0.0 // indirect\nreplace " + currentModuleName + " v0.0.0 => ../\n" +// } + +// // 扫描 replace,处理路径后加入到 _makePluginCode/go.mod +// for _, m := range replaceMatcher.FindAllStringSubmatch(findGoModCode, 100) { +// replacePath := m[3] +// if absPath, err := filepath.Abs(m[3]); err == nil { +// replacePath = absPath +// } +// goModCode += fmt.Sprintln("replace", m[1], m[2], "=>", replacePath) +// } + +// if !isMainProject && !isEmptyProject { +// imports = append(imports, currentModuleName) +// } + +// _ = u.WriteFile(filepath.Join("_makePluginCode", "go.mod"), goModCode) +// writeFile(filepath.Join("_makePluginCode", "main.go"), makePluginCodeTPL, map[string]any{"imports": imports}) +// _ = os.Chdir("_makePluginCode") +// defer func() { +// _ = os.Chdir("..") +// //_ = os.RemoveAll("_makePluginCode") +// }() +// _ = runCommand(goPath, "mod", "tidy") +// if err := runCommandWithEnv(goPath, goRunEnv, "run", "."); err != nil { +// fmt.Println(u.Red(err.Error())) +// } +// return true +// } + +func runCommand(name string, args ...string) error { + return runCommandWithEnv(name, nil, args...) +} + +func runCommandWithEnv(name string, env []string, args ...string) error { + pathname, _ := os.Getwd() + fmt.Println(u.BMagenta(pathname), u.BCyan(name), u.Cyan(strings.Join(args, " "))) + cmd := exec.Command(name, args...) + if env != nil { + if runtime.GOOS == "windows" { + env = append(env, "GOTMPDIR="+pathname, "GOWORK="+pathname) + } + cmd.Env = append(cmd.Env, env...) + } + + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + fmt.Println(u.Red(err.Error())) + } + return err +} + +func runCommandPipe(pipeCommandName, commandName string, args ...string) error { + return runCommandPipeWithEnv(pipeCommandName, commandName, nil, args...) +} + +func runCommandPipeWithEnv(pipeCommandName, commandName string, env []string, args ...string) error { + pathname, _ := os.Getwd() + fmt.Println(u.BMagenta(pathname), u.BCyan(commandName), u.Cyan(strings.Join(args, " ")), u.BMagenta(pipeCommandName)) + cmd1 := exec.Command(commandName, args...) + cmd2 := exec.Command(pipeCommandName) + if env != nil { + if runtime.GOOS == "windows" { + env = append(env, "GOTMPDIR="+pathname, "GOWORK="+pathname) + } + cmd1.Env = append(cmd1.Env, env...) + cmd2.Env = append(cmd2.Env, env...) + } + + r, w := io.Pipe() + wClosed := false + defer func() { + if !wClosed { + w.Close() + } + }() + cmd1.Stdin = os.Stdin + cmd1.Stdout = w + cmd1.Stderr = w + cmd2.Stdin = r + cmd2.Stdout = os.Stdout + cmd2.Stderr = os.Stderr + var err error + if err = cmd2.Start(); err == nil { + if err = cmd1.Start(); err == nil { + if err = cmd1.Wait(); err == nil { + w.Close() + wClosed = true + // 等待第二个命令完成 + if err = cmd2.Wait(); err == nil { + return nil + } + } + } + } + fmt.Println(u.Red(err.Error())) + return err +} + +func main() { + var err error + if goPath, err = exec.LookPath("go"); err != nil || goPath == "" { + fmt.Println(u.Red("Please install Go SDK first")) + fmt.Println(u.Cyan("https://go.dev/")) + return + } + + if len(os.Args) > 1 { + cmd1 := os.Args[1] + cmd2 := cmd1 + if len(os.Args) > 2 { + cmd2 += " " + os.Args[2] + } + for i := len(commands) - 1; i >= 0; i-- { + cmdInfo := commands[i] + if len(os.Args) > 2 && (cmd2 == cmdInfo.Name || cmd2 == cmdInfo.ShortName) { + cmdInfo.Func(os.Args[3:]) + return + } else if cmd1 == cmdInfo.Name || cmd1 == cmdInfo.ShortName { + cmdInfo.Func(os.Args[2:]) + return + } + } + } + + fmt.Println("tools for apigo.cloud", version) + fmt.Println("go sdk", goPath) + fmt.Println() + fmt.Println("Usage:") + fmt.Println(" ", u.Cyan("ag [command] [...]")) + fmt.Println(" ", u.Magenta("ag [short command] [...]")) + fmt.Println() + fmt.Println("Commands:") + for _, cmdInfo := range commands { + padStr := "" + padN := 30 - len(cmdInfo.Name) - len(cmdInfo.ShortName) - len(cmdInfo.Args) + if padN > 0 { + padStr = strings.Repeat(" ", padN) + } + fmt.Println(" ", u.Cyan(cmdInfo.Name), u.Dim("[")+u.Magenta(cmdInfo.ShortName)+u.Dim("]"), cmdInfo.Args, padStr, cmdInfo.Comment) + } + fmt.Println() + fmt.Println("Examples:") + fmt.Println(" ", u.Magenta("ag +"), " create a new simple project with Hello World") + fmt.Println(" ", u.Magenta("ag +p ali"), " create a new plugin project named ali for use aliyun services") + fmt.Println() +} diff --git a/templates/_gitignore b/templates/_gitignore new file mode 100644 index 0000000..bab99ec --- /dev/null +++ b/templates/_gitignore @@ -0,0 +1,7 @@ +.* +!.gitignore +go.sum +/dist +/build* +node_modules +package.json \ No newline at end of file diff --git a/templates/_main.go b/templates/_main.go new file mode 100644 index 0000000..241b44c --- /dev/null +++ b/templates/_main.go @@ -0,0 +1,197 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "syscall" + "time" + + "apigo.cc/gojs" + {{range $pkg, $ver := .Module}} + _ "{{$pkg}}" + {{- end}} + {{range $pkg, $ver := .ExtraImport}} + _ "{{$pkg}}" + {{- end}} + "github.com/ssgo/log" + "github.com/ssgo/u" +) + +var appName = "{{.Name}}" +var appVersion = "{{.Version}}" +var idFixMatcher = regexp.MustCompile(`[^a-zA-Z0-9_]`) + +// TODO embed .CacheFiles + +func init() { + {{- range $alias, $pkg := .ModuleAlias }} + gojs.Alias("{{$alias}}", "{{$pkg}}") + {{- end}} +} + +func main() { + if appName == "" { + appName = filepath.Base(os.Args[0]) + appName = strings.TrimSuffix(appName, ".exe") + } + + if appVersion == "" { + appVersion = "0.0.1" + } + + args := os.Args[1:] + if len(args) > 0 { + if args[0] == "help" || args[0] == "--help" || args[0] == "-h" { + printUsage() + return + } + + if args[0] == "version" || args[0] == "--version" || args[0] == "-v" { + fmt.Println(appName, appVersion) + return + } + + if args[0] == "export" || args[0] == "-e" { + gojs.ExportForDev() + return + } + } + + cmd := "" + if len(args) > 0 && (args[0] == "start" || args[0] == "stop" || args[0] == "restart" || args[0] == "test") { + cmd = args[0] + args = args[1:] + } + + id := "" + mainFile := "{{.Main}}" + isWatch := false + mainArgs := make([]any, 0) + for i := 0; i < len(args); i++ { + arg := args[i] + if strings.HasPrefix(arg, "-") { + switch arg[1:] { + case "id": + i++ + strings.TrimSpace(args[i]) + case "main": + i++ + strings.TrimSpace(args[i]) + case "w": + isWatch = true + } + } else { + mainArgs = append(mainArgs, arg) + } + } + + if !u.FileExists(mainFile) { + log.DefaultLogger.Error("main file not found", "mainFile", mainFile) + return + } + + if id == "" { + id = fmt.Sprintf("%s_%s_%s", appName, mainFile, appVersion) + } + + id = idFixMatcher.ReplaceAllString(id, "_") + homeDir, _ := os.UserHomeDir() + pidFile := filepath.Join(homeDir, ".pids", id+".pid") + pid := u.Int(u.ReadFileN(pidFile)) + fmt.Println(appName, appVersion, mainFile, id, pidFile, pid) + switch cmd { + case "start": + if pid > 0 && checkProcess(pid) { + log.DefaultLogger.Info("process already started", "pid", pid) + } else { + startProcess(pidFile, mainFile) + } + case "stop": + if pid > 0 { + killProcess(pid, mainFile) + _ = os.Remove(pidFile) + } + case "restart": + if pid > 0 { + killProcess(pid, mainFile) + _ = os.Remove(pidFile) + } + startProcess(pidFile, mainFile) + case "test": + if pid > 0 { + killProcess(pid, mainFile) + _ = os.Remove(pidFile) + } + startProcess(pidFile, mainFile) + default: + if isWatch { + gojs.WatchRun(mainFile, mainArgs...) + } else { + gojs.RunFile(mainFile, mainArgs...) + gojs.WaitAll() + } + } +} + +func startProcess(pidFile string, mainFile string) { + var cmd *exec.Cmd + cmd = exec.Command(os.Args[0], "-main", mainFile) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Start(); err == nil { + u.WriteFile(pidFile, u.String(cmd.Process.Pid)) + log.DefaultLogger.Info("started", "appName", appName, "appVersion", appVersion, "mainFile", mainFile, "pid", cmd.Process.Pid) + } else { + log.DefaultLogger.Error("start failed", "appName", appName, "appVersion", appVersion, "mainFile", mainFile, "err", err) + } +} + +func kill(pid int, sig os.Signal) error { + if proc, err := os.FindProcess(pid); err == nil { + return proc.Signal(sig) + } else { + return err + } +} + +func killProcess(pid int, mainFile string) { + if err := kill(pid, syscall.SIGTERM); err == nil { + log.DefaultLogger.Info("killing", "appName", appName, "appVersion", appVersion, "mainFile", mainFile, "pid", pid) + t1 := time.Now().UnixMilli() + for i := 0; i < 100; i++ { + if !checkProcess(pid) { + log.DefaultLogger.Info("killed", "appName", appName, "appVersion", appVersion, "mainFile", mainFile, "pid", pid, "usedTime", (time.Duration(time.Now().UnixMilli()-t1) * time.Millisecond).String()) + return + } + time.Sleep(100 * time.Millisecond) + } + err := kill(pid, syscall.SIGKILL) + if checkProcess(pid) { + log.DefaultLogger.Error("fource kill failed", "appName", appName, "appVersion", appVersion, "mainFile", mainFile, "pid", pid, "err", err) + } else { + log.DefaultLogger.Info("fource killed", "appName", appName, "appVersion", appVersion, "mainFile", mainFile, "pid", pid) + } + } +} + +func checkProcess(pid int) bool { + // return kill(pid, 0) == nil + _, err := os.FindProcess(pid) + return err == nil +} + +func printUsage() { + fmt.Println(appName, appVersion) + fmt.Println("Usage:") + fmt.Println(" ", appName, "[-main mainFile] ...", "run script") + fmt.Println(" ", appName, "version", "- show version") + fmt.Println(" ", appName, "export", "- export for development") + fmt.Println(" ", appName, "start [-id id] [-main mainFile]", "start server") + fmt.Println(" ", appName, "stop [-id id]", "stop server") + fmt.Println(" ", appName, "restart [-id id] [-main mainFile]", "restart server") + fmt.Println(" ", appName, "help", "- show help") +} diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..bab99ec --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,7 @@ +.* +!.gitignore +go.sum +/dist +/build* +node_modules +package.json \ No newline at end of file diff --git a/tests/apigo.yml b/tests/apigo.yml new file mode 100644 index 0000000..d6f7696 --- /dev/null +++ b/tests/apigo.yml @@ -0,0 +1,25 @@ +name: +version: v0.0.1 +main: main.js +output: dist +target: + darwin: amd64 arm64 + linux: amd64 arm64 + windows: amd64 +module: + apigo.cc/gojs/console: latest + apigo.cc/gojs/util: latest + apigo.cc/gojs/log: latest + apigo.cc/gojs/file: latest + apigo.cc/gojs/http: latest + apigo.cc/gojs/db: latest + apigo.cc/gojs/client: latest + apigo.cc/gojs/service: latest + apigo.cc/ai/llm: latest +modulealias: +extraimport: + apigo.cc/ai/llm/openai: latest + apigo.cc/ai/llm/zhipu: latest + modernc.org/sqlite: latest +cachefile: + - main.js diff --git a/tests/go.mod b/tests/go.mod new file mode 100644 index 0000000..1976c5f --- /dev/null +++ b/tests/go.mod @@ -0,0 +1,70 @@ +module tests + +go 1.18 + +require ( + apigo.cc/ai/llm v0.0.1 + apigo.cc/gojs v0.0.2 + apigo.cc/gojs/client v0.0.0-20241013151255-35c2a0f3cf99 + apigo.cc/gojs/console v0.0.1 + apigo.cc/gojs/db v0.0.1 + apigo.cc/gojs/file v0.0.1 + apigo.cc/gojs/http v0.0.3 + apigo.cc/gojs/log v0.0.1 + apigo.cc/gojs/service v0.0.1 + apigo.cc/gojs/util v0.0.2 + github.com/ssgo/log v1.7.7 + github.com/ssgo/s v1.7.16 + github.com/ssgo/u v1.7.9 + modernc.org/sqlite v1.33.1 +) + +require ( + apigo.cc/apigo/gojs v0.1.1 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/dlclark/regexp2 v1.11.4 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-resty/resty/v2 v2.15.2 // indirect + github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/gomodule/redigo v1.9.2 // indirect + github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/sashabaranov/go-openai v1.32.0 // indirect + github.com/shirou/gopsutil/v3 v3.24.5 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/ssgo/config v1.7.7 // indirect + github.com/ssgo/dao v0.1.5 // indirect + github.com/ssgo/db v1.7.9 // indirect + github.com/ssgo/discover v1.7.9 // indirect + github.com/ssgo/httpclient v1.7.8 // indirect + github.com/ssgo/redis v1.7.7 // indirect + github.com/ssgo/standard v1.7.7 // indirect + github.com/ssgo/tool v0.4.27 // indirect + github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/numcpus v0.8.0 // indirect + github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6 // indirect + github.com/yankeguo/zhipu v0.1.2 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect + modernc.org/libc v1.55.3 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.8.0 // indirect + modernc.org/strutil v1.2.0 // indirect + modernc.org/token v1.1.0 // indirect +) diff --git a/tests/main.go b/tests/main.go new file mode 100644 index 0000000..bc4ad3e --- /dev/null +++ b/tests/main.go @@ -0,0 +1,205 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "syscall" + "time" + + "apigo.cc/gojs" + + _ "apigo.cc/ai/llm" + _ "apigo.cc/gojs/client" + _ "apigo.cc/gojs/console" + _ "apigo.cc/gojs/db" + _ "apigo.cc/gojs/file" + _ "apigo.cc/gojs/http" + _ "apigo.cc/gojs/log" + _ "apigo.cc/gojs/service" + _ "apigo.cc/gojs/util" + + _ "apigo.cc/ai/llm/openai" + _ "apigo.cc/ai/llm/zhipu" + "github.com/ssgo/log" + _ "github.com/ssgo/s" + "github.com/ssgo/u" + _ "modernc.org/sqlite" +) + +var appName = "tests" +var appVersion = "v0.0.1" +var idFixMatcher = regexp.MustCompile(`[^a-zA-Z0-9_]`) + +// TODO embed .CacheFiles + +func init() { + gojs.Alias("ai/llm", "apigo.cc/ai/llm") + gojs.Alias("client", "apigo.cc/gojs/client") + gojs.Alias("console", "apigo.cc/gojs/console") + gojs.Alias("db", "apigo.cc/gojs/db") + gojs.Alias("file", "apigo.cc/gojs/file") + gojs.Alias("http", "apigo.cc/gojs/http") + gojs.Alias("log", "apigo.cc/gojs/log") + gojs.Alias("service", "apigo.cc/gojs/service") + gojs.Alias("util", "apigo.cc/gojs/util") +} + +func main() { + if appName == "" { + appName = filepath.Base(os.Args[0]) + appName = strings.TrimSuffix(appName, ".exe") + } + + if appVersion == "" { + appVersion = "0.0.1" + } + + args := os.Args[1:] + if len(args) > 0 { + if args[0] == "help" || args[0] == "--help" || args[0] == "-h" { + printUsage() + return + } + + if args[0] == "version" || args[0] == "--version" || args[0] == "-v" { + fmt.Println(appName, appVersion) + return + } + + if args[0] == "export" || args[0] == "-e" { + gojs.ExportForDev() + return + } + } + + cmd := "" + if len(args) > 0 && (args[0] == "start" || args[0] == "stop" || args[0] == "restart") { + cmd = args[0] + args = args[1:] + } + + id := "" + if len(args) > 1 && args[0] == "-id" { + id = strings.TrimSpace(args[1]) + args = args[2:] + } + + mainFile := "main.js" + if len(args) > 1 && args[0] == "-main" { + mainFile = strings.TrimSpace(args[1]) + args = args[2:] + } + if !u.FileExists(mainFile) { + log.DefaultLogger.Error("main file not found", "mainFile", mainFile) + return + } + + if id == "" { + id = fmt.Sprintf("%s_%s_%s", appName, mainFile, appVersion) + } + + id = idFixMatcher.ReplaceAllString(id, "_") + homeDir, _ := os.UserHomeDir() + pidFile := filepath.Join(homeDir, ".pids", id+".pid") + pid := u.Int(u.ReadFileN(pidFile)) + fmt.Println(appName, appVersion, mainFile, id, pidFile, pid) + switch cmd { + case "start": + if pid > 0 && checkProcess(pid) { + log.DefaultLogger.Info("process already started", "pid", pid) + } else { + startProcess(pidFile, mainFile) + } + case "stop": + if pid > 0 { + killProcess(pid, mainFile) + _ = os.Remove(pidFile) + } + case "restart": + if pid > 0 { + killProcess(pid, mainFile) + _ = os.Remove(pidFile) + } + startProcess(pidFile, mainFile) + default: + gojs.RunFile(mainFile, u.ToInterfaceArray(args)...) + gojs.WaitAll() + } +} + +func startProcess(pidFile string, mainFile string) { + var cmd *exec.Cmd + cmd = exec.Command(os.Args[0], "-main", mainFile) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Start(); err == nil { + u.WriteFile(pidFile, u.String(cmd.Process.Pid)) + log.DefaultLogger.Info("started", "appName", appName, "appVersion", appVersion, "mainFile", mainFile, "pid", cmd.Process.Pid) + } else { + log.DefaultLogger.Error("start failed", "appName", appName, "appVersion", appVersion, "mainFile", mainFile, "err", err) + } +} + +// func killProcess1(pid int) error { +// switch runtime.GOOS { +// case "windows": +// // Windows 系统使用 taskkill 命令 +// cmd := exec.Command("taskkill", "/F", "/PID", fmt.Sprintf("%d", pid)) +// return cmd.Run() +// case "linux", "darwin": +// // Linux 和 macOS 系统使用 syscall.Kill +// return syscall.Kill(pid, syscall.SIGKILL) +// default: +// return fmt.Errorf("unsupported platform") +// } +// } + +func kill(pid int, sig os.Signal) error { + if proc, err := os.FindProcess(pid); err == nil { + return proc.Signal(sig) + } else { + return err + } +} + +func killProcess(pid int, mainFile string) { + if err := kill(pid, syscall.SIGTERM); err == nil { + log.DefaultLogger.Info("killing", "appName", appName, "appVersion", appVersion, "mainFile", mainFile, "pid", pid) + t1 := time.Now().UnixMilli() + for i := 0; i < 100; i++ { + if !checkProcess(pid) { + log.DefaultLogger.Info("killed", "appName", appName, "appVersion", appVersion, "mainFile", mainFile, "pid", pid, "usedTime", (time.Duration(time.Now().UnixMilli()-t1) * time.Millisecond).String()) + return + } + time.Sleep(100 * time.Millisecond) + } + err := kill(pid, syscall.SIGKILL) + if checkProcess(pid) { + log.DefaultLogger.Error("fource kill failed", "appName", appName, "appVersion", appVersion, "mainFile", mainFile, "pid", pid, "err", err) + } else { + log.DefaultLogger.Info("fource killed", "appName", appName, "appVersion", appVersion, "mainFile", mainFile, "pid", pid) + } + } +} + +func checkProcess(pid int) bool { + // return kill(pid, 0) == nil + _, err := os.FindProcess(pid) + return err == nil +} + +func printUsage() { + fmt.Println(appName, appVersion) + fmt.Println("Usage:") + fmt.Println(" ", appName, "[-main mainFile] ...", "run script") + fmt.Println(" ", appName, "version", "- show version") + fmt.Println(" ", appName, "export", "- export for development") + fmt.Println(" ", appName, "start [-id id] [-main mainFile]", "start server") + fmt.Println(" ", appName, "stop [-id id]", "stop server") + fmt.Println(" ", appName, "restart [-id id] [-main mainFile]", "restart server") + fmt.Println(" ", appName, "help", "- show help") +}