diff --git a/cmd/gf/internal/cmd/cmd_build.go b/cmd/gf/internal/cmd/cmd_build.go index df85957eb..0b72b496b 100644 --- a/cmd/gf/internal/cmd/cmd_build.go +++ b/cmd/gf/internal/cmd/cmd_build.go @@ -206,11 +206,14 @@ func (c cBuild) Index(ctx context.Context, in cBuildInput) (out *cBuildOutput, e } packCmd := fmt.Sprintf(`gf pack %s %s`, in.PackSrc, in.PackDst) mlog.Print(packCmd) - gproc.MustShellRun(packCmd) + gproc.MustShellRun(ctx, packCmd) } // Injected information by building flags. - ldFlags := fmt.Sprintf(`-X 'github.com/gogf/gf/v2/os/gbuild.builtInVarStr=%v'`, c.getBuildInVarStr(in)) + ldFlags := fmt.Sprintf( + `-X 'github.com/gogf/gf/v2/os/gbuild.builtInVarStr=%v'`, + c.getBuildInVarStr(ctx, in), + ) // start building mlog.Print("start building...") @@ -261,7 +264,7 @@ func (c cBuild) Index(ctx context.Context, in cBuildInput) (out *cBuildOutput, e // It's not necessary printing the complete command string. cmdShow, _ := gregex.ReplaceString(`\s+(-ldflags ".+?")\s+`, " ", cmd) mlog.Print(cmdShow) - if result, err := gproc.ShellExec(cmd); err != nil { + if result, err := gproc.ShellExec(ctx, cmd); err != nil { mlog.Printf( "failed to build, os:%s, arch:%s, error:\n%s\n\n%s\n", system, arch, gstr.Trim(result), @@ -287,12 +290,12 @@ buildDone: // getBuildInVarMapJson retrieves and returns the custom build-in variables in configuration // file as json. -func (c cBuild) getBuildInVarStr(in cBuildInput) string { +func (c cBuild) getBuildInVarStr(ctx context.Context, in cBuildInput) string { buildInVarMap := in.VarMap if buildInVarMap == nil { buildInVarMap = make(g.Map) } - buildInVarMap["builtGit"] = c.getGitCommit() + buildInVarMap["builtGit"] = c.getGitCommit(ctx) buildInVarMap["builtTime"] = gtime.Now().String() b, err := json.Marshal(buildInVarMap) if err != nil { @@ -302,13 +305,13 @@ func (c cBuild) getBuildInVarStr(in cBuildInput) string { } // getGitCommit retrieves and returns the latest git commit hash string if present. -func (c cBuild) getGitCommit() string { +func (c cBuild) getGitCommit(ctx context.Context) string { if gproc.SearchBinary("git") == "" { return "" } var ( cmd = `git log -1 --format="%cd %H" --date=format:"%Y-%m-%d %H:%M:%S"` - s, _ = gproc.ShellExec(cmd) + s, _ = gproc.ShellExec(ctx, cmd) ) mlog.Debug(cmd) if s != "" { diff --git a/cmd/gf/internal/cmd/cmd_docker.go b/cmd/gf/internal/cmd/cmd_docker.go index b111ecbc5..12b1be1c6 100644 --- a/cmd/gf/internal/cmd/cmd_docker.go +++ b/cmd/gf/internal/cmd/cmd_docker.go @@ -88,14 +88,14 @@ func (c cDocker) Index(ctx context.Context, in cDockerInput) (out *cDockerOutput // Binary build. in.Build += " --exit" if in.Main != "" { - if err = gproc.ShellRun(fmt.Sprintf(`gf build %s %s`, in.Main, in.Build)); err != nil { + if err = gproc.ShellRun(ctx, fmt.Sprintf(`gf build %s %s`, in.Main, in.Build)); err != nil { return } } // Shell executing. if in.Shell != "" && gfile.Exists(in.Shell) { - if err = c.exeDockerShell(in.Shell); err != nil { + if err = c.exeDockerShell(ctx, in.Shell); err != nil { return } } @@ -116,7 +116,7 @@ func (c cDocker) Index(ctx context.Context, in cDockerInput) (out *cDockerOutput } for i, dockerTag := range dockerTags { if i > 0 { - err = gproc.ShellRun(fmt.Sprintf(`docker tag %s %s`, dockerTagBase, dockerTag)) + err = gproc.ShellRun(ctx, fmt.Sprintf(`docker tag %s %s`, dockerTagBase, dockerTag)) if err != nil { return } @@ -130,7 +130,7 @@ func (c cDocker) Index(ctx context.Context, in cDockerInput) (out *cDockerOutput if in.Extra != "" { dockerBuildOptions = fmt.Sprintf(`%s %s`, dockerBuildOptions, in.Extra) } - err = gproc.ShellRun(fmt.Sprintf(`docker build -f %s . %s`, in.File, dockerBuildOptions)) + err = gproc.ShellRun(ctx, fmt.Sprintf(`docker build -f %s . %s`, in.File, dockerBuildOptions)) if err != nil { return } @@ -144,7 +144,7 @@ func (c cDocker) Index(ctx context.Context, in cDockerInput) (out *cDockerOutput if dockerTag == "" { continue } - err = gproc.ShellRun(fmt.Sprintf(`docker push %s`, dockerTag)) + err = gproc.ShellRun(ctx, fmt.Sprintf(`docker push %s`, dockerTag)) if err != nil { return } @@ -152,10 +152,10 @@ func (c cDocker) Index(ctx context.Context, in cDockerInput) (out *cDockerOutput return } -func (c cDocker) exeDockerShell(shellFilePath string) error { +func (c cDocker) exeDockerShell(ctx context.Context, shellFilePath string) error { if gfile.ExtName(shellFilePath) == "sh" && runtime.GOOS == "windows" { mlog.Debugf(`ignore shell file "%s", as it cannot be run on windows system`, shellFilePath) return nil } - return gproc.ShellRun(gfile.GetContents(shellFilePath)) + return gproc.ShellRun(ctx, gfile.GetContents(shellFilePath)) } diff --git a/cmd/gf/internal/cmd/cmd_env.go b/cmd/gf/internal/cmd/cmd_env.go index b6538414b..a38819501 100644 --- a/cmd/gf/internal/cmd/cmd_env.go +++ b/cmd/gf/internal/cmd/cmd_env.go @@ -26,7 +26,7 @@ type cEnvInput struct { type cEnvOutput struct{} func (c cEnv) Index(ctx context.Context, in cEnvInput) (out *cEnvOutput, err error) { - result, err := gproc.ShellExec("go env") + result, err := gproc.ShellExec(ctx, "go env") if err != nil { mlog.Fatal(err) } diff --git a/cmd/gf/internal/cmd/cmd_gen_pb.go b/cmd/gf/internal/cmd/cmd_gen_pb.go index 35babce43..0643cb3d0 100644 --- a/cmd/gf/internal/cmd/cmd_gen_pb.go +++ b/cmd/gf/internal/cmd/cmd_gen_pb.go @@ -57,7 +57,7 @@ func (c cGenPb) Pb(ctx context.Context, in cGenPbInput) (out *cGenPbOutput, err parsingCommand += " -I" + goPathSrc } mlog.Print(parsingCommand) - if output, err := gproc.ShellExec(parsingCommand); err != nil { + if output, err := gproc.ShellExec(ctx, parsingCommand); err != nil { mlog.Print(output) mlog.Fatal(err) } diff --git a/cmd/gf/internal/cmd/cmd_gen_service.go b/cmd/gf/internal/cmd/cmd_gen_service.go index 82213da69..ab995b71f 100644 --- a/cmd/gf/internal/cmd/cmd_gen_service.go +++ b/cmd/gf/internal/cmd/cmd_gen_service.go @@ -80,7 +80,7 @@ func (c cGenService) Service(ctx context.Context, in cGenServiceInput) (out *cGe `%s gen service -packages=%s`, gfile.SelfName(), gfile.Basename(watchFileDir), ) - err = gproc.ShellRun(command) + err = gproc.ShellRun(ctx, command) return } diff --git a/cmd/gf/internal/cmd/cmd_init.go b/cmd/gf/internal/cmd/cmd_init.go index 269bf6853..ce8d024e3 100644 --- a/cmd/gf/internal/cmd/cmd_init.go +++ b/cmd/gf/internal/cmd/cmd_init.go @@ -98,7 +98,7 @@ func (c cInit) Index(ctx context.Context, in cInitInput) (out *cInitOutput, err if in.Name != "." { updateCommand = fmt.Sprintf(`cd %s && %s`, in.Name, updateCommand) } - if err = gproc.ShellRun(updateCommand); err != nil { + if err = gproc.ShellRun(ctx, updateCommand); err != nil { mlog.Fatal(err) } } diff --git a/cmd/gf/internal/cmd/cmd_run.go b/cmd/gf/internal/cmd/cmd_run.go index 1787422df..2187c13b9 100644 --- a/cmd/gf/internal/cmd/cmd_run.go +++ b/cmd/gf/internal/cmd/cmd_run.go @@ -99,17 +99,17 @@ func (c cRun) Index(ctx context.Context, in cRunInput) (out *cRunOutput, err err gtimer.SetTimeout(ctx, 1500*gtime.MS, func(ctx context.Context) { defer dirty.Set(false) mlog.Printf(`go file changes: %s`, event.String()) - app.Run() + app.Run(ctx) }) }) if err != nil { mlog.Fatal(err) } - go app.Run() + go app.Run(ctx) select {} } -func (app *cRunApp) Run() { +func (app *cRunApp) Run(ctx context.Context) { // Rebuild and run the codes. renamePath := "" mlog.Printf("build: %s", app.File) @@ -132,7 +132,7 @@ func (app *cRunApp) Run() { app.File, ) mlog.Print(buildCommand) - result, err := gproc.ShellExec(buildCommand) + result, err := gproc.ShellExec(ctx, buildCommand) if err != nil { mlog.Printf("build error: \n%s%s", result, err.Error()) return @@ -154,7 +154,7 @@ func (app *cRunApp) Run() { } else { process = gproc.NewProcessCmd(outputPath, gstr.SplitAndTrim(" ", app.Args)) } - if pid, err := process.Start(); err != nil { + if pid, err := process.Start(ctx); err != nil { mlog.Printf("build running error: %s", err.Error()) } else { mlog.Printf("build running pid: %d", pid) diff --git a/cmd/gf/internal/utility/utils/utils.go b/cmd/gf/internal/utility/utils/utils.go index 5a1655c2e..2a3ac2da9 100644 --- a/cmd/gf/internal/utility/utils/utils.go +++ b/cmd/gf/internal/utility/utils/utils.go @@ -1,6 +1,7 @@ package utils import ( + "context" "fmt" "github.com/gogf/gf/cmd/gf/v2/internal/consts" @@ -31,7 +32,7 @@ func GoFmt(path string) { mlog.Fatal(`command "gofmt" not found`) } var command = fmt.Sprintf(`%s -w %s`, gofmtPath, path) - result, err := gproc.ShellExec(command) + result, err := gproc.ShellExec(context.Background(), command) if err != nil { mlog.Fatalf(`error executing command "%s": %s`, command, result) } @@ -43,7 +44,7 @@ func GoImports(path string) { mlog.Fatal(`command "goimports" not found`) } var command = fmt.Sprintf(`%s -w %s`, goimportsPath, path) - result, err := gproc.ShellExec(command) + result, err := gproc.ShellExec(context.Background(), command) if err != nil { mlog.Fatalf(`error executing command "%s": %s`, command, result) } diff --git a/example/go.mod b/example/go.mod index 86bae38a7..5c05a6661 100644 --- a/example/go.mod +++ b/example/go.mod @@ -7,8 +7,8 @@ require ( github.com/gogf/gf/contrib/registry/etcd/v2 v2.1.0-rc3.0.20220523034830-510fa3faf03f github.com/gogf/gf/contrib/registry/polaris/v2 v2.0.0-rc2 github.com/gogf/gf/contrib/trace/jaeger/v2 v2.0.0-rc2 - github.com/gogf/gf/v2 v2.1.0-rc3.0.20220523034830-510fa3faf03f - github.com/gogf/katyusha v0.4.0 + github.com/gogf/gf/v2 v2.1.0-rc4.0.20220620123459-52056644d499 + github.com/gogf/katyusha v0.4.1-0.20220620125113-f55d6f739773 github.com/gogo/protobuf v1.3.2 github.com/golang/protobuf v1.5.2 github.com/polarismesh/polaris-go v1.2.0-beta.0.0.20220517041223-596a6a63b00f diff --git a/example/go.sum b/example/go.sum index 476035be6..76515c09f 100644 --- a/example/go.sum +++ b/example/go.sum @@ -113,6 +113,8 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg78 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogf/katyusha v0.4.0 h1:mQVfXHhzC+UQf11Q8HAk9IOhQZ1VMXqGUqezyywZUOs= github.com/gogf/katyusha v0.4.0/go.mod h1:nqsIWBsImnq9+OLlfB6iNef6ZLRyR2L1Bnk9h2aZvKs= +github.com/gogf/katyusha v0.4.1-0.20220620125113-f55d6f739773 h1:YQBLawktoymYtPGs9idE9JS5Wqd3SjIzUEZOPKCdSw0= +github.com/gogf/katyusha v0.4.1-0.20220620125113-f55d6f739773/go.mod h1:Z0GCeHXz1UI0HtA0K45c6TzEGM4DL/PLatS747/WarI= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= diff --git a/example/trace/processes/gcmd/main.go b/example/trace/processes/gcmd/main.go new file mode 100644 index 000000000..8015a2e08 --- /dev/null +++ b/example/trace/processes/gcmd/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "context" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gcmd" + "github.com/gogf/gf/v2/os/gctx" + "github.com/gogf/gf/v2/os/gproc" +) + +var ( + Main = &gcmd.Command{ + Name: "main", + Brief: "main process", + Func: func(ctx context.Context, parser *gcmd.Parser) (err error) { + g.Log().Debug(ctx, `this is main process`) + return gproc.ShellRun(ctx, `go run sub/sub.go`) + }, + } +) + +func main() { + Main.Run(gctx.New()) +} diff --git a/example/trace/processes/gcmd/sub/sub.go b/example/trace/processes/gcmd/sub/sub.go new file mode 100644 index 000000000..2e546a8a4 --- /dev/null +++ b/example/trace/processes/gcmd/sub/sub.go @@ -0,0 +1,24 @@ +package main + +import ( + "context" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gcmd" + "github.com/gogf/gf/v2/os/gctx" +) + +var ( + Sub = &gcmd.Command{ + Name: "sub", + Brief: "sub process", + Func: func(ctx context.Context, parser *gcmd.Parser) (err error) { + g.Log().Debug(ctx, `this is sub process`) + return nil + }, + } +) + +func main() { + Sub.Run(gctx.New()) +} diff --git a/example/trace/processes/gproc/main.go b/example/trace/processes/gproc/main.go new file mode 100644 index 000000000..50c701a9d --- /dev/null +++ b/example/trace/processes/gproc/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gctx" + "github.com/gogf/gf/v2/os/gproc" +) + +func main() { + ctx := gctx.New() + g.Log().Debug(ctx, `this is main process`) + if err := gproc.ShellRun(ctx, `go run sub/sub.go`); err != nil { + panic(err) + } +} diff --git a/example/trace/processes/gproc/sub/sub.go b/example/trace/processes/gproc/sub/sub.go new file mode 100644 index 000000000..8a743de16 --- /dev/null +++ b/example/trace/processes/gproc/sub/sub.go @@ -0,0 +1,11 @@ +package main + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gctx" +) + +func main() { + ctx := gctx.New() + g.Log().Debug(ctx, `this is sub process`) +} diff --git a/net/ghttp/ghttp_server_admin_process.go b/net/ghttp/ghttp_server_admin_process.go index fffe50ae8..4ddff1b42 100644 --- a/net/ghttp/ghttp_server_admin_process.go +++ b/net/ghttp/ghttp_server_admin_process.go @@ -144,7 +144,7 @@ func forkReloadProcess(ctx context.Context, newExeFilePath ...string) error { } buffer, _ := gjson.Encode(sfm) p.Env = append(p.Env, adminActionReloadEnvKey+"="+string(buffer)) - if _, err := p.Start(); err != nil { + if _, err := p.Start(ctx); err != nil { glog.Errorf( ctx, "%d: fork process failed, error:%s, %s", @@ -169,7 +169,7 @@ func forkRestartProcess(ctx context.Context, newExeFilePath ...string) error { env := os.Environ() env = append(env, adminActionRestartEnvKey+"=1") p := gproc.NewProcess(path, os.Args, env) - if _, err := p.Start(); err != nil { + if _, err := p.Start(ctx); err != nil { glog.Errorf( ctx, `%d: fork process failed, error:%s, are you running using "go run"?`, diff --git a/os/genv/genv.go b/os/genv/genv.go index f687f3220..895563055 100644 --- a/os/genv/genv.go +++ b/os/genv/genv.go @@ -8,6 +8,7 @@ package genv import ( + "fmt" "os" "strings" @@ -25,13 +26,7 @@ func All() []string { // Map returns a copy of strings representing the environment as a map. func Map() map[string]string { - m := make(map[string]string) - i := 0 - for _, s := range os.Environ() { - i = strings.IndexByte(s, '=') - m[s[0:i]] = s[i+1:] - } - return m + return MapFromEnv(os.Environ()) } // Get creates and returns a Var with the value of the environment variable @@ -117,3 +112,28 @@ func Build(m map[string]string) []string { } return array } + +// MapFromEnv converts environment variables from slice to map. +func MapFromEnv(envs []string) map[string]string { + m := make(map[string]string) + i := 0 + for _, s := range envs { + i = strings.IndexByte(s, '=') + m[s[0:i]] = s[i+1:] + } + return m +} + +// MapToEnv converts environment variables from map to slice. +func MapToEnv(m map[string]string) []string { + envs := make([]string, 0) + for k, v := range m { + envs = append(envs, fmt.Sprintf(`%s=%s`, k, v)) + } + return envs +} + +// Filter filters repeated items from given environment variables. +func Filter(envs []string) []string { + return MapToEnv(MapFromEnv(envs)) +} diff --git a/os/genv/genv_z_unit_test.go b/os/genv/genv_z_unit_test.go index b1b23e64c..0f9bb2e77 100644 --- a/os/genv/genv_z_unit_test.go +++ b/os/genv/genv_z_unit_test.go @@ -121,3 +121,24 @@ func Test_GetWithCmd(t *testing.T) { t.Assert(genv.GetWithCmd("test"), 1) }) } + +func Test_MapFromEnv(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + m := genv.MapFromEnv([]string{"a=1", "b=2"}) + t.Assert(m, g.Map{"a": 1, "b": 2}) + }) +} + +func Test_MapToEnv(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + s := genv.MapToEnv(g.MapStrStr{"a": "1"}) + t.Assert(s, []string{"a=1"}) + }) +} + +func Test_Filter(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + s := genv.Filter([]string{"a=1", "a=3"}) + t.Assert(s, []string{"a=3"}) + }) +} diff --git a/os/gproc/gproc.go b/os/gproc/gproc.go index 3c9607383..d088b2b4e 100644 --- a/os/gproc/gproc.go +++ b/os/gproc/gproc.go @@ -8,8 +8,6 @@ package gproc import ( - "bytes" - "io" "os" "runtime" "time" @@ -21,7 +19,8 @@ import ( ) const ( - envKeyPPid = "GPROC_PPID" + envKeyPPid = "GPROC_PPID" + tracingInstrumentName = "github.com/gogf/gf/v2/os/gproc.Process" ) var ( @@ -80,120 +79,6 @@ func Uptime() time.Duration { return time.Now().Sub(processStartTime) } -// Shell executes command `cmd` synchronously with given input pipe `in` and output pipe `out`. -// The command `cmd` reads the input parameters from input pipe `in`, and writes its output automatically -// to output pipe `out`. -func Shell(cmd string, out io.Writer, in io.Reader) error { - p := NewProcess( - getShell(), - append([]string{getShellOption()}, parseCommand(cmd)...), - ) - p.Stdin = in - p.Stdout = out - return p.Run() -} - -// ShellRun executes given command `cmd` synchronously and outputs the command result to the stdout. -func ShellRun(cmd string) error { - p := NewProcess( - getShell(), - append([]string{getShellOption()}, parseCommand(cmd)...), - ) - return p.Run() -} - -// ShellExec executes given command `cmd` synchronously and returns the command result. -func ShellExec(cmd string, environment ...[]string) (result string, err error) { - var ( - buf = bytes.NewBuffer(nil) - p = NewProcess( - getShell(), - append([]string{getShellOption()}, parseCommand(cmd)...), - environment..., - ) - ) - p.Stdout = buf - p.Stderr = buf - err = p.Run() - result = buf.String() - return -} - -// parseCommand parses command `cmd` into slice arguments. -// -// Note that it just parses the `cmd` for "cmd.exe" binary in windows, but it is not necessary -// parsing the `cmd` for other systems using "bash"/"sh" binary. -func parseCommand(cmd string) (args []string) { - if runtime.GOOS != "windows" { - return []string{cmd} - } - // Just for "cmd.exe" in windows. - var argStr string - var firstChar, prevChar, lastChar1, lastChar2 byte - array := gstr.SplitAndTrim(cmd, " ") - for _, v := range array { - if len(argStr) > 0 { - argStr += " " - } - firstChar = v[0] - lastChar1 = v[len(v)-1] - lastChar2 = 0 - if len(v) > 1 { - lastChar2 = v[len(v)-2] - } - if prevChar == 0 && (firstChar == '"' || firstChar == '\'') { - // It should remove the first quote char. - argStr += v[1:] - prevChar = firstChar - } else if prevChar != 0 && lastChar2 != '\\' && lastChar1 == prevChar { - // It should remove the last quote char. - argStr += v[:len(v)-1] - args = append(args, argStr) - argStr = "" - prevChar = 0 - } else if len(argStr) > 0 { - argStr += v - } else { - args = append(args, v) - } - } - return -} - -// getShell returns the shell command depending on current working operating system. -// It returns "cmd.exe" for windows, and "bash" or "sh" for others. -func getShell() string { - switch runtime.GOOS { - case "windows": - return SearchBinary("cmd.exe") - default: - // Check the default binary storage path. - if gfile.Exists("/bin/bash") { - return "/bin/bash" - } - if gfile.Exists("/bin/sh") { - return "/bin/sh" - } - // Else search the env PATH. - path := SearchBinary("bash") - if path == "" { - path = SearchBinary("sh") - } - return path - } -} - -// getShellOption returns the shell option depending on current working operating system. -// It returns "/c" for windows, and "-c" for others. -func getShellOption() string { - switch runtime.GOOS { - case "windows": - return "/c" - default: - return "-c" - } -} - // SearchBinary searches the binary `file` in current working folder and PATH environment. func SearchBinary(file string) string { // Check if it is absolute path of exists at current working directory. diff --git a/os/gproc/gproc_must.go b/os/gproc/gproc_must.go index 5e7584f01..1aa3c44ee 100644 --- a/os/gproc/gproc_must.go +++ b/os/gproc/gproc_must.go @@ -7,26 +7,27 @@ package gproc import ( + "context" "io" ) // MustShell performs as Shell, but it panics if any error occurs. -func MustShell(cmd string, out io.Writer, in io.Reader) { - if err := Shell(cmd, out, in); err != nil { +func MustShell(ctx context.Context, cmd string, out io.Writer, in io.Reader) { + if err := Shell(ctx, cmd, out, in); err != nil { panic(err) } } // MustShellRun performs as ShellRun, but it panics if any error occurs. -func MustShellRun(cmd string) { - if err := ShellRun(cmd); err != nil { +func MustShellRun(ctx context.Context, cmd string) { + if err := ShellRun(ctx, cmd); err != nil { panic(err) } } // MustShellExec performs as ShellExec, but it panics if any error occurs. -func MustShellExec(cmd string, environment ...[]string) string { - result, err := ShellExec(cmd, environment...) +func MustShellExec(ctx context.Context, cmd string, environment ...[]string) string { + result, err := ShellExec(ctx, cmd, environment...) if err != nil { panic(err) } diff --git a/os/gproc/gproc_process.go b/os/gproc/gproc_process.go index b1a589428..4b6e7470a 100644 --- a/os/gproc/gproc_process.go +++ b/os/gproc/gproc_process.go @@ -14,9 +14,16 @@ import ( "runtime" "strings" + "github.com/gogf/gf/v2" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/internal/intlog" + "github.com/gogf/gf/v2/net/gtrace" + "github.com/gogf/gf/v2/os/genv" + "github.com/gogf/gf/v2/text/gstr" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" ) // Process is the struct for a single process. @@ -64,11 +71,37 @@ func NewProcessCmd(cmd string, environment ...[]string) *Process { // Start starts executing the process in non-blocking way. // It returns the pid if success, or else it returns an error. -func (p *Process) Start() (int, error) { +func (p *Process) Start(ctx context.Context) (int, error) { if p.Process != nil { return p.Pid(), nil } + // OpenTelemetry for command. + var ( + span trace.Span + tr = otel.GetTracerProvider().Tracer( + tracingInstrumentName, + trace.WithInstrumentationVersion(gf.VERSION), + ) + ) + ctx, span = tr.Start( + otel.GetTextMapPropagator().Extract( + ctx, + propagation.MapCarrier(genv.Map()), + ), + gstr.Join(os.Args, " "), + trace.WithSpanKind(trace.SpanKindInternal), + ) + defer span.End() + span.SetAttributes(gtrace.CommonLabels()...) + + // OpenTelemetry propagation. + tracingEnv := tracingEnvFromCtx(ctx) + if len(tracingEnv) > 0 { + p.Env = append(p.Env, tracingEnv...) + } p.Env = append(p.Env, fmt.Sprintf("%s=%d", envKeyPPid, p.PPid)) + p.Env = genv.Filter(p.Env) + if err := p.Cmd.Start(); err == nil { if p.Manager != nil { p.Manager.processes.Set(p.Process.Pid, p) @@ -80,8 +113,8 @@ func (p *Process) Start() (int, error) { } // Run executes the process in blocking way. -func (p *Process) Run() error { - if _, err := p.Start(); err == nil { +func (p *Process) Run(ctx context.Context) error { + if _, err := p.Start(ctx); err == nil { return p.Wait() } else { return err diff --git a/os/gproc/gproc_shell.go b/os/gproc/gproc_shell.go new file mode 100644 index 000000000..6a894aa6d --- /dev/null +++ b/os/gproc/gproc_shell.go @@ -0,0 +1,149 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package gproc + +import ( + "bytes" + "context" + "fmt" + "io" + "runtime" + + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/text/gstr" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" +) + +// Shell executes command `cmd` synchronously with given input pipe `in` and output pipe `out`. +// The command `cmd` reads the input parameters from input pipe `in`, and writes its output automatically +// to output pipe `out`. +func Shell(ctx context.Context, cmd string, out io.Writer, in io.Reader) error { + p := NewProcess( + getShell(), + append([]string{getShellOption()}, parseCommand(cmd)...), + ) + p.Stdin = in + p.Stdout = out + return p.Run(ctx) +} + +// ShellRun executes given command `cmd` synchronously and outputs the command result to the stdout. +func ShellRun(ctx context.Context, cmd string) error { + p := NewProcess( + getShell(), + append([]string{getShellOption()}, parseCommand(cmd)...), + ) + return p.Run(ctx) +} + +// ShellExec executes given command `cmd` synchronously and returns the command result. +func ShellExec(ctx context.Context, cmd string, environment ...[]string) (result string, err error) { + var ( + buf = bytes.NewBuffer(nil) + p = NewProcess( + getShell(), + append([]string{getShellOption()}, parseCommand(cmd)...), + environment..., + ) + ) + p.Stdout = buf + p.Stderr = buf + err = p.Run(ctx) + result = buf.String() + return +} + +// parseCommand parses command `cmd` into slice arguments. +// +// Note that it just parses the `cmd` for "cmd.exe" binary in windows, but it is not necessary +// parsing the `cmd` for other systems using "bash"/"sh" binary. +func parseCommand(cmd string) (args []string) { + if runtime.GOOS != "windows" { + return []string{cmd} + } + // Just for "cmd.exe" in windows. + var argStr string + var firstChar, prevChar, lastChar1, lastChar2 byte + array := gstr.SplitAndTrim(cmd, " ") + for _, v := range array { + if len(argStr) > 0 { + argStr += " " + } + firstChar = v[0] + lastChar1 = v[len(v)-1] + lastChar2 = 0 + if len(v) > 1 { + lastChar2 = v[len(v)-2] + } + if prevChar == 0 && (firstChar == '"' || firstChar == '\'') { + // It should remove the first quote char. + argStr += v[1:] + prevChar = firstChar + } else if prevChar != 0 && lastChar2 != '\\' && lastChar1 == prevChar { + // It should remove the last quote char. + argStr += v[:len(v)-1] + args = append(args, argStr) + argStr = "" + prevChar = 0 + } else if len(argStr) > 0 { + argStr += v + } else { + args = append(args, v) + } + } + return +} + +// getShell returns the shell command depending on current working operating system. +// It returns "cmd.exe" for windows, and "bash" or "sh" for others. +func getShell() string { + switch runtime.GOOS { + case "windows": + return SearchBinary("cmd.exe") + + default: + // Check the default binary storage path. + if gfile.Exists("/bin/bash") { + return "/bin/bash" + } + if gfile.Exists("/bin/sh") { + return "/bin/sh" + } + // Else search the env PATH. + path := SearchBinary("bash") + if path == "" { + path = SearchBinary("sh") + } + return path + } +} + +// getShellOption returns the shell option depending on current working operating system. +// It returns "/c" for windows, and "-c" for others. +func getShellOption() string { + switch runtime.GOOS { + case "windows": + return "/c" + + default: + return "-c" + } +} + +// tracingEnvFromCtx converts OpenTelemetry propagation data as environment variables. +func tracingEnvFromCtx(ctx context.Context) []string { + var ( + a = make([]string, 0) + m = make(map[string]string) + ) + otel.GetTextMapPropagator().Inject(ctx, propagation.MapCarrier(m)) + for k, v := range m { + a = append(a, fmt.Sprintf(`%s=%s`, k, v)) + } + return a +} diff --git a/os/gproc/gproc_z_unit_test.go b/os/gproc/gproc_z_unit_test.go index a02087d3b..b4e21bbfc 100644 --- a/os/gproc/gproc_z_unit_test.go +++ b/os/gproc/gproc_z_unit_test.go @@ -11,19 +11,20 @@ package gproc_test import ( "testing" + "github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gproc" "github.com/gogf/gf/v2/test/gtest" ) func Test_ShellExec(t *testing.T) { gtest.C(t, func(t *gtest.T) { - s, err := gproc.ShellExec(`echo 123`) + s, err := gproc.ShellExec(gctx.New(), `echo 123`) t.AssertNil(err) t.Assert(s, "123\n") }) // error gtest.C(t, func(t *gtest.T) { - _, err := gproc.ShellExec(`NoneExistCommandCall`) + _, err := gproc.ShellExec(gctx.New(), `NoneExistCommandCall`) t.AssertNE(err, nil) }) }