From 2e476269006778ea4ea69a4cfe6ce1fb0047bca8 Mon Sep 17 00:00:00 2001 From: Tom Wiesing Date: Wed, 19 Oct 2022 13:52:24 +0200 Subject: [PATCH] php: Move server code into new phpx package --- .../php/phpserver => phpx}/errors.go | 13 +- .../php/phpserver => phpx}/marshal.go | 2 +- internal/phpx/php.go | 18 +++ .../php/phpserver => phpx}/server.go | 132 +++++++++++------- .../php/phpserver => phpx}/server.php | 0 .../wisski/ingredient/barrel/drush/cron.go | 4 +- internal/wisski/ingredient/fetcher.go | 4 +- internal/wisski/ingredient/info/info.go | 4 +- .../ingredient/php/extras/pathbuilder.go | 7 +- .../wisski/ingredient/php/extras/prefixes.go | 5 +- .../wisski/ingredient/php/extras/settings.go | 5 +- internal/wisski/ingredient/php/php.go | 9 +- internal/wisski/ingredient/php/server.go | 17 ++- 13 files changed, 134 insertions(+), 86 deletions(-) rename internal/{wisski/ingredient/php/phpserver => phpx}/errors.go (67%) rename internal/{wisski/ingredient/php/phpserver => phpx}/marshal.go (97%) create mode 100644 internal/phpx/php.go rename internal/{wisski/ingredient/php/phpserver => phpx}/server.go (69%) rename internal/{wisski/ingredient/php/phpserver => phpx}/server.php (100%) diff --git a/internal/wisski/ingredient/php/phpserver/errors.go b/internal/phpx/errors.go similarity index 67% rename from internal/wisski/ingredient/php/phpserver/errors.go rename to internal/phpx/errors.go index 58304fc..b600f82 100644 --- a/internal/wisski/ingredient/php/phpserver/errors.go +++ b/internal/phpx/errors.go @@ -1,14 +1,12 @@ -package phpserver +package phpx import "fmt" // Common PHP Errors -var ( - errPHPInit = "Unable to initialize" - errPHPMarshal = "Marshal failed" - errPHPInvalid = ServerError{Message: "Invalid code to execute"} - errPHPReceive = "Failed to receive response" - errPHPClosed = ServerError{Message: "Server closed"} +const ( + errInit = "Server initialization failed" + errClosed = "Server closed" + errReceive = "Failed to decode response" ) // PHPError represents an error during PHPServer logic @@ -17,6 +15,7 @@ type ServerError struct { Err error } +// Unwrap returns the underlying error func (err ServerError) Unwrap() error { return err.Err } diff --git a/internal/wisski/ingredient/php/phpserver/marshal.go b/internal/phpx/marshal.go similarity index 97% rename from internal/wisski/ingredient/php/phpserver/marshal.go rename to internal/phpx/marshal.go index 1f321b5..f06bfd0 100644 --- a/internal/wisski/ingredient/php/phpserver/marshal.go +++ b/internal/phpx/marshal.go @@ -1,4 +1,4 @@ -package phpserver +package phpx import ( "encoding/json" diff --git a/internal/phpx/php.go b/internal/phpx/php.go new file mode 100644 index 0000000..a830fee --- /dev/null +++ b/internal/phpx/php.go @@ -0,0 +1,18 @@ +// Package phpx provides functionalities for interacting with PHP code +package phpx + +import "github.com/tkw1536/goprogram/stream" + +// Executor represents anything that can spawn +type Executor interface { + // Spawn spawns a new (independent) process executing code. + // It should return only once the execution terminates. + Spawn(str stream.IOStream, code string) error +} + +// SpawnFunc implements Executor +type SpawnFunc func(str stream.IOStream, code string) error + +func (sf SpawnFunc) Spawn(str stream.IOStream, code string) error { + return sf(str, code) +} diff --git a/internal/wisski/ingredient/php/phpserver/server.go b/internal/phpx/server.go similarity index 69% rename from internal/wisski/ingredient/php/phpserver/server.go rename to internal/phpx/server.go index 6ec271b..d0ac88f 100644 --- a/internal/wisski/ingredient/php/phpserver/server.go +++ b/internal/phpx/server.go @@ -1,4 +1,4 @@ -package phpserver +package phpx import ( "context" @@ -10,62 +10,78 @@ import ( _ "embed" + "github.com/FAU-CDI/wisski-distillery/pkg/lazy" "github.com/tkw1536/goprogram/lib/collection" "github.com/tkw1536/goprogram/lib/nobufio" "github.com/tkw1536/goprogram/stream" ) -// New creates a new server, with execPHP as a method to call a PHP Shell. -func New(execPHP func(str stream.IOStream, script string)) (*Server, error) { - // create input and output pipes - ir, iw, err := os.Pipe() - if err != nil { - return nil, ServerError{errPHPInit, err} - } - or, ow, err := os.Pipe() - if err != nil { - ir.Close() - iw.Close() - return nil, ServerError{errPHPInit, err} - } - - // create a context to close the server - context, cancel := context.WithCancel(context.Background()) - - // start the shell process, which will close everything once done - go func() { - defer func() { - ir.Close() - iw.Close() - or.Close() - ow.Close() - - cancel() - }() - - // start the server - io := stream.NewIOStream(ow, nil, ir, 0) - execPHP(io, serverPHP) - }() - - // return a new server - return &Server{ - in: iw, - out: or, - c: context, - }, nil -} - -// Server represents a server that executes code within a distillery. +// Server represents a server that executes PHP code. // A typical use-case is to define functions using [MarshalEval], and then call those functions [MarshalCall]. // -// A nil Server will return [ErrServerBroken] on every function call. +// A server, once used, should be closed using the [Close] method. type Server struct { - m sync.Mutex + // Executor is the executor used by this server. + // It may not be modified concurrently with other processes. + Executor Executor + // prepares the server + init sync.Once + err lazy.Lazy[error] + + // input / output for underlying executor in io.WriteCloser out io.Reader - c context.Context + + m sync.Mutex // prevents concurrent access on any of the methods + + c context.Context // closed when server is finished + +} + +func (server *Server) prepare() error { + server.init.Do(func() { + // create input and output pipes + ir, iw, err := os.Pipe() + if err != nil { + server.err.Set(ServerError{errInit, err}) + return + } + or, ow, err := os.Pipe() + if err != nil { + ir.Close() + iw.Close() + + server.err.Set(ServerError{errInit, err}) + return + } + + // create a context to close the server + context, cancel := context.WithCancel(context.Background()) + + // start the shell process, which will close everything once done + go func() { + defer func() { + ir.Close() + iw.Close() + or.Close() + ow.Close() + + cancel() + }() + + // start the server + io := stream.NewIOStream(ow, nil, ir, 0) + err := server.Executor.Spawn(io, serverPHP) + server.err.Set(ServerError{errClosed, err}) + }() + + server.in = iw + server.out = or + server.c = context + }) + + return server.err.Get(nil) } // MarshalEval evaluates code on the server and Marshals the result into value. @@ -76,28 +92,35 @@ type Server struct { // // When an exception is thrown by the PHP Code, error is not nil, and dest remains unchanged. func (server *Server) MarshalEval(value any, code string) error { + if err := server.prepare(); err != nil { + 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() - // quick hack: when the server is already done + // when the server is already done if err := server.c.Err(); err != nil { - return errPHPClosed + return ServerError{Message: errClosed} } // find a delimiter for the code, and then send - delim := findDelimiter(code) - io.WriteString(server.in, delim+"\n"+code+"\n"+delim+"\n") + io.WriteString(server.in, input) // read the next line (as a response) data, err := nobufio.ReadLine(server.out) if err != nil { - return ServerError{Message: errPHPReceive, Err: err} + return ServerError{Message: errReceive, Err: err} } // read whatever we received var received [2]json.RawMessage if err := json.Unmarshal([]byte(data), &received); err != nil { - return ServerError{Message: errPHPMarshal, Err: err} + return ServerError{Message: errReceive, Err: err} } // check if there was an error @@ -126,7 +149,6 @@ func (server *Server) Eval(code string) (value any, err error) { // // Return values are received as in [MarshalEval]. func (server *Server) MarshalCall(value any, function string, args ...any) error { - // name of function to call name := MarshalString(function) @@ -182,12 +204,14 @@ func findDelimiter(input string) string { // Close closes this server and prevents any further code from being run. func (server *Server) Close() error { + server.prepare() + server.m.Lock() defer server.m.Unlock() // if the context is already closed if err := server.c.Err(); err != nil { - return errPHPClosed + return ServerError{Message: errClosed} } server.in.Close() diff --git a/internal/wisski/ingredient/php/phpserver/server.php b/internal/phpx/server.php similarity index 100% rename from internal/wisski/ingredient/php/phpserver/server.php rename to internal/phpx/server.php diff --git a/internal/wisski/ingredient/barrel/drush/cron.go b/internal/wisski/ingredient/barrel/drush/cron.go index 9482788..cd208b2 100644 --- a/internal/wisski/ingredient/barrel/drush/cron.go +++ b/internal/wisski/ingredient/barrel/drush/cron.go @@ -3,8 +3,8 @@ package drush import ( "time" + "github.com/FAU-CDI/wisski-distillery/internal/phpx" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient" - "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php" "github.com/tkw1536/goprogram/exit" "github.com/tkw1536/goprogram/stream" ) @@ -28,7 +28,7 @@ func (drush *Drush) Cron(io stream.IOStream) error { return nil } -func (drush *Drush) LastCron(server *php.Server) (t time.Time, err error) { +func (drush *Drush) LastCron(server *phpx.Server) (t time.Time, err error) { var timestamp int64 err = drush.PHP.EvalCode(server, ×tamp, `$val = \Drupal::state()->get('system.cron_last'); return $val; `) if err != nil { diff --git a/internal/wisski/ingredient/fetcher.go b/internal/wisski/ingredient/fetcher.go index 0b73f63..67166b7 100644 --- a/internal/wisski/ingredient/fetcher.go +++ b/internal/wisski/ingredient/fetcher.go @@ -4,7 +4,7 @@ import ( "time" "github.com/FAU-CDI/wisski-distillery/internal/models" - "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php/phpserver" + "github.com/FAU-CDI/wisski-distillery/internal/phpx" ) // Fetcher is an ingredient with a fetch method @@ -19,7 +19,7 @@ type Fetcher interface { // FetchFlags specifies what information to fetch type FetchFlags struct { Quick bool - Server *phpserver.Server + Server *phpx.Server } // Information represents fetched information about a WissKI diff --git a/internal/wisski/ingredient/info/info.go b/internal/wisski/ingredient/info/info.go index 483b151..22d8687 100644 --- a/internal/wisski/ingredient/info/info.go +++ b/internal/wisski/ingredient/info/info.go @@ -38,9 +38,9 @@ func (wisski *Info) Information(quick bool) (info WissKIInfo, err error) { // potentially setup a new server if !flags.Quick { - server, err := wisski.PHP.NewServer() + flags.Server = wisski.PHP.NewServer() if err == nil { - defer server.Close() + defer flags.Server.Close() } } diff --git a/internal/wisski/ingredient/php/extras/pathbuilder.go b/internal/wisski/ingredient/php/extras/pathbuilder.go index a2bf782..8051e20 100644 --- a/internal/wisski/ingredient/php/extras/pathbuilder.go +++ b/internal/wisski/ingredient/php/extras/pathbuilder.go @@ -3,6 +3,7 @@ package extras import ( _ "embed" + "github.com/FAU-CDI/wisski-distillery/internal/phpx" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php" "golang.org/x/exp/slices" @@ -20,7 +21,7 @@ var pathbuilderPHP string // All returns the ids of all pathbuilders in consistent order. // // server is the server to fetch the pathbuilders from, any may be nil. -func (pathbuilder *Pathbuilder) All(server *php.Server) (ids []string, err error) { +func (pathbuilder *Pathbuilder) All(server *phpx.Server) (ids []string, err error) { err = pathbuilder.PHP.ExecScript(server, &ids, pathbuilderPHP, "all_list") slices.Sort(ids) return @@ -30,7 +31,7 @@ func (pathbuilder *Pathbuilder) All(server *php.Server) (ids []string, err error // If it does not exist, it returns the empty string and nil error. // // server is the server to fetch the pathbuilders from, any may be nil. -func (pathbuilder *Pathbuilder) Get(server *php.Server, id string) (xml string, err error) { +func (pathbuilder *Pathbuilder) Get(server *phpx.Server, id string) (xml string, err error) { err = pathbuilder.PHP.ExecScript(server, &xml, pathbuilderPHP, "one_xml", id) return } @@ -38,7 +39,7 @@ func (pathbuilder *Pathbuilder) Get(server *php.Server, id string) (xml string, // GetAll returns all pathbuilders serialized as xml // // server is the server to fetch the pathbuilders from, any may be nil. -func (pathbuilder *Pathbuilder) GetAll(server *php.Server) (pathbuilders map[string]string, err error) { +func (pathbuilder *Pathbuilder) GetAll(server *phpx.Server) (pathbuilders map[string]string, err error) { err = pathbuilder.PHP.ExecScript(server, &pathbuilders, pathbuilderPHP, "all_xml") return } diff --git a/internal/wisski/ingredient/php/extras/prefixes.go b/internal/wisski/ingredient/php/extras/prefixes.go index 6ab5511..33608a3 100644 --- a/internal/wisski/ingredient/php/extras/prefixes.go +++ b/internal/wisski/ingredient/php/extras/prefixes.go @@ -5,6 +5,7 @@ import ( "path/filepath" "strings" + "github.com/FAU-CDI/wisski-distillery/internal/phpx" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/mstore" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php" @@ -35,7 +36,7 @@ var listURIPrefixesPHP string // // server is an optional server to fetch prefixes from. // server may be nil. -func (prefixes *Prefixes) All(server *php.Server) ([]string, error) { +func (prefixes *Prefixes) All(server *phpx.Server) ([]string, error) { uris, err := prefixes.database(server) if err != nil { return nil, err @@ -49,7 +50,7 @@ func (prefixes *Prefixes) All(server *php.Server) ([]string, error) { return append(uris, uris2...), nil } -func (wisski *Prefixes) database(server *php.Server) (prefixes []string, err error) { +func (wisski *Prefixes) database(server *phpx.Server) (prefixes []string, err error) { // get all the ugly prefixes err = wisski.PHP.ExecScript(server, &prefixes, listURIPrefixesPHP, "list_prefixes") if err != nil { diff --git a/internal/wisski/ingredient/php/extras/settings.go b/internal/wisski/ingredient/php/extras/settings.go index f390690..168d709 100644 --- a/internal/wisski/ingredient/php/extras/settings.go +++ b/internal/wisski/ingredient/php/extras/settings.go @@ -3,6 +3,7 @@ package extras import ( _ "embed" + "github.com/FAU-CDI/wisski-distillery/internal/phpx" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php" ) @@ -16,11 +17,11 @@ type Settings struct { //go:embed settings.php var settingsPHP string -func (settings *Settings) Get(server *php.Server, key string) (value any, err error) { +func (settings *Settings) Get(server *phpx.Server, key string) (value any, err error) { err = settings.PHP.ExecScript(server, &value, settingsPHP, "get_setting", key) return } -func (settings *Settings) Set(server *php.Server, key string, value any) error { +func (settings *Settings) Set(server *phpx.Server, key string, value any) error { return settings.PHP.ExecScript(server, nil, settingsPHP, "set_setting", key, value) } diff --git a/internal/wisski/ingredient/php/php.go b/internal/wisski/ingredient/php/php.go index 37832cb..11ba7cd 100644 --- a/internal/wisski/ingredient/php/php.go +++ b/internal/wisski/ingredient/php/php.go @@ -3,6 +3,7 @@ package php import ( "strings" + "github.com/FAU-CDI/wisski-distillery/internal/phpx" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel" ) @@ -27,9 +28,9 @@ type PHP struct { // It's arguments are encoded as json using [json.Marshal] and decoded within php. // // The return value of the function is again marshaled with json and returned to the caller. -func (php *PHP) ExecScript(server *Server, value any, code string, entrypoint string, args ...any) (err error) { +func (php *PHP) ExecScript(server *phpx.Server, value any, code string, entrypoint string, args ...any) (err error) { if server == nil { - server, err = php.NewServer() + server = php.NewServer() if err != nil { return } @@ -45,9 +46,9 @@ func (php *PHP) ExecScript(server *Server, value any, code string, entrypoint st return server.MarshalCall(value, entrypoint, args...) } -func (php *PHP) EvalCode(server *Server, value any, code string) (err error) { +func (php *PHP) EvalCode(server *phpx.Server, value any, code string) (err error) { if server == nil { - server, err = php.NewServer() + server = php.NewServer() if err != nil { return } diff --git a/internal/wisski/ingredient/php/server.go b/internal/wisski/ingredient/php/server.go index 14e2c33..6c81a89 100644 --- a/internal/wisski/ingredient/php/server.go +++ b/internal/wisski/ingredient/php/server.go @@ -3,19 +3,22 @@ package php import ( _ "embed" - "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php/phpserver" + "github.com/FAU-CDI/wisski-distillery/internal/phpx" "github.com/alessio/shellescape" "github.com/tkw1536/goprogram/stream" ) -type Server = phpserver.Server - // NewServer returns a new server that can execute code within this distillery. // When err == nil, the caller must call server.Close(). // // See [PHPServer]. -func (php *PHP) NewServer() (*Server, error) { - return phpserver.New(func(str stream.IOStream, script string) { - php.Barrel.Shell(str, "-c", shellescape.QuoteCommand([]string{"drush", "php:eval", script})) - }) +func (php *PHP) NewServer() *phpx.Server { + return &phpx.Server{ + Executor: phpx.SpawnFunc(php.spawn), + } +} + +func (php *PHP) spawn(str stream.IOStream, code string) error { + _, err := php.Barrel.Shell(str, "-c", shellescape.QuoteCommand([]string{"drush", "php:eval", code})) + return err }