Skip to content

Commit

Permalink
feat: 实现exec执行命令功能
Browse files Browse the repository at this point in the history
  • Loading branch information
googs1025 committed Jul 30, 2023
1 parent 465cca1 commit cea6b77
Show file tree
Hide file tree
Showing 9 changed files with 438 additions and 0 deletions.
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: CI

on:
# Triggers the workflow on push or pull request events but only for the "main" branch
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

env:
GO_VERSION: '1.18.5'

jobs:

golang-lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: true

- name: Set up Golang
uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}

- name: Run unit test
run: go test -v ./...
27 changes: 27 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@


# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
bin
testbin/*

# Test binary, build with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Kubernetes Generated files - skip generated files, except for vendored files

!vendor/**/zz_generated.*

# editor and IDE paraphernalia
.idea
*.swp
*.swo
*~

44 changes: 44 additions & 0 deletions example/example.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package main

import (
"fmt"
"github.com/practice/shell_extender/pkg/command"
)

func main() {
fmt.Println("==============ExecShellCommand=================")
out, i, err := command.ExecShellCommand("kubectl get node")
if err != nil {
fmt.Println(err)
return
}

fmt.Printf("i: %v, out: %s", i, out)
fmt.Println("===============================")

fmt.Println("==============ExecShellCommandWithResult=================")
stdout, stderr, code, err := command.ExecShellCommandWithResult("kubectl get node")
fmt.Printf("stdout: %v, stderr: %s, code: %v, err: %v\n", stdout, stderr, code, err)
fmt.Println("===============================")

fmt.Println("==============ExecShellCommandWithTimeout=================")
stdout, stderr, code, err = command.ExecShellCommandWithTimeout("sleep 10; kubectl get node", 3)
fmt.Printf("stdout: %v, stderr: %s, code: %v, err: %v\n", stdout, stderr, code, err)
fmt.Println("===============================")

fmt.Println("==============ExecShellCommandWithChan=================")
outputC := make(chan string, 10)

go func() {
for i := range outputC {
fmt.Println("output line: ", i)
}
}()

err = command.ExecShellCommandWithChan("kubectl get node", outputC)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("===============================")
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/practice/shell_extender

go 1.18
116 changes: 116 additions & 0 deletions pkg/command/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package command

import (
"bufio"
"bytes"
"errors"
"io"
"os/exec"
"time"
)

var (
ErrTimeout = errors.New("exec command timeout")
)

// ExecShellCommand 执行命令
// 输出:1.命令行结果 2.进程输出code 3.错误
func ExecShellCommand(cmd string) (string, int, error) {
executor := exec.Command("bash", "-c", cmd)
outByte, err := executor.CombinedOutput()
out := string(outByte)
return out, executor.ProcessState.ExitCode(), err
}

// ExecShellCommandWithResult 执行命令并输出所有结果
// 输出:1.命令行结果 2.命令行错误 3.进程输出code 4.错误
func ExecShellCommandWithResult(cmd string) (string, string, int, error) {

executor := exec.Command("bash", "-c", cmd)
var (
stdout, stderr bytes.Buffer
err error
)
executor.Stdout = &stdout
executor.Stderr = &stderr
err = executor.Start()
if err != nil {
return string(stdout.Bytes()), string(stderr.Bytes()), executor.ProcessState.ExitCode(), err
}

err = executor.Wait()
return string(stdout.Bytes()), string(stderr.Bytes()), executor.ProcessState.ExitCode(), err
}

// ExecShellCommandWithTimeout 执行命令并超时时间
// 输出:1.命令行结果 2.命令行错误 3.进程输出code 4.错误
func ExecShellCommandWithTimeout(cmd string, timeout int64) (string, string, int, error) {
executor := exec.Command("bash", "-c", cmd)
// executor.Run() 会阻塞,因此开一个goroutine异步执行,
// 当执行结束时,使用chan通知
notifyC := make(chan struct{})
var err error
execFunc := func() {
err = executor.Run()
close(notifyC)
}
go execFunc()

var (
stdout, stderr bytes.Buffer
)
executor.Stdout = &stdout
executor.Stderr = &stderr

if err != nil {
return string(stdout.Bytes()), string(stderr.Bytes()), executor.ProcessState.ExitCode(), err
}

// 超时执行返回
t := time.Duration(timeout) * time.Second
select {
case <-notifyC:
return string(stdout.Bytes()), string(stderr.Bytes()), executor.ProcessState.ExitCode(), err
case <-time.After(t):
return string(stdout.Bytes()), string(stderr.Bytes()), executor.ProcessState.ExitCode(), ErrTimeout
}
}


// ExecShellCommandWithChan 执行命令并使用管道输出
// 输入:chan 输出:错误
func ExecShellCommandWithChan(cmd string, queue chan string) error {
executor := exec.Command("bash", "-c", cmd)
stdout, err := executor.StdoutPipe()
if err != nil {
return err
}

stderr, err := executor.StderrPipe()
if err != nil {
return err
}

executor.Start()

callbackFunc := func(in io.ReadCloser) {
reader := bufio.NewReader(in)
for {
line, _, err := reader.ReadLine()
if err != nil || io.EOF == err {
break
}

select {
case queue <- string(line):
}
}
}

go callbackFunc(stdout)
go callbackFunc(stderr)

executor.Wait()
close(queue)
return nil
}
43 changes: 43 additions & 0 deletions pkg/command/command_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package command

import (
"fmt"
"testing"
)

func TestExecShellCommand(t *testing.T) {
out, i, err := ExecShellCommand("echo TestExecShellCommand")
if err != nil {
fmt.Println(err)
return
}

fmt.Printf("i: %v, out: %s", i, out)

}

func TestExecShellCommandWithResult(t *testing.T) {
stdout, stderr, code, err := ExecShellCommandWithResult("echo TestExecShellCommandWithResult")
fmt.Printf("stdout: %v, stderr: %s, code: %v, err: %v\n", stdout, stderr, code, err)
}

func TestExecShellCommandWithTimeout(t *testing.T) {
stdout, stderr, code, err := ExecShellCommandWithTimeout("sleep 15; echo TestExecShellCommandWithTimeout; kubectl get pods", 20)
fmt.Printf("stdout: %v, stderr: %s, code: %v, err: %v\n", stdout, stderr, code, err)
}

func TestExecShellCommandWithChan(t *testing.T) {
outputC := make(chan string, 10)

go func() {
for i := range outputC {
fmt.Println("output line: ", i)
}
}()

err := ExecShellCommandWithChan("echo TestExecShellCommandWithChan ;sleep 1;kubectl get node", outputC)
if err != nil {
fmt.Println(err)
return
}
}
40 changes: 40 additions & 0 deletions pkg/output/out_byte.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package output

import (
"bufio"
"bytes"
"sync"
)

type OutputBuffer struct {
buf *bytes.Buffer
lines []string
*sync.Mutex
}

func NewOutputBuffer() *OutputBuffer {
out := &OutputBuffer{
buf: &bytes.Buffer{},
lines: []string{},
Mutex: &sync.Mutex{},
}
return out
}

func (rw *OutputBuffer) Write(p []byte) (n int, err error) {
rw.Lock()
n, err = rw.buf.Write(p) // and bytes.Buffer implements io.Writer
rw.Unlock()
return
}

func (rw *OutputBuffer) Lines() []string {
rw.Lock()
s := bufio.NewScanner(rw.buf)
for s.Scan() {
rw.lines = append(rw.lines, s.Text())
}
rw.Unlock()
return rw.lines
}

Loading

0 comments on commit cea6b77

Please sign in to comment.