126 lines
3.4 KiB
Go
126 lines
3.4 KiB
Go
|
|
//go:build cgo
|
||
|
|
|
||
|
|
package php
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"fmt"
|
||
|
|
"log"
|
||
|
|
"net/http"
|
||
|
|
"os"
|
||
|
|
"os/signal"
|
||
|
|
"syscall"
|
||
|
|
|
||
|
|
"forge.lthn.ai/core/cli/pkg/cli"
|
||
|
|
)
|
||
|
|
|
||
|
|
var (
|
||
|
|
serveFPPort int
|
||
|
|
serveFPPath string
|
||
|
|
serveFPWorkers int
|
||
|
|
serveFPThreads int
|
||
|
|
)
|
||
|
|
|
||
|
|
func init() {
|
||
|
|
registerFrankenPHP = addFrankenPHPCommands
|
||
|
|
}
|
||
|
|
|
||
|
|
// addFrankenPHPCommands adds FrankenPHP-specific commands to the php parent command.
|
||
|
|
// Called from AddPHPCommands when CGO is enabled.
|
||
|
|
func addFrankenPHPCommands(phpCmd *cli.Command) {
|
||
|
|
serveCmd := &cli.Command{
|
||
|
|
Use: "serve:embedded",
|
||
|
|
Short: "Serve Laravel via embedded FrankenPHP runtime",
|
||
|
|
Long: "Start an HTTP server using the embedded FrankenPHP runtime with Octane worker mode support.",
|
||
|
|
RunE: runFrankenPHPServe,
|
||
|
|
}
|
||
|
|
serveCmd.Flags().IntVar(&serveFPPort, "port", 8000, "HTTP listen port")
|
||
|
|
serveCmd.Flags().StringVar(&serveFPPath, "path", ".", "Laravel application root")
|
||
|
|
serveCmd.Flags().IntVar(&serveFPWorkers, "workers", 2, "Octane worker count")
|
||
|
|
serveCmd.Flags().IntVar(&serveFPThreads, "threads", 4, "PHP thread count")
|
||
|
|
phpCmd.AddCommand(serveCmd)
|
||
|
|
|
||
|
|
execCmd := &cli.Command{
|
||
|
|
Use: "exec [command...]",
|
||
|
|
Short: "Execute a PHP artisan command via FrankenPHP",
|
||
|
|
Long: "Boot FrankenPHP, run an artisan command, then exit. Stdin/stdout pass-through.",
|
||
|
|
Args: cli.MinimumNArgs(1),
|
||
|
|
RunE: runFrankenPHPExec,
|
||
|
|
}
|
||
|
|
execCmd.Flags().StringVar(&serveFPPath, "path", ".", "Laravel application root")
|
||
|
|
phpCmd.AddCommand(execCmd)
|
||
|
|
}
|
||
|
|
|
||
|
|
func runFrankenPHPServe(cmd *cli.Command, args []string) error {
|
||
|
|
handler, cleanup, err := NewHandler(serveFPPath, HandlerConfig{
|
||
|
|
NumThreads: serveFPThreads,
|
||
|
|
NumWorkers: serveFPWorkers,
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("init FrankenPHP: %w", err)
|
||
|
|
}
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
addr := fmt.Sprintf(":%d", serveFPPort)
|
||
|
|
srv := &http.Server{
|
||
|
|
Addr: addr,
|
||
|
|
Handler: handler,
|
||
|
|
}
|
||
|
|
|
||
|
|
// Graceful shutdown
|
||
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||
|
|
defer stop()
|
||
|
|
|
||
|
|
go func() {
|
||
|
|
log.Printf("core-php: serving on http://localhost%s (doc root: %s)", addr, handler.DocRoot())
|
||
|
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||
|
|
log.Fatalf("core-php: server error: %v", err)
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
|
||
|
|
<-ctx.Done()
|
||
|
|
log.Println("core-php: shutting down...")
|
||
|
|
return srv.Shutdown(context.Background())
|
||
|
|
}
|
||
|
|
|
||
|
|
func runFrankenPHPExec(cmd *cli.Command, args []string) error {
|
||
|
|
handler, cleanup, err := NewHandler(serveFPPath, HandlerConfig{
|
||
|
|
NumThreads: 1,
|
||
|
|
NumWorkers: 0,
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("init FrankenPHP: %w", err)
|
||
|
|
}
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
// Build an artisan request
|
||
|
|
artisanArgs := "artisan"
|
||
|
|
for _, a := range args {
|
||
|
|
artisanArgs += " " + a
|
||
|
|
}
|
||
|
|
|
||
|
|
log.Printf("core-php: exec %s (root: %s)", artisanArgs, handler.LaravelRoot())
|
||
|
|
|
||
|
|
// Execute via internal HTTP request to FrankenPHP
|
||
|
|
// This routes through the PHP runtime as if it were a CLI call
|
||
|
|
req, err := http.NewRequest("GET", "/artisan-exec?cmd="+artisanArgs, nil)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
// For now, use the handler directly
|
||
|
|
w := &execResponseWriter{os.Stdout}
|
||
|
|
handler.ServeHTTP(w, req)
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// execResponseWriter writes HTTP response body directly to stdout.
|
||
|
|
type execResponseWriter struct {
|
||
|
|
out *os.File
|
||
|
|
}
|
||
|
|
|
||
|
|
func (w *execResponseWriter) Header() http.Header { return http.Header{} }
|
||
|
|
func (w *execResponseWriter) WriteHeader(statusCode int) {}
|
||
|
|
func (w *execResponseWriter) Write(b []byte) (int, error) { return w.out.Write(b) }
|