mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-20 21:21:44 +01:00
c6a8797ddd
This improves the ephemeral command runner to allow for honoring of timeouts and proper handling of overriding the current working directory. It also fixes some partially transparent font colors in light mode, making them solid instead. It also updates the InputModel to be auto-observable and utilize some getters to ensure the cmdinput is getting updated whenever necessary state changes take place.
175 lines
5.3 KiB
Go
175 lines
5.3 KiB
Go
// Copyright 2023, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// Provides a mechanism for writing data to a buffer and reading it later. The buffer is stored in a map of buffered pipes, which are removed after a certain amount of time. The output of a buffered pipe can be read by sending a GET request to a specific URL.
|
|
package bufferedpipe
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/wavetermdev/waveterm/waveshell/pkg/wlog"
|
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbase"
|
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/waveenc"
|
|
)
|
|
|
|
const (
|
|
BufferedPipeMapTTL = 30 * time.Second // The time-to-live for a buffered pipe in the map of buffered pipes.
|
|
BufferedPipeGetterUrl = "/api/buffered-pipe" // The URL for getting the output of a buffered pipe.
|
|
)
|
|
|
|
// A pipe that allows for lazy writing to a downstream writer. Data written to the pipe is buffered until WriteTo is called.
|
|
type BufferedPipe struct {
|
|
Key string // a unique key for the pipe
|
|
buffer bytes.Buffer // buffer of data to be written to the downstream writer once it is ready
|
|
closed atomic.Bool // whether the pipe has been closed
|
|
bufferDataCond *sync.Cond // Condition variable to signal waiting writers that there is either data to write or the pipe has been closed
|
|
downstreamLock *sync.Mutex // Lock to ensure that only one goroutine can read from the buffer at a time
|
|
}
|
|
|
|
// Create a new BufferedPipe with a timeout. The writer will be closed after the timeout
|
|
func NewBufferedPipe(timeout time.Duration) *BufferedPipe {
|
|
newPipe := &BufferedPipe{
|
|
Key: uuid.New().String(),
|
|
buffer: bytes.Buffer{},
|
|
closed: atomic.Bool{},
|
|
bufferDataCond: &sync.Cond{L: &sync.Mutex{}},
|
|
downstreamLock: &sync.Mutex{},
|
|
}
|
|
SetBufferedPipe(newPipe)
|
|
time.AfterFunc(timeout, func() {
|
|
newPipe.Close()
|
|
})
|
|
return newPipe
|
|
}
|
|
|
|
// Get the URL for reading the output of the pipe.
|
|
func (pipe *BufferedPipe) GetOutputUrl() (string, error) {
|
|
qvals := make(url.Values)
|
|
qvals.Set("key", pipe.Key)
|
|
qvals.Set("nonce", uuid.New().String())
|
|
hmacStr, err := waveenc.ComputeUrlHmac([]byte(scbase.WaveAuthKey), BufferedPipeGetterUrl, qvals)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
qvals.Set("hmac", hmacStr)
|
|
return BufferedPipeGetterUrl + "?" + qvals.Encode(), nil
|
|
}
|
|
|
|
// Write data to the buffer.
|
|
func (pipe *BufferedPipe) Write(p []byte) (n int, err error) {
|
|
if pipe.closed.Load() {
|
|
return 0, io.ErrClosedPipe
|
|
}
|
|
|
|
defer func() {
|
|
pipe.bufferDataCond.L.Unlock()
|
|
pipe.bufferDataCond.Broadcast()
|
|
}()
|
|
pipe.bufferDataCond.L.Lock()
|
|
|
|
return pipe.buffer.Write(p)
|
|
}
|
|
|
|
// Write all buffered data to a waiting writer and block, sending all subsequent data until the pipe is closed. Only one goroutine should call this method.
|
|
func (pipe *BufferedPipe) WriteTo(w io.Writer) (n int64, err error) {
|
|
// Lock the buffer to ensure that only one downstream writer can read from it at a time.
|
|
if !pipe.downstreamLock.TryLock() {
|
|
return 0, io.ErrClosedPipe
|
|
}
|
|
|
|
defer func() {
|
|
pipe.bufferDataCond.L.Unlock()
|
|
pipe.downstreamLock.Unlock()
|
|
}()
|
|
pipe.bufferDataCond.L.Lock()
|
|
for {
|
|
n1, err := pipe.buffer.WriteTo(w)
|
|
if err != nil {
|
|
return n, err
|
|
}
|
|
n += n1
|
|
|
|
// Check if the pipe has been closed. If it has, we don't need to wait for more data.
|
|
if pipe.closed.Load() {
|
|
break
|
|
}
|
|
|
|
// Wait for more data to be written to the buffer or for the pipe to be closed.
|
|
pipe.bufferDataCond.Wait()
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
// Close the pipe. This will cause any blocking WriteTo calls to return.
|
|
func (pipe *BufferedPipe) Close() error {
|
|
wlog.Logf("closing buffered pipe %s", pipe.Key)
|
|
defer pipe.bufferDataCond.Broadcast()
|
|
pipe.closed.Store(true)
|
|
return nil
|
|
}
|
|
|
|
// Ensure that BufferedPipe implements the io.WriteCloser and io.WriterTo interfaces.
|
|
var _ io.WriteCloser = (*BufferedPipe)(nil)
|
|
var _ io.WriterTo = (*BufferedPipe)(nil)
|
|
|
|
type BufferedPipeMap struct {
|
|
_map map[string]*BufferedPipe
|
|
lock sync.Mutex
|
|
}
|
|
|
|
// A global map of registered buffered pipes.
|
|
var bufferedPipes = BufferedPipeMap{_map: make(map[string]*BufferedPipe)}
|
|
|
|
// Get a buffered pipe from the map of buffered pipes, given a key.
|
|
func GetBufferedPipe(key string) (*BufferedPipe, bool) {
|
|
bufferedPipes.lock.Lock()
|
|
defer bufferedPipes.lock.Unlock()
|
|
|
|
ewc, ok := bufferedPipes._map[key]
|
|
return ewc, ok
|
|
}
|
|
|
|
// Set a buffered pipe in the map of buffered pipes.
|
|
func SetBufferedPipe(pipe *BufferedPipe) {
|
|
bufferedPipes.lock.Lock()
|
|
defer bufferedPipes.lock.Unlock()
|
|
key := pipe.Key
|
|
bufferedPipes._map[key] = pipe
|
|
|
|
// Remove the buffered pipe after a certain amount of time
|
|
time.AfterFunc(BufferedPipeMapTTL, func() {
|
|
bufferedPipes.lock.Lock()
|
|
defer bufferedPipes.lock.Unlock()
|
|
pipe.Close()
|
|
log.Printf("removing buffered pipe %s", key)
|
|
delete(bufferedPipes._map, key)
|
|
})
|
|
}
|
|
|
|
// Handle a HTTP GET request to get the output of a buffered pipe, given a key.
|
|
func HandleGetBufferedPipeOutput(w http.ResponseWriter, r *http.Request) {
|
|
qvals := r.URL.Query()
|
|
key := qvals.Get("key")
|
|
pipe, ok := GetBufferedPipe(key)
|
|
if !ok {
|
|
http.Error(w, "buffered pipe not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
_, err := pipe.WriteTo(w)
|
|
if err != nil {
|
|
http.Error(w, "error writing from buffer", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|