Go语言学习#13-借助exec执行外部命令


=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)
}

 

参考链接:

=END=


《 “Go语言学习#13-借助exec执行外部命令” 》 有 2 条评论

  1. 用Go编写的快速开启HTTP文件浏览服务的小工具,能够执行shell命令
    https://github.com/tinyibird/HTTPServerGO
    `
    这是一个用Go编写的红队内网环境中一个能快速开启HTTP文件浏览服务的小工具,能够执行shell命令。它支持以下功能:
    -提供指定目录中的文件
    -能够使用指定的查询参数执行shell命令
    -可自定义外壳路径和查询参数
    -可自定义的IP地址和端口
    -支持PHP、Java和.NET shell(目前仅支持转储PHP shell)
    -在后台运行服务器而不向控制台打印任何内容的选项
    -转储shell并在服务器上执行的选项(目前仅支持转储PHP shell)
    `

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注