php: Move server code into new phpx package

This commit is contained in:
Tom Wiesing 2022-10-19 13:52:24 +02:00
parent 4df5f6387c
commit 2e47626900
No known key found for this signature in database
13 changed files with 134 additions and 86 deletions

View file

@ -1,14 +1,12 @@
package phpserver package phpx
import "fmt" import "fmt"
// Common PHP Errors // Common PHP Errors
var ( const (
errPHPInit = "Unable to initialize" errInit = "Server initialization failed"
errPHPMarshal = "Marshal failed" errClosed = "Server closed"
errPHPInvalid = ServerError{Message: "Invalid code to execute"} errReceive = "Failed to decode response"
errPHPReceive = "Failed to receive response"
errPHPClosed = ServerError{Message: "Server closed"}
) )
// PHPError represents an error during PHPServer logic // PHPError represents an error during PHPServer logic
@ -17,6 +15,7 @@ type ServerError struct {
Err error Err error
} }
// Unwrap returns the underlying error
func (err ServerError) Unwrap() error { func (err ServerError) Unwrap() error {
return err.Err return err.Err
} }

View file

@ -1,4 +1,4 @@
package phpserver package phpx
import ( import (
"encoding/json" "encoding/json"

18
internal/phpx/php.go Normal file
View file

@ -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)
}

View file

