英文:
Streaming commands output progress
问题
我正在编写一个服务,需要将执行命令的输出流同时发送给父进程和日志。当有一个长时间运行的进程时,问题在于cmd.StdoutPipe
只会给我一个最终的(字符串)结果。
是否可能给出正在进行的部分输出,就像在shell中一样?
func main() {
cmd := exec.Command("sh", "-c", "some long running task")
stdout, _ := cmd.StdoutPipe()
cmd.Start()
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
m := scanner.Text()
fmt.Println(m)
log.Printf(m)
}
cmd.Wait()
}
P.S. 只要输出是:
cmd.Stdout = os.Stdout
但在我的情况下,这是不够的。
英文:
I'm writing a service that has to stream output of a executed command both to parent and to log. When there is a long process, the problem is that cmd.StdoutPipe
gives me a final (string) result.
Is it possible to give partial output of what is going on, like in shell
func main() {
cmd := exec.Command("sh", "-c", "some long runnig task")
stdout, _ := cmd.StdoutPipe()
cmd.Start()
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
m := scanner.Text()
fmt.Println(m)
log.Printf(m)
}
cmd.Wait()
}
P.S. Just to output would be:
cmd.Stdout = os.Stdout
But in my case it is not enough.
答案1
得分: 12
你发布的代码是有效的(在执行合理的命令时)。以下是一个简单的Go代码示例,用于测试你的代码:
func main() {
fmt.Println("Child started.")
time.Sleep(time.Second*2)
fmt.Println("Tick...")
time.Sleep(time.Second*2)
fmt.Println("Child ended.")
}
编译并将其作为你的命令调用。你将立即看到由子进程输出的不同行,即“流式”。
可能导致你的代码无法正常工作的原因有:
bufio.NewScanner()
返回的Scanner
读取整行,并且只有在遇到换行符时(由bufio.ScanLines()
函数定义)才返回内容。
如果你执行的命令没有打印换行符,其输出将不会立即返回(只有在打印换行符、内部缓冲区填满或进程结束时才会返回)。
可能的解决方法是:
如果你无法保证子进程打印换行符,但仍希望流式输出,你不能读取整行。一种解决方案是按单词读取,甚至按字符(符文)读取。你可以使用Scanner.Split()
方法设置不同的分割函数来实现:
scanner := bufio.NewScanner(stdout)
scanner.Split(bufio.ScanRunes)
bufio.ScanRunes
函数按rune
读取输入,因此Scanner.Scan()
将在每个新的rune
可用时返回。
或者,可以手动读取而不使用Scanner
(在此示例中逐字节读取):
oneByte := make([]byte, 1)
for {
_, err := stdout.Read(oneByte)
if err != nil {
break
}
fmt.Printf("%c", oneByte[0])
}
请注意,上述代码会错误地读取多字节的UTF-8编码的rune
。为了正确读取多字节的UTF-8编码的rune
,我们需要一个更大的缓冲区:
oneRune := make([]byte, utf8.UTFMax)
for {
count, err := stdout.Read(oneRune)
if err != nil {
break
}
fmt.Printf("%s", oneRune[:count])
}
需要注意的是,进程有默认的标准输出和标准错误缓冲区(通常为几KB的大小)。如果进程写入标准输出或标准错误,它将进入相应的缓冲区。如果该缓冲区已满,进一步的写入操作将阻塞(在子进程中)。如果你不读取子进程的标准输出和标准错误,如果缓冲区已满,你的子进程可能会挂起。
因此,建议始终读取子进程的标准输出和错误。即使你知道该命令通常不会写入其标准错误,如果发生错误,它可能会开始将错误消息转储到其标准错误。
**编辑:**正如Dave C所提到的,默认情况下,子进程的标准输出和错误流会被丢弃,如果不读取它们,不会导致阻塞/挂起。但是,如果不读取错误流,你可能会错过一些进程的信息。
英文:
The code you posted works (with a reasonable command executed).
Here is a simple "some long running task" written in Go for you to call and test your code:
func main() {
fmt.Println("Child started.")
time.Sleep(time.Second*2)
fmt.Println("Tick...")
time.Sleep(time.Second*2)
fmt.Println("Child ended.")
}
Compile it and call it as your command. You will see the different lines appear immediately as written by the child process, "streamed".
Reasons why it may not work for you
The Scanner
returned by bufio.NewScanner()
reads whole lines and only returns something if a newline character is encountered (as defined by the bufio.ScanLines()
function).
If the command you execute doesn't print newline characters, its output won't be returned immediately (only when newline character is printed, internal buffer is filled or the process ends).
Possible workarounds
If you have no guarantee that the child process prints newline characters but you still want to stream the output, you can't read whole lines. One solution is to read by words, or even read by characters (runes). You can achieve this by setting a different split function using the Scanner.Split()
method:
scanner := bufio.NewScanner(stdout)
scanner.Split(bufio.ScanRunes)
The bufio.ScanRunes
function reads the input by rune
s so Scanner.Scan()
will return whenever a new rune
is available.
Or reading manually without a Scanner
(in this example byte-by-byte):
oneByte := make([]byte, 1)
for {
_, err := stdout.Read(oneByte)
if err != nil {
break
}
fmt.Printf("%c", oneByte[0])
}
Note that the above code would read rune
s that multiple bytes in UTF-8 encoding incorrectly. To read multi UTF-8-byte runes, we need a bigger buffer:
oneRune := make([]byte, utf8.UTFMax)
for {
count, err := stdout.Read(oneRune)
if err != nil {
break
}
fmt.Printf("%s", oneRune[:count])
}
Things to keep in mind
Processes have default buffers for standard output and for standard error (usually the size of a few KB). If a process writes to the standard output or standard error, it goes into the respective buffer. If this buffer gets full, further writes will block (in the child process). If you don't read the standard output and standard error of a child process, your child process may hang if the buffer is full.
So it is recommended to always read both the standard output and error of a child process. Even if you know that the command don't normally write to its standard error, if some error occurs, it will probably start dumping error messages to its standard error.
Edit: As Dave C mentions by default the standard output and error streams of the child process are discarded and will not cause a block / hang if not read. But still, by not reading the error stream you might miss a thing or two from the process.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论