Получение данных из Stdout в режиме реального времени в Golang

В своей программе я хотел бы иметь возможность следить за выполнением некоторых консольных команд в режиме реального времени.

Вот два примера кода, который отображает данные в реальном времени в консоли, но на веб-странице данные отображаются не в реальном времени, а только после выполнения команды.

Пожалуйста, сообщите, как я могу получить данные в реальном времени на веб-странице?

В обоих примерах: Консоль отображает данные в режиме реального времени. На веб-странице данные отображаются не в реальном времени, а только после выполнения команды. Мне нужно отображать данные в режиме реального времени на веб-странице.

Пример 1

package main

import (
    "bytes"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/exec"
    "io"
)

func main() {
    http.HandleFunc("/", mainLogic)
    http.ListenAndServe(":8080", nil)
}

func mainLogic(w http.ResponseWriter, r *http.Request) {
    cmd := exec.Command("ping", "8.8.8.8", "-c5")
    var stdBuffer bytes.Buffer
    mw := io.MultiWriter(os.Stdout, &stdBuffer)
    cmd.Stdout = mw
    cmd.Stderr = mw
    if err := cmd.Run(); err != nil {
        log.Panic(err)
    }
    fmt.Fprintf(w, stdBuffer.String())
}

Пример 2

package main

import (
    "fmt"
    "log"
    "net/http"
    "os"
    "os/exec"
    "io"
    "sync"
)

func main() {
    http.HandleFunc("/", mainLogic)
    http.ListenAndServe(":8080", nil)
}

func mainLogic(w http.ResponseWriter, r *http.Request) {
    cmd := exec.Command("ping", "8.8.8.8", "-c5")
    var stdout []byte
    var errStdout, errStderr error
    stdoutIn, _ := cmd.StdoutPipe()
    stderrIn, _ := cmd.StderrPipe()
    err := cmd.Start()
    if err != nil {
        log.Fatalf("cmd.Start() failed with '%s'\n", err)
    }
    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        stdout, errStdout = copyAndCapture(os.Stdout, stdoutIn)
        wg.Done()
    }()
    _, errStderr = copyAndCapture(os.Stderr, stderrIn)
    wg.Wait()
    err = cmd.Wait()
    if err != nil {
        log.Fatalf("cmd.Run() failed with %s\n", err)
    }
    if errStdout != nil || errStderr != nil {
        log.Fatal("failed to capture stdout or stderr\n")
    }
    fmt.Fprintf(w, string(stdout))
}

func copyAndCapture(w io.Writer, r io.Reader) ([]byte, error) {
    var out []byte
    buf := make([]byte, 1024, 1024)
    for {
        n, err := r.Read(buf[:])
        if n > 0 {
            d := buf[:n]
            out = append(out, d...)
            _, err := w.Write(d)
            if err != nil {
                return out, err
            }
        }
        if err != nil {
            if err == io.EOF {
                err = nil
            }
            return out, err
        }
    }
}

Если ваша первая реализация работает, почему вторая совершенно другая? Почему бы не использовать тот же подход?

Adrian 30.03.2023 15:34

Возможно, вы неправильно поняли мою проблему. Может я не ясно описал. Обе версии кода работают одинаково. Консоль отображает данные в режиме реального времени. На веб-странице данные отображаются не в реальном времени, а только после выполнения команды. Мне нужно отображать данные в режиме реального времени на веб-странице.

WayMax 30.03.2023 15:37

Да, я понимаю проблему, но они работают совершенно по-разному. Первый, который ведет себя так, как хотелось бы, прост: настройте команду, установите ее стандартный вывод, запустите ее. Второй, который работает не так, как хотелось бы, совершенно другой и использует каналы, и горутины, и группы ожидания, и асинхронное выполнение команд. Почему?

Adrian 30.03.2023 15:38

Потому что я открыт для любого решения моей проблемы. Потому что я показываю, что пробовал разные варианты, и не требую от меня написания всего кода за меня с нуля.

WayMax 30.03.2023 15:41
Создание API ввода вопросов на разных языках программирования (Python, PHP, Go и Node.js)
Создание API ввода вопросов на разных языках программирования (Python, PHP, Go и Node.js)
API ввода вопросов - это полезный инструмент для интеграции моделей машинного обучения, таких как ChatGPT, в приложения, требующие обработки...
3
4
92
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Ответы HTTP по умолчанию буферизуются из соображений производительности, поэтому вам нужно явно вызвать Flush, чтобы отправить данные немедленно.

Если вы хотите сделать это из модуля записи, назначенного для выходов exec.Cmd, вы можете обернуть http.ResponseWriter в тип, который будет вызывать Flush для вас на каждом Write.

type flushWriter interface {
    io.Writer
    http.Flusher
}

type flusher struct {
    w flushWriter
}

func (f *flusher) Write(d []byte) (int, error) {
    n, err := f.w.Write(d)
    if err != nil {
        return n, err
    }
    f.w.Flush()
    return n, nil
}

...

if w, ok := w.(flushWriter); ok {
    cmd.Stdout = &flusher{w}
    cmd.Stderr = &flusher{w}
}

Если вам нужна копия данных, вы можете использовать io.MultiWriter для захвата вывода во время его записи. Здесь мы также рассмотрим случай, когда http.ResponseWriter не является http.Flusher, но это может произойти только в том случае, если вы уже завернуты через какое-то другое промежуточное ПО.

var mw io.Writer
var buff bytes.Buffer
if w, ok := w.(flushWriter); ok {
    mw = io.MultiWriter(&buff, &flusher{w})
} else {
    mw = io.MultiWriter(&buff, w)
}
cmd.Stdout = mw
cmd.Stderr = mw

Большое спасибо. Мне это кажется магией, но это работает.

WayMax 31.03.2023 08:16

Извините, можно еще вопрос? Возможно ли в Go подписаться на событие получения новых данных в cmd.Stdout? Я хотел бы проанализировать данные, полученные от cmd.Stdout, и выполнить различные действия на основе анализа.

WayMax 31.03.2023 12:16

@WayMax, «подписки» нет, команда вызывает Write все, что назначено Stdout. Если вы хотите скопировать данные, вы можете просто использовать io.MultiWriter, как вы это делали изначально.

JimB 31.03.2023 14:46

Другие вопросы по теме