OrzLee

这个世界上只有一个问题
那就是时间问题

Golang oh-my-zsh 自定义插件批量更新程序

goland.png

前言

Golang很火啊,引用维基百科词条说明 Go(又称Golang)是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。。并发对于我来说接触很少,几乎没有写过多线程并发之类的程序。之前折腾Python的时候使用过多线程,执行效率要高不少。PHP也有多线程,我真是一次都没有用到过。

oh my zsh有兴趣可以了解下oh-my-zsh强大的zsh配置管理)会自动提示更新,但是自己安装的一些插件却不会一起更新,最近翻了一遍Go语言官方文档,写了个oh my zsh自定义插件更新程序。

开搞

oh my zsh的自定义插件都安装在了/root/.oh-my-zsh/custom/plugins目录下,我随便找了个已安装的插件试着git pull,果然没有更新。插件也不多,就两个。但是我看了Go语言文档有劲没处使啊,就要折腾下。

我不仅要写,我还要多线程,各种多线程

第一步,找出需要更新的目录

上网查、看文档,折腾出这么个玩意:

func GetDir(path string) ([]string, error) {
    var s []string
    rd, err := ioutil.ReadDir(path)
    if err != nil {
        fmt.Println("read dir error:", err)
        return s, err
    }

    for _, dir := range rd {
        if dir.IsDir() {
            s = append(s, path+"/"+dir.Name())
        }
    }

    return s, err
}

ioutil.ReadDir读取传入路径下所有文件和目录,然后找出所有目录返回,这样一个简单的获取oh my zsh自定义插件目录的函数就封装好了。

第二步,进入目录更新

其实写这篇文章的时候我已经修改了好几遍程序了,还是贴最终的吧。 更新函数,遍历目录然后执行pull函数拉代码。

var wg sync.WaitGroup
...
func Update(path string) {
    //不异步不需要执行
    defer wg.Done()
    paths, _ := GetDir(path)
    if len(paths) != 0 {
        for _, dir := range paths {
            //添加进程数
            wg.Add(1)
            fmt.Println("get pull dir:", dir)
            go Pull(dir)
        }
    }
}

最开始的版本没有使用sync.WaitGroup,会出现问题。涉及到Channels,主线程也是用多线程,update函数中也有多线程。Channels在使用<- ch接收值时会堵塞,直到ch接收到Channels的值后继续执行。那么不使用sync.WaitGroup堵塞进程,还没等子线程/协程执行完主线程就已经执行完毕退出了。

func Pull(path string) {
    //每执行一个进程都要标记完成
    defer wg.Done()
    //command := exec.Command("sh", "-c", "cd ", path, "&&", "git", "pull")
    command := exec.Command("sh", "-c", "cd " + path + "&& git pull")
    ch := make(chan int)
    go Stdout(command, ch)
    fmt.Println("ProcessState PID:", <- ch)
}

exec.Command有点讲究,起初我直接把命令作为第一个参数传入是不行的,需要把命令拆分成参数才能执行。 后面我又把获取参数封装成了Stdout函数,其实我想折腾下Channels

第三步,返回命令行输出

折腾起来浑身是劲,编写命令行返回输出函数,我要看到所有git pull的执行结果。

func Stdout(command *exec.Cmd, ch chan int) {
    stdout, _ := command.StdoutPipe()
    stderr, _ := command.StderrPipe()
    if err := command.Start(); nil != err {
        fmt.Println("command start err:", err)
        ch <- 0
        return
    }
    pid := command.Process.Pid
    output(stdout, pid)
    output(stderr, pid)

    err := command.Wait() //等待执行完成
    if nil != err {
        fmt.Printf("Process PID %d command wait err: %s", pid, err)
    }
    ch <- command.ProcessState.Pid()
    return
}

command.StdoutPipe,command.StderrPipe分别获取命令正确输出返回和错误输出返回,好打印出来。command.Wait()等待命令结束。exec.Command需要好好看看文档。直接执行命令不需要这么麻烦,使用exec.Run()就行,但是我要捕获命令输出,所以相对麻烦。

func outputLine(oReader *bufio.Reader, pid int) {
    for {
        line, err := oReader.ReadString('\n')
        if nil != err{
            if io.EOF != err {
                log.Printf("Process PID %d: err %v", pid, err)
            }
            break
        }
        log.Printf("Process PID %d: %s", pid, line)
    }
}

func output(stdout io.ReadCloser, pid int)  {
    oReader := bufio.NewReader(stdout)
    outputLine(oReader, pid)
}

