go - 使用 Go Routines 将控制台日志连续打印到网页屏幕

标签 go

我让下面的 go 例程可以工作,但问题是它打印到控制台而不是屏幕。我的想法是在网页上显示脚本中发生的命令或输出的运行日志,可以实时观看。使用 fmt.Fprint 并不能解决问题。所发生的只是我的网页永远不会完全加载。请帮忙?

Running external python in Golang, Catching continuous exec.Command Stdout

去代码

package main

import (
    "log"
    "net/http"
    "time"
    "os/exec"
    "io"
    "bufio"
    "fmt"
    "github.com/gorilla/mux"
)

func main() {
    r := mux.NewRouter()
    s := r.PathPrefix("/api/").Subrouter()
    s.HandleFunc("/export", export).Methods("GET")
    http.Handle("/", r)
    log.Panic(http.ListenAndServe(":80", nil))
}

func export(w http.ResponseWriter, r *http.Request) {
    cmd := exec.Command("python", "game.py")
    stdout, err := cmd.StdoutPipe()
    if err != nil {
        panic(err)
    }
    stderr, err := cmd.StderrPipe()
    if err != nil {
        panic(err)
    }
    err = cmd.Start()
    if err != nil {
        panic(err)
    }

    go copyOutput(stdout)
    go copyOutput(stderr)
    cmd.Wait()
}

func copyOutput(r io.Reader, w http.ResponseWriter) {
    scanner := bufio.NewScanner(r)
    for scanner.Scan() {
        fmt.Fprint(w, scanner.Text()) //line I expect to print to the screen, but doesn't
    }
}

python脚本

import time
import sys

while True:
    print "Hello"
    sys.stdout.flush()
    time.sleep(1)

该站点还有很多内容,所以我知道路由配置正确,因为当我不使用 go 例程时,打印到屏幕是有效的'

更新:

这是我的新更新功能,它会打印到屏幕上,但仅在整个脚本运行之后,而不是按原样显示

func export(w http.ResponseWriter, r *http.Request) {
    cmd := exec.Command("python", "game.py")
    cmd.Stdout = w
    cmd.Start()
    cmd.Wait()
}

我相信我可能仍然需要一个 go 例程以便在我进行时打印它,但是将 cmd.Start 和/或 cmd.Wait 放在一个中是行不通的

更新:

因此,即使给出了所有内容,我也无法在浏览器上显示输出,因为它们正在运行。它只是锁定了浏览器,即使有标题和冲洗。我希望有时间对此给出一个完整的、有效的答案,但就目前而言,上面的代码在运行后正确地将代码打印到浏览器。我找到了一个我认为可能是我正在寻找的 repo 协议(protocol),也许它也会帮助遇到这个问题的其他人。

https://github.com/yudai/gotty

最佳答案

这是一个非常基本(幼稚)的示例,但如何让您了解如何连续传输数据:

https://play.golang.org/p/vtXPEHSv-Sg

game.py 的代码是:

import time
import sys

while True:
    print("Hello")
    sys.stdout.flush()
    time.sleep(1)

网络应用程序代码:

package main

import (
    "bufio"
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
    "os/exec"

    "github.com/nbari/violetear"
)

func stream(w http.ResponseWriter, r *http.Request) {
    cmd := exec.Command("python", "game.py")
    rPipe, wPipe, err := os.Pipe()
    if err != nil {
        log.Fatal(err)
    }
    cmd.Stdout = wPipe
    cmd.Stderr = wPipe
    if err := cmd.Start(); err != nil {
        log.Fatal(err)
    }
    go writeOutput(w, rPipe)
    cmd.Wait()
    wPipe.Close()
}

func writeOutput(w http.ResponseWriter, input io.ReadCloser) {
    flusher, ok := w.(http.Flusher)
    if !ok {
        http.Error(w, "Streaming not supported", http.StatusInternalServerError)
        return
    }

    // Important to make it work in browsers
    w.Header().Set("Content-Type", "text/event-stream")
    w.Header().Set("Cache-Control", "no-cache")
    w.Header().Set("Connection", "keep-alive")

    in := bufio.NewScanner(input)
    for in.Scan() {
        fmt.Fprintf(w, "data: %s\n", in.Text())
        flusher.Flush()
    }
    input.Close()
}