@ -1,4 +1,4 @@
package phpserver package phpx
import ( import (
"context" "context"
@ -10,62 +10,78 @@ import (
_ "embed" _ "embed"
"github.com/FAU-CDI/wisski-distillery/pkg/lazy"
"github.com/tkw1536/goprogram/lib/collection" "github.com/tkw1536/goprogram/lib/collection"
"github.com/tkw1536/goprogram/lib/nobufio" "github.com/tkw1536/goprogram/lib/nobufio"
"github.com/tkw1536/goprogram/stream" "github.com/tkw1536/goprogram/stream"
) )
// New creates a new server, with execPHP as a method to call a PHP Shell. // Server represents a server that executes PHP code.
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.
// A typical use-case is to define functions using [MarshalEval], and then call those functions [MarshalCall]. // 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 { 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 in io.WriteCloser
out io.Reader 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. // 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. // 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 { 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() server.m.Lock()
defer server.m.Unlock() 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 { if err := server.c.Err(); err != nil {
return errPHPClosed return ServerError{Message: errClosed}
} }
// find a delimiter for the code, and then send // find a delimiter for the code, and then send
delim := findDelimiter(code) io.WriteString(server.in, input)
io.WriteString(server.in, delim+"\n"+code+"\n"+delim+"\n")
// read the next line (as a response) // read the next line (as a response)
data, err := nobufio.ReadLine(server.out) data, err := nobufio.ReadLine(server.out)
if err != nil { if err != nil {
return ServerError{Message: errPHPReceive, Err: err} return ServerError{Message: errReceive, Err: err}
} }
// read whatever we received // read whatever we received
var received [2]json.RawMessage var received [2]json.RawMessage
if err := json.Unmarshal([]byte(data), &received); err != nil { 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 // 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]. // Return values are received as in [MarshalEval].
func (server *Server) MarshalCall(value any, function string, args ...any) error { func (server *Server) MarshalCall(value any, function string, args ...any) error {
// name of function to call // name of function to call
name := MarshalString(function) name := MarshalString(function)
@ -182,12 +204,14 @@ func findDelimiter(input string) string {
// Close closes this server and prevents any further code from being run. // Close closes this server and prevents any further code from being run.
func (server *Server) Close() error { func (server *Server) Close() error {
server.prepare()
server.m.Lock() server.m.Lock()
defer server.m.Unlock() defer server.m.Unlock()
// if the context is already closed // if the context is already closed
if err := server.c.Err(); err != nil { if err := server.c.Err(); err != nil {
return errPHPClosed return ServerError{Message: errClosed}
} }
server.in.Close() server.in.Close()

View file

@ -3,8 +3,8 @@ package drush
import ( import (
"time" "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"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php"
"github.com/tkw1536/goprogram/exit" "github.com/tkw1536/goprogram/exit"
"github.com/tkw1536/goprogram/stream" "github.com/tkw1536/goprogram/stream"
) )
@ -28,7 +28,7 @@ func (drush *Drush) Cron(io stream.IOStream) error {
return nil 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 var timestamp int64
err = drush.PHP.EvalCode(server, &timestamp, `$val = \Drupal::state()->get('system.cron_last'); return $val; `) err = drush.PHP.EvalCode(server, &timestamp, `$val = \Drupal::state()->get('system.cron_last'); return $val; `)
if err != nil { if err != nil {

View file

@ -4,7 +4,7 @@ import (
"time" "time"
"github.com/FAU-CDI/wisski-distillery/internal/models" "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 // Fetcher is an ingredient with a fetch method
@ -19,7 +19,7 @@ type Fetcher interface {
// FetchFlags specifies what information to fetch // FetchFlags specifies what information to fetch
type FetchFlags struct { type FetchFlags struct {
Quick bool Quick bool
Server *phpserver.Server Server *phpx.Server
} }
// Information represents fetched information about a WissKI // Information represents fetched information about a WissKI

View file

@ -38,9 +38,9 @@ func (wisski *Info) Information(quick bool) (info WissKIInfo, err error) {
// potentially setup a new server // potentially setup a new server
if !flags.Quick { if !flags.Quick {
server, err := wisski.PHP.NewServer() flags.Server = wisski.PHP.NewServer()
if err == nil { if err == nil {
defer server.Close() defer flags.Server.Close()
} }
} }

View file

@ -3,6 +3,7 @@ package extras
import ( import (
_ "embed" _ "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"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
@ -20,7 +21,7 @@ var pathbuilderPHP string
// All returns the ids of all pathbuilders in consistent order. // All returns the ids of all pathbuilders in consistent order.
// //
// server is the server to fetch the pathbuilders from, any may be nil. // 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") err = pathbuilder.PHP.ExecScript(server, &ids, pathbuilderPHP, "all_list")
slices.Sort(ids) slices.Sort(ids)
return 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. // 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. // 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) err = pathbuilder.PHP.ExecScript(server, &xml, pathbuilderPHP, "one_xml", id)
return return
} }
@ -38,7 +39,7 @@ func (pathbuilder *Pathbuilder) Get(server *php.Server, id string) (xml string,
// GetAll returns all pathbuilders serialized as xml // GetAll returns all pathbuilders serialized as xml
// //
// server is the server to fetch the pathbuilders from, any may be nil. // 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") err = pathbuilder.PHP.ExecScript(server, &pathbuilders, pathbuilderPHP, "all_xml")
return return
} }

View file

@ -5,6 +5,7 @@ import (
"path/filepath" "path/filepath"
"strings" "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"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/mstore" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/mstore"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php" "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 is an optional server to fetch prefixes from.
// server may be nil. // 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) uris, err := prefixes.database(server)
if err != nil { if err != nil {
return nil, err return nil, err
@ -49,7 +50,7 @@ func (prefixes *Prefixes) All(server *php.Server) ([]string, error) {
return append(uris, uris2...), nil 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 // get all the ugly prefixes
err = wisski.PHP.ExecScript(server, &prefixes, listURIPrefixesPHP, "list_prefixes") err = wisski.PHP.ExecScript(server, &prefixes, listURIPrefixesPHP, "list_prefixes")
if err != nil { if err != nil {

View file

@ -3,6 +3,7 @@ package extras
import ( import (
_ "embed" _ "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"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php"
) )
@ -16,11 +17,11 @@ type Settings struct {
//go:embed settings.php //go:embed settings.php
var settingsPHP string 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) err = settings.PHP.ExecScript(server, &value, settingsPHP, "get_setting", key)
return 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) return settings.PHP.ExecScript(server, nil, settingsPHP, "set_setting", key, value)
} }

View file

@ -3,6 +3,7 @@ package php
import ( import (
"strings" "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"
"github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/barrel" "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. // 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. // 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 { if server == nil {
server, err = php.NewServer() server = php.NewServer()
if err != nil { if err != nil {
return return
} }
@ -45,9 +46,9 @@ func (php *PHP) ExecScript(server *Server, value any, code string, entrypoint st
return server.MarshalCall(value, entrypoint, args...) 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 { if server == nil {
server, err = php.NewServer() server = php.NewServer()
if err != nil { if err != nil {
return return
} }

View file

@ -3,19 +3,22 @@ package php
import ( import (
_ "embed" _ "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/alessio/shellescape"
"github.com/tkw1536/goprogram/stream" "github.com/tkw1536/goprogram/stream"
) )
type Server = phpserver.Server
// NewServer returns a new server that can execute code within this distillery. // NewServer returns a new server that can execute code within this distillery.
// When err == nil, the caller must call server.Close(). // When err == nil, the caller must call server.Close().
// //
// See [PHPServer]. // See [PHPServer].
func (php *PHP) NewServer() (*Server, error) { func (php *PHP) NewServer() *phpx.Server {
return phpserver.New(func(str stream.IOStream, script string) { return &phpx.Server{
php.Barrel.Shell(str, "-c", shellescape.QuoteCommand([]string{"drush", "php:eval", script})) 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
} }