diff --git a/cmd/info.go b/cmd/info.go index 6224a16..4c02a6f 100644 --- a/cmd/info.go +++ b/cmd/info.go @@ -34,7 +34,7 @@ func (i info) Run(context wisski_distillery.Context) error { return err } - info, err := instance.Info().Fetch(false) + info, err := instance.Info().Information(false) if err != nil { return err } diff --git a/cmd/update_prefix_config.go b/cmd/update_prefix_config.go index 970dfc0..45f88c9 100644 --- a/cmd/update_prefix_config.go +++ b/cmd/update_prefix_config.go @@ -44,7 +44,7 @@ func (upc updateprefixconfig) Run(context wisski_distillery.Context) error { return status.StreamGroup(context.IOStream, upc.Parallel, func(instance *wisski.WissKI, io stream.IOStream) error { io.Println("reading prefixes") - err := instance.Prefixes().UpdatePrefixes() + err := instance.Prefixes().Update() if err != nil { return errPrefixUpdateFailed.Wrap(err) } diff --git a/internal/dis/component/home/public.go b/internal/dis/component/home/public.go index cb57690..4c46379 100644 --- a/internal/dis/component/home/public.go +++ b/internal/dis/component/home/public.go @@ -73,7 +73,7 @@ func (home *Home) homeRender() ([]byte, error) { i := i wissKI := instance eg.Go(func() (err error) { - context.Instances[i], err = wissKI.Info().Fetch(true) + context.Instances[i], err = wissKI.Info().Information(true) return }) } diff --git a/internal/dis/component/info/index.go b/internal/dis/component/info/index.go index f19d0ab..1785c5c 100644 --- a/internal/dis/component/info/index.go +++ b/internal/dis/component/info/index.go @@ -50,7 +50,7 @@ func (nfo *Info) indexPageAPI(r *http.Request) (idx indexPageContext, err error) // store the info for this group! group.Go(func() (err error) { - idx.Instances[i], err = instance.Info().Fetch(true) + idx.Instances[i], err = instance.Info().Information(true) return err }) } diff --git a/internal/dis/component/info/instance.go b/internal/dis/component/info/instance.go index 777c813..c418dda 100644 --- a/internal/dis/component/info/instance.go +++ b/internal/dis/component/info/instance.go @@ -40,7 +40,7 @@ func (info *Info) instancePageAPI(r *http.Request) (is instancePageContext, err is.Instance = instance.Instance // get some more info about the wisski - is.Info, err = instance.Info().Fetch(false) + is.Info, err = instance.Info().Information(false) if err != nil { return is, err } diff --git a/internal/dis/component/resolver/prefixes.go b/internal/dis/component/resolver/prefixes.go index b2ae2ff..1e90b1a 100644 --- a/internal/dis/component/resolver/prefixes.go +++ b/internal/dis/component/resolver/prefixes.go @@ -37,7 +37,7 @@ func (resolver *Resolver) AllPrefixes() (map[string]string, error) { // failed to fetch prefixes for this particular instance // => skip it! - prefixes, err := instance.Prefixes().PrefixesCached() + prefixes, err := instance.Prefixes().AllCached() if err != nil { lastErr = err continue diff --git a/internal/wisski/ingredient/barrel/build.go b/internal/wisski/ingredient/barrel/build.go index 57b7e23..5502e4f 100644 --- a/internal/wisski/ingredient/barrel/build.go +++ b/internal/wisski/ingredient/barrel/build.go @@ -5,6 +5,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/dis/component" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/meta" + "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/locker" "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/mstore" "github.com/tkw1536/goprogram/stream" @@ -61,3 +62,14 @@ func (barrel Barrel) LastRebuild() (t time.Time, err error) { func (barrel *Barrel) setLastRebuild() error { return lastRebuild.Set(barrel.MStore, time.Now().Unix()) } + +type LastRebuildFetcher struct { + ingredient.Base + + Barrel *Barrel +} + +func (lbr *LastRebuildFetcher) Fetch(flags ingredient.FetchFlags, info *ingredient.Information) (err error) { + info.LastRebuild, _ = lbr.Barrel.LastRebuild() + return +} diff --git a/internal/wisski/ingredient/barrel/drush/cron.go b/internal/wisski/ingredient/barrel/drush/cron.go index a88b84d..9482788 100644 --- a/internal/wisski/ingredient/barrel/drush/cron.go +++ b/internal/wisski/ingredient/barrel/drush/cron.go @@ -3,6 +3,7 @@ package drush import ( "time" + "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" @@ -35,3 +36,18 @@ func (drush *Drush) LastCron(server *php.Server) (t time.Time, err error) { } return time.Unix(timestamp, 0), nil } + +type LastCronFetcher struct { + ingredient.Base + + Drush *Drush +} + +func (lbr *LastCronFetcher) Fetch(flags ingredient.FetchFlags, info *ingredient.Information) (err error) { + if flags.Quick { + return + } + + info.LastRebuild, _ = lbr.Drush.LastCron(flags.Server) + return +} diff --git a/internal/wisski/ingredient/barrel/drush/update.go b/internal/wisski/ingredient/barrel/drush/update.go index 3b2c186..0141200 100644 --- a/internal/wisski/ingredient/barrel/drush/update.go +++ b/internal/wisski/ingredient/barrel/drush/update.go @@ -4,6 +4,7 @@ import ( "time" "github.com/FAU-CDI/wisski-distillery/internal/dis/component/meta" + "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/pkg/environment" "github.com/tkw1536/goprogram/exit" @@ -46,3 +47,14 @@ func (drush *Drush) LastUpdate() (t time.Time, err error) { func (drush *Drush) setLastUpdate() error { return lastUpdate.Set(drush.MStore, time.Now().Unix()) } + +type LastUpdateFetcher struct { + ingredient.Base + + Drush *Drush +} + +func (lbr *LastUpdateFetcher) Fetch(flags ingredient.FetchFlags, info *ingredient.Information) (err error) { + info.LastUpdate, err = lbr.Drush.LastUpdate() + return +} diff --git a/internal/wisski/ingredient/barrel/running.go b/internal/wisski/ingredient/barrel/running.go index f6d41a4..f71dbc8 100644 --- a/internal/wisski/ingredient/barrel/running.go +++ b/internal/wisski/ingredient/barrel/running.go @@ -1,6 +1,9 @@ package barrel -import "github.com/tkw1536/goprogram/stream" +import ( + "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient" + "github.com/tkw1536/goprogram/stream" +) // Running checks if this WissKI is currently running. func (barrel *Barrel) Running() (bool, error) { @@ -10,3 +13,14 @@ func (barrel *Barrel) Running() (bool, error) { } return len(ps) > 0, nil } + +type RunningFetcher struct { + ingredient.Base + + Barrel *Barrel +} + +func (rf *RunningFetcher) Fetch(flags ingredient.FetchFlags, info *ingredient.Information) (err error) { + info.Running, err = rf.Barrel.Running() + return +} diff --git a/internal/wisski/ingredient/fetcher.go b/internal/wisski/ingredient/fetcher.go new file mode 100644 index 0000000..0b73f63 --- /dev/null +++ b/internal/wisski/ingredient/fetcher.go @@ -0,0 +1,48 @@ +package ingredient + +import ( + "time" + + "github.com/FAU-CDI/wisski-distillery/internal/models" + "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient/php/phpserver" +) + +// Fetcher is an ingredient with a fetch method +type Fetcher interface { + Ingredient + + // Fetch fetchs information with the given information and writes it into info. + // Distinct Fetchers must write into distinct fields. + Fetch(flags FetchFlags, info *Information) error +} + +// FetchFlags specifies what information to fetch +type FetchFlags struct { + Quick bool + Server *phpserver.Server +} + +// Information represents fetched information about a WissKI +type Information struct { + Time time.Time // Time this info was built + + // Generic Information + Slug string // slug + URL string // complete URL, including http(s) + + Locked bool // Is this instance currently locked? + + // Information about the running instance + Running bool + LastRebuild time.Time + LastUpdate time.Time + LastCron time.Time + + // List of backups made + Snapshots []models.Export + + // WissKI content information + NoPrefixes bool // TODO: Move this into the database + Prefixes []string // list of prefixes + Pathbuilders map[string]string // all the pathbuilders +} diff --git a/internal/wisski/ingredient/info/info.go b/internal/wisski/ingredient/info/info.go index bd0e02c..483b151 100644 --- a/internal/wisski/ingredient/info/info.go +++ b/internal/wisski/ingredient/info/info.go @@ -3,7 +3,6 @@ package info import ( "time" - "github.com/FAU-CDI/wisski-distillery/internal/models" "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/drush" @@ -16,7 +15,9 @@ import ( type Info struct { ingredient.Base - PHP *php.PHP + PHP *php.PHP + Fetchers []ingredient.Fetcher + Barrel *barrel.Barrel Locker *locker.Locker Drush *drush.Drush @@ -24,98 +25,41 @@ type Info struct { Pathbuilder *extras.Pathbuilder } -// WissKIInfo represents information about this WissKI Instance. -type WissKIInfo struct { - Time time.Time // Time this info was built +// TODO: Use the information struct globally +type WissKIInfo = ingredient.Information - // Generic Information - Slug string // slug - URL string // complete URL, including http(s) - - Locked bool // Is this instance currently locked? - - // Information about the running instance - Running bool - LastRebuild time.Time - LastUpdate time.Time - LastCron time.Time - - // List of backups made - Snapshots []models.Export - - // WissKI content information - NoPrefixes bool // TODO: Move this into the database - Prefixes []string // list of prefixes - Pathbuilders map[string]string // all the pathbuilders -} - -// Fetch fetches information about this WissKI. +// Information fetches information about this WissKI. // TODO: Rework this to be able to determine what kind of information is available. -func (wisski *Info) Fetch(quick bool) (info WissKIInfo, err error) { - var group errgroup.Group - wisski.infoQuick(&info, &group) +func (wisski *Info) Information(quick bool) (info WissKIInfo, err error) { + // setup flags + flags := ingredient.FetchFlags{ + Quick: quick, + } - if !quick { + // potentially setup a new server + if !flags.Quick { server, err := wisski.PHP.NewServer() if err == nil { defer server.Close() } - wisski.infoSlow(&info, server, &group) + } + + // run all the fetchers! + var group errgroup.Group + for _, fetcher := range wisski.Fetchers { + fetcher := fetcher + group.Go(func() error { + return fetcher.Fetch(flags, &info) + }) } err = group.Wait() return } -func (wisski *Info) infoQuick(info *WissKIInfo, group *errgroup.Group) { +func (wisski *Info) Fetch(flags ingredient.FetchFlags, info *ingredient.Information) error { info.Time = time.Now().UTC() info.Slug = wisski.Slug info.URL = wisski.URL().String() - - group.Go(func() (err error) { - info.Running, err = wisski.Barrel.Running() - return - }) - - group.Go(func() (err error) { - info.Locked = wisski.Locker.Locked() - return - }) - - group.Go(func() (err error) { - info.LastRebuild, _ = wisski.Barrel.LastRebuild() - return - }) - - group.Go(func() (err error) { - info.LastUpdate, _ = wisski.Drush.LastUpdate() - return - }) - - group.Go(func() (err error) { - info.NoPrefixes = wisski.Prefixes.NoPrefix() - return - }) -} - -func (wisski *Info) infoSlow(info *WissKIInfo, server *php.Server, group *errgroup.Group) { - group.Go(func() (err error) { - info.Prefixes, _ = wisski.Prefixes.All(server) - return nil - }) - - group.Go(func() (err error) { - info.Snapshots, _ = wisski.Snapshots() - return nil - }) - - group.Go(func() (err error) { - info.Pathbuilders, _ = wisski.Pathbuilder.GetAll(server) - return nil - }) - - group.Go(func() (err error) { - info.LastCron, _ = wisski.Drush.LastCron(server) - return - }) + return nil } diff --git a/internal/wisski/ingredient/info/snapshots.go b/internal/wisski/ingredient/info/snapshots.go new file mode 100644 index 0000000..b9ab477 --- /dev/null +++ b/internal/wisski/ingredient/info/snapshots.go @@ -0,0 +1,18 @@ +package info + +import "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient" + +type SnapshotsFetcher struct { + ingredient.Base + + Info *Info +} + +func (lbr *SnapshotsFetcher) Fetch(flags ingredient.FetchFlags, info *ingredient.Information) (err error) { + if flags.Quick { + return + } + + info.Snapshots, _ = lbr.Snapshots() + return +} diff --git a/internal/wisski/ingredient/locker/locked.go b/internal/wisski/ingredient/locker/locked.go index d91e2f0..a6a39b5 100644 --- a/internal/wisski/ingredient/locker/locked.go +++ b/internal/wisski/ingredient/locker/locked.go @@ -1,6 +1,9 @@ package locker -import "github.com/FAU-CDI/wisski-distillery/internal/models" +import ( + "github.com/FAU-CDI/wisski-distillery/internal/models" + "github.com/FAU-CDI/wisski-distillery/internal/wisski/ingredient" +) // Locked checks if this WissKI is currently locked. func (lock *Locker) Locked() (locked bool) { @@ -13,3 +16,8 @@ func (lock *Locker) Locked() (locked bool) { table.Select("count(*) > 0").Where("slug = ?", lock.Slug).Find(&locked) return } + +func (locker *Locker) Fetch(flags ingredient.FetchFlags, info *ingredient.Information) (err error) { + info.Locked = locker.Locked() + return +} diff --git a/internal/wisski/ingredient/php/extras/pathbuilder.go b/internal/wisski/ingredient/php/extras/pathbuilder.go index 6f8615c..a2bf782 100644 --- a/internal/wisski/ingredient/php/extras/pathbuilder.go +++ b/internal/wisski/ingredient/php/extras/pathbuilder.go @@ -42,3 +42,12 @@ func (pathbuilder *Pathbuilder) GetAll(server *php.Server) (pathbuilders map[str err = pathbuilder.PHP.ExecScript(server, &pathbuilders, pathbuilderPHP, "all_xml") return } + +func (pathbuilder *Pathbuilder) Fetch(flags ingredient.FetchFlags, info *ingredient.Information) (err error) { + if flags.Quick { + return + } + + info.Pathbuilders, _ = pathbuilder.GetAll(flags.Server) + return +} diff --git a/internal/wisski/ingredient/php/extras/prefixes.go b/internal/wisski/ingredient/php/extras/prefixes.go index 94b8742..6ab5511 100644 --- a/internal/wisski/ingredient/php/extras/prefixes.go +++ b/internal/wisski/ingredient/php/extras/prefixes.go @@ -141,15 +141,28 @@ func (wisski *Prefixes) filePrefixes() (prefixes []string, err error) { var prefix = mstore.For[string]("prefix") // Prefixes returns the cached prefixes from the given instance -func (wisski *Prefixes) PrefixesCached() (results []string, err error) { +func (wisski *Prefixes) AllCached() (results []string, err error) { return prefix.GetAll(wisski.MStore) } -// UpdatePrefixes updates the cached prefixes of this instance -func (wisski *Prefixes) UpdatePrefixes() error { +// Update updates the cached prefixes of this instance +func (wisski *Prefixes) Update() error { prefixes, err := wisski.All(nil) if err != nil { return err } return prefix.SetAll(wisski.MStore, prefixes...) } + +func (prefixes *Prefixes) Fetch(flags ingredient.FetchFlags, info *ingredient.Information) (err error) { + info.NoPrefixes = prefixes.NoPrefix() + if flags.Quick { + // quick mode: grab only the cached prefixes + info.Prefixes, _ = prefixes.AllCached() + } else { + // slow mode: grab the fresh prefixes from the server + // TODO: Do we want to update them while we are at it? + info.Prefixes, _ = prefixes.All(flags.Server) + } + return +} diff --git a/internal/wisski/ingredient/php/phpserver/errors.go b/internal/wisski/ingredient/php/phpserver/errors.go new file mode 100644 index 0000000..58304fc --- /dev/null +++ b/internal/wisski/ingredient/php/phpserver/errors.go @@ -0,0 +1,36 @@ +package phpserver + +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"} +) + +// PHPError represents an error during PHPServer logic +type ServerError struct { + Message string + Err error +} + +func (err ServerError) Unwrap() error { + return err.Err +} + +func (err ServerError) Error() string { + if err.Err == nil { + return fmt.Sprintf("PHPServer: %s", err.Message) + } + return fmt.Sprintf("PHPServer: %s: %s", err.Message, err.Err) +} + +// Throwable represents an error during php code +type Throwable string + +func (throwable Throwable) Error() string { + return string(throwable) +} diff --git a/internal/wisski/ingredient/php/marshal.go b/internal/wisski/ingredient/php/phpserver/marshal.go similarity index 97% rename from internal/wisski/ingredient/php/marshal.go rename to internal/wisski/ingredient/php/phpserver/marshal.go index 5c7eb1e..1f321b5 100644 --- a/internal/wisski/ingredient/php/marshal.go +++ b/internal/wisski/ingredient/php/phpserver/marshal.go @@ -1,4 +1,4 @@ -package php +package phpserver import ( "encoding/json" diff --git a/internal/wisski/ingredient/php/phpserver/server.go b/internal/wisski/ingredient/php/phpserver/server.go new file mode 100644 index 0000000..6ec271b --- /dev/null +++ b/internal/wisski/ingredient/php/phpserver/server.go @@ -0,0 +1,216 @@ +package phpserver + +import ( + "context" + "encoding/json" + "io" + "os" + "strings" + "sync" + + _ "embed" + + "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. +// 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 +} + +// MarshalEval evaluates code on the server and Marshals the result into value. +// When value is nil, the results are discarded. +// +// code is directly passed to php's "eval" function. +// as such any functions defined will remain in server memory. +// +// 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 { + server.m.Lock() + defer server.m.Unlock() + + // quick hack: when the server is already done + if err := server.c.Err(); err != nil { + return errPHPClosed + } + + // find a delimiter for the code, and then send + delim := findDelimiter(code) + io.WriteString(server.in, delim+"\n"+code+"\n"+delim+"\n") + + // read the next line (as a response) + data, err := nobufio.ReadLine(server.out) + if err != nil { + return ServerError{Message: errPHPReceive, 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} + } + + // check if there was an error + var errString string + if err := json.Unmarshal(received[1], &errString); err == nil && errString != "" { + return Throwable(errString) + } + + // special case: no return value => no unmarshaling needed + if value == nil { + return nil + } + + // read the actual result! + return json.Unmarshal(received[0], value) +} + +// Eval is like [MarshalEval], but returns the value as an any +func (server *Server) Eval(code string) (value any, err error) { + err = server.MarshalEval(&value, code) + return +} + +// MarshalCall calls a previously defined function with the given arguments. +// Arguments are sent to php using json Marshal, and are 'json_decode'd on the php side. +// +// 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) + + // generate code to call + var code string + switch len(args) { + case 0: + code = "return call_user_func(" + name + ");" + case 1: + param, err := Marshal(args[0]) + if err != nil { + return err + } + code = "return call_user_func(" + name + "," + param + ");" + default: + params, err := Marshal(args) + if err != nil { + return err + } + code = "return call_user_func_array(" + name + "," + params + ");" + } + + // and evaluate the code + return server.MarshalEval(value, code) +} + +// Call is like [MarshalCall] but returns the return value of the function as an any +func (server *Server) Call(function string, args ...any) (value any, err error) { + err = server.MarshalCall(&value, function, args...) + 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.m.Lock() + defer server.m.Unlock() + + // if the context is already closed + if err := server.c.Err(); err != nil { + return errPHPClosed + } + + server.in.Close() + <-server.c.Done() + + return nil +} + +//go:embed server.php +var serverPHP string + +// pre-process the server.php code to make it shorter +func init() { + // remove the first ' no unmarshaling needed - if value == nil { - return nil - } - - // read the actual result! - return json.Unmarshal(received[0], value) -} - -// Eval is like [MarshalEval], but returns the value as an any -func (server *Server) Eval(code string) (value any, err error) { - err = server.MarshalEval(&value, code) - return -} - -// MarshalCall calls a previously defined function with the given arguments. -// Arguments are sent to php using json Marshal, and are 'json_decode'd on the php side. -// -// 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) - - // generate code to call - var code string - switch len(args) { - case 0: - code = "return call_user_func(" + name + ");" - case 1: - param, err := Marshal(args[0]) - if err != nil { - return err - } - code = "return call_user_func(" + name + "," + param + ");" - default: - params, err := Marshal(args) - if err != nil { - return err - } - code = "return call_user_func_array(" + name + "," + params + ");" - } - - // and evaluate the code - return server.MarshalEval(value, code) -} - -// Call is like [MarshalCall] but returns the return value of the function as an any -func (server *Server) Call(function string, args ...any) (value any, err error) { - err = server.MarshalCall(&value, function, args...) - 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.m.Lock() - defer server.m.Unlock() - - // if the context is already closed - if err := server.c.Err(); err != nil { - return errPHPClosed - } - - server.in.Close() - <-server.c.Done() - - return nil -} - -//go:embed server.php -var serverPHP string - -// pre-process the server.php code to make it shorter -func init() { - // remove the first '