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"
// 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
}

View file

@ -1,4 +1,4 @@
package phpserver
package phpx
import (
"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 (
"context"
@ -10,23 +10,50 @@ 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) {
// 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 server, once used, should be closed using the [Close] method.
type Server struct {
// 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
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 {
return nil, ServerError{errPHPInit, err}
server.err.Set(ServerError{errInit, err})
return
}
or, ow, err := os.Pipe()
if err != nil {
ir.Close()
iw.Close()
return nil, ServerError{errPHPInit, err}
server.err.Set(ServerError{errInit, err})
return
}
// create a context to close the server
@ -45,27 +72,16 @@ func New(execPHP func(str stream.IOStream, script string)) (*Server, error) {
// start the server
io := stream.NewIOStream(ow, nil, ir, 0)
execPHP(io, serverPHP)
err := server.Executor.Spawn(io, serverPHP)
server.err.Set(ServerError{errClosed, err})
}()
// return a new server
return &Server{
in: iw,
out: or,
c: context,
}, nil
}
server.in = iw
server.out = or
server.c = context
})
// 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 nil Server will return [ErrServerBroken] on every function call.
type Server struct {
m sync.Mutex
in io.WriteCloser
out io.Reader
c context.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()

View file

@ -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, &timestamp, `$val = \Drupal::state()->get('system.cron_last'); return $val; `)
if err != nil {

View file

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

View file

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

View file

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

View file

@ -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 {

View file

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

View file

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

View file

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