func main() {
    router := violetear.New()
    router.HandleFunc("/", stream, "GET")
    log.Fatal(http.ListenAndServe(":8080", router))
}

这里的关键部分是使用 http.Flusher和一些标题使其在浏览器中工作:

 w.Header().Set("Content-Type", "text/event-stream")

请注意此代码的问题在于,一旦请求到达,它将执行 永远循环的命令,因此永远不会调用wPipe.Close()

    cmd.Wait()
    wPipe.Close()

为了更详细,您可以在浏览器旁边的终端打印输出:

 for in.Scan() {
     data := in.Text()
     log.Printf("data: %s\n", data)
     fmt.Fprintf(w, "data: %s\n", data)
     flusher.Flush()
 }

如果您有多个请求,您会注意到它在终端中的写入速度会更快,这还不错,但您还会注意到,如果客户端关闭了连接/浏览器,您仍然会看到数据输出。

更好的方法是在上下文中执行命令,例如:https://golang.org/pkg/os/exec/#CommandContext

ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()

if err := exec.CommandContext(ctx, "sleep", "5").Run(); err != nil {
    // This will fail after 100 milliseconds. The 5 second sleep
    // will be interrupted.
}

另请查看上下文 (https://stackoverflow.com/a/44146619/1135424) 未替换 http.CloseNotifier 因此在客户端关闭浏览器、disconetcs 后终止进程可能很有用。

最后取决于您的需求,但希望可以让您了解如何使用 http.Flusher 接口(interface)以简单的方式流式传输数据。

只是为了好玩,这里有一个使用 context 的例子:

https://play.golang.org/p/V69BuDUceBA

仍然是非常基本的,但在这种情况下,如果客户端关闭浏览器,程序也会终止,因为练习可以很好地改进它并分享回来 ;-),注意 CommandContext 的使用和 ctx.Done()

package main

import (
    "bufio"
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
    "os/exec"

    "github.com/nbari/violetear"
)

func stream(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    ch := make(chan struct{})

    cmd := exec.CommandContext(ctx, "python", "game.py")
    rPipe, wPipe, err := os.Pipe()
    if err != nil {
        log.Fatal(err)
    }
    cmd.Stdout = wPipe
    cmd.Stderr = wPipe
    if err := cmd.Start(); err != nil {
        log.Fatal(err)
    }

    go writeOutput(w, rPipe)

    go func(ch chan struct{}) {
        cmd.Wait()
        wPipe.Close()
        ch <- struct{}{}
    }(ch)

    select {
    case <-ch:
    case <-ctx.Done():
        err := ctx.Err()
        log.Printf("Client disconnected: %s\n", err)
    }
}

func writeOutput(w http.ResponseWriter, input io.ReadCloser) {
    flusher, ok := w.(http.Flusher)
    if !ok {
        http.Error(w, "Streaming not supported", http.StatusInternalServerError)
        return
    }

    // Important to make it work in browsers
    w.Header().Set("Content-Type", "text/event-stream")
    w.Header().Set("Cache-Control", "no-cache")
    w.Header().Set("Connection", "keep-alive")

    in := bufio.NewScanner(input)
    for in.Scan() {
        data := in.Text()
        log.Printf("data: %s\n", data)
        fmt.Fprintf(w, "data: %s\n", data)
        flusher.Flush()
    }
    input.Close()
}

func main() {
    router := violetear.New()
    router.HandleFunc("/", stream, "GET")
    log.Fatal(http.ListenAndServe(":8080", router))
}

关于go - 使用 Go Routines 将控制台日志连续打印到网页屏幕,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49761966/

相关文章:

go - 如何让我的 GoLand 检测 $GOPATH/pkg/mod 下的依赖包?

go - 写入 TOML 文件时发现重复键; tree.Has() 没有按预期工作

go - 替换 Go 默认日志的 *os.File

go - Go 中的 new(Struct) 和 &Struct{} 有什么区别?

go - 获取二维 slice 的尺寸

go - 如何使用 Channels 使 goroutines 相互通信

go - 如何在不阻塞 channel 的情况下用股票报告统计数据?

google-app-engine - 使用 Go、App Engine、专用内存缓存和实例内存实现分片计数器

sqlite - Go sqlite 无法在 mac osx 上构建/编译

http - 去重试 403 禁止的 http 请求?