github.com/ladydascalie/elvish@v0.0.0-20170703214355-2964dd3ece7f/web/web.go (about)

     1  // Package web is the entry point for the backend of the web interface of
     2  // Elvish.
     3  package web
     4  
     5  //go:generate ./embed-html
     6  
     7  import (
     8  	"encoding/json"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"log"
    13  	"net/http"
    14  	"os"
    15  
    16  	"github.com/elves/elvish/eval"
    17  	"github.com/elves/elvish/parse"
    18  )
    19  
    20  type Web struct {
    21  	ev   *eval.Evaler
    22  	port int
    23  }
    24  
    25  type ExecuteResponse struct {
    26  	OutBytes  string
    27  	OutValues []eval.Value
    28  	ErrBytes  string
    29  	Err       string
    30  }
    31  
    32  func NewWeb(ev *eval.Evaler, port int) *Web {
    33  	return &Web{ev, port}
    34  }
    35  
    36  func (web *Web) Run(args []string) int {
    37  	if len(args) > 0 {
    38  		fmt.Fprintln(os.Stderr, "arguments to -web are not supported yet")
    39  		return 2
    40  	}
    41  
    42  	http.HandleFunc("/", web.handleMainPage)
    43  	http.HandleFunc("/execute", web.handleExecute)
    44  	addr := fmt.Sprintf("localhost:%d", web.port)
    45  	log.Println("going to listen", addr)
    46  	err := http.ListenAndServe(addr, nil)
    47  
    48  	log.Println(err)
    49  	return 0
    50  }
    51  
    52  func (web *Web) handleMainPage(w http.ResponseWriter, r *http.Request) {
    53  	_, err := w.Write([]byte(mainPageHTML))
    54  	if err != nil {
    55  		log.Println("cannot write response:", err)
    56  	}
    57  }
    58  
    59  func (web *Web) handleExecute(w http.ResponseWriter, r *http.Request) {
    60  	bytes, err := ioutil.ReadAll(r.Body)
    61  	if err != nil {
    62  		log.Println("cannot read request body:", err)
    63  		return
    64  	}
    65  	text := string(bytes)
    66  
    67  	outBytes, outValues, errBytes, err := evalAndCollect(web.ev, "<web>", text)
    68  	errText := ""
    69  	if err != nil {
    70  		errText = err.Error()
    71  	}
    72  	responseBody, err := json.Marshal(
    73  		&ExecuteResponse{string(outBytes), outValues, string(errBytes), errText})
    74  	if err != nil {
    75  		log.Println("cannot marshal response body:", err)
    76  	}
    77  
    78  	_, err = w.Write(responseBody)
    79  	if err != nil {
    80  		log.Println("cannot write response:", err)
    81  	}
    82  }
    83  
    84  const (
    85  	outFileBufferSize = 1024
    86  	outChanBufferSize = 32
    87  )
    88  
    89  // evalAndCollect evaluates a piece of code with null stdin, and stdout and
    90  // stderr connected to pipes (value part of stderr being a blackhole), and
    91  // return the results collected on stdout and stderr, and the possible error
    92  // that occurred.
    93  func evalAndCollect(ev *eval.Evaler, name, text string) (
    94  	outBytes []byte, outValues []eval.Value, errBytes []byte, err error) {
    95  
    96  	node, err := parse.Parse(name, text)
    97  	if err != nil {
    98  		return
    99  	}
   100  	op, err := ev.Compile(node, name, text)
   101  	if err != nil {
   102  		return
   103  	}
   104  
   105  	outFile, chanOutBytes := makeBytesWriterAndCollect()
   106  	outChan, chanOutValues := makeValuesWriterAndCollect()
   107  	errFile, chanErrBytes := makeBytesWriterAndCollect()
   108  
   109  	ports := []*eval.Port{
   110  		eval.DevNullClosedChan,
   111  		{File: outFile, Chan: outChan},
   112  		{File: errFile, Chan: eval.BlackholeChan},
   113  	}
   114  	err = ev.EvalWithPorts(ports, op, name, text)
   115  
   116  	outFile.Close()
   117  	close(outChan)
   118  	errFile.Close()
   119  	return <-chanOutBytes, <-chanOutValues, <-chanErrBytes, err
   120  }
   121  
   122  // makeBytesWriterAndCollect makes an in-memory file that can be written to, and
   123  // the written bytes will be collected in a byte slice that will be put on a
   124  // channel as soon as the writer is closed.
   125  func makeBytesWriterAndCollect() (*os.File, <-chan []byte) {
   126  	r, w, err := os.Pipe()
   127  	// os.Pipe returns error only on resource exhaustion.
   128  	if err != nil {
   129  		panic(err)
   130  	}
   131  	chanCollected := make(chan []byte)
   132  
   133  	go func() {
   134  		var (
   135  			collected []byte
   136  			buf       [outFileBufferSize]byte
   137  		)
   138  		for {
   139  			n, err := r.Read(buf[:])
   140  			collected = append(collected, buf[:n]...)
   141  			if err != nil {
   142  				if err != io.EOF {
   143  					log.Println("error when reading output pipe:", err)
   144  				}
   145  				break
   146  			}
   147  		}
   148  		r.Close()
   149  		chanCollected <- collected
   150  	}()
   151  
   152  	return w, chanCollected
   153  }
   154  
   155  // makeValuesWriterAndCollect makes a Value channel for writing, and the written
   156  // values will be collected in a Value slice that will be put on a channel as
   157  // soon as the writer is closed.
   158  func makeValuesWriterAndCollect() (chan eval.Value, <-chan []eval.Value) {
   159  	chanValues := make(chan eval.Value, outChanBufferSize)
   160  	chanCollected := make(chan []eval.Value)
   161  
   162  	go func() {
   163  		var collected []eval.Value
   164  		for {
   165  			for v := range chanValues {
   166  				collected = append(collected, v)
   167  			}
   168  			chanCollected <- collected
   169  		}
   170  	}()
   171  
   172  	return chanValues, chanCollected
   173  }