php/cmd_serve_frankenphp.go

126 lines
3.4 KiB
Go
Raw Normal View History

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