From a9572e6613ef5bd8af175f9a5fbf82fe85f15d00 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 1 May 2023 15:50:21 +0200 Subject: [PATCH] phpx/server: improve wire format This commit updates the wire format of the phpx server. Instead of being string-based, messages sent back and forth between go and php are now base64-encoded DEFLATEd strings. This makes them a lot smaller and faster to send. --- internal/phpx/errors.go | 1 + internal/phpx/server.go | 93 +++++++++++++------ internal/phpx/server.php | 22 +++-- .../ingredient/php/extras/pathbuilder.go | 2 +- 4 files changed, 80 insertions(+), 38 deletions(-) diff --git a/internal/phpx/errors.go b/internal/phpx/errors.go index b600f82..af8559e 100644 --- a/internal/phpx/errors.go +++ b/internal/phpx/errors.go @@ -6,6 +6,7 @@ import "fmt" const ( errInit = "Server initialization failed" errClosed = "Server closed" + errSend = "Failed to encode request" errReceive = "Failed to decode response" ) diff --git a/internal/phpx/server.go b/internal/phpx/server.go index da311f4..567089d 100644 --- a/internal/phpx/server.go +++ b/internal/phpx/server.go @@ -1,7 +1,10 @@ package phpx import ( + "bytes" + "compress/flate" "context" + "encoding/base64" "encoding/json" "io" "os" @@ -102,10 +105,6 @@ func (server *Server) MarshalEval(ctx context.Context, value any, code string) e return err } - // input to be sent to the server - delim := findDelimiter(code) - input := delim + "\n" + code + "\n" + delim + "\n" - server.m.Lock() defer server.m.Unlock() @@ -114,9 +113,13 @@ func (server *Server) MarshalEval(ctx context.Context, value any, code string) e return ServerError{Message: errClosed} } - // find a delimiter for the code, and then send - io.WriteString(server.in, input) + // encode a message to the server! + if err := server.encode(server.in, code); err != nil { + server.cancel() + return ServerError{Message: errSend, Err: err} + } + // read the response data, err, _ := contextx.Run2(ctx, func(start func()) (string, error) { return nobufio.ReadLine(server.out) }, func() { @@ -126,9 +129,9 @@ func (server *Server) MarshalEval(ctx context.Context, value any, code string) e return ServerError{Message: errReceive, Err: err} } - // read whatever we received + // decode the response var received [2]json.RawMessage - if err := json.Unmarshal([]byte(data), &received); err != nil { + if err := server.decode(&received, []byte(data)); err != nil { return ServerError{Message: errReceive, Err: err} } @@ -147,6 +150,59 @@ func (server *Server) MarshalEval(ctx context.Context, value any, code string) e return json.Unmarshal(received[0], value) } +// Decode decodes a message received from the server. +// The message is assumed to be encoded by server.php. +// +// This function does the following: +// - decode base64 (opposite of php's "base64_encode") +// - inflate (opposite of php's "gzdeflate") +// - decode json (opposite of php's "json_encode") +func (*Server) decode(dest *[2]json.RawMessage, message []byte) error { + // decode base64 + raw := base64.NewDecoder(base64.StdEncoding, bytes.NewReader(message)) + + // unpack gzip + unpacker := flate.NewReader(raw) + defer unpacker.Close() + + // and read the value + decoder := json.NewDecoder(unpacker) + return decoder.Decode(dest) +} + +// Encode encodes and writes a message for the server into dest. +// The message is assumed to be received by server.php. +// +// This function does the following: +// - inflate (opposite of php's "gzdeflate") +// - encode base64 (opposite of php's "base64_decode") +func (*Server) encode(dest io.WriteCloser, code string) (err error) { + + // write a final newline at the end! + defer func() { + if err != nil { + return + } + _, err = dest.Write([]byte("\n")) + }() + + // base64 encode all the things! + encoder := base64.NewEncoder(base64.StdEncoding, dest) + defer encoder.Close() + + // compress all the things! + compressor, err := flate.NewWriter(encoder, 9) + if err != nil { + return err + } + defer compressor.Close() + + // do the write! + _, err = compressor.Write([]byte(code)) + + return +} + // Eval is like [MarshalEval], but returns the value as an any func (server *Server) Eval(ctx context.Context, code string) (value any, err error) { err = server.MarshalEval(ctx, &value, code) @@ -190,27 +246,6 @@ func (server *Server) Call(ctx context.Context, function string, args ...any) (v return } -const delimiterRune = 'F' // press to pay respect - -// findDelimiter finds a delimiter that does not occur in the input string -func findDelimiter(input string) string { - // find the longest sequence of delimiter rune - var current, longest int - for _, r := range input { - if r == delimiterRune { - current++ - } else { - current = 0 - } - - if current > longest { - longest = current - } - } - // and then return it multipled longer than that - return strings.Repeat(string(delimiterRune), longest+1) -} - // Close closes this server and prevents any further code from being run. func (server *Server) Close() error { server.prepare() diff --git a/internal/phpx/server.php b/internal/phpx/server.php index ef97454..613b7e6 100644 --- a/internal/phpx/server.php +++ b/internal/phpx/server.php @@ -8,6 +8,13 @@ // It should only contain comments at the beginning of each line, and only starting with '//'. // See server.go. +// This file runs an infinite REPL-like loop. +// It continously reads from standard input, decodes code to pass to eval(), and send the result back. +// +// Commands are read in DEFLATE base64 encoding (to prevent having to send too much over the wire). +// Results are written to STDOUT back in base64 DEFLATE encoding. +// The results are of the form [$result,$error] - $result being the actual object returned, and $error an error string. + // define a json_encode alias, this saves a single character! // (we also reuse it in the error string) $E = 'json_encode'; @@ -19,15 +26,11 @@ while(1) { // stop outputting errors when executing ob_start(null,0,PHP_OUTPUT_HANDLER_CLEANABLE); - // read the next line to get an end-of-line marker - $m = fgets(STDIN) or exit(0); + // read the next line (which will be code to execute) + $b = fgets(STDIN) or exit(0); - // accumulate the buffer until the marker is reached - // bail out if there is an unexpected end of input - for($b = $l = ""; $l !== $m;) { - $b .= $l; - $l = fgets(STDIN) or exit(1); - } + // read the next command to parse + $b = gzinflate(base64_decode($b)); // execute the code, and json_encode it try { @@ -43,5 +46,8 @@ while(1) { // and write out the result ob_end_clean(); + + // deflate and base64_encode + $j = base64_encode(gzdeflate($j)); fwrite(STDOUT,"$j\n"); } \ No newline at end of file diff --git a/internal/wisski/ingredient/php/extras/pathbuilder.go b/internal/wisski/ingredient/php/extras/pathbuilder.go index 57b19d1..d4049e5 100644 --- a/internal/wisski/ingredient/php/extras/pathbuilder.go +++ b/internal/wisski/ingredient/php/extras/pathbuilder.go @@ -56,6 +56,6 @@ func (pathbuilder *Pathbuilder) Fetch(flags ingredient.FetcherFlags, info *statu return } - info.Pathbuilders, _ = pathbuilder.GetAll(flags.Context, flags.Server) + info.Pathbuilders, err = pathbuilder.GetAll(flags.Context, flags.Server) return }