这两个函数主要是重复代码太多,封装了下,outputLine函数主要是按行输出命令返回信息。

第四部,主函数

主函数我改进接收参数功能,在执行的时候可以接收多个目录参数,然后并发git pull

func main() {
    // 接收多目录参数
    args := os.Args[1:]
    if len(args) != 0 {
        for _, path := range args {
            fmt.Println(path)
            wg.Add(1)
            go Update(path)
        }
    } else {
        path := "/root/.oh-my-zsh/custom/plugins"
        //添加进程数,相对于Update异步,如果Update不异步可以不需要add
        wg.Add(1)
        go Update(path)
        //等待所有进程全部结束后退出主线程
    }
    wg.Wait()
}

sync.WaitGroup就是一个计数器,使用Add()添加,Done()就释放,Wait()会等待所有Add()全部Done()后向下执行,否则一直堵塞进程。如果这里我使用Channels无法控制,主线程传入Channels,那么Update()函数何时返回呢?函数中又是多进程/协程,没有找到其他办法,因为Update()函数中还要输出,全部用Channels会乱成一锅粥的。

贴一下完整代码吧,看著挺乱的:

package main

import (
    "bufio"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "os"
    "os/exec"
    "runtime"
    "sync"
)

// 更新oh my zsh 自定义插件
//同步
var wg sync.WaitGroup

func GetDir(path string) ([]string, error) {
    var s []string
    rd, err := ioutil.ReadDir(path)
    if err != nil {
        fmt.Println("read dir error:", err)
        return s, err
    }

    for _, dir := range rd {
        if dir.IsDir() {
            s = append(s, path+"/"+dir.Name())
        }
    }

    return s, err
}

func Update(path string) {
    //不异步不需要执行
    defer wg.Done()
    paths, _ := GetDir(path)
    if len(paths) != 0 {
        for _, dir := range paths {
            //添加进程数
            wg.Add(1)
            fmt.Println("get pull dir:", dir)
            go Pull(dir)
        }
    }
}

func Stdout(command *exec.Cmd, ch chan int) {
    stdout, _ := command.StdoutPipe()
    stderr, _ := command.StderrPipe()
    if err := command.Start(); nil != err {
        fmt.Println("command start err:", err)
        ch <- 0
        return
    }
    pid := command.Process.Pid
    output(stdout, pid)
    output(stderr, pid)

    err := command.Wait() //等待执行完成
    if nil != err {
        fmt.Printf("Process PID %d command wait err: %s", pid, err)
    }
    ch <- command.ProcessState.Pid()
    return
}

func outputLine(oReader *bufio.Reader, pid int) {
    for {
        line, err := oReader.ReadString('\n')
        if nil != err{
            if io.EOF != err {
                log.Printf("Process PID %d: err %v", pid, err)
            }
            break
        }
        log.Printf("Process PID %d: %s", pid, line)
    }
}

func output(stdout io.ReadCloser, pid int)  {
    oReader := bufio.NewReader(stdout)
    outputLine(oReader, pid)
}

func Pull(path string) {
    //每执行一个进程都要标记完成
    defer wg.Done()
    //command := exec.Command("sh", "-c", "cd ", path, "&&", "git", "pull")
    command := exec.Command("sh", "-c", "cd " + path + "&& git pull")
    ch := make(chan int)
    go Stdout(command, ch)
    fmt.Println("ProcessState PID:", <- ch)
}

func main() {
    // 接收多目录参数
    args := os.Args[1:]
    if len(args) != 0 {
        for _, path := range args {
            fmt.Println(path)
            wg.Add(1)
            go Update(path)
        }
    } else {
        path := "/root/.oh-my-zsh/custom/plugins"
        //添加进程数,相对于Update异步,如果Update不异步可以不需要add
        wg.Add(1)
        go Update(path)
        //等待所有进程全部结束后退出主线程
    }
    wg.Wait()
}

最后使用go build 文件名编译就好了,跨平台。

结语

在看Golang文档的时候语法有点别扭,不过敲了几遍后居然特别顺手。敲这个程序真的试了很多只用Channels的方法,但是达不到想要的效果,还是总线程不等待所有子线程执行完毕的问题。因为这里线程/协程用得太杂乱了。不过这就是我的目的,让线程/协程极致的情况下怎么控制好。各种线程/协程满天飞是怎么样的体验。

本原创文章未经允许不得转载 | 当前页面:OrzLee » Golang oh-my-zsh 自定义插件批量更新程序

评论