mirror of
https://github.com/php/frankenphp.git
synced 2026-03-23 16:42:13 +01:00
feat: Windows support (#2119)
Closes #83 #880 #1286. Working patch for Windows support. Supports linking to the [official PHP release (TS version)](https://www.php.net/downloads.php). Includes some work from #1286 (thanks @TenHian!!) This patch allows using Visual Studio to compile the cgo code. To do so, it must be compiled with Go 1.26 (RC) with the following setup: ```powershell winget install -e --id Microsoft.VisualStudio.2022.Community --override "--passive --wait --add Microsoft.VisualStudio.Workload.NativeDesktop --add Microsoft.VisualStudio.Component.VC.Llvm.Clang --includeRecommended" winget install -e --id GoLang.Go $env:PATH += ';C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\bin' cd c:\ gh repo clone microsoft/vcpkg .\vcpkg\bootstrap-vcpkg.bat .\vcpkg\vcpkg install pthreads brotli # build watcher Invoke-WebRequest -Uri "https://github.com/e-dant/watcher/releases/download/0.14.3/x86_64-pc-windows-msvc.tar" -OutFile "$env:TEMP\watcher.tar" tar -xf "$env:TEMP\watcher.tar" -C C:\ Rename-Item -Path "C:\x86_64-pc-windows-msvc" -NewName "watcher-x86_64-pc-windows-msvc" Remove-Item "$env:TEMP\watcher.tar" # download php Invoke-WebRequest -Uri "https://downloads.php.net/~windows/releases/archives/php-8.5.1-Win32-vs17-x64.zip" -OutFile "$env:TEMP\php.zip" Expand-Archive -Path "$env:TEMP\php.zip" -DestinationPath "C:\" Remove-Item "$env:TEMP\php.zip" # download php development package Invoke-WebRequest -Uri "https://downloads.php.net/~windows/releases/archives/php-devel-pack-8.5.1-Win32-vs17-x64.zip" -OutFile "$env:TEMP\php-devel.zip" Expand-Archive -Path "$env:TEMP\php-devel.zip" -DestinationPath "C:\" Remove-Item "$env:TEMP\php-devel.zip" $env:GOTOOLCHAIN = 'go1.26rc1' $env:CC = 'clang' $env:CXX = 'clang++' $env:CGO_CFLAGS = "-I$env:C:\vcpkg\installed\x64-windows\include -IC:\watcher-x86_64-pc-windows-msvc -IC:\php-8.5.1-devel-vs17-x64\include -IC:\php-8.5.1-devel-vs17-x64\include\main -IC:\php-8.5.1-devel-vs17-x64\include\TSRM -IC:\php-8.5.1-devel-vs17-x64\include\Zend -IC:\php-8.5.1-devel-vs17-x64\include\ext" $env:CGO_LDFLAGS = '-LC:\vcpkg\installed\x64-windows\lib -lbrotlienc -LC:\watcher-x86_64-pc-windows-msvc -llibwatcher-c -LC:\php-8.5.1-Win32-vs17-x64 -LC:\php-8.5.1-devel-vs17-x64\lib -lphp8ts -lphp8embed' # clone frankenphp and build git clone -b windows https://github.com/php/frankenphp.git cd frankenphp\caddy\frankenphp go build -ldflags '-extldflags="-fuse-ld=lld"' -tags nowatcher,nobadger,nomysql,nopgx # Tests $env:PATH += ";$env:VCPKG_ROOT\installed\x64-windows\bin;C:\watcher-x86_64-pc-windows-msvc";C:\php-8.5.1-Win32-vs17-x64" "opcache.enable=0`r`nopcache.enable_cli=0" | Out-File -Encoding ascii php.ini $env:PHPRC = Get-Location go test -ldflags '-extldflags="-fuse-ld=lld"' -tags nowatcher,nobadger,nomysql,nopgx . ``` TODO: - [x] Fix remaining skipped tests (scaling and watcher) - [x] Test if the watcher mode works as expected - [x] Automate the build with GitHub Actions --------- Signed-off-by: Marc <m@pyc.ac> Co-authored-by: Kévin Dunglas <kevin@dunglas.dev> Co-authored-by: DubbleClick <m@pyc.ac>
This commit is contained in:
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
4
.github/workflows/static.yaml
vendored
4
.github/workflows/static.yaml
vendored
@@ -453,12 +453,12 @@ jobs:
|
||||
ref: ${{ needs.prepare.outputs.ref }}
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
with: # zizmor: ignore[cache-poisoning]
|
||||
go-version: "1.26"
|
||||
cache-dependency-path: |
|
||||
go.sum
|
||||
caddy/go.sum
|
||||
cache: false
|
||||
cache: ${{ github.event_name != 'release' }}
|
||||
- name: Set FRANKENPHP_VERSION
|
||||
run: |
|
||||
if [ "${GITHUB_REF_TYPE}" == "tag" ]; then
|
||||
|
||||
8
.github/workflows/tests.yaml
vendored
8
.github/workflows/tests.yaml
vendored
@@ -35,6 +35,7 @@ jobs:
|
||||
env:
|
||||
GOMAXPROCS: 10
|
||||
LIBRARY_PATH: ${{ github.workspace }}/watcher/target/lib
|
||||
GOFLAGS: "-tags=nobadger,nomysql,nopgx"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
@@ -69,7 +70,7 @@ jobs:
|
||||
run: ./frankenphp.test -test.v
|
||||
- name: Run Caddy module tests
|
||||
working-directory: caddy/
|
||||
run: go test -tags nobadger,nomysql,nopgx -race -v ./...
|
||||
run: go test -race -v ./...
|
||||
- name: Run Fuzzing Tests
|
||||
working-directory: caddy/
|
||||
run: go test -fuzz FuzzRequest -fuzztime 20s
|
||||
@@ -100,6 +101,8 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php-versions: ["8.3", "8.4", "8.5"]
|
||||
env:
|
||||
XCADDY_GO_BUILD_FLAGS: "-tags=nobadger,nomysql,nopgx"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
@@ -141,6 +144,7 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
env:
|
||||
HOMEBREW_NO_AUTO_UPDATE: 1
|
||||
GOFLAGS: "-tags=nowatcher,nobadger,nomysql,nopgx"
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
@@ -172,4 +176,4 @@ jobs:
|
||||
run: go test -tags nowatcher -race -v ./...
|
||||
- name: Run Caddy module tests
|
||||
working-directory: caddy/
|
||||
run: go test -tags nowatcher,nobadger,nomysql,nopgx -race -v ./...
|
||||
run: go test -race -v ./...
|
||||
|
||||
223
.github/workflows/windows.yaml
vendored
Normal file
223
.github/workflows/windows.yaml
vendored
Normal file
@@ -0,0 +1,223 @@
|
||||
name: Build Windows release
|
||||
|
||||
concurrency:
|
||||
cancel-in-progress: true
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "docker-bake.hcl"
|
||||
- "vcpkg.json"
|
||||
- ".github/workflows/static.yaml"
|
||||
- "**cgo.go"
|
||||
- "**Dockerfile"
|
||||
- "**.c"
|
||||
- "**.h"
|
||||
- "**.sh"
|
||||
- "**.stub.php"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- v*.*.*
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
#checkov:skip=CKV_GHA_7
|
||||
version:
|
||||
description: "FrankenPHP version"
|
||||
required: false
|
||||
type: string
|
||||
schedule:
|
||||
- cron: "0 8 * * *"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
GOTOOLCHAIN: local
|
||||
GOFLAGS: "-ldflags=-extldflags=-fuse-ld=lld -tags=nobadger,nomysql,nopgx"
|
||||
PHP_DOWNLOAD_BASE: "https://downloads.php.net/~windows/releases/"
|
||||
CC: clang
|
||||
CXX: clang++
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: powershell
|
||||
|
||||
steps:
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config --global core.autocrlf false
|
||||
git config --global core.eol lf
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: frankenphp
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set FRANKENPHP_VERSION
|
||||
run: |
|
||||
if ($env:GITHUB_REF_TYPE -eq "tag") {
|
||||
$frankenphpVersion = $env:GITHUB_REF_NAME.Substring(1)
|
||||
} elseif ($env:GITHUB_EVENT_NAME -eq "schedule") {
|
||||
$frankenphpVersion = $env:GITHUB_REF
|
||||
} else {
|
||||
$frankenphpVersion = $env:GITHUB_SHA
|
||||
}
|
||||
|
||||
echo "FRANKENPHP_VERSION=$frankenphpVersion" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with: # zizmor: ignore[cache-poisoning]
|
||||
go-version: "1.26"
|
||||
cache-dependency-path: |
|
||||
frankenphp/go.sum
|
||||
frankenphp/caddy/go.sum
|
||||
cache: ${{ github.event_name != 'release' }}
|
||||
check-latest: true
|
||||
|
||||
- name: Install Vcpkg Libraries
|
||||
working-directory: frankenphp
|
||||
run: "vcpkg install"
|
||||
|
||||
- name: Download Watcher
|
||||
run: |
|
||||
$latestTag = gh release list --repo e-dant/watcher --limit 1 --exclude-drafts --exclude-pre-releases --json tagName --jq '.[0].tagName'
|
||||
Write-Host "Latest Watcher version: $latestTag"
|
||||
|
||||
gh release download $latestTag --repo e-dant/watcher --pattern "*x86_64-pc-windows-msvc.tar" -O watcher.tar
|
||||
|
||||
tar -xf "watcher.tar" -C "$env:GITHUB_WORKSPACE"
|
||||
Rename-Item -Path "$env:GITHUB_WORKSPACE\x86_64-pc-windows-msvc" -NewName "watcher"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Download PHP
|
||||
run: |
|
||||
$webContent = Invoke-WebRequest -Uri $env:PHP_DOWNLOAD_BASE -UseBasicParsing
|
||||
$links = $webContent.Links.Href | Where-Object { $_ -match "php-\d+\.\d+\.\d+-Win32-vs17-x64\.zip$" }
|
||||
|
||||
if (-not $links) { throw "Could not find PHP zip files at $env:PHP_DOWNLOAD_BASE" }
|
||||
|
||||
$latestFile = $links | Sort-Object { if ($_ -match '(\d+\.\d+\.\d+)') { [version]$matches[1] } } | Select-Object -Last 1
|
||||
|
||||
$version = if ($latestFile -match '(\d+\.\d+\.\d+)') { $matches[1] }
|
||||
Write-Host "Detected latest PHP version: $version"
|
||||
|
||||
"PHP_VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
|
||||
$phpZip = "php-$version-Win32-vs17-x64.zip"
|
||||
$develZip = "php-devel-pack-$version-Win32-vs17-x64.zip"
|
||||
|
||||
$dirName = "frankenphp-$env:FRANKENPHP_VERSION-php-$version-Win32-vs17-x64"
|
||||
|
||||
echo "DIR_NAME=$dirName" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
|
||||
Invoke-WebRequest -Uri "$env:PHP_DOWNLOAD_BASE/$phpZip" -OutFile "$env:TEMP\php.zip"
|
||||
Expand-Archive -Path "$env:TEMP\php.zip" -DestinationPath "$env:GITHUB_WORKSPACE\$dirName"
|
||||
|
||||
Invoke-WebRequest -Uri "$env:PHP_DOWNLOAD_BASE/$develZip" -OutFile "$env:TEMP\php-devel.zip"
|
||||
Expand-Archive -Path "$env:TEMP\php-devel.zip" -DestinationPath "$env:GITHUB_WORKSPACE\php-devel"
|
||||
|
||||
- name: Prepare env
|
||||
run: |
|
||||
$vcpkgRoot = "$env:GITHUB_WORKSPACE\frankenphp\vcpkg_installed\x64-windows"
|
||||
$watcherRoot = "$env:GITHUB_WORKSPACE\watcher"
|
||||
$phpBin = "$env:GITHUB_WORKSPACE\$env:DIR_NAME"
|
||||
$phpDevel = "$env:GITHUB_WORKSPACE\php-devel\php-$env:PHP_VERSION-devel-vs17-x64"
|
||||
|
||||
echo "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\Llvm\bin" | Out-File -FilePath $env:GITHUB_PATH -Append
|
||||
echo "$vcpkgRoot\bin" | Out-File -FilePath $env:GITHUB_PATH -Append
|
||||
echo "$watcherRoot" | Out-File -FilePath $env:GITHUB_PATH -Append
|
||||
echo "$phpBin" | Out-File -FilePath $env:GITHUB_PATH -Append
|
||||
|
||||
echo "CGO_CFLAGS=-DFRANKENPHP_VERSION=$env:FRANKENPHP_VERSION -I$vcpkgRoot\include -I$watcherRoot -I$phpDevel\include -I$phpDevel\include\main -I$phpDevel\include\TSRM -I$phpDevel\include\Zend -I$phpDevel\include\ext" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
echo "CGO_LDFLAGS=-L$vcpkgRoot\lib -lbrotlienc -L$watcherRoot -llibwatcher-c -L$phpBin -L$phpDevel\lib -lphp8ts -lphp8embed" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
|
||||
- name: Embed Windows icon and metadata
|
||||
working-directory: frankenphp\caddy\frankenphp
|
||||
run: |
|
||||
go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@latest
|
||||
|
||||
$major = 0; $minor = 0; $patch = 0; $build = 0
|
||||
if ($env:FRANKENPHP_VERSION -match '^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)$') {
|
||||
$major = [int]$Matches['major']
|
||||
$minor = [int]$Matches['minor']
|
||||
$patch = [int]$Matches['patch']
|
||||
}
|
||||
|
||||
$json = @{
|
||||
FixedFileInfo = @{
|
||||
FileVersion = @{ Major = $major; Minor = $minor; Patch = $patch; Build = $build }
|
||||
ProductVersion = @{ Major = $major; Minor = $minor; Patch = $patch; Build = $build }
|
||||
}
|
||||
StringFileInfo = @{
|
||||
CompanyName = "FrankenPHP"
|
||||
FileDescription = "The modern PHP app server"
|
||||
FileVersion = $env:FRANKENPHP_VERSION
|
||||
InternalName = "frankenphp"
|
||||
OriginalFilename = "frankenphp.exe"
|
||||
LegalCopyright = "(c) 2022 Kévin Dunglas, MIT License"
|
||||
ProductName = "FrankenPHP"
|
||||
ProductVersion = $env:FRANKENPHP_VERSION
|
||||
Comments = "https://frankenphp.dev/"
|
||||
}
|
||||
VarFileInfo = @{
|
||||
Translation = @{ LangID = 9; CharsetID = 1200 }
|
||||
}
|
||||
} | ConvertTo-Json -Depth 10
|
||||
[System.IO.File]::WriteAllText("versioninfo.json", $json, [System.Text.Encoding]::Default)
|
||||
|
||||
goversioninfo -64 -icon ..\..\frankenphp.ico versioninfo.json -o resource.syso
|
||||
|
||||
- name: Build FrankenPHP
|
||||
run: |
|
||||
$customVersion = "FrankenPHP $env:FRANKENPHP_VERSION PHP $env:PHP_VERSION Caddy"
|
||||
go build -ldflags="-extldflags=-fuse-ld=lld -X 'github.com/caddyserver/caddy/v2.CustomVersion=$customVersion' -X 'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy'"
|
||||
working-directory: frankenphp\caddy\frankenphp
|
||||
|
||||
- name: Create Directory
|
||||
run: |
|
||||
Copy-Item frankenphp\caddy\frankenphp\frankenphp.exe $env:DIR_NAME
|
||||
Copy-Item watcher\libwatcher-c.dll $env:DIR_NAME
|
||||
Copy-Item frankenphp\vcpkg_installed\x64-windows\bin\brotlienc.dll $env:DIR_NAME
|
||||
Copy-Item frankenphp\vcpkg_installed\x64-windows\bin\brotlidec.dll $env:DIR_NAME
|
||||
Copy-Item frankenphp\vcpkg_installed\x64-windows\bin\brotlicommon.dll $env:DIR_NAME
|
||||
Copy-Item frankenphp\vcpkg_installed\x64-windows\bin\pthreadVC3.dll $env:DIR_NAME
|
||||
|
||||
- name: Upload Artifact
|
||||
if: github.event_name != 'release'
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ env.DIR_NAME }}
|
||||
path: ${{ env.DIR_NAME }}
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Zip Release Artifact
|
||||
if: github.event_name == 'release'
|
||||
run: Compress-Archive -Path "$env:DIR_NAME\*" -DestinationPath "$env:DIR_NAME.zip"
|
||||
|
||||
- name: Upload Release Asset
|
||||
if: github.event_name == 'release'
|
||||
run: gh release upload $env:GITHUB_EVENT_RELEASE_TAG_NAME "$env:DIR_NAME.zip" --clobber
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_EVENT_RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
"opcache.enable=0`r`nopcache.enable_cli=0" | Out-File php.ini
|
||||
$env:PHPRC = Get-Location
|
||||
|
||||
go test ./...
|
||||
cd caddy
|
||||
go test ./...
|
||||
working-directory: ${{ github.workspace }}\frankenphp
|
||||
@@ -42,6 +42,25 @@ func waitForServerReady(t *testing.T, url string) {
|
||||
|
||||
var testPort = "9080"
|
||||
|
||||
// skipIfSymlinkNotValid skips the test if the given path is not a valid symlink
|
||||
func skipIfSymlinkNotValid(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
|
||||
info, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
t.Skipf("symlink test skipped: cannot stat %s: %v", path, err)
|
||||
}
|
||||
|
||||
if info.Mode()&os.ModeSymlink == 0 {
|
||||
t.Skipf("symlink test skipped: %s is not a symlink (git may not support symlinks on this platform)", path)
|
||||
}
|
||||
}
|
||||
|
||||
// escapeMetricLabel escapes backslashes in label values for Prometheus text format
|
||||
func escapeMetricLabel(s string) string {
|
||||
return strings.ReplaceAll(s, "\\", "\\\\")
|
||||
}
|
||||
|
||||
func TestPHP(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
tester := caddytest.NewTester(t)
|
||||
@@ -573,6 +592,7 @@ func TestWorkerMetrics(t *testing.T) {
|
||||
`, "caddyfile")
|
||||
|
||||
workerName, _ := fastabs.FastAbs("../testdata/index.php")
|
||||
workerName = escapeMetricLabel(workerName)
|
||||
|
||||
// Make some requests
|
||||
for i := range 10 {
|
||||
@@ -760,6 +780,7 @@ func TestAutoWorkerConfig(t *testing.T) {
|
||||
`, "caddyfile")
|
||||
|
||||
workerName, _ := fastabs.FastAbs("../testdata/index.php")
|
||||
workerName = escapeMetricLabel(workerName)
|
||||
|
||||
// Make some requests
|
||||
for i := range 10 {
|
||||
@@ -835,6 +856,7 @@ func TestAllDefinedServerVars(t *testing.T) {
|
||||
expectedBody = strings.ReplaceAll(expectedBody, "{documentRoot}", documentRoot)
|
||||
expectedBody = strings.ReplaceAll(expectedBody, "\r\n", "\n")
|
||||
expectedBody = strings.ReplaceAll(expectedBody, "{testPort}", testPort)
|
||||
expectedBody = strings.ReplaceAll(expectedBody, documentRoot+"/", documentRoot+string(filepath.Separator))
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(`
|
||||
{
|
||||
@@ -1545,6 +1567,7 @@ func TestLog(t *testing.T) {
|
||||
func TestSymlinkWorkerPaths(t *testing.T) {
|
||||
cwd, _ := os.Getwd()
|
||||
publicDir := filepath.Join(cwd, "..", "testdata", "symlinks", "public")
|
||||
skipIfSymlinkNotValid(t, publicDir)
|
||||
|
||||
t.Run("NeighboringWorkerScript", func(t *testing.T) {
|
||||
// Scenario: neighboring worker script
|
||||
@@ -1680,6 +1703,7 @@ func TestSymlinkResolveRoot(t *testing.T) {
|
||||
cwd, _ := os.Getwd()
|
||||
testDir := filepath.Join(cwd, "..", "testdata", "symlinks", "test")
|
||||
publicDir := filepath.Join(cwd, "..", "testdata", "symlinks", "public")
|
||||
skipIfSymlinkNotValid(t, publicDir)
|
||||
|
||||
t.Run("ResolveRootSymlink", func(t *testing.T) {
|
||||
// Tests that resolve_root_symlink directive works correctly
|
||||
@@ -1738,6 +1762,7 @@ func TestSymlinkResolveRoot(t *testing.T) {
|
||||
func TestSymlinkWorkerBehavior(t *testing.T) {
|
||||
cwd, _ := os.Getwd()
|
||||
publicDir := filepath.Join(cwd, "..", "testdata", "symlinks", "public")
|
||||
skipIfSymlinkNotValid(t, publicDir)
|
||||
|
||||
t.Run("WorkerScriptFailsWithoutWorkerMode", func(t *testing.T) {
|
||||
// Tests that accessing a worker-only script without configuring it as a worker actually results in an error
|
||||
|
||||
5
caddy/frankenphp/cbrotli.go
Normal file
5
caddy/frankenphp/cbrotli.go
Normal file
@@ -0,0 +1,5 @@
|
||||
//go:build !nobrotli
|
||||
|
||||
package main
|
||||
|
||||
import _ "github.com/dunglas/caddy-cbrotli"
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
// plug in Caddy modules here.
|
||||
_ "github.com/caddyserver/caddy/v2/modules/standard"
|
||||
_ "github.com/dunglas/caddy-cbrotli"
|
||||
_ "github.com/dunglas/frankenphp/caddy"
|
||||
_ "github.com/dunglas/mercure/caddy"
|
||||
_ "github.com/dunglas/vulcain/caddy"
|
||||
|
||||
@@ -64,7 +64,7 @@ require (
|
||||
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-20260104182512-c28e9078050a // indirect
|
||||
github.com/e-dant/watcher v0.0.0-20260223030516-06f84a1314be // 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
|
||||
|
||||
@@ -165,8 +165,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-20260104182512-c28e9078050a h1:e/m9m8cJgjzw2Ol7tKTu4B/lM5F3Ym7ryKI+oyw0T8Y=
|
||||
github.com/e-dant/watcher/watcher-go v0.0.0-20260104182512-c28e9078050a/go.mod h1:sVUOkwtftoj71nnJRG2S0oWNfXFdKpz/M9vK0z06nmM=
|
||||
github.com/e-dant/watcher v0.0.0-20260223030516-06f84a1314be h1:vqHrvilasyJcnru/0Z4FoojsQJUIfXGVplte7JtupfY=
|
||||
github.com/e-dant/watcher v0.0.0-20260223030516-06f84a1314be/go.mod h1:PmV4IVmBJVqT2NcfTGN4+sZ+qGe3PA0qkphAtOHeFG0=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -493,7 +494,13 @@ func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
|
||||
if indexFile != "off" {
|
||||
dirRedir := false
|
||||
dirIndex := "{http.request.uri.path}/" + indexFile
|
||||
// On Windows, first_exist_fallback doesn't work correctly because
|
||||
// glob is skipped and patterns are returned as-is without checking existence.
|
||||
// Use first_exist instead to ensure all files are checked.
|
||||
tryPolicy := "first_exist_fallback"
|
||||
if runtime.GOOS == "windows" {
|
||||
tryPolicy = "first_exist"
|
||||
}
|
||||
|
||||
// if tryFiles wasn't overridden, use a reasonable default
|
||||
if len(tryFiles) == 0 {
|
||||
|
||||
2
cgi.go
2
cgi.go
@@ -8,8 +8,8 @@ package frankenphp
|
||||
// #cgo noescape frankenphp_register_variables_from_request_info
|
||||
// #cgo noescape frankenphp_register_variable_safe
|
||||
// #cgo noescape frankenphp_register_single
|
||||
// #include <php_variables.h>
|
||||
// #include "frankenphp.h"
|
||||
// #include <php_variables.h>
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
|
||||
6
cgo.go
6
cgo.go
@@ -1,9 +1,11 @@
|
||||
package frankenphp
|
||||
|
||||
// #cgo darwin pkg-config: libxml-2.0
|
||||
// #cgo CFLAGS: -Wall -Werror
|
||||
// #cgo unix CFLAGS: -Wall -Werror
|
||||
// #cgo linux CFLAGS: -D_GNU_SOURCE
|
||||
// #cgo LDFLAGS: -lphp -lm -lutil
|
||||
// #cgo unix LDFLAGS: -lphp -lm -lutil
|
||||
// #cgo linux LDFLAGS: -ldl -lresolv
|
||||
// #cgo darwin LDFLAGS: -Wl,-rpath,/usr/local/lib -liconv -ldl
|
||||
// #cgo windows CFLAGS: -D_WINDOWS -DWINDOWS=1 -DZEND_WIN32=1 -DPHP_WIN32=1 -DWIN32 -D_MBCS -D_USE_MATH_DEFINES -DNDebug -DNDEBUG -DZEND_DEBUG=0 -DZTS=1 -DFD_SETSIZE=256
|
||||
// #cgo windows LDFLAGS: -lpthreadVC3
|
||||
import "C"
|
||||
|
||||
29
cli.go
Normal file
29
cli.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package frankenphp
|
||||
|
||||
// #include "frankenphp.h"
|
||||
import "C"
|
||||
import "unsafe"
|
||||
|
||||
// ExecuteScriptCLI executes the PHP script passed as parameter.
|
||||
// It returns the exit status code of the script.
|
||||
func ExecuteScriptCLI(script string, args []string) int {
|
||||
// Ensure extensions are registered before CLI execution
|
||||
registerExtensions()
|
||||
|
||||
cScript := C.CString(script)
|
||||
defer C.free(unsafe.Pointer(cScript))
|
||||
|
||||
argc, argv := convertArgs(args)
|
||||
defer freeArgs(argv)
|
||||
|
||||
return int(C.frankenphp_execute_script_cli(cScript, argc, (**C.char)(unsafe.Pointer(&argv[0])), false))
|
||||
}
|
||||
|
||||
func ExecutePHPCode(phpCode string) int {
|
||||
// Ensure extensions are registered before CLI execution
|
||||
registerExtensions()
|
||||
|
||||
cCode := C.CString(phpCode)
|
||||
defer C.free(unsafe.Pointer(cCode))
|
||||
return int(C.frankenphp_execute_script_cli(cCode, 0, nil, true))
|
||||
}
|
||||
55
cli_test.go
Normal file
55
cli_test.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package frankenphp_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestExecuteScriptCLI(t *testing.T) {
|
||||
if _, err := os.Stat("internal/testcli/testcli"); err != nil {
|
||||
t.Skip("internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`")
|
||||
}
|
||||
|
||||
cmd := exec.Command("internal/testcli/testcli", "testdata/command.php", "foo", "bar")
|
||||
stdoutStderr, err := cmd.CombinedOutput()
|
||||
assert.Error(t, err)
|
||||
|
||||
var exitError *exec.ExitError
|
||||
if errors.As(err, &exitError) {
|
||||
assert.Equal(t, 3, exitError.ExitCode())
|
||||
}
|
||||
|
||||
stdoutStderrStr := string(stdoutStderr)
|
||||
|
||||
assert.Contains(t, stdoutStderrStr, `"foo"`)
|
||||
assert.Contains(t, stdoutStderrStr, `"bar"`)
|
||||
assert.Contains(t, stdoutStderrStr, "From the CLI")
|
||||
}
|
||||
|
||||
func TestExecuteCLICode(t *testing.T) {
|
||||
if _, err := os.Stat("internal/testcli/testcli"); err != nil {
|
||||
t.Skip("internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`")
|
||||
}
|
||||
|
||||
cmd := exec.Command("internal/testcli/testcli", "-r", "echo 'Hello World';")
|
||||
stdoutStderr, err := cmd.CombinedOutput()
|
||||
assert.NoError(t, err)
|
||||
|
||||
stdoutStderrStr := string(stdoutStderr)
|
||||
assert.Equal(t, stdoutStderrStr, `Hello World`)
|
||||
}
|
||||
|
||||
func ExampleExecuteScriptCLI() {
|
||||
if len(os.Args) <= 1 {
|
||||
log.Println("Usage: my-program script.php")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(frankenphp.ExecuteScriptCLI(os.Args[1], os.Args))
|
||||
}
|
||||
2
ext.go
2
ext.go
@@ -1,6 +1,6 @@
|
||||
package frankenphp
|
||||
|
||||
//#include "frankenphp.h"
|
||||
// #include "frankenphp.h"
|
||||
import "C"
|
||||
import (
|
||||
"sync"
|
||||
|
||||
14
frankenphp.c
14
frankenphp.c
@@ -1,8 +1,8 @@
|
||||
#include "frankenphp.h"
|
||||
#include <SAPI.h>
|
||||
#include <Zend/zend_alloc.h>
|
||||
#include <Zend/zend_exceptions.h>
|
||||
#include <Zend/zend_interfaces.h>
|
||||
#include <Zend/zend_types.h>
|
||||
#include <errno.h>
|
||||
#include <ext/spl/spl_exceptions.h>
|
||||
#include <ext/standard/head.h>
|
||||
@@ -11,7 +11,11 @@
|
||||
#endif
|
||||
#include <inttypes.h>
|
||||
#include <php.h>
|
||||
#ifdef PHP_WIN32
|
||||
#include <config.w32.h>
|
||||
#else
|
||||
#include <php_config.h>
|
||||
#endif
|
||||
#include <php_ini.h>
|
||||
#include <php_main.h>
|
||||
#include <php_output.h>
|
||||
@@ -22,7 +26,9 @@
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#ifndef ZEND_WIN32
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#if defined(__linux__)
|
||||
#include <sys/prctl.h>
|
||||
#elif defined(__FreeBSD__) || defined(__OpenBSD__)
|
||||
@@ -279,7 +285,7 @@ bool frankenphp_shutdown_dummy_request(void) {
|
||||
return true;
|
||||
}
|
||||
|
||||
PHPAPI void get_full_env(zval *track_vars_array) {
|
||||
void get_full_env(zval *track_vars_array) {
|
||||
go_getfullenv(thread_index, track_vars_array);
|
||||
}
|
||||
|
||||
@@ -1033,6 +1039,7 @@ static void *php_thread(void *arg) {
|
||||
}
|
||||
|
||||
static void *php_main(void *arg) {
|
||||
#ifndef ZEND_WIN32
|
||||
/*
|
||||
* SIGPIPE must be masked in non-Go threads:
|
||||
* https://pkg.go.dev/os/signal#hdr-Go_programs_that_use_cgo_or_SWIG
|
||||
@@ -1045,6 +1052,7 @@ static void *php_main(void *arg) {
|
||||
perror("failed to block SIGPIPE");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
#endif
|
||||
|
||||
set_thread_name("php-main");
|
||||
|
||||
@@ -1340,7 +1348,7 @@ static zend_module_entry **modules = NULL;
|
||||
static int modules_len = 0;
|
||||
static int (*original_php_register_internal_extensions_func)(void) = NULL;
|
||||
|
||||
PHPAPI int register_internal_extensions(void) {
|
||||
int register_internal_extensions(void) {
|
||||
if (original_php_register_internal_extensions_func != NULL &&
|
||||
original_php_register_internal_extensions_func() != SUCCESS) {
|
||||
return FAILURE;
|
||||
|
||||
@@ -14,10 +14,10 @@ package frankenphp
|
||||
|
||||
// #include <stdlib.h>
|
||||
// #include <stdint.h>
|
||||
// #include "frankenphp.h"
|
||||
// #include <php_variables.h>
|
||||
// #include <zend_llist.h>
|
||||
// #include <SAPI.h>
|
||||
// #include "frankenphp.h"
|
||||
import "C"
|
||||
import (
|
||||
"bytes"
|
||||
@@ -750,30 +750,6 @@ func go_is_context_done(threadIndex C.uintptr_t) C.bool {
|
||||
return C.bool(phpThreads[threadIndex].frankenPHPContext().isDone)
|
||||
}
|
||||
|
||||
// ExecuteScriptCLI executes the PHP script passed as parameter.
|
||||
// It returns the exit status code of the script.
|
||||
func ExecuteScriptCLI(script string, args []string) int {
|
||||
// Ensure extensions are registered before CLI execution
|
||||
registerExtensions()
|
||||
|
||||
cScript := C.CString(script)
|
||||
defer C.free(unsafe.Pointer(cScript))
|
||||
|
||||
argc, argv := convertArgs(args)
|
||||
defer freeArgs(argv)
|
||||
|
||||
return int(C.frankenphp_execute_script_cli(cScript, argc, (**C.char)(unsafe.Pointer(&argv[0])), false))
|
||||
}
|
||||
|
||||
func ExecutePHPCode(phpCode string) int {
|
||||
// Ensure extensions are registered before CLI execution
|
||||
registerExtensions()
|
||||
|
||||
cCode := C.CString(phpCode)
|
||||
defer C.free(unsafe.Pointer(cCode))
|
||||
return int(C.frankenphp_execute_script_cli(cCode, 0, nil, true))
|
||||
}
|
||||
|
||||
func convertArgs(args []string) (C.int, []*C.char) {
|
||||
argc := C.int(len(args))
|
||||
argv := make([]*C.char, argc)
|
||||
|
||||
40
frankenphp.h
40
frankenphp.h
@@ -1,6 +1,46 @@
|
||||
#ifndef _FRANKENPHP_H
|
||||
#define _FRANKENPHP_H
|
||||
|
||||
#ifdef _WIN32
|
||||
// Define this to prevent windows.h from including legacy winsock.h
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
|
||||
// Explicitly include Winsock2 BEFORE windows.h
|
||||
#include <windows.h>
|
||||
#include <winerror.h>
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
|
||||
// Fix for missing IntSafe functions (LongLongAdd) when building with Clang
|
||||
#ifdef __clang__
|
||||
#ifndef INTSAFE_E_ARITHMETIC_OVERFLOW
|
||||
#define INTSAFE_E_ARITHMETIC_OVERFLOW ((HRESULT)0x80070216L)
|
||||
#endif
|
||||
|
||||
#ifndef LongLongAdd
|
||||
static inline HRESULT LongLongAdd(LONGLONG llAugend, LONGLONG llAddend,
|
||||
LONGLONG *pllResult) {
|
||||
if (__builtin_add_overflow(llAugend, llAddend, pllResult)) {
|
||||
return INTSAFE_E_ARITHMETIC_OVERFLOW;
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef LongLongSub
|
||||
static inline HRESULT LongLongSub(LONGLONG llMinuend, LONGLONG llSubtrahend,
|
||||
LONGLONG *pllResult) {
|
||||
if (__builtin_sub_overflow(llMinuend, llSubtrahend, pllResult)) {
|
||||
return INTSAFE_E_ARITHMETIC_OVERFLOW;
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include <Zend/zend_modules.h>
|
||||
#include <Zend/zend_types.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
BIN
frankenphp.ico
Normal file
BIN
frankenphp.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 256 KiB |
@@ -784,40 +784,6 @@ func testFileUpload(t *testing.T, opts *testOptions) {
|
||||
}, opts)
|
||||
}
|
||||
|
||||
func TestExecuteScriptCLI(t *testing.T) {
|
||||
if _, err := os.Stat("internal/testcli/testcli"); err != nil {
|
||||
t.Skip("internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`")
|
||||
}
|
||||
|
||||
cmd := exec.Command("internal/testcli/testcli", "testdata/command.php", "foo", "bar")
|
||||
stdoutStderr, err := cmd.CombinedOutput()
|
||||
assert.Error(t, err)
|
||||
|
||||
var exitError *exec.ExitError
|
||||
if errors.As(err, &exitError) {
|
||||
assert.Equal(t, 3, exitError.ExitCode())
|
||||
}
|
||||
|
||||
stdoutStderrStr := string(stdoutStderr)
|
||||
|
||||
assert.Contains(t, stdoutStderrStr, `"foo"`)
|
||||
assert.Contains(t, stdoutStderrStr, `"bar"`)
|
||||
assert.Contains(t, stdoutStderrStr, "From the CLI")
|
||||
}
|
||||
|
||||
func TestExecuteCLICode(t *testing.T) {
|
||||
if _, err := os.Stat("internal/testcli/testcli"); err != nil {
|
||||
t.Skip("internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`")
|
||||
}
|
||||
|
||||
cmd := exec.Command("internal/testcli/testcli", "-r", "echo 'Hello World';")
|
||||
stdoutStderr, err := cmd.CombinedOutput()
|
||||
assert.NoError(t, err)
|
||||
|
||||
stdoutStderrStr := string(stdoutStderr)
|
||||
assert.Equal(t, stdoutStderrStr, `Hello World`)
|
||||
}
|
||||
|
||||
func ExampleServeHTTP() {
|
||||
if err := frankenphp.Init(); err != nil {
|
||||
panic(err)
|
||||
@@ -837,15 +803,6 @@ func ExampleServeHTTP() {
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
|
||||
func ExampleExecuteScriptCLI() {
|
||||
if len(os.Args) <= 1 {
|
||||
log.Println("Usage: my-program script.php")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(frankenphp.ExecuteScriptCLI(os.Args[1], os.Args))
|
||||
}
|
||||
|
||||
func BenchmarkHelloWorld(b *testing.B) {
|
||||
require.NoError(b, frankenphp.Init())
|
||||
b.Cleanup(frankenphp.Shutdown)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build unix
|
||||
|
||||
package cpu
|
||||
|
||||
// #include <time.h>
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
)
|
||||
|
||||
// ProbeCPUs fallback that always determines that the CPU limits are not reached
|
||||
func ProbeCPUs(probeTime time.Duration, maxCPUUsage float64, abort chan struct{}) bool {
|
||||
func ProbeCPUs(probeTime time.Duration, _ float64, abort chan struct{}) bool {
|
||||
select {
|
||||
case <-abort:
|
||||
return false
|
||||
|
||||
@@ -3,6 +3,7 @@ package extgen
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -68,6 +69,10 @@ func TestWriteFile(t *testing.T) {
|
||||
assert.NoError(t, err, "Failed to stat file")
|
||||
|
||||
expectedMode := os.FileMode(0644)
|
||||
if runtime.GOOS == "windows" {
|
||||
expectedMode = os.FileMode(0666)
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedMode, info.Mode().Perm(), "writeFile() wrong permissions")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,5 +9,6 @@ import (
|
||||
// FastAbs can't be optimized on Windows because the
|
||||
// syscall.FullPath function takes an input.
|
||||
func FastAbs(path string) (string, error) {
|
||||
return filepath.Abs(path)
|
||||
// Normalize forward slashes to backslashes for Windows compatibility
|
||||
return filepath.Abs(filepath.FromSlash(path))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef _EXTENSIONS_H
|
||||
#define _EXTENSIONS_H
|
||||
|
||||
#include "../../frankenphp.h"
|
||||
#include <php.h>
|
||||
|
||||
extern zend_module_entry module1_entry;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include "extension.h"
|
||||
#include <php.h>
|
||||
#include <zend_exceptions.h>
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package testext
|
||||
|
||||
// #cgo darwin pkg-config: libxml-2.0
|
||||
// #cgo CFLAGS: -Wall -Werror
|
||||
// #cgo CFLAGS: -I/usr/local/include -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib
|
||||
// #cgo unix CFLAGS: -Wall -Werror
|
||||
// #cgo unix CFLAGS: -I/usr/local/include -I/usr/local/include/php -I/usr/local/include/php/main -I/usr/local/include/php/TSRM -I/usr/local/include/php/Zend -I/usr/local/include/php/ext -I/usr/local/include/php/ext/date/lib
|
||||
// #cgo linux CFLAGS: -D_GNU_SOURCE
|
||||
// #cgo darwin CFLAGS: -I/opt/homebrew/include
|
||||
// #cgo LDFLAGS: -L/usr/local/lib -L/usr/lib -lphp -lm -lutil
|
||||
// #cgo unix LDFLAGS: -L/usr/local/lib -L/usr/lib -lphp -lm -lutil
|
||||
// #cgo linux LDFLAGS: -ldl -lresolv
|
||||
// #cgo darwin LDFLAGS: -Wl,-rpath,/usr/local/lib -L/opt/homebrew/lib -L/opt/homebrew/opt/libiconv/lib -liconv -ldl
|
||||
// #cgo windows CFLAGS: -D_WINDOWS -DWINDOWS=1 -DZEND_WIN32=1 -DPHP_WIN32=1 -DWIN32 -D_MBCS -D_USE_MATH_DEFINES -DNDebug -DNDEBUG -DZEND_DEBUG=0 -DZTS=1 -DFD_SETSIZE=256
|
||||
// #include "extension.h"
|
||||
import "C"
|
||||
import (
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"github.com/e-dant/watcher/watcher-go"
|
||||
)
|
||||
|
||||
const sep = string(filepath.Separator)
|
||||
|
||||
type pattern struct {
|
||||
patternGroup *PatternGroup
|
||||
value string
|
||||
@@ -39,8 +41,11 @@ func (p *pattern) parse() (err error) {
|
||||
|
||||
p.value = absPattern
|
||||
|
||||
volumeName := filepath.VolumeName(p.value)
|
||||
p.value = strings.TrimPrefix(p.value, volumeName)
|
||||
|
||||
// then we split the pattern to determine where the directory ends and the pattern starts
|
||||
splitPattern := strings.Split(absPattern, string(filepath.Separator))
|
||||
splitPattern := strings.Split(p.value, sep)
|
||||
patternWithoutDir := ""
|
||||
for i, part := range splitPattern {
|
||||
isFilename := i == len(splitPattern)-1 && strings.Contains(part, ".")
|
||||
@@ -57,11 +62,15 @@ func (p *pattern) parse() (err error) {
|
||||
// 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))
|
||||
p.parsedValues[i] = strings.Trim(pp, sep)
|
||||
}
|
||||
|
||||
// remove the trailing separator and add leading separator
|
||||
p.value = string(filepath.Separator) + strings.Trim(p.value, string(filepath.Separator))
|
||||
// remove the trailing separator and add leading separator (except on Windows)
|
||||
if volumeName == "" {
|
||||
p.value = sep + strings.Trim(p.value, sep)
|
||||
} else {
|
||||
p.value = volumeName + sep + strings.Trim(p.value, sep)
|
||||
}
|
||||
|
||||
// try to canonicalize the path
|
||||
canonicalPattern, err := filepath.EvalSymlinks(p.value)
|
||||
@@ -123,7 +132,7 @@ func (p *pattern) isValidPattern(fileName string) bool {
|
||||
}
|
||||
|
||||
// remove the directory path and separator from the filename
|
||||
fileNameWithoutDir := strings.TrimPrefix(strings.TrimPrefix(fileName, p.value), string(filepath.Separator))
|
||||
fileNameWithoutDir := strings.TrimPrefix(strings.TrimPrefix(fileName, p.value), sep)
|
||||
|
||||
// if the pattern has size 1 we can match it directly against the filename
|
||||
if len(p.parsedValues) == 1 {
|
||||
@@ -134,12 +143,12 @@ func (p *pattern) isValidPattern(fileName string) bool {
|
||||
}
|
||||
|
||||
func (p *pattern) matchPatterns(fileName string) bool {
|
||||
partsToMatch := strings.Split(fileName, string(filepath.Separator))
|
||||
partsToMatch := strings.Split(fileName, sep)
|
||||
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
|
||||
patternSize := strings.Count(pattern, sep) + 1
|
||||
|
||||
// if we are at the last pattern we will start matching from the end of the filename
|
||||
if i == len(p.parsedValues)-1 {
|
||||
@@ -157,7 +166,7 @@ func (p *pattern) matchPatterns(fileName string) bool {
|
||||
}
|
||||
|
||||
cursor = j
|
||||
subPattern := strings.Join(partsToMatch[j:j+patternSize], string(filepath.Separator))
|
||||
subPattern := strings.Join(partsToMatch[j:j+patternSize], sep)
|
||||
|
||||
if matchCurlyBracePattern(pattern, subPattern) {
|
||||
cursor = j + patternSize - 1
|
||||
|
||||
@@ -4,6 +4,7 @@ package watcher
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/e-dant/watcher/watcher-go"
|
||||
@@ -11,16 +12,42 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func normalizePath(t *testing.T, path string) string {
|
||||
t.Helper()
|
||||
|
||||
if filepath.Separator == '/' {
|
||||
return path
|
||||
}
|
||||
|
||||
path = filepath.FromSlash(path)
|
||||
if strings.HasPrefix(path, "\\") {
|
||||
path = "C:\\" + path[1:]
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
func newPattern(t *testing.T, value string) pattern {
|
||||
t.Helper()
|
||||
|
||||
p := pattern{value: normalizePath(t, value)}
|
||||
require.NoError(t, p.parse())
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func TestDisallowOnEventTypeBiggerThan3(t *testing.T) {
|
||||
w := pattern{value: "/some/path"}
|
||||
require.NoError(t, w.parse())
|
||||
t.Parallel()
|
||||
|
||||
w := newPattern(t, "/some/path")
|
||||
|
||||
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())
|
||||
t.Parallel()
|
||||
|
||||
w := newPattern(t, "/some/path")
|
||||
|
||||
assert.False(t, w.allowReload(&watcher.Event{PathName: "/some/path/watch-me.php", PathType: watcher.PathTypeSymLink}))
|
||||
}
|
||||
@@ -59,7 +86,7 @@ func TestValidRecursiveDirectories(t *testing.T) {
|
||||
|
||||
data := []struct {
|
||||
pattern string
|
||||
dir string
|
||||
file string
|
||||
}{
|
||||
{"/path", "/path/file.php"},
|
||||
{"/path", "/path/subpath/file.php"},
|
||||
@@ -77,7 +104,7 @@ func TestValidRecursiveDirectories(t *testing.T) {
|
||||
t.Run(d.pattern, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
shouldMatch(t, d.pattern, d.dir)
|
||||
assertPatternMatch(t, d.pattern, d.file)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -98,7 +125,7 @@ func TestInvalidRecursiveDirectories(t *testing.T) {
|
||||
t.Run(d.pattern, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
shouldNotMatch(t, d.pattern, d.dir)
|
||||
assertPatternNotMatch(t, d.pattern, d.dir)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -122,7 +149,7 @@ func TestValidNonRecursiveFilePatterns(t *testing.T) {
|
||||
t.Run(d.pattern, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
shouldMatch(t, d.pattern, d.dir)
|
||||
assertPatternMatch(t, d.pattern, d.dir)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -145,7 +172,7 @@ func TestInValidNonRecursiveFilePatterns(t *testing.T) {
|
||||
t.Run(d.pattern, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
shouldNotMatch(t, d.pattern, d.dir)
|
||||
assertPatternNotMatch(t, d.pattern, d.dir)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -170,7 +197,7 @@ func TestValidRecursiveFilePatterns(t *testing.T) {
|
||||
t.Run(d.pattern, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
shouldMatch(t, d.pattern, d.dir)
|
||||
assertPatternMatch(t, d.pattern, d.dir)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -198,7 +225,7 @@ func TestInvalidRecursiveFilePatterns(t *testing.T) {
|
||||
t.Run(d.pattern, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
shouldNotMatch(t, d.pattern, d.dir)
|
||||
assertPatternNotMatch(t, d.pattern, d.dir)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -225,13 +252,14 @@ func TestValidDirectoryPatterns(t *testing.T) {
|
||||
t.Run(d.pattern, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
shouldMatch(t, d.pattern, d.dir)
|
||||
assertPatternMatch(t, d.pattern, d.dir)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidDirectoryPatterns(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := []struct {
|
||||
pattern string
|
||||
dir string
|
||||
@@ -254,15 +282,17 @@ func TestInvalidDirectoryPatterns(t *testing.T) {
|
||||
t.Run(d.pattern, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
shouldNotMatch(t, d.pattern, d.dir)
|
||||
assertPatternNotMatch(t, d.pattern, d.dir)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidCurlyBracePatterns(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := []struct {
|
||||
pattern string
|
||||
dir string
|
||||
file string
|
||||
}{
|
||||
{"/path/*.{php}", "/path/file.php"},
|
||||
{"/path/*.{php,twig}", "/path/file.php"},
|
||||
@@ -282,12 +312,14 @@ func TestValidCurlyBracePatterns(t *testing.T) {
|
||||
t.Run(d.pattern, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
shouldMatch(t, d.pattern, d.dir)
|
||||
assertPatternMatch(t, d.pattern, d.file)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidCurlyBracePatterns(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := []struct {
|
||||
pattern string
|
||||
dir string
|
||||
@@ -306,52 +338,52 @@ func TestInvalidCurlyBracePatterns(t *testing.T) {
|
||||
t.Run(d.pattern, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
shouldNotMatch(t, d.pattern, d.dir)
|
||||
assertPatternNotMatch(t, d.pattern, d.dir)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestAnAssociatedEventTriggersTheWatcher(t *testing.T) {
|
||||
w := pattern{value: "/**/*.php"}
|
||||
require.NoError(t, w.parse())
|
||||
t.Parallel()
|
||||
|
||||
w := newPattern(t, "/**/*.php")
|
||||
w.events = make(chan eventHolder)
|
||||
|
||||
e := &watcher.Event{PathName: "/path/temporary_file", AssociatedPathName: "/path/file.php"}
|
||||
e := &watcher.Event{PathName: normalizePath(t, "/path/temporary_file"), AssociatedPathName: normalizePath(t, "/path/file.php")}
|
||||
go w.handle(e)
|
||||
|
||||
assert.Equal(t, e, (<-w.events).event)
|
||||
}
|
||||
|
||||
func relativeDir(t *testing.T, relativePath string) string {
|
||||
t.Helper()
|
||||
|
||||
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())
|
||||
w := newPattern(t, p)
|
||||
|
||||
assert.Equal(t, dir, w.value)
|
||||
assert.Equal(t, normalizePath(t, dir), w.value)
|
||||
}
|
||||
|
||||
func shouldMatch(t *testing.T, p string, fileName string) {
|
||||
func assertPatternMatch(t *testing.T, p, fileName string) {
|
||||
t.Helper()
|
||||
|
||||
w := pattern{value: p}
|
||||
require.NoError(t, w.parse())
|
||||
w := newPattern(t, p)
|
||||
|
||||
assert.True(t, w.allowReload(&watcher.Event{PathName: fileName}))
|
||||
assert.True(t, w.allowReload(&watcher.Event{PathName: normalizePath(t, fileName)}))
|
||||
}
|
||||
|
||||
func shouldNotMatch(t *testing.T, p string, fileName string) {
|
||||
func assertPatternNotMatch(t *testing.T, p, fileName string) {
|
||||
t.Helper()
|
||||
|
||||
w := pattern{value: p}
|
||||
require.NoError(t, w.parse())
|
||||
w := newPattern(t, p)
|
||||
|
||||
assert.False(t, w.allowReload(&watcher.Event{PathName: fileName}))
|
||||
assert.False(t, w.allowReload(&watcher.Event{PathName: normalizePath(t, fileName)}))
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package frankenphp
|
||||
|
||||
// #include <stdint.h>
|
||||
// #include "frankenphp.h"
|
||||
// #include <php.h>
|
||||
import "C"
|
||||
import (
|
||||
|
||||
@@ -4,8 +4,8 @@ package frankenphp
|
||||
// #cgo nocallback frankenphp_init_persistent_string
|
||||
// #cgo noescape frankenphp_new_main_thread
|
||||
// #cgo noescape frankenphp_init_persistent_string
|
||||
// #include <php_variables.h>
|
||||
// #include "frankenphp.h"
|
||||
// #include <php_variables.h>
|
||||
import "C"
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
@@ -101,9 +101,9 @@ func TestTransitionThreadsWhileDoingRequests(t *testing.T) {
|
||||
|
||||
numThreads := 10
|
||||
numRequestsPerThread := 100
|
||||
worker1Path := testDataPath + "/transition-worker-1.php"
|
||||
worker1Path := filepath.Join(testDataPath, "transition-worker-1.php")
|
||||
worker1Name := "worker-1"
|
||||
worker2Path := testDataPath + "/transition-worker-2.php"
|
||||
worker2Path := filepath.Join(testDataPath, "transition-worker-2.php")
|
||||
worker2Name := "worker-2"
|
||||
|
||||
assert.NoError(t, Init(
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package frankenphp
|
||||
|
||||
//#include "frankenphp.h"
|
||||
//#include <sys/resource.h>
|
||||
import "C"
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package frankenphp
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -33,7 +34,7 @@ func TestScaleAWorkerThreadUpAndDown(t *testing.T) {
|
||||
t.Cleanup(Shutdown)
|
||||
|
||||
workerName := "worker1"
|
||||
workerPath := testDataPath + "/transition-worker-1.php"
|
||||
workerPath := filepath.Join(testDataPath, "transition-worker-1.php")
|
||||
assert.NoError(t, Init(
|
||||
WithNumThreads(2),
|
||||
WithMaxThreads(3),
|
||||
|
||||
4
testdata/mercure-publish.php
vendored
4
testdata/mercure-publish.php
vendored
@@ -3,6 +3,6 @@
|
||||
require_once __DIR__.'/_executor.php';
|
||||
|
||||
return function () {
|
||||
echo "update 1: " . mercure_publish('foo', 'bar', true, 'myid', 'mytype', 10) . PHP_EOL;
|
||||
echo "update 2: " . mercure_publish(['baz', 'bar']) . PHP_EOL;
|
||||
echo "update 1: " . mercure_publish('foo', 'bar', true, 'myid', 'mytype', 10) . "\n";
|
||||
echo "update 2: " . mercure_publish(['baz', 'bar']) . "\n";
|
||||
};
|
||||
|
||||
2
testdata/session.php
vendored
2
testdata/session.php
vendored
@@ -11,5 +11,5 @@ return function () {
|
||||
$_SESSION['count'] = 0;
|
||||
}
|
||||
|
||||
echo 'Count: '.$_SESSION['count'].PHP_EOL;
|
||||
echo 'Count: '.$_SESSION['count']."\n";
|
||||
};
|
||||
|
||||
4
types.c
4
types.c
@@ -14,9 +14,9 @@ Bucket *get_ht_bucket_data(HashTable *ht, uint32_t index) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void *__emalloc__(size_t size) { return emalloc(size); }
|
||||
void *__emalloc__(size_t size) { return malloc(size); }
|
||||
|
||||
void __efree__(void *ptr) { efree(ptr); }
|
||||
void __efree__(void *ptr) { free(ptr); }
|
||||
|
||||
void __zend_hash_init__(HashTable *ht, uint32_t nSize, dtor_func_t pDestructor,
|
||||
bool persistent) {
|
||||
|
||||
2
types.go
2
types.go
@@ -15,6 +15,8 @@ package frankenphp
|
||||
#cgo noescape __zval_double__
|
||||
#cgo noescape __zval_string__
|
||||
#cgo noescape __zval_arr__
|
||||
#cgo noescape __emalloc__
|
||||
#cgo noescape __efree__
|
||||
#include "types.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
3
types.h
3
types.h
@@ -1,11 +1,12 @@
|
||||
#ifndef TYPES_H
|
||||
#define TYPES_H
|
||||
|
||||
#include "frankenphp.h"
|
||||
#include <Zend/zend.h>
|
||||
#include <Zend/zend_API.h>
|
||||
#include <Zend/zend_alloc.h>
|
||||
#include <Zend/zend_hash.h>
|
||||
#include <Zend/zend_types.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
zval *get_ht_packed_data(HashTable *, uint32_t index);
|
||||
Bucket *get_ht_bucket_data(HashTable *, uint32_t index);
|
||||
|
||||
3
vcpkg.json
Normal file
3
vcpkg.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"dependencies": ["brotli", "pthreads"]
|
||||
}
|
||||
@@ -50,7 +50,7 @@ func pollForWorkerReset(t *testing.T, handler func(http.ResponseWriter, *http.Re
|
||||
|
||||
// now we spam file updates and check if the request counter resets
|
||||
for range limit {
|
||||
updateTestFile("./testdata/files/test.txt", "updated", t)
|
||||
updateTestFile(t, filepath.Join(".", "testdata", "files", "test.txt"), "updated")
|
||||
time.Sleep(pollingTime * time.Millisecond)
|
||||
body, _ := testGet("http://example.com/worker-with-counter.php", handler, t)
|
||||
if body == "requests:1" {
|
||||
@@ -61,15 +61,10 @@ func pollForWorkerReset(t *testing.T, handler func(http.ResponseWriter, *http.Re
|
||||
return false
|
||||
}
|
||||
|
||||
func updateTestFile(fileName string, content string, t *testing.T) {
|
||||
func updateTestFile(t *testing.T, fileName, content string) {
|
||||
absFileName, err := filepath.Abs(fileName)
|
||||
require.NoError(t, err)
|
||||
|
||||
dirName := filepath.Dir(absFileName)
|
||||
if _, err = os.Stat(dirName); os.IsNotExist(err) {
|
||||
err = os.MkdirAll(dirName, 0700)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(absFileName), 0700))
|
||||
require.NoError(t, os.WriteFile(absFileName, []byte(content), 0644))
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ func newWorker(o workerOpt) (*worker, error) {
|
||||
// Order is important!
|
||||
// This order ensures that FrankenPHP started from inside a symlinked directory will properly resolve any paths.
|
||||
// If it is started from outside a symlinked directory, it is resolved to the same path that we use in the Caddy module.
|
||||
absFileName, err := filepath.EvalSymlinks(o.fileName)
|
||||
absFileName, err := filepath.EvalSymlinks(filepath.FromSlash(o.fileName))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("worker filename is invalid %q: %w", o.fileName, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user