mirror of
https://github.com/go-sylixos/elvish.git
synced 2024-12-14 11:08:13 +08:00
be3431bbbf
Instead of a separate ShouldRun method, use a special error value to signify that a Program should not be run.
184 lines
4.3 KiB
Go
184 lines
4.3 KiB
Go
// Package web is the entry point for the backend of the web interface of
|
|
// Elvish.
|
|
package web
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
|
|
"src.elv.sh/pkg/eval"
|
|
"src.elv.sh/pkg/parse"
|
|
"src.elv.sh/pkg/prog"
|
|
"src.elv.sh/pkg/shell"
|
|
)
|
|
|
|
// Program is the web subprogram.
|
|
var Program prog.Program = program{}
|
|
|
|
type program struct{}
|
|
|
|
func (program) Run(fds [3]*os.File, f *prog.Flags, args []string) error {
|
|
if !f.Web {
|
|
return prog.ErrNotSuitable
|
|
}
|
|
if len(args) > 0 {
|
|
return prog.BadUsage("arguments are not allowed with -web")
|
|
}
|
|
if f.CodeInArg {
|
|
return prog.BadUsage("-c cannot be used together with -web")
|
|
}
|
|
p := Web{Port: f.Port}
|
|
return p.Main(fds, nil)
|
|
}
|
|
|
|
type Web struct {
|
|
Port int
|
|
}
|
|
|
|
type httpHandler struct {
|
|
ev *eval.Evaler
|
|
}
|
|
|
|
type ExecuteResponse struct {
|
|
OutBytes string
|
|
OutValues []interface{}
|
|
ErrBytes string
|
|
Err string
|
|
}
|
|
|
|
func (web *Web) Main(fds [3]*os.File, _ []string) error {
|
|
restore := shell.IncSHLVL()
|
|
defer restore()
|
|
ev := shell.MakeEvaler(fds[2])
|
|
|
|
h := httpHandler{ev}
|
|
|
|
http.HandleFunc("/", h.handleMainPage)
|
|
http.HandleFunc("/execute", h.handleExecute)
|
|
addr := fmt.Sprintf("localhost:%d", web.Port)
|
|
log.Println("going to listen", addr)
|
|
err := http.ListenAndServe(addr, nil)
|
|
|
|
log.Println(err)
|
|
return nil
|
|
}
|
|
|
|
func (h httpHandler) handleMainPage(w http.ResponseWriter, r *http.Request) {
|
|
_, err := w.Write([]byte(mainPageHTML))
|
|
if err != nil {
|
|
log.Println("cannot write response:", err)
|
|
}
|
|
}
|
|
|
|
func (h httpHandler) handleExecute(w http.ResponseWriter, r *http.Request) {
|
|
bytes, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
log.Println("cannot read request body:", err)
|
|
return
|
|
}
|
|
code := string(bytes)
|
|
|
|
outBytes, outValues, errBytes, err := evalAndCollect(h.ev, code)
|
|
errText := ""
|
|
if err != nil {
|
|
errText = err.Error()
|
|
}
|
|
responseBody, err := json.Marshal(
|
|
&ExecuteResponse{string(outBytes), outValues, string(errBytes), errText})
|
|
if err != nil {
|
|
log.Println("cannot marshal response body:", err)
|
|
}
|
|
|
|
_, err = w.Write(responseBody)
|
|
if err != nil {
|
|
log.Println("cannot write response:", err)
|
|
}
|
|
}
|
|
|
|
const (
|
|
outFileBufferSize = 1024
|
|
outChanBufferSize = 32
|
|
)
|
|
|
|
// evalAndCollect evaluates a piece of code with null stdin, and stdout and
|
|
// stderr connected to pipes (value part of stderr being a blackhole), and
|
|
// return the results collected on stdout and stderr, and the possible error
|
|
// that occurred.
|
|
func evalAndCollect(ev *eval.Evaler, code string) (
|
|
outBytes []byte, outValues []interface{}, errBytes []byte, err error) {
|
|
|
|
outFile, chanOutBytes := makeBytesWriterAndCollect()
|
|
outChan, chanOutValues := makeValuesWriterAndCollect()
|
|
errFile, chanErrBytes := makeBytesWriterAndCollect()
|
|
|
|
ports := []*eval.Port{
|
|
eval.DummyInputPort,
|
|
{File: outFile, Chan: outChan},
|
|
{File: errFile, Chan: eval.BlackholeChan},
|
|
}
|
|
err = ev.Eval(
|
|
parse.Source{Name: "[web]", Code: code}, eval.EvalCfg{Ports: ports})
|
|
|
|
outFile.Close()
|
|
close(outChan)
|
|
errFile.Close()
|
|
return <-chanOutBytes, <-chanOutValues, <-chanErrBytes, err
|
|
}
|
|
|
|
// makeBytesWriterAndCollect makes an in-memory file that can be written to, and
|
|
// the written bytes will be collected in a byte slice that will be put on a
|
|
// channel as soon as the writer is closed.
|
|
func makeBytesWriterAndCollect() (*os.File, <-chan []byte) {
|
|
r, w, err := os.Pipe()
|
|
// os.Pipe returns error only on resource exhaustion.
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
chanCollected := make(chan []byte)
|
|
|
|
go func() {
|
|
var (
|
|
collected []byte
|
|
buf [outFileBufferSize]byte
|
|
)
|
|
for {
|
|
n, err := r.Read(buf[:])
|
|
collected = append(collected, buf[:n]...)
|
|
if err != nil {
|
|
if err != io.EOF {
|
|
log.Println("error when reading output pipe:", err)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
r.Close()
|
|
chanCollected <- collected
|
|
}()
|
|
|
|
return w, chanCollected
|
|
}
|
|
|
|
// makeValuesWriterAndCollect makes a Value channel for writing, and the written
|
|
// values will be collected in a Value slice that will be put on a channel as
|
|
// soon as the writer is closed.
|
|
func makeValuesWriterAndCollect() (chan interface{}, <-chan []interface{}) {
|
|
chanValues := make(chan interface{}, outChanBufferSize)
|
|
chanCollected := make(chan []interface{})
|
|
|
|
go func() {
|
|
var collected []interface{}
|
|
for {
|
|
for v := range chanValues {
|
|
collected = append(collected, v)
|
|
}
|
|
chanCollected <- collected
|
|
}
|
|
}()
|
|
|
|
return chanValues, chanCollected
|
|
}
|