duanjing2013 2018-04-10 19:58
浏览 160
已采纳

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

I got the go routine below to work but the problem is that it prints to the console instead of to the screen. My idea is to have a running log of what commands or output is happening in a script show on a webpage where it can be watched in real time. Using fmt.Fprint doesn't do the trick. All that happens is that my webpage will never fully load. Help please?

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

go code

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 script

import time
import sys

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

There's a lot more to the site so I know the route is configured correctly because printing to the screen works when I'm not using the go routine'

UPDATE:

Here is my new update function which prints to the screen, but only after the entire script has ran, not as it goes

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

I believe I may still need a go routine in order to get it to print as I go, but putting cmd.Start and/or cmd.Wait in one doesn't work

UPDATE:

So even with everything given, I have not been able to get having the outputs show on a browser as they are ran working. It simply locks up the browser, even with the headers and flush. I will hopefully have time to give a complete, working answer to this but for now, the code above prints the code to the browser correctly after it has ran. I found a repo that I think may be what I'm looking for and maybe it will help others who come across this question as well.

https://github.com/yudai/gotty

  • 写回答

1条回答 默认 最新

  • doudu161481 2018-04-11 09:06
    关注

    This is a very basic (naive) example but how can give you an idea of how to stream data continuously:

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

    The code for game.py is:

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

    The web app code:

    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
    ", in.Text())
            flusher.Flush()
        }
        input.Close()
    }
    
    func main() {
        router := violetear.New()
        router.HandleFunc("/", stream, "GET")
        log.Fatal(http.ListenAndServe(":8080", router))
    }
    

    The key part here is the use of http.Flusher and some headers to make it work within a browser:

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

    Note the problem with this code is that once a request arrives it will exec the command that loops forever, so the wPipe.Close() will never be called

        cmd.Wait()
        wPipe.Close()
    

    To be more verbose you could print the output the terminal beside the browser:

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

    If you have more than one request you will notice it will write faster in the terminal, not bad but you will also notice that if the client closed the connection/browser you will still see data going out.

    A better way could execute the command within a context, from the example: 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.
    }
    

    Also take a look at the context (https://stackoverflow.com/a/44146619/1135424) not replaces http.CloseNotifier so could be usefull for terminate the process once the client close browser, disconetcs.

    At the end depends on your needs but hope can give you an idea about how to stream data in an easy way by using the http.Flusher interface.

    Just for fun here is an example using the context:

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

    Still very basic, but in this case if client closes the browser the program also terminates, as an exercice could be nice to improve it an share back ;-), notice the use of CommandContext and 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
    ", 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
    ", data)
            fmt.Fprintf(w, "data: %s
    ", data)
            flusher.Flush()
        }
        input.Close()
    }
    
    func main() {
        router := violetear.New()
        router.HandleFunc("/", stream, "GET")
        log.Fatal(http.ListenAndServe(":8080", router))
    }
    
    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论

报告相同问题?

悬赏问题

  • ¥15 fluent的在模拟压强时使用希望得到一些建议
  • ¥15 STM32驱动继电器
  • ¥15 Windows server update services
  • ¥15 关于#c语言#的问题:我现在在做一个墨水屏设计,2.9英寸的小屏怎么换4.2英寸大屏
  • ¥15 模糊pid与pid仿真结果几乎一样
  • ¥15 java的GUI的运用
  • ¥15 Web.config连不上数据库
  • ¥15 我想付费需要AKM公司DSP开发资料及相关开发。
  • ¥15 怎么配置广告联盟瀑布流
  • ¥15 Rstudio 保存代码闪退