mirror of
https://github.com/dunglas/frankenphp.git
synced 2025-12-24 13:38:11 +08:00
Compare commits
24 Commits
cache
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e6f665d82 | ||
|
|
57c58faf1c | ||
|
|
25d9cb9600 | ||
|
|
4092ecb5b5 | ||
|
|
75ccccf1b2 | ||
|
|
6231bf4a1c | ||
|
|
e01e40fd97 | ||
|
|
175e644d10 | ||
|
|
a8f75d0eef | ||
|
|
91c553f3d9 | ||
|
|
7fca07ed67 | ||
|
|
3599299cde | ||
|
|
bb1c3678dc | ||
|
|
58a63703b4 | ||
|
|
694ab86cef | ||
|
|
e23e0c571e | ||
|
|
f02e6f2f85 | ||
|
|
11213fd1de | ||
|
|
41da660088 | ||
|
|
599c92b15d | ||
|
|
225ca409d3 | ||
|
|
d2007620a4 | ||
|
|
4ac024a1d0 | ||
|
|
e0dcf42852 |
6
.github/workflows/docker.yaml
vendored
6
.github/workflows/docker.yaml
vendored
@@ -208,7 +208,7 @@ jobs:
|
||||
VARIANT: ${{ matrix.variant }}
|
||||
- name: Upload builder metadata
|
||||
if: fromJson(needs.prepare.outputs.push)
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: metadata-builder-${{ matrix.variant }}-${{ steps.prepare.outputs.sanitized_platform }}
|
||||
path: /tmp/metadata/builder/*
|
||||
@@ -216,7 +216,7 @@ jobs:
|
||||
retention-days: 1
|
||||
- name: Upload runner metadata
|
||||
if: fromJson(needs.prepare.outputs.push)
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: metadata-runner-${{ matrix.variant }}-${{ steps.prepare.outputs.sanitized_platform }}
|
||||
path: /tmp/metadata/runner/*
|
||||
@@ -248,7 +248,7 @@ jobs:
|
||||
target: ["builder", "runner"]
|
||||
steps:
|
||||
- name: Download metadata
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
pattern: metadata-${{ matrix.target }}-${{ matrix.variant }}-*
|
||||
path: /tmp/metadata
|
||||
|
||||
2
.github/workflows/sanitizers.yaml
vendored
2
.github/workflows/sanitizers.yaml
vendored
@@ -57,7 +57,7 @@ jobs:
|
||||
echo archive="$(jq -r '.[] .source[] | select(.filename |endswith(".xz")) | "https://www.php.net/distributions/" + .filename' version.json)" >> "$GITHUB_OUTPUT"
|
||||
- name: Cache PHP
|
||||
id: cache-php
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: php/target
|
||||
key: php-sanitizers-${{ matrix.sanitizer }}-${{ runner.arch }}-${{ steps.determine-php-version.outputs.version }}
|
||||
|
||||
19
.github/workflows/static.yaml
vendored
19
.github/workflows/static.yaml
vendored
@@ -10,7 +10,7 @@ on:
|
||||
- main
|
||||
paths:
|
||||
- "docker-bake.hcl"
|
||||
- ".github/workflows/docker.yaml"
|
||||
- ".github/workflows/static.yaml"
|
||||
- "**cgo.go"
|
||||
- "**Dockerfile"
|
||||
- "**.c"
|
||||
@@ -37,6 +37,7 @@ permissions:
|
||||
|
||||
env:
|
||||
IMAGE_NAME: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/')) && 'dunglas/frankenphp' || 'dunglas/frankenphp-dev' }}
|
||||
SPC_OPT_BUILD_ARGS: --debug
|
||||
GOTOOLCHAIN: local
|
||||
|
||||
jobs:
|
||||
@@ -169,7 +170,7 @@ jobs:
|
||||
METADATA: ${{ steps.build.outputs.metadata }}
|
||||
- name: Upload metadata
|
||||
if: fromJson(needs.prepare.outputs.push) && !matrix.debug && !matrix.mimalloc
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: metadata-static-builder-musl-${{ steps.prepare.outputs.sanitized_platform }}
|
||||
path: /tmp/metadata/*
|
||||
@@ -187,7 +188,7 @@ jobs:
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
- name: Upload artifact
|
||||
if: ${{ !fromJson(needs.prepare.outputs.push) }}
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}
|
||||
path: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }}
|
||||
@@ -319,7 +320,7 @@ jobs:
|
||||
METADATA: ${{ steps.build.outputs.metadata }}
|
||||
- name: Upload metadata
|
||||
if: fromJson(needs.prepare.outputs.push)
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: metadata-static-builder-gnu-${{ steps.prepare.outputs.sanitized_platform }}
|
||||
path: /tmp/metadata-gnu/*
|
||||
@@ -343,7 +344,7 @@ jobs:
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
- name: Upload artifact
|
||||
if: ${{ !fromJson(needs.prepare.outputs.push) }}
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}-gnu-files
|
||||
path: gh-output/*
|
||||
@@ -379,13 +380,13 @@ jobs:
|
||||
if: fromJson(needs.prepare.outputs.push)
|
||||
steps:
|
||||
- name: Download metadata
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
pattern: metadata-static-builder-musl-*
|
||||
path: /tmp/metadata
|
||||
merge-multiple: true
|
||||
- name: Download GNU metadata
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
pattern: metadata-static-builder-gnu-*
|
||||
path: /tmp/metadata-gnu
|
||||
@@ -474,7 +475,7 @@ jobs:
|
||||
NO_COMPRESS: ${{ github.event_name == 'pull_request' && '1' || '' }}
|
||||
- name: Upload logs
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
path: dist/static-php-cli/log
|
||||
name: static-php-cli-log-${{ matrix.platform }}-${{ github.sha }}
|
||||
@@ -484,7 +485,7 @@ jobs:
|
||||
subject-path: ${{ github.workspace }}/dist/frankenphp-mac-*
|
||||
- name: Upload artifact
|
||||
if: github.ref_type == 'branch'
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: frankenphp-mac-${{ matrix.platform }}
|
||||
path: dist/frankenphp-mac-${{ matrix.platform }}
|
||||
|
||||
45
.github/workflows/tests.yaml
vendored
45
.github/workflows/tests.yaml
vendored
@@ -93,6 +93,49 @@ jobs:
|
||||
if: matrix.php-versions == '8.5'
|
||||
run: go mod tidy -diff
|
||||
working-directory: caddy/
|
||||
integration-tests:
|
||||
name: Integration Tests (Linux, PHP ${{ matrix.php-versions }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php-versions: ["8.3", "8.4", "8.5"]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25"
|
||||
cache-dependency-path: |
|
||||
go.sum
|
||||
caddy/go.sum
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
ini-file: development
|
||||
coverage: none
|
||||
tools: none
|
||||
env:
|
||||
phpts: ts
|
||||
debug: true
|
||||
- name: Install PHP development libraries
|
||||
run: sudo apt-get update && sudo apt-get install -y libkrb5-dev libsodium-dev libargon2-dev
|
||||
- name: Install xcaddy
|
||||
run: go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
|
||||
- name: Download PHP sources
|
||||
run: |
|
||||
PHP_VERSION=$(php -r "echo PHP_VERSION;")
|
||||
wget -q "https://www.php.net/distributions/php-${PHP_VERSION}.tar.gz"
|
||||
tar xzf "php-${PHP_VERSION}.tar.gz"
|
||||
echo "GEN_STUB_SCRIPT=${PWD}/php-${PHP_VERSION}/build/gen_stub.php" >> "${GITHUB_ENV}"
|
||||
- name: Set CGO flags
|
||||
run: |
|
||||
echo "CGO_CFLAGS=$(php-config --includes)" >> "${GITHUB_ENV}"
|
||||
echo "CGO_LDFLAGS=$(php-config --ldflags) $(php-config --libs)" >> "${GITHUB_ENV}"
|
||||
- name: Run integration tests
|
||||
working-directory: internal/extgen/
|
||||
run: go test -tags integration -v -timeout 30m
|
||||
tests-mac:
|
||||
name: Tests (macOS, PHP 8.5)
|
||||
runs-on: macos-latest
|
||||
@@ -106,7 +149,7 @@ jobs:
|
||||
with:
|
||||
go-version: "1.25"
|
||||
cache-dependency-path: |
|
||||
go.sum
|
||||
go.sum
|
||||
caddy/go.sum
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
|
||||
@@ -4,12 +4,13 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/dunglas/frankenphp/internal/fastabs"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/dunglas/frankenphp/internal/fastabs"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddytest"
|
||||
"github.com/dunglas/frankenphp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
43
caddy/app.go
43
caddy/app.go
@@ -49,13 +49,14 @@ type FrankenPHPApp struct {
|
||||
NumThreads int `json:"num_threads,omitempty"`
|
||||
// MaxThreads limits how many threads can be started at runtime. Default 2x NumThreads
|
||||
MaxThreads int `json:"max_threads,omitempty"`
|
||||
// Workers configures the worker scripts to start.
|
||||
// Workers configures the worker scripts to start
|
||||
Workers []workerConfig `json:"workers,omitempty"`
|
||||
// Overwrites the default php ini configuration
|
||||
PhpIni map[string]string `json:"php_ini,omitempty"`
|
||||
// The maximum amount of time a request may be stalled waiting for a thread
|
||||
MaxWaitTime time.Duration `json:"max_wait_time,omitempty"`
|
||||
|
||||
opts []frankenphp.Option
|
||||
metrics frankenphp.Metrics
|
||||
ctx context.Context
|
||||
logger *slog.Logger
|
||||
@@ -76,6 +77,9 @@ func (f *FrankenPHPApp) Provision(ctx caddy.Context) error {
|
||||
f.ctx = ctx
|
||||
f.logger = ctx.Slogger()
|
||||
|
||||
// We have at least 7 hardcoded options
|
||||
f.opts = make([]frankenphp.Option, 0, 7+len(options))
|
||||
|
||||
if httpApp, err := ctx.AppIfConfigured("http"); err == nil {
|
||||
if httpApp.(*caddyhttp.App).Metrics != nil {
|
||||
f.metrics = frankenphp.NewPrometheusMetrics(ctx.GetMetricsRegistry())
|
||||
@@ -135,11 +139,10 @@ func (f *FrankenPHPApp) Start() error {
|
||||
repl := caddy.NewReplacer()
|
||||
|
||||
optionsMU.RLock()
|
||||
opts := make([]frankenphp.Option, 0, len(options)+len(f.Workers)+7)
|
||||
opts = append(opts, options...)
|
||||
f.opts = append(f.opts, options...)
|
||||
optionsMU.RUnlock()
|
||||
|
||||
opts = append(opts,
|
||||
f.opts = append(f.opts,
|
||||
frankenphp.WithContext(f.ctx),
|
||||
frankenphp.WithLogger(f.logger),
|
||||
frankenphp.WithNumThreads(f.NumThreads),
|
||||
@@ -150,31 +153,19 @@ func (f *FrankenPHPApp) Start() error {
|
||||
)
|
||||
|
||||
for _, w := range f.Workers {
|
||||
workerOpts := make([]frankenphp.WorkerOption, 0, len(w.requestOptions)+4)
|
||||
w.options = append(w.options,
|
||||
frankenphp.WithWorkerEnv(w.Env),
|
||||
frankenphp.WithWorkerWatchMode(w.Watch),
|
||||
frankenphp.WithWorkerMaxFailures(w.MaxConsecutiveFailures),
|
||||
frankenphp.WithWorkerMaxThreads(w.MaxThreads),
|
||||
frankenphp.WithWorkerRequestOptions(w.requestOptions...),
|
||||
)
|
||||
|
||||
if w.requestOptions == nil {
|
||||
workerOpts = append(workerOpts,
|
||||
frankenphp.WithWorkerEnv(w.Env),
|
||||
frankenphp.WithWorkerWatchMode(w.Watch),
|
||||
frankenphp.WithWorkerMaxFailures(w.MaxConsecutiveFailures),
|
||||
frankenphp.WithWorkerMaxThreads(w.MaxThreads),
|
||||
)
|
||||
} else {
|
||||
workerOpts = append(
|
||||
workerOpts,
|
||||
frankenphp.WithWorkerEnv(w.Env),
|
||||
frankenphp.WithWorkerWatchMode(w.Watch),
|
||||
frankenphp.WithWorkerMaxFailures(w.MaxConsecutiveFailures),
|
||||
frankenphp.WithWorkerMaxThreads(w.MaxThreads),
|
||||
frankenphp.WithWorkerRequestOptions(w.requestOptions...),
|
||||
)
|
||||
}
|
||||
|
||||
opts = append(opts, frankenphp.WithWorkers(w.Name, repl.ReplaceKnown(w.FileName, ""), w.Num, workerOpts...))
|
||||
f.opts = append(f.opts, frankenphp.WithWorkers(w.Name, repl.ReplaceKnown(w.FileName, ""), w.Num, w.options...))
|
||||
}
|
||||
|
||||
frankenphp.Shutdown()
|
||||
if err := frankenphp.Init(opts...); err != nil {
|
||||
if err := frankenphp.Init(f.opts...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -288,7 +279,7 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
}
|
||||
|
||||
case "worker":
|
||||
wc, err := parseWorkerConfig(d)
|
||||
wc, err := unmarshalWorker(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
const (
|
||||
defaultDocumentRoot = "public"
|
||||
defaultWatchPattern = "./**/*.{php,yaml,yml,twig,env}"
|
||||
defaultWatchPattern = "./**/*.{env,php,twig,yaml,yml}"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -1362,7 +1362,7 @@ func TestWorkerMatchDirective(t *testing.T) {
|
||||
}
|
||||
`, "caddyfile")
|
||||
|
||||
// worker is outside of public directory, match anyways
|
||||
// worker is outside public directory, match anyway
|
||||
tester.AssertGetResponse("http://localhost:"+testPort+"/matched-path", http.StatusOK, "requests:1")
|
||||
tester.AssertGetResponse("http://localhost:"+testPort+"/matched-path/anywhere", http.StatusOK, "requests:2")
|
||||
|
||||
@@ -1472,3 +1472,31 @@ func TestDd(t *testing.T) {
|
||||
"dump123",
|
||||
)
|
||||
}
|
||||
|
||||
func TestLog(t *testing.T) {
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(`
|
||||
{
|
||||
skip_install_trust
|
||||
admin localhost:2999
|
||||
}
|
||||
|
||||
http://localhost:`+testPort+` {
|
||||
log {
|
||||
output stdout
|
||||
format json
|
||||
}
|
||||
|
||||
root ../testdata
|
||||
php_server {
|
||||
worker ../testdata/log-frankenphp_log.php
|
||||
}
|
||||
}
|
||||
`, "caddyfile")
|
||||
|
||||
tester.AssertGetResponse(
|
||||
"http://localhost:"+testPort+"/log-frankenphp_log.php?i=0",
|
||||
http.StatusOK,
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ func TestModuleWorkerWithWatchConfiguration(t *testing.T) {
|
||||
|
||||
// Verify that the watch directories were set correctly
|
||||
require.Len(t, module.Workers[0].Watch, 3, "Expected three watch patterns")
|
||||
require.Equal(t, "./**/*.{php,yaml,yml,twig,env}", module.Workers[0].Watch[0], "First watch pattern should be the default")
|
||||
require.Equal(t, defaultWatchPattern, module.Workers[0].Watch[0], "First watch pattern should be the default")
|
||||
require.Equal(t, "./src/**/*.php", module.Workers[0].Watch[1], "Second watch pattern should match the configuration")
|
||||
require.Equal(t, "./config/**/*.yaml", module.Workers[0].Watch[2], "Third watch pattern should match the configuration")
|
||||
}
|
||||
|
||||
@@ -30,8 +30,6 @@
|
||||
# Uncomment the following lines to enable Mercure and Vulcain modules
|
||||
#mercure {
|
||||
# # Publisher JWT key
|
||||
# transport_url {$MERCURE_TRANSPORT_URL:bolt:///data/mercure.db}
|
||||
# # Publisher JWT key
|
||||
# publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG}
|
||||
# # Subscriber JWT key
|
||||
# subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG}
|
||||
|
||||
91
caddy/go.mod
91
caddy/go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/dunglas/frankenphp/caddy
|
||||
|
||||
go 1.25.0
|
||||
go 1.25.4
|
||||
|
||||
replace github.com/dunglas/frankenphp => ../
|
||||
|
||||
@@ -10,11 +10,12 @@ require (
|
||||
github.com/caddyserver/caddy/v2 v2.10.2
|
||||
github.com/caddyserver/certmagic v0.25.0
|
||||
github.com/dunglas/caddy-cbrotli v1.0.1
|
||||
github.com/dunglas/frankenphp v1.10.1
|
||||
github.com/dunglas/mercure/caddy v0.21.2
|
||||
github.com/dunglas/frankenphp v1.11.1
|
||||
github.com/dunglas/mercure v0.21.4
|
||||
github.com/dunglas/mercure/caddy v0.21.4
|
||||
github.com/dunglas/vulcain/caddy v1.2.1
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
)
|
||||
|
||||
@@ -22,7 +23,7 @@ require github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.25.1 // indirect
|
||||
cloud.google.com/go/auth v0.17.0 // indirect
|
||||
cloud.google.com/go/auth v0.18.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
@@ -36,15 +37,14 @@ require (
|
||||
github.com/MauriceGit/skiplist v0.0.0-20211105230623-77f5c8d3e145 // indirect
|
||||
github.com/MicahParks/jwkset v0.11.0 // indirect
|
||||
github.com/MicahParks/keyfunc/v3 v3.7.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/RoaringBitmap/roaring/v2 v2.14.4 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.20.0 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.21.0 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||
github.com/caddyserver/zerossl v0.1.3 // indirect
|
||||
github.com/ccoveille/go-safecast v1.8.2 // indirect
|
||||
github.com/ccoveille/go-safecast/v2 v2.0.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
@@ -59,10 +59,10 @@ require (
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/dunglas/httpsfv v1.1.0 // indirect
|
||||
github.com/dunglas/mercure v0.21.2 // indirect
|
||||
github.com/dunglas/skipfilter v1.0.0 // indirect
|
||||
github.com/dunglas/vulcain v1.2.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/e-dant/watcher/watcher-go v0.0.0-20251208164151-f88ec3b7e146 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
@@ -72,8 +72,8 @@ require (
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.3 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.4 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||
@@ -100,7 +100,7 @@ require (
|
||||
github.com/jackc/pgx/v5 v5.7.6 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/libdns/libdns v1.1.1 // indirect
|
||||
@@ -111,7 +111,7 @@ require (
|
||||
github.com/maypok86/otter/v2 v2.2.1 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mholt/acmez/v3 v3.1.4 // indirect
|
||||
github.com/miekg/dns v1.1.68 // indirect
|
||||
github.com/miekg/dns v1.1.69 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
@@ -130,7 +130,7 @@ require (
|
||||
github.com/prometheus/common v0.67.4 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.57.0 // indirect
|
||||
github.com/quic-go/quic-go v0.57.1 // indirect
|
||||
github.com/rs/cors v1.11.1 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
@@ -139,7 +139,7 @@ require (
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/slackhq/nebula v1.9.7 // indirect
|
||||
github.com/smallstep/certificates v0.28.4 // indirect
|
||||
github.com/smallstep/certificates v0.29.0 // indirect
|
||||
github.com/smallstep/cli-utils v0.12.2 // indirect
|
||||
github.com/smallstep/linkedca v0.25.0 // indirect
|
||||
github.com/smallstep/nosql v0.7.0 // indirect
|
||||
@@ -152,7 +152,8 @@ require (
|
||||
github.com/spf13/viper v1.21.0 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.1 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 // indirect
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
|
||||
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
@@ -167,44 +168,44 @@ require (
|
||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||
go.etcd.io/bbolt v1.4.3 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.63.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.38.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.38.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.64.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.39.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.39.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.39.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
go.step.sm/crypto v0.74.0 // indirect
|
||||
go.step.sm/crypto v0.75.0 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.1 // indirect
|
||||
go.uber.org/zap/exp v0.3.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20251119195548-4e0068c0098b // indirect
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/oauth2 v0.33.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20251210140736-7dacc380ba00 // indirect
|
||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/oauth2 v0.34.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/term v0.38.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
google.golang.org/api v0.256.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
google.golang.org/api v0.257.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect
|
||||
google.golang.org/grpc v1.77.0 // indirect
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
howett.net/plist v1.0.1 // indirect
|
||||
|
||||
251
caddy/go.sum
251
caddy/go.sum
@@ -1,9 +1,9 @@
|
||||
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA=
|
||||
cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q=
|
||||
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
|
||||
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
|
||||
cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=
|
||||
cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=
|
||||
cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0=
|
||||
cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
@@ -12,8 +12,8 @@ cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
|
||||
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
|
||||
cloud.google.com/go/kms v1.23.2 h1:4IYDQL5hG4L+HzJBhzejUySoUOheh3Lk5YT4PCyyW6k=
|
||||
cloud.google.com/go/kms v1.23.2/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g=
|
||||
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
|
||||
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
|
||||
cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E=
|
||||
cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
@@ -37,9 +37,6 @@ github.com/MicahParks/jwkset v0.11.0 h1:yc0zG+jCvZpWgFDFmvs8/8jqqVBG9oyIbmBtmjOh
|
||||
github.com/MicahParks/jwkset v0.11.0/go.mod h1:U2oRhRaLgDCLjtpGL2GseNKGmZtLs/3O7p+OZaL5vo0=
|
||||
github.com/MicahParks/keyfunc/v3 v3.7.0 h1:pdafUNyq+p3ZlvjJX1HWFP7MA3+cLpDtg69U3kITJGM=
|
||||
github.com/MicahParks/keyfunc/v3 v3.7.0/go.mod h1:z66bkCviwqfg2YUp+Jcc/xRE9IXLcMq6DrgV/+Htru0=
|
||||
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/RoaringBitmap/roaring/v2 v2.14.4 h1:4aKySrrg9G/5oRtJ3TrZLObVqxgQ9f1znCRBwEwjuVw=
|
||||
@@ -47,44 +44,46 @@ github.com/RoaringBitmap/roaring/v2 v2.14.4/go.mod h1:oMvV6omPWr+2ifRdeZvVJyaz+a
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
|
||||
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
|
||||
github.com/alecthomas/chroma/v2 v2.21.0 h1:YVW9qQAFnQm2OFPPFQg6G/TpMxKSsUr/KUPDi/BEqtY=
|
||||
github.com/alecthomas/chroma/v2 v2.21.0/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||
github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
|
||||
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.5 h1:e/SXuia3rkFtapghJROrydtQpfQaaUgd1cUvyO1mp2w=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.5/go.mod h1:yWSxrnioGUZ4WVv9TgMrNUeLV3PFESn/v+6T/Su8gnM=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.16 h1:E4Tz+tJiPc7kGnXwIfCyUj6xHJNpENlY11oKpRTgsjc=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.16/go.mod h1:2S9hBElpCyGMifv14WxQ7EfPumgoeCPZUpuPX8VtW34=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.20 h1:KFndAnHd9NUuzikHjQ8D5CfFVO+bgELkmcGY8yAw98Q=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.20/go.mod h1:9mCi28a+fmBHSQ0UM79omkz6JtN+PEsvLrnG36uoUv0=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.12 h1:VO3FIM2TDbm0kqp6sFNR0PbioXJb/HzCDW6NtIZpIWE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.12/go.mod h1:6C39gB8kg82tx3r72muZSrNhHia9rjGkX7ORaS2GKNE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.12 h1:p/9flfXdoAnwJnuW9xHEAFY22R3A6skYkW19JFF9F+8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.12/go.mod h1:ZTLHakoVCTtW8AaLGSwJ3LXqHD9uQKnOcv1TrpO6u2k=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.12 h1:2lTWFvRcnWFFLzHWmtddu5MTchc5Oj2OOey++99tPZ0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.12/go.mod h1:hI92pK+ho8HVcWMHKHrK3Uml4pfG7wvL86FzO0LVtQQ=
|
||||
github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc=
|
||||
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.1 h1:iODUDLgk3q8/flEC7ymhmxjfoAnBDwEEYEVyKZ9mzjU=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.1/go.mod h1:xoAgo17AGrPpJBSLg81W+ikM0cpOZG8ad04T2r+d5P0=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.1 h1:JeW+EwmtTE0yXFK8SmklrFh/cGTTXsQJumgMZNlbxfM=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.1/go.mod h1:BOoXiStwTF+fT2XufhO0Efssbi1CNIO/ZXpZu87N0pw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 h1:WZVR5DbDgxzA0BJeudId89Kmgy6DIU4ORpxwsVHz0qA=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14/go.mod h1:Dadl9QO0kHgbrH1GRqGiZdYtW5w+IXXaBNCHTIaheM4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2 h1:xtuxji5CS0JknaXoACOunXOYOQzgfTvGAc9s2QdCJA4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2/go.mod h1:zxwi0DIR0rcRcgdbl7E2MSOvxDyyXGBlScvBkARFaLQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.12 h1:MM8imH7NZ0ovIVX7D2RxfMDv7Jt9OiUXkcQ+GqywA7M=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.12/go.mod h1:gf4OGwdNkbEsb7elw2Sy76odfhwNktWII3WgvQgQQ6w=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.47.0 h1:A97YCVyGz19rRs3+dWf3GpMPflCswgETA9r6/Q0JNSY=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.47.0/go.mod h1:ZJ1ghBt9gQM8JoNscUua1siIgao8w74o3kvdWUU6N/Q=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.0 h1:xHXvxst78wBpJFgDW07xllOx0IAzbryrSdM4nMVQ4Dw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.0/go.mod h1:/e8m+AO6HNPPqMyfKRtzZ9+mBF5/x1Wk8QiDva4m07I=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.4 h1:tBw2Qhf0kj4ZwtsVpDiVRU3zKLvjvjgIjHMKirxXg8M=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.4/go.mod h1:Deq4B7sRM6Awq/xyOBlxBdgW8/Z926KYNNaGMW2lrkA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.0 h1:C+BRMnasSYFcgDw8o9H5hzehKzXyAb9GY5v/8bP9DUY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.39.0/go.mod h1:4EjU+4mIx6+JqKQkruye+CaigV7alL3thVPfDd9VlMs=
|
||||
github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M=
|
||||
github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 h1:FIouAnCE46kyYqyhs0XEBDFFSREtdnr8HQuLPQPLCrY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14/go.mod h1:UTwDc5COa5+guonQU8qBikJo1ZJ4ln2r1MkF7Dqag1E=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.48.0 h1:pQgVxqqNOacqb19+xaoih/wNLil4d8tgi+FxtBi/qQY=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.48.0/go.mod h1:VJcNH6BLr+3VJwinRKdotLOMglHO8mIKlD3ea5c7hbw=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.1 h1:BDgIUYGEo5TkayOWv/oBLPphWwNm/A91AebUjAu5L5g=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.1/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.4 h1:U//SlnkE1wOQiIImxzdY5PXat4Wq+8rlfVEw4Y7J8as=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.4/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.9 h1:LU8S9W/mPDAU9q0FjCLi0TrCheLMGwzbRpvUMwYspcA=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.9/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.1 h1:GdGmKtG+/Krag7VfyOXV17xjTCz0i9NT+JnqLTOI5nA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.1/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso=
|
||||
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
|
||||
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
|
||||
@@ -95,8 +94,8 @@ github.com/caddyserver/certmagic v0.25.0 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx6
|
||||
github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA=
|
||||
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
|
||||
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
||||
github.com/ccoveille/go-safecast v1.8.2 h1:+d+s5UGQiCVJX9oYc8XvYcB2zCMBlax6lIP7YdxXLHA=
|
||||
github.com/ccoveille/go-safecast v1.8.2/go.mod h1:M0Ubpl11x63fE7iOfk5MtngQFXsntcRzOoSsFDqQYDY=
|
||||
github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0=
|
||||
github.com/ccoveille/go-safecast/v2 v2.0.0/go.mod h1:JIYA4CAR33blIDuE6fSwCp2sz1oOBahXnvmdBhOAABs=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
@@ -146,10 +145,10 @@ github.com/dunglas/caddy-cbrotli v1.0.1 h1:mkg7EB1GmoyfBt3kY3mq4o/0bfnBeq7ZLQjmV
|
||||
github.com/dunglas/caddy-cbrotli v1.0.1/go.mod h1:uXABy3tjy1FABF+3JWKVh1ajFvIO/kfpwHaeZGSBaAY=
|
||||
github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54=
|
||||
github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg=
|
||||
github.com/dunglas/mercure v0.21.2 h1:qaLTScSwsCHDps++4AeLWrRp3BysdR5EoHBqu7JNhas=
|
||||
github.com/dunglas/mercure v0.21.2/go.mod h1:3ElA7VwRI8BHUIAVU8oGlvPaqGwsKU5zZVWFNSFg/+U=
|
||||
github.com/dunglas/mercure/caddy v0.21.2 h1:olBXnXbmLvqlawykVIUgjh/yTrp55hUO6NoIQeUX9x4=
|
||||
github.com/dunglas/mercure/caddy v0.21.2/go.mod h1:mnzjwP1r6pYV01XR4euxPkBNwqoB9gJrIgQngscjkqs=
|
||||
github.com/dunglas/mercure v0.21.4 h1:mXPXHfB+4cYfFFCRRDY198mfef5+MQcdCpUnAHBUW2M=
|
||||
github.com/dunglas/mercure v0.21.4/go.mod h1:l/dglCjp/OQx8/quRyceRPx2hqZQ3CNviwoLMRQiJ/k=
|
||||
github.com/dunglas/mercure/caddy v0.21.4 h1:7o+6rDqfwj1EOmXOgfBFsayvJvOUP37xujQHaxuX4ps=
|
||||
github.com/dunglas/mercure/caddy v0.21.4/go.mod h1:EM2s+OVGExbSXObUdAPDwPsbQw4t/icLtQv9CFylDvY=
|
||||
github.com/dunglas/skipfilter v1.0.0 h1:JG9SgGg4n6BlFwuTYzb9RIqjH7PfwszvWehanrYWPF4=
|
||||
github.com/dunglas/skipfilter v1.0.0/go.mod h1:ryhr8j7CAHSjzeN7wI6YEuwoArQ3OQmRqWWVCEAfb9w=
|
||||
github.com/dunglas/vulcain v1.2.1 h1:pkPwvIfoa/xmWSVUyhntbIKT+XO2VFMyhLKv1gA61O8=
|
||||
@@ -159,6 +158,8 @@ github.com/dunglas/vulcain/caddy v1.2.1/go.mod h1:8QrmLTfURmW2VgjTR6Gb9a53FrZjsp
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/e-dant/watcher/watcher-go v0.0.0-20251208164151-f88ec3b7e146 h1:h3vVM6X45PK0mAk8NqiYNQGXTyhvXy1HQ5GhuQN4eeA=
|
||||
github.com/e-dant/watcher/watcher-go v0.0.0-20251208164151-f88ec3b7e146/go.mod h1:sVUOkwtftoj71nnJRG2S0oWNfXFdKpz/M9vK0z06nmM=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
@@ -181,10 +182,10 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.22.3 h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYAsPotrkn8=
|
||||
github.com/go-openapi/jsonpointer v0.22.3/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo=
|
||||
github.com/go-openapi/swag/jsonname v0.25.3 h1:U20VKDS74HiPaLV7UZkztpyVOw3JNVsit+w+gTXRj0A=
|
||||
github.com/go-openapi/swag/jsonname v0.25.3/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
|
||||
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
|
||||
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
|
||||
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
|
||||
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
|
||||
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
@@ -220,8 +221,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-tpm v0.9.7 h1:u89J4tUUeDTlH8xxC3CTW7OHZjbjKoHdQ9W7gCUhtxA=
|
||||
github.com/google/go-tpm v0.9.7/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/go-tpm-tools v0.4.6 h1:hwIwPG7w4z5eQEBq11gYw8YYr9xXLfBQ/0JsKyq5AJM=
|
||||
github.com/google/go-tpm-tools v0.4.6/go.mod h1:MsVQbJnRhKDfWwf5zgr3cDGpj13P1uLAFF0wMEP/n5w=
|
||||
github.com/google/go-tpm-tools v0.4.7 h1:J3ycC8umYxM9A4eF73EofRZu4BxY0jjQnUnkhIBbvws=
|
||||
github.com/google/go-tpm-tools v0.4.7/go.mod h1:gSyXTZHe3fgbzb6WEGd90QucmsnT1SRdlye82gH8QjQ=
|
||||
github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
|
||||
github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
@@ -258,8 +259,8 @@ github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJS
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
@@ -289,8 +290,8 @@ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQ
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/mholt/acmez/v3 v3.1.4 h1:DyzZe/RnAzT3rpZj/2Ii5xZpiEvvYk3cQEN/RmqxwFQ=
|
||||
github.com/mholt/acmez/v3 v3.1.4/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
|
||||
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
@@ -338,8 +339,8 @@ github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE=
|
||||
github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
||||
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
|
||||
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||
@@ -357,15 +358,14 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/slackhq/nebula v1.9.7 h1:v5u46efIyYHGdfjFnozQbRRhMdaB9Ma1SSTcUcE2lfE=
|
||||
github.com/slackhq/nebula v1.9.7/go.mod h1:1+4q4wd3dDAjO8rKCttSb9JIVbklQhuJiBp5I0lbIsQ=
|
||||
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
|
||||
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
|
||||
github.com/smallstep/certificates v0.28.4 h1:JTU6/A5Xes6m+OsR6fw1RACSA362vJc9SOFVG7poBEw=
|
||||
github.com/smallstep/certificates v0.28.4/go.mod h1:LUqo+7mKZE7FZldlTb0zhU4A0bq4G4+akieFMcTaWvA=
|
||||
github.com/smallstep/certificates v0.29.0 h1:f90szTKYTW62bmCc+qE5doGqIGPVxTQb8Ba37e/K8Zs=
|
||||
github.com/smallstep/certificates v0.29.0/go.mod h1:27WI0od6gu84mvE4mYQ/QZGyYwHXvhsiSRNC+y3t+mo=
|
||||
github.com/smallstep/cli-utils v0.12.2 h1:lGzM9PJrH/qawbzMC/s2SvgLdJPKDWKwKzx9doCVO+k=
|
||||
github.com/smallstep/cli-utils v0.12.2/go.mod h1:uCPqefO29goHLGqFnwk0i8W7XJu18X3WHQFRtOm/00Y=
|
||||
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4=
|
||||
@@ -390,8 +390,8 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
@@ -419,8 +419,10 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 h1:uxMgm0C+EjytfAqyfBG55ZONKQ7mvd7x4YYCWsf8QHQ=
|
||||
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
|
||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
|
||||
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 h1:RnBbFMmodYzhC6adOjTbtUQXyzV8dcvKYbolzs6Qch0=
|
||||
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747/go.mod h1:ejPAJui3kVK4u5TgMtqtXlWf5HnKh9fLy5kvpaeuas0=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
@@ -464,36 +466,36 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.63.0 h1:S3+4UwR3Y1tUKklruMwOacAFInNvtuOexz4ZTmJNAyw=
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.63.0/go.mod h1:qpIuOggbbw2T9nKRaO1je/oTRKd4zslAcJonN8LYbTg=
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.38.0 h1:eRZ7asSbLc5dH7+TBzL6hFKb1dabz0IV51uUUwYRZts=
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.38.0/go.mod h1:wXqc9NTGcXapBExHBDVLEZlByu6quiQL8w7Tjgv8TCg=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.38.0 h1:uHsCCOSKl0kLrV2dLkFK+8Ywk9iKa/fptkytc6aFFEo=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.38.0/go.mod h1:wMRSZJZcY8ya9mApLLhwIMjqmApy2o/Ml+62lhvxyHU=
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0 h1:nXGeLvT1QtCAhkASkP/ksjkTKZALIaQBIW+JSIw1KIc=
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.38.0/go.mod h1:oMvOXk78ZR3KEuPMBgp/ThAMDy9ku/eyUVztr+3G6Wo=
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.38.0 h1:k4gSyyohaDXI8F9BDXYC3uO2vr5sRNeQFMsN9Zn0EoI=
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.38.0/go.mod h1:2hDsuiHRO39SRUMhYGqmj64z/IuMRoxE4bBSFR82Lo8=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.64.0 h1:VVrb1ErDD0Tlh/0K0rUqjky1e8AekjspTFN9sU2ekaA=
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.64.0/go.mod h1:QCsOQk+9Ep8Mkp4/aPtSzUT0dc8SaPYzBAE6o1jYuSE=
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.39.0 h1:IvNR8pAVGpkK1CHMjU/YE6B6TlnAPGFvogkMWRWU6wo=
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.39.0/go.mod h1:TUsFCERuGM4IGhJG9w+9l0nzmHUKHuaDYYNF6mtNgjY=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.39.0 h1:PI7pt9pkSnimWcp5sQhUA9OzLbc3Ba4sL+VEUTNsxrk=
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.39.0/go.mod h1:5gV/EzPnfYIwjzj+6y8tbGW2PKWhcsz5e/7twptRVQY=
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.39.0 h1:Gz3yKzfMSEFzF0Vy5eIpu9ndpo4DhXMCxsLMF0OOApo=
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.39.0/go.mod h1:2D/cxxCqTlrday0rZrPujjg5aoAdqk1NaNyoXn8FJn8=
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.39.0 h1:vKTve1W/WKPVp1fzJamhCDDECt+5upJJ65bPyWoddGg=
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.39.0/go.mod h1:FH5VB2N19duNzh1Q8ks6CsZFyu3LFhNLiA9lPxyEkvU=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
go.step.sm/crypto v0.74.0 h1:/APBEv45yYR4qQFg47HA8w1nesIGcxh44pGyQNw6JRA=
|
||||
go.step.sm/crypto v0.74.0/go.mod h1:UoXqCAJjjRgzPte0Llaqen7O9P7XjPmgjgTHQGkKCDk=
|
||||
go.step.sm/crypto v0.75.0 h1:UAHYD6q6ggYyzLlIKHv1MCUVjZIesXRZpGTlRC/HSHw=
|
||||
go.step.sm/crypto v0.75.0/go.mod h1:wwQ57+ajmDype9mrI/2hRyrvJd7yja5xVgWYqpUN3PE=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
@@ -517,19 +519,19 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20251119195548-4e0068c0098b h1:VI77LRI9gm150dbLwyi9yxd2VxVCm4mFzrZqkz7ahFo=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20251119195548-4e0068c0098b/go.mod h1:MEIPiCnxvQEjA4astfaKItNwEVZA5Ki+3+nyGbJ5N18=
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 h1:zfMcR1Cs4KNuomFFgGefv5N0czO2XZpUbxGUy8i8ug0=
|
||||
golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20251210140736-7dacc380ba00 h1:qObov2/X4yIpr98j5t6samg3mMF12Rl4taUJd1rWj+c=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20251210140736-7dacc380ba00/go.mod h1:MEIPiCnxvQEjA4astfaKItNwEVZA5Ki+3+nyGbJ5N18=
|
||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM=
|
||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
@@ -538,10 +540,10 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -549,13 +551,12 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -569,8 +570,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -580,8 +581,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
@@ -591,8 +592,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -601,25 +602,25 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=
|
||||
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
|
||||
google.golang.org/api v0.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA=
|
||||
google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4=
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0 h1:6Al3kEFFP9VJhRz3DID6quisgPnTeZVr4lep9kkxdPA=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.0/go.mod h1:QLvsjh0OIR0TYBeiu2bkWGTJBUNQ64st52iWj/yA93I=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
|
||||
20
caddy/hotreload-skip.go
Normal file
20
caddy/hotreload-skip.go
Normal file
@@ -0,0 +1,20 @@
|
||||
//go:build nowatcher || nomercure
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
)
|
||||
|
||||
type hotReloadContext struct {
|
||||
}
|
||||
|
||||
func (_ *FrankenPHPModule) configureHotReload(_ *FrankenPHPApp) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (_ *FrankenPHPModule) unmarshalHotReload(d *caddyfile.Dispenser) error {
|
||||
return errors.New("hot reload support disabled")
|
||||
}
|
||||
104
caddy/hotreload.go
Normal file
104
caddy/hotreload.go
Normal file
@@ -0,0 +1,104 @@
|
||||
//go:build !nowatcher && !nomercure
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"net/url"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
const defaultHotReloadPattern = "./**/*.{css,env,gif,htm,html,jpg,jpeg,js,mjs,php,png,svg,twig,webp,xml,yaml,yml}"
|
||||
|
||||
type hotReloadContext struct {
|
||||
// HotReload specifies files to watch for file changes to trigger hot reloads updates. Supports the glob syntax.
|
||||
HotReload *hotReloadConfig `json:"hot_reload,omitempty"`
|
||||
}
|
||||
|
||||
type hotReloadConfig struct {
|
||||
Topic string `json:"topic"`
|
||||
Watch []string `json:"watch"`
|
||||
}
|
||||
|
||||
func (f *FrankenPHPModule) configureHotReload(app *FrankenPHPApp) error {
|
||||
if f.HotReload == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if f.mercureHub == nil {
|
||||
return errors.New("unable to enable hot reloading: no Mercure hub configured")
|
||||
}
|
||||
|
||||
if len(f.HotReload.Watch) == 0 {
|
||||
f.HotReload.Watch = []string{defaultHotReloadPattern}
|
||||
}
|
||||
|
||||
if f.HotReload.Topic == "" {
|
||||
uid, err := uniqueID(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.HotReload.Topic = "https://frankenphp.dev/hot-reload/" + uid
|
||||
}
|
||||
|
||||
app.opts = append(app.opts, frankenphp.WithHotReload(f.HotReload.Topic, f.mercureHub, f.HotReload.Watch))
|
||||
f.preparedEnv["FRANKENPHP_HOT_RELOAD\x00"] = "/.well-known/mercure?topic=" + url.QueryEscape(f.HotReload.Topic)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FrankenPHPModule) unmarshalHotReload(d *caddyfile.Dispenser) error {
|
||||
f.HotReload = &hotReloadConfig{
|
||||
Watch: d.RemainingArgs(),
|
||||
}
|
||||
|
||||
for d.NextBlock(1) {
|
||||
switch v := d.Val(); v {
|
||||
case "topic":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
|
||||
if f.HotReload == nil {
|
||||
f.HotReload = &hotReloadConfig{}
|
||||
}
|
||||
|
||||
f.HotReload.Topic = d.Val()
|
||||
|
||||
case "watch":
|
||||
patterns := d.RemainingArgs()
|
||||
if len(patterns) == 0 {
|
||||
return d.ArgErr()
|
||||
}
|
||||
|
||||
f.HotReload.Watch = append(f.HotReload.Watch, patterns...)
|
||||
|
||||
default:
|
||||
return wrongSubDirectiveError("hot_reload", "topic, watch", v)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func uniqueID(s any) (string, error) {
|
||||
var b bytes.Buffer
|
||||
|
||||
if err := gob.NewEncoder(&b).Encode(s); err != nil {
|
||||
return "", fmt.Errorf("unable to generate unique name: %w", err)
|
||||
}
|
||||
|
||||
h := fnv.New64a()
|
||||
if _, err := h.Write(b.Bytes()); err != nil {
|
||||
return "", fmt.Errorf("unable to generate unique name: %w", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%016x", h.Sum64()), nil
|
||||
}
|
||||
88
caddy/hotreload_test.go
Normal file
88
caddy/hotreload_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
//go:build !nowatcher && !nomercure
|
||||
|
||||
package caddy_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddytest"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHotReload(t *testing.T) {
|
||||
const topic = "https://frankenphp.dev/hot-reload/test"
|
||||
|
||||
u := "/.well-known/mercure?topic=" + url.QueryEscape(topic)
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
indexFile := filepath.Join(tmpDir, "index.php")
|
||||
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(`
|
||||
{
|
||||
debug
|
||||
skip_install_trust
|
||||
admin localhost:2999
|
||||
}
|
||||
|
||||
http://localhost:`+testPort+` {
|
||||
mercure {
|
||||
transport local
|
||||
subscriber_jwt TestKey
|
||||
anonymous
|
||||
}
|
||||
|
||||
php_server {
|
||||
root `+tmpDir+`
|
||||
hot_reload {
|
||||
topic `+topic+`
|
||||
watch `+tmpDir+`/*.php
|
||||
}
|
||||
}
|
||||
`, "caddyfile")
|
||||
|
||||
var connected, received sync.WaitGroup
|
||||
|
||||
connected.Add(1)
|
||||
received.Go(func() {
|
||||
cx, cancel := context.WithCancel(t.Context())
|
||||
req, _ := http.NewRequest(http.MethodGet, "http://localhost:"+testPort+u, nil)
|
||||
req = req.WithContext(cx)
|
||||
resp := tester.AssertResponseCode(req, http.StatusOK)
|
||||
|
||||
connected.Done()
|
||||
|
||||
var receivedBody strings.Builder
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
_, err := resp.Body.Read(buf)
|
||||
require.NoError(t, err)
|
||||
|
||||
receivedBody.Write(buf)
|
||||
|
||||
if strings.Contains(receivedBody.String(), "index.php") {
|
||||
cancel()
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
require.NoError(t, resp.Body.Close())
|
||||
})
|
||||
|
||||
connected.Wait()
|
||||
|
||||
require.NoError(t, os.WriteFile(indexFile, []byte("<?=$_SERVER['FRANKENPHP_HOT_RELOAD'];"), 0644))
|
||||
|
||||
received.Wait()
|
||||
|
||||
tester.AssertGetResponse("http://localhost:"+testPort+"/index.php", http.StatusOK, u)
|
||||
}
|
||||
@@ -2,9 +2,12 @@
|
||||
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func (f *FrankenPHPModule) assignMercureHubRequestOption(_ caddy.Context) {
|
||||
type mercureContext struct {
|
||||
}
|
||||
|
||||
func (f *FrankenPHPModule) configureHotReload(_ *FrankenPHPApp) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FrankenPHPModule) assignMercureHub(_ caddy.Context) {
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package caddy
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/dunglas/frankenphp"
|
||||
"github.com/dunglas/mercure"
|
||||
mercureCaddy "github.com/dunglas/mercure/caddy"
|
||||
)
|
||||
|
||||
@@ -12,9 +13,22 @@ func init() {
|
||||
mercureCaddy.AllowNoPublish = true
|
||||
}
|
||||
|
||||
func (f *FrankenPHPModule) assignMercureHubRequestOption(ctx caddy.Context) {
|
||||
if hub := mercureCaddy.FindHub(ctx.Modules()); hub != nil {
|
||||
opt := frankenphp.WithMercureHub(hub)
|
||||
f.mercureHubRequestOption = &opt
|
||||
type mercureContext struct {
|
||||
mercureHub *mercure.Hub
|
||||
}
|
||||
|
||||
func (f *FrankenPHPModule) assignMercureHub(ctx caddy.Context) {
|
||||
if f.mercureHub = mercureCaddy.FindHub(ctx.Modules()); f.mercureHub == nil {
|
||||
return
|
||||
}
|
||||
|
||||
opt := frankenphp.WithMercureHub(f.mercureHub)
|
||||
f.mercureHubRequestOption = &opt
|
||||
|
||||
for i, wc := range f.Workers {
|
||||
wc.mercureHub = f.mercureHub
|
||||
wc.options = append(wc.options, frankenphp.WithWorkerMercureHub(wc.mercureHub))
|
||||
|
||||
f.Workers[i] = wc
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,9 @@ var serverHeader = []string{"FrankenPHP Caddy"}
|
||||
// }
|
||||
// }
|
||||
type FrankenPHPModule struct {
|
||||
mercureContext
|
||||
hotReloadContext
|
||||
|
||||
// Root sets the root folder to the site. Default: `root` directive, or the path of the public directory of the embed app it exists.
|
||||
Root string `json:"root,omitempty"`
|
||||
// SplitPath sets the substrings for splitting the URI into two parts. The first matching substring will be used to split the "path info" from the path. The first piece is suffixed with the matching substring and will be assumed as the actual resource (CGI script) name. The second piece will be set to PATH_INFO for the CGI script to use. Default: `.php`.
|
||||
@@ -74,8 +77,9 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
|
||||
return fmt.Errorf(`expected ctx.App("frankenphp") to return *FrankenPHPApp, got nil`)
|
||||
}
|
||||
|
||||
f.assignMercureHubRequestOption(ctx)
|
||||
f.assignMercureHub(ctx)
|
||||
|
||||
loggerOpt := frankenphp.WithRequestLogger(f.logger)
|
||||
for i, wc := range f.Workers {
|
||||
// make the file path absolute from the public directory
|
||||
// this can only be done if the root is defined inside php_server
|
||||
@@ -88,11 +92,7 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
|
||||
wc.inheritEnv(f.Env)
|
||||
}
|
||||
|
||||
wc.requestOptions = []frankenphp.RequestOption{frankenphp.WithRequestLogger(f.logger)}
|
||||
if f.mercureHubRequestOption != nil {
|
||||
wc.requestOptions = append(wc.requestOptions, *f.mercureHubRequestOption)
|
||||
}
|
||||
|
||||
wc.requestOptions = append(wc.requestOptions, loggerOpt)
|
||||
f.Workers[i] = wc
|
||||
}
|
||||
|
||||
@@ -106,14 +106,13 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
|
||||
if frankenphp.EmbeddedAppPath == "" {
|
||||
f.Root = "{http.vars.root}"
|
||||
} else {
|
||||
rrs := false
|
||||
f.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
|
||||
|
||||
var rrs bool
|
||||
f.ResolveRootSymlink = &rrs
|
||||
}
|
||||
} else {
|
||||
if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(f.Root) {
|
||||
f.Root = filepath.Join(frankenphp.EmbeddedAppPath, f.Root)
|
||||
}
|
||||
} else if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(f.Root) {
|
||||
f.Root = filepath.Join(frankenphp.EmbeddedAppPath, f.Root)
|
||||
}
|
||||
|
||||
if len(f.SplitPath) == 0 {
|
||||
@@ -154,6 +153,10 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := f.configureHotReload(fapp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -164,11 +167,15 @@ func needReplacement(s string) bool {
|
||||
|
||||
// ServeHTTP implements caddyhttp.MiddlewareHandler.
|
||||
func (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error {
|
||||
origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
|
||||
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
||||
ctx := r.Context()
|
||||
origReq := ctx.Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
|
||||
repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
||||
|
||||
var (
|
||||
documentRootOption frankenphp.RequestOption
|
||||
documentRoot string
|
||||
)
|
||||
|
||||
var documentRootOption frankenphp.RequestOption
|
||||
var documentRoot string
|
||||
if f.resolvedDocumentRoot == "" {
|
||||
documentRoot = repl.ReplaceKnown(f.Root, "")
|
||||
if documentRoot == "" && frankenphp.EmbeddedAppPath != "" {
|
||||
@@ -278,14 +285,20 @@ func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
f.ResolveRootSymlink = &v
|
||||
|
||||
case "worker":
|
||||
wc, err := parseWorkerConfig(d)
|
||||
wc, err := unmarshalWorker(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.Workers = append(f.Workers, wc)
|
||||
|
||||
case "hot_reload":
|
||||
if err := f.unmarshalHotReload(d); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
return wrongSubDirectiveError("php or php_server", "root, split, env, resolve_root_symlink, worker", d.Val())
|
||||
return wrongSubDirectiveError("php or php_server", "hot_reload, name, root, split, env, resolve_root_symlink, worker", d.Val())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -294,7 +307,7 @@ func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
fileNames := make(map[string]struct{}, len(f.Workers))
|
||||
for _, w := range f.Workers {
|
||||
if _, ok := fileNames[w.FileName]; ok {
|
||||
return fmt.Errorf(`workers in a single "php_server" block must not have duplicate filenames: %q`, w.FileName)
|
||||
return fmt.Errorf(`workers in a single "php" or "php_server" block must not have duplicate filenames: %q`, w.FileName)
|
||||
}
|
||||
|
||||
if len(w.MatchPath) == 0 {
|
||||
|
||||
@@ -22,6 +22,8 @@ import (
|
||||
// }
|
||||
// }
|
||||
type workerConfig struct {
|
||||
mercureContext
|
||||
|
||||
// Name for the worker. Default: the filename for FrankenPHPApp workers, always prefixed with "m#" for FrankenPHPModule workers.
|
||||
Name string `json:"name,omitempty"`
|
||||
// FileName sets the path to the worker script.
|
||||
@@ -39,10 +41,11 @@ type workerConfig struct {
|
||||
// MaxConsecutiveFailures sets the maximum number of consecutive failures before panicking (defaults to 6, set to -1 to never panick)
|
||||
MaxConsecutiveFailures int `json:"max_consecutive_failures,omitempty"`
|
||||
|
||||
options []frankenphp.WorkerOption
|
||||
requestOptions []frankenphp.RequestOption
|
||||
}
|
||||
|
||||
func parseWorkerConfig(d *caddyfile.Dispenser) (workerConfig, error) {
|
||||
func unmarshalWorker(d *caddyfile.Dispenser) (workerConfig, error) {
|
||||
wc := workerConfig{}
|
||||
if d.NextArg() {
|
||||
wc.FileName = d.Val()
|
||||
@@ -66,8 +69,7 @@ func parseWorkerConfig(d *caddyfile.Dispenser) (workerConfig, error) {
|
||||
}
|
||||
|
||||
for d.NextBlock(1) {
|
||||
v := d.Val()
|
||||
switch v {
|
||||
switch v := d.Val(); v {
|
||||
case "name":
|
||||
if !d.NextArg() {
|
||||
return wc, d.ArgErr()
|
||||
@@ -110,11 +112,12 @@ func parseWorkerConfig(d *caddyfile.Dispenser) (workerConfig, error) {
|
||||
}
|
||||
wc.Env[args[0]] = args[1]
|
||||
case "watch":
|
||||
if !d.NextArg() {
|
||||
patterns := d.RemainingArgs()
|
||||
if len(patterns) == 0 {
|
||||
// the default if the watch directory is left empty:
|
||||
wc.Watch = append(wc.Watch, defaultWatchPattern)
|
||||
} else {
|
||||
wc.Watch = append(wc.Watch, d.Val())
|
||||
wc.Watch = append(wc.Watch, patterns...)
|
||||
}
|
||||
case "match":
|
||||
// provision the path so it's identical to Caddy match rules
|
||||
|
||||
@@ -14,6 +14,10 @@ variable "GO_VERSION" {
|
||||
default = "1.25"
|
||||
}
|
||||
|
||||
variable "SPC_OPT_BUILD_ARGS" {
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "SHA" {}
|
||||
|
||||
variable "LATEST" {
|
||||
@@ -146,6 +150,7 @@ target "static-builder-musl" {
|
||||
args = {
|
||||
FRANKENPHP_VERSION = VERSION
|
||||
CI = CI
|
||||
SPC_OPT_BUILD_ARGS = SPC_OPT_BUILD_ARGS
|
||||
}
|
||||
secret = ["id=github-token,env=GITHUB_TOKEN"]
|
||||
}
|
||||
@@ -171,6 +176,7 @@ target "static-builder-gnu" {
|
||||
FRANKENPHP_VERSION = VERSION
|
||||
GO_VERSION = GO_VERSION
|
||||
CI = CI
|
||||
SPC_OPT_BUILD_ARGS = SPC_OPT_BUILD_ARGS
|
||||
}
|
||||
secret = ["id=github-token,env=GITHUB_TOKEN"]
|
||||
}
|
||||
|
||||
@@ -53,13 +53,12 @@ RUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini
|
||||
FrankenPHP:
|
||||
|
||||
- `/etc/frankenphp/Caddyfile`: the main configuration file
|
||||
- `/etc/frankenphp/caddy.d/*.caddy`: additional configuration files that are loaded automatically
|
||||
- `/etc/frankenphp/Caddyfile.d/*.caddyfile`: additional configuration files that are loaded automatically
|
||||
|
||||
PHP:
|
||||
|
||||
- `php.ini`: `/etc/frankenphp/php.ini` (a `php.ini` file with production presets is provided by default)
|
||||
- additional configuration files: `/etc/frankenphp/php.d/*.ini`
|
||||
- PHP extensions: `/usr/lib/frankenphp/modules/`
|
||||
- `php.ini`: `/etc/php-zts/php.ini` (a `php.ini` file with production presets is provided by default)
|
||||
- additional configuration files: `/etc/php-zts/conf.d/*.ini`
|
||||
|
||||
## Static binary
|
||||
|
||||
|
||||
@@ -88,19 +88,20 @@ While some variable types have the same memory representation between C/PHP and
|
||||
This table summarizes what you need to know:
|
||||
|
||||
| PHP type | Go type | Direct conversion | C to Go helper | Go to C helper | Class Methods Support |
|
||||
| ------------------ | ----------------------------- | ----------------- | --------------------------------- | ---------------------------------- | --------------------- |
|
||||
| `int` | `int64` | ✅ | - | - | ✅ |
|
||||
| `?int` | `*int64` | ✅ | - | - | ✅ |
|
||||
| `float` | `float64` | ✅ | - | - | ✅ |
|
||||
| `?float` | `*float64` | ✅ | - | - | ✅ |
|
||||
| `bool` | `bool` | ✅ | - | - | ✅ |
|
||||
| `?bool` | `*bool` | ✅ | - | - | ✅ |
|
||||
| `string`/`?string` | `*C.zend_string` | ❌ | `frankenphp.GoString()` | `frankenphp.PHPString()` | ✅ |
|
||||
| `array` | `frankenphp.AssociativeArray` | ❌ | `frankenphp.GoAssociativeArray()` | `frankenphp.PHPAssociativeArray()` | ✅ |
|
||||
| `array` | `map[string]any` | ❌ | `frankenphp.GoMap()` | `frankenphp.PHPMap()` | ✅ |
|
||||
| `array` | `[]any` | ❌ | `frankenphp.GoPackedArray()` | `frankenphp.PHPPackedArray()` | ✅ |
|
||||
| `mixed` | `any` | ❌ | `GoValue()` | `PHPValue()` | ❌ |
|
||||
| `object` | `struct` | ❌ | _Not yet implemented_ | _Not yet implemented_ | ❌ |
|
||||
|--------------------|-------------------------------|-------------------|-----------------------------------|------------------------------------|-----------------------|
|
||||
| `int` | `int64` | ✅ | - | - | ✅ |
|
||||
| `?int` | `*int64` | ✅ | - | - | ✅ |
|
||||
| `float` | `float64` | ✅ | - | - | ✅ |
|
||||
| `?float` | `*float64` | ✅ | - | - | ✅ |
|
||||
| `bool` | `bool` | ✅ | - | - | ✅ |
|
||||
| `?bool` | `*bool` | ✅ | - | - | ✅ |
|
||||
| `string`/`?string` | `*C.zend_string` | ❌ | `frankenphp.GoString()` | `frankenphp.PHPString()` | ✅ |
|
||||
| `array` | `frankenphp.AssociativeArray` | ❌ | `frankenphp.GoAssociativeArray()` | `frankenphp.PHPAssociativeArray()` | ✅ |
|
||||
| `array` | `map[string]any` | ❌ | `frankenphp.GoMap()` | `frankenphp.PHPMap()` | ✅ |
|
||||
| `array` | `[]any` | ❌ | `frankenphp.GoPackedArray()` | `frankenphp.PHPPackedArray()` | ✅ |
|
||||
| `mixed` | `any` | ❌ | `GoValue()` | `PHPValue()` | ❌ |
|
||||
| `callable` | `*C.zval` | ❌ | - | frankenphp.CallPHPCallable() | ❌ |
|
||||
| `object` | `struct` | ❌ | _Not yet implemented_ | _Not yet implemented_ | ❌ |
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
@@ -132,7 +133,7 @@ import (
|
||||
)
|
||||
|
||||
// export_php:function process_data_ordered(array $input): array
|
||||
func process_data_ordered_map(arr *C.zval) unsafe.Pointer {
|
||||
func process_data_ordered_map(arr *C.zend_array) unsafe.Pointer {
|
||||
// Convert PHP associative array to Go while keeping the order
|
||||
associativeArray, err := frankenphp.GoAssociativeArray[any](unsafe.Pointer(arr))
|
||||
if err != nil {
|
||||
@@ -157,7 +158,7 @@ func process_data_ordered_map(arr *C.zval) unsafe.Pointer {
|
||||
}
|
||||
|
||||
// export_php:function process_data_unordered(array $input): array
|
||||
func process_data_unordered_map(arr *C.zval) unsafe.Pointer {
|
||||
func process_data_unordered_map(arr *C.zend_array) unsafe.Pointer {
|
||||
// Convert PHP associative array to a Go map without keeping the order
|
||||
// ignoring the order will be more performant
|
||||
goMap, err := frankenphp.GoMap[any](unsafe.Pointer(arr))
|
||||
@@ -178,7 +179,7 @@ func process_data_unordered_map(arr *C.zval) unsafe.Pointer {
|
||||
}
|
||||
|
||||
// export_php:function process_data_packed(array $input): array
|
||||
func process_data_packed(arr *C.zval) unsafe.Pointer {
|
||||
func process_data_packed(arr *C.zend_array) unsafe.Pointer {
|
||||
// Convert PHP packed array to Go
|
||||
goSlice, err := frankenphp.GoPackedArray(unsafe.Pointer(arr))
|
||||
if err != nil {
|
||||
@@ -211,6 +212,43 @@ func process_data_packed(arr *C.zval) unsafe.Pointer {
|
||||
- `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray` - Convert a PHP array to an ordered Go `AssociativeArray` (map with order)
|
||||
- `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convert a PHP array to an unordered Go map
|
||||
- `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Convert a PHP array to a Go slice
|
||||
- `frankenphp.IsPacked(zval *C.zend_array) bool` - Check if a PHP array is packed (indexed only) or associative (key-value pairs)
|
||||
|
||||
### Working with Callables
|
||||
|
||||
FrankenPHP provides a way to work with PHP callables using the `frankenphp.CallPHPCallable` helper. This allows you to call PHP functions or methods from Go code.
|
||||
|
||||
To showcase this, let's create our own `array_map()` function that takes a callable and an array, applies the callable to each element of the array, and returns a new array with the results:
|
||||
|
||||
```go
|
||||
// export_php:function my_array_map(array $data, callable $callback): array
|
||||
func my_array_map(arr *C.zend_array, callback *C.zval) unsafe.Pointer {
|
||||
goSlice, err := frankenphp.GoPackedArray[any](unsafe.Pointer(arr))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
result := make([]any, len(goSlice))
|
||||
|
||||
for index, value := range goSlice {
|
||||
result[index] = frankenphp.CallPHPCallable(unsafe.Pointer(callback), []interface{}{value})
|
||||
}
|
||||
|
||||
return frankenphp.PHPPackedArray(result)
|
||||
}
|
||||
```
|
||||
|
||||
Notice how we use `frankenphp.CallPHPCallable()` to call the PHP callable passed as a parameter. This function takes a pointer to the callable and an array of arguments, and it returns the result of the callable execution. You can use the callable syntax you're used to:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
$result = my_array_map([1, 2, 3], function($x) { return $x * 2; });
|
||||
// $result will be [2, 4, 6]
|
||||
|
||||
$result = my_array_map(['hello', 'world'], 'strtoupper');
|
||||
// $result will be ['HELLO', 'WORLD']
|
||||
```
|
||||
|
||||
### Declaring a Native PHP Class
|
||||
|
||||
|
||||
@@ -209,6 +209,43 @@ func process_data_packed(arr *C.zval) unsafe.Pointer {
|
||||
- `frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool) frankenphp.AssociativeArray` - Convertir un tableau PHP vers un `AssociativeArray` Go ordonné (map avec ordre)
|
||||
- `frankenphp.GoMap(arr unsafe.Pointer) map[string]any` - Convertir un tableau PHP vers une map Go non ordonnée
|
||||
- `frankenphp.GoPackedArray(arr unsafe.Pointer) []any` - Convertir un tableau PHP vers un slice Go
|
||||
- `frankenphp.IsPacked(zval *C.zend_array) bool` - Vérifie si le tableau PHP est une liste ou un tableau associatif
|
||||
|
||||
### Travailler avec des Callables
|
||||
|
||||
FrankenPHP propose un moyen de travailler avec les _callables_ PHP grâce au helper `frankenphp.CallPHPCallable()`. Cela permet d'appeler des fonctions ou des méthodes PHP depuis du code Go.
|
||||
|
||||
Pour illustrer cela, créons notre propre fonction `array_map()` qui prend un _callable_ et un tableau, applique le _callable_ à chaque élément du tableau, et retourne un nouveau tableau avec les résultats :
|
||||
|
||||
```go
|
||||
// export_php:function my_array_map(array $data, callable $callback): array
|
||||
func my_array_map(arr *C.zend_array, callback *C.zval) unsafe.Pointer {
|
||||
goSlice, err := frankenphp.GoPackedArray[any](unsafe.Pointer(arr))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
result := make([]any, len(goSlice))
|
||||
|
||||
for index, value := range goSlice {
|
||||
result[index] = frankenphp.CallPHPCallable(unsafe.Pointer(callback), []interface{}{value})
|
||||
}
|
||||
|
||||
return frankenphp.PHPPackedArray(result)
|
||||
}
|
||||
```
|
||||
|
||||
Remarquez comment nous utilisons `frankenphp.CallPHPCallable()` pour appeler le _callable_ PHP passé en paramètre. Cette fonction prend un pointeur vers le _callable_ et un tableau d'arguments, et elle retourne le résultat de l'exécution du _callable_. Vous pouvez utiliser la syntaxe habituelle des _callables_ :
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
$result = my_array_map([1, 2, 3], function($x) { return $x * 2; });
|
||||
// $result vaudra [2, 4, 6]
|
||||
|
||||
$result = my_array_map(['hello', 'world'], 'strtoupper');
|
||||
// $result vaudra ['HELLO', 'WORLD']
|
||||
```
|
||||
|
||||
### Déclarer une Classe PHP Native
|
||||
|
||||
|
||||
@@ -37,6 +37,9 @@ frankenphp php-server --worker /path/to/your/worker/script.php --watch="/path/to
|
||||
|
||||
## Runtime Symfony
|
||||
|
||||
> [!TIP]
|
||||
> La section suivante est nécessaire uniquement avant Symfony 7.4, où le support natif du mode worker de FrankenPHP a été introduit.
|
||||
|
||||
Le mode worker de FrankenPHP est pris en charge par le [Composant Runtime de Symfony](https://symfony.com/doc/current/components/runtime.html).
|
||||
Pour démarrer une application Symfony dans un worker, installez le package FrankenPHP de [PHP Runtime](https://github.com/php-runtime/runtime) :
|
||||
|
||||
|
||||
@@ -37,6 +37,9 @@ frankenphp php-server --worker /path/to/your/worker/script.php --watch="/path/to
|
||||
|
||||
## Symfony Runtime
|
||||
|
||||
> [!TIP]
|
||||
> The following section is only necessary prior to Symfony 7.4, where native support for FrankenPHP worker mode was introduced.
|
||||
|
||||
The worker mode of FrankenPHP is supported by the [Symfony Runtime Component](https://symfony.com/doc/current/components/runtime.html).
|
||||
To start any Symfony application in a worker, install the FrankenPHP package of [PHP Runtime](https://github.com/php-runtime/runtime):
|
||||
|
||||
|
||||
23
frankenphp.c
23
frankenphp.c
@@ -549,7 +549,30 @@ PHP_FUNCTION(mercure_publish) {
|
||||
RETURN_THROWS();
|
||||
}
|
||||
|
||||
PHP_FUNCTION(frankenphp_log) {
|
||||
zend_string *message = NULL;
|
||||
zend_long level = 0;
|
||||
zval *context = NULL;
|
||||
|
||||
ZEND_PARSE_PARAMETERS_START(1, 3)
|
||||
Z_PARAM_STR(message)
|
||||
Z_PARAM_OPTIONAL
|
||||
Z_PARAM_LONG(level)
|
||||
Z_PARAM_ARRAY(context)
|
||||
ZEND_PARSE_PARAMETERS_END();
|
||||
|
||||
char *ret = NULL;
|
||||
ret = go_log_attrs(thread_index, message, level, context);
|
||||
if (ret != NULL) {
|
||||
zend_throw_exception(spl_ce_RuntimeException, ret, 0);
|
||||
free(ret);
|
||||
RETURN_THROWS();
|
||||
}
|
||||
}
|
||||
|
||||
PHP_MINIT_FUNCTION(frankenphp) {
|
||||
register_frankenphp_symbols(module_number);
|
||||
|
||||
zend_function *func;
|
||||
|
||||
// Override putenv
|
||||
|
||||
@@ -251,6 +251,7 @@ func Init(options ...Option) error {
|
||||
opt := &opt{}
|
||||
for _, o := range options {
|
||||
if err := o(opt); err != nil {
|
||||
Shutdown()
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -277,6 +278,7 @@ func Init(options ...Option) error {
|
||||
|
||||
workerThreadCount, err := calculateMaxThreads(opt)
|
||||
if err != nil {
|
||||
Shutdown()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -285,6 +287,7 @@ func Init(options ...Option) error {
|
||||
config := Config()
|
||||
|
||||
if config.Version.MajorVersion < 8 || (config.Version.MajorVersion == 8 && config.Version.MinorVersion < 2) {
|
||||
Shutdown()
|
||||
return ErrInvalidPHPVersion
|
||||
}
|
||||
|
||||
@@ -304,6 +307,7 @@ func Init(options ...Option) error {
|
||||
|
||||
mainThread, err := initPHPThreads(opt.numThreads, opt.maxThreads, opt.phpIni)
|
||||
if err != nil {
|
||||
Shutdown()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -314,6 +318,13 @@ func Init(options ...Option) error {
|
||||
}
|
||||
|
||||
if err := initWorkers(opt.workers); err != nil {
|
||||
Shutdown()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if err := initWatchers(opt); err != nil {
|
||||
Shutdown()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -352,7 +363,7 @@ func Shutdown() {
|
||||
fn()
|
||||
}
|
||||
|
||||
drainWatcher()
|
||||
drainWatchers()
|
||||
drainAutoScaling()
|
||||
drainPHPThreads()
|
||||
|
||||
@@ -649,9 +660,28 @@ func go_read_cookies(threadIndex C.uintptr_t) *C.char {
|
||||
return C.CString(cookie)
|
||||
}
|
||||
|
||||
func getLogger(threadIndex C.uintptr_t) (*slog.Logger, context.Context) {
|
||||
ctxHolder := phpThreads[threadIndex]
|
||||
if ctxHolder == nil {
|
||||
return globalLogger, globalCtx
|
||||
}
|
||||
|
||||
ctx := ctxHolder.context()
|
||||
if ctxHolder.handler == nil {
|
||||
return globalLogger, ctx
|
||||
}
|
||||
|
||||
fCtx := ctxHolder.frankenPHPContext()
|
||||
if fCtx == nil || fCtx.logger == nil {
|
||||
return globalLogger, ctx
|
||||
}
|
||||
|
||||
return fCtx.logger, ctx
|
||||
}
|
||||
|
||||
//export go_log
|
||||
func go_log(threadIndex C.uintptr_t, message *C.char, level C.int) {
|
||||
ctx := phpThreads[threadIndex].context()
|
||||
logger, ctx := getLogger(threadIndex)
|
||||
|
||||
m := C.GoString(message)
|
||||
le := syslogLevelInfo
|
||||
@@ -662,26 +692,62 @@ func go_log(threadIndex C.uintptr_t, message *C.char, level C.int) {
|
||||
|
||||
switch le {
|
||||
case syslogLevelEmerg, syslogLevelAlert, syslogLevelCrit, syslogLevelErr:
|
||||
if globalLogger.Enabled(ctx, slog.LevelError) {
|
||||
globalLogger.LogAttrs(ctx, slog.LevelError, m, slog.String("syslog_level", syslogLevel(level).String()))
|
||||
if logger.Enabled(ctx, slog.LevelError) {
|
||||
logger.LogAttrs(ctx, slog.LevelError, m, slog.String("syslog_level", le.String()))
|
||||
}
|
||||
|
||||
case syslogLevelWarn:
|
||||
if globalLogger.Enabled(ctx, slog.LevelWarn) {
|
||||
globalLogger.LogAttrs(ctx, slog.LevelWarn, m, slog.String("syslog_level", syslogLevel(level).String()))
|
||||
if logger.Enabled(ctx, slog.LevelWarn) {
|
||||
logger.LogAttrs(ctx, slog.LevelWarn, m, slog.String("syslog_level", le.String()))
|
||||
}
|
||||
|
||||
case syslogLevelDebug:
|
||||
if globalLogger.Enabled(ctx, slog.LevelDebug) {
|
||||
globalLogger.LogAttrs(ctx, slog.LevelDebug, m, slog.String("syslog_level", syslogLevel(level).String()))
|
||||
if logger.Enabled(ctx, slog.LevelDebug) {
|
||||
logger.LogAttrs(ctx, slog.LevelDebug, m, slog.String("syslog_level", le.String()))
|
||||
}
|
||||
|
||||
default:
|
||||
if globalLogger.Enabled(ctx, slog.LevelInfo) {
|
||||
globalLogger.LogAttrs(ctx, slog.LevelInfo, m, slog.String("syslog_level", syslogLevel(level).String()))
|
||||
if logger.Enabled(ctx, slog.LevelInfo) {
|
||||
logger.LogAttrs(ctx, slog.LevelInfo, m, slog.String("syslog_level", le.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//export go_log_attrs
|
||||
func go_log_attrs(threadIndex C.uintptr_t, message *C.zend_string, cLevel C.zend_long, cAttrs *C.zval) *C.char {
|
||||
logger, ctx := getLogger(threadIndex)
|
||||
|
||||
level := slog.Level(cLevel)
|
||||
|
||||
if !logger.Enabled(ctx, level) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var attrs map[string]any
|
||||
|
||||
if cAttrs != nil {
|
||||
var err error
|
||||
if attrs, err = GoMap[any](unsafe.Pointer(*(**C.zend_array)(unsafe.Pointer(&cAttrs.value[0])))); err != nil {
|
||||
// PHP exception message.
|
||||
return C.CString("Failed to log message: converting attrs: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogAttrs(ctx, level, GoString(unsafe.Pointer(message)), mapToAttr(attrs)...)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mapToAttr(input map[string]any) []slog.Attr {
|
||||
out := make([]slog.Attr, 0, len(input))
|
||||
|
||||
for key, val := range input {
|
||||
out = append(out, slog.Any(key, val))
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
//export go_is_context_done
|
||||
func go_is_context_done(threadIndex C.uintptr_t) C.bool {
|
||||
return C.bool(phpThreads[threadIndex].frankenPHPContext().isDone)
|
||||
@@ -739,5 +805,6 @@ func resetGlobals() {
|
||||
globalCtx = context.Background()
|
||||
globalLogger = slog.Default()
|
||||
workers = nil
|
||||
watcherIsEnabled = false
|
||||
globalMu.Unlock()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
|
||||
/** @generate-class-entries */
|
||||
|
||||
/** @var int */
|
||||
const FRANKENPHP_LOG_LEVEL_DEBUG = -4;
|
||||
|
||||
/** @var int */
|
||||
const FRANKENPHP_LOG_LEVEL_INFO = 0;
|
||||
|
||||
/** @var int */
|
||||
const FRANKENPHP_LOG_LEVEL_WARN = 4;
|
||||
|
||||
/** @var int */
|
||||
const FRANKENPHP_LOG_LEVEL_ERROR = 8;
|
||||
|
||||
function frankenphp_handle_request(callable $callback): bool {}
|
||||
|
||||
function headers_send(int $status = 200): int {}
|
||||
@@ -36,3 +48,9 @@ function apache_response_headers(): array|bool {}
|
||||
* @param string|string[] $topics
|
||||
*/
|
||||
function mercure_publish(string|array $topics, string $data = '', bool $private = false, ?string $id = null, ?string $type = null, ?int $retry = null): string {}
|
||||
|
||||
/**
|
||||
* @param int $level The importance or severity of a log event. The higher the level, the more important or severe the event. For more details, see: https://pkg.go.dev/log/slog#Level
|
||||
* array<string, any> $context Values of the array will be converted to the corresponding Go type (if supported by FrankenPHP) and added to the context of the structured logs using https://pkg.go.dev/log/slog#Attr
|
||||
*/
|
||||
function frankenphp_log(string $message, int $level = 0, array $context = []): void {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* This is a generated file, edit the .stub.php file instead.
|
||||
* Stub hash: cd534a8394f535a600bf45a333955d23b5154756 */
|
||||
* Stub hash: 60f0d27c04f94d7b24c052e91ef294595a2bc421 */
|
||||
|
||||
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_handle_request, 0, 1, _IS_BOOL, 0)
|
||||
ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0)
|
||||
@@ -35,6 +35,12 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_mercure_publish, 0, 1, IS_STRING
|
||||
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, retry, IS_LONG, 1, "null")
|
||||
ZEND_END_ARG_INFO()
|
||||
|
||||
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_frankenphp_log, 0, 1, IS_VOID, 0)
|
||||
ZEND_ARG_TYPE_INFO(0, message, IS_STRING, 0)
|
||||
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, level, IS_LONG, 0, "0")
|
||||
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, context, IS_ARRAY, 0, "[]")
|
||||
ZEND_END_ARG_INFO()
|
||||
|
||||
|
||||
ZEND_FUNCTION(frankenphp_handle_request);
|
||||
ZEND_FUNCTION(headers_send);
|
||||
@@ -42,6 +48,7 @@ ZEND_FUNCTION(frankenphp_finish_request);
|
||||
ZEND_FUNCTION(frankenphp_request_headers);
|
||||
ZEND_FUNCTION(frankenphp_response_headers);
|
||||
ZEND_FUNCTION(mercure_publish);
|
||||
ZEND_FUNCTION(frankenphp_log);
|
||||
|
||||
|
||||
static const zend_function_entry ext_functions[] = {
|
||||
@@ -55,5 +62,14 @@ static const zend_function_entry ext_functions[] = {
|
||||
ZEND_FE(frankenphp_response_headers, arginfo_frankenphp_response_headers)
|
||||
ZEND_FALIAS(apache_response_headers, frankenphp_response_headers, arginfo_apache_response_headers)
|
||||
ZEND_FE(mercure_publish, arginfo_mercure_publish)
|
||||
ZEND_FE(frankenphp_log, arginfo_frankenphp_log)
|
||||
ZEND_FE_END
|
||||
};
|
||||
|
||||
static void register_frankenphp_symbols(int module_number)
|
||||
{
|
||||
REGISTER_LONG_CONSTANT("FRANKENPHP_LOG_LEVEL_DEBUG", -4, CONST_PERSISTENT);
|
||||
REGISTER_LONG_CONSTANT("FRANKENPHP_LOG_LEVEL_INFO", 0, CONST_PERSISTENT);
|
||||
REGISTER_LONG_CONSTANT("FRANKENPHP_LOG_LEVEL_WARN", 4, CONST_PERSISTENT);
|
||||
REGISTER_LONG_CONSTANT("FRANKENPHP_LOG_LEVEL_ERROR", 8, CONST_PERSISTENT);
|
||||
}
|
||||
|
||||
@@ -32,10 +32,6 @@ import (
|
||||
"github.com/dunglas/frankenphp/internal/fastabs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap/exp/zapslog"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"go.uber.org/zap/zaptest"
|
||||
"go.uber.org/zap/zaptest/observer"
|
||||
)
|
||||
|
||||
type testOptions struct {
|
||||
@@ -62,10 +58,6 @@ func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *
|
||||
cwd, _ := os.Getwd()
|
||||
testDataDir := cwd + "/testdata/"
|
||||
|
||||
if opts.logger == nil {
|
||||
opts.logger = slog.New(zapslog.NewHandler(zaptest.NewLogger(t).Core()))
|
||||
}
|
||||
|
||||
initOpts := []frankenphp.Option{frankenphp.WithLogger(opts.logger)}
|
||||
if opts.workerScript != "" {
|
||||
workerOpts := []frankenphp.WorkerOption{
|
||||
@@ -423,36 +415,61 @@ my_autoloader`, i), body)
|
||||
}, opts)
|
||||
}
|
||||
|
||||
func TestLog_module(t *testing.T) { testLog(t, &testOptions{}) }
|
||||
func TestLog_worker(t *testing.T) {
|
||||
testLog(t, &testOptions{workerScript: "log.php"})
|
||||
func TestLog_error_log_module(t *testing.T) { testLog_error_log(t, &testOptions{}) }
|
||||
func TestLog_error_log_worker(t *testing.T) {
|
||||
testLog_error_log(t, &testOptions{workerScript: "log-error_log.php"})
|
||||
}
|
||||
func testLog(t *testing.T, opts *testOptions) {
|
||||
logger, logs := observer.New(zapcore.InfoLevel)
|
||||
opts.logger = slog.New(zapslog.NewHandler(logger))
|
||||
func testLog_error_log(t *testing.T, opts *testOptions) {
|
||||
var buf fmt.Stringer
|
||||
opts.logger, buf = newTestLogger(t)
|
||||
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/log.php?i=%d", i), nil)
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/log-error_log.php?i=%d", i), nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
for logs.FilterMessage(fmt.Sprintf("request %d", i)).Len() <= 0 {
|
||||
assert.Contains(t, buf.String(), fmt.Sprintf("request %d", i))
|
||||
}, opts)
|
||||
}
|
||||
|
||||
func TestLog_frankenphp_log_module(t *testing.T) { testLog_frankenphp_log(t, &testOptions{}) }
|
||||
func TestLog_frankenphp_log_worker(t *testing.T) {
|
||||
testLog_frankenphp_log(t, &testOptions{workerScript: "log-frankenphp_log.php"})
|
||||
}
|
||||
func testLog_frankenphp_log(t *testing.T, opts *testOptions) {
|
||||
var buf fmt.Stringer
|
||||
opts.logger, buf = newTestLogger(t)
|
||||
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/log-frankenphp_log.php?i=%d", i), nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
|
||||
logs := buf.String()
|
||||
for _, message := range []string{
|
||||
`level=INFO msg="default level message"`,
|
||||
fmt.Sprintf(`level=DEBUG msg="some debug message %d" "key int"=1`, i),
|
||||
fmt.Sprintf(`level=INFO msg="some info message %d" "key string"=string`, i),
|
||||
fmt.Sprintf(`level=WARN msg="some warn message %d"`, i),
|
||||
fmt.Sprintf(`level=ERROR msg="some error message %d" err="[a v]"`, i),
|
||||
} {
|
||||
assert.Contains(t, logs, message)
|
||||
}
|
||||
}, opts)
|
||||
}
|
||||
|
||||
func TestConnectionAbort_module(t *testing.T) { testConnectionAbort(t, &testOptions{}) }
|
||||
func TestConnectionAbort_worker(t *testing.T) {
|
||||
testConnectionAbort(t, &testOptions{workerScript: "connectionStatusLog.php"})
|
||||
testConnectionAbort(t, &testOptions{workerScript: "connection_status.php"})
|
||||
}
|
||||
func testConnectionAbort(t *testing.T, opts *testOptions) {
|
||||
testFinish := func(finish string) {
|
||||
t.Run(fmt.Sprintf("finish=%s", finish), func(t *testing.T) {
|
||||
logger, logs := observer.New(zapcore.InfoLevel)
|
||||
opts.logger = slog.New(zapslog.NewHandler(logger))
|
||||
var buf syncBuffer
|
||||
opts.logger = slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}))
|
||||
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/connectionStatusLog.php?i=%d&finish=%s", i, finish), nil)
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/connection_status.php?i=%d&finish=%s", i, finish), nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
@@ -460,7 +477,7 @@ func testConnectionAbort(t *testing.T, opts *testOptions) {
|
||||
cancel()
|
||||
handler(w, req)
|
||||
|
||||
for logs.FilterMessage(fmt.Sprintf("request %d: 1", i)).Len() <= 0 {
|
||||
for !strings.Contains(buf.String(), fmt.Sprintf("request %d: 1", i)) {
|
||||
}
|
||||
}, opts)
|
||||
})
|
||||
@@ -618,8 +635,9 @@ func testRequestHeaders(t *testing.T, opts *testOptions) {
|
||||
}
|
||||
|
||||
func TestFailingWorker(t *testing.T) {
|
||||
t.Cleanup(frankenphp.Shutdown)
|
||||
|
||||
err := frankenphp.Init(
|
||||
frankenphp.WithLogger(slog.New(slog.NewTextHandler(io.Discard, nil))),
|
||||
frankenphp.WithWorkers("failing worker", "testdata/failing-worker.php", 4, frankenphp.WithWorkerMaxFailures(1)),
|
||||
frankenphp.WithNumThreads(5),
|
||||
)
|
||||
@@ -1057,7 +1075,6 @@ func FuzzRequest(f *testing.F) {
|
||||
// Headers should always be present even if empty
|
||||
assert.Contains(t, body, fmt.Sprintf("[CONTENT_TYPE] => %s", fuzzedString))
|
||||
assert.Contains(t, body, fmt.Sprintf("[HTTP_FUZZED] => %s", fuzzedString))
|
||||
|
||||
}, &testOptions{workerScript: "request-headers.php"})
|
||||
})
|
||||
}
|
||||
|
||||
18
go.mod
18
go.mod
@@ -1,18 +1,17 @@
|
||||
module github.com/dunglas/frankenphp
|
||||
|
||||
go 1.25.0
|
||||
go 1.25.4
|
||||
|
||||
retract v1.0.0-rc.1 // Human error
|
||||
|
||||
require (
|
||||
github.com/Masterminds/sprig/v3 v3.3.0
|
||||
github.com/dunglas/mercure v0.21.2
|
||||
github.com/dunglas/mercure v0.21.4
|
||||
github.com/e-dant/watcher/watcher-go v0.0.0-20251208164151-f88ec3b7e146
|
||||
github.com/maypok86/otter/v2 v2.2.1
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
go.uber.org/zap v1.27.1
|
||||
go.uber.org/zap/exp v0.3.0
|
||||
golang.org/x/net v0.47.0
|
||||
golang.org/x/net v0.48.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -57,12 +56,11 @@ require (
|
||||
github.com/unrolled/secure v1.17.0 // indirect
|
||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||
go.etcd.io/bbolt v1.4.3 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
36
go.sum
36
go.sum
@@ -18,10 +18,12 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dunglas/mercure v0.21.2 h1:qaLTScSwsCHDps++4AeLWrRp3BysdR5EoHBqu7JNhas=
|
||||
github.com/dunglas/mercure v0.21.2/go.mod h1:3ElA7VwRI8BHUIAVU8oGlvPaqGwsKU5zZVWFNSFg/+U=
|
||||
github.com/dunglas/mercure v0.21.4 h1:mXPXHfB+4cYfFFCRRDY198mfef5+MQcdCpUnAHBUW2M=
|
||||
github.com/dunglas/mercure v0.21.4/go.mod h1:l/dglCjp/OQx8/quRyceRPx2hqZQ3CNviwoLMRQiJ/k=
|
||||
github.com/dunglas/skipfilter v1.0.0 h1:JG9SgGg4n6BlFwuTYzb9RIqjH7PfwszvWehanrYWPF4=
|
||||
github.com/dunglas/skipfilter v1.0.0/go.mod h1:ryhr8j7CAHSjzeN7wI6YEuwoArQ3OQmRqWWVCEAfb9w=
|
||||
github.com/e-dant/watcher/watcher-go v0.0.0-20251208164151-f88ec3b7e146 h1:h3vVM6X45PK0mAk8NqiYNQGXTyhvXy1HQ5GhuQN4eeA=
|
||||
github.com/e-dant/watcher/watcher-go v0.0.0-20251208164151-f88ec3b7e146/go.mod h1:sVUOkwtftoj71nnJRG2S0oWNfXFdKpz/M9vK0z06nmM=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
@@ -102,28 +104,22 @@ go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
|
||||
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
||||
44
hotreload.go
Normal file
44
hotreload.go
Normal file
@@ -0,0 +1,44 @@
|
||||
//go:build !nomercure && !nowatcher
|
||||
|
||||
package frankenphp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
|
||||
"github.com/dunglas/frankenphp/internal/watcher"
|
||||
"github.com/dunglas/mercure"
|
||||
watcherGo "github.com/e-dant/watcher/watcher-go"
|
||||
)
|
||||
|
||||
// WithHotReload sets files to watch for file changes to trigger a hot reload update.
|
||||
func WithHotReload(topic string, hub *mercure.Hub, patterns []string) Option {
|
||||
return func(o *opt) error {
|
||||
o.hotReload = append(o.hotReload, &watcher.PatternGroup{
|
||||
Patterns: patterns,
|
||||
Callback: func(events []*watcherGo.Event) {
|
||||
// Wait for workers to restart before sending the update
|
||||
go func() {
|
||||
data, err := json.Marshal(events)
|
||||
if err != nil {
|
||||
if globalLogger.Enabled(globalCtx, slog.LevelError) {
|
||||
globalLogger.LogAttrs(globalCtx, slog.LevelError, "error marshaling watcher events", slog.Any("error", err))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := hub.Publish(globalCtx, &mercure.Update{
|
||||
Topics: []string{topic},
|
||||
Event: mercure.Event{Data: string(data)},
|
||||
Debug: globalLogger.Enabled(globalCtx, slog.LevelDebug),
|
||||
}); err != nil && globalLogger.Enabled(globalCtx, slog.LevelError) {
|
||||
globalLogger.LogAttrs(globalCtx, slog.LevelError, "error publishing hot reloading Mercure update", slog.Any("error", err))
|
||||
}
|
||||
}()
|
||||
},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ func (ag *arginfoGenerator) generate() error {
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Print("gen_stub.php output:\n", string(output))
|
||||
return fmt.Errorf("running gen_stub script: %w", err)
|
||||
return fmt.Errorf("running gen_stub script: %w\nOutput: %s", err, string(output))
|
||||
}
|
||||
|
||||
return ag.fixArginfoFile(stubFile)
|
||||
|
||||
@@ -128,14 +128,15 @@ type GoParameter struct {
|
||||
Type string
|
||||
}
|
||||
|
||||
var phpToGoTypeMap = map[phpType]string{
|
||||
phpString: "string",
|
||||
phpInt: "int64",
|
||||
phpFloat: "float64",
|
||||
phpBool: "bool",
|
||||
phpArray: "*frankenphp.Array",
|
||||
phpMixed: "any",
|
||||
phpVoid: "",
|
||||
var phpToGoTypeMap= map[phpType]string{
|
||||
phpString: "string",
|
||||
phpInt: "int64",
|
||||
phpFloat: "float64",
|
||||
phpBool: "bool",
|
||||
phpArray: "*frankenphp.Array",
|
||||
phpMixed: "any",
|
||||
phpVoid: "",
|
||||
phpCallable: "*C.zval",
|
||||
}
|
||||
|
||||
func (gg *GoFileGenerator) phpTypeToGoType(phpT phpType) string {
|
||||
|
||||
@@ -703,6 +703,125 @@ func testGoFileExportedFunctions(t *testing.T, content string, functions []phpFu
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoFileGenerator_MethodWrapperWithCallableParams(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
sourceContent := `package main
|
||||
|
||||
import "C"
|
||||
|
||||
//export_php:class CallableClass
|
||||
type CallableStruct struct{}
|
||||
|
||||
//export_php:method CallableClass::processCallback(callable $callback): string
|
||||
func (cs *CallableStruct) ProcessCallback(callback *C.zval) string {
|
||||
return "processed"
|
||||
}
|
||||
|
||||
//export_php:method CallableClass::processOptionalCallback(?callable $callback): string
|
||||
func (cs *CallableStruct) ProcessOptionalCallback(callback *C.zval) string {
|
||||
return "processed_optional"
|
||||
}`
|
||||
|
||||
sourceFile := filepath.Join(tmpDir, "test.go")
|
||||
require.NoError(t, os.WriteFile(sourceFile, []byte(sourceContent), 0644))
|
||||
|
||||
methods := []phpClassMethod{
|
||||
{
|
||||
Name: "ProcessCallback",
|
||||
PhpName: "processCallback",
|
||||
ClassName: "CallableClass",
|
||||
Signature: "processCallback(callable $callback): string",
|
||||
ReturnType: phpString,
|
||||
Params: []phpParameter{
|
||||
{Name: "callback", PhpType: phpCallable, IsNullable: false},
|
||||
},
|
||||
GoFunction: `func (cs *CallableStruct) ProcessCallback(callback *C.zval) string {
|
||||
return "processed"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
Name: "ProcessOptionalCallback",
|
||||
PhpName: "processOptionalCallback",
|
||||
ClassName: "CallableClass",
|
||||
Signature: "processOptionalCallback(?callable $callback): string",
|
||||
ReturnType: phpString,
|
||||
Params: []phpParameter{
|
||||
{Name: "callback", PhpType: phpCallable, IsNullable: true},
|
||||
},
|
||||
GoFunction: `func (cs *CallableStruct) ProcessOptionalCallback(callback *C.zval) string {
|
||||
return "processed_optional"
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
classes := []phpClass{
|
||||
{
|
||||
Name: "CallableClass",
|
||||
GoStruct: "CallableStruct",
|
||||
Methods: methods,
|
||||
},
|
||||
}
|
||||
|
||||
generator := &Generator{
|
||||
BaseName: "callable_test",
|
||||
SourceFile: sourceFile,
|
||||
Classes: classes,
|
||||
BuildDir: tmpDir,
|
||||
}
|
||||
|
||||
goGen := GoFileGenerator{generator}
|
||||
content, err := goGen.buildContent()
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedCallableWrapperSignature := "func ProcessCallback_wrapper(handle C.uintptr_t, callback *C.zval) unsafe.Pointer"
|
||||
assert.Contains(t, content, expectedCallableWrapperSignature, "Generated content should contain callable wrapper signature: %s", expectedCallableWrapperSignature)
|
||||
|
||||
expectedOptionalCallableWrapperSignature := "func ProcessOptionalCallback_wrapper(handle C.uintptr_t, callback *C.zval) unsafe.Pointer"
|
||||
assert.Contains(t, content, expectedOptionalCallableWrapperSignature, "Generated content should contain optional callable wrapper signature: %s", expectedOptionalCallableWrapperSignature)
|
||||
|
||||
expectedCallableCall := "structObj.ProcessCallback(callback)"
|
||||
assert.Contains(t, content, expectedCallableCall, "Generated content should contain callable method call: %s", expectedCallableCall)
|
||||
|
||||
expectedOptionalCallableCall := "structObj.ProcessOptionalCallback(callback)"
|
||||
assert.Contains(t, content, expectedOptionalCallableCall, "Generated content should contain optional callable method call: %s", expectedOptionalCallableCall)
|
||||
|
||||
assert.Contains(t, content, "//export ProcessCallback_wrapper", "Generated content should contain ProcessCallback export directive")
|
||||
assert.Contains(t, content, "//export ProcessOptionalCallback_wrapper", "Generated content should contain ProcessOptionalCallback export directive")
|
||||
}
|
||||
|
||||
func TestGoFileGenerator_phpTypeToGoType(t *testing.T) {
|
||||
generator := &Generator{}
|
||||
goGen := GoFileGenerator{generator}
|
||||
|
||||
tests := []struct {
|
||||
phpType phpType
|
||||
expected string
|
||||
}{
|
||||
{phpString, "string"},
|
||||
{phpInt, "int64"},
|
||||
{phpFloat, "float64"},
|
||||
{phpBool, "bool"},
|
||||
{phpArray, "*frankenphp.Array"},
|
||||
{phpMixed, "any"},
|
||||
{phpVoid, ""},
|
||||
{phpCallable, "*C.zval"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.phpType), func(t *testing.T) {
|
||||
result := goGen.phpTypeToGoType(tt.phpType)
|
||||
assert.Equal(t, tt.expected, result, "phpTypeToGoType(%s) should return %s", tt.phpType, tt.expected)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("unknown_type", func(t *testing.T) {
|
||||
unknownType := phpType("unknown")
|
||||
result := goGen.phpTypeToGoType(unknownType)
|
||||
assert.Equal(t, "any", result, "phpTypeToGoType should fallback to interface{} for unknown types")
|
||||
})
|
||||
}
|
||||
|
||||
func testGoFileInternalFunctions(t *testing.T, content string) {
|
||||
internalIndicators := []string{
|
||||
"func internalHelper",
|
||||
|
||||
793
internal/extgen/integration_test.go
Normal file
793
internal/extgen/integration_test.go
Normal file
@@ -0,0 +1,793 @@
|
||||
//go:build integration
|
||||
|
||||
package extgen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
testModuleName = "github.com/frankenphp/test-extension"
|
||||
)
|
||||
|
||||
type IntegrationTestSuite struct {
|
||||
tempDir string
|
||||
genStubScript string
|
||||
xcaddyPath string
|
||||
frankenphpPath string
|
||||
phpConfigPath string
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func setupTest(t *testing.T) *IntegrationTestSuite {
|
||||
t.Helper()
|
||||
|
||||
suite := &IntegrationTestSuite{t: t}
|
||||
|
||||
suite.genStubScript = os.Getenv("GEN_STUB_SCRIPT")
|
||||
if suite.genStubScript == "" {
|
||||
suite.genStubScript = "/usr/local/src/php/build/gen_stub.php"
|
||||
}
|
||||
|
||||
if _, err := os.Stat(suite.genStubScript); os.IsNotExist(err) {
|
||||
t.Error("GEN_STUB_SCRIPT not found. Integration tests require PHP sources. Set GEN_STUB_SCRIPT environment variable.")
|
||||
}
|
||||
|
||||
xcaddyPath, err := exec.LookPath("xcaddy")
|
||||
if err != nil {
|
||||
t.Error("xcaddy not found in PATH. Integration tests require xcaddy to build FrankenPHP.")
|
||||
}
|
||||
suite.xcaddyPath = xcaddyPath
|
||||
|
||||
phpConfigPath, err := exec.LookPath("php-config")
|
||||
if err != nil {
|
||||
t.Error("php-config not found in PATH. Integration tests require PHP development headers.")
|
||||
}
|
||||
suite.phpConfigPath = phpConfigPath
|
||||
|
||||
tempDir := t.TempDir()
|
||||
suite.tempDir = tempDir
|
||||
|
||||
return suite
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) createGoModule(sourceFile string) (string, error) {
|
||||
s.t.Helper()
|
||||
|
||||
moduleDir := filepath.Join(s.tempDir, "module")
|
||||
if err := os.MkdirAll(moduleDir, 0o755); err != nil {
|
||||
return "", fmt.Errorf("failed to create module directory: %w", err)
|
||||
}
|
||||
|
||||
// Get project root for replace directive
|
||||
projectRoot, err := filepath.Abs(filepath.Join("..", ".."))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get project root: %w", err)
|
||||
}
|
||||
|
||||
goModContent := fmt.Sprintf(`module %s
|
||||
|
||||
go 1.25
|
||||
|
||||
require github.com/dunglas/frankenphp v0.0.0
|
||||
|
||||
replace github.com/dunglas/frankenphp => %s
|
||||
`, testModuleName, projectRoot)
|
||||
|
||||
if err := os.WriteFile(filepath.Join(moduleDir, "go.mod"), []byte(goModContent), 0o644); err != nil {
|
||||
return "", fmt.Errorf("failed to create go.mod: %w", err)
|
||||
}
|
||||
|
||||
sourceContent, err := os.ReadFile(sourceFile)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read source file: %w", err)
|
||||
}
|
||||
|
||||
targetFile := filepath.Join(moduleDir, filepath.Base(sourceFile))
|
||||
if err := os.WriteFile(targetFile, sourceContent, 0o644); err != nil {
|
||||
return "", fmt.Errorf("failed to write source file: %w", err)
|
||||
}
|
||||
|
||||
return targetFile, nil
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) runExtensionInit(sourceFile string) error {
|
||||
s.t.Helper()
|
||||
|
||||
os.Setenv("GEN_STUB_SCRIPT", s.genStubScript)
|
||||
|
||||
baseName := SanitizePackageName(strings.TrimSuffix(filepath.Base(sourceFile), ".go"))
|
||||
generator := Generator{
|
||||
BaseName: baseName,
|
||||
SourceFile: sourceFile,
|
||||
BuildDir: filepath.Dir(sourceFile),
|
||||
}
|
||||
|
||||
if err := generator.Generate(); err != nil {
|
||||
return fmt.Errorf("generation failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanupGeneratedFiles removes generated files from the original source directory
|
||||
func (s *IntegrationTestSuite) cleanupGeneratedFiles(originalSourceFile string) {
|
||||
s.t.Helper()
|
||||
|
||||
sourceDir := filepath.Dir(originalSourceFile)
|
||||
baseName := SanitizePackageName(strings.TrimSuffix(filepath.Base(originalSourceFile), ".go"))
|
||||
|
||||
generatedFiles := []string{
|
||||
baseName + ".stub.php",
|
||||
baseName + "_arginfo.h",
|
||||
baseName + ".h",
|
||||
baseName + ".c",
|
||||
baseName + ".go",
|
||||
"README.md",
|
||||
}
|
||||
|
||||
for _, file := range generatedFiles {
|
||||
fullPath := filepath.Join(sourceDir, file)
|
||||
if _, err := os.Stat(fullPath); err == nil {
|
||||
os.Remove(fullPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// compileFrankenPHP compiles FrankenPHP with the generated extension
|
||||
func (s *IntegrationTestSuite) compileFrankenPHP(moduleDir string) (string, error) {
|
||||
s.t.Helper()
|
||||
|
||||
projectRoot, err := filepath.Abs(filepath.Join("..", ".."))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get project root: %w", err)
|
||||
}
|
||||
|
||||
cflags, err := exec.Command(s.phpConfigPath, "--includes").Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get PHP includes: %w", err)
|
||||
}
|
||||
|
||||
ldflags, err := exec.Command(s.phpConfigPath, "--ldflags").Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get PHP ldflags: %w", err)
|
||||
}
|
||||
|
||||
libs, err := exec.Command(s.phpConfigPath, "--libs").Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get PHP libs: %w", err)
|
||||
}
|
||||
|
||||
cgoCflags := strings.TrimSpace(string(cflags))
|
||||
cgoLdflags := strings.TrimSpace(string(ldflags)) + " " + strings.TrimSpace(string(libs))
|
||||
|
||||
outputBinary := filepath.Join(s.tempDir, "frankenphp")
|
||||
|
||||
cmd := exec.Command(
|
||||
s.xcaddyPath,
|
||||
"build",
|
||||
"--output", outputBinary,
|
||||
"--with", "github.com/dunglas/frankenphp="+projectRoot,
|
||||
"--with", "github.com/dunglas/frankenphp/caddy="+projectRoot+"/caddy",
|
||||
"--with", testModuleName+"="+moduleDir,
|
||||
)
|
||||
|
||||
cmd.Env = append(os.Environ(),
|
||||
"CGO_ENABLED=1",
|
||||
"CGO_CFLAGS="+cgoCflags,
|
||||
"CGO_LDFLAGS="+cgoLdflags,
|
||||
fmt.Sprintf("XCADDY_GO_BUILD_FLAGS=-ldflags='-w -s' -tags=nobadger,nomysql,nopgx,nowatcher"),
|
||||
)
|
||||
|
||||
cmd.Dir = s.tempDir
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("xcaddy build failed: %w\nOutput: %s", err, string(output))
|
||||
}
|
||||
|
||||
s.frankenphpPath = outputBinary
|
||||
return outputBinary, nil
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) runPHPCode(phpCode string) (string, error) {
|
||||
s.t.Helper()
|
||||
|
||||
if s.frankenphpPath == "" {
|
||||
return "", fmt.Errorf("FrankenPHP not compiled yet")
|
||||
}
|
||||
|
||||
phpFile := filepath.Join(s.tempDir, "test.php")
|
||||
if err := os.WriteFile(phpFile, []byte(phpCode), 0o644); err != nil {
|
||||
return "", fmt.Errorf("failed to create PHP file: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(s.frankenphpPath, "php-cli", phpFile)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("PHP execution failed: %w\nOutput: %s", err, string(output))
|
||||
}
|
||||
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
// verifyPHPSymbols checks if PHP can find the exposed functions, classes, and constants
|
||||
func (s *IntegrationTestSuite) verifyPHPSymbols(functions []string, classes []string, constants []string) error {
|
||||
s.t.Helper()
|
||||
|
||||
var checks []string
|
||||
|
||||
for _, fn := range functions {
|
||||
checks = append(checks, fmt.Sprintf("if (!function_exists('%s')) { echo 'MISSING_FUNCTION: %s'; exit(1); }", fn, fn))
|
||||
}
|
||||
|
||||
for _, cls := range classes {
|
||||
checks = append(checks, fmt.Sprintf("if (!class_exists('%s')) { echo 'MISSING_CLASS: %s'; exit(1); }", cls, cls))
|
||||
}
|
||||
|
||||
for _, cnst := range constants {
|
||||
checks = append(checks, fmt.Sprintf("if (!defined('%s')) { echo 'MISSING_CONSTANT: %s'; exit(1); }", cnst, cnst))
|
||||
}
|
||||
|
||||
checks = append(checks, "echo 'OK';")
|
||||
|
||||
phpCode := "<?php\n" + strings.Join(checks, "\n")
|
||||
|
||||
output, err := s.runPHPCode(phpCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.Contains(output, "OK") {
|
||||
return fmt.Errorf("symbol verification failed: %s", output)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) verifyFunctionBehavior(phpCode string, expectedOutput string) error {
|
||||
s.t.Helper()
|
||||
|
||||
output, err := s.runPHPCode(phpCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.Contains(output, expectedOutput) {
|
||||
return fmt.Errorf("unexpected output.\nExpected to contain: %q\nGot: %q", expectedOutput, output)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestBasicFunction(t *testing.T) {
|
||||
suite := setupTest(t)
|
||||
|
||||
sourceFile := filepath.Join("..", "..", "testdata", "integration", "basic_function.go")
|
||||
sourceFile, err := filepath.Abs(sourceFile)
|
||||
require.NoError(t, err)
|
||||
defer suite.cleanupGeneratedFiles(sourceFile)
|
||||
|
||||
targetFile, err := suite.createGoModule(sourceFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = suite.runExtensionInit(targetFile)
|
||||
require.NoError(t, err, "extension-init should succeed")
|
||||
|
||||
baseDir := filepath.Dir(targetFile)
|
||||
baseName := strings.TrimSuffix(filepath.Base(targetFile), ".go")
|
||||
|
||||
expectedFiles := []string{
|
||||
baseName + ".stub.php",
|
||||
baseName + "_arginfo.h",
|
||||
baseName + ".h",
|
||||
baseName + ".c",
|
||||
baseName + ".go",
|
||||
"README.md",
|
||||
}
|
||||
|
||||
for _, file := range expectedFiles {
|
||||
fullPath := filepath.Join(baseDir, file)
|
||||
assert.FileExists(t, fullPath, "Generated file should exist: %s", file)
|
||||
}
|
||||
|
||||
_, err = suite.compileFrankenPHP(filepath.Dir(targetFile))
|
||||
require.NoError(t, err, "FrankenPHP compilation should succeed")
|
||||
|
||||
err = suite.verifyPHPSymbols(
|
||||
[]string{"test_uppercase", "test_add_numbers", "test_multiply", "test_is_enabled"},
|
||||
[]string{},
|
||||
[]string{},
|
||||
)
|
||||
require.NoError(t, err, "all functions should be accessible from PHP")
|
||||
|
||||
err = suite.verifyFunctionBehavior(`<?php
|
||||
$result = test_uppercase("hello world");
|
||||
if ($result !== "HELLO WORLD") {
|
||||
echo "FAIL: test_uppercase expected 'HELLO WORLD', got '$result'";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$result = test_uppercase("");
|
||||
if ($result !== "") {
|
||||
echo "FAIL: test_uppercase with empty string expected '', got '$result'";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$sum = test_add_numbers(5, 7);
|
||||
if ($sum !== 12) {
|
||||
echo "FAIL: test_add_numbers(5, 7) expected 12, got $sum";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$result = test_is_enabled(true);
|
||||
if ($result !== false) {
|
||||
echo "FAIL: test_is_enabled(true) expected false, got " . ($result ? "true" : "false");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$result = test_is_enabled(false);
|
||||
if ($result !== true) {
|
||||
echo "FAIL: test_is_enabled(false) expected true, got " . ($result ? "true" : "false");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "OK";
|
||||
`, "OK")
|
||||
require.NoError(t, err, "all function calls should work correctly")
|
||||
}
|
||||
|
||||
func TestClassMethodsIntegration(t *testing.T) {
|
||||
suite := setupTest(t)
|
||||
|
||||
sourceFile := filepath.Join("..", "..", "testdata", "integration", "class_methods.go")
|
||||
sourceFile, err := filepath.Abs(sourceFile)
|
||||
require.NoError(t, err)
|
||||
defer suite.cleanupGeneratedFiles(sourceFile)
|
||||
|
||||
targetFile, err := suite.createGoModule(sourceFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = suite.runExtensionInit(targetFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = suite.compileFrankenPHP(filepath.Dir(targetFile))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = suite.verifyPHPSymbols(
|
||||
[]string{},
|
||||
[]string{"Counter", "StringHolder"},
|
||||
[]string{},
|
||||
)
|
||||
require.NoError(t, err, "all classes should be accessible from PHP")
|
||||
|
||||
err = suite.verifyFunctionBehavior(`<?php
|
||||
$counter = new Counter();
|
||||
if ($counter->getValue() !== 0) {
|
||||
echo "FAIL: Counter initial value expected 0, got " . $counter->getValue();
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$counter->increment();
|
||||
if ($counter->getValue() !== 1) {
|
||||
echo "FAIL: Counter after increment expected 1, got " . $counter->getValue();
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$counter->decrement();
|
||||
if ($counter->getValue() !== 0) {
|
||||
echo "FAIL: Counter after decrement expected 0, got " . $counter->getValue();
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$counter->setValue(10);
|
||||
if ($counter->getValue() !== 10) {
|
||||
echo "FAIL: Counter after setValue(10) expected 10, got " . $counter->getValue();
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$newValue = $counter->addValue(5);
|
||||
if ($newValue !== 15) {
|
||||
echo "FAIL: Counter addValue(5) expected to return 15, got $newValue";
|
||||
exit(1);
|
||||
}
|
||||
if ($counter->getValue() !== 15) {
|
||||
echo "FAIL: Counter value after addValue(5) expected 15, got " . $counter->getValue();
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$counter->updateWithNullable(50);
|
||||
if ($counter->getValue() !== 50) {
|
||||
echo "FAIL: Counter after updateWithNullable(50) expected 50, got " . $counter->getValue();
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$counter->updateWithNullable(null);
|
||||
if ($counter->getValue() !== 50) {
|
||||
echo "FAIL: Counter after updateWithNullable(null) expected 50 (unchanged), got " . $counter->getValue();
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$counter->reset();
|
||||
if ($counter->getValue() !== 0) {
|
||||
echo "FAIL: Counter after reset expected 0, got " . $counter->getValue();
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$counter1 = new Counter();
|
||||
$counter2 = new Counter();
|
||||
$counter1->setValue(100);
|
||||
$counter2->setValue(200);
|
||||
if ($counter1->getValue() !== 100 || $counter2->getValue() !== 200) {
|
||||
echo "FAIL: Multiple Counter instances should be independent";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$holder = new StringHolder();
|
||||
$holder->setData("test string");
|
||||
if ($holder->getData() !== "test string") {
|
||||
echo "FAIL: StringHolder getData expected 'test string', got '" . $holder->getData() . "'";
|
||||
exit(1);
|
||||
}
|
||||
if ($holder->getLength() !== 11) {
|
||||
echo "FAIL: StringHolder getLength expected 11, got " . $holder->getLength();
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$holder->setData("");
|
||||
if ($holder->getData() !== "") {
|
||||
echo "FAIL: StringHolder empty string expected '', got '" . $holder->getData() . "'";
|
||||
exit(1);
|
||||
}
|
||||
if ($holder->getLength() !== 0) {
|
||||
echo "FAIL: StringHolder empty string length expected 0, got " . $holder->getLength();
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "OK";
|
||||
`, "OK")
|
||||
require.NoError(t, err, "all class methods should work correctly")
|
||||
}
|
||||
|
||||
func TestConstants(t *testing.T) {
|
||||
suite := setupTest(t)
|
||||
|
||||
sourceFile := filepath.Join("..", "..", "testdata", "integration", "constants.go")
|
||||
sourceFile, err := filepath.Abs(sourceFile)
|
||||
require.NoError(t, err)
|
||||
defer suite.cleanupGeneratedFiles(sourceFile)
|
||||
|
||||
targetFile, err := suite.createGoModule(sourceFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = suite.runExtensionInit(targetFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = suite.compileFrankenPHP(filepath.Dir(targetFile))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = suite.verifyPHPSymbols(
|
||||
[]string{"test_with_constants"},
|
||||
[]string{"Config"},
|
||||
[]string{
|
||||
"TEST_MAX_RETRIES", "TEST_API_VERSION", "TEST_ENABLED", "TEST_PI",
|
||||
"STATUS_PENDING", "STATUS_PROCESSING", "STATUS_COMPLETED",
|
||||
},
|
||||
)
|
||||
require.NoError(t, err, "all constants, functions, and classes should be accessible from PHP")
|
||||
|
||||
err = suite.verifyFunctionBehavior(`<?php
|
||||
if (TEST_MAX_RETRIES !== 100) {
|
||||
echo "FAIL: TEST_MAX_RETRIES expected 100, got " . TEST_MAX_RETRIES;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (TEST_API_VERSION !== "2.0.0") {
|
||||
echo "FAIL: TEST_API_VERSION expected '2.0.0', got '" . TEST_API_VERSION . "'";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (TEST_ENABLED !== true) {
|
||||
var_dump(TEST_ENABLED);
|
||||
echo "FAIL: TEST_ENABLED expected true, got " . (TEST_ENABLED ? "true" : "false");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (abs(TEST_PI - 3.14159) > 0.00001) {
|
||||
echo "FAIL: TEST_PI expected 3.14159, got " . TEST_PI;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (Config::MODE_DEBUG !== 1) {
|
||||
echo "FAIL: Config::MODE_DEBUG expected 1, got " . Config::MODE_DEBUG;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (Config::MODE_PRODUCTION !== 2) {
|
||||
echo "FAIL: Config::MODE_PRODUCTION expected 2, got " . Config::MODE_PRODUCTION;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (Config::DEFAULT_TIMEOUT !== 30) {
|
||||
echo "FAIL: Config::DEFAULT_TIMEOUT expected 30, got " . Config::DEFAULT_TIMEOUT;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$config = new Config();
|
||||
$config->setMode(Config::MODE_DEBUG);
|
||||
if ($config->getMode() !== Config::MODE_DEBUG) {
|
||||
echo "FAIL: Config getMode expected MODE_DEBUG, got " . $config->getMode();
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$result = test_with_constants(STATUS_PENDING);
|
||||
if ($result !== "pending") {
|
||||
echo "FAIL: test_with_constants(STATUS_PENDING) expected 'pending', got '$result'";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$result = test_with_constants(STATUS_PROCESSING);
|
||||
if ($result !== "processing") {
|
||||
echo "FAIL: test_with_constants(STATUS_PROCESSING) expected 'processing', got '$result'";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$result = test_with_constants(STATUS_COMPLETED);
|
||||
if ($result !== "completed") {
|
||||
echo "FAIL: test_with_constants(STATUS_COMPLETED) expected 'completed', got '$result'";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$result = test_with_constants(999);
|
||||
if ($result !== "unknown") {
|
||||
echo "FAIL: test_with_constants(999) expected 'unknown', got '$result'";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "OK";
|
||||
`, "OK")
|
||||
require.NoError(t, err, "all constants should have correct values and functions should work")
|
||||
}
|
||||
|
||||
func TestNamespace(t *testing.T) {
|
||||
suite := setupTest(t)
|
||||
|
||||
sourceFile := filepath.Join("..", "..", "testdata", "integration", "namespace.go")
|
||||
sourceFile, err := filepath.Abs(sourceFile)
|
||||
require.NoError(t, err)
|
||||
defer suite.cleanupGeneratedFiles(sourceFile)
|
||||
|
||||
targetFile, err := suite.createGoModule(sourceFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = suite.runExtensionInit(targetFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = suite.compileFrankenPHP(filepath.Dir(targetFile))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = suite.verifyPHPSymbols(
|
||||
[]string{`\\TestIntegration\\Extension\\greet`},
|
||||
[]string{`\\TestIntegration\\Extension\\Person`},
|
||||
[]string{`\\TestIntegration\\Extension\\NAMESPACE_VERSION`},
|
||||
)
|
||||
require.NoError(t, err, "all namespaced symbols should be accessible from PHP")
|
||||
|
||||
err = suite.verifyFunctionBehavior(`<?php
|
||||
use TestIntegration\Extension;
|
||||
|
||||
if (Extension\NAMESPACE_VERSION !== "1.0.0") {
|
||||
echo "FAIL: NAMESPACE_VERSION expected '1.0.0', got '" . Extension\NAMESPACE_VERSION . "'";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$greeting = Extension\greet("Alice");
|
||||
if ($greeting !== "Hello, Alice!") {
|
||||
echo "FAIL: greet('Alice') expected 'Hello, Alice!', got '$greeting'";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$greeting = Extension\greet("");
|
||||
if ($greeting !== "Hello, !") {
|
||||
echo "FAIL: greet('') expected 'Hello, !', got '$greeting'";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (Extension\Person::DEFAULT_AGE !== 18) {
|
||||
echo "FAIL: Person::DEFAULT_AGE expected 18, got " . Extension\Person::DEFAULT_AGE;
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$person = new Extension\Person();
|
||||
$person->setName("Bob");
|
||||
$person->setAge(25);
|
||||
|
||||
if ($person->getName() !== "Bob") {
|
||||
echo "FAIL: Person getName expected 'Bob', got '" . $person->getName() . "'";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if ($person->getAge() !== 25) {
|
||||
echo "FAIL: Person getAge expected 25, got " . $person->getAge();
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$person->setAge(Extension\Person::DEFAULT_AGE);
|
||||
if ($person->getAge() !== 18) {
|
||||
echo "FAIL: Person setAge(DEFAULT_AGE) expected 18, got " . $person->getAge();
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$person1 = new Extension\Person();
|
||||
$person2 = new Extension\Person();
|
||||
$person1->setName("Alice");
|
||||
$person1->setAge(30);
|
||||
$person2->setName("Charlie");
|
||||
$person2->setAge(40);
|
||||
|
||||
if ($person1->getName() !== "Alice" || $person1->getAge() !== 30) {
|
||||
echo "FAIL: person1 should have independent state";
|
||||
exit(1);
|
||||
}
|
||||
if ($person2->getName() !== "Charlie" || $person2->getAge() !== 40) {
|
||||
echo "FAIL: person2 should have independent state";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "OK";
|
||||
`, "OK")
|
||||
require.NoError(t, err, "all namespaced symbols should work correctly")
|
||||
}
|
||||
|
||||
func TestInvalidSignature(t *testing.T) {
|
||||
suite := setupTest(t)
|
||||
|
||||
sourceFile := filepath.Join("..", "..", "testdata", "integration", "invalid_signature.go")
|
||||
sourceFile, err := filepath.Abs(sourceFile)
|
||||
require.NoError(t, err)
|
||||
defer suite.cleanupGeneratedFiles(sourceFile)
|
||||
|
||||
targetFile, err := suite.createGoModule(sourceFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = suite.runExtensionInit(targetFile)
|
||||
assert.Error(t, err, "extension-init should fail for invalid return type")
|
||||
assert.Contains(t, err.Error(), "no PHP functions, classes, or constants found", "invalid functions should be ignored, resulting in no valid exports")
|
||||
}
|
||||
|
||||
func TestTypeMismatch(t *testing.T) {
|
||||
suite := setupTest(t)
|
||||
|
||||
sourceFile := filepath.Join("..", "..", "testdata", "integration", "type_mismatch.go")
|
||||
sourceFile, err := filepath.Abs(sourceFile)
|
||||
require.NoError(t, err)
|
||||
defer suite.cleanupGeneratedFiles(sourceFile)
|
||||
|
||||
targetFile, err := suite.createGoModule(sourceFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = suite.runExtensionInit(targetFile)
|
||||
assert.NoError(t, err, "generation should succeed - class is valid even though function/method have type mismatches")
|
||||
|
||||
baseDir := filepath.Dir(targetFile)
|
||||
baseName := strings.TrimSuffix(filepath.Base(targetFile), ".go")
|
||||
stubFile := filepath.Join(baseDir, baseName+".stub.php")
|
||||
assert.FileExists(t, stubFile, "stub file should be generated for valid class")
|
||||
}
|
||||
|
||||
func TestMissingGenStub(t *testing.T) {
|
||||
// temp override of GEN_STUB_SCRIPT
|
||||
originalValue := os.Getenv("GEN_STUB_SCRIPT")
|
||||
defer os.Setenv("GEN_STUB_SCRIPT", originalValue)
|
||||
|
||||
os.Setenv("GEN_STUB_SCRIPT", "/nonexistent/gen_stub.php")
|
||||
|
||||
tempDir := t.TempDir()
|
||||
sourceFile := filepath.Join(tempDir, "test.go")
|
||||
|
||||
err := os.WriteFile(sourceFile, []byte(`package test
|
||||
|
||||
//export_php:function dummy(): void
|
||||
func dummy() {}
|
||||
`), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
baseName := SanitizePackageName(strings.TrimSuffix(filepath.Base(sourceFile), ".go"))
|
||||
gen := Generator{
|
||||
BaseName: baseName,
|
||||
SourceFile: sourceFile,
|
||||
BuildDir: filepath.Dir(sourceFile),
|
||||
}
|
||||
|
||||
err = gen.Generate()
|
||||
assert.Error(t, err, "should fail when gen_stub.php is missing")
|
||||
assert.Contains(t, err.Error(), "gen_stub.php", "error should mention missing script")
|
||||
}
|
||||
|
||||
func TestCallable(t *testing.T) {
|
||||
suite := setupTest(t)
|
||||
|
||||
sourceFile := filepath.Join("..", "..", "testdata", "integration", "callable.go")
|
||||
sourceFile, err := filepath.Abs(sourceFile)
|
||||
require.NoError(t, err)
|
||||
defer suite.cleanupGeneratedFiles(sourceFile)
|
||||
|
||||
targetFile, err := suite.createGoModule(sourceFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = suite.runExtensionInit(targetFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = suite.compileFrankenPHP(filepath.Dir(targetFile))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = suite.verifyPHPSymbols(
|
||||
[]string{"my_array_map", "my_filter"},
|
||||
[]string{"Processor"},
|
||||
[]string{},
|
||||
)
|
||||
require.NoError(t, err, "all functions and classes should be accessible from PHP")
|
||||
|
||||
err = suite.verifyFunctionBehavior(`<?php
|
||||
|
||||
$result = my_array_map([1, 2, 3], function($x) { return $x * 2; });
|
||||
if ($result !== [2, 4, 6]) {
|
||||
echo "FAIL: my_array_map with closure expected [2, 4, 6], got " . json_encode($result);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$result = my_array_map(['hello', 'world'], 'strtoupper');
|
||||
if ($result !== ['HELLO', 'WORLD']) {
|
||||
echo "FAIL: my_array_map with function name expected ['HELLO', 'WORLD'], got " . json_encode($result);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$result = my_array_map([], function($x) { return $x; });
|
||||
if ($result !== []) {
|
||||
echo "FAIL: my_array_map with empty array expected [], got " . json_encode($result);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$result = my_filter([1, 2, 3, 4, 5, 6], function($x) { return $x % 2 === 0; });
|
||||
if ($result !== [2, 4, 6]) {
|
||||
echo "FAIL: my_filter expected [2, 4, 6], got " . json_encode($result);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$result = my_filter([1, 2, 3, 4], null);
|
||||
if ($result !== [1, 2, 3, 4]) {
|
||||
echo "FAIL: my_filter with null callback expected [1, 2, 3, 4], got " . json_encode($result);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$processor = new Processor();
|
||||
$result = $processor->transform('hello', function($s) { return strtoupper($s); });
|
||||
if ($result !== 'HELLO') {
|
||||
echo "FAIL: Processor::transform with closure expected 'HELLO', got '$result'";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$result = $processor->transform('world', 'strtoupper');
|
||||
if ($result !== 'WORLD') {
|
||||
echo "FAIL: Processor::transform with function name expected 'WORLD', got '$result'";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$result = $processor->transform(' test ', 'trim');
|
||||
if ($result !== 'test') {
|
||||
echo "FAIL: Processor::transform with trim expected 'test', got '$result'";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "OK";
|
||||
`, "OK")
|
||||
require.NoError(t, err, "all callable tests should pass")
|
||||
}
|
||||
@@ -9,17 +9,18 @@ import (
|
||||
type phpType string
|
||||
|
||||
const (
|
||||
phpString phpType = "string"
|
||||
phpInt phpType = "int"
|
||||
phpFloat phpType = "float"
|
||||
phpBool phpType = "bool"
|
||||
phpArray phpType = "array"
|
||||
phpObject phpType = "object"
|
||||
phpMixed phpType = "mixed"
|
||||
phpVoid phpType = "void"
|
||||
phpNull phpType = "null"
|
||||
phpTrue phpType = "true"
|
||||
phpFalse phpType = "false"
|
||||
phpString phpType = "string"
|
||||
phpInt phpType = "int"
|
||||
phpFloat phpType = "float"
|
||||
phpBool phpType = "bool"
|
||||
phpArray phpType = "array"
|
||||
phpObject phpType = "object"
|
||||
phpMixed phpType = "mixed"
|
||||
phpVoid phpType = "void"
|
||||
phpNull phpType = "null"
|
||||
phpTrue phpType = "true"
|
||||
phpFalse phpType = "false"
|
||||
phpCallable phpType = "callable"
|
||||
)
|
||||
|
||||
type phpFunction struct {
|
||||
|
||||
@@ -68,8 +68,12 @@ func (pp *ParameterParser) generateSingleParamDeclaration(param phpParameter) []
|
||||
if param.IsNullable {
|
||||
decls = append(decls, fmt.Sprintf("zend_bool %s_is_null = 0;", param.Name))
|
||||
}
|
||||
case phpArray, phpMixed:
|
||||
case phpArray:
|
||||
decls = append(decls, fmt.Sprintf("zend_array *%s = NULL;", param.Name))
|
||||
case phpMixed:
|
||||
decls = append(decls, fmt.Sprintf("zval *%s = NULL;", param.Name))
|
||||
case "callable":
|
||||
decls = append(decls, fmt.Sprintf("zval *%s_callback;", param.Name))
|
||||
}
|
||||
|
||||
return decls
|
||||
@@ -118,9 +122,11 @@ func (pp *ParameterParser) generateParamParsingMacro(param phpParameter) string
|
||||
case phpBool:
|
||||
return fmt.Sprintf("\n Z_PARAM_BOOL_OR_NULL(%s, %s_is_null)", param.Name, param.Name)
|
||||
case phpArray:
|
||||
return fmt.Sprintf("\n Z_PARAM_ARRAY_OR_NULL(%s)", param.Name)
|
||||
return fmt.Sprintf("\n Z_PARAM_ARRAY_HT_OR_NULL(%s)", param.Name)
|
||||
case phpMixed:
|
||||
return fmt.Sprintf("\n Z_PARAM_ZVAL_OR_NULL(%s)", param.Name)
|
||||
case phpCallable:
|
||||
return fmt.Sprintf("\n Z_PARAM_ZVAL_OR_NULL(%s_callback)", param.Name)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
@@ -135,9 +141,11 @@ func (pp *ParameterParser) generateParamParsingMacro(param phpParameter) string
|
||||
case phpBool:
|
||||
return fmt.Sprintf("\n Z_PARAM_BOOL(%s)", param.Name)
|
||||
case phpArray:
|
||||
return fmt.Sprintf("\n Z_PARAM_ARRAY(%s)", param.Name)
|
||||
return fmt.Sprintf("\n Z_PARAM_ARRAY_HT(%s)", param.Name)
|
||||
case phpMixed:
|
||||
return fmt.Sprintf("\n Z_PARAM_ZVAL(%s)", param.Name)
|
||||
case phpCallable:
|
||||
return fmt.Sprintf("\n Z_PARAM_ZVAL(%s_callback)", param.Name)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
@@ -168,6 +176,8 @@ func (pp *ParameterParser) generateSingleGoCallParam(param phpParameter) string
|
||||
return fmt.Sprintf("%s_is_null ? NULL : &%s", param.Name, param.Name)
|
||||
case phpBool:
|
||||
return fmt.Sprintf("%s_is_null ? NULL : &%s", param.Name, param.Name)
|
||||
case phpCallable:
|
||||
return fmt.Sprintf("%s_callback", param.Name)
|
||||
default:
|
||||
return param.Name
|
||||
}
|
||||
@@ -180,6 +190,8 @@ func (pp *ParameterParser) generateSingleGoCallParam(param phpParameter) string
|
||||
return fmt.Sprintf("(double) %s", param.Name)
|
||||
case phpBool:
|
||||
return fmt.Sprintf("(int) %s", param.Name)
|
||||
case phpCallable:
|
||||
return fmt.Sprintf("%s_callback", param.Name)
|
||||
default:
|
||||
return param.Name
|
||||
}
|
||||
|
||||
@@ -145,14 +145,14 @@ func TestParameterParser_GenerateParamDeclarations(t *testing.T) {
|
||||
params: []phpParameter{
|
||||
{Name: "items", PhpType: phpArray, HasDefault: false},
|
||||
},
|
||||
expected: " zval *items = NULL;",
|
||||
expected: " zend_array *items = NULL;",
|
||||
},
|
||||
{
|
||||
name: "nullable array parameter",
|
||||
params: []phpParameter{
|
||||
{Name: "items", PhpType: phpArray, HasDefault: false, IsNullable: true},
|
||||
},
|
||||
expected: " zval *items = NULL;",
|
||||
expected: " zend_array *items = NULL;",
|
||||
},
|
||||
{
|
||||
name: "mixed types with array",
|
||||
@@ -161,7 +161,7 @@ func TestParameterParser_GenerateParamDeclarations(t *testing.T) {
|
||||
{Name: "items", PhpType: phpArray, HasDefault: false},
|
||||
{Name: "count", PhpType: phpInt, HasDefault: true, DefaultValue: "5"},
|
||||
},
|
||||
expected: " zend_string *name = NULL;\n zval *items = NULL;\n zend_long count = 5;",
|
||||
expected: " zend_string *name = NULL;\n zend_array *items = NULL;\n zend_long count = 5;",
|
||||
},
|
||||
{
|
||||
name: "mixed parameter",
|
||||
@@ -177,6 +177,29 @@ func TestParameterParser_GenerateParamDeclarations(t *testing.T) {
|
||||
},
|
||||
expected: " zval *m = NULL;",
|
||||
},
|
||||
{
|
||||
name: "callable parameter",
|
||||
params: []phpParameter{
|
||||
{Name: "callback", PhpType: phpCallable, HasDefault: false},
|
||||
},
|
||||
expected: " zval *callback_callback;",
|
||||
},
|
||||
{
|
||||
name: "nullable callable parameter",
|
||||
params: []phpParameter{
|
||||
{Name: "callback", PhpType: phpCallable, HasDefault: false, IsNullable: true},
|
||||
},
|
||||
expected: " zval *callback_callback;",
|
||||
},
|
||||
{
|
||||
name: "mixed types with callable",
|
||||
params: []phpParameter{
|
||||
{Name: "data", PhpType: phpArray, HasDefault: false},
|
||||
{Name: "callback", PhpType: phpCallable, HasDefault: false},
|
||||
{Name: "options", PhpType: phpInt, HasDefault: true, DefaultValue: "0"},
|
||||
},
|
||||
expected: " zend_array *data = NULL;\n zval *callback_callback;\n zend_long options = 0;",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -292,6 +315,29 @@ func TestParameterParser_GenerateGoCallParams(t *testing.T) {
|
||||
},
|
||||
expected: "name, items, (long) count",
|
||||
},
|
||||
{
|
||||
name: "callable parameter",
|
||||
params: []phpParameter{
|
||||
{Name: "callback", PhpType: "callable"},
|
||||
},
|
||||
expected: "callback_callback",
|
||||
},
|
||||
{
|
||||
name: "nullable callable parameter",
|
||||
params: []phpParameter{
|
||||
{Name: "callback", PhpType: "callable", IsNullable: true},
|
||||
},
|
||||
expected: "callback_callback",
|
||||
},
|
||||
{
|
||||
name: "mixed parameters with callable",
|
||||
params: []phpParameter{
|
||||
{Name: "data", PhpType: "array"},
|
||||
{Name: "callback", PhpType: "callable"},
|
||||
{Name: "limit", PhpType: "int"},
|
||||
},
|
||||
expected: "data, callback_callback, (long) limit",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -353,12 +399,12 @@ func TestParameterParser_GenerateParamParsingMacro(t *testing.T) {
|
||||
{
|
||||
name: "array parameter",
|
||||
param: phpParameter{Name: "items", PhpType: phpArray},
|
||||
expected: "\n Z_PARAM_ARRAY(items)",
|
||||
expected: "\n Z_PARAM_ARRAY_HT(items)",
|
||||
},
|
||||
{
|
||||
name: "nullable array parameter",
|
||||
param: phpParameter{Name: "items", PhpType: phpArray, IsNullable: true},
|
||||
expected: "\n Z_PARAM_ARRAY_OR_NULL(items)",
|
||||
expected: "\n Z_PARAM_ARRAY_HT_OR_NULL(items)",
|
||||
},
|
||||
{
|
||||
name: "mixed parameter",
|
||||
@@ -370,6 +416,16 @@ func TestParameterParser_GenerateParamParsingMacro(t *testing.T) {
|
||||
param: phpParameter{Name: "m", PhpType: phpMixed, IsNullable: true},
|
||||
expected: "\n Z_PARAM_ZVAL_OR_NULL(m)",
|
||||
},
|
||||
{
|
||||
name: "callable parameter",
|
||||
param: phpParameter{Name: "callback", PhpType: phpCallable},
|
||||
expected: "\n Z_PARAM_ZVAL(callback_callback)",
|
||||
},
|
||||
{
|
||||
name: "nullable callable parameter",
|
||||
param: phpParameter{Name: "callback", PhpType: phpCallable, IsNullable: true},
|
||||
expected: "\n Z_PARAM_ZVAL_OR_NULL(callback_callback)",
|
||||
},
|
||||
{
|
||||
name: "unknown type",
|
||||
param: phpParameter{Name: "unknown", PhpType: phpType("unknown")},
|
||||
@@ -480,6 +536,16 @@ func TestParameterParser_GenerateSingleGoCallParam(t *testing.T) {
|
||||
param: phpParameter{Name: "items", PhpType: phpArray, IsNullable: true},
|
||||
expected: "items",
|
||||
},
|
||||
{
|
||||
name: "callable parameter",
|
||||
param: phpParameter{Name: "callback", PhpType: "callable"},
|
||||
expected: "callback_callback",
|
||||
},
|
||||
{
|
||||
name: "nullable callable parameter",
|
||||
param: phpParameter{Name: "callback", PhpType: "callable", IsNullable: true},
|
||||
expected: "callback_callback",
|
||||
},
|
||||
{
|
||||
name: "unknown type",
|
||||
param: phpParameter{Name: "unknown", PhpType: phpType("unknown")},
|
||||
@@ -551,12 +617,22 @@ func TestParameterParser_GenerateSingleParamDeclaration(t *testing.T) {
|
||||
{
|
||||
name: "array parameter",
|
||||
param: phpParameter{Name: "items", PhpType: phpArray, HasDefault: false},
|
||||
expected: []string{"zval *items = NULL;"},
|
||||
expected: []string{"zend_array *items = NULL;"},
|
||||
},
|
||||
{
|
||||
name: "nullable array parameter",
|
||||
param: phpParameter{Name: "items", PhpType: phpArray, HasDefault: false, IsNullable: true},
|
||||
expected: []string{"zval *items = NULL;"},
|
||||
expected: []string{"zend_array *items = NULL;"},
|
||||
},
|
||||
{
|
||||
name: "callable parameter",
|
||||
param: phpParameter{Name: "callback", PhpType: "callable", HasDefault: false},
|
||||
expected: []string{"zval *callback_callback;"},
|
||||
},
|
||||
{
|
||||
name: "nullable callable parameter",
|
||||
param: phpParameter{Name: "callback", PhpType: "callable", HasDefault: false, IsNullable: true},
|
||||
expected: []string{"zval *callback_callback;"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -107,8 +107,8 @@ func TestPHPFunctionGenerator_Generate(t *testing.T) {
|
||||
},
|
||||
contains: []string{
|
||||
"PHP_FUNCTION(process_array)",
|
||||
"zval *input = NULL;",
|
||||
"Z_PARAM_ARRAY(input)",
|
||||
"zend_array *input = NULL;",
|
||||
"Z_PARAM_ARRAY_HT(input)",
|
||||
"zend_array *result = process_array(input);",
|
||||
"RETURN_ARR(result)",
|
||||
},
|
||||
@@ -126,10 +126,10 @@ func TestPHPFunctionGenerator_Generate(t *testing.T) {
|
||||
},
|
||||
contains: []string{
|
||||
"PHP_FUNCTION(filter_array)",
|
||||
"zval *data = NULL;",
|
||||
"zend_array *data = NULL;",
|
||||
"zend_string *key = NULL;",
|
||||
"zend_long limit = 10;",
|
||||
"Z_PARAM_ARRAY(data)",
|
||||
"Z_PARAM_ARRAY_HT(data)",
|
||||
"Z_PARAM_STR(key)",
|
||||
"Z_PARAM_LONG(limit)",
|
||||
"ZEND_PARSE_PARAMETERS_START(2, 3)",
|
||||
@@ -201,7 +201,7 @@ func TestPHPFunctionGenerator_GenerateParamDeclarations(t *testing.T) {
|
||||
{Name: "items", PhpType: phpArray},
|
||||
},
|
||||
contains: []string{
|
||||
"zval *items = NULL;",
|
||||
"zend_array *items = NULL;",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -213,7 +213,7 @@ func TestPHPFunctionGenerator_GenerateParamDeclarations(t *testing.T) {
|
||||
},
|
||||
contains: []string{
|
||||
"zend_string *name = NULL;",
|
||||
"zval *data = NULL;",
|
||||
"zend_array *data = NULL;",
|
||||
"zend_long count = 0;",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -95,7 +95,9 @@ PHP_METHOD({{namespacedClassName $.Namespace .ClassName}}, {{.PhpName}}) {
|
||||
zend_bool {{$param.Name}} = {{if $param.HasDefault}}{{if eq $param.DefaultValue "true"}}1{{else}}0{{end}}{{else}}0{{end}};{{if $param.IsNullable}}
|
||||
zend_bool {{$param.Name}}_is_null = 0;{{end}}
|
||||
{{- else if eq $param.PhpType "array"}}
|
||||
zval *{{$param.Name}} = NULL;
|
||||
zend_array *{{$param.Name}} = NULL;
|
||||
{{- else if eq $param.PhpType "callable"}}
|
||||
zval *{{$param.Name}}_callback;
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
@@ -104,7 +106,7 @@ PHP_METHOD({{namespacedClassName $.Namespace .ClassName}}, {{.PhpName}}) {
|
||||
{{$optionalStarted := false}}{{range .Params}}{{if .HasDefault}}{{if not $optionalStarted -}}
|
||||
Z_PARAM_OPTIONAL
|
||||
{{$optionalStarted = true}}{{end}}{{end -}}
|
||||
{{if .IsNullable}}{{if eq .PhpType "string"}}Z_PARAM_STR_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType "int"}}Z_PARAM_LONG_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType "float"}}Z_PARAM_DOUBLE_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType "bool"}}Z_PARAM_BOOL_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType "array"}}Z_PARAM_ARRAY_OR_NULL({{.Name}}){{end}}{{else}}{{if eq .PhpType "string"}}Z_PARAM_STR({{.Name}}){{else if eq .PhpType "int"}}Z_PARAM_LONG({{.Name}}){{else if eq .PhpType "float"}}Z_PARAM_DOUBLE({{.Name}}){{else if eq .PhpType "bool"}}Z_PARAM_BOOL({{.Name}}){{else if eq .PhpType "array"}}Z_PARAM_ARRAY({{.Name}}){{end}}{{end}}
|
||||
{{if .IsNullable}}{{if eq .PhpType "string"}}Z_PARAM_STR_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType "int"}}Z_PARAM_LONG_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType "float"}}Z_PARAM_DOUBLE_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType "bool"}}Z_PARAM_BOOL_OR_NULL({{.Name}}, {{.Name}}_is_null){{else if eq .PhpType "array"}}Z_PARAM_ARRAY_HT_OR_NULL({{.Name}}){{else if eq .PhpType "callable"}}Z_PARAM_ZVAL_OR_NULL({{.Name}}_callback){{end}}{{else}}{{if eq .PhpType "string"}}Z_PARAM_STR({{.Name}}){{else if eq .PhpType "int"}}Z_PARAM_LONG({{.Name}}){{else if eq .PhpType "float"}}Z_PARAM_DOUBLE({{.Name}}){{else if eq .PhpType "bool"}}Z_PARAM_BOOL({{.Name}}){{else if eq .PhpType "array"}}Z_PARAM_ARRAY_HT({{.Name}}){{else if eq .PhpType "callable"}}Z_PARAM_ZVAL({{.Name}}_callback){{end}}{{end}}
|
||||
{{end -}}
|
||||
ZEND_PARSE_PARAMETERS_END();
|
||||
{{else}}
|
||||
@@ -113,22 +115,22 @@ PHP_METHOD({{namespacedClassName $.Namespace .ClassName}}, {{.PhpName}}) {
|
||||
|
||||
{{- if ne .ReturnType "void"}}
|
||||
{{- if eq .ReturnType "string"}}
|
||||
zend_string* result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{end}}{{else}}{{.Name}}{{end}}{{end}}{{end}});
|
||||
zend_string* result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{end}}{{else}}{{if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{else}}{{.Name}}{{end}}{{end}}{{end}}{{end}});
|
||||
if (result) {
|
||||
RETURN_STR(result);
|
||||
}
|
||||
RETURN_EMPTY_STRING();
|
||||
{{- else if eq .ReturnType "int"}}
|
||||
zend_long result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{end}}{{else}}{{if eq .PhpType "array"}}{{.Name}}{{else}}(long){{.Name}}{{end}}{{end}}{{end}}{{end}});
|
||||
zend_long result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{end}}{{else}}{{if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{else}}(long){{.Name}}{{end}}{{end}}{{end}}{{end}});
|
||||
RETURN_LONG(result);
|
||||
{{- else if eq .ReturnType "float"}}
|
||||
double result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{end}}{{else}}{{if eq .PhpType "array"}}{{.Name}}{{else}}(double){{.Name}}{{end}}{{end}}{{end}}{{end}});
|
||||
double result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{end}}{{else}}{{if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{else}}(double){{.Name}}{{end}}{{end}}{{end}}{{end}});
|
||||
RETURN_DOUBLE(result);
|
||||
{{- else if eq .ReturnType "bool"}}
|
||||
int result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{end}}{{else}}{{if eq .PhpType "array"}}{{.Name}}{{else}}(int){{.Name}}{{end}}{{end}}{{end}}{{end}});
|
||||
int result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{end}}{{else}}{{if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{else}}(int){{.Name}}{{end}}{{end}}{{end}}{{end}});
|
||||
RETURN_BOOL(result);
|
||||
{{- else if eq .ReturnType "array"}}
|
||||
void* result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{end}}{{else}}{{.Name}}{{end}}{{end}}{{end}});
|
||||
void* result = {{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{end}}{{else}}{{if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{else}}{{.Name}}{{end}}{{end}}{{end}}{{end}});
|
||||
if (result != NULL) {
|
||||
HashTable *ht = (HashTable*)result;
|
||||
RETURN_ARR(ht);
|
||||
@@ -137,7 +139,7 @@ PHP_METHOD({{namespacedClassName $.Namespace .ClassName}}, {{.PhpName}}) {
|
||||
}
|
||||
{{- end}}
|
||||
{{- else}}
|
||||
{{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{end}}{{else}}{{if eq .PhpType "string"}}{{.Name}}{{else if eq .PhpType "int"}}(long){{.Name}}{{else if eq .PhpType "float"}}(double){{.Name}}{{else if eq .PhpType "bool"}}(int){{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{end}}{{end}}{{end}}{{end}});
|
||||
{{.Name}}_wrapper(intern->go_handle{{if .Params}}{{range .Params}}, {{if .IsNullable}}{{if eq .PhpType "string"}}{{.Name}}_is_null ? NULL : {{.Name}}{{else if eq .PhpType "int"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "float"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "bool"}}{{.Name}}_is_null ? NULL : &{{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{end}}{{else}}{{if eq .PhpType "string"}}{{.Name}}{{else if eq .PhpType "int"}}(long){{.Name}}{{else if eq .PhpType "float"}}(double){{.Name}}{{else if eq .PhpType "bool"}}(int){{.Name}}{{else if eq .PhpType "array"}}{{.Name}}{{else if eq .PhpType "callable"}}{{.Name}}_callback{{end}}{{end}}{{end}}{{end}});
|
||||
{{- end}}
|
||||
}
|
||||
{{end}}{{end}}
|
||||
|
||||
@@ -76,7 +76,7 @@ func create_{{.GoStruct}}_object() C.uintptr_t {
|
||||
{{- end}}
|
||||
{{- range .Methods}}
|
||||
//export {{.Name}}_wrapper
|
||||
func {{.Name}}_wrapper(handle C.uintptr_t{{range .Params}}{{if eq .PhpType "string"}}, {{.Name}} *C.zend_string{{else if eq .PhpType "array"}}, {{.Name}} *C.zval{{else}}, {{.Name}} {{if .IsNullable}}*{{end}}{{phpTypeToGoType .PhpType}}{{end}}{{end}}){{if not (isVoid .ReturnType)}}{{if isStringOrArray .ReturnType}} unsafe.Pointer{{else}} {{phpTypeToGoType .ReturnType}}{{end}}{{end}} {
|
||||
func {{.Name}}_wrapper(handle C.uintptr_t{{range .Params}}{{if eq .PhpType "string"}}, {{.Name}} *C.zend_string{{else if eq .PhpType "array"}}, {{.Name}} *C.zval{{else if eq .PhpType "callable"}}, {{.Name}} *C.zval{{else}}, {{.Name}} {{if .IsNullable}}*{{end}}{{phpTypeToGoType .PhpType}}{{end}}{{end}}){{if not (isVoid .ReturnType)}}{{if isStringOrArray .ReturnType}} unsafe.Pointer{{else}} {{phpTypeToGoType .ReturnType}}{{end}}{{end}} {
|
||||
obj := getGoObject(handle)
|
||||
if obj == nil {
|
||||
{{- if not (isVoid .ReturnType)}}
|
||||
|
||||
@@ -11,10 +11,10 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
paramTypes = []phpType{phpString, phpInt, phpFloat, phpBool, phpArray, phpObject, phpMixed}
|
||||
paramTypes = []phpType{phpString, phpInt, phpFloat, phpBool, phpArray, phpObject, phpMixed, phpCallable}
|
||||
returnTypes = []phpType{phpVoid, phpString, phpInt, phpFloat, phpBool, phpArray, phpObject, phpMixed, phpNull, phpTrue, phpFalse}
|
||||
propTypes = []phpType{phpString, phpInt, phpFloat, phpBool, phpArray, phpObject, phpMixed}
|
||||
supportedTypes = []phpType{phpString, phpInt, phpFloat, phpBool, phpArray, phpMixed}
|
||||
supportedTypes = []phpType{phpString, phpInt, phpFloat, phpBool, phpArray, phpMixed, phpCallable}
|
||||
|
||||
functionNameRegex = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
||||
parameterNameRegex = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
||||
@@ -160,8 +160,10 @@ func (v *Validator) validateGoFunctionSignatureWithOptions(phpFunc phpFunction,
|
||||
effectiveGoParamCount = goParamCount - 1
|
||||
}
|
||||
|
||||
if len(phpFunc.Params) != effectiveGoParamCount {
|
||||
return fmt.Errorf("parameter count mismatch: PHP function has %d parameters but Go function has %d", len(phpFunc.Params), effectiveGoParamCount)
|
||||
expectedGoParams := len(phpFunc.Params)
|
||||
|
||||
if expectedGoParams != effectiveGoParamCount {
|
||||
return fmt.Errorf("parameter count mismatch: PHP function has %d parameters (expecting %d Go parameters) but Go function has %d", len(phpFunc.Params), expectedGoParams, effectiveGoParamCount)
|
||||
}
|
||||
|
||||
if goFunc.Type.Params != nil && len(phpFunc.Params) > 0 {
|
||||
@@ -203,13 +205,17 @@ func (v *Validator) phpTypeToGoType(t phpType, isNullable bool) string {
|
||||
baseType = "float64"
|
||||
case phpBool:
|
||||
baseType = "bool"
|
||||
case phpArray, phpMixed:
|
||||
case phpArray:
|
||||
baseType = "*C.zend_array"
|
||||
case phpMixed:
|
||||
baseType = "*C.zval"
|
||||
case phpCallable:
|
||||
baseType = "*C.zval"
|
||||
default:
|
||||
baseType = "any"
|
||||
}
|
||||
|
||||
if isNullable && t != phpString && t != phpArray {
|
||||
if isNullable && t != phpString && t != phpArray && t != phpCallable {
|
||||
return "*" + baseType
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,53 @@ func TestValidateFunction(t *testing.T) {
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "valid function with array parameter",
|
||||
function: phpFunction{
|
||||
Name: "arrayFunction",
|
||||
ReturnType: "array",
|
||||
Params: []phpParameter{
|
||||
{Name: "items", PhpType: phpArray},
|
||||
{Name: "filter", PhpType: phpString},
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "valid function with nullable array parameter",
|
||||
function: phpFunction{
|
||||
Name: "nullableArrayFunction",
|
||||
ReturnType: "string",
|
||||
Params: []phpParameter{
|
||||
{Name: "items", PhpType: phpArray, IsNullable: true},
|
||||
{Name: "name", PhpType: phpString},
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "valid function with callable parameter",
|
||||
function: phpFunction{
|
||||
Name: "callableFunction",
|
||||
ReturnType: "array",
|
||||
Params: []phpParameter{
|
||||
{Name: "data", PhpType: phpArray},
|
||||
{Name: "callback", PhpType: phpCallable},
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "valid function with nullable callable parameter",
|
||||
function: phpFunction{
|
||||
Name: "nullableCallableFunction",
|
||||
ReturnType: "string",
|
||||
Params: []phpParameter{
|
||||
{Name: "callback", PhpType: phpCallable, IsNullable: true},
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "empty function name",
|
||||
function: phpFunction{
|
||||
@@ -304,6 +351,23 @@ func TestValidateParameter(t *testing.T) {
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "valid callable parameter",
|
||||
param: phpParameter{
|
||||
Name: "callbackParam",
|
||||
PhpType: phpCallable,
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "valid nullable callable parameter",
|
||||
param: phpParameter{
|
||||
Name: "nullableCallbackParam",
|
||||
PhpType: "callable",
|
||||
IsNullable: true,
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "empty parameter name",
|
||||
param: phpParameter{
|
||||
@@ -484,6 +548,28 @@ func TestValidateTypes(t *testing.T) {
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "valid callable parameter",
|
||||
function: phpFunction{
|
||||
Name: "callableFunction",
|
||||
ReturnType: "array",
|
||||
Params: []phpParameter{
|
||||
{Name: "callbackParam", PhpType: phpCallable},
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "valid nullable callable parameter",
|
||||
function: phpFunction{
|
||||
Name: "nullableCallableFunction",
|
||||
ReturnType: "string",
|
||||
Params: []phpParameter{
|
||||
{Name: "callbackParam", PhpType: phpCallable, IsNullable: true},
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "invalid object parameter",
|
||||
function: phpFunction{
|
||||
@@ -600,7 +686,7 @@ func TestValidateGoFunctionSignature(t *testing.T) {
|
||||
}`,
|
||||
},
|
||||
expectError: true,
|
||||
errorMsg: "parameter count mismatch: PHP function has 2 parameters but Go function has 1",
|
||||
errorMsg: "parameter count mismatch: PHP function has 2 parameters (expecting 2 Go parameters) but Go function has 1",
|
||||
},
|
||||
{
|
||||
name: "parameter type mismatch",
|
||||
@@ -669,7 +755,7 @@ func TestValidateGoFunctionSignature(t *testing.T) {
|
||||
Params: []phpParameter{
|
||||
{Name: "items", PhpType: phpArray},
|
||||
},
|
||||
GoFunction: `func arrayFunc(items *C.zval) unsafe.Pointer {
|
||||
GoFunction: `func arrayFunc(items *C.zend_array) unsafe.Pointer {
|
||||
return nil
|
||||
}`,
|
||||
},
|
||||
@@ -684,7 +770,7 @@ func TestValidateGoFunctionSignature(t *testing.T) {
|
||||
{Name: "items", PhpType: phpArray, IsNullable: true},
|
||||
{Name: "name", PhpType: phpString},
|
||||
},
|
||||
GoFunction: `func nullableArrayFunc(items *C.zval, name *C.zend_string) unsafe.Pointer {
|
||||
GoFunction: `func nullableArrayFunc(items *C.zend_array, name *C.zend_string) unsafe.Pointer {
|
||||
return nil
|
||||
}`,
|
||||
},
|
||||
@@ -700,7 +786,51 @@ func TestValidateGoFunctionSignature(t *testing.T) {
|
||||
{Name: "filter", PhpType: phpString},
|
||||
{Name: "limit", PhpType: phpInt},
|
||||
},
|
||||
GoFunction: `func mixedFunc(data *C.zval, filter *C.zend_string, limit int64) unsafe.Pointer {
|
||||
GoFunction: `func mixedFunc(data *C.zend_array, filter *C.zend_string, limit int64) unsafe.Pointer {
|
||||
return nil
|
||||
}`,
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "valid callable parameter",
|
||||
phpFunc: phpFunction{
|
||||
Name: "callableFunc",
|
||||
ReturnType: "array",
|
||||
Params: []phpParameter{
|
||||
{Name: "callback", PhpType: phpCallable},
|
||||
},
|
||||
GoFunction: `func callableFunc(callback *C.zval) unsafe.Pointer {
|
||||
return nil
|
||||
}`,
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "valid nullable callable parameter",
|
||||
phpFunc: phpFunction{
|
||||
Name: "nullableCallableFunc",
|
||||
ReturnType: "string",
|
||||
Params: []phpParameter{
|
||||
{Name: "callback", PhpType: phpCallable, IsNullable: true},
|
||||
},
|
||||
GoFunction: `func nullableCallableFunc(callback *C.zval) unsafe.Pointer {
|
||||
return nil
|
||||
}`,
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "mixed callable and other parameters",
|
||||
phpFunc: phpFunction{
|
||||
Name: "mixedCallableFunc",
|
||||
ReturnType: "array",
|
||||
Params: []phpParameter{
|
||||
{Name: "data", PhpType: phpArray},
|
||||
{Name: "callback", PhpType: phpCallable},
|
||||
{Name: "options", PhpType: "int"},
|
||||
},
|
||||
GoFunction: `func mixedCallableFunc(data *C.zend_array, callback *C.zval, options int64) unsafe.Pointer {
|
||||
return nil
|
||||
}`,
|
||||
},
|
||||
@@ -737,8 +867,10 @@ func TestPhpTypeToGoType(t *testing.T) {
|
||||
{"float", true, "*float64"},
|
||||
{"bool", false, "bool"},
|
||||
{"bool", true, "*bool"},
|
||||
{"array", false, "*C.zval"},
|
||||
{"array", true, "*C.zval"},
|
||||
{"array", false, "*C.zend_array"},
|
||||
{"array", true, "*C.zend_array"},
|
||||
{"callable", false, "*C.zval"},
|
||||
{"callable", true, "*C.zval"},
|
||||
{"unknown", false, "any"},
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,23 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
var (
|
||||
wd string
|
||||
wderr error
|
||||
)
|
||||
|
||||
func init() {
|
||||
wd, wderr = os.Getwd()
|
||||
|
||||
if wderr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
canonicalWD, err := filepath.EvalSymlinks(wd)
|
||||
if err != nil {
|
||||
wd = canonicalWD
|
||||
}
|
||||
}
|
||||
|
||||
// FastAbs is an optimized version of filepath.Abs for Unix systems,
|
||||
// since we don't expect the working directory to ever change once
|
||||
@@ -22,4 +39,3 @@ func FastAbs(path string) (string, error) {
|
||||
return filepath.Join(wd, path), nil
|
||||
}
|
||||
|
||||
var wd, wderr = os.Getwd()
|
||||
|
||||
@@ -96,15 +96,20 @@ func (ts *ThreadState) notifySubscribers(nextState State) {
|
||||
if len(ts.subscribers) == 0 {
|
||||
return
|
||||
}
|
||||
newSubscribers := []stateSubscriber{}
|
||||
|
||||
var newSubscribers []stateSubscriber
|
||||
|
||||
// notify subscribers to the state change
|
||||
for _, sub := range ts.subscribers {
|
||||
if !slices.Contains(sub.states, nextState) {
|
||||
newSubscribers = append(newSubscribers, sub)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
close(sub.ch)
|
||||
}
|
||||
|
||||
ts.subscribers = newSubscribers
|
||||
}
|
||||
|
||||
@@ -131,22 +136,25 @@ func (ts *ThreadState) RequestSafeStateChange(nextState State) bool {
|
||||
// disallow state changes if shutting down or done
|
||||
case ShuttingDown, Done, Reserved:
|
||||
ts.mu.Unlock()
|
||||
|
||||
return false
|
||||
// ready and inactive are safe states to transition from
|
||||
case Ready, Inactive:
|
||||
ts.currentState = nextState
|
||||
ts.notifySubscribers(nextState)
|
||||
ts.mu.Unlock()
|
||||
|
||||
return true
|
||||
}
|
||||
ts.mu.Unlock()
|
||||
|
||||
// wait for the state to change to a safe state
|
||||
ts.WaitFor(Ready, Inactive, ShuttingDown)
|
||||
|
||||
return ts.RequestSafeStateChange(nextState)
|
||||
}
|
||||
|
||||
// markAsWaiting hints that the thread reached a stable state and is waiting for requests or shutdown
|
||||
// MarkAsWaiting hints that the thread reached a stable state and is waiting for requests or shutdown
|
||||
func (ts *ThreadState) MarkAsWaiting(isWaiting bool) {
|
||||
ts.mu.Lock()
|
||||
if isWaiting {
|
||||
@@ -158,15 +166,16 @@ func (ts *ThreadState) MarkAsWaiting(isWaiting bool) {
|
||||
ts.mu.Unlock()
|
||||
}
|
||||
|
||||
// isWaitingState returns true if a thread is waiting for a request or shutdown
|
||||
// IsInWaitingState returns true if a thread is waiting for a request or shutdown
|
||||
func (ts *ThreadState) IsInWaitingState() bool {
|
||||
ts.mu.RLock()
|
||||
isWaiting := ts.isWaiting
|
||||
ts.mu.RUnlock()
|
||||
|
||||
return isWaiting
|
||||
}
|
||||
|
||||
// waitTime returns the time since the thread is waiting in a stable state in ms
|
||||
// WaitTime returns the time since the thread is waiting in a stable state in ms
|
||||
func (ts *ThreadState) WaitTime() int64 {
|
||||
ts.mu.RLock()
|
||||
waitTime := int64(0)
|
||||
@@ -174,6 +183,7 @@ func (ts *ThreadState) WaitTime() int64 {
|
||||
waitTime = time.Now().UnixMilli() - ts.waitingSince.UnixMilli()
|
||||
}
|
||||
ts.mu.RUnlock()
|
||||
|
||||
return waitTime
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
//go:build !nowatcher
|
||||
|
||||
package watcher
|
||||
|
||||
// #cgo LDFLAGS: -lwatcher-c -lstdc++
|
||||
import "C"
|
||||
224
internal/watcher/pattern.go
Normal file
224
internal/watcher/pattern.go
Normal file
@@ -0,0 +1,224 @@
|
||||
//go:build !nowatcher
|
||||
|
||||
package watcher
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/dunglas/frankenphp/internal/fastabs"
|
||||
"github.com/e-dant/watcher/watcher-go"
|
||||
)
|
||||
|
||||
type pattern struct {
|
||||
patternGroup *PatternGroup
|
||||
value string
|
||||
parsedValues []string
|
||||
events chan eventHolder
|
||||
failureCount int
|
||||
|
||||
watcher *watcher.Watcher
|
||||
}
|
||||
|
||||
func (p *pattern) startSession() {
|
||||
p.watcher = watcher.NewWatcher(p.value, p.handle)
|
||||
|
||||
if globalLogger.Enabled(globalCtx, slog.LevelDebug) {
|
||||
globalLogger.LogAttrs(globalCtx, slog.LevelDebug, "watching", slog.String("pattern", p.value))
|
||||
}
|
||||
}
|
||||
|
||||
// this method prepares the pattern struct (aka /path/*pattern)
|
||||
func (p *pattern) parse() (err error) {
|
||||
// first we clean the value
|
||||
absPattern, err := fastabs.FastAbs(p.value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.value = absPattern
|
||||
|
||||
// then we split the pattern to determine where the directory ends and the pattern starts
|
||||
splitPattern := strings.Split(absPattern, string(filepath.Separator))
|
||||
patternWithoutDir := ""
|
||||
for i, part := range splitPattern {
|
||||
isFilename := i == len(splitPattern)-1 && strings.Contains(part, ".")
|
||||
isGlobCharacter := strings.ContainsAny(part, "[*?{")
|
||||
|
||||
if isFilename || isGlobCharacter {
|
||||
patternWithoutDir = filepath.Join(splitPattern[i:]...)
|
||||
p.value = filepath.Join(splitPattern[:i]...)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// now we split the pattern according to the recursive '**' syntax
|
||||
p.parsedValues = strings.Split(patternWithoutDir, "**")
|
||||
for i, pp := range p.parsedValues {
|
||||
p.parsedValues[i] = strings.Trim(pp, string(filepath.Separator))
|
||||
}
|
||||
|
||||
// remove the trailing separator and add leading separator
|
||||
p.value = string(filepath.Separator) + strings.Trim(p.value, string(filepath.Separator))
|
||||
|
||||
// try to canonicalize the path
|
||||
canonicalPattern, err := filepath.EvalSymlinks(p.value)
|
||||
if err == nil {
|
||||
p.value = canonicalPattern
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *pattern) allowReload(event *watcher.Event) bool {
|
||||
if !isValidEventType(event.EffectType) || !isValidPathType(event) {
|
||||
return false
|
||||
}
|
||||
|
||||
// some editors create temporary files and never actually modify the original file
|
||||
// so we need to also check Event.AssociatedPathName
|
||||
// see https://github.com/php/frankenphp/issues/1375
|
||||
return p.isValidPattern(event.PathName) || p.isValidPattern(event.AssociatedPathName)
|
||||
}
|
||||
|
||||
func (p *pattern) handle(event *watcher.Event) {
|
||||
// If the watcher prematurely sends the die@ event, retry watching
|
||||
if event.PathType == watcher.PathTypeWatcher && strings.HasPrefix(event.PathName, "e/self/die@") && watcherIsActive.Load() {
|
||||
p.retryWatching()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if p.allowReload(event) {
|
||||
p.events <- eventHolder{p.patternGroup, event}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *pattern) stop() {
|
||||
p.watcher.Close()
|
||||
}
|
||||
|
||||
func isValidEventType(effectType watcher.EffectType) bool {
|
||||
return effectType <= watcher.EffectTypeDestroy
|
||||
}
|
||||
|
||||
func isValidPathType(event *watcher.Event) bool {
|
||||
if event.PathType == watcher.PathTypeWatcher && globalLogger.Enabled(globalCtx, slog.LevelDebug) {
|
||||
globalLogger.LogAttrs(globalCtx, slog.LevelDebug, "special e-dant/watcher event", slog.Any("event", event))
|
||||
}
|
||||
|
||||
return event.PathType <= watcher.PathTypeHardLink
|
||||
}
|
||||
|
||||
func (p *pattern) isValidPattern(fileName string) bool {
|
||||
if fileName == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// first we remove the dir from the file name
|
||||
if !strings.HasPrefix(fileName, p.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
// remove the directory path and separator from the filename
|
||||
fileNameWithoutDir := strings.TrimPrefix(strings.TrimPrefix(fileName, p.value), string(filepath.Separator))
|
||||
|
||||
// if the pattern has size 1 we can match it directly against the filename
|
||||
if len(p.parsedValues) == 1 {
|
||||
return matchCurlyBracePattern(p.parsedValues[0], fileNameWithoutDir)
|
||||
}
|
||||
|
||||
return p.matchPatterns(fileNameWithoutDir)
|
||||
}
|
||||
|
||||
func (p *pattern) matchPatterns(fileName string) bool {
|
||||
partsToMatch := strings.Split(fileName, string(filepath.Separator))
|
||||
cursor := 0
|
||||
|
||||
// if there are multiple parsedValues due to '**' we need to match them individually
|
||||
for i, pattern := range p.parsedValues {
|
||||
patternSize := strings.Count(pattern, string(filepath.Separator)) + 1
|
||||
|
||||
// if we are at the last pattern we will start matching from the end of the filename
|
||||
if i == len(p.parsedValues)-1 {
|
||||
cursor = len(partsToMatch) - patternSize
|
||||
|
||||
if cursor < 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// the cursor will move through the fileName until the pattern matches
|
||||
for j := cursor; j < len(partsToMatch); j++ {
|
||||
if j+patternSize > len(partsToMatch) {
|
||||
return false
|
||||
}
|
||||
|
||||
cursor = j
|
||||
subPattern := strings.Join(partsToMatch[j:j+patternSize], string(filepath.Separator))
|
||||
|
||||
if matchCurlyBracePattern(pattern, subPattern) {
|
||||
cursor = j + patternSize - 1
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if cursor > len(partsToMatch)-patternSize-1 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// we also check for the following syntax: /path/*.{php,twig,yaml}
|
||||
func matchCurlyBracePattern(pattern string, fileName string) bool {
|
||||
for _, subPattern := range expandCurlyBraces(pattern) {
|
||||
if matchPattern(subPattern, fileName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// {dir1,dir2}/path -> []string{"dir1/path", "dir2/path"}
|
||||
func expandCurlyBraces(s string) []string {
|
||||
before, rest, found := strings.Cut(s, "{")
|
||||
if !found {
|
||||
return []string{s}
|
||||
}
|
||||
|
||||
inside, after, found := strings.Cut(rest, "}")
|
||||
if !found {
|
||||
return []string{s} // no closing brace
|
||||
}
|
||||
|
||||
var out []string
|
||||
for _, subPattern := range strings.Split(inside, ",") {
|
||||
out = append(out, expandCurlyBraces(before+subPattern+after)...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func matchPattern(pattern string, fileName string) bool {
|
||||
if pattern == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
patternMatches, err := filepath.Match(pattern, fileName)
|
||||
|
||||
if err != nil {
|
||||
if globalLogger.Enabled(globalCtx, slog.LevelError) {
|
||||
globalLogger.LogAttrs(globalCtx, slog.LevelError, "failed to match filename", slog.String("file", fileName), slog.Any("error", err))
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return patternMatches
|
||||
}
|
||||
357
internal/watcher/pattern_test.go
Normal file
357
internal/watcher/pattern_test.go
Normal file
@@ -0,0 +1,357 @@
|
||||
//go:build !nowatcher
|
||||
|
||||
package watcher
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/e-dant/watcher/watcher-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDisallowOnEventTypeBiggerThan3(t *testing.T) {
|
||||
w := pattern{value: "/some/path"}
|
||||
require.NoError(t, w.parse())
|
||||
|
||||
assert.False(t, w.allowReload(&watcher.Event{PathName: "/some/path/watch-me.php", EffectType: watcher.EffectTypeOwner}))
|
||||
}
|
||||
|
||||
func TestDisallowOnPathTypeBiggerThan2(t *testing.T) {
|
||||
w := pattern{value: "/some/path"}
|
||||
require.NoError(t, w.parse())
|
||||
|
||||
assert.False(t, w.allowReload(&watcher.Event{PathName: "/some/path/watch-me.php", PathType: watcher.PathTypeSymLink}))
|
||||
}
|
||||
|
||||
func TestWatchesCorrectDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := []struct {
|
||||
pattern string
|
||||
dir string
|
||||
}{
|
||||
{"/path", "/path"},
|
||||
{"/path/", "/path"},
|
||||
{"/path/**/*.php", "/path"},
|
||||
{"/path/*.php", "/path"},
|
||||
{"/path/*/*.php", "/path"},
|
||||
{"/path/?path/*.php", "/path"},
|
||||
{"/path/{dir1,dir2}/**/*.php", "/path"},
|
||||
{".", relativeDir(t, "")},
|
||||
{"./", relativeDir(t, "")},
|
||||
{"./**", relativeDir(t, "")},
|
||||
{"..", relativeDir(t, "/..")},
|
||||
}
|
||||
|
||||
for _, d := range data {
|
||||
t.Run(d.pattern, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
hasDir(t, d.pattern, d.dir)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidRecursiveDirectories(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := []struct {
|
||||
pattern string
|
||||
dir string
|
||||
}{
|
||||
{"/path", "/path/file.php"},
|
||||
{"/path", "/path/subpath/file.php"},
|
||||
{"/path/", "/path/subpath/file.php"},
|
||||
{"/path**", "/path/subpath/file.php"},
|
||||
{"/path/**", "/path/subpath/file.php"},
|
||||
{"/path/**/", "/path/subpath/file.php"},
|
||||
{".", relativeDir(t, "file.php")},
|
||||
{".", relativeDir(t, "subpath/file.php")},
|
||||
{"./**", relativeDir(t, "subpath/file.php")},
|
||||
{"..", relativeDir(t, "subpath/file.php")},
|
||||
}
|
||||
|
||||
for _, d := range data {
|
||||
t.Run(d.pattern, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
shouldMatch(t, d.pattern, d.dir)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidRecursiveDirectories(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := []struct {
|
||||
pattern string
|
||||
dir string
|
||||
}{
|
||||
{"/path", "/other/file.php"},
|
||||
{"/path/**", "/other/file.php"},
|
||||
{".", "/other/file.php"},
|
||||
}
|
||||
|
||||
for _, d := range data {
|
||||
t.Run(d.pattern, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
shouldNotMatch(t, d.pattern, d.dir)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidNonRecursiveFilePatterns(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := []struct {
|
||||
pattern string
|
||||
dir string
|
||||
}{
|
||||
{"/*.php", "/file.php"},
|
||||
{"/path/*.php", "/path/file.php"},
|
||||
{"/path/?ile.php", "/path/file.php"},
|
||||
{"/path/file.php", "/path/file.php"},
|
||||
{"*.php", relativeDir(t, "file.php")},
|
||||
{"./*.php", relativeDir(t, "file.php")},
|
||||
}
|
||||
|
||||
for _, d := range data {
|
||||
t.Run(d.pattern, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
shouldMatch(t, d.pattern, d.dir)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInValidNonRecursiveFilePatterns(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := []struct {
|
||||
pattern string
|
||||
dir string
|
||||
}{
|
||||
{"/path/*.txt", "/path/file.php"},
|
||||
{"/path/*.php", "/path/subpath/file.php"},
|
||||
{"/*.php", "/path/file.php"},
|
||||
{"*.txt", relativeDir(t, "file.php")},
|
||||
{"*.php", relativeDir(t, "subpath/file.php")},
|
||||
}
|
||||
|
||||
for _, d := range data {
|
||||
t.Run(d.pattern, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
shouldNotMatch(t, d.pattern, d.dir)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidRecursiveFilePatterns(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := []struct {
|
||||
pattern string
|
||||
dir string
|
||||
}{
|
||||
{"/path/**/*.php", "/path/file.php"},
|
||||
{"/path/**/*.php", "/path/subpath/file.php"},
|
||||
{"/path/**/?ile.php", "/path/subpath/file.php"},
|
||||
{"/path/**/file.php", "/path/subpath/file.php"},
|
||||
{"**/*.php", relativeDir(t, "file.php")},
|
||||
{"**/*.php", relativeDir(t, "subpath/file.php")},
|
||||
{"./**/*.php", relativeDir(t, "subpath/file.php")},
|
||||
}
|
||||
|
||||
for _, d := range data {
|
||||
t.Run(d.pattern, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
shouldMatch(t, d.pattern, d.dir)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidRecursiveFilePatterns(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := []struct {
|
||||
pattern string
|
||||
dir string
|
||||
}{
|
||||
{"/path/**/*.txt", "/path/file.php"},
|
||||
{"/path/**/*.txt", "/other/file.php"},
|
||||
{"/path/**/*.txt", "/path/subpath/file.php"},
|
||||
{"/path/**/?ilm.php", "/path/subpath/file.php"},
|
||||
{"**/*.php", "/other/file.php"},
|
||||
{".**/*.php", "/other/file.php"},
|
||||
{"./**/*.php", "/other/file.php"},
|
||||
{"/a/**/very/long/path.php", "/a/short.php"},
|
||||
{"", ""},
|
||||
{"/a/**/b/c/d/**/e.php", "/a/x/e.php"},
|
||||
}
|
||||
|
||||
for _, d := range data {
|
||||
t.Run(d.pattern, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
shouldNotMatch(t, d.pattern, d.dir)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidDirectoryPatterns(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := []struct {
|
||||
pattern string
|
||||
dir string
|
||||
}{
|
||||
{"/path/*/*.php", "/path/subpath/file.php"},
|
||||
{"/path/*/*/*.php", "/path/subpath/subpath/file.php"},
|
||||
{"/path/?/*.php", "/path/1/file.php"},
|
||||
{"/path/**/vendor/*.php", "/path/vendor/file.php"},
|
||||
{"/path/**/vendor/*.php", "/path/subpath/vendor/file.php"},
|
||||
{"/path/**/vendor/**/*.php", "/path/vendor/file.php"},
|
||||
{"/path/**/vendor/**/*.php", "/path/subpath/subpath/vendor/subpath/subpath/file.php"},
|
||||
{"/path/**/vendor/*/*.php", "/path/subpath/subpath/vendor/subpath/file.php"},
|
||||
{"/path*/path*/*", "/path1/path2/file.php"},
|
||||
}
|
||||
|
||||
for _, d := range data {
|
||||
t.Run(d.pattern, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
shouldMatch(t, d.pattern, d.dir)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidDirectoryPatterns(t *testing.T) {
|
||||
t.Parallel()
|
||||
data := []struct {
|
||||
pattern string
|
||||
dir string
|
||||
}{
|
||||
{"/path/subpath/*.php", "/path/other/file.php"},
|
||||
{"/path/*/*.php", "/path/subpath/subpath/file.php"},
|
||||
{"/path/?/*.php", "/path/subpath/file.php"},
|
||||
{"/path/*/*/*.php", "/path/subpath/file.php"},
|
||||
{"/path/*/*/*.php", "/path/subpath/subpath/subpath/file.php"},
|
||||
{"/path/**/vendor/*.php", "/path/subpath/vendor/subpath/file.php"},
|
||||
{"/path/**/vendor/*.php", "/path/subpath/file.php"},
|
||||
{"/path/**/vendor/**/*.php", "/path/subpath/file.php"},
|
||||
{"/path/**/vendor/**/*.txt", "/path/subpath/vendor/subpath/file.php"},
|
||||
{"/path/**/vendor/**/*.php", "/path/subpath/subpath/subpath/file.php"},
|
||||
{"/path/**/vendor/*/*.php", "/path/subpath/vendor/subpath/subpath/file.php"},
|
||||
{"/path*/path*", "/path1/path1/file.php"},
|
||||
}
|
||||
|
||||
for _, d := range data {
|
||||
t.Run(d.pattern, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
shouldNotMatch(t, d.pattern, d.dir)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidCurlyBracePatterns(t *testing.T) {
|
||||
data := []struct {
|
||||
pattern string
|
||||
dir string
|
||||
}{
|
||||
{"/path/*.{php}", "/path/file.php"},
|
||||
{"/path/*.{php,twig}", "/path/file.php"},
|
||||
{"/path/*.{php,twig}", "/path/file.twig"},
|
||||
{"/path/**/{file.php,file.twig}", "/path/subpath/file.twig"},
|
||||
{"/path/{dir1,dir2}/file.php", "/path/dir1/file.php"},
|
||||
{"/path/{dir1,dir2}/file.php", "/path/dir2/file.php"},
|
||||
{"/app/{app,config,resources}/**/*.php", "/app/app/subpath/file.php"},
|
||||
{"/app/{app,config,resources}/**/*.php", "/app/config/subpath/file.php"},
|
||||
{"/path/{dir1,dir2}/{a,b}{a,b}.php", "/path/dir1/ab.php"},
|
||||
{"/path/{dir1,dir2}/{a,b}{a,b}.php", "/path/dir2/aa.php"},
|
||||
{"/path/{dir1,dir2}/{a,b}{a,b}.php", "/path/dir2/bb.php"},
|
||||
{"/path/{dir1/test.php,dir2/test.php}", "/path/dir1/test.php"},
|
||||
}
|
||||
|
||||
for _, d := range data {
|
||||
t.Run(d.pattern, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
shouldMatch(t, d.pattern, d.dir)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidCurlyBracePatterns(t *testing.T) {
|
||||
data := []struct {
|
||||
pattern string
|
||||
dir string
|
||||
}{
|
||||
{"/path/*.{php}", "/path/file.txt"},
|
||||
{"/path/*.{php,twig}", "/path/file.txt"},
|
||||
{"/path/{file.php,file.twig}", "/path/file.txt"},
|
||||
{"/path/{dir1,dir2}/file.php", "/path/dir3/file.php"},
|
||||
{"/path/{dir1,dir2}/**/*.php", "/path/dir1/subpath/file.txt"},
|
||||
{"/path/{dir1,dir2}/{a,b}{a,b}.php", "/path/dir1/ac.php"},
|
||||
{"/path/{}/{a,b}{a,b}.php", "/path/dir1/ac.php"},
|
||||
{"/path/}dir{/{a,b}{a,b}.php", "/path/dir1/aa.php"},
|
||||
}
|
||||
|
||||
for _, d := range data {
|
||||
t.Run(d.pattern, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
shouldNotMatch(t, d.pattern, d.dir)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAnAssociatedEventTriggersTheWatcher(t *testing.T) {
|
||||
w := pattern{value: "/**/*.php"}
|
||||
require.NoError(t, w.parse())
|
||||
w.events = make(chan eventHolder)
|
||||
|
||||
e := &watcher.Event{PathName: "/path/temporary_file", AssociatedPathName: "/path/file.php"}
|
||||
go w.handle(e)
|
||||
|
||||
assert.Equal(t, e, (<-w.events).event)
|
||||
}
|
||||
|
||||
func relativeDir(t *testing.T, relativePath string) string {
|
||||
dir, err := filepath.Abs("./" + relativePath)
|
||||
assert.NoError(t, err)
|
||||
return dir
|
||||
}
|
||||
|
||||
func hasDir(t *testing.T, p string, dir string) {
|
||||
t.Helper()
|
||||
|
||||
w := pattern{value: p}
|
||||
require.NoError(t, w.parse())
|
||||
|
||||
assert.Equal(t, dir, w.value)
|
||||
}
|
||||
|
||||
func shouldMatch(t *testing.T, p string, fileName string) {
|
||||
t.Helper()
|
||||
|
||||
w := pattern{value: p}
|
||||
require.NoError(t, w.parse())
|
||||
|
||||
assert.True(t, w.allowReload(&watcher.Event{PathName: fileName}))
|
||||
}
|
||||
|
||||
func shouldNotMatch(t *testing.T, p string, fileName string) {
|
||||
t.Helper()
|
||||
|
||||
w := pattern{value: p}
|
||||
require.NoError(t, w.parse())
|
||||
|
||||
assert.False(t, w.allowReload(&watcher.Event{PathName: fileName}))
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
//go:build !nowatcher
|
||||
|
||||
package watcher
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/dunglas/frankenphp/internal/fastabs"
|
||||
)
|
||||
|
||||
type watchPattern struct {
|
||||
dir string
|
||||
patterns []string
|
||||
trigger chan string
|
||||
failureCount int
|
||||
}
|
||||
|
||||
func parseFilePatterns(filePatterns []string) ([]*watchPattern, error) {
|
||||
watchPatterns := make([]*watchPattern, 0, len(filePatterns))
|
||||
for _, filePattern := range filePatterns {
|
||||
watchPattern, err := parseFilePattern(filePattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
watchPatterns = append(watchPatterns, watchPattern)
|
||||
}
|
||||
return watchPatterns, nil
|
||||
}
|
||||
|
||||
// this method prepares the watchPattern struct for a single file pattern (aka /path/*pattern)
|
||||
func parseFilePattern(filePattern string) (*watchPattern, error) {
|
||||
w := &watchPattern{}
|
||||
|
||||
// first we clean the pattern
|
||||
absPattern, err := fastabs.FastAbs(filePattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w.dir = absPattern
|
||||
|
||||
// then we split the pattern to determine where the directory ends and the pattern starts
|
||||
splitPattern := strings.Split(absPattern, string(filepath.Separator))
|
||||
patternWithoutDir := ""
|
||||
for i, part := range splitPattern {
|
||||
isFilename := i == len(splitPattern)-1 && strings.Contains(part, ".")
|
||||
isGlobCharacter := strings.ContainsAny(part, "[*?{")
|
||||
if isFilename || isGlobCharacter {
|
||||
patternWithoutDir = filepath.Join(splitPattern[i:]...)
|
||||
w.dir = filepath.Join(splitPattern[:i]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// now we split the pattern according to the recursive '**' syntax
|
||||
w.patterns = strings.Split(patternWithoutDir, "**")
|
||||
for i, pattern := range w.patterns {
|
||||
w.patterns[i] = strings.Trim(pattern, string(filepath.Separator))
|
||||
}
|
||||
|
||||
// finally, we remove the trailing separator and add leading separator
|
||||
w.dir = string(filepath.Separator) + strings.Trim(w.dir, string(filepath.Separator))
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
func (watchPattern *watchPattern) allowReload(fileName string, eventType int, pathType int) bool {
|
||||
if !isValidEventType(eventType) || !isValidPathType(pathType, fileName) {
|
||||
return false
|
||||
}
|
||||
|
||||
return isValidPattern(fileName, watchPattern.dir, watchPattern.patterns)
|
||||
}
|
||||
|
||||
// 0:rename,1:modify,2:create,3:destroy,4:owner,5:other,
|
||||
func isValidEventType(eventType int) bool {
|
||||
return eventType <= 3
|
||||
}
|
||||
|
||||
// 0:dir,1:file,2:hard_link,3:sym_link,4:watcher,5:other,
|
||||
func isValidPathType(pathType int, fileName string) bool {
|
||||
if pathType == 4 && logger.Enabled(ctx, slog.LevelDebug) {
|
||||
logger.LogAttrs(ctx, slog.LevelDebug, "special edant/watcher event", slog.String("fileName", fileName))
|
||||
}
|
||||
|
||||
return pathType <= 2
|
||||
}
|
||||
|
||||
func isValidPattern(fileName string, dir string, patterns []string) bool {
|
||||
// first we remove the dir from the pattern
|
||||
if !strings.HasPrefix(fileName, dir) {
|
||||
return false
|
||||
}
|
||||
|
||||
// remove the dir and separator from the filename
|
||||
fileNameWithoutDir := strings.TrimPrefix(strings.TrimPrefix(fileName, dir), string(filepath.Separator))
|
||||
|
||||
// if the pattern has size 1 we can match it directly against the filename
|
||||
if len(patterns) == 1 {
|
||||
return matchBracketPattern(patterns[0], fileNameWithoutDir)
|
||||
}
|
||||
|
||||
return matchPatterns(patterns, fileNameWithoutDir)
|
||||
}
|
||||
|
||||
func matchPatterns(patterns []string, fileName string) bool {
|
||||
partsToMatch := strings.Split(fileName, string(filepath.Separator))
|
||||
cursor := 0
|
||||
|
||||
// if there are multiple patterns due to '**' we need to match them individually
|
||||
for i, pattern := range patterns {
|
||||
patternSize := strings.Count(pattern, string(filepath.Separator)) + 1
|
||||
|
||||
// if we are at the last pattern we will start matching from the end of the filename
|
||||
if i == len(patterns)-1 {
|
||||
cursor = len(partsToMatch) - patternSize
|
||||
}
|
||||
|
||||
// the cursor will move through the fileName until the pattern matches
|
||||
for j := cursor; j < len(partsToMatch); j++ {
|
||||
cursor = j
|
||||
subPattern := strings.Join(partsToMatch[j:j+patternSize], string(filepath.Separator))
|
||||
if matchBracketPattern(pattern, subPattern) {
|
||||
cursor = j + patternSize - 1
|
||||
break
|
||||
}
|
||||
if cursor > len(partsToMatch)-patternSize-1 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// we also check for the following bracket syntax: /path/*.{php,twig,yaml}
|
||||
func matchBracketPattern(pattern string, fileName string) bool {
|
||||
openingBracket := strings.Index(pattern, "{")
|
||||
closingBracket := strings.Index(pattern, "}")
|
||||
|
||||
// if there are no brackets we can match regularly
|
||||
if openingBracket == -1 || closingBracket == -1 {
|
||||
return matchPattern(pattern, fileName)
|
||||
}
|
||||
|
||||
beforeTheBrackets := pattern[:openingBracket]
|
||||
betweenTheBrackets := pattern[openingBracket+1 : closingBracket]
|
||||
afterTheBrackets := pattern[closingBracket+1:]
|
||||
|
||||
// all bracket entries are checked individually, only one needs to match
|
||||
// *.{php,twig,yaml} -> *.php, *.twig, *.yaml
|
||||
for pattern := range strings.SplitSeq(betweenTheBrackets, ",") {
|
||||
if matchPattern(beforeTheBrackets+pattern+afterTheBrackets, fileName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func matchPattern(pattern string, fileName string) bool {
|
||||
if pattern == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
patternMatches, err := filepath.Match(pattern, fileName)
|
||||
|
||||
if err != nil {
|
||||
if logger.Enabled(ctx, slog.LevelError) {
|
||||
logger.LogAttrs(ctx, slog.LevelError, "failed to match filename", slog.String("file", fileName), slog.Any("error", err))
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return patternMatches
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
//go:build !nowatcher
|
||||
|
||||
package watcher
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDisallowOnEventTypeBiggerThan3(t *testing.T) {
|
||||
const fileName = "/some/path/watch-me.php"
|
||||
const eventType = 4
|
||||
|
||||
watchPattern, err := parseFilePattern("/some/path")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, watchPattern.allowReload(fileName, eventType, 0))
|
||||
}
|
||||
|
||||
func TestDisallowOnPathTypeBiggerThan2(t *testing.T) {
|
||||
const fileName = "/some/path/watch-me.php"
|
||||
const pathType = 3
|
||||
|
||||
watchPattern, err := parseFilePattern("/some/path")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, watchPattern.allowReload(fileName, 0, pathType))
|
||||
}
|
||||
|
||||
func TestWatchesCorrectDir(t *testing.T) {
|
||||
hasDir(t, "/path", "/path")
|
||||
hasDir(t, "/path/", "/path")
|
||||
hasDir(t, "/path/**/*.php", "/path")
|
||||
hasDir(t, "/path/*.php", "/path")
|
||||
hasDir(t, "/path/*/*.php", "/path")
|
||||
hasDir(t, "/path/?dir/*.php", "/path")
|
||||
hasDir(t, "/path/{dir1,dir2}/**/*.php", "/path")
|
||||
hasDir(t, ".", relativeDir(t, ""))
|
||||
hasDir(t, "./", relativeDir(t, ""))
|
||||
hasDir(t, "./**", relativeDir(t, ""))
|
||||
hasDir(t, "..", relativeDir(t, "/.."))
|
||||
}
|
||||
|
||||
func TestValidRecursiveDirectories(t *testing.T) {
|
||||
shouldMatch(t, "/path", "/path/file.php")
|
||||
shouldMatch(t, "/path", "/path/subpath/file.php")
|
||||
shouldMatch(t, "/path/", "/path/subpath/file.php")
|
||||
shouldMatch(t, "/path**", "/path/subpath/file.php")
|
||||
shouldMatch(t, "/path/**", "/path/subpath/file.php")
|
||||
shouldMatch(t, "/path/**/", "/path/subpath/file.php")
|
||||
shouldMatch(t, ".", relativeDir(t, "file.php"))
|
||||
shouldMatch(t, ".", relativeDir(t, "subpath/file.php"))
|
||||
shouldMatch(t, "./**", relativeDir(t, "subpath/file.php"))
|
||||
shouldMatch(t, "..", relativeDir(t, "subpath/file.php"))
|
||||
}
|
||||
|
||||
func TestInvalidRecursiveDirectories(t *testing.T) {
|
||||
shouldNotMatch(t, "/path", "/other/file.php")
|
||||
shouldNotMatch(t, "/path/**", "/other/file.php")
|
||||
shouldNotMatch(t, ".", "/other/file.php")
|
||||
}
|
||||
|
||||
func TestValidNonRecursiveFilePatterns(t *testing.T) {
|
||||
shouldMatch(t, "/*.php", "/file.php")
|
||||
shouldMatch(t, "/path/*.php", "/path/file.php")
|
||||
shouldMatch(t, "/path/?ile.php", "/path/file.php")
|
||||
shouldMatch(t, "/path/file.php", "/path/file.php")
|
||||
shouldMatch(t, "*.php", relativeDir(t, "file.php"))
|
||||
shouldMatch(t, "./*.php", relativeDir(t, "file.php"))
|
||||
}
|
||||
|
||||
func TestInValidNonRecursiveFilePatterns(t *testing.T) {
|
||||
shouldNotMatch(t, "/path/*.txt", "/path/file.php")
|
||||
shouldNotMatch(t, "/path/*.php", "/path/subpath/file.php")
|
||||
shouldNotMatch(t, "/*.php", "/path/file.php")
|
||||
shouldNotMatch(t, "*.txt", relativeDir(t, "file.php"))
|
||||
shouldNotMatch(t, "*.php", relativeDir(t, "subpath/file.php"))
|
||||
}
|
||||
|
||||
func TestValidRecursiveFilePatterns(t *testing.T) {
|
||||
shouldMatch(t, "/path/**/*.php", "/path/file.php")
|
||||
shouldMatch(t, "/path/**/*.php", "/path/subpath/file.php")
|
||||
shouldMatch(t, "/path/**/?ile.php", "/path/subpath/file.php")
|
||||
shouldMatch(t, "/path/**/file.php", "/path/subpath/file.php")
|
||||
shouldMatch(t, "**/*.php", relativeDir(t, "file.php"))
|
||||
shouldMatch(t, "**/*.php", relativeDir(t, "subpath/file.php"))
|
||||
shouldMatch(t, "./**/*.php", relativeDir(t, "subpath/file.php"))
|
||||
}
|
||||
|
||||
func TestInvalidRecursiveFilePatterns(t *testing.T) {
|
||||
shouldNotMatch(t, "/path/**/*.txt", "/path/file.php")
|
||||
shouldNotMatch(t, "/path/**/*.txt", "/other/file.php")
|
||||
shouldNotMatch(t, "/path/**/*.txt", "/path/subpath/file.php")
|
||||
shouldNotMatch(t, "/path/**/?ilm.php", "/path/subpath/file.php")
|
||||
shouldNotMatch(t, "**/*.php", "/other/file.php")
|
||||
shouldNotMatch(t, ".**/*.php", "/other/file.php")
|
||||
shouldNotMatch(t, "./**/*.php", "/other/file.php")
|
||||
}
|
||||
|
||||
func TestValidDirectoryPatterns(t *testing.T) {
|
||||
shouldMatch(t, "/path/*/*.php", "/path/subpath/file.php")
|
||||
shouldMatch(t, "/path/*/*/*.php", "/path/subpath/subpath/file.php")
|
||||
shouldMatch(t, "/path/?/*.php", "/path/1/file.php")
|
||||
shouldMatch(t, "/path/**/vendor/*.php", "/path/vendor/file.php")
|
||||
shouldMatch(t, "/path/**/vendor/*.php", "/path/subpath/vendor/file.php")
|
||||
shouldMatch(t, "/path/**/vendor/**/*.php", "/path/vendor/file.php")
|
||||
shouldMatch(t, "/path/**/vendor/**/*.php", "/path/subpath/subpath/vendor/subpath/subpath/file.php")
|
||||
shouldMatch(t, "/path/**/vendor/*/*.php", "/path/subpath/subpath/vendor/subpath/file.php")
|
||||
shouldMatch(t, "/path*/path*/*", "/path1/path2/file.php")
|
||||
}
|
||||
|
||||
func TestInvalidDirectoryPatterns(t *testing.T) {
|
||||
shouldNotMatch(t, "/path/subpath/*.php", "/path/other/file.php")
|
||||
shouldNotMatch(t, "/path/*/*.php", "/path/subpath/subpath/file.php")
|
||||
shouldNotMatch(t, "/path/?/*.php", "/path/subpath/file.php")
|
||||
shouldNotMatch(t, "/path/*/*/*.php", "/path/subpath/file.php")
|
||||
shouldNotMatch(t, "/path/*/*/*.php", "/path/subpath/subpath/subpath/file.php")
|
||||
shouldNotMatch(t, "/path/**/vendor/*.php", "/path/subpath/vendor/subpath/file.php")
|
||||
shouldNotMatch(t, "/path/**/vendor/*.php", "/path/subpath/file.php")
|
||||
shouldNotMatch(t, "/path/**/vendor/**/*.php", "/path/subpath/file.php")
|
||||
shouldNotMatch(t, "/path/**/vendor/**/*.txt", "/path/subpath/vendor/subpath/file.php")
|
||||
shouldNotMatch(t, "/path/**/vendor/**/*.php", "/path/subpath/subpath/subpath/file.php")
|
||||
shouldNotMatch(t, "/path/**/vendor/*/*.php", "/path/subpath/vendor/subpath/subpath/file.php")
|
||||
shouldNotMatch(t, "/path*/path*", "/path1/path1/file.php")
|
||||
}
|
||||
|
||||
func TestValidExtendedPatterns(t *testing.T) {
|
||||
shouldMatch(t, "/path/*.{php}", "/path/file.php")
|
||||
shouldMatch(t, "/path/*.{php,twig}", "/path/file.php")
|
||||
shouldMatch(t, "/path/*.{php,twig}", "/path/file.twig")
|
||||
shouldMatch(t, "/path/**/{file.php,file.twig}", "/path/subpath/file.twig")
|
||||
shouldMatch(t, "/path/{dir1,dir2}/file.php", "/path/dir1/file.php")
|
||||
shouldMatch(t, "/path/{dir1,dir2}/file.php", "/path/dir2/file.php")
|
||||
shouldMatch(t, "/app/{app,config,resources}/**/*.php", "/app/app/subpath/file.php")
|
||||
shouldMatch(t, "/app/{app,config,resources}/**/*.php", "/app/config/subpath/file.php")
|
||||
}
|
||||
|
||||
func TestInValidExtendedPatterns(t *testing.T) {
|
||||
shouldNotMatch(t, "/path/*.{php}", "/path/file.txt")
|
||||
shouldNotMatch(t, "/path/*.{php,twig}", "/path/file.txt")
|
||||
shouldNotMatch(t, "/path/{file.php,file.twig}", "/path/file.txt")
|
||||
shouldNotMatch(t, "/path/{dir1,dir2}/file.php", "/path/dir3/file.php")
|
||||
shouldNotMatch(t, "/path/{dir1,dir2}/**/*.php", "/path/dir1/subpath/file.txt")
|
||||
}
|
||||
|
||||
func TestAnAssociatedEventTriggersTheWatcher(t *testing.T) {
|
||||
watchPattern, err := parseFilePattern("/**/*.php")
|
||||
assert.NoError(t, err)
|
||||
watchPattern.trigger = make(chan string)
|
||||
|
||||
go handleWatcherEvent(watchPattern, "/path/temorary_file", "/path/file.php", 0, 0)
|
||||
|
||||
var path string
|
||||
select {
|
||||
case path = <-watchPattern.trigger:
|
||||
assert.Equal(t, "/path/file.php", path, "should be associated file path")
|
||||
case <-time.After(2 * time.Second):
|
||||
assert.Fail(t, "associated watchPattern did not trigger after 2s")
|
||||
}
|
||||
}
|
||||
|
||||
func relativeDir(t *testing.T, relativePath string) string {
|
||||
dir, err := filepath.Abs("./" + relativePath)
|
||||
assert.NoError(t, err)
|
||||
return dir
|
||||
}
|
||||
|
||||
func hasDir(t *testing.T, pattern string, dir string) {
|
||||
watchPattern, err := parseFilePattern(pattern)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, dir, watchPattern.dir)
|
||||
}
|
||||
|
||||
func shouldMatch(t *testing.T, pattern string, fileName string) {
|
||||
watchPattern, err := parseFilePattern(pattern)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, watchPattern.allowReload(fileName, 0, 0))
|
||||
}
|
||||
|
||||
func shouldNotMatch(t *testing.T, pattern string, fileName string) {
|
||||
watchPattern, err := parseFilePattern(pattern)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, watchPattern.allowReload(fileName, 0, 0))
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
//go:build nowatcher
|
||||
|
||||
package watcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
func InitWatcher(ct context.Context, filePatterns []string, callback func(), logger *slog.Logger) error {
|
||||
logger.Error("watcher support is not enabled")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DrainWatcher() {
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// clang-format off
|
||||
//go:build !nowatcher
|
||||
// clang-format on
|
||||
#include "_cgo_export.h"
|
||||
#include "wtr/watcher-c.h"
|
||||
|
||||
void handle_event(struct wtr_watcher_event event, void *data) {
|
||||
go_handle_file_watcher_event(
|
||||
(char *)event.path_name, (char *)event.associated_path_name,
|
||||
event.effect_type, event.path_type, (uintptr_t)data);
|
||||
}
|
||||
|
||||
uintptr_t start_new_watcher(char const *const path, uintptr_t data) {
|
||||
void *watcher = wtr_watcher_open(path, handle_event, (void *)data);
|
||||
if (watcher == NULL) {
|
||||
return 0;
|
||||
}
|
||||
return (uintptr_t)watcher;
|
||||
}
|
||||
|
||||
int stop_watcher(uintptr_t watcher) {
|
||||
if (!wtr_watcher_close((void *)watcher)) {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
@@ -2,55 +2,60 @@
|
||||
|
||||
package watcher
|
||||
|
||||
// #include <stdint.h>
|
||||
// #include <stdlib.h>
|
||||
// #include "watcher.h"
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"runtime/cgo"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/e-dant/watcher/watcher-go"
|
||||
)
|
||||
|
||||
type watcher struct {
|
||||
sessions []C.uintptr_t
|
||||
callback func()
|
||||
trigger chan string
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
// duration to wait before triggering a reload after a file change
|
||||
const debounceDuration = 150 * time.Millisecond
|
||||
|
||||
// times to retry watching if the watcher was closed prematurely
|
||||
const maxFailureCount = 5
|
||||
const failureResetDuration = 5 * time.Second
|
||||
|
||||
var failureMu = sync.Mutex{}
|
||||
var watcherIsActive = atomic.Bool{}
|
||||
const (
|
||||
// duration to wait before triggering a reload after a file change
|
||||
debounceDuration = 150 * time.Millisecond
|
||||
// times to retry watching if the watcher was closed prematurely
|
||||
maxFailureCount = 5
|
||||
failureResetDuration = 5 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
ErrAlreadyStarted = errors.New("the watcher is already running")
|
||||
ErrUnableToStartWatching = errors.New("unable to start the watcher")
|
||||
ErrAlreadyStarted = errors.New("watcher is already running")
|
||||
|
||||
failureMu sync.Mutex
|
||||
watcherIsActive atomic.Bool
|
||||
|
||||
// the currently active file watcher
|
||||
activeWatcher *watcher
|
||||
activeWatcher *globalWatcher
|
||||
// after stopping the watcher we will wait for eventual reloads to finish
|
||||
reloadWaitGroup sync.WaitGroup
|
||||
// we are passing the context from the main package to the watcher
|
||||
ctx context.Context
|
||||
// we are passing the logger from the main package to the watcher
|
||||
logger *slog.Logger
|
||||
globalCtx context.Context
|
||||
// we are passing the globalLogger from the main package to the watcher
|
||||
globalLogger *slog.Logger
|
||||
)
|
||||
|
||||
func InitWatcher(ct context.Context, filePatterns []string, callback func(), slogger *slog.Logger) error {
|
||||
if len(filePatterns) == 0 {
|
||||
type PatternGroup struct {
|
||||
Patterns []string
|
||||
Callback func([]*watcher.Event)
|
||||
}
|
||||
|
||||
type eventHolder struct {
|
||||
patternGroup *PatternGroup
|
||||
event *watcher.Event
|
||||
}
|
||||
|
||||
type globalWatcher struct {
|
||||
groups []*PatternGroup
|
||||
watchers []*pattern
|
||||
events chan eventHolder
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
func InitWatcher(ct context.Context, slogger *slog.Logger, groups []*PatternGroup) error {
|
||||
if len(groups) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -59,11 +64,22 @@ func InitWatcher(ct context.Context, filePatterns []string, callback func(), slo
|
||||
}
|
||||
|
||||
watcherIsActive.Store(true)
|
||||
ctx = ct
|
||||
logger = slogger
|
||||
activeWatcher = &watcher{callback: callback}
|
||||
globalCtx = ct
|
||||
globalLogger = slogger
|
||||
|
||||
if err := activeWatcher.startWatching(ctx, filePatterns); err != nil {
|
||||
activeWatcher = &globalWatcher{groups: groups}
|
||||
|
||||
for _, g := range groups {
|
||||
if len(g.Patterns) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, p := range g.Patterns {
|
||||
activeWatcher.watchers = append(activeWatcher.watchers, &pattern{patternGroup: g, value: p})
|
||||
}
|
||||
}
|
||||
|
||||
if err := activeWatcher.startWatching(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -77,8 +93,8 @@ func DrainWatcher() {
|
||||
|
||||
watcherIsActive.Store(false)
|
||||
|
||||
if logger.Enabled(ctx, slog.LevelDebug) {
|
||||
logger.LogAttrs(ctx, slog.LevelDebug, "stopping watcher")
|
||||
if globalLogger.Enabled(globalCtx, slog.LevelDebug) {
|
||||
globalLogger.LogAttrs(globalCtx, slog.LevelDebug, "stopping watcher")
|
||||
}
|
||||
|
||||
activeWatcher.stopWatching()
|
||||
@@ -87,142 +103,119 @@ func DrainWatcher() {
|
||||
}
|
||||
|
||||
// TODO: how to test this?
|
||||
func retryWatching(watchPattern *watchPattern) {
|
||||
func (p *pattern) retryWatching() {
|
||||
failureMu.Lock()
|
||||
defer failureMu.Unlock()
|
||||
if watchPattern.failureCount >= maxFailureCount {
|
||||
if logger.Enabled(ctx, slog.LevelWarn) {
|
||||
logger.LogAttrs(ctx, slog.LevelWarn, "giving up watching", slog.String("dir", watchPattern.dir))
|
||||
|
||||
if p.failureCount >= maxFailureCount {
|
||||
if globalLogger.Enabled(globalCtx, slog.LevelWarn) {
|
||||
globalLogger.LogAttrs(globalCtx, slog.LevelWarn, "giving up watching", slog.String("pattern", p.value))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if logger.Enabled(ctx, slog.LevelInfo) {
|
||||
logger.LogAttrs(ctx, slog.LevelInfo, "watcher was closed prematurely, retrying...", slog.String("dir", watchPattern.dir))
|
||||
if globalLogger.Enabled(globalCtx, slog.LevelInfo) {
|
||||
globalLogger.LogAttrs(globalCtx, slog.LevelInfo, "watcher was closed prematurely, retrying...", slog.String("pattern", p.value))
|
||||
}
|
||||
|
||||
watchPattern.failureCount++
|
||||
session, err := startSession(watchPattern)
|
||||
if err != nil {
|
||||
activeWatcher.sessions = append(activeWatcher.sessions, session)
|
||||
}
|
||||
p.failureCount++
|
||||
|
||||
p.startSession()
|
||||
|
||||
// reset the failure-count if the watcher hasn't reached max failures after 5 seconds
|
||||
go func() {
|
||||
time.Sleep(failureResetDuration * time.Second)
|
||||
time.Sleep(failureResetDuration)
|
||||
|
||||
failureMu.Lock()
|
||||
if watchPattern.failureCount < maxFailureCount {
|
||||
watchPattern.failureCount = 0
|
||||
if p.failureCount < maxFailureCount {
|
||||
p.failureCount = 0
|
||||
}
|
||||
failureMu.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
func (w *watcher) startWatching(ctx context.Context, filePatterns []string) error {
|
||||
w.trigger = make(chan string)
|
||||
w.stop = make(chan struct{})
|
||||
w.sessions = make([]C.uintptr_t, len(filePatterns))
|
||||
watchPatterns, err := parseFilePatterns(filePatterns)
|
||||
if err != nil {
|
||||
func (g *globalWatcher) startWatching() error {
|
||||
g.events = make(chan eventHolder)
|
||||
g.stop = make(chan struct{})
|
||||
|
||||
if err := g.parseFilePatterns(); err != nil {
|
||||
return err
|
||||
}
|
||||
for i, watchPattern := range watchPatterns {
|
||||
watchPattern.trigger = w.trigger
|
||||
session, err := startSession(watchPattern)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.sessions[i] = session
|
||||
|
||||
for _, w := range g.watchers {
|
||||
w.events = g.events
|
||||
w.startSession()
|
||||
}
|
||||
go listenForFileEvents(w.trigger, w.stop)
|
||||
|
||||
go g.listenForFileEvents()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *watcher) stopWatching() {
|
||||
close(w.stop)
|
||||
for _, session := range w.sessions {
|
||||
stopSession(session)
|
||||
}
|
||||
}
|
||||
|
||||
func startSession(w *watchPattern) (C.uintptr_t, error) {
|
||||
handle := cgo.NewHandle(w)
|
||||
cDir := C.CString(w.dir)
|
||||
defer C.free(unsafe.Pointer(cDir))
|
||||
watchSession := C.start_new_watcher(cDir, C.uintptr_t(handle))
|
||||
if watchSession != 0 {
|
||||
if logger.Enabled(ctx, slog.LevelDebug) {
|
||||
logger.LogAttrs(ctx, slog.LevelDebug, "watching", slog.String("dir", w.dir), slog.Any("patterns", w.patterns))
|
||||
func (g *globalWatcher) parseFilePatterns() error {
|
||||
for _, w := range g.watchers {
|
||||
if err := w.parse(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return watchSession, nil
|
||||
}
|
||||
|
||||
if logger.Enabled(ctx, slog.LevelError) {
|
||||
logger.LogAttrs(ctx, slog.LevelError, "couldn't start watching", slog.String("dir", w.dir))
|
||||
}
|
||||
|
||||
return watchSession, ErrUnableToStartWatching
|
||||
return nil
|
||||
}
|
||||
|
||||
func stopSession(session C.uintptr_t) {
|
||||
success := C.stop_watcher(session)
|
||||
if success == 0 && logger.Enabled(ctx, slog.LevelWarn) {
|
||||
logger.LogAttrs(ctx, slog.LevelWarn, "couldn't close the watcher")
|
||||
func (g *globalWatcher) stopWatching() {
|
||||
close(g.stop)
|
||||
for _, w := range g.watchers {
|
||||
w.stop()
|
||||
}
|
||||
}
|
||||
|
||||
//export go_handle_file_watcher_event
|
||||
func go_handle_file_watcher_event(path *C.char, associatedPath *C.char, eventType C.int, pathType C.int, handle C.uintptr_t) {
|
||||
watchPattern := cgo.Handle(handle).Value().(*watchPattern)
|
||||
handleWatcherEvent(watchPattern, C.GoString(path), C.GoString(associatedPath), int(eventType), int(pathType))
|
||||
}
|
||||
|
||||
func handleWatcherEvent(watchPattern *watchPattern, path string, associatedPath string, eventType int, pathType int) {
|
||||
// If the watcher prematurely sends the die@ event, retry watching
|
||||
if pathType == 4 && strings.HasPrefix(path, "e/self/die@") && watcherIsActive.Load() {
|
||||
retryWatching(watchPattern)
|
||||
return
|
||||
}
|
||||
|
||||
if watchPattern.allowReload(path, eventType, pathType) {
|
||||
watchPattern.trigger <- path
|
||||
return
|
||||
}
|
||||
|
||||
// some editors create temporary files and never actually modify the original file
|
||||
// so we need to also check the associated path of an event
|
||||
// see https://github.com/php/frankenphp/issues/1375
|
||||
if associatedPath != "" && watchPattern.allowReload(associatedPath, eventType, pathType) {
|
||||
watchPattern.trigger <- associatedPath
|
||||
}
|
||||
}
|
||||
|
||||
func listenForFileEvents(triggerWatcher chan string, stopWatcher chan struct{}) {
|
||||
func (g *globalWatcher) listenForFileEvents() {
|
||||
timer := time.NewTimer(debounceDuration)
|
||||
timer.Stop()
|
||||
lastChangedFile := ""
|
||||
|
||||
eventsPerGroup := make(map[*PatternGroup][]*watcher.Event, len(g.groups))
|
||||
|
||||
defer timer.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-stopWatcher:
|
||||
case lastChangedFile = <-triggerWatcher:
|
||||
case <-g.stop:
|
||||
return
|
||||
case eh := <-g.events:
|
||||
timer.Reset(debounceDuration)
|
||||
|
||||
eventsPerGroup[eh.patternGroup] = append(eventsPerGroup[eh.patternGroup], eh.event)
|
||||
case <-timer.C:
|
||||
timer.Stop()
|
||||
|
||||
if logger.Enabled(ctx, slog.LevelInfo) {
|
||||
logger.LogAttrs(ctx, slog.LevelInfo, "filesystem change detected", slog.String("file", lastChangedFile))
|
||||
if globalLogger.Enabled(globalCtx, slog.LevelInfo) {
|
||||
var events []*watcher.Event
|
||||
for _, eventList := range eventsPerGroup {
|
||||
events = append(events, eventList...)
|
||||
}
|
||||
|
||||
globalLogger.LogAttrs(globalCtx, slog.LevelInfo, "filesystem changes detected", slog.Any("events", events))
|
||||
}
|
||||
|
||||
scheduleReload()
|
||||
g.scheduleReload(eventsPerGroup)
|
||||
eventsPerGroup = make(map[*PatternGroup][]*watcher.Event, len(g.groups))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func scheduleReload() {
|
||||
func (g *globalWatcher) scheduleReload(eventsPerGroup map[*PatternGroup][]*watcher.Event) {
|
||||
reloadWaitGroup.Add(1)
|
||||
activeWatcher.callback()
|
||||
|
||||
// Call callbacks in order
|
||||
for _, g := range g.groups {
|
||||
if len(g.Patterns) == 0 {
|
||||
g.Callback(nil)
|
||||
}
|
||||
|
||||
if e, ok := eventsPerGroup[g]; ok {
|
||||
g.Callback(e)
|
||||
}
|
||||
}
|
||||
|
||||
reloadWaitGroup.Done()
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
uintptr_t start_new_watcher(char const *const path, uintptr_t data);
|
||||
|
||||
int stop_watcher(uintptr_t watcher);
|
||||
37
log_test.go
Normal file
37
log_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package frankenphp_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func newTestLogger(t *testing.T) (*slog.Logger, fmt.Stringer) {
|
||||
t.Helper()
|
||||
|
||||
var buf syncBuffer
|
||||
|
||||
return slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})), &buf
|
||||
}
|
||||
|
||||
// SyncBuffer is a thread-safe buffer for capturing logs in tests.
|
||||
type syncBuffer struct {
|
||||
b bytes.Buffer
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (s *syncBuffer) Write(p []byte) (n int, err error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
return s.b.Write(p)
|
||||
}
|
||||
|
||||
func (s *syncBuffer) String() string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
return s.b.String()
|
||||
}
|
||||
@@ -5,14 +5,14 @@ package frankenphp
|
||||
// #include <stdint.h>
|
||||
// #include <php.h>
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type mercureContext struct {
|
||||
}
|
||||
|
||||
//export go_mercure_publish
|
||||
func go_mercure_publish(_ C.uintptr_t, _ *C.struct__zval_struct, _ unsafe.Pointer, _ bool, _, _ unsafe.Pointer, _ uint64) (*C.zend_string, C.short) {
|
||||
func go_mercure_publish(threadIndex C.uintptr_t, topics *C.struct__zval_struct, data *C.zend_string, private bool, id, typ *C.zend_string, retry uint64) (generatedID *C.zend_string, error C.short) {
|
||||
return nil, 3
|
||||
}
|
||||
|
||||
func (w *worker) configureMercure(_ *workerOpt) {
|
||||
}
|
||||
|
||||
22
mercure.go
22
mercure.go
@@ -38,6 +38,7 @@ func go_mercure_publish(threadIndex C.uintptr_t, topics *C.struct__zval_struct,
|
||||
Type: GoString(unsafe.Pointer(typ)),
|
||||
},
|
||||
Private: private,
|
||||
Debug: fc.logger.Enabled(ctx, slog.LevelDebug),
|
||||
}
|
||||
|
||||
zvalType := C.zval_get_type(topics)
|
||||
@@ -45,7 +46,7 @@ func go_mercure_publish(threadIndex C.uintptr_t, topics *C.struct__zval_struct,
|
||||
case C.IS_STRING:
|
||||
u.Topics = []string{GoString(unsafe.Pointer(*(**C.zend_string)(unsafe.Pointer(&topics.value[0]))))}
|
||||
case C.IS_ARRAY:
|
||||
ts, err := GoPackedArray[string](unsafe.Pointer(topics))
|
||||
ts, err := GoPackedArray[string](unsafe.Pointer(*(**C.zend_array)(unsafe.Pointer(&topics.value[0]))))
|
||||
if err != nil {
|
||||
if fc.logger.Enabled(ctx, slog.LevelError) {
|
||||
fc.logger.LogAttrs(ctx, slog.LevelError, "invalid topics type", slog.Any("error", err))
|
||||
@@ -71,6 +72,14 @@ func go_mercure_publish(threadIndex C.uintptr_t, topics *C.struct__zval_struct,
|
||||
return (*C.zend_string)(PHPString(u.ID, false)), 0
|
||||
}
|
||||
|
||||
func (w *worker) configureMercure(o *workerOpt) {
|
||||
if o.mercureHub == nil {
|
||||
return
|
||||
}
|
||||
|
||||
w.mercureHub = o.mercureHub
|
||||
}
|
||||
|
||||
// WithMercureHub sets the mercure.Hub to use to publish updates
|
||||
func WithMercureHub(hub *mercure.Hub) RequestOption {
|
||||
return func(o *frankenPHPContext) error {
|
||||
@@ -79,3 +88,14 @@ func WithMercureHub(hub *mercure.Hub) RequestOption {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithWorkerMercureHub sets the mercure.Hub in the worker script and used to dispatch hot reloading-related mercure.Update.
|
||||
func WithWorkerMercureHub(hub *mercure.Hub) WorkerOption {
|
||||
return func(w *workerOpt) error {
|
||||
w.mercureHub = hub
|
||||
|
||||
w.requestOptions = append(w.requestOptions, WithMercureHub(hub))
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ type WorkerOption func(*workerOpt) error
|
||||
//
|
||||
// If you change this, also update the Caddy module and the documentation.
|
||||
type opt struct {
|
||||
hotReloadOpt
|
||||
|
||||
ctx context.Context
|
||||
numThreads int
|
||||
maxThreads int
|
||||
@@ -31,6 +33,8 @@ type opt struct {
|
||||
}
|
||||
|
||||
type workerOpt struct {
|
||||
mercureContext
|
||||
|
||||
name string
|
||||
fileName string
|
||||
num int
|
||||
|
||||
@@ -2,7 +2,6 @@ package frankenphp
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"math/rand/v2"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
@@ -93,10 +92,15 @@ func TestTransitionAThreadBetween2DifferentWorkers(t *testing.T) {
|
||||
// try all possible handler transitions
|
||||
// takes around 200ms and is supposed to force race conditions
|
||||
func TestTransitionThreadsWhileDoingRequests(t *testing.T) {
|
||||
t.Cleanup(Shutdown)
|
||||
|
||||
var (
|
||||
isDone atomic.Bool
|
||||
wg sync.WaitGroup
|
||||
)
|
||||
|
||||
numThreads := 10
|
||||
numRequestsPerThread := 100
|
||||
isDone := atomic.Bool{}
|
||||
wg := sync.WaitGroup{}
|
||||
worker1Path := testDataPath + "/transition-worker-1.php"
|
||||
worker1Name := "worker-1"
|
||||
worker2Path := testDataPath + "/transition-worker-2.php"
|
||||
@@ -114,7 +118,6 @@ func TestTransitionThreadsWhileDoingRequests(t *testing.T) {
|
||||
WithWorkerWatchMode([]string{}),
|
||||
WithWorkerMaxFailures(0),
|
||||
),
|
||||
WithLogger(slog.New(slog.NewTextHandler(io.Discard, nil))),
|
||||
))
|
||||
|
||||
// try all possible permutations of transition, transition every ms
|
||||
@@ -155,7 +158,6 @@ func TestTransitionThreadsWhileDoingRequests(t *testing.T) {
|
||||
// we are finished as soon as all 1000 requests are done
|
||||
wg.Wait()
|
||||
isDone.Store(true)
|
||||
Shutdown()
|
||||
}
|
||||
|
||||
func TestFinishBootingAWorkerScript(t *testing.T) {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package frankenphp
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -11,10 +9,11 @@ import (
|
||||
)
|
||||
|
||||
func TestScaleARegularThreadUpAndDown(t *testing.T) {
|
||||
t.Cleanup(Shutdown)
|
||||
|
||||
assert.NoError(t, Init(
|
||||
WithNumThreads(1),
|
||||
WithMaxThreads(2),
|
||||
WithLogger(slog.New(slog.NewTextHandler(io.Discard, nil))),
|
||||
))
|
||||
|
||||
autoScaledThread := phpThreads[1]
|
||||
@@ -25,14 +24,14 @@ func TestScaleARegularThreadUpAndDown(t *testing.T) {
|
||||
assert.IsType(t, ®ularThread{}, autoScaledThread.handler)
|
||||
|
||||
// on down-scale, the thread will be marked as inactive
|
||||
setLongWaitTime(autoScaledThread)
|
||||
setLongWaitTime(t, autoScaledThread)
|
||||
deactivateThreads()
|
||||
assert.IsType(t, &inactiveThread{}, autoScaledThread.handler)
|
||||
|
||||
Shutdown()
|
||||
}
|
||||
|
||||
func TestScaleAWorkerThreadUpAndDown(t *testing.T) {
|
||||
t.Cleanup(Shutdown)
|
||||
|
||||
workerName := "worker1"
|
||||
workerPath := testDataPath + "/transition-worker-1.php"
|
||||
assert.NoError(t, Init(
|
||||
@@ -43,7 +42,6 @@ func TestScaleAWorkerThreadUpAndDown(t *testing.T) {
|
||||
WithWorkerWatchMode([]string{}),
|
||||
WithWorkerMaxFailures(0),
|
||||
),
|
||||
WithLogger(slog.New(slog.NewTextHandler(io.Discard, nil))),
|
||||
))
|
||||
|
||||
autoScaledThread := phpThreads[2]
|
||||
@@ -53,13 +51,13 @@ func TestScaleAWorkerThreadUpAndDown(t *testing.T) {
|
||||
assert.Equal(t, state.Ready, autoScaledThread.state.Get())
|
||||
|
||||
// on down-scale, the thread will be marked as inactive
|
||||
setLongWaitTime(autoScaledThread)
|
||||
setLongWaitTime(t, autoScaledThread)
|
||||
deactivateThreads()
|
||||
assert.IsType(t, &inactiveThread{}, autoScaledThread.handler)
|
||||
|
||||
Shutdown()
|
||||
}
|
||||
|
||||
func setLongWaitTime(thread *phpThread) {
|
||||
func setLongWaitTime(t *testing.T, thread *phpThread) {
|
||||
t.Helper()
|
||||
|
||||
thread.state.SetWaitTime(time.Now().Add(-time.Hour))
|
||||
}
|
||||
|
||||
@@ -12,9 +12,11 @@ ENV PHP_VERSION=${PHP_VERSION}
|
||||
# args passed to static-php-cli
|
||||
ARG PHP_EXTENSIONS=''
|
||||
ARG PHP_EXTENSION_LIBS=''
|
||||
ARG SPC_OPT_BUILD_ARGS
|
||||
|
||||
# args passed to xcaddy
|
||||
ARG XCADDY_ARGS=''
|
||||
ARG XCADDY_ARGS='--with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy'
|
||||
ENV SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES="${XCADDY_ARGS}"
|
||||
ARG CLEAN=''
|
||||
ARG EMBED=''
|
||||
ARG DEBUG_SYMBOLS=''
|
||||
@@ -121,7 +123,8 @@ ENV SPC_DEFAULT_C_FLAGS='-fPIE -fPIC -O3'
|
||||
ENV SPC_LIBC='glibc'
|
||||
ENV SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM='-Wl,-O3 -pie'
|
||||
ENV SPC_CMD_VAR_PHP_MAKE_EXTRA_LIBS='-ldl -lpthread -lm -lresolv -lutil -lrt'
|
||||
ENV SPC_OPT_BUILD_ARGS='--with-config-file-path=/etc/frankenphp --with-config-file-scan-dir=/etc/frankenphp/php.d'
|
||||
# Keep default config paths and append any externally provided SPC_OPT_BUILD_ARGS (e.g., from CI)
|
||||
ENV SPC_OPT_BUILD_ARGS="--with-config-file-path=/etc/frankenphp --with-config-file-scan-dir=/etc/frankenphp/php.d ${SPC_OPT_BUILD_ARGS}"
|
||||
ENV SPC_REL_TYPE='binary'
|
||||
ENV EXTENSION_DIR='/usr/lib/frankenphp/modules'
|
||||
|
||||
|
||||
@@ -12,9 +12,14 @@ ENV FRANKENPHP_VERSION=${FRANKENPHP_VERSION}
|
||||
ARG PHP_VERSION=''
|
||||
ENV PHP_VERSION=${PHP_VERSION}
|
||||
|
||||
# args passed to static-php-cli
|
||||
ARG PHP_EXTENSIONS=''
|
||||
ARG PHP_EXTENSION_LIBS=''
|
||||
ARG XCADDY_ARGS=''
|
||||
ARG SPC_OPT_BUILD_ARGS
|
||||
|
||||
# args passed to xcaddy
|
||||
ARG XCADDY_ARGS='--with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy'
|
||||
ENV SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES="${XCADDY_ARGS}"
|
||||
ARG CLEAN=''
|
||||
ARG EMBED=''
|
||||
ARG DEBUG_SYMBOLS=''
|
||||
@@ -99,7 +104,8 @@ COPY --link . ./
|
||||
ENV SPC_DEFAULT_C_FLAGS='-fPIE -fPIC -O3'
|
||||
ENV SPC_LIBC='musl'
|
||||
ENV SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM='-Wl,-O3 -pie'
|
||||
ENV SPC_OPT_BUILD_ARGS='--with-config-file-path=/etc/frankenphp --with-config-file-scan-dir=/etc/frankenphp/php.d'
|
||||
# Keep default config paths and append any externally provided SPC_OPT_BUILD_ARGS (e.g., from CI)
|
||||
ENV SPC_OPT_BUILD_ARGS="--with-config-file-path=/etc/frankenphp --with-config-file-scan-dir=/etc/frankenphp/php.d ${SPC_OPT_BUILD_ARGS}"
|
||||
ENV SPC_REL_TYPE='binary'
|
||||
ENV EXTENSION_DIR='/usr/lib/frankenphp/modules'
|
||||
|
||||
|
||||
32
testdata/integration/basic_function.go
vendored
Normal file
32
testdata/integration/basic_function.go
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
package testintegration
|
||||
|
||||
// #include <Zend/zend_types.h>
|
||||
import "C"
|
||||
import (
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
// export_php:function test_uppercase(string $str): string
|
||||
func test_uppercase(s *C.zend_string) unsafe.Pointer {
|
||||
str := frankenphp.GoString(unsafe.Pointer(s))
|
||||
upper := strings.ToUpper(str)
|
||||
return frankenphp.PHPString(upper, false)
|
||||
}
|
||||
|
||||
// export_php:function test_add_numbers(int $a, int $b): int
|
||||
func test_add_numbers(a int64, b int64) int64 {
|
||||
return a + b
|
||||
}
|
||||
|
||||
// export_php:function test_multiply(float $a, float $b): float
|
||||
func test_multiply(a float64, b float64) float64 {
|
||||
return a * b
|
||||
}
|
||||
|
||||
// export_php:function test_is_enabled(bool $flag): bool
|
||||
func test_is_enabled(flag bool) bool {
|
||||
return !flag
|
||||
}
|
||||
64
testdata/integration/callable.go
vendored
Normal file
64
testdata/integration/callable.go
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
package testintegration
|
||||
|
||||
// #include <Zend/zend_types.h>
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
// export_php:function my_array_map(array $data, callable $callback): array
|
||||
func my_array_map(arr *C.zend_array, callback *C.zval) unsafe.Pointer {
|
||||
goArray, err := frankenphp.GoPackedArray[any](unsafe.Pointer(arr))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]any, len(goArray))
|
||||
for i, item := range goArray {
|
||||
callResult := frankenphp.CallPHPCallable(unsafe.Pointer(callback), []any{item})
|
||||
result[i] = callResult
|
||||
}
|
||||
|
||||
return frankenphp.PHPPackedArray[any](result)
|
||||
}
|
||||
|
||||
// export_php:function my_filter(array $data, ?callable $callback): array
|
||||
func my_filter(arr *C.zend_array, callback *C.zval) unsafe.Pointer {
|
||||
goArray, err := frankenphp.GoPackedArray[any](unsafe.Pointer(arr))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if callback == nil {
|
||||
return unsafe.Pointer(arr)
|
||||
}
|
||||
|
||||
result := make([]any, 0)
|
||||
for _, item := range goArray {
|
||||
callResult := frankenphp.CallPHPCallable(unsafe.Pointer(callback), []any{item})
|
||||
if boolResult, ok := callResult.(bool); ok && boolResult {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
return frankenphp.PHPPackedArray[any](result)
|
||||
}
|
||||
|
||||
// export_php:class Processor
|
||||
type Processor struct{}
|
||||
|
||||
// export_php:method Processor::transform(string $input, callable $transformer): string
|
||||
func (p *Processor) Transform(input *C.zend_string, callback *C.zval) unsafe.Pointer {
|
||||
goInput := frankenphp.GoString(unsafe.Pointer(input))
|
||||
|
||||
callResult := frankenphp.CallPHPCallable(unsafe.Pointer(callback), []any{goInput})
|
||||
|
||||
resultStr, ok := callResult.(string)
|
||||
if !ok {
|
||||
return unsafe.Pointer(input)
|
||||
}
|
||||
|
||||
return frankenphp.PHPString(resultStr, false)
|
||||
}
|
||||
72
testdata/integration/class_methods.go
vendored
Normal file
72
testdata/integration/class_methods.go
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
package testintegration
|
||||
|
||||
// #include <Zend/zend_types.h>
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
// export_php:class Counter
|
||||
type CounterStruct struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
// export_php:method Counter::increment(): void
|
||||
func (c *CounterStruct) Increment() {
|
||||
c.Value++
|
||||
}
|
||||
|
||||
// export_php:method Counter::decrement(): void
|
||||
func (c *CounterStruct) Decrement() {
|
||||
c.Value--
|
||||
}
|
||||
|
||||
// export_php:method Counter::getValue(): int
|
||||
func (c *CounterStruct) GetValue() int64 {
|
||||
return int64(c.Value)
|
||||
}
|
||||
|
||||
// export_php:method Counter::setValue(int $value): void
|
||||
func (c *CounterStruct) SetValue(value int64) {
|
||||
c.Value = int(value)
|
||||
}
|
||||
|
||||
// export_php:method Counter::reset(): void
|
||||
func (c *CounterStruct) Reset() {
|
||||
c.Value = 0
|
||||
}
|
||||
|
||||
// export_php:method Counter::addValue(int $amount): int
|
||||
func (c *CounterStruct) AddValue(amount int64) int64 {
|
||||
c.Value += int(amount)
|
||||
return int64(c.Value)
|
||||
}
|
||||
|
||||
// export_php:method Counter::updateWithNullable(?int $newValue): void
|
||||
func (c *CounterStruct) UpdateWithNullable(newValue *int64) {
|
||||
if newValue != nil {
|
||||
c.Value = int(*newValue)
|
||||
}
|
||||
}
|
||||
|
||||
// export_php:class StringHolder
|
||||
type StringHolderStruct struct {
|
||||
Data string
|
||||
}
|
||||
|
||||
// export_php:method StringHolder::setData(string $data): void
|
||||
func (sh *StringHolderStruct) SetData(data *C.zend_string) {
|
||||
sh.Data = frankenphp.GoString(unsafe.Pointer(data))
|
||||
}
|
||||
|
||||
// export_php:method StringHolder::getData(): string
|
||||
func (sh *StringHolderStruct) GetData() unsafe.Pointer {
|
||||
return frankenphp.PHPString(sh.Data, false)
|
||||
}
|
||||
|
||||
// export_php:method StringHolder::getLength(): int
|
||||
func (sh *StringHolderStruct) GetLength() int64 {
|
||||
return int64(len(sh.Data))
|
||||
}
|
||||
70
testdata/integration/constants.go
vendored
Normal file
70
testdata/integration/constants.go
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
package testintegration
|
||||
|
||||
// #include <Zend/zend_types.h>
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
// export_php:const
|
||||
const TEST_MAX_RETRIES = 100
|
||||
|
||||
// export_php:const
|
||||
const TEST_API_VERSION = "2.0.0"
|
||||
|
||||
// export_php:const
|
||||
const TEST_ENABLED = true
|
||||
|
||||
// export_php:const
|
||||
const TEST_PI = 3.14159
|
||||
|
||||
// export_php:const
|
||||
const STATUS_PENDING = iota
|
||||
|
||||
// export_php:const
|
||||
const STATUS_PROCESSING = iota
|
||||
|
||||
// export_php:const
|
||||
const STATUS_COMPLETED = iota
|
||||
|
||||
// export_php:class Config
|
||||
type ConfigStruct struct {
|
||||
Mode int
|
||||
}
|
||||
|
||||
// export_php:classconst Config
|
||||
const MODE_DEBUG = 1
|
||||
|
||||
// export_php:classconst Config
|
||||
const MODE_PRODUCTION = 2
|
||||
|
||||
// export_php:classconst Config
|
||||
const DEFAULT_TIMEOUT = 30
|
||||
|
||||
// export_php:method Config::setMode(int $mode): void
|
||||
func (c *ConfigStruct) SetMode(mode int64) {
|
||||
c.Mode = int(mode)
|
||||
}
|
||||
|
||||
// export_php:method Config::getMode(): int
|
||||
func (c *ConfigStruct) GetMode() int64 {
|
||||
return int64(c.Mode)
|
||||
}
|
||||
|
||||
// export_php:function test_with_constants(int $status): string
|
||||
func test_with_constants(status int64) unsafe.Pointer {
|
||||
var result string
|
||||
switch status {
|
||||
case STATUS_PENDING:
|
||||
result = "pending"
|
||||
case STATUS_PROCESSING:
|
||||
result = "processing"
|
||||
case STATUS_COMPLETED:
|
||||
result = "completed"
|
||||
default:
|
||||
result = "unknown"
|
||||
}
|
||||
return frankenphp.PHPString(result, false)
|
||||
}
|
||||
9
testdata/integration/invalid_signature.go
vendored
Normal file
9
testdata/integration/invalid_signature.go
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
package testintegration
|
||||
|
||||
// #include <Zend/zend_types.h>
|
||||
import "C"
|
||||
|
||||
// export_php:function invalid_return_type(string $str): unsupported_type
|
||||
func invalid_return_type(s *C.zend_string) int {
|
||||
return 42
|
||||
}
|
||||
50
testdata/integration/namespace.go
vendored
Normal file
50
testdata/integration/namespace.go
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
package testintegration
|
||||
|
||||
// export_php:namespace TestIntegration\Extension
|
||||
|
||||
// #include <Zend/zend_types.h>
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
// export_php:const
|
||||
const NAMESPACE_VERSION = "1.0.0"
|
||||
|
||||
// export_php:function greet(string $name): string
|
||||
func greet(name *C.zend_string) unsafe.Pointer {
|
||||
str := frankenphp.GoString(unsafe.Pointer(name))
|
||||
result := "Hello, " + str + "!"
|
||||
return frankenphp.PHPString(result, false)
|
||||
}
|
||||
|
||||
// export_php:class Person
|
||||
type PersonStruct struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
// export_php:method Person::setName(string $name): void
|
||||
func (p *PersonStruct) SetName(name *C.zend_string) {
|
||||
p.Name = frankenphp.GoString(unsafe.Pointer(name))
|
||||
}
|
||||
|
||||
// export_php:method Person::getName(): string
|
||||
func (p *PersonStruct) GetName() unsafe.Pointer {
|
||||
return frankenphp.PHPString(p.Name, false)
|
||||
}
|
||||
|
||||
// export_php:method Person::setAge(int $age): void
|
||||
func (p *PersonStruct) SetAge(age int64) {
|
||||
p.Age = int(age)
|
||||
}
|
||||
|
||||
// export_php:method Person::getAge(): int
|
||||
func (p *PersonStruct) GetAge() int64 {
|
||||
return int64(p.Age)
|
||||
}
|
||||
|
||||
// export_php:classconst Person
|
||||
const DEFAULT_AGE = 18
|
||||
19
testdata/integration/type_mismatch.go
vendored
Normal file
19
testdata/integration/type_mismatch.go
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
package testintegration
|
||||
|
||||
// #include <Zend/zend_types.h>
|
||||
import "C"
|
||||
|
||||
// export_php:function mismatched_param_type(int $value): int
|
||||
func mismatched_param_type(value string) int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// export_php:class BadClass
|
||||
type BadClassStruct struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
// export_php:method BadClass::wrongReturnType(): string
|
||||
func (bc *BadClassStruct) WrongReturnType() int {
|
||||
return bc.Value
|
||||
}
|
||||
21
testdata/log-frankenphp_log.php
vendored
Normal file
21
testdata/log-frankenphp_log.php
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__.'/_executor.php';
|
||||
|
||||
frankenphp_log("default level message");
|
||||
|
||||
return function () {
|
||||
frankenphp_log("some debug message {$_GET['i']}", FRANKENPHP_LOG_LEVEL_DEBUG, [
|
||||
"key int" => 1,
|
||||
]);
|
||||
|
||||
frankenphp_log("some info message {$_GET['i']}", FRANKENPHP_LOG_LEVEL_INFO, [
|
||||
"key string" => "string",
|
||||
]);
|
||||
|
||||
frankenphp_log("some warn message {$_GET['i']}", FRANKENPHP_LOG_LEVEL_WARN);
|
||||
|
||||
frankenphp_log("some error message {$_GET['i']}", FRANKENPHP_LOG_LEVEL_ERROR, [
|
||||
"err" => ["a", "v"],
|
||||
]);
|
||||
};
|
||||
20
testdata/worker-getopt.php
vendored
20
testdata/worker-getopt.php
vendored
@@ -3,17 +3,17 @@
|
||||
do {
|
||||
$ok = frankenphp_handle_request(function (): void {
|
||||
print_r($_SERVER);
|
||||
|
||||
if (!isset($_SERVER['HTTP_REQUEST'])) {
|
||||
exit(1);
|
||||
}
|
||||
if (isset($_SERVER['FRANKENPHP_WORKER'])) {
|
||||
exit(2);
|
||||
}
|
||||
if (isset($_SERVER['FOO'])) {
|
||||
exit(3);
|
||||
}
|
||||
});
|
||||
|
||||
getopt('abc');
|
||||
|
||||
if (!isset($_SERVER['HTTP_REQUEST'])) {
|
||||
exit(1);
|
||||
}
|
||||
if (isset($_SERVER['FRANKENPHP_WORKER'])) {
|
||||
exit(2);
|
||||
}
|
||||
if (isset($_SERVER['FOO'])) {
|
||||
exit(3);
|
||||
}
|
||||
} while ($ok);
|
||||
|
||||
12
types.c
12
types.c
@@ -16,6 +16,8 @@ Bucket *get_ht_bucket_data(HashTable *ht, uint32_t index) {
|
||||
|
||||
void *__emalloc__(size_t size) { return emalloc(size); }
|
||||
|
||||
void __efree__(void *ptr) { efree(ptr); }
|
||||
|
||||
void __zend_hash_init__(HashTable *ht, uint32_t nSize, dtor_func_t pDestructor,
|
||||
bool persistent) {
|
||||
zend_hash_init(ht, nSize, NULL, pDestructor, persistent);
|
||||
@@ -31,6 +33,16 @@ void __zval_double__(zval *zv, double val) { ZVAL_DOUBLE(zv, val); }
|
||||
|
||||
void __zval_string__(zval *zv, zend_string *str) { ZVAL_STR(zv, str); }
|
||||
|
||||
void __zval_empty_string__(zval *zv) { ZVAL_EMPTY_STRING(zv); }
|
||||
|
||||
void __zval_arr__(zval *zv, zend_array *arr) { ZVAL_ARR(zv, arr); }
|
||||
|
||||
zend_array *__zend_new_array__(uint32_t size) { return zend_new_array(size); }
|
||||
|
||||
int __zend_is_callable__(zval *cb) { return zend_is_callable(cb, 0, NULL); }
|
||||
|
||||
int __call_user_function__(zval *function_name, zval *retval,
|
||||
uint32_t param_count, zval params[]) {
|
||||
return call_user_function(CG(function_table), NULL, function_name, retval,
|
||||
param_count, params);
|
||||
}
|
||||
|
||||
200
types.go
200
types.go
@@ -1,6 +1,20 @@
|
||||
package frankenphp
|
||||
|
||||
/*
|
||||
#cgo nocallback __zend_new_array__
|
||||
#cgo nocallback __zval_null__
|
||||
#cgo nocallback __zval_bool__
|
||||
#cgo nocallback __zval_long__
|
||||
#cgo nocallback __zval_double__
|
||||
#cgo nocallback __zval_string__
|
||||
#cgo nocallback __zval_arr__
|
||||
#cgo noescape __zend_new_array__
|
||||
#cgo noescape __zval_null__
|
||||
#cgo noescape __zval_bool__
|
||||
#cgo noescape __zval_long__
|
||||
#cgo noescape __zval_double__
|
||||
#cgo noescape __zval_string__
|
||||
#cgo noescape __zval_arr__
|
||||
#include "types.h"
|
||||
*/
|
||||
import "C"
|
||||
@@ -13,7 +27,7 @@ import (
|
||||
)
|
||||
|
||||
type toZval interface {
|
||||
toZval() *C.zval
|
||||
toZval(*C.zval)
|
||||
}
|
||||
|
||||
// EXPERIMENTAL: GoString copies a zend_string to a Go string.
|
||||
@@ -50,8 +64,8 @@ type AssociativeArray[T any] struct {
|
||||
Order []string
|
||||
}
|
||||
|
||||
func (a AssociativeArray[T]) toZval() *C.zval {
|
||||
return (*C.zval)(PHPAssociativeArray[T](a))
|
||||
func (a AssociativeArray[T]) toZval(zval *C.zval) {
|
||||
C.__zval_arr__(zval, (*C.zend_array)(PHPAssociativeArray[T](a)))
|
||||
}
|
||||
|
||||
// EXPERIMENTAL: GoAssociativeArray converts a zend_array to a Go AssociativeArray
|
||||
@@ -61,7 +75,7 @@ func GoAssociativeArray[T any](arr unsafe.Pointer) (AssociativeArray[T], error)
|
||||
return AssociativeArray[T]{entries, order}, err
|
||||
}
|
||||
|
||||
// EXPERIMENTAL: GoMap converts a zval having a zend_array value to an unordered Go map
|
||||
// EXPERIMENTAL: GoMap converts a zend_array to an unordered Go map
|
||||
func GoMap[T any](arr unsafe.Pointer) (map[string]T, error) {
|
||||
entries, _, err := goArray[T](arr, false)
|
||||
|
||||
@@ -73,27 +87,25 @@ func goArray[T any](arr unsafe.Pointer, ordered bool) (map[string]T, []string, e
|
||||
return nil, nil, errors.New("received a nil pointer on array conversion")
|
||||
}
|
||||
|
||||
zval := (*C.zval)(arr)
|
||||
v, err := extractZvalValue(zval, C.IS_ARRAY)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("received a *zval that wasn't a HashTable on array conversion: %w", err)
|
||||
array := (*C.zend_array)(arr)
|
||||
|
||||
if array == nil {
|
||||
return nil, nil, fmt.Errorf("received a *zval that wasn't a HashTable on array conversion")
|
||||
}
|
||||
|
||||
hashTable := (*C.HashTable)(v)
|
||||
|
||||
nNumUsed := hashTable.nNumUsed
|
||||
nNumUsed := array.nNumUsed
|
||||
entries := make(map[string]T, nNumUsed)
|
||||
var order []string
|
||||
if ordered {
|
||||
order = make([]string, 0, nNumUsed)
|
||||
}
|
||||
|
||||
if htIsPacked(hashTable) {
|
||||
// if the HashTable is packed, convert all integer keys to strings
|
||||
if htIsPacked(array) {
|
||||
// if the array is packed, convert all integer keys to strings
|
||||
// this is probably a bug by the dev using this function
|
||||
// still, we'll (inefficiently) convert to an associative array
|
||||
for i := C.uint32_t(0); i < nNumUsed; i++ {
|
||||
v := C.get_ht_packed_data(hashTable, i)
|
||||
v := C.get_ht_packed_data(array, i)
|
||||
if v != nil && C.zval_get_type(v) != C.IS_UNDEF {
|
||||
strIndex := strconv.Itoa(int(i))
|
||||
e, err := goValue[T](v)
|
||||
@@ -114,7 +126,7 @@ func goArray[T any](arr unsafe.Pointer, ordered bool) (map[string]T, []string, e
|
||||
var zeroVal T
|
||||
|
||||
for i := C.uint32_t(0); i < nNumUsed; i++ {
|
||||
bucket := C.get_ht_bucket_data(hashTable, i)
|
||||
bucket := C.get_ht_bucket_data(array, i)
|
||||
if bucket == nil || C.zval_get_type(&bucket.val) == C.IS_UNDEF {
|
||||
continue
|
||||
}
|
||||
@@ -150,26 +162,24 @@ func goArray[T any](arr unsafe.Pointer, ordered bool) (map[string]T, []string, e
|
||||
return entries, order, nil
|
||||
}
|
||||
|
||||
// EXPERIMENTAL: GoPackedArray converts a zval with a zend_array value to a Go slice
|
||||
// EXPERIMENTAL: GoPackedArray converts a zend_array to a Go slice
|
||||
func GoPackedArray[T any](arr unsafe.Pointer) ([]T, error) {
|
||||
if arr == nil {
|
||||
return nil, errors.New("GoPackedArray received a nil value")
|
||||
}
|
||||
|
||||
zval := (*C.zval)(arr)
|
||||
v, err := extractZvalValue(zval, C.IS_ARRAY)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GoPackedArray received *zval that wasn't a HashTable: %w", err)
|
||||
array := (*C.zend_array)(arr)
|
||||
|
||||
if array == nil {
|
||||
return nil, fmt.Errorf("GoPackedArray received *zval that wasn't a HashTable")
|
||||
}
|
||||
|
||||
hashTable := (*C.HashTable)(v)
|
||||
|
||||
nNumUsed := hashTable.nNumUsed
|
||||
nNumUsed := array.nNumUsed
|
||||
result := make([]T, 0, nNumUsed)
|
||||
|
||||
if htIsPacked(hashTable) {
|
||||
if htIsPacked(array) {
|
||||
for i := C.uint32_t(0); i < nNumUsed; i++ {
|
||||
v := C.get_ht_packed_data(hashTable, i)
|
||||
v := C.get_ht_packed_data(array, i)
|
||||
if v != nil && C.zval_get_type(v) != C.IS_UNDEF {
|
||||
v, err := goValue[T](v)
|
||||
if err != nil {
|
||||
@@ -185,7 +195,7 @@ func GoPackedArray[T any](arr unsafe.Pointer) ([]T, error) {
|
||||
|
||||
// fallback if ht isn't packed - equivalent to array_values()
|
||||
for i := C.uint32_t(0); i < nNumUsed; i++ {
|
||||
bucket := C.get_ht_bucket_data(hashTable, i)
|
||||
bucket := C.get_ht_bucket_data(array, i)
|
||||
if bucket != nil && C.zval_get_type(&bucket.val) != C.IS_UNDEF {
|
||||
v, err := goValue[T](&bucket.val)
|
||||
if err != nil {
|
||||
@@ -199,18 +209,18 @@ func GoPackedArray[T any](arr unsafe.Pointer) ([]T, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// EXPERIMENTAL: PHPMap converts an unordered Go map to a PHP zend_array
|
||||
// EXPERIMENTAL: PHPMap converts an unordered Go map to a zend_array
|
||||
func PHPMap[T any](arr map[string]T) unsafe.Pointer {
|
||||
return phpArray[T](arr, nil)
|
||||
}
|
||||
|
||||
// EXPERIMENTAL: PHPAssociativeArray converts a Go AssociativeArray to a PHP zval with a zend_array value
|
||||
// EXPERIMENTAL: PHPAssociativeArray converts a Go AssociativeArray to a zend_array
|
||||
func PHPAssociativeArray[T any](arr AssociativeArray[T]) unsafe.Pointer {
|
||||
return phpArray[T](arr.Map, arr.Order)
|
||||
}
|
||||
|
||||
func phpArray[T any](entries map[string]T, order []string) unsafe.Pointer {
|
||||
var zendArray *C.HashTable
|
||||
var zendArray *C.zend_array
|
||||
|
||||
if len(order) != 0 {
|
||||
zendArray = createNewArray((uint32)(len(order)))
|
||||
@@ -218,19 +228,18 @@ func phpArray[T any](entries map[string]T, order []string) unsafe.Pointer {
|
||||
val := entries[key]
|
||||
zval := phpValue(val)
|
||||
C.zend_hash_str_update(zendArray, toUnsafeChar(key), C.size_t(len(key)), zval)
|
||||
C.__efree__(unsafe.Pointer(zval))
|
||||
}
|
||||
} else {
|
||||
zendArray = createNewArray((uint32)(len(entries)))
|
||||
for key, val := range entries {
|
||||
zval := phpValue(val)
|
||||
C.zend_hash_str_update(zendArray, toUnsafeChar(key), C.size_t(len(key)), zval)
|
||||
C.__efree__(unsafe.Pointer(zval))
|
||||
}
|
||||
}
|
||||
|
||||
var zval C.zval
|
||||
C.__zval_arr__(&zval, zendArray)
|
||||
|
||||
return unsafe.Pointer(&zval)
|
||||
return unsafe.Pointer(zendArray)
|
||||
}
|
||||
|
||||
// EXPERIMENTAL: PHPPackedArray converts a Go slice to a PHP zval with a zend_array value.
|
||||
@@ -239,12 +248,10 @@ func PHPPackedArray[T any](slice []T) unsafe.Pointer {
|
||||
for _, val := range slice {
|
||||
zval := phpValue(val)
|
||||
C.zend_hash_next_index_insert(zendArray, zval)
|
||||
C.__efree__(unsafe.Pointer(zval))
|
||||
}
|
||||
|
||||
var zval C.zval
|
||||
C.__zval_arr__(&zval, zendArray)
|
||||
|
||||
return unsafe.Pointer(&zval)
|
||||
return unsafe.Pointer(zendArray)
|
||||
}
|
||||
|
||||
// EXPERIMENTAL: GoValue converts a PHP zval to a Go value
|
||||
@@ -316,11 +323,11 @@ func goValue[T any](zval *C.zval) (res T, err error) {
|
||||
return resZero, err
|
||||
}
|
||||
|
||||
hashTable := (*C.HashTable)(v)
|
||||
if hashTable != nil && htIsPacked(hashTable) {
|
||||
array := (*C.zend_array)(v)
|
||||
if array != nil && htIsPacked(array) {
|
||||
typ := reflect.TypeOf(res)
|
||||
if typ == nil || typ.Kind() == reflect.Interface && typ.NumMethod() == 0 {
|
||||
r, e := GoPackedArray[any](unsafe.Pointer(zval))
|
||||
r, e := GoPackedArray[any](unsafe.Pointer(array))
|
||||
if e != nil {
|
||||
return resZero, e
|
||||
}
|
||||
@@ -333,7 +340,7 @@ func goValue[T any](zval *C.zval) (res T, err error) {
|
||||
return resZero, fmt.Errorf("cannot convert packed array to non-any Go type %s", typ.String())
|
||||
}
|
||||
|
||||
a, err := GoAssociativeArray[T](unsafe.Pointer(zval))
|
||||
a, err := GoAssociativeArray[T](unsafe.Pointer(array))
|
||||
if err != nil {
|
||||
return resZero, err
|
||||
}
|
||||
@@ -364,45 +371,63 @@ func PHPValue(value any) unsafe.Pointer {
|
||||
}
|
||||
|
||||
func phpValue(value any) *C.zval {
|
||||
var zval C.zval
|
||||
zval := (*C.zval)(C.__emalloc__(C.size_t(unsafe.Sizeof(C.zval{}))))
|
||||
|
||||
if toZvalObj, ok := value.(toZval); ok {
|
||||
return toZvalObj.toZval()
|
||||
toZvalObj.toZval(zval)
|
||||
return zval
|
||||
}
|
||||
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
C.__zval_null__(&zval)
|
||||
C.__zval_null__(zval)
|
||||
case bool:
|
||||
C.__zval_bool__(&zval, C._Bool(v))
|
||||
C.__zval_bool__(zval, C._Bool(v))
|
||||
case int:
|
||||
C.__zval_long__(&zval, C.zend_long(v))
|
||||
C.__zval_long__(zval, C.zend_long(v))
|
||||
case int64:
|
||||
C.__zval_long__(&zval, C.zend_long(v))
|
||||
C.__zval_long__(zval, C.zend_long(v))
|
||||
case float64:
|
||||
C.__zval_double__(&zval, C.double(v))
|
||||
C.__zval_double__(zval, C.double(v))
|
||||
case string:
|
||||
if v == "" {
|
||||
C.__zval_empty_string__(zval)
|
||||
break
|
||||
}
|
||||
str := (*C.zend_string)(PHPString(v, false))
|
||||
C.__zval_string__(&zval, str)
|
||||
C.__zval_string__(zval, str)
|
||||
case AssociativeArray[any]:
|
||||
C.__zval_arr__(zval, (*C.zend_array)(PHPAssociativeArray[any](v)))
|
||||
case map[string]any:
|
||||
return (*C.zval)(PHPAssociativeArray[any](AssociativeArray[any]{Map: v}))
|
||||
C.__zval_arr__(zval, (*C.zend_array)(PHPMap[any](v)))
|
||||
case []any:
|
||||
return (*C.zval)(PHPPackedArray(v))
|
||||
C.__zval_arr__(zval, (*C.zend_array)(PHPPackedArray[any](v)))
|
||||
default:
|
||||
C.__efree__(unsafe.Pointer(zval))
|
||||
panic(fmt.Sprintf("unsupported Go type %T", v))
|
||||
}
|
||||
|
||||
return &zval
|
||||
return zval
|
||||
}
|
||||
|
||||
// createNewArray creates a new zend_array with the specified size.
|
||||
func createNewArray(size uint32) *C.HashTable {
|
||||
func createNewArray(size uint32) *C.zend_array {
|
||||
arr := C.__zend_new_array__(C.uint32_t(size))
|
||||
return (*C.HashTable)(unsafe.Pointer(arr))
|
||||
return (*C.zend_array)(unsafe.Pointer(arr))
|
||||
}
|
||||
|
||||
// htIsPacked checks if a HashTable is a list (packed) or hashmap (not packed).
|
||||
func htIsPacked(ht *C.HashTable) bool {
|
||||
// IsPacked determines if the given zend_array is a packed array (list).
|
||||
// Returns false if the array is nil or not packed.
|
||||
func IsPacked(arr unsafe.Pointer) bool {
|
||||
if arr == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return htIsPacked((*C.zend_array)(arr))
|
||||
}
|
||||
|
||||
// htIsPacked checks if a zend_array is a list (packed) or hashmap (not packed).
|
||||
func htIsPacked(ht *C.zend_array) bool {
|
||||
flags := *(*C.uint32_t)(unsafe.Pointer(&ht.u[0]))
|
||||
|
||||
return (flags & C.HASH_FLAG_PACKED) != 0
|
||||
@@ -435,3 +460,66 @@ func extractZvalValue(zval *C.zval, expectedType C.uint8_t) (unsafe.Pointer, err
|
||||
|
||||
return nil, fmt.Errorf("unsupported zval type %d", expectedType)
|
||||
}
|
||||
|
||||
func zendStringRelease(p unsafe.Pointer) {
|
||||
zs := (*C.zend_string)(p)
|
||||
C.zend_string_release(zs)
|
||||
}
|
||||
|
||||
func zendHashDestroy(p unsafe.Pointer) {
|
||||
ht := (*C.zend_array)(p)
|
||||
C.zend_hash_destroy(ht)
|
||||
}
|
||||
|
||||
// EXPERIMENTAL: CallPHPCallable executes a PHP callable with the given parameters.
|
||||
// Returns the result of the callable as a Go interface{}, or nil if the call failed.
|
||||
func CallPHPCallable(cb unsafe.Pointer, params []interface{}) interface{} {
|
||||
if cb == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
callback := (*C.zval)(cb)
|
||||
if callback == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if C.__zend_is_callable__(callback) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
paramCount := len(params)
|
||||
var paramStorage *C.zval
|
||||
if paramCount > 0 {
|
||||
paramStorage = (*C.zval)(C.__emalloc__(C.size_t(paramCount) * C.size_t(unsafe.Sizeof(C.zval{}))))
|
||||
defer func() {
|
||||
for i := 0; i < paramCount; i++ {
|
||||
targetZval := (*C.zval)(unsafe.Pointer(uintptr(unsafe.Pointer(paramStorage)) + uintptr(i)*unsafe.Sizeof(C.zval{})))
|
||||
C.zval_ptr_dtor(targetZval)
|
||||
}
|
||||
C.__efree__(unsafe.Pointer(paramStorage))
|
||||
}()
|
||||
|
||||
for i, param := range params {
|
||||
targetZval := (*C.zval)(unsafe.Pointer(uintptr(unsafe.Pointer(paramStorage)) + uintptr(i)*unsafe.Sizeof(C.zval{})))
|
||||
sourceZval := phpValue(param)
|
||||
*targetZval = *sourceZval
|
||||
C.__efree__(unsafe.Pointer(sourceZval))
|
||||
}
|
||||
}
|
||||
|
||||
var retval C.zval
|
||||
|
||||
result := C.__call_user_function__(callback, &retval, C.uint32_t(paramCount), paramStorage)
|
||||
if result != C.SUCCESS {
|
||||
return nil
|
||||
}
|
||||
|
||||
goResult, err := goValue[any](&retval)
|
||||
C.zval_ptr_dtor(&retval)
|
||||
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return goResult
|
||||
}
|
||||
|
||||
6
types.h
6
types.h
@@ -11,14 +11,20 @@ zval *get_ht_packed_data(HashTable *, uint32_t index);
|
||||
Bucket *get_ht_bucket_data(HashTable *, uint32_t index);
|
||||
|
||||
void *__emalloc__(size_t size);
|
||||
void __efree__(void *ptr);
|
||||
void __zend_hash_init__(HashTable *ht, uint32_t nSize, dtor_func_t pDestructor,
|
||||
bool persistent);
|
||||
|
||||
int __zend_is_callable__(zval *cb);
|
||||
int __call_user_function__(zval *function_name, zval *retval,
|
||||
uint32_t param_count, zval params[]);
|
||||
|
||||
void __zval_null__(zval *zv);
|
||||
void __zval_bool__(zval *zv, bool val);
|
||||
void __zval_long__(zval *zv, zend_long val);
|
||||
void __zval_double__(zval *zv, double val);
|
||||
void __zval_string__(zval *zv, zend_string *str);
|
||||
void __zval_empty_string__(zval *zv);
|
||||
void __zval_arr__(zval *zv, zend_array *arr);
|
||||
zend_array *__zend_new_array__(uint32_t size);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package frankenphp
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
@@ -13,7 +12,8 @@ import (
|
||||
// this is necessary if tests make use of PHP's internal allocation
|
||||
func testOnDummyPHPThread(t *testing.T, test func()) {
|
||||
t.Helper()
|
||||
globalLogger = slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
|
||||
globalLogger = slog.Default()
|
||||
_, err := initPHPThreads(1, 1, nil) // boot 1 thread
|
||||
assert.NoError(t, err)
|
||||
handler := convertToTaskThread(phpThreads[0])
|
||||
@@ -29,9 +29,10 @@ func TestGoString(t *testing.T) {
|
||||
testOnDummyPHPThread(t, func() {
|
||||
originalString := "Hello, World!"
|
||||
|
||||
convertedString := GoString(PHPString(originalString, false))
|
||||
phpString := PHPString(originalString, false)
|
||||
defer zendStringRelease(phpString)
|
||||
|
||||
assert.Equal(t, originalString, convertedString, "string -> zend_string -> string should yield an equal string")
|
||||
assert.Equal(t, originalString, GoString(phpString), "string -> zend_string -> string should yield an equal string")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -42,7 +43,9 @@ func TestPHPMap(t *testing.T) {
|
||||
"foo2": "bar2",
|
||||
}
|
||||
|
||||
convertedMap, err := GoMap[string](PHPMap(originalMap))
|
||||
phpArray := PHPMap(originalMap)
|
||||
defer zendHashDestroy(phpArray)
|
||||
convertedMap, err := GoMap[string](phpArray)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, originalMap, convertedMap, "associative array should be equal after conversion")
|
||||
@@ -59,7 +62,9 @@ func TestOrderedPHPAssociativeArray(t *testing.T) {
|
||||
Order: []string{"foo2", "foo1"},
|
||||
}
|
||||
|
||||
convertedArray, err := GoAssociativeArray[string](PHPAssociativeArray(originalArray))
|
||||
phpArray := PHPAssociativeArray(originalArray)
|
||||
defer zendHashDestroy(phpArray)
|
||||
convertedArray, err := GoAssociativeArray[string](phpArray)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, originalArray, convertedArray, "associative array should be equal after conversion")
|
||||
@@ -70,7 +75,9 @@ func TestPHPPackedArray(t *testing.T) {
|
||||
testOnDummyPHPThread(t, func() {
|
||||
originalSlice := []string{"bar1", "bar2"}
|
||||
|
||||
convertedSlice, err := GoPackedArray[string](PHPPackedArray(originalSlice))
|
||||
phpArray := PHPPackedArray(originalSlice)
|
||||
defer zendHashDestroy(phpArray)
|
||||
convertedSlice, err := GoPackedArray[string](phpArray)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, originalSlice, convertedSlice, "slice should be equal after conversion")
|
||||
@@ -85,7 +92,9 @@ func TestPHPPackedArrayToGoMap(t *testing.T) {
|
||||
"1": "bar2",
|
||||
}
|
||||
|
||||
convertedMap, err := GoMap[string](PHPPackedArray(originalSlice))
|
||||
phpArray := PHPPackedArray(originalSlice)
|
||||
defer zendHashDestroy(phpArray)
|
||||
convertedMap, err := GoMap[string](phpArray)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expectedMap, convertedMap, "convert a packed to an associative array")
|
||||
@@ -103,7 +112,9 @@ func TestPHPAssociativeArrayToPacked(t *testing.T) {
|
||||
}
|
||||
expectedSlice := []string{"bar1", "bar2"}
|
||||
|
||||
convertedSlice, err := GoPackedArray[string](PHPAssociativeArray(originalArray))
|
||||
phpArray := PHPAssociativeArray(originalArray)
|
||||
defer zendHashDestroy(phpArray)
|
||||
convertedSlice, err := GoPackedArray[string](phpArray)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expectedSlice, convertedSlice, "convert an associative array to a slice")
|
||||
@@ -126,7 +137,9 @@ func TestNestedMixedArray(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
convertedArray, err := GoMap[any](PHPMap(originalArray))
|
||||
phpArray := PHPMap(originalArray)
|
||||
defer zendHashDestroy(phpArray)
|
||||
convertedArray, err := GoMap[any](phpArray)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, originalArray, convertedArray, "nested mixed array should be equal after conversion")
|
||||
|
||||
23
watcher-skip.go
Normal file
23
watcher-skip.go
Normal file
@@ -0,0 +1,23 @@
|
||||
//go:build nowatcher
|
||||
|
||||
package frankenphp
|
||||
|
||||
import "errors"
|
||||
|
||||
type hotReloadOpt struct {
|
||||
}
|
||||
|
||||
var errWatcherNotEnabled = errors.New("watcher support is not enabled")
|
||||
|
||||
func initWatchers(o *opt) error {
|
||||
for _, o := range o.workers {
|
||||
if len(o.watch) != 0 {
|
||||
return errWatcherNotEnabled
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func drainWatchers() {
|
||||
}
|
||||
47
watcher.go
Normal file
47
watcher.go
Normal file
@@ -0,0 +1,47 @@
|
||||
//go:build !nowatcher
|
||||
|
||||
package frankenphp
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/dunglas/frankenphp/internal/watcher"
|
||||
watcherGo "github.com/e-dant/watcher/watcher-go"
|
||||
)
|
||||
|
||||
type hotReloadOpt struct {
|
||||
hotReload []*watcher.PatternGroup
|
||||
}
|
||||
|
||||
var restartWorkers atomic.Bool
|
||||
|
||||
func initWatchers(o *opt) error {
|
||||
watchPatterns := make([]*watcher.PatternGroup, 0, len(o.hotReload))
|
||||
|
||||
for _, o := range o.workers {
|
||||
if len(o.watch) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
watcherIsEnabled = true
|
||||
watchPatterns = append(watchPatterns, &watcher.PatternGroup{Patterns: o.watch, Callback: func(_ []*watcherGo.Event) {
|
||||
restartWorkers.Store(true)
|
||||
}})
|
||||
}
|
||||
|
||||
if watcherIsEnabled {
|
||||
watchPatterns = append(watchPatterns, &watcher.PatternGroup{
|
||||
Callback: func(_ []*watcherGo.Event) {
|
||||
if restartWorkers.Swap(false) {
|
||||
RestartWorkers()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return watcher.InitWatcher(globalCtx, globalLogger, append(watchPatterns, o.hotReload...))
|
||||
}
|
||||
|
||||
func drainWatchers() {
|
||||
watcher.DrainWatcher()
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// we have to wait a few milliseconds for the watcher debounce to take effect
|
||||
@@ -41,6 +42,8 @@ func TestWorkersShouldNotReloadOnExcludingPattern(t *testing.T) {
|
||||
}
|
||||
|
||||
func pollForWorkerReset(t *testing.T, handler func(http.ResponseWriter, *http.Request), limit int) bool {
|
||||
t.Helper()
|
||||
|
||||
// first we make an initial request to start the request counter
|
||||
body, _ := testGet("http://example.com/worker-with-counter.php", handler, t)
|
||||
assert.Equal(t, "requests:1", body)
|
||||
@@ -54,18 +57,19 @@ func pollForWorkerReset(t *testing.T, handler func(http.ResponseWriter, *http.Re
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func updateTestFile(fileName string, content string, t *testing.T) {
|
||||
absFileName, err := filepath.Abs(fileName)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
dirName := filepath.Dir(absFileName)
|
||||
if _, err := os.Stat(dirName); os.IsNotExist(err) {
|
||||
if _, err = os.Stat(dirName); os.IsNotExist(err) {
|
||||
err = os.MkdirAll(dirName, 0700)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
bytes := []byte(content)
|
||||
err = os.WriteFile(absFileName, bytes, 0644)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, os.WriteFile(absFileName, []byte(content), 0644))
|
||||
}
|
||||
|
||||
62
worker.go
62
worker.go
@@ -14,11 +14,12 @@ import (
|
||||
|
||||
"github.com/dunglas/frankenphp/internal/fastabs"
|
||||
"github.com/dunglas/frankenphp/internal/state"
|
||||
"github.com/dunglas/frankenphp/internal/watcher"
|
||||
)
|
||||
|
||||
// represents a worker script and can have many threads assigned to it
|
||||
type worker struct {
|
||||
mercureContext
|
||||
|
||||
name string
|
||||
fileName string
|
||||
num int
|
||||
@@ -37,25 +38,31 @@ type worker struct {
|
||||
var (
|
||||
workers []*worker
|
||||
watcherIsEnabled bool
|
||||
startupFailChan chan (error)
|
||||
startupFailChan chan error
|
||||
)
|
||||
|
||||
func initWorkers(opt []workerOpt) error {
|
||||
if len(opt) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
workersReady sync.WaitGroup
|
||||
totalThreadsToStart int
|
||||
)
|
||||
|
||||
workers = make([]*worker, 0, len(opt))
|
||||
directoriesToWatch := getDirectoriesToWatch(opt)
|
||||
watcherIsEnabled = len(directoriesToWatch) > 0
|
||||
totalThreadsToStart := 0
|
||||
|
||||
for _, o := range opt {
|
||||
w, err := newWorker(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
totalThreadsToStart += w.num
|
||||
workers = append(workers, w)
|
||||
}
|
||||
|
||||
var workersReady sync.WaitGroup
|
||||
startupFailChan = make(chan error, totalThreadsToStart)
|
||||
|
||||
for _, w := range workers {
|
||||
@@ -73,22 +80,13 @@ func initWorkers(opt []workerOpt) error {
|
||||
|
||||
select {
|
||||
case err := <-startupFailChan:
|
||||
// at least 1 worker has failed, shut down and return an error
|
||||
Shutdown()
|
||||
// at least 1 worker has failed, return an error
|
||||
return fmt.Errorf("failed to initialize workers: %w", err)
|
||||
default:
|
||||
// all workers started successfully
|
||||
startupFailChan = nil
|
||||
}
|
||||
|
||||
if !watcherIsEnabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := watcher.InitWatcher(globalCtx, directoriesToWatch, RestartWorkers, globalLogger); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -156,6 +154,8 @@ func newWorker(o workerOpt) (*worker, error) {
|
||||
onThreadShutdown: o.onThreadShutdown,
|
||||
}
|
||||
|
||||
w.configureMercure(&o)
|
||||
|
||||
w.requestOptions = append(
|
||||
w.requestOptions,
|
||||
WithRequestDocumentRoot(filepath.Dir(o.fileName), false),
|
||||
@@ -175,39 +175,43 @@ func DrainWorkers() {
|
||||
}
|
||||
|
||||
func drainWorkerThreads() []*phpThread {
|
||||
ready := sync.WaitGroup{}
|
||||
drainedThreads := make([]*phpThread, 0)
|
||||
var (
|
||||
ready sync.WaitGroup
|
||||
drainedThreads []*phpThread
|
||||
)
|
||||
|
||||
for _, worker := range workers {
|
||||
worker.threadMutex.RLock()
|
||||
ready.Add(len(worker.threads))
|
||||
|
||||
for _, thread := range worker.threads {
|
||||
if !thread.state.RequestSafeStateChange(state.Restarting) {
|
||||
ready.Done()
|
||||
|
||||
// no state change allowed == thread is shutting down
|
||||
// we'll proceed to restart all other threads anyways
|
||||
// we'll proceed to restart all other threads anyway
|
||||
continue
|
||||
}
|
||||
|
||||
close(thread.drainChan)
|
||||
drainedThreads = append(drainedThreads, thread)
|
||||
|
||||
go func(thread *phpThread) {
|
||||
thread.state.WaitFor(state.Yielding)
|
||||
ready.Done()
|
||||
}(thread)
|
||||
}
|
||||
|
||||
worker.threadMutex.RUnlock()
|
||||
}
|
||||
|
||||
ready.Wait()
|
||||
|
||||
return drainedThreads
|
||||
}
|
||||
|
||||
func drainWatcher() {
|
||||
if watcherIsEnabled {
|
||||
watcher.DrainWatcher()
|
||||
}
|
||||
}
|
||||
|
||||
// RestartWorkers attempts to restart all workers gracefully
|
||||
// All workers must be restarted at the same time to prevent issues with opcache resetting.
|
||||
func RestartWorkers() {
|
||||
// disallow scaling threads while restarting workers
|
||||
scalingMu.Lock()
|
||||
@@ -221,14 +225,6 @@ func RestartWorkers() {
|
||||
}
|
||||
}
|
||||
|
||||
func getDirectoriesToWatch(workerOpts []workerOpt) []string {
|
||||
directoriesToWatch := []string{}
|
||||
for _, w := range workerOpts {
|
||||
directoriesToWatch = append(directoriesToWatch, w.watch...)
|
||||
}
|
||||
return directoriesToWatch
|
||||
}
|
||||
|
||||
func (worker *worker) attachThread(thread *phpThread) {
|
||||
worker.threadMutex.Lock()
|
||||
worker.threads = append(worker.threads, thread)
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
@@ -17,9 +16,6 @@ import (
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/zap/exp/zapslog"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"go.uber.org/zap/zaptest/observer"
|
||||
)
|
||||
|
||||
func TestWorker(t *testing.T) {
|
||||
@@ -97,8 +93,7 @@ func TestWorkerEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestWorkerGetOpt(t *testing.T) {
|
||||
obs, logs := observer.New(zapcore.InfoLevel)
|
||||
logger := slog.New(zapslog.NewHandler(obs))
|
||||
logger, buf := newTestLogger(t)
|
||||
|
||||
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) {
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/worker-getopt.php?i=%d", i), nil)
|
||||
@@ -114,9 +109,7 @@ func TestWorkerGetOpt(t *testing.T) {
|
||||
assert.Contains(t, string(body), fmt.Sprintf("[REQUEST_URI] => /worker-getopt.php?i=%d", i))
|
||||
}, &testOptions{logger: logger, workerScript: "worker-getopt.php", env: map[string]string{"FOO": "bar"}})
|
||||
|
||||
for _, l := range logs.FilterFieldKey("exit_status").All() {
|
||||
assert.Failf(t, "unexpected exit status", "exit status: %d", l.ContextMap()["exit_status"])
|
||||
}
|
||||
assert.NotRegexp(t, buf.String(), "exit_status=[1-9]")
|
||||
}
|
||||
|
||||
func ExampleServeHTTP_workers() {
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
)
|
||||
|
||||
func TestWorkersExtension(t *testing.T) {
|
||||
t.Cleanup(Shutdown)
|
||||
|
||||
readyWorkers := 0
|
||||
shutdownWorkers := 0
|
||||
serverStarts := 0
|
||||
|
||||
Reference in New Issue
Block a user