golang自定义 os.stderr 数据读取逻辑
原始需求
只是一个很简单的需求, 使用golang的exec运行一个命令然后获取实时结果, 命令是
trivy image --download-db-only
正常的打印应该是
2023-08-08T17:06:02.929+0800 INFO Need to update DB
2023-08-08T17:06:02.929+0800 INFO DB Repository: ghcr.io/aquasecurity/trivy-db
2023-08-08T17:06:02.929+0800 INFO Downloading DB...
867.62 KiB / 38.79 MiB [->_____________________________________________________________________] 2.18% 804.52 KiB p/s ETA 48s
然后最下面是个进度条, 随着时间会慢慢的加满, 最后是到100结束
2023-08-08T17:36:33.103+0800 INFO Need to update DB
2023-08-08T17:36:33.103+0800 INFO DB Repository: ghcr.io/aquasecurity/trivy-db
2023-08-08T17:36:33.103+0800 INFO Downloading DB...
38.79 MiB / 38.79 MiB [--------------------------------------------------------------------------] 100.00% 835.73 KiB p/s 48s
然后我就使用普通的 exec.Command
运行并监听其 stderr 和 stdout 通道, 结果发现进度条数据获取不到, 我将每一个数据都记录到了日志文件中, 日志文件如下:
{"level":"warn","ts":"2023-08-08T16:29:04.978+0800","caller":"trivy-db/download.go:46","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"2023-08-08T16:29:04.978+0800\t\u001b[34mINFO\u001b[0m\tNeed to update DB"}
{"level":"warn","ts":"2023-08-08T16:29:04.978+0800","caller":"trivy-db/download.go:46","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"2023-08-08T16:29:04.978+0800\t\u001b[34mINFO\u001b[0m\tDB Repository: ghcr.io/aquasecurity/trivy-db"}
{"level":"warn","ts":"2023-08-08T16:29:04.978+0800","caller":"trivy-db/download.go:46","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"2023-08-08T16:29:04.978+0800\t\u001b[34mINFO\u001b[0m\tDownloading DB..."}
请忽略\t
, [34m
这种特殊字符, 可以看到后续的进度条日志完全未捕捉到, 然后开始了找问题之旅, 因为觉得挺有意思所以把过程分享给大家 👀
确认代码
首先, 需要确认代码是不是有问题, 我的代码如下
var downloadDBCmd = []string{"trivy", "image", "--download-db-only", "--cache-dir", config.TrivyDBOriginPath}
// 返回DB的存储地址
func DownloadDB() (string, error) {
// 创建Cmd对象
cmd := exec.Command(downloadDBCmd[0], downloadDBCmd[1:]...)
// 执行命令,并获取输出
stdout, err := cmd.StdoutPipe()
if err != nil {
zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
return "", err
}
outScanner := bufio.NewScanner(stdout)
go func() {
for outScanner.Scan() {
zap.L().Info("命令正常输出", log.NewCmdFiled(downloadDBCmd), log.NewOutputFiled(outScanner.Text()))
}
}()
stderr, err := cmd.StderrPipe()
if err != nil {
zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
return "", err
}
errScanner := bufio.NewScanner(stderr)
go func() {
for errScanner.Scan() {
zap.L().Warn("命令错误输出", log.NewCmdFiled(downloadDBCmd), log.NewOutputFiled(errScanner.Text()))
}
}()
if err := cmd.Start(); err != nil {
zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
return "", err
}
if err := cmd.Wait(); err != nil {
zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
return "", err
}
return config.TrivyDBOriginPath, nil
}
然后我更换了命令, 例如 ping www.baidu.com
, apt update -y
等等, 发现, 当出现进度条的输出时, 程序会等待, 当程序正确结束后, 发现会在cmd结束时一起打印出来, 例如
16.00 KiB / 38.79 MiB [>_____________________________________________________________________________________________________] 0.04% ? p/s ?64.00 KiB / 38.79 MiB [>_____________________________________________________________________________________________________] 0.16% ? p/s ?176.00 KiB / 38.79 MiB [>____________________________________________________________________________________________________] 0.44% ? p/s ?304.00 KiB / 38.79 MiB [>___________________________________________________________________________________] 0.77% 480.27 KiB p/s ETA 1m22s496.00 KiB / 38.79 MiB [->__________________________________________________________________________________] 1.25% 480.27 KiB p/s ETA 1m21s496.00 KiB / 38.79 MiB [->__________________________________________________________________________________] 1.25% 480.27 KiB p/s ETA 1m21s496.00 KiB / 38.79 MiB [->__________________________________________________________________________________] 1.25% 469.91 KiB p/s ETA 1m23s688.00 KiB / 38.79 MiB [->__________________________________________________________________________________]
那么基本可以确定的是, 针对这种进度条在一行刷新的情况, 捕捉的时候会出现问题, 他不会将结果及时输出, 而之前 trivy 一直没输出, 大概率是因为网络问题导致进程一直卡住没有结束, 而后我通过手段对外网访问进行加速, 结果确实与我想象的一样, trivy 的进度条也会在结束的一瞬间全部打印出来, 为保险起见, 我还需要看一下 trivy 的代码, 确认一下trivy的打印实现.
查看 Trivy 代码
Trivy代码本身是开源的, 代码在 aquasecurity/trivy: Find vulnerabilities, misconfigurations, secrets, SBOM in containers, Kubernetes, code repositories, clouds and more (github.com)
我将其clone下来, 通过命令行参数一步步寻找到其--download-db-only
的处理部分, 在 trivy/pkg/commands/operation/operation.go at main · aquasecurity/trivy (github.com) 这里的 DownloadDB
函数, 随后又一步步的点击进逻辑 trivy/pkg/oci/artifact.go at main · aquasecurity/trivy (github.com) 这里的函数 download
中, 函数如下
func (a *Artifact) download(ctx context.Context, layer v1.Layer, fileName, dir string) error {
size, err := layer.Size()
if err != nil {
return xerrors.Errorf("size error: %w", err)
}
rc, err := layer.Compressed()
if err != nil {
return xerrors.Errorf("failed to fetch the layer: %w", err)
}
defer rc.Close()
// Show progress bar
bar := pb.Full.Start64(size)
if a.quiet {
bar.SetWriter(io.Discard)
}
pr := bar.NewProxyReader(rc)
defer bar.Finish()
// https://github.com/hashicorp/go-getter/issues/326
tempDir, err := os.MkdirTemp("", "trivy")
if err != nil {
return xerrors.Errorf("failed to create a temp dir: %w", err)
}
f, err := os.Create(filepath.Join(tempDir, fileName))
if err != nil {
return xerrors.Errorf("failed to create a temp file: %w", err)
}
defer func() {
_ = f.Close()
_ = os.RemoveAll(tempDir)
}()
// Download the layer content into a temporal file
if _, err = io.Copy(f, pr); err != nil {
return xerrors.Errorf("copy error: %w", err)
}
// Decompress the downloaded file if it is compressed and copy it into the dst
if err = downloader.Download(ctx, f.Name(), dir, dir); err != nil {
return xerrors.Errorf("download error: %w", err)
}
return nil
}
其中, 代码段的注释提醒我这里是进度条的展示
// Show progress bar
bar := pb.Full.Start64(size)
if a.quiet {
bar.SetWriter(io.Discard)
}
pr := bar.NewProxyReader(rc)
defer bar.Finish()
然后看了一下pb
发现是一个第三方的模块, 专门进行进度条的展示
cheggaaa/pb: Console progress bar for Golang (github.com)
然后又去 trivy 的 go.mod
中确认其使用的是最新的v3版本
github.com/cheggaaa/pb/v3 v3.1.2
而后, 我查看了pb项目的文档, 发现 SetWriter
函数来设置进度条输出的地方 cheggaaa/pb: Console progress bar for Golang (github.com)
我也查看了该函数的注释,确实如此, 并且默认情况下是输出到 stderr
中
// SetWriter sets the io.Writer. Bar will write in this writer
// By default this is os.Stderr
func (pb *ProgressBar) SetWriter(w io.Writer) *ProgressBar {
pb.mu.Lock()
pb.output = w
pb.configured = false
pb.configure()
pb.mu.Unlock()
return pb
}
我注意到, trivy这里的代码有一个分支处理, 如果 a.quiet
为 true
则设置输出通道为 io.Discard
, 而io.Discard
是一个不会在任何地方打印的通道, 所以我要确认什么时候 a.quiet
是 True
后来, 我一步步通过代码发现, 只有在携带参数 --quiet
时, a.quiet
才为真, 这点也在 help 里得到了证实
Scanner for vulnerabilities in container images, file systems, and Git repositories, as well as for configuration issues and hard-coded secrets
Usage:
trivy [global flags] command [flags] target
trivy [command]
Examples:
# Scan a container image
$ trivy image python:3.4-alpine
# Scan a container image from a tar archive
$ trivy image --input ruby-3.1.tar
# Scan local filesystem
$ trivy fs .
# Run in server mode
$ trivy server
Scanning Commands
aws [EXPERIMENTAL] Scan AWS account
config Scan config files for misconfigurations
filesystem Scan local filesystem
image Scan a container image
kubernetes [EXPERIMENTAL] Scan kubernetes cluster
repository Scan a remote repository
rootfs Scan rootfs
sbom Scan SBOM for vulnerabilities
vm [EXPERIMENTAL] Scan a virtual machine image
Management Commands
module Manage modules
plugin Manage plugins
Utility Commands
completion Generate the autocompletion script for the specified shell
convert Convert Trivy JSON report into a different format
help Help about any command
server Server mode
version Print the version
Flags:
--cache-dir string cache directory (default "/root/.cache/trivy")
-c, --config string config path (default "trivy.yaml")
-d, --debug debug mode
-f, --format string version format (json)
--generate-default-config write the default config to trivy-default.yaml
-h, --help help for trivy
--insecure allow insecure server connections
-q, --quiet suppress progress bar and log output
--timeout duration timeout (default 5m0s)
-v, --version show version
Use "trivy [command] --help" for more information about a command.
当然我也加上了 --quiet
进行了测试发现加上参数后才是真正的不输出.
到目前为止, 我确认了, 默认情况下还是会输入到os.stderr
中, 那么我现在需要考虑的是, 为什么他在结束的全部打印, 从最后的全部打印来看, 每一行数据中间并没有 \n
分隔, 那么我第一时间认为, 是不是因为捕捉到的结果都是堆到一起的, 而因为中间的分隔符错误, 导致程序认为这是一行输出, 还没有结束, 一直积攒起来, 等到程序结束了, 一起打印出来呢?
stderr监听方式
我查看了我的stderr监听代码, 主要这一段
stderr, err := cmd.StderrPipe()
if err != nil {
zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
return "", err
}
errScanner := bufio.NewScanner(stderr)
errScanner.Split(ScanLines)
go func() {
for errScanner.Scan() {
zap.L().Warn("命令错误输出", log.NewCmdFiled(downloadDBCmd), log.NewOutputFiled(errScanner.Text()))
}
}()
我通过查看这些函数内部代码, 发现 bufio.NewScanner
返回的对象中有一个split
属性, 默认是 ScanLines
这个函数
// NewScanner returns a new Scanner to read from r.
// The split function defaults to ScanLines.
func NewScanner(r io.Reader) *Scanner {
return &Scanner{
r: r,
split: ScanLines,
maxTokenSize: MaxScanTokenSize,
}
}
我发现这个 split
属性是一个函数, 函数内部定义了怎么将studerr内的数据分割和输出
// ScanLines is a split function for a Scanner that returns each line of
// text, stripped of any trailing end-of-line marker. The returned line may
// be empty. The end-of-line marker is one optional carriage return followed
// by one mandatory newline. In regular expression notation, it is `\r?\n`.
// The last non-empty line of input will be returned even if it has no
// newline.
func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexByte(data, '\n'); i >= 0 {
// We have a full newline-terminated line.
return i + 1, dropCR(data[0:i]), nil
}
// If we're at EOF, we have a final, non-terminated line. Return it.
if atEOF {
return len(data), dropCR(data), nil
}
// Request more data.
return 0, nil, nil
}
从代码里可以清晰看出, 这里是根据data的\n
进行分割, 如果没有\n
, 并且没有EOF结束, 则返回0, 等待下次有数据来再进行这个函数的调用.
而之前的 Trivy 运行, 因为网络问题, 虽然有打印输出到 stderr, 进入了这个ScanLines
函数, 但是因为没有\n
, 导致一直没有发送到scanner中
而后来进行了网络加速, 可以正常下载了, 于是正常触发了EOF将所有data一并打出
那么下一步, 想办法能不能把这个函数给替换成我自定义的
自定义split逻辑
我查看了 Scanner
的其他方法, 果然发现了一个 Split
方法
// Split sets the split function for the Scanner.
// The default split function is ScanLines.
//
// Split panics if it is called after scanning has started.
func (s *Scanner) Split(split SplitFunc) {
if s.scanCalled {
panic("Split called after Scan")
}
s.split = split
}
看注释, 可以替换默认的分割器, 参数 split
类型是一个函数
// SplitFunc is the signature of the split function used to tokenize the
// input. The arguments are an initial substring of the remaining unprocessed
// data and a flag, atEOF, that reports whether the Reader has no more data
// to give. The return values are the number of bytes to advance the input
// and the next token to return to the user, if any, plus an error, if any.
//
// Scanning stops if the function returns an error, in which case some of
// the input may be discarded. If that error is ErrFinalToken, scanning
// stops with no error.
//
// Otherwise, the Scanner advances the input. If the token is not nil,
// the Scanner returns it to the user. If the token is nil, the
// Scanner reads more data and continues scanning; if there is no more
// data--if atEOF was true--the Scanner returns. If the data does not
// yet hold a complete token, for instance if it has no newline while
// scanning lines, a SplitFunc can return (0, nil, nil) to signal the
// Scanner to read more data into the slice and try again with a
// longer slice starting at the same point in the input.
//
// The function is never called with an empty data slice unless atEOF
// is true. If atEOF is true, however, data may be non-empty and,
// as always, holds unprocessed text.
type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)
于是我进行测试, 直接将默认的ScanLines
抄下来, 改成检测到 空格 就分割抛出, 看看效果
// dropCR drops a terminal \r from the data.
func dropCR(data []byte) []byte {
if len(data) > 0 && data[len(data)-1] == '\r' {
return data[0 : len(data)-1]
}
return data
}
func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
// 检测到空格就抛出
if i := bytes.IndexByte(data, ' '); i >= 0 {
// We have a full newline-terminated line.
return i + 1, dropCR(data[0:i]), nil
}
// If we're at EOF, we have a final, non-terminated line. Return it.
if atEOF {
return len(data), dropCR(data), nil
}
// Request more data.
return 0, nil, nil
}
// 返回DB的存储地址
func DownloadDB() (string, error) {
// 创建Cmd对象
cmd := exec.Command(downloadDBCmd[0], downloadDBCmd[1:]...)
// 执行命令,并获取输出
stdout, err := cmd.StdoutPipe()
if err != nil {
zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
return "", err
}
outScanner := bufio.NewScanner(stdout)
go func() {
for outScanner.Scan() {
zap.L().Info("命令正常输出", log.NewCmdFiled(downloadDBCmd), log.NewOutputFiled(outScanner.Text()))
}
}()
stderr, err := cmd.StderrPipe()
if err != nil {
zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
return "", err
}
errScanner := bufio.NewScanner(stderr)
errScanner.Split(ScanLines) // 替换默认的分割器
go func() {
for errScanner.Scan() {
zap.L().Warn("命令错误输出", log.NewCmdFiled(downloadDBCmd), log.NewOutputFiled(errScanner.Text()))
}
}()
if err := cmd.Start(); err != nil {
zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
return "", err
}
if err := cmd.Wait(); err != nil {
zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
return "", err
}
return config.TrivyDBOriginPath, nil
}
经过运行, 发现果然是立刻就记录了, 当然是按照空格区分肯定是不满足要求的
{"level":"warn","ts":"2023-08-08T17:03:37.395+0800","caller":"trivy-db/download.go:72","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"DB...\n32.00"}
{"level":"warn","ts":"2023-08-08T17:03:37.395+0800","caller":"trivy-db/download.go:72","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"KiB"}
{"level":"warn","ts":"2023-08-08T17:03:37.395+0800","caller":"trivy-db/download.go:72","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"/"}
{"level":"warn","ts":"2023-08-08T17:03:37.395+0800","caller":"trivy-db/download.go:72","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"38.79"}
{"level":"warn","ts":"2023-08-08T17:03:37.395+0800","caller":"trivy-db/download.go:72","msg":"命令错误输出","command":"trivy image --download-db-only","output":"MiB"}
{"level":"warn","ts":"2023-08-08T17:03:37.395+0800","caller":"trivy-db/download.go:72","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"[>_____________________________________________________________________________________________________]"}
{"level":"warn","ts":"2023-08-08T17:03:37.395+0800","caller":"trivy-db/download.go:72","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"0.08%"}
{"level":"warn","ts":"2023-08-08T17:03:37.395+0800","caller":"trivy-db/download.go:72","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"?"}
于是我分析了真正每行的数据, 然后通过 errScanner.Bytes
打印原有的 bytes 值, 期望能找到隐藏的分割每次输出的标识, 很遗憾, 并没有.
而后, 我只能选择根据一定的特征, 去做切分, 毕竟我的需求是日志中能看到进度就行, 我注意到, 每行的数据的后面一定包含字符串 p/s
,其他是会变得, 只有这个是永远存在的, 可以考虑按照字符串 p/s
分割切分
于是将代码修改成按照p/s
切割
// dropCR drops a terminal \r from the data.
func dropCR(data []byte) []byte {
if len(data) > 0 && data[len(data)-1] == '\r' {
return data[0 : len(data)-1]
}
return data
}
func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := strings.Index(string(data), "p/s"); i >= 0 {
// We have a full newline-terminated line.
return i + len("p/s") + 1, dropCR(data[0 : i+len("p/s")]), nil
}
// If we're at EOF, we have a final, non-terminated line. Return it.
if atEOF {
return len(data), dropCR(data), nil
}
// Request more data.
return 0, nil, nil
}
查看运行结果
{"level":"warn","ts":"2023-08-08T19:41:54.404+0800","caller":"trivy-db/download.go:65","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"ETA 54s2.45 MiB / 38.80 MiB [---->____________________________________________________________________] 6.31% 681.84 KiB p/s"}
因为按照 p/s
切分, 所以原本后面的 ETA xx
会放到下一行最前面, 但是也可以解决查看不了进度的问题
后来我又突发奇想, 进度条是每次都完整输出一行到data, 触发一次切割器, 能不能通过在触发器里直接将这个data所有都传出来的方式呢? 于是我将自定义切割器直接修改成
// dropCR drops a terminal \r from the data.
func dropCR(data []byte) []byte {
if len(data) > 0 && data[len(data)-1] == '\r' {
return data[0 : len(data)-1]
}
return data
}
func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
return len(data), dropCR(data), nil
}
运行结果果然如我所料, 只是进度条之前打印的基础信息, 因为是 fmt.Printf
一次出来的, 所以不会分割
{"level":"warn","ts":"2023-08-08T19:32:51.680+0800","caller":"trivy-db/download.go:68","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"2023-08-08T19:32:51.680+0800\t\u001b[34mINFO\u001b[0m\tNeed to update DB\n2023-08-08T19:32:51.680+0800\t\u001b[34mINFO\u001b[0m\tDB Repository: ghcr.io/aquasecurity/trivy-db\n2023-08-08T19:32:51.680+0800\t\u001b[34mINFO\u001b[0m\tDownloading DB...\n"}
{"level":"warn","ts":"2023-08-08T19:32:54.683+0800","caller":"trivy-db/download.go:68","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"16.00 KiB / 38.80 MiB [>______________________________________________________________________________________] 0.04% ? p/s ?"}
{"level":"warn","ts":"2023-08-08T19:32:54.881+0800","caller":"trivy-db/download.go:68","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"96.00 KiB / 38.80 MiB [>______________________________________________________________________________________] 0.24% ? p/s ?"}
{"level":"warn","ts":"2023-08-08T19:32:55.082+0800","caller":"trivy-db/download.go:68","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"240.00 KiB / 38.80 MiB [>_____________________________________________________________________________________] 0.60% ? p/s ?"}
{"level":"warn","ts":"2023-08-08T19:32:55.284+0800","caller":"trivy-db/download.go:68","msg":"命令错误输出","command":"trivy image --download-db-only ","output":"496.00 KiB / 38.80 MiB [>______________________________________________________________________] 1.25% 797.25 KiB p/s ETA 49s"}
可以看到这样的效果更加的好评
最终代码
贴一下最终的代码
package trivydb
import (
"bufio"
"os/exec"
"go.uber.org/zap"
"hids-v3-container-manager/config"
"hids-v3-container-manager/utils/log"
)
var downloadDBCmd = []string{"trivy", "image", "--download-db-only", "--cache-dir", config.TrivyDBOriginPath}
// dropCR drops a terminal \r from the data.
func dropCR(data []byte) []byte {
if len(data) > 0 && data[len(data)-1] == '\r' {
return data[0 : len(data)-1]
}
return data
}
// 适配进度条
func scanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
return len(data), dropCR(data), nil
}
// 返回DB的存储地址
func DownloadDB() (string, error) {
// 创建Cmd对象
cmd := exec.Command(downloadDBCmd[0], downloadDBCmd[1:]...)
// 执行命令,并获取输出
stdout, err := cmd.StdoutPipe()
if err != nil {
zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
return "", err
}
outScanner := bufio.NewScanner(stdout)
go func() {
for outScanner.Scan() {
zap.L().Info("命令输出", log.NewCmdFiled(downloadDBCmd), log.NewOutputFiled(outScanner.Text()))
}
}()
stderr, err := cmd.StderrPipe()
if err != nil {
zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
return "", err
}
errScanner := bufio.NewScanner(stderr)
errScanner.Split(scanLines) // 自定义切割
go func() {
for errScanner.Scan() {
zap.L().Info("命令输出", log.NewCmdFiled(downloadDBCmd), log.NewOutputFiled(errScanner.Text()))
}
}()
if err := cmd.Start(); err != nil {
zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
return "", err
}
if err := cmd.Wait(); err != nil {
zap.L().Error("命令执行失败", log.NewCmdFiled(downloadDBCmd), zap.Error(err))
return "", err
}
return config.TrivyDBOriginPath, nil
}
收获
- 对 Trivy 代码更加了解
- 知道进度条模块pb基本使用
- 了解了
os.stderr
os.stdout
的处理逻辑 - 了解了如何自定义切割器实现自己想要的效果
ps: 除了默认切割器scanLines
, 他还自带了一写其他的, 比如按照bytes直接切分的ScanBytes
等等, 读者有兴趣也可以看一下