Rewrite HTTP server without net/http

This cuts down the binary size from 5.7MB to 3.3MB.
This commit is contained in:
Junegunn Choi 2022-12-20 16:28:36 +09:00
parent 1ba7484d60
commit 4b055bf260
2 changed files with 113 additions and 36 deletions

112
src/server.go Normal file
View File

@ -0,0 +1,112 @@
package fzf
import (
"bufio"
"bytes"
"errors"
"fmt"
"net"
"strconv"
"strings"
)
const (
crlf = "\r\n"
httpOk = "HTTP/1.1 200 OK" + crlf
httpBadRequest = "HTTP/1.1 400 Bad Request" + crlf
)
func startHttpServer(port int, channel chan []*action) {
if port == 0 {
return
}
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
return
}
go func() {
for {
conn, err := listener.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) {
break
} else {
continue
}
}
conn.Write([]byte(handleHttpRequest(conn, channel)))
conn.Close()
}
listener.Close()
}()
}
// Here we are writing a simplistic HTTP server without using net/http
// package to reduce the size of the binary.
//
// * No --listen: 2.8MB
// * --listen with net/http: 5.7MB
// * --listen w/o net/http: 3.3MB
func handleHttpRequest(conn net.Conn, channel chan []*action) string {
line := 0
headerRead := false
contentLength := 0
body := ""
bad := func(message string) string {
message += "\n"
return httpBadRequest + fmt.Sprintf("Content-Length: %d%s", len(message), crlf+crlf+message)
}
scanner := bufio.NewScanner(conn)
scanner.Split(func(data []byte, atEOF bool) (int, []byte, error) {
found := bytes.Index(data, []byte(crlf))
if found >= 0 {
token := data[:found+len(crlf)]
return len(token), token, nil
}
if atEOF || len(body)+len(data) >= contentLength {
return 0, data, bufio.ErrFinalToken
}
return 0, nil, nil
})
for scanner.Scan() {
text := scanner.Text()
if line == 0 && !strings.HasPrefix(text, "POST / HTTP") {
return bad("invalid request method")
}
if text == crlf {
headerRead = true
}
if !headerRead {
pair := strings.SplitN(text, ":", 2)
if len(pair) == 2 && strings.ToLower(pair[0]) == "content-length" {
length, err := strconv.Atoi(strings.TrimSpace(pair[1]))
if err != nil {
return bad("invalid content length")
}
contentLength = length
}
} else if contentLength <= 0 {
break
} else {
body += text
}
line++
}
errorMessage := ""
actions := parseSingleActionList(strings.TrimSpace(string(body)), func(message string) {
errorMessage = message
})
if len(errorMessage) > 0 {
return bad(errorMessage)
}
if len(actions) == 0 {
return bad("no action specified")
}
channel <- actions
return httpOk
}

View File

@ -3,10 +3,8 @@ package fzf
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"math"
"net/http"
"os"
"os/signal"
"regexp"
@ -626,39 +624,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
return &t
}
func (t *Terminal) startServer() {
if t.listenPort == 0 {
return
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.WriteHeader(http.StatusBadRequest)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
response := ""
actions := parseSingleActionList(string(body), func(message string) {
response = message
})
if len(response) > 0 {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintln(w, response)
return
}
t.serverChan <- actions
})
go func() {
http.ListenAndServe(fmt.Sprintf(":%d", t.listenPort), nil)
}()
}
func borderLines(shape tui.BorderShape) int {
switch shape {
case tui.BorderHorizontal, tui.BorderRounded, tui.BorderSharp, tui.BorderBold, tui.BorderDouble:
@ -2535,7 +2500,7 @@ func (t *Terminal) Loop() {
looping := true
_, startEvent := t.keymap[tui.Start.AsEvent()]
t.startServer()
startHttpServer(t.listenPort, t.serverChan)
eventChan := make(chan tui.Event)
needBarrier := true
barrier := make(chan bool)