=Start=
缘由:
在实际编程过程中,不论是底层的C语言还是上层的Java/Python,在实现某些功能的时候,少不了调用外部命令。所以这里整理总结一下在Go 语言中借助os/exec包执行外部命令的方法,以及一个实现了超时退出功能的命令执行函数。
正文:
参考解答:
Go 语言中使用 exec.Cmd 结构类型来抽象用户要执行的外部命令;该类型下的 Command 方法,接收给定参数并返回 Cmd 结构体,但它只设置返回Cmd结构中的Path和Args。
func Command(name string, arg ...string) *Cmd
type Cmd struct {
Path string
Args []string
Env []string
Dir string
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
// ExtraFiles specifies additional open files to be inherited by the
// new process. It does not include standard input, standard output, or
// standard error. If non-nil, entry i becomes file descriptor 3+i.
ExtraFiles []*os.File
// SysProcAttr holds optional, operating system-specific attributes.
// Run passes it to os.StartProcess as the os.ProcAttr's Sys field.
SysProcAttr *syscall.SysProcAttr
// Process is the underlying process, once started.
Process *os.Process
// ProcessState contains information about an exited process,
// available after a call to Wait or Run.
ProcessState *os.ProcessState
// contains filtered or unexported fields
}
如果名称 name 中不包含路径分隔符,命令将使用LookPath函数将名称解析为完整路径。否则,它直接使用名称作为路径。LookPath函数用于在PATH环境变量指定的目录中搜索一个名为file的可执行二进制文件。如果文件包含斜杠,则直接尝试使用该文件,并且不查看路径。查询结果可能是绝对路径或相对于当前目录的相对路径。
返回的Cmd中的Args字段由 命令名name 跟着 参数arg 一起组成,因此 arg 本身不应该包含命令名。例如,命令(“echo”,”hello”)。Args[0]始终是名称,而不是可能解析的路径。
下面是一个简单的示例(包含 tr 命令和 echo 命令的调用,还有对LookPath函数的测试):
package main
import (
"bytes"
"fmt"
"log"
"os/exec"
"strings"
)
func main() {
cmd := exec.Command("tr", "a-z", "A-Z")
cmd.Stdin = strings.NewReader("some input")
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
log.Fatal(err)
}
fmt.Printf("in all caps: %q\n", out.String())
// log.Fatal() is equivalent to fmt.Print() followed by a call to os.Exit(1).
// cmd = exec.Command("sleep", "5") // need 5 seconds to finish
cmd = exec.Command("echo", "hello") // finish immediately
err = cmd.Start()
if err != nil {
log.Fatal(err)
}
log.Printf("Waiting for command to finish...")
err = cmd.Wait()
log.Printf("Command finished with error: %v", err)
path, err := exec.LookPath("/bin/w")
if err != nil {
log.Println("installing `/bin/w` is in your future")
} else {
fmt.Printf("`/bin/w` is available at %q\n", path)
}
path, err = exec.LookPath("bin/w")
if err != nil {
log.Println("installing `bin/w` is in your future")
} else {
fmt.Printf("`bin/w` is available at %q\n", path)
}
path, err = exec.LookPath("w")
if err != nil {
log.Println("installing w is in your future")
} else {
fmt.Printf("`w` is available at %q\n", path)
}
}
但是在实际环境中,各种各样的情况都有可能出现,比如一个本地以及测试环境中执行只需要1、2秒的命令或脚本到了线上可能10秒钟也执行不完,但是,我们的流程又不能一直卡在这里等待完成,需要有一个超时退出的机制。这时就可以参考下面的示例:
package main
import (
"bytes"
"fmt"
"log"
"os/exec"
"strings"
"syscall"
"time"
)
// https://linkscue.com/2018/06/03/2018-06-03-golang-exec-command-timeout-trace/
// 删除输出内容中的\x00和多余的空格
func trimOutput(buffer bytes.Buffer) string {
return strings.TrimSpace(string(bytes.TrimRight(buffer.Bytes(), "\x00")))
}
// 实时打印输出(利用Ticker每隔一秒从out缓冲区中取出一部分内容进行打印)
func traceOutput(out *bytes.Buffer) {
offset := 0
t := time.NewTicker(time.Second)
defer t.Stop()
for {
<-t.C
result := bytes.TrimRight((*out).Bytes(), "\x00") // 每次读取的是所有的输出内容
size := len(result)
rows := bytes.Split(bytes.TrimSpace(result), []byte{'\n'})
nRows := len(rows)
newRows := rows[offset:nRows]
if result[size-1] != '\n' {
newRows = rows[offset : nRows-1]
}
if len(newRows) < offset { // 判断是否满足打印条件(之前是否已打印过)
continue
}
for _, row := range newRows {
// log.Println(string(row)) // 会在每行日志开头添加日期时间信息
fmt.Println(string(row))
}
offset += len(newRows) // offset记录的是已打印行数的偏移值
}
}
// 运行Shell命令,设定超时时间(秒)
func ExecCmdWithTimeout(timeout int, cmd string, args ...string) (stdout, stderr string, e error) {
if len(cmd) == 0 {
e = fmt.Errorf("cannot run a empty command")
return
}
var out, err bytes.Buffer
command := exec.Command(cmd, args...) // 根据输入的参数生成 Cmd 类型的结构体
command.Stdout = &out
command.Stderr = &err
command.Start() // 开始执行命令
// 下面2行用于启动一个单独的goroutine等待命令执行结束
done := make(chan error) // 创建一个 channel 用于承载命令执行状态的通讯
go func() { done <- command.Wait() }()
// 启动routine持续打印输出(需要时再打开,不需要实时打印的话可以在最后一次性打印出来)
// go traceOutput(&out)
// 设定超时时间,并select它
after := time.After(time.Duration(timeout) * time.Second) // 可以理解为启动一个定时器
select {
case <-after: // 当到了设定的超时时间后,进入这个 case 语句
command.Process.Signal(syscall.SIGINT) // 先发送SIGINT再去kill掉命令,以确保命令的输出完整
time.Sleep(time.Second) // 等待1秒后再去kill
command.Process.Kill()
// if err2 := command.Process.Kill(); err2 != nil { // "os: process already finished"
// log.Printf("failed to kill: %s, error: %q\n", command.Path, err2)
// }
go func() {
<-done // 当主动kill进程时,其实就不太关心done中的结果了,所以主动消费,且用go以避免这里卡住
}()
log.Printf("运行命令(%s)超时,超时设定:%v 秒。",
fmt.Sprintf(`%s %s`, cmd, strings.Join(args, " ")), timeout)
case <-done: // 从 channel 中取出命令的执行状态并存入变量 e 中
}
stdout = trimOutput(out)
stderr = trimOutput(err)
return
}
func main() {
// _, _, e := ExecCmdWithTimeout(5, "ping", "114.114.114.114")
stdout, stderr, e := ExecCmdWithTimeout(5, "ping", "114.114.114.114")
log.Println("stdout:\n", stdout)
log.Println("stderr:\n", stderr)
log.Println("error:\n", e)
}
参考链接:
- GO语言执行命令超时的设置#nice
- 使用Golang为脚本执行设置超时时间
- Go: Timeout Commands with os/exec CommandContext
https://www.cnblogs.com/shhnwangjian/p/9039742.html - Go 语言中如何终止一个用os/exec启动的进程
=END=
《 “Go语言学习#13-借助exec执行外部命令” 》 有 2 条评论
[译]使用os/exec执行命令
https://colobu.com/2017/06/19/advanced-command-execution-in-Go-with-os-exec/
Executing Commands In Go
http://www.darrencoxall.com/golang/executing-commands-in-go/
用Go编写的快速开启HTTP文件浏览服务的小工具,能够执行shell命令
https://github.com/tinyibird/HTTPServerGO
`
这是一个用Go编写的红队内网环境中一个能快速开启HTTP文件浏览服务的小工具,能够执行shell命令。它支持以下功能:
-提供指定目录中的文件
-能够使用指定的查询参数执行shell命令
-可自定义外壳路径和查询参数
-可自定义的IP地址和端口
-支持PHP、Java和.NET shell(目前仅支持转储PHP shell)
-在后台运行服务器而不向控制台打印任何内容的选项
-转储shell并在服务器上执行的选项(目前仅支持转储PHP shell)